Skip to content

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Nov 26, 2025

Fixes #5969

Summary by CodeRabbit

  • Bug Fixes

    • Improved route matching to prefer the more specific route when both a specific match and a wildcard match exist, ensuring correct route selection in ambiguous cases.
  • Tests

    • Added test coverage for edge cases involving trailing empty wildcards and optional segments to verify correct matching behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

Updates route matching so when both a bestMatch and wildcardMatch exist the algorithm chooses the more specific frame via isFrameMoreSpecific before continuing normal match checks. Adds an edge-case test suite for trailing empty wildcard matching (duplicate suite appears twice).

Changes

Cohort / File(s) Summary
Route matching logic
packages/router-core/src/new-process-route-tree.ts
When both bestMatch and wildcardMatch are present, compare specificity with isFrameMoreSpecific and select the more specific match before the existing bestMatch logic. No exported signatures changed.
Tests — trailing empty wildcard (duplicate suite)
packages/router-core/tests/new-process-route-tree.test.ts
Added an edge-case test suite "edge-case #5969: trailing empty wildcard should match" with four test cases covering basic matching, layout route interactions, index-route precedence, and a deeper optional-segment case. The suite appears twice (duplicate blocks).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Request as HTTP Request (path)
  participant Router as new-process-route-tree
  participant isSpec as isFrameMoreSpecific

  Note right of Router `#d1f0d9`: match discovery phase
  Request->>Router: find candidate frames (bestMatch, wildcardMatch, others)
  alt both bestMatch & wildcardMatch exist
    Router->>isSpec: isFrameMoreSpecific(bestMatch, wildcardMatch)
    alt bestMatch more specific
      isSpec-->>Router: bestMatch
      Router->>Router: select bestMatch
    else wildcard more specific
      isSpec-->>Router: wildcardMatch
      Router->>Router: select wildcardMatch
    end
  else one or none present
    Router->>Router: proceed with existing bestMatch checks
  end
  Router->>Router: continue standard resolution & return chosen match
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Areas to inspect closely:
    • Correctness and ordering of the new isFrameMoreSpecific comparison relative to existing guards.
    • Interaction with index-route vs. trailing-wildcard semantics (tests added cover several cases).
    • Duplicate test-suite blocks (ensure duplicates are intentional or remove one).

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • nlynzaad

Poem

🐰 I hopped through routes both wide and spry,
A wildcard and a match caught my eye,
I sniffed who was finer, who fit the map,
Chose the truest path — a tidy little clap! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Out of Scope Changes check ❓ Inconclusive The PR contains duplicate test suite blocks in the test file, which may indicate incomplete changes or accidental duplication unrelated to fixing the wildcard matching issue. Review and remove duplicate test suite blocks in new-process-route-tree.test.ts to ensure changes are intentional and focused on the fix.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(router-core): empty wildcard node is matched over sibling layout route' clearly describes the main change: fixing behavior where an empty wildcard node should match instead of a sibling layout route.
Linked Issues check ✅ Passed The PR addresses issue #5969 by restoring prior behavior where both shallow and trailing-splat routes match paths. Changes modify node matching logic to return the more specific match and add test cases validating trailing empty wildcard matching.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-router-core-empty-wildcard-beats-layout-route-match

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

@nx-cloud
Copy link

nx-cloud bot commented Nov 26, 2025

View your CI Pipeline Execution ↗ for commit b11d530

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

☁️ Nx Cloud last updated this comment at 2025-11-26 12:49:38 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 26, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5971

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5971

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5971

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5971

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5971

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5971

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5971

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5971

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5971

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5971

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5971

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5971

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5971

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5971

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5971

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5971

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5971

commit: b11d530

Copy link
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: 0

🧹 Nitpick comments (2)
packages/router-core/src/new-process-route-tree.ts (1)

1021-1025: Specificity resolution between bestMatch and wildcardMatch looks correct but is subtle

The new logic correctly defers to isFrameMoreSpecific to pick between a concrete match and a wildcard match, and the choice of argument order:

if (bestMatch && wildcardMatch) {
  return isFrameMoreSpecific(wildcardMatch, bestMatch)
    ? bestMatch
    : wildcardMatch
}

means:

  • If bestMatch is strictly more specific than wildcardMatch, you return bestMatch.
  • On ties, isFrameMoreSpecific returns false, so the wildcard wins, which matches the new tests (e.g. /a vs /a/$ and index vs wildcard).

This aligns with the intended behavior and the pre‑existing comparator semantics.

