diff --git a/Cargo.lock b/Cargo.lock
index 4a2ba8198d7b..6f2ca1fef51a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3169,6 +3169,8 @@ version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
+ "allocator-api2",
+ "equivalent",
"foldhash 0.1.5",
"serde",
]
diff --git a/lerna.json b/lerna.json
index 1abd07f907ce..6e082cd5b47f 100644
--- a/lerna.json
+++ b/lerna.json
@@ -15,5 +15,5 @@
"registry": "https://registry.npmjs.org/"
}
},
- "version": "16.4.0-canary.10"
+ "version": "16.4.0-canary.11"
}
\ No newline at end of file
diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json
index cf56b456fd6d..be3ae47823de 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.10",
+ "version": "16.4.0-canary.11",
"keywords": [
"react",
"next",
diff --git a/packages/devlow-bench/package.json b/packages/devlow-bench/package.json
index 293b6a02d0ae..c725443b1ec7 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.10",
+ "version": "16.4.0-canary.11",
"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 c03c631fc11e..3892c11c18ca 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.10",
+ "version": "16.4.0-canary.11",
"description": "ESLint configuration used by Next.js.",
"license": "MIT",
"repository": {
@@ -12,7 +12,7 @@
"dist"
],
"dependencies": {
- "@next/eslint-plugin-next": "16.4.0-canary.10",
+ "@next/eslint-plugin-next": "16.4.0-canary.11",
"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 187883fffc01..a13b9d3494c9 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.10",
+ "version": "16.4.0-canary.11",
"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 f8847e7785ff..7f30170aa0b6 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.10",
+ "version": "16.4.0-canary.11",
"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 b19dcdbcad2c..b264b04499d5 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.10",
+ "version": "16.4.0-canary.11",
"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 09409943ab50..8061e0975e16 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.10",
+ "version": "16.4.0-canary.11",
"main": "index.js",
"types": "index.d.ts",
"license": "MIT",
diff --git a/packages/next-codemod/bin/transform.ts b/packages/next-codemod/bin/transform.ts
index c8efc5aa532b..120cbb4d8766 100644
--- a/packages/next-codemod/bin/transform.ts
+++ b/packages/next-codemod/bin/transform.ts
@@ -80,7 +80,7 @@ export async function runTransform(
transformer = res.transformer
}
- if (transformer === 'next-request-geo-ip') {
+ if (transformer === 'next-request-geo-ip' && !options.nonInteractive) {
const { isAppDeployedToVercel } = await prompts(
{
type: 'confirm',
@@ -139,7 +139,7 @@ export async function runTransform(
args.push('--ignore-pattern=**/node_modules/**')
args.push('--ignore-pattern=**/.next/**')
- args.push('--extensions=tsx,ts,jsx,js')
+ args.push('--extensions=tsx,ts,jsx,js,mjs')
args = args.concat(['--transform', transformerPath])
diff --git a/packages/next-codemod/bin/upgrade.ts b/packages/next-codemod/bin/upgrade.ts
index a1b83f2bd9e2..cca79d83bbc0 100644
--- a/packages/next-codemod/bin/upgrade.ts
+++ b/packages/next-codemod/bin/upgrade.ts
@@ -447,7 +447,11 @@ export async function runUpgrade(
runInstallation(packageManager, { cwd })
for (const codemod of codemods) {
- await runTransform(codemod, cwd, { force: true, verbose })
+ await runTransform(codemod, cwd, {
+ force: true,
+ verbose,
+ nonInteractive,
+ })
}
// To reduce user-side burden of selecting which codemods to run as it needs additional
diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json
index bd5c6dc82cfd..e10068efe64b 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.10",
+ "version": "16.4.0-canary.11",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/packages/next-env/package.json b/packages/next-env/package.json
index 50daed34841d..d38cb2c0d12b 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.10",
+ "version": "16.4.0-canary.11",
"keywords": [
"react",
"next",
diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json
index 4e19075d1408..99868f6b87c5 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.10",
+ "version": "16.4.0-canary.11",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json
index 9656d7411628..fda7cff10352 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.10",
+ "version": "16.4.0-canary.11",
"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 f3f3ddc055f0..6dfb79810ebc 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.10",
+ "version": "16.4.0-canary.11",
"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 27bbfee20b09..46b933008304 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.10",
+ "version": "16.4.0-canary.11",
"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 be939f2fde28..27e80379e5da 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.10",
+ "version": "16.4.0-canary.11",
"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 b93ef8b3eb46..af6e684a14e8 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.10",
+ "version": "16.4.0-canary.11",
"keywords": [
"react",
"next",
diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json
index 560c76396d82..457162ce6a1f 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.10",
+ "version": "16.4.0-canary.11",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-rspack"
diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json
index 59286d533d02..f78f4a55d8b4 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.10",
+ "version": "16.4.0-canary.11",
"private": true,
"files": [
"native/"
diff --git a/packages/next/package.json b/packages/next/package.json
index 321ba3dc6367..a7d94c1770e0 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "16.4.0-canary.10",
+ "version": "16.4.0-canary.11",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -100,7 +100,7 @@
]
},
"dependencies": {
- "@next/env": "16.4.0-canary.10",
+ "@next/env": "16.4.0-canary.11",
"@swc/helpers": "0.5.23",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -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.10",
- "@next/polyfill-module": "16.4.0-canary.10",
- "@next/polyfill-nomodule": "16.4.0-canary.10",
- "@next/react-refresh-utils": "16.4.0-canary.10",
- "@next/swc": "16.4.0-canary.10",
+ "@next/font": "16.4.0-canary.11",
+ "@next/polyfill-module": "16.4.0-canary.11",
+ "@next/polyfill-nomodule": "16.4.0-canary.11",
+ "@next/react-refresh-utils": "16.4.0-canary.11",
+ "@next/swc": "16.4.0-canary.11",
"@opentelemetry/api": "1.6.0",
"@playwright/test": "1.61.0",
"@rspack/core": "1.6.7",
diff --git a/packages/next/src/client/components/segment-cache/optimistic-routes.ts b/packages/next/src/client/components/segment-cache/optimistic-routes.ts
index 6630b903f6eb..d5245d828be4 100644
--- a/packages/next/src/client/components/segment-cache/optimistic-routes.ts
+++ b/packages/next/src/client/components/segment-cache/optimistic-routes.ts
@@ -921,7 +921,7 @@ function matchKnownRoutePart(
) {
resolvedParams.set(
paramName,
- pathnameParts.slice(partIndex).join('/')
+ pathnameParts.slice(partIndex).map(canonicalizeURLPart).join('/')
)
return { part: dynamicPart, pattern: dynamicPattern }
}
@@ -932,7 +932,7 @@ function matchKnownRoutePart(
if (urlPart !== null) {
resolvedParams.set(
paramName,
- pathnameParts.slice(partIndex).join('/')
+ pathnameParts.slice(partIndex).map(canonicalizeURLPart).join('/')
)
return { part: dynamicPart, pattern: dynamicPattern }
}
@@ -951,7 +951,7 @@ function matchKnownRoutePart(
// Unlike catch-all which terminates here, regular dynamic must
// continue recursing to find the leaf pattern.
if (urlPart !== null) {
- resolvedParams.set(paramName, urlPart)
+ resolvedParams.set(paramName, canonicalizeURLPart(urlPart))
return matchKnownRoutePart(
now,
dynamicPart,
diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index 27e19ac968fe..7329af702e90 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -1598,17 +1598,6 @@ async function generateRuntimePrefetchResult(
// but we're not going to persist this anywhere.
const prerenderResumeDataCache = createPrerenderResumeDataCache()
- await prospectiveRuntimeServerPrerender(
- ctx,
- isShellPrefetch,
- generateDynamicRSCPayload.bind(null, ctx),
- prerenderResumeDataCache,
- rootParams,
- requestStore.headers,
- requestStore.cookies,
- requestStore.draftMode
- )
-
const mode: RuntimePrerenderMode = isShellPrefetch
? {
type: 'session-shell-only',
@@ -1620,6 +1609,17 @@ async function generateRuntimePrefetchResult(
shellByteLengthDeferred: createPromiseWithResolvers(),
}
+ await prospectiveRuntimeServerPrerender(
+ ctx,
+ getFinalStageForRuntimePrerenderMode(mode),
+ generateDynamicRSCPayload.bind(null, ctx),
+ prerenderResumeDataCache,
+ rootParams,
+ requestStore.headers,
+ requestStore.cookies,
+ requestStore.draftMode
+ )
+
const debugChannel = setReactDebugChannel
? createWebDebugChannel()
: undefined
@@ -1656,7 +1656,7 @@ async function generateRuntimePrefetchResult(
async function prospectiveRuntimeServerPrerender(
ctx: AppRenderContext,
- isShellPrefetch: boolean,
+ finalStage: AdvanceableRenderStage,
getPayload: () => Promise,
resumeDataCache: PrerenderResumeDataCache | null,
rootParams: Params,
@@ -1705,9 +1705,10 @@ async function prospectiveRuntimeServerPrerender(
hmrRefreshHash: undefined,
// We don't track vary params during initial prerender, only the final one
varyParamsAccumulator: null,
- // No stage sequencing needed for prospective renders.
+ // We don't use sequencing needed for prospective renders, but we need to
+ // track where the final prerender is going to end to avoid warming too much.
stagedRendering: null,
- isSessionShell: isShellPrefetch,
+ finalStage,
// These are not present in regular prerenders, but allowed in a runtime
// prerender.
// Any cache keyed on headers() or cookies() needs to be invalidated.
@@ -1854,19 +1855,8 @@ async function finalRuntimeServerPrerender(
isDebugDynamicAccesses
)
- let finalStage: AdvanceableRenderStage
- switch (mode.type) {
- case 'session-shell-only':
- finalStage = RenderStage.ShellRuntime
- break
- case 'rewindable-session-shell':
- finalStage = RenderStage.Runtime
- break
- case 'navigation':
- finalStage = RenderStage.NavigationRuntime
- break
- }
- const finalStageController = new StagedRenderingController({
+ const finalStage = getFinalStageForRuntimePrerenderMode(mode)
+ const stageController = new StagedRenderingController({
abortSignal: finalServerController.signal,
abandonController: null,
// In dynamic renders, we allow Sync IO in the Runtime stage
@@ -1898,8 +1888,8 @@ async function finalRuntimeServerPrerender(
resumeDataCache,
hmrRefreshHash: undefined,
varyParamsAccumulator,
- stagedRendering: finalStageController,
- isSessionShell: mode.type === 'session-shell-only',
+ stagedRendering: stageController,
+ finalStage,
// These are not present in regular prerenders, but allowed in a runtime
// prerender.
headers: HeadersAdapter.fresh(headers),
@@ -1923,7 +1913,7 @@ async function finalRuntimeServerPrerender(
collectPrerenderChunk(collectedChunks, finalServerController.signal, chunk)
increaseChunkByteLengths(
stageByteLengths,
- finalStageController.currentStage,
+ stageController.currentStage,
chunk.byteLength
)
}
@@ -1963,7 +1953,7 @@ async function finalRuntimeServerPrerender(
await runInSequentialTasks(
async () => {
- finalStageController.advanceStage(RenderStage.ShellStatic)
+ stageController.advanceStage(RenderStage.ShellStatic)
let stream = workUnitAsyncStorage.run(
finalServerPrerenderStore,
@@ -1989,19 +1979,19 @@ async function finalRuntimeServerPrerender(
},
() => {
if (checkUnexpectedAbort()) return
- finalStageController.advanceStage(RenderStage.PrefetchStatic)
+ stageController.advanceStage(RenderStage.PrefetchStatic)
},
() => {
if (checkUnexpectedAbort()) return
- finalStageController.advanceStage(RenderStage.NavigationStatic)
+ stageController.advanceStage(RenderStage.NavigationStatic)
},
() => {
if (checkUnexpectedAbort()) return
- finalStageController.advanceStage(RenderStage.Static)
+ stageController.advanceStage(RenderStage.Static)
},
() => {
if (checkUnexpectedAbort()) return
- finalStageController.advanceStage(RenderStage.ShellRuntime)
+ stageController.advanceStage(RenderStage.ShellRuntime)
},
() => {
if (checkUnexpectedAbort()) return
@@ -2009,7 +1999,7 @@ async function finalRuntimeServerPrerender(
// We may not reach this stage depending on the mode.
if (finalStage < RenderStage.Runtime) return
- finalStageController.advanceStage(RenderStage.Runtime)
+ stageController.advanceStage(RenderStage.Runtime)
},
() => {
if (checkUnexpectedAbort()) return
@@ -2040,7 +2030,7 @@ async function finalRuntimeServerPrerender(
// We may not reach this stage depending on the mode.
if (finalStage < RenderStage.NavigationRuntime) return
- finalStageController.advanceStage(RenderStage.NavigationRuntime)
+ stageController.advanceStage(RenderStage.NavigationRuntime)
},
() => {
// Finish the accumulators. We need to wait for Flight to flush the result into the stream,
@@ -2090,6 +2080,19 @@ async function finalRuntimeServerPrerender(
}
}
+function getFinalStageForRuntimePrerenderMode(
+ mode: RuntimePrerenderMode
+): AdvanceableRenderStage {
+ switch (mode.type) {
+ case 'session-shell-only':
+ return RenderStage.ShellRuntime
+ case 'rewindable-session-shell':
+ return RenderStage.Runtime
+ case 'navigation':
+ return RenderStage.NavigationRuntime
+ }
+}
+
/**
* Crawlers will inadvertently think the canonicalUrl in the RSC payload should be crawled
* when our intention is to just seed the router state with the current URL.
diff --git a/packages/next/src/server/app-render/work-unit-async-storage.external.ts b/packages/next/src/server/app-render/work-unit-async-storage.external.ts
index 69b35b95902f..7674db24fead 100644
--- a/packages/next/src/server/app-render/work-unit-async-storage.external.ts
+++ b/packages/next/src/server/app-render/work-unit-async-storage.external.ts
@@ -16,7 +16,10 @@ import type { Params } from '../request/params'
import type { ImplicitTags } from '../lib/implicit-tags'
import type { WorkStore } from './work-async-storage.external'
import { InvariantError } from '../../shared/lib/invariant-error'
-import type { StagedRenderingController } from './staged-rendering'
+import type {
+ AdvanceableRenderStage,
+ StagedRenderingController,
+} from './staged-rendering'
import type { ValidationBoundaryTracking } from './instant-validation/boundary-tracking'
import type { InstantValidationSampleTracking } from './instant-validation/instant-samples'
@@ -226,7 +229,7 @@ export interface PrerenderStoreModernRuntime
* renders where all stages run without sequencing.
*/
readonly stagedRendering: StagedRenderingController | null
- readonly isSessionShell: boolean
+ readonly finalStage: AdvanceableRenderStage
readonly headers: RequestStore['headers']
readonly cookies: RequestStore['cookies']
diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts
index 6438eafaf1da..01803ef268f9 100644
--- a/packages/next/src/server/lib/router-utils/resolve-routes.ts
+++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts
@@ -888,12 +888,17 @@ export function getResolveRoutes(
(!parsedDestination.origin || isAllowedOrigin)
) {
// We set the rewritten path and query headers on the response now
- // that we know that the it's not an external rewrite.
+ // that we know that the it's not an external rewrite. Mirror the
+ // path in resHeaders because those are applied after route
+ // resolution; otherwise, an earlier Proxy rewrite would overwrite
+ // the final route destination.
if (parsedUrl.pathname !== parsedDestination.pathname) {
res.setHeader(
NEXT_REWRITTEN_PATH_HEADER,
parsedDestination.pathname
)
+ resHeaders[NEXT_REWRITTEN_PATH_HEADER] =
+ parsedDestination.pathname
}
if (parsedUrl.search !== parsedDestination.search) {
res.setHeader(
diff --git a/packages/next/src/server/request/cache-stages.ts b/packages/next/src/server/request/cache-stages.ts
index b1ebfb99265e..2850ecacf0f3 100644
--- a/packages/next/src/server/request/cache-stages.ts
+++ b/packages/next/src/server/request/cache-stages.ts
@@ -5,6 +5,7 @@ import {
} from '../app-render/work-unit-async-storage.external'
import {
applyOwnerStack,
+ makeUntrackedHangingPromise,
RENDER_STAGES_BY_DATA_KIND,
trackIncompatibleShellContent,
} from '../dynamic-rendering-utils'
@@ -52,6 +53,7 @@ export function unstable_prefetch(): Promise {
const { stagedRendering } = workUnitStore
if (!stagedRendering) {
// Prospective prerender
+ // `unstable_prefetch()` will resolve in the final prerender, so resolve it here as well.
return Promise.resolve(undefined)
} else {
// Final prerender
@@ -69,13 +71,23 @@ export function unstable_prefetch(): Promise {
// Note that this does not mark the subtree as dynamic -- content guarded by
// prefetch() is still considered cacheable.
const { stagedRendering } = workUnitStore
+ const prefetchStage = RENDER_STAGES_BY_DATA_KIND.runtimeLinkData
if (!stagedRendering) {
// Prospective prerender
- return Promise.resolve(undefined)
+ // Make sure we don't unblock content that won't be reached in the final prerender.
+ if (workUnitStore.finalStage < prefetchStage) {
+ return makeUntrackedHangingPromise(
+ workUnitStore.renderSignal,
+ workStore.route,
+ '`unstable_prefetch()`'
+ )
+ } else {
+ return Promise.resolve(undefined)
+ }
} else {
// Final prerender
return stagedRendering.delayUntilStage(
- RENDER_STAGES_BY_DATA_KIND.runtimeLinkData,
+ prefetchStage,
'unstable_prefetch',
undefined
)
@@ -194,6 +206,7 @@ export function unstable_navigation(): Promise {
const { stagedRendering } = workUnitStore
if (!stagedRendering) {
// Prospective prerender
+ // `unstable_navigation()` will resolve in the final prerender, so resolve it here as well.
return Promise.resolve(undefined)
} else {
// Final prerender
@@ -211,13 +224,23 @@ export function unstable_navigation(): Promise {
// Note that this does not mark the subtree as dynamic -- content guarded by
// navigation() is still considered cacheable.
const { stagedRendering } = workUnitStore
+ const navigationStage = RenderStage.NavigationRuntime
if (!stagedRendering) {
// Prospective prerender
- return Promise.resolve(undefined)
+ // Make sure we don't unblock content that won't be reached in the final prerender.
+ if (workUnitStore.finalStage < navigationStage) {
+ return makeUntrackedHangingPromise(
+ workUnitStore.renderSignal,
+ workStore.route,
+ '`unstable_navigation()`'
+ )
+ } else {
+ return Promise.resolve(undefined)
+ }
} else {
// Final prerender
return stagedRendering.delayUntilStage(
- RenderStage.NavigationRuntime,
+ navigationStage,
'unstable_navigation',
undefined
)
diff --git a/packages/next/src/server/request/params.ts b/packages/next/src/server/request/params.ts
index 9e01ada7c870..d8e417ceca77 100644
--- a/packages/next/src/server/request/params.ts
+++ b/packages/next/src/server/request/params.ts
@@ -427,36 +427,27 @@ function createRuntimePrerenderParams(
return makeUntrackedParams(userspaceParams)
}
+ // Root params are allowed in shells, so we allow them to resolve without a delay.
+ if (allParamsAreRootParams(underlyingParams, workUnitStore.rootParams)) {
+ return makeUntrackedParams(userspaceParams)
+ }
+
+ // Non-root params are URL data, and we need to recover a param-less session shell,
+ // so we delay all params until the runtime stage (even if they're static)
+ const paramsStage = RENDER_STAGES_BY_DATA_KIND.runtimeLinkData
+
const { stagedRendering } = workUnitStore
if (!stagedRendering) {
- // If there's no staging, we're in a prospective runtime prerender.
- if (workUnitStore.isSessionShell) {
- // If we're warming up for a session shell, params should be hanging,
- // because they'll be a hanging input in the final prerender.
+ // If there's no stage controller, we're in a prospective runtime prerender.
+ // Make sure we don't unblock content that won't be reached in the final prerender.
+ if (workUnitStore.finalStage < paramsStage) {
return makeHangingParams(underlyingParams, workStore, workUnitStore)
} else {
return makeUntrackedParams(userspaceParams)
}
}
- // We don't have fallbackParams in runtime prerenders, so we don't know
- // when params are static. However, root params are static by definition,
- // so we can at least check for that.
- // Note that resolving them without a delay is valid because root params are
- // allowed in shells.
- if (allParamsAreRootParams(underlyingParams, workUnitStore.rootParams)) {
- return makeUntrackedParams(userspaceParams)
- }
-
- // Semantically, we should resolve static params in the static stage.
- // But params are link data, and we need to recover a param-less session shell,
- // so we delay all params until the runtime stage instead.
- const staticParamsStage = RENDER_STAGES_BY_DATA_KIND.runtimeLinkData
- return stagedRendering.delayUntilStage(
- staticParamsStage,
- 'params',
- userspaceParams
- )
+ return stagedRendering.delayUntilStage(paramsStage, 'params', userspaceParams)
}
function createRenderParamsForPage(
diff --git a/packages/next/src/server/request/pathname.ts b/packages/next/src/server/request/pathname.ts
index dd78fea607ea..167b14d1c20c 100644
--- a/packages/next/src/server/request/pathname.ts
+++ b/packages/next/src/server/request/pathname.ts
@@ -57,15 +57,15 @@ export function createServerPathnameForMetadata(
// behavior of always resolving in the runtime stage
// (i.e. assuming that we have non-static params in the pathname)
const { stagedRendering } = workUnitStore
+ const pathnameStage = RENDER_STAGES_BY_DATA_KIND.runtimeLinkData
if (stagedRendering) {
- const pathnameStage = RENDER_STAGES_BY_DATA_KIND.runtimeLinkData
return stagedRendering.delayUntilStage(
pathnameStage,
undefined,
underlyingPathname
)
} else {
- if (workUnitStore.isSessionShell) {
+ if (workUnitStore.finalStage < pathnameStage) {
return makeDynamicHangingPromise(
workUnitStore.renderSignal,
workStore.route,
diff --git a/packages/next/src/server/request/search-params.ts b/packages/next/src/server/request/search-params.ts
index 3f0760fdae4c..ce5d9688de2f 100644
--- a/packages/next/src/server/request/search-params.ts
+++ b/packages/next/src/server/request/search-params.ts
@@ -245,16 +245,20 @@ function createRuntimePrerenderSearchParams(
: underlyingSearchParams
const result = makeUntrackedSearchParams(userspaceSearchParams)
+
+ const searchParamsStage = RENDER_STAGES_BY_DATA_KIND.runtimeLinkData
+
const { stagedRendering } = workUnitStore
if (!stagedRendering) {
- // If there's no staging, we're in a prospective runtime prerender.
- if (workUnitStore.isSessionShell) {
- // If we're warming up for a session shell, search params should hang,
- // because they'll be a hanging input in the final prerender.
+ // If there's no stage controller, we're in a prospective runtime prerender.
+ // Make sure we don't unblock content that won't be reached in the final prerender.
+ if (workUnitStore.finalStage < searchParamsStage) {
return makeHangingSearchParams(workStore, workUnitStore)
+ } else {
+ return result
}
- return result
}
+
// Unlike `createRuntimePrerenderParams`, which uses `delayUntilStage`, we
// resolve with `waitForStage(...).then(...)` here. Switching search params to
// `delayUntilStage` drops the source code frame from the instant-validation
@@ -264,7 +268,6 @@ function createRuntimePrerenderSearchParams(
// `suspense-boundaries` tests. The underlying reason in React's async I/O
// await tracking isn't understood yet. TODO: align search params with params
// on `delayUntilStage` once resolved.
- const searchParamsStage = RENDER_STAGES_BY_DATA_KIND.runtimeLinkData
return stagedRendering.waitForStage(searchParamsStage).then(() => result)
}
@@ -382,9 +385,9 @@ function makeHangingSearchParams(
prerenderStore.renderSignal,
workStore.route,
'`searchParams`',
- // This promise is created for every page whether or not it reads search
- // params, so recording the access at creation would mark every render.
- // The access is tracked in the proxy traps below instead.
+ // Passing `null` for the store disables tracking of params usage.
+ // We want accesses of chained promises to be tracked as well.
+ // TODO: The custom tracking seems unnecessary, we should standardize it
null
)
diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json
index 3df93e3f67e7..be8d7f6c8519 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.10",
+ "version": "16.4.0-canary.11",
"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 7d450309beea..62c8e74571ed 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.10",
+ "version": "16.4.0-canary.11",
"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.10",
+ "next": "16.4.0-canary.11",
"outdent": "0.8.0",
"prettier": "2.5.1",
"typescript": "6.0.2"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0ae4a6114100..23d9b1fc06dd 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.10
+ specifier: 16.4.0-canary.11
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.10
+ specifier: 16.4.0-canary.11
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.10
+ specifier: 16.4.0-canary.11
version: link:../font
'@next/polyfill-module':
- specifier: 16.4.0-canary.10
+ specifier: 16.4.0-canary.11
version: link:../next-polyfill-module
'@next/polyfill-nomodule':
- specifier: 16.4.0-canary.10
+ specifier: 16.4.0-canary.11
version: link:../next-polyfill-nomodule
'@next/react-refresh-utils':
- specifier: 16.4.0-canary.10
+ specifier: 16.4.0-canary.11
version: link:../react-refresh-utils
'@next/swc':
- specifier: 16.4.0-canary.10
+ specifier: 16.4.0-canary.11
version: link:../next-swc
'@opentelemetry/api':
specifier: 1.6.0
@@ -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.10
+ specifier: 16.4.0-canary.11
version: link:../next
outdent:
specifier: 0.8.0
@@ -21870,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
@@ -21885,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'
diff --git a/test/e2e/app-dir/interception-dynamic-segment-middleware/interception-dynamic-segment-middleware.test.ts b/test/e2e/app-dir/interception-dynamic-segment-middleware/interception-dynamic-segment-middleware.test.ts
index 53e4dbdf4dc0..c8cb97cce9a6 100644
--- a/test/e2e/app-dir/interception-dynamic-segment-middleware/interception-dynamic-segment-middleware.test.ts
+++ b/test/e2e/app-dir/interception-dynamic-segment-middleware/interception-dynamic-segment-middleware.test.ts
@@ -65,4 +65,22 @@ describe('interception-dynamic-segment-middleware', () => {
})
}
})
+
+ it('should preserve the final interception rewrite after a middleware rewrite', async () => {
+ let rewrittenPath: string | undefined
+ const browser = await next.browser('/', {
+ beforePageLoad(page) {
+ page.on('response', (response) => {
+ if (new URL(response.url()).pathname === '/foo/p/1') {
+ rewrittenPath = response.headers()['x-nextjs-rewritten-path']
+ }
+ })
+ },
+ })
+
+ await browser.elementByCss('[href="/foo/p/1"]').click()
+ await retry(() => {
+ expect(rewrittenPath).toBe('/en/(.)foo/p/1')
+ })
+ })
})
diff --git a/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/app/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/app/page.tsx
deleted file mode 100644
index 78e4e16658da..000000000000
--- a/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/app/page.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { LinkAccordion } from '../components/link-accordion'
-
-export default function Page() {
- return (
-
-
- -
-
- prerendered from gSP - /slug/prerendered
-
-
- -
-
- not prerendered - /slug/not-prerendered
-
-
-
-
- )
-}
diff --git a/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/prefetch-app-shell-cached-gsp.test.ts b/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/prefetch-app-shell-cached-gsp.test.ts
deleted file mode 100644
index a28caeeb88b3..000000000000
--- a/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/prefetch-app-shell-cached-gsp.test.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { nextTestSetup } from 'e2e-utils'
-import type * as Playwright from 'playwright'
-import { createRouterAct } from 'router-act'
-import { retry } from 'next-test-utils'
-
-const CACHE_MISS_WARNING = 'Unexpected cache miss after cache warming phase'
-
-// The App Shell prefetch (next-router-prefetch: '3') renders the runtime shell
-// of the /slug/[slug] route. Because the page is a `'use cache'` page that
-// awaits static params, params are a hanging input in the final prerender. If
-// the prospective (cache-warming) prerender resolved those params instead of
-// leaving them hanging, the cached page's key would differ between the two
-// prerenders and the final prerender would log an "Unexpected cache miss"
-// warning and degrade the cached segment to a dynamic hole.
-describe('App Shell prefetching - cached page with generateStaticParams', () => {
- const { next, isNextDev } = nextTestSetup({
- files: __dirname,
- })
- if (isNextDev) {
- it('is skipped', () => {})
- return
- }
-
- it('does not report a cache miss when prefetching the shell of a cached page that reads static params', async () => {
- let page: Playwright.Page
- const browser = await next.browser('/', {
- beforePageLoad(p: Playwright.Page) {
- page = p
- },
- })
- const act = createRouterAct(page, { includeAppShellRequests: true })
-
- // Revealing the link prefetches the App Shell for /slug/[slug]. Because the
- // shell is param-independent, this single request drives the runtime shell
- // prerender of the cached page whose params are a hanging input.
- await act(async () => {
- await browser
- .elementByCss('input[data-link-accordion="/slug/prerendered"]')
- .click()
- })
-
- expect(next.cliOutput).not.toContain(CACHE_MISS_WARNING)
- })
-
- it('renders the cached page content after navigating to a prefetched shell', async () => {
- let page: Playwright.Page
- const browser = await next.browser('/', {
- beforePageLoad(p: Playwright.Page) {
- page = p
- },
- })
- const act = createRouterAct(page, { includeAppShellRequests: true })
-
- await act(async () => {
- await browser
- .elementByCss('input[data-link-accordion="/slug/prerendered"]')
- .click()
- })
-
- await browser.elementByCss('a[href="/slug/prerendered"]').click()
-
- await retry(async () => {
- expect(await browser.elementById('slug').text()).toEqual(
- 'Slug: prerendered'
- )
- })
-
- expect(next.cliOutput).not.toContain(CACHE_MISS_WARNING)
- })
-})
diff --git a/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/app/layout.tsx b/test/e2e/app-dir/segment-cache/runtime-prerender-cache-warming/app/layout.tsx
similarity index 57%
rename from test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/app/layout.tsx
rename to test/e2e/app-dir/segment-cache/runtime-prerender-cache-warming/app/layout.tsx
index fa1546fe2d01..3e61d2fa2638 100644
--- a/test/e2e/app-dir/segment-cache/prefetch-app-shell-cached-gsp/app/layout.tsx
+++ b/test/e2e/app-dir/segment-cache/runtime-prerender-cache-warming/app/layout.tsx
@@ -2,12 +2,10 @@ import { Suspense } from 'react'
import { cookies } from 'next/headers'
// Reading a cookie makes the shell depend on session data, so a prefetch
-// renders a runtime App Shell. This is what exercises the prospective/final
-// runtime prerender where params are a hanging input in a cached page.
+// renders a runtime App Shell.
async function SessionData() {
- const cookieStore = await cookies()
- const value = cookieStore.get('testCookie')?.value ?? 'none'
- return {`Cookie: ${value}`}
+ await cookies()
+ return null
}
export default async function Layout({ children }: LayoutProps<'/'>) {
@@ -17,7 +15,7 @@ export default async function Layout({ children }: LayoutProps<'/'>) {
Loading cookie...
}>
- {children}
+ {children}