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)
- })
})