From 4797acb82d509f920355ab311db564d2a93a8ac9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 28 May 2026 10:28:00 +0200 Subject: [PATCH 1/6] Turbopack: fix instrumentationClientInject with type:module (#94184) If the user's `package.json` contains, `type: module`, then the file generated by Turbopack at `[project]/__next_instrumentation_client.js` would get treated as ESM. But it always contains CJS, causing ``` Uncaught (in promise) ReferenceError: module is not defined at module evaluation (__next_instrumentation_client.js:3:1) ``` --- crates/next-core/src/next_import_map.rs | 5 ++++- .../inject/{next.config.js => next.config.mjs} | 2 +- .../inject/package.json | 3 +++ .../instrumentation-client-hook.test.ts | 18 +++++++++++++----- 4 files changed, 21 insertions(+), 7 deletions(-) rename test/e2e/instrumentation-client-hook/inject/{next.config.js => next.config.mjs} (85%) create mode 100644 test/e2e/instrumentation-client-hook/inject/package.json diff --git a/crates/next-core/src/next_import_map.rs b/crates/next-core/src/next_import_map.rs index 5b51301db73a..ef64e8857c70 100644 --- a/crates/next-core/src/next_import_map.rs +++ b/crates/next-core/src/next_import_map.rs @@ -1452,7 +1452,10 @@ async fn insert_instrumentation_client_alias( body.push_str("}};\n"); let virtual_source = VirtualSource::new( - project_path.join("__next_instrumentation_client.js")?, + // Use cjs here in case the user has type:module in the package.json. We do intentionally + // place this file in the user's folder, so that the `require`s inserted above resolve + // as expected. + project_path.join("__next_instrumentation_client.cjs")?, AssetContent::file(FileContent::Content(body.into()).cell()), ) .to_resolved() diff --git a/test/e2e/instrumentation-client-hook/inject/next.config.js b/test/e2e/instrumentation-client-hook/inject/next.config.mjs similarity index 85% rename from test/e2e/instrumentation-client-hook/inject/next.config.js rename to test/e2e/instrumentation-client-hook/inject/next.config.mjs index ee32e6ca3fed..113cdbe86295 100644 --- a/test/e2e/instrumentation-client-hook/inject/next.config.js +++ b/test/e2e/instrumentation-client-hook/inject/next.config.mjs @@ -1,4 +1,4 @@ /** @type {import('next').NextConfig} */ -module.exports = { +export default { instrumentationClientInject: ['./inject-a.js', './inject-b.js'], } diff --git a/test/e2e/instrumentation-client-hook/inject/package.json b/test/e2e/instrumentation-client-hook/inject/package.json new file mode 100644 index 000000000000..3dbc1ca591c0 --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts b/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts index 2a03659e432c..61f939a0f383 100644 --- a/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts +++ b/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts @@ -3,7 +3,7 @@ import { retry } from 'next-test-utils' import path from 'path' describe('Instrumentation Client Hook', () => { - const testCases = [ + describe.each([ { name: 'With src folder', appDir: 'app-with-src', @@ -19,9 +19,7 @@ describe('Instrumentation Client Hook', () => { appDir: 'pages-router', shouldLog: false, }, - ] - - testCases.forEach(({ name, appDir, shouldLog }) => { + ])('$name', ({ name, appDir, shouldLog }) => { describe(name, () => { const { next, isNextDev } = nextTestSetup({ files: path.join(__dirname, appDir), @@ -102,9 +100,19 @@ describe('Instrumentation Client Hook', () => { }) }) - describe('instrumentationClientInject', () => { + describe.each([ + { + name: 'default', + packageJson: {}, + }, + { + name: 'with type:module', + packageJson: { type: 'module' }, + }, + ])('instrumentationClientInject $name', ({ packageJson }) => { const { next } = nextTestSetup({ files: path.join(__dirname, 'inject'), + packageJson, }) it('runs each injected entry before the user instrumentation-client and before hydration, in array order', async () => { From 93eaf146ecd2d5b2f291b04a7c51bca0b02f2dec Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 28 May 2026 11:15:02 +0200 Subject: [PATCH 2/6] [test] Add test for static metadata files and `generateStaticParams` (#93465) --- .../metadata-static-file-gsp/app/layout.tsx | 11 +++++++++ .../app/results/[id]/opengraph-image.png | Bin 0 -> 67 bytes .../app/results/[id]/page.tsx | 12 ++++++++++ .../metadata-static-file-gsp.test.ts | 15 ++++++++++++ .../metadata-static-file-gsp/next.config.js | 6 +++++ ...metadata-static-file-dynamic-route.test.ts | 22 ------------------ 6 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 test/e2e/app-dir/metadata-static-file-gsp/app/layout.tsx create mode 100644 test/e2e/app-dir/metadata-static-file-gsp/app/results/[id]/opengraph-image.png create mode 100644 test/e2e/app-dir/metadata-static-file-gsp/app/results/[id]/page.tsx create mode 100644 test/e2e/app-dir/metadata-static-file-gsp/metadata-static-file-gsp.test.ts create mode 100644 test/e2e/app-dir/metadata-static-file-gsp/next.config.js diff --git a/test/e2e/app-dir/metadata-static-file-gsp/app/layout.tsx b/test/e2e/app-dir/metadata-static-file-gsp/app/layout.tsx new file mode 100644 index 000000000000..dbce4ea8e3ae --- /dev/null +++ b/test/e2e/app-dir/metadata-static-file-gsp/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/metadata-static-file-gsp/app/results/[id]/opengraph-image.png b/test/e2e/app-dir/metadata-static-file-gsp/app/results/[id]/opengraph-image.png new file mode 100644 index 0000000000000000000000000000000000000000..aea7f5ff8ad3e3c7c084080bda8ac3b95e9a71cd GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bPE>9Q7kcv6U2|zXz1Ea_KC51p1 NgQu&X%Q~loCIDk>43q!> literal 0 HcmV?d00001 diff --git a/test/e2e/app-dir/metadata-static-file-gsp/app/results/[id]/page.tsx b/test/e2e/app-dir/metadata-static-file-gsp/app/results/[id]/page.tsx new file mode 100644 index 000000000000..4a83012671a1 --- /dev/null +++ b/test/e2e/app-dir/metadata-static-file-gsp/app/results/[id]/page.tsx @@ -0,0 +1,12 @@ +export function generateStaticParams() { + return [{ id: 'one' }, { id: 'two' }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + return

result: {id}

+} diff --git a/test/e2e/app-dir/metadata-static-file-gsp/metadata-static-file-gsp.test.ts b/test/e2e/app-dir/metadata-static-file-gsp/metadata-static-file-gsp.test.ts new file mode 100644 index 000000000000..c0b67b179deb --- /dev/null +++ b/test/e2e/app-dir/metadata-static-file-gsp/metadata-static-file-gsp.test.ts @@ -0,0 +1,15 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('metadata-static-file-gsp', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + }) + + if (skipped) { + return + } + + it('should build and serve a static metadata file colocated with generateStaticParams', async () => { + await next.render('/results/two') + }) +}) diff --git a/test/e2e/app-dir/metadata-static-file-gsp/next.config.js b/test/e2e/app-dir/metadata-static-file-gsp/next.config.js new file mode 100644 index 000000000000..807126e4cf0b --- /dev/null +++ b/test/e2e/app-dir/metadata-static-file-gsp/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/metadata-static-file/metadata-static-file-dynamic-route.test.ts b/test/e2e/app-dir/metadata-static-file/metadata-static-file-dynamic-route.test.ts index cc5849d91973..2a9f6a67ad2e 100644 --- a/test/e2e/app-dir/metadata-static-file/metadata-static-file-dynamic-route.test.ts +++ b/test/e2e/app-dir/metadata-static-file/metadata-static-file-dynamic-route.test.ts @@ -6,28 +6,6 @@ import { } from './utils' describe('metadata-files-static-output-dynamic-route', () => { - if (process.env.__NEXT_CACHE_COMPONENTS) { - // Cache Components build fails when metadata files are inside a dynamic route. - // - // Route "/dynamic/[id]": Next.js encountered uncached or runtime data in `generateMetadata()`. - // - // This prevents the page from being prerendered, leading to a slower user experience. - // - // Ways to fix this: - // - Use a static metadata export instead of `generateMetadata()` - // - Cache the metadata with `"use cache"` in `generateMetadata()` - // - Add a dynamic data access (e.g. `await connection()`) to the page to render it at request time - // - Set `export const instant = false` to allow a blocking route - // - // Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata - // Error occurred prerendering page "/dynamic/[id]". Read more: https://nextjs.org/docs/messages/prerender-error - // Export encountered an error on /dynamic/[id]/page: /dynamic/[id], exiting the build. - // - // TODO: Remove this skip when metadata files are supported in dynamic routes for Cache Components. - it.skip('should skip test for Cache Components', () => {}) - return - } - const { next, skipped } = nextTestSetup({ files: __dirname, }) From dd0cae63e01534a9813437d40dd8b85cf33ebe05 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 28 May 2026 13:24:18 +0200 Subject: [PATCH 3/6] Align issue triage guidance with automated behavior (#94189) ### What? - Align the public triage guide with the issue workflows current response windows and confirmed-team labels. - Update reproduction and canary response comments to describe automatic closure separately from the later lock workflow. ### Why? The documented triage contract had drifted from the active automation: the guide described 30-day response windows and unsupported triage labeling, while automated comments implied that timed closure immediately locks issues. This sets inaccurate expectations for reporters and maintainers. ### How? Update the four Markdown surfaces that explain manual triage and automated follow-up behavior to match the existing stale and lock workflows. ### Verification - `pnpm prettier --with-node-modules --ignore-path .prettierignore --check contributing/repository/triaging.md .github/comments/invalid-reproduction.md .github/comments/simplify-reproduction.md .github/comments/verify-canary.md` - `git diff --cached --check` - Not run: `pnpm --filter=next build` (`documentation and GitHub comment text only; no framework source changes`) --- .github/comments/invalid-reproduction.md | 4 ++-- .github/comments/simplify-reproduction.md | 4 ++-- .github/comments/verify-canary.md | 4 ++-- contributing/repository/triaging.md | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/comments/invalid-reproduction.md b/.github/comments/invalid-reproduction.md index 0bd0d4861574..628934e21008 100644 --- a/.github/comments/invalid-reproduction.md +++ b/.github/comments/invalid-reproduction.md @@ -16,9 +16,9 @@ Ensure the link is pointing to a codebase that is accessible (e.g. not a private ### What happens if I don't provide a sufficient minimal reproduction? -Issues with the `please add a complete reproduction` label that receives no meaningful activity (e.g. new comments with a reproduction link) are automatically closed and locked after **2** days. +Issues with the `please add a complete reproduction` label that receives no meaningful activity (e.g. new comments with a reproduction link) are automatically closed after **2** days. -If your issue has _not_ been resolved in that time and it has been closed/locked, please open a new issue with the required reproduction. +If your issue has _not_ been resolved in that time and it has been closed, please open a new issue with the required reproduction. ### I did not open this issue, but it is relevant to me, what can I do to help? diff --git a/.github/comments/simplify-reproduction.md b/.github/comments/simplify-reproduction.md index f81e5dcebb71..bb786640f7f0 100644 --- a/.github/comments/simplify-reproduction.md +++ b/.github/comments/simplify-reproduction.md @@ -21,9 +21,9 @@ If you cannot create a clean reproduction, another way you can help the maintain ### What happens if I don't provide a sufficient minimal reproduction? -Issues with the `please simplify reproduction` label that receive no meaningful activity (e.g. new comments with a simplified reproduction link) are automatically closed and locked after **14** days. +Issues with the `please simplify reproduction` label that receive no meaningful activity (e.g. new comments with a simplified reproduction link) are automatically closed after **14** days. -If your issue has _not_ been resolved in that time and it has been closed/locked, please open a new issue with the required reproduction. +If your issue has _not_ been resolved in that time and it has been closed, please open a new issue with the required reproduction. ### I did not open this issue, but it is relevant to me, what can I do to help? diff --git a/.github/comments/verify-canary.md b/.github/comments/verify-canary.md index e10573989400..509141d159f1 100644 --- a/.github/comments/verify-canary.md +++ b/.github/comments/verify-canary.md @@ -18,9 +18,9 @@ Next.js does not backport bug fixes to older versions of Next.js. Instead, we ar ### What happens if I don't verify against the canary version of Next.js? -An issue with the `please verify canary` that receives no meaningful activity (e.g. new comments that acknowledge verification against `canary`) will be automatically closed and locked after **14** days. +An issue with the `please verify canary` that receives no meaningful activity (e.g. new comments that acknowledge verification against `canary`) will be automatically closed after **14** days. -If your issue has not been resolved in that time and it has been closed/locked, please open a new issue, with the required reproduction, using `next@canary`. +If your issue has not been resolved in that time and it has been closed, please open a new issue, with the required reproduction, using `next@canary`. ### I did not open this issue, but it is relevant to me, what can I do to help? diff --git a/contributing/repository/triaging.md b/contributing/repository/triaging.md index df4b21d568f8..16222fd55318 100644 --- a/contributing/repository/triaging.md +++ b/contributing/repository/triaging.md @@ -26,19 +26,19 @@ A maintainer can also manually label an issue with one of the following labels, 1. `please add a complete reproduction` -The provided reproduction is not enough for the maintainers to investigate. If sufficient reproduction is not provided for more than 30 days, the issue becomes stale and will be automatically closed. If a reproduction is provided within 30 days, a `needs triage` label is added, indicating that the issue needs another look from a maintainer. +The provided reproduction is not enough for the maintainers to investigate. If a sufficient reproduction is not provided, the issue will be automatically closed after 2 days of inactivity. The issue will receive [this comment](https://github.com/vercel/next.js/blob/canary/.github/comments/invalid-reproduction.md) 2. `please verify canary` -The issue is not verified against the `next@canary` release. The canary version of Next.js ships daily and includes all features and fixes that have not been released to the stable version yet. Think of canary as a public beta. Some issues may already be fixed in the canary version, so please verify that your issue reproduces before opening a new issue. Issues not verified against `next@canary` will be closed after 30 days. +The issue is not verified against the `next@canary` release. The canary version of Next.js ships daily and includes all features and fixes that have not been released to the stable version yet. Think of canary as a public beta. Some issues may already be fixed in the canary version, so please verify that your issue reproduces before opening a new issue. Issues not verified against `next@canary` will be automatically closed after 14 days of inactivity. The issue will receive [this comment](https://github.com/vercel/next.js/blob/canary/.github/comments/verify-canary.md) 3. `please simplify reproduction` -The provided reproduction is too complex or requires too many steps to reproduce. If a simplified reproduction is not provided for more than 30 days, the issue becomes stale and will be automatically closed. If a reproduction is provided within 30 days, a `needs triage` label is added, indicating that the issue needs another look from a maintainer. +The provided reproduction is too complex or requires too many steps to reproduce. If a simplified reproduction is not provided, the issue will be automatically closed after 14 days of inactivity. The issue will receive [this comment](https://github.com/vercel/next.js/blob/canary/.github/comments/simplify-reproduction.md) 4. `good first issue` @@ -53,7 +53,7 @@ The issue will receive [this comment](https://github.com/vercel/next.js/blob/can ## Verified issues -If an issue is verified, it will receive the `linear: next`, `linear: dx` or `linear: web` label and will be tracked by the maintainers. Additionally, one or more [label(s)](https://github.com/vercel/next.js/labels) can be added to indicate which part of Next.js is affected. +If an issue is verified, it will receive the `linear: next`, `linear: turbopack` or `linear: docs` label and will be tracked by the maintainers. Additionally, one or more [label(s)](https://github.com/vercel/next.js/labels) can be added to indicate which part of Next.js is affected. Confirmed issues never become stale or are closed before resolution. From 5edf48238f2ea36051ed556207488f7168c97e7a Mon Sep 17 00:00:00 2001 From: "next-js-bot[bot]" <279046576+next-js-bot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 11:33:06 +0000 Subject: [PATCH 4/6] v16.3.0-canary.32 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-internal/package.json | 2 +- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-playwright/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-routing/package.json | 2 +- packages/next-rspack/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 21 files changed, 36 insertions(+), 36 deletions(-) diff --git a/lerna.json b/lerna.json index 23c1ea330a93..8d9de6286200 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.3.0-canary.31" + "version": "16.3.0-canary.32" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index c3124b11ad01..2308a63920b6 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.3.0-canary.31", + "version": "16.3.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 5a9f8dcc40e1..606cbacd9465 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.3.0-canary.31", + "version": "16.3.0-canary.32", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.3.0-canary.31", + "@next/eslint-plugin-next": "16.3.0-canary.32", "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 44bd53320d81..b0cd27d6c03c 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.3.0-canary.31", + "version": "16.3.0-canary.32", "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 c6182c215a05..a459bf0ec10b 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.3.0-canary.31", + "version": "16.3.0-canary.32", "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 75dc04ab137b..f027260dec97 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "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 e8dcb7b1bd39..80be48efc9eb 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.3.0-canary.31", + "version": "16.3.0-canary.32", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index e05e85ada44f..b58e39f9abb6 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 6e80619001df..50b1a6bbbbb0 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 9f7dfd65ef00..267b27b2a251 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index 6ca13fb32db1..8bbca23b6844 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@next/playwright", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "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 c3e0fd4e8a00..6d2919dd338e 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.3.0-canary.31", + "version": "16.3.0-canary.32", "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 a0256a8f11c2..1045d1dfe2c5 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.3.0-canary.31", + "version": "16.3.0-canary.32", "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 ce86dbe11442..9f196015c541 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.3.0-canary.31", + "version": "16.3.0-canary.32", "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 a765d9cad1f7..c99984ae5048 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index cd1b37c9832b..2035ebf4f3d3 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 65ce254d1d5d..5dd048a2029b 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 5e0263d6d40a..993664a0d79c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -101,7 +101,7 @@ ] }, "dependencies": { - "@next/env": "16.3.0-canary.31", + "@next/env": "16.3.0-canary.32", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -165,11 +165,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "16.3.0-canary.31", - "@next/polyfill-module": "16.3.0-canary.31", - "@next/polyfill-nomodule": "16.3.0-canary.31", - "@next/react-refresh-utils": "16.3.0-canary.31", - "@next/swc": "16.3.0-canary.31", + "@next/font": "16.3.0-canary.32", + "@next/polyfill-module": "16.3.0-canary.32", + "@next/polyfill-nomodule": "16.3.0-canary.32", + "@next/react-refresh-utils": "16.3.0-canary.32", + "@next/swc": "16.3.0-canary.32", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.58.2", "@rspack/core": "1.6.7", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 43076cff0126..85eddc5eea2f 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.3.0-canary.31", + "version": "16.3.0-canary.32", "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 ef236ccf4dcc..07ed01081aa2 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.3.0-canary.31", + "version": "16.3.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -27,7 +27,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.3.0-canary.31", + "next": "16.3.0-canary.32", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "6.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 106329ae7860..8236e87567e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -986,7 +986,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1063,7 +1063,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1184,19 +1184,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../font '@next/polyfill-module': - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../react-refresh-utils '@next/swc': - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1930,7 +1930,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.3.0-canary.31 + specifier: 16.3.0-canary.32 version: link:../next outdent: specifier: 0.8.0 From b14d9a42137ec666ec37489f8ebeccec4ef7f064 Mon Sep 17 00:00:00 2001 From: Stanislav Kozachenko <49488521+StanislavKozachenko@users.noreply.github.com> Date: Thu, 28 May 2026 14:49:58 +0300 Subject: [PATCH 5/6] docs: fix code examples in getting-started guides (#93273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Fix three broken code examples in the Getting Started section. ### Why? Per the contribution guide, code blocks should contain a minimum working example that can be copied and pasted. These examples have bugs that would cause runtime errors or incorrect behavior: 1. `02-project-structure.mdx`: Extra comma in App Icons table — `apple-icon` row has `.jpg` `.jpeg`, `.png` while all other rows use space-separated extensions without commas. 2. `03-layouts-and-pages.mdx`: `Post` component has an unused `post` prop — the component fetches and renders all posts internally via `getPosts()`, so the prop is never referenced. Removed the unused prop, renamed to `Posts` to match its actual behavior, and added the missing `import { getPosts } from '@/lib/posts'`. 3. `04-linking-and-navigating.mdx`: JSX `Layout` function is missing `{ children }` in its parameter list, causing `ReferenceError: children is not defined` at runtime. The TSX version is correct. ### How? - Remove stray comma from App Icons table - Remove unused `post` prop from `Post` component, rename to `Posts`, add missing import - Add `{ children }` destructuring to JSX `Layout` function signature Closes #93272 --------- Co-authored-by: Joseph --- .../01-app/01-getting-started/02-project-structure.mdx | 2 +- .../01-app/01-getting-started/03-layouts-and-pages.mdx | 10 ++++++---- .../01-getting-started/04-linking-and-navigating.mdx | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/01-app/01-getting-started/02-project-structure.mdx b/docs/01-app/01-getting-started/02-project-structure.mdx index 613057acf7ac..1fefcd731913 100644 --- a/docs/01-app/01-getting-started/02-project-structure.mdx +++ b/docs/01-app/01-getting-started/02-project-structure.mdx @@ -122,7 +122,7 @@ Use `@slot` for named slots rendered by a parent layout. Use intercept patterns | [`favicon`](/docs/app/api-reference/file-conventions/metadata/app-icons#favicon) | `.ico` | Favicon file | | [`icon`](/docs/app/api-reference/file-conventions/metadata/app-icons#icon) | `.ico` `.jpg` `.jpeg` `.png` `.svg` | App Icon file | | [`icon`](/docs/app/api-reference/file-conventions/metadata/app-icons#generate-icons-using-code-js-ts-tsx) | `.js` `.ts` `.tsx` | Generated App Icon | -| [`apple-icon`](/docs/app/api-reference/file-conventions/metadata/app-icons#apple-icon) | `.jpg` `.jpeg`, `.png` | Apple App Icon file | +| [`apple-icon`](/docs/app/api-reference/file-conventions/metadata/app-icons#apple-icon) | `.jpg` `.jpeg` `.png` | Apple App Icon file | | [`apple-icon`](/docs/app/api-reference/file-conventions/metadata/app-icons#generate-icons-using-code-js-ts-tsx) | `.js` `.ts` `.tsx` | Generated Apple App Icon | #### Open Graph and Twitter images diff --git a/docs/01-app/01-getting-started/03-layouts-and-pages.mdx b/docs/01-app/01-getting-started/03-layouts-and-pages.mdx index f8472b31e294..de973e77efc3 100644 --- a/docs/01-app/01-getting-started/03-layouts-and-pages.mdx +++ b/docs/01-app/01-getting-started/03-layouts-and-pages.mdx @@ -287,10 +287,11 @@ You can use the [`` component](/docs/app/api-reference/components/link) to For example, to generate a list of blog posts, import `` from `next/link` and pass a `href` prop to the component: -```tsx filename="app/ui/post.tsx" highlight={1,10} switcher +```tsx filename="app/ui/post.tsx" highlight={1,2,11} switcher import Link from 'next/link' +import { getPosts } from '@/lib/posts' -export default async function Post({ post }) { +export default async function Posts() { const posts = await getPosts() return ( @@ -305,10 +306,11 @@ export default async function Post({ post }) { } ``` -```jsx filename="app/ui/post.js" highlight={1,10} switcher +```jsx filename="app/ui/post.js" highlight={1,2,11} switcher import Link from 'next/link' +import { getPosts } from '@/lib/posts' -export default async function Post({ post }) { +export default async function Posts() { const posts = await getPosts() return ( diff --git a/docs/01-app/01-getting-started/04-linking-and-navigating.mdx b/docs/01-app/01-getting-started/04-linking-and-navigating.mdx index 0e0bf63d8ea9..db8b8fd07e04 100644 --- a/docs/01-app/01-getting-started/04-linking-and-navigating.mdx +++ b/docs/01-app/01-getting-started/04-linking-and-navigating.mdx @@ -65,7 +65,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { ```jsx filename="app/layout.js" switcher import Link from 'next/link' -export default function Layout() { +export default function Layout({ children }) { return ( From 1bc1da711cd0699b620716b204b480485274ff05 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Thu, 28 May 2026 14:16:01 +0200 Subject: [PATCH 6/6] [CC] Streaming prerender (#94044) We want to be able to recover a shell from static and runtime prerenders. This means that prerenders need to become staged renders, so that we can collect the outputs as we progress through the stages. This is a first step before #93801, where we'll actually implement the staging We do this with a regular `renderToReadableStream` so we can get the results streamingly. We run the render, abort it, and omit any chunks emitted after the abort (because in a render, pending chunks become errored instead of halting). This logic is encapsulated in `collectPrerenderChunksWeb`, which also tracks if the stream is still pending. In order to get debug info for hanging promises in `--debug-prerender`, we also need to still collect the debug chunks emitted after the abort and give them to the client just before aborting (so that the debug info can be read, but nothing new can render). We already do this during the dev staged render, so we can just re-use the existing `createNodeStreamWithLateRelease` to achieve this. I've also removed the unecessary microtasks from stale time and vary params tracking. we need to wait an immediate for them to actually get written into the stream, and the awaits weren't doing anything useful. --- packages/next/errors.json | 3 +- .../next/src/server/app-render/app-render.tsx | 348 +++++++++++------- .../next/src/server/app-render/stale-time.ts | 21 -- .../next/src/server/app-render/vary-params.ts | 28 +- 4 files changed, 232 insertions(+), 168 deletions(-) diff --git a/packages/next/errors.json b/packages/next/errors.json index 20bff7b45e29..29c81c03a2ba 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -1284,5 +1284,6 @@ "1283": "Cache Components error recovery expected to know whether the original Flight prerender result was dynamic", "1284": "Cache Components error recovery expected an original prerender resume data cache", "1285": "Cache Components error recovery expected an original prerender store", - "1286": "Cache Components error recovery unexpectedly produced a dynamic HTML hole" + "1286": "Cache Components error recovery unexpectedly produced a dynamic HTML hole", + "1287": "Expected stream state to be initialized before reading" } diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 5413c1e4ec34..1e04d41f67d3 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -189,8 +189,8 @@ import { type OpaqueFallbackRouteParams, } from '../request/fallback-params' import { + ReactServerPrerenderResult, createReactServerPrerenderResult, - type ReactServerPrerenderResult, ReactServerResult, ReplayableNodeStream, createReactServerPrerenderResultFromRender, @@ -219,7 +219,6 @@ import { StaleTimeIterable, createSelectStaleTime, trackStaleTime, - finishStaleTimeTracking, } from './stale-time' import { HTML_CONTENT_TYPE_HEADER, INFINITE_CACHE } from '../../lib/constants' @@ -1029,11 +1028,11 @@ async function generateStagedDynamicFlightRenderResultWeb( }, () => { // This is a separate task that doesn't advance a stage. It forces - // draining the microtask queue so that the stale time iterable and vary - // params accumulators are closed before we advance to the dynamic stage. - void finishStaleTimeTracking(staleTimeIterable) + // draining the immediate queue so that the stale time iterable and vary + // params accumulators are flushed before we advance to the dynamic stage. + staleTimeIterable.close() if (requestStore.varyParamsAccumulator) { - void finishAccumulatingVaryParams(requestStore.varyParamsAccumulator) + finishAccumulatingVaryParams(requestStore.varyParamsAccumulator) } }, () => { @@ -1191,11 +1190,11 @@ async function generateStagedDynamicFlightRenderResultNode( }, () => { // This is a separate task that doesn't advance a stage. It forces - // draining the microtask queue so that the stale time iterable and vary - // params accumulators are closed before we advance to the dynamic stage. - void finishStaleTimeTracking(staleTimeIterable) + // draining the immediate queue so that the stale time iterable and vary + // params accumulators are flushed before we advance to the dynamic stage. + staleTimeIterable.close() if (requestStore.varyParamsAccumulator) { - void finishAccumulatingVaryParams(requestStore.varyParamsAccumulator) + finishAccumulatingVaryParams(requestStore.varyParamsAccumulator) } }, () => { @@ -1934,16 +1933,18 @@ async function finalRuntimeServerPrerender( getPayload ) - let prerenderIsPending = true - const result = await runInSequentialTasks( + const streamState = createStreamPendingState() + const collectedChunks = createPrerenderChunksAccumulator() + + await runInSequentialTasks( async () => { // EarlyStatic stage: render begins. // Runtime-prefetchable segments render immediately. // Non-prefetchable segments are gated until the Static stage. finalStageController.advanceStage(RenderStage.EarlyStatic) - const prerenderResult = await workUnitAsyncStorage.run( + const stream = workUnitAsyncStorage.run( finalServerPrerenderStore, - getServerPrerender(ComponentMod), + ComponentMod.renderToReadableStream, finalRSCPayload, clientModules, { @@ -1952,8 +1953,15 @@ async function finalRuntimeServerPrerender( signal: finalServerController.signal, } ) - prerenderIsPending = false - return prerenderResult + + // Note: this await will only resolve after the last task (unless sync IO aborts the render earlier) + // We await it here so that if the stream errors, it's not an unhandled rejection. + await collectPrerenderChunksWeb( + stream, + collectedChunks, + streamState, + finalServerController.signal + ) }, () => { // Advance to Static stage: resolve promise holding back @@ -1970,32 +1978,34 @@ async function finalRuntimeServerPrerender( // non-prefetchable segments. Sync IO is allowed here. finalStageController.advanceStage(RenderStage.Runtime) }, - () => { - Promise.all([ - finishStaleTimeTracking(staleTimeIterable), - finishAccumulatingVaryParams(varyParamsAccumulator), - ]).then(() => { - // Abort. This runs as a microtask after Flight has flushed the - // staleTime and varyParams closing chunks, but before the next - // macrotask resolves the overall result. - if (finalServerController.signal.aborted) { - // If the server controller is already aborted we must have called - // something that required aborting the prerender synchronously such - // as with new Date() - serverIsDynamic = true - return - } + async () => { + if (finalServerController.signal.aborted) { + // If the server controller is already aborted we must have called + // something that required aborting the prerender synchronously such + // as with new Date() + serverIsDynamic = true + return + } - if (prerenderIsPending) { - // If prerenderIsPending then we have blocked for longer than a Task - // and we assume there is something unfinished. - serverIsDynamic = true - } - finalServerController.abort() - }) + staleTimeIterable.close() + finishAccumulatingVaryParams(varyParamsAccumulator) + // We're using a render, not a prerender, so React schedules rendering work in fast immediates, + // and we need to wait a fast immediate for the stale time/vary params chunks to flush. + await waitAtLeastOneReactRenderTask() + + if (streamState.isPending) { + // If the prerender is still pending then it must depend on dynamic data. + serverIsDynamic = true + } + finalServerController.abort() } ) + const result = { + prelude: new ReactServerPrerenderResult( + collectedChunks.prerenderChunks + ).consumeAsStream(), + } result.prelude = prependIsPartialByte(result.prelude, serverIsDynamic) return { @@ -3790,14 +3800,11 @@ async function renderToStream( }, () => { // This is a separate task that doesn't advance a stage. It forces - // draining the microtask queue so that the stale time iterable and - // vary params accumulators are closed before we advance to the - // dynamic stage. - void finishStaleTimeTracking(staleTimeIterable) + // draining the immediate queue so that the stale time iterable and vary + // params accumulators are flushed before we advance to the dynamic stage. + staleTimeIterable.close() if (requestStore.varyParamsAccumulator) { - void finishAccumulatingVaryParams( - requestStore.varyParamsAccumulator - ) + finishAccumulatingVaryParams(requestStore.varyParamsAccumulator) } }, () => { @@ -3922,14 +3929,11 @@ async function renderToStream( }, () => { // This is a separate task that doesn't advance a stage. It forces - // draining the microtask queue so that the stale time iterable and - // vary params accumulators are closed before we advance to the - // dynamic stage. - void finishStaleTimeTracking(staleTimeIterable) + // draining the immediate queue so that the stale time iterable and vary + // params accumulators are flushed before we advance to the dynamic stage. + staleTimeIterable.close() if (requestStore.varyParamsAccumulator) { - void finishAccumulatingVaryParams( - requestStore.varyParamsAccumulator - ) + finishAccumulatingVaryParams(requestStore.varyParamsAccumulator) } }, () => { @@ -7720,7 +7724,7 @@ async function prerenderToStream( varyParamsAccumulator, } - const finalAttemptRSCPayload = await workUnitAsyncStorage.run( + const finalServerPayload = await workUnitAsyncStorage.run( finalServerPayloadPrerenderStore, getRSCPayload, tree, @@ -7731,7 +7735,7 @@ async function prerenderToStream( let staleTimeIterable: StaleTimeIterable | undefined if (cachedNavigations) { staleTimeIterable = new StaleTimeIterable() - finalAttemptRSCPayload.s = staleTimeIterable + finalServerPayload.s = staleTimeIterable } const serverDynamicTracking = createDynamicTrackingState( @@ -7769,80 +7773,93 @@ async function prerenderToStream( ) } - let prerenderIsPending = true - const finalRSCPrerenderOptions = { - filterStackFrame, - onError: (err: unknown) => { - return serverComponentsErrorHandler(err) - }, - signal: finalServerReactController.signal, - } - const finalRSCAbortCallback = async () => { - // Now that the prerendering is complete, we know the final stale - // time and vary params. Close the stale time iterable and resolve - // the vary params thenable so Flight can serialize their values - // into the stream. The timing here is important: both were - // included in the Flight payload, but they can only be serialized - // at the very end, after all the components have finished. - // - // We resolve these directly here instead of reading from the work - // unit store because this callback runs in a separate task (via - // setTimeout) and may not have access to the async storage context. - const pendingFinishes: Promise[] = [ - finishAccumulatingVaryParams(varyParamsAccumulator), - ] - if (staleTimeIterable !== undefined) { - pendingFinishes.push(finishStaleTimeTracking(staleTimeIterable)) - } - await Promise.all(pendingFinishes) + const streamState = createStreamPendingState() + const collectedChunks = createPrerenderChunksAccumulator() + let debugEndTime: number | undefined = undefined + + await runInSequentialTasks( + async () => { + if (process.env.NODE_ENV === 'development') { + // The end time should be tracked whenever we abort. + // We defensively do this before React runs its abort listener, + // although in practice this shouldn't matter. + finalServerReactController.signal.addEventListener( + 'abort', + () => { + debugEndTime = performance.timeOrigin + performance.now() + }, + { once: true } + ) + } - if (finalServerReactController.signal.aborted) { - // If the server controller is already aborted we must have called something - // that required aborting the prerender synchronously such as with new Date() - serverIsDynamic = true - return - } + const stream = workUnitAsyncStorage.run( + finalServerPrerenderStore, + ComponentMod.renderToReadableStream, + finalServerPayload, + clientModules, + { + filterStackFrame, + onError: (err: unknown) => { + return serverComponentsErrorHandler(err) + }, + signal: finalServerReactController.signal, + } + ) - if (prerenderIsPending) { - // If prerenderIsPending then we have blocked for longer than a Task and we assume - // there is something unfinished. - serverIsDynamic = true - } + // The listener to abort our own render controller must be added + // after React has added its listener, to ensure that pending I/O + // is not aborted/rejected too early. + finalServerReactController.signal.addEventListener( + 'abort', + () => { + finalServerRenderController.abort() + }, + { once: true } + ) - finalServerReactController.abort() - } - const finalRSCPrerenderFn = async () => { - const pendingPrerenderResult = workUnitAsyncStorage.run( - // The store to scope - finalServerPrerenderStore, - // The function to run - getServerPrerender(ComponentMod), - // ... the arguments for the function to run - finalAttemptRSCPayload, - clientModules, - finalRSCPrerenderOptions - ) + // Note: this await will only resolve after the last task (unless sync IO aborts the render earlier) + // We await it here so that if the stream errors, it's not an unhandled rejection. + await collectPrerenderChunksWeb( + stream, + collectedChunks, + streamState, + finalServerReactController.signal + ) + }, + async () => { + if (finalServerReactController.signal.aborted) { + // If the server controller is already aborted we must have called something + // that required aborting the prerender synchronously such as with new Date() + serverIsDynamic = true + return + } - // The listener to abort our own render controller must be added - // after React has added its listener, to ensure that pending I/O - // is not aborted/rejected too early. - finalServerReactController.signal.addEventListener( - 'abort', - () => { - finalServerRenderController.abort() - }, - { once: true } - ) + // Now that the prerendering is complete, we know the final stale + // time and vary params. Close the stale time iterable and resolve + // the vary params thenable so Flight can serialize their values + // into the stream. The timing here is important: both were + // included in the Flight payload, but they can only be serialized + // at the very end, after all the components have finished. + finishAccumulatingVaryParams(varyParamsAccumulator) + if (staleTimeIterable !== undefined) { + staleTimeIterable.close() + } + // We're using a render, not a prerender, so React schedules rendering work in fast immediates, + // and we need to wait a fast immediate for the stale time/vary params chunks to flush. + await waitAtLeastOneReactRenderTask() + + if (streamState.isPending) { + // If prerenderIsPending then we have blocked for longer than a Task and we assume + // there is something unfinished. + serverIsDynamic = true + } - const prerenderResult = await pendingPrerenderResult - prerenderIsPending = false + finalServerReactController.abort() + } + ) - return prerenderResult - } const reactServerResult = (reactServerPrerenderResult = - await createReactServerPrerenderResult( - runInSequentialTasks(finalRSCPrerenderFn, finalRSCAbortCallback) - )) + new ReactServerPrerenderResult(collectedChunks.prerenderChunks)) reactServerPrerenderResultIsDynamic = serverIsDynamic reactServerPrerenderStore = finalServerPrerenderStore @@ -7908,14 +7925,24 @@ async function prerenderToStream( let { prelude: unprocessedPrelude, postponed } = await runInSequentialTasks( () => { + const stream = + process.env.NODE_ENV === 'development' && + collectedChunks.allChunks + ? createNodeStreamWithLateRelease( + collectedChunks.prerenderChunks, + collectedChunks.allChunks, + finalClientReactController.signal + ) + : reactServerResult.asUnclosingStream() + const pendingFinalClientResult = workUnitAsyncStorage.run( finalClientPrerenderStore, getClientPrerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx , + chunks: PrerenderChunksAccumulator, + streamState: StreamPendingState, + signal: AbortSignal +): Promise { + const reader = stream.getReader() + streamState.isPending = true + + // In production, there's no debug info, so we don't need to capture + // anything emitted after the abort and can cancel immediately. + if (process.env.NODE_ENV !== 'development') { + signal.addEventListener( + 'abort', + () => { + reader.cancel(signal.reason) + }, + { once: true } + ) + } + + while (true) { + const { done, value } = await reader.read() + if (done) { + streamState.isPending = false + break + } + + if (!signal.aborted) { + chunks.prerenderChunks.push(value) + } + chunks.allChunks?.push(value) + } +} + const getGlobalErrorStyles = async ( tree: LoaderTree, ctx: AppRenderContext diff --git a/packages/next/src/server/app-render/stale-time.ts b/packages/next/src/server/app-render/stale-time.ts index 6759a64a6e58..564789a3c198 100644 --- a/packages/next/src/server/app-render/stale-time.ts +++ b/packages/next/src/server/app-render/stale-time.ts @@ -87,24 +87,3 @@ export function trackStaleTime( enumerable: true, }) } - -/** - * Closes the stale time iterable and waits for React to flush the closing - * chunk into the Flight stream. This also allows the prerender to complete if - * no other work is pending. - * - * Flight's internal work gets scheduled as a microtask when we close the - * iterable. We need to ensure Flight's pending queues are emptied before this - * function returns, because the caller will abort the prerender immediately - * after. We can't use a macrotask (that would allow dynamic IO to sneak into - * the response), so we use microtasks instead. The exact number of awaits - * isn't important as long as we wait enough ticks for Flight to finish writing. - */ -export async function finishStaleTimeTracking( - iterable: StaleTimeIterable -): Promise { - iterable.close() - await Promise.resolve() - await Promise.resolve() - await Promise.resolve() -} diff --git a/packages/next/src/server/app-render/vary-params.ts b/packages/next/src/server/app-render/vary-params.ts index 10fb58c3cf55..8c3bae371f29 100644 --- a/packages/next/src/server/app-render/vary-params.ts +++ b/packages/next/src/server/app-render/vary-params.ts @@ -335,10 +335,13 @@ export function createVaryingSearchParams( * root params. If we can't track vary params (e.g., legacy prerender), simply * don't call this function - the client treats unresolved thenables as * "unknown" vary params. + * + * NOTE: This function does not wait for the resulting flight chunks to flush. + * The appropriate wait time depends on whether we're using a prerender or a render. */ -export async function finishAccumulatingVaryParams( +export function finishAccumulatingVaryParams( responseAccumulator: ResponseVaryParamsAccumulator -): Promise { +): void { const rootVaryParams = responseAccumulator.rootParams.varyParams // Resolve head @@ -348,27 +351,6 @@ export async function finishAccumulatingVaryParams( for (const segmentAccumulator of responseAccumulator.segments) { finishSegmentAccumulator(segmentAccumulator, rootVaryParams) } - - // Now that the thenables are resolved, Flight should be able to flush the - // vary params into the response stream. This work gets scheduled internally - // by Flight using a microtask as soon as we notify the thenable listeners. - // - // We need to ensure that Flight's pending queues are emptied before this - // function returns; the caller will abort the prerender immediately after. - // We can't use a macrotask, because that would allow dynamic IO to sneak - // into the response. So we use microtasks instead. - // - // The exact number of awaits here isn't important (indeed, one seems to be - // sufficient, at the time of writing), as long as we wait enough ticks for - // Flight to finish writing the response. - // - // Anything that remains in Flight's internal queue after these awaits must - // be actual dynamic IO, not caused by pending vary params tasks. In other - // words, failing to do this would cause us to treat a fully static prerender - // as if it were partially dynamic. - await Promise.resolve() - await Promise.resolve() - await Promise.resolve() } function finishSegmentAccumulator(