@tanstack/router-core 1.171.13 → 1.171.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/utils.js CHANGED
@@ -176,12 +176,21 @@ function findLast(array, predicate) {
176
176
  }
177
177
  }
178
178
  /**
179
- * Remove control characters that can cause open redirect vulnerabilities.
180
- * Characters like \r (CR) and \n (LF) can trick URL parsers into interpreting
181
- * paths like "/\r/evil.com" as "http://evil.com".
179
+ * Re-encode characters that are unsafe in URL paths.
180
+ * Includes ASCII control characters (0x00-0x1F, 0x7F) and a subset of the
181
+ * WHATWG URL "path percent-encode set" (", <, >, `, {, }).
182
+ *
183
+ * Space (0x20) is intentionally excluded — decodeURI decodes %20 to space
184
+ * and the router stores decoded spaces in location.pathname. The existing
185
+ * encodePathLikeUrl already handles re-encoding spaces for outgoing URLs.
186
+ *
187
+ * These characters are decoded by decodeURI but must remain percent-encoded
188
+ * in paths to match how upstream layers (CDNs, edge middleware, browsers)
189
+ * interpret the URL, preventing infinite redirect loops and path mismatches.
182
190
  */
191
+ const PATH_UNSAFE_RE = /[\x00-\x1f\x7f"<>`{}]/g;
183
192
  function sanitizePathSegment(segment) {
184
- return segment.replace(/[\x00-\x1f\x7f]/g, "");
193
+ return segment.replace(PATH_UNSAFE_RE, (ch) => "%" + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0"));
185
194
  }
186
195
  function decodeSegment(segment) {
187
196
  let decoded;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../../src/utils.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport type { RouteIds } from './routeInfo'\nimport type { AnyRouter } from './router'\n\nexport type Awaitable<T> = T | Promise<T>\nexport type NoInfer<T> = [T][T extends any ? 0 : never]\nexport type IsAny<TValue, TYesResult, TNoResult = TValue> = 1 extends 0 & TValue\n ? TYesResult\n : TNoResult\n\nexport type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<\n TValue,\n TKey\n> &\n Required<Pick<TValue, TKey>>\n\nexport type PickRequired<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\nexport type PickOptional<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]: T[K]\n}\n\n// from https://stackoverflow.com/a/76458160\nexport type WithoutEmpty<T> = T extends any ? ({} extends T ? never : T) : never\n\nexport type Expand<T> = T extends object\n ? T extends infer O\n ? O extends Function\n ? O\n : { [K in keyof O]: O[K] }\n : never\n : T\n\nexport type DeepPartial<T> = T extends object\n ? {\n [P in keyof T]?: DeepPartial<T[P]>\n }\n : T\n\nexport type MakeDifferenceOptional<TLeft, TRight> = keyof TLeft &\n keyof TRight extends never\n ? TRight\n : Omit<TRight, keyof TLeft & keyof TRight> & {\n [K in keyof TLeft & keyof TRight]?: TRight[K]\n }\n\n// from https://stackoverflow.com/a/53955431\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type IsUnion<T, U extends T = T> = (\n T extends any ? (U extends T ? false : true) : never\n) extends false\n ? false\n : true\n\nexport type IsNonEmptyObject<T> = T extends object\n ? keyof T extends never\n ? false\n : true\n : false\n\nexport type Assign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : keyof TLeft & keyof TRight extends never\n ? TLeft & TRight\n : Omit<TLeft, keyof TRight> & TRight\n : never\n : never\n\nexport type IntersectAssign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : TRight & TLeft\n : never\n : never\n\nexport type Timeout = ReturnType<typeof setTimeout>\n\nexport type Updater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev?: TPrevious) => TResult)\n\nexport type NonNullableUpdater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev: TPrevious) => TResult)\n\nexport type ExtractObjects<TUnion> = TUnion extends MergeAllPrimitive\n ? never\n : TUnion\n\nexport type PartialMergeAllObject<TUnion> =\n ExtractObjects<TUnion> extends infer TObj\n ? [TObj] extends [never]\n ? never\n : {\n [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any\n ? TKey extends keyof TObj\n ? TObj[TKey]\n : never\n : never\n }\n : never\n\nexport type MergeAllPrimitive =\n | ReadonlyArray<any>\n | number\n | string\n | bigint\n | boolean\n | symbol\n | undefined\n | null\n\nexport type ExtractPrimitives<TUnion> = TUnion extends MergeAllPrimitive\n ? TUnion\n : TUnion extends object\n ? never\n : TUnion\n\nexport type PartialMergeAll<TUnion> =\n | ExtractPrimitives<TUnion>\n | PartialMergeAllObject<TUnion>\n\nexport type Constrain<T, TConstraint, TDefault = TConstraint> =\n | (T extends TConstraint ? T : never)\n | TDefault\n\nexport type ConstrainLiteral<T, TConstraint, TDefault = TConstraint> =\n | (T & TConstraint)\n | TDefault\n\n/**\n * To be added to router types\n */\nexport type UnionToIntersection<T> = (\n T extends any ? (arg: T) => any : never\n) extends (arg: infer T) => any\n ? T\n : never\n\n/**\n * Merges everything in a union into one object.\n * This mapped type is homomorphic which means it preserves stuff! :)\n */\nexport type MergeAllObjects<\n TUnion,\n TIntersected = UnionToIntersection<ExtractObjects<TUnion>>,\n> = [keyof TIntersected] extends [never]\n ? never\n : {\n [TKey in keyof TIntersected]: TUnion extends any\n ? TUnion[TKey & keyof TUnion]\n : never\n }\n\nexport type MergeAll<TUnion> =\n | MergeAllObjects<TUnion>\n | ExtractPrimitives<TUnion>\n\nexport type ValidateJSON<T> = ((...args: Array<any>) => any) extends T\n ? unknown extends T\n ? never\n : 'Function is not serializable'\n : { [K in keyof T]: ValidateJSON<T[K]> }\n\nexport type LooseReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn\n : never\n\nexport type LooseAsyncReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn extends Promise<infer TReturn>\n ? TReturn\n : TReturn\n : never\n\n/**\n * Return the last element of an array.\n * Intended for non-empty arrays used within router internals.\n */\nexport function last<T>(arr: ReadonlyArray<T>) {\n return arr[arr.length - 1]\n}\n\nfunction isFunction(d: any): d is Function {\n return typeof d === 'function'\n}\n\n/**\n * Apply a value-or-updater to a previous value.\n * Accepts either a literal value or a function of the previous value.\n */\nexport function functionalUpdate<TPrevious, TResult = TPrevious>(\n updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>,\n previous: TPrevious,\n): TResult {\n if (isFunction(updater)) {\n return updater(previous)\n }\n\n return updater\n}\n\nexport const hasOwn = Object.prototype.hasOwnProperty\nconst isEnumerable = Object.prototype.propertyIsEnumerable\n\nexport function hasKeys(obj: Record<string, unknown>) {\n for (const key in obj) {\n if (hasOwn.call(obj, key)) return true\n }\n return false\n}\n\nexport const createNull = () => Object.create(null)\nexport const nullReplaceEqualDeep: typeof replaceEqualDeep = (prev, next) =>\n replaceEqualDeep(prev, next, createNull)\n\n/**\n * This function returns `prev` if `_next` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between immutable JSON values for example.\n * Do not use this with signals\n */\nexport function replaceEqualDeep<T>(\n prev: any,\n _next: T,\n _makeObj = () => ({}),\n _depth = 0,\n): T {\n if (isServer) {\n return _next\n }\n if (prev === _next) {\n return prev\n }\n\n if (_depth > 500) return _next\n\n const next = _next as any\n\n const array = isPlainArray(prev) && isPlainArray(next)\n\n if (!array && !(isPlainObject(prev) && isPlainObject(next))) return next\n\n const prevItems = array ? prev : getEnumerableOwnKeys(prev)\n if (!prevItems) return next\n const nextItems = array ? next : getEnumerableOwnKeys(next)\n if (!nextItems) return next\n const prevSize = prevItems.length\n const nextSize = nextItems.length\n const copy: any = array ? new Array(nextSize) : _makeObj()\n\n let equalItems = 0\n\n for (let i = 0; i < nextSize; i++) {\n const key = array ? i : (nextItems[i] as any)\n const p = prev[key]\n const n = next[key]\n\n if (p === n) {\n copy[key] = p\n if (array ? i < prevSize : hasOwn.call(prev, key)) equalItems++\n continue\n }\n\n if (\n p === null ||\n n === null ||\n typeof p !== 'object' ||\n typeof n !== 'object'\n ) {\n copy[key] = n\n continue\n }\n\n const v = replaceEqualDeep(p, n, _makeObj, _depth + 1)\n copy[key] = v\n if (v === p) equalItems++\n }\n\n return prevSize === nextSize && equalItems === prevSize ? prev : copy\n}\n\n/**\n * Equivalent to `Reflect.ownKeys`, but ensures that objects are \"clone-friendly\":\n * will return false if object has any non-enumerable properties.\n *\n * Optimized for the common case where objects have no symbol properties.\n */\nfunction getEnumerableOwnKeys(o: object) {\n const names = Object.getOwnPropertyNames(o)\n\n // Fast path: check all string property names are enumerable\n for (const name of names) {\n if (!isEnumerable.call(o, name)) return false\n }\n\n // Only check symbols if the object has any (most plain objects don't)\n const symbols = Object.getOwnPropertySymbols(o)\n\n // Fast path: no symbols, return names directly (avoids array allocation/concat)\n if (symbols.length === 0) return names\n\n // Slow path: has symbols, need to check and merge\n const keys: Array<string | symbol> = names\n for (const symbol of symbols) {\n if (!isEnumerable.call(o, symbol)) return false\n keys.push(symbol)\n }\n return keys\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\nexport function isPlainObject(o: any) {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has modified constructor\n const ctor = o.constructor\n if (typeof ctor === 'undefined') {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any) {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n\n/**\n * Check if a value is a \"plain\" array (no extra enumerable keys).\n */\nexport function isPlainArray(value: unknown): value is Array<unknown> {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n/**\n * Perform a deep equality check with options for partial comparison and\n * ignoring `undefined` values. Optimized for router state comparisons.\n */\nexport function deepEqual(\n a: any,\n b: any,\n opts?: { partial?: boolean; ignoreUndefined?: boolean },\n): boolean {\n if (a === b) {\n return true\n }\n\n if (typeof a !== typeof b) {\n return false\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0, l = a.length; i < l; i++) {\n if (!deepEqual(a[i], b[i], opts)) return false\n }\n return true\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const ignoreUndefined = opts?.ignoreUndefined ?? true\n\n if (opts?.partial) {\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n if (!deepEqual(a[k], b[k], opts)) return false\n }\n }\n return true\n }\n\n let aCount = 0\n if (!ignoreUndefined) {\n aCount = Object.keys(a).length\n } else {\n for (const k in a) {\n if (a[k] !== undefined) aCount++\n }\n }\n\n let bCount = 0\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n bCount++\n if (bCount > aCount || !deepEqual(a[k], b[k], opts)) return false\n }\n }\n\n return aCount === bCount\n }\n\n return false\n}\n\nexport type StringLiteral<T> = T extends string\n ? string extends T\n ? string\n : T\n : never\n\nexport type ThrowOrOptional<T, TThrow extends boolean> = TThrow extends true\n ? T\n : T | undefined\n\nexport type StrictOrFrom<\n TRouter extends AnyRouter,\n TFrom,\n TStrict extends boolean = true,\n> = TStrict extends false\n ? {\n from?: never\n strict: TStrict\n }\n : {\n from: ConstrainLiteral<TFrom, RouteIds<TRouter['routeTree']>>\n strict?: TStrict\n }\n\nexport type ThrowConstraint<\n TStrict extends boolean,\n TThrow extends boolean,\n> = TStrict extends false ? (TThrow extends true ? never : TThrow) : TThrow\n\nexport type ControlledPromise<T> = Promise<T> & {\n resolve: (value: T) => void\n reject: (value: any) => void\n status: 'pending' | 'resolved' | 'rejected'\n value?: T\n}\n\n/**\n * Create a promise with exposed resolve/reject and status fields.\n * Useful for coordinating async router lifecycle operations.\n */\nexport function createControlledPromise<T>(onResolve?: (value: T) => void) {\n let resolveLoadPromise!: (value: T) => void\n let rejectLoadPromise!: (value: any) => void\n\n const controlledPromise = new Promise<T>((resolve, reject) => {\n resolveLoadPromise = resolve\n rejectLoadPromise = reject\n }) as ControlledPromise<T>\n\n controlledPromise.status = 'pending'\n\n controlledPromise.resolve = (value: T) => {\n controlledPromise.status = 'resolved'\n controlledPromise.value = value\n resolveLoadPromise(value)\n onResolve?.(value)\n }\n\n controlledPromise.reject = (e) => {\n controlledPromise.status = 'rejected'\n rejectLoadPromise(e)\n }\n\n return controlledPromise\n}\n\n/**\n * Heuristically detect dynamic import \"module not found\" errors\n * across major browsers for lazy route component handling.\n */\nexport function isModuleNotFoundError(error: any): boolean {\n // chrome: \"Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // firefox: \"error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // safari: \"Importing a module script failed.\"\n if (typeof error?.message !== 'string') return false\n return (\n error.message.startsWith('Failed to fetch dynamically imported module') ||\n error.message.startsWith('error loading dynamically imported module') ||\n error.message.startsWith('Importing a module script failed')\n )\n}\n\nexport function isPromise<T>(\n value: Promise<Awaited<T>> | T,\n): value is Promise<Awaited<T>> {\n return Boolean(\n value &&\n typeof value === 'object' &&\n typeof (value as Promise<T>).then === 'function',\n )\n}\n\nexport function findLast<T>(\n array: ReadonlyArray<T>,\n predicate: (item: T) => boolean,\n): T | undefined {\n for (let i = array.length - 1; i >= 0; i--) {\n const item = array[i]!\n if (predicate(item)) return item\n }\n return undefined\n}\n\n/**\n * Remove control characters that can cause open redirect vulnerabilities.\n * Characters like \\r (CR) and \\n (LF) can trick URL parsers into interpreting\n * paths like \"/\\r/evil.com\" as \"http://evil.com\".\n */\nfunction sanitizePathSegment(segment: string): string {\n // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F)\n // These include CR (\\r = 0x0D), LF (\\n = 0x0A), and other potentially dangerous characters\n // eslint-disable-next-line no-control-regex\n return segment.replace(/[\\x00-\\x1f\\x7f]/g, '')\n}\n\nfunction decodeSegment(segment: string): string {\n let decoded: string\n try {\n decoded = decodeURI(segment)\n } catch {\n // if the decoding fails, try to decode the various parts leaving the malformed tags in place\n decoded = segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {\n try {\n return decodeURI(match)\n } catch {\n return match\n }\n })\n }\n return sanitizePathSegment(decoded)\n}\n\n/**\n * Default list of URL protocols to allow in links, redirects, and navigation.\n * Any absolute URL protocol not in this list is treated as dangerous by default.\n */\nexport const DEFAULT_PROTOCOL_ALLOWLIST = [\n // Standard web navigation\n 'http:',\n 'https:',\n\n // Common browser-safe actions\n 'mailto:',\n 'tel:',\n]\n\n/**\n * Check if a URL string uses a protocol that is not in the allowlist.\n * Returns true for blocked protocols like javascript:, blob:, data:, etc.\n *\n * The URL constructor correctly normalizes:\n * - Mixed case (JavaScript: → javascript:)\n * - Whitespace/control characters (java\\nscript: → javascript:)\n * - Leading whitespace\n *\n * For relative URLs (no protocol), returns false (safe).\n *\n * @param url - The URL string to check\n * @param allowlist - Set of protocols to allow\n * @returns true if the URL uses a protocol that is not allowed\n */\nexport function isDangerousProtocol(\n url: string,\n allowlist: Set<string>,\n): boolean {\n if (!url) return false\n\n try {\n // Use the URL constructor - it correctly normalizes protocols\n // per WHATWG URL spec, handling all bypass attempts automatically\n const parsed = new URL(url)\n return !allowlist.has(parsed.protocol)\n } catch {\n // URL constructor throws for relative URLs (no protocol)\n // These are safe - they can't execute scripts\n return false\n }\n}\n\n// This utility is based on https://github.com/zertosh/htmlescape\n// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\nconst HTML_ESCAPE_LOOKUP: { [match: string]: string } = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\nconst HTML_ESCAPE_REGEX = /[&><\\u2028\\u2029]/g\n\n/**\n * Escape HTML special characters in a string to prevent XSS attacks\n * when embedding strings in script tags during SSR.\n *\n * This is essential for preventing XSS vulnerabilities when user-controlled\n * content is embedded in inline scripts.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]!)\n}\n\nexport function decodePath(path: string) {\n if (!path) return { path, handledProtocolRelativeURL: false }\n\n // Fast path: most paths are already decoded and safe.\n // Only fall back to the slower scan/regex path when we see a '%' (encoded),\n // a backslash (explicitly handled), a control character, or a protocol-relative\n // prefix which needs collapsing.\n // eslint-disable-next-line no-control-regex\n if (!/[%\\\\\\x00-\\x1f\\x7f]/.test(path) && !path.startsWith('//')) {\n return { path, handledProtocolRelativeURL: false }\n }\n\n const re = /%25|%5C/gi\n let cursor = 0\n let result = ''\n let match\n while (null !== (match = re.exec(path))) {\n result += decodeSegment(path.slice(cursor, match.index)) + match[0]\n cursor = re.lastIndex\n }\n result = result + decodeSegment(cursor ? path.slice(cursor) : path)\n\n // Prevent open redirect via protocol-relative URLs (e.g. \"//evil.com\")\n // After sanitizing control characters, paths like \"/\\r/evil.com\" become \"//evil.com\"\n // Collapse leading double slashes to a single slash\n let handledProtocolRelativeURL = false\n if (result.startsWith('//')) {\n handledProtocolRelativeURL = true\n result = '/' + result.replace(/^\\/+/, '')\n }\n\n return { path: result, handledProtocolRelativeURL }\n}\n\n/**\n * Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing.\n *\n * This function encodes:\n * - Whitespace characters (spaces → %20, tabs → %09, etc.)\n * - Non-ASCII/Unicode characters (emojis, accented characters, etc.)\n *\n * It preserves:\n * - Already percent-encoded sequences (won't double-encode %2F, %25, etc.)\n * - ASCII special characters valid in URL paths (@, $, &, +, etc.)\n * - Forward slashes as path separators\n *\n * Used to generate proper href values for SSR without constructing URL objects.\n *\n * @example\n * encodePathLikeUrl('/path/file name.pdf') // '/path/file%20name.pdf'\n * encodePathLikeUrl('/path/日本語') // '/path/%E6%97%A5%E6%9C%AC%E8%AA%9E'\n * encodePathLikeUrl('/path/already%20encoded') // '/path/already%20encoded' (preserved)\n */\nexport function encodePathLikeUrl(path: string): string {\n // Encode whitespace and non-ASCII characters that browsers encode in URLs\n\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n if (!/\\s|[^\\u0000-\\u007F]/.test(path)) return path\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n return path.replace(/\\s|[^\\u0000-\\u007F]/gu, encodeURIComponent)\n}\n\n/**\n * Builds the dev-mode CSS styles URL for route-scoped CSS collection.\n * Used by HeadContent components in all framework implementations to construct\n * the URL for the `/@tanstack-start/styles.css` endpoint.\n *\n * @param basepath - The router's basepath (may or may not have leading slash)\n * @param routeIds - Array of matched route IDs to include in the CSS collection\n * @returns The full URL path for the dev styles CSS endpoint\n */\nexport function buildDevStylesUrl(\n basepath: string,\n routeIds: Array<string>,\n): string {\n // Trim all leading and trailing slashes from basepath\n const trimmedBasepath = basepath.replace(/^\\/+|\\/+$/g, '')\n // Build normalized basepath: empty string for root, or '/path' for non-root\n const normalizedBasepath = trimmedBasepath === '' ? '' : `/${trimmedBasepath}`\n return `${normalizedBasepath}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`\n}\n\nexport function arraysEqual<T>(a: Array<T>, b: Array<T>) {\n if (a === b) return true\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../../src/utils.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport type { RouteIds } from './routeInfo'\nimport type { AnyRouter } from './router'\n\nexport type Awaitable<T> = T | Promise<T>\nexport type NoInfer<T> = [T][T extends any ? 0 : never]\nexport type IsAny<TValue, TYesResult, TNoResult = TValue> = 1 extends 0 & TValue\n ? TYesResult\n : TNoResult\n\nexport type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<\n TValue,\n TKey\n> &\n Required<Pick<TValue, TKey>>\n\nexport type PickRequired<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\nexport type PickOptional<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]: T[K]\n}\n\n// from https://stackoverflow.com/a/76458160\nexport type WithoutEmpty<T> = T extends any ? ({} extends T ? never : T) : never\n\nexport type Expand<T> = T extends object\n ? T extends infer O\n ? O extends Function\n ? O\n : { [K in keyof O]: O[K] }\n : never\n : T\n\nexport type DeepPartial<T> = T extends object\n ? {\n [P in keyof T]?: DeepPartial<T[P]>\n }\n : T\n\nexport type MakeDifferenceOptional<TLeft, TRight> = keyof TLeft &\n keyof TRight extends never\n ? TRight\n : Omit<TRight, keyof TLeft & keyof TRight> & {\n [K in keyof TLeft & keyof TRight]?: TRight[K]\n }\n\n// from https://stackoverflow.com/a/53955431\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type IsUnion<T, U extends T = T> = (\n T extends any ? (U extends T ? false : true) : never\n) extends false\n ? false\n : true\n\nexport type IsNonEmptyObject<T> = T extends object\n ? keyof T extends never\n ? false\n : true\n : false\n\nexport type Assign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : keyof TLeft & keyof TRight extends never\n ? TLeft & TRight\n : Omit<TLeft, keyof TRight> & TRight\n : never\n : never\n\nexport type IntersectAssign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : TRight & TLeft\n : never\n : never\n\nexport type Timeout = ReturnType<typeof setTimeout>\n\nexport type Updater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev?: TPrevious) => TResult)\n\nexport type NonNullableUpdater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev: TPrevious) => TResult)\n\nexport type ExtractObjects<TUnion> = TUnion extends MergeAllPrimitive\n ? never\n : TUnion\n\nexport type PartialMergeAllObject<TUnion> =\n ExtractObjects<TUnion> extends infer TObj\n ? [TObj] extends [never]\n ? never\n : {\n [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any\n ? TKey extends keyof TObj\n ? TObj[TKey]\n : never\n : never\n }\n : never\n\nexport type MergeAllPrimitive =\n | ReadonlyArray<any>\n | number\n | string\n | bigint\n | boolean\n | symbol\n | undefined\n | null\n\nexport type ExtractPrimitives<TUnion> = TUnion extends MergeAllPrimitive\n ? TUnion\n : TUnion extends object\n ? never\n : TUnion\n\nexport type PartialMergeAll<TUnion> =\n | ExtractPrimitives<TUnion>\n | PartialMergeAllObject<TUnion>\n\nexport type Constrain<T, TConstraint, TDefault = TConstraint> =\n | (T extends TConstraint ? T : never)\n | TDefault\n\nexport type ConstrainLiteral<T, TConstraint, TDefault = TConstraint> =\n | (T & TConstraint)\n | TDefault\n\n/**\n * To be added to router types\n */\nexport type UnionToIntersection<T> = (\n T extends any ? (arg: T) => any : never\n) extends (arg: infer T) => any\n ? T\n : never\n\n/**\n * Merges everything in a union into one object.\n * This mapped type is homomorphic which means it preserves stuff! :)\n */\nexport type MergeAllObjects<\n TUnion,\n TIntersected = UnionToIntersection<ExtractObjects<TUnion>>,\n> = [keyof TIntersected] extends [never]\n ? never\n : {\n [TKey in keyof TIntersected]: TUnion extends any\n ? TUnion[TKey & keyof TUnion]\n : never\n }\n\nexport type MergeAll<TUnion> =\n | MergeAllObjects<TUnion>\n | ExtractPrimitives<TUnion>\n\nexport type ValidateJSON<T> = ((...args: Array<any>) => any) extends T\n ? unknown extends T\n ? never\n : 'Function is not serializable'\n : { [K in keyof T]: ValidateJSON<T[K]> }\n\nexport type LooseReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn\n : never\n\nexport type LooseAsyncReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn extends Promise<infer TReturn>\n ? TReturn\n : TReturn\n : never\n\n/**\n * Return the last element of an array.\n * Intended for non-empty arrays used within router internals.\n */\nexport function last<T>(arr: ReadonlyArray<T>) {\n return arr[arr.length - 1]\n}\n\nfunction isFunction(d: any): d is Function {\n return typeof d === 'function'\n}\n\n/**\n * Apply a value-or-updater to a previous value.\n * Accepts either a literal value or a function of the previous value.\n */\nexport function functionalUpdate<TPrevious, TResult = TPrevious>(\n updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>,\n previous: TPrevious,\n): TResult {\n if (isFunction(updater)) {\n return updater(previous)\n }\n\n return updater\n}\n\nexport const hasOwn = Object.prototype.hasOwnProperty\nconst isEnumerable = Object.prototype.propertyIsEnumerable\n\nexport function hasKeys(obj: Record<string, unknown>) {\n for (const key in obj) {\n if (hasOwn.call(obj, key)) return true\n }\n return false\n}\n\nexport const createNull = () => Object.create(null)\nexport const nullReplaceEqualDeep: typeof replaceEqualDeep = (prev, next) =>\n replaceEqualDeep(prev, next, createNull)\n\n/**\n * This function returns `prev` if `_next` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between immutable JSON values for example.\n * Do not use this with signals\n */\nexport function replaceEqualDeep<T>(\n prev: any,\n _next: T,\n _makeObj = () => ({}),\n _depth = 0,\n): T {\n if (isServer) {\n return _next\n }\n if (prev === _next) {\n return prev\n }\n\n if (_depth > 500) return _next\n\n const next = _next as any\n\n const array = isPlainArray(prev) && isPlainArray(next)\n\n if (!array && !(isPlainObject(prev) && isPlainObject(next))) return next\n\n const prevItems = array ? prev : getEnumerableOwnKeys(prev)\n if (!prevItems) return next\n const nextItems = array ? next : getEnumerableOwnKeys(next)\n if (!nextItems) return next\n const prevSize = prevItems.length\n const nextSize = nextItems.length\n const copy: any = array ? new Array(nextSize) : _makeObj()\n\n let equalItems = 0\n\n for (let i = 0; i < nextSize; i++) {\n const key = array ? i : (nextItems[i] as any)\n const p = prev[key]\n const n = next[key]\n\n if (p === n) {\n copy[key] = p\n if (array ? i < prevSize : hasOwn.call(prev, key)) equalItems++\n continue\n }\n\n if (\n p === null ||\n n === null ||\n typeof p !== 'object' ||\n typeof n !== 'object'\n ) {\n copy[key] = n\n continue\n }\n\n const v = replaceEqualDeep(p, n, _makeObj, _depth + 1)\n copy[key] = v\n if (v === p) equalItems++\n }\n\n return prevSize === nextSize && equalItems === prevSize ? prev : copy\n}\n\n/**\n * Equivalent to `Reflect.ownKeys`, but ensures that objects are \"clone-friendly\":\n * will return false if object has any non-enumerable properties.\n *\n * Optimized for the common case where objects have no symbol properties.\n */\nfunction getEnumerableOwnKeys(o: object) {\n const names = Object.getOwnPropertyNames(o)\n\n // Fast path: check all string property names are enumerable\n for (const name of names) {\n if (!isEnumerable.call(o, name)) return false\n }\n\n // Only check symbols if the object has any (most plain objects don't)\n const symbols = Object.getOwnPropertySymbols(o)\n\n // Fast path: no symbols, return names directly (avoids array allocation/concat)\n if (symbols.length === 0) return names\n\n // Slow path: has symbols, need to check and merge\n const keys: Array<string | symbol> = names\n for (const symbol of symbols) {\n if (!isEnumerable.call(o, symbol)) return false\n keys.push(symbol)\n }\n return keys\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\nexport function isPlainObject(o: any) {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has modified constructor\n const ctor = o.constructor\n if (typeof ctor === 'undefined') {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any) {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n\n/**\n * Check if a value is a \"plain\" array (no extra enumerable keys).\n */\nexport function isPlainArray(value: unknown): value is Array<unknown> {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n/**\n * Perform a deep equality check with options for partial comparison and\n * ignoring `undefined` values. Optimized for router state comparisons.\n */\nexport function deepEqual(\n a: any,\n b: any,\n opts?: { partial?: boolean; ignoreUndefined?: boolean },\n): boolean {\n if (a === b) {\n return true\n }\n\n if (typeof a !== typeof b) {\n return false\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0, l = a.length; i < l; i++) {\n if (!deepEqual(a[i], b[i], opts)) return false\n }\n return true\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const ignoreUndefined = opts?.ignoreUndefined ?? true\n\n if (opts?.partial) {\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n if (!deepEqual(a[k], b[k], opts)) return false\n }\n }\n return true\n }\n\n let aCount = 0\n if (!ignoreUndefined) {\n aCount = Object.keys(a).length\n } else {\n for (const k in a) {\n if (a[k] !== undefined) aCount++\n }\n }\n\n let bCount = 0\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n bCount++\n if (bCount > aCount || !deepEqual(a[k], b[k], opts)) return false\n }\n }\n\n return aCount === bCount\n }\n\n return false\n}\n\nexport type StringLiteral<T> = T extends string\n ? string extends T\n ? string\n : T\n : never\n\nexport type ThrowOrOptional<T, TThrow extends boolean> = TThrow extends true\n ? T\n : T | undefined\n\nexport type StrictOrFrom<\n TRouter extends AnyRouter,\n TFrom,\n TStrict extends boolean = true,\n> = TStrict extends false\n ? {\n from?: never\n strict: TStrict\n }\n : {\n from: ConstrainLiteral<TFrom, RouteIds<TRouter['routeTree']>>\n strict?: TStrict\n }\n\nexport type ThrowConstraint<\n TStrict extends boolean,\n TThrow extends boolean,\n> = TStrict extends false ? (TThrow extends true ? never : TThrow) : TThrow\n\nexport type ControlledPromise<T> = Promise<T> & {\n resolve: (value: T) => void\n reject: (value: any) => void\n status: 'pending' | 'resolved' | 'rejected'\n value?: T\n}\n\n/**\n * Create a promise with exposed resolve/reject and status fields.\n * Useful for coordinating async router lifecycle operations.\n */\nexport function createControlledPromise<T>(onResolve?: (value: T) => void) {\n let resolveLoadPromise!: (value: T) => void\n let rejectLoadPromise!: (value: any) => void\n\n const controlledPromise = new Promise<T>((resolve, reject) => {\n resolveLoadPromise = resolve\n rejectLoadPromise = reject\n }) as ControlledPromise<T>\n\n controlledPromise.status = 'pending'\n\n controlledPromise.resolve = (value: T) => {\n controlledPromise.status = 'resolved'\n controlledPromise.value = value\n resolveLoadPromise(value)\n onResolve?.(value)\n }\n\n controlledPromise.reject = (e) => {\n controlledPromise.status = 'rejected'\n rejectLoadPromise(e)\n }\n\n return controlledPromise\n}\n\n/**\n * Heuristically detect dynamic import \"module not found\" errors\n * across major browsers for lazy route component handling.\n */\nexport function isModuleNotFoundError(error: any): boolean {\n // chrome: \"Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // firefox: \"error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // safari: \"Importing a module script failed.\"\n if (typeof error?.message !== 'string') return false\n return (\n error.message.startsWith('Failed to fetch dynamically imported module') ||\n error.message.startsWith('error loading dynamically imported module') ||\n error.message.startsWith('Importing a module script failed')\n )\n}\n\nexport function isPromise<T>(\n value: Promise<Awaited<T>> | T,\n): value is Promise<Awaited<T>> {\n return Boolean(\n value &&\n typeof value === 'object' &&\n typeof (value as Promise<T>).then === 'function',\n )\n}\n\nexport function findLast<T>(\n array: ReadonlyArray<T>,\n predicate: (item: T) => boolean,\n): T | undefined {\n for (let i = array.length - 1; i >= 0; i--) {\n const item = array[i]!\n if (predicate(item)) return item\n }\n return undefined\n}\n\n/**\n * Re-encode characters that are unsafe in URL paths.\n * Includes ASCII control characters (0x00-0x1F, 0x7F) and a subset of the\n * WHATWG URL \"path percent-encode set\" (\", <, >, `, {, }).\n *\n * Space (0x20) is intentionally excluded — decodeURI decodes %20 to space\n * and the router stores decoded spaces in location.pathname. The existing\n * encodePathLikeUrl already handles re-encoding spaces for outgoing URLs.\n *\n * These characters are decoded by decodeURI but must remain percent-encoded\n * in paths to match how upstream layers (CDNs, edge middleware, browsers)\n * interpret the URL, preventing infinite redirect loops and path mismatches.\n */\n// eslint-disable-next-line no-control-regex\nconst PATH_UNSAFE_RE = /[\\x00-\\x1f\\x7f\"<>`{}]/g\n\nfunction sanitizePathSegment(segment: string): string {\n return segment.replace(\n PATH_UNSAFE_RE,\n (ch) => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'),\n )\n}\n\nfunction decodeSegment(segment: string): string {\n let decoded: string\n try {\n decoded = decodeURI(segment)\n } catch {\n // if the decoding fails, try to decode the various parts leaving the malformed tags in place\n decoded = segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {\n try {\n return decodeURI(match)\n } catch {\n return match\n }\n })\n }\n return sanitizePathSegment(decoded)\n}\n\n/**\n * Default list of URL protocols to allow in links, redirects, and navigation.\n * Any absolute URL protocol not in this list is treated as dangerous by default.\n */\nexport const DEFAULT_PROTOCOL_ALLOWLIST = [\n // Standard web navigation\n 'http:',\n 'https:',\n\n // Common browser-safe actions\n 'mailto:',\n 'tel:',\n]\n\n/**\n * Check if a URL string uses a protocol that is not in the allowlist.\n * Returns true for blocked protocols like javascript:, blob:, data:, etc.\n *\n * The URL constructor correctly normalizes:\n * - Mixed case (JavaScript: → javascript:)\n * - Whitespace/control characters (java\\nscript: → javascript:)\n * - Leading whitespace\n *\n * For relative URLs (no protocol), returns false (safe).\n *\n * @param url - The URL string to check\n * @param allowlist - Set of protocols to allow\n * @returns true if the URL uses a protocol that is not allowed\n */\nexport function isDangerousProtocol(\n url: string,\n allowlist: Set<string>,\n): boolean {\n if (!url) return false\n\n try {\n // Use the URL constructor - it correctly normalizes protocols\n // per WHATWG URL spec, handling all bypass attempts automatically\n const parsed = new URL(url)\n return !allowlist.has(parsed.protocol)\n } catch {\n // URL constructor throws for relative URLs (no protocol)\n // These are safe - they can't execute scripts\n return false\n }\n}\n\n// This utility is based on https://github.com/zertosh/htmlescape\n// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\nconst HTML_ESCAPE_LOOKUP: { [match: string]: string } = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\nconst HTML_ESCAPE_REGEX = /[&><\\u2028\\u2029]/g\n\n/**\n * Escape HTML special characters in a string to prevent XSS attacks\n * when embedding strings in script tags during SSR.\n *\n * This is essential for preventing XSS vulnerabilities when user-controlled\n * content is embedded in inline scripts.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]!)\n}\n\nexport function decodePath(path: string) {\n if (!path) return { path, handledProtocolRelativeURL: false }\n\n // Fast path: most paths are already decoded and safe.\n // Only fall back to the slower scan/regex path when we see a '%' (encoded),\n // a backslash (explicitly handled), a control character, or a protocol-relative\n // prefix which needs collapsing.\n // eslint-disable-next-line no-control-regex\n if (!/[%\\\\\\x00-\\x1f\\x7f]/.test(path) && !path.startsWith('//')) {\n return { path, handledProtocolRelativeURL: false }\n }\n\n const re = /%25|%5C/gi\n let cursor = 0\n let result = ''\n let match\n while (null !== (match = re.exec(path))) {\n result += decodeSegment(path.slice(cursor, match.index)) + match[0]\n cursor = re.lastIndex\n }\n result = result + decodeSegment(cursor ? path.slice(cursor) : path)\n\n // Prevent open redirect via protocol-relative URLs (e.g. \"//evil.com\")\n // This is defense-in-depth: since control characters are no longer decoded,\n // paths like \"/%0d/evil.com\" can no longer become \"//evil.com\". But we keep\n // this check to guard against other edge cases.\n let handledProtocolRelativeURL = false\n if (result.startsWith('//')) {\n handledProtocolRelativeURL = true\n result = '/' + result.replace(/^\\/+/, '')\n }\n\n return { path: result, handledProtocolRelativeURL }\n}\n\n/**\n * Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing.\n *\n * This function encodes:\n * - Whitespace characters (spaces → %20, tabs → %09, etc.)\n * - Non-ASCII/Unicode characters (emojis, accented characters, etc.)\n *\n * It preserves:\n * - Already percent-encoded sequences (won't double-encode %2F, %25, etc.)\n * - ASCII special characters valid in URL paths (@, $, &, +, etc.)\n * - Forward slashes as path separators\n *\n * Used to generate proper href values for SSR without constructing URL objects.\n *\n * @example\n * encodePathLikeUrl('/path/file name.pdf') // '/path/file%20name.pdf'\n * encodePathLikeUrl('/path/日本語') // '/path/%E6%97%A5%E6%9C%AC%E8%AA%9E'\n * encodePathLikeUrl('/path/already%20encoded') // '/path/already%20encoded' (preserved)\n */\nexport function encodePathLikeUrl(path: string): string {\n // Encode whitespace and non-ASCII characters that browsers encode in URLs\n\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n if (!/\\s|[^\\u0000-\\u007F]/.test(path)) return path\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n return path.replace(/\\s|[^\\u0000-\\u007F]/gu, encodeURIComponent)\n}\n\n/**\n * Builds the dev-mode CSS styles URL for route-scoped CSS collection.\n * Used by HeadContent components in all framework implementations to construct\n * the URL for the `/@tanstack-start/styles.css` endpoint.\n *\n * @param basepath - The router's basepath (may or may not have leading slash)\n * @param routeIds - Array of matched route IDs to include in the CSS collection\n * @returns The full URL path for the dev styles CSS endpoint\n */\nexport function buildDevStylesUrl(\n basepath: string,\n routeIds: Array<string>,\n): string {\n // Trim all leading and trailing slashes from basepath\n const trimmedBasepath = basepath.replace(/^\\/+|\\/+$/g, '')\n // Build normalized basepath: empty string for root, or '/path' for non-root\n const normalizedBasepath = trimmedBasepath === '' ? '' : `/${trimmedBasepath}`\n return `${normalizedBasepath}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`\n}\n\nexport function arraysEqual<T>(a: Array<T>, b: Array<T>) {\n if (a === b) return true\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.171.13",
3
+ "version": "1.171.14",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
package/src/router.ts CHANGED
@@ -909,6 +909,18 @@ export const locationHistoryActions = new WeakMap<
909
909
  HistoryAction
910
910
  >()
911
911
 
912
+ type LightweightRouteMatchResult = {
913
+ matchedRoutes: ReadonlyArray<AnyRoute>
914
+ fullPath: string
915
+ search: Record<string, unknown>
916
+ params: Record<string, unknown>
917
+ }
918
+
919
+ type LightweightRouteMatchCacheEntry = [
920
+ lastMatchId: string | undefined,
921
+ result: LightweightRouteMatchResult,
922
+ ]
923
+
912
924
  export type CreateRouterFn = <
913
925
  TRouteTree extends AnyRoute,
914
926
  TTrailingSlashOption extends TrailingSlashOption = 'never',
@@ -1002,6 +1014,10 @@ export class RouterCore<
1002
1014
  processedTree!: ProcessedTree<TRouteTree, any, any>
1003
1015
  resolvePathCache!: LRUCache<string, string>
1004
1016
  private routeBranchCache = new WeakMap<AnyRoute, ReadonlyArray<AnyRoute>>()
1017
+ private lightweightCache = new WeakMap<
1018
+ ParsedLocation,
1019
+ LightweightRouteMatchCacheEntry
1020
+ >()
1005
1021
  isServer!: boolean
1006
1022
  pathParamsDecoder?: (encoded: string) => string
1007
1023
  protocolAllowlist!: Set<string>
@@ -1719,12 +1735,15 @@ export class RouterCore<
1719
1735
  * Only computes fullPath, accumulated search, and params - skipping expensive
1720
1736
  * operations like AbortController, ControlledPromise, loaderDeps, and full match objects.
1721
1737
  */
1722
- private matchRoutesLightweight(location: ParsedLocation): {
1723
- matchedRoutes: ReadonlyArray<AnyRoute>
1724
- fullPath: string
1725
- search: Record<string, unknown>
1726
- params: Record<string, unknown>
1727
- } {
1738
+ private matchRoutesLightweight(
1739
+ location: ParsedLocation,
1740
+ ): LightweightRouteMatchResult {
1741
+ const lastStateMatchId = last(this.stores.matchesId.get())
1742
+ const cached = this.lightweightCache.get(location)
1743
+ if (cached && cached[0] === lastStateMatchId) {
1744
+ return cached[1]
1745
+ }
1746
+
1728
1747
  const { matchedRoutes, routeParams } = this.getMatchedRoutes(
1729
1748
  location.pathname,
1730
1749
  )
@@ -1753,7 +1772,6 @@ export class RouterCore<
1753
1772
  }
1754
1773
 
1755
1774
  // Determine params: reuse from state if possible, otherwise parse
1756
- const lastStateMatchId = last(this.stores.matchesId.get())
1757
1775
  const lastStateMatch =
1758
1776
  lastStateMatchId && this.stores.matchStores.get(lastStateMatchId)?.get()
1759
1777
  const canReuseParams =
@@ -1780,12 +1798,14 @@ export class RouterCore<
1780
1798
  params = strictParams
1781
1799
  }
1782
1800
 
1783
- return {
1801
+ const result = {
1784
1802
  matchedRoutes,
1785
1803
  fullPath: lastRoute.fullPath,
1786
1804
  search: accumulatedSearch,
1787
1805
  params,
1788
1806
  }
1807
+ this.lightweightCache.set(location, [lastStateMatchId, result])
1808
+ return result
1789
1809
  }
1790
1810
 
1791
1811
  cancelMatch = (id: string) => {
@@ -2294,7 +2314,7 @@ export class RouterCore<
2294
2314
 
2295
2315
  // Clear pending location after commit starts
2296
2316
  // We do this on next microtask to allow synchronous navigate calls to chain
2297
- Promise.resolve().then(() => {
2317
+ queueMicrotask(() => {
2298
2318
  if (this.pendingBuiltLocation === location) {
2299
2319
  this.pendingBuiltLocation = undefined
2300
2320
  }
@@ -2354,7 +2374,7 @@ export class RouterCore<
2354
2374
  `Blocked navigation to dangerous protocol: ${reloadHref}`,
2355
2375
  )
2356
2376
  }
2357
- return Promise.resolve()
2377
+ return
2358
2378
  }
2359
2379
 
2360
2380
  // Check blockers for external URLs unless ignoreBlocker is true
@@ -2370,7 +2390,7 @@ export class RouterCore<
2370
2390
  action: 'PUSH',
2371
2391
  })
2372
2392
  if (shouldBlock) {
2373
- return Promise.resolve()
2393
+ return
2374
2394
  }
2375
2395
  }
