Skip to content

Commit 7102050

Browse files
authored
Add support for images.loaderFile config (vercel#41585)
This PR adds a new configure property, `images.loaderFile` that allow you to define a path to a file with an exported image loader function. This is useful when migrating from `next/legacy/image` to `next/image` because it lets you configure the loader for every instance of `next/image` once, similar to the legacy "built-in loaders".
1 parent 6f43c90 commit 7102050

File tree

20 files changed

+549
-88
lines changed

20 files changed

+549
-88
lines changed

docs/api-reference/next/image.md

+25
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ const MyImage = (props) => {
111111
}
112112
```
113113

114+
Alternatively, you can use the [loaderFile](#loader-configuration) configuration in next.config.js to configure every instance of `next/image` in your application, without passing a prop.
115+
114116
### fill
115117

116118
A boolean that causes the image to fill the parent element instead of setting [`width`](#width) and [`height`](#height).
@@ -343,6 +345,29 @@ module.exports = {
343345
}
344346
```
345347

348+
### Loader Configuration
349+
350+
If you want to use a cloud provider to optimize images instead of using the Next.js built-in Image Optimization API, you can configure the `loaderFile` in your `next.config.js` like the following:
351+
352+
```js
353+
module.exports = {
354+
images: {
355+
loader: 'custom',
356+
loaderFile: './my/image/loader.js',
357+
},
358+
}
359+
```
360+
361+
This must point to a file relative to the root of your Next.js application. The file must export a default function that returns a string, for example:
362+
363+
```js
364+
export default function myImageLoader({ src, width, quality }) {
365+
return `https://example.com/${src}?w=${width}&q=${quality || 75}`
366+
}
367+
```
368+
369+
Alternatively, you can use the [`loader` prop](#loader) to configure each instance of `next/image`.
370+
346371
## Advanced
347372

348373
The following configuration is for advanced use cases and is usually not necessary. If you choose to configure the properties below, you will override any changes to the Next.js defaults in future updates.

docs/basic-features/image-optimization.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,13 @@ To protect your application from malicious users, you must define a list of remo
9999
100100
### Loaders
101101

102-
Note that in the [example earlier](#remote-images), a partial URL (`"/me.png"`) is provided for a remote image. This is possible because of the `next/image` [loader](/docs/api-reference/next/image.md#loader) architecture.
102+
Note that in the [example earlier](#remote-images), a partial URL (`"/me.png"`) is provided for a remote image. This is possible because of the loader architecture.
103103

104104
A loader is a function that generates the URLs for your image. It modifies the provided `src`, and generates multiple URLs to request the image at different sizes. These multiple URLs are used in the automatic [srcset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset) generation, so that visitors to your site will be served an image that is the right size for their viewport.
105105

106-
The default loader for Next.js applications uses the built-in Image Optimization API, which optimizes images from anywhere on the web, and then serves them directly from the Next.js web server. If you would like to serve your images directly from a CDN or image server, you can use one of the [built-in loaders](/docs/api-reference/next/image.md#built-in-loaders) or write your own with a few lines of JavaScript.
106+
The default loader for Next.js applications uses the built-in Image Optimization API, which optimizes images from anywhere on the web, and then serves them directly from the Next.js web server. If you would like to serve your images directly from a CDN or image server, you can write your own loader function with a few lines of JavaScript.
107107

108-
Loaders can be defined per-image, or at the application level.
108+
You can define a loader per-image with the [`loader` prop](/docs/api-reference/next/image.md#loader), or at the application level with the [`loaderFile` configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration).
109109

110110
### Priority
111111

@@ -151,7 +151,7 @@ Because `next/image` is designed to guarantee good performance results, it canno
151151
>
152152
> If you are accessing images from a source without knowledge of the images' sizes, there are several things you can do:
153153
>
154-
> **Use `fill``**
154+
> **Use `fill`**
155155
>
156156
> The [`fill`](/docs/api-reference/next/image#fill) prop allows your image to be sized by its parent element. Consider using CSS to give the image's parent element space on the page along [`sizes`](/docs/api-reference/next/image#sizes) prop to match any media query break points. You can also use [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) with `fill`, `contain`, or `cover`, and [`object-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) to define how the image should occupy that space.
157157
>

errors/invalid-images-config.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ module.exports = {
2121
path: '/_next/image',
2222
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom'
2323
loader: 'default',
24+
// file with `export default function loader({src, width, quality})`
25+
loaderFile: '',
2426
// disable static imports for image files
2527
disableStaticImages: false,
2628
// minimumCacheTTL is in seconds, must be integer 0 or more

packages/next/build/webpack-config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,12 @@ export default async function getBaseWebpackConfig(
866866
}
867867
: undefined),
868868

869+
...(config.images.loaderFile
870+
? {
871+
'next/dist/shared/lib/image-loader': config.images.loaderFile,
872+
}
873+
: undefined),
874+
869875
next: NEXT_PROJECT_ROOT,
870876

871877
...(hasServerComponents

packages/next/client/image.tsx

+23-76
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
} from '../shared/lib/image-config'
1717
import { ImageConfigContext } from '../shared/lib/image-config-context'
1818
import { warnOnce } from '../shared/lib/utils'
19+
// @ts-ignore - This is replaced by webpack alias
20+
import defaultLoader from 'next/dist/shared/lib/image-loader'
1921

2022
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
2123
const allImgs = new Map<
@@ -468,70 +470,6 @@ const ImageElement = ({
468470
)
469471
}
470472

471-
function defaultLoader({
472-
config,
473-
src,
474-
width,
475-
quality,
476-
}: ImageLoaderPropsWithConfig): string {
477-
if (process.env.NODE_ENV !== 'production') {
478-
const missingValues = []
479-
480-
// these should always be provided but make sure they are
481-
if (!src) missingValues.push('src')
482-
if (!width) missingValues.push('width')
483-
484-
if (missingValues.length > 0) {
485-
throw new Error(
486-
`Next Image Optimization requires ${missingValues.join(
487-
', '
488-
)} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify(
489-
{ src, width, quality }
490-
)}`
491-
)
492-
}
493-
494-
if (src.startsWith('//')) {
495-
throw new Error(
496-
`Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)`
497-
)
498-
}
499-
500-
if (!src.startsWith('/') && (config.domains || config.remotePatterns)) {
501-
let parsedSrc: URL
502-
try {
503-
parsedSrc = new URL(src)
504-
} catch (err) {
505-
console.error(err)
506-
throw new Error(
507-
`Failed to parse src "${src}" on \`next/image\`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)`
508-
)
509-
}
510-
511-
if (process.env.NODE_ENV !== 'test') {
512-
// We use dynamic require because this should only error in development
513-
const { hasMatch } = require('../shared/lib/match-remote-pattern')
514-
if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) {
515-
throw new Error(
516-
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
517-
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host`
518-
)
519-
}
520-
}
521-
}
522-
}
523-
524-
if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) {
525-
// Special case to make svg serve as-is to avoid proxying
526-
// through the built-in Image Optimization API.
527-
return src
528-
}
529-
530-
return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${
531-
quality || 75
532-
}`
533-
}
534-
535473
export default function Image({
536474
src,
537475
sizes,
@@ -559,20 +497,29 @@ export default function Image({
559497
}, [configContext])
560498

561499
let rest: Partial<ImageProps> = all
500+
let loader: ImageLoaderWithConfig = rest.loader || defaultLoader
562501

563-
let loader: ImageLoaderWithConfig = defaultLoader
564-
if ('loader' in rest) {
565-
if (rest.loader) {
566-
const customImageLoader = rest.loader
567-
loader = (obj) => {
568-
const { config: _, ...opts } = obj
569-
// The config object is internal only so we must
570-
// not pass it to the user-defined loader()
571-
return customImageLoader(opts)
572-
}
502+
// Remove property so it's not spread on <img> element
503+
delete rest.loader
504+
505+
if ('__next_img_default' in loader) {
506+
// This special value indicates that the user
507+
// didn't define a "loader" prop or config.
508+
if (config.loader === 'custom') {
509+
throw new Error(
510+
`Image with src "${src}" is missing "loader" prop.` +
511+
`\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader`
512+
)
513+
}
514+
} else {
515+
// The user defined a "loader" prop or config.
516+
// Since the config object is internal only, we
517+
// must not pass it to the user-defined "loader".
518+
const customImageLoader = loader as ImageLoader
519+
loader = (obj) => {
520+
const { config: _, ...opts } = obj
521+
return customImageLoader(opts)
573522
}
574-
// Remove property so it's not spread on <img>
575-
delete rest.loader
576523
}
577524

578525
let staticSrc = ''

packages/next/server/config-schema.ts

+4
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,10 @@ const configSchema = {
585585
enum: VALID_LOADERS as any,
586586
type: 'string',
587587
},
588+
loaderFile: {
589+
minLength: 1,
590+
type: 'string',
591+
},
588592
minimumCacheTTL: {
589593
type: 'number',
590594
},

packages/next/server/config.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { basename, extname, relative, isAbsolute, resolve } from 'path'
1+
import { existsSync } from 'fs'
2+
import { basename, extname, join, relative, isAbsolute, resolve } from 'path'
23
import { pathToFileURL } from 'url'
34
import { Agent as HttpAgent } from 'http'
45
import { Agent as HttpsAgent } from 'https'
@@ -76,7 +77,7 @@ export function setHttpClientAndAgentOptions(options: NextConfig) {
7677
;(global as any).__NEXT_HTTPS_AGENT = new HttpsAgent(options.httpAgentOptions)
7778
}
7879

79-
function assignDefaults(userConfig: { [key: string]: any }) {
80+
function assignDefaults(dir: string, userConfig: { [key: string]: any }) {
8081
const configFileName = userConfig.configFileName
8182
if (typeof userConfig.exportTrailingSlash !== 'undefined') {
8283
console.warn(
@@ -379,7 +380,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
379380
images.path === imageConfigDefault.path
380381
) {
381382
throw new Error(
382-
`Specified images.loader property (${images.loader}) also requires images.path property to be assigned to a URL prefix.\nSee more info here: https://nextjs.org/docs/api-reference/next/image#loader-configuration`
383+
`Specified images.loader property (${images.loader}) also requires images.path property to be assigned to a URL prefix.\nSee more info here: https://nextjs.org/docs/api-reference/next/legacy/image#loader-configuration`
383384
)
384385
}
385386

@@ -398,6 +399,22 @@ function assignDefaults(userConfig: { [key: string]: any }) {
398399
images.path = `${result.basePath}${images.path}`
399400
}
400401

402+
if (images.loaderFile) {
403+
if (images.loader !== 'default' && images.loader !== 'custom') {
404+
throw new Error(
405+
`Specified images.loader property (${images.loader}) cannot be used with images.loaderFile property. Please set images.loader to "custom".`
406+
)
407+
}
408+
const absolutePath = join(dir, images.loaderFile)
409+
if (!existsSync(absolutePath)) {
410+
throw new Error(
411+
`Specified images.loaderFile does not exist at "${absolutePath}".`
412+
)
413+
}
414+
images.loader = 'custom'
415+
images.loaderFile = absolutePath
416+
}
417+
401418
if (
402419
images.minimumCacheTTL &&
403420
(!Number.isInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)
@@ -739,7 +756,7 @@ export default async function loadConfig(
739756
let configFileName = 'next.config.js'
740757

741758
if (customConfig) {
742-
return assignDefaults({
759+
return assignDefaults(dir, {
743760
configOrigin: 'server',
744761
configFileName,
745762
...customConfig,
@@ -818,7 +835,7 @@ export default async function loadConfig(
818835
: canonicalBase) || ''
819836
}
820837

821-
return assignDefaults({
838+
return assignDefaults(dir, {
822839
configOrigin: relative(dir, path),
823840
configFile: path,
824841
configFileName,
@@ -846,7 +863,10 @@ export default async function loadConfig(
846863

847864
// always call assignDefaults to ensure settings like
848865
// reactRoot can be updated correctly even with no next.config.js
849-
const completeConfig = assignDefaults(defaultConfig) as NextConfigComplete
866+
const completeConfig = assignDefaults(
867+
dir,
868+
defaultConfig
869+
) as NextConfigComplete
850870
completeConfig.configFileName = configFileName
851871
setHttpClientAndAgentOptions(completeConfig)
852872
return completeConfig

packages/next/shared/lib/image-config.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,15 @@ export type ImageConfigComplete = {
4949
/** @see [Image sizing documentation](https://nextjs.org/docs/basic-features/image-optimization#image-sizing) */
5050
imageSizes: number[]
5151

52-
/** @see [Image loaders configuration](https://nextjs.org/docs/basic-features/image-optimization#loaders) */
52+
/** @see [Image loaders configuration](https://nextjs.org/docs/api-reference/next/legacy/image#loader) */
5353
loader: LoaderValue
5454

55-
/** @see [Image loader configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration) */
55+
/** @see [Image loader configuration](https://nextjs.org/docs/api-reference/next/legacy/image#loader-configuration) */
5656
path: string
5757

58+
/** @see [Image loader configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration) */
59+
loaderFile: string
60+
5861
/**
5962
* @see [Image domains configuration](https://nextjs.org/docs/api-reference/next/image#domains)
6063
*/
@@ -89,6 +92,7 @@ export const imageConfigDefault: ImageConfigComplete = {
8992
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
9093
path: '/_next/image',
9194
loader: 'default',
95+
loaderFile: '',
9296
domains: [],
9397
disableStaticImages: false,
9498
minimumCacheTTL: 60,
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// TODO: change "any" to actual type
2+
function defaultLoader({ config, src, width, quality }: any): string {
3+
if (process.env.NODE_ENV !== 'production') {
4+
const missingValues = []
5+
6+
// these should always be provided but make sure they are
7+
if (!src) missingValues.push('src')
8+
if (!width) missingValues.push('width')
9+
10+
if (missingValues.length > 0) {
11+
throw new Error(
12+
`Next Image Optimization requires ${missingValues.join(
13+
', '
14+
)} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify(
15+
{ src, width, quality }
16+
)}`
17+
)
18+
}
19+
20+
if (src.startsWith('//')) {
21+
throw new Error(
22+
`Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)`
23+
)
24+
}
25+
26+
if (!src.startsWith('/') && (config.domains || config.remotePatterns)) {
27+
let parsedSrc: URL
28+
try {
29+
parsedSrc = new URL(src)
30+
} catch (err) {
31+
console.error(err)
32+
throw new Error(
33+
`Failed to parse src "${src}" on \`next/image\`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)`
34+
)
35+
}
36+
37+
if (process.env.NODE_ENV !== 'test') {
38+
// We use dynamic require because this should only error in development
39+
const { hasMatch } = require('./match-remote-pattern')
40+
if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) {
41+
throw new Error(
42+
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
43+
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host`
44+
)
45+
}
46+
}
47+
}
48+
}
49+
50+
if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) {
51+
// Special case to make svg serve as-is to avoid proxying
52+
// through the built-in Image Optimization API.
53+
return src
54+
}
55+
56+
return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${
57+
quality || 75
58+
}`
59+
}
60+
61+
// We use this to determine if the import is the default loader
62+
// or a custom loader defined by the user in next.config.js
63+
defaultLoader.__next_img_default = true
64+
65+
export default defaultLoader
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// prettier-ignore
2+
module.exports = { /* replaceme */ }

0 commit comments

Comments
 (0)