|
| 1 | +import { |
| 2 | + IncrementalCache, |
| 3 | + type CacheHandler, |
| 4 | +} from '../../server/lib/incremental-cache' |
| 5 | +import type { AppPageModule } from '../../server/route-modules/app-page/module.compiled' |
| 6 | +import type { AppSegment } from '../segment-config/app/app-segments' |
| 7 | +import type { StaticPathsResult } from './types' |
| 8 | +import type { Params } from '../../server/request/params' |
| 9 | + |
| 10 | +import path from 'path' |
| 11 | +import { |
| 12 | + FallbackMode, |
| 13 | + fallbackModeToStaticPathsResult, |
| 14 | +} from '../../lib/fallback' |
| 15 | +import * as ciEnvironment from '../../server/ci-info' |
| 16 | +import { formatDynamicImportPath } from '../../lib/format-dynamic-import-path' |
| 17 | +import { interopDefault } from '../../lib/interop-default' |
| 18 | +import { AfterRunner } from '../../server/after/run-with-after' |
| 19 | +import { createWorkStore } from '../../server/async-storage/work-store' |
| 20 | +import { nodeFs } from '../../server/lib/node-fs-methods' |
| 21 | +import { getParamKeys } from '../../server/request/fallback-params' |
| 22 | +import { buildStaticPaths } from './pages' |
| 23 | + |
| 24 | +export async function buildAppStaticPaths({ |
| 25 | + dir, |
| 26 | + page, |
| 27 | + distDir, |
| 28 | + dynamicIO, |
| 29 | + authInterrupts, |
| 30 | + configFileName, |
| 31 | + segments, |
| 32 | + isrFlushToDisk, |
| 33 | + cacheHandler, |
| 34 | + cacheLifeProfiles, |
| 35 | + requestHeaders, |
| 36 | + maxMemoryCacheSize, |
| 37 | + fetchCacheKeyPrefix, |
| 38 | + nextConfigOutput, |
| 39 | + ComponentMod, |
| 40 | + isRoutePPREnabled, |
| 41 | + buildId, |
| 42 | +}: { |
| 43 | + dir: string |
| 44 | + page: string |
| 45 | + dynamicIO: boolean |
| 46 | + authInterrupts: boolean |
| 47 | + configFileName: string |
| 48 | + segments: AppSegment[] |
| 49 | + distDir: string |
| 50 | + isrFlushToDisk?: boolean |
| 51 | + fetchCacheKeyPrefix?: string |
| 52 | + cacheHandler?: string |
| 53 | + cacheLifeProfiles?: { |
| 54 | + [profile: string]: import('../../server/use-cache/cache-life').CacheLife |
| 55 | + } |
| 56 | + maxMemoryCacheSize?: number |
| 57 | + requestHeaders: IncrementalCache['requestHeaders'] |
| 58 | + nextConfigOutput: 'standalone' | 'export' | undefined |
| 59 | + ComponentMod: AppPageModule |
| 60 | + isRoutePPREnabled: boolean | undefined |
| 61 | + buildId: string |
| 62 | +}): Promise<Partial<StaticPathsResult>> { |
| 63 | + if ( |
| 64 | + segments.some((generate) => generate.config?.dynamicParams === true) && |
| 65 | + nextConfigOutput === 'export' |
| 66 | + ) { |
| 67 | + throw new Error( |
| 68 | + '"dynamicParams: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/app/building-your-application/deploying/static-exports' |
| 69 | + ) |
| 70 | + } |
| 71 | + |
| 72 | + ComponentMod.patchFetch() |
| 73 | + |
| 74 | + let CurCacheHandler: typeof CacheHandler | undefined |
| 75 | + if (cacheHandler) { |
| 76 | + CurCacheHandler = interopDefault( |
| 77 | + await import(formatDynamicImportPath(dir, cacheHandler)).then( |
| 78 | + (mod) => mod.default || mod |
| 79 | + ) |
| 80 | + ) |
| 81 | + } |
| 82 | + |
| 83 | + const incrementalCache = new IncrementalCache({ |
| 84 | + fs: nodeFs, |
| 85 | + dev: true, |
| 86 | + dynamicIO, |
| 87 | + flushToDisk: isrFlushToDisk, |
| 88 | + serverDistDir: path.join(distDir, 'server'), |
| 89 | + fetchCacheKeyPrefix, |
| 90 | + maxMemoryCacheSize, |
| 91 | + getPrerenderManifest: () => ({ |
| 92 | + version: -1 as any, // letting us know this doesn't conform to spec |
| 93 | + routes: {}, |
| 94 | + dynamicRoutes: {}, |
| 95 | + notFoundRoutes: [], |
| 96 | + preview: null as any, // `preview` is special case read in next-dev-server |
| 97 | + }), |
| 98 | + CurCacheHandler, |
| 99 | + requestHeaders, |
| 100 | + minimalMode: ciEnvironment.hasNextSupport, |
| 101 | + }) |
| 102 | + |
| 103 | + const paramKeys = new Set<string>() |
| 104 | + |
| 105 | + const staticParamKeys = new Set<string>() |
| 106 | + for (const segment of segments) { |
| 107 | + if (segment.param) { |
| 108 | + paramKeys.add(segment.param) |
| 109 | + |
| 110 | + if (segment.config?.dynamicParams === false) { |
| 111 | + staticParamKeys.add(segment.param) |
| 112 | + } |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + const afterRunner = new AfterRunner() |
| 117 | + |
| 118 | + const store = createWorkStore({ |
| 119 | + page, |
| 120 | + // We're discovering the parameters here, so we don't have any unknown |
| 121 | + // ones. |
| 122 | + fallbackRouteParams: null, |
| 123 | + renderOpts: { |
| 124 | + incrementalCache, |
| 125 | + cacheLifeProfiles, |
| 126 | + supportsDynamicResponse: true, |
| 127 | + isRevalidate: false, |
| 128 | + experimental: { |
| 129 | + dynamicIO, |
| 130 | + authInterrupts, |
| 131 | + }, |
| 132 | + waitUntil: afterRunner.context.waitUntil, |
| 133 | + onClose: afterRunner.context.onClose, |
| 134 | + onAfterTaskError: afterRunner.context.onTaskError, |
| 135 | + buildId, |
| 136 | + }, |
| 137 | + }) |
| 138 | + |
| 139 | + const routeParams = await ComponentMod.workAsyncStorage.run( |
| 140 | + store, |
| 141 | + async () => { |
| 142 | + async function builtRouteParams( |
| 143 | + parentsParams: Params[] = [], |
| 144 | + idx = 0 |
| 145 | + ): Promise<Params[]> { |
| 146 | + // If we don't have any more to process, then we're done. |
| 147 | + if (idx === segments.length) return parentsParams |
| 148 | + |
| 149 | + const current = segments[idx] |
| 150 | + |
| 151 | + if ( |
| 152 | + typeof current.generateStaticParams !== 'function' && |
| 153 | + idx < segments.length |
| 154 | + ) { |
| 155 | + return builtRouteParams(parentsParams, idx + 1) |
| 156 | + } |
| 157 | + |
| 158 | + const params: Params[] = [] |
| 159 | + |
| 160 | + if (current.generateStaticParams) { |
| 161 | + // fetchCache can be used to inform the fetch() defaults used inside |
| 162 | + // of generateStaticParams. revalidate and dynamic options don't come into |
| 163 | + // play within generateStaticParams. |
| 164 | + if (typeof current.config?.fetchCache !== 'undefined') { |
| 165 | + store.fetchCache = current.config.fetchCache |
| 166 | + } |
| 167 | + |
| 168 | + if (parentsParams.length > 0) { |
| 169 | + for (const parentParams of parentsParams) { |
| 170 | + const result = await current.generateStaticParams({ |
| 171 | + params: parentParams, |
| 172 | + }) |
| 173 | + |
| 174 | + for (const item of result) { |
| 175 | + params.push({ ...parentParams, ...item }) |
| 176 | + } |
| 177 | + } |
| 178 | + } else { |
| 179 | + const result = await current.generateStaticParams({ params: {} }) |
| 180 | + |
| 181 | + params.push(...result) |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + if (idx < segments.length) { |
| 186 | + return builtRouteParams(params, idx + 1) |
| 187 | + } |
| 188 | + |
| 189 | + return params |
| 190 | + } |
| 191 | + |
| 192 | + return builtRouteParams() |
| 193 | + } |
| 194 | + ) |
| 195 | + |
| 196 | + let lastDynamicSegmentHadGenerateStaticParams = false |
| 197 | + for (const segment of segments) { |
| 198 | + // Check to see if there are any missing params for segments that have |
| 199 | + // dynamicParams set to false. |
| 200 | + if ( |
| 201 | + segment.param && |
| 202 | + segment.isDynamicSegment && |
| 203 | + segment.config?.dynamicParams === false |
| 204 | + ) { |
| 205 | + for (const params of routeParams) { |
| 206 | + if (segment.param in params) continue |
| 207 | + |
| 208 | + const relative = segment.filePath |
| 209 | + ? path.relative(dir, segment.filePath) |
| 210 | + : undefined |
| 211 | + |
| 212 | + throw new Error( |
| 213 | + `Segment "${relative}" exports "dynamicParams: false" but the param "${segment.param}" is missing from the generated route params.` |
| 214 | + ) |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + if ( |
| 219 | + segment.isDynamicSegment && |
| 220 | + typeof segment.generateStaticParams !== 'function' |
| 221 | + ) { |
| 222 | + lastDynamicSegmentHadGenerateStaticParams = false |
| 223 | + } else if (typeof segment.generateStaticParams === 'function') { |
| 224 | + lastDynamicSegmentHadGenerateStaticParams = true |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + // Determine if all the segments have had their parameters provided. If there |
| 229 | + // was no dynamic parameters, then we've collected all the params. |
| 230 | + const hadAllParamsGenerated = |
| 231 | + paramKeys.size === 0 || |
| 232 | + (routeParams.length > 0 && |
| 233 | + routeParams.every((params) => { |
| 234 | + for (const key of paramKeys) { |
| 235 | + if (key in params) continue |
| 236 | + return false |
| 237 | + } |
| 238 | + return true |
| 239 | + })) |
| 240 | + |
| 241 | + // TODO: dynamic params should be allowed to be granular per segment but |
| 242 | + // we need additional information stored/leveraged in the prerender |
| 243 | + // manifest to allow this behavior. |
| 244 | + const dynamicParams = segments.every( |
| 245 | + (segment) => segment.config?.dynamicParams !== false |
| 246 | + ) |
| 247 | + |
| 248 | + const supportsRoutePreGeneration = |
| 249 | + hadAllParamsGenerated || process.env.NODE_ENV === 'production' |
| 250 | + |
| 251 | + const fallbackMode = dynamicParams |
| 252 | + ? supportsRoutePreGeneration |
| 253 | + ? isRoutePPREnabled |
| 254 | + ? FallbackMode.PRERENDER |
| 255 | + : FallbackMode.BLOCKING_STATIC_RENDER |
| 256 | + : undefined |
| 257 | + : FallbackMode.NOT_FOUND |
| 258 | + |
| 259 | + let result: Partial<StaticPathsResult> = { |
| 260 | + fallbackMode, |
| 261 | + prerenderedRoutes: lastDynamicSegmentHadGenerateStaticParams |
| 262 | + ? [] |
| 263 | + : undefined, |
| 264 | + } |
| 265 | + |
| 266 | + if (hadAllParamsGenerated && fallbackMode) { |
| 267 | + result = await buildStaticPaths({ |
| 268 | + staticPathsResult: { |
| 269 | + fallback: fallbackModeToStaticPathsResult(fallbackMode), |
| 270 | + paths: routeParams.map((params) => ({ params })), |
| 271 | + }, |
| 272 | + page, |
| 273 | + configFileName, |
| 274 | + appDir: true, |
| 275 | + }) |
| 276 | + } |
| 277 | + |
| 278 | + // If the fallback mode is a prerender, we want to include the dynamic |
| 279 | + // route in the prerendered routes too. |
| 280 | + if (isRoutePPREnabled) { |
| 281 | + result.prerenderedRoutes ??= [] |
| 282 | + result.prerenderedRoutes.unshift({ |
| 283 | + path: page, |
| 284 | + encoded: page, |
| 285 | + fallbackRouteParams: getParamKeys(page), |
| 286 | + }) |
| 287 | + } |
| 288 | + |
| 289 | + await afterRunner.executeAfter() |
| 290 | + |
| 291 | + return result |
| 292 | +} |
0 commit comments