Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/quiet-zeros-stay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/router-core': patch
---

Treat `0` and `false` as provided `_splat` values when interpolating paths. Only `undefined`, `null`, and `''` now omit a splat segment, matching how other path params are stringified.
18 changes: 9 additions & 9 deletions benchmarks/client-nav/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ construction. `scenarios/mount` includes construction but also rendering and
loading. To isolate initialization, use the core construction benchmark:

```bash
TSR_LINK_PERF=1 CI=1 NX_DAEMON=false pnpm nx run @tanstack/router-core:test:unit --outputStyle=stream --skipRemoteCache -- bench tests/route-tree-construction.bench.ts --run --testNamePattern=1000.routes --outputJson /tmp/route-tree-construction.json
CI=1 NX_DAEMON=false pnpm nx run @tanstack/router-core:test:unit --outputStyle=stream --skipRemoteCache -- bench tests/route-tree-construction.bench.ts --run --testNamePattern=1000.routes --outputJson /tmp/route-tree-construction.json
```

It covers 100, 1,000, and 10,000 static, dynamic, nested, or mixed routes.
Expand All @@ -138,16 +138,16 @@ router or repeatedly initialized tree is not a construction baseline.

`link-performance/` contains additional client-navigation and SSR workloads for
focused Link work. They are **not included** in the regular client-nav/SSR
aggregate projects or their CodSpeed build dependencies. Benchmark discovery
also requires `TSR_LINK_PERF=1`; without it, no extended benchmark files or app
bundles are imported.
aggregate projects or their CodSpeed build dependencies; they only run through
the dedicated `@benchmarks/react-link-performance` targets below. Use `-t` to
narrow a run to specific cases.

```bash
TSR_LINK_PERF=1 CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:client --outputStyle=stream --skipRemoteCache -- --run
TSR_LINK_PERF=1 CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:ssr --outputStyle=stream --skipRemoteCache -- --run
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:client --outputStyle=stream --skipRemoteCache -- --run
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:ssr --outputStyle=stream --skipRemoteCache -- --run

# Select a feature and save the normal Vitest JSON report.
TSR_LINK_PERF=1 CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:client --outputStyle=stream --skipRemoteCache -- --run -t "updater|optional|splat" --outputJson /tmp/link-perf.json
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:client --outputStyle=stream --skipRemoteCache -- --run -t "updater|optional|splat" --outputJson /tmp/link-perf.json
```

