From fd8a8dd2f1a585851f44961e2a96b53f26f43640 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 25 Sep 2024 10:52:12 -0700 Subject: [PATCH] refactor: added more strict app segment config parsing --- .../build/app-segments/app-segment-config.ts | 125 +++++ .../app-segments/collect-app-segments.ts | 182 ++++++++ packages/next/src/build/index.ts | 5 +- packages/next/src/build/utils.ts | 430 +++++++----------- ...tatic-generation-async-storage.external.ts | 9 +- .../next/src/lib/metadata/resolve-metadata.ts | 5 +- .../app-render/create-component-tree.tsx | 2 +- .../with-static-generation-store.ts | 3 +- .../src/server/dev/static-paths-worker.ts | 34 +- .../next/src/server/lib/app-dir-module.ts | 14 +- .../server/route-modules/app-route/module.ts | 12 +- packages/next/types/$$compiled.internal.d.ts | 4 +- .../app/dynamic-param-edge/[slug]/page.tsx | 4 +- 13 files changed, 501 insertions(+), 328 deletions(-) create mode 100644 packages/next/src/build/app-segments/app-segment-config.ts create mode 100644 packages/next/src/build/app-segments/collect-app-segments.ts diff --git a/packages/next/src/build/app-segments/app-segment-config.ts b/packages/next/src/build/app-segments/app-segment-config.ts new file mode 100644 index 0000000000000..b82623a9a100d --- /dev/null +++ b/packages/next/src/build/app-segments/app-segment-config.ts @@ -0,0 +1,125 @@ +import { z } from 'next/dist/compiled/zod' + +/** + * The schema for configuration for a page. + * + * @internal + */ +export const AppSegmentConfigSchema = z.object({ + /** + * The number of seconds to revalidate the page or false to disable revalidation. + */ + revalidate: z + .union([z.number().int().nonnegative(), z.literal(false)]) + .optional(), + + /** + * Whether the page supports dynamic parameters. + */ + dynamicParams: z.boolean().optional(), + + /** + * The dynamic behavior of the page. + */ + dynamic: z + .enum(['auto', 'error', 'force-static', 'force-dynamic']) + .optional(), + + /** + * The caching behavior of the page. + */ + fetchCache: z + .enum([ + 'auto', + 'default-cache', + 'only-cache', + 'force-cache', + 'force-no-store', + 'default-no-store', + 'only-no-store', + ]) + .optional(), + + /** + * The preferred region for the page. + */ + preferredRegion: z.union([z.string(), z.array(z.string())]).optional(), + + /** + * Whether the page supports partial prerendering. When true, the page will be + * served using partial prerendering. This setting will only take affect if + * it's enabled via the `experimental.ppr = "incremental"` option. + */ + experimental_ppr: z.boolean().optional(), + + /** + * The runtime to use for the page. + */ + runtime: z.enum(['edge', 'nodejs', 'experimental-edge']).optional(), + + /** + * The maximum duration for the page in seconds. + */ + maxDuration: z.number().int().nonnegative().optional(), +}) + +/** + * The configuration for a page. + */ +export type AppSegmentConfig = { + /** + * The revalidation period for the page in seconds, or false to disable ISR. + */ + revalidate?: number | false + + /** + * Whether the page supports dynamic parameters. + */ + dynamicParams?: boolean + + /** + * The dynamic behavior of the page. + */ + dynamic?: 'auto' | 'error' | 'force-static' | 'force-dynamic' + + /** + * The caching behavior of the page. + */ + fetchCache?: + | 'auto' + | 'default-cache' + | 'default-no-store' + | 'force-cache' + | 'force-no-store' + | 'only-cache' + | 'only-no-store' + + /** + * The preferred region for the page. + */ + preferredRegion?: string | string[] + + /** + * Whether the page supports partial prerendering. When true, the page will be + * served using partial prerendering. This setting will only take affect if + * it's enabled via the `experimental.ppr = "incremental"` option. + */ + experimental_ppr?: boolean + + /** + * The runtime to use for the page. + */ + runtime?: 'edge' | 'nodejs' | 'experimental-edge' + + /** + * The maximum duration for the page in seconds. + */ + maxDuration?: number +} + +/** + * The keys of the configuration for a page. + * + * @internal + */ +export const AppSegmentConfigSchemaKeys = AppSegmentConfigSchema.keyof().options diff --git a/packages/next/src/build/app-segments/collect-app-segments.ts b/packages/next/src/build/app-segments/collect-app-segments.ts new file mode 100644 index 0000000000000..3ba166a53189b --- /dev/null +++ b/packages/next/src/build/app-segments/collect-app-segments.ts @@ -0,0 +1,182 @@ +import type { LoadComponentsReturnType } from '../../server/load-components' +import type { Params } from '../../server/request/params' +import type { + AppPageRouteModule, + AppPageModule, +} from '../../server/route-modules/app-page/module.compiled' +import type { + AppRouteRouteModule, + AppRouteModule, +} from '../../server/route-modules/app-route/module.compiled' +import { + type AppSegmentConfig, + AppSegmentConfigSchema, +} from './app-segment-config' + +import { InvariantError } from '../../shared/lib/invariant-error' +import { + isAppRouteRouteModule, + isAppPageRouteModule, +} from '../../server/route-modules/checks' +import { isClientReference } from '../../lib/client-reference' +import { getSegmentParam } from '../../server/app-render/get-segment-param' +import { getLayoutOrPageModule } from '../../server/lib/app-dir-module' + +type GenerateStaticParams = (options: { params?: Params }) => Promise + +/** + * Filters out segments that don't contribute to static generation. + * + * @param segments the segments to filter + * @returns the filtered segments + */ +function filterSegments(segments: AppSegment[]) { + return segments.filter((result) => { + return ( + result.config || result.generateStaticParams || result.isDynamicSegment + ) + }) +} + +/** + * Parses the app config and attaches it to the segment. + */ +function attach(segment: AppSegment, userland: unknown) { + // If the userland is not an object, then we can't do anything with it. + if (typeof userland !== 'object' || userland === null) { + return + } + + // Try to parse the application configuration. If there were any keys, attach + // it to the segment. + const config = AppSegmentConfigSchema.safeParse(userland) + if (config.success && Object.keys(config.data).length > 0) { + segment.config = config.data + } + + if ( + 'generateStaticParams' in userland && + typeof userland.generateStaticParams === 'function' + ) { + segment.generateStaticParams = + userland.generateStaticParams as GenerateStaticParams + } +} + +export type AppSegment = { + name: string + param: string | undefined + filePath: string | undefined + config: AppSegmentConfig | undefined + isDynamicSegment: boolean + generateStaticParams: GenerateStaticParams | undefined +} + +/** + * Walks the loader tree and collects the generate parameters for each segment. + * + * @param routeModule the app page route module + * @returns the segments for the app page route module + */ +async function collectAppPageSegments(routeModule: AppPageRouteModule) { + const segments: AppSegment[] = [] + + let current = routeModule.userland.loaderTree + while (current) { + const [name, parallelRoutes] = current + const { mod: userland, filePath } = await getLayoutOrPageModule(current) + + const isClientComponent: boolean = userland && isClientReference(userland) + const isDynamicSegment = /^\[.*\]$/.test(name) + const param = isDynamicSegment ? getSegmentParam(name)?.param : undefined + + const segment: AppSegment = { + name, + param, + filePath, + config: undefined, + isDynamicSegment, + generateStaticParams: undefined, + } + + // Only server components can have app segment configurations. If this isn't + // an object, then we should skip it. This can happen when parsing the + // error components. + if (!isClientComponent) { + attach(segment, userland) + } + + segments.push(segment) + + // Use this route's parallel route children as the next segment. + current = parallelRoutes.children + } + + return filterSegments(segments) +} + +/** + * Collects the segments for a given app route module. + * + * @param routeModule the app route module + * @returns the segments for the app route module + */ +function collectAppRouteSegments( + routeModule: AppRouteRouteModule +): AppSegment[] { + // Get the pathname parts, slice off the first element (which is empty). + const parts = routeModule.definition.pathname.split('/').slice(1) + if (parts.length === 0) { + throw new InvariantError('Expected at least one segment') + } + + // Generate all the segments. + const segments: AppSegment[] = parts.map((name) => { + const isDynamicSegment = /^\[.*\]$/.test(name) + const param = isDynamicSegment ? getSegmentParam(name)?.param : undefined + + return { + name, + param, + filePath: undefined, + isDynamicSegment, + config: undefined, + generateStaticParams: undefined, + } + }) + + // We know we have at least one, we verified this above. We should get the + // last segment which represents the root route module. + const segment = segments[segments.length - 1] + + segment.filePath = routeModule.definition.filename + + // Extract the segment config from the userland module. + attach(segment, routeModule.userland) + + return filterSegments(segments) +} + +/** + * Collects the segments for a given route module. + * + * @param components the loaded components + * @returns the segments for the route module + */ +export function collectSegments({ + routeModule, +}: LoadComponentsReturnType): + | Promise + | AppSegment[] { + if (isAppRouteRouteModule(routeModule)) { + return collectAppRouteSegments(routeModule) + } + + if (isAppPageRouteModule(routeModule)) { + return collectAppPageSegments(routeModule) + } + + throw new InvariantError( + 'Expected a route module to be one of app route or page' + ) +} diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index fba27b7a3e7a8..be1ea1d6b4754 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -130,7 +130,8 @@ import { collectMeta, // getSupportedBrowsers, } from './utils' -import type { PageInfo, PageInfos, AppConfig, PrerenderedRoute } from './utils' +import type { PageInfo, PageInfos, PrerenderedRoute } from './utils' +import type { AppSegmentConfig } from './app-segments/app-segment-config' import { writeBuildId } from './write-build-id' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import isError from '../lib/is-error' @@ -1818,7 +1819,7 @@ export default async function build( const staticPaths = new Map() const appNormalizedPaths = new Map() const fallbackModes = new Map() - const appDefaultConfigs = new Map() + const appDefaultConfigs = new Map() const pageInfos: PageInfos = new Map() let pagesManifest = await readManifest(pagesManifestPath) const buildManifest = await readManifest(buildManifestPath) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index d6c3a3f4a1d13..1114833899d7f 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -22,7 +22,6 @@ import type { import type { WebpackLayerName } from '../lib/constants' import type { AppPageModule } from '../server/route-modules/app-page/module' import type { RouteModule } from '../server/route-modules/route-module' -import type { LoaderTree } from '../server/lib/app-dir-module' import type { NextComponentType } from '../shared/lib/utils' import '../server/require-hook' @@ -83,7 +82,6 @@ import * as ciEnvironment from '../server/ci-info' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { denormalizeAppPagePath } from '../shared/lib/page-path/denormalize-app-path' import { RouteKind } from '../server/route-kind' -import { isAppRouteRouteModule } from '../server/route-modules/checks' import { interopDefault } from '../lib/interop-default' import type { PageExtensions } from './page-extensions-type' import { formatDynamicImportPath } from '../lib/format-dynamic-import-path' @@ -97,6 +95,9 @@ import { } from '../lib/fallback' import { getParamKeys } from '../server/request/fallback-params' import type { OutgoingHttpHeaders } from 'http' +import type { AppSegmentConfig } from './app-segments/app-segment-config' +import type { AppSegment } from './app-segments/collect-app-segments' +import { collectSegments } from './app-segments/collect-app-segments' export type ROUTER_TYPE = 'pages' | 'app' @@ -1121,7 +1122,7 @@ export async function buildStaticPaths({ (!repeat && typeof paramValue !== 'string') ) { // If this is from app directory, and not all params were provided, - // then filter this out if the route is not PPR enabled. + // then filter this out. if (appDir && typeof paramValue === 'undefined') { builtPage = '' encodedBuiltPage = '' @@ -1200,146 +1201,6 @@ export async function buildStaticPaths({ } } -export type AppConfigDynamic = - | 'auto' - | 'error' - | 'force-static' - | 'force-dynamic' - -export type AppConfig = { - revalidate?: number | false - dynamicParams?: true | false - dynamic?: AppConfigDynamic - fetchCache?: 'force-cache' | 'only-cache' - preferredRegion?: string - - /** - * When true, the page will be served using partial prerendering. - * This setting will only take affect if it's enabled via - * the `experimental.ppr = "incremental"` option. - */ - experimental_ppr?: boolean -} - -type GenerateStaticParams = (options: { params?: Params }) => Promise - -type GenerateParamsResult = { - config?: AppConfig - isDynamicSegment?: boolean - segmentPath: string - generateStaticParams?: GenerateStaticParams - isLayout?: boolean -} - -export type GenerateParamsResults = GenerateParamsResult[] - -const collectAppConfig = ( - mod: Partial | undefined -): AppConfig | undefined => { - let hasConfig = false - const config: AppConfig = {} - - if (typeof mod?.revalidate !== 'undefined') { - config.revalidate = mod.revalidate - hasConfig = true - } - if (typeof mod?.dynamicParams !== 'undefined') { - config.dynamicParams = mod.dynamicParams - hasConfig = true - } - if (typeof mod?.dynamic !== 'undefined') { - config.dynamic = mod.dynamic - hasConfig = true - } - if (typeof mod?.fetchCache !== 'undefined') { - config.fetchCache = mod.fetchCache - hasConfig = true - } - if (typeof mod?.preferredRegion !== 'undefined') { - config.preferredRegion = mod.preferredRegion - hasConfig = true - } - if (typeof mod?.experimental_ppr !== 'undefined') { - config.experimental_ppr = mod.experimental_ppr - hasConfig = true - } - - if (!hasConfig) return undefined - - return config -} - -/** - * Walks the loader tree and collects the generate parameters for each segment. - * - * @param tree the loader tree - * @returns the generate parameters for each segment - */ -export async function collectGenerateParams(tree: LoaderTree) { - const generateParams: GenerateParamsResults = [] - const parentSegments: string[] = [] - - let currentLoaderTree = tree - while (currentLoaderTree) { - const [ - // TODO: check if this is ever undefined - page = '', - parallelRoutes, - components, - ] = currentLoaderTree - - // If the segment doesn't have any components, then skip it. - if (!components) continue - - const isLayout = !!components.layout - const mod = await (isLayout - ? components.layout?.[0]?.() - : components.page?.[0]?.()) - - if (page) { - parentSegments.push(page) - } - - const config = mod ? collectAppConfig(mod) : undefined - const isClientComponent = isClientReference(mod) - - const isDynamicSegment = /^\[.+\]$/.test(page) - - const { generateStaticParams } = mod || {} - - if (isDynamicSegment && isClientComponent && generateStaticParams) { - throw new Error( - `Page "${page}" cannot export "generateStaticParams()" because it is a client component` - ) - } - - const segmentPath = `/${parentSegments.join('/')}${ - page && parentSegments.length > 0 ? '/' : '' - }${page}` - - const result: GenerateParamsResult = { - isLayout, - isDynamicSegment, - segmentPath, - config, - generateStaticParams: !isClientComponent - ? generateStaticParams - : undefined, - } - - // If the configuration contributes to the static generation, then add it - // to the list. - if (result.config || result.generateStaticParams || isDynamicSegment) { - generateParams.push(result) - } - - // Use this route's parallel route children as the next segment. - currentLoaderTree = parallelRoutes.children - } - - return generateParams -} - export type PartialStaticPathsResult = { [P in keyof StaticPathsResult]: StaticPathsResult[P] | undefined } @@ -1350,7 +1211,7 @@ export async function buildAppStaticPaths({ distDir, dynamicIO, configFileName, - generateParams, + segments, isrFlushToDisk, cacheHandler, requestHeaders, @@ -1366,7 +1227,7 @@ export async function buildAppStaticPaths({ page: string dynamicIO: boolean configFileName: string - generateParams: GenerateParamsResults + segments: AppSegment[] distDir: string isrFlushToDisk?: boolean fetchCacheKeyPrefix?: string @@ -1411,7 +1272,20 @@ export async function buildAppStaticPaths({ minimalMode: ciEnvironment.hasNextSupport, }) - return withStaticGenerationStore( + const paramKeys = new Set() + + const staticParamKeys = new Set() + for (const segment of segments) { + if (segment.param) { + paramKeys.add(segment.param) + + if (segment.config?.dynamicParams === false) { + staticParamKeys.add(segment.param) + } + } + } + + const routeParams = await withStaticGenerationStore( ComponentMod.staticGenerationAsyncStorage, { page, @@ -1429,134 +1303,157 @@ export async function buildAppStaticPaths({ buildId, }, }, - async (): Promise => { - let hadAllParamsGenerated = false - - const buildParams = async ( - paramsItems: Params[] = [{}], + async (store) => { + async function builtRouteParams( + parentsParams: Params[] = [], idx = 0 - ): Promise => { - const current = generateParams[idx] + ): Promise { + // If we don't have any more to process, then we're done. + if (idx === segments.length) return parentsParams - if (idx === generateParams.length) { - return paramsItems - } + const current = segments[idx] if ( typeof current.generateStaticParams !== 'function' && - idx < generateParams.length + idx < segments.length ) { - if (current.isDynamicSegment) { - // This dynamic level has no generateStaticParams so we change - // this flag to false, but it could be covered by a later - // generateStaticParams so it could be set back to true. - hadAllParamsGenerated = false - } - return buildParams(paramsItems, idx + 1) + return builtRouteParams(parentsParams, idx + 1) } - hadAllParamsGenerated = true - const newParams: Params[] = [] + const params: Params[] = [] if (current.generateStaticParams) { - const store = ComponentMod.staticGenerationAsyncStorage.getStore() - - if (store) { - if (typeof current?.config?.fetchCache !== 'undefined') { - store.fetchCache = current.config.fetchCache - } - if (typeof current?.config?.revalidate !== 'undefined') { - store.revalidate = current.config.revalidate - } - if (current?.config?.dynamic === 'force-dynamic') { - store.forceDynamic = true - } + if (typeof current.config?.fetchCache !== 'undefined') { + store.fetchCache = current.config.fetchCache + } + if (typeof current.config?.revalidate !== 'undefined') { + store.revalidate = current.config.revalidate + } + if (current.config?.dynamic === 'force-dynamic') { + store.forceDynamic = true } - for (const params of paramsItems) { - const result = await current.generateStaticParams({ - params, - }) + if (parentsParams.length > 0) { + for (const parentParams of parentsParams) { + const result = await current.generateStaticParams({ + params: parentParams, + }) - // TODO: validate the result is valid here or wait for buildStaticPaths to validate? - for (const item of result) { - newParams.push({ ...params, ...item }) + for (const item of result) { + params.push({ ...parentParams, ...item }) + } } + } else { + const result = await current.generateStaticParams({ params: {} }) + + params.push(...result) } } - if (idx < generateParams.length) { - return buildParams(newParams, idx + 1) + if (idx < segments.length) { + return builtRouteParams(params, idx + 1) } - return newParams + return params } - const builtParams = await buildParams() + return builtRouteParams() + } + ) + + if ( + segments.some((generate) => generate.config?.dynamicParams === true) && + nextConfigOutput === 'export' + ) { + throw new Error( + '"dynamicParams: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/app/building-your-application/deploying/static-exports' + ) + } + + for (const segment of segments) { + // Check to see if there are any missing params for segments that have + // dynamicParams set to false. + if ( + segment.param && + segment.isDynamicSegment && + segment.config?.dynamicParams === false + ) { + for (const params of routeParams) { + if (segment.param in params) continue + + const relative = segment.filePath + ? path.relative(dir, segment.filePath) + : undefined - if ( - generateParams.some( - (generate) => generate.config?.dynamicParams === true - ) && - nextConfigOutput === 'export' - ) { throw new Error( - '"dynamicParams: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/app/building-your-application/deploying/static-exports' + `Segment "${relative}" exports "dynamicParams: false" but the param "${segment.param}" is missing from the generated route params.` ) } + } + } - // TODO: dynamic params should be allowed to be granular per segment but - // we need additional information stored/leveraged in the prerender - // manifest to allow this behavior. - const dynamicParams = generateParams.every( - (param) => param.config?.dynamicParams !== false - ) - - const isProduction = process.env.NODE_ENV === 'production' + // Determine if all the segments have had their parameters provided. If there + // was no dynamic parameters, then we've collected all the params. + const hadAllParamsGenerated = + paramKeys.size === 0 || + (routeParams.length > 0 && + routeParams.every((params) => { + for (const key of paramKeys) { + if (key in params) continue + return false + } + return true + })) + + // TODO: dynamic params should be allowed to be granular per segment but + // we need additional information stored/leveraged in the prerender + // manifest to allow this behavior. + const dynamicParams = segments.every( + (segment) => segment.config?.dynamicParams !== false + ) - const supportsStaticGeneration = hadAllParamsGenerated || isProduction + const supportsRoutePreGeneration = + hadAllParamsGenerated || process.env.NODE_ENV === 'production' - const supportsPPRFallbacks = isRoutePPREnabled && isAppPPRFallbacksEnabled + const supportsPPRFallbacks = isRoutePPREnabled && isAppPPRFallbacksEnabled - const fallbackMode = dynamicParams - ? supportsStaticGeneration - ? supportsPPRFallbacks - ? FallbackMode.PRERENDER - : FallbackMode.BLOCKING_STATIC_RENDER - : undefined - : FallbackMode.NOT_FOUND + const fallbackMode = dynamicParams + ? supportsRoutePreGeneration + ? supportsPPRFallbacks + ? FallbackMode.PRERENDER + : FallbackMode.BLOCKING_STATIC_RENDER + : undefined + : FallbackMode.NOT_FOUND - let result: PartialStaticPathsResult = { - fallbackMode, - prerenderedRoutes: undefined, - } + let result: PartialStaticPathsResult = { + fallbackMode, + prerenderedRoutes: undefined, + } - if (hadAllParamsGenerated && fallbackMode) { - result = await buildStaticPaths({ - staticPathsResult: { - fallback: fallbackModeToStaticPathsResult(fallbackMode), - paths: builtParams.map((params) => ({ params })), - }, - page, - configFileName, - appDir: true, - }) - } + if (hadAllParamsGenerated && fallbackMode) { + result = await buildStaticPaths({ + staticPathsResult: { + fallback: fallbackModeToStaticPathsResult(fallbackMode), + paths: routeParams.map((params) => ({ params })), + }, + page, + configFileName, + appDir: true, + }) + } - // If the fallback mode is a prerender, we want to include the dynamic - // route in the prerendered routes too. - if (isRoutePPREnabled && isAppPPRFallbacksEnabled) { - result.prerenderedRoutes ??= [] - result.prerenderedRoutes.unshift({ - path: page, - encoded: page, - fallbackRouteParams: getParamKeys(page), - }) - } + // If the fallback mode is a prerender, we want to include the dynamic + // route in the prerendered routes too. + if (isRoutePPREnabled && isAppPPRFallbacksEnabled) { + result.prerenderedRoutes ??= [] + result.prerenderedRoutes.unshift({ + path: page, + encoded: page, + fallbackRouteParams: getParamKeys(page), + }) + } - return result - } - ) + return result } type PageIsStaticResult = { @@ -1571,7 +1468,7 @@ type PageIsStaticResult = { isNextImageImported?: boolean traceIncludes?: string[] traceExcludes?: string[] - appConfig?: AppConfig + appConfig?: AppSegmentConfig } export async function isPageStatic({ @@ -1632,7 +1529,7 @@ export async function isPageStatic({ let componentsResult: LoadComponentsReturnType let prerenderedRoutes: PrerenderedRoute[] | undefined let prerenderFallbackMode: FallbackMode | undefined - let appConfig: AppConfig = {} + let appConfig: AppSegmentConfig = {} let isClientComponent: boolean = false const pathIsEdgeRuntime = isEdgeRuntime(pageRuntime) @@ -1654,13 +1551,19 @@ export async function isPageStatic({ await runtime.context._ENTRIES[`middleware_${edgeInfo.name}`] ).ComponentMod + // This is not needed during require. + const buildManifest = {} as BuildManifest + isClientComponent = isClientReference(mod) componentsResult = { Component: mod.default, + Document: mod.Document, + App: mod.App, + routeModule: mod.routeModule, + page, ComponentMod: mod, pageConfig: mod.config || {}, - // @ts-expect-error this is not needed during require - buildManifest: {}, + buildManifest, reactLoadableManifest: {}, getServerSideProps: mod.getServerSideProps, getStaticPaths: mod.getStaticPaths, @@ -1676,8 +1579,7 @@ export async function isPageStatic({ const Comp = componentsResult.Component as NextComponentType | undefined let staticPathsResult: GetStaticPathsResult | undefined - const routeModule: RouteModule = - componentsResult.ComponentMod?.routeModule + const routeModule: RouteModule = componentsResult.routeModule let isRoutePPREnabled: boolean = false @@ -1686,25 +1588,9 @@ export async function isPageStatic({ isClientComponent = isClientReference(componentsResult.ComponentMod) - const { tree } = ComponentMod - - const generateParams: GenerateParamsResults = - routeModule && isAppRouteRouteModule(routeModule) - ? [ - { - config: { - revalidate: routeModule.userland.revalidate, - dynamic: routeModule.userland.dynamic, - dynamicParams: routeModule.userland.dynamicParams, - }, - generateStaticParams: - routeModule.userland.generateStaticParams, - segmentPath: page, - }, - ] - : await collectGenerateParams(tree) - - appConfig = reduceAppConfig(generateParams) + const segments = await collectSegments(componentsResult) + + appConfig = reduceAppConfig(await collectSegments(componentsResult)) if (appConfig.dynamic === 'force-static' && pathIsEdgeRuntime) { Log.warn( @@ -1734,7 +1620,7 @@ export async function isPageStatic({ page, dynamicIO, configFileName, - generateParams, + segments, distDir, requestHeaders: {}, isrFlushToDisk, @@ -1839,7 +1725,7 @@ export async function isPageStatic({ } type ReducedAppConfig = Pick< - AppConfig, + AppSegmentConfig, | 'dynamic' | 'fetchCache' | 'preferredRegion' @@ -1854,21 +1740,17 @@ type ReducedAppConfig = Pick< * @param segments the generate param segments * @returns the reduced app config */ -export function reduceAppConfig( - segments: GenerateParamsResults -): ReducedAppConfig { +export function reduceAppConfig(segments: AppSegment[]): ReducedAppConfig { const config: ReducedAppConfig = {} for (const segment of segments) { - if (!segment.config) continue - const { dynamic, fetchCache, preferredRegion, revalidate, experimental_ppr, - } = segment.config + } = segment.config || {} // TODO: should conflicting configs here throw an error // e.g. if layout defines one region but page defines another diff --git a/packages/next/src/client/components/static-generation-async-storage.external.ts b/packages/next/src/client/components/static-generation-async-storage.external.ts index 610a6227d1fdd..2c1a120f8c5cb 100644 --- a/packages/next/src/client/components/static-generation-async-storage.external.ts +++ b/packages/next/src/client/components/static-generation-async-storage.external.ts @@ -7,6 +7,7 @@ import type { FallbackRouteParams } from '../../server/request/fallback-params' // Share the instance module in the next-shared layer import { staticGenerationAsyncStorage } from './static-generation-async-storage-instance' with { 'turbopack-transition': 'next-shared' } +import type { AppSegmentConfig } from '../../build/app-segments/app-segment-config' export interface StaticGenerationStore { readonly isStaticGeneration: boolean @@ -35,13 +36,7 @@ export interface StaticGenerationStore { readonly isUnstableCacheCallback?: boolean forceDynamic?: boolean - fetchCache?: - | 'only-cache' - | 'force-cache' - | 'default-cache' - | 'force-no-store' - | 'default-no-store' - | 'only-no-store' + fetchCache?: AppSegmentConfig['fetchCache'] revalidate?: Revalidate forceStatic?: boolean diff --git a/packages/next/src/lib/metadata/resolve-metadata.ts b/packages/next/src/lib/metadata/resolve-metadata.ts index 508643c49c98c..0a8c490bd1181 100644 --- a/packages/next/src/lib/metadata/resolve-metadata.ts +++ b/packages/next/src/lib/metadata/resolve-metadata.ts @@ -441,7 +441,10 @@ export async function collectMetadata({ mod = await getComponentTypeModule(tree, 'layout') modType = errorConvention } else { - ;[mod, modType] = await getLayoutOrPageModule(tree) + const { mod: layoutOrPageMod, modType: layoutOrPageModType } = + await getLayoutOrPageModule(tree) + mod = layoutOrPageMod + modType = layoutOrPageModType } if (modType) { 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 dfb0c2d39cae5..20bb19cae0bf6 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -159,7 +159,7 @@ async function createComponentTreeInternal({ const isLayout = typeof layout !== 'undefined' const isPage = typeof page !== 'undefined' - const [layoutOrPageMod] = await getTracer().trace( + const { mod: layoutOrPageMod } = await getTracer().trace( NextNodeServerSpan.getLayoutOrPageModule, { hideSpan: !(isLayout || isPage), diff --git a/packages/next/src/server/async-storage/with-static-generation-store.ts b/packages/next/src/server/async-storage/with-static-generation-store.ts index a6458fe9125ac..8054a81de0fb6 100644 --- a/packages/next/src/server/async-storage/with-static-generation-store.ts +++ b/packages/next/src/server/async-storage/with-static-generation-store.ts @@ -6,6 +6,7 @@ import type { RenderOptsPartial } from '../app-render/types' import type { FetchMetric } from '../base-http' import type { RequestLifecycleOpts } from '../base-server' import type { FallbackRouteParams } from '../../server/request/fallback-params' +import type { AppSegmentConfig } from '../../build/app-segments/app-segment-config' import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' @@ -25,7 +26,7 @@ export type StaticGenerationContext = { renderOpts: { incrementalCache?: IncrementalCache isOnDemandRevalidate?: boolean - fetchCache?: StaticGenerationStore['fetchCache'] + fetchCache?: AppSegmentConfig['fetchCache'] isServerAction?: boolean pendingWaitUntil?: Promise experimental: Pick< diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index 1d3e5071b4f59..82cede435e819 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -6,20 +6,14 @@ import '../node-environment' import { buildAppStaticPaths, buildStaticPaths, - collectGenerateParams, reduceAppConfig, } from '../../build/utils' -import type { - GenerateParamsResults, - PartialStaticPathsResult, -} from '../../build/utils' +import { collectSegments } from '../../build/app-segments/collect-app-segments' +import type { PartialStaticPathsResult } from '../../build/utils' import { loadComponents } from '../load-components' import { setHttpClientAndAgentOptions } from '../setup-http-agent-env' import type { IncrementalCache } from '../lib/incremental-cache' -import { - isAppPageRouteModule, - isAppRouteRouteModule, -} from '../route-modules/checks' +import { isAppPageRouteModule } from '../route-modules/checks' import { checkIsRoutePPREnabled, type ExperimentalPPRConfig, @@ -95,31 +89,17 @@ export async function loadStaticPaths({ } if (isAppPath) { - const { routeModule } = components - const generateParams: GenerateParamsResults = - routeModule && isAppRouteRouteModule(routeModule) - ? [ - { - config: { - revalidate: routeModule.userland.revalidate, - dynamic: routeModule.userland.dynamic, - dynamicParams: routeModule.userland.dynamicParams, - }, - generateStaticParams: routeModule.userland.generateStaticParams, - segmentPath: pathname, - }, - ] - : await collectGenerateParams(components.ComponentMod.tree) + const segments = await collectSegments(components) const isRoutePPREnabled = - isAppPageRouteModule(routeModule) && - checkIsRoutePPREnabled(config.pprConfig, reduceAppConfig(generateParams)) + isAppPageRouteModule(components.routeModule) && + checkIsRoutePPREnabled(config.pprConfig, reduceAppConfig(segments)) return await buildAppStaticPaths({ dir, page: pathname, dynamicIO: config.dynamicIO, - generateParams, + segments, configFileName: config.configFileName, distDir, requestHeaders, diff --git a/packages/next/src/server/lib/app-dir-module.ts b/packages/next/src/server/lib/app-dir-module.ts index 9de970147a2a9..3e2438ed074a5 100644 --- a/packages/next/src/server/lib/app-dir-module.ts +++ b/packages/next/src/server/lib/app-dir-module.ts @@ -17,21 +17,25 @@ export async function getLayoutOrPageModule(loaderTree: LoaderTree) { const isDefaultPage = typeof defaultPage !== 'undefined' && loaderTree[0] === DEFAULT_SEGMENT_KEY - let value = undefined + let mod = undefined let modType: 'layout' | 'page' | undefined = undefined + let filePath = undefined if (isLayout) { - value = await layout[0]() + mod = await layout[0]() modType = 'layout' + filePath = layout[1] } else if (isPage) { - value = await page[0]() + mod = await page[0]() modType = 'page' + filePath = page[1] } else if (isDefaultPage) { - value = await defaultPage[0]() + mod = await defaultPage[0]() modType = 'page' + filePath = defaultPage[1] } - return [value, modType] as const + return { mod, modType, filePath } } export async function getComponentTypeModule( diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 74a70bc354895..fc37dd349598c 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -1,6 +1,6 @@ import type { NextConfig } from '../../config-shared' import type { AppRouteRouteDefinition } from '../../route-definitions/app-route-route-definition' -import type { AppConfig } from '../../../build/utils' +import type { AppSegmentConfig } from '../../../build/app-segments/app-segment-config' import type { NextRequest } from '../../web/spec-extension/request' import type { PrerenderManifest } from '../../../build' import type { NextURL } from '../../web/next-url' @@ -69,6 +69,7 @@ import type { RenderOptsPartial } from '../../app-render/types' import { CacheSignal } from '../../app-render/cache-signal' import { scheduleImmediate } from '../../../lib/scheduler' import { createServerParamsForRoute } from '../../request/params' +import type { AppSegment } from '../../../build/app-segments/collect-app-segments' /** * The AppRouteModule is the type of the module exported by the bundled App @@ -123,10 +124,11 @@ export type AppRouteHandlers = { * routes. This contains all the user generated code. */ export type AppRouteUserlandModule = AppRouteHandlers & - Pick & { - // TODO: (wyattjoh) create a type for this - generateStaticParams?: any - } + Pick< + AppSegmentConfig, + 'dynamic' | 'revalidate' | 'dynamicParams' | 'fetchCache' + > & + Pick /** * AppRouteRouteModuleOptions is the options that are passed to the app route diff --git a/packages/next/types/$$compiled.internal.d.ts b/packages/next/types/$$compiled.internal.d.ts index c5b048b6cecf1..ba1de837ef409 100644 --- a/packages/next/types/$$compiled.internal.d.ts +++ b/packages/next/types/$$compiled.internal.d.ts @@ -523,8 +523,8 @@ declare module 'next/dist/compiled/@opentelemetry/api' { } declare module 'next/dist/compiled/zod' { - import * as m from 'zod' - export = m + import * as z from 'zod' + export = z } declare module 'mini-css-extract-plugin' diff --git a/test/e2e/app-dir/app-static/app/dynamic-param-edge/[slug]/page.tsx b/test/e2e/app-dir/app-static/app/dynamic-param-edge/[slug]/page.tsx index 4da62f81383f1..5eaf59c542e19 100644 --- a/test/e2e/app-dir/app-static/app/dynamic-param-edge/[slug]/page.tsx +++ b/test/e2e/app-dir/app-static/app/dynamic-param-edge/[slug]/page.tsx @@ -5,9 +5,7 @@ export default async function Hello({ params }) { export function generateStaticParams() { return [ { - params: { - slug: 'hello', - }, + slug: 'hello', }, ] }