next.js/packages/next/src/build/static-paths/app.ts
app.ts1133 lines40.7 KB
import type { Params } from '../../server/request/params'
import type { AppPageModule } from '../../server/route-modules/app-page/module'
import type { AppSegment } from '../segment-config/app/app-segments'
import type {
  FallbackRouteParam,
  PrerenderedRoute,
  StaticPathsResult,
} from './types'

import path from 'node:path'
import { AfterRunner } from '../../server/after/run-with-after'
import { createWorkStore } from '../../server/async-storage/work-store'
import { FallbackMode } from '../../lib/fallback'
import type { IncrementalCache } from '../../server/lib/incremental-cache'
import {
  normalizePathname,
  encodeParam,
  extractPathnameRouteParamSegments,
  resolveRouteParamsFromTree,
} from './utils'
import escapePathDelimiters from '../../shared/lib/router/utils/escape-path-delimiters'
import { createIncrementalCache } from '../../export/helpers/create-incremental-cache'
import type { NextConfigComplete } from '../../server/config-shared'
import {
  type WorkStore,
  workAsyncStorage,
} from '../../server/app-render/work-async-storage.external'
import type { DynamicParamTypes } from '../../shared/lib/app-router-types'
import { getParamProperties } from '../../shared/lib/router/utils/get-segment-param'
import { throwEmptyGenerateStaticParamsError } from '../../shared/lib/errors/empty-generate-static-params-error'
import type { AppRouteModule } from '../../server/route-modules/app-route/module.compiled'
import type { NormalizedAppRoute } from '../../shared/lib/router/routes/app'
import { interceptionPrefixFromParamType } from '../../shared/lib/router/utils/interception-prefix-from-param-type'
import {
  type GenerateStaticParamsStore,
  workUnitAsyncStorage,
} from '../../server/app-render/work-unit-async-storage.external'
import type { ImplicitTags } from '../../server/lib/implicit-tags'
import { getImplicitTags } from '../../server/lib/implicit-tags'

/**
 * Filters out duplicate parameters from a list of parameters.
 * This function uses a Map to efficiently store and retrieve unique parameter combinations.
 *
 * @param childrenRouteParams - The keys of the parameters. These should be sorted to ensure consistent key generation.
 * @param routeParams - The list of parameter objects to filter.
 * @returns A new array containing only the unique parameter combinations.
 */
export function filterUniqueParams(
  childrenRouteParams: readonly { paramName: string }[],
  routeParams: readonly Params[]
): Params[] {
  // A Map is used to store unique parameter combinations. The key of the Map
  // is a string representation of the parameter combination, and the value
  // is the actual `Params` object.
  const unique = new Map<string, Params>()

  // Iterate over each parameter object in the input array.
  for (const params of routeParams) {
    let key = '' // Initialize an empty string to build the unique key for the current `params` object.

    // Iterate through the `routeParamKeys` (which are assumed to be sorted).
    // This consistent order is crucial for generating a stable and unique key
    // for each parameter combination.
    for (const { paramName: paramKey } of childrenRouteParams) {
      const value = params[paramKey]

      // Construct a part of the key using the parameter key and its value.
      // A type prefix (`A:` for Array, `S:` for String, `U:` for undefined) is added to the value
      // to prevent collisions. For example, `['a', 'b']` and `'a,b'` would
      // otherwise generate the same string representation, leading to incorrect
      // deduplication. This ensures that different types with the same string
      // representation are treated as distinct.
      let valuePart: string
      if (Array.isArray(value)) {
        valuePart = `A:${value.join(',')}`
      } else if (value === undefined) {
        valuePart = `U:undefined`
      } else {
        valuePart = `S:${value}`
      }
      key += `${paramKey}:${valuePart}|`
    }

    // If the generated key is not already in the `unique` Map, it means this
    // parameter combination is unique so far. Add it to the Map.
    if (!unique.has(key)) {
      unique.set(key, params)
    }
  }

  // Convert the Map's values (the unique `Params` objects) back into an array
  // and return it.
  return Array.from(unique.values())
}

