UNPKG

next

Version:

The React Framework

170 lines (169 loc) • 7.19 kB
/** * One entry in the route table that a build passes to an adapter can serve * several fallback shells. * * A fallback shell repeats the whole path of its source page, and it resolves * the leading params of that page to concrete values. Those values form the * prefix of the shell path, so the shells of one source page differ only in * that prefix. A pattern can list several prefixes as alternatives, which lets * one entry match them all. * * The shells that one entry serves are a run: a stretch of neighbours in the * manifest. Adjacency is what makes a run safe to serve from one entry. That * entry takes the position of the first shell of the run, which this file * calls the representative, so every shell that the entry replaces keeps its * place relative to the routes around it. */ import { getNamedRouteRegex } from '../../shared/lib/router/utils/route-regex'; import { escapeStringRegexp } from '../../shared/lib/escape-regexp'; /** * Splits the page of a fallback shell into the prefix that holds its resolved * param values, and the path that follows. * * Returns undefined when the page resolves nothing, or when the resolved * segments are not consecutive from the first segment onwards. */ function splitShellPage(page, sourcePage) { const pageSegments = page.split('/'); const sourceSegments = sourcePage.split('/'); if (pageSegments.length !== sourceSegments.length) { return undefined; } const resolved = []; for(let index = 0; index < pageSegments.length; index++){ if (pageSegments[index] !== sourceSegments[index]) { resolved.push(index); } } if (resolved.length === 0) { return undefined; } // A shell resolves the leading params of its source page, so the resolved // segments are consecutive and the first of them is the first segment of the // path. `split` returns an empty string at index 0, so they start at index 1. for(let position = 0; position < resolved.length; position++){ if (resolved[position] !== position + 1) { return undefined; } } // The source page declares a param at each resolved position, and the shell // holds a value there. Anything else means the two pages differ for another // reason, and the leading segments are not a prefix of resolved values. for (const index of resolved){ if (!sourceSegments[index].startsWith('[') || pageSegments[index].startsWith('[')) { return undefined; } } const tailStart = resolved.length + 1; return { prefix: pageSegments.slice(1, tailStart).join('/'), tail: pageSegments.slice(tailStart).join('/') }; } /** * Collects the runs of fallback shells that one entry can serve. * * The shells of a run are neighbours in the manifest that agree on: * * - The source page. * - The path that follows the prefix. * - The value of `fallback: false`. * * They have to be neighbours because the entry takes the position of the first * shell of the run. Every shell that the entry replaces then keeps its place * relative to the routes around it. Any other route between two shells ends the * run, because an entry that reached across it would move ahead of a route that * a request matches first. * * They have to agree on `fallback: false` because an entry carries one set of * conditions. * * The caller builds one pattern for a run, and it lists the prefixes of the run * as complete alternatives. For a source page `/[team]/[locale]/posts/[id]` * with shells for `acme/en`, `acme/de` and `globex/en`, that pattern holds: * * ``` * (acme/en|acme/de|globex/en) * ``` * * A pattern that offered a choice per param instead, such as * `(acme|globex)/(en|de)`, would also match `globex/de`. The build never * prerendered that pair, so a request for it would resolve to an output that * does not exist, and it would then fall through to whichever route claims the * rewritten path. * * A shell can belong to no run, and one source page can hold several runs. That * happens when the build resolves a different number of params for neighbouring * shells, because their prefixes then have different lengths and the paths that * follow them differ. * * A run of only one shell has a single prefix, so an entry for it would match * what the entry for that shell already matches. This function leaves such a * shell out of the result. */ export function collectFallbackShellRuns(dynamicRoutes, hasFallbackFalse) { const runs = []; let current; for (const route of dynamicRoutes){ // `pageToRoute` sets `sourcePage` only when the build passes it a source // page, and the build does that for a fallback shell. The field therefore // names the page whose path this shell repeats, and it is absent on every // other route. const { sourcePage } = route; const split = sourcePage && sourcePage !== route.page ? splitShellPage(route.page, sourcePage) : undefined; // This route is not a shell that a run can hold, so it ends the run in // progress. if (!sourcePage || !split) { current = undefined; continue; } const isFallbackFalse = hasFallbackFalse(route.page); if (current && (current.sourcePage !== sourcePage || current.tail !== split.tail || current.isFallbackFalse !== isFallbackFalse)) { current = undefined; } if (!current) { current = { sourcePage, tail: split.tail, isFallbackFalse, shells: [] }; runs.push(current); } current.shells.push({ page: route.page, prefix: split.prefix }); } const byRepresentativePage = new Map(); const replacedPages = new Set(); for (const run of runs){ if (run.shells.length < 2) { continue; } // The caller replaces the escaped prefix at the start of the pattern for // the representative, so this function keeps the run only when that pattern // starts with the prefix. // // Two things make that true today. `getNamedRouteRegex` escapes each // segment the same way, and a fallback shell keeps at least one param // unresolved, so a slash always follows its prefix. This check holds the // run back if either stops being true. const [representative, ...replaced] = run.shells; const { namedRegex } = getNamedRouteRegex(representative.page, { prefixRouteKeys: true }); if (!namedRegex.startsWith(`^/${escapeStringRegexp(representative.prefix)}/`)) { continue; } byRepresentativePage.set(representative.page, { prefixes: run.shells.map((shell)=>shell.prefix), tail: run.tail }); for (const shell of replaced){ replacedPages.add(shell.page); } } return { byRepresentativePage, replacedPages }; } //# sourceMappingURL=fallback-shell-runs.js.map