Skip to content

feat: match params#7263

Merged
schiller-manuel merged 2 commits into
mainfrom
match-params
Apr 30, 2026
Merged

feat: match params#7263
schiller-manuel merged 2 commits into
mainfrom
match-params

Conversation

@schiller-manuel
Copy link
Copy Markdown
Collaborator

@schiller-manuel schiller-manuel commented Apr 25, 2026

Summary by CodeRabbit

  • New Features

    • Experimental route-matching controls: params.match predicate and params.matchPriority tie-breaker.
  • Removals

    • Deprecated skipRouteOnParseError option removed.
  • Documentation

    • RouteOptions docs updated with the new experimental matching controls.
  • Tests

    • Extensive unit and e2e test coverage added for param-based matching and ambiguous/priority cases.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces experimental route-matching controls (params.match, params.matchPriority) and removes the skipRouteOnParseError mechanism; parsing now gates matching via params.parse returning false. Adds exported hasKeys utility and integrates it across packages. Adds comprehensive e2e and unit tests for guarded/ambiguous param matching.

Changes

Cohort / File(s) Summary
Docs & Changeset
docs/router/api/router/RouteOptionsType.md, .changeset/clean-routes-match.md
Documents new experimental params.match and params.matchPriority, and announces removal of skipRouteOnParseError.
E2E: match-params app & test infra
e2e/react-router/match-params/.gitignore, e2e/react-router/match-params/index.html, e2e/react-router/match-params/package.json, e2e/react-router/match-params/playwright.config.ts, e2e/react-router/match-params/src/main.tsx, e2e/react-router/match-params/tests/match-params.spec.ts, e2e/react-router/match-params/tsconfig.json, e2e/react-router/match-params/vite.config.js
Adds a new React Router test app, Playwright config, build/dev scripts, and an E2E spec validating params.parse/stringify, guarded routes, ambiguous templates, and link generation.
Public exports / utils
packages/router-core/src/index.ts, packages/router-core/src/utils.ts
Adds and re-exports hasKeys(obj) utility for lightweight own-key presence checks.
Route API types
packages/router-core/src/route.ts
Updates ParseParamsFn return type to allow `
Route tree processing
packages/router-core/src/new-process-route-tree.ts
Removes skipRouteOnParseError and parsedParams accumulation. Reworks matching logic to depend on presence of parse funcs; exports buildRouteBranch.
Router logic & buildLocation
packages/router-core/src/router.ts
Removes parsedParams from match shapes, always applies parse on strict extraction (throws on false), changes buildLocation to prefer routesByPath and buildRouteBranch, and gates stringify using hasKeys. Adds dev-only round-trip pathname check.
Path handling
packages/router-core/src/path.ts
Simplifies resolvePath to join/clean baseSegments rather than re-parsing each segment.
Core tests: new & updated
packages/router-core/tests/match-params.test.ts, packages/router-core/tests/build-location.test.ts, packages/router-core/tests/path.test.ts, packages/router-core/tests/new-process-route-tree.test.ts
Adds large suite validating guarded params.parse matching, stringify interplay, path-template preservation, and updates process-tree assertions to reflect removal of skipRouteOnParseError.
Core tests: removed
packages/router-core/tests/skip-route-on-parse-error.test.ts
Deletes the legacy 890+ line test suite referencing skipRouteOnParseError.
hasKeys integration — React / Devtools / frameworks
packages/react-router/src/RouterProvider.tsx, packages/react-router/src/link.tsx, packages/react-router/tests/useParams.test.tsx, packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx, packages/solid-router/src/link.tsx, packages/solid-router/tests/useParams.test.tsx, packages/vue-router/src/link.tsx, packages/vue-router/tests/useParams.test.tsx, packages/start-plugin-core/src/rsbuild/plugin.ts, packages/start-plugin-core/src/start-compiler/handleCreateServerFn.ts
Replaces various Object.keys(...).length emptiness checks with hasKeys(...). Updates a few test expectations to reflect buildLocation round-trip/dev-only behavior.
Package config (E2E)
e2e/react-router/match-params/package.json, e2e/react-router/match-params/vite.config.js, e2e/react-router/match-params/tsconfig.json
New package and build/test configuration for the e2e fixture.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant RouterProvider
    participant RouterCore
    participant RouteMatcher
    participant RouteComponent
    Browser->>RouterProvider: navigation event / link click
    RouterProvider->>RouterCore: buildLocation / resolve destination
    RouterCore->>RouteMatcher: findRouteMatch(rawPath, rawParams)
    RouteMatcher->>RouteMatcher: invoke params.match? / params.parse?
    alt params.match returns false or parse returns false
        RouteMatcher-->>RouterCore: skip route -> try next candidate
    else params.match/parse pass
        RouteMatcher-->>RouterCore: selected match (typed params)
    end
    RouterCore->>RouteComponent: render selected route with params
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibble paths where params hide,
Match and priority now decide,
parse says "no" and routes step back,
hasKeys hops in to keep no slack,
A fluffy test suite cheers the track. 🌿

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'match params' is vague and generic, using non-descriptive terms that don't clearly convey what the changeset accomplishes. Use a more descriptive title that explains the main change, such as 'Add experimental params.match and params.matchPriority route matching controls' or 'Refactor route matching to use params.match instead of skipRouteOnParseError'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch match-params

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented Apr 25, 2026

View your CI Pipeline Execution ↗ for commit af011ad

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 6m 34s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 4s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-30 17:35:48 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 25, 2026

🚀 Changeset Version Preview

5 package(s) bumped directly, 18 bumped as dependents.

🟨 Minor bumps

Package Version Reason
@tanstack/react-router 1.168.26 → 1.169.0 Changeset
@tanstack/router-core 1.168.18 → 1.169.0 Changeset
@tanstack/solid-router 1.168.26 → 1.169.0 Changeset
@tanstack/vue-router 1.168.22 → 1.169.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/react-start 1.167.52 → 1.167.53 Changeset
@tanstack/react-start-client 1.166.44 → 1.166.45 Dependent
@tanstack/react-start-rsc 0.0.31 → 0.0.32 Dependent
@tanstack/react-start-server 1.166.45 → 1.166.46 Dependent
@tanstack/router-cli 1.166.38 → 1.166.39 Dependent
@tanstack/router-generator 1.166.37 → 1.166.38 Dependent
@tanstack/router-plugin 1.167.29 → 1.167.30 Dependent
@tanstack/router-vite-plugin 1.166.44 → 1.166.45 Dependent
@tanstack/solid-start 1.167.49 → 1.167.50 Dependent
@tanstack/solid-start-client 1.166.43 → 1.166.44 Dependent
@tanstack/solid-start-server 1.166.44 → 1.166.45 Dependent
@tanstack/start-client-core 1.167.21 → 1.167.22 Dependent
@tanstack/start-plugin-core 1.169.7 → 1.169.8 Dependent
@tanstack/start-server-core 1.167.23 → 1.167.24 Dependent
@tanstack/start-static-server-functions 1.166.37 → 1.166.38 Dependent
@tanstack/start-storage-context 1.166.32 → 1.166.33 Dependent
@tanstack/vue-start 1.167.45 → 1.167.46 Dependent
@tanstack/vue-start-client 1.166.39 → 1.166.40 Dependent
@tanstack/vue-start-server 1.166.40 → 1.166.41 Dependent

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 25, 2026

Bundle Size Benchmarks

  • Commit: 7fa0f39cabf4
  • Measured at: 2026-04-30T17:29:54.211Z
  • Baseline source: history:7fa0f39cabf4
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 87.15 KiB -155 B (-0.17%) 273.94 KiB 75.70 KiB ██████▇▇▇▇▇▁
react-router.full 90.68 KiB -120 B (-0.13%) 285.45 KiB 78.71 KiB ▇▇▇▇▇▇█████▁
solid-router.minimal 35.38 KiB -165 B (-0.45%) 106.25 KiB 31.81 KiB ▇▇████▇▇▇▇▇▁
solid-router.full 40.10 KiB -178 B (-0.43%) 120.46 KiB 36.04 KiB ▆▆▇███▇▇▇▇▇▁
vue-router.minimal 53.15 KiB -147 B (-0.27%) 151.39 KiB 47.73 KiB ███████████▁
vue-router.full 58.28 KiB -169 B (-0.28%) 167.56 KiB 52.18 KiB ███████████▁
react-start.minimal 101.76 KiB -164 B (-0.16%) 322.10 KiB 87.97 KiB ███████████▁
react-start.full 105.19 KiB -159 B (-0.15%) 332.43 KiB 90.96 KiB ███████████▁
react-start.rsbuild.minimal 99.33 KiB -204 B (-0.20%) 316.47 KiB 85.46 KiB █████████▂▂▁
react-start.rsbuild.full 102.63 KiB -215 B (-0.20%) 326.90 KiB 88.28 KiB █████████▂▂▁
solid-start.minimal 49.40 KiB -154 B (-0.30%) 152.08 KiB 43.60 KiB ▆▆████▇▇▇▇▇▁
solid-start.full 55.20 KiB -141 B (-0.25%) 168.98 KiB 48.51 KiB ▅▅▆███▇▇▇▇▇▁

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/new-process-route-tree.ts (1)

473-478: ⚠️ Potential issue | 🟠 Major

Sort pathless candidates before traversal.

matchPriority is applied to dynamic/optional/wildcard nodes, but not to node.pathless. That means two sibling pathless layouts that both use params.match are still resolved by declaration order, so a lower-priority matcher can shadow a higher-priority one.

Suggested fix
 function sortTreeNodes(node: SegmentNode<RouteLike>) {
   if (node.pathless) {
+    node.pathless.sort((a, b) => {
+      if (a.skipOnParamError && !b.skipOnParamError) return -1
+      if (!a.skipOnParamError && b.skipOnParamError) return 1
+      if (a.skipOnParamError && b.skipOnParamError) {
+        return b.matchPriority - a.matchPriority
+      }
+      return 0
+    })
     for (const child of node.pathless) {
       sortTreeNodes(child)
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/new-process-route-tree.ts` around lines 473 - 478,
The traversal in sortTreeNodes currently recurses over node.pathless in
declaration order, letting lower-priority pathless layouts shadow
higher-priority ones; update sortTreeNodes to sort node.pathless by the same
matchPriority logic used for dynamic/optional/wildcard nodes before iterating
(i.e., compute each child's matchPriority and sort the node.pathless array
descending so higher-priority candidates are visited first), then recurse as
before into each child; reference the existing sortTreeNodes function and the
node.pathless children and reuse the same matchPriority comparator used
elsewhere to ensure consistent ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/router-core/src/new-process-route-tree.ts`:
- Around line 473-478: The traversal in sortTreeNodes currently recurses over
node.pathless in declaration order, letting lower-priority pathless layouts
shadow higher-priority ones; update sortTreeNodes to sort node.pathless by the
same matchPriority logic used for dynamic/optional/wildcard nodes before
iterating (i.e., compute each child's matchPriority and sort the node.pathless
array descending so higher-priority candidates are visited first), then recurse
as before into each child; reference the existing sortTreeNodes function and the
node.pathless children and reuse the same matchPriority comparator used
elsewhere to ensure consistent ordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0707134-a8d1-4562-bb36-dc542db09dde

📥 Commits

Reviewing files that changed from the base of the PR and between 97207a1 and 4e4130cd14ed36dd50bf272693c0cd73b32a86de.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • docs/router/api/router/RouteOptionsType.md
  • e2e/react-router/match-params/.gitignore
  • e2e/react-router/match-params/index.html
  • e2e/react-router/match-params/package.json
  • e2e/react-router/match-params/playwright.config.ts
  • e2e/react-router/match-params/src/main.tsx
  • e2e/react-router/match-params/tests/match-params.spec.ts
  • e2e/react-router/match-params/tsconfig.json
  • e2e/react-router/match-params/vite.config.js
  • packages/react-router/src/index.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/route.ts
  • packages/router-core/src/router.ts
  • packages/router-core/tests/build-location.test.ts
  • packages/router-core/tests/match-params.test.ts
  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
  • packages/solid-router/src/index.tsx
  • packages/vue-router/src/index.tsx
💤 Files with no reviewable changes (1)
  • packages/router-core/tests/skip-route-on-parse-error.test.ts

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 25, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7263

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7263

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7263

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7263

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7263

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7263

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7263

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7263

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7263

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7263

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7263

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7263

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7263

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7263

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7263

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7263

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7263

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7263

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7263

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7263

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7263

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7263

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7263

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7263

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7263

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7263

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7263

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7263

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7263

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7263

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7263

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7263

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7263

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7263

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7263

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7263

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7263

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7263

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7263

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7263

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7263

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7263

commit: 056e0b6

Comment thread packages/router-core/src/new-process-route-tree.ts Outdated
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 25, 2026

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing match-params (056e0b6) with main (7fa0f39)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@schiller-manuel schiller-manuel force-pushed the match-params branch 2 times, most recently from 6fec866 to db920c6 Compare April 25, 2026 22:13
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/new-process-route-tree.ts (1)

1019-1025: ⚠️ Potential issue | 🟠 Major

Honor params.match before taking the root-index shortcut.

This fast path now skips validateMatchParams, so a guarded root index route still matches / even when its matcher returns false. It also prevents fallback to sibling optional/wildcard routes. The safest fix is to only keep the shortcut for unguarded root indexes and let guarded ones fall through to the normal stack logic.

Suggested fix
-  if (path === '/' && segmentTree.index)
+  if (path === '/' && segmentTree.index && !segmentTree.index.match)
     return { node: segmentTree.index, skipped: 0 } as Pick<
       Frame,
       'node' | 'skipped'
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/new-process-route-tree.ts` around lines 1019 - 1025,
The root-index fast path incorrectly bypasses guarded matchers; change the
shortcut so it only returns the index immediately when path === '/' and
segmentTree.index exists AND that index has no matcher to validate (i.e.,
params.match is undefined/not present) — otherwise fall through to the existing
stack logic which calls validateMatchParams. Locate the block that returns {
node: segmentTree.index, skipped: 0 } and add a guard checking the index node's
params.match (or equivalent matcher field) is absent before returning, ensuring
guarded root indexes are validated by validateMatchParams instead of being
short-circuited.
🧹 Nitpick comments (3)
e2e/react-router/match-params/src/main.tsx (2)