/**
 * Generates all unique sub-combinations of Route Parameters from a list of Static Parameters.
 * This function creates all possible prefixes of the Route Parameters, which is
 * useful for generating Static Shells that can serve as Fallback Shells for more specific Route Shells.
 *
 * When Root Parameters are provided, the function ensures that Static Shells only
 * include complete sets of Root Parameters. This prevents generating invalid Static Shells
 * that are missing required Root Parameters.
 *
 * Example with Root Parameters ('lang', 'region') and Route Parameters ('lang', 'region', 'slug'):
 *
 * Given the following Static Parameters:
 * ```
 * [
 *   { lang: 'en', region: 'US', slug: ['home'] },
 *   { lang: 'en', region: 'US', slug: ['about'] },
 *   { lang: 'fr', region: 'CA', slug: ['about'] },
 * ]
 * ```
 *
 * The result will be:
 * ```
 * [
 *   { lang: 'en', region: 'US' },  // Complete Root Parameters
 *   { lang: 'en', region: 'US', slug: ['home'] },
 *   { lang: 'en', region: 'US', slug: ['about'] },
 *   { lang: 'fr', region: 'CA' },  // Complete Root Parameters
 *   { lang: 'fr', region: 'CA', slug: ['about'] },
 * ]
 * ```
 *
 * Note that partial combinations like `{ lang: 'en' }` are NOT generated because
 * they don't include the complete set of Root Parameters.
 *
 * For routes without Root Parameters (e.g., `/[slug]`), all sub-combinations are generated
 * as before.
 *
 * @param childrenRouteParams - The children route params. These should be sorted
 *   to ensure consistent key generation for the internal Map.
 * @param routeParams - The list of Static Parameters to filter.
 * @param rootParamKeys - The keys of the Root Parameters. When provided, ensures Static Shells
 *   include all Root Parameters.
 * @returns A new array containing all unique sub-combinations of Route Parameters.
 */
export function generateAllParamCombinations(
  childrenRouteParams: ReadonlyArray<{
    readonly paramName: string
  }>,
  routeParams: readonly Params[],
  rootParamKeys: readonly string[]
): Params[] {
  // A Map is used to store unique combinations of Route Parameters.
  // The key of the Map is a string representation of the Route Parameter
  // combination, and the value is the `Params` object containing only
  // the Route Parameters.
  const combinations = new Map<string, Params>()

  // Determine the minimum index where all Root Parameters are included.
  // This optimization ensures we only generate combinations that include
  // a complete set of Root Parameters, preventing invalid Static Shells.
  //
  // For example, if rootParamKeys = ['lang', 'region'] and routeParamKeys = ['lang', 'region', 'slug']:
  // - 'lang' is at index 0, 'region' is at index 1
  // - minIndexForCompleteRootParams = max(0, 1) = 1
  // - We'll only generate combinations starting from index 1 (which includes both lang and region)
  let minIndexForCompleteRootParams = -1
  if (rootParamKeys.length > 0) {
    // Find the index of the last Root Parameter in routeParamKeys.
    // This tells us the minimum combination length needed to include all Root Parameters.
    for (const rootParamKey of rootParamKeys) {
      const index = childrenRouteParams.findIndex(
        (param) => param.paramName === rootParamKey
      )
      if (index === -1) {
        // Root Parameter not found in Route Parameters - this shouldn't happen in normal cases
        // but we handle it gracefully by treating it as if there are no Root Parameters.
        // This allows the function to fall back to generating all sub-combinations.
        minIndexForCompleteRootParams = -1
        break
      }
      // Track the highest index among all Root Parameters.
      // This ensures all Root Parameters are included in any generated combination.
      minIndexForCompleteRootParams = Math.max(
        minIndexForCompleteRootParams,
        index
      )
    }
  }

  // Iterate over each Static Parameter object in the input array.
  // Each params object represents one potential route combination (e.g., { lang: 'en', region: 'US', slug: 'home' })
  for (const params of routeParams) {
    // Generate all possible prefix combinations for this Static Parameter set.
    // For routeParamKeys = ['lang', 'region', 'slug'], we'll generate combinations at:
    // - i=0: { lang: 'en' }
    // - i=1: { lang: 'en', region: 'US' }
    // - i=2: { lang: 'en', region: 'US', slug: 'home' }
    //
    // The iteration order is crucial for generating stable and unique keys
    // for each Route Parameter combination.
    for (let i = 0; i < childrenRouteParams.length; i++) {
      // Skip generating combinations that don't include all Root Parameters.
      // This prevents creating invalid Static Shells that are missing required Root Parameters.
      //
      // For example, if Root Parameters are ['lang', 'region'] and minIndexForCompleteRootParams = 1:
      // - Skip i=0 (would only include 'lang', missing 'region')
      // - Process i=1 and higher (includes both 'lang' and 'region')
      if (
        minIndexForCompleteRootParams >= 0 &&
        i < minIndexForCompleteRootParams
      ) {
        continue
      }

      // Initialize data structures for building this specific combination
      const combination: Params = {}
      const keyParts: string[] = []
      let hasAllRootParams = true

      // Build the sub-combination with parameters from index 0 to i (inclusive).
      // This creates a prefix of the full parameter set, building up combinations incrementally.
      //
      // For example, if routeParamKeys = ['lang', 'region', 'slug'] and i = 1:
      // - j=0: Add 'lang' parameter
      // - j=1: Add 'region' parameter
      // Result: { lang: 'en', region: 'US' }
      for (let j = 0; j <= i; j++) {
        const { paramName: routeKey } = childrenRouteParams[j]

        // Check if the parameter exists in the original params object and has a defined value.
        // This handles cases where generateStaticParams doesn't provide all possible parameters,
        // or where some parameters are optional/undefined.
        if (
          !params.hasOwnProperty(routeKey) ||
          params[routeKey] === undefined
        ) {
          // If this missing parameter is a Root Parameter, mark the combination as invalid.
          // Root Parameters are required for Static Shells, so we can't generate partial combinations without them.
          if (rootParamKeys.includes(routeKey)) {
            hasAllRootParams = false
          }
          // Stop building this combination since we've hit a missing parameter.
          // This ensures we only generate valid prefix combinations with consecutive parameters.
          break
        }

        const value = params[routeKey]
        combination[routeKey] = value

        // Construct a unique key part for this parameter to enable deduplication.
        // We use type prefixes to prevent collisions between different value types
        // that might have the same string representation.
        //
        // Examples:
        // - Array ['foo', 'bar'] becomes "A:foo,bar"
        // - String "foo,bar" becomes "S:foo,bar"
        // - This prevents collisions between ['foo', 'bar'] and "foo,bar"
        let valuePart: string
        if (Array.isArray(value)) {
          valuePart = `A:${value.join(',')}`
        } else {
          valuePart = `S:${value}`
        }
        keyParts.push(`${routeKey}:${valuePart}`)
      }

      // Build the final unique key by joining all parameter parts.
      // This key is used for deduplication in the combinations Map.
      // Format: "lang:S:en|region:S:US|slug:A:home,about"
      const currentKey = keyParts.join('|')

      // Only add the combination if it meets our criteria:
      // 1. hasAllRootParams: Contains all required Root Parameters
      // 2. !combinations.has(currentKey): Is not a duplicate of an existing combination
      //
      // This ensures we only generate valid, unique parameter combinations for Static Shells.
      if (hasAllRootParams && !combinations.has(currentKey)) {
        combinations.set(currentKey, combination)
      }
    }
  }

  // Convert the Map's values back into an array and return the final result.
  // The Map ensures all combinations are unique, and we return only the
  // parameter objects themselves, discarding the internal deduplication keys.
  return Array.from(combinations.values())
}

