Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Commit

Permalink
Rename next/image dangerously-unoptimized to custom and warn when…
Browse files Browse the repository at this point in the history
… applicable (vercel#26998)

Since we are no longer accepting new built-in loaders, users may wish to use a different cloud provider.

So this PR renames `dangerously-unoptimized` to `custom` to handle this case as well as the intention of `next export`.

If the user doesn't add a `loader` prop, we throw an error.
If the user adds a `loader` prop but it doesn't return the width, we print a warning.

- Follow up to vercel#26847 
- Fixes vercel#21079 
- Fixes vercel#19612 
- Related to vercel#26850
  • Loading branch information
styfle committed Jul 8, 2021
1 parent b09335a commit 906dd61
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 23 deletions.
2 changes: 1 addition & 1 deletion docs/basic-features/image-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ The following Image Optimization cloud providers are included:
- [Imgix](https://www.imgix.com): `loader: 'imgix'`
- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'`
- [Akamai](https://www.akamai.com): `loader: 'akamai'`
- dangerously-unoptimized: uses image directly from `src` prop without optimization (only needed with `next export` or when the built-in `_next/image` optimizer can not be leveraged) `loader: 'dangerously-unoptimized'`
- Custom: `loader: 'custom'` use a custom cloud provider by implementing the [`loader`](/docs/api-reference/next/image.md#loader) prop on the `next/image` component
- Default: Works automatically with `next dev`, `next start`, or a custom server

If you need a different provider, you can use the [`loader`](/docs/api-reference/next/image.md#loader) prop with `next/image`.
Expand Down
8 changes: 8 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@
"title": "next-head-count-missing",
"path": "/errors/next-head-count-missing.md"
},
{
"title": "next-image-missing-loader",
"path": "/errors/next-image-missing-loader.md"
},
{
"title": "next-image-missing-loader-width",
"path": "/errors/next-image-missing-loader-width.md"
},
{
"title": "next-image-unconfigured-host",
"path": "/errors/next-image-unconfigured-host.md"
Expand Down
17 changes: 17 additions & 0 deletions errors/next-image-missing-loader-width.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Missing `width` in the URL Returned by the Loader Prop on `next/image`

#### Why This Error Occurred

The [`loader`](https://nextjs.org/docs/api-reference/next/image#loader) prop on the `next/image` component allows you to override the built-in URL resolution with a custom implementation in order to support any 3rd party cloud provider that can perform Image Optimization.

This error occurred because the provided `loader()` function did not use `width` in the returned URL string. This means that the image will likely not be resized and therefore degrade performance.

#### Possible Ways to Fix It

- Ensure your Image Optimization provider can resize images. Then use the `width` parameter from the [`loader()`](https://nextjs.org/docs/api-reference/next/image#loader) function to construct the correct URL string.
- Add the [`unoptimized`](https://nextjs.org/docs/api-reference/next/image#unoptimized) prop.

### Useful Links

- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)
15 changes: 15 additions & 0 deletions errors/next-image-missing-loader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Missing `loader` Prop on `next/image`

#### Why This Error Occurred

When using the `next/image` component with [`loader="custom"`](https://nextjs.org/docs/basic-features/image-optimization#loader) in `next.config.js`, you must provide the [`loader`](https://nextjs.org/docs/api-reference/next/image#loader) prop to the component with your custom implementation.

#### Possible Ways to Fix It

- Add the [`loader`](https://nextjs.org/docs/api-reference/next/image#loader) prop to all usages of the `next/image` component.
- Change the [`loader`](https://nextjs.org/docs/basic-features/image-optimization#loader) configuration in `next.config.js`.

### Useful Links

- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)
44 changes: 28 additions & 16 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ const loaders = new Map<
LoaderValue,
(props: DefaultImageLoaderProps) => string
>([
['default', defaultLoader],
['imgix', imgixLoader],
['cloudinary', cloudinaryLoader],
['akamai', akamaiLoader],
['default', defaultLoader],
['dangerously-unoptimized', unoptimizedLoader],
['custom', customLoader],
])

const VALID_LAYOUT_VALUES = [
Expand Down Expand Up @@ -338,6 +338,17 @@ export default function Image({
const heightInt = getInt(height)
const qualityInt = getInt(quality)

let isLazy =
!priority && (loading === 'lazy' || typeof loading === 'undefined')
if (src.startsWith('data:')) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
unoptimized = true
isLazy = false
}
if (typeof window !== 'undefined' && loadedImageURLs.has(src)) {
isLazy = false
}

if (process.env.NODE_ENV !== 'production') {
if (!src) {
throw new Error(
Expand Down Expand Up @@ -404,16 +415,16 @@ export default function Image({
`Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.`
)
}
}
let isLazy =
!priority && (loading === 'lazy' || typeof loading === 'undefined')
if (src && src.startsWith('data:')) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
unoptimized = true
isLazy = false
}
if (src && typeof window !== 'undefined' && loadedImageURLs.has(src)) {
isLazy = false
const rand = Math.floor(Math.random() * 1000) + 100
if (
!unoptimized &&
!loader({ src, width: rand, quality: 75 }).includes(rand.toString())
) {
console.warn(
`Image with src "${src}" has a "loader" property that does not implement width. Please implement it or use the "unoptimized" property instead.` +
`\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader-width`
)
}
}

const [setRef, isIntersected] = useIntersection<HTMLImageElement>({
Expand Down Expand Up @@ -626,8 +637,6 @@ export default function Image({
)
}

//BUILT IN LOADERS

function normalizeSrc(src: string): string {
return src[0] === '/' ? src.slice(1) : src
}
Expand Down Expand Up @@ -669,8 +678,11 @@ function cloudinaryLoader({
return `${root}${paramsString}${normalizeSrc(src)}`
}

function unoptimizedLoader({ src }: DefaultImageLoaderProps): string {
return src
function customLoader({ src }: DefaultImageLoaderProps): string {
throw new Error(
`Image with src "${src}" is missing "loader" prop.` +
`\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader`
)
}

function defaultLoader({
Expand Down
2 changes: 1 addition & 1 deletion packages/next/server/image-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const VALID_LOADERS = [
'imgix',
'cloudinary',
'akamai',
'dangerously-unoptimized',
'custom',
] as const

export type LoaderValue = typeof VALID_LOADERS[number]
Expand Down
4 changes: 3 additions & 1 deletion test/integration/export-image-loader/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Image from 'next/image'

const loader = undefined

export default () => (
<div>
<p>Should succeed during export</p>
<Image alt="icon" src="/i.png" width="10" height="10" />
<Image alt="icon" src="/i.png" width="10" height="10" loader={loader} />
</div>
)
12 changes: 9 additions & 3 deletions test/integration/export-image-loader/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jest.setTimeout(1000 * 60 * 5)
const appDir = join(__dirname, '../')
const outdir = join(appDir, 'out')
const nextConfig = new File(join(appDir, 'next.config.js'))
const pagesIndexJs = new File(join(appDir, 'pages', 'index.js'))

describe('Export with cloudinary loader next/image component', () => {
beforeAll(async () => {
Expand Down Expand Up @@ -44,16 +45,20 @@ describe('Export with cloudinary loader next/image component', () => {
})
})

describe('Export with dangerously-unoptimized loader next/image component', () => {
describe('Export with custom loader next/image component', () => {
beforeAll(async () => {
await nextConfig.replace(
'{ /* replaceme */ }',
JSON.stringify({
images: {
loader: 'dangerously-unoptimized',
loader: 'custom',
},
})
)
await pagesIndexJs.replace(
'loader = undefined',
'loader = ({src}) => "/custom" + src'
)
})
it('should build successfully', async () => {
await fs.remove(join(appDir, '.next'))
Expand All @@ -69,10 +74,11 @@ describe('Export with dangerously-unoptimized loader next/image component', () =
it('should contain img element with same src in html output', async () => {
const html = await fs.readFile(join(outdir, 'index.html'))
const $ = cheerio.load(html)
expect($('img[alt="icon"]').attr('src')).toBe('/i.png')
expect($('img[alt="icon"]').attr('src')).toBe('/custom/i.png')
})

afterAll(async () => {
await nextConfig.restore()
await pagesIndexJs.restore()
})
})
30 changes: 29 additions & 1 deletion test/integration/image-optimizer/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
launchApp,
nextBuild,
nextStart,
renderViaHTTP,
waitFor,
} from 'next-test-utils'
import isAnimated from 'next/dist/compiled/is-animated'
Expand Down Expand Up @@ -730,7 +731,34 @@ describe('Image Optimizer', () => {
await nextConfig.restore()

expect(stderr).toContain(
'Specified images.loader should be one of (default, imgix, cloudinary, akamai, dangerously-unoptimized), received invalid value (notreal)'
'Specified images.loader should be one of (default, imgix, cloudinary, akamai, custom), received invalid value (notreal)'
)
})

it('should error when loader=custom but loader prop is undefined', async () => {
await nextConfig.replace(
'{ /* replaceme */ }',
JSON.stringify({
images: {
loader: 'custom',
},
})
)
let output = ''
const appPort = await findPort()
app = await launchApp(appDir, appPort, {
onStderr(msg) {
output += msg || ''
},
onStdout(msg) {
output += msg || ''
},
})
await renderViaHTTP(appPort, '/', {})
await killApp(app).catch(() => {})
await nextConfig.restore()
expect(output).toMatch(
/Error: Image with src "(.+)" is missing "loader" prop/
)
})
})
Expand Down

0 comments on commit 906dd61

Please sign in to comment.