The cases cover repeated versus unique destination params, updater functions,
Expand Down Expand Up @@ -203,12 +203,12 @@ harness loader does not transpile their emitted JavaScript a second time.
```bash
# Build the baseline using these same benchmark sources in its own checkout.
# --baseline points to that checkout's link-performance/dist directory.
TSR_LINK_PERF=1 pnpm nx run @benchmarks/react-link-performance:test:perf:stable -- \
pnpm nx run @benchmarks/react-link-performance:test:perf:stable -- \
--baseline /path/to/baseline/benchmarks/client-nav/link-performance/dist \
--outputJson /tmp/paired-links.json

# Narrow a comparison, or increase independent process repetitions.
TSR_LINK_PERF=1 pnpm nx run @benchmarks/react-link-performance:test:perf:stable -- \
pnpm nx run @benchmarks/react-link-performance:test:perf:stable -- \
--baseline /path/to/baseline/benchmarks/client-nav/link-performance/dist \
--mode ssr -t "middleware|unique-params" --repeats 6 \
--outputJson /tmp/paired-links-ssr.json
Expand Down
83 changes: 34 additions & 49 deletions benchmarks/client-nav/link-performance/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,40 @@ import { createLinkPerformanceConfig } from './config'

afterEach(() => vi.unstubAllEnvs())

describe.each(['client', 'ssr'] as const)(
'%s opt-in configuration',
(target) => {
test.each([undefined, '', '0', 'false'])(
'does not discover benchmarks with TSR_LINK_PERF=%s',
(value) => {
vi.stubEnv('TSR_LINK_PERF', value)
const config = createLinkPerformanceConfig(target)
expect(config.test?.benchmark?.include).toEqual([])
expect(config.test?.passWithNoTests).toBe(true)
},
)

test('discovers only the requested suite when explicitly enabled', () => {
vi.stubEnv('TSR_LINK_PERF', '1')
const config = createLinkPerformanceConfig(target)
expect(config.test?.benchmark?.include).toEqual([`${target}.bench.ts`])
expect(config.test?.passWithNoTests).toBe(false)
})
describe.each(['client', 'ssr'] as const)('%s configuration', (target) => {
test('discovers only the requested suite', () => {
const config = createLinkPerformanceConfig(target)
expect(config.test?.benchmark?.include).toEqual([`${target}.bench.ts`])
})

test('builds production code for the correct environment', () => {
const config = createLinkPerformanceConfig(target)
expect(config.define?.['process.env.NODE_ENV']).toBe('"production"')
expect(config.resolve?.conditions).toContain(
target === 'ssr' ? 'node' : 'browser',
)
expect(config.build?.ssr).toBe(target === 'ssr')
expect(config.build?.outDir).toBe(`./dist/${target}`)
expect(config.build?.rolldownOptions?.platform).toBe('node')
expect(config.build?.rolldownOptions?.external).toEqual([
'node:module',
'module',
/^react(?:\/|$)/,
/^react-dom(?:\/|$)/,
])
})
test('builds production code for the correct environment', () => {
const config = createLinkPerformanceConfig(target)
expect(config.define?.['process.env.NODE_ENV']).toBe('"production"')
expect(config.resolve?.conditions).toContain(
target === 'ssr' ? 'node' : 'browser',
)
expect(config.build?.ssr).toBe(target === 'ssr')
expect(config.build?.outDir).toBe(`./dist/${target}`)
expect(config.build?.rolldownOptions?.platform).toBe('node')
expect(config.build?.rolldownOptions?.external).toEqual([
'node:module',
'module',
/^react(?:\/|$)/,
/^react-dom(?:\/|$)/,
])
})

test('does not retransform built bundles in the test environment', () => {
vi.stubEnv('VITEST', 'true')
const config = createLinkPerformanceConfig(target)
expect(config.ssr?.noExternal).toBeUndefined()
expect(config.test?.server?.deps?.external).toEqual([
/\/link-performance\/dist\//,
])
})
test('does not retransform built bundles in the test environment', () => {
vi.stubEnv('VITEST', 'true')
const config = createLinkPerformanceConfig(target)
expect(config.ssr?.noExternal).toBeUndefined()
expect(config.test?.server?.deps?.external).toEqual([
/\/link-performance\/dist\//,
])
})

test('bundles router dependencies when building app snapshots', () => {
vi.stubEnv('VITEST', undefined)
expect(createLinkPerformanceConfig(target).ssr?.noExternal).toBe(true)
})
},
)
test('bundles router dependencies when building app snapshots', () => {
vi.stubEnv('VITEST', undefined)
expect(createLinkPerformanceConfig(target).ssr?.noExternal).toBe(true)
})
})
4 changes: 1 addition & 3 deletions benchmarks/client-nav/link-performance/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export function createLinkPerformanceConfig(
target: 'client' | 'ssr',
): UserConfig {
const server = target === 'ssr'
const enabled = process.env.TSR_LINK_PERF === '1'

return {
root,
Expand Down Expand Up @@ -62,9 +61,8 @@ export function createLinkPerformanceConfig(
},
},
include: [],
passWithNoTests: !enabled,
benchmark: {
include: enabled ? [`${target}.bench.ts`] : [],
include: [`${target}.bench.ts`],
},
},
}
Expand Down
4 changes: 0 additions & 4 deletions benchmarks/client-nav/link-performance/stable-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,6 @@ async function sampleReplica(
}

async function main() {
if (process.env.TSR_LINK_PERF !== '1') {
console.log('Link performance sampling is disabled; set TSR_LINK_PERF=1.')
return
}
const { values } = parseArgs({
options: {
baseline: { type: 'string' },
Expand Down
5 changes: 2 additions & 3 deletions benchmarks/ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,8 @@ rewrites, and active props. Each timed batch creates four fresh routers and
renders their Links to HTML; it does not measure Start HTTP/streaming overhead.

```bash
TSR_LINK_PERF=1 CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:ssr --outputStyle=stream --skipRemoteCache -- --run
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:ssr --outputStyle=stream --skipRemoteCache -- --run
```

These cases are excluded from this directory's aggregate projects and normal
CodSpeed dependency graph. The flag must be explicitly enabled when invoking
the dedicated target.
CodSpeed dependency graph; they only run through the dedicated target above.
9 changes: 7 additions & 2 deletions packages/router-core/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ export function getRouteSegments(route: AnyRoute) {
return route._interpolation
}

/** A splat is missing when it has no value; `0` and `false` are stringified like any other param. */
function isMissingSplat(value: unknown): boolean {
return value == null || value === ''
}

/** Devtools checks navigation availability separately from the hot formatter. */
export function hasMissingPathParams(
segments: RouteInterpolation,
Expand All @@ -229,7 +234,7 @@ export function hasMissingPathParams(
}
const [kind, key] = part
return kind === SEGMENT_TYPE_WILDCARD
? !params[key]
? isMissingSplat(params[key])
: kind === SEGMENT_TYPE_PARAM && !(key in params)
})
}
Expand Down Expand Up @@ -290,7 +295,7 @@ export function interpolatePath(
usedParams['*'] = paramValue
}
}
if (splat && !paramValue) {
if (splat && isMissingSplat(paramValue)) {
// A missing wildcard keeps its affixes, but omits a bare segment.
if (prefix === '/' && !suffix) {
continue
Expand Down
109 changes: 52 additions & 57 deletions packages/router-core/tests/lightweight-location.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,57 @@ import { createMemoryHistory } from '@tanstack/history'
import { BaseRootRoute, BaseRoute } from '../src'
import { createTestRouter } from './routerTestUtils'

if (process.env.TSR_LINK_PERF === '1') {
describe.each([false, true])(
'cold lightweight locations (server: %s)',
(isServer) => {
for (const count of [1, 8, 32]) {
const root = new BaseRootRoute({})
const segments = Array.from(
{ length: count },
(_, index) => `$p${index}`,
)
const source = new BaseRoute({
getParentRoute: () => root,
path: `/source/${segments.join('/')}`,
})
const target = new BaseRoute({
getParentRoute: () => root,
path: '/target',
})
const history = createMemoryHistory({ initialEntries: ['/'] })
const router = createTestRouter({
routeTree: root.addChildren([source, target]),
history,
isServer,
scrollRestoration: false,
})
history.destroy()
const pathname = `/source/${segments.map((_, index) => String(index)).join('/')}`
const location = {
...router.latestLocation,
pathname,
href: pathname,
publicHref: pathname,
}
let checksum = 0
const iterations = 256
function run() {
checksum = 0
for (let index = 0; index < iterations; index++) {
checksum += router.buildLocation({
to: '/target',
params: true,
_fromLocation: { ...location },
}).pathname.length
}
describe.each([false, true])(
'cold lightweight locations (server: %s)',
(isServer) => {
for (const count of [1, 8, 32]) {
const root = new BaseRootRoute({})
const segments = Array.from({ length: count }, (_, index) => `$p${index}`)
const source = new BaseRoute({
getParentRoute: () => root,
path: `/source/${segments.join('/')}`,
})
const target = new BaseRoute({
getParentRoute: () => root,
path: '/target',
})
const history = createMemoryHistory({ initialEntries: ['/'] })
const router = createTestRouter({
routeTree: root.addChildren([source, target]),
history,
isServer,
scrollRestoration: false,
})
history.destroy()
const pathname = `/source/${segments.map((_, index) => String(index)).join('/')}`
const location = {
...router.latestLocation,
pathname,
href: pathname,
publicHref: pathname,
}
let checksum = 0
const iterations = 256
function run() {
checksum = 0
for (let index = 0; index < iterations; index++) {
checksum += router.buildLocation({
to: '/target',
params: true,
_fromLocation: { ...location },
}).pathname.length
}
run()
expect(checksum).toBe(iterations * '/target'.length)
bench(`${count} source params`, run, {
time: 1000,
warmupTime: 200,
throws: true,
teardown: () => {
expect(checksum).toBe(iterations * '/target'.length)
},
})
}
},
)
}
run()
expect(checksum).toBe(iterations * '/target'.length)
bench(`${count} source params`, run, {
time: 1000,
warmupTime: 200,
throws: true,
teardown: () => {
expect(checksum).toBe(iterations * '/target'.length)
},
})
}
},
)
Loading
Loading