From 71ee383fba975e9078602dded015d05363d44d5e Mon Sep 17 00:00:00 2001 From: atcastle Date: Sun, 18 Jul 2021 00:56:07 -0700 Subject: [PATCH 01/16] Use sharp for image transformations when available --- .../webpack/loaders/next-image-loader.js | 16 +- packages/next/server/image-optimizer.ts | 174 +++++++++++++----- 2 files changed, 142 insertions(+), 48 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index 87dd74de63d78..e4aff1b76e32f 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -1,6 +1,6 @@ import loaderUtils from 'next/dist/compiled/loader-utils' import sizeOf from 'image-size' -import { processBuffer } from '../../../server/lib/squoosh/main' +import { resizeImage } from '../../../server/image-optimizer' const BLUR_IMG_SIZE = 8 const BLUR_QUALITY = 70 @@ -38,14 +38,18 @@ function nextImageLoader(content) { blurDataURL = url.href.slice(prefix.length) } else { // Shrink the image's largest dimension - const resizeOperationOpts = - imageSize.width >= imageSize.height - ? { type: 'resize', width: BLUR_IMG_SIZE } - : { type: 'resize', height: BLUR_IMG_SIZE } + const dimension = + imageSize.width >= imageSize.height ? 'width' : 'height' const resizeImageSpan = imageLoaderSpan.traceChild('image-resize') const resizedImage = await resizeImageSpan.traceAsyncFn(() => - processBuffer(content, [resizeOperationOpts], extension, BLUR_QUALITY) + resizeImage( + content, + dimension, + BLUR_IMG_SIZE, + extension, + BLUR_QUALITY + ) ) const blurDataURLSpan = imageLoaderSpan.traceChild( 'image-base64-tostring' diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 2b3dfb2ccaac0..3a49f92fda025 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -29,6 +29,13 @@ const VECTOR_TYPES = [SVG] const BLUR_IMG_SIZE = 8 // should match `next-image-loader` const inflightRequests = new Map>() +let sharp: any +try { + sharp = require('sharp') +} catch (e) { + // Sharp not present on the server, Squoosh fallback will be used +} + export async function imageOptimizer( server: Server, req: IncomingMessage, @@ -314,53 +321,28 @@ export async function imageOptimizer( } else { contentType = JPEG } - try { - const orientation = await getOrientation(upstreamBuffer) + if (sharp) { + // Begin sharp transformation logic + const transformer = sharp(upstreamBuffer) - const operations: Operation[] = [] + transformer.rotate() - if (orientation === Orientation.RIGHT_TOP) { - operations.push({ type: 'rotate', numRotations: 1 }) - } else if (orientation === Orientation.BOTTOM_RIGHT) { - operations.push({ type: 'rotate', numRotations: 2 }) - } else if (orientation === Orientation.LEFT_BOTTOM) { - operations.push({ type: 'rotate', numRotations: 3 }) - } else { - // TODO: support more orientations - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // const _: never = orientation - } + const { width: metaWidth } = await transformer.metadata() - operations.push({ type: 'resize', width }) + if (metaWidth && metaWidth > width) { + transformer.resize(width) + } - let optimizedBuffer: Buffer | undefined - //if (contentType === AVIF) { - //} else - if (contentType === WEBP) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'webp', - quality - ) - } else if (contentType === PNG) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'png', - quality - ) - } else if (contentType === JPEG) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'jpeg', - quality - ) - } + if (contentType === WEBP) { + transformer.webp({ quality }) + } else if (contentType === PNG) { + transformer.png({ quality }) + } else if (contentType === JPEG) { + transformer.jpeg({ quality }) + } - if (optimizedBuffer) { + const optimizedBuffer = await transformer.toBuffer() await writeToCacheDir( hashDir, contentType, @@ -377,8 +359,74 @@ export async function imageOptimizer( isStatic, isDev ) + // End sharp transformation logic } else { - throw new Error('Unable to optimize buffer') + // Begin Squoosh transformation logic + const orientation = await getOrientation(upstreamBuffer) + + const operations: Operation[] = [] + + if (orientation === Orientation.RIGHT_TOP) { + operations.push({ type: 'rotate', numRotations: 1 }) + } else if (orientation === Orientation.BOTTOM_RIGHT) { + operations.push({ type: 'rotate', numRotations: 2 }) + } else if (orientation === Orientation.LEFT_BOTTOM) { + operations.push({ type: 'rotate', numRotations: 3 }) + } else { + // TODO: support more orientations + // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const _: never = orientation + } + + operations.push({ type: 'resize', width }) + + let optimizedBuffer: Buffer | undefined + //if (contentType === AVIF) { + //} else + if (contentType === WEBP) { + optimizedBuffer = await processBuffer( + upstreamBuffer, + operations, + 'webp', + quality + ) + } else if (contentType === PNG) { + optimizedBuffer = await processBuffer( + upstreamBuffer, + operations, + 'png', + quality + ) + } else if (contentType === JPEG) { + optimizedBuffer = await processBuffer( + upstreamBuffer, + operations, + 'jpeg', + quality + ) + } + + if (optimizedBuffer) { + await writeToCacheDir( + hashDir, + contentType, + maxAge, + expireAt, + optimizedBuffer + ) + sendResponse( + req, + res, + maxAge, + contentType, + optimizedBuffer, + isStatic, + isDev + ) + } else { + throw new Error('Unable to optimize buffer') + } + // End Squoosh transformation logic } } catch (error) { sendResponse( @@ -544,3 +592,45 @@ export function getMaxAge(str: string | null): number { } return minimum } + +export async function resizeImage( + content: Buffer, + dimension: 'width' | 'height', + size: number, + extension: 'webp' | 'png' | 'jpeg', + quality: number +) { + if (sharp) { + const transformer = sharp(content) + + if (extension === 'webp') { + transformer.webp({ quality }) + } else if (extension === 'png') { + transformer.png({ quality }) + } else if (extension === 'jpeg') { + transformer.jpeg({ quality }) + } + if (dimension === 'width') { + transformer.resize(size) + } else { + transformer.resize(null, size) + } + const buf = await transformer.toBuffer() + console.log( + 'Build-resizing image using SHARP in ' + (Date.now() - startTime) + 'ms' + ) + return buf + } else { + const resizeOperationOpts: Operation = + dimension === 'width' + ? { type: 'resize', width: size } + : { type: 'resize', height: size } + const buf = await processBuffer( + content, + [resizeOperationOpts], + extension, + quality + ) + return buf + } +} From 69518e3b60dc097ffe1df38a03bd9eba93d9204a Mon Sep 17 00:00:00 2001 From: atcastle Date: Sun, 18 Jul 2021 17:56:28 -0700 Subject: [PATCH 02/16] Refactor resizeImage and add sharp warning --- packages/next/server/image-optimizer.ts | 74 +++++++++++-------------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 3a49f92fda025..58f8b656308bb 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -47,6 +47,7 @@ export async function imageOptimizer( ) { const imageData: ImageConfig = nextConfig.images || imageConfigDefault const { deviceSizes = [], imageSizes = [], domains = [], loader } = imageData + let shouldShowSharpWarning = process.env.NODE_ENV === 'production' if (loader !== 'default') { await server.render404(req, res, parsedUrl) @@ -322,6 +323,7 @@ export async function imageOptimizer( contentType = JPEG } try { + let optimizedBuffer: Buffer | undefined if (sharp) { // Begin sharp transformation logic const transformer = sharp(upstreamBuffer) @@ -342,25 +344,17 @@ export async function imageOptimizer( transformer.jpeg({ quality }) } - const optimizedBuffer = await transformer.toBuffer() - await writeToCacheDir( - hashDir, - contentType, - maxAge, - expireAt, - optimizedBuffer - ) - sendResponse( - req, - res, - maxAge, - contentType, - optimizedBuffer, - isStatic, - isDev - ) + optimizedBuffer = await transformer.toBuffer() // End sharp transformation logic } else { + // Show sharp warning in production once + if (shouldShowSharpWarning) { + console.warn( + `WARNING: For production image optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp' or install sharp globally, and Next.js will use it automatically for image optimization.` + ) + shouldShowSharpWarning = false + } + // Begin Squoosh transformation logic const orientation = await getOrientation(upstreamBuffer) @@ -380,7 +374,6 @@ export async function imageOptimizer( operations.push({ type: 'resize', width }) - let optimizedBuffer: Buffer | undefined //if (contentType === AVIF) { //} else if (contentType === WEBP) { @@ -406,28 +399,28 @@ export async function imageOptimizer( ) } - if (optimizedBuffer) { - await writeToCacheDir( - hashDir, - contentType, - maxAge, - expireAt, - optimizedBuffer - ) - sendResponse( - req, - res, - maxAge, - contentType, - optimizedBuffer, - isStatic, - isDev - ) - } else { - throw new Error('Unable to optimize buffer') - } // End Squoosh transformation logic } + if (optimizedBuffer) { + await writeToCacheDir( + hashDir, + contentType, + maxAge, + expireAt, + optimizedBuffer + ) + sendResponse( + req, + res, + maxAge, + contentType, + optimizedBuffer, + isStatic, + isDev + ) + } else { + throw new Error('Unable to optimize buffer') + } } catch (error) { sendResponse( req, @@ -599,7 +592,7 @@ export async function resizeImage( size: number, extension: 'webp' | 'png' | 'jpeg', quality: number -) { +): Promise { if (sharp) { const transformer = sharp(content) @@ -616,9 +609,6 @@ export async function resizeImage( transformer.resize(null, size) } const buf = await transformer.toBuffer() - console.log( - 'Build-resizing image using SHARP in ' + (Date.now() - startTime) + 'ms' - ) return buf } else { const resizeOperationOpts: Operation = From c46fe9ba915931b11727f7df890dd24d9dc2f410 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 19 Jul 2021 16:29:45 -0700 Subject: [PATCH 03/16] only show sharp warning once per instance --- packages/next/server/image-optimizer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 58f8b656308bb..656cfab229df7 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -36,6 +36,8 @@ try { // Sharp not present on the server, Squoosh fallback will be used } +let shouldShowSharpWarning = process.env.NODE_ENV === 'production' + export async function imageOptimizer( server: Server, req: IncomingMessage, @@ -47,7 +49,6 @@ export async function imageOptimizer( ) { const imageData: ImageConfig = nextConfig.images || imageConfigDefault const { deviceSizes = [], imageSizes = [], domains = [], loader } = imageData - let shouldShowSharpWarning = process.env.NODE_ENV === 'production' if (loader !== 'default') { await server.render404(req, res, parsedUrl) From 0635318346cf82f7bca8a65d5d6fde17d2871ed4 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 20 Jul 2021 09:43:43 -0700 Subject: [PATCH 04/16] Modify sharp error message --- packages/next/server/image-optimizer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 98163185a12b9..389c757bd2153 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -359,7 +359,7 @@ export async function imageOptimizer( // Show sharp warning in production once if (shouldShowSharpWarning) { console.warn( - `WARNING: For production image optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp' or install sharp globally, and Next.js will use it automatically for image optimization.` + `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.` ) shouldShowSharpWarning = false } From f1543f4f90a51362c9bc57a2a0b30cc9bafaeeb9 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 20 Jul 2021 09:52:26 -0700 Subject: [PATCH 05/16] Add documentation for optional sharp dependency --- docs/basic-features/image-optimization.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index e9da1121f65b6..a86a1bd12c9cf 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -117,7 +117,7 @@ module.exports = { The following Image Optimization cloud providers are included: -- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel, no configuration necessary. [Learn more](https://vercel.com/docs/next.js/image-optimization) +- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel, no configuration necessary. [Learn more](/docs/next.js/image-optimization) - [Imgix](https://www.imgix.com): `loader: 'imgix'` - [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'` - [Akamai](https://www.akamai.com): `loader: 'akamai'` @@ -128,6 +128,8 @@ If you need a different provider, you can use the [`loader`](/docs/api-reference > The `next/image` component's default loader is not supported when using [`next export`](/docs/advanced-features/static-html-export.md). However, other loader options will work. +> By default, Next.js uses the ['squoosh'](https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. If you are going to use the default image loader and built-in image optimizer in production, it is strongly recommended that you install the optional [`sharp`](https://github.com/lovell/sharp) image optimization library. Do this by running `yarn add sharp` in your application directory. + ## Caching The following describes the caching algorithm for the default [loader](#loader). For all other loaders, please refer to your cloud provider's documentation. From e9ae0470258a031ff24a373a45701c360d5cfea4 Mon Sep 17 00:00:00 2001 From: Alex Castle Date: Tue, 20 Jul 2021 15:15:31 -0700 Subject: [PATCH 06/16] Update docs/basic-features/image-optimization.md Co-authored-by: Steven --- docs/basic-features/image-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index a86a1bd12c9cf..cce296fc5cf6d 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -128,7 +128,7 @@ If you need a different provider, you can use the [`loader`](/docs/api-reference > The `next/image` component's default loader is not supported when using [`next export`](/docs/advanced-features/static-html-export.md). However, other loader options will work. -> By default, Next.js uses the ['squoosh'](https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. If you are going to use the default image loader and built-in image optimizer in production, it is strongly recommended that you install the optional [`sharp`](https://github.com/lovell/sharp) image optimization library. Do this by running `yarn add sharp` in your application directory. +> The `next/image` component's default loader uses the ['squoosh'](https://www.npmjs.com/package/@squoosh/lib) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. For a production environment, it is strongly recommended that you install the optional [`sharp`](https://www.npmjs.com/package/sharp) library by running `yarn add sharp` in your project directory. ## Caching From bc5c2fa42ead1d215ef0e79cb3163df4b2376af5 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 20 Jul 2021 16:06:12 -0700 Subject: [PATCH 07/16] Import types for sharp --- docs/basic-features/image-optimization.md | 2 +- package.json | 1 + packages/next/server/image-optimizer.ts | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index cce296fc5cf6d..58a390aae6dca 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -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'` -- 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 +- Custom: `loader: 'custom'` use a custom cloud provider by implementing the [`loader`](https://vercel.com/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`. diff --git a/package.json b/package.json index cd1a90d36a8b1..5919da3ea226e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@types/fs-extra": "8.1.0", "@types/http-proxy": "1.17.3", "@types/jest": "24.0.13", + "@types/sharp": "0.28.4", "@types/string-hash": "1.1.1", "@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/parser": "4.22.0", diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 389c757bd2153..d7fbdaef16d6c 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -15,6 +15,7 @@ import { processBuffer, Operation } from './lib/squoosh/main' import Server from './next-server' import { sendEtagResponse } from './send-payload' import { getContentType, getExtension } from './serve-static' +import Sharp from 'sharp' //const AVIF = 'image/avif' const WEBP = 'image/webp' @@ -29,7 +30,11 @@ const VECTOR_TYPES = [SVG] const BLUR_IMG_SIZE = 8 // should match `next-image-loader` const inflightRequests = new Map>() -let sharp: any +let sharp: ( + input?: string | Buffer, + options?: Sharp.SharpOptions +) => Sharp.Sharp + try { sharp = require('sharp') } catch (e) { From 71e2a4f440a460bdad27239aa6d7a97a7a1535b6 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 20 Jul 2021 16:09:24 -0700 Subject: [PATCH 08/16] Update lockfile --- yarn.lock | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yarn.lock b/yarn.lock index 74beced370355..d2fb8074cbb84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4316,6 +4316,13 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/sharp@0.28.4": + version "0.28.4" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.28.4.tgz#7afdcf979069ddc68a915603fdaa412ada93f833" + integrity sha512-vfz+RlJU5FgXyyf9w8wc+JwwT8MFI98NZr0272umQegrggAQhTwwb8pKZn0PTtd+j0crXkZDWw5ABG/i6Lu/Lw== + dependencies: + "@types/node" "*" + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" From 68f4e00098c63dad21b8d2fd7ff1e8c6ba9946e6 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 15:45:22 -0500 Subject: [PATCH 09/16] Add testing against sharp --- docs/basic-features/image-optimization.md | 4 +- errors/manifest.json | 4 + errors/sharp-missing-in-production.md | 13 + packages/next/server/image-optimizer.ts | 18 +- .../image-optimizer/app/.gitignore | 3 + .../image-optimizer/{ => app}/next.config.js | 0 .../image-optimizer/{ => app}/pages/index.js | 0 .../{ => app}/public/animated.gif | Bin .../{ => app}/public/animated.png | Bin .../{ => app}/public/animated.webp | Bin .../{ => app}/public/grayscale.png | Bin .../image-optimizer/{ => app}/public/test.bmp | Bin .../image-optimizer/{ => app}/public/test.gif | Bin .../image-optimizer/{ => app}/public/test.ico | Bin .../image-optimizer/{ => app}/public/test.jpg | Bin .../image-optimizer/{ => app}/public/test.png | Bin .../image-optimizer/{ => app}/public/test.svg | 0 .../{ => app}/public/test.tiff | Bin .../image-optimizer/{ => app}/public/text.txt | 0 ...st.js => detect-content-type.unit.test.js} | 8 +- ...x-age.test.js => get-max-age.unit.test.js} | 0 .../image-optimizer/test/index.test.js | 277 +++++++++++------- 22 files changed, 211 insertions(+), 116 deletions(-) create mode 100644 errors/sharp-missing-in-production.md create mode 100644 test/integration/image-optimizer/app/.gitignore rename test/integration/image-optimizer/{ => app}/next.config.js (100%) rename test/integration/image-optimizer/{ => app}/pages/index.js (100%) rename test/integration/image-optimizer/{ => app}/public/animated.gif (100%) rename test/integration/image-optimizer/{ => app}/public/animated.png (100%) rename test/integration/image-optimizer/{ => app}/public/animated.webp (100%) rename test/integration/image-optimizer/{ => app}/public/grayscale.png (100%) rename test/integration/image-optimizer/{ => app}/public/test.bmp (100%) rename test/integration/image-optimizer/{ => app}/public/test.gif (100%) rename test/integration/image-optimizer/{ => app}/public/test.ico (100%) rename test/integration/image-optimizer/{ => app}/public/test.jpg (100%) rename test/integration/image-optimizer/{ => app}/public/test.png (100%) rename test/integration/image-optimizer/{ => app}/public/test.svg (100%) rename test/integration/image-optimizer/{ => app}/public/test.tiff (100%) rename test/integration/image-optimizer/{ => app}/public/text.txt (100%) rename test/integration/image-optimizer/test/{detect-content-type.test.js => detect-content-type.unit.test.js} (74%) rename test/integration/image-optimizer/test/{get-max-age.test.js => get-max-age.unit.test.js} (100%) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 58a390aae6dca..4dd69119ddf65 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -117,11 +117,11 @@ module.exports = { The following Image Optimization cloud providers are included: -- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel, no configuration necessary. [Learn more](/docs/next.js/image-optimization) +- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel, no configuration necessary. [Learn more](https://vercel.com/docs/next.js/image-optimization) - [Imgix](https://www.imgix.com): `loader: 'imgix'` - [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'` - [Akamai](https://www.akamai.com): `loader: 'akamai'` -- Custom: `loader: 'custom'` use a custom cloud provider by implementing the [`loader`](https://vercel.com/docs/api-reference/next/image.md#loader) prop on the `next/image` component +- 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`. diff --git a/errors/manifest.json b/errors/manifest.json index 1ba5b512db5f4..7948b55bc5e7d 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -426,6 +426,10 @@ { "title": "page-data-collection-timeout", "path": "/errors/page-data-collection-timeout.md" + }, + { + "titlle": "sharp-missing-in-production", + "path": "/errors/sharp-missing-in-production.md" } ] } diff --git a/errors/sharp-missing-in-production.md b/errors/sharp-missing-in-production.md new file mode 100644 index 0000000000000..77aebfe6b4750 --- /dev/null +++ b/errors/sharp-missing-in-production.md @@ -0,0 +1,13 @@ +# Sharp Missing In Production + +#### Why This Error Occurred + +The `next/image` component's default loader uses the ['squoosh'](https://www.npmjs.com/package/@squoosh/lib) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. For a production environment, it is strongly recommended that you install the optional [`sharp`](https://www.npmjs.com/package/sharp). This package was not detected when leveraging the image optimization in production mode (`next start`). + +#### Possible Ways to Fix It + +Install `sharp` by running `yarn add sharp` in your project directory. + +### Useful Links + +- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index d7fbdaef16d6c..f2ef7a438ebd5 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -15,7 +15,7 @@ import { processBuffer, Operation } from './lib/squoosh/main' import Server from './next-server' import { sendEtagResponse } from './send-payload' import { getContentType, getExtension } from './serve-static' -import Sharp from 'sharp' +import chalk from 'chalk' //const AVIF = 'image/avif' const WEBP = 'image/webp' @@ -30,13 +30,15 @@ const VECTOR_TYPES = [SVG] const BLUR_IMG_SIZE = 8 // should match `next-image-loader` const inflightRequests = new Map>() -let sharp: ( - input?: string | Buffer, - options?: Sharp.SharpOptions -) => Sharp.Sharp +let sharp: + | (( + input?: string | Buffer, + options?: import('sharp').SharpOptions + ) => import('sharp').Sharp) + | undefined try { - sharp = require('sharp') + sharp = require(process.env.__NEXT_TEST_SHARP_PATH || 'sharp') } catch (e) { // Sharp not present on the server, Squoosh fallback will be used } @@ -364,7 +366,9 @@ export async function imageOptimizer( // Show sharp warning in production once if (shouldShowSharpWarning) { console.warn( - `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.` + 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/improper-devtool' ) shouldShowSharpWarning = false } diff --git a/test/integration/image-optimizer/app/.gitignore b/test/integration/image-optimizer/app/.gitignore new file mode 100644 index 0000000000000..259596b506cff --- /dev/null +++ b/test/integration/image-optimizer/app/.gitignore @@ -0,0 +1,3 @@ +node_modules +package.json +yarn.lock \ No newline at end of file diff --git a/test/integration/image-optimizer/next.config.js b/test/integration/image-optimizer/app/next.config.js similarity index 100% rename from test/integration/image-optimizer/next.config.js rename to test/integration/image-optimizer/app/next.config.js diff --git a/test/integration/image-optimizer/pages/index.js b/test/integration/image-optimizer/app/pages/index.js similarity index 100% rename from test/integration/image-optimizer/pages/index.js rename to test/integration/image-optimizer/app/pages/index.js diff --git a/test/integration/image-optimizer/public/animated.gif b/test/integration/image-optimizer/app/public/animated.gif similarity index 100% rename from test/integration/image-optimizer/public/animated.gif rename to test/integration/image-optimizer/app/public/animated.gif diff --git a/test/integration/image-optimizer/public/animated.png b/test/integration/image-optimizer/app/public/animated.png similarity index 100% rename from test/integration/image-optimizer/public/animated.png rename to test/integration/image-optimizer/app/public/animated.png diff --git a/test/integration/image-optimizer/public/animated.webp b/test/integration/image-optimizer/app/public/animated.webp similarity index 100% rename from test/integration/image-optimizer/public/animated.webp rename to test/integration/image-optimizer/app/public/animated.webp diff --git a/test/integration/image-optimizer/public/grayscale.png b/test/integration/image-optimizer/app/public/grayscale.png similarity index 100% rename from test/integration/image-optimizer/public/grayscale.png rename to test/integration/image-optimizer/app/public/grayscale.png diff --git a/test/integration/image-optimizer/public/test.bmp b/test/integration/image-optimizer/app/public/test.bmp similarity index 100% rename from test/integration/image-optimizer/public/test.bmp rename to test/integration/image-optimizer/app/public/test.bmp diff --git a/test/integration/image-optimizer/public/test.gif b/test/integration/image-optimizer/app/public/test.gif similarity index 100% rename from test/integration/image-optimizer/public/test.gif rename to test/integration/image-optimizer/app/public/test.gif diff --git a/test/integration/image-optimizer/public/test.ico b/test/integration/image-optimizer/app/public/test.ico similarity index 100% rename from test/integration/image-optimizer/public/test.ico rename to test/integration/image-optimizer/app/public/test.ico diff --git a/test/integration/image-optimizer/public/test.jpg b/test/integration/image-optimizer/app/public/test.jpg similarity index 100% rename from test/integration/image-optimizer/public/test.jpg rename to test/integration/image-optimizer/app/public/test.jpg diff --git a/test/integration/image-optimizer/public/test.png b/test/integration/image-optimizer/app/public/test.png similarity index 100% rename from test/integration/image-optimizer/public/test.png rename to test/integration/image-optimizer/app/public/test.png diff --git a/test/integration/image-optimizer/public/test.svg b/test/integration/image-optimizer/app/public/test.svg similarity index 100% rename from test/integration/image-optimizer/public/test.svg rename to test/integration/image-optimizer/app/public/test.svg diff --git a/test/integration/image-optimizer/public/test.tiff b/test/integration/image-optimizer/app/public/test.tiff similarity index 100% rename from test/integration/image-optimizer/public/test.tiff rename to test/integration/image-optimizer/app/public/test.tiff diff --git a/test/integration/image-optimizer/public/text.txt b/test/integration/image-optimizer/app/public/text.txt similarity index 100% rename from test/integration/image-optimizer/public/text.txt rename to test/integration/image-optimizer/app/public/text.txt diff --git a/test/integration/image-optimizer/test/detect-content-type.test.js b/test/integration/image-optimizer/test/detect-content-type.unit.test.js similarity index 74% rename from test/integration/image-optimizer/test/detect-content-type.test.js rename to test/integration/image-optimizer/test/detect-content-type.unit.test.js index 3902daf1c1fbb..8e881dbfb5ca4 100644 --- a/test/integration/image-optimizer/test/detect-content-type.test.js +++ b/test/integration/image-optimizer/test/detect-content-type.unit.test.js @@ -7,19 +7,19 @@ const getImage = (filepath) => readFile(join(__dirname, filepath)) describe('detectContentType', () => { it('should return jpg', async () => { - const buffer = await getImage('../public/test.jpg') + const buffer = await getImage('../app/public/test.jpg') expect(detectContentType(buffer)).toBe('image/jpeg') }) it('should return png', async () => { - const buffer = await getImage('../public/test.png') + const buffer = await getImage('../app/public/test.png') expect(detectContentType(buffer)).toBe('image/png') }) it('should return webp', async () => { - const buffer = await getImage('../public/animated.webp') + const buffer = await getImage('../app/public/animated.webp') expect(detectContentType(buffer)).toBe('image/webp') }) it('should return svg', async () => { - const buffer = await getImage('../public/test.svg') + const buffer = await getImage('../app/public/test.svg') expect(detectContentType(buffer)).toBe('image/svg+xml') }) }) diff --git a/test/integration/image-optimizer/test/get-max-age.test.js b/test/integration/image-optimizer/test/get-max-age.unit.test.js similarity index 100% rename from test/integration/image-optimizer/test/get-max-age.test.js rename to test/integration/image-optimizer/test/get-max-age.unit.test.js diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index 6a8145b3272f3..c1c06bb109e70 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -1,4 +1,5 @@ /* eslint-env jest */ +import execa from 'execa' import fs from 'fs-extra' import sizeOf from 'image-size' import { @@ -15,9 +16,9 @@ import { import isAnimated from 'next/dist/compiled/is-animated' import { join } from 'path' -jest.setTimeout(1000 * 60 * 2) +jest.setTimeout(1000 * 60 * 5) -const appDir = join(__dirname, '../') +const appDir = join(__dirname, '../app') const imagesDir = join(appDir, '.next', 'cache', 'images') const nextConfig = new File(join(appDir, 'next.config.js')) const largeSize = 1080 // defaults defined in server/config.ts @@ -105,7 +106,7 @@ function runTests({ w, isDev, domains = [], ttl }) { expect(res.headers.get('etag')).toBeTruthy() const actual = await res.text() const expected = await fs.readFile( - join(__dirname, '..', 'public', 'test.svg'), + join(appDir, 'public', 'test.svg'), 'utf8' ) expect(actual).toMatch(expected) @@ -124,7 +125,7 @@ function runTests({ w, isDev, domains = [], ttl }) { expect(res.headers.get('etag')).toBeTruthy() const actual = await res.text() const expected = await fs.readFile( - join(__dirname, '..', 'public', 'test.ico'), + join(appDir, 'public', 'test.ico'), 'utf8' ) expect(actual).toMatch(expected) @@ -782,81 +783,6 @@ describe('Image Optimizer', () => { 'image-optimization-test.vercel.app', ] - describe('dev support w/o next.config.js', () => { - const size = 384 // defaults defined in server/config.ts - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(async () => { - await killApp(app) - await fs.remove(imagesDir) - }) - - runTests({ w: size, isDev: true, domains: [] }) - }) - - describe('dev support with next.config.js', () => { - const size = 64 - beforeAll(async () => { - const json = JSON.stringify({ - images: { - deviceSizes: [largeSize], - imageSizes: [size], - domains, - }, - }) - nextConfig.replace('{ /* replaceme */ }', json) - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(async () => { - await killApp(app) - nextConfig.restore() - await fs.remove(imagesDir) - }) - - runTests({ w: size, isDev: true, domains }) - }) - - describe('Server support w/o next.config.js', () => { - const size = 384 // defaults defined in server/config.ts - beforeAll(async () => { - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(async () => { - await killApp(app) - await fs.remove(imagesDir) - }) - - runTests({ w: size, isDev: false, domains: [] }) - }) - - describe('Server support with next.config.js', () => { - const size = 128 - beforeAll(async () => { - const json = JSON.stringify({ - images: { - deviceSizes: [size, largeSize], - domains, - }, - }) - nextConfig.replace('{ /* replaceme */ }', json) - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(async () => { - await killApp(app) - nextConfig.restore() - await fs.remove(imagesDir) - }) - - runTests({ w: size, isDev: false, domains }) - }) - describe('Server support for minimumCacheTTL in next.config.js', () => { const size = 96 // defaults defined in server/config.ts const ttl = 5 // super low ttl in seconds @@ -932,30 +858,6 @@ describe('Image Optimizer', () => { }) }) - describe('Serverless support with next.config.js', () => { - const size = 256 - beforeAll(async () => { - const json = JSON.stringify({ - target: 'experimental-serverless-trace', - images: { - deviceSizes: [size, largeSize], - domains, - }, - }) - nextConfig.replace('{ /* replaceme */ }', json) - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(async () => { - await killApp(app) - nextConfig.restore() - await fs.remove(imagesDir) - }) - - runTests({ w: size, isDev: false, domains }) - }) - describe('dev support next.config.js cloudinary loader', () => { beforeAll(async () => { const json = JSON.stringify({ @@ -1045,4 +947,173 @@ describe('Image Optimizer', () => { await expectWidth(res, 8) }) }) + + const sharpMissingText = `For production image optimization with Next.js, the optional 'sharp' package is strongly recommended` + + const setupTests = (isSharp = false) => { + describe('dev support w/o next.config.js', () => { + let output = '' + const size = 384 // defaults defined in server/config.ts + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + output += msg + }, + cwd: appDir, + }) + }) + afterAll(async () => { + await killApp(app) + await fs.remove(imagesDir) + }) + + runTests({ w: size, isDev: true, domains: [] }) + + it('should not have sharp warning in development', () => { + expect(output).not.toContain(sharpMissingText) + }) + }) + + describe('dev support with next.config.js', () => { + let output = '' + const size = 64 + beforeAll(async () => { + const json = JSON.stringify({ + images: { + deviceSizes: [largeSize], + imageSizes: [size], + domains, + }, + }) + nextConfig.replace('{ /* replaceme */ }', json) + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + output += msg + }, + cwd: appDir, + }) + }) + afterAll(async () => { + await killApp(app) + nextConfig.restore() + await fs.remove(imagesDir) + }) + + runTests({ w: size, isDev: true, domains }) + + it('should not have sharp warning in development', () => { + expect(output).not.toContain(sharpMissingText) + }) + }) + + describe('Server support w/o next.config.js', () => { + let output = '' + const size = 384 // defaults defined in server/config.ts + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort, { + onStderr(msg) { + output += msg + }, + env: { + __NEXT_TEST_SHARP_PATH: isSharp + ? require.resolve('sharp', { + paths: [join(appDir, 'node_modules')], + }) + : '', + }, + cwd: appDir, + }) + }) + afterAll(async () => { + await killApp(app) + await fs.remove(imagesDir) + }) + + runTests({ w: size, isDev: false, domains: [] }) + + if (isSharp) { + it('should not have sharp warning when installed', () => { + expect(output).not.toContain(sharpMissingText) + }) + } else { + it('should have sharp warning when not installed', () => { + expect(output).toContain(sharpMissingText) + }) + } + }) + + describe('Server support with next.config.js', () => { + const size = 128 + let output = '' + beforeAll(async () => { + const json = JSON.stringify({ + images: { + deviceSizes: [size, largeSize], + domains, + }, + }) + nextConfig.replace('{ /* replaceme */ }', json) + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort, { + onStderr(msg) { + output += msg + }, + env: { + __NEXT_TEST_SHARP_PATH: isSharp + ? require.resolve('sharp', { + paths: [join(appDir, 'node_modules')], + }) + : '', + }, + cwd: appDir, + }) + }) + afterAll(async () => { + await killApp(app) + nextConfig.restore() + await fs.remove(imagesDir) + }) + + runTests({ w: size, isDev: false, domains }) + + if (isSharp) { + it('should not have sharp warning when installed', () => { + expect(output).not.toContain(sharpMissingText) + }) + } else { + it('should have sharp warning when not installed', () => { + expect(output).toContain(sharpMissingText) + }) + } + }) + } + + describe('with squoosh', () => { + setupTests() + }) + + describe('with sharp', () => { + beforeAll(async () => { + await execa('yarn', ['init', '-y'], { + cwd: appDir, + stdio: 'inherit', + }) + await execa('yarn', ['add', 'sharp'], { + 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(true) + }) }) From 60420356a2a81c00ddd15b8e678fc53017f0fbe6 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 16:02:04 -0500 Subject: [PATCH 10/16] use fs-extra for node 12 --- .../image-optimizer/test/detect-content-type.unit.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/image-optimizer/test/detect-content-type.unit.test.js b/test/integration/image-optimizer/test/detect-content-type.unit.test.js index 8e881dbfb5ca4..bc2df8ba165a2 100644 --- a/test/integration/image-optimizer/test/detect-content-type.unit.test.js +++ b/test/integration/image-optimizer/test/detect-content-type.unit.test.js @@ -1,6 +1,6 @@ /* eslint-env jest */ import { detectContentType } from '../../../../packages/next/dist/server/image-optimizer.js' -import { readFile } from 'fs/promises' +import { readFile } from 'fs-extra' import { join } from 'path' const getImage = (filepath) => readFile(join(__dirname, filepath)) From 180f074a4f0535171ec18cc3c2755fbcd06df26b Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 16:22:15 -0500 Subject: [PATCH 11/16] Rename test sharp path variable --- docs/basic-features/image-optimization.md | 2 +- packages/next/server/image-optimizer.ts | 2 +- test/integration/image-optimizer/test/index.test.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 4dd69119ddf65..f7ce5c5d92230 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -128,7 +128,7 @@ If you need a different provider, you can use the [`loader`](/docs/api-reference > The `next/image` component's default loader is not supported when using [`next export`](/docs/advanced-features/static-html-export.md). However, other loader options will work. -> The `next/image` component's default loader uses the ['squoosh'](https://www.npmjs.com/package/@squoosh/lib) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. For a production environment, it is strongly recommended that you install the optional [`sharp`](https://www.npmjs.com/package/sharp) library by running `yarn add sharp` in your project directory. +> The `next/image` component's default loader uses the ['squoosh'](https://www.npmjs.com/package/@squoosh/lib) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. For a production environment, it is strongly recommended that you install the optional [`sharp`](https://www.npmjs.com/package/sharp) library by running `yarn add sharp` in your project directory. If sharp is already installed but can't be resolved you can manually pass the path to it via the `NEXT_SHARP_PATH` environment variable e.g. `NEXT_SHARP_PATH=/tmp/node_modules/sharp` ## Caching diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index f2ef7a438ebd5..41d6324b9a170 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -38,7 +38,7 @@ let sharp: | undefined try { - sharp = require(process.env.__NEXT_TEST_SHARP_PATH || 'sharp') + sharp = require(process.env.NEXT_SHARP_PATH || 'sharp') } catch (e) { // Sharp not present on the server, Squoosh fallback will be used } diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index c1c06bb109e70..dc324caa5dc91 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -1019,7 +1019,7 @@ describe('Image Optimizer', () => { output += msg }, env: { - __NEXT_TEST_SHARP_PATH: isSharp + NEXT_SHARP_PATH: isSharp ? require.resolve('sharp', { paths: [join(appDir, 'node_modules')], }) @@ -1064,7 +1064,7 @@ describe('Image Optimizer', () => { output += msg }, env: { - __NEXT_TEST_SHARP_PATH: isSharp + NEXT_SHARP_PATH: isSharp ? require.resolve('sharp', { paths: [join(appDir, 'node_modules')], }) From 82c0a8fd3da6c62f5afc6df6b494b93b9d9a06f8 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 16:24:50 -0500 Subject: [PATCH 12/16] Apply suggestions from code review Co-authored-by: Steven --- errors/sharp-missing-in-production.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors/sharp-missing-in-production.md b/errors/sharp-missing-in-production.md index 77aebfe6b4750..01fc602b4b4e3 100644 --- a/errors/sharp-missing-in-production.md +++ b/errors/sharp-missing-in-production.md @@ -2,7 +2,7 @@ #### Why This Error Occurred -The `next/image` component's default loader uses the ['squoosh'](https://www.npmjs.com/package/@squoosh/lib) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. For a production environment, it is strongly recommended that you install the optional [`sharp`](https://www.npmjs.com/package/sharp). This package was not detected when leveraging the image optimization in production mode (`next start`). +The `next/image` component's default loader uses the ['squoosh'](https://www.npmjs.com/package/@squoosh/lib) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. For a production environment, it is strongly recommended that you install the optional [`sharp`](https://www.npmjs.com/package/sharp). This package was not detected when leveraging the Image Optimization in production mode (`next start`). #### Possible Ways to Fix It From 38a43c844491ef465e2d73d0eff56f4a78f21eab Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 16:37:47 -0500 Subject: [PATCH 13/16] update squoosh specific test --- .../image-optimizer/test/index.test.js | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index dc324caa5dc91..ab8b4bd0d30fc 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -46,7 +46,7 @@ async function expectWidth(res, w) { expect(d.width).toBe(w) } -function runTests({ w, isDev, domains = [], ttl }) { +function runTests({ w, isDev, domains = [], ttl, isSharp }) { it('should return home page', async () => { const res = await fetchViaHTTP(appPort, '/', null, {}) expect(await res.text()).toMatch(/Image Optimizer Home/m) @@ -542,26 +542,30 @@ function runTests({ w, isDev, domains = [], ttl }) { await expectWidth(res, 400) }) - it('should not change the color type of a png', async () => { - // https://github.com/vercel/next.js/issues/22929 - // A grayscaled PNG with transparent pixels. - const query = { url: '/grayscale.png', w: largeSize, q: 80 } - const opts = { headers: { accept: 'image/png' } } - const res = await fetchViaHTTP(appPort, '/_next/image', query, opts) - expect(res.status).toBe(200) - expect(res.headers.get('Content-Type')).toBe('image/png') - expect(res.headers.get('Cache-Control')).toBe( - `public, max-age=0, must-revalidate` - ) - expect(res.headers.get('Vary')).toBe('Accept') + if (!isSharp) { + // this checks for specific color type output by squoosh + // which differs in sharp + it('should not change the color type of a png', async () => { + // https://github.com/vercel/next.js/issues/22929 + // A grayscaled PNG with transparent pixels. + const query = { url: '/grayscale.png', w: largeSize, q: 80 } + const opts = { headers: { accept: 'image/png' } } + const res = await fetchViaHTTP(appPort, '/_next/image', query, opts) + expect(res.status).toBe(200) + expect(res.headers.get('Content-Type')).toBe('image/png') + expect(res.headers.get('Cache-Control')).toBe( + `public, max-age=0, must-revalidate` + ) + expect(res.headers.get('Vary')).toBe('Accept') - const png = await res.buffer() + const png = await res.buffer() - // Read the color type byte (offset 9 + magic number 16). - // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html - const colorType = png.readUIntBE(25, 1) - expect(colorType).toBe(4) - }) + // Read the color type byte (offset 9 + magic number 16). + // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html + const colorType = png.readUIntBE(25, 1) + expect(colorType).toBe(4) + }) + } it('should set cache-control to immutable for static images', async () => { if (!isDev) { @@ -968,7 +972,7 @@ describe('Image Optimizer', () => { await fs.remove(imagesDir) }) - runTests({ w: size, isDev: true, domains: [] }) + runTests({ w: size, isDev: true, domains: [], isSharp }) it('should not have sharp warning in development', () => { expect(output).not.toContain(sharpMissingText) @@ -1001,7 +1005,7 @@ describe('Image Optimizer', () => { await fs.remove(imagesDir) }) - runTests({ w: size, isDev: true, domains }) + runTests({ w: size, isDev: true, domains, isSharp }) it('should not have sharp warning in development', () => { expect(output).not.toContain(sharpMissingText) @@ -1033,7 +1037,7 @@ describe('Image Optimizer', () => { await fs.remove(imagesDir) }) - runTests({ w: size, isDev: false, domains: [] }) + runTests({ w: size, isDev: false, domains: [], isSharp }) if (isSharp) { it('should not have sharp warning when installed', () => { @@ -1079,7 +1083,7 @@ describe('Image Optimizer', () => { await fs.remove(imagesDir) }) - runTests({ w: size, isDev: false, domains }) + runTests({ w: size, isDev: false, domains, isSharp }) if (isSharp) { it('should not have sharp warning when installed', () => { From 1c1f9966eedec1fdd095e6730b0fa13d1bd46147 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 16:41:11 -0500 Subject: [PATCH 14/16] Apply suggestions from code review Co-authored-by: Steven --- test/integration/image-optimizer/test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index ab8b4bd0d30fc..2c32398ca4e1f 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -952,7 +952,7 @@ describe('Image Optimizer', () => { }) }) - const sharpMissingText = `For production image optimization with Next.js, the optional 'sharp' package is strongly recommended` + const sharpMissingText = `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended` const setupTests = (isSharp = false) => { describe('dev support w/o next.config.js', () => { From 040e283c61fedddb3e4d6d33a38f86f8834d4ded Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 16:56:06 -0500 Subject: [PATCH 15/16] update tests --- packages/next/server/image-optimizer.ts | 2 +- .../image-optimizer/test/index.test.js | 66 ++++++++----------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 41d6324b9a170..089e3a1890334 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -367,7 +367,7 @@ export async function imageOptimizer( if (shouldShowSharpWarning) { 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` + + `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/improper-devtool' ) shouldShowSharpWarning = false diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index 2c32398ca4e1f..9bde62dc650f2 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -22,9 +22,12 @@ const appDir = join(__dirname, '../app') const imagesDir = join(appDir, '.next', 'cache', 'images') const nextConfig = new File(join(appDir, 'next.config.js')) const largeSize = 1080 // defaults defined in server/config.ts +let nextOutput let appPort let app +const sharpMissingText = `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended` + async function fsToJson(dir, output = {}) { const files = await fs.readdir(dir) for (let file of files) { @@ -622,6 +625,16 @@ function runTests({ w, isDev, domains = [], ttl, isSharp }) { const json1 = await fsToJson(imagesDir) expect(Object.keys(json1).length).toBe(1) }) + + if (isDev || isSharp) { + it('should not have sharp missing warning', () => { + expect(nextOutput).not.toContain(sharpMissingText) + }) + } else { + it('should have sharp missing warning', () => { + expect(nextOutput).toContain(sharpMissingText) + }) + } } describe('Image Optimizer', () => { @@ -796,10 +809,15 @@ describe('Image Optimizer', () => { minimumCacheTTL: ttl, }, }) + nextOutput = '' nextConfig.replace('{ /* replaceme */ }', json) await nextBuild(appDir) appPort = await findPort() - app = await nextStart(appDir, appPort) + app = await nextStart(appDir, appPort, { + onStderr(msg) { + nextOutput += msg + }, + }) }) afterAll(async () => { await killApp(app) @@ -952,17 +970,15 @@ describe('Image Optimizer', () => { }) }) - const sharpMissingText = `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended` - const setupTests = (isSharp = false) => { describe('dev support w/o next.config.js', () => { - let output = '' const size = 384 // defaults defined in server/config.ts beforeAll(async () => { + nextOutput = '' appPort = await findPort() app = await launchApp(appDir, appPort, { onStderr(msg) { - output += msg + nextOutput += msg }, cwd: appDir, }) @@ -973,14 +989,9 @@ describe('Image Optimizer', () => { }) runTests({ w: size, isDev: true, domains: [], isSharp }) - - it('should not have sharp warning in development', () => { - expect(output).not.toContain(sharpMissingText) - }) }) describe('dev support with next.config.js', () => { - let output = '' const size = 64 beforeAll(async () => { const json = JSON.stringify({ @@ -990,11 +1001,12 @@ describe('Image Optimizer', () => { domains, }, }) + nextOutput = '' nextConfig.replace('{ /* replaceme */ }', json) appPort = await findPort() app = await launchApp(appDir, appPort, { onStderr(msg) { - output += msg + nextOutput += msg }, cwd: appDir, }) @@ -1006,21 +1018,17 @@ describe('Image Optimizer', () => { }) runTests({ w: size, isDev: true, domains, isSharp }) - - it('should not have sharp warning in development', () => { - expect(output).not.toContain(sharpMissingText) - }) }) describe('Server support w/o next.config.js', () => { - let output = '' const size = 384 // defaults defined in server/config.ts beforeAll(async () => { + nextOutput = '' await nextBuild(appDir) appPort = await findPort() app = await nextStart(appDir, appPort, { onStderr(msg) { - output += msg + nextOutput += msg }, env: { NEXT_SHARP_PATH: isSharp @@ -1038,21 +1046,10 @@ describe('Image Optimizer', () => { }) runTests({ w: size, isDev: false, domains: [], isSharp }) - - if (isSharp) { - it('should not have sharp warning when installed', () => { - expect(output).not.toContain(sharpMissingText) - }) - } else { - it('should have sharp warning when not installed', () => { - expect(output).toContain(sharpMissingText) - }) - } }) describe('Server support with next.config.js', () => { const size = 128 - let output = '' beforeAll(async () => { const json = JSON.stringify({ images: { @@ -1060,12 +1057,13 @@ describe('Image Optimizer', () => { domains, }, }) + nextOutput = '' nextConfig.replace('{ /* replaceme */ }', json) await nextBuild(appDir) appPort = await findPort() app = await nextStart(appDir, appPort, { onStderr(msg) { - output += msg + nextOutput += msg }, env: { NEXT_SHARP_PATH: isSharp @@ -1084,16 +1082,6 @@ describe('Image Optimizer', () => { }) runTests({ w: size, isDev: false, domains, isSharp }) - - if (isSharp) { - it('should not have sharp warning when installed', () => { - expect(output).not.toContain(sharpMissingText) - }) - } else { - it('should have sharp warning when not installed', () => { - expect(output).toContain(sharpMissingText) - }) - } }) } From 2eb9f5f0bda5b4918e1def2bc2afe13d765ab138 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 22 Jul 2021 17:00:34 -0500 Subject: [PATCH 16/16] Apply suggestions from code review Co-authored-by: Steven --- packages/next/server/image-optimizer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 089e3a1890334..5ded690e69cca 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -368,7 +368,7 @@ export async function imageOptimizer( 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/improper-devtool' + 'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production' ) shouldShowSharpWarning = false }