/**
 * Calculates the fallback mode based on the given parameters.
 *
 * @param dynamicParams - Whether dynamic params are enabled.
 * @param fallbackRootParams - The root params that are part of the fallback.
 * @param baseFallbackMode - The base fallback mode to use.
 * @returns The calculated fallback mode.
 */
export function calculateFallbackMode(
  dynamicParams: boolean,
  fallbackRootParams: readonly string[],
  baseFallbackMode: FallbackMode | undefined
): FallbackMode {
  return dynamicParams
    ? // If the fallback params includes any root params, then we need to
      // perform a blocking static render.
      fallbackRootParams.length > 0
      ? FallbackMode.BLOCKING_STATIC_RENDER
      : (baseFallbackMode ?? FallbackMode.NOT_FOUND)
    : FallbackMode.NOT_FOUND
}

/**
 * Validates the parameters to ensure they're accessible and have the correct
 * types.
 *
 * @param page - The page to validate.
 * @param regex - The route regex.
 * @param isRoutePPREnabled - Whether the route has partial prerendering enabled.
 * @param pathnameSegments - The keys of the parameters.
 * @param rootParamKeys - The keys of the root params.
 * @param routeParams - The list of parameters to validate.
 * @returns The list of validated parameters.
 */
function validateParams(
  page: string,
  isRoutePPREnabled: boolean,
  pathnameSegments: ReadonlyArray<{
    readonly paramName: string
    readonly paramType: DynamicParamTypes
  }>,
  rootParamKeys: readonly string[],
  routeParams: readonly Params[]
): Params[] {
  const valid: Params[] = []

  // Validate that if there are any root params, that the user has provided at
  // least one value for them only if we're using partial prerendering.
  if (isRoutePPREnabled && rootParamKeys.length > 0) {
    if (
      routeParams.length === 0 ||
      rootParamKeys.some((key) =>
        routeParams.some((params) => !(key in params))
      )
    ) {
      if (rootParamKeys.length === 1) {
        throw new Error(
          `A required root parameter (${rootParamKeys[0]}) was not provided in generateStaticParams for ${page}, please provide at least one value.`
        )
      }

      throw new Error(
        `Required root params (${rootParamKeys.join(', ')}) were not provided in generateStaticParams for ${page}, please provide at least one value for each.`
      )
    }
  }

  for (const params of routeParams) {
    const item: Params = {}

    for (const { paramName: key, paramType } of pathnameSegments) {
      const { repeat, optional } = getParamProperties(paramType)

      let paramValue = params[key]

      if (
        optional &&
        params.hasOwnProperty(key) &&
        (paramValue === null ||
          paramValue === undefined ||
          (paramValue as any) === false)
      ) {
        paramValue = []
      }

      // A parameter is missing, so the rest of the params are not accessible.
      // We only support this when the route has partial prerendering enabled.
      // This will make it so that the remaining params are marked as missing so
      // we can generate a fallback route for them.
      if (!paramValue && isRoutePPREnabled) {
        break
      }

      // Perform validation for the parameter based on whether it's a repeat
      // parameter or not.
      if (repeat) {
        if (!Array.isArray(paramValue)) {
          throw new Error(
            `A required parameter (${key}) was not provided as an array received ${typeof paramValue} in generateStaticParams for ${page}`
          )
        }
      } else {
        if (typeof paramValue !== 'string') {
          throw new Error(
            `A required parameter (${key}) was not provided as a string received ${typeof paramValue} in generateStaticParams for ${page}`
          )
        }
      }

      item[key] = paramValue
    }

    valid.push(item)
  }

  return valid
}

