UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

746 lines (745 loc) 24.4 kB
import { last } from "./utils.js"; import { createLRUCache } from "./lru-cache.js"; import invariant from "tiny-invariant"; var SEGMENT_TYPE_INDEX = 4; var SEGMENT_TYPE_PATHLESS = 5; function getOpenAndCloseBraces(part) { const openBrace = part.indexOf("{"); if (openBrace === -1) return null; const closeBrace = part.indexOf("}", openBrace); if (closeBrace === -1) return null; if (openBrace + 1 >= part.length) return null; return [openBrace, closeBrace]; } /** * Populates the `output` array with the parsed representation of the given `segment` string. * * Usage: * ```ts * let output * let cursor = 0 * while (cursor < path.length) { * output = parseSegment(path, cursor, output) * const end = output[5] * cursor = end + 1 * ``` * * `output` is stored outside to avoid allocations during repeated calls. It doesn't need to be typed * or initialized, it will be done automatically. */ function parseSegment(path, start, output = new Uint16Array(6)) { const next = path.indexOf("/", start); const end = next === -1 ? path.length : next; const part = path.substring(start, end); if (!part || !part.includes("$")) { output[0] = 0; output[1] = start; output[2] = start; output[3] = end; output[4] = end; output[5] = end; return output; } if (part === "$") { const total = path.length; output[0] = 2; output[1] = start; output[2] = start; output[3] = total; output[4] = total; output[5] = total; return output; } if (part.charCodeAt(0) === 36) { output[0] = 1; output[1] = start; output[2] = start + 1; output[3] = end; output[4] = end; output[5] = end; return output; } const braces = getOpenAndCloseBraces(part); if (braces) { const [openBrace, closeBrace] = braces; const firstChar = part.charCodeAt(openBrace + 1); if (firstChar === 45) { if (openBrace + 2 < part.length && part.charCodeAt(openBrace + 2) === 36) { const paramStart = openBrace + 3; const paramEnd = closeBrace; if (paramStart < paramEnd) { output[0] = 3; output[1] = start + openBrace; output[2] = start + paramStart; output[3] = start + paramEnd; output[4] = start + closeBrace + 1; output[5] = end; return output; } } } else if (firstChar === 36) { const dollarPos = openBrace + 1; const afterDollar = openBrace + 2; if (afterDollar === closeBrace) { output[0] = 2; output[1] = start + openBrace; output[2] = start + dollarPos; output[3] = start + afterDollar; output[4] = start + closeBrace + 1; output[5] = path.length; return output; } output[0] = 1; output[1] = start + openBrace; output[2] = start + afterDollar; output[3] = start + closeBrace; output[4] = start + closeBrace + 1; output[5] = end; return output; } } output[0] = 0; output[1] = start; output[2] = start; output[3] = end; output[4] = end; output[5] = end; return output; } /** * Recursively parses the segments of the given route tree and populates a segment trie. * * @param data A reusable Uint16Array for parsing segments. (non important, we're just avoiding allocations) * @param route The current route to parse. * @param start The starting index for parsing within the route's full path. * @param node The current segment node in the trie to populate. * @param onRoute Callback invoked for each route processed. */ function parseSegments(defaultCaseSensitive, data, route, start, node, depth, onRoute) { onRoute?.(route); let cursor = start; { const path = route.fullPath ?? route.from; const length = path.length; const caseSensitive = route.options?.caseSensitive ?? defaultCaseSensitive; const skipOnParamError = !!(route.options?.params?.parse && route.options?.skipRouteOnParseError?.params); while (cursor < length) { const segment = parseSegment(path, cursor, data); let nextNode; const start = cursor; const end = segment[5]; cursor = end + 1; depth++; switch (segment[0]) { case 0: { const value = path.substring(segment[2], segment[3]); if (caseSensitive) { const existingNode = node.static?.get(value); if (existingNode) nextNode = existingNode; else { node.static ??= /* @__PURE__ */ new Map(); const next = createStaticNode(route.fullPath ?? route.from); next.parent = node; next.depth = depth; nextNode = next; node.static.set(value, next); } } else { const name = value.toLowerCase(); const existingNode = node.staticInsensitive?.get(name); if (existingNode) nextNode = existingNode; else { node.staticInsensitive ??= /* @__PURE__ */ new Map(); const next = createStaticNode(route.fullPath ?? route.from); next.parent = node; next.depth = depth; nextNode = next; node.staticInsensitive.set(name, next); } } break; } case 1: { const prefix_raw = path.substring(start, segment[1]); const suffix_raw = path.substring(segment[4], end); const actuallyCaseSensitive = caseSensitive && !!(prefix_raw || suffix_raw); const prefix = !prefix_raw ? void 0 : actuallyCaseSensitive ? prefix_raw : prefix_raw.toLowerCase(); const suffix = !suffix_raw ? void 0 : actuallyCaseSensitive ? suffix_raw : suffix_raw.toLowerCase(); const existingNode = !skipOnParamError && node.dynamic?.find((s) => !s.skipOnParamError && s.caseSensitive === actuallyCaseSensitive && s.prefix === prefix && s.suffix === suffix); if (existingNode) nextNode = existingNode; else { const next = createDynamicNode(1, route.fullPath ?? route.from, actuallyCaseSensitive, prefix, suffix); nextNode = next; next.depth = depth; next.parent = node; node.dynamic ??= []; node.dynamic.push(next); } break; } case 3: { const prefix_raw = path.substring(start, segment[1]); const suffix_raw = path.substring(segment[4], end); const actuallyCaseSensitive = caseSensitive && !!(prefix_raw || suffix_raw); const prefix = !prefix_raw ? void 0 : actuallyCaseSensitive ? prefix_raw : prefix_raw.toLowerCase(); const suffix = !suffix_raw ? void 0 : actuallyCaseSensitive ? suffix_raw : suffix_raw.toLowerCase(); const existingNode = !skipOnParamError && node.optional?.find((s) => !s.skipOnParamError && s.caseSensitive === actuallyCaseSensitive && s.prefix === prefix && s.suffix === suffix); if (existingNode) nextNode = existingNode; else { const next = createDynamicNode(3, route.fullPath ?? route.from, actuallyCaseSensitive, prefix, suffix); nextNode = next; next.parent = node; next.depth = depth; node.optional ??= []; node.optional.push(next); } break; } case 2: { const prefix_raw = path.substring(start, segment[1]); const suffix_raw = path.substring(segment[4], end); const actuallyCaseSensitive = caseSensitive && !!(prefix_raw || suffix_raw); const prefix = !prefix_raw ? void 0 : actuallyCaseSensitive ? prefix_raw : prefix_raw.toLowerCase(); const suffix = !suffix_raw ? void 0 : actuallyCaseSensitive ? suffix_raw : suffix_raw.toLowerCase(); const next = createDynamicNode(2, route.fullPath ?? route.from, actuallyCaseSensitive, prefix, suffix); nextNode = next; next.parent = node; next.depth = depth; node.wildcard ??= []; node.wildcard.push(next); } } node = nextNode; } if (skipOnParamError && route.children && !route.isRoot && route.id && route.id.charCodeAt(route.id.lastIndexOf("/") + 1) === 95) { const pathlessNode = createStaticNode(route.fullPath ?? route.from); pathlessNode.kind = SEGMENT_TYPE_PATHLESS; pathlessNode.parent = node; depth++; pathlessNode.depth = depth; node.pathless ??= []; node.pathless.push(pathlessNode); node = pathlessNode; } const isLeaf = (route.path || !route.children) && !route.isRoot; if (isLeaf && path.endsWith("/")) { const indexNode = createStaticNode(route.fullPath ?? route.from); indexNode.kind = SEGMENT_TYPE_INDEX; indexNode.parent = node; depth++; indexNode.depth = depth; node.index = indexNode; node = indexNode; } node.parse = route.options?.params?.parse ?? null; node.skipOnParamError = skipOnParamError; node.parsingPriority = route.options?.skipRouteOnParseError?.priority ?? 0; if (isLeaf && !node.route) { node.route = route; node.fullPath = route.fullPath ?? route.from; } } if (route.children) for (const child of route.children) parseSegments(defaultCaseSensitive, data, child, cursor, node, depth, onRoute); } function sortDynamic(a, b) { if (a.skipOnParamError && !b.skipOnParamError) return -1; if (!a.skipOnParamError && b.skipOnParamError) return 1; if (a.skipOnParamError && b.skipOnParamError && (a.parsingPriority || b.parsingPriority)) return b.parsingPriority - a.parsingPriority; if (a.prefix && b.prefix && a.prefix !== b.prefix) { if (a.prefix.startsWith(b.prefix)) return -1; if (b.prefix.startsWith(a.prefix)) return 1; } if (a.suffix && b.suffix && a.suffix !== b.suffix) { if (a.suffix.endsWith(b.suffix)) return -1; if (b.suffix.endsWith(a.suffix)) return 1; } if (a.prefix && !b.prefix) return -1; if (!a.prefix && b.prefix) return 1; if (a.suffix && !b.suffix) return -1; if (!a.suffix && b.suffix) return 1; if (a.caseSensitive && !b.caseSensitive) return -1; if (!a.caseSensitive && b.caseSensitive) return 1; return 0; } function sortTreeNodes(node) { if (node.pathless) for (const child of node.pathless) sortTreeNodes(child); if (node.static) for (const child of node.static.values()) sortTreeNodes(child); if (node.staticInsensitive) for (const child of node.staticInsensitive.values()) sortTreeNodes(child); if (node.dynamic?.length) { node.dynamic.sort(sortDynamic); for (const child of node.dynamic) sortTreeNodes(child); } if (node.optional?.length) { node.optional.sort(sortDynamic); for (const child of node.optional) sortTreeNodes(child); } if (node.wildcard?.length) { node.wildcard.sort(sortDynamic); for (const child of node.wildcard) sortTreeNodes(child); } } function createStaticNode(fullPath) { return { kind: 0, depth: 0, pathless: null, index: null, static: null, staticInsensitive: null, dynamic: null, optional: null, wildcard: null, route: null, fullPath, parent: null, parse: null, skipOnParamError: false, parsingPriority: 0 }; } /** * Keys must be declared in the same order as in `SegmentNode` type, * to ensure they are represented as the same object class in the engine. */ function createDynamicNode(kind, fullPath, caseSensitive, prefix, suffix) { return { kind, depth: 0, pathless: null, index: null, static: null, staticInsensitive: null, dynamic: null, optional: null, wildcard: null, route: null, fullPath, parent: null, parse: null, skipOnParamError: false, parsingPriority: 0, caseSensitive, prefix, suffix }; } function processRouteMasks(routeList, processedTree) { const segmentTree = createStaticNode("/"); const data = new Uint16Array(6); for (const route of routeList) parseSegments(false, data, route, 1, segmentTree, 0); sortTreeNodes(segmentTree); processedTree.masksTree = segmentTree; processedTree.flatCache = createLRUCache(1e3); } /** * Take an arbitrary list of routes, create a tree from them (if it hasn't been created already), and match a path against it. */ function findFlatMatch(path, processedTree) { path ||= "/"; const cached = processedTree.flatCache.get(path); if (cached) return cached; const result = findMatch(path, processedTree.masksTree); processedTree.flatCache.set(path, result); return result; } /** * @deprecated keep until v2 so that `router.matchRoute` can keep not caring about the actual route tree */ function findSingleMatch(from, caseSensitive, fuzzy, path, processedTree) { from ||= "/"; path ||= "/"; const key = caseSensitive ? `case\0${from}` : from; let tree = processedTree.singleCache.get(key); if (!tree) { tree = createStaticNode("/"); parseSegments(caseSensitive, new Uint16Array(6), { from }, 1, tree, 0); processedTree.singleCache.set(key, tree); } return findMatch(path, tree, fuzzy); } function findRouteMatch(path, processedTree, fuzzy = false) { const key = fuzzy ? path : `nofuzz\0${path}`; const cached = processedTree.matchCache.get(key); if (cached !== void 0) return cached; path ||= "/"; let result; try { result = findMatch(path, processedTree.segmentTree, fuzzy); } catch (err) { if (err instanceof URIError) result = null; else throw err; } if (result) result.branch = buildRouteBranch(result.route); processedTree.matchCache.set(key, result); return result; } /** Trim trailing slashes (except preserving root '/'). */ function trimPathRight(path) { return path === "/" ? path : path.replace(/\/{1,}$/, ""); } /** * Processes a route tree into a segment trie for efficient path matching. * Also builds lookup maps for routes by ID and by trimmed full path. */ function processRouteTree(routeTree, caseSensitive = false, initRoute) { const segmentTree = createStaticNode(routeTree.fullPath); const data = new Uint16Array(6); const routesById = {}; const routesByPath = {}; let index = 0; parseSegments(caseSensitive, data, routeTree, 1, segmentTree, 0, (route) => { initRoute?.(route, index); invariant(!(route.id in routesById), `Duplicate routes found with id: ${String(route.id)}`); routesById[route.id] = route; if (index !== 0 && route.path) { const trimmedFullPath = trimPathRight(route.fullPath); if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith("/")) routesByPath[trimmedFullPath] = route; } index++; }); sortTreeNodes(segmentTree); return { processedTree: { segmentTree, singleCache: createLRUCache(1e3), matchCache: createLRUCache(1e3), flatCache: null, masksTree: null }, routesById, routesByPath }; } function findMatch(path, segmentTree, fuzzy = false) { const parts = path.split("/"); const leaf = getNodeMatch(path, parts, segmentTree, fuzzy); if (!leaf) return null; const [rawParams] = extractParams(path, parts, leaf); return { route: leaf.node.route, rawParams, parsedParams: leaf.parsedParams }; } /** * This function is "resumable": * - the `leaf` input can contain `extract` and `rawParams` properties from a previous `extractParams` call * - the returned `state` can be passed back as `extract` in a future call to continue extracting params from where we left off * * Inputs are *not* mutated. */ function extractParams(path, parts, leaf) { const list = buildBranch(leaf.node); let nodeParts = null; const rawParams = Object.create(null); /** which segment of the path we're currently processing */ let partIndex = leaf.extract?.part ?? 0; /** which node of the route tree branch we're currently processing */ let nodeIndex = leaf.extract?.node ?? 0; /** index of the 1st character of the segment we're processing in the path string */ let pathIndex = leaf.extract?.path ?? 0; /** which fullPath segment we're currently processing */ let segmentCount = leaf.extract?.segment ?? 0; for (; nodeIndex < list.length; partIndex++, nodeIndex++, pathIndex++, segmentCount++) { const node = list[nodeIndex]; if (node.kind === SEGMENT_TYPE_INDEX) break; if (node.kind === SEGMENT_TYPE_PATHLESS) { segmentCount--; partIndex--; pathIndex--; continue; } const part = parts[partIndex]; const currentPathIndex = pathIndex; if (part) pathIndex += part.length; if (node.kind === 1) { nodeParts ??= leaf.node.fullPath.split("/"); const nodePart = nodeParts[segmentCount]; const preLength = node.prefix?.length ?? 0; if (nodePart.charCodeAt(preLength) === 123) { const sufLength = node.suffix?.length ?? 0; const name = nodePart.substring(preLength + 2, nodePart.length - sufLength - 1); const value = part.substring(preLength, part.length - sufLength); rawParams[name] = decodeURIComponent(value); } else { const name = nodePart.substring(1); rawParams[name] = decodeURIComponent(part); } } else if (node.kind === 3) { if (leaf.skipped & 1 << nodeIndex) { partIndex--; pathIndex = currentPathIndex - 1; continue; } nodeParts ??= leaf.node.fullPath.split("/"); const nodePart = nodeParts[segmentCount]; const preLength = node.prefix?.length ?? 0; const sufLength = node.suffix?.length ?? 0; const name = nodePart.substring(preLength + 3, nodePart.length - sufLength - 1); const value = node.suffix || node.prefix ? part.substring(preLength, part.length - sufLength) : part; if (value) rawParams[name] = decodeURIComponent(value); } else if (node.kind === 2) { const n = node; const value = path.substring(currentPathIndex + (n.prefix?.length ?? 0), path.length - (n.suffix?.length ?? 0)); const splat = decodeURIComponent(value); rawParams["*"] = splat; rawParams._splat = splat; break; } } if (leaf.rawParams) Object.assign(rawParams, leaf.rawParams); return [rawParams, { part: partIndex, node: nodeIndex, path: pathIndex, segment: segmentCount }]; } function buildRouteBranch(route) { const list = [route]; while (route.parentRoute) { route = route.parentRoute; list.push(route); } list.reverse(); return list; } function buildBranch(node) { const list = Array(node.depth + 1); do { list[node.depth] = node; node = node.parent; } while (node); return list; } function getNodeMatch(path, parts, segmentTree, fuzzy) { if (path === "/" && segmentTree.index) return { node: segmentTree.index, skipped: 0 }; const trailingSlash = !last(parts); const pathIsIndex = trailingSlash && path !== "/"; const partsLength = parts.length - (trailingSlash ? 1 : 0); const stack = [{ node: segmentTree, index: 1, skipped: 0, depth: 1, statics: 1, dynamics: 0, optionals: 0 }]; let wildcardMatch = null; let bestFuzzy = null; let bestMatch = null; while (stack.length) { const frame = stack.pop(); const { node, index, skipped, depth, statics, dynamics, optionals } = frame; let { extract, rawParams, parsedParams } = frame; if (node.skipOnParamError) { if (!validateMatchParams(path, parts, frame)) continue; rawParams = frame.rawParams; extract = frame.extract; parsedParams = frame.parsedParams; } if (fuzzy && node.route && node.kind !== SEGMENT_TYPE_INDEX && isFrameMoreSpecific(bestFuzzy, frame)) bestFuzzy = frame; const isBeyondPath = index === partsLength; if (isBeyondPath) { if (node.route && !pathIsIndex && isFrameMoreSpecific(bestMatch, frame)) bestMatch = frame; if (!node.optional && !node.wildcard && !node.index && !node.pathless) continue; } const part = isBeyondPath ? void 0 : parts[index]; let lowerPart; if (isBeyondPath && node.index) { const indexFrame = { node: node.index, index, skipped, depth: depth + 1, statics, dynamics, optionals, extract, rawParams, parsedParams }; let indexValid = true; if (node.index.skipOnParamError) { if (!validateMatchParams(path, parts, indexFrame)) indexValid = false; } if (indexValid) { if (statics === partsLength && !dynamics && !optionals && !skipped) return indexFrame; if (isFrameMoreSpecific(bestMatch, indexFrame)) bestMatch = indexFrame; } } if (node.wildcard && isFrameMoreSpecific(wildcardMatch, frame)) for (const segment of node.wildcard) { const { prefix, suffix } = segment; if (prefix) { if (isBeyondPath) continue; if (!(segment.caseSensitive ? part : lowerPart ??= part.toLowerCase()).startsWith(prefix)) continue; } if (suffix) { if (isBeyondPath) continue; const end = parts.slice(index).join("/").slice(-suffix.length); if ((segment.caseSensitive ? end : end.toLowerCase()) !== suffix) continue; } const frame = { node: segment, index: partsLength, skipped, depth, statics, dynamics, optionals, extract, rawParams, parsedParams }; if (segment.skipOnParamError) { if (!validateMatchParams(path, parts, frame)) continue; } wildcardMatch = frame; break; } if (node.optional) { const nextSkipped = skipped | 1 << depth; const nextDepth = depth + 1; for (let i = node.optional.length - 1; i >= 0; i--) { const segment = node.optional[i]; stack.push({ node: segment, index, skipped: nextSkipped, depth: nextDepth, statics, dynamics, optionals, extract, rawParams, parsedParams }); } if (!isBeyondPath) for (let i = node.optional.length - 1; i >= 0; i--) { const segment = node.optional[i]; const { prefix, suffix } = segment; if (prefix || suffix) { const casePart = segment.caseSensitive ? part : lowerPart ??= part.toLowerCase(); if (prefix && !casePart.startsWith(prefix)) continue; if (suffix && !casePart.endsWith(suffix)) continue; } stack.push({ node: segment, index: index + 1, skipped, depth: nextDepth, statics, dynamics, optionals: optionals + 1, extract, rawParams, parsedParams }); } } if (!isBeyondPath && node.dynamic && part) for (let i = node.dynamic.length - 1; i >= 0; i--) { const segment = node.dynamic[i]; const { prefix, suffix } = segment; if (prefix || suffix) { const casePart = segment.caseSensitive ? part : lowerPart ??= part.toLowerCase(); if (prefix && !casePart.startsWith(prefix)) continue; if (suffix && !casePart.endsWith(suffix)) continue; } stack.push({ node: segment, index: index + 1, skipped, depth: depth + 1, statics, dynamics: dynamics + 1, optionals, extract, rawParams, parsedParams }); } if (!isBeyondPath && node.staticInsensitive) { const match = node.staticInsensitive.get(lowerPart ??= part.toLowerCase()); if (match) stack.push({ node: match, index: index + 1, skipped, depth: depth + 1, statics: statics + 1, dynamics, optionals, extract, rawParams, parsedParams }); } if (!isBeyondPath && node.static) { const match = node.static.get(part); if (match) stack.push({ node: match, index: index + 1, skipped, depth: depth + 1, statics: statics + 1, dynamics, optionals, extract, rawParams, parsedParams }); } if (node.pathless) { const nextDepth = depth + 1; for (let i = node.pathless.length - 1; i >= 0; i--) { const segment = node.pathless[i]; stack.push({ node: segment, index, skipped, depth: nextDepth, statics, dynamics, optionals, extract, rawParams, parsedParams }); } } } if (bestMatch && wildcardMatch) return isFrameMoreSpecific(wildcardMatch, bestMatch) ? bestMatch : wildcardMatch; if (bestMatch) return bestMatch; if (wildcardMatch) return wildcardMatch; if (fuzzy && bestFuzzy) { let sliceIndex = bestFuzzy.index; for (let i = 0; i < bestFuzzy.index; i++) sliceIndex += parts[i].length; const splat = sliceIndex === path.length ? "/" : path.slice(sliceIndex); bestFuzzy.rawParams ??= Object.create(null); bestFuzzy.rawParams["**"] = decodeURIComponent(splat); return bestFuzzy; } return null; } function validateMatchParams(path, parts, frame) { try { const [rawParams, state] = extractParams(path, parts, frame); frame.rawParams = rawParams; frame.extract = state; const parsed = frame.node.parse(rawParams); frame.parsedParams = Object.assign(Object.create(null), frame.parsedParams, parsed); return true; } catch { return null; } } function isFrameMoreSpecific(prev, next) { if (!prev) return true; return next.statics > prev.statics || next.statics === prev.statics && (next.dynamics > prev.dynamics || next.dynamics === prev.dynamics && (next.optionals > prev.optionals || next.optionals === prev.optionals && ((next.node.kind === SEGMENT_TYPE_INDEX) > (prev.node.kind === SEGMENT_TYPE_INDEX) || next.node.kind === SEGMENT_TYPE_INDEX === (prev.node.kind === SEGMENT_TYPE_INDEX) && next.depth > prev.depth))); } //#endregion export { findFlatMatch, findRouteMatch, findSingleMatch, parseSegment, processRouteMasks, processRouteTree }; //# sourceMappingURL=new-process-route-tree.js.map