UNPKG

next

Version:

The React Framework

176 lines (175 loc) • 9.23 kB
import { PARAM_MATCHING_MODES } from '../segment-config/app/app-segments'; import { FallbackMode } from '../../lib/fallback'; import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'; import { getSegmentParam } from '../../shared/lib/router/utils/get-segment-param'; export async function resolveParamMatching(page, segmentTree, pathnameSegments) { const configuredSegments = []; const nodes = [ ...segmentTree ]; for(let index = 0; index < nodes.length; index++){ const [segment, children] = nodes[index]; if (segment.paramMatching !== undefined) configuredSegments.push(segment); nodes.push(...children); } if (configuredSegments.length === 0) return undefined; const routeParamNames = new Set(pathnameSegments.map(({ paramName })=>paramName)); const candidates = new Map(); const paramsMissingPolicy = new Set(); // Start independent generators together, before merging their results down // each branch. const fragments = new Map(); await Promise.all(configuredSegments.map(async (segment)=>{ const paramMatchingExport = segment.paramMatching; const value = typeof paramMatchingExport === 'function' ? await paramMatchingExport() : paramMatchingExport; for (const paramName of Object.keys(value)){ if (!routeParamNames.has(paramName)) { const exportName = typeof paramMatchingExport === 'function' ? 'unstable_generateParamMatching' : 'unstable_paramMatching'; throw new Error(`Invalid parameter "${paramName}" in \`${exportName}\` for "${page}". Parameter matching may only configure dynamic parameters in this route.`); } } fragments.set(segment, value); })); function visit([segment, children], inherited, paramNames) { const branchCandidates = new Map(inherited); const fragment = fragments.get(segment); if (fragment) { for (const [paramName, mode] of Object.entries(fragment)){ branchCandidates.set(paramName, { mode, filePath: segment.filePath }); } } if (segment.paramName) paramNames = [ ...paramNames, segment.paramName ]; if (children.length > 0) { for (const child of children)visit(child, branchCandidates, paramNames); return; } // Compare the effective policies at every leaf, including leaves without // exports. A sibling's override must not erase this branch's inheritance. for (const paramName of paramNames){ if (!branchCandidates.has(paramName)) { paramsMissingPolicy.add(paramName); } } for (const [paramName, candidate] of branchCandidates){ const parallelCandidates = candidates.get(paramName); if (!parallelCandidates) { candidates.set(paramName, [ candidate ]); } else if (!parallelCandidates.some(({ mode })=>mode === candidate.mode)) { parallelCandidates.push(candidate); } } } for (const root of segmentTree)visit(root, new Map(), []); const policy = {}; for (const { paramName } of pathnameSegments){ const paramCandidates = candidates.get(paramName); if (!paramCandidates) continue; const mode = paramCandidates[0].mode; if (paramCandidates.some((candidate)=>candidate.mode !== mode)) { const definitions = paramCandidates.map((candidate)=>`${candidate.filePath ?? '<unknown module>'} (${candidate.mode})`).join(', '); throw new Error(`Route "${page}" has conflicting parallel parameter matching modes for parameter "${paramName}": ${definitions}.`); } if (mode === 'not-found' && paramsMissingPolicy.has(paramName)) { throw new Error(`Parameter "${paramName}" in route "${page}" uses "not-found" in one parallel branch, but another branch has no explicit policy. Every parallel branch sharing this parameter must explicitly configure "not-found", either directly or through an inherited layout.`); } policy[paramName] = mode; } let previousPhase = -1; let previousParamName; let unconfiguredPrefixParamName; for (const { paramName } of pathnameSegments){ const mode = policy[paramName]; if (!mode) { unconfiguredPrefixParamName ??= paramName; continue; } if (mode === 'not-found' && unconfiguredPrefixParamName !== undefined) { throw new Error(`Invalid parameter matching for "${page}": Parameter "${unconfiguredPrefixParamName}" must explicitly configure "not-found" before parameter "${paramName}" uses "not-found". Configure the preceding parameter directly or in an inherited layout; it cannot be closed by inference.`); } const currentPhase = PARAM_MATCHING_MODES.indexOf(mode); if (currentPhase < previousPhase) { throw new Error(`Invalid parameter matching for "${page}": parameter "${paramName}" uses "${mode}" after parameter "${previousParamName}" uses a later matching phase. Expected parameters in this order: "not-found", "blocking", "fallback", then "dynamic".`); } previousPhase = currentPhase; previousParamName = paramName; } return policy; } /** * Validate closure after every page's policies have been evaluated and merged. * Identify each parameter definition by its normalized URL prefix, including * the parameter name. /blog/[slug] and /shop/[slug] are independent; route * groups and parallel slots are removed by normalization and share the same * definition when their URL prefixes match. */ export function validateParamMatchingCoherence(paramMatchingByRoute) { const parameters = new Map(); for (const [appPath, paramMatching] of [ ...paramMatchingByRoute ].sort(([a], [b])=>a.localeCompare(b))){ const route = normalizeAppPath(appPath); let prefix = ''; for (const segment of route.split('/')){ if (!segment) continue; prefix += `/${segment}`; const param = getSegmentParam(segment); if (!param) continue; const notFound = (paramMatching == null ? void 0 : paramMatching[param.paramName]) === 'not-found'; const previous = parameters.get(prefix); if (previous === undefined) { parameters.set(prefix, [ route, notFound ]); } else if (previous[1] !== notFound) { const closedRoute = notFound ? route : previous[0]; const openRoute = notFound ? previous[0] : route; throw new Error(`Parameter "${param.paramName}" at "${prefix}" uses "not-found" in route "${closedRoute}". Route "${openRoute}" must explicitly configure "not-found" for this parameter, either directly or through an inherited layout.`); } } } } export function getParamMatchingFallbackMode(paramMatching, fallbackRouteParams, inferredFallbackMode, rootParamKeys) { let hasInferredBlockingRoot = false; for (const { paramName } of fallbackRouteParams){ switch(paramMatching[paramName]){ case undefined: if (rootParamKeys.has(paramName)) hasInferredBlockingRoot = true; break; case 'not-found': return FallbackMode.NOT_FOUND; case 'blocking': return FallbackMode.BLOCKING_STATIC_RENDER; case 'fallback': case 'dynamic': return hasInferredBlockingRoot ? FallbackMode.BLOCKING_STATIC_RENDER : FallbackMode.PRERENDER; } } // Root parameters retain their existing blocking inference. Keep walking // above so a later explicit not-found can still reject the whole match. return hasInferredBlockingRoot ? FallbackMode.BLOCKING_STATIC_RENDER : inferredFallbackMode; } export function validateParamMatchingParams(page, paramMatching, generatedParamNames, missingParamNames, pathnameSegments, output) { let dynamicParamName; for (const { paramName } of pathnameSegments){ const mode = paramMatching[paramName]; if (mode === 'dynamic') dynamicParamName = paramName; if (dynamicParamName && generatedParamNames.has(paramName)) { throw new Error(`Route "${page}" cannot prerender parameter "${paramName}" because parameter "${dynamicParamName}" is configured as "dynamic".`); } if (mode === 'not-found' && missingParamNames.has(paramName)) { throw new Error(`Route "${page}" configures parameter "${paramName}" as "not-found", but generateStaticParams returned a result without that parameter.`); } if (output === 'export' && mode !== 'not-found') { throw new Error(`Route "${page}" must configure parameter "${paramName}" as "not-found" when using parameter matching with "output: export".`); } } } //# sourceMappingURL=param-matching.js.map