Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
69 changes: 0 additions & 69 deletions packages/router-core/src/path.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isServer } from '@tanstack/router-core/isServer'
import { last } from './utils'
import {
SEGMENT_TYPE_OPTIONAL_PARAM,
Expand Down Expand Up @@ -199,11 +198,6 @@ interface InterpolatePathOptions {
* Obtained from `compileDecodeCharMap(pathParamsAllowedCharacters)`.
*/
decoder?: (encoded: string) => string
/**
* @internal
* For testing only, in development mode we use the router.isServer value
*/
server?: boolean
}

type InterPolatePathResult = {
Expand Down Expand Up @@ -245,10 +239,6 @@ export function interpolatePath({
path,
params,
decoder,
// `server` is marked @internal and stripped from .d.ts by `stripInternal`.
// We avoid destructuring it in the function signature so the emitted
// declaration doesn't reference a property that no longer exists.
...rest
}: InterpolatePathOptions): InterPolatePathResult {
// Tracking if any params are missing in the `params` object
// when interpolating the path
Expand All @@ -260,65 +250,6 @@ export function interpolatePath({
if (!path.includes('$'))
return { interpolatedPath: path, usedParams, isMissingParams }

if (isServer ?? rest.server) {
// Fast path for common templates like `/posts/$id` or `/files/$`.
// Braced segments (`{...}`) are more complex (prefix/suffix/optional) and are
// handled by the general parser below.
if (path.indexOf('{') === -1) {
const length = path.length
let cursor = 0
let joined = ''

while (cursor < length) {
// Skip slashes between segments. '/' code is 47
while (cursor < length && path.charCodeAt(cursor) === 47) cursor++
if (cursor >= length) break

const start = cursor
let end = path.indexOf('/', cursor)
if (end === -1) end = length
cursor = end

const part = path.substring(start, end)
if (!part) continue

// `$id` or `$` (splat). '$' code is 36
if (part.charCodeAt(0) === 36) {
if (part.length === 1) {
const splat = params._splat
usedParams._splat = splat
// TODO: Deprecate *
usedParams['*'] = splat

if (!splat) {
isMissingParams = true
continue
}

const value = encodeParam('_splat', params, decoder)
joined += '/' + value
} else {
const key = part.substring(1)
if (!isMissingParams && !(key in params)) {
isMissingParams = true
}
usedParams[key] = params[key]

const value = encodeParam(key, params, decoder) ?? 'undefined'
joined += '/' + value
}
} else {
joined += '/' + part
}
}

if (path.endsWith('/')) joined += '/'

const interpolatedPath = joined || '/'
return { usedParams, interpolatedPath, isMissingParams }
}
}

const length = path.length
let cursor = 0
let segment
Expand Down
2 changes: 0 additions & 2 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1620,7 +1620,6 @@ export class RouterCore<
path: route.fullPath,
params: rawParams,
decoder: this.pathParamsDecoder,
server: this.isServer,
})

// Seed planning from the accepted same-ID cache generation first, then
Expand Down Expand Up @@ -1958,7 +1957,6 @@ export class RouterCore<
path: nextTo,
params: nextParams,
decoder: this.pathParamsDecoder,
server: this.isServer,
}).interpolatedPath,
).path

Expand Down
40 changes: 40 additions & 0 deletions packages/router-core/tests/path.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { bench, describe, expect } from 'vitest'
import { interpolatePath } from '../src/path'

const iterations = 10_000
const plainOptions = {
path: '/organizations/$organizationId/projects/$projectId',
params: { organizationId: 'tanstack', projectId: 'router' },
}
const bracedOptions = {
path: '/organizations/prefix{$organizationId}/projects/{-$projectId}',
params: { organizationId: 'tanstack', projectId: 'router' },
}
let benchmarkSink = 0

expect(interpolatePath(plainOptions).interpolatedPath).toBe(
'/organizations/tanstack/projects/router',
)
expect(interpolatePath(bracedOptions).interpolatedPath).toBe(
'/organizations/prefixtanstack/projects/router',
)

function interpolateBatch(options: Parameters<typeof interpolatePath>[0]) {
let size = 0
for (let index = 0; index < iterations; index++) {
size += interpolatePath(options).interpolatedPath.length
}
benchmarkSink = size
}

describe('path interpolation', () => {
bench('plain route templates', () => {
interpolateBatch(plainOptions)
})

bench('braced route templates', () => {
interpolateBatch(bracedOptions)
})
})

void benchmarkSink
94 changes: 84 additions & 10 deletions packages/router-core/tests/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,8 @@ describe('resolvePath', () => {
})
})

