diff --git a/lerna.json b/lerna.json index 6282750f385e..04824ab1f7a9 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.4.0-canary.8" + "version": "16.4.0-canary.9" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 41b9ea6ef356..71258a474d2c 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "keywords": [ "react", "next", diff --git a/packages/devlow-bench/package.json b/packages/devlow-bench/package.json index ae4aa9aa773e..303e4ae2ee51 100644 --- a/packages/devlow-bench/package.json +++ b/packages/devlow-bench/package.json @@ -1,7 +1,7 @@ { "name": "@vercel/devlow-bench", "private": true, - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "Benchmarking tool for the developer workflow", "repository": { "type": "git", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 6bd5e7afaa0a..542da0d12882 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.4.0-canary.8", + "@next/eslint-plugin-next": "16.4.0-canary.9", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 215f59177ecd..e3f646e8057a 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 788f3fd6c202..f4d15882b4ec 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index 5a35012aa7fb..cdeead0ad9d5 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 7a4c83db176f..718ddf415121 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 8e69a08dad32..e3241759aa4a 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 45b6d988e572..8ab949ff0093 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index bd890b557e78..02f27860793d 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index b003a35f9a11..ecba30b4d73e 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@next/playwright", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "repository": { "url": "vercel/next.js", "directory": "packages/next-playwright" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index e315f64635b6..98a9a7760487 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 0c3e0576ebd1..b80f09efc720 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 655b5413ff64..ca359cb692e9 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index 2365bc062344..6da1f2bb499f 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index ccbfdd790aa2..ba0514f5c0cb 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index ffbce7bf6403..6a36849bcd7c 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 733bc9a41f26..71d761b7aa49 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -100,7 +100,7 @@ ] }, "dependencies": { - "@next/env": "16.4.0-canary.8", + "@next/env": "16.4.0-canary.9", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -130,7 +130,7 @@ } }, "optionalDependencies": { - "sharp": "^0.35.3" + "sharp": "^0.35.4" }, "devDependencies": { "@babel/core": "7.26.10", @@ -164,11 +164,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.42.0", "@napi-rs/triples": "1.2.0", - "@next/font": "16.4.0-canary.8", - "@next/polyfill-module": "16.4.0-canary.8", - "@next/polyfill-nomodule": "16.4.0-canary.8", - "@next/react-refresh-utils": "16.4.0-canary.8", - "@next/swc": "16.4.0-canary.8", + "@next/font": "16.4.0-canary.9", + "@next/polyfill-module": "16.4.0-canary.9", + "@next/polyfill-nomodule": "16.4.0-canary.9", + "@next/react-refresh-utils": "16.4.0-canary.9", + "@next/swc": "16.4.0-canary.9", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.61.0", "@rspack/core": "1.6.7", diff --git a/packages/next/src/build/define-env.ts b/packages/next/src/build/define-env.ts index 556439d2c5bf..2cfae9d2f556 100644 --- a/packages/next/src/build/define-env.ts +++ b/packages/next/src/build/define-env.ts @@ -253,6 +253,9 @@ export function getDefineEnv({ 'process.env.__NEXT_DYNAMIC_ON_HOVER': Boolean( config.experimental.dynamicOnHover ), + 'process.env.__NEXT_EXPERIMENTAL_REACT_BROWSER_BAILOUT': Boolean( + config.experimental.reactBrowserBailout + ), 'process.env.__NEXT_USE_OFFLINE': Boolean(config.experimental.useOffline), 'process.env.__NEXT_PREFETCH_INLINING': Boolean( config.experimental.prefetchInlining diff --git a/packages/next/src/build/templates/app-page-runtime.ts b/packages/next/src/build/templates/app-page-runtime.ts index 34313659ede1..e2af1eea0185 100644 --- a/packages/next/src/build/templates/app-page-runtime.ts +++ b/packages/next/src/build/templates/app-page-runtime.ts @@ -941,6 +941,9 @@ export function createAppPageEntrypoint({ prefetchInlining: nextConfig.experimental.prefetchInlining ?? false, authInterrupts: Boolean(nextConfig.experimental.authInterrupts), + reactBrowserBailout: Boolean( + nextConfig.experimental.reactBrowserBailout + ), serverComponentsHmrCancellation: Boolean( nextConfig.experimental.serverComponentsHmrCancellation ), diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index f70cdad431ad..969373c59b3f 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -173,6 +173,9 @@ async function requestHandler( inlineCss: Boolean(nextConfig.experimental.inlineCss), prefetchInlining: nextConfig.experimental.prefetchInlining ?? false, authInterrupts: Boolean(nextConfig.experimental.authInterrupts), + reactBrowserBailout: Boolean( + nextConfig.experimental.reactBrowserBailout + ), // Edge has no Node response-close signal, so HMR cancellation is a // no-op. serverComponentsHmrCancellation: false, diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 92dd8692f129..8180dde30cb9 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1822,8 +1822,6 @@ export default async function getBaseWebpackConfig( basePath: config.basePath, assetPrefix: config.assetPrefix, outputHashSalt: config.outputHashSalt, - imgOptDangerouslyAllowAVIF: - config.experimental.imgOptDangerouslyAllowAVIF, }, }, ] diff --git a/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts b/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts index f81e417e9d88..b7dbb2508625 100644 --- a/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts +++ b/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts @@ -3,7 +3,7 @@ import { optimizeImage } from '../../../../server/image-optimizer' const BLUR_IMG_SIZE = 8 const BLUR_QUALITY = 70 -const VALID_BLUR_EXT = ['jpeg', 'png', 'webp'] // should match other usages +const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match other usages export async function getBlurImage( content: Buffer, @@ -13,7 +13,6 @@ export async function getBlurImage( basePath, outputPath, isDev, - imgOptDangerouslyAllowAVIF = false, tracing = () => ({ traceFn: (fn) => @@ -28,7 +27,6 @@ export async function getBlurImage( basePath: string outputPath: string isDev: boolean - imgOptDangerouslyAllowAVIF?: boolean tracing: (name?: string) => { traceFn(fn: Function): any traceAsyncFn(fn: Function): any @@ -39,11 +37,7 @@ export async function getBlurImage( let blurWidth: number = 0 let blurHeight: number = 0 - const isValidBlurExtension = - VALID_BLUR_EXT.includes(extension) || - (extension === 'avif' && imgOptDangerouslyAllowAVIF) - - if (isValidBlurExtension && !isAnimated(content)) { + if (VALID_BLUR_EXT.includes(extension) && !isAnimated(content)) { // Shrink the image's largest dimension if (imageSize.width >= imageSize.height) { blurWidth = BLUR_IMG_SIZE @@ -78,7 +72,6 @@ export async function getBlurImage( height: blurHeight, contentType: `image/${extension}`, quality: BLUR_QUALITY, - dangerouslyAllowAVIF: imgOptDangerouslyAllowAVIF, }) ) const blurDataURLSpan = tracing('image-base64-tostring') diff --git a/packages/next/src/build/webpack/loaders/next-image-loader/index.ts b/packages/next/src/build/webpack/loaders/next-image-loader/index.ts index 8be4507a871f..7590ea3e7671 100644 --- a/packages/next/src/build/webpack/loaders/next-image-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-image-loader/index.ts @@ -11,21 +11,14 @@ interface Options { assetPrefix: string basePath: string outputHashSalt: string - imgOptDangerouslyAllowAVIF: boolean } function nextImageLoader(this: any, content: Buffer) { const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader') return imageLoaderSpan.traceAsyncFn(async () => { const options: Options = this.getOptions() - const { - compilerType, - isDev, - assetPrefix, - basePath, - outputHashSalt, - imgOptDangerouslyAllowAVIF, - } = options + const { compilerType, isDev, assetPrefix, basePath, outputHashSalt } = + options const context = this.rootContext // Prepend the hash salt to the content for filename hash computation, so @@ -65,7 +58,6 @@ function nextImageLoader(this: any, content: Buffer) { basePath, outputPath, isDev, - imgOptDangerouslyAllowAVIF, tracing: imageLoaderSpan.traceChild.bind(imageLoaderSpan), }) diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index 43f2ee279121..23ae2238d889 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -523,7 +523,7 @@ export class ClientReferenceManifestPlugin { const concatenatedMod = connection.module const concatenatedModId = compilation.chunkGraph.getModuleId(concatenatedMod) - if (concatenatedModId) { + if (concatenatedModId !== null) { recordModule(concatenatedModId, clientEntryMod) } } diff --git a/packages/next/src/client/legacy/image.tsx b/packages/next/src/client/legacy/image.tsx index cb454f948ca0..339ca10ee82c 100644 --- a/packages/next/src/client/legacy/image.tsx +++ b/packages/next/src/client/legacy/image.tsx @@ -748,7 +748,6 @@ export default function Image({ } let staticSrc = '' - let isStaticAvif = false if (isStaticImport(src)) { const staticImageData = isStaticRequire(src) ? src.default : src @@ -761,7 +760,6 @@ export default function Image({ } blurDataURL = blurDataURL || staticImageData.blurDataURL staticSrc = staticImageData.src - isStaticAvif = /\.avif(?:\?|$)/i.test(staticSrc) if (!layout || layout !== 'fill') { height = height || staticImageData.height width = width || staticImageData.width @@ -776,12 +774,6 @@ export default function Image({ } src = typeof src === 'string' ? src : staticSrc - // Generating a blurDataURL requires decoding the source image, so static AVIF - // imports do not receive one while AVIF input optimization is disabled. - if (isStaticAvif && placeholder === 'blur' && !blurDataURL) { - placeholder = 'empty' - } - warnOnce( `Image with src "${src}" is using next/legacy/image which is deprecated and will be removed in a future version of Next.js.` ) @@ -916,7 +908,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. diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index e197d0bb8c78..8a7947724354 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -523,6 +523,7 @@ async function exportAppImpl( inlineCss: nextConfig.experimental.inlineCss ?? false, prefetchInlining: nextConfig.experimental.prefetchInlining ?? false, authInterrupts: !!nextConfig.experimental.authInterrupts, + reactBrowserBailout: nextConfig.experimental.reactBrowserBailout ?? false, useCacheTimeout: nextConfig.experimental.useCacheTimeout, cachedNavigations: nextConfig.experimental.cachedNavigations ?? false, maxPostponedStateSizeBytes: parseMaxPostponedStateSize( diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 2a2121e3f764..459847b650bf 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -68,6 +68,7 @@ import { createNodeInlinedDataStream, } from './stream-ops' import type { AnyStream } from './stream-ops' +import { createRenderInBrowserAbortSignal } from './render-in-browser' import { getInstantTestBootstrapScriptContent } from './instant-test-bootstrap' import { stripInternalQueries } from '../internal-utils' import { @@ -137,6 +138,7 @@ import { } from './create-transport-tree-from-loader-tree' import { handleAction } from './action-handler' import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' +import { getReactBrowserBailoutReason } from '../../shared/lib/lazy-dynamic/react-browser-bailout' import { warn, error } from '../../build/output/log' import { appendMutableCookies, @@ -179,7 +181,6 @@ import { import { getStackWithoutErrorMessage } from '../../lib/format-server-error' import { accessedDynamicData, - createRenderInBrowserAbortSignal, formatDynamicAPIAccesses, isPrerenderInterruptedError, createDynamicTrackingState, @@ -2919,7 +2920,7 @@ async function prerenderAppPage({ // Pick first userland SSR error, which is also not a RSC error. if (response.ssrErrors.length) { const buildFailingError = response.ssrErrors.find((err) => - isUserLandError(err) + isUserLandError(err, ctx.renderOpts.experimental.reactBrowserBailout) ) if (buildFailingError) throw buildFailingError } @@ -3698,6 +3699,7 @@ async function renderToStream( const htmlRendererErrorHandler = createHTMLErrorHandler( process.env.NODE_ENV === 'development', isBuildTimePrerendering, + ctx.renderOpts.experimental.reactBrowserBailout, reactServerErrorsByDigest, allCapturedErrors, onHTMLRenderSSRError, @@ -6519,6 +6521,7 @@ export type ValidationRenderContext = Pick< | 'workStore' > & { renderOpts: Pick + reactBrowserBailout: boolean isDebugChannelEnabled: boolean } @@ -6542,6 +6545,7 @@ export function toValidationRenderContext( images: ctx.renderOpts.images, allowEmptyStaticShell: ctx.renderOpts.allowEmptyStaticShell, }, + reactBrowserBailout: ctx.renderOpts.experimental.reactBrowserBailout, isDebugChannelEnabled: !!ctx.renderOpts.setReactDebugChannel, } } @@ -6666,6 +6670,7 @@ export async function runValidationInDevFromSnapshot( images: message.renderOpts.images, allowEmptyStaticShell: message.renderOpts.allowEmptyStaticShell, }, + reactBrowserBailout: message.reactBrowserBailout, isDebugChannelEnabled: message.isDebugChannelEnabled, } @@ -7584,6 +7589,20 @@ async function validateInstantConfigs( />, { signal: reactSignal, + onBrowserBailout: (err: unknown, errorInfo: ErrorInfo) => { + if (!reactSignal.aborted) { + const componentStack = errorInfo.componentStack + if (typeof componentStack === 'string') { + trackThrownErrorInNavigation( + workStore, + instantValidationState, + err, + componentStack, + ctx.reactBrowserBailout + ) + } + } + }, onError: (err: unknown, errorInfo: ErrorInfo) => { if (isPrerenderInterruptedError(err) || reactSignal.aborted) { const componentStack = errorInfo.componentStack @@ -7625,7 +7644,8 @@ async function validateInstantConfigs( workStore, instantValidationState, errorForDisplay, - componentStack + componentStack, + ctx.reactBrowserBailout ) } } @@ -8671,6 +8691,7 @@ async function prerenderToStream( const htmlRendererErrorHandler = createHTMLErrorHandler( process.env.NODE_ENV === 'development', isBuildTimePrerendering, + ctx.renderOpts.experimental.reactBrowserBailout, reactServerErrorsByDigest, allCapturedErrors, onHTMLRenderSSRError @@ -9560,7 +9581,9 @@ async function prerenderToStream( />, JSON.parse(JSON.stringify(postponed)), { - signal: createRenderInBrowserAbortSignal(), + signal: createRenderInBrowserAbortSignal( + ctx.renderOpts.experimental.reactBrowserBailout + ), onError: htmlRendererErrorHandler, nonce, } @@ -9737,6 +9760,19 @@ async function prerenderToStream( throw err } + if (ctx.renderOpts.experimental.reactBrowserBailout) { + // React preserves the reason passed to browser() as the fatal error's cause. + const browserBailoutReason = getReactBrowserBailoutReason(err) + if (browserBailoutReason !== undefined) { + const stack = getStackWithoutErrorMessage(err as Error) + error( + `${browserBailoutReason} should be wrapped in a suspense boundary at page "${pagePath}". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\n${stack}` + ) + + throw new StaticGenBailoutError() + } + } + // If we errored when we did not have an RSC stream to read from. This is // not just a render error, we need to throw early. if (reactServerPrerenderResult === null) { @@ -10086,7 +10122,9 @@ async function prerenderToStream( />, JSON.parse(JSON.stringify(errorPostponed)), { - signal: createRenderInBrowserAbortSignal(), + signal: createRenderInBrowserAbortSignal( + ctx.renderOpts.experimental.reactBrowserBailout + ), onError: htmlRendererErrorHandler, nonce, } diff --git a/packages/next/src/server/app-render/create-error-handler.tsx b/packages/next/src/server/app-render/create-error-handler.tsx index ebfe8d2fc6f0..e7110c11b77d 100644 --- a/packages/next/src/server/app-render/create-error-handler.tsx +++ b/packages/next/src/server/app-render/create-error-handler.tsx @@ -12,6 +12,7 @@ import { isPrerenderInterruptedError } from './dynamic-rendering' import { getProperError } from '../../lib/is-error' import { isReactLargeShellError } from './react-large-shell-error' import { isInstantValidationError } from './instant-validation/instant-validation-error' +import { isNextBrowserBailoutError } from '../../shared/lib/lazy-dynamic/react-browser-bailout' declare global { var __next_log_error__: undefined | ((err: unknown) => void) @@ -149,6 +150,7 @@ export function createReactServerErrorHandler( export function createHTMLErrorHandler( shouldFormatError: boolean, isBuildTimePrerendering: boolean, + reactBrowserBailout: boolean, reactServerErrors: Map, allCapturedErrors: Array, onHTMLRenderSSRError: (err: DigestedError, errorInfo?: ErrorInfo) => void, @@ -168,6 +170,13 @@ export function createHTMLErrorHandler( // If the response was closed, we don't need to log the error. if (isAbortError(thrownValue)) return + // React turns a browser bailout outside Suspense into a fatal error. This + // is a framework signal handled by the prerender caller, so don't report it + // as a userland render error here. + if (reactBrowserBailout && isNextBrowserBailoutError(thrownValue)) { + return + } + const digest = getDigestForWellKnownError(thrownValue) if (digest) { @@ -229,8 +238,14 @@ export function createHTMLErrorHandler( } } -export function isUserLandError(err: any): boolean { +export function isUserLandError( + err: any, + reactBrowserBailout: boolean +): boolean { return ( - !isAbortError(err) && !isBailoutToCSRError(err) && !isNextRouterError(err) + !isAbortError(err) && + !isBailoutToCSRError(err) && + !(reactBrowserBailout && isNextBrowserBailoutError(err)) && + !isNextRouterError(err) ) } diff --git a/packages/next/src/server/app-render/dev-validation-worker-globals.ts b/packages/next/src/server/app-render/dev-validation-worker-globals.ts index 9cde0cab7b58..21a585511bb7 100644 --- a/packages/next/src/server/app-render/dev-validation-worker-globals.ts +++ b/packages/next/src/server/app-render/dev-validation-worker-globals.ts @@ -98,6 +98,7 @@ export interface DevValidationSnapshot { * rare renders that React's I/O tracking carries a reference into. */ additionalClientReferenceManifestPages: string[] + reactBrowserBailout: boolean isDebugChannelEnabled: boolean renderOpts: { images: ImageConfigComplete diff --git a/packages/next/src/server/app-render/dev-validation-worker-snapshot.ts b/packages/next/src/server/app-render/dev-validation-worker-snapshot.ts index 23934dfc3406..8656ceaa910e 100644 --- a/packages/next/src/server/app-render/dev-validation-worker-snapshot.ts +++ b/packages/next/src/server/app-render/dev-validation-worker-snapshot.ts @@ -88,6 +88,7 @@ export async function buildDevValidationSnapshot( .additionalClientReferenceManifestPages ? [...ctx.workStore.additionalClientReferenceManifestPages] : [], + reactBrowserBailout: ctx.renderOpts.experimental.reactBrowserBailout, isDebugChannelEnabled: !!ctx.renderOpts.setReactDebugChannel, renderOpts: { images: ctx.renderOpts.images, diff --git a/packages/next/src/server/app-render/dynamic-rendering-client.ts b/packages/next/src/server/app-render/dynamic-rendering-client.ts index 0f9d7a25cf5d..19c24ead4638 100644 --- a/packages/next/src/server/app-render/dynamic-rendering-client.ts +++ b/packages/next/src/server/app-render/dynamic-rendering-client.ts @@ -1,4 +1,5 @@ import React from 'react' +import { browser } from 'react-dom' import { throwForMissingRequestStore, workUnitAsyncStorage, @@ -10,6 +11,12 @@ import { } from '../dynamic-rendering-utils' import { InvariantError } from '../../shared/lib/invariant-error' import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' +import { createReactBrowserBailoutReason } from '../../shared/lib/lazy-dynamic/react-browser-bailout' + +const getUseSearchParamsBailoutReason = createReactBrowserBailoutReason.bind( + null, + 'useSearchParams()' +) // TODO(veil): This module is separated from `dynamic-rendering.ts` as a workaround. // When these hooks were part of `dynamic-rendering.ts, the source location of these @@ -101,7 +108,13 @@ export function useDynamicSearchParams(expression: string) { if (workStore.forceStatic) { return } - throw new BailoutToCSRError(expression) + if (process.env.__NEXT_EXPERIMENTAL_REACT_BROWSER_BAILOUT) { + // @ts-expect-error TODO: Update @types/react-dom to include the reason argument. + React.use(browser(getUseSearchParamsBailoutReason)) + return + } else { + throw new BailoutToCSRError(expression) + } } case 'prerender': case 'prerender-runtime': diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 45fef9364fa4..c0aacca7d3cd 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -46,7 +46,6 @@ import { ROOT_LAYOUT_BOUNDARY_NAME, } from '../../lib/framework/boundary-constants' import { scheduleOnNextTick } from '../../lib/scheduler' -import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' import { createRuntimeBodyError, createDynamicBodyError, @@ -79,6 +78,7 @@ import { } from './instant-validation/boundary-tracking' import type { InstantValidationSampleTracking } from './instant-validation/instant-samples' import { createUnrenderedSegmentError } from '../../shared/lib/instant-messages' +import { getReactBrowserBailoutReason } from '../../shared/lib/lazy-dynamic/react-browser-bailout' export type DynamicAccess = { /** @@ -459,12 +459,6 @@ export function formatDynamicAPIAccesses( }) } -export function createRenderInBrowserAbortSignal(): AbortSignal { - const controller = new AbortController() - controller.abort(new BailoutToCSRError('Render in Browser')) - return controller.signal -} - /** * In a prerender, we may end up with hanging Promises as inputs due them * stalling on connection() or because they're loading dynamic data. In that @@ -920,7 +914,8 @@ export function trackThrownErrorInNavigation( workStore: WorkStore, dynamicValidation: InstantValidationState, thrownValue: unknown, - componentStack: string + componentStack: string, + reactBrowserBailout: boolean ) { const boundaryLocation = hasInstantValidationBoundaryRegex.exec(componentStack) @@ -932,6 +927,16 @@ export function trackThrownErrorInNavigation( // This helps for errors from node_modules which would otherwise // have no useful stack information due to ignore-listing, // e.g. next/dynamic with `ssr: false`. + if (reactBrowserBailout) { + // React preserves Next's branded bailout reason as the error cause. + // Replace the internal wrapper before storing the user-facing diagnostic. + const browserBailoutReason = getReactBrowserBailoutReason(thrownValue) + if (browserBailoutReason !== undefined) { + const browserBailoutError = thrownValue as Error + browserBailoutError.cause = browserBailoutReason + } + } + const error = addErrorContext( new Error( 'An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.', @@ -958,6 +963,16 @@ export function trackThrownErrorInNavigation( // invalid - fallthrough } } + if (reactBrowserBailout) { + // React preserves Next's branded bailout reason as the error cause. + // Replace the internal wrapper before storing the user-facing diagnostic. + const browserBailoutReason = getReactBrowserBailoutReason(thrownValue) + if (browserBailoutReason !== undefined) { + const browserBailoutError = thrownValue as Error + browserBailoutError.cause = browserBailoutReason + } + } + const message = `Route "${workStore.route}": Could not validate \`instant\` because an error prevented the target segment from rendering.` const error = addErrorContext( new Error(message, { cause: thrownValue }), diff --git a/packages/next/src/server/app-render/render-in-browser.ts b/packages/next/src/server/app-render/render-in-browser.ts new file mode 100644 index 000000000000..db93028d380d --- /dev/null +++ b/packages/next/src/server/app-render/render-in-browser.ts @@ -0,0 +1,26 @@ +import { browser } from 'react-dom' +import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' +import { createReactBrowserBailoutReason } from '../../shared/lib/lazy-dynamic/react-browser-bailout' + +const RENDER_IN_BROWSER_BAILOUT_REASON = 'Render in Browser' +const getRenderInBrowserBailoutReason = createReactBrowserBailoutReason.bind( + null, + RENDER_IN_BROWSER_BAILOUT_REASON +) + +/** + * Aborting a resumed render with a recoverable reason tells React to leave the + * postponed boundary for the browser instead of reporting a render error. + */ +export function createRenderInBrowserAbortSignal( + reactBrowserBailout: boolean +): AbortSignal { + const controller = new AbortController() + if (reactBrowserBailout) { + // @ts-expect-error TODO: Update @types/react-dom to include the reason argument. + controller.abort(browser(getRenderInBrowserBailoutReason)) + } else { + controller.abort(new BailoutToCSRError(RENDER_IN_BROWSER_BAILOUT_REASON)) + } + return controller.signal +} diff --git a/packages/next/src/server/app-render/stream-ops.node.ts b/packages/next/src/server/app-render/stream-ops.node.ts index 89fe256fda2b..beb1eb49c780 100644 --- a/packages/next/src/server/app-render/stream-ops.node.ts +++ b/packages/next/src/server/app-render/stream-ops.node.ts @@ -48,6 +48,7 @@ import { atLeastOneTask, waitAtLeastOneReactRenderTask, } from '../../lib/scheduler' +import { ResponseAborted } from '../web/spec-extension/adapters/next-request' import type { FlightPayload, FlightClientModules, @@ -562,6 +563,17 @@ export function renderToNodeFlightStream( clientModules, renderOptions ) + + // If the destination is destroyed before the render ended, React aborts with a + // generic "The destination stream closed early." error that `onError` can't + // tell apart from a real render error. Abort first with the reason we already + // know; the listener is registered before piping so it runs before React's. + pt.once('close', () => { + if (!pt.writableEnded) { + pipeable.abort(new ResponseAborted()) + } + }) + pipeable.pipe(pt) if (signal) { diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 4df5d0f1a0a3..039f197eaa4d 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -166,6 +166,7 @@ export interface RenderOptsPartial { inlineCss: boolean prefetchInlining: PrefetchInliningConfig authInterrupts: boolean + reactBrowserBailout: boolean serverComponentsHmrCancellation?: boolean useCacheTimeout: number cachedNavigations: boolean diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index da46058be8d6..befa70e40205 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -609,6 +609,8 @@ export default abstract class Server< prefetchInlining: this.nextConfig.experimental.prefetchInlining ?? false, authInterrupts: !!this.nextConfig.experimental.authInterrupts, + reactBrowserBailout: + this.nextConfig.experimental.reactBrowserBailout ?? false, serverComponentsHmrCancellation: this.nextConfig.experimental.serverComponentsHmrCancellation, useCacheTimeout: this.nextConfig.experimental.useCacheTimeout, diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index cc34cab5836a..c4e8eeecafa4 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -226,6 +226,7 @@ export const experimentalSchema = { clientParamParsingOrigins: z.array(z.string()).optional(), cachedNavigations: z.boolean().optional(), dynamicOnHover: z.boolean().optional(), + reactBrowserBailout: z.boolean().optional(), useOffline: z.boolean().optional(), optimisticRouting: z.boolean().optional(), concurrentRouterQueue: z.boolean().optional(), @@ -266,7 +267,6 @@ export const experimentalSchema = { forceSwcTransforms: z.boolean().optional(), fullySpecified: z.boolean().optional(), gzipSize: z.boolean().optional(), - imgOptDangerouslyAllowAVIF: z.boolean().optional(), imgOptConcurrency: z.number().int().optional().nullable(), imgOptOperationCache: z.boolean().optional().nullable(), imgOptTimeoutInSeconds: z.number().int().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 2ae91997b7c3..385eab6a7fa0 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -533,6 +533,11 @@ export interface ExperimentalConfig { */ cachedNavigations?: boolean dynamicOnHover?: boolean + /** + * Uses ReactDOM's browser rendering primitive for supported client-rendering + * bailouts instead of Next.js' internal bailout error. + */ + reactBrowserBailout?: boolean useOffline?: boolean optimisticRouting?: boolean /** @@ -597,11 +602,6 @@ export interface ExperimentalConfig { extensionAlias?: Record allowedRevalidateHeaderKeys?: string[] fetchCacheKeyPrefix?: string - /** - * Re-enables AVIF input optimization and automatic blur generation. - * This may expose applications to security risks in native image decoders. - */ - imgOptDangerouslyAllowAVIF?: boolean imgOptConcurrency?: number | null imgOptOperationCache?: boolean | null imgOptTimeoutInSeconds?: number @@ -2267,6 +2267,7 @@ export const defaultConfig = Object.freeze({ clientParamParsingOrigins: undefined, cachedNavigations: false, dynamicOnHover: false, + reactBrowserBailout: false, useOffline: false, varyParams: true, optimisticRouting: true, @@ -2286,7 +2287,6 @@ export const defaultConfig = Object.freeze({ (os.cpus() || { length: 1 }).length) - 1 ), memoryBasedWorkersCount: false, - imgOptDangerouslyAllowAVIF: false, imgOptConcurrency: null, imgOptOperationCache: null, imgOptTimeoutInSeconds: 7, @@ -2428,6 +2428,7 @@ export interface NextConfigRuntime { | 'inlineCss' | 'prefetchInlining' | 'authInterrupts' + | 'reactBrowserBailout' | 'useCacheTimeout' | 'clientTraceMetadata' | 'clientParamParsingOrigins' @@ -2447,7 +2448,6 @@ export interface NextConfigRuntime { | 'preloadEntriesOnStart' | 'hideLogsAfterAbort' | 'removeUncaughtErrorAndRejectionListeners' - | 'imgOptDangerouslyAllowAVIF' | 'imgOptConcurrency' | 'imgOptOperationCache' | 'imgOptMaxInputPixels' @@ -2496,6 +2496,7 @@ export function getNextConfigRuntime( inlineCss: ex.inlineCss, prefetchInlining: ex.prefetchInlining, authInterrupts: ex.authInterrupts, + reactBrowserBailout: ex.reactBrowserBailout, useCacheTimeout: ex.useCacheTimeout, clientTraceMetadata: ex.clientTraceMetadata, clientParamParsingOrigins: ex.clientParamParsingOrigins, @@ -2516,7 +2517,6 @@ export function getNextConfigRuntime( hideLogsAfterAbort: ex.hideLogsAfterAbort, removeUncaughtErrorAndRejectionListeners: ex.removeUncaughtErrorAndRejectionListeners, - imgOptDangerouslyAllowAVIF: ex.imgOptDangerouslyAllowAVIF, imgOptConcurrency: ex.imgOptConcurrency, imgOptOperationCache: ex.imgOptOperationCache, imgOptMaxInputPixels: ex.imgOptMaxInputPixels, diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 0e0257515d89..077eaf355f91 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -85,8 +85,7 @@ async function initCacheEntries( export function getSharp( concurrency: number | null | undefined, - operationCache: boolean | null | undefined, - dangerouslyAllowAVIF = false + operationCache: boolean | null | undefined ) { if (_sharp) { return _sharp @@ -96,6 +95,7 @@ export function getSharp( _sharp.block({ operation: ['VipsForeignLoad'] }) _sharp.unblock({ operation: [ + 'VipsForeignLoadHeif', // avif 'VipsForeignLoadJpeg', 'VipsForeignLoadNsgif', 'VipsForeignLoadPng', @@ -104,9 +104,6 @@ export function getSharp( 'VipsForeignLoadWebp', ], }) - if (dangerouslyAllowAVIF) { - _sharp.unblock({ operation: ['VipsForeignLoadHeif'] }) - } if (typeof operationCache === 'boolean') { _sharp.cache(operationCache) } @@ -810,7 +807,6 @@ export async function optimizeImage({ limitInputPixels, sequentialRead, timeoutInSeconds, - dangerouslyAllowAVIF, }: { buffer: Buffer contentType: string @@ -822,9 +818,8 @@ export async function optimizeImage({ limitInputPixels?: number sequentialRead?: boolean | null timeoutInSeconds?: number - dangerouslyAllowAVIF?: boolean }): Promise { - const sharp = getSharp(concurrency, operationCache, dangerouslyAllowAVIF) + const sharp = getSharp(concurrency, operationCache) const transformer = sharp(buffer, { limitInputPixels, sequentialRead: sequentialRead ?? undefined, @@ -1003,8 +998,16 @@ export async function fetchInternalImage( await handleRequest(mocked.req, mocked.res, parseReqUrl(href)) await mocked.res.hasStreamed - if (!mocked.res.statusCode) { - Log.error('image response failed for', href, mocked.res.statusCode) + if ( + !mocked.res.statusCode || + mocked.res.statusCode < 200 || + mocked.res.statusCode > 299 + ) { + Log.error( + 'internal image response failed for', + href, + mocked.res.statusCode + ) throw new ImageError( mocked.res.statusCode, '"url" parameter is valid but internal response is invalid' @@ -1061,7 +1064,6 @@ export async function imageOptimizer( experimental: Pick< NextConfigComplete['experimental'], | 'imgOptConcurrency' - | 'imgOptDangerouslyAllowAVIF' | 'imgOptOperationCache' | 'imgOptMaxInputPixels' | 'imgOptSequentialRead' @@ -1137,11 +1139,7 @@ export async function imageOptimizer( upstreamEtag, } } - if ( - BYPASS_TYPES.includes(upstreamType) || - (upstreamType === AVIF && - !nextConfig.experimental.imgOptDangerouslyAllowAVIF) - ) { + if (BYPASS_TYPES.includes(upstreamType)) { return { buffer: upstreamBuffer, contentType: upstreamType, @@ -1185,7 +1183,6 @@ export async function imageOptimizer( quality, width, concurrency: nextConfig.experimental.imgOptConcurrency, - dangerouslyAllowAVIF: nextConfig.experimental.imgOptDangerouslyAllowAVIF, operationCache: nextConfig.experimental.imgOptOperationCache, limitInputPixels: nextConfig.experimental.imgOptMaxInputPixels, sequentialRead: nextConfig.experimental.imgOptSequentialRead, diff --git a/packages/next/src/shared/lib/get-img-props.ts b/packages/next/src/shared/lib/get-img-props.ts index 52be2d407870..ee9f277da020 100644 --- a/packages/next/src/shared/lib/get-img-props.ts +++ b/packages/next/src/shared/lib/get-img-props.ts @@ -398,7 +398,6 @@ export function getImgProps( let heightInt = getInt(height) let blurWidth: number | undefined let blurHeight: number | undefined - let isStaticAvif = false if (isStaticImport(src)) { const staticImageData = isStaticRequire(src) ? src.default : src @@ -421,7 +420,6 @@ export function getImgProps( blurHeight = staticImageData.blurHeight blurDataURL = blurDataURL || staticImageData.blurDataURL staticSrc = staticImageData.src - isStaticAvif = /\.avif(?:\?|$)/i.test(staticSrc) if (!fill) { if (!widthInt && !heightInt) { @@ -438,12 +436,6 @@ export function getImgProps( } src = typeof src === 'string' ? src : staticSrc - // Generating a blurDataURL requires decoding the source image, so static AVIF - // imports do not receive one while AVIF input optimization is disabled. - if (isStaticAvif && placeholder === 'blur' && !blurDataURL) { - placeholder = 'empty' - } - let isLazy = !priority && !preload && @@ -594,7 +586,7 @@ export function getImgProps( ) } if (placeholder === 'blur' && !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. diff --git a/packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx b/packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx index dcb1ed1d8d74..0f82a2bb53ae 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx +++ b/packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx @@ -1,20 +1,34 @@ 'use client' -import type { ReactElement } from 'react' +import { use, type ReactElement } from 'react' +import { browser } from 'react-dom' import { BailoutToCSRError } from './bailout-to-csr' +import { createReactBrowserBailoutReason } from './react-browser-bailout' -interface BailoutToCSRProps { - reason: string +interface BailoutToCSRForNextDynamicProps { children: ReactElement } +const NEXT_DYNAMIC_BAILOUT_REASON = 'next/dynamic' +const getNextDynamicBailoutReason = createReactBrowserBailoutReason.bind( + null, + NEXT_DYNAMIC_BAILOUT_REASON +) + /** - * If rendered on the server, this component throws an error - * to signal Next.js that it should bail out to client-side rendering instead. + * Signals during server rendering that this subtree should be client-rendered. */ -export function BailoutToCSR({ reason, children }: BailoutToCSRProps) { +export function BailoutToCSRForNextDynamic({ + children, +}: BailoutToCSRForNextDynamicProps) { + if (process.env.__NEXT_EXPERIMENTAL_REACT_BROWSER_BAILOUT) { + // @ts-expect-error TODO: Update @types/react-dom to include the reason argument. + use(browser(getNextDynamicBailoutReason)) + return children + } + if (typeof window === 'undefined') { - throw new BailoutToCSRError(reason) + throw new BailoutToCSRError(NEXT_DYNAMIC_BAILOUT_REASON) } return children diff --git a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx index 3f74a077e54c..da78ac5dc2d5 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx +++ b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx @@ -1,5 +1,5 @@ import { Suspense, Fragment, lazy } from 'react' -import { BailoutToCSR } from './dynamic-bailout-to-csr' +import { BailoutToCSRForNextDynamic } from './dynamic-bailout-to-csr' import type { ComponentModule } from './types' import { PreloadChunks } from './preload-chunks' @@ -61,9 +61,9 @@ function Loadable(options: LoadableOptions) { ) : ( - + - + ) return {children} diff --git a/packages/next/src/shared/lib/lazy-dynamic/react-browser-bailout.ts b/packages/next/src/shared/lib/lazy-dynamic/react-browser-bailout.ts new file mode 100644 index 000000000000..ac7549785f4c --- /dev/null +++ b/packages/next/src/shared/lib/lazy-dynamic/react-browser-bailout.ts @@ -0,0 +1,32 @@ +// This module is bundled separately into user code and the precompiled App +// Router runtime. Use the global symbol registry so both copies recognize the +// same browser bailout reason. +const REACT_BROWSER_BAILOUT_REASON = Symbol.for('next.browser-bailout-reason') + +type ReactBrowserBailoutReason = { + $$typeof: symbol + reason: string +} + +export function createReactBrowserBailoutReason( + reason: string +): ReactBrowserBailoutReason { + return { + $$typeof: REACT_BROWSER_BAILOUT_REASON, + reason, + } +} + +export function getReactBrowserBailoutReason( + error: unknown +): string | undefined { + const cause = (error as Error)?.cause as ReactBrowserBailoutReason | undefined + + return cause?.$$typeof === REACT_BROWSER_BAILOUT_REASON + ? cause.reason + : undefined +} + +export function isNextBrowserBailoutError(error: unknown): boolean { + return getReactBrowserBailoutReason(error) !== undefined +} diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index b4c34cdb6092..35c9cee16be4 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index bb692b949e64..c4ca16a8e89d 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.4.0-canary.8", + "version": "16.4.0-canary.9", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.4.0-canary.8", + "next": "16.4.0-canary.9", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "6.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77dc0718e2d4..ccd717c869db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1024,7 +1024,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1107,7 +1107,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../next-env '@swc/helpers': specifier: 0.5.23 @@ -1228,19 +1228,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../font '@next/polyfill-module': - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../react-refresh-utils '@next/swc': - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1808,8 +1808,8 @@ importers: version: 3.4.0(zod@3.25.76) optionalDependencies: sharp: - specifier: ^0.35.3 - version: 0.35.3(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)) + specifier: ^0.35.4 + version: 0.35.4(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)) packages/next-bundle-analyzer: dependencies: @@ -1956,7 +1956,7 @@ importers: devDependencies: '@napi-rs/cli': specifier: 3.7.2 - version: 3.7.2(@emnapi/runtime@1.9.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0) + version: 3.7.2(@emnapi/runtime@1.11.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0) cross-env: specifier: 6.0.3 version: 6.0.3 @@ -1983,7 +1983,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.4.0-canary.8 + specifier: 16.4.0-canary.9 version: link:../next outdent: specifier: 0.8.0 @@ -3059,6 +3059,9 @@ packages: '@emnapi/runtime@1.11.2': resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} @@ -3716,8 +3719,8 @@ packages: cpu: [arm64] os: [darwin] - '@img/sharp-darwin-arm64@0.35.3': - resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] @@ -3728,14 +3731,14 @@ packages: cpu: [x64] os: [darwin] - '@img/sharp-darwin-x64@0.35.3': - resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.3': - resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] @@ -3744,8 +3747,8 @@ packages: cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] @@ -3754,8 +3757,8 @@ packages: cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] @@ -3765,8 +3768,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] @@ -3777,8 +3780,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.2': - resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] @@ -3789,8 +3792,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] @@ -3801,8 +3804,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] @@ -3813,8 +3816,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] @@ -3825,8 +3828,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.2': - resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] @@ -3837,8 +3840,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] @@ -3849,8 +3852,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] @@ -3862,8 +3865,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm64@0.35.3': - resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] @@ -3876,8 +3879,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.3': - resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] @@ -3890,8 +3893,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.3': - resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] @@ -3904,8 +3907,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.3': - resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] @@ -3918,8 +3921,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.3': - resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] @@ -3932,8 +3935,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.3': - resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] @@ -3946,8 +3949,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] @@ -3960,8 +3963,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.3': - resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] @@ -3972,12 +3975,12 @@ packages: engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-wasm32@0.35.3': - resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] @@ -3987,8 +3990,8 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-arm64@0.35.3': - resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] @@ -3999,8 +4002,8 @@ packages: cpu: [ia32] os: [win32] - '@img/sharp-win32-ia32@0.35.3': - resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] @@ -4011,8 +4014,8 @@ packages: cpu: [x64] os: [win32] - '@img/sharp-win32-x64@0.35.3': - resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -16878,8 +16881,8 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.35.3: - resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -20197,10 +20200,15 @@ snapshots: '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 + optional: true '@emnapi/runtime@1.11.2': dependencies: tslib: 2.8.1 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 optional: true '@emnapi/runtime@1.9.2': @@ -20805,9 +20813,9 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-arm64@0.35.3': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true '@img/sharp-darwin-x64@0.34.5': @@ -20815,74 +20823,74 @@ snapshots: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.35.3': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.3': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.3.2': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.3.2': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.3.2': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.3.2': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.2': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.2': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.3.2': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.3.2': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.2': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true '@img/sharp-linux-arm64@0.34.5': @@ -20890,9 +20898,9 @@ snapshots: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm64@0.35.3': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true '@img/sharp-linux-arm@0.34.5': @@ -20900,9 +20908,9 @@ snapshots: '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-arm@0.35.3': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true '@img/sharp-linux-ppc64@0.34.5': @@ -20910,9 +20918,9 @@ snapshots: '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.35.3': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true '@img/sharp-linux-riscv64@0.34.5': @@ -20920,9 +20928,9 @@ snapshots: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-riscv64@0.35.3': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true '@img/sharp-linux-s390x@0.34.5': @@ -20930,9 +20938,9 @@ snapshots: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.35.3': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true '@img/sharp-linux-x64@0.34.5': @@ -20940,9 +20948,9 @@ snapshots: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.35.3': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true '@img/sharp-linuxmusl-arm64@0.34.5': @@ -20950,9 +20958,9 @@ snapshots: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.35.3': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true '@img/sharp-linuxmusl-x64@0.34.5': @@ -20960,42 +20968,42 @@ snapshots: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.35.3': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.2 optional: true - '@img/sharp-wasm32@0.35.3': + '@img/sharp-wasm32@0.35.4': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.3': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-arm64@0.35.3': + '@img/sharp-win32-arm64@0.35.4': optional: true '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-ia32@0.35.3': + '@img/sharp-win32-ia32@0.35.4': optional: true '@img/sharp-win32-x64@0.34.5': optional: true - '@img/sharp-win32-x64@0.35.3': + '@img/sharp-win32-x64@0.35.4': optional: true '@inquirer/ansi@1.0.2': {} @@ -21862,7 +21870,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/cli@3.7.2(@emnapi/runtime@1.9.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0)': + '@napi-rs/cli@3.7.2(@emnapi/runtime@1.11.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0)': dependencies: '@inquirer/prompts': 8.5.2(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)) '@napi-rs/cross-toolchain': 1.0.3 @@ -21877,7 +21885,7 @@ snapshots: semver: 7.8.5 typanion: 3.14.0 optionalDependencies: - '@emnapi/runtime': 1.9.2 + '@emnapi/runtime': 1.11.2 transitivePeerDependencies: - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7' @@ -22052,14 +22060,14 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.2 '@tybys/wasm-util': 0.10.1 optional: true '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.2 '@tybys/wasm-util': 0.9.0 '@napi-rs/wasm-runtime@1.0.7': @@ -25058,7 +25066,7 @@ snapshots: '@resvg/resvg-wasm': 2.4.1 satori: 0.29.0 optionalDependencies: - sharp: 0.35.3(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)) + sharp: 0.35.4(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)) transitivePeerDependencies: - '@types/node' @@ -30373,7 +30381,7 @@ snapshots: npm-package-arg: 13.0.1 promzard: 2.0.0 read: 4.1.0 - semver: 7.7.3 + semver: 7.8.5 validate-npm-package-license: 3.0.4 validate-npm-package-name: 6.0.2 @@ -36603,37 +36611,37 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true - sharp@0.35.3(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)): + sharp@0.35.4(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.3 - '@img/sharp-darwin-x64': 0.35.3 - '@img/sharp-freebsd-wasm32': 0.35.3 - '@img/sharp-libvips-darwin-arm64': 1.3.2 - '@img/sharp-libvips-darwin-x64': 1.3.2 - '@img/sharp-libvips-linux-arm': 1.3.2 - '@img/sharp-libvips-linux-arm64': 1.3.2 - '@img/sharp-libvips-linux-ppc64': 1.3.2 - '@img/sharp-libvips-linux-riscv64': 1.3.2 - '@img/sharp-libvips-linux-s390x': 1.3.2 - '@img/sharp-libvips-linux-x64': 1.3.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 - '@img/sharp-linux-arm': 0.35.3 - '@img/sharp-linux-arm64': 0.35.3 - '@img/sharp-linux-ppc64': 0.35.3 - '@img/sharp-linux-riscv64': 0.35.3 - '@img/sharp-linux-s390x': 0.35.3 - '@img/sharp-linux-x64': 0.35.3 - '@img/sharp-linuxmusl-arm64': 0.35.3 - '@img/sharp-linuxmusl-x64': 0.35.3 - '@img/sharp-webcontainers-wasm32': 0.35.3 - '@img/sharp-win32-arm64': 0.35.3 - '@img/sharp-win32-ia32': 0.35.3 - '@img/sharp-win32-x64': 0.35.3 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 '@types/node': 20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab) optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 33e83211d500..90d951a4fd91 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -36,6 +36,35 @@ ignoredBuiltDependencies: blockExoticSubdeps: true minimumReleaseAge: 2880 # 48 hrs minimumReleaseAgeExclude: + # sharp 0.35.4 and its @img/* binaries fix a security vulnerability, so they + # are exempt from the 48h release age gate. + # These exemptions should be removed on 2026-08-28 + - '@img/sharp-darwin-arm64@0.35.4' + - '@img/sharp-darwin-x64@0.35.4' + - '@img/sharp-freebsd-wasm32@0.35.4' + - '@img/sharp-libvips-darwin-arm64@1.3.3' + - '@img/sharp-libvips-darwin-x64@1.3.3' + - '@img/sharp-libvips-linux-arm64@1.3.3' + - '@img/sharp-libvips-linux-arm@1.3.3' + - '@img/sharp-libvips-linux-ppc64@1.3.3' + - '@img/sharp-libvips-linux-riscv64@1.3.3' + - '@img/sharp-libvips-linux-s390x@1.3.3' + - '@img/sharp-libvips-linux-x64@1.3.3' + - '@img/sharp-libvips-linuxmusl-arm64@1.3.3' + - '@img/sharp-libvips-linuxmusl-x64@1.3.3' + - '@img/sharp-linux-arm64@0.35.4' + - '@img/sharp-linux-arm@0.35.4' + - '@img/sharp-linux-ppc64@0.35.4' + - '@img/sharp-linux-riscv64@0.35.4' + - '@img/sharp-linux-s390x@0.35.4' + - '@img/sharp-linux-x64@0.35.4' + - '@img/sharp-linuxmusl-arm64@0.35.4' + - '@img/sharp-linuxmusl-x64@0.35.4' + - '@img/sharp-wasm32@0.35.4' + - '@img/sharp-webcontainers-wasm32@0.35.4' + - '@img/sharp-win32-arm64@0.35.4' + - '@img/sharp-win32-ia32@0.35.4' + - '@img/sharp-win32-x64@0.35.4' - '@mswjs/interceptors@0.42.0' - '@next/*' - '@turbo/*' @@ -51,4 +80,5 @@ minimumReleaseAgeExclude: - react-is - react-server-dom-* - scheduler + - sharp@0.35.4 - turbo diff --git a/test/e2e/app-dir/instant-validation/client.util.ts b/test/e2e/app-dir/instant-validation/client.util.ts index d3d92e2109dc..99b8281c9595 100644 --- a/test/e2e/app-dir/instant-validation/client.util.ts +++ b/test/e2e/app-dir/instant-validation/client.util.ts @@ -416,7 +416,7 @@ export function registerClientTests(ctx: InstantValidationCaseContext) { "cause": [ { "label": "Caused by: Error", - "message": "Bail out to client-side rendering: next/dynamic", + "message": "Browser-only rendering was requested by \`browser()\`.", "source": null, "stack": [], }, @@ -448,10 +448,9 @@ export function registerClientTests(ctx: InstantValidationCaseContext) { at body () at html () at c () { - [cause]: Error: Bail out to client-side rendering: next/dynamic + [cause]: Error: Browser-only rendering was requested by \`browser()\`. at ignore-listed frames { - reason: 'next/dynamic', - digest: 'BAILOUT_TO_CLIENT_SIDE_RENDERING' + [cause]: 'next/dynamic' } } Build-time instant validation failed for route "/suspense-in-root/static/invalid-csr-bailout-blocks-children". diff --git a/test/e2e/app-dir/instant-validation/next.config.ts b/test/e2e/app-dir/instant-validation/next.config.ts index 177c5d1c3678..fcc7533f75b3 100644 --- a/test/e2e/app-dir/instant-validation/next.config.ts +++ b/test/e2e/app-dir/instant-validation/next.config.ts @@ -3,6 +3,7 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { cacheComponents: true, experimental: { + reactBrowserBailout: true, instantInsights: { validationLevel: 'manual-warning', }, diff --git a/test/e2e/app-dir/missing-suspense-with-csr-bailout/missing-suspense-with-browser-bailout.test.ts b/test/e2e/app-dir/missing-suspense-with-csr-bailout/missing-suspense-with-browser-bailout.test.ts new file mode 100644 index 000000000000..d0f0465947df --- /dev/null +++ b/test/e2e/app-dir/missing-suspense-with-csr-bailout/missing-suspense-with-browser-bailout.test.ts @@ -0,0 +1,2 @@ +process.env.TEST_REACT_BROWSER_BAILOUT = '1' +require('./missing-suspense-with-csr-bailout.test') diff --git a/test/e2e/app-dir/missing-suspense-with-csr-bailout/missing-suspense-with-csr-bailout.test.ts b/test/e2e/app-dir/missing-suspense-with-csr-bailout/missing-suspense-with-csr-bailout.test.ts index 46f02b1fb5a7..ddf0ebbfe8e4 100644 --- a/test/e2e/app-dir/missing-suspense-with-csr-bailout/missing-suspense-with-csr-bailout.test.ts +++ b/test/e2e/app-dir/missing-suspense-with-csr-bailout/missing-suspense-with-csr-bailout.test.ts @@ -33,6 +33,9 @@ describe('missing-suspense-with-csr-bailout', () => { const { exitCode } = await next.build() expect(exitCode).toBe(1) expect(next.cliOutput).toContain(message) + expect(next.cliOutput).not.toContain( + 'The server render could not complete because client rendering was requested outside a Suspense boundary' + ) // Can show the trace where the searchParams hook is used // TODO: This path is different for Turbopack. Builds need to have sourcemaps support. if (!process.env.IS_TURBOPACK_TEST) { diff --git a/test/e2e/app-dir/missing-suspense-with-csr-bailout/next.config.js b/test/e2e/app-dir/missing-suspense-with-csr-bailout/next.config.js index 3477de6e00c9..e68f7bcadfd8 100644 --- a/test/e2e/app-dir/missing-suspense-with-csr-bailout/next.config.js +++ b/test/e2e/app-dir/missing-suspense-with-csr-bailout/next.config.js @@ -1,4 +1,8 @@ /** @type {import("next").NextConfig} */ -const config = {} +const config = { + experimental: { + reactBrowserBailout: process.env.TEST_REACT_BROWSER_BAILOUT === '1', + }, +} module.exports = config diff --git a/test/e2e/image-optimizer/image-optimizer.test.ts b/test/e2e/image-optimizer/image-optimizer.test.ts index 434ad1056be2..cda4b5eb996f 100644 --- a/test/e2e/image-optimizer/image-optimizer.test.ts +++ b/test/e2e/image-optimizer/image-optimizer.test.ts @@ -445,26 +445,6 @@ describe('Image Optimizer', () => { }) } ) - describe('experimental.imgOptDangerouslyAllowAVIF in next.config.js', () => { - const { next, skipped } = nextTestSetup({ - files: join(__dirname, 'app'), - nextConfig: { - experimental: { imgOptDangerouslyAllowAVIF: true }, - }, - skipDeployment: true, - }) - if (skipped) return - - it('should optimize avif input when enabled', async () => { - const query = { w: 256, q: 75, url: '/test.avif' } - const opts = { headers: { accept: 'image/webp' } } - const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) - - expect(res.status).toBe(200) - expect(res.headers.get('Content-Type')).toBe('image/webp') - await expectWidth(res, 256) - }) - }) ;(isNextStart ? describe : describe.skip)( 'External rewrite support with for serving static content in images', () => { diff --git a/test/e2e/image-optimizer/sharp.test.ts b/test/e2e/image-optimizer/sharp.test.ts index f92c490f29f0..fd03aabda713 100644 --- a/test/e2e/image-optimizer/sharp.test.ts +++ b/test/e2e/image-optimizer/sharp.test.ts @@ -1,21 +1,5 @@ -import fs from 'fs-extra' -import { join } from 'path' -import { optimizeImage } from 'next/dist/server/image-optimizer' import { setupTests } from './util' describe('with latest sharp', () => { - it('should block avif decoding below the optimizer bypass', async () => { - const buffer = await fs.readFile(join(__dirname, 'app/public/test.avif')) - - await expect( - optimizeImage({ - buffer, - contentType: 'image/webp', - quality: 75, - width: 100, - }) - ).rejects.toThrow() - }) - setupTests({}) }) diff --git a/test/e2e/image-optimizer/util.ts b/test/e2e/image-optimizer/util.ts index f1e66c890e43..2eec29e266b0 100644 --- a/test/e2e/image-optimizer/util.ts +++ b/test/e2e/image-optimizer/util.ts @@ -122,15 +122,6 @@ export async function expectWidth(res, w, { expectAnimated = false } = {}) { expect(isAnimated(buffer)).toBe(expectAnimated) } -async function expectUnoptimizedImage(res, filename: string) { - const buffer = Buffer.from(await res.arrayBuffer()) - const source = await fs.readFile(join(__dirname, 'app/public', filename)) - expect(buffer).toEqual(source) - expect(res.headers.get('Content-Length')).toBe( - Buffer.byteLength(source).toString() - ) -} - export const cleanImagesDir = async (imagesDir) => { console.warn('Cleaning', imagesDir) await fs.remove(imagesDir) @@ -419,7 +410,8 @@ export function runTests(ctx: RunTestsCtx) { const query = { w: ctx.w, q: ctx.q, url: '/api/conditional-cookie' } const opts = { headers: { accept: 'image/webp', cookie: '1' } } const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) - expect(res.status).toBe(400) + // the cookie is not forwarded so the internal handler responds 401 + expect(res.status).toBe(401) }) if (ctx.nextConfigImages?.dangerouslyAllowSVG) { @@ -614,23 +606,23 @@ export function runTests(ctx: RunTestsCtx) { await expectWidth(res, ctx.w) }) - it('should bypass avif input for old Safari', async () => { + it('should downlevel avif format to jpeg for old Safari', async () => { const accept = 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5' const query = { w: ctx.w, q: ctx.q, url: '/test.avif' } const opts = { headers: { accept } } const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) - expect(res.headers.get('Content-Type')).toContain('image/avif') + expect(res.headers.get('Content-Type')).toContain('image/jpeg') expect(res.headers.get('Cache-Control')).toBe( `public, max-age=${isDev ? 0 : minimumCacheTTL}, must-revalidate` ) expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `${contentDispositionType}; filename="test.avif"` + `${contentDispositionType}; filename="test.jpeg"` ) - await expectUnoptimizedImage(res, 'test.avif') + await expectWidth(res, ctx.w) }) it('should fail when url is missing', async () => { @@ -941,21 +933,21 @@ export function runTests(ctx: RunTestsCtx) { await expectWidth(res, ctx.w) }) - it('should bypass optimization for avif input', async () => { + it('should resize avif', async () => { const query = { url: '/test.avif', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) - expect(res.headers.get('Content-Type')).toBe('image/avif') + expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( `public, max-age=${isDev ? 0 : minimumCacheTTL}, must-revalidate` ) expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `${contentDispositionType}; filename="test.avif"` + `${contentDispositionType}; filename="test.webp"` ) - await expectUnoptimizedImage(res, 'test.avif') + await expectWidth(res, ctx.w) }) it('should resize relative url and old Chrome accept header as webp', async () => { @@ -999,7 +991,7 @@ export function runTests(ctx: RunTestsCtx) { await expectWidth(res, ctx.w) }) - it('should bypass avif input when avif output is enabled', async () => { + it('should resize avif and maintain format', async () => { const query = { url: '/test.avif', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/avif' } } const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) @@ -1013,7 +1005,7 @@ export function runTests(ctx: RunTestsCtx) { expect(res.headers.get('Content-Disposition')).toBe( `${contentDispositionType}; filename="test.avif"` ) - await expectUnoptimizedImage(res, 'test.avif') + await expectWidth(res, ctx.w) }) it('should compress avif smaller than webp at q=100', async () => { @@ -1615,7 +1607,7 @@ export function runTests(ctx: RunTestsCtx) { }) it("should error if the resource isn't a valid image", async () => { - const query = { url: '/test.txt', w: ctx.w, q: ctx.q } + const query = { url: '/text.txt', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) @@ -1626,8 +1618,10 @@ export function runTests(ctx: RunTestsCtx) { const query = { url: '/does_not_exist.jpg', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) - expect(res.status).toBe(400) - expect(await res.text()).toBe("The requested resource isn't a valid image.") + expect(res.status).toBe(404) + expect(await res.text()).toBe( + '"url" parameter is valid but internal response is invalid' + ) }) if (domains.length > 0 && dangerouslyAllowLocalIP) { diff --git a/test/e2e/on-request-error/client-abort/app/error/page.js b/test/e2e/on-request-error/client-abort/app/error/page.js new file mode 100644 index 000000000000..caf5743d4895 --- /dev/null +++ b/test/e2e/on-request-error/client-abort/app/error/page.js @@ -0,0 +1,15 @@ +import { connection } from 'next/server' +import { Suspense } from 'react' + +async function Boom() { + await connection() + throw new Error('server-side-error') +} + +export default function Page() { + return ( + error-pending

}> + +
+ ) +} diff --git a/test/e2e/on-request-error/client-abort/app/hang/page.js b/test/e2e/on-request-error/client-abort/app/hang/page.js new file mode 100644 index 000000000000..c1f6931f5f1b --- /dev/null +++ b/test/e2e/on-request-error/client-abort/app/hang/page.js @@ -0,0 +1,16 @@ +import { connection } from 'next/server' +import { Suspense } from 'react' + +async function Hang() { + await connection() + await new Promise(() => {}) + return null +} + +export default function Page() { + return ( + stream-started

}> + +
+ ) +} diff --git a/test/e2e/on-request-error/client-abort/app/layout.js b/test/e2e/on-request-error/client-abort/app/layout.js new file mode 100644 index 000000000000..750eb927b198 --- /dev/null +++ b/test/e2e/on-request-error/client-abort/app/layout.js @@ -0,0 +1,7 @@ +export default function Layout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/on-request-error/client-abort/client-abort.test.ts b/test/e2e/on-request-error/client-abort/client-abort.test.ts new file mode 100644 index 000000000000..6f7799474586 --- /dev/null +++ b/test/e2e/on-request-error/client-abort/client-abort.test.ts @@ -0,0 +1,43 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('on-request-error - client-abort', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + + if (skipped) { + return + } + + it('should not report an aborted RSC stream but still report render errors', async () => { + const controller = new AbortController() + const res = await next.fetch('/hang', { + headers: { RSC: '1' }, + signal: controller.signal, + }) + expect(res.status).toBe(200) + + const decoder = new TextDecoder() + for await (const rawChunk of res.body) { + const chunk = + typeof rawChunk === 'string' ? rawChunk : decoder.decode(rawChunk) + // the fallback is flushed as soon as the stream opens, so nothing else + // arrives until we abort + if (chunk.includes('stream-started')) { + break + } + } + controller.abort() + + await next.fetch('/error') + + await retry(async () => { + expect(next.cliOutput).toContain( + '[instrumentation]:error server-side-error' + ) + }) + expect(next.cliOutput).not.toContain('The destination stream closed early') + }) +}) diff --git a/test/e2e/on-request-error/client-abort/instrumentation.js b/test/e2e/on-request-error/client-abort/instrumentation.js new file mode 100644 index 000000000000..869ed28156fd --- /dev/null +++ b/test/e2e/on-request-error/client-abort/instrumentation.js @@ -0,0 +1,3 @@ +export function onRequestError(err) { + console.log('[instrumentation]:error', err.message) +} diff --git a/test/e2e/on-request-error/client-abort/next.config.js b/test/e2e/on-request-error/client-abort/next.config.js new file mode 100644 index 000000000000..4ba52ba2c8df --- /dev/null +++ b/test/e2e/on-request-error/client-abort/next.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/test/production/deterministic-build/deployment-id.test.ts b/test/production/deterministic-build/deployment-id.test.ts index 5d1f80ef9ed1..bd0617ee2d53 100644 --- a/test/production/deterministic-build/deployment-id.test.ts +++ b/test/production/deterministic-build/deployment-id.test.ts @@ -242,7 +242,8 @@ async function runTest( }, // We use NEXT_TEST_PREFER_OFFLINE, so just declaring `vercel: latest` as a dependency still // doesn't force the latest version. - buildCommand: 'pnpm dlx vercel@latest build', + // TODO: Go back to integration testing with latest once breakage in 59.6.2 is resolved. + buildCommand: 'pnpm dlx vercel@59.5.0 build', env: mode === 'adapter' ? { diff --git a/test/production/next-server-nft/next-server-nft.test.ts b/test/production/next-server-nft/next-server-nft.test.ts index 6a914dba10eb..43769c4b53ad 100644 --- a/test/production/next-server-nft/next-server-nft.test.ts +++ b/test/production/next-server-nft/next-server-nft.test.ts @@ -712,7 +712,7 @@ async function readNormalizedNFT(next, name) { "/node_modules/next/dist/shared/lib/invariant-error.js", "/node_modules/next/dist/shared/lib/is-plain-object.js", "/node_modules/next/dist/shared/lib/is-thenable.js", - "/node_modules/next/dist/shared/lib/lazy-dynamic/bailout-to-csr.js", + "/node_modules/next/dist/shared/lib/lazy-dynamic/react-browser-bailout.js", "/node_modules/next/dist/shared/lib/no-fallback-error.external.js", "/node_modules/next/dist/shared/lib/promise-with-resolvers.js", "/node_modules/next/dist/shared/lib/server-reference-info.js", diff --git a/test/unit/image-optimizer/fetch-internal-image.test.ts b/test/unit/image-optimizer/fetch-internal-image.test.ts index e9acf219aa61..800f6307b7ed 100644 --- a/test/unit/image-optimizer/fetch-internal-image.test.ts +++ b/test/unit/image-optimizer/fetch-internal-image.test.ts @@ -6,6 +6,63 @@ import { import type { IncomingMessage, ServerResponse } from 'http' describe('fetchInternalImage', () => { + describe('non-2xx responses', () => { + it('should throw error when the internal response is a redirect', async () => { + const mockReq = {} as IncomingMessage + const mockRes = {} as ServerResponse + const maximumResponseBody = 300_000_000 + + const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => { + res.statusCode = 307 + res.getHeader = jest.fn(() => 'text/plain') + res.write('/redirected') + res.end() + }) + + const error = await fetchInternalImage( + '/test-image.jpg', + mockReq, + mockRes, + maximumResponseBody, + handleRequest + ).catch((e) => e) + + expect(error).toBeInstanceOf(ImageError) + // ImageError maps any status below 400 to 500 + expect((error as ImageError).statusCode).toBe(500) + expect((error as ImageError).message).toBe( + '"url" parameter is valid but internal response is invalid' + ) + }) + + it('should throw error when the internal response is a 404', async () => { + const mockReq = {} as IncomingMessage + const mockRes = {} as ServerResponse + const maximumResponseBody = 300_000_000 + + const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => { + res.statusCode = 404 + res.getHeader = jest.fn(() => 'text/html') + res.write('not found') + res.end() + }) + + const error = await fetchInternalImage( + '/test-image.jpg', + mockReq, + mockRes, + maximumResponseBody, + handleRequest + ).catch((e) => e) + + expect(error).toBeInstanceOf(ImageError) + expect((error as ImageError).statusCode).toBe(404) + expect((error as ImageError).message).toBe( + '"url" parameter is valid but internal response is invalid' + ) + }) + }) + describe('response size limit', () => { it('should throw error when response has no buffers', async () => { const mockReq = {} as IncomingMessage diff --git a/test/unit/next-image-get-img-props.test.ts b/test/unit/next-image-get-img-props.test.ts index 17f8a27fdc3f..9320a467e2bc 100644 --- a/test/unit/next-image-get-img-props.test.ts +++ b/test/unit/next-image-get-img-props.test.ts @@ -90,36 +90,6 @@ describe('getImageProps()', () => { expect(props.srcSet).toBeString() }) - it('should disable an automatic blur placeholder for a static avif', () => { - const { props } = getImageProps({ - alt: 'a nice desc', - src: { - src: '/_next/static/media/test.abc123.avif', - width: 100, - height: 200, - }, - placeholder: 'blur', - }) - - expect(props.style).toStrictEqual({ color: 'transparent' }) - }) - - it('should preserve a custom blur placeholder for a static avif', () => { - const blurDataURL = 'data:image/png;base64,custom' - const { props } = getImageProps({ - alt: 'a nice desc', - src: { - src: '/_next/static/media/test.abc123.avif', - width: 100, - height: 200, - }, - placeholder: 'blur', - blurDataURL, - }) - - expect(props.style?.backgroundImage).toContain(blurDataURL) - }) - it('should handle preload', async () => { const { props } = getImageProps({ alt: 'a nice desc', diff --git a/test/unit/next-image-legacy.test.ts b/test/unit/next-image-legacy.test.ts index 73ee76a730d5..e179d34d0972 100644 --- a/test/unit/next-image-legacy.test.ts +++ b/test/unit/next-image-legacy.test.ts @@ -103,40 +103,6 @@ describe('Image Legacy Rendering', () => { expect($3('noscript').length).toBe(1) }) - it('should disable an automatic blur placeholder for a static avif', () => { - const element = React.createElement(Image, { - src: { - src: '/_next/static/media/test.abc123.avif', - width: 100, - height: 200, - }, - placeholder: 'blur', - loading: 'eager', - }) - const $ = cheerio.load(ReactDOM.renderToString(element)) - - expect($('noscript').length).toBe(0) - expect($.html()).not.toContain('background-image') - }) - - it('should preserve a custom blur placeholder for a static avif', () => { - const blurDataURL = 'data:image/png;base64,custom' - const element = React.createElement(Image, { - src: { - src: '/_next/static/media/test.abc123.avif', - width: 100, - height: 200, - }, - placeholder: 'blur', - blurDataURL, - loading: 'eager', - }) - const $ = cheerio.load(ReactDOM.renderToString(element)) - - expect($('noscript').length).toBe(1) - expect($.html()).toContain(blurDataURL) - }) - it('should render the correct sizes passed when a noscript element is rendered', async () => { const element = React.createElement(Image, { src: '/test.png', diff --git a/test/unit/next-image-loader/get-blur-image.test.ts b/test/unit/next-image-loader/get-blur-image.test.ts index 340c186db374..f958694a9338 100644 --- a/test/unit/next-image-loader/get-blur-image.test.ts +++ b/test/unit/next-image-loader/get-blur-image.test.ts @@ -35,42 +35,4 @@ describe('getBlurImage', () => { expect(result).toBeObject() expect(result.dataURL).toBeUndefined() }) - it('should not generate a blur placeholder for avif', async () => { - const buffer = await getImage('../image-optimizer/images/test.avif') - const result = await getBlurImage( - buffer, - 'avif', - { width: 400, height: 400 }, - { - ...context, - outputPath: '/_next/static/media/test.avif', - } - ) - expect(result).toBeObject() - expect(result).toStrictEqual({ - dataURL: undefined, - width: 0, - height: 0, - }) - }) - it('should generate a blur placeholder for avif when enabled', async () => { - jest.resetModules() - const { getBlurImage: getBlurImageWithAVIF } = await import( - 'next/dist/build/webpack/loaders/next-image-loader/blur' - ) - const buffer = await getImage('../image-optimizer/images/test.avif') - const result = await getBlurImageWithAVIF( - buffer, - 'avif', - { width: 400, height: 400 }, - { - ...context, - imgOptDangerouslyAllowAVIF: true, - } - ) - expect(result).toBeObject() - expect(result.dataURL).toStartWith('data:image/avif;base64,') - expect(result.width).toBe(8) - expect(result.height).toBe(8) - }) })