interface TrieNode {
  /**
   * The children of the node. Each key is a unique string representation of a parameter value,
   * and the value is the next TrieNode in the path.
   */
  children: Map<string, TrieNode>

  /**
   * The routes that are associated with this specific parameter combination (node).
   * These are the routes whose concrete parameters lead to this node in the Trie.
   */
  routes: PrerenderedRoute[]
}

/**
 * Assigns static shell metadata to each prerendered route.
 * This function uses a Trie data structure to efficiently determine whether each route
 * should throw an error when its static shell is empty and whether a fallback shell
 * can still be completed into a more specific prerendered shell.
 *
 * A route should not throw on empty static shell if it has child routes in the Trie. For example,
 * if we have two routes, `/blog/first-post` and `/blog/[slug]`, the route for
 * `/blog/[slug]` should not throw because `/blog/first-post` is a more specific concrete route.
 *
 * @param prerenderedRoutes - The prerendered routes.
 * @param pathnameSegments - The pathname params and whether each one is still
 * prerenderable via generateStaticParams.
 */
export function assignStaticShellMetadata(
  prerenderedRoutes: readonly PrerenderedRoute[],
  pathnameSegments: ReadonlyArray<{
    readonly paramName: string
    readonly hasGenerateStaticParams: boolean
  }>,
  computeRemainingPrerenderableParams: boolean
): void {
  // If there are no routes to process, exit early.
  if (prerenderedRoutes.length === 0) {
    return
  }

  // Initialize the root of the Trie. This node represents the starting point
  // before any parameters have been considered.
  const root: TrieNode = { children: new Map(), routes: [] }

  // Phase 1: Build the Trie.
  // Iterate over each prerendered route and insert it into the Trie.
  // Each route's concrete parameter values form a path in the Trie.
  for (const route of prerenderedRoutes) {
    let currentNode = root // Start building the path from the root for each route.

    // Iterate through the sorted parameter keys. The order of keys is crucial
    // for ensuring that routes with the same concrete parameters follow the
    // same path in the Trie, regardless of the original order of properties
    // in the `params` object.
    for (const { paramName: key } of pathnameSegments) {
      // Check if the current route actually has a concrete value for this parameter.
      // If a dynamic segment is not filled (i.e., it's a fallback), it won't have
      // this property, and we stop building the path for this route at this point.
      if (route.params.hasOwnProperty(key)) {
        const value = route.params[key]

        // Generate a unique key for the parameter's value. This is critical
        // to prevent collisions between different data types that might have
        // the same string representation (e.g., `['a', 'b']` vs `'a,b'`).
        // A type prefix (`A:` for Array, `S:` for String, `U:` for undefined)
        // is added to the value to prevent collisions. This ensures that
        // different types with the same string representation are treated as
        // distinct.
        let valueKey: string
        if (Array.isArray(value)) {
          valueKey = `A:${value.join(',')}`
        } else if (value === undefined) {
          valueKey = `U:undefined`
        } else {
          valueKey = `S:${value}`
        }

        // Look for a child node corresponding to this `valueKey` from the `currentNode`.
        let childNode = currentNode.children.get(valueKey)
        if (!childNode) {
          // If the child node doesn't exist, create a new one and add it to
          // the current node's children.
          childNode = {
            children: new Map(),
            routes: [],
          }
          currentNode.children.set(valueKey, childNode)
        }
        // Move deeper into the Trie to the `childNode` for the next parameter.
        currentNode = childNode
      }
    }
    // After processing all concrete parameters for the route, add the full
    // `PrerenderedRoute` object to the `routes` array of the `currentNode`.
    // This node represents the unique concrete parameter combination for this route.
    currentNode.routes.push(route)
  }

  // Phase 2: Traverse the Trie to assign the `throwOnEmptyStaticShell` property.
  // This is done using an iterative Depth-First Search (DFS) approach with an
  // explicit stack to avoid JavaScript's recursion depth limits (stack overflow)
  // for very deep routing structures.
  const stack: TrieNode[] = [root] // Initialize the stack with the root node.

  while (stack.length > 0) {
    const node = stack.pop()! // Pop the next node to process from the stack.

    // `hasChildren` indicates if this node has any more specific concrete
    // parameter combinations branching off from it. If true, it means this
    // node represents a prefix for other, more specific routes.
    const hasChildren = node.children.size > 0

    // If the current node has routes associated with it (meaning, routes whose
    // concrete parameters lead to this node's path in the Trie).
    if (node.routes.length > 0) {
      // Determine the minimum number of fallback parameters among all routes
      // that are associated with this current Trie node. This is used to
      // identify if a route should not throw on empty static shell relative to another route *at the same level*
      // of concrete parameters, but with fewer fallback parameters.
      let minFallbacks = Infinity
      for (const r of node.routes) {
        // `fallbackRouteParams?.length ?? 0` handles cases where `fallbackRouteParams`
        // might be `undefined` or `null`, treating them as 0 length.
        minFallbacks = Math.min(
          minFallbacks,
          r.fallbackRouteParams ? r.fallbackRouteParams.length : 0
        )
      }

      // Now, for each `PrerenderedRoute` associated with this node:
      for (const route of node.routes) {
        // A route is ok not to throw on an empty static shell (and thus
        // `throwOnEmptyStaticShell` should be `false`) if either of the
        // following conditions is met:
        // 1. `hasChildren` is true: This node has further concrete parameter children.
        //    This means the current route is a parent to more specific routes (e.g.,
        //    `/blog/[slug]` should not throw when concrete routes like `/blog/first-post` exist).
        // OR
        // 2. `route.fallbackRouteParams.length > minFallbacks`: This route has
        //    more fallback parameters than another route at the same Trie node.
        //    This implies the current route is a more general version that should not throw
        //    compared to a more specific route that has fewer fallback parameters
        //    (e.g., `/1234/[...slug]` should not throw relative to `/[id]/[...slug]`).
        if (
          hasChildren ||
          (route.fallbackRouteParams &&
            route.fallbackRouteParams.length > minFallbacks)
        ) {
          route.throwOnEmptyStaticShell = false // Should not throw on empty static shell.
        } else {
          route.throwOnEmptyStaticShell = true // Should throw on empty static shell.
        }

        if (
          computeRemainingPrerenderableParams &&
          route.fallbackRouteParams &&
          route.fallbackRouteParams.length > 0
        ) {
          const fallbackRouteParamsByName = new Map(
            route.fallbackRouteParams.map((param) => [param.paramName, param])
          )
          const remainingPrerenderableParams: FallbackRouteParam[] = []

          // Only unresolved pathname params that can still be filled by
          // generateStaticParams belong here. Once we hit an unresolved param
          // that is purely dynamic, the rest of the shell also stays dynamic
          // and cannot be completed into a more specific prerendered shell.
          for (const segment of pathnameSegments) {
            if (route.params.hasOwnProperty(segment.paramName)) {
              continue
            }

            if (!segment.hasGenerateStaticParams) {
              break
            }

            const fallbackRouteParam = fallbackRouteParamsByName.get(
              segment.paramName
            )
            if (!fallbackRouteParam) {
              break
            }

            remainingPrerenderableParams.push(fallbackRouteParam)
          }

          route.remainingPrerenderableParams =
            remainingPrerenderableParams.length > 0
              ? remainingPrerenderableParams
              : undefined
        }
      }
    }

    // Add all children of the current node to the stack. This ensures that
    // the traversal continues to explore deeper paths in the Trie.
    for (const child of node.children.values()) {
      stack.push(child)
    }
  }
}

