From 7b6125d4ab502f0f42ac8e4bb3e49432b2f56734 Mon Sep 17 00:00:00 2001 From: Manuel Schiller <6340397+schiller-manuel@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:58:30 +0200 Subject: [PATCH] perf(router-core): pass resolvePath arguments positionally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolvePath(base, to, trailingSlash?, cache?)` replaces the options object, so `resolvePathWithBase` — called from `buildLocation` and `matchRoute` — no longer allocates per path resolution. `resolvePath` is an internal helper. react-router.minimal shrinks by 17 B gzip (60 raw). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> (cherry picked from commit de7bc72a952aaf77bbedfd4453a4d12edadaad53) --- .changeset/resolve-path-positional.md | 8 ++ packages/router-core/src/path.ts | 22 ++--- packages/router-core/src/router.ts | 12 +-- packages/router-core/tests/path.test.ts | 110 +++++------------------- 4 files changed, 46 insertions(+), 106 deletions(-) create mode 100644 .changeset/resolve-path-positional.md diff --git a/.changeset/resolve-path-positional.md b/.changeset/resolve-path-positional.md new file mode 100644 index 0000000000..018d4f93b2 --- /dev/null +++ b/.changeset/resolve-path-positional.md @@ -0,0 +1,8 @@ +--- +'@tanstack/router-core': patch +'@tanstack/react-router': patch +'@tanstack/solid-router': patch +'@tanstack/vue-router': patch +--- + +`resolvePath` (internal helper) now takes positional arguments — `resolvePath(base, to, trailingSlash?, cache?)` — so `buildLocation` and `matchRoute` no longer allocate an options object per path resolution. diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index 88837db2b1..1b6288dc36 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -94,23 +94,19 @@ export function exactPathTest( // /a/b/c + d = /a/b/c/d // /a/b/c + d/ = /a/b/c/d // /a/b/c + d/e = /a/b/c/d/e -interface ResolvePathOptions { - base: string - to: string - trailingSlash?: 'always' | 'never' | 'preserve' - cache?: SieveCache -} - /** * Resolve a destination path against a base, honoring trailing-slash policy * and supporting relative segments (`.`/`..`) and absolute `to` values. + * + * Internal: parameters are positional so the router's hot callers pass no + * options object. */ -export function resolvePath({ - base, - to, - trailingSlash = 'never', - cache, -}: ResolvePathOptions) { +export function resolvePath( + base: string, + to: string, + trailingSlash: 'always' | 'never' | 'preserve' = 'never', + cache?: SieveCache, +) { if (to.includes('//')) { to = cleanPath(to) } diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index cdf1c0e363..7f951d77bd 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1522,12 +1522,12 @@ export class RouterCore< /** Resolve a path using the router's trailing-slash policy. */ resolvePathWithBase = (from: string, path: string) => { - return resolvePath({ - base: from, - to: path, - trailingSlash: this.options.trailingSlash, - cache: this.resolvePathCache, - }) + return resolvePath( + from, + path, + this.options.trailingSlash, + this.resolvePathCache, + ) } matchRoutes: MatchRoutesFn = ( diff --git a/packages/router-core/tests/path.test.ts b/packages/router-core/tests/path.test.ts index 8dc964bcd6..dac91a80fa 100644 --- a/packages/router-core/tests/path.test.ts +++ b/packages/router-core/tests/path.test.ts @@ -476,79 +476,43 @@ describe('resolvePath', () => { ['/posts', '../../data:text/html,test', '/data:text/html,test'], ])('resolves correctly', (a, b, eq) => { it(`${a} to ${b} === ${eq}`, () => { - expect(resolvePath({ base: a, to: b })).toEqual(eq) + expect(resolvePath(a, b)).toEqual(eq) }) it(`${a}/ to ${b} === ${eq} (trailing slash)`, () => { - expect(resolvePath({ base: a + '/', to: b })).toEqual(eq) + expect(resolvePath(a + '/', b)).toEqual(eq) }) it(`${a}/ to ${b}/ === ${eq} (trailing slash + trailing slash)`, () => { - expect(resolvePath({ base: a + '/', to: b + '/' })).toEqual(eq) + expect(resolvePath(a + '/', b + '/')).toEqual(eq) }) }) it('normalizes repeated slashes when resolving the base path', () => { - expect(resolvePath({ base: '/a//b', to: '.' })).toBe('/a/b') + expect(resolvePath('/a//b', '.')).toBe('/a/b') }) describe('trailingSlash', () => { describe(`'always'`, () => { it('keeps trailing slash', () => { - expect( - resolvePath({ - base: '/a/b/c', - to: 'd/', - trailingSlash: 'always', - }), - ).toBe('/a/b/c/d/') + expect(resolvePath('/a/b/c', 'd/', 'always')).toBe('/a/b/c/d/') }) it('adds trailing slash', () => { - expect( - resolvePath({ - base: '/a/b/c', - to: 'd', - trailingSlash: 'always', - }), - ).toBe('/a/b/c/d/') + expect(resolvePath('/a/b/c', 'd', 'always')).toBe('/a/b/c/d/') }) }) describe(`'never'`, () => { it('removes trailing slash', () => { - expect( - resolvePath({ - base: '/a/b/c', - to: 'd/', - trailingSlash: 'never', - }), - ).toBe('/a/b/c/d') + expect(resolvePath('/a/b/c', 'd/', 'never')).toBe('/a/b/c/d') }) it('does not add trailing slash', () => { - expect( - resolvePath({ - base: '/a/b/c', - to: 'd', - trailingSlash: 'never', - }), - ).toBe('/a/b/c/d') + expect(resolvePath('/a/b/c', 'd', 'never')).toBe('/a/b/c/d') }) }) describe(`'preserve'`, () => { it('keeps trailing slash', () => { - expect( - resolvePath({ - base: '/a/b/c', - to: 'd/', - trailingSlash: 'preserve', - }), - ).toBe('/a/b/c/d/') + expect(resolvePath('/a/b/c', 'd/', 'preserve')).toBe('/a/b/c/d/') }) it('does not add trailing slash', () => { - expect( - resolvePath({ - base: '/a/b/c', - to: 'd', - trailingSlash: 'preserve', - }), - ).toBe('/a/b/c/d') + expect(resolvePath('/a/b/c', 'd', 'preserve')).toBe('/a/b/c/d') }) }) @@ -559,7 +523,7 @@ describe('resolvePath', () => { ] as const)( "normalizes repeated slashes with trailingSlash '%s'", (trailingSlash, to, expected) => { - expect(resolvePath({ base: '/', to, trailingSlash })).toBe(expected) + expect(resolvePath('/', to, trailingSlash)).toBe(expected) }, ) }) @@ -585,13 +549,7 @@ describe('resolvePath', () => { }, ])('$name', ({ to }) => { const candidate = base + trimPathLeft(to) - expect( - resolvePath({ - base, - to: candidate, - trailingSlash: 'never', - }), - ).toEqual(candidate) + expect(resolvePath(base, candidate, 'never')).toEqual(candidate) }) }) @@ -613,42 +571,28 @@ describe('resolvePath', () => { }, ])('$name', ({ to }) => { const candidate = base + trimPathLeft(to) - expect( - resolvePath({ - base, - to: candidate, - trailingSlash: 'never', - }), - ).toEqual(candidate) + expect(resolvePath(base, candidate, 'never')).toEqual(candidate) }) }) }, ) it('preserves explicit route-template param syntax', () => { - expect( - resolvePath({ - base: '/{$language}', - to: '.', - }), - ).toBe('/{$language}') + expect(resolvePath('/{$language}', '.')).toBe('/{$language}') - expect( - resolvePath({ - base: '/{$language}/posts', - to: '../{$language}', - }), - ).toBe('/{$language}/{$language}') + expect(resolvePath('/{$language}/posts', '../{$language}')).toBe( + '/{$language}/{$language}', + ) }) it('caches route-template paths without changing param syntax', () => { const cache = createSieveCache(10) const set = vi.spyOn(cache, 'set') - expect(resolvePath({ base: '/', to: '{$id}', cache })).toBe('/{$id}') - expect(resolvePath({ base: '/', to: '$id', cache })).toBe('/$id') - expect(resolvePath({ base: '/', to: '{$id}', cache })).toBe('/{$id}') - expect(resolvePath({ base: '/', to: '$id', cache })).toBe('/$id') + expect(resolvePath('/', '{$id}', undefined, cache)).toBe('/{$id}') + expect(resolvePath('/', '$id', undefined, cache)).toBe('/$id') + expect(resolvePath('/', '{$id}', undefined, cache)).toBe('/{$id}') + expect(resolvePath('/', '$id', undefined, cache)).toBe('/$id') expect(set).toHaveBeenCalledTimes(2) }) }) @@ -1073,16 +1017,8 @@ describe('interpolatePath', () => { (trailingSlash) => { const tail = trailingSlash === 'always' ? '/' : '' const defaultedFromPath = '/' - const fromPath = resolvePath({ - base: defaultedFromPath, - to: '.', - trailingSlash, - }) - const nextTo = resolvePath({ - base: fromPath, - to: '/splat/$', - trailingSlash, - }) + const fromPath = resolvePath(defaultedFromPath, '.', trailingSlash) + const nextTo = resolvePath(fromPath, '/splat/$', trailingSlash) const nextParams = { _splat: '' } const interpolatedNextTo = interpolatePath( nextTo,