Because the helper is documented as “prev best, next candidate”, using wildcardMatch as prev is a bit non‑intuitive on first read. A tiny inline comment here (e.g. “// wildcard wins ties, so treat it as prev”) would make this intent clearer to future readers, but functionally the change looks sound.

packages/router-core/tests/new-process-route-tree.test.ts (1)

549-565: New trailing-empty-wildcard tests are well targeted

These tests precisely cover the intended behavior:

  • /a/$ matching both /a and /a/.
  • With a sibling layout route, the trailing wildcard wins (/a + /a/$/a/$).
  • With a sibling index route, the index correctly wins over the wildcard (/a/ + /a/$/a/).

They effectively lock in the fix for #5969 and protect the index‑vs‑wildcard precedence. If you want even more confidence later, you could add a deeper variant mirroring the original report (e.g. /alpha/bravo/$charlie vs /alpha/bravo/$charlie/$), but the current coverage is already solid.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7ba00c and 7902736.

📒 Files selected for processing (2)
  • packages/router-core/src/new-process-route-tree.ts (1 hunks)
  • packages/router-core/tests/new-process-route-tree.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety throughout the codebase

Files:

  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/src/new-process-route-tree.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
📚 Learning: 2025-09-28T21:41:45.233Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/src/new-process-route-tree.ts
📚 Learning: 2025-10-14T18:59:33.990Z
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
📚 Learning: 2025-11-25T00:18:21.258Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.258Z
Learning: Applies to packages/solid-router/**/*.{ts,tsx} : Solid Router components and primitives should use the tanstack/solid-router package

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
📚 Learning: 2025-11-25T00:18:21.258Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.258Z
Learning: Applies to **/src/routes/**/*.{ts,tsx} : Use file-based routing in src/routes/ directories or code-based routing with route definitions

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
🧬 Code graph analysis (1)
packages/router-core/tests/new-process-route-tree.test.ts (1)
packages/router-core/src/new-process-route-tree.ts (1)
  • findRouteMatch (612-634)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test
  • GitHub Check: Preview
  • GitHub Check: autofix

Copy link
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: 0

🧹 Nitpick comments (1)
packages/router-core/tests/new-process-route-tree.test.ts (1)

549-572: Consider asserting splat params for the empty wildcard case

The new tests nicely pin down the routing priority around /a/$, layouts, and index routes. To fully lock in the behavior for the empty wildcard specifically, it would help to also assert the splat params in the "basic" case (similar to the existing root wildcard tests), e.g. that the empty tail yields { '*': '', _splat: '' }.

That way future refactors can’t accidentally change the param semantics while still passing on route IDs.

Example diff:

-      it('basic', () => {
-        const tree = makeTree(['/a/$'])
-        expect(findRouteMatch('/a/', tree)?.route.id).toBe('/a/$')
-        expect(findRouteMatch('/a', tree)?.route.id).toBe('/a/$')
-      })
+      it('basic', () => {
+        const tree = makeTree(['/a/$'])
+
+        const withSlash = findRouteMatch('/a/', tree)
+        expect(withSlash?.route.id).toBe('/a/$')
+        expect(withSlash?.params).toEqual({ '*': '', _splat: '' })
+
+        const withoutSlash = findRouteMatch('/a', tree)
+        expect(withoutSlash?.route.id).toBe('/a/$')
+        expect(withoutSlash?.params).toEqual({ '*': '', _splat: '' })
+      })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7902736 and b11d530.

📒 Files selected for processing (1)
  • packages/router-core/tests/new-process-route-tree.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety throughout the codebase

Files:

  • packages/router-core/tests/new-process-route-tree.test.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
📚 Learning: 2025-10-14T18:59:33.990Z
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
📚 Learning: 2025-09-28T21:41:45.233Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
📚 Learning: 2025-11-25T00:18:21.258Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.258Z
Learning: Applies to packages/solid-router/**/*.{ts,tsx} : Solid Router components and primitives should use the tanstack/solid-router package

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
📚 Learning: 2025-11-25T00:18:21.258Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.258Z
Learning: Applies to **/src/routes/**/*.{ts,tsx} : Use file-based routing in src/routes/ directories or code-based routing with route definitions

Applied to files:

  • packages/router-core/tests/new-process-route-tree.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test

@Sheraff Sheraff merged commit 1718199 into main Nov 26, 2025
6 checks passed
@Sheraff Sheraff deleted the fix-router-core-empty-wildcard-beats-layout-route-match branch November 26, 2025 12:56
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.

Change in behavior for nested routing with trailing splat

2 participants