From 192fcaf789b81812f09eefdf0af9763b1706b11e Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 21 Sep 2026 07:46:42 -0700 Subject: [PATCH 1/5] Generalize the build-time generator work-unit store name (#98891) ## Summary Rename the internal `GenerateStaticParamsStore` work-unit context to `BuildTimeGeneratorStore`, with discriminant `build-time-generator`. Carry the public export name in a required `functionName` field so shared error paths identify the generator that is actually running. This is a behavior-preserving preparatory refactor. `generateStaticParams` remains the only producer. Every consumer retains the same branch, so public-cache support, unavailable request APIs, inherited root parameters, and existing error messages are unchanged. The name describes a build-time generator's execution context rather than one function. For example, another generator that loads configuration through a cached helper should be able to reuse this context without introducing another work-unit variant throughout the framework. Function-specific contexts can be added later if their semantics actually diverge. For example, a generator that calls `headers()` needs an error naming that generator, not an error that always says `generateStaticParams`. The field is set explicitly rather than read from `Function.name`, which may change during bundling. Request APIs, private-cache rejection, revalidation, and rendering invariants use this name. Missing-root-param advice about parent gSP calls remains specific to gSP; other generators report that the value is unavailable in their own scope. This independent prerequisite for #97393 introduces no API or feature flag. The API-specific initializer and its error fixtures live in #97393, so this PR remains independently landable and behavior-preserving for existing gSP users. ## Verification - Existing `generate-static-params-errors`, `app-root-params-getters/generate-static-params`, and `generate-static-params-error` fixtures: all ten production Webpack tests pass, including unchanged error strings and parent root-param behavior. - Existing `use-cache/use-cache.test.ts` static-HTML assertion passes, exercising cached helpers called from gSP in both a page and route handler. - Full bootstrap and TypeScript declaration build pass. --- packages/next/src/build/static-paths/app.ts | 7 +-- .../client/components/handle-isr-error.tsx | 2 +- .../src/client/components/instant-samples.ts | 6 +-- .../client/components/navigation-untracked.ts | 2 +- .../next/src/server/app-render/app-render.tsx | 2 +- .../app-render/create-component-tree.tsx | 4 +- .../app-render/dynamic-rendering-client.ts | 8 ++-- .../server/app-render/dynamic-rendering.ts | 8 ++-- .../next/src/server/app-render/encryption.ts | 2 +- .../instant-validation/boundary-impl.tsx | 2 +- .../instant-validation/instant-samples.ts | 2 +- .../server/app-render/manifests-singleton.ts | 2 +- .../server/app-render/use-flight-response.tsx | 2 +- .../work-unit-async-storage.external.ts | 26 +++++------ .../src/server/dynamic-rendering-utils.ts | 4 +- packages/next/src/server/lib/patch-fetch.ts | 20 ++++----- .../console-dim.external.tsx | 2 +- .../node-environment-extensions/io-utils.tsx | 2 +- .../unhandled-rejection.external.tsx | 2 +- .../src/server/og/cache-image-response.ts | 2 +- .../next/src/server/request/cache-stages.ts | 8 ++-- .../next/src/server/request/connection.ts | 4 +- packages/next/src/server/request/cookies.ts | 4 +- .../next/src/server/request/draft-mode.ts | 8 ++-- packages/next/src/server/request/headers.ts | 4 +- packages/next/src/server/request/io.ts | 2 +- packages/next/src/server/request/params.ts | 16 +++---- packages/next/src/server/request/pathname.ts | 4 +- .../next/src/server/request/root-params.ts | 7 ++- .../next/src/server/request/search-params.ts | 12 +++--- .../server/route-modules/app-route/module.ts | 2 +- .../next/src/server/use-cache/cache-life.ts | 2 +- .../next/src/server/use-cache/cache-tag.ts | 2 +- .../server/use-cache/use-cache-messages.ts | 10 +++-- .../src/server/use-cache/use-cache-wrapper.ts | 43 ++++++++++--------- .../server/web/spec-extension/revalidate.ts | 23 +++++++++- .../web/spec-extension/unstable-cache.ts | 4 +- .../web/spec-extension/unstable-no-store.ts | 2 +- 38 files changed, 149 insertions(+), 115 deletions(-) diff --git a/packages/next/src/build/static-paths/app.ts b/packages/next/src/build/static-paths/app.ts index 6f7186aabcba..e4f6d8b764f7 100644 --- a/packages/next/src/build/static-paths/app.ts +++ b/packages/next/src/build/static-paths/app.ts @@ -34,7 +34,7 @@ import type { NormalizedAppRoute } from '../../shared/lib/router/routes/app' import { interceptionPrefixFromParamType } from '../../shared/lib/router/utils/interception-prefix-from-param-type' import { isPlainObject } from '../../shared/lib/is-plain-object' import { - type GenerateStaticParamsStore, + type BuildTimeGeneratorStore, workUnitAsyncStorage, } from '../../server/app-render/work-unit-async-storage.external' import type { ImplicitTags } from '../../server/lib/implicit-tags' @@ -625,8 +625,9 @@ async function callGenerateStaticParams( } } - const workUnitStore: GenerateStaticParamsStore = { - type: 'generate-static-params', + const workUnitStore: BuildTimeGeneratorStore = { + type: 'build-time-generator', + functionName: 'generateStaticParams', phase: 'render', implicitTags, rootParams, diff --git a/packages/next/src/client/components/handle-isr-error.tsx b/packages/next/src/client/components/handle-isr-error.tsx index 9e2b45efdc9b..67e70f5bec54 100644 --- a/packages/next/src/client/components/handle-isr-error.tsx +++ b/packages/next/src/client/components/handle-isr-error.tsx @@ -23,7 +23,7 @@ export function handleISRError({ error }: { error: any }) { case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': case undefined: return default: diff --git a/packages/next/src/client/components/instant-samples.ts b/packages/next/src/client/components/instant-samples.ts index d987f890e129..a259c04a30b8 100644 --- a/packages/next/src/client/components/instant-samples.ts +++ b/packages/next/src/client/components/instant-samples.ts @@ -37,7 +37,7 @@ export function instrumentParamsForClientValidation( case 'request': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -76,7 +76,7 @@ export function expectCompleteParamsInClientValidation( case 'request': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -112,7 +112,7 @@ export function instrumentSearchParamsForClientValidation( case 'request': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never diff --git a/packages/next/src/client/components/navigation-untracked.ts b/packages/next/src/client/components/navigation-untracked.ts index a53f57b91c7c..549669785e85 100644 --- a/packages/next/src/client/components/navigation-untracked.ts +++ b/packages/next/src/client/components/navigation-untracked.ts @@ -29,7 +29,7 @@ function hasFallbackRouteParams(): boolean { case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index aa6d73100024..c148c052003e 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -2599,7 +2599,7 @@ function installGlobalModuleLoadingHandlers( case 'prerender-legacy': case 'request': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': return false default: workUnitStore satisfies never diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index abda22d84620..c7134cc5f1c2 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -368,7 +368,7 @@ async function createComponentTreeInternal( case 'prerender-client': case 'validation-client': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -412,7 +412,7 @@ async function createComponentTreeInternal( case 'prerender-client': case 'validation-client': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never diff --git a/packages/next/src/server/app-render/dynamic-rendering-client.ts b/packages/next/src/server/app-render/dynamic-rendering-client.ts index 19c24ead4638..2871f4772ded 100644 --- a/packages/next/src/server/app-render/dynamic-rendering-client.ts +++ b/packages/next/src/server/app-render/dynamic-rendering-client.ts @@ -63,9 +63,9 @@ export function useDynamicRouteParams(expression: string) { throw new InvariantError( `\`${expression}\` was called inside a cache scope. Next.js should be preventing ${expression} from being included in server components statically, but did not in this case.` ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - `\`${expression}\` was called in \`generateStaticParams\`. Next.js should be preventing ${expression} from being included in server component files statically, but did not in this case.` + `\`${expression}\` was called in \`${workUnitStore.functionName}\`. Next.js should be preventing ${expression} from being included in server component files statically, but did not in this case.` ) case 'prerender-legacy': case 'request': @@ -127,9 +127,9 @@ export function useDynamicSearchParams(expression: string) { throw new InvariantError( `\`${expression}\` was called inside a cache scope. Next.js should be preventing ${expression} from being included in server components statically, but did not in this case.` ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - `\`${expression}\` was called in \`generateStaticParams\`. Next.js should be preventing ${expression} from being included in server component files statically, but did not in this case.` + `\`${expression}\` was called in \`${workUnitStore.functionName}\`. Next.js should be preventing ${expression} from being included in server component files statically, but did not in this case.` ) case 'request': return diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index c0aacca7d3cd..48ec985e2fb6 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -182,7 +182,7 @@ export function markCurrentScopeAsDynamic( return case 'prerender-legacy': case 'request': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -219,7 +219,7 @@ export function markCurrentScopeAsDynamic( workUnitStore.usedDynamic = true } break - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -275,7 +275,7 @@ export function trackDynamicDataInDynamicRender(workUnitStore: WorkUnitStore) { case 'prerender-legacy': case 'prerender-client': case 'validation-client': - case 'generate-static-params': + case 'build-time-generator': break case 'request': if (process.env.NODE_ENV !== 'production') { @@ -515,7 +515,7 @@ export function createHangingInputAbortSignal( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': return undefined default: workUnitStore satisfies never diff --git a/packages/next/src/server/app-render/encryption.ts b/packages/next/src/server/app-render/encryption.ts index e650c280ecd2..167a29d2cde8 100644 --- a/packages/next/src/server/app-render/encryption.ts +++ b/packages/next/src/server/app-render/encryption.ts @@ -298,7 +298,7 @@ export async function decryptActionBoundArgs( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': case undefined: return controller.close() default: diff --git a/packages/next/src/server/app-render/instant-validation/boundary-impl.tsx b/packages/next/src/server/app-render/instant-validation/boundary-impl.tsx index 04b7c5088c5c..37d016c173a2 100644 --- a/packages/next/src/server/app-render/instant-validation/boundary-impl.tsx +++ b/packages/next/src/server/app-render/instant-validation/boundary-impl.tsx @@ -30,7 +30,7 @@ function getValidationBoundaryTracking(): ValidationBoundaryTracking | null { case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: store satisfies never diff --git a/packages/next/src/server/app-render/instant-validation/instant-samples.ts b/packages/next/src/server/app-render/instant-validation/instant-samples.ts index d5456092e753..2ac591f73128 100644 --- a/packages/next/src/server/app-render/instant-validation/instant-samples.ts +++ b/packages/next/src/server/app-render/instant-validation/instant-samples.ts @@ -45,7 +45,7 @@ function getExpectedSampleTracking(): InstantValidationSampleTracking { case 'prerender-client': case 'prerender': case 'prerender-runtime': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never diff --git a/packages/next/src/server/app-render/manifests-singleton.ts b/packages/next/src/server/app-render/manifests-singleton.ts index a866f9bc6469..17327e905f6b 100644 --- a/packages/next/src/server/app-render/manifests-singleton.ts +++ b/packages/next/src/server/app-render/manifests-singleton.ts @@ -111,7 +111,7 @@ function isUseCacheStore(workUnitStore: WorkUnitStore | undefined): boolean { case 'prerender-legacy': case 'prerender-runtime': case 'validation-client': - case 'generate-static-params': + case 'build-time-generator': return false default: return workUnitStore satisfies never diff --git a/packages/next/src/server/app-render/use-flight-response.tsx b/packages/next/src/server/app-render/use-flight-response.tsx index 0a3279c09220..ee4f169206a5 100644 --- a/packages/next/src/server/app-render/use-flight-response.tsx +++ b/packages/next/src/server/app-render/use-flight-response.tsx @@ -140,7 +140,7 @@ export function getFlightStream( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never 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 c06f5fd7f9fe..d7b23bbcb91a 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 @@ -436,8 +436,10 @@ export interface UnstableCacheStore extends CommonCacheStore { */ export type CacheStore = UseCacheStore | UnstableCacheStore -export interface GenerateStaticParamsStore extends CommonWorkUnitStore { - readonly type: 'generate-static-params' +export interface BuildTimeGeneratorStore extends CommonWorkUnitStore { + readonly type: 'build-time-generator' + /** The public export name to use in diagnostics, regardless of bundling. */ + readonly functionName: string readonly rootParams: Params } @@ -445,7 +447,7 @@ export type WorkUnitStore = | RequestStore | CacheStore | PrerenderStore - | GenerateStaticParamsStore + | BuildTimeGeneratorStore export function willConsumerServerCache( workUnitStore: WorkUnitStore | undefined @@ -466,7 +468,7 @@ export function willConsumerServerCache( case 'request': case 'prerender-runtime': case 'validation-client': - case 'generate-static-params': + case 'build-time-generator': return false default: return workUnitStore satisfies never @@ -506,7 +508,7 @@ export function getResumeDataCache( case 'private-cache': case 'unstable-cache': case 'prerender-legacy': - case 'generate-static-params': + case 'build-time-generator': return null default: return workUnitStore satisfies never @@ -528,7 +530,7 @@ export function getHmrRefreshHash( case 'validation-client': case 'prerender-legacy': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -551,7 +553,7 @@ export function isHmrRefresh(workUnitStore: WorkUnitStore): boolean { case 'prerender-runtime': case 'prerender-legacy': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -576,7 +578,7 @@ export function getServerComponentsHmrCache( case 'prerender-runtime': case 'prerender-legacy': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -605,7 +607,7 @@ export function getDraftModeProviderForCacheScope( case 'prerender-client': case 'validation-client': case 'prerender-legacy': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -629,7 +631,7 @@ export function getStagedRenderingController( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': return null default: return workUnitStore satisfies never @@ -656,7 +658,7 @@ export function getCacheSignal( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': return null default: return workUnitStore satisfies never @@ -678,7 +680,7 @@ export function getVaryParamsAccumulator( case 'prerender-client': case 'validation-client': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': return null default: workUnitStore satisfies never diff --git a/packages/next/src/server/dynamic-rendering-utils.ts b/packages/next/src/server/dynamic-rendering-utils.ts index 52e4ecd6338e..b42fe3df022e 100644 --- a/packages/next/src/server/dynamic-rendering-utils.ts +++ b/packages/next/src/server/dynamic-rendering-utils.ts @@ -296,7 +296,7 @@ function trackRuntimeDataAccessedImpl( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': // Only the modern server prerender tracks this; see the field docs on // PrerenderStoreModernServer. break @@ -524,7 +524,7 @@ export function applyOwnerStack(error: Error): Error { case 'prerender-runtime': case 'prerender-client': case 'validation-client': - case 'generate-static-params': + case 'build-time-generator': case undefined: ownerStack = innerOwnerStack break diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 7f51fad8521b..492b3e3f1ae3 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -72,7 +72,7 @@ function shouldProcessFetchConfigForWorkUnit( ) case 'prerender-runtime': case 'validation-client': - case 'generate-static-params': + case 'build-time-generator': return false default: return workUnitStore satisfies never @@ -473,7 +473,7 @@ export function createPatchedFetcher( break case 'request': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -512,7 +512,7 @@ export function createPatchedFetcher( case 'request': case 'cache': case 'private-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -683,7 +683,7 @@ export function createPatchedFetcher( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -815,7 +815,7 @@ export function createPatchedFetcher( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -859,7 +859,7 @@ export function createPatchedFetcher( case 'prerender-runtime': case 'prerender-legacy': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -1013,7 +1013,7 @@ export function createPatchedFetcher( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': case undefined: return createCachedDynamicResponse( workStore, @@ -1096,7 +1096,7 @@ export function createPatchedFetcher( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -1219,7 +1219,7 @@ export function createPatchedFetcher( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -1266,7 +1266,7 @@ export function createPatchedFetcher( case 'private-cache': case 'unstable-cache': case 'prerender-legacy': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never diff --git a/packages/next/src/server/node-environment-extensions/console-dim.external.tsx b/packages/next/src/server/node-environment-extensions/console-dim.external.tsx index a99c2b885810..ea00787b961e 100644 --- a/packages/next/src/server/node-environment-extensions/console-dim.external.tsx +++ b/packages/next/src/server/node-environment-extensions/console-dim.external.tsx @@ -261,7 +261,7 @@ function patchConsoleMethod(methodName: InterceptableConsoleMethod): void { case 'unstable-cache': case 'private-cache': case 'request': - case 'generate-static-params': + case 'build-time-generator': case undefined: if (consoleStore?.dim === true) { return applyWithDimming.call( diff --git a/packages/next/src/server/node-environment-extensions/io-utils.tsx b/packages/next/src/server/node-environment-extensions/io-utils.tsx index 44261d0f122d..3c71288ea0eb 100644 --- a/packages/next/src/server/node-environment-extensions/io-utils.tsx +++ b/packages/next/src/server/node-environment-extensions/io-utils.tsx @@ -107,7 +107,7 @@ export function io(expression: string, type: SyncIOApiType) { case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never diff --git a/packages/next/src/server/node-environment-extensions/unhandled-rejection.external.tsx b/packages/next/src/server/node-environment-extensions/unhandled-rejection.external.tsx index 4ac532b3f06a..773f066b1e66 100644 --- a/packages/next/src/server/node-environment-extensions/unhandled-rejection.external.tsx +++ b/packages/next/src/server/node-environment-extensions/unhandled-rejection.external.tsx @@ -619,7 +619,7 @@ function filteringUnhandledRejectionHandler( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never diff --git a/packages/next/src/server/og/cache-image-response.ts b/packages/next/src/server/og/cache-image-response.ts index a05853aadbc6..94f3cb4cfdce 100644 --- a/packages/next/src/server/og/cache-image-response.ts +++ b/packages/next/src/server/og/cache-image-response.ts @@ -78,7 +78,7 @@ async function getCachedImageResponseArrayBuffer( case 'prerender-client': case 'validation-client': case 'prerender-legacy': - case 'generate-static-params': + case 'build-time-generator': return renderImageResponseArrayBuffer(args) default: return workUnitStore satisfies never diff --git a/packages/next/src/server/request/cache-stages.ts b/packages/next/src/server/request/cache-stages.ts index c18e7d15fa6f..9e0657d62714 100644 --- a/packages/next/src/server/request/cache-stages.ts +++ b/packages/next/src/server/request/cache-stages.ts @@ -135,9 +135,9 @@ export function unstable_prefetch(): Promise { `Route ${workStore.route} used \`unstable_prefetch()\` inside a function cached with \`unstable_cache()\`. The \`unstable_prefetch()\` function is used to indicate the subsequent code must not run in the app shell, but \`unstable_cache()\` caches must be able to be produced before a prefetch, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) } - case 'generate-static-params': { + case 'build-time-generator': { throw new Error( - `Route ${workStore.route} used \`unstable_prefetch()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without a prefetch. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + `Route ${workStore.route} used \`unstable_prefetch()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without a prefetch. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` ) } case 'prerender-client': @@ -288,9 +288,9 @@ export function unstable_navigation(): Promise { `Route ${workStore.route} used \`unstable_navigation()\` inside a function cached with \`unstable_cache()\`. The \`unstable_navigation()\` function is used to indicate the subsequent code must only run during an actual navigation, but \`unstable_cache()\` caches must be able to be produced before a navigation, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) } - case 'generate-static-params': { + case 'build-time-generator': { throw new Error( - `Route ${workStore.route} used \`unstable_navigation()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without a navigation. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + `Route ${workStore.route} used \`unstable_navigation()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without a navigation. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` ) } case 'prerender-client': diff --git a/packages/next/src/server/request/connection.ts b/packages/next/src/server/request/connection.ts index eb902b15bc87..05d8d77c5cfe 100644 --- a/packages/next/src/server/request/connection.ts +++ b/packages/next/src/server/request/connection.ts @@ -72,9 +72,9 @@ export function connection(): Promise { } case 'unstable-cache': throw createConnectionInUnstableCacheError(workStore.route) - case 'generate-static-params': + case 'build-time-generator': throw new Error( - `Route ${workStore.route} used \`connection()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + `Route ${workStore.route} used \`connection()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` ) case 'prerender': case 'prerender-client': diff --git a/packages/next/src/server/request/cookies.ts b/packages/next/src/server/request/cookies.ts index 37875657b934..4fb8818e640c 100644 --- a/packages/next/src/server/request/cookies.ts +++ b/packages/next/src/server/request/cookies.ts @@ -68,9 +68,9 @@ export function cookies(): Promise { throw error case 'unstable-cache': throw createCookiesInUnstableCacheError(workStore.route) - case 'generate-static-params': + case 'build-time-generator': throw new Error( - `Route ${workStore.route} used \`cookies()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + `Route ${workStore.route} used \`cookies()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` ) case 'prerender': return makeHangingCookies(workStore, workUnitStore) diff --git a/packages/next/src/server/request/draft-mode.ts b/packages/next/src/server/request/draft-mode.ts index f3e84ef552d2..890760e6470e 100644 --- a/packages/next/src/server/request/draft-mode.ts +++ b/packages/next/src/server/request/draft-mode.ts @@ -82,9 +82,9 @@ export function draftMode(): Promise { `${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.` ) } - case 'generate-static-params': + case 'build-time-generator': throw new Error( - `Route ${workStore.route} used \`${callingExpression}()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + `Route ${workStore.route} used \`${callingExpression}()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` ) default: @@ -260,9 +260,9 @@ function trackDynamicDraftMode(expression: string, constructorOpt: Function) { case 'request': trackDynamicDataInDynamicRender(workUnitStore) break - case 'generate-static-params': + case 'build-time-generator': throw new Error( - `Route ${workStore.route} used \`${expression}\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + `Route ${workStore.route} used \`${expression}\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` ) default: workUnitStore satisfies never diff --git a/packages/next/src/server/request/headers.ts b/packages/next/src/server/request/headers.ts index ec0696d1b328..c2364fb0e2b2 100644 --- a/packages/next/src/server/request/headers.ts +++ b/packages/next/src/server/request/headers.ts @@ -70,9 +70,9 @@ export function headers(): Promise { } case 'unstable-cache': throw createHeadersInUnstableCacheError(workStore.route) - case 'generate-static-params': + case 'build-time-generator': throw new Error( - `Route ${workStore.route} used \`headers()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + `Route ${workStore.route} used \`headers()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` ) case 'prerender': case 'prerender-client': diff --git a/packages/next/src/server/request/io.ts b/packages/next/src/server/request/io.ts index 8af60e866724..048f1b30c3e6 100644 --- a/packages/next/src/server/request/io.ts +++ b/packages/next/src/server/request/io.ts @@ -74,7 +74,7 @@ export function io(): Promise { // simply return the value at cache-fill time. // ... // intentional fallthrough - case 'generate-static-params': + case 'build-time-generator': // generateStaticParams runs at build time. There is no prerender // to stall so we resolve immediately. // ... diff --git a/packages/next/src/server/request/params.ts b/packages/next/src/server/request/params.ts index c38cea5d543c..14dc05b57a38 100644 --- a/packages/next/src/server/request/params.ts +++ b/packages/next/src/server/request/params.ts @@ -77,9 +77,9 @@ export function createParamsFromClient( throw new InvariantError( 'createParamsFromClient should not be called in a runtime prerender.' ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - 'createParamsFromClient should not be called inside generateStaticParams.' + `createParamsFromClient should not be called inside ${workUnitStore.functionName}.` ) case 'validation-client': { if (workUnitStore.validationSamples) { @@ -166,9 +166,9 @@ export function createServerParamsForRoute( throw new InvariantError( 'createServerParamsForRoute should not be called in cache contexts.' ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - 'createServerParamsForRoute should not be called inside generateStaticParams.' + `createServerParamsForRoute should not be called inside ${workUnitStore.functionName}.` ) case 'prerender-runtime': { throw new InvariantError( @@ -228,9 +228,9 @@ export function createServerParamsForServerSegment( throw new InvariantError( 'createServerParamsForServerSegment should not be called in cache contexts.' ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - 'createServerParamsForServerSegment should not be called inside generateStaticParams.' + `createServerParamsForServerSegment should not be called inside ${workUnitStore.functionName}.` ) case 'prerender-runtime': return createRuntimePrerenderParams( @@ -300,9 +300,9 @@ export function createPrerenderParamsForClientSegment( throw new InvariantError( 'createPrerenderParamsForClientSegment should not be called in cache contexts.' ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - 'createPrerenderParamsForClientSegment should not be called inside generateStaticParams.' + `createPrerenderParamsForClientSegment should not be called inside ${workUnitStore.functionName}.` ) case 'prerender-runtime': case 'prerender-legacy': diff --git a/packages/next/src/server/request/pathname.ts b/packages/next/src/server/request/pathname.ts index 167b14d1c20c..f56c244ac0a9 100644 --- a/packages/next/src/server/request/pathname.ts +++ b/packages/next/src/server/request/pathname.ts @@ -45,9 +45,9 @@ export function createServerPathnameForMetadata( throw new InvariantError( 'createServerPathnameForMetadata should not be called in cache contexts.' ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - 'createServerPathnameForMetadata should not be called inside generateStaticParams.' + `createServerPathnameForMetadata should not be called inside ${workUnitStore.functionName}.` ) case 'prerender-runtime': { // TODO(app-shells): whether or not this is included in the shell diff --git a/packages/next/src/server/request/root-params.ts b/packages/next/src/server/request/root-params.ts index b3bcaafc0455..4e1547991cbd 100644 --- a/packages/next/src/server/request/root-params.ts +++ b/packages/next/src/server/request/root-params.ts @@ -112,8 +112,13 @@ export function getRootParam(paramName: string): Promise { case 'prerender-runtime': { break } - case 'generate-static-params': { + case 'build-time-generator': { if (!(paramName in workUnitStore.rootParams)) { + if (workUnitStore.functionName !== 'generateStaticParams') { + throw new Error( + `Route ${workStore.route} used ${apiName} inside \`${workUnitStore.functionName}\`, but the \`${paramName}\` parameter is not available in this build-time generator.` + ) + } throw new Error( `Route ${workStore.route} used ${apiName} inside \`generateStaticParams\`, but the \`${paramName}\` parameter was not provided by a parent \`generateStaticParams\`. In \`generateStaticParams\`, root params are only available for segments nested below the segment that provides them.` ) diff --git a/packages/next/src/server/request/search-params.ts b/packages/next/src/server/request/search-params.ts index 62ab161e499d..53468e1c62ba 100644 --- a/packages/next/src/server/request/search-params.ts +++ b/packages/next/src/server/request/search-params.ts @@ -70,9 +70,9 @@ export function createSearchParamsFromClient( throw new InvariantError( 'createSearchParamsFromClient should not be called in cache contexts.' ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - 'createSearchParamsFromClient should not be called inside generateStaticParams.' + `createSearchParamsFromClient should not be called inside ${workUnitStore.functionName}.` ) case 'validation-client': { if (workUnitStore.validationSamples) { @@ -133,9 +133,9 @@ export function createServerSearchParamsForServerPage( throw new InvariantError( 'createServerSearchParamsForServerPage should not be called in cache contexts.' ) - case 'generate-static-params': + case 'build-time-generator': throw new InvariantError( - 'createServerSearchParamsForServerPage should not be called inside generateStaticParams.' + `createServerSearchParamsForServerPage should not be called inside ${workUnitStore.functionName}.` ) case 'prerender-runtime': return createRuntimePrerenderSearchParams( @@ -195,9 +195,9 @@ export function createPrerenderSearchParamsForClientPage(): Promise( case 'prerender-client': case 'validation-client': case 'request': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never @@ -441,7 +441,7 @@ function getFetchUrlPrefix( case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': return workStore.route default: return workUnitStore satisfies never diff --git a/packages/next/src/server/web/spec-extension/unstable-no-store.ts b/packages/next/src/server/web/spec-extension/unstable-no-store.ts index 4a84369001ec..6a30d57bf7df 100644 --- a/packages/next/src/server/web/spec-extension/unstable-no-store.ts +++ b/packages/next/src/server/web/spec-extension/unstable-no-store.ts @@ -43,7 +43,7 @@ export function unstable_noStore() { case 'cache': case 'private-cache': case 'unstable-cache': - case 'generate-static-params': + case 'build-time-generator': break default: workUnitStore satisfies never From e7d9685dadb89f9797b2d0731bf831d4cd916009 Mon Sep 17 00:00:00 2001 From: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:11:59 +0200 Subject: [PATCH 2/5] Surface agent feedback gate connection failures (#98991) ## Summary - Distinguish an unreachable agent-feedback rollout gate from an explicitly disabled rollout. - Exit the internal feedback-instructions command with actionable retry guidance so sandboxed agents can request network access instead of silently skipping reports. ## Verification - `pnpm exec jest packages/next/src/cli/internal/agent-feedback-status.test.ts packages/next/src/cli/internal/agent-feedback-instructions.test.ts --runInBand` - `pnpm --filter=next types` - `pnpm --filter=next build` - Verified the built CLI exits 1 with retry guidance without network access and returns the protocol with network access. --- .../agent-feedback-instructions.test.ts | 37 ++++++++++++++++++- .../internal/agent-feedback-instructions.ts | 26 +++++++++---- .../internal/agent-feedback-status.test.ts | 12 ++++-- .../src/cli/internal/agent-feedback-status.ts | 2 - .../src/server/lib/generate-agent-files.ts | 2 +- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/packages/next/src/cli/internal/agent-feedback-instructions.test.ts b/packages/next/src/cli/internal/agent-feedback-instructions.test.ts index 62e0b7620160..b972d29f8bc2 100644 --- a/packages/next/src/cli/internal/agent-feedback-instructions.test.ts +++ b/packages/next/src/cli/internal/agent-feedback-instructions.test.ts @@ -1,4 +1,7 @@ -import { loadAgentFeedbackInstructions } from './agent-feedback-instructions' +import { + agentFeedbackInstructionsCli, + loadAgentFeedbackInstructions, +} from './agent-feedback-instructions' describe('loadAgentFeedbackInstructions', () => { it('returns the protocol when feedback is enabled', async () => { @@ -29,4 +32,36 @@ describe('loadAgentFeedbackInstructions', () => { ) ).resolves.toBeNull() }) + + it('propagates feedback status errors', async () => { + await expect( + loadAgentFeedbackInstructions(async () => { + throw new Error('network unavailable') + }) + ).rejects.toThrow('network unavailable') + }) +}) + +describe('agentFeedbackInstructionsCli', () => { + const originalExitCode = process.exitCode + + afterEach(() => { + process.exitCode = originalExitCode + jest.restoreAllMocks() + }) + + it('reports feedback status errors and exits with a failure', async () => { + const writeError = jest + .spyOn(process.stderr, 'write') + .mockImplementation(() => true) + + await agentFeedbackInstructionsCli(async () => { + throw new Error('network unavailable') + }) + + expect(writeError).toHaveBeenCalledWith( + 'Unable to check whether Next.js agent feedback is enabled. Rerun this command with network access.\n' + ) + expect(process.exitCode).toBe(1) + }) }) diff --git a/packages/next/src/cli/internal/agent-feedback-instructions.ts b/packages/next/src/cli/internal/agent-feedback-instructions.ts index bd96afb7d6f9..0456377f4293 100644 --- a/packages/next/src/cli/internal/agent-feedback-instructions.ts +++ b/packages/next/src/cli/internal/agent-feedback-instructions.ts @@ -9,26 +9,36 @@ const AGENT_FEEDBACK_PROTOCOL_PATH = path.join( type IsEnabled = () => Promise type ReadProtocol = () => Promise +type LoadInstructions = () => Promise export async function loadAgentFeedbackInstructions( isEnabled: IsEnabled = isAgentFeedbackEnabled, readProtocol: ReadProtocol = () => readFile(AGENT_FEEDBACK_PROTOCOL_PATH, 'utf8') ): Promise { - try { - if (!(await isEnabled())) { - return null - } + if (!(await isEnabled())) { + return null + } + try { return await readProtocol() } catch { return null } } -export async function agentFeedbackInstructionsCli(): Promise { - const instructions = await loadAgentFeedbackInstructions() - if (instructions) { - process.stdout.write(instructions) +export async function agentFeedbackInstructionsCli( + loadInstructions: LoadInstructions = loadAgentFeedbackInstructions +): Promise { + try { + const instructions = await loadInstructions() + if (instructions) { + process.stdout.write(instructions) + } + } catch { + process.stderr.write( + 'Unable to check whether Next.js agent feedback is enabled. Rerun this command with network access.\n' + ) + process.exitCode = 1 } } diff --git a/packages/next/src/cli/internal/agent-feedback-status.test.ts b/packages/next/src/cli/internal/agent-feedback-status.test.ts index d2dd7d9eab83..9ef02e9e3c96 100644 --- a/packages/next/src/cli/internal/agent-feedback-status.test.ts +++ b/packages/next/src/cli/internal/agent-feedback-status.test.ts @@ -23,15 +23,17 @@ describe('isAgentFeedbackEnabled', () => { await expect(isAgentFeedbackEnabled(fetchImpl)).resolves.toBe(false) }) - it('fails closed when the request rejects', async () => { + it('rejects when the request fails', async () => { const fetchImpl: typeof fetch = async () => { throw new Error('network unavailable') } - await expect(isAgentFeedbackEnabled(fetchImpl)).resolves.toBe(false) + await expect(isAgentFeedbackEnabled(fetchImpl)).rejects.toThrow( + 'network unavailable' + ) }) - it('fails closed when the request times out', async () => { + it('rejects when the request times out', async () => { const fetchImpl: typeof fetch = (_input, init) => { return new Promise((_resolve, reject) => { init?.signal?.addEventListener('abort', () => { @@ -40,6 +42,8 @@ describe('isAgentFeedbackEnabled', () => { }) } - await expect(isAgentFeedbackEnabled(fetchImpl, 1)).resolves.toBe(false) + await expect(isAgentFeedbackEnabled(fetchImpl, 1)).rejects.toThrow( + 'aborted' + ) }) }) diff --git a/packages/next/src/cli/internal/agent-feedback-status.ts b/packages/next/src/cli/internal/agent-feedback-status.ts index 7f66094c8e87..b985d33fa747 100644 --- a/packages/next/src/cli/internal/agent-feedback-status.ts +++ b/packages/next/src/cli/internal/agent-feedback-status.ts @@ -17,8 +17,6 @@ export async function isAgentFeedbackEnabled( }) return response.ok && (await response.text()) === 'true' - } catch { - return false } finally { clearTimeout(timeout) } diff --git a/packages/next/src/server/lib/generate-agent-files.ts b/packages/next/src/server/lib/generate-agent-files.ts index b0e371eed6f0..a614f0714c7d 100644 --- a/packages/next/src/server/lib/generate-agent-files.ts +++ b/packages/next/src/server/lib/generate-agent-files.ts @@ -56,7 +56,7 @@ During the task, queue anonymized Next.js issues that cause a meaningful detour Continue the task without reporting immediately. Ignore routine debugging and issues caused by unverified changes. Never retain source code, logs, paths, URLs, secrets, personal information, or project-specific data. -At the final stopping point, from the directory containing this file, run \`node node_modules/next/dist/bin/next internal agent-feedback-instructions\` once per task and follow any instructions returned. If it returns nothing, continue without reporting. +At the final stopping point, from the directory containing this file, run \`node node_modules/next/dist/bin/next internal agent-feedback-instructions\` once per task and follow any instructions returned. If the command succeeds with no output, continue without reporting. If it fails, follow its error guidance; retrying with network access is allowed. ${AGENT_FEEDBACK_END_MARKER}` } From 26bf404d923ee71764922a060939b26f2fce3a2f Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 21 Sep 2026 08:17:56 -0700 Subject: [PATCH 3/5] Enable strict route matching by default (#97397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This enables strict route matching by default. App Router URLs that cannot construct a complete route tree are rejected, unreachable pages are reported, and incomplete catch-all matchers are pruned. If strict route matching finds one of these invalid route structures, Next.js warns that the validation implies a bug in the app and points to `deprecated.looseRouteMatching: true` as a temporary escape hatch. The deprecated option only accepts `true`; strict matching remains the normalized internal behavior when the option is absent. For example, this layout cannot render either URL as a complete route because neither named slot has a page or default for the URL matched by its sibling: ```text app/ ├── layout.tsx ├── @left/foo/page.tsx └── @right/bar/page.tsx ``` Previously the app could depend on soft navigation preserving stale slot state even though `/foo` and `/bar` could not be loaded directly. With strict route matching enabled, the build reports the incompatible slots and the two unreachable pages. Adding explicit defaults makes the intended fallback behavior valid again. ## Verification - `pnpm build-all` - `pnpm exec jest packages/next/src/server/config.test.ts --runInBand` - `parallel-routes-pruned-matchers` in production with Turbopack and Webpack - `parallel-routes-leaf-segments` in production with Turbopack - strict diagnostic fixtures in production with Turbopack --- packages/next/src/build/entries.ts | 8 ++++ packages/next/src/build/print-build-errors.ts | 32 ++++++++++++++- .../next/src/build/turbopack-build/impl.ts | 3 ++ packages/next/src/server/config-schema.ts | 5 +++ packages/next/src/server/config-shared.ts | 25 ++++++++++-- packages/next/src/server/config.test.ts | 40 ++++++++++++++++--- packages/next/src/server/config.ts | 3 ++ .../src/server/dev/hot-reloader-turbopack.ts | 6 ++- .../lib/router-utils/setup-dev-bundler.ts | 5 +++ .../strict-route-matching-config.ts | 12 ++++++ .../canonical-interception-routes.test.ts | 9 +++++ .../next.config.js | 10 ----- .../incompatible-parallel-route-slots.test.ts | 8 ++++ .../next.config.js | 10 ----- .../next.config.js | 4 -- .../app/parallel-scroll/default.js | 6 +++ .../fixtures/build-error/README.md | 5 +++ .../fixtures/build-error/next.config.js | 12 ++++++ .../fixtures/no-build-error/next.config.js | 4 +- .../next.config.js | 10 ----- .../app/nested-revalidate/default.tsx | 6 +++ .../unmatched-app-pages/next.config.js | 10 ----- .../unmatched-app-pages.test.ts | 9 +++++ .../app/nested/default.tsx | 8 ++++ 24 files changed, 191 insertions(+), 59 deletions(-) create mode 100644 packages/next/src/server/lib/router-utils/strict-route-matching-config.ts delete mode 100644 test/e2e/app-dir/canonical-interception-routes/next.config.js delete mode 100644 test/e2e/app-dir/incompatible-parallel-route-slots/next.config.js create mode 100644 test/e2e/app-dir/parallel-routes-and-interception/app/parallel-scroll/default.js create mode 100644 test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/next.config.js delete mode 100644 test/e2e/app-dir/parallel-routes-pruned-matchers/next.config.js create mode 100644 test/e2e/app-dir/parallel-routes-revalidation/app/nested-revalidate/default.tsx delete mode 100644 test/e2e/app-dir/unmatched-app-pages/next.config.js create mode 100644 test/production/app-dir/parallel-routes-static/app/nested/default.tsx diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index ee8ec1dd23bb..a4997362c0f1 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -64,6 +64,8 @@ import { MissingCanonicalInterceptionRoutesError } from '../shared/lib/errors/mi import { IncompatibleParallelRouteSlotsError } from '../shared/lib/errors/incompatible-parallel-route-slots-error' import { findMissingCanonicalInterceptionRoutes } from '../shared/lib/router/utils/interception-routes' import { findPageFile } from '../server/lib/find-page-file' +import * as Log from './output/log' +import { getStrictRouteMatchingDefaultWarning } from '../server/lib/router-utils/strict-route-matching-config' type ObjectValue = T extends { [key: string]: infer V } ? V : never import { getStaticInfoIncludingLayouts } from './get-static-info-including-layouts' @@ -508,6 +510,12 @@ export async function createEntrypoints( ) ) } + if (routeMatchingErrors.length > 0) { + const warning = getStrictRouteMatchingDefaultWarning(config) + if (warning) { + Log.warnOnce(warning) + } + } if (routeMatchingErrors.length === 1) { throw routeMatchingErrors[0] } diff --git a/packages/next/src/build/print-build-errors.ts b/packages/next/src/build/print-build-errors.ts index cf29eca3ab7e..840a6f98c9f7 100644 --- a/packages/next/src/build/print-build-errors.ts +++ b/packages/next/src/build/print-build-errors.ts @@ -1,5 +1,16 @@ -import { formatIssue, isRelevantWarning } from '../shared/lib/turbopack/utils' +import { + formatIssue, + isRelevantWarning, + renderStyledStringToErrorAnsi, +} from '../shared/lib/turbopack/utils' import type { TurbopackResult } from './swc/types' +import * as Log from './output/log' + +const STRICT_ROUTE_MATCHING_ISSUE_TITLES = new Set([ + 'Interception routes must have a canonical route', + 'Parallel route slots cannot render the same URLs', + 'Unmatched app pages', +]) export function formatWarningsHeader(count: number): string { return `Turbopack build encountered ${count} ${count === 1 ? 'warning' : 'warnings'}:` @@ -19,7 +30,10 @@ export function formatWarningsHeader(count: number): string { export function printBuildErrors( result: TurbopackResult, isDev: boolean, - opts?: { deferWarnings?: boolean } + opts?: { + deferWarnings?: boolean + strictRouteMatchingDefaultWarning?: string + } ): { warnings: string[] } { // Issues that we want to stop the server from executing const topLevelFatalIssues = [] @@ -32,8 +46,18 @@ export function printBuildErrors( const seenFatalIssues = new Set() const seenErrors = new Set() const seenWarnings = new Set() + let hasStrictRouteMatchingIssue = false for (const issue of result.issues) { + if ( + issue.severity === 'error' && + STRICT_ROUTE_MATCHING_ISSUE_TITLES.has( + renderStyledStringToErrorAnsi(issue.title) + ) + ) { + hasStrictRouteMatchingIssue = true + } + // We only want to completely shut down the server if (issue.severity === 'fatal' || issue.severity === 'bug') { const formatted = formatIssue(issue) @@ -83,6 +107,10 @@ export function printBuildErrors( ) } + if (hasStrictRouteMatchingIssue && opts?.strictRouteMatchingDefaultWarning) { + Log.warnOnce(opts.strictRouteMatchingDefaultWarning) + } + if (topLevelFatalIssues.length > 0) { throw new Error( `Turbopack build failed with ${ diff --git a/packages/next/src/build/turbopack-build/impl.ts b/packages/next/src/build/turbopack-build/impl.ts index 38e0d2c50556..a5d1e82625d4 100644 --- a/packages/next/src/build/turbopack-build/impl.ts +++ b/packages/next/src/build/turbopack-build/impl.ts @@ -20,6 +20,7 @@ import { printBuildErrors } from '../print-build-errors' import { normalizePath } from '../../lib/normalize-path' import type { ProjectOptions, RawEntrypoints } from '../swc/types' import { Bundler } from '../../lib/bundler' +import { getStrictRouteMatchingDefaultWarning } from '../../server/lib/router-utils/strict-route-matching-config' export async function turbopackBuild(telemetry: Telemetry): Promise<{ duration: number @@ -184,6 +185,8 @@ export async function turbopackBuild(telemetry: Telemetry): Promise<{ // keeping SSG errors more prominent than compile warnings. const { warnings } = printBuildErrors(entrypoints, dev, { deferWarnings: true, + strictRouteMatchingDefaultWarning: + getStrictRouteMatchingDefaultWarning(config), }) // Skip when telemetry is fully off — featureUsage() isn't free. diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index e9e5bbc41458..bd4797fcc9a3 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -644,6 +644,11 @@ export const configSchema: zod.ZodType = z.lazy(() => crossOrigin: z .union([z.literal('anonymous'), z.literal('use-credentials')]) .optional(), + deprecated: z + .strictObject({ + looseRouteMatching: z.literal(true).optional(), + }) + .optional(), deploymentId: z.string().optional(), supportsImmutableAssets: z.boolean().optional(), outputHashSalt: z.string().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 07c6bf517bbc..7a86bd995f4e 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -488,6 +488,16 @@ export function resolveCssChunkingMode( return 'loose' } +export interface DeprecatedConfig { + /** + * Use the legacy loose App Router matching behavior instead of requiring + * every URL to construct a complete parallel route tree. + * + * @default false + */ + looseRouteMatching?: true +} + export interface ExperimentalConfig { /** Nudge coding agents about security upgrades, stable releases, or Future Defaults. */ agenticAutoUpgrade?: 'security' | 'latest' | 'future' | false @@ -1478,9 +1488,9 @@ export interface ExperimentalConfig { /** * Omits catch-all-derived App Router matchers that cannot construct a - * complete parallel route tree for their URL. This requires - * `explicitParallelRouteChildren`; setting that option to `false` also - * disables strict route matching. + * complete parallel route tree. + * + * @internal Used by the Next.js internals only. */ strictRouteMatching?: boolean @@ -2159,6 +2169,12 @@ export interface NextConfig { */ agentRules?: boolean + /** + * Options for deprecated features that are still available for backwards + * compatibility. + */ + deprecated?: DeprecatedConfig + /** * Enable experimental features. Note that all experimental features are subject to breaking changes in the future. */ @@ -2338,6 +2354,7 @@ export const defaultConfig = Object.freeze({ static: process.env.NEXT_STATIC_CACHE_HANDLER_PATH, }, adapterPath: process.env.NEXT_ADAPTER_PATH || undefined, + deprecated: {} as DeprecatedConfig, experimental: { agentFeedback: false, coldCacheBadge: false, @@ -2432,7 +2449,7 @@ export const defaultConfig = Object.freeze({ slowModuleDetection: undefined, globalNotFound: false, explicitParallelRouteChildren: true, - strictRouteMatching: false, + strictRouteMatching: true, browserDebugInfoInTerminal: 'warn', lockDistDir: true, disableResumeDataCacheCompression: false, diff --git a/packages/next/src/server/config.test.ts b/packages/next/src/server/config.test.ts index f3f6c1b80445..c41fde36be18 100644 --- a/packages/next/src/server/config.test.ts +++ b/packages/next/src/server/config.test.ts @@ -1,4 +1,9 @@ import { PHASE_INFO, PHASE_PRODUCTION_BUILD } from '../api/constants' +import { + getStrictRouteMatchingDefaultWarning, + STRICT_ROUTE_MATCHING_DEFAULT_WARNING, +} from './lib/router-utils/strict-route-matching-config' +import { configSchema } from './config-schema' describe('loadConfig', () => { let loadConfig: typeof import('./config').default @@ -194,18 +199,41 @@ describe('loadConfig', () => { }) describe('parallel route matching flags', () => { - it('allows explicit children detection without strict route matching', async () => { + it('enables strict route matching by default and exposes the opt-out warning', async () => { + const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, { + customConfig: {}, + }) + + expect(result.experimental.strictRouteMatching).toBe(true) + expect(getStrictRouteMatchingDefaultWarning(result)).toBe( + STRICT_ROUTE_MATCHING_DEFAULT_WARNING + ) + }) + + it('allows loose route matching through the deprecated opt-out', async () => { const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, { customConfig: { - experimental: { - explicitParallelRouteChildren: true, - strictRouteMatching: false, + deprecated: { + looseRouteMatching: true, }, }, }) - expect(result.experimental.explicitParallelRouteChildren).toBe(true) expect(result.experimental.strictRouteMatching).toBe(false) + expect(getStrictRouteMatchingDefaultWarning(result)).toBeUndefined() + }) + + it('only accepts true for the deprecated opt-out', () => { + expect( + configSchema.safeParse({ + deprecated: { looseRouteMatching: true }, + }).success + ).toBe(true) + expect( + configSchema.safeParse({ + deprecated: { looseRouteMatching: false }, + }).success + ).toBe(false) }) it('disables strict route matching when explicit children detection is disabled', async () => { @@ -213,13 +241,13 @@ describe('loadConfig', () => { customConfig: { experimental: { explicitParallelRouteChildren: false, - strictRouteMatching: true, }, }, }) expect(result.experimental.explicitParallelRouteChildren).toBe(false) expect(result.experimental.strictRouteMatching).toBe(false) + expect(getStrictRouteMatchingDefaultWarning(result)).toBeUndefined() }) }) diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index de55a4b1ad2c..65e9e8e9f8bb 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -456,6 +456,9 @@ function assignDefaultsAndValidate( }, } + result.experimental.strictRouteMatching = + !result.deprecated.looseRouteMatching + // Pruning assumes that children only exists when it is backed by an // ordinary route branch. Restoring the legacy implicit children slot must // therefore also restore the legacy matcher behavior. diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 11c30586b974..d32f781f2afc 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -116,6 +116,7 @@ import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compi import { DeferredEmit } from '../../shared/lib/turbopack/deferred-emit' import { getSupportedBrowsers } from '../../build/get-supported-browsers' import { printBuildErrors } from '../../build/print-build-errors' +import { getStrictRouteMatchingDefaultWarning } from '../lib/router-utils/strict-route-matching-config' import { receiveBrowserLogsTurbopack } from './browser-logs/receive-logs' import { normalizePath } from '../../lib/normalize-path' import { seedTurbopackCacheIfNeeded } from '../../lib/turbopack-cache-seed' @@ -1140,7 +1141,10 @@ export async function createHotReloaderTurbopack( // Certain crtical issues prevent any entrypoints from being constructed so return early if (!('routes' in entrypoints.value)) { - printBuildErrors(entrypoints, true) + printBuildErrors(entrypoints, true, { + strictRouteMatchingDefaultWarning: + getStrictRouteMatchingDefaultWarning(nextConfig), + }) currentEntriesHandlingResolve!() currentEntriesHandlingResolve = undefined diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 63ac4b070dbc..209d95248189 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -102,6 +102,7 @@ import { UnmatchedAppPagesError } from '../../../shared/lib/errors/unmatched-app import { MissingCanonicalInterceptionRoutesError } from '../../../shared/lib/errors/missing-canonical-interception-routes-error' import { IncompatibleParallelRouteSlotsError } from '../../../shared/lib/errors/incompatible-parallel-route-slots-error' import { findMissingCanonicalInterceptionRoutes } from '../../../shared/lib/router/utils/interception-routes' +import { getStrictRouteMatchingDefaultWarning } from './strict-route-matching-config' import { createRouteTypesManifest, writeRouteTypesManifest, @@ -1147,6 +1148,10 @@ async function startWatcher( for (const error of routeMatchingErrors) { Log.error(error.message) } + const warning = getStrictRouteMatchingDefaultWarning(nextConfig) + if (warning) { + Log.warnOnce(warning) + } } // Turbopack reports route-matching failures as app-structure issues. // Webpack needs an HMR server error so dev can finish booting and surface diff --git a/packages/next/src/server/lib/router-utils/strict-route-matching-config.ts b/packages/next/src/server/lib/router-utils/strict-route-matching-config.ts new file mode 100644 index 000000000000..e9ab0e3e4dd0 --- /dev/null +++ b/packages/next/src/server/lib/router-utils/strict-route-matching-config.ts @@ -0,0 +1,12 @@ +import type { NextConfigComplete } from '../../config-shared' + +export const STRICT_ROUTE_MATCHING_DEFAULT_WARNING = + "Strict route matching is enabled by default. This validation indicates a bug in your app's route structure, but you can temporarily restore loose route matching by setting `deprecated.looseRouteMatching` to `true` in your Next.js config." + +export function getStrictRouteMatchingDefaultWarning( + config: NextConfigComplete +): string | undefined { + return config.experimental.strictRouteMatching + ? STRICT_ROUTE_MATCHING_DEFAULT_WARNING + : undefined +} diff --git a/test/e2e/app-dir/canonical-interception-routes/canonical-interception-routes.test.ts b/test/e2e/app-dir/canonical-interception-routes/canonical-interception-routes.test.ts index c2c67a215bed..6dce225fecae 100644 --- a/test/e2e/app-dir/canonical-interception-routes/canonical-interception-routes.test.ts +++ b/test/e2e/app-dir/canonical-interception-routes/canonical-interception-routes.test.ts @@ -42,6 +42,8 @@ describe('canonical-interception-routes', () => { } `) } + + expectDefaultStrictRouteMatchingWarning(next.cliOutput) } else { const { exitCode, cliOutput } = await next.build() expect(exitCode).toBe(1) @@ -53,10 +55,17 @@ describe('canonical-interception-routes', () => { Every interception route must have a matching non-interception route so the URL can be loaded directly or refreshed." `) + expectDefaultStrictRouteMatchingWarning(cliOutput) } }) }) +function expectDefaultStrictRouteMatchingWarning(output: string): void { + expect(stripAnsi(output)).toContain( + 'you can temporarily restore loose route matching by setting `deprecated.looseRouteMatching` to `true`' + ) +} + function extractMissingCanonicalRoutesError(output: string): string { const normalizedOutput = stripAnsi(output) const start = normalizedOutput.indexOf( diff --git a/test/e2e/app-dir/canonical-interception-routes/next.config.js b/test/e2e/app-dir/canonical-interception-routes/next.config.js deleted file mode 100644 index ff01ea9b86a3..000000000000 --- a/test/e2e/app-dir/canonical-interception-routes/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const nextConfig = { - experimental: { - strictRouteMatching: true, - }, -} - -module.exports = nextConfig diff --git a/test/e2e/app-dir/incompatible-parallel-route-slots/incompatible-parallel-route-slots.test.ts b/test/e2e/app-dir/incompatible-parallel-route-slots/incompatible-parallel-route-slots.test.ts index 2538eaa23a08..4e5384fa1579 100644 --- a/test/e2e/app-dir/incompatible-parallel-route-slots/incompatible-parallel-route-slots.test.ts +++ b/test/e2e/app-dir/incompatible-parallel-route-slots/incompatible-parallel-route-slots.test.ts @@ -53,6 +53,7 @@ describe('incompatible-parallel-route-slots', () => { expect(`${stripAnsi(next.cliOutput)}\n${response}`).not.toContain( 'strict route matching retained the incomplete route matcher' ) + expectDefaultStrictRouteMatchingWarning(next.cliOutput) } else { const { exitCode, cliOutput } = await next.build() expect(exitCode).toBe(1) @@ -71,10 +72,17 @@ describe('incompatible-parallel-route-slots', () => { expect(stripAnsi(cliOutput)).not.toContain( 'strict route matching retained the incomplete route matcher' ) + expectDefaultStrictRouteMatchingWarning(cliOutput) } }) }) +function expectDefaultStrictRouteMatchingWarning(output: string): void { + expect(stripAnsi(output)).toContain( + 'you can temporarily restore loose route matching by setting `deprecated.looseRouteMatching` to `true`' + ) +} + function extractIncompatibleSlotsError(output: string): string { const normalizedOutput = stripAnsi(output) const start = normalizedOutput.indexOf( diff --git a/test/e2e/app-dir/incompatible-parallel-route-slots/next.config.js b/test/e2e/app-dir/incompatible-parallel-route-slots/next.config.js deleted file mode 100644 index ff01ea9b86a3..000000000000 --- a/test/e2e/app-dir/incompatible-parallel-route-slots/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const nextConfig = { - experimental: { - strictRouteMatching: true, - }, -} - -module.exports = nextConfig diff --git a/test/e2e/app-dir/interception-dynamic-segment/next.config.js b/test/e2e/app-dir/interception-dynamic-segment/next.config.js index cce199489275..1a89f53f4774 100644 --- a/test/e2e/app-dir/interception-dynamic-segment/next.config.js +++ b/test/e2e/app-dir/interception-dynamic-segment/next.config.js @@ -7,10 +7,6 @@ const nextConfig = { // intercepted layouts omit children, while a real children branch still // uses the marker needed to preserve its active state during navigation. explicitParallelRouteChildren: true, - // Interception catch-alls retain every sibling owned by the host layout. - // Strict matching must not mistake those retained slots for incomplete - // route coverage and prune the interception matcher. - strictRouteMatching: true, }, } diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-scroll/default.js b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-scroll/default.js new file mode 100644 index 000000000000..039f188c1f6a --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-scroll/default.js @@ -0,0 +1,6 @@ +// `/parallel-scroll/nav` only has a page in @modal. Declare what a direct +// request should render for children instead of relying on the legacy +// synthesized not-found fallback that soft navigation happened to hide. +export default function Default() { + return null +} diff --git a/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/README.md b/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/README.md index e684a1492a7a..7ca6c1ddc1c4 100644 --- a/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/README.md +++ b/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/README.md @@ -2,6 +2,11 @@ This fixture contains scenarios that **SHOULD throw `MissingDefaultParallelRouteError`** during build. +It opts out of strict route matching because strict matching removes these +incomplete routes before the legacy loader-tree validation can run. Once strict +matching is the only mode, this fixture should be removed in favor of the +equivalent strict diagnostic. + ## Why These Should Error All scenarios in this fixture have: diff --git a/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/next.config.js b/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/next.config.js new file mode 100644 index 000000000000..b1308c64fc24 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/build-error/next.config.js @@ -0,0 +1,12 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + deprecated: { + // This fixture intentionally exercises the legacy loader-tree validation + // for incomplete parallel routes. Strict matching removes those routes + // before that validation can run. Once strict matching is the only mode, + // this fixture should be removed in favor of the strict diagnostic. + looseRouteMatching: true, + }, +} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/no-build-error/next.config.js b/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/no-build-error/next.config.js index bae6c3e02525..f138469ab4b3 100644 --- a/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/no-build-error/next.config.js +++ b/test/e2e/app-dir/parallel-routes-leaf-segments/fixtures/no-build-error/next.config.js @@ -1,10 +1,10 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - experimental: { + deprecated: { // This fixture exercises the legacy missing-default validation directly. // Keep that coverage if strict route matching becomes the default; once // strict matching is the only mode, this fixture can be removed. - strictRouteMatching: false, + looseRouteMatching: true, }, } diff --git a/test/e2e/app-dir/parallel-routes-pruned-matchers/next.config.js b/test/e2e/app-dir/parallel-routes-pruned-matchers/next.config.js deleted file mode 100644 index ff01ea9b86a3..000000000000 --- a/test/e2e/app-dir/parallel-routes-pruned-matchers/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const nextConfig = { - experimental: { - strictRouteMatching: true, - }, -} - -module.exports = nextConfig diff --git a/test/e2e/app-dir/parallel-routes-revalidation/app/nested-revalidate/default.tsx b/test/e2e/app-dir/parallel-routes-revalidation/app/nested-revalidate/default.tsx new file mode 100644 index 000000000000..26e2c4ebec3c --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-revalidation/app/nested-revalidate/default.tsx @@ -0,0 +1,6 @@ +// The drawer and modal URLs only match their respective named slots. This +// explicit fallback makes those URLs complete on a direct request while soft +// navigation can continue to preserve the active children page. +export default function Default() { + return null +} diff --git a/test/e2e/app-dir/unmatched-app-pages/next.config.js b/test/e2e/app-dir/unmatched-app-pages/next.config.js deleted file mode 100644 index ff01ea9b86a3..000000000000 --- a/test/e2e/app-dir/unmatched-app-pages/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const nextConfig = { - experimental: { - strictRouteMatching: true, - }, -} - -module.exports = nextConfig diff --git a/test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts b/test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts index 58dc2455f46d..5131ecef3c45 100644 --- a/test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts +++ b/test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts @@ -47,6 +47,8 @@ describe('unmatched-app-pages', () => { } `) } + + expectDefaultStrictRouteMatchingWarning(next.cliOutput) } else { const { exitCode, cliOutput } = await next.build() expect(exitCode).toBe(1) @@ -64,10 +66,17 @@ describe('unmatched-app-pages', () => { Every page must be part of at least one complete route. Add matching pages or default files for the sibling parallel route slots, or remove the unreachable pages." `) + expectDefaultStrictRouteMatchingWarning(cliOutput) } }) }) +function expectDefaultStrictRouteMatchingWarning(output: string): void { + expect(stripAnsi(output)).toContain( + 'you can temporarily restore loose route matching by setting `deprecated.looseRouteMatching` to `true`' + ) +} + function extractUnmatchedPagesError(output: string): string { const normalizedOutput = stripAnsi(output) const start = normalizedOutput.indexOf( diff --git a/test/production/app-dir/parallel-routes-static/app/nested/default.tsx b/test/production/app-dir/parallel-routes-static/app/nested/default.tsx new file mode 100644 index 000000000000..3303b0fda547 --- /dev/null +++ b/test/production/app-dir/parallel-routes-static/app/nested/default.tsx @@ -0,0 +1,8 @@ +import { notFound } from 'next/navigation' + +// The layout intentionally does not render children for /nested/foo or +// /nested/bar. An explicit not-found default keeps that choice valid under +// strict matching without changing the rendered named slots. +export default function Default() { + notFound() +} From 72858d7178162ff0d51fc54f37a1dbc8d1040a45 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 21 Sep 2026 09:29:03 -0700 Subject: [PATCH 4/5] Split revalidation errors by execution context (#98993) ## Summary - Narrow build-time generator names to a literal type that can be extended as new generators are added. - Split the combined revalidation diagnostic into render, cached-function, and build-time-generator errors so each failure reports only its actual context and never needs a fallback generator name. - Cover all three contexts in the existing development and production test suites. Follow-up to the review feedback on #98891. ## Verification - `HEADLESS=true pnpm test-dev-webpack test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts` (3 tests) - `HEADLESS=true pnpm test-start-webpack test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts` (3 tests) - Production Webpack `generate-static-params-errors` suite (6 tests) - `pnpm --filter=next types` --- .../work-unit-async-storage.external.ts | 4 ++- .../server/use-cache/use-cache-messages.ts | 24 +++++++++++++++-- .../server/web/spec-extension/revalidate.ts | 15 ++++++----- .../app-dir/revalidatetag-rsc/app/page.tsx | 3 +++ .../app/revalidate_via_cache/page.tsx | 13 ++++++++++ .../revalidatetag-rsc.test.ts | 26 +++++++++++++++++-- .../app/[lang]/revalidate-tag/[slug]/page.tsx | 10 +++++++ .../generate-static-params-errors.test.ts | 7 +++++ 8 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_cache/page.tsx create mode 100644 test/production/app-dir/generate-static-params-errors/app/[lang]/revalidate-tag/[slug]/page.tsx 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 d7b23bbcb91a..01a3c73c0d94 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 @@ -436,10 +436,12 @@ export interface UnstableCacheStore extends CommonCacheStore { */ export type CacheStore = UseCacheStore | UnstableCacheStore +export type BuildTimeGeneratorName = 'generateStaticParams' + export interface BuildTimeGeneratorStore extends CommonWorkUnitStore { readonly type: 'build-time-generator' /** The public export name to use in diagnostics, regardless of bundling. */ - readonly functionName: string + readonly functionName: BuildTimeGeneratorName readonly rootParams: Params } diff --git a/packages/next/src/server/use-cache/use-cache-messages.ts b/packages/next/src/server/use-cache/use-cache-messages.ts index 8c21f8c3f765..5955c10ca379 100644 --- a/packages/next/src/server/use-cache/use-cache-messages.ts +++ b/packages/next/src/server/use-cache/use-cache-messages.ts @@ -4,6 +4,8 @@ * immediate fix, then link to the relevant docs. */ +import type { BuildTimeGeneratorName } from '../app-render/work-unit-async-storage.external' + const NEXT_REQUEST_IN_USE_CACHE = 'https://nextjs.org/docs/messages/next-request-in-use-cache' @@ -122,12 +124,30 @@ export function createDraftModeMutationInUnstableCacheError( } export function createRevalidateDuringRenderError( + route: string, + expression: string +): Error { + return new Error( + `Route "${route}": \`${expression}\` can't be called during render. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}` + ) +} + +export function createRevalidateInCachedFunctionError( + route: string, + expression: string +): Error { + return new Error( + `Route "${route}": \`${expression}\` can't be called inside a cached function. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}` + ) +} + +export function createRevalidateInBuildTimeGeneratorError( route: string, expression: string, - generatorName?: string + generatorName: BuildTimeGeneratorName ): Error { return new Error( - `Route "${route}": \`${expression}\` can't be called during render, inside a cached function, or inside \`${generatorName ?? 'generateStaticParams'}\`. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}` + `Route "${route}": \`${expression}\` can't be called inside \`${generatorName}\`. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}` ) } diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index ee3c2af0e8e4..b12cbd75aaa4 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -14,7 +14,11 @@ import { } from '../../../shared/lib/action-revalidation-kind' import { removeTrailingSlash } from '../../../shared/lib/router/utils/remove-trailing-slash' import { encodeHeaderSafe } from '../../lib/encode-header-safe' -import { createRevalidateDuringRenderError } from '../../use-cache/use-cache-messages' +import { + createRevalidateDuringRenderError, + createRevalidateInBuildTimeGeneratorError, + createRevalidateInCachedFunctionError, +} from '../../use-cache/use-cache-messages' import { validateAndNormalizeCacheLifeProfile } from '../../use-cache/cache-life-profile' type CacheLifeConfig = { @@ -145,10 +149,10 @@ function revalidate( const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { - // Keep the generator's name before applying the general render-phase check. + // Prefer the more specific contexts over the general render-phase error. switch (workUnitStore.type) { case 'build-time-generator': - throw createRevalidateDuringRenderError( + throw createRevalidateInBuildTimeGeneratorError( store.route, expression, workUnitStore.functionName @@ -156,6 +160,7 @@ function revalidate( case 'cache': case 'private-cache': case 'unstable-cache': + throw createRevalidateInCachedFunctionError(store.route, expression) case 'prerender': case 'prerender-runtime': case 'prerender-client': @@ -172,10 +177,6 @@ function revalidate( } switch (workUnitStore.type) { - case 'cache': - case 'private-cache': - case 'unstable-cache': - throw createRevalidateDuringRenderError(store.route, expression) case 'prerender': case 'prerender-runtime': // cacheComponents Prerender diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx index e4ced2724de3..d95c208d2b2e 100644 --- a/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx +++ b/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx @@ -19,6 +19,9 @@ export default async function Page() { Revalidate via page + + Revalidate via cache + ) } diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_cache/page.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_cache/page.tsx new file mode 100644 index 000000000000..43d066ff7146 --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_cache/page.tsx @@ -0,0 +1,13 @@ +import { revalidateTag, unstable_cache } from 'next/cache' + +export const dynamic = 'force-dynamic' + +const revalidateInCache = unstable_cache(async () => { + revalidateTag('data', 'max') +}) + +export default async function Page() { + await revalidateInCache() + + return

revalidated

+} diff --git a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts index f115886a664e..cdaf8396da1f 100644 --- a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts +++ b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts @@ -30,7 +30,7 @@ describe('revalidateTag-rsc', () => { if (isNextDev) { await waitForRedbox(browser) await expect(getRedboxHeader(browser)).resolves.toContain( - 'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render, inside a cached function, or inside `generateStaticParams`.' + 'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render.' ) } else { await retry(async () => { @@ -41,7 +41,29 @@ describe('revalidateTag-rsc', () => { } expect(next.cliOutput).toContain( - 'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render, inside a cached function, or inside `generateStaticParams`. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache' + 'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache' + ) + }) + + it('should error if revalidateTag is called inside a cached function', async () => { + const browser = await next.browser('/') + await browser.elementByCss('#revalidate-via-cache').click() + + if (isNextDev) { + await waitForRedbox(browser) + await expect(getRedboxHeader(browser)).resolves.toContain( + 'Route "/revalidate_via_cache": `revalidateTag("data")` can\'t be called inside a cached function.' + ) + } else { + await retry(async () => { + expect( + await browser.eval('document.documentElement.innerHTML') + ).toContain('This page couldn\u2019t load') + }) + } + + expect(next.cliOutput).toContain( + 'Route "/revalidate_via_cache": `revalidateTag("data")` can\'t be called inside a cached function. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache' ) }) } diff --git a/test/production/app-dir/generate-static-params-errors/app/[lang]/revalidate-tag/[slug]/page.tsx b/test/production/app-dir/generate-static-params-errors/app/[lang]/revalidate-tag/[slug]/page.tsx new file mode 100644 index 000000000000..0ad1356930dd --- /dev/null +++ b/test/production/app-dir/generate-static-params-errors/app/[lang]/revalidate-tag/[slug]/page.tsx @@ -0,0 +1,10 @@ +import { revalidateTag } from 'next/cache' + +export default function Page() { + return

revalidate-tag

+} + +export async function generateStaticParams() { + revalidateTag('data', 'max') + return [{ slug: 'test' }] +} diff --git a/test/production/app-dir/generate-static-params-errors/generate-static-params-errors.test.ts b/test/production/app-dir/generate-static-params-errors/generate-static-params-errors.test.ts index a2a9dc2489a1..73096e6294b3 100644 --- a/test/production/app-dir/generate-static-params-errors/generate-static-params-errors.test.ts +++ b/test/production/app-dir/generate-static-params-errors/generate-static-params-errors.test.ts @@ -47,6 +47,13 @@ describe('generate-static-params-errors', () => { ) }) + it('should error when revalidateTag() is called inside generateStaticParams', async () => { + await buildRoute('app/[lang]/revalidate-tag/[slug]/page.tsx') + expect(getCliOutput()).toContain( + 'Error: Route "/[lang]/revalidate-tag/[slug]": `revalidateTag("data")` can\'t be called inside `generateStaticParams`. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache' + ) + }) + it('should allow root params access inside generateStaticParams', async () => { await buildRoute('app/[lang]/root-params/[slug]/page.tsx') expect(getCliOutput()).not.toContain('Error') From 0f3a2bef2692e785b1d59579241a1f00df1118bc Mon Sep 17 00:00:00 2001 From: Jamiboy Mohammad Date: Mon, 21 Sep 2026 10:08:19 -0700 Subject: [PATCH 5/5] test: migrate legacy deployment exclusion callers (#98473) ## Summary Migrate the shared cache-components error tests and two additional legacy deployment exclusions from `skipDeployment`/`skipped` to test gates, so these callers can land on canary before the API is removed. Place the cache-error gate on the existing named `describe` boundary in each of the 12 `.test.ts` entry files. Jest transforms gate pragmas in test files; a gate in the shared utility would be inert. The helper continues registering the same tests inside those entry-level suites, preserving their names and snapshots. These tests inspect local prerender builds and cannot run against a deployment. The other two callers cover segment-cache fixture mutation and production output-file tracing. This is a standalone preparatory migration, not the removal of the legacy API itself. ## Verification - Rebased onto canary at `ccef3db535`; the stable patch ID matches the original migration exactly. - On the rebased tree, all 77 gate infrastructure unit tests passed. - Deploy-mode Jest collection of all 12 cache-error entry files: 236 skipped, zero executed, and no deployment started. - Prettier, ESLint, and diff whitespace checks passed. - Full bootstrap attempted but blocked by missing package-level dependencies (`microbundle`) in the temporary worktree; no full dev/start integration run is claimed. --- ...or-attribution.partial-prefetching.test.ts | 6 +- .../error-attribution.test.ts | 6 +- ...a-and-viewport.partial-prefetching.test.ts | 6 +- .../metadata-and-viewport.test.ts | 6 +- .../cache-components-errors/shared.util.ts | 124 ++++++++---------- .../sync-dynamic.partial-prefetching.test.ts | 10 +- .../sync-dynamic.test.ts | 10 +- ...io-node-crypto.partial-prefetching.test.ts | 6 +- .../sync-io-node-crypto.test.ts | 6 +- ...ime-and-random.partial-prefetching.test.ts | 6 +- .../sync-io-time-and-random.test.ts | 6 +- .../use-cache.partial-prefetching.test.ts | 10 +- .../cache-components-errors/use-cache.test.ts | 10 +- .../prefetch-app-shell-revalidation.test.ts | 7 +- ...ut-file-tracing-includes-read-glob.test.ts | 7 +- 15 files changed, 134 insertions(+), 92 deletions(-) diff --git a/test/e2e/app-dir/cache-components-errors/error-attribution.partial-prefetching.test.ts b/test/e2e/app-dir/cache-components-errors/error-attribution.partial-prefetching.test.ts index 8e5f6df734ec..f8612747124b 100644 --- a/test/e2e/app-dir/cache-components-errors/error-attribution.partial-prefetching.test.ts +++ b/test/e2e/app-dir/cache-components-errors/error-attribution.partial-prefetching.test.ts @@ -3,4 +3,8 @@ import { registerErrorAttributionTests } from './error-attribution.util' process.env.__NEXT_PARTIAL_PREFETCHING = 'true' -runCacheComponentsErrorsTests(registerErrorAttributionTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerErrorAttributionTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/error-attribution.test.ts b/test/e2e/app-dir/cache-components-errors/error-attribution.test.ts index a1054da7e4a9..8c2f617901f4 100644 --- a/test/e2e/app-dir/cache-components-errors/error-attribution.test.ts +++ b/test/e2e/app-dir/cache-components-errors/error-attribution.test.ts @@ -1,4 +1,8 @@ import { runCacheComponentsErrorsTests } from './shared.util' import { registerErrorAttributionTests } from './error-attribution.util' -runCacheComponentsErrorsTests(registerErrorAttributionTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerErrorAttributionTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.partial-prefetching.test.ts b/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.partial-prefetching.test.ts index d9bfa6be3708..27a688eab25f 100644 --- a/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.partial-prefetching.test.ts +++ b/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.partial-prefetching.test.ts @@ -3,4 +3,8 @@ import { registerMetadataAndViewportTests } from './metadata-and-viewport.util' process.env.__NEXT_PARTIAL_PREFETCHING = 'true' -runCacheComponentsErrorsTests(registerMetadataAndViewportTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerMetadataAndViewportTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.test.ts b/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.test.ts index 74bddbb0f15d..4c62a32963b5 100644 --- a/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.test.ts +++ b/test/e2e/app-dir/cache-components-errors/metadata-and-viewport.test.ts @@ -1,4 +1,8 @@ import { runCacheComponentsErrorsTests } from './shared.util' import { registerMetadataAndViewportTests } from './metadata-and-viewport.util' -runCacheComponentsErrorsTests(registerMetadataAndViewportTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerMetadataAndViewportTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/shared.util.ts b/test/e2e/app-dir/cache-components-errors/shared.util.ts index 521c65b14ae9..d18a00e33fe6 100644 --- a/test/e2e/app-dir/cache-components-errors/shared.util.ts +++ b/test/e2e/app-dir/cache-components-errors/shared.util.ts @@ -14,73 +14,50 @@ export interface CacheComponentsErrorsContext { // into one `*.test.ts` entry file per group of sections (each with a // `.partial-prefetching` variant), all sharing this wrapper. Each entry // boots its own server (and, in `next start` mode, runs its own builds). -// Snapshots can be updated with the sibling update-snapshots.sh script. +// Each entry owns its gated describe because pragmas are only transformed in +// test files. Snapshots can be updated with the sibling update-snapshots.sh script. export function runCacheComponentsErrorsTests( registerTests: (ctx: CacheComponentsErrorsContext) => void ) { - describe('Cache Components Errors', () => { - const { next, isTurbopack, isNextStart, isRspack, skipped } = nextTestSetup( - { - files: __dirname + '/fixtures/default', - skipStart: !isNextDev, - // TODO(deploy-test-completion): Re-enable this suite in deploy mode. - // No deploy-specific incompatibility is documented. - skipDeployment: true, - } - ) - - if (skipped) return - - afterEach(async () => { - if (isNextStart) { - await next.stop() - } - }) - - const testCases: { isDebugPrerender: boolean; name: string }[] = [] + const { next, isTurbopack, isNextStart, isRspack } = nextTestSetup({ + files: __dirname + '/fixtures/default', + skipStart: !isNextDev, + }) - if (isNextDev) { - testCases.push({ isDebugPrerender: false, name: 'Dev' }) - } else { - const prerenderMode = process.env.NEXT_TEST_DEBUG_PRERENDER - // The snapshots can't be created for both modes at the same time because of - // an issue in the typescript plugin for prettier. Defining - // NEXT_TEST_DEBUG_PRERENDER allows us to run them sequentially, when we - // need to update the snapshots. - if (!prerenderMode || prerenderMode === 'true') { - testCases.push({ - isDebugPrerender: true, - name: 'Build With --prerender-debug', - }) - } - if (!prerenderMode || prerenderMode === 'false') { - testCases.push({ - isDebugPrerender: false, - name: 'Build Without --prerender-debug', - }) - } + afterEach(async () => { + if (isNextStart) { + await next.stop() } + }) - describe.each(testCases)('$name', ({ isDebugPrerender }) => { - beforeAll(async () => { - if (isNextStart) { - const args = ['--experimental-build-mode', 'compile'] - - if (isDebugPrerender) { - args.push('--debug-prerender') - } + const testCases: { isDebugPrerender: boolean; name: string }[] = [] - await next.build({ args }) - } + if (isNextDev) { + testCases.push({ isDebugPrerender: false, name: 'Dev' }) + } else { + const prerenderMode = process.env.NEXT_TEST_DEBUG_PRERENDER + // The snapshots can't be created for both modes at the same time because of + // an issue in the typescript plugin for prettier. Defining + // NEXT_TEST_DEBUG_PRERENDER allows us to run them sequentially, when we + // need to update the snapshots. + if (!prerenderMode || prerenderMode === 'true') { + testCases.push({ + isDebugPrerender: true, + name: 'Build With --prerender-debug', + }) + } + if (!prerenderMode || prerenderMode === 'false') { + testCases.push({ + isDebugPrerender: false, + name: 'Build Without --prerender-debug', }) + } + } - const prerender = async (pathname: string) => { - const args = [ - '--experimental-build-mode', - 'generate', - '--debug-build-paths', - `app${pathname}/page.tsx`, - ] + describe.each(testCases)('$name', ({ isDebugPrerender }) => { + beforeAll(async () => { + if (isNextStart) { + const args = ['--experimental-build-mode', 'compile'] if (isDebugPrerender) { args.push('--debug-prerender') @@ -88,15 +65,30 @@ export function runCacheComponentsErrorsTests( await next.build({ args }) } + }) + + const prerender = async (pathname: string) => { + const args = [ + '--experimental-build-mode', + 'generate', + '--debug-build-paths', + `app${pathname}/page.tsx`, + ] + + if (isDebugPrerender) { + args.push('--debug-prerender') + } - registerTests({ - next, - isTurbopack, - isRspack, - isNextStart, - isDebugPrerender, - prerender, - }) + await next.build({ args }) + } + + registerTests({ + next, + isTurbopack, + isRspack, + isNextStart, + isDebugPrerender, + prerender, }) }) } diff --git a/test/e2e/app-dir/cache-components-errors/sync-dynamic.partial-prefetching.test.ts b/test/e2e/app-dir/cache-components-errors/sync-dynamic.partial-prefetching.test.ts index 5e1729de25be..e84a415fc576 100644 --- a/test/e2e/app-dir/cache-components-errors/sync-dynamic.partial-prefetching.test.ts +++ b/test/e2e/app-dir/cache-components-errors/sync-dynamic.partial-prefetching.test.ts @@ -7,7 +7,11 @@ process.env.__NEXT_PARTIAL_PREFETCHING = 'true' // Registers two section groups in one entry: dynamic-root-and-boundary is too // small to justify its own CI test file (per-file server boot and, in start // mode, build costs). -runCacheComponentsErrorsTests((ctx) => { - registerDynamicRootAndBoundaryTests(ctx) - registerSyncDynamicTests(ctx) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests((ctx) => { + registerDynamicRootAndBoundaryTests(ctx) + registerSyncDynamicTests(ctx) + }) }) diff --git a/test/e2e/app-dir/cache-components-errors/sync-dynamic.test.ts b/test/e2e/app-dir/cache-components-errors/sync-dynamic.test.ts index 818e7078d9f2..bce0aef8094e 100644 --- a/test/e2e/app-dir/cache-components-errors/sync-dynamic.test.ts +++ b/test/e2e/app-dir/cache-components-errors/sync-dynamic.test.ts @@ -5,7 +5,11 @@ import { registerSyncDynamicTests } from './sync-dynamic.util' // Registers two section groups in one entry: dynamic-root-and-boundary is too // small to justify its own CI test file (per-file server boot and, in start // mode, build costs). -runCacheComponentsErrorsTests((ctx) => { - registerDynamicRootAndBoundaryTests(ctx) - registerSyncDynamicTests(ctx) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests((ctx) => { + registerDynamicRootAndBoundaryTests(ctx) + registerSyncDynamicTests(ctx) + }) }) diff --git a/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.partial-prefetching.test.ts b/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.partial-prefetching.test.ts index 74f91a71c52e..4bfe132fe4f4 100644 --- a/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.partial-prefetching.test.ts +++ b/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.partial-prefetching.test.ts @@ -3,4 +3,8 @@ import { registerSyncIoNodeCryptoTests } from './sync-io-node-crypto.util' process.env.__NEXT_PARTIAL_PREFETCHING = 'true' -runCacheComponentsErrorsTests(registerSyncIoNodeCryptoTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerSyncIoNodeCryptoTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.test.ts b/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.test.ts index ff8ec9f42643..8977cf579085 100644 --- a/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.test.ts +++ b/test/e2e/app-dir/cache-components-errors/sync-io-node-crypto.test.ts @@ -1,4 +1,8 @@ import { runCacheComponentsErrorsTests } from './shared.util' import { registerSyncIoNodeCryptoTests } from './sync-io-node-crypto.util' -runCacheComponentsErrorsTests(registerSyncIoNodeCryptoTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerSyncIoNodeCryptoTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.partial-prefetching.test.ts b/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.partial-prefetching.test.ts index 2592484ecf5f..eff819ee1590 100644 --- a/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.partial-prefetching.test.ts +++ b/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.partial-prefetching.test.ts @@ -3,4 +3,8 @@ import { registerSyncIoTimeAndRandomTests } from './sync-io-time-and-random.util process.env.__NEXT_PARTIAL_PREFETCHING = 'true' -runCacheComponentsErrorsTests(registerSyncIoTimeAndRandomTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerSyncIoTimeAndRandomTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.test.ts b/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.test.ts index 346f60730cfc..a52cb649fcc5 100644 --- a/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.test.ts +++ b/test/e2e/app-dir/cache-components-errors/sync-io-time-and-random.test.ts @@ -1,4 +1,8 @@ import { runCacheComponentsErrorsTests } from './shared.util' import { registerSyncIoTimeAndRandomTests } from './sync-io-time-and-random.util' -runCacheComponentsErrorsTests(registerSyncIoTimeAndRandomTests) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests(registerSyncIoTimeAndRandomTests) +}) diff --git a/test/e2e/app-dir/cache-components-errors/use-cache.partial-prefetching.test.ts b/test/e2e/app-dir/cache-components-errors/use-cache.partial-prefetching.test.ts index a5c32a2f5c55..804ec20f361c 100644 --- a/test/e2e/app-dir/cache-components-errors/use-cache.partial-prefetching.test.ts +++ b/test/e2e/app-dir/cache-components-errors/use-cache.partial-prefetching.test.ts @@ -7,7 +7,11 @@ process.env.__NEXT_PARTIAL_PREFETCHING = 'true' // Registers two section groups in one entry: use-cache-private is too small // to justify its own CI test file (per-file server boot and, in start mode, // build costs). -runCacheComponentsErrorsTests((ctx) => { - registerUseCacheTests(ctx) - registerUseCachePrivateTests(ctx) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests((ctx) => { + registerUseCacheTests(ctx) + registerUseCachePrivateTests(ctx) + }) }) diff --git a/test/e2e/app-dir/cache-components-errors/use-cache.test.ts b/test/e2e/app-dir/cache-components-errors/use-cache.test.ts index 412fb4f38377..237f0d8be120 100644 --- a/test/e2e/app-dir/cache-components-errors/use-cache.test.ts +++ b/test/e2e/app-dir/cache-components-errors/use-cache.test.ts @@ -5,7 +5,11 @@ import { registerUseCachePrivateTests } from './use-cache-private.util' // Registers two section groups in one entry: use-cache-private is too small // to justify its own CI test file (per-file server boot and, in start mode, // build costs). -runCacheComponentsErrorsTests((ctx) => { - registerUseCacheTests(ctx) - registerUseCachePrivateTests(ctx) +// These tests run local builds to inspect prerender error diagnostics. +// @force-gate !deploy +describe('Cache Components Errors', () => { + runCacheComponentsErrorsTests((ctx) => { + registerUseCacheTests(ctx) + registerUseCachePrivateTests(ctx) + }) }) diff --git a/test/e2e/app-dir/segment-cache/prefetch-app-shell-revalidation/prefetch-app-shell-revalidation.test.ts b/test/e2e/app-dir/segment-cache/prefetch-app-shell-revalidation/prefetch-app-shell-revalidation.test.ts index b2344d1a70fa..277b688409d2 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-app-shell-revalidation/prefetch-app-shell-revalidation.test.ts +++ b/test/e2e/app-dir/segment-cache/prefetch-app-shell-revalidation/prefetch-app-shell-revalidation.test.ts @@ -17,12 +17,13 @@ const REPRODUCE_MISSING_RUNTIME_SHELL_FOLLOW_UP = const REPRODUCE_STATIC_PAGE_UPGRADE_BUG = !!process.env.REPRODUCE_STATIC_PAGE_UPGRADE_BUG || false +// The fixture rewrites value.json during requests; deployments do not provide +// a writable, shared application filesystem. +// @force-gate !deploy describe('App Shell revalidation', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, // modifies files at runtime }) - if (skipped) return if (isNextDev) { it('is skipped', () => {}) return diff --git a/test/production/app-dir/output-file-tracing-includes-read-glob/output-file-tracing-includes-read-glob.test.ts b/test/production/app-dir/output-file-tracing-includes-read-glob/output-file-tracing-includes-read-glob.test.ts index f741aa47feaf..2cd694b84b56 100644 --- a/test/production/app-dir/output-file-tracing-includes-read-glob/output-file-tracing-includes-read-glob.test.ts +++ b/test/production/app-dir/output-file-tracing-includes-read-glob/output-file-tracing-includes-read-glob.test.ts @@ -1,16 +1,17 @@ import path from 'path' import { nextTestSetup } from 'e2e-utils' +// Runs a local build and reads its page.js.nft.json file to verify traced files. +// Deployment mode does not expose those local build artifacts. +// @force-gate !deploy describe('outputFileTracingIncludes read glob', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, dependencies: { 'lightningcss-wasm': '1.28.2', }, skipStart: true, - skipDeployment: true, }) - if (skipped) return it('traces a file from the monorepo node_modules directory', async () => { const wasmPath = require.resolve(