Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for images.loaderFile config #41585

Merged
merged 14 commits into from Oct 21, 2022
24 changes: 24 additions & 0 deletions docs/api-reference/next/image.md
Expand Up @@ -111,6 +111,8 @@ const MyImage = (props) => {
}
```

Alternatively, you can use the [loader configuration](#loader-configuration) in next.config.js to configure every instance of `next/image` in your application.

### fill

A boolean that causes the image to fill the parent element instead of setting [`width`](#width) and [`height`](#height).
Expand Down Expand Up @@ -343,6 +345,28 @@ module.exports = {
}
```

### Loader Configuration

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 `loader` in your `next.config.js` file like the following:

```js
module.exports = {
images: {
loader: './my/image/loader.js',
},
}
```

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.

```js
export default function myImageLoader({ src, width, quality }) {
return `https://example.com/${src}?w=${width}&q=${quality || 75}`
}
```

Alternatively, you can use the [`loader` prop](#loader) to configure each instance of `next/image`.

## Advanced

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.
Expand Down
8 changes: 4 additions & 4 deletions docs/basic-features/image-optimization.md
Expand Up @@ -99,13 +99,13 @@ To protect your application from malicious users, you must define a list of remo

### Loaders

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.
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](/docs/api-reference/next/image.md#loader) architecture.

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.

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.
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.

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

### Priority

Expand Down Expand Up @@ -151,7 +151,7 @@ Because `next/image` is designed to guarantee good performance results, it canno
>
> If you are accessing images from a source without knowledge of the images' sizes, there are several things you can do:
>
> **Use `fill``**
> **Use `fill`**
>
> 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.
>
Expand Down
4 changes: 4 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -863,6 +863,10 @@ export default async function getBaseWebpackConfig(
}
: undefined),

'next/image/loader': config.images.loader?.startsWith('./')
? require.resolve(path.join(dir, config.images.loader))
: '../shared/lib/image-loader',

next: NEXT_PROJECT_ROOT,

