Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/router-core/src/new-process-route-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ function extractParams<T extends RouteLike>(
]
}

function buildRouteBranch<T extends RouteLike>(route: T) {
export function buildRouteBranch<T extends RouteLike>(route: T) {
const list = [route]
while (route.parentRoute) {
route = route.parentRoute as T
Expand Down
49 changes: 42 additions & 7 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
replaceEqualDeep,
} from './utils'
import {
buildRouteBranch,
findFlatMatch,
findRouteMatch,
findSingleMatch,
Expand Down Expand Up @@ -1775,17 +1776,47 @@ export class RouterCore<
functionalUpdate(dest.params as any, fromParams),
)

// Apply stringify BEFORE interpolating to ensure route matching works with skipRouteOnParseError.params: true
// We look up the route by its template path and apply stringify functions from the route branch before interpolation
const trimmedNextTo = trimPathRight(nextTo)
const targetRoute = this.routesByPath[trimmedNextTo]
let prestringifiedParams: Record<string, unknown> | null = null
if (targetRoute && Object.keys(nextParams).length > 0) {
const routeBranch = buildRouteBranch<AnyRoute>(targetRoute)
if (
routeBranch.some(
(route) => route.options.skipRouteOnParseError?.params,
)
) {
prestringifiedParams = { ...nextParams }
for (const route of routeBranch) {
const fn =
route.options.params?.stringify ?? route.options.stringifyParams
if (fn) {
Object.assign(prestringifiedParams!, fn(prestringifiedParams))
}
}
}
}

// Interpolate the path first to get the actual resolved path, then match against that
const interpolatedNextTo = interpolatePath({
path: nextTo,
params: nextParams,
decoder: this.pathParamsDecoder,
}).interpolatedPath
Comment thread
Sheraff marked this conversation as resolved.

// Use lightweight getMatchedRoutes instead of matchRoutesInternal
// This avoids creating full match objects (AbortController, ControlledPromise, etc.)
// which are expensive and not needed for buildLocation
const destMatchResult = this.getMatchedRoutes(interpolatedNextTo)
let destRoutes = destMatchResult.matchedRoutes
if (
!destMatchResult.foundRoute ||
destMatchResult.foundRoute.fullPath !== trimmedNextTo
) {
prestringifiedParams = null
}

// Compute globalNotFoundRouteId using the same logic as matchRoutesInternal
const isGlobalNotFound = destMatchResult.foundRoute
Expand All @@ -1806,12 +1837,14 @@ export class RouterCore<
}

// If there are any params, we need to stringify them
if (Object.keys(nextParams).length > 0) {
let stringifiedParams = prestringifiedParams
if (!stringifiedParams && Object.keys(nextParams).length > 0) {
stringifiedParams = nextParams
for (const route of destRoutes) {
const fn =
route.options.params?.stringify ?? route.options.stringifyParams
if (fn) {
Object.assign(nextParams, fn(nextParams))
Object.assign(stringifiedParams!, fn(stringifiedParams))
}
}
}
Expand All @@ -1821,11 +1854,13 @@ export class RouterCore<
// This preserves the original parameter syntax including optional parameters
nextTo
: decodePath(
interpolatePath({
path: nextTo,
params: nextParams,
decoder: this.pathParamsDecoder,
}).interpolatedPath,
prestringifiedParams
? interpolatedNextTo
: interpolatePath({
path: nextTo,
params: stringifiedParams ?? {},
decoder: this.pathParamsDecoder,
}).interpolatedPath,
)

// Resolve the next search
Expand Down
102 changes: 102 additions & 0 deletions packages/router-core/tests/build-location.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, expect, test } from 'vitest'
import { createMemoryHistory } from '@tanstack/history'
import { BaseRootRoute, BaseRoute, RouterCore } from '../src'

describe('buildLocation', () => {
describe('#6490 - skipRouteOnParseError respects params.stringify', () => {
test('skipRouteOnParseError is true', () => {
const rootRoute = new BaseRootRoute({})
const langRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/$lang',
skipRouteOnParseError: {
params: true,
},
params: {
parse: (rawParams) => {
if (rawParams.lang === 'en') {
return { lang: 'en-US' }
}

if (rawParams.lang === 'pl') {
return { lang: 'pl-PL' }
}

throw new Error('Invalid language')
},
stringify: (params) => {
if (params.lang === 'en-US') {
return { lang: 'en' }
}

if (params.lang === 'pl-PL') {
return { lang: 'pl' }
}

return params
},
},
})
const routeTree = rootRoute.addChildren([langRoute])

const router = new RouterCore({
routeTree,
history: createMemoryHistory(),
})

const location = router.buildLocation({
to: '/$lang',
params: { lang: 'en-US' },
})

expect(location.pathname).toBe('/en')
})
test('skipRouteOnParseError is false', () => {
const rootRoute = new BaseRootRoute({})
const langRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/$lang',
skipRouteOnParseError: {
params: false,
},
params: {
parse: (rawParams) => {
if (rawParams.lang === 'en') {
return { lang: 'en-US' }
}

if (rawParams.lang === 'pl') {
return { lang: 'pl-PL' }
}

throw new Error('Invalid language')
},
stringify: (params) => {
if (params.lang === 'en-US') {
return { lang: 'en' }
}

if (params.lang === 'pl-PL') {
return { lang: 'pl' }
}

return params
},
},
})
const routeTree = rootRoute.addChildren([langRoute])

const router = new RouterCore({
routeTree,
history: createMemoryHistory(),
})

const location = router.buildLocation({
to: '/$lang',
params: { lang: 'en-US' },
})

expect(location.pathname).toBe('/en')
})
})
})
Loading