UNPKG

next

Version:

The React Framework

127 lines (126 loc) • 6.4 kB
import { normalizeAppPath } from './app-paths'; // order matters here, the first match will be used export const INTERCEPTION_ROUTE_MARKERS = [ '(..)(..)', '(.)', '(..)', '(...)' ]; function parseRoutePattern(route) { const segments = route.split('/').filter(Boolean); const tail = segments.at(-1); const isOptionalCatchAll = tail !== undefined && tail.startsWith('[[...') && tail.endsWith(']]'); const isCatchAll = tail !== undefined && tail.startsWith('[...') && tail.endsWith(']'); const unbounded = isOptionalCatchAll || isCatchAll; const prefixSegments = unbounded ? segments.slice(0, -1) : segments; return { prefix: prefixSegments.map((segment)=>segment.startsWith('[') && segment.endsWith(']') ? null : segment), minLength: prefixSegments.length + (isCatchAll && !isOptionalCatchAll ? 1 : 0), unbounded }; } function acceptsLength(pattern, length) { return pattern.unbounded ? length >= pattern.minLength : length === pattern.minLength; } function prefixCovers(canonical, intercepted) { return canonical.prefix.every((canonicalSegment, index)=>{ if (canonicalSegment === null) return true; const interceptedSegment = intercepted.prefix[index]; return interceptedSegment !== undefined && interceptedSegment !== null && canonicalSegment === interceptedSegment; }); } function patternCoversAtLength(canonical, intercepted, length) { return acceptsLength(canonical, length) && acceptsLength(intercepted, length) && prefixCovers(canonical, intercepted); } /** Returns whether ordinary route patterns cover every URL in `route`. */ function isRoutePatternCovered(route, ordinaryRoutes) { const intercepted = parseRoutePattern(route); if (!intercepted.unbounded) { return ordinaryRoutes.some((pattern)=>patternCoversAtLength(pattern, intercepted, intercepted.minLength)); } let unboundedCoverageStart = Infinity; for (const pattern of ordinaryRoutes){ if (pattern.unbounded && prefixCovers(pattern, intercepted)) { unboundedCoverageStart = Math.min(unboundedCoverageStart, Math.max(intercepted.minLength, pattern.minLength)); } } if (unboundedCoverageStart === Infinity) return false; // An optional or required catchall can have its shorter paths covered by // fixed routes before another catchall takes over the remaining suffix. for(let length = intercepted.minLength; length < unboundedCoverageStart; length++){ if (!ordinaryRoutes.some((pattern)=>patternCoversAtLength(pattern, intercepted, length))) { return false; } } return true; } export function isInterceptionRouteAppPath(path) { // TODO-APP: add more serious validation return path.split('/').find((segment)=>INTERCEPTION_ROUTE_MARKERS.find((m)=>segment.startsWith(m))) !== undefined; } export function extractInterceptionRouteInformation(path) { let interceptingRoute; let marker; let interceptedRoute; for (const segment of path.split('/')){ marker = INTERCEPTION_ROUTE_MARKERS.find((m)=>segment.startsWith(m)); if (marker) { ; [interceptingRoute, interceptedRoute] = path.split(marker, 2); break; } } if (!interceptingRoute || !marker || !interceptedRoute) { throw new Error(`Invalid interception route: ${path}. Must be in the format /<intercepting route>/(..|...|..)(..)/<intercepted route>`); } interceptingRoute = normalizeAppPath(interceptingRoute) // normalize the path, e.g. /(blog)/feed -> /feed ; switch(marker){ case '(.)': // (.) indicates that we should match with sibling routes, so we just need to append the intercepted route to the intercepting route if (interceptingRoute === '/') { interceptedRoute = `/${interceptedRoute}`; } else { interceptedRoute = interceptingRoute + '/' + interceptedRoute; } break; case '(..)': // (..) indicates that we should match at one level up, so we need to remove the last segment of the intercepting route if (interceptingRoute === '/') { throw new Error(`Invalid interception route: ${path}. Cannot use (..) marker at the root level, use (.) instead.`); } interceptedRoute = interceptingRoute.split('/').slice(0, -1).concat(interceptedRoute).join('/'); break; case '(...)': // (...) will match the route segment in the root directory, so we need to use the root directory to prepend the intercepted route interceptedRoute = '/' + interceptedRoute; break; case '(..)(..)': // (..)(..) indicates that we should match at two levels up, so we need to remove the last two segments of the intercepting route const splitInterceptingRoute = interceptingRoute.split('/'); if (splitInterceptingRoute.length <= 2) { throw new Error(`Invalid interception route: ${path}. Cannot use (..)(..) marker at the root level or one level up.`); } interceptedRoute = splitInterceptingRoute.slice(0, -2).concat(interceptedRoute).join('/'); break; default: throw new Error('Invariant: unexpected marker'); } return { interceptingRoute, interceptedRoute }; } /** * Finds interception matchers that cannot be loaded as an ordinary request. * * This must run after catch-all normalization and pruning so `appPaths` * contains the final matcher set rather than every possible matcher candidate. */ export function findMissingCanonicalInterceptionRoutes(appPaths) { const canonicalRoutes = Object.keys(appPaths).filter((route)=>!isInterceptionRouteAppPath(route)); const canonicalRoutePatterns = canonicalRoutes.map(parseRoutePattern); return Object.keys(appPaths).filter(isInterceptionRouteAppPath).map((interceptionRoute)=>({ interceptionRoute, canonicalRoute: extractInterceptionRouteInformation(interceptionRoute).interceptedRoute })).filter(({ canonicalRoute })=>!isRoutePatternCovered(canonicalRoute, canonicalRoutePatterns)).sort((a, b)=>a.interceptionRoute.localeCompare(b.interceptionRoute)); } //# sourceMappingURL=interception-routes.js.map