describe.each([{ server: true }, { server: false }])(
'interpolatePath (server: $server)',
({ server }) => {
describe('interpolatePath', () => {
describe('shared behavior', () => {
describe('regular usage', () => {
it.each([
{
Expand Down Expand Up @@ -373,13 +372,18 @@ describe.each([{ server: true }, { server: false }])(
params: { _splat: 'sean/cassiere' },
result: '/users/sean/cassiere',
},
{
name: 'should stop interpolating after a non-terminal splat',
path: '/users/$/ignored',
params: { _splat: 'sean/cassiere' },
result: '/users/sean/cassiere',
},
])('$name', ({ path, params, decoder, result }) => {
expect(
interpolatePath({
path,
params,
decoder,
server,
}).interpolatedPath,
).toBe(result)
})
Expand Down Expand Up @@ -414,7 +418,6 @@ describe.each([{ server: true }, { server: false }])(
interpolatePath({
path,
params,
server,
}).interpolatedPath,
).toBe(result)
},
Expand Down Expand Up @@ -458,7 +461,6 @@ describe.each([{ server: true }, { server: false }])(
interpolatePath({
path: to,
params,
server,
}).interpolatedPath,
).toBe(result)
})
Expand Down Expand Up @@ -525,7 +527,6 @@ describe.each([{ server: true }, { server: false }])(
interpolatePath({
path,
params,
server,
}).interpolatedPath,
).toBe(result)
})
Expand Down Expand Up @@ -574,7 +575,6 @@ describe.each([{ server: true }, { server: false }])(
interpolatePath({
path: to,
params,
server,
}).interpolatedPath,
).toBe(result)
})
Expand Down Expand Up @@ -622,11 +622,16 @@ describe.each([{ server: true }, { server: false }])(
},
expectedResult: '/hello',
},
{
name: 'non-terminal splat route',
path: '/hello/$/ignored',
params: {},
expectedResult: '/hello',
},
])('$name', ({ path, params, expectedResult }) => {
const result = interpolatePath({
path,
params,
server,
})
expect(result.interpolatedPath).toBe(expectedResult)
expect(result.isMissingParams).toBe(true)
Expand All @@ -653,12 +658,81 @@ describe.each([{ server: true }, { server: false }])(
const interpolatedNextTo = interpolatePath({
path: nextTo,
params: nextParams,
server,
}).interpolatedPath
expect(interpolatedNextTo).toBe(`/splat${tail}`)
},
)
})
})
})

it.each([
{
name: 'multiple params',
path: '//organizations/$organizationId//projects/$projectId/',
params: { organizationId: 'tanstack', projectId: 'router' },
expectedPath: '/organizations/tanstack/projects/router/',
expectedParams: { organizationId: 'tanstack', projectId: 'router' },
isMissingParams: false,
},
{
name: 'missing param',
path: '/organizations/$organizationId/projects/$projectId',
params: { organizationId: 'tanstack' },
expectedPath: '/organizations/tanstack/projects/undefined',
expectedParams: { organizationId: 'tanstack', projectId: undefined },
isMissingParams: true,
},
{
name: 'terminal splat',
path: '/files/$',
params: { _splat: 'docs/router guide.pdf' },
expectedPath: '/files/docs/router%20guide.pdf',
expectedParams: {
_splat: 'docs/router guide.pdf',
'*': 'docs/router guide.pdf',
},
isMissingParams: false,
},
{
name: 'non-terminal splat',
path: '/files/$/ignored/$param',
params: { _splat: 'docs/router', param: 'unused' },
expectedPath: '/files/docs/router',
expectedParams: { _splat: 'docs/router', '*': 'docs/router' },
isMissingParams: false,
},
{
name: 'missing non-terminal splat',
path: '/files/$/ignored',
params: {},
expectedPath: '/files',
expectedParams: { _splat: undefined, '*': undefined },
isMissingParams: true,
},
{
name: 'custom decoder',
path: '/users/$id',
params: { id: 'tanner+linsley@example.com' },
decoder: compileDecodeCharMap(['+', '@']),
expectedPath: '/users/tanner+linsley@example.com',
expectedParams: { id: 'tanner+linsley@example.com' },
isMissingParams: false,
},
])(
'plain-template interpolation: $name',
({
path,
params,
decoder,
expectedPath,
expectedParams,
isMissingParams,
}) => {
const result = interpolatePath({ path, params, decoder })
expect(result.interpolatedPath).toBe(expectedPath)
expect(result.usedParams).toEqual(expectedParams)
expect(result.isMissingParams).toBe(isMissingParams)
},
)

Expand Down
Loading