From ff0ef9cf6d6011e8cbb6218c6e76866748c0f9b5 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 17 Dec 2024 10:16:46 -0700 Subject: [PATCH] chore: move static paths utils into own folder (#73928) Modifies no code, just moving files around to simplify the upstream review. --- packages/next/src/build/index.ts | 3 +- packages/next/src/build/static-paths/app.ts | 292 +++++++++ packages/next/src/build/static-paths/pages.ts | 244 ++++++++ packages/next/src/build/static-paths/types.ts | 20 + packages/next/src/build/utils.ts | 559 +----------------- .../src/server/dev/static-paths-worker.ts | 12 +- 6 files changed, 568 insertions(+), 562 deletions(-) create mode 100644 packages/next/src/build/static-paths/app.ts create mode 100644 packages/next/src/build/static-paths/pages.ts create mode 100644 packages/next/src/build/static-paths/types.ts diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index a2d9c4731741f..db15eb0aaef0c 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -133,7 +133,7 @@ import { collectRoutesUsingEdgeRuntime, collectMeta, } from './utils' -import type { PageInfo, PageInfos, PrerenderedRoute } from './utils' +import type { PageInfo, PageInfos } from './utils' import type { AppSegmentConfig } from './segment-config/app/app-segment-config' import { writeBuildId } from './write-build-id' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' @@ -212,6 +212,7 @@ import { formatNodeOptions, getParsedNodeOptionsWithoutInspect, } from '../server/lib/utils' +import type { PrerenderedRoute } from './static-paths/types' type Fallback = null | boolean | string diff --git a/packages/next/src/build/static-paths/app.ts b/packages/next/src/build/static-paths/app.ts new file mode 100644 index 0000000000000..a8c34019d2632 --- /dev/null +++ b/packages/next/src/build/static-paths/app.ts @@ -0,0 +1,292 @@ +import { + IncrementalCache, + type CacheHandler, +} from '../../server/lib/incremental-cache' +import type { AppPageModule } from '../../server/route-modules/app-page/module.compiled' +import type { AppSegment } from '../segment-config/app/app-segments' +import type { StaticPathsResult } from './types' +import type { Params } from '../../server/request/params' + +import path from 'path' +import { + FallbackMode, + fallbackModeToStaticPathsResult, +} from '../../lib/fallback' +import * as ciEnvironment from '../../server/ci-info' +import { formatDynamicImportPath } from '../../lib/format-dynamic-import-path' +import { interopDefault } from '../../lib/interop-default' +import { AfterRunner } from '../../server/after/run-with-after' +import { createWorkStore } from '../../server/async-storage/work-store' +import { nodeFs } from '../../server/lib/node-fs-methods' +import { getParamKeys } from '../../server/request/fallback-params' +import { buildStaticPaths } from './pages' + +export async function buildAppStaticPaths({ + dir, + page, + distDir, + dynamicIO, + authInterrupts, + configFileName, + segments, + isrFlushToDisk, + cacheHandler, + cacheLifeProfiles, + requestHeaders, + maxMemoryCacheSize, + fetchCacheKeyPrefix, + nextConfigOutput, + ComponentMod, + isRoutePPREnabled, + buildId, +}: { + dir: string + page: string + dynamicIO: boolean + authInterrupts: boolean + configFileName: string + segments: AppSegment[] + distDir: string + isrFlushToDisk?: boolean + fetchCacheKeyPrefix?: string + cacheHandler?: string + cacheLifeProfiles?: { + [profile: string]: import('../../server/use-cache/cache-life').CacheLife + } + maxMemoryCacheSize?: number + requestHeaders: IncrementalCache['requestHeaders'] + nextConfigOutput: 'standalone' | 'export' | undefined + ComponentMod: AppPageModule + isRoutePPREnabled: boolean | undefined + buildId: string +}): Promise> { + 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' + ) + } + + ComponentMod.patchFetch() + + let CurCacheHandler: typeof CacheHandler | undefined + if (cacheHandler) { + CurCacheHandler = interopDefault( + await import(formatDynamicImportPath(dir, cacheHandler)).then( + (mod) => mod.default || mod + ) + ) + } + + const incrementalCache = new IncrementalCache({ + fs: nodeFs, + dev: true, + dynamicIO, + flushToDisk: isrFlushToDisk, + serverDistDir: path.join(distDir, 'server'), + fetchCacheKeyPrefix, + maxMemoryCacheSize, + getPrerenderManifest: () => ({ + version: -1 as any, // letting us know this doesn't conform to spec + routes: {}, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: null as any, // `preview` is special case read in next-dev-server + }), + CurCacheHandler, + requestHeaders, + minimalMode: ciEnvironment.hasNextSupport, + }) + + 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 afterRunner = new AfterRunner() + + const store = createWorkStore({ + page, + // We're discovering the parameters here, so we don't have any unknown + // ones. + fallbackRouteParams: null, + renderOpts: { + incrementalCache, + cacheLifeProfiles, + supportsDynamicResponse: true, + isRevalidate: false, + experimental: { + dynamicIO, + authInterrupts, + }, + waitUntil: afterRunner.context.waitUntil, + onClose: afterRunner.context.onClose, + onAfterTaskError: afterRunner.context.onTaskError, + buildId, + }, + }) + + const routeParams = await ComponentMod.workAsyncStorage.run( + store, + async () => { + async function builtRouteParams( + parentsParams: Params[] = [], + idx = 0 + ): Promise { + // If we don't have any more to process, then we're done. + if (idx === segments.length) return parentsParams + + const current = segments[idx] + + if ( + typeof current.generateStaticParams !== 'function' && + idx < segments.length + ) { + return builtRouteParams(parentsParams, idx + 1) + } + + const params: Params[] = [] + + if (current.generateStaticParams) { + // fetchCache can be used to inform the fetch() defaults used inside + // of generateStaticParams. revalidate and dynamic options don't come into + // play within generateStaticParams. + if (typeof current.config?.fetchCache !== 'undefined') { + store.fetchCache = current.config.fetchCache + } + + if (parentsParams.length > 0) { + for (const parentParams of parentsParams) { + const result = await current.generateStaticParams({ + params: parentParams, + }) + + for (const item of result) { + params.push({ ...parentParams, ...item }) + } + } + } else { + const result = await current.generateStaticParams({ params: {} }) + + params.push(...result) + } + } + + if (idx < segments.length) { + return builtRouteParams(params, idx + 1) + } + + return params + } + + return builtRouteParams() + } + ) + + let lastDynamicSegmentHadGenerateStaticParams = false + 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 + + throw new Error( + `Segment "${relative}" exports "dynamicParams: false" but the param "${segment.param}" is missing from the generated route params.` + ) + } + } + + if ( + segment.isDynamicSegment && + typeof segment.generateStaticParams !== 'function' + ) { + lastDynamicSegmentHadGenerateStaticParams = false + } else if (typeof segment.generateStaticParams === 'function') { + lastDynamicSegmentHadGenerateStaticParams = true + } + } + + // 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 supportsRoutePreGeneration = + hadAllParamsGenerated || process.env.NODE_ENV === 'production' + + const fallbackMode = dynamicParams + ? supportsRoutePreGeneration + ? isRoutePPREnabled + ? FallbackMode.PRERENDER + : FallbackMode.BLOCKING_STATIC_RENDER + : undefined + : FallbackMode.NOT_FOUND + + let result: Partial = { + fallbackMode, + prerenderedRoutes: lastDynamicSegmentHadGenerateStaticParams + ? [] + : undefined, + } + + 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) { + result.prerenderedRoutes ??= [] + result.prerenderedRoutes.unshift({ + path: page, + encoded: page, + fallbackRouteParams: getParamKeys(page), + }) + } + + await afterRunner.executeAfter() + + return result +} diff --git a/packages/next/src/build/static-paths/pages.ts b/packages/next/src/build/static-paths/pages.ts new file mode 100644 index 0000000000000..6c5f72c923207 --- /dev/null +++ b/packages/next/src/build/static-paths/pages.ts @@ -0,0 +1,244 @@ +import type { GetStaticPaths, GetStaticPathsResult } from '../../types' +import type { PrerenderedRoute, StaticPathsResult } from './types' + +import { normalizeLocalePath } from '../../shared/lib/i18n/normalize-locale-path' +import { parseStaticPathsResult } from '../../lib/fallback' +import escapePathDelimiters from '../../shared/lib/router/utils/escape-path-delimiters' +import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash' +import { getRouteMatcher } from '../../shared/lib/router/utils/route-matcher' +import { getRouteRegex } from '../../shared/lib/router/utils/route-regex' + +export async function buildStaticPaths({ + page, + getStaticPaths, + staticPathsResult, + configFileName, + locales, + defaultLocale, + appDir, +}: { + page: string + getStaticPaths?: GetStaticPaths + staticPathsResult?: GetStaticPathsResult + configFileName: string + locales?: string[] + defaultLocale?: string + appDir?: boolean +}): Promise { + const prerenderedRoutes: PrerenderedRoute[] = [] + const _routeRegex = getRouteRegex(page) + const _routeMatcher = getRouteMatcher(_routeRegex) + + // Get the default list of allowed params. + const routeParameterKeys = Object.keys(_routeMatcher(page)) + + if (!staticPathsResult) { + if (getStaticPaths) { + staticPathsResult = await getStaticPaths({ locales, defaultLocale }) + } else { + throw new Error( + `invariant: attempted to buildStaticPaths without "staticPathsResult" or "getStaticPaths" ${page}` + ) + } + } + + const expectedReturnVal = + `Expected: { paths: [], fallback: boolean }\n` + + `See here for more info: https://nextjs.org/docs/messages/invalid-getstaticpaths-value` + + if ( + !staticPathsResult || + typeof staticPathsResult !== 'object' || + Array.isArray(staticPathsResult) + ) { + throw new Error( + `Invalid value returned from getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}` + ) + } + + const invalidStaticPathKeys = Object.keys(staticPathsResult).filter( + (key) => !(key === 'paths' || key === 'fallback') + ) + + if (invalidStaticPathKeys.length > 0) { + throw new Error( + `Extra keys returned from getStaticPaths in ${page} (${invalidStaticPathKeys.join( + ', ' + )}) ${expectedReturnVal}` + ) + } + + if ( + !( + typeof staticPathsResult.fallback === 'boolean' || + staticPathsResult.fallback === 'blocking' + ) + ) { + throw new Error( + `The \`fallback\` key must be returned from getStaticPaths in ${page}.\n` + + expectedReturnVal + ) + } + + const toPrerender = staticPathsResult.paths + + if (!Array.isArray(toPrerender)) { + throw new Error( + `Invalid \`paths\` value returned from getStaticPaths in ${page}.\n` + + `\`paths\` must be an array of strings or objects of shape { params: [key: string]: string }` + ) + } + + toPrerender.forEach((entry) => { + // For a string-provided path, we must make sure it matches the dynamic + // route. + if (typeof entry === 'string') { + entry = removeTrailingSlash(entry) + + const localePathResult = normalizeLocalePath(entry, locales) + let cleanedEntry = entry + + if (localePathResult.detectedLocale) { + cleanedEntry = entry.slice(localePathResult.detectedLocale.length + 1) + } else if (defaultLocale) { + entry = `/${defaultLocale}${entry}` + } + + const result = _routeMatcher(cleanedEntry) + if (!result) { + throw new Error( + `The provided path \`${cleanedEntry}\` does not match the page: \`${page}\`.` + ) + } + + // If leveraging the string paths variant the entry should already be + // encoded so we decode the segments ensuring we only escape path + // delimiters + prerenderedRoutes.push({ + path: entry + .split('/') + .map((segment) => + escapePathDelimiters(decodeURIComponent(segment), true) + ) + .join('/'), + encoded: entry, + fallbackRouteParams: undefined, + }) + } + // For the object-provided path, we must make sure it specifies all + // required keys. + else { + const invalidKeys = Object.keys(entry).filter( + (key) => key !== 'params' && key !== 'locale' + ) + + if (invalidKeys.length) { + throw new Error( + `Additional keys were returned from \`getStaticPaths\` in page "${page}". ` + + `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` + + `\n\n\treturn { params: { ${routeParameterKeys + .map((k) => `${k}: ...`) + .join(', ')} } }` + + `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n` + ) + } + + const { params = {} } = entry + let builtPage = page + let encodedBuiltPage = page + + routeParameterKeys.forEach((validParamKey) => { + const { repeat, optional } = _routeRegex.groups[validParamKey] + let paramValue = params[validParamKey] + if ( + optional && + params.hasOwnProperty(validParamKey) && + (paramValue === null || + paramValue === undefined || + (paramValue as any) === false) + ) { + paramValue = [] + } + if ( + (repeat && !Array.isArray(paramValue)) || + (!repeat && typeof paramValue !== 'string') + ) { + // If this is from app directory, and not all params were provided, + // then filter this out. + if (appDir && typeof paramValue === 'undefined') { + builtPage = '' + encodedBuiltPage = '' + return + } + + throw new Error( + `A required parameter (${validParamKey}) was not provided as ${ + repeat ? 'an array' : 'a string' + } received ${typeof paramValue} in ${ + appDir ? 'generateStaticParams' : 'getStaticPaths' + } for ${page}` + ) + } + let replaced = `[${repeat ? '...' : ''}${validParamKey}]` + if (optional) { + replaced = `[${replaced}]` + } + builtPage = builtPage + .replace( + replaced, + repeat + ? (paramValue as string[]) + .map((segment) => escapePathDelimiters(segment, true)) + .join('/') + : escapePathDelimiters(paramValue as string, true) + ) + .replace(/\\/g, '/') + .replace(/(?!^)\/$/, '') + + encodedBuiltPage = encodedBuiltPage + .replace( + replaced, + repeat + ? (paramValue as string[]).map(encodeURIComponent).join('/') + : encodeURIComponent(paramValue as string) + ) + .replace(/\\/g, '/') + .replace(/(?!^)\/$/, '') + }) + + if (!builtPage && !encodedBuiltPage) { + return + } + + if (entry.locale && !locales?.includes(entry.locale)) { + throw new Error( + `Invalid locale returned from getStaticPaths for ${page}, the locale ${entry.locale} is not specified in ${configFileName}` + ) + } + const curLocale = entry.locale || defaultLocale || '' + + prerenderedRoutes.push({ + path: `${curLocale ? `/${curLocale}` : ''}${ + curLocale && builtPage === '/' ? '' : builtPage + }`, + encoded: `${curLocale ? `/${curLocale}` : ''}${ + curLocale && encodedBuiltPage === '/' ? '' : encodedBuiltPage + }`, + fallbackRouteParams: undefined, + }) + } + }) + + const seen = new Set() + + return { + fallbackMode: parseStaticPathsResult(staticPathsResult.fallback), + prerenderedRoutes: prerenderedRoutes.filter((route) => { + if (seen.has(route.path)) return false + + // Filter out duplicate paths. + seen.add(route.path) + return true + }), + } +} diff --git a/packages/next/src/build/static-paths/types.ts b/packages/next/src/build/static-paths/types.ts new file mode 100644 index 0000000000000..ac9a8fd124c28 --- /dev/null +++ b/packages/next/src/build/static-paths/types.ts @@ -0,0 +1,20 @@ +import type { FallbackMode } from '../../lib/fallback' + +type StaticPrerenderedRoute = { + path: string + encoded: string + fallbackRouteParams: undefined +} + +type FallbackPrerenderedRoute = { + path: string + encoded: string + fallbackRouteParams: readonly string[] +} + +export type PrerenderedRoute = StaticPrerenderedRoute | FallbackPrerenderedRoute + +export type StaticPathsResult = { + fallbackMode: FallbackMode + prerenderedRoutes: PrerenderedRoute[] +} diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 2359def18ce4f..80cf8ce046e74 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -2,12 +2,7 @@ import type { NextConfig, NextConfigComplete } from '../server/config-shared' import type { ExperimentalPPRConfig } from '../server/lib/experimental/ppr' import type { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin' import type { AssetBinding } from './webpack/loaders/get-module-build-info' -import type { - GetStaticPaths, - GetStaticPathsResult, - PageConfig, - ServerRuntime, -} from '../types' +import type { GetStaticPathsResult, PageConfig, ServerRuntime } from '../types' import type { BuildManifest } from '../server/get-page-files' import type { Redirect, @@ -57,14 +52,9 @@ import { UNDERSCORE_NOT_FOUND_ROUTE, } from '../shared/lib/constants' import prettyBytes from '../lib/pretty-bytes' -import { getRouteRegex } from '../shared/lib/router/utils/route-regex' -import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher' import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic' -import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters' import { findPageFile } from '../server/lib/find-page-file' -import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' import { isEdgeRuntime } from '../lib/is-edge-runtime' -import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import * as Log from './output/log' import { loadComponents } from '../server/load-components' import type { LoadComponentsReturnType } from '../server/load-components' @@ -75,32 +65,21 @@ import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-pa import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { getRuntimeContext } from '../server/web/sandbox' import { isClientReference } from '../lib/client-reference' -import { createWorkStore } from '../server/async-storage/work-store' -import type { CacheHandler } from '../server/lib/incremental-cache' -import { IncrementalCache } from '../server/lib/incremental-cache' -import { nodeFs } from '../server/lib/node-fs-methods' -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 { interopDefault } from '../lib/interop-default' import type { PageExtensions } from './page-extensions-type' -import { formatDynamicImportPath } from '../lib/format-dynamic-import-path' import { isInterceptionRouteAppPath } from '../server/lib/interception-routes' import { checkIsRoutePPREnabled } from '../server/lib/experimental/ppr' -import type { Params } from '../server/request/params' -import { FallbackMode } from '../lib/fallback' -import { - fallbackModeToStaticPathsResult, - parseStaticPathsResult, -} from '../lib/fallback' -import { getParamKeys } from '../server/request/fallback-params' +import type { FallbackMode } from '../lib/fallback' import type { OutgoingHttpHeaders } from 'http' import type { AppSegmentConfig } from './segment-config/app/app-segment-config' import type { AppSegment } from './segment-config/app/app-segments' import { collectSegments } from './segment-config/app/app-segments' import { createIncrementalCache } from '../export/helpers/create-incremental-cache' -import { AfterRunner } from '../server/after/run-with-after' +import type { PrerenderedRoute } from './static-paths/types' +import { buildAppStaticPaths } from './static-paths/app' +import { buildStaticPaths } from './static-paths/pages' export type ROUTER_TYPE = 'pages' | 'app' @@ -952,534 +931,6 @@ export async function getJsPageSizeInKb( return [-1, -1] } -type StaticPrerenderedRoute = { - path: string - encoded: string - fallbackRouteParams: undefined -} - -type FallbackPrerenderedRoute = { - path: string - encoded: string - fallbackRouteParams: readonly string[] -} - -export type PrerenderedRoute = StaticPrerenderedRoute | FallbackPrerenderedRoute - -export type StaticPathsResult = { - fallbackMode: FallbackMode - prerenderedRoutes: PrerenderedRoute[] -} - -export async function buildStaticPaths({ - page, - getStaticPaths, - staticPathsResult, - configFileName, - locales, - defaultLocale, - appDir, -}: { - page: string - getStaticPaths?: GetStaticPaths - staticPathsResult?: GetStaticPathsResult - configFileName: string - locales?: string[] - defaultLocale?: string - appDir?: boolean -}): Promise { - const prerenderedRoutes: PrerenderedRoute[] = [] - const _routeRegex = getRouteRegex(page) - const _routeMatcher = getRouteMatcher(_routeRegex) - - // Get the default list of allowed params. - const routeParameterKeys = Object.keys(_routeMatcher(page)) - - if (!staticPathsResult) { - if (getStaticPaths) { - staticPathsResult = await getStaticPaths({ locales, defaultLocale }) - } else { - throw new Error( - `invariant: attempted to buildStaticPaths without "staticPathsResult" or "getStaticPaths" ${page}` - ) - } - } - - const expectedReturnVal = - `Expected: { paths: [], fallback: boolean }\n` + - `See here for more info: https://nextjs.org/docs/messages/invalid-getstaticpaths-value` - - if ( - !staticPathsResult || - typeof staticPathsResult !== 'object' || - Array.isArray(staticPathsResult) - ) { - throw new Error( - `Invalid value returned from getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}` - ) - } - - const invalidStaticPathKeys = Object.keys(staticPathsResult).filter( - (key) => !(key === 'paths' || key === 'fallback') - ) - - if (invalidStaticPathKeys.length > 0) { - throw new Error( - `Extra keys returned from getStaticPaths in ${page} (${invalidStaticPathKeys.join( - ', ' - )}) ${expectedReturnVal}` - ) - } - - if ( - !( - typeof staticPathsResult.fallback === 'boolean' || - staticPathsResult.fallback === 'blocking' - ) - ) { - throw new Error( - `The \`fallback\` key must be returned from getStaticPaths in ${page}.\n` + - expectedReturnVal - ) - } - - const toPrerender = staticPathsResult.paths - - if (!Array.isArray(toPrerender)) { - throw new Error( - `Invalid \`paths\` value returned from getStaticPaths in ${page}.\n` + - `\`paths\` must be an array of strings or objects of shape { params: [key: string]: string }` - ) - } - - toPrerender.forEach((entry) => { - // For a string-provided path, we must make sure it matches the dynamic - // route. - if (typeof entry === 'string') { - entry = removeTrailingSlash(entry) - - const localePathResult = normalizeLocalePath(entry, locales) - let cleanedEntry = entry - - if (localePathResult.detectedLocale) { - cleanedEntry = entry.slice(localePathResult.detectedLocale.length + 1) - } else if (defaultLocale) { - entry = `/${defaultLocale}${entry}` - } - - const result = _routeMatcher(cleanedEntry) - if (!result) { - throw new Error( - `The provided path \`${cleanedEntry}\` does not match the page: \`${page}\`.` - ) - } - - // If leveraging the string paths variant the entry should already be - // encoded so we decode the segments ensuring we only escape path - // delimiters - prerenderedRoutes.push({ - path: entry - .split('/') - .map((segment) => - escapePathDelimiters(decodeURIComponent(segment), true) - ) - .join('/'), - encoded: entry, - fallbackRouteParams: undefined, - }) - } - // For the object-provided path, we must make sure it specifies all - // required keys. - else { - const invalidKeys = Object.keys(entry).filter( - (key) => key !== 'params' && key !== 'locale' - ) - - if (invalidKeys.length) { - throw new Error( - `Additional keys were returned from \`getStaticPaths\` in page "${page}". ` + - `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` + - `\n\n\treturn { params: { ${routeParameterKeys - .map((k) => `${k}: ...`) - .join(', ')} } }` + - `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n` - ) - } - - const { params = {} } = entry - let builtPage = page - let encodedBuiltPage = page - - routeParameterKeys.forEach((validParamKey) => { - const { repeat, optional } = _routeRegex.groups[validParamKey] - let paramValue = params[validParamKey] - if ( - optional && - params.hasOwnProperty(validParamKey) && - (paramValue === null || - paramValue === undefined || - (paramValue as any) === false) - ) { - paramValue = [] - } - if ( - (repeat && !Array.isArray(paramValue)) || - (!repeat && typeof paramValue !== 'string') - ) { - // If this is from app directory, and not all params were provided, - // then filter this out. - if (appDir && typeof paramValue === 'undefined') { - builtPage = '' - encodedBuiltPage = '' - return - } - - throw new Error( - `A required parameter (${validParamKey}) was not provided as ${ - repeat ? 'an array' : 'a string' - } received ${typeof paramValue} in ${ - appDir ? 'generateStaticParams' : 'getStaticPaths' - } for ${page}` - ) - } - let replaced = `[${repeat ? '...' : ''}${validParamKey}]` - if (optional) { - replaced = `[${replaced}]` - } - builtPage = builtPage - .replace( - replaced, - repeat - ? (paramValue as string[]) - .map((segment) => escapePathDelimiters(segment, true)) - .join('/') - : escapePathDelimiters(paramValue as string, true) - ) - .replace(/\\/g, '/') - .replace(/(?!^)\/$/, '') - - encodedBuiltPage = encodedBuiltPage - .replace( - replaced, - repeat - ? (paramValue as string[]).map(encodeURIComponent).join('/') - : encodeURIComponent(paramValue as string) - ) - .replace(/\\/g, '/') - .replace(/(?!^)\/$/, '') - }) - - if (!builtPage && !encodedBuiltPage) { - return - } - - if (entry.locale && !locales?.includes(entry.locale)) { - throw new Error( - `Invalid locale returned from getStaticPaths for ${page}, the locale ${entry.locale} is not specified in ${configFileName}` - ) - } - const curLocale = entry.locale || defaultLocale || '' - - prerenderedRoutes.push({ - path: `${curLocale ? `/${curLocale}` : ''}${ - curLocale && builtPage === '/' ? '' : builtPage - }`, - encoded: `${curLocale ? `/${curLocale}` : ''}${ - curLocale && encodedBuiltPage === '/' ? '' : encodedBuiltPage - }`, - fallbackRouteParams: undefined, - }) - } - }) - - const seen = new Set() - - return { - fallbackMode: parseStaticPathsResult(staticPathsResult.fallback), - prerenderedRoutes: prerenderedRoutes.filter((route) => { - if (seen.has(route.path)) return false - - // Filter out duplicate paths. - seen.add(route.path) - return true - }), - } -} - -export type PartialStaticPathsResult = { - [P in keyof StaticPathsResult]: StaticPathsResult[P] | undefined -} - -export async function buildAppStaticPaths({ - dir, - page, - distDir, - dynamicIO, - authInterrupts, - configFileName, - segments, - isrFlushToDisk, - cacheHandler, - cacheLifeProfiles, - requestHeaders, - maxMemoryCacheSize, - fetchCacheKeyPrefix, - nextConfigOutput, - ComponentMod, - isRoutePPREnabled, - buildId, -}: { - dir: string - page: string - dynamicIO: boolean - authInterrupts: boolean - configFileName: string - segments: AppSegment[] - distDir: string - isrFlushToDisk?: boolean - fetchCacheKeyPrefix?: string - cacheHandler?: string - cacheLifeProfiles?: { - [profile: string]: import('../server/use-cache/cache-life').CacheLife - } - maxMemoryCacheSize?: number - requestHeaders: IncrementalCache['requestHeaders'] - nextConfigOutput: 'standalone' | 'export' | undefined - ComponentMod: AppPageModule - isRoutePPREnabled: boolean | undefined - buildId: string -}): Promise { - 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' - ) - } - - ComponentMod.patchFetch() - - let CurCacheHandler: typeof CacheHandler | undefined - if (cacheHandler) { - CurCacheHandler = interopDefault( - await import(formatDynamicImportPath(dir, cacheHandler)).then( - (mod) => mod.default || mod - ) - ) - } - - const incrementalCache = new IncrementalCache({ - fs: nodeFs, - dev: true, - dynamicIO, - flushToDisk: isrFlushToDisk, - serverDistDir: path.join(distDir, 'server'), - fetchCacheKeyPrefix, - maxMemoryCacheSize, - getPrerenderManifest: () => ({ - version: -1 as any, // letting us know this doesn't conform to spec - routes: {}, - dynamicRoutes: {}, - notFoundRoutes: [], - preview: null as any, // `preview` is special case read in next-dev-server - }), - CurCacheHandler, - requestHeaders, - minimalMode: ciEnvironment.hasNextSupport, - }) - - 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 afterRunner = new AfterRunner() - - const store = createWorkStore({ - page, - // We're discovering the parameters here, so we don't have any unknown - // ones. - fallbackRouteParams: null, - renderOpts: { - incrementalCache, - cacheLifeProfiles, - supportsDynamicResponse: true, - isRevalidate: false, - experimental: { - dynamicIO, - authInterrupts, - }, - waitUntil: afterRunner.context.waitUntil, - onClose: afterRunner.context.onClose, - onAfterTaskError: afterRunner.context.onTaskError, - buildId, - }, - }) - - const routeParams = await ComponentMod.workAsyncStorage.run( - store, - async () => { - async function builtRouteParams( - parentsParams: Params[] = [], - idx = 0 - ): Promise { - // If we don't have any more to process, then we're done. - if (idx === segments.length) return parentsParams - - const current = segments[idx] - - if ( - typeof current.generateStaticParams !== 'function' && - idx < segments.length - ) { - return builtRouteParams(parentsParams, idx + 1) - } - - const params: Params[] = [] - - if (current.generateStaticParams) { - // fetchCache can be used to inform the fetch() defaults used inside - // of generateStaticParams. revalidate and dynamic options don't come into - // play within generateStaticParams. - if (typeof current.config?.fetchCache !== 'undefined') { - store.fetchCache = current.config.fetchCache - } - - if (parentsParams.length > 0) { - for (const parentParams of parentsParams) { - const result = await current.generateStaticParams({ - params: parentParams, - }) - - for (const item of result) { - params.push({ ...parentParams, ...item }) - } - } - } else { - const result = await current.generateStaticParams({ params: {} }) - - params.push(...result) - } - } - - if (idx < segments.length) { - return builtRouteParams(params, idx + 1) - } - - return params - } - - return builtRouteParams() - } - ) - - let lastDynamicSegmentHadGenerateStaticParams = false - 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 - - throw new Error( - `Segment "${relative}" exports "dynamicParams: false" but the param "${segment.param}" is missing from the generated route params.` - ) - } - } - - if ( - segment.isDynamicSegment && - typeof segment.generateStaticParams !== 'function' - ) { - lastDynamicSegmentHadGenerateStaticParams = false - } else if (typeof segment.generateStaticParams === 'function') { - lastDynamicSegmentHadGenerateStaticParams = true - } - } - - // 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 supportsRoutePreGeneration = - hadAllParamsGenerated || process.env.NODE_ENV === 'production' - - const fallbackMode = dynamicParams - ? supportsRoutePreGeneration - ? isRoutePPREnabled - ? FallbackMode.PRERENDER - : FallbackMode.BLOCKING_STATIC_RENDER - : undefined - : FallbackMode.NOT_FOUND - - let result: PartialStaticPathsResult = { - fallbackMode, - prerenderedRoutes: lastDynamicSegmentHadGenerateStaticParams - ? [] - : undefined, - } - - 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) { - result.prerenderedRoutes ??= [] - result.prerenderedRoutes.unshift({ - path: page, - encoded: page, - fallbackRouteParams: getParamKeys(page), - }) - } - - await afterRunner.executeAfter() - - return result -} - type PageIsStaticResult = { isRoutePPREnabled?: boolean isStatic?: boolean diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index 30b5338287c0a..3d7a4cb6acd8e 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -3,13 +3,9 @@ import type { NextConfigComplete } from '../config-shared' import '../require-hook' import '../node-environment' -import { - buildAppStaticPaths, - buildStaticPaths, - reduceAppConfig, -} from '../../build/utils' +import { reduceAppConfig } from '../../build/utils' import { collectSegments } from '../../build/segment-config/app/app-segments' -import type { PartialStaticPathsResult } from '../../build/utils' +import type { StaticPathsResult } from '../../build/static-paths/types' import { loadComponents } from '../load-components' import { setHttpClientAndAgentOptions } from '../setup-http-agent-env' import type { IncrementalCache } from '../lib/incremental-cache' @@ -19,6 +15,8 @@ import { type ExperimentalPPRConfig, } from '../lib/experimental/ppr' import { InvariantError } from '../../shared/lib/invariant-error' +import { buildAppStaticPaths } from '../../build/static-paths/app' +import { buildStaticPaths } from '../../build/static-paths/pages' type RuntimeConfig = { pprConfig: ExperimentalPPRConfig | undefined @@ -71,7 +69,7 @@ export async function loadStaticPaths({ nextConfigOutput: 'standalone' | 'export' | undefined buildId: string authInterrupts: boolean -}): Promise { +}): Promise> { // update work memory runtime-config require('../../shared/lib/runtime-config.external').setConfig(config) setHttpClientAndAgentOptions({