2376
2396
  }
@@ -2381,7 +2401,7 @@ export class RouterCore<
2381
2401
  } else {
2382
2402
  window.location.href = reloadHref
2383
2403
  }
2384
- return Promise.resolve()
2404
+ return
2385
2405
  }
2386
2406
 
2387
2407
  return this.buildAndCommitLocation({
package/src/utils.ts CHANGED
@@ -523,15 +523,26 @@ export function findLast<T>(
523
523
  }
524
524
 
525
525
  /**
526
- * Remove control characters that can cause open redirect vulnerabilities.
527
- * Characters like \r (CR) and \n (LF) can trick URL parsers into interpreting
528
- * paths like "/\r/evil.com" as "http://evil.com".
526
+ * Re-encode characters that are unsafe in URL paths.
527
+ * Includes ASCII control characters (0x00-0x1F, 0x7F) and a subset of the
528
+ * WHATWG URL "path percent-encode set" (", <, >, `, {, }).
529
+ *
530
+ * Space (0x20) is intentionally excluded — decodeURI decodes %20 to space
531
+ * and the router stores decoded spaces in location.pathname. The existing
532
+ * encodePathLikeUrl already handles re-encoding spaces for outgoing URLs.
533
+ *
534
+ * These characters are decoded by decodeURI but must remain percent-encoded
535
+ * in paths to match how upstream layers (CDNs, edge middleware, browsers)
536
+ * interpret the URL, preventing infinite redirect loops and path mismatches.
529
537
  */
538
+ // eslint-disable-next-line no-control-regex
539
+ const PATH_UNSAFE_RE = /[\x00-\x1f\x7f"<>`{}]/g
540
+
530
541
  function sanitizePathSegment(segment: string): string {
531
- // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F)
532
- // These include CR (\r = 0x0D), LF (\n = 0x0A), and other potentially dangerous characters
533
- // eslint-disable-next-line no-control-regex
534
- return segment.replace(/[\x00-\x1f\x7f]/g, '')
542
+ return segment.replace(
543
+ PATH_UNSAFE_RE,
544
+ (ch) => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'),
545
+ )
535
546
  }
536
547
 
537
548
  function decodeSegment(segment: string): string {
@@ -644,8 +655,9 @@ export function decodePath(path: string) {
644
655
  result = result + decodeSegment(cursor ? path.slice(cursor) : path)
645
656
 
646
657
  // Prevent open redirect via protocol-relative URLs (e.g. "//evil.com")
647
- // After sanitizing control characters, paths like "/\r/evil.com" become "//evil.com"
648
- // Collapse leading double slashes to a single slash
658
+ // This is defense-in-depth: since control characters are no longer decoded,
659
+ // paths like "/%0d/evil.com" can no longer become "//evil.com". But we keep
660
+ // this check to guard against other edge cases.
649
661
  let handledProtocolRelativeURL = false
650
662
  if (result.startsWith('//')) {
651
663
  handledProtocolRelativeURL = true