From dfa51e9a3cfcbfce81cbebf5e68edd58efa29b4a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 13 Aug 2024 17:01:43 -0600 Subject: [PATCH] refactor: reduce app config --- packages/next/src/build/index.ts | 14 +- packages/next/src/build/utils.ts | 137 +++++++++--------- packages/next/src/lib/fallback.ts | 58 +++----- packages/next/src/server/base-server.ts | 17 ++- .../next/src/server/dev/next-dev-server.ts | 17 ++- .../test/dynamic-missing-gsp-dev.test.ts | 10 ++ test/integration/app-dir-export/test/utils.ts | 8 + 7 files changed, 141 insertions(+), 120 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 4986996c0ab98..991a7c9da45da 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -2167,7 +2167,11 @@ export default async function build( } } - if (workerResult.prerenderFallback) { + if ( + workerResult.prerenderFallbackMode !== undefined && + workerResult.prerenderFallbackMode !== + FallbackMode.NOT_FOUND + ) { // whether or not to allow requests for paths not // returned from generateStaticParams appDynamicParamPaths.add(originalAppPath) @@ -2221,13 +2225,13 @@ export default async function build( } if ( - workerResult.prerenderFallback === - FallbackMode.BLOCKING_RENDER + workerResult.prerenderFallbackMode === + FallbackMode.BLOCKING_STATIC_RENDER ) { ssgBlockingFallbackPages.add(page) } else if ( - workerResult.prerenderFallback === - FallbackMode.SERVE_PRERENDER + workerResult.prerenderFallbackMode === + FallbackMode.SERVE_STATIC_PRERENDER ) { ssgStaticFallbackPages.add(page) } diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 20409438efc32..d7e24c9365159 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -93,7 +93,6 @@ import type { Params } from '../client/components/params' import { FallbackMode } from '../lib/fallback' import { fallbackToStaticPathsResult, - parseFallbackAppConfig, parseFallbackStaticPathsResult, } from '../lib/fallback' @@ -951,7 +950,7 @@ export type PrerenderedRoute = { } export type StaticPathsResult = { - fallback: FallbackMode + fallbackMode: FallbackMode prerenderedRoutes: PrerenderedRoute[] } @@ -1178,7 +1177,7 @@ export async function buildStaticPaths({ const seen = new Set() return { - fallback: parseFallbackStaticPathsResult(staticPathsResult.fallback), + fallbackMode: parseFallbackStaticPathsResult(staticPathsResult.fallback), prerenderedRoutes: prerenderRoutes.filter((route) => { if (seen.has(route.path)) return false @@ -1487,12 +1486,11 @@ export async function buildAppStaticPaths({ } const builtParams = await buildParams() - const fallback = parseFallbackAppConfig( - reduceAppConfig(generateParams, nextConfigOutput) - ) if ( - fallback === FallbackMode.BLOCKING_RENDER && + generateParams.some( + (generate) => generate.config?.dynamicParams === true + ) && nextConfigOutput === 'export' ) { throw new Error( @@ -1500,16 +1498,29 @@ export async function buildAppStaticPaths({ ) } + const fallbackMode = !generateParams.some( + // 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 + (generate) => generate.config?.dynamicParams === false + ) + ? FallbackMode.BLOCKING_STATIC_RENDER + : FallbackMode.NOT_FOUND + if (!hadAllParamsGenerated) { return { - fallback, + fallbackMode: + process.env.NODE_ENV === 'production' && isDynamicRoute(page) + ? FallbackMode.BLOCKING_STATIC_RENDER + : undefined, prerenderedRoutes: undefined, } } return buildStaticPaths({ staticPathsResult: { - fallback: fallbackToStaticPathsResult(fallback), + fallback: fallbackToStaticPathsResult(fallbackMode), paths: builtParams.map((params) => ({ params })), }, page, @@ -1529,7 +1540,7 @@ type PageIsStaticResult = { hasServerProps?: boolean hasStaticProps?: boolean prerenderedRoutes: PrerenderedRoute[] | undefined - prerenderFallback: FallbackMode | undefined + prerenderFallbackMode: FallbackMode | undefined isNextImageImported?: boolean traceIncludes?: string[] traceExcludes?: string[] @@ -1587,7 +1598,7 @@ export async function isPageStatic({ let componentsResult: LoadComponentsReturnType let prerenderedRoutes: PrerenderedRoute[] | undefined - let prerenderFallback: FallbackMode | undefined + let prerenderFallbackMode: FallbackMode | undefined let appConfig: AppConfig = {} let isClientComponent: boolean = false const pathIsEdgeRuntime = isEdgeRuntime(pageRuntime) @@ -1660,7 +1671,7 @@ export async function isPageStatic({ ] : await collectGenerateParams(tree) - appConfig = reduceAppConfig(generateParams, nextConfigOutput) + appConfig = reduceAppConfig(generateParams) if (appConfig.dynamic === 'force-static' && pathIsEdgeRuntime) { Log.warn( @@ -1684,7 +1695,7 @@ export async function isPageStatic({ } if (isDynamicRoute(page)) { - ;({ fallback: prerenderFallback, prerenderedRoutes } = + ;({ fallbackMode: prerenderFallbackMode, prerenderedRoutes } = await buildAppStaticPaths({ dir, page, @@ -1741,7 +1752,7 @@ export async function isPageStatic({ } if ((hasStaticProps && hasStaticPaths) || staticPathsResult) { - ;({ fallback: prerenderFallback, prerenderedRoutes } = + ;({ fallbackMode: prerenderFallbackMode, prerenderedRoutes } = await buildStaticPaths({ page, locales, @@ -1773,7 +1784,7 @@ export async function isPageStatic({ isRoutePPREnabled, isHybridAmp: config.amp === 'hybrid', isAmpOnly: config.amp === true, - prerenderFallback, + prerenderFallbackMode, prerenderedRoutes, hasStaticProps, hasServerProps, @@ -1790,75 +1801,71 @@ export async function isPageStatic({ }) } -function reduceAppConfig( - generateParams: GenerateParamsResults, - nextConfigOutput: 'standalone' | 'export' | undefined -): AppConfig { - return generateParams.reduce((builtConfig, curGenParams) => { +type ReducedAppConfig = Pick< + AppConfig, + | 'dynamic' + | 'fetchCache' + | 'preferredRegion' + | 'revalidate' + | 'experimental_ppr' +> + +/** + * Collect the app config from the generate param segments. This only gets a + * subset of the config options. + * + * @param segments the generate param segments + * @returns the reduced app config + */ +function reduceAppConfig(segments: GenerateParamsResults): ReducedAppConfig { + const config: ReducedAppConfig = {} + + for (const segment of segments) { + if (!segment.config) continue + const { dynamic, fetchCache, preferredRegion, - revalidate: curRevalidate, + revalidate, experimental_ppr, - dynamicParams, - } = curGenParams?.config || {} + } = segment.config // TODO: should conflicting configs here throw an error // e.g. if layout defines one region but page defines another - if (typeof builtConfig.preferredRegion === 'undefined') { - builtConfig.preferredRegion = preferredRegion - } - if (typeof builtConfig.dynamic === 'undefined') { - builtConfig.dynamic = dynamic + + // Get the first value of preferredRegion, dynamic, revalidate, and + // fetchCache. + if (typeof config.preferredRegion === 'undefined') { + config.preferredRegion = preferredRegion } - if (typeof builtConfig.fetchCache === 'undefined') { - builtConfig.fetchCache = fetchCache + if (typeof config.dynamic === 'undefined') { + config.dynamic = dynamic } - // If partial prerendering has been set, only override it if the current value is - // provided as it's resolved from root layout to leaf page. - if (typeof experimental_ppr !== 'undefined') { - builtConfig.experimental_ppr = experimental_ppr + if (typeof config.fetchCache === 'undefined') { + config.fetchCache = fetchCache } - - // any revalidate number overrides false - // shorter revalidate overrides longer (initially) - if (typeof builtConfig.revalidate === 'undefined') { - builtConfig.revalidate = curRevalidate + if (typeof config.revalidate === 'undefined') { + config.revalidate = revalidate } + // Any revalidate number overrides false, and shorter revalidate overrides + // longer (initially). if ( - typeof curRevalidate === 'number' && - (typeof builtConfig.revalidate !== 'number' || - curRevalidate < builtConfig.revalidate) + typeof revalidate === 'number' && + (typeof config.revalidate !== 'number' || revalidate < config.revalidate) ) { - builtConfig.revalidate = curRevalidate - } - - // The default for the dynamicParams will be based on the dynamic config - // and the next config output option. If any of these are set or the - // output config is set to export, set the dynamicParams to false, - // otherwise the default is true. - if (typeof builtConfig.dynamicParams === 'undefined') { - if ( - builtConfig.dynamic === 'error' || - builtConfig.dynamic === 'force-static' || - nextConfigOutput === 'export' - ) { - builtConfig.dynamicParams = false - } else { - builtConfig.dynamicParams = true - } + config.revalidate = revalidate } - // If the dynamicParams is explicitly set to false, we should not - // dynamically generate params for this page. - if (dynamicParams === false) { - builtConfig.dynamicParams = false + // If partial prerendering has been set, only override it if the current + // value is provided as it's resolved from root layout to leaf page. + if (typeof experimental_ppr !== 'undefined') { + config.experimental_ppr = experimental_ppr } + } - return builtConfig - }, {}) + return config } export async function hasCustomGetInitialProps({ diff --git a/packages/next/src/lib/fallback.ts b/packages/next/src/lib/fallback.ts index daa30ca0050e3..70d45dcfbdc7e 100644 --- a/packages/next/src/lib/fallback.ts +++ b/packages/next/src/lib/fallback.ts @@ -1,28 +1,26 @@ -import type { AppConfig } from '../build/utils' - /** * Describes the different fallback modes that a given page can have. */ export const enum FallbackMode { /** - * A BLOCKING_RENDER fallback will block the request until the page is\ + * A BLOCKING_STATIC_RENDER fallback will block the request until the page is * generated. No fallback page will be rendered, and users will have to wait * to render the page. */ - BLOCKING_RENDER = 'BLOCKING_RENDER', - - /** - * When set to NOT_FOUND, pages that are not already prerendered will result - * in a not found response. - */ - NOT_FOUND = 'NOT_FOUND', + BLOCKING_STATIC_RENDER = 'BLOCKING_STATIC_RENDER', /** * When set to PRERENDER, a fallback page will be sent to users in place of * forcing them to wait for the page to be generated. This allows the user to * see a rendered page earlier. */ - SERVE_PRERENDER = 'SERVE_PRERENDER', + SERVE_STATIC_PRERENDER = 'SERVE_STATIC_PRERENDER', + + /** + * When set to NOT_FOUND, pages that are not already prerendered will result + * in a not found response. + */ + NOT_FOUND = 'NOT_FOUND', } /** @@ -37,14 +35,20 @@ export type GetStaticPathsFallback = boolean | 'blocking' * @returns The fallback mode. */ export function parseFallbackField( - fallbackField: string | false | null -): FallbackMode { + fallbackField: string | false | null | undefined +): FallbackMode | undefined { if (typeof fallbackField === 'string') { - return FallbackMode.SERVE_PRERENDER + return FallbackMode.SERVE_STATIC_PRERENDER } else if (fallbackField === null) { - return FallbackMode.BLOCKING_RENDER - } else { + return FallbackMode.BLOCKING_STATIC_RENDER + } else if (fallbackField === false) { return FallbackMode.NOT_FOUND + } else if (fallbackField === undefined) { + return undefined + } else { + throw new Error( + `Invalid fallback option: ${fallbackField}. Fallback option must be a string, null, undefined, or false.` + ) } } @@ -58,9 +62,9 @@ export function parseFallbackStaticPathsResult( result: GetStaticPathsFallback ): FallbackMode { if (result === true) { - return FallbackMode.SERVE_PRERENDER + return FallbackMode.SERVE_STATIC_PRERENDER } else if (result === 'blocking') { - return FallbackMode.BLOCKING_RENDER + return FallbackMode.BLOCKING_STATIC_RENDER } else { return FallbackMode.NOT_FOUND } @@ -76,26 +80,12 @@ export function fallbackToStaticPathsResult( fallback: FallbackMode ): GetStaticPathsFallback { switch (fallback) { - case FallbackMode.SERVE_PRERENDER: + case FallbackMode.SERVE_STATIC_PRERENDER: return true - case FallbackMode.BLOCKING_RENDER: + case FallbackMode.BLOCKING_STATIC_RENDER: return 'blocking' case FallbackMode.NOT_FOUND: default: return false } } - -/** - * Parses the fallback field from the app config. - * - * @param appConfig The app config. - * @returns The fallback mode. - */ -export function parseFallbackAppConfig(appConfig: AppConfig): FallbackMode { - if (appConfig.dynamicParams === false) { - return FallbackMode.NOT_FOUND - } else { - return FallbackMode.BLOCKING_RENDER - } -} diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index f91d4f4fc145b..26961726ca64e 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -585,7 +585,7 @@ export default abstract class Server< ? publicRuntimeConfig : undefined, - // @ts-expect-error internal field + // @ts-expect-error internal field not publicly exposed isExperimentalCompile: this.nextConfig.experimental.isExperimentalCompile, experimental: { swrDelta: this.nextConfig.swrDelta, @@ -1950,13 +1950,14 @@ export default abstract class Server< if (this.nextConfig.output === 'export') { const page = components.page - if (fallbackMode === FallbackMode.BLOCKING_RENDER) { + if (!staticPaths) { throw new Error( `Page "${page}" is missing exported function "generateStaticParams()", which is required with "output: export" config.` ) } + const resolvedWithoutSlash = removeTrailingSlash(resolvedUrlPathname) - if (!staticPaths?.includes(resolvedWithoutSlash)) { + if (!staticPaths.includes(resolvedWithoutSlash)) { throw new Error( `Page "${page}" is missing param "${resolvedWithoutSlash}" in "generateStaticParams()", which is required with "output: export" config.` ) @@ -2732,10 +2733,10 @@ export default abstract class Server< // the prerendered page. This ensures that the correct content is served // to the bot in the head. if ( - fallbackMode === FallbackMode.SERVE_PRERENDER && + fallbackMode === FallbackMode.SERVE_STATIC_PRERENDER && isBot(req.headers['user-agent'] || '') ) { - fallbackMode = FallbackMode.BLOCKING_RENDER + fallbackMode = FallbackMode.BLOCKING_STATIC_RENDER } // skip on-demand revalidate if cache is not present and @@ -2760,7 +2761,7 @@ export default abstract class Server< isOnDemandRevalidate && (fallbackMode !== FallbackMode.NOT_FOUND || previousCacheEntry) ) { - fallbackMode = FallbackMode.BLOCKING_RENDER + fallbackMode = FallbackMode.BLOCKING_STATIC_RENDER } // We use `ssgCacheKey` here as it is normalized to match the encoding @@ -2782,7 +2783,7 @@ export default abstract class Server< // @ts-expect-error internal field if (this.nextConfig.experimental.isExperimentalCompile) { - fallbackMode = FallbackMode.BLOCKING_RENDER + fallbackMode = FallbackMode.BLOCKING_STATIC_RENDER } // When we did not respond from cache, we need to choose to block on @@ -2799,7 +2800,7 @@ export default abstract class Server< if ( process.env.NEXT_RUNTIME !== 'edge' && !this.minimalMode && - fallbackMode !== FallbackMode.BLOCKING_RENDER && + fallbackMode !== FallbackMode.BLOCKING_STATIC_RENDER && staticPathKey && !didRespond && !isPreviewMode && diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index 60ccdcb9c409c..32914300210fa 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -170,7 +170,7 @@ export default class DevServer extends Server { // 5MB max: 5 * 1024 * 1024, length(value) { - return JSON.stringify(value.staticPaths).length + return JSON.stringify(value.staticPaths)?.length ?? 0 }, }) this.renderOpts.ampSkipValidation = @@ -787,13 +787,14 @@ export default class DevServer extends Server { [] ) .then((res) => { - const { prerenderedRoutes: staticPaths = [], fallback } = res.value + const { prerenderedRoutes: staticPaths, fallbackMode: fallback } = + res.value if (!isAppPath && this.nextConfig.output === 'export') { - if (fallback === FallbackMode.BLOCKING_RENDER) { + if (fallback === FallbackMode.BLOCKING_STATIC_RENDER) { throw new Error( 'getStaticPaths with "fallback: blocking" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export' ) - } else if (fallback === FallbackMode.SERVE_PRERENDER) { + } else if (fallback === FallbackMode.SERVE_STATIC_PRERENDER) { throw new Error( 'getStaticPaths with "fallback: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export' ) @@ -801,11 +802,11 @@ export default class DevServer extends Server { } const value: { - staticPaths: string[] - fallbackMode: FallbackMode + staticPaths: string[] | undefined + fallbackMode: FallbackMode | undefined } = { - staticPaths: staticPaths.map((route) => route.path), - fallbackMode: fallback ?? FallbackMode.NOT_FOUND, + staticPaths: staticPaths?.map((route) => route.path), + fallbackMode: fallback, } this.staticPathsCache.set(pathname, value) return value diff --git a/test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts b/test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts index 2e7fb9f90aa59..fa63a3f46133e 100644 --- a/test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts +++ b/test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts @@ -14,6 +14,16 @@ describe('app dir - with output export - dynamic missing gsp dev', () => { }) }) + it('should error when dynamic route is set to true', async () => { + await runTests({ + isDev: true, + dynamicPage: 'undefined', + dynamicParams: 'true', + expectedErrMsg: + '"dynamicParams: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/app/building-your-application/deploying/static-exports', + }) + }) + it('should error when client component has generateStaticParams', async () => { const expectedErrMsg = process.env.TURBOPACK_DEV ? 'Page "test/integration/app-dir-export/app/another/[slug]/page.js" cannot use both "use client" and export function "generateStaticParams()".' diff --git a/test/integration/app-dir-export/test/utils.ts b/test/integration/app-dir-export/test/utils.ts index 8e8d00d29d711..82b4e6c08f840 100644 --- a/test/integration/app-dir-export/test/utils.ts +++ b/test/integration/app-dir-export/test/utils.ts @@ -106,6 +106,7 @@ export async function runTests({ isDev = false, trailingSlash = true, dynamicPage, + dynamicParams, dynamicApiRoute, generateStaticParamsOpt, expectedErrMsg, @@ -113,6 +114,7 @@ export async function runTests({ isDev?: boolean trailingSlash?: boolean dynamicPage?: string + dynamicParams?: string dynamicApiRoute?: string generateStaticParamsOpt?: 'set noop' | 'set client' expectedErrMsg?: string @@ -129,12 +131,18 @@ export async function runTests({ `const dynamic = ${dynamicPage}` ) } + if (dynamicApiRoute !== undefined) { apiJson.replace( `const dynamic = 'force-static'`, `const dynamic = ${dynamicApiRoute}` ) } + + if (dynamicParams !== undefined) { + slugPage.prepend(`export const dynamicParams = ${dynamicParams}\n`) + } + if (generateStaticParamsOpt === 'set noop') { slugPage.replace('export function generateStaticParams', 'function noop') } else if (generateStaticParamsOpt === 'set client') {