/**
 * Calls a single generateStaticParams function within a WorkUnitStore context,
 * making root param getters available during static param generation.
 */
async function callGenerateStaticParams(
  generateStaticParams: NonNullable<AppSegment['generateStaticParams']>,
  parentParams: Params,
  rootParamKeys: readonly string[],
  implicitTags: ImplicitTags
): Promise<Params[]> {
  const rootParams: Params = {}
  for (const key of rootParamKeys) {
    if (key in parentParams) {
      rootParams[key] = parentParams[key]
    }
  }

  const workUnitStore: GenerateStaticParamsStore = {
    type: 'generate-static-params',
    phase: 'render',
    implicitTags,
    rootParams,
  }

  return workUnitAsyncStorage.run(workUnitStore, generateStaticParams, {
    params: parentParams,
  })
}

/**
 * Processes app directory segments to build route parameters from generateStaticParams functions.
 * This function walks through the segments array and calls generateStaticParams for each segment that has it,
 * combining parent parameters with child parameters to build the complete parameter combinations.
 * Uses iterative processing instead of recursion for better performance.
 *
 * @param segments - Array of app directory segments to process
 * @param store - Work store for tracking fetch cache configuration
 * @param isRoutePPREnabled - Whether PPR is enabled for this route
 * @param rootParamKeys - The keys identifying which params are root params
 * @returns Promise that resolves to an array of all parameter combinations
 */
