From 6ba53d8f1fdad3c0586c04627b3ab4016fe7624f Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Sun, 11 Dec 2022 21:43:08 -0600 Subject: [PATCH 1/3] Update flakey dev context tests (#43951) x-ref: https://github.com/vercel/next.js/actions/runs/3672193364/jobs/6208192356 x-ref: https://github.com/vercel/next.js/actions/runs/3672193364/jobs/6208192142 x-ref: https://github.com/vercel/next.js/actions/runs/3672172302/jobs/6208150047 --- .../server-components.test.ts.snap | 42 ------------------- .../acceptance-app/server-components.test.ts | 27 ++++++++++-- 2 files changed, 23 insertions(+), 46 deletions(-) delete mode 100644 test/development/acceptance-app/__snapshots__/server-components.test.ts.snap diff --git a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap deleted file mode 100644 index 5c76475e0fc1da1..000000000000000 --- a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called 1`] = ` -" 1 of 1 unhandled error -Server Error - -TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context - -This error happened while generating the page. Any console logs will be displayed in the terminal window. - -app/page.js (3:24) @ React - - 1 | - 2 | import React from 'react' -> 3 | const Context = React.createContext() - | ^ - 4 | export default function Page() { - 5 | return ( - 6 | <>" -`; - -exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called in external package 1`] = ` -" 1 of 1 unhandled error -Server Error - -TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context - -This error happened while generating the page. Any console logs will be displayed in the terminal window. - -null" -`; - -exports[`Error Overlay for server components createContext called in Server Component should show error when createContext is called in external package 1`] = ` -" 1 of 1 unhandled error -Server Error - -TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context - -This error happened while generating the page. Any console logs will be displayed in the terminal window. - -null" -`; diff --git a/test/development/acceptance-app/server-components.test.ts b/test/development/acceptance-app/server-components.test.ts index 47246440498c07c..cdace90cd7b8d05 100644 --- a/test/development/acceptance-app/server-components.test.ts +++ b/test/development/acceptance-app/server-components.test.ts @@ -3,6 +3,7 @@ import { sandbox } from './helpers' import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' import path from 'path' +import { check } from 'next-test-utils' describe('Error Overlay for server components', () => { if (process.env.NEXT_TEST_REACT_VERSION === '^17') { @@ -51,7 +52,12 @@ describe('Error Overlay for server components', () => { await browser.refresh() expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource(true)).toMatchSnapshot() + await check(async () => { + expect(await session.getRedboxSource(true)).toContain( + `TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component` + ) + return 'success' + }, 'success') expect(next.cliOutput).toContain( 'createContext only works in Client Components' ) @@ -100,7 +106,14 @@ describe('Error Overlay for server components', () => { await browser.refresh() expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource(true)).toMatchSnapshot() + + await check(async () => { + expect(await session.getRedboxSource(true)).toContain( + `TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component` + ) + return 'success' + }, 'success') + expect(next.cliOutput).toContain( 'createContext only works in Client Components' ) @@ -149,11 +162,17 @@ describe('Error Overlay for server components', () => { await browser.refresh() expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource(true)).toMatchSnapshot() + + await check(async () => { + expect(await session.getRedboxSource(true)).toContain( + `TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component` + ) + return 'success' + }, 'success') + expect(next.cliOutput).toContain( 'createContext only works in Client Components' ) - await cleanup() }) }) From f6f1f50238aab9d9da766a19f800d562b28d568a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Mon, 12 Dec 2022 05:35:55 +0100 Subject: [PATCH 2/3] Increase stack trace limit on the server (#43800) Increases `Error.stackTraceLimit` on the server to match the client. Adds `if (typeof window !== 'undefined') {` in `use-error-handler`, otherwise it also affects the server - but only after that file is compiled. Closes NEXT-125 ### Before ![image](https://user-images.githubusercontent.com/25056922/206123948-0c92009a-e5e8-4519-9862-1c1b83d88168.png) ### After ![image](https://user-images.githubusercontent.com/25056922/206124053-ba792463-76d4-457c-ac3b-d3e5b95b7bf9.png) ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: JJ Kasper --- .../internal/helpers/use-error-handler.ts | 9 ++- packages/next/server/dev/next-dev-server.ts | 4 ++ .../acceptance-app/ReactRefreshLogBox.test.ts | 68 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts index bd96c248ea0c71b..fa86bc92f4434ca 100644 --- a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts +++ b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts @@ -22,9 +22,12 @@ function isHydrationError(error: Error): boolean { ) } -try { - Error.stackTraceLimit = 50 -} catch {} +if (typeof window !== 'undefined') { + try { + // Increase the number of stack frames on the client + Error.stackTraceLimit = 50 + } catch {} +} const errorQueue: Array = [] const rejectionQueue: Array = [] diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 336686fbe51f339..4f126062f435109 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -150,6 +150,10 @@ export default class DevServer extends Server { } constructor(options: Options) { + try { + // Increase the number of stack frames on the server + Error.stackTraceLimit = 50 + } catch {} super({ ...options, dev: true }) this.persistPatchedGlobals() this.renderOpts.dev = true diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts index 6e73c94dfa900eb..7b7a416c68bc75f 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts @@ -1153,4 +1153,72 @@ describe('ReactRefreshLogBox app', () => { await cleanup() }) + + test('Call stack count is correct for server error', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + export default function Page() { + throw new Error('Server error') + } +`, + ], + ]) + ) + + expect(await session.hasRedbox(true)).toBe(true) + + // Open full Call Stack + await browser + .elementByCss('[data-nextjs-data-runtime-error-collapsed-action]') + .click() + const callStackCount = ( + await browser.elementsByCss('[data-nextjs-call-stack-frame]') + ).length + + // Expect more than the default amount of frames + // The default stackTraceLimit results in max 9 [data-nextjs-call-stack-frame] elements + expect(callStackCount).toBeGreaterThan(9) + + await cleanup() + }) + + test('Call stack count is correct for client error', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + 'use client' + export default function Page() { + if (typeof window !== 'undefined') { + throw new Error('Client error') + } + return null + } +`, + ], + ]) + ) + + expect(await session.hasRedbox(true)).toBe(true) + + // Open full Call Stack + await browser + .elementByCss('[data-nextjs-data-runtime-error-collapsed-action]') + .click() + const callStackCount = ( + await browser.elementsByCss('[data-nextjs-call-stack-frame]') + ).length + + // Expect more than the default amount of frames + // The default stackTraceLimit results in max 9 [data-nextjs-call-stack-frame] elements + expect(callStackCount).toBeGreaterThan(9) + + await cleanup() + }) }) From d2c23bb5e83397b996b143d2a020ae7816220251 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 12 Dec 2022 00:02:52 -0500 Subject: [PATCH 3/3] Refactor image optimization util (#43868) This PR doesn't change any behavior, its just refactoring. - renamed `webpack/loaders/next-image-loader.js` to `.ts` - moved duplicate code into shared function `optimizeImage()` - support `height` as optional param - convert `extension` to `contentType` --- ...t-image-loader.js => next-image-loader.ts} | 40 ++- packages/next/server/image-optimizer.ts | 274 ++++++++---------- 2 files changed, 149 insertions(+), 165 deletions(-) rename packages/next/build/webpack/loaders/{next-image-loader.js => next-image-loader.ts} (77%) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.ts similarity index 77% rename from packages/next/build/webpack/loaders/next-image-loader.js rename to packages/next/build/webpack/loaders/next-image-loader.ts index 7a24bd9c7b4a0ef..cc33c6c532a231d 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.ts @@ -1,14 +1,23 @@ +import isAnimated from 'next/dist/compiled/is-animated' import loaderUtils from 'next/dist/compiled/loader-utils3' -import { resizeImage, getImageSize } from '../../../server/image-optimizer' +import { optimizeImage, getImageSize } from '../../../server/image-optimizer' const BLUR_IMG_SIZE = 8 const BLUR_QUALITY = 70 const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next/client/image.tsx -function nextImageLoader(content) { +interface Options { + isServer: boolean + isDev: boolean + assetPrefix: string + basePath: string +} + +function nextImageLoader(this: any, content: Buffer) { const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader') return imageLoaderSpan.traceAsyncFn(async () => { - const { isServer, isDev, assetPrefix, basePath } = this.getOptions() + const options: Options = this.getOptions() + const { isServer, isDev, assetPrefix, basePath } = options const context = this.rootContext const opts = { context, content } const interpolatedName = loaderUtils.interpolateName( @@ -33,9 +42,9 @@ function nextImageLoader(content) { throw err } - let blurDataURL - let blurWidth - let blurHeight + let blurDataURL: string + let blurWidth: number + let blurHeight: number if (VALID_BLUR_EXT.includes(extension)) { // Shrink the image's largest dimension @@ -60,14 +69,23 @@ function nextImageLoader(content) { const prefix = 'http://localhost' const url = new URL(`${basePath || ''}/_next/image`, prefix) url.searchParams.set('url', outputPath) - url.searchParams.set('w', blurWidth) - url.searchParams.set('q', BLUR_QUALITY) + url.searchParams.set('w', String(blurWidth)) + url.searchParams.set('q', String(BLUR_QUALITY)) blurDataURL = url.href.slice(prefix.length) } else { const resizeImageSpan = imageLoaderSpan.traceChild('image-resize') - const resizedImage = await resizeImageSpan.traceAsyncFn(() => - resizeImage(content, blurWidth, blurHeight, extension, BLUR_QUALITY) - ) + const resizedImage = await resizeImageSpan.traceAsyncFn(() => { + if (isAnimated(content)) { + return content + } + return optimizeImage({ + buffer: content, + width: blurWidth, + height: blurHeight, + contentType: `image/${extension}`, + quality: 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 d0518eb7ab98358..6dc952e90c4bdc8 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -390,6 +390,119 @@ export function getMaxAge(str: string | null): number { return 0 } +export async function optimizeImage({ + buffer, + contentType, + quality, + width, + height, + nextConfigOutput, +}: { + buffer: Buffer + contentType: string + quality: number + width: number + height?: number + nextConfigOutput?: 'standalone' +}): Promise { + let optimizedBuffer = buffer + if (sharp) { + // Begin sharp transformation logic + const transformer = sharp(buffer) + + transformer.rotate() + + if (height) { + transformer.resize(width, height) + } else { + const { width: metaWidth } = await transformer.metadata() + + if (metaWidth && metaWidth > width) { + transformer.resize(width) + } + } + + if (contentType === AVIF) { + if (transformer.avif) { + const avifQuality = quality - 15 + transformer.avif({ + quality: Math.max(avifQuality, 0), + chromaSubsampling: '4:2:0', // same as webp + }) + } 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 }) + } else if (contentType === JPEG) { + transformer.jpeg({ quality }) + } + + optimizedBuffer = await transformer.toBuffer() + // End sharp transformation logic + } else { + if (showSharpMissingWarning && nextConfigOutput) { + // TODO: should we ensure squoosh also works even though we don't + // recommend it be used in production and this is a production feature + console.error( + `Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production` + ) + throw new ImageError(500, 'internal server error') + } + // Show sharp warning in production once + 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' + ) + showSharpMissingWarning = false + } + + // Begin Squoosh transformation logic + const orientation = await getOrientation(buffer) + + 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 + } + + if (height) { + operations.push({ type: 'resize', width, height }) + } else { + operations.push({ type: 'resize', width }) + } + + if (contentType === AVIF) { + optimizedBuffer = await processBuffer(buffer, operations, 'avif', quality) + } else if (contentType === WEBP) { + optimizedBuffer = await processBuffer(buffer, operations, 'webp', quality) + } else if (contentType === PNG) { + optimizedBuffer = await processBuffer(buffer, operations, 'png', quality) + } else if (contentType === JPEG) { + optimizedBuffer = await processBuffer(buffer, operations, 'jpeg', quality) + } + } + + return optimizedBuffer +} + export async function imageOptimizer( _req: IncomingMessage, _res: ServerResponse, @@ -504,114 +617,13 @@ export async function imageOptimizer( contentType = JPEG } try { - let optimizedBuffer: Buffer | undefined - if (sharp) { - // Begin sharp transformation logic - const transformer = sharp(upstreamBuffer) - - transformer.rotate() - - const { width: metaWidth } = await transformer.metadata() - - if (metaWidth && metaWidth > width) { - transformer.resize(width) - } - - if (contentType === AVIF) { - if (transformer.avif) { - const avifQuality = quality - 15 - transformer.avif({ - quality: Math.max(avifQuality, 0), - chromaSubsampling: '4:2:0', // same as webp - }) - } 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 }) - } else if (contentType === JPEG) { - transformer.jpeg({ quality }) - } - - optimizedBuffer = await transformer.toBuffer() - // End sharp transformation logic - } else { - if (showSharpMissingWarning && nextConfig.output === 'standalone') { - // TODO: should we ensure squoosh also works even though we don't - // recommend it be used in production and this is a production feature - console.error( - `Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production` - ) - throw new ImageError(500, 'internal server error') - } - // Show sharp warning in production once - 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' - ) - showSharpMissingWarning = false - } - - // 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 }) - - if (contentType === AVIF) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'avif', - quality - ) - } 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 - ) - } - - // End Squoosh transformation logic - } + let optimizedBuffer = await optimizeImage({ + buffer: upstreamBuffer, + contentType, + quality, + width, + nextConfigOutput: nextConfig.output, + }) if (optimizedBuffer) { if (isDev && width <= BLUR_IMG_SIZE && quality === BLUR_QUALITY) { // During `next dev`, we don't want to generate blur placeholders with webpack @@ -743,52 +755,6 @@ export function sendResponse( } } -export async function resizeImage( - content: Buffer, - width: number, - height: number, - // Should match VALID_BLUR_EXT - extension: 'avif' | 'webp' | 'png' | 'jpeg', - quality: number -): Promise { - if (isAnimated(content)) { - return content - } else if (sharp) { - const transformer = sharp(content) - - 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 }) - } else if (extension === 'jpeg') { - transformer.jpeg({ quality }) - } - transformer.resize(width, height) - const buf = await transformer.toBuffer() - return buf - } else { - const resizeOperationOpts: Operation = { type: 'resize', width, height } - const buf = await processBuffer( - content, - [resizeOperationOpts], - extension, - quality - ) - return buf - } -} - export async function getImageSize( buffer: Buffer, // Should match VALID_BLUR_EXT