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/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/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/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/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..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,8 +436,12 @@ export interface UnstableCacheStore extends CommonCacheStore { */ export type CacheStore = UseCacheStore | UnstableCacheStore -export interface GenerateStaticParamsStore extends CommonWorkUnitStore { - readonly type: 'generate-static-params' +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: BuildTimeGeneratorName readonly rootParams: Params } @@ -445,7 +449,7 @@ export type WorkUnitStore = | RequestStore | CacheStore | PrerenderStore - | GenerateStaticParamsStore + | BuildTimeGeneratorStore export function willConsumerServerCache( workUnitStore: WorkUnitStore | undefined @@ -466,7 +470,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 +510,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 +532,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 +555,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 +580,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 +609,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 +633,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 +660,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 +682,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/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/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/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}` } 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/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/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 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/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/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/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/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/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') 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( 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() +}