export async function generateRouteStaticParams(
  segments: ReadonlyArray<
    Readonly<Pick<AppSegment, 'config' | 'generateStaticParams'>>
  >,
  store: Pick<WorkStore, 'fetchCache' | 'page'>,
  isRoutePPREnabled: boolean,
  rootParamKeys: readonly string[]
): Promise<Params[]> {
  // Early return if no segments to process
  if (segments.length === 0) return []

  const implicitTags = await getImplicitTags(store.page, store.page, null)

  // Use iterative processing with a work queue to avoid recursion overhead
  interface WorkItem {
    segmentIndex: number
    params: Params[]
  }

  const queue: WorkItem[] = [{ segmentIndex: 0, params: [] }]
  let currentParams: Params[] = []

  while (queue.length > 0) {
    const { segmentIndex, params } = queue.shift()!

    // If we've processed all segments, this is our final result
    if (segmentIndex >= segments.length) {
      currentParams = params
      break
    }

    const current = segments[segmentIndex]

    // Skip segments without generateStaticParams and continue to next
    if (typeof current.generateStaticParams !== 'function') {
      queue.push({ segmentIndex: segmentIndex + 1, params })
      continue
    }

    // Configure fetchCache if specified
    if (current.config?.fetchCache !== undefined) {
      store.fetchCache = current.config.fetchCache
    }

    const nextParams: Params[] = []

    // If there are parent params, we need to process them.
    if (params.length > 0) {
      // Process each parent parameter combination
      for (const parentParams of params) {
        const result = await callGenerateStaticParams(
          current.generateStaticParams,
          parentParams,
          rootParamKeys,
          implicitTags
        )

        if (result.length > 0) {
          // Merge parent params with each result item
          for (const item of result) {
            nextParams.push({ ...parentParams, ...item })
          }
        } else if (isRoutePPREnabled) {
          throwEmptyGenerateStaticParamsError()
        } else {
          // No results, just pass through parent params
          nextParams.push(parentParams)
        }
      }
    } else {
      // No parent params, call generateStaticParams with empty object
      const result = await callGenerateStaticParams(
        current.generateStaticParams,
        {},
        rootParamKeys,
        implicitTags
      )
      if (result.length === 0 && isRoutePPREnabled) {
        throwEmptyGenerateStaticParamsError()
      }

      nextParams.push(...result)
    }

    // Add next segment to work queue
    queue.push({ segmentIndex: segmentIndex + 1, params: nextParams })
  }

  return currentParams
}

function createReplacements(
  segment: Pick<AppSegment, 'paramType'>,
  paramValue: string | string[]
) {
  // Determine the prefix to use for the interception marker.
  let prefix: string
  if (segment.paramType) {
    prefix = interceptionPrefixFromParamType(segment.paramType) ?? ''
  } else {
    prefix = ''
  }

  return {
    pathname:
      prefix +
      encodeParam(paramValue, (value) =>
        // Only escape path delimiters if the value is a string, the following
        // version will URL encode the value.
        escapePathDelimiters(value, true)
      ),
    encodedPathname:
      prefix +
      encodeParam(
        paramValue,
        // URL encode the value.
        encodeURIComponent
      ),
  }
}

/**
 * Processes app directory segments to build route parameters from generateStaticParams functions.
 * This function walks through the segments array and calls generateStaticParams for each segment that has it,
 * combining parent parameters with child parameters to build the complete parameter combinations.
 * Uses iterative processing instead of recursion for better performance.
 *
 * @param segments - Array of app directory segments to process
 * @param store - Work store for tracking fetch cache configuration
 * @returns Promise that resolves to an array of all parameter combinations
 */