...(hasServerComponents
Expand Down
76 changes: 12 additions & 64 deletions packages/next/client/image.tsx
Expand Up @@ -16,6 +16,8 @@ import {
} from '../shared/lib/image-config'
import { ImageConfigContext } from '../shared/lib/image-config-context'
import { warnOnce } from '../shared/lib/utils'
// @ts-ignore - This is replaced by webpack alias
import defaultLoader from 'next/image/loader'
styfle marked this conversation as resolved.
Show resolved Hide resolved

const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
const allImgs = new Map<
Expand Down Expand Up @@ -468,70 +470,6 @@ const ImageElement = ({
)
}

function defaultLoader({
config,
src,
width,
quality,
}: ImageLoaderPropsWithConfig): string {
if (process.env.NODE_ENV !== 'production') {
const missingValues = []

// these should always be provided but make sure they are
if (!src) missingValues.push('src')
if (!width) missingValues.push('width')

if (missingValues.length > 0) {
throw new Error(
`Next Image Optimization requires ${missingValues.join(
', '
)} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify(
{ src, width, quality }
)}`
)
}

if (src.startsWith('//')) {
throw new Error(
`Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)`
)
}

if (!src.startsWith('/') && (config.domains || config.remotePatterns)) {
let parsedSrc: URL
try {
parsedSrc = new URL(src)
} catch (err) {
console.error(err)
throw new Error(
`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://)`
)
}

if (process.env.NODE_ENV !== 'test') {
// We use dynamic require because this should only error in development
const { hasMatch } = require('../shared/lib/match-remote-pattern')
if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) {
throw new Error(
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host`
)
}
}
}
}

if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) {
// Special case to make svg serve as-is to avoid proxying
// through the built-in Image Optimization API.
return src
}

return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${
quality || 75
}`
}

export default function Image({
src,
sizes,
Expand Down Expand Up @@ -563,6 +501,7 @@ export default function Image({
let loader: ImageLoaderWithConfig = defaultLoader
if ('loader' in rest) {
if (rest.loader) {
// The user provided custom "loader" prop
const customImageLoader = rest.loader
loader = (obj) => {
const { config: _, ...opts } = obj
Expand All @@ -573,6 +512,15 @@ export default function Image({
}
// Remove property so it's not spread on <img>
delete rest.loader
} else if (!defaultLoader.__next_loader) {
// The user provided a custom "loader" in next.config.js
const customImageLoader = defaultLoader
loader = (obj) => {
const { config: _, ...opts } = obj
// The config object is internal only so we must
// not pass it to the user-defined loader()
return customImageLoader(opts)
}
}

let staticSrc = ''
Expand Down
3 changes: 0 additions & 3 deletions packages/next/server/config-schema.ts
@@ -1,6 +1,5 @@
import { NextConfig } from './config'
import type { JSONSchemaType } from 'ajv'
import { VALID_LOADERS } from '../shared/lib/image-config'

const configSchema = {
type: 'object',
Expand Down Expand Up @@ -575,8 +574,6 @@ const configSchema = {
type: 'array',
},
loader: {
// automatic typing does not like enum
enum: VALID_LOADERS as any,
type: 'string',
},
minimumCacheTTL: {
Expand Down
6 changes: 5 additions & 1 deletion packages/next/server/config.ts
Expand Up @@ -363,7 +363,10 @@ function assignDefaults(userConfig: { [key: string]: any }) {
images.loader = 'default'
}

if (!VALID_LOADERS.includes(images.loader)) {
if (
!VALID_LOADERS.includes(images.loader) &&
!images.loader.startsWith('./')
) {
throw new Error(
`Specified images.loader should be one of (${VALID_LOADERS.join(
', '
Expand All @@ -376,6 +379,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (
images.loader !== 'default' &&
images.loader !== 'custom' &&
!images.loader.startsWith('./') &&
images.path === imageConfigDefault.path
) {
throw new Error(
Expand Down
65 changes: 65 additions & 0 deletions packages/next/shared/lib/image-loader.ts
@@ -0,0 +1,65 @@
// TODO: change "any" to actual type
function defaultLoader({ config, src, width, quality }: any): string {
if (process.env.NODE_ENV !== 'production') {
const missingValues = []

// these should always be provided but make sure they are
if (!src) missingValues.push('src')
if (!width) missingValues.push('width')

if (missingValues.length > 0) {
throw new Error(
`Next Image Optimization requires ${missingValues.join(
', '
)} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify(
{ src, width, quality }
)}`
)
}

if (src.startsWith('//')) {
throw new Error(
`Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)`
)
}

if (!src.startsWith('/') && (config.domains || config.remotePatterns)) {
let parsedSrc: URL
try {
parsedSrc = new URL(src)
} catch (err) {
console.error(err)
throw new Error(
`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://)`
)
}

if (process.env.NODE_ENV !== 'test') {
// We use dynamic require because this should only error in development
const { hasMatch } = require('./match-remote-pattern')
if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) {
throw new Error(
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host`
)
}
}
}
}

if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) {
// Special case to make svg serve as-is to avoid proxying
// through the built-in Image Optimization API.
return src
}

return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${
quality || 75
}`
}

// We use this to determine if the import is the default loader
// or a custom loader defined by the user in next.config.js
defaultLoader.__next_loader = true

export default defaultLoader
@@ -0,0 +1,3 @@
export default function dummyLoader({ src, width, quality }) {
return `${src}#w:${width},q:${quality || 50}`
}
@@ -0,0 +1,5 @@
module.exports = {
images: {
loader: './dummy-loader.js',
},
}
35 changes: 35 additions & 0 deletions test/integration/next-image-new/loader-config/pages/index.js
@@ -0,0 +1,35 @@
import React from 'react'
import Image from 'next/image'

function loader({ src, width, quality }) {
return `${src}?wid=${width}&qual=${quality || 35}`
}

const Page = () => {
return (
<div>
<h1>Loader Config</h1>
<Image
id="img1"
alt="img1"
src="/logo.png"
width="400"
height="400"
priority
/>
<p>Scroll down...</p>
<div style={{ height: '100vh' }} />
<h2>Loader Prop</h2>
<Image
id="img2"
alt="img2"
src="/logo.png"
width="200"
height="200"
loader={loader}
/>
</div>
)
}

export default Page
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.