Add support for AVIF to next/image (#29683)

Add support for AVIF to `next/image`

- Fixes #27882 
- Closes #27432 

## Feature

- [x] Implements an existing feature request
- [x] Related issues linked
- [x] Integration tests added
- [x] Documentation added
- [x] Update manifest output
- [x] Warn when `sharp` is outdated
- [x] Errors & Warnings have helpful link attached
- [ ] Remove `image-size` in favor of `squoosh`/`sharp` (optional, need to benchmark)
This commit is contained in:
Steven 2021-10-11 19:17:47 -04:00 committed by GitHub
parent 13f68debfe
commit cc1f3b8a38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 302 additions and 43 deletions

View file

@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component.
| Version | Changes |
| --------- | ------------------------------------------------------------------------------------------------- |
| `v12.0.0` | `formats` configuration added as well as AVIF support. |
| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. |
| `v11.0.0` | `src` prop support for static import.<br/>`placeholder` prop added.<br/>`blurDataURL` prop added. |
| `v10.0.5` | `loader` prop added. |
@ -141,7 +142,7 @@ Should only be used when the image is visible above the fold. Defaults to `false
A placeholder to use while the image is loading. Possible values are `blur` or `empty`. Defaults to `empty`.
When `blur`, the [`blurDataURL`](#blurdataurl) property will be used as the placeholder. If `src` is an object from a [static import](#local-images) and the imported image is `.jpg`, `.png`, or `.webp`, then `blurDataURL` will be automatically populated.
When `blur`, the [`blurDataURL`](#blurdataurl) property will be used as the placeholder. If `src` is an object from a [static import](#local-images) and the imported image is `.jpg`, `.png`, `.webp`, or `.avif`, then `blurDataURL` will be automatically populated.
For dynamic images, you must provide the [`blurDataURL`](#blurdataurl) property. Solutions such as [Plaiceholder](https://github.com/joe-bell/plaiceholder) can help with `base64` generation.
@ -322,6 +323,7 @@ The expiration (or rather Max Age) is defined by the upstream server's `Cache-Co
- If `s-maxage` is found in `Cache-Control`, it is used. If no `s-maxage` is found, then `max-age` is used. If no `max-age` is found, then [`minimumCacheTTL`](#minimum-cache-ttl) is used.
- You can configure [`minimumCacheTTL`](#minimum-cache-ttl) to increase the cache duration when the upstream image does not include `max-age`.
- You can also configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images.
- You can also configure [formats](/docs/basic-features/image-optimization.md#acceptable-formats) to disable multiple formats in favor of a single image format.
You can configure the Time to Live (TTL) in seconds for cached optimized images. In many cases, it's better to use a [Static Image Import](/docs/basic-features/image-optimization.md#local-images) which will automatically hash the file contents and cache the image forever with a `Cache-Control` header of `immutable`.
@ -351,6 +353,22 @@ module.exports = {
}
```
### Acceptable Formats
The default [Image Optimization API](#loader-configuration) will automatically detect the browser's supported image formats via the request's `Accept` header.
If the `Accept` matches more than one of the configured formats, the first match in the array is used. Therefore, the array order matters. If there is no match, the Image Optimization API will fallback to the original image's format.
If no configuration is provided, the default below is used.
```js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
},
}
```
## Related
For an overview of the Image component features and usage guidelines, see:

View file

@ -38,7 +38,7 @@ To use a local image, `import` your `.jpg`, `.png`, or `.webp` files:
import profilePic from '../public/me.png'
```
Dynamic `await import()` or `require()` are _not_ supported. The `import` must be static. Also note that static image support requires Webpack 5, which is enabled by default in Next.js applications.
Dynamic `await import()` or `require()` are _not_ supported. The `import` must be static so it can be analyzed at build time.
Next.js will automatically determine the `width` and `height` of your image based on the imported file. These values are used to prevent [Cumulative Layout Shift](https://nextjs.org/learn/seo/web-performance/cls) while your image is loading.

View file

@ -180,7 +180,8 @@ module.exports = {
/* Handle image imports
https://jestjs.io/docs/webpack#handling-static-assets */
'^.+\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js',
'^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$':
'<rootDir>/__mocks__/fileMock.js',
},
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
testEnvironment: 'jsdom',

View file

@ -17,6 +17,7 @@ module.exports = {
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// limit of 50 domains values
domains: [],
// path prefix for Image Optimization API, useful with `loader`
path: '/_next/image',
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom'
loader: 'default',
@ -24,6 +25,8 @@ module.exports = {
disableStaticImages: false,
// minimumCacheTTL is in seconds, must be integer 0 or more
minimumCacheTTL: 60,
// ordered list of acceptable optimized image formats (mime types)
formats: ['image/avif', 'image/webp'],
},
}
```
@ -31,3 +34,4 @@ module.exports = {
### Useful Links
- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)

View file

@ -447,6 +447,10 @@
"title": "sharp-missing-in-production",
"path": "/errors/sharp-missing-in-production.md"
},
{
"title": "sharp-version-avif",
"path": "/errors/sharp-version-avif.md"
},
{
"title": "script-in-document-page",
"path": "/errors/no-script-in-document-page.md"

View file

@ -6,7 +6,7 @@ You are attempting use the `next/image` component with `placeholder=blur` proper
The `blurDataURL` might be missing because you're using a string for `src` instead of a static import.
Or `blurDataURL` might be missing because the static import is an unsupported image format. Only jpg, png, and webp are supported at this time.
Or `blurDataURL` might be missing because the static import is an unsupported image format. Only jpg, png, webp, and avif are supported at this time.
#### Possible Ways to Fix It

View file

@ -16,3 +16,4 @@ You are seeing this error because Image Optimization in production mode (`next s
### Useful Links
- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)

View file

@ -0,0 +1,24 @@
# Sharp Version Does Not Support AVIF
#### Why This Error Occurred
The `next/image` component's default loader uses [`sharp`](https://www.npmjs.com/package/sharp) if its installed.
You are seeing this error because you have an outdated version of [`sharp`](https://www.npmjs.com/package/sharp) installed that does not support the AVIF image format.
AVIF support was added to [`sharp`](https://www.npmjs.com/package/sharp) in version 0.27.0 (December 2020) so your installed version is likely older.
#### Possible Ways to Fix It
- Install the latest version of `sharp` by running `yarn add sharp@latest` in your project directory
- If you're using the `NEXT_SHARP_PATH` environment variable, then update the `sharp` install referenced in that path, for example `cd "$NEXT_SHARP_PATH/../" && yarn add sharp@latest`
- If you cannot upgrade `sharp`, you can instead disable AVIF by configuring [`formats`](https://nextjs.org/docs/api-reference/next/image#image-formats) in your `next.config.js`
After choosing an option above, reboot the server by running either `next dev` or `next start` for development or production respectively.
> Note: This is not necessary for Vercel deployments, since `sharp` is installed automatically for you.
### Useful Links
- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)

View file

@ -14,7 +14,7 @@ module.exports = {
// Handle image imports
// https://jestjs.io/docs/webpack#handling-static-assets
'^.+\\.(jpg|jpeg|png|gif|webp|svg)$': `<rootDir>/__mocks__/fileMock.js`,
'^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$': `<rootDir>/__mocks__/fileMock.js`,
// Handle module aliases
'^@/components/(.*)$': '<rootDir>/components/$1',

View file

@ -1051,7 +1051,7 @@ export default async function getBaseWebpackConfig(
...(!config.images.disableStaticImages
? [
{
test: /\.(png|jpg|jpeg|gif|webp|ico|bmp|svg)$/i,
test: /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i,
loader: 'next-image-loader',
issuer: { not: regexLikeCss },
dependency: { not: ['url'] },
@ -1512,7 +1512,7 @@ export default async function getBaseWebpackConfig(
// Exclude svg if the user already defined it in custom
// webpack config such as `@svgr/webpack` plugin or
// the `babel-plugin-inline-react-svg` plugin.
nextImageRule.test = /\.(png|jpg|jpeg|gif|webp|ico|bmp)$/i
nextImageRule.test = /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp)$/i
}
}

View file

@ -12,7 +12,7 @@ export const images = curry(async function images(
loader({
oneOf: [
{
test: /\.(png|jpg|jpeg|gif|webp|ico|bmp|svg)$/i,
test: /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i,
use: {
loader: 'error-loader',
options: {

View file

@ -1,10 +1,9 @@
import loaderUtils from 'next/dist/compiled/loader-utils'
import sizeOf from 'image-size'
import { resizeImage } from '../../../server/image-optimizer'
import { resizeImage, getImageSize } from '../../../server/image-optimizer'
const BLUR_IMG_SIZE = 8
const BLUR_QUALITY = 70
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp']
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next/client/image.tsx
function nextImageLoader(content) {
const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader')
@ -26,7 +25,9 @@ function nextImageLoader(content) {
}
const imageSizeSpan = imageLoaderSpan.traceChild('image-size-calculation')
const imageSize = imageSizeSpan.traceFn(() => sizeOf(content))
const imageSize = await imageSizeSpan.traceAsyncFn(() =>
getImageSize(content, extension)
)
let blurDataURL
if (VALID_BLUR_EXT.includes(extension)) {

View file

@ -416,7 +416,7 @@ export default function Image({
)
}
if (!blurDataURL) {
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp'] // should match next-image-loader
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader
throw new Error(
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.

View file

@ -49,6 +49,12 @@ declare module '*.webp' {
export default content
}
declare module '*.avif' {
const content: StaticImageData
export default content
}
declare module '*.ico' {
const content: StaticImageData

View file

@ -312,6 +312,32 @@ function assignDefaults(userConfig: { [key: string]: any }) {
)}), received (${images.minimumCacheTTL}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
if (images.formats) {
const { formats } = images
if (!Array.isArray(formats)) {
throw new Error(
`Specified images.formats should be an Array received ${typeof formats}.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
if (formats.length < 1 || formats.length > 2) {
throw new Error(
`Specified images.formats must be length 1 or 2, received length (${formats.length}), please reduce the length of the array to continue.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
const invalid = formats.filter((f) => {
return f !== 'image/avif' && f !== 'image/webp'
})
if (invalid.length > 0) {
throw new Error(
`Specified images.formats should be an Array of mime type strings, received invalid values (${invalid.join(
', '
)}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
}
}
if (result.webpack5 === false) {

View file

@ -8,6 +8,8 @@ export const VALID_LOADERS = [
export type LoaderValue = typeof VALID_LOADERS[number]
type ImageFormat = 'image/avif' | 'image/webp'
export type ImageConfigComplete = {
deviceSizes: number[]
imageSizes: number[]
@ -16,6 +18,7 @@ export type ImageConfigComplete = {
domains?: string[]
disableStaticImages?: boolean
minimumCacheTTL?: number
formats?: ImageFormat[]
}
export type ImageConfig = Partial<ImageConfigComplete>
@ -28,4 +31,5 @@ export const imageConfigDefault: ImageConfigComplete = {
domains: [],
disableStaticImages: false,
minimumCacheTTL: 60,
formats: ['image/avif', 'image/webp'],
}

View file

@ -2,6 +2,7 @@ import { mediaType } from '@hapi/accept'
import { createHash } from 'crypto'
import { createReadStream, promises } from 'fs'
import { getOrientation, Orientation } from 'get-orientation'
import imageSizeOf from 'image-size'
import { IncomingMessage, ServerResponse } from 'http'
// @ts-ignore no types for is-animated
import isAnimated from 'next/dist/compiled/is-animated'
@ -11,20 +12,19 @@ import nodeUrl, { UrlWithParsedQuery } from 'url'
import { NextConfig } from './config-shared'
import { fileExists } from '../lib/file-exists'
import { ImageConfig, imageConfigDefault } from './image-config'
import { processBuffer, Operation } from './lib/squoosh/main'
import { processBuffer, decodeBuffer, Operation } from './lib/squoosh/main'
import Server from './next-server'
import { sendEtagResponse } from './send-payload'
import { getContentType, getExtension } from './serve-static'
import chalk from 'chalk'
//const AVIF = 'image/avif'
const AVIF = 'image/avif'
const WEBP = 'image/webp'
const PNG = 'image/png'
const JPEG = 'image/jpeg'
const GIF = 'image/gif'
const SVG = 'image/svg+xml'
const CACHE_VERSION = 3
const MODERN_TYPES = [/* AVIF, */ WEBP]
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
const VECTOR_TYPES = [SVG]
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
@ -43,7 +43,7 @@ try {
// Sharp not present on the server, Squoosh fallback will be used
}
let shouldShowSharpWarning = process.env.NODE_ENV === 'production'
let showSharpMissingWarning = process.env.NODE_ENV === 'production'
export async function imageOptimizer(
server: Server,
@ -61,6 +61,7 @@ export async function imageOptimizer(
domains = [],
loader,
minimumCacheTTL = 60,
formats = ['image/avif', 'image/webp'],
} = imageData
if (loader !== 'default') {
@ -70,7 +71,7 @@ export async function imageOptimizer(
const { headers } = req
const { url, w, q } = parsedUrl.query
const mimeType = getSupportedMimeType(MODERN_TYPES, headers.accept)
const mimeType = getSupportedMimeType(formats, headers.accept)
let href: string
if (!url) {
@ -359,7 +360,18 @@ export async function imageOptimizer(
transformer.resize(width)
}
if (contentType === WEBP) {
if (contentType === AVIF) {
if (transformer.avif) {
transformer.avif({ quality })
} else {
console.warn(
chalk.yellow.bold('Warning: ') +
`Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` +
'Read more: https://nextjs.org/docs/messages/sharp-version-avif'
)
transformer.webp({ quality })
}
} else if (contentType === WEBP) {
transformer.webp({ quality })
} else if (contentType === PNG) {
transformer.png({ quality })
@ -371,13 +383,13 @@ export async function imageOptimizer(
// End sharp transformation logic
} else {
// Show sharp warning in production once
if (shouldShowSharpWarning) {
if (showSharpMissingWarning) {
console.warn(
chalk.yellow.bold('Warning: ') +
`For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically for Image Optimization.\n` +
'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production'
)
shouldShowSharpWarning = false
showSharpMissingWarning = false
}
// Begin Squoosh transformation logic
@ -399,9 +411,14 @@ export async function imageOptimizer(
operations.push({ type: 'resize', width })
//if (contentType === AVIF) {
//} else
if (contentType === WEBP) {
if (contentType === AVIF) {
optimizedBuffer = await processBuffer(
upstreamBuffer,
operations,
'avif',
quality
)
} else if (contentType === WEBP) {
optimizedBuffer = await processBuffer(
upstreamBuffer,
operations,
@ -620,6 +637,13 @@ export function detectContentType(buffer: Buffer) {
if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
return SVG
}
if (
[0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every(
(b, i) => !b || buffer[i] === b
)
) {
return AVIF
}
return null
}
@ -642,13 +666,25 @@ export async function resizeImage(
content: Buffer,
dimension: 'width' | 'height',
size: number,
extension: 'webp' | 'png' | 'jpeg',
// Should match VALID_BLUR_EXT
extension: 'avif' | 'webp' | 'png' | 'jpeg',
quality: number
): Promise<Buffer> {
if (sharp) {
const transformer = sharp(content)
if (extension === 'webp') {
if (extension === 'avif') {
if (transformer.avif) {
transformer.avif({ quality })
} else {
console.warn(
chalk.yellow.bold('Warning: ') +
`Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` +
'Read more: https://nextjs.org/docs/messages/sharp-version-avif'
)
transformer.webp({ quality })
}
} else if (extension === 'webp') {
transformer.webp({ quality })
} else if (extension === 'png') {
transformer.png({ quality })
@ -676,3 +712,28 @@ export async function resizeImage(
return buf
}
}
export async function getImageSize(
buffer: Buffer,
// Should match VALID_BLUR_EXT
extension: 'avif' | 'webp' | 'png' | 'jpeg'
): Promise<{
width?: number
height?: number
}> {
// TODO: upgrade "image-size" package to support AVIF
// See https://github.com/image-size/image-size/issues/348
if (extension === 'avif') {
if (sharp) {
const transformer = sharp(buffer)
const { width, height } = await transformer.metadata()
return { width, height }
} else {
const { width, height } = await decodeBuffer(buffer)
return { width, height }
}
}
const { width, height } = imageSizeOf(buffer)
return { width, height }
}

View file

@ -72,3 +72,9 @@ export async function processBuffer(
throw Error(`Unsupported encoding format`)
}
}
export async function decodeBuffer(buffer: Buffer) {
const worker: typeof import('./impl') = getWorker() as any
const imageData = await worker.decodeBuffer(buffer)
return imageData
}

View file

@ -21,6 +21,10 @@ export function serveStatic(
}
export function getContentType(extWithoutDot: string): string | null {
if (extWithoutDot === 'avif') {
// TODO: update "mime" package
return 'image/avif'
}
const { mime } = send
if ('getType' in mime) {
// 2.0
@ -31,6 +35,10 @@ export function getContentType(extWithoutDot: string): string | null {
}
export function getExtension(contentType: string): string | null {
if (contentType === 'image/avif') {
// TODO: update "mime" package
return 'avif'
}
const { mime } = send
if ('getExtension' in mime) {
// 2.0

View file

@ -5,6 +5,7 @@ import Image from 'next/image'
import testJPG from '../public/test.jpg'
import testPNG from '../public/test.png'
import testWEBP from '../public/test.webp'
import testAVIF from '../public/test.avif'
import testSVG from '../public/test.svg'
import testGIF from '../public/test.gif'
import testBMP from '../public/test.bmp'
@ -41,6 +42,7 @@ const Page = () => {
<Image id="blur-png" src={testPNG} placeholder="blur" />
<Image id="blur-jpg" src={testJPG} placeholder="blur" />
<Image id="blur-webp" src={testWEBP} placeholder="blur" />
<Image id="blur-avif" src={testAVIF} placeholder="blur" />
<Image id="static-svg" src={testSVG} />
<Image id="static-gif" src={testGIF} />
<Image id="static-bmp" src={testBMP} />

View file

@ -24,6 +24,7 @@ const runTests = (isDev = false) => {
expect(await browser.elementById('basic-static')).toBeTruthy()
expect(await browser.elementById('blur-png')).toBeTruthy()
expect(await browser.elementById('blur-webp')).toBeTruthy()
expect(await browser.elementById('blur-avif')).toBeTruthy()
expect(await browser.elementById('blur-jpg')).toBeTruthy()
expect(await browser.elementById('static-svg')).toBeTruthy()
expect(await browser.elementById('static-gif')).toBeTruthy()

View file

@ -5,6 +5,7 @@ import Image from 'next/image'
import testJPG from '../public/test.jpg'
import testPNG from '../public/test.png'
import testWEBP from '../public/test.webp'
import testAVIF from '../public/test.avif'
import testSVG from '../public/test.svg'
import testGIF from '../public/test.gif'
import testBMP from '../public/test.bmp'
@ -41,6 +42,7 @@ const Page = () => {
<Image id="blur-png" src={testPNG} placeholder="blur" />
<Image id="blur-jpg" src={testJPG} placeholder="blur" />
<Image id="blur-webp" src={testWEBP} placeholder="blur" />
<Image id="blur-avif" src={testAVIF} placeholder="blur" />
<Image id="static-svg" src={testSVG} />
<Image id="static-gif" src={testGIF} />
<Image id="static-bmp" src={testBMP} />

View file

@ -23,6 +23,7 @@ const runTests = () => {
expect(await browser.elementById('basic-static')).toBeTruthy()
expect(await browser.elementById('blur-png')).toBeTruthy()
expect(await browser.elementById('blur-webp')).toBeTruthy()
expect(await browser.elementById('blur-avif')).toBeTruthy()
expect(await browser.elementById('blur-jpg')).toBeTruthy()
expect(await browser.elementById('static-svg')).toBeTruthy()
expect(await browser.elementById('static-gif')).toBeTruthy()

View file

@ -2,6 +2,7 @@ import React from 'react'
import Image from 'next/image'
import testTall from '../public/tall.png'
import svg from '../public/test.svg'
import avif from '../public/test.avif'
import { ImageCard } from '../components/image-card'
import { DynamicSrcImage } from '../components/image-dynamic-src'
@ -77,6 +78,7 @@ const Page = () => {
placeholder="blur"
/>
<Image id="object-src-with-svg" src={svg} />
<Image id="object-src-with-avif" src={avif} />
<Image
id="fill-with-unused-width-height"
src="https://via.placeholder.com/200"

View file

@ -25,6 +25,7 @@ let appPort
let app
const sharpMissingText = `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended`
const sharpOutdatedText = `Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version`
async function fsToJson(dir, output = {}) {
const files = await fs.readdir(dir)
@ -47,7 +48,7 @@ async function expectWidth(res, w) {
expect(d.width).toBe(w)
}
function runTests({ w, isDev, domains = [], ttl, isSharp }) {
function runTests({ w, isDev, domains = [], ttl, isSharp, isOutdatedSharp }) {
it('should return home page', async () => {
const res = await fetchViaHTTP(appPort, '/', null, {})
expect(await res.text()).toMatch(/Image Optimizer Home/m)
@ -359,10 +360,10 @@ function runTests({ w, isDev, domains = [], ttl, isSharp }) {
// FIXME: await expectWidth(res, w)
})
it('should resize relative url and Chrome accept header as webp', async () => {
it('should resize relative url and old Chrome accept header as webp', async () => {
const query = { url: '/test.png', w, q: 80 }
const opts = {
headers: { accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8' },
headers: { accept: 'image/webp,image/apng,image/*,*/*;q=0.8' },
}
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
@ -378,6 +379,27 @@ function runTests({ w, isDev, domains = [], ttl, isSharp }) {
await expectWidth(res, w)
})
it('should resize relative url and new Chrome accept header as avif', async () => {
const query = { url: '/test.png', w, q: 80 }
const opts = {
headers: { accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8' },
}
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/avif')
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=0, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
expect(res.headers.get('Content-Disposition')).toBe(
`inline; filename="test.avif"`
)
// TODO: upgrade "image-size" package to support AVIF
// See https://github.com/image-size/image-size/issues/348
//await expectWidth(res, w)
})
if (domains.includes('localhost')) {
it('should resize absolute url from localhost', async () => {
const url = `http://localhost:${appPort}/test.png`
@ -733,6 +755,16 @@ function runTests({ w, isDev, domains = [], ttl, isSharp }) {
expect(nextOutput).toContain(sharpMissingText)
})
}
if (isSharp && isOutdatedSharp) {
it('should have sharp outdated warning', () => {
expect(nextOutput).toContain(sharpOutdatedText)
})
} else {
it('should not have sharp outdated warning', () => {
expect(nextOutput).not.toContain(sharpOutdatedText)
})
}
}
describe('Image Optimizer', () => {
@ -888,6 +920,31 @@ describe('Image Optimizer', () => {
/Error: Image with src "(.+)" is missing "loader" prop/
)
})
it('should error when images.formats contains invalid values', async () => {
await nextConfig.replace(
'{ /* replaceme */ }',
JSON.stringify({
images: {
formats: ['image/avif', 'jpeg'],
},
})
)
let stderr = ''
app = await launchApp(appDir, await findPort(), {
onStderr(msg) {
stderr += msg || ''
},
})
await waitFor(1000)
await killApp(app).catch(() => {})
await nextConfig.restore()
expect(stderr).toContain(
`Specified images.formats should be an Array of mime type strings, received invalid values (jpeg)`
)
})
})
// domains for testing
@ -1077,7 +1134,7 @@ describe('Image Optimizer', () => {
})
})
const setupTests = (isSharp = false) => {
const setupTests = ({ isSharp = false, isOutdatedSharp = false }) => {
describe('dev support w/o next.config.js', () => {
const size = 384 // defaults defined in server/config.ts
beforeAll(async () => {
@ -1087,6 +1144,11 @@ describe('Image Optimizer', () => {
onStderr(msg) {
nextOutput += msg
},
env: {
NEXT_SHARP_PATH: isSharp
? join(appDir, 'node_modules', 'sharp')
: '',
},
cwd: appDir,
})
})
@ -1095,7 +1157,7 @@ describe('Image Optimizer', () => {
await fs.remove(imagesDir)
})
runTests({ w: size, isDev: true, domains: [], isSharp })
runTests({ w: size, isDev: true, domains: [], isSharp, isOutdatedSharp })
})
describe('dev support with next.config.js', () => {
@ -1115,6 +1177,11 @@ describe('Image Optimizer', () => {
onStderr(msg) {
nextOutput += msg
},
env: {
NEXT_SHARP_PATH: isSharp
? join(appDir, 'node_modules', 'sharp')
: '',
},
cwd: appDir,
})
})
@ -1124,7 +1191,7 @@ describe('Image Optimizer', () => {
await fs.remove(imagesDir)
})
runTests({ w: size, isDev: true, domains, isSharp })
runTests({ w: size, isDev: true, domains, isSharp, isOutdatedSharp })
})
describe('Server support w/o next.config.js', () => {
@ -1139,9 +1206,7 @@ describe('Image Optimizer', () => {
},
env: {
NEXT_SHARP_PATH: isSharp
? require.resolve('sharp', {
paths: [join(appDir, 'node_modules')],
})
? join(appDir, 'node_modules', 'sharp')
: '',
},
cwd: appDir,
@ -1152,7 +1217,7 @@ describe('Image Optimizer', () => {
await fs.remove(imagesDir)
})
runTests({ w: size, isDev: false, domains: [], isSharp })
runTests({ w: size, isDev: false, domains: [], isSharp, isOutdatedSharp })
})
describe('Server support with next.config.js', () => {
@ -1174,9 +1239,7 @@ describe('Image Optimizer', () => {
},
env: {
NEXT_SHARP_PATH: isSharp
? require.resolve('sharp', {
paths: [join(appDir, 'node_modules')],
})
? join(appDir, 'node_modules', 'sharp')
: '',
},
cwd: appDir,
@ -1188,15 +1251,15 @@ describe('Image Optimizer', () => {
await fs.remove(imagesDir)
})
runTests({ w: size, isDev: false, domains, isSharp })
runTests({ w: size, isDev: false, domains, isSharp, isOutdatedSharp })
})
}
describe('with squoosh', () => {
setupTests()
setupTests({ isSharp: false, isOutdatedSharp: false })
})
describe('with sharp', () => {
describe('with latest sharp', () => {
beforeAll(async () => {
await execa('yarn', ['init', '-y'], {
cwd: appDir,
@ -1213,6 +1276,26 @@ describe('Image Optimizer', () => {
await fs.remove(join(appDir, 'package.json'))
})
setupTests(true)
setupTests({ isSharp: true, isOutdatedSharp: false })
})
describe('with outdated sharp', () => {
beforeAll(async () => {
await execa('yarn', ['init', '-y'], {
cwd: appDir,
stdio: 'inherit',
})
await execa('yarn', ['add', 'sharp@0.26.3'], {
cwd: appDir,
stdio: 'inherit',
})
})
afterAll(async () => {
await fs.remove(join(appDir, 'node_modules'))
await fs.remove(join(appDir, 'yarn.lock'))
await fs.remove(join(appDir, 'package.json'))
})
setupTests({ isSharp: true, isOutdatedSharp: true })
})
})

View file

@ -22,4 +22,8 @@ describe('detectContentType', () => {
const buffer = await getImage('./images/test.svg')
expect(detectContentType(buffer)).toBe('image/svg+xml')
})
it('should return avif', async () => {
const buffer = await getImage('./images/test.avif')
expect(detectContentType(buffer)).toBe('image/avif')
})
})

Binary file not shown.