.
This commit is contained in:
+783
@@ -0,0 +1,783 @@
|
||||
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 { 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 { getParamProperties } from '../../shared/lib/router/utils/get-segment-param';
|
||||
import { throwEmptyGenerateStaticParamsError } from '../../shared/lib/errors/empty-generate-static-params-error';
|
||||
import { interceptionPrefixFromParamType } from '../../shared/lib/router/utils/interception-prefix-from-param-type';
|
||||
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, routeParams) {
|
||||
// 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();
|
||||
// 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;
|
||||
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, routeParams, rootParamKeys) {
|
||||
// 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();
|
||||
// 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 = {};
|
||||
const keyParts = [];
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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, fallbackRootParams, baseFallbackMode) {
|
||||
return dynamicParams ? // 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, isRoutePPREnabled, pathnameSegments, rootParamKeys, routeParams) {
|
||||
const valid = [];
|
||||
// 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 Object.defineProperty(new Error(`A required root parameter (${rootParamKeys[0]}) was not provided in generateStaticParams for ${page}, please provide at least one value.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E622",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
throw Object.defineProperty(new Error(`Required root params (${rootParamKeys.join(', ')}) were not provided in generateStaticParams for ${page}, please provide at least one value for each.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E621",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const params of routeParams){
|
||||
const item = {};
|
||||
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 === 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 Object.defineProperty(new Error(`A required parameter (${key}) was not provided as an array received ${typeof paramValue} in generateStaticParams for ${page}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E618",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (typeof paramValue !== 'string') {
|
||||
throw Object.defineProperty(new Error(`A required parameter (${key}) was not provided as a string received ${typeof paramValue} in generateStaticParams for ${page}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E617",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
item[key] = paramValue;
|
||||
}
|
||||
valid.push(item);
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
/**
|
||||
* 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, pathnameSegments, computeRemainingPrerenderableParams) {
|
||||
// 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 = {
|
||||
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;
|
||||
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 = [
|
||||
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 = [];
|
||||
// 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, workUnitAsyncStorage, parentParams, rootParamKeys, implicitTags) {
|
||||
const rootParams = {};
|
||||
for (const key of rootParamKeys){
|
||||
if (key in parentParams) {
|
||||
rootParams[key] = parentParams[key];
|
||||
}
|
||||
}
|
||||
const workUnitStore = {
|
||||
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 workUnitAsyncStorage - AsyncLocalStorage for work unit stores
|
||||
* @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, store, workUnitAsyncStorage, isRoutePPREnabled, rootParamKeys) {
|
||||
// Early return if no segments to process
|
||||
if (segments.length === 0) return [];
|
||||
const implicitTags = await getImplicitTags(store.page, store.page, null);
|
||||
const queue = [
|
||||
{
|
||||
segmentIndex: 0,
|
||||
params: []
|
||||
}
|
||||
];
|
||||
let currentParams = [];
|
||||
while(queue.length > 0){
|
||||
var _current_config;
|
||||
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 = current.config) == null ? void 0 : _current_config.fetchCache) !== undefined) {
|
||||
store.fetchCache = current.config.fetchCache;
|
||||
}
|
||||
const nextParams = [];
|
||||
// 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, workUnitAsyncStorage, 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, workUnitAsyncStorage, {}, 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, paramValue) {
|
||||
// Determine the prefix to use for the interception marker.
|
||||
let prefix;
|
||||
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, segments, isrFlushToDisk, cacheHandler, cacheLifeProfiles, requestHeaders, cacheHandlers, cacheMaxMemorySize, fetchCacheKeyPrefix, nextConfigOutput, ComponentMod, isRoutePPREnabled = false, partialFallbacksEnabled = false, buildId, rootParamKeys }) {
|
||||
if (segments.some((generate)=>{
|
||||
var _generate_config;
|
||||
return ((_generate_config = generate.config) == null ? void 0 : _generate_config.dynamicParams) === true;
|
||||
}) && nextConfigOutput === 'export') {
|
||||
throw Object.defineProperty(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'), "__NEXT_ERROR_CODE", {
|
||||
value: "E393",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
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,
|
||||
supportsDynamicResponse: true,
|
||||
cacheComponents,
|
||||
experimental: {
|
||||
authInterrupts
|
||||
},
|
||||
waitUntil: afterRunner.context.waitUntil,
|
||||
onClose: afterRunner.context.onClose,
|
||||
onAfterTaskError: afterRunner.context.onTaskError
|
||||
},
|
||||
buildId,
|
||||
previouslyRevalidatedTags: []
|
||||
});
|
||||
const routeParams = await ComponentMod.workAsyncStorage.run(store, generateRouteStaticParams, segments, store, ComponentMod.workUnitAsyncStorage, isRoutePPREnabled, rootParamKeys);
|
||||
const generatedParamNames = new Set();
|
||||
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){
|
||||
var _segment_config;
|
||||
// Check to see if there are any missing params for segments that have
|
||||
// dynamicParams set to false.
|
||||
if (segment.paramName && segment.paramType && ((_segment_config = segment.config) == null ? void 0 : _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 Object.defineProperty(new Error(`Segment "${relative}" exports "dynamicParams: false" but the param "${segment.paramName}" is missing from the generated route params.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E280",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
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)=>{
|
||||
var _segment_config;
|
||||
return ((_segment_config = segment.config) == null ? void 0 : _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();
|
||||
// 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 = [];
|
||||
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 = [];
|
||||
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 = [];
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=app.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+134
@@ -0,0 +1,134 @@
|
||||
import { parseAppRouteSegment } from '../../../shared/lib/router/routes/app';
|
||||
import { parseLoaderTree } from '../../../shared/lib/router/utils/parse-loader-tree';
|
||||
import { resolveParamValue } from '../../../shared/lib/router/utils/resolve-param-value';
|
||||
/**
|
||||
* Validates that the static segments in currentPath match the corresponding
|
||||
* segments in targetSegments. This ensures we only extract dynamic parameters
|
||||
* that are part of the target pathname structure.
|
||||
*
|
||||
* Segments are compared literally - interception markers like "(.)photo" are
|
||||
* part of the pathname and must match exactly.
|
||||
*
|
||||
* @example
|
||||
* // Matching paths
|
||||
* currentPath: ['blog', '(.)photo']
|
||||
* targetSegments: ['blog', '(.)photo', '[id]']
|
||||
* → Returns true (both static segments match exactly)
|
||||
*
|
||||
* @example
|
||||
* // Non-matching paths
|
||||
* currentPath: ['blog', '(.)photo']
|
||||
* targetSegments: ['blog', 'photo', '[id]']
|
||||
* → Returns false (segments don't match - marker is part of pathname)
|
||||
*
|
||||
* @param currentPath - The accumulated path segments from the loader tree
|
||||
* @param targetSegments - The target pathname split into segments
|
||||
* @returns true if all static segments match, false otherwise
|
||||
*/ function validatePrefixMatch(currentPath, route) {
|
||||
for(let i = 0; i < currentPath.length; i++){
|
||||
const pathSegment = currentPath[i];
|
||||
const targetPathSegment = route.segments[i];
|
||||
// Type mismatch - one is static, one is dynamic
|
||||
if (pathSegment.type !== targetPathSegment.type) {
|
||||
return false;
|
||||
}
|
||||
// One has an interception marker, the other doesn't.
|
||||
if (pathSegment.interceptionMarker !== targetPathSegment.interceptionMarker) {
|
||||
return false;
|
||||
}
|
||||
// Both are static but names don't match
|
||||
if (pathSegment.type === 'static' && targetPathSegment.type === 'static' && pathSegment.name !== targetPathSegment.name) {
|
||||
return false;
|
||||
} else if (pathSegment.type === 'dynamic' && targetPathSegment.type === 'dynamic' && pathSegment.param.paramType !== targetPathSegment.param.paramType && pathSegment.param.paramName !== targetPathSegment.param.paramName) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Extracts pathname route param segments from a loader tree and resolves
|
||||
* parameter values from static segments in the route.
|
||||
*
|
||||
* @param loaderTree - The loader tree structure containing route hierarchy
|
||||
* @param route - The target route to match against
|
||||
* @returns Object containing pathname route param segments and resolved params
|
||||
*/ export function extractPathnameRouteParamSegmentsFromLoaderTree(loaderTree, route) {
|
||||
const pathnameRouteParamSegments = [];
|
||||
const params = {};
|
||||
// BFS traversal with depth and path tracking
|
||||
const queue = [
|
||||
{
|
||||
tree: loaderTree,
|
||||
depth: 0,
|
||||
currentPath: []
|
||||
}
|
||||
];
|
||||
while(queue.length > 0){
|
||||
const { tree, depth, currentPath } = queue.shift();
|
||||
const { segment, parallelRoutes } = parseLoaderTree(tree);
|
||||
// Build the path for the current node
|
||||
let updatedPath = currentPath;
|
||||
let nextDepth = depth;
|
||||
const appSegment = parseAppRouteSegment(segment);
|
||||
// Only add to path if it's a real segment that appears in the URL
|
||||
// Route groups and parallel markers don't contribute to URL pathname
|
||||
if (appSegment && appSegment.type !== 'route-group' && appSegment.type !== 'parallel-route') {
|
||||
updatedPath = [
|
||||
...currentPath,
|
||||
appSegment
|
||||
];
|
||||
nextDepth = depth + 1;
|
||||
}
|
||||
// Check if this segment has a param and matches the target pathname at this depth
|
||||
if ((appSegment == null ? void 0 : appSegment.type) === 'dynamic') {
|
||||
const { paramName, paramType } = appSegment.param;
|
||||
// Check if this segment is at the correct depth in the target pathname
|
||||
// A segment matches if:
|
||||
// 1. There's a dynamic segment at this depth in the pathname
|
||||
// 2. The parameter names match (e.g., [id] matches [id], not [category])
|
||||
// 3. The static segments leading up to this point match (prefix check)
|
||||
if (depth < route.segments.length) {
|
||||
const targetSegment = route.segments[depth];
|
||||
// Match if the target pathname has a dynamic segment at this depth
|
||||
if (targetSegment.type === 'dynamic') {
|
||||
// Check that parameter names match exactly
|
||||
// This prevents [category] from matching against /[id]
|
||||
if (paramName !== targetSegment.param.paramName) {
|
||||
continue; // Different param names, skip this segment
|
||||
}
|
||||
// Validate that the path leading up to this dynamic segment matches
|
||||
// the target pathname. This prevents false matches like extracting
|
||||
// [slug] from "/news/[slug]" when the tree has "/blog/[slug]"
|
||||
if (validatePrefixMatch(currentPath, route)) {
|
||||
pathnameRouteParamSegments.push({
|
||||
name: segment,
|
||||
paramName,
|
||||
paramType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Resolve parameter value if it's not already known.
|
||||
if (!params.hasOwnProperty(paramName)) {
|
||||
const paramValue = resolveParamValue(paramName, paramType, depth, route, params);
|
||||
if (paramValue !== undefined) {
|
||||
params[paramName] = paramValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Continue traversing all parallel routes to find matching segments
|
||||
for (const parallelRoute of Object.values(parallelRoutes)){
|
||||
queue.push({
|
||||
tree: parallelRoute,
|
||||
depth: nextDepth,
|
||||
currentPath: updatedPath
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
pathnameRouteParamSegments,
|
||||
params
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=extract-pathname-route-param-segments-from-loader-tree.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+154
@@ -0,0 +1,154 @@
|
||||
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';
|
||||
import { encodeParam, normalizePathname } from './utils';
|
||||
export async function buildPagesStaticPaths({ page, getStaticPaths, configFileName, locales, defaultLocale }) {
|
||||
const prerenderedRoutesByPathname = new Map();
|
||||
const _routeRegex = getRouteRegex(page);
|
||||
const _routeMatcher = getRouteMatcher(_routeRegex);
|
||||
// Get the default list of allowed params.
|
||||
const routeParameterKeys = Object.keys(_routeMatcher(page));
|
||||
const staticPathsResult = await getStaticPaths({
|
||||
// We create a copy here to avoid having the types of `getStaticPaths`
|
||||
// change. This ensures that users can't mutate this array and have it
|
||||
// poison the reference.
|
||||
locales: [
|
||||
...locales ?? []
|
||||
],
|
||||
defaultLocale
|
||||
});
|
||||
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 Object.defineProperty(new Error(`Invalid value returned from getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1004",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const invalidStaticPathKeys = Object.keys(staticPathsResult).filter((key)=>!(key === 'paths' || key === 'fallback'));
|
||||
if (invalidStaticPathKeys.length > 0) {
|
||||
throw Object.defineProperty(new Error(`Extra keys returned from getStaticPaths in ${page} (${invalidStaticPathKeys.join(', ')}) ${expectedReturnVal}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1047",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
if (!(typeof staticPathsResult.fallback === 'boolean' || staticPathsResult.fallback === 'blocking')) {
|
||||
throw Object.defineProperty(new Error(`The \`fallback\` key must be returned from getStaticPaths in ${page}.\n` + expectedReturnVal), "__NEXT_ERROR_CODE", {
|
||||
value: "E1034",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const toPrerender = staticPathsResult.paths;
|
||||
if (!Array.isArray(toPrerender)) {
|
||||
throw Object.defineProperty(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 }`), "__NEXT_ERROR_CODE", {
|
||||
value: "E83",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
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 params = _routeMatcher(cleanedEntry);
|
||||
if (!params) {
|
||||
throw Object.defineProperty(new Error(`The provided path \`${cleanedEntry}\` does not match the page: \`${page}\`.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E481",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// If leveraging the string paths variant the entry should already be
|
||||
// encoded so we decode the segments ensuring we only escape path
|
||||
// delimiters
|
||||
const pathname = entry.split('/').map((segment)=>escapePathDelimiters(decodeURIComponent(segment), true)).join('/');
|
||||
if (!prerenderedRoutesByPathname.has(pathname)) {
|
||||
prerenderedRoutesByPathname.set(pathname, {
|
||||
params,
|
||||
pathname,
|
||||
encodedPathname: entry,
|
||||
fallbackRouteParams: undefined,
|
||||
fallbackMode: parseStaticPathsResult(staticPathsResult.fallback),
|
||||
fallbackRootParams: undefined,
|
||||
throwOnEmptyStaticShell: undefined
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const invalidKeys = Object.keys(entry).filter((key)=>key !== 'params' && key !== 'locale');
|
||||
if (invalidKeys.length) {
|
||||
throw Object.defineProperty(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`), "__NEXT_ERROR_CODE", {
|
||||
value: "E322",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
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 === false)) {
|
||||
paramValue = [];
|
||||
}
|
||||
if (repeat && !Array.isArray(paramValue) || !repeat && typeof paramValue !== 'string' || typeof paramValue === 'undefined') {
|
||||
throw Object.defineProperty(new Error(`A required parameter (${validParamKey}) was not provided as ${repeat ? 'an array' : 'a string'} received ${typeof paramValue} in getStaticPaths for ${page}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E620",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
let replaced = `[${repeat ? '...' : ''}${validParamKey}]`;
|
||||
if (optional) {
|
||||
replaced = `[${replaced}]`;
|
||||
}
|
||||
builtPage = builtPage.replace(replaced, encodeParam(paramValue, (value)=>escapePathDelimiters(value, true)));
|
||||
encodedBuiltPage = encodedBuiltPage.replace(replaced, encodeParam(paramValue, encodeURIComponent));
|
||||
});
|
||||
if (!builtPage && !encodedBuiltPage) {
|
||||
return;
|
||||
}
|
||||
if (entry.locale && !(locales == null ? void 0 : locales.includes(entry.locale))) {
|
||||
throw Object.defineProperty(new Error(`Invalid locale returned from getStaticPaths for ${page}, the locale ${entry.locale} is not specified in ${configFileName}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E358",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const curLocale = entry.locale || defaultLocale || '';
|
||||
const pathname = normalizePathname(`${curLocale ? `/${curLocale}` : ''}${curLocale && builtPage === '/' ? '' : builtPage}`);
|
||||
if (!prerenderedRoutesByPathname.has(pathname)) {
|
||||
prerenderedRoutesByPathname.set(pathname, {
|
||||
params,
|
||||
pathname,
|
||||
encodedPathname: normalizePathname(`${curLocale ? `/${curLocale}` : ''}${curLocale && encodedBuiltPage === '/' ? '' : encodedBuiltPage}`),
|
||||
fallbackRouteParams: undefined,
|
||||
fallbackMode: parseStaticPathsResult(staticPathsResult.fallback),
|
||||
fallbackRootParams: undefined,
|
||||
throwOnEmptyStaticShell: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
fallbackMode: parseStaticPathsResult(staticPathsResult.fallback),
|
||||
prerenderedRoutes: [
|
||||
...prerenderedRoutesByPathname.values()
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=pages.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=types.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/build/static-paths/types.ts"],"sourcesContent":["import type { FallbackMode } from '../../lib/fallback'\nimport type { Params } from '../../server/request/params'\nimport type { DynamicParamTypes } from '../../shared/lib/app-router-types'\n\ntype StaticPrerenderedRoute = {\n readonly params: Params\n readonly pathname: string\n readonly encodedPathname: string\n readonly fallbackRouteParams: undefined\n readonly fallbackMode: FallbackMode | undefined\n readonly fallbackRootParams: undefined\n remainingPrerenderableParams?: undefined\n\n /**\n * When enabled, the route will be rendered with diagnostics enabled which\n * will error the build if the route that is generated is empty.\n */\n throwOnEmptyStaticShell: undefined\n}\n\nexport type FallbackRouteParam = {\n /**\n * The name of the param.\n */\n readonly paramName: string\n\n /**\n * The type of the param.\n */\n readonly paramType: DynamicParamTypes\n}\n\ntype FallbackPrerenderedRoute = {\n readonly params: Params\n readonly pathname: string\n readonly encodedPathname: string\n\n /**\n * The fallback route params for the route. This includes all route parameters\n * that are unknown at build time, from both the main children route and any\n * parallel routes.\n */\n readonly fallbackRouteParams: readonly FallbackRouteParam[]\n readonly fallbackMode: FallbackMode | undefined\n readonly fallbackRootParams: readonly string[]\n remainingPrerenderableParams?: readonly FallbackRouteParam[]\n\n /**\n * When enabled, the route will be rendered with diagnostics enabled which\n * will error the build if the route that is generated is empty.\n */\n throwOnEmptyStaticShell: boolean\n}\n\nexport type PrerenderedRoute = StaticPrerenderedRoute | FallbackPrerenderedRoute\n\nexport type StaticPathsResult = {\n fallbackMode: FallbackMode | undefined\n prerenderedRoutes: PrerenderedRoute[] | undefined\n}\n"],"names":[],"mappings":"AAwDA,WAGC","ignoreList":[0]}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { isAppPageRouteModule } from '../../server/route-modules/checks';
|
||||
import { parseAppRouteSegment } from '../../shared/lib/router/routes/app';
|
||||
import { parseLoaderTree } from '../../shared/lib/router/utils/parse-loader-tree';
|
||||
import { extractPathnameRouteParamSegmentsFromLoaderTree } from './app/extract-pathname-route-param-segments-from-loader-tree';
|
||||
import { resolveParamValue } from '../../shared/lib/router/utils/resolve-param-value';
|
||||
/**
|
||||
* Encodes a parameter value using the provided encoder.
|
||||
*
|
||||
* @param value - The value to encode.
|
||||
* @param encoder - The encoder to use.
|
||||
* @returns The encoded value.
|
||||
*/ export function encodeParam(value, encoder) {
|
||||
let replaceValue;
|
||||
if (Array.isArray(value)) {
|
||||
replaceValue = value.map(encoder).join('/');
|
||||
} else {
|
||||
replaceValue = encoder(value);
|
||||
}
|
||||
return replaceValue;
|
||||
}
|
||||
/**
|
||||
* Normalizes a pathname to a consistent format.
|
||||
*
|
||||
* @param pathname - The pathname to normalize.
|
||||
* @returns The normalized pathname.
|
||||
*/ export function normalizePathname(pathname) {
|
||||
return pathname.replace(/\\/g, '/').replace(/(?!^)\/$/, '');
|
||||
}
|
||||
/**
|
||||
* Extracts segments that contribute to the pathname by traversing the loader tree
|
||||
* based on the route module type.
|
||||
*
|
||||
* @param routeModule - The app route module (page or route handler)
|
||||
* @param segments - Array of AppSegment objects collected from the route
|
||||
* @param page - The target pathname to match against, INCLUDING interception
|
||||
* markers (e.g., "/blog/[slug]", "/(.)photo/[id]")
|
||||
* @returns Array of segments with param info that contribute to the pathname
|
||||
*/ export function extractPathnameRouteParamSegments(routeModule, segments, route) {
|
||||
// For AppPageRouteModule, use the loaderTree traversal approach
|
||||
if (isAppPageRouteModule(routeModule)) {
|
||||
const { pathnameRouteParamSegments } = extractPathnameRouteParamSegmentsFromLoaderTree(routeModule.userland.loaderTree, route);
|
||||
return pathnameRouteParamSegments;
|
||||
}
|
||||
return extractPathnameRouteParamSegmentsFromSegments(segments);
|
||||
}
|
||||
export function extractPathnameRouteParamSegmentsFromSegments(segments) {
|
||||
// TODO: should we consider what values are already present in the page?
|
||||
// For AppRouteRouteModule, filter the segments array to get the route params
|
||||
// that contribute to the pathname.
|
||||
const result = [];
|
||||
for (const segment of segments){
|
||||
// Skip segments without param info.
|
||||
if (!segment.paramName || !segment.paramType) continue;
|
||||
// Collect all the route param keys that contribute to the pathname.
|
||||
result.push({
|
||||
name: segment.name,
|
||||
paramName: segment.paramName,
|
||||
paramType: segment.paramType
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Resolves all route parameters from the loader tree. This function uses
|
||||
* tree-based traversal to correctly handle the hierarchical structure of routes
|
||||
* and accurately determine parameter values based on their depth in the tree.
|
||||
*
|
||||
* This processes both regular route parameters (from the main children route) and
|
||||
* parallel route parameters (from slots like @modal, @sidebar).
|
||||
*
|
||||
* Unlike interpolateParallelRouteParams (which has a complete URL at runtime),
|
||||
* this build-time function determines which route params are unknown.
|
||||
* The pathname may contain placeholders like [slug], making it incomplete.
|
||||
*
|
||||
* @param loaderTree - The loader tree structure containing route hierarchy
|
||||
* @param params - The current route parameters object (will be mutated)
|
||||
* @param route - The current route being processed
|
||||
* @param fallbackRouteParams - Array of fallback route parameters (will be mutated)
|
||||
*/ export function resolveRouteParamsFromTree(loaderTree, params, route, fallbackRouteParams) {
|
||||
// Stack-based traversal with depth tracking
|
||||
const stack = [
|
||||
{
|
||||
tree: loaderTree,
|
||||
depth: 0
|
||||
}
|
||||
];
|
||||
while(stack.length > 0){
|
||||
const { tree, depth } = stack.pop();
|
||||
const { segment, parallelRoutes } = parseLoaderTree(tree);
|
||||
const appSegment = parseAppRouteSegment(segment);
|
||||
// If this segment is a route parameter, then we should process it if it's
|
||||
// not already known and is not already marked as a fallback route param.
|
||||
if ((appSegment == null ? void 0 : appSegment.type) === 'dynamic' && !params.hasOwnProperty(appSegment.param.paramName) && !fallbackRouteParams.some((param)=>param.paramName === appSegment.param.paramName)) {
|
||||
const { paramName, paramType } = appSegment.param;
|
||||
const paramValue = resolveParamValue(paramName, paramType, depth, route, params);
|
||||
if (paramValue !== undefined) {
|
||||
params[paramName] = paramValue;
|
||||
} else if (paramType !== 'optional-catchall') {
|
||||
// If we couldn't resolve the param, mark it as a fallback
|
||||
fallbackRouteParams.push({
|
||||
paramName,
|
||||
paramType
|
||||
});
|
||||
}
|
||||
}
|
||||
// Calculate next depth - increment if this is not a route group and not empty
|
||||
let nextDepth = depth;
|
||||
if (appSegment && appSegment.type !== 'route-group' && appSegment.type !== 'parallel-route') {
|
||||
nextDepth++;
|
||||
}
|
||||
// Add all parallel routes to the stack for processing.
|
||||
for (const parallelRoute of Object.values(parallelRoutes)){
|
||||
stack.push({
|
||||
tree: parallelRoute,
|
||||
depth: nextDepth
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=utils.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user