export async function buildAppStaticPaths({
  dir,
  page,
  route,
  distDir,
  cacheComponents,
  authInterrupts,
  useCacheTimeout,
  staticPageGenerationTimeout,
  segments,
  isrFlushToDisk,
  cacheHandler,
  cacheLifeProfiles,
  requestHeaders,
  cacheHandlers,
  cacheMaxMemorySize,
  fetchCacheKeyPrefix,
  nextConfigOutput,
  ComponentMod,
  isRoutePPREnabled = false,
  partialFallbacksEnabled = false,
  buildId,
  deploymentId,
  rootParamKeys,
}: {
  dir: string
  page: string
  route: NormalizedAppRoute
  cacheComponents: boolean
  authInterrupts: boolean
  useCacheTimeout: number
  staticPageGenerationTimeout: number
  segments: readonly Readonly<AppSegment>[]
  distDir: string
  isrFlushToDisk?: boolean
  fetchCacheKeyPrefix?: string
  cacheHandler?: string
  cacheHandlers?: NextConfigComplete['cacheHandlers']
  cacheLifeProfiles?: {
    [profile: string]: import('../../server/use-cache/cache-life').CacheLife
  }
  cacheMaxMemorySize: number
  requestHeaders: IncrementalCache['requestHeaders']
  nextConfigOutput: 'standalone' | 'export' | undefined
  ComponentMod: AppPageModule | AppRouteModule
  isRoutePPREnabled: boolean
  partialFallbacksEnabled?: boolean
  buildId: string
  deploymentId: string
  rootParamKeys: readonly string[]
}): Promise<StaticPathsResult> {
  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()

  const incrementalCache = await createIncrementalCache({
    dir,
    distDir,
    cacheHandler,
    cacheHandlers,
    requestHeaders,
    fetchCacheKeyPrefix,
    flushToDisk: isrFlushToDisk,
    cacheMaxMemorySize,
  })

  // Extract segments that contribute to the pathname.
  // For AppPageRouteModule: Traverses the loader tree to find all segments (including
  //   interception routes in parallel slots) that match the pathname
  // For AppRouteRouteModule: Filters the segments array to get non-parallel route params
  const pathnameRouteParamSegments = extractPathnameRouteParamSegments(
    ComponentMod.routeModule,
    segments,
    route
  )

  const afterRunner = new AfterRunner()

  const store = createWorkStore({
    page,
    renderOpts: {
      incrementalCache,
      cacheLifeProfiles,
      staticPageGenerationTimeout,
      supportsDynamicResponse: true,
      cacheComponents,
      experimental: {
        authInterrupts,
        useCacheTimeout,
      },
      waitUntil: afterRunner.context.waitUntil,
      onClose: afterRunner.context.onClose,
      onAfterTaskError: afterRunner.context.onTaskError,
    },
    buildId,
    deploymentId,
    previouslyRevalidatedTags: [],
  })

  const routeParams = await workAsyncStorage.run(
    store,
    generateRouteStaticParams,
    segments,
    store,
    isRoutePPREnabled,
    rootParamKeys
  )
  const generatedParamNames = new Set<string>()
  for (const params of routeParams) {
    for (const paramName of Object.keys(params)) {
      generatedParamNames.add(paramName)
    }
  }
  const prerenderablePathSegments = pathnameRouteParamSegments.map(
    (segment) => ({
      paramName: segment.paramName,
      hasGenerateStaticParams: generatedParamNames.has(segment.paramName),
    })
  )

  await afterRunner.executeAfter()

  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.paramName &&
      segment.paramType &&
      segment.config?.dynamicParams === false
    ) {
      for (const params of routeParams) {
        if (segment.paramName 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.paramName}" is missing from the generated route params.`
        )
      }
    }

    if (
      segment.paramName &&
      segment.paramType &&
      typeof segment.generateStaticParams !== 'function'
    ) {
      lastDynamicSegmentHadGenerateStaticParams = false
    } else if (typeof segment.generateStaticParams === 'function') {
      lastDynamicSegmentHadGenerateStaticParams = true
    }
  }

  // Determine if all the segments have had their parameters provided.
  const hadAllParamsGenerated =
    pathnameRouteParamSegments.length === 0 ||
    (routeParams.length > 0 &&
      routeParams.every((params) => {
        for (const { paramName } of pathnameRouteParamSegments) {
          if (paramName 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.__NEXT_DEV_SERVER

  const fallbackMode = dynamicParams
    ? supportsRoutePreGeneration
      ? isRoutePPREnabled
        ? FallbackMode.PRERENDER
        : FallbackMode.BLOCKING_STATIC_RENDER
      : undefined
    : FallbackMode.NOT_FOUND

  const prerenderedRoutesByPathname = new Map<string, PrerenderedRoute>()

  // Convert rootParamKeys to Set for O(1) lookup.
  const rootParamSet = new Set(rootParamKeys)

  if (hadAllParamsGenerated || isRoutePPREnabled) {
    let paramsToProcess = routeParams

    if (isRoutePPREnabled) {
      // Discover all unique combinations of the routeParams so we can generate
      // routes that won't throw on empty static shell for each of them if
      // they're available.
      paramsToProcess = generateAllParamCombinations(
        pathnameRouteParamSegments,
        routeParams,
        rootParamKeys
      )

      // Collect all the fallback route params for the segments.
      const fallbackRouteParams: FallbackRouteParam[] = []
      for (const segment of segments) {
        if (!segment.paramName || !segment.paramType) continue
        fallbackRouteParams.push({
          paramName: segment.paramName,
          paramType: segment.paramType,
        })
      }

      // Add the base route, this is the route with all the placeholders as it's
      // derived from the `page` string.
      prerenderedRoutesByPathname.set(page, {
        params: {},
        pathname: page,
        encodedPathname: page,
        fallbackRouteParams,
        fallbackMode: calculateFallbackMode(
          dynamicParams,
          rootParamKeys,
          fallbackMode
        ),
        fallbackRootParams: rootParamKeys,
        throwOnEmptyStaticShell: true,
      })
    }

    filterUniqueParams(
      pathnameRouteParamSegments,
      validateParams(
        page,
        isRoutePPREnabled,
        pathnameRouteParamSegments,
        rootParamKeys,
        paramsToProcess
      )
    ).forEach((params) => {
      let pathname = page
      let encodedPathname = page

      const fallbackRouteParams: FallbackRouteParam[] = []

      for (const { name, paramName, paramType } of pathnameRouteParamSegments) {
        const paramValue = params[paramName]

        if (!paramValue) {
          if (isRoutePPREnabled) {
            // Mark remaining params as fallback params.
            fallbackRouteParams.push({ paramName, paramType })
            for (
              let i =
                pathnameRouteParamSegments.findIndex(
                  (param) => param.paramName === paramName
                ) + 1;
              i < pathnameRouteParamSegments.length;
              i++
            ) {
              fallbackRouteParams.push({
                paramName: pathnameRouteParamSegments[i].paramName,
                paramType: pathnameRouteParamSegments[i].paramType,
              })
            }
            break
          } else {
            // This route is not complete, and we aren't performing a partial
            // prerender, so we should return, skipping this route.
            return
          }
        }

        const replacements = createReplacements({ paramType }, paramValue)

        pathname = pathname.replace(
          name,
          // We're replacing the segment name with the replacement pathname
          // which will include the interception marker prefix if it exists.
          replacements.pathname
        )

        encodedPathname = encodedPathname.replace(
          name,
          // We're replacing the segment name with the replacement encoded
          // pathname which will include the encoded param value.
          replacements.encodedPathname
        )
      }

      // Resolve all route params from the loader tree if this is from an
      // app page. This processes both regular route params and parallel route params.
      if (
        'loaderTree' in ComponentMod.routeModule.userland &&
        Array.isArray(ComponentMod.routeModule.userland.loaderTree)
      ) {
        resolveRouteParamsFromTree(
          ComponentMod.routeModule.userland.loaderTree,
          params,
          route,
          fallbackRouteParams
        )
      }

      const fallbackRootParams: string[] = []
      for (const { paramName } of fallbackRouteParams) {
        // If the param is a root param then we can add it to the fallback
        // root params.
        if (rootParamSet.has(paramName)) {
          fallbackRootParams.push(paramName)
        }
      }

      pathname = normalizePathname(pathname)

      prerenderedRoutesByPathname.set(pathname, {
        params,
        pathname,
        encodedPathname: normalizePathname(encodedPathname),
        fallbackRouteParams,
        fallbackMode: calculateFallbackMode(
          dynamicParams,
          fallbackRootParams,
          fallbackMode
        ),
        fallbackRootParams,
        throwOnEmptyStaticShell: true,
      })
    })
  }

  const prerenderedRoutes =
    prerenderedRoutesByPathname.size > 0 ||
    lastDynamicSegmentHadGenerateStaticParams
      ? [...prerenderedRoutesByPathname.values()]
      : undefined

  // Now we have to set the throwOnEmptyStaticShell for each of the routes.
  if (prerenderedRoutes && cacheComponents) {
    assignStaticShellMetadata(
      prerenderedRoutes,
      prerenderablePathSegments,
      partialFallbacksEnabled
    )
  }

  return { fallbackMode, prerenderedRoutes }
}
Quest for Codev2.0.0
/
SIGN IN