235-239: Avoid non-null assertion for the mount element.

Using ! here bypasses runtime safety. A guard keeps the failure explicit and type-safe.

Proposed fix
-const rootElement = document.getElementById('app')!
+const rootElement = document.getElementById('app')
 
-if (!rootElement.innerHTML) {
+if (!rootElement) {
+  throw new Error('Missing `#app` mount element')
+}
+
+if (!rootElement.innerHTML) {
   ReactDOM.createRoot(rootElement).render(<RouterProvider router={router} />)
 }

As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-router/match-params/src/main.tsx` around lines 235 - 239, Remove
the non-null assertion on document.getElementById('app') and add an explicit
runtime guard: change the const rootElement = document.getElementById('app')! to
const rootElement = document.getElementById('app'); then check if (!rootElement)
{ throw new Error('Mount element "app" not found'); } (or log and exit), and
only call ReactDOM.createRoot(rootElement).render(<RouterProvider
router={router} />) when rootElement exists; update references in this block
(rootElement and ReactDOM.createRoot) accordingly to satisfy TypeScript strict
mode and keep failure explicit.

81-84: Make language parsing explicit instead of defaulting unknown values to pl-PL.

The current fallback silently coerces unexpected values. Prefer explicit branches so parsing remains invariant with the match contract.

Proposed fix
     parse: ({ language }) => {
       if (language === 'en') return { language: 'en-US' as const }
-      return { language: 'pl-PL' as const }
+      if (language === 'pl') return { language: 'pl-PL' as const }
+      throw new Error(`Unexpected language segment: ${language}`)
     },

As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-router/match-params/src/main.tsx` around lines 81 - 84, The parse
function currently defaults any non-'en' value to 'pl-PL'; change the parse
implementation for the parse({ language }) handler to be explicit: handle 'en'
(return { language: 'en-US' }) and handle 'pl' (return { language: 'pl-PL' }),
and for any other input do not coerce—either throw an Error (e.g. "Unsupported
language") or return undefined so the match fails; update the parse function in
main.tsx accordingly to ensure it only accepts explicit supported values.
packages/router-core/src/router.ts (1)

1357-1364: Clear routeBranchCache when route trees are rebuilt.

getRouteBranch caches by route object identity, but the cache is never reset when update() swaps/reprocesses the route tree. If route objects are reused across updates (eg HMR-like flows), cached ancestry can go stale and produce wrong middleware/params-stringify traversal in buildLocation.

♻️ Proposed fix
 setRoutes({
   routesById,
   routesByPath,
   processedTree,
 }: ProcessRouteTreeResult<TRouteTree>) {
+  this.routeBranchCache = new WeakMap()
   this.routesById = routesById as RoutesById<TRouteTree>
   this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
   this.processedTree = processedTree
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/router.ts` around lines 1357 - 1364, getRouteBranch
caches branches by route object identity using routeBranchCache but the cache is
never invalidated when the router's route tree is rebuilt; update() should clear
or reset routeBranchCache whenever the route tree is swapped/reprocessed so
stale branches (from reused route objects) are not returned. Modify the update()
implementation to call this.routeBranchCache.clear() (or replace the Map)
immediately after the route tree is replaced/reprocessed (the code paths that
reconstruct routes), so getRouteBranch and buildRouteBranch always compute
branches against the current tree.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/react-router/match-params/src/main.tsx`:
- Around line 108-114: The numeric route matcher currently uses
Number.isInteger(Number(userId)) which allows scientific notation and signed
forms; change the match function for the route (the match: ({ userId }) => ...)
to use a strict digits-only regex test like /^\d+$/. After validating with the
regex, keep parse: ({ userId }) => ({ userId: Number(userId) }) to convert the
string to a number. Apply the same change to the other numeric matcher block
later in the file (the similar match/parse pair around lines 140-146).

---

Outside diff comments:
In `@packages/router-core/src/new-process-route-tree.ts`:
- Around line 1019-1025: The root-index fast path incorrectly bypasses guarded
matchers; change the shortcut so it only returns the index immediately when path
=== '/' and segmentTree.index exists AND that index has no matcher to validate
(i.e., params.match is undefined/not present) — otherwise fall through to the
existing stack logic which calls validateMatchParams. Locate the block that
returns { node: segmentTree.index, skipped: 0 } and add a guard checking the
index node's params.match (or equivalent matcher field) is absent before
returning, ensuring guarded root indexes are validated by validateMatchParams
instead of being short-circuited.

---

Nitpick comments:
In `@e2e/react-router/match-params/src/main.tsx`:
- Around line 235-239: Remove the non-null assertion on
document.getElementById('app') and add an explicit runtime guard: change the
const rootElement = document.getElementById('app')! to const rootElement =
document.getElementById('app'); then check if (!rootElement) { throw new
Error('Mount element "app" not found'); } (or log and exit), and only call
ReactDOM.createRoot(rootElement).render(<RouterProvider router={router} />) when
rootElement exists; update references in this block (rootElement and
ReactDOM.createRoot) accordingly to satisfy TypeScript strict mode and keep
failure explicit.
- Around line 81-84: The parse function currently defaults any non-'en' value to
'pl-PL'; change the parse implementation for the parse({ language }) handler to
be explicit: handle 'en' (return { language: 'en-US' }) and handle 'pl' (return
{ language: 'pl-PL' }), and for any other input do not coerce—either throw an
Error (e.g. "Unsupported language") or return undefined so the match fails;
update the parse function in main.tsx accordingly to ensure it only accepts
explicit supported values.

In `@packages/router-core/src/router.ts`:
- Around line 1357-1364: getRouteBranch caches branches by route object identity
using routeBranchCache but the cache is never invalidated when the router's
route tree is rebuilt; update() should clear or reset routeBranchCache whenever
the route tree is swapped/reprocessed so stale branches (from reused route
objects) are not returned. Modify the update() implementation to call
this.routeBranchCache.clear() (or replace the Map) immediately after the route
tree is replaced/reprocessed (the code paths that reconstruct routes), so
getRouteBranch and buildRouteBranch always compute branches against the current
tree.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1243a0b5-386d-4e2a-a4f6-24021fa0e42a

📥 Commits

Reviewing files that changed from the base of the PR and between 4e4130cd14ed36dd50bf272693c0cd73b32a86de and 6fec86621ddde1e996fc9d36e7032a99e16ec48b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • .changeset/clean-routes-match.md
  • docs/router/api/router/RouteOptionsType.md
  • e2e/react-router/match-params/.gitignore
  • e2e/react-router/match-params/index.html
  • e2e/react-router/match-params/package.json
  • e2e/react-router/match-params/playwright.config.ts
  • e2e/react-router/match-params/src/main.tsx
  • e2e/react-router/match-params/tests/match-params.spec.ts
  • e2e/react-router/match-params/tsconfig.json
  • e2e/react-router/match-params/vite.config.js
  • packages/react-router/src/index.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/path.ts
  • packages/router-core/src/route.ts
  • packages/router-core/src/router.ts
  • packages/router-core/tests/build-location.test.ts
  • packages/router-core/tests/match-params.test.ts
  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/tests/path.test.ts
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
  • packages/solid-router/src/index.tsx
  • packages/vue-router/src/index.tsx
💤 Files with no reviewable changes (1)
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
✅ Files skipped from review due to trivial changes (9)
  • packages/vue-router/src/index.tsx
  • e2e/react-router/match-params/index.html
  • e2e/react-router/match-params/vite.config.js
  • packages/solid-router/src/index.tsx
  • packages/react-router/src/index.tsx
  • e2e/react-router/match-params/.gitignore
  • .changeset/clean-routes-match.md
  • packages/router-core/src/index.ts
  • e2e/react-router/match-params/package.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/router/api/router/RouteOptionsType.md
  • e2e/react-router/match-params/tests/match-params.spec.ts
  • e2e/react-router/match-params/playwright.config.ts
  • packages/router-core/tests/build-location.test.ts
  • e2e/react-router/match-params/tsconfig.json

Comment on lines +108 to +114
match: ({ userId }) => {
const parsed = Number(userId)
return Number.isInteger(parsed)
},
parse: ({ userId }) => {
return { userId: Number(userId) }
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n e2e/react-router/match-params/src/main.tsx | sed -n '100,150p'

Repository: TanStack/router

Length of output: 1658


🏁 Script executed:

python3 << 'EOF'
# Test Number.isInteger() behavior with the claimed examples
test_values = ["1", "1e2", "+7", "-1", "123abc", "123"]

for val in test_values:
    # Simulate Number(val) in JavaScript -> converts to number or NaN
    try:
        if val == "123abc":
            num_result = float('nan')  # Number("123abc") returns NaN in JS
        else:
            num_result = float(val)
        is_integer = (num_result == int(num_result)) if num_result == num_result else False  # NaN check
        print(f"Number('{val}') = {num_result}, Number.isInteger() = {is_integer}")
    except ValueError:
        print(f"Number('{val}') = NaN, Number.isInteger() = False")
EOF

Repository: TanStack/router

Length of output: 346


🏁 Script executed:

# Look for any tests related to numeric params matching
fd -e test -e spec | head -20
rg "numericUserRoute|numericOrgRoute" --type tsx --type ts

Repository: TanStack/router

Length of output: 87


🏁 Script executed:

# Check if there's any documentation or comments about the intended behavior
rg -A 5 -B 5 "match.*userId" e2e/react-router/match-params/src/main.tsx | head -50

Repository: TanStack/router

Length of output: 345


🏁 Script executed:

# Check if there are e2e tests that verify the numeric matching behavior
rg "1e2|\\+7|-1" e2e/react-router/match-params/ --type ts --type js

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Look for test files related to match-params
find e2e/react-router/match-params -name "*.test.*" -o -name "*.spec.*" | head -10

Repository: TanStack/router

Length of output: 116


🏁 Script executed:

# Check the full context of the file to understand the purpose of numericUserRoute
cat -n e2e/react-router/match-params/src/main.tsx | head -80

Repository: TanStack/router

Length of output: 2481


🏁 Script executed:

# Look for comments or documentation about the intended matching behavior
rg -B 10 "numericUserRoute|numericOrgRoute" e2e/react-router/match-params/src/main.tsx | grep -A 10 -B 10 "match\|parse"

Repository: TanStack/router

Length of output: 1474


🏁 Script executed:

cat -n e2e/react-router/match-params/tests/match-params.spec.ts

Repository: TanStack/router

Length of output: 2988


🏁 Script executed:

# Check if there are any issues or PRs discussing this specific behavior
git log --oneline --all --grep="numeric.*match\|match.*param" | head -20

Repository: TanStack/router

Length of output: 954


🏁 Script executed:

# Look at the route tree structure to understand how routes are prioritized
cat -n e2e/react-router/match-params/src/main.tsx | sed -n '240,260p'

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check if there are explicit numeric validation patterns elsewhere in the router codebase
rg "\\^\\\\d\|test.*digit\|numeric.*regex" packages/ --type ts --type tsx -A 2 | head -40

Repository: TanStack/router

Length of output: 87


🏁 Script executed:

# Look at the git history for the match-params e2e test to understand intent
git show 6fec86621 --stat

Repository: TanStack/router

Length of output: 50372


🏁 Script executed:

# Check if there are any tests that explicitly test scientific notation or special numeric forms
rg "1e2|\\+.*userId|\\-.*userId|\\.isInteger" e2e/react-router/match-params/tests/

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Look for any documentation about the params.match intent
rg -B 5 -A 5 "match.*intent\|match.*behavior\|numeric.*routing" e2e/react-router/match-params/ --type ts

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check if there are any discussions in recent commits about this behavior
git log --oneline --all --since="2025-01-01" | grep -i "match\|param" | head -20

Repository: TanStack/router

Length of output: 1757


🏁 Script executed:

# Double-check: verify the test only uses normal digits and strings, no scientific notation
grep -n "goto.*users\|goto.*orgs" e2e/react-router/match-params/tests/match-params.spec.ts

Repository: TanStack/router

Length of output: 258


Numeric route matchers are too permissive for digit-only matching.

Number.isInteger(Number(param)) accepts scientific notation (e.g., 1e2 → 100), signed numbers (e.g., +7, -1), and other coercible forms. This allows unintended paths like /users/1e2 to match the numeric route. The e2e tests expect only plain digit strings to match (e.g., /users/123 matches, /users/alice does not).

Use /^\d+$/ to enforce strict digit-only validation:

Proposed fix
+const digitsOnly = /^\d+$/
+
 const numericUserRoute = createRoute({
   getParentRoute: () => usersRoute,
   path: '$userId',
   params: {
     match: ({ userId }) => {
-      const parsed = Number(userId)
-      return Number.isInteger(parsed)
+      return digitsOnly.test(userId)
     },
     parse: ({ userId }) => {
-      return { userId: Number(userId) }
+      return { userId: Number.parseInt(userId, 10) }
     },
     stringify: ({ userId }) => ({ userId: String(userId) }),
   },
   component: NumericUserComponent,
 })
@@
 const numericOrgRoute = createRoute({
   getParentRoute: () => rootRoute,
   path: '/orgs/$orgId',
   params: {
     match: ({ orgId }) => {
-      const parsed = Number(orgId)
-      return Number.isInteger(parsed)
+      return digitsOnly.test(orgId)
     },
     parse: ({ orgId }) => {
-      return { orgId: Number(orgId) }
+      return { orgId: Number.parseInt(orgId, 10) }
     },
     stringify: ({ orgId }) => ({ orgId: String(orgId) }),
   },
 })

Also applies to: 140-146

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-router/match-params/src/main.tsx` around lines 108 - 114, The
numeric route matcher currently uses Number.isInteger(Number(userId)) which
allows scientific notation and signed forms; change the match function for the
route (the match: ({ userId }) => ...) to use a strict digits-only regex test
like /^\d+$/. After validating with the regex, keep parse: ({ userId }) => ({
userId: Number(userId) }) to convert the string to a number. Apply the same
change to the other numeric matcher block later in the file (the similar
match/parse pair around lines 140-146).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/router-core/src/route.ts (1)

188-194: Consider documenting params.match and params.matchPriority inline.

These are user-facing API knobs; short JSDoc here would reduce misuse and clarify priority semantics.

📝 Suggested doc-only improvement
 export type ParamsOptions<in out TPath extends string, in out TParams> = {
   params?: {
+    /**
+     * Boolean predicate evaluated during route matching using raw path params.
+     * Return `true` to allow this route candidate, `false` to skip it.
+     */
     match?: MatchParamsFn<TPath, TParams>
+    /**
+     * Higher values are evaluated first when multiple sibling routes define `params.match`.
+     * `@default` 0
+     */
     matchPriority?: number
     parse?: ParseParamsFn<TPath, TParams>
     stringify?: StringifyParamsFn<TPath, TParams>
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/route.ts` around lines 188 - 194, Add concise JSDoc
on the ParamsOptions type for the params.match and params.matchPriority fields:
describe that params.match is a user-supplied matcher implementing
MatchParamsFn<TPath,TParams> used to determine whether a path matches and to
extract params, and note expected return/side-effect semantics (e.g., truthy for
match + extracted params); for params.matchPriority document that it is a
numeric tie-breaker used when multiple matchers apply (higher number wins,
default value if omitted) and how it interacts with router selection. Place
these inline above the params.match and params.matchPriority declarations in the
ParamsOptions type so users see the API contract and priority semantics
immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/router-core/src/route.ts`:
- Around line 188-194: Add concise JSDoc on the ParamsOptions type for the
params.match and params.matchPriority fields: describe that params.match is a
user-supplied matcher implementing MatchParamsFn<TPath,TParams> used to
determine whether a path matches and to extract params, and note expected
return/side-effect semantics (e.g., truthy for match + extracted params); for
params.matchPriority document that it is a numeric tie-breaker used when
multiple matchers apply (higher number wins, default value if omitted) and how
it interacts with router selection. Place these inline above the params.match
and params.matchPriority declarations in the ParamsOptions type so users see the
API contract and priority semantics immediately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f526d26-a7b0-4c10-bfb2-e5c11a005420

📥 Commits

Reviewing files that changed from the base of the PR and between 6fec86621ddde1e996fc9d36e7032a99e16ec48b and db920c6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • .changeset/clean-routes-match.md
  • docs/router/api/router/RouteOptionsType.md
  • e2e/react-router/match-params/.gitignore
  • e2e/react-router/match-params/index.html
  • e2e/react-router/match-params/package.json
  • e2e/react-router/match-params/playwright.config.ts
  • e2e/react-router/match-params/src/main.tsx
  • e2e/react-router/match-params/tests/match-params.spec.ts
  • e2e/react-router/match-params/tsconfig.json
  • e2e/react-router/match-params/vite.config.js
  • packages/react-router/src/index.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/path.ts
  • packages/router-core/src/route.ts
  • packages/router-core/src/router.ts
  • packages/router-core/tests/build-location.test.ts
  • packages/router-core/tests/match-params.test.ts
  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/tests/path.test.ts
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
  • packages/solid-router/src/index.tsx
  • packages/vue-router/src/index.tsx
💤 Files with no reviewable changes (1)
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
✅ Files skipped from review due to trivial changes (9)
  • e2e/react-router/match-params/vite.config.js
  • e2e/react-router/match-params/index.html
  • packages/react-router/src/index.tsx
  • packages/solid-router/src/index.tsx
  • e2e/react-router/match-params/.gitignore
  • e2e/react-router/match-params/tsconfig.json
  • .changeset/clean-routes-match.md
  • e2e/react-router/match-params/package.json
  • packages/router-core/src/router.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/vue-router/src/index.tsx
  • docs/router/api/router/RouteOptionsType.md
  • packages/router-core/tests/path.test.ts
  • e2e/react-router/match-params/playwright.config.ts
  • packages/router-core/tests/build-location.test.ts
  • e2e/react-router/match-params/src/main.tsx
  • packages/router-core/src/new-process-route-tree.ts

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/new-process-route-tree.ts (1)

431-458: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't let parse outrank more specific dynamic patterns.

Putting the parse check before prefix/suffix and case-sensitivity means a guarded plain route like /a/$id can win over a more specific sibling such as /a/file{$slug}.txt whenever its guard returns truthy. That changes matching semantics for concrete paths instead of only breaking ties between otherwise equivalent dynamic routes.

Suggested comparator order
 function sortDynamic(
   a: {
     prefix?: string
     suffix?: string
     caseSensitive: boolean
     parse: null | ((params: Record<string, string>) => unknown)
   },
   b: {
     prefix?: string
     suffix?: string
     caseSensitive: boolean
     parse: null | ((params: Record<string, string>) => unknown)
   },
 ) {
-  if (a.parse && !b.parse) return -1
-  if (!a.parse && b.parse) return 1
   if (a.prefix && b.prefix && a.prefix !== b.prefix) {
     if (a.prefix.startsWith(b.prefix)) return -1
     if (b.prefix.startsWith(a.prefix)) return 1
   }
   if (a.suffix && b.suffix && a.suffix !== b.suffix) {
     if (a.suffix.endsWith(b.suffix)) return -1
     if (b.suffix.endsWith(a.suffix)) return 1
   }
   if (a.prefix && !b.prefix) return -1
   if (!a.prefix && b.prefix) return 1
   if (a.suffix && !b.suffix) return -1
   if (!a.suffix && b.suffix) return 1
   if (a.caseSensitive && !b.caseSensitive) return -1
   if (!a.caseSensitive && b.caseSensitive) return 1
+  if (a.parse && !b.parse) return -1
+  if (!a.parse && b.parse) return 1

   return 0
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/new-process-route-tree.ts` around lines 431 - 458,
The comparator currently checks a.parse vs b.parse first, which lets guarded
dynamic routes outrank more specific patterns; move the parse presence checks
(the lines "if (a.parse && !b.parse) ... if (!a.parse && b.parse) ...") so they
occur after the prefix, suffix and caseSensitive comparisons (i.e., after the
blocks that compare a.prefix/a.suffix and a.caseSensitive) so that parse only
breaks ties among otherwise equal-specific routes; keep the final "return 0" to
preserve stable declaration order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/router/api/router/RouteOptionsType.md`:
- Around line 86-97: Update the docs for params.parse to document that it may
return false to skip a route (same behavior as throwing), mention that this
interacts with params.match/params.matchPriority so routes can be skipped either
during match (params.match returning false) or during parse (params.parse
returning false or throwing), and add a short note in the RouteOptionsType.md
section for params.parse explaining returning false skips the route and how that
affects matching priority/tie-breaking with params.matchPriority; reference
params.parse, params.match, and params.matchPriority in the text so readers can
discover the migration path.

In `@packages/router-core/src/router.ts`:
- Line 975: The routeBranchCache (private routeBranchCache) is never cleared
when update() replaces the route tree from processRouteTree, so buildLocation()
may use stale ancestor chains; modify update() (the code that applies
processRouteTree results) to invalidate/clear routeBranchCache (e.g.,
routeBranchCache = new WeakMap() or routeBranchCache.clear()) whenever the route
tree is rebuilt, and apply the same invalidation in the other code paths that
rebuild the tree (the alternate update/path-rebuild block corresponding to the
second region referenced) so buildLocation() and related logic always compute
fresh ancestor chains.

In `@packages/solid-router/tests/useParams.test.tsx`:
- Around line 191-192: The test's flaky assertion checks a dev-only parse side
effect via mockedfn; make it env-aware by gating that expectation on the runtime
environment (e.g. only call expect(mockedfn).toHaveBeenCalled() when
process.env.NODE_ENV !== 'production'), or better replace the parse-side-effect
assertion with a stable assertion of the rendered navigation result (assert the
DOM/route outcome produced by the navigation instead of relying on mockedfn).
Locate the assertion referencing mockedfn in the useParams.test.tsx test and
update it accordingly.

In `@packages/vue-router/tests/useParams.test.tsx`:
- Around line 196-197: The test's expect(mockedfn).toHaveBeenCalled() depends on
a dev-only side-effect; make the assertion env-aware by either wrapping it in a
conditional: if (process.env.NODE_ENV !== 'production')
expect(mockedfn).toHaveBeenCalled(), or replace it with a stable assertion that
checks the navigation/rendered state instead (e.g., assert the expected
DOM/render result or router state) so the test doesn't rely on the dev-only
roundtrip validation; locate the usage of mockedfn in the useParams.test.tsx
test and apply one of these fixes.

---

Outside diff comments:
In `@packages/router-core/src/new-process-route-tree.ts`:
- Around line 431-458: The comparator currently checks a.parse vs b.parse first,
which lets guarded dynamic routes outrank more specific patterns; move the parse
presence checks (the lines "if (a.parse && !b.parse) ... if (!a.parse &&
b.parse) ...") so they occur after the prefix, suffix and caseSensitive
comparisons (i.e., after the blocks that compare a.prefix/a.suffix and
a.caseSensitive) so that parse only breaks ties among otherwise equal-specific
routes; keep the final "return 0" to preserve stable declaration order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6176814-b269-401e-83e7-25bb7e14a8ab

📥 Commits

Reviewing files that changed from the base of the PR and between db920c6 and af0a966bf89992c4dbc215ffcf7c7be157821d5e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • .changeset/clean-routes-match.md
  • docs/router/api/router/RouteOptionsType.md
  • e2e/react-router/match-params/.gitignore
  • e2e/react-router/match-params/index.html
  • e2e/react-router/match-params/package.json
  • e2e/react-router/match-params/playwright.config.ts
  • e2e/react-router/match-params/src/main.tsx
  • e2e/react-router/match-params/tests/match-params.spec.ts
  • e2e/react-router/match-params/tsconfig.json
  • e2e/react-router/match-params/vite.config.js
  • packages/react-router/src/RouterProvider.tsx
  • packages/react-router/src/link.tsx
  • packages/react-router/tests/useParams.test.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/path.ts
  • packages/router-core/src/route.ts
  • packages/router-core/src/router.ts
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/build-location.test.ts
  • packages/router-core/tests/match-params.test.ts
  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/tests/path.test.ts
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
  • packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
  • packages/solid-router/src/link.tsx
  • packages/solid-router/tests/useParams.test.tsx
  • packages/start-plugin-core/src/rsbuild/plugin.ts
  • packages/start-plugin-core/src/start-compiler/handleCreateServerFn.ts
  • packages/vue-router/src/link.tsx
  • packages/vue-router/tests/useParams.test.tsx
💤 Files with no reviewable changes (1)
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
✅ Files skipped from review due to trivial changes (10)
  • e2e/react-router/match-params/.gitignore
  • packages/start-plugin-core/src/start-compiler/handleCreateServerFn.ts
  • packages/solid-router/src/link.tsx
  • .changeset/clean-routes-match.md
  • packages/start-plugin-core/src/rsbuild/plugin.ts
  • e2e/react-router/match-params/package.json
  • e2e/react-router/match-params/tsconfig.json
  • packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
  • packages/vue-router/src/link.tsx
  • e2e/react-router/match-params/tests/match-params.spec.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • e2e/react-router/match-params/vite.config.js
  • e2e/react-router/match-params/index.html
  • packages/router-core/src/route.ts
  • packages/router-core/tests/build-location.test.ts
  • packages/router-core/src/index.ts
  • e2e/react-router/match-params/playwright.config.ts
  • e2e/react-router/match-params/src/main.tsx

Comment thread docs/router/api/router/RouteOptionsType.md Outdated
Comment thread packages/router-core/src/router.ts
Comment thread packages/solid-router/tests/useParams.test.tsx
Comment thread packages/vue-router/tests/useParams.test.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/new-process-route-tree.ts (1)

427-459: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep structural specificity ahead of parse precedence.

Comparing a.parse/b.parse before prefix/suffix makes a generic parsed route like /$id outrank a more specific unparsed route like /prefix{$id} for /prefix123. That changes the existing specificity rules and can send matching to the wrong branch even when both candidates match. parse should only break ties after the structural comparisons.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/new-process-route-tree.ts` around lines 427 - 459,
In sortDynamic, the parse-precedence checks (a.parse / b.parse) must be moved
after the structural comparisons so structural specificity (a.prefix / b.prefix,
a.suffix / b.suffix, and a.caseSensitive / b.caseSensitive) wins first; update
the function so you first compare prefix startsWith logic, suffix endsWith
logic, presence of prefix/suffix, and caseSensitive, and only if those yield
equality evaluate a.parse vs b.parse to break ties (preserving the final return
0 for stable sort).
♻️ Duplicate comments (1)
packages/router-core/src/router.ts (1)

1357-1364: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear routeBranchCache when the route tree is rebuilt.

buildRouteBranch() is cached by route object identity, but processRouteTree() reinitializes parentRoute on reused route instances during update(). Since setRoutes() never resets this WeakMap, a rebuild can keep returning stale ancestor chains here and make the exact-path fast path use the wrong route branch.

Suggested fix
   setRoutes({
     routesById,
     routesByPath,
     processedTree,
   }: ProcessRouteTreeResult<TRouteTree>) {
     this.routesById = routesById as RoutesById<TRouteTree>
     this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
     this.processedTree = processedTree
+    this.routeBranchCache = new WeakMap()

     const notFoundRoute = this.options.notFoundRoute
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/router.ts` around lines 1357 - 1364, The
routeBranchCache WeakMap can hold stale branch data when the route tree is
rebuilt; update setRoutes (or the function that fully reconstructs parentRoute,
e.g., processRouteTree/update) to clear or reinitialize routeBranchCache so
getRouteBranch and buildRouteBranch always compute fresh branches for the new
tree; specifically, add a routeBranchCache.clear() (or assign a new WeakMap to
routeBranchCache) at the start/end of setRoutes/processRouteTree so that cached
entries keyed by old route identity are invalidated when parentRoute
relationships are reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/router-core/src/router.ts`:
- Around line 1873-1896: The fast-path uses routesByPath and
this.getRouteBranch(destRoute) which can skip runtime matching (and thus
route-level validateSearch/search middlewares); instead call
this.getMatchedRoutes(nextTo) and use its matchedRoutes unless the matched leaf
equals the routesByPath entry. Concretely, after resolving destRoute from
routesByPath, compute destMatchResult = this.getMatchedRoutes(nextTo) and set
destRoutes = destMatchResult.matchedRoutes; only if destMatchResult.foundRoute
=== destRoute (i.e., the matched leaf is the same route object) may you replace
destRoutes with this.getRouteBranch(destRoute); otherwise keep matchedRoutes so
validateSearch and middlewares run.

---

Outside diff comments:
In `@packages/router-core/src/new-process-route-tree.ts`:
- Around line 427-459: In sortDynamic, the parse-precedence checks (a.parse /
b.parse) must be moved after the structural comparisons so structural
specificity (a.prefix / b.prefix, a.suffix / b.suffix, and a.caseSensitive /
b.caseSensitive) wins first; update the function so you first compare prefix
startsWith logic, suffix endsWith logic, presence of prefix/suffix, and
caseSensitive, and only if those yield equality evaluate a.parse vs b.parse to
break ties (preserving the final return 0 for stable sort).

---

Duplicate comments:
In `@packages/router-core/src/router.ts`:
- Around line 1357-1364: The routeBranchCache WeakMap can hold stale branch data
when the route tree is rebuilt; update setRoutes (or the function that fully
reconstructs parentRoute, e.g., processRouteTree/update) to clear or
reinitialize routeBranchCache so getRouteBranch and buildRouteBranch always
compute fresh branches for the new tree; specifically, add a
routeBranchCache.clear() (or assign a new WeakMap to routeBranchCache) at the
start/end of setRoutes/processRouteTree so that cached entries keyed by old
route identity are invalidated when parentRoute relationships are reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdd45c0d-8a93-44ea-b12f-12f14ead179d

📥 Commits

Reviewing files that changed from the base of the PR and between af0a966bf89992c4dbc215ffcf7c7be157821d5e and e0dd96c346886df8a80ee5c2933989ce55ec29a0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • .changeset/clean-routes-match.md
  • docs/router/api/router/RouteOptionsType.md
  • e2e/react-router/match-params/.gitignore
  • e2e/react-router/match-params/index.html
  • e2e/react-router/match-params/package.json
  • e2e/react-router/match-params/playwright.config.ts
  • e2e/react-router/match-params/src/main.tsx
  • e2e/react-router/match-params/tests/match-params.spec.ts
  • e2e/react-router/match-params/tsconfig.json
  • e2e/react-router/match-params/vite.config.js
  • packages/react-router/src/RouterProvider.tsx
  • packages/react-router/src/link.tsx
  • packages/react-router/tests/useParams.test.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/path.ts
  • packages/router-core/src/route.ts
  • packages/router-core/src/router.ts
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/build-location.test.ts
  • packages/router-core/tests/match-params.test.ts
  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/tests/path.test.ts
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
  • packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
  • packages/solid-router/src/link.tsx
  • packages/solid-router/tests/useParams.test.tsx
  • packages/start-plugin-core/src/rsbuild/plugin.ts
  • packages/start-plugin-core/src/start-compiler/handleCreateServerFn.ts
  • packages/vue-router/src/link.tsx
  • packages/vue-router/tests/useParams.test.tsx
💤 Files with no reviewable changes (1)
  • packages/router-core/tests/skip-route-on-parse-error.test.ts
✅ Files skipped from review due to trivial changes (11)
  • e2e/react-router/match-params/vite.config.js
  • packages/start-plugin-core/src/rsbuild/plugin.ts
  • e2e/react-router/match-params/index.html
  • packages/start-plugin-core/src/start-compiler/handleCreateServerFn.ts
  • e2e/react-router/match-params/package.json
  • .changeset/clean-routes-match.md
  • packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
  • packages/router-core/src/index.ts
  • packages/solid-router/src/link.tsx
  • packages/vue-router/src/link.tsx
  • packages/router-core/tests/build-location.test.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/react-router/src/RouterProvider.tsx
  • packages/router-core/src/path.ts
  • e2e/react-router/match-params/tsconfig.json
  • packages/vue-router/tests/useParams.test.tsx
  • packages/solid-router/tests/useParams.test.tsx
  • e2e/react-router/match-params/playwright.config.ts
  • docs/router/api/router/RouteOptionsType.md
  • packages/react-router/tests/useParams.test.tsx
  • packages/router-core/tests/new-process-route-tree.test.ts
  • e2e/react-router/match-params/.gitignore
  • packages/react-router/src/link.tsx
  • e2e/react-router/match-params/src/main.tsx
  • e2e/react-router/match-params/tests/match-params.spec.ts

Comment thread packages/router-core/src/router.ts Outdated
Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a flaky task in your failed CI:

🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.

Nx Cloud View detailed reasoning in Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

@schiller-manuel schiller-manuel changed the title match params feat: match params Apr 30, 2026
@schiller-manuel schiller-manuel merged commit c992495 into main Apr 30, 2026
16 of 17 checks passed
@schiller-manuel schiller-manuel deleted the match-params branch April 30, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants