UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

1 lines 17.1 kB
{"version":3,"file":"path.cjs","names":[],"sources":["../../src/path.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport { last } from './utils'\nimport {\n SEGMENT_TYPE_OPTIONAL_PARAM,\n SEGMENT_TYPE_PARAM,\n SEGMENT_TYPE_PATHNAME,\n SEGMENT_TYPE_WILDCARD,\n parseSegment,\n} from './new-process-route-tree'\nimport type { LRUCache } from './lru-cache'\n\n/** Join path segments, cleaning duplicate slashes between parts. */\nexport function joinPaths(paths: Array<string | undefined>) {\n return cleanPath(\n paths\n .filter((val) => {\n return val !== undefined\n })\n .join('/'),\n )\n}\n\n/** Remove repeated slashes from a path string. */\nexport function cleanPath(path: string) {\n // remove double slashes\n return path.replace(/\\/{2,}/g, '/')\n}\n\n/** Trim leading slashes (except preserving root '/'). */\nexport function trimPathLeft(path: string) {\n return path === '/' ? path : path.replace(/^\\/{1,}/, '')\n}\n\n/** Trim trailing slashes (except preserving root '/'). */\nexport function trimPathRight(path: string) {\n const len = path.length\n return len > 1 && path[len - 1] === '/' ? path.replace(/\\/{1,}$/, '') : path\n}\n\n/** Trim both leading and trailing slashes. */\nexport function trimPath(path: string) {\n return trimPathRight(trimPathLeft(path))\n}\n\n/** Remove a trailing slash from value when appropriate for comparisons. */\nexport function removeTrailingSlash(value: string, basepath: string): string {\n if (value?.endsWith('/') && value !== '/' && value !== `${basepath}/`) {\n return value.slice(0, -1)\n }\n return value\n}\n\n// intended to only compare path name\n// see the usage in the isActive under useLinkProps\n// /sample/path1 = /sample/path1/\n// /sample/path1/some <> /sample/path1\n/**\n * Compare two pathnames for exact equality after normalizing trailing slashes\n * relative to the provided `basepath`.\n */\nexport function exactPathTest(\n pathName1: string,\n pathName2: string,\n basepath: string,\n): boolean {\n return (\n removeTrailingSlash(pathName1, basepath) ===\n removeTrailingSlash(pathName2, basepath)\n )\n}\n\n// When resolving relative paths, we treat all paths as if they are trailing slash\n// documents. All trailing slashes are removed after the path is resolved.\n// Here are a few examples:\n//\n// /a/b/c + ./d = /a/b/c/d\n// /a/b/c + ../d = /a/b/d\n// /a/b/c + ./d/ = /a/b/c/d\n// /a/b/c + ../d/ = /a/b/d\n// /a/b/c + ./ = /a/b/c\n//\n// Absolute paths that start with `/` short circuit the resolution process to the root\n// path.\n//\n// Here are some examples:\n//\n// /a/b/c + /d = /d\n// /a/b/c + /d/ = /d\n// /a/b/c + / = /\n//\n// Non-.-prefixed paths are still treated as relative paths, resolved like `./`\n//\n// Here are some examples:\n//\n// /a/b/c + d = /a/b/c/d\n// /a/b/c + d/ = /a/b/c/d\n// /a/b/c + d/e = /a/b/c/d/e\ninterface ResolvePathOptions {\n base: string\n to: string\n trailingSlash?: 'always' | 'never' | 'preserve'\n cache?: LRUCache<string, string>\n}\n\n/**\n * Resolve a destination path against a base, honoring trailing-slash policy\n * and supporting relative segments (`.`/`..`) and absolute `to` values.\n */\nexport function resolvePath({\n base,\n to,\n trailingSlash = 'never',\n cache,\n}: ResolvePathOptions) {\n const isAbsolute = to.startsWith('/')\n const isBase = !isAbsolute && to === '.'\n\n let key\n if (cache) {\n // `trailingSlash` is static per router, so it doesn't need to be part of the cache key\n key = isAbsolute ? to : isBase ? base : base + '\\0' + to\n const cached = cache.get(key)\n if (cached) return cached\n }\n\n let baseSegments: Array<string>\n if (isBase) {\n baseSegments = base.split('/')\n } else if (isAbsolute) {\n baseSegments = to.split('/')\n } else {\n baseSegments = base.split('/')\n while (baseSegments.length > 1 && last(baseSegments) === '') {\n baseSegments.pop()\n }\n\n const toSegments = to.split('/')\n for (let index = 0, length = toSegments.length; index < length; index++) {\n const value = toSegments[index]!\n if (value === '') {\n if (!index) {\n // Leading slash\n baseSegments = [value]\n } else if (index === length - 1) {\n // Trailing Slash\n baseSegments.push(value)\n } else {\n // ignore inter-slashes\n }\n } else if (value === '..') {\n baseSegments.pop()\n } else if (value === '.') {\n // ignore\n } else {\n baseSegments.push(value)\n }\n }\n }\n\n if (baseSegments.length > 1) {\n if (last(baseSegments) === '') {\n if (trailingSlash === 'never') {\n baseSegments.pop()\n }\n } else if (trailingSlash === 'always') {\n baseSegments.push('')\n }\n }\n\n const result = cleanPath(baseSegments.join('/')) || '/'\n if (key && cache) cache.set(key, result)\n return result\n}\n\n/**\n * Create a pre-compiled decode config from allowed characters.\n * This should be called once at router initialization.\n */\nexport function compileDecodeCharMap(\n pathParamsAllowedCharacters: ReadonlyArray<string>,\n) {\n const charMap = new Map(\n pathParamsAllowedCharacters.map((char) => [encodeURIComponent(char), char]),\n )\n // Escape special regex characters and join with |\n const pattern = Array.from(charMap.keys())\n .map((key) => key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('|')\n const regex = new RegExp(pattern, 'g')\n return (encoded: string) =>\n encoded.replace(regex, (match) => charMap.get(match) ?? match)\n}\n\ninterface InterpolatePathOptions {\n path?: string\n params: Record<string, unknown>\n /**\n * A function that decodes a path parameter value.\n * Obtained from `compileDecodeCharMap(pathParamsAllowedCharacters)`.\n */\n decoder?: (encoded: string) => string\n /**\n * @internal\n * For testing only, in development mode we use the router.isServer value\n */\n server?: boolean\n}\n\ntype InterPolatePathResult = {\n interpolatedPath: string\n usedParams: Record<string, unknown>\n isMissingParams: boolean // true if any params were not available when being looked up in the params object\n}\n\nfunction encodeParam(\n key: string,\n params: InterpolatePathOptions['params'],\n decoder: InterpolatePathOptions['decoder'],\n): any {\n const value = params[key]\n if (typeof value !== 'string') return value\n\n if (key === '_splat') {\n // Early return if value only contains URL-safe characters (performance optimization)\n if (/^[a-zA-Z0-9\\-._~!/]*$/.test(value)) return value\n // the splat/catch-all routes shouldn't have the '/' encoded out\n // Use encodeURIComponent for each segment to properly encode spaces,\n // plus signs, and other special characters that encodeURI leaves unencoded\n return value\n .split('/')\n .map((segment) => encodePathParam(segment, decoder))\n .join('/')\n } else {\n return encodePathParam(value, decoder)\n }\n}\n\n/**\n * Interpolate params and wildcards into a route path template.\n *\n * - Encodes params safely (configurable allowed characters)\n * - Supports `{-$optional}` segments, `{prefix{$id}suffix}` and `{$}` wildcards\n */\nexport function interpolatePath({\n path,\n params,\n decoder,\n // `server` is marked @internal and stripped from .d.ts by `stripInternal`.\n // We avoid destructuring it in the function signature so the emitted\n // declaration doesn't reference a property that no longer exists.\n ...rest\n}: InterpolatePathOptions): InterPolatePathResult {\n // Tracking if any params are missing in the `params` object\n // when interpolating the path\n let isMissingParams = false\n const usedParams: Record<string, unknown> = Object.create(null)\n\n if (!path || path === '/')\n return { interpolatedPath: '/', usedParams, isMissingParams }\n if (!path.includes('$'))\n return { interpolatedPath: path, usedParams, isMissingParams }\n\n if (isServer ?? rest.server) {\n // Fast path for common templates like `/posts/$id` or `/files/$`.\n // Braced segments (`{...}`) are more complex (prefix/suffix/optional) and are\n // handled by the general parser below.\n if (path.indexOf('{') === -1) {\n const length = path.length\n let cursor = 0\n let joined = ''\n\n while (cursor < length) {\n // Skip slashes between segments. '/' code is 47\n while (cursor < length && path.charCodeAt(cursor) === 47) cursor++\n if (cursor >= length) break\n\n const start = cursor\n let end = path.indexOf('/', cursor)\n if (end === -1) end = length\n cursor = end\n\n const part = path.substring(start, end)\n if (!part) continue\n\n // `$id` or `$` (splat). '$' code is 36\n if (part.charCodeAt(0) === 36) {\n if (part.length === 1) {\n const splat = params._splat\n usedParams._splat = splat\n // TODO: Deprecate *\n usedParams['*'] = splat\n\n if (!splat) {\n isMissingParams = true\n continue\n }\n\n const value = encodeParam('_splat', params, decoder)\n joined += '/' + value\n } else {\n const key = part.substring(1)\n if (!isMissingParams && !(key in params)) {\n isMissingParams = true\n }\n usedParams[key] = params[key]\n\n const value = encodeParam(key, params, decoder) ?? 'undefined'\n joined += '/' + value\n }\n } else {\n joined += '/' + part\n }\n }\n\n if (path.endsWith('/')) joined += '/'\n\n const interpolatedPath = joined || '/'\n return { usedParams, interpolatedPath, isMissingParams }\n }\n }\n\n const length = path.length\n let cursor = 0\n let segment\n let joined = ''\n while (cursor < length) {\n const start = cursor\n segment = parseSegment(path, start, segment)\n const end = segment[5]\n cursor = end + 1\n\n if (start === end) continue\n\n const kind = segment[0]\n\n if (kind === SEGMENT_TYPE_PATHNAME) {\n joined += '/' + path.substring(start, end)\n continue\n }\n\n if (kind === SEGMENT_TYPE_WILDCARD) {\n const splat = params._splat\n usedParams._splat = splat\n // TODO: Deprecate *\n usedParams['*'] = splat\n\n const prefix = path.substring(start, segment[1])\n const suffix = path.substring(segment[4], end)\n\n // Check if _splat parameter is missing. _splat could be missing if undefined or an empty string or some other falsy value.\n if (!splat) {\n isMissingParams = true\n // For missing splat parameters, just return the prefix and suffix without the wildcard\n // If there is a prefix or suffix, return them joined, otherwise omit the segment\n if (prefix || suffix) {\n joined += '/' + prefix + suffix\n }\n continue\n }\n\n const value = encodeParam('_splat', params, decoder)\n joined += '/' + prefix + value + suffix\n continue\n }\n\n if (kind === SEGMENT_TYPE_PARAM) {\n const key = path.substring(segment[2], segment[3])\n if (!isMissingParams && !(key in params)) {\n isMissingParams = true\n }\n usedParams[key] = params[key]\n\n const prefix = path.substring(start, segment[1])\n const suffix = path.substring(segment[4], end)\n const value = encodeParam(key, params, decoder) ?? 'undefined'\n joined += '/' + prefix + value + suffix\n continue\n }\n\n if (kind === SEGMENT_TYPE_OPTIONAL_PARAM) {\n const key = path.substring(segment[2], segment[3])\n const valueRaw = params[key]\n\n // Check if optional parameter is missing or undefined\n if (valueRaw == null) continue\n\n usedParams[key] = valueRaw\n\n const prefix = path.substring(start, segment[1])\n const suffix = path.substring(segment[4], end)\n const value = encodeParam(key, params, decoder) ?? ''\n joined += '/' + prefix + value + suffix\n continue\n }\n }\n\n if (path.endsWith('/')) joined += '/'\n\n const interpolatedPath = joined || '/'\n\n return { usedParams, interpolatedPath, isMissingParams }\n}\n\nfunction encodePathParam(\n value: string,\n decoder?: InterpolatePathOptions['decoder'],\n) {\n const encoded = encodeURIComponent(value)\n return decoder?.(encoded) ?? encoded\n}\n"],"mappings":"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"}