From 0271a0cb73022a2de077e3878bd9da069b059c4b Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 25 Jan 2026 22:14:51 +0100 Subject: [PATCH 1/4] refactor(router-core): parseLocation fast path w/ no rewrites --- packages/router-core/src/router.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index e783bb7f0a2..354a9c61c35 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1252,9 +1252,29 @@ export class RouterCore< previousLocation, ) => { const parse = ({ + pathname, + search, + hash, href, state, }: HistoryLocation): ParsedLocation> => { + // Fast path: no rewrite configured, avoid URL construction + if (!this.rewrite) { + const parsedSearch = this.options.parseSearch(search) + const searchStr = this.options.stringifySearch(parsedSearch) + + return { + href: pathname + searchStr + hash, + publicHref: href, + pathname: decodePath(pathname), + external: false, + searchStr, + search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any, + hash: decodePath(hash.split('#').reverse()[0] ?? ''), + state: replaceEqualDeep(previousLocation?.state, state), + } + } + // Before we do any processing, we need to allow rewrites to modify the URL // build up the full URL by combining the href from history with the router's origin const fullUrl = new URL(href, this.origin) @@ -1273,7 +1293,7 @@ export class RouterCore< href: fullPath, publicHref: href, pathname: decodePath(url.pathname), - external: !!this.rewrite && url.origin !== this.origin, + external: url.origin !== this.origin, searchStr, search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any, hash: decodePath(url.hash.split('#').reverse()[0] ?? ''), From 5c9226036ae9d0d9652f6e5cb5bc0f69e8fe3862 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:16:27 +0000 Subject: [PATCH 2/4] ci: apply automated fixes --- packages/router-core/src/router.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 354a9c61c35..7abcb5288f4 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1269,7 +1269,10 @@ export class RouterCore< pathname: decodePath(pathname), external: false, searchStr, - search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any, + search: replaceEqualDeep( + previousLocation?.search, + parsedSearch, + ) as any, hash: decodePath(hash.split('#').reverse()[0] ?? ''), state: replaceEqualDeep(previousLocation?.state, state), } From 0853d9fb55b6de24c0f9b0171d9bfb886b95815d Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 25 Jan 2026 22:59:50 +0100 Subject: [PATCH 3/4] fix --- packages/react-router/tests/router.test.tsx | 6 - packages/router-core/src/router.ts | 174 ++++++++++---------- 2 files changed, 88 insertions(+), 92 deletions(-) diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx index 585c641297a..052dafa3774 100644 --- a/packages/react-router/tests/router.test.tsx +++ b/packages/react-router/tests/router.test.tsx @@ -706,12 +706,6 @@ describe('encoding/decoding: wildcard routes/params', () => { await router.load() - expect( - router.state.location.href.endsWith( - '/framework/react/guide/file-based-routing%20tanstack', - ), - ).toBe(true) - expect(router.state.location.href).toBe( '/framework/react/guide/file-based-routing%20tanstack', ) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 7abcb5288f4..2139a325dda 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -133,21 +133,21 @@ export type RegisteredConfigType = TRegister extends { config: infer TConfig } ? TConfig extends { - '~types': infer TTypes - } - ? TKey extends keyof TTypes - ? TTypes[TKey] - : unknown - : unknown + '~types': infer TTypes + } + ? TKey extends keyof TTypes + ? TTypes[TKey] + : unknown + : unknown : unknown export type DefaultRemountDepsFn = ( opts: MakeRemountDepsOptionsUnion, ) => any -export interface DefaultRouterOptionsExtensions {} +export interface DefaultRouterOptionsExtensions { } -export interface RouterOptionsExtensions extends DefaultRouterOptionsExtensions {} +export interface RouterOptionsExtensions extends DefaultRouterOptionsExtensions { } export type SSROption = boolean | 'data-only' @@ -437,8 +437,8 @@ export interface RouterOptions< * @default false */ scrollRestoration?: - | boolean - | ((opts: { location: ParsedLocation }) => boolean) + | boolean + | ((opts: { location: ParsedLocation }) => boolean) /** * A function that will be called to get the key for the scroll restoration cache. @@ -604,12 +604,12 @@ export type InferRouterContext = export type RouterContextOptions = AnyContext extends InferRouterContext - ? { - context?: InferRouterContext - } - : { - context: InferRouterContext - } + ? { + context?: InferRouterContext + } + : { + context: InferRouterContext + } export type RouterConstructorOptions< TRouteTree extends AnyRoute, @@ -795,14 +795,14 @@ export type AnyRouter = RouterCore export interface ViewTransitionOptions { types: - | Array - | ((locationChangeInfo: { - fromLocation?: ParsedLocation - toLocation: ParsedLocation - pathChanged: boolean - hrefChanged: boolean - hashChanged: boolean - }) => Array | false) + | Array + | ((locationChangeInfo: { + fromLocation?: ParsedLocation + toLocation: ParsedLocation + pathChanged: boolean + hrefChanged: boolean + hashChanged: boolean + }) => Array | false) } // TODO where is this used? can we remove this? @@ -818,7 +818,7 @@ export function defaultSerializeError(err: unknown) { } if (process.env.NODE_ENV === 'development') { - ;(obj as any).stack = err.stack + ; (obj as any).stack = err.stack } return obj @@ -865,12 +865,12 @@ export type CreateRouterFn = < options: undefined extends number ? 'strictNullChecks must be enabled in tsconfig.json' : RouterConstructorOptions< - TRouteTree, - TTrailingSlashOption, - TDefaultStructuralSharingOption, - TRouterHistory, - TDehydrated - >, + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated + >, ) => RouterCore< TRouteTree, TTrailingSlashOption, @@ -883,10 +883,10 @@ declare global { // eslint-disable-next-line no-var var __TSR_CACHE__: | { - routeTree: AnyRoute - processRouteTreeResult: ProcessRouteTreeResult - resolvePathCache: LRUCache - } + routeTree: AnyRoute + processRouteTreeResult: ProcessRouteTreeResult + resolvePathCache: LRUCache + } | undefined } @@ -1258,8 +1258,10 @@ export class RouterCore< href, state, }: HistoryLocation): ParsedLocation> => { - // Fast path: no rewrite configured, avoid URL construction - if (!this.rewrite) { + // Fast path: no rewrite configured and pathname doesn't need encoding + // Characters that need encoding: space, high unicode, control chars + // eslint-disable-next-line no-control-regex + if (!this.rewrite && !/[ \x00-\x1f\x7f\u0080-\uffff]/.test(pathname)) { const parsedSearch = this.options.parseSearch(search) const searchStr = this.options.stringifySearch(parsedSearch) @@ -1273,7 +1275,7 @@ export class RouterCore< previousLocation?.search, parsedSearch, ) as any, - hash: decodePath(hash.split('#').reverse()[0] ?? ''), + hash: decodePath(hash.slice(1)), state: replaceEqualDeep(previousLocation?.state, state), } } @@ -1296,10 +1298,10 @@ export class RouterCore< href: fullPath, publicHref: href, pathname: decodePath(url.pathname), - external: url.origin !== this.origin, + external: !!this.rewrite && url.origin !== this.origin, searchStr, search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any, - hash: decodePath(url.hash.split('#').reverse()[0] ?? ''), + hash: decodePath(url.hash.slice(1)), state: replaceEqualDeep(previousLocation?.state, state), } } @@ -1372,7 +1374,7 @@ export class RouterCore< foundRoute ? foundRoute.path !== '/' && routeParams['**'] : // Or if we didn't find a route and we have left over path - trimPathRight(next.pathname) + trimPathRight(next.pathname) ) { // If the user has defined an (old) 404 route, use it if (this.options.notFoundRoute) { @@ -1530,9 +1532,9 @@ export class RouterCore< } else { const status = route.options.loader || - route.options.beforeLoad || - route.lazyFn || - routeNeedsPreload(route) + route.options.beforeLoad || + route.lazyFn || + routeNeedsPreload(route) ? 'pending' : 'success' @@ -1820,9 +1822,9 @@ export class RouterCore< : (dest.params ?? true) === true ? fromParams : Object.assign( - fromParams, - functionalUpdate(dest.params as any, fromParams), - ) + fromParams, + functionalUpdate(dest.params as any, fromParams), + ) // Interpolate the path first to get the actual resolved path, then match against that const interpolatedNextTo = interpolatePath({ @@ -1841,7 +1843,7 @@ export class RouterCore< // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal const isGlobalNotFound = destMatchResult.foundRoute ? destMatchResult.foundRoute.path !== '/' && - destMatchResult.routeParams['**'] + destMatchResult.routeParams['**'] : trimPathRight(interpolatedNextTo) if (isGlobalNotFound && this.options.notFoundRoute) { @@ -1863,18 +1865,18 @@ export class RouterCore< const nextPathname = opts.leaveParams ? // Use the original template path for interpolation - // This preserves the original parameter syntax including optional parameters - nextTo + // This preserves the original parameter syntax including optional parameters + nextTo : decodePath( - !changedParams - ? interpolatedNextTo - : interpolatePath({ - path: nextTo, - params: nextParams, - decoder: this.pathParamsDecoder, - server: this.isServer, - }).interpolatedPath, - ) + !changedParams + ? interpolatedNextTo + : interpolatePath({ + path: nextTo, + params: nextParams, + decoder: this.pathParamsDecoder, + server: this.isServer, + }).interpolatedPath, + ) // Resolve the next search let nextSearch = fromSearch @@ -2059,7 +2061,7 @@ export class RouterCore< '__hashScrollIntoViewOptions', ] as const ignoredProps.forEach((prop) => { - ;(next.state as any)[prop] = this.latestLocation.state[prop] + ; (next.state as any)[prop] = this.latestLocation.state[prop] }) const isEqual = deepEqual(next.state, this.latestLocation.state) ignoredProps.forEach((prop) => { @@ -2216,7 +2218,7 @@ export class RouterCore< try { new URL(`${href}`) hrefIsUrl = true - } catch {} + } catch { } } if (hrefIsUrl && !reloadDocument) { @@ -2423,20 +2425,20 @@ export class RouterCore< this.clearExpiredCache() }) - // - ;( - [ - [exitingMatches, 'onLeave'], - [enteringMatches, 'onEnter'], - [stayingMatches, 'onStay'], - ] as const - ).forEach(([matches, hook]) => { - matches.forEach((match) => { - this.looseRoutesById[match.routeId]!.options[hook]?.( - match, - ) + // + ; ( + [ + [exitingMatches, 'onLeave'], + [enteringMatches, 'onEnter'], + [stayingMatches, 'onStay'], + ] as const + ).forEach(([matches, hook]) => { + matches.forEach((match) => { + this.looseRoutesById[match.routeId]!.options[hook]?.( + match, + ) + }) }) - }) }) }) }, @@ -2533,11 +2535,11 @@ export class RouterCore< const resolvedViewTransitionTypes = typeof shouldViewTransition.types === 'function' ? shouldViewTransition.types( - getLocationChangeInfo({ - resolvedLocation: prevLocation, - location: next, - }), - ) + getLocationChangeInfo({ + resolvedLocation: prevLocation, + location: next, + }), + ) : shouldViewTransition.types if (resolvedViewTransitionTypes === false) { @@ -2612,8 +2614,8 @@ export class RouterCore< ...d, invalid: true, ...(opts?.forcePending || - d.status === 'error' || - d.status === 'notFound' + d.status === 'error' || + d.status === 'notFound' ? ({ status: 'pending', error: undefined } as const) : undefined), } @@ -2799,9 +2801,9 @@ export class RouterCore< ...location, to: location.to ? this.resolvePathWithBase( - (location.from || '') as string, - location.to as string, - ) + (location.from || '') as string, + location.to as string, + ) : undefined, params: location.params || {}, leaveParams: true, @@ -2860,10 +2862,10 @@ export class RouterCore< } /** Error thrown when search parameter validation fails. */ -export class SearchParamError extends Error {} +export class SearchParamError extends Error { } /** Error thrown when path parameter parsing/validation fails. */ -export class PathParamError extends Error {} +export class PathParamError extends Error { } const normalize = (str: string) => str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str From b542bc13358be812dfc9444eb4e29c401252d6e0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 22:01:13 +0000 Subject: [PATCH 4/4] ci: apply automated fixes --- packages/router-core/src/router.ts | 162 ++++++++++++++--------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 2139a325dda..9fbb6744bb8 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -133,21 +133,21 @@ export type RegisteredConfigType = TRegister extends { config: infer TConfig } ? TConfig extends { - '~types': infer TTypes - } - ? TKey extends keyof TTypes - ? TTypes[TKey] - : unknown - : unknown + '~types': infer TTypes + } + ? TKey extends keyof TTypes + ? TTypes[TKey] + : unknown + : unknown : unknown export type DefaultRemountDepsFn = ( opts: MakeRemountDepsOptionsUnion, ) => any -export interface DefaultRouterOptionsExtensions { } +export interface DefaultRouterOptionsExtensions {} -export interface RouterOptionsExtensions extends DefaultRouterOptionsExtensions { } +export interface RouterOptionsExtensions extends DefaultRouterOptionsExtensions {} export type SSROption = boolean | 'data-only' @@ -437,8 +437,8 @@ export interface RouterOptions< * @default false */ scrollRestoration?: - | boolean - | ((opts: { location: ParsedLocation }) => boolean) + | boolean + | ((opts: { location: ParsedLocation }) => boolean) /** * A function that will be called to get the key for the scroll restoration cache. @@ -604,12 +604,12 @@ export type InferRouterContext = export type RouterContextOptions = AnyContext extends InferRouterContext - ? { - context?: InferRouterContext - } - : { - context: InferRouterContext - } + ? { + context?: InferRouterContext + } + : { + context: InferRouterContext + } export type RouterConstructorOptions< TRouteTree extends AnyRoute, @@ -795,14 +795,14 @@ export type AnyRouter = RouterCore export interface ViewTransitionOptions { types: - | Array - | ((locationChangeInfo: { - fromLocation?: ParsedLocation - toLocation: ParsedLocation - pathChanged: boolean - hrefChanged: boolean - hashChanged: boolean - }) => Array | false) + | Array + | ((locationChangeInfo: { + fromLocation?: ParsedLocation + toLocation: ParsedLocation + pathChanged: boolean + hrefChanged: boolean + hashChanged: boolean + }) => Array | false) } // TODO where is this used? can we remove this? @@ -818,7 +818,7 @@ export function defaultSerializeError(err: unknown) { } if (process.env.NODE_ENV === 'development') { - ; (obj as any).stack = err.stack + ;(obj as any).stack = err.stack } return obj @@ -865,12 +865,12 @@ export type CreateRouterFn = < options: undefined extends number ? 'strictNullChecks must be enabled in tsconfig.json' : RouterConstructorOptions< - TRouteTree, - TTrailingSlashOption, - TDefaultStructuralSharingOption, - TRouterHistory, - TDehydrated - >, + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated + >, ) => RouterCore< TRouteTree, TTrailingSlashOption, @@ -883,10 +883,10 @@ declare global { // eslint-disable-next-line no-var var __TSR_CACHE__: | { - routeTree: AnyRoute - processRouteTreeResult: ProcessRouteTreeResult - resolvePathCache: LRUCache - } + routeTree: AnyRoute + processRouteTreeResult: ProcessRouteTreeResult + resolvePathCache: LRUCache + } | undefined } @@ -1374,7 +1374,7 @@ export class RouterCore< foundRoute ? foundRoute.path !== '/' && routeParams['**'] : // Or if we didn't find a route and we have left over path - trimPathRight(next.pathname) + trimPathRight(next.pathname) ) { // If the user has defined an (old) 404 route, use it if (this.options.notFoundRoute) { @@ -1532,9 +1532,9 @@ export class RouterCore< } else { const status = route.options.loader || - route.options.beforeLoad || - route.lazyFn || - routeNeedsPreload(route) + route.options.beforeLoad || + route.lazyFn || + routeNeedsPreload(route) ? 'pending' : 'success' @@ -1822,9 +1822,9 @@ export class RouterCore< : (dest.params ?? true) === true ? fromParams : Object.assign( - fromParams, - functionalUpdate(dest.params as any, fromParams), - ) + fromParams, + functionalUpdate(dest.params as any, fromParams), + ) // Interpolate the path first to get the actual resolved path, then match against that const interpolatedNextTo = interpolatePath({ @@ -1843,7 +1843,7 @@ export class RouterCore< // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal const isGlobalNotFound = destMatchResult.foundRoute ? destMatchResult.foundRoute.path !== '/' && - destMatchResult.routeParams['**'] + destMatchResult.routeParams['**'] : trimPathRight(interpolatedNextTo) if (isGlobalNotFound && this.options.notFoundRoute) { @@ -1865,18 +1865,18 @@ export class RouterCore< const nextPathname = opts.leaveParams ? // Use the original template path for interpolation - // This preserves the original parameter syntax including optional parameters - nextTo + // This preserves the original parameter syntax including optional parameters + nextTo : decodePath( - !changedParams - ? interpolatedNextTo - : interpolatePath({ - path: nextTo, - params: nextParams, - decoder: this.pathParamsDecoder, - server: this.isServer, - }).interpolatedPath, - ) + !changedParams + ? interpolatedNextTo + : interpolatePath({ + path: nextTo, + params: nextParams, + decoder: this.pathParamsDecoder, + server: this.isServer, + }).interpolatedPath, + ) // Resolve the next search let nextSearch = fromSearch @@ -2061,7 +2061,7 @@ export class RouterCore< '__hashScrollIntoViewOptions', ] as const ignoredProps.forEach((prop) => { - ; (next.state as any)[prop] = this.latestLocation.state[prop] + ;(next.state as any)[prop] = this.latestLocation.state[prop] }) const isEqual = deepEqual(next.state, this.latestLocation.state) ignoredProps.forEach((prop) => { @@ -2218,7 +2218,7 @@ export class RouterCore< try { new URL(`${href}`) hrefIsUrl = true - } catch { } + } catch {} } if (hrefIsUrl && !reloadDocument) { @@ -2425,20 +2425,20 @@ export class RouterCore< this.clearExpiredCache() }) - // - ; ( - [ - [exitingMatches, 'onLeave'], - [enteringMatches, 'onEnter'], - [stayingMatches, 'onStay'], - ] as const - ).forEach(([matches, hook]) => { - matches.forEach((match) => { - this.looseRoutesById[match.routeId]!.options[hook]?.( - match, - ) - }) + // + ;( + [ + [exitingMatches, 'onLeave'], + [enteringMatches, 'onEnter'], + [stayingMatches, 'onStay'], + ] as const + ).forEach(([matches, hook]) => { + matches.forEach((match) => { + this.looseRoutesById[match.routeId]!.options[hook]?.( + match, + ) }) + }) }) }) }, @@ -2535,11 +2535,11 @@ export class RouterCore< const resolvedViewTransitionTypes = typeof shouldViewTransition.types === 'function' ? shouldViewTransition.types( - getLocationChangeInfo({ - resolvedLocation: prevLocation, - location: next, - }), - ) + getLocationChangeInfo({ + resolvedLocation: prevLocation, + location: next, + }), + ) : shouldViewTransition.types if (resolvedViewTransitionTypes === false) { @@ -2614,8 +2614,8 @@ export class RouterCore< ...d, invalid: true, ...(opts?.forcePending || - d.status === 'error' || - d.status === 'notFound' + d.status === 'error' || + d.status === 'notFound' ? ({ status: 'pending', error: undefined } as const) : undefined), } @@ -2801,9 +2801,9 @@ export class RouterCore< ...location, to: location.to ? this.resolvePathWithBase( - (location.from || '') as string, - location.to as string, - ) + (location.from || '') as string, + location.to as string, + ) : undefined, params: location.params || {}, leaveParams: true, @@ -2862,10 +2862,10 @@ export class RouterCore< } /** Error thrown when search parameter validation fails. */ -export class SearchParamError extends Error { } +export class SearchParamError extends Error {} /** Error thrown when path parameter parsing/validation fails. */ -export class PathParamError extends Error { } +export class PathParamError extends Error {} const normalize = (str: string) => str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str