Skip to content

Conversation

nlynzaad
Copy link
Contributor

@nlynzaad nlynzaad commented Sep 21, 2025

#4933 highlighted a case where index routes would not resolve when an optional param is followed by a required path param. this pr resolves #4933

apart from this specific issue, this PR updates isMatch to unlock some more variations in the usage of optional params including:

{-$optional}.$pathParam.index
{-$optional}.{-$optional}.$pathParam.path (with and without index)
{-$optional}.$pathParam.{-$optional}.path (with and without index)
{-$optional}.path.{-$optional}.$pathParam (with and without index)
{-$optional}.path.$pathParam.{-$optional} (with and without index)
etc.

bunch of unit tests added to match-path tests to test various variations and completions.
also added e2e tests on react and solid to test functionality of this. not all variations are covered here since they are covered in the unit test. this is just to ensure findings from unit tests carry over to e2e.

seperate PR to follow to merge into alpha

Summary by CodeRabbit

  • New Features

    • Improved route matching for complex optional/required parameter combinations and many new optional-params demo pages (React & Solid).
  • Documentation

    • Expanded guide with additional optional-parameter routing examples covering multi-segment and mixed-structure routes.
  • Tests

    • Added extensive end-to-end suites (React & Solid) and broadened core path-matching tests for trailing slashes, wildcards, case sensitivity, and complex optional sequences.

Copy link
Contributor

coderabbitai bot commented Sep 21, 2025

Walkthrough

Adds comprehensive optional-parameter examples and tests for React and Solid, introduces many new file-based optional-params routes, removes the React generated routeTree.gen.ts, updates Solid's generated route tree, and changes core path matching to a stateful lookahead-driven, recursive algorithm for optional segments.

Changes

Cohort / File(s) Summary
Docs: Optional path params
docs/router/framework/react/guide/path-params.md
Adds multiple optional-parameter example patterns covering multi-segment and mixed optional/required combinations.
React: Generated route tree (removed)
e2e/react-router/basic-file-based/src/routeTree.gen.ts
Deleted auto-generated React route tree and its many exported route constants, type maps, and TanStack React Router module augmentations.
React: Optional-params routes
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx, .../simple/{-$id}.index.tsx, .../simple/{-$id}.path.tsx, .../withIndex/{-$id}.$category.*, .../withRequiredParam/{-$id}.$category.{-$slug}.info.tsx, .../withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx, .../consecutive/{-$id}.{-$slug}.$category.info.tsx, .../single/*
Adds file-based React routes under /optional-params; each exports a Route via createFileRoute(...) and renders params via Route.useParams() for examples/tests.
React: E2E tests
e2e/react-router/basic-file-based/tests/optionalParams.spec.ts
New Playwright suite validating hrefs, navigation, pathname, headings, and JSON-serialized params across many optional/required route permutations.
Solid: Generated route tree (added/updated)
e2e/solid-router/basic-file-based/src/routeTree.gen.ts
Adds /optional-params group with many child route constants, wiring, and module augmentations for Solid router.
Solid: Optional-params routes
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx, .../simple/{-$id}.*, .../withIndex/{-$id}.$category.*, .../withRequiredParam/{-$id}.$category.{-$slug}.info.tsx, .../withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx, .../consecutive/{-$id}.{-$slug}.$category.info.tsx
Adds file-based Solid routes mirroring the React examples; each exports Route and renders params for tests.
Solid: E2E tests
e2e/solid-router/basic-file-based/tests/optionalParams.spec.ts
New Playwright tests for Solid routes covering the same optional/mixed param scenarios.
Core: Path matching logic
packages/router-core/src/path.ts
Reworks optional-segment handling to a stateful lookahead-driven algorithm: tracks processed optionals, analyzes future route segments (including recursive isMatch probing), and refines match vs skip decisions for optional params.
Core: Unit tests expanded
packages/router-core/tests/match-by-path.test.ts
Large expansion of parameterized tests covering trailing-slash variants, complex optional/wildcard permutations, consecutive optionals, and case-insensitive/fuzzy matching modes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant Router
  participant Matcher as PathMatcher
  participant Lookahead as OptionalLookahead

  App->>Router: navigate(to, params)
  Router->>Matcher: isMatch(routePattern, urlSegments)
  Matcher->>Lookahead: evaluate current optional (processedOptionals state)
  alt lookahead finds viable future match
    Note right of Lookahead #DFF2E1: bind optional param and advance
    Lookahead-->>Matcher: matched (advance both)
    Matcher->>Matcher: continue matching remaining segments
  else lookahead cannot satisfy later segments
    Note right of Lookahead #FFF3D9: skip optional (advance route index, may recurse)
    Lookahead-->>Matcher: skipped (possibly recurse into isMatch)
    Matcher->>Matcher: recurse/continue matching remaining segments
  end
  Matcher-->>Router: match result + resolved params
  Router-->>App: render matched component
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

package: react-router, package: solid-router

Suggested reviewers

  • schiller-manuel
  • Sheraff

Poem

I hop through segments, optional and spry,
I peek ahead where hidden slugs lie.
If future paths call, I bind what I see,
If not, I skip lightly and set the route free.
Tests clap their paws — hooray for routes that agree! 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The removal of e2e/react-router/basic-file-based/src/routeTree.gen.ts (deleting the generated route-tree exports) is not described in the PR objectives and appears unrelated to the router-core matching fix; that deletion can affect public exports and the React e2e harness. Other changes (router-core logic, extensive tests, and new optional-params route files) are in-scope for validating matching behavior, but the unexplained deletion of a generated artifact is unexpected and potentially disruptive. This constitutes an out-of-scope or at least insufficiently-justified change. Either restore the deleted routeTree.gen.ts or provide a clear justification and replacement (for example an updated generation step or alternate wiring) in the PR description, update the e2e React harness/CI to reflect the intentional change, and confirm all e2e tests pass in CI before merging.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "fix(router-core): optional-path-param usage variation matches" clearly identifies the subsystem (router-core) and the intent to fix matching behavior for optional path parameters; it aligns with the PR objective to update isMatch. It is concise and actionable for a reviewer scanning history. The phrasing is slightly awkward but not misleading.
Linked Issues Check ✅ Passed The changes to packages/router-core/src/path.ts introduce lookahead, recursion, and per-call optional bookkeeping specifically to improve optional-param matching, and the repo adds broad unit tests in packages/router-core/tests/match-by-path.test.ts plus React and Solid e2e tests that exercise the described permutations. These code and test updates directly target the failure mode described in issue #4933 (index route not resolving when an optional param is followed by a required param) and thus satisfy the linked issue's coding objective. I see the intended fix and validation coverage present in the diff.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch optional-param-required-param-index-route

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 956d51a and 659b011.

📒 Files selected for processing (1)
  • e2e/react-router/basic-file-based/tests/optionalParams.spec.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/react-router/basic-file-based/tests/optionalParams.spec.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). (2)
  • GitHub Check: Preview
  • GitHub Check: Test

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

@github-actions github-actions bot added documentation Everything documentation related package: router-core labels Sep 21, 2025
Copy link

nx-cloud bot commented Sep 21, 2025

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 659b011

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

☁️ Nx Cloud last updated this comment at 2025-09-21 13:37:40 UTC

@nlynzaad nlynzaad changed the title fix(router-core): optional-path-param usage variations fix(router-core): optional-path-param usage variation matches Sep 21, 2025
Copy link

pkg-pr-new bot commented Sep 21, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-plugin

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-plugin

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-server-functions-client

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

@tanstack/start-server-functions-fetcher

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

@tanstack/start-server-functions-server

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/zod-adapter

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

commit: 659b011

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: 2

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/path.ts (1)

758-770: Fix lookahead bugs: wrong base slice and param side‑effects; keep optional counters consistent

  • The lookahead recursion slices baseSegments using lookAhead (a route index), which desynchronizes route/base alignment. Use baseIndex (or baseIndex + (lookAhead - routeIndex) if you advance base) instead.
  • isMatch recursion mutates the live params object during speculative lookahead. Pass a scratch object to avoid polluting params on failed lookaheads.
  • When skipping an optional due to missing baseSegment or encountering '/', processedOptionals is not incremented, skewing remainingOptionals and routeSegmentLength math for subsequent decisions.

Apply this minimal patch:

@@
-        if (!baseSegment) {
-          // No base segment for optional param - skip this route segment
-          routeIndex++
-          continue
-        }
+        if (!baseSegment) {
+          // No base segment for optional param - skip this route segment
+          processedOptionals++
+          routeIndex++
+          continue
+        }
@@
-        if (baseSegment.value === '/') {
-          // Skip slash segments for optional params
-          routeIndex++
-          continue
-        }
+        if (baseSegment.value === '/') {
+          // Skip slash segments for optional params
+          processedOptionals++
+          routeIndex++
+          continue
+        }
@@
-              const remainingRouteSegments = routeSegments.slice(
-                lookAhead + 1,
-              )
+              const remainingRouteSegments = routeSegments.slice(lookAhead + 1)
@@
-              const remainingBaseSegments = baseSegments.slice(lookAhead)
+              // Align to the current base position; skipping optionals consumes no base segments
+              const remainingBaseSegments = baseSegments.slice(baseIndex)
@@
-              isMatchedFurtherDown =
-                remainingRouteSegmentLength ===
-                  remainingBaseSegments.length &&
-                isMatch(
-                  remainingBaseSegments,
-                  remainingRouteSegments,
-                  params,
-                  fuzzy,
-                  caseSensitive,
-                )
+              {
+                const scratchParams: Record<string, string> = {}
+                isMatchedFurtherDown =
+                  remainingRouteSegmentLength ===
+                    remainingBaseSegments.length &&
+                  isMatch(
+                    remainingBaseSegments,
+                    remainingRouteSegments,
+                    scratchParams,
+                    fuzzy,
+                    caseSensitive,
+                  )
+              }
@@
-        } else {
-          skippedOptionals++
-        }
+        } else {
+          skippedOptionals++
+        }
 
         processedOptionals++

These fixes prevent false negatives/positives in optional chains and stop accidental param leakage during speculative checks.

Also applies to: 805-812, 821-836, 850-905, 919-924

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

821-836: Comment/code mismatch (nit): “bail out” vs behavior

The comment says we “bail out” on a non-matching future pathname, but the code just breaks the lookahead and proceeds (often correctly matching the optional). Either adjust the comment or add an explicit decision branch.

packages/router-core/tests/match-by-path.test.ts (1)

588-595: Non‑nested case: missing “_” in route token

The last entry uses /B/ instead of /B_/, inconsistent with the non‑nested pattern used elsewhere. Likely a typo that weakens the assertion.

-      ['/a/1/b/2', '/A_/{-$id}_/B/{-$id}_', { id: '2' }],
+      ['/a/1/b/2', '/A_/{-$id}_/B_/{-$id}_', { id: '2' }],
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)

12-15: Trailing-slash mismatch in heading text.

Path is defined with a trailing slash (/path/), but the heading omits it. Align to avoid brittle e2e assertions.

Apply this diff:

-      Hello "/optional-params/withIndex/-$id/$category/path"!
+      Hello "/optional-params/withIndex/-$id/$category/path/"!
e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1)

14-17: Placeholder typo: missing “-” before $slug in heading.

The route uses {-$slug} but the heading shows $slug. Fix for consistency and to prevent e2e flakiness.

Apply this diff:

-        "/optional-params/withRequiredInBetween/-$id/$category/path/$slug"!
+        "/optional-params/withRequiredInBetween/-$id/$category/path/-$slug"!
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)

12-15: Trailing-slash mismatch in heading text.

Path is /path/ but heading shows /path. Align for consistency with Solid and to avoid brittle tests.

Apply this diff:

-      Hello "/optional-params/withIndex/-$id/$category/path"!
+      Hello "/optional-params/withIndex/-$id/$category/path/"!
docs/router/framework/react/guide/path-params.md (5)

137-151: Fix grammar and example comments; clarify wildcard vs prefixed param

  • “combines” → “combine”
  • Align file path comments with route code.
  • Clarify that postId is captured by the prefixed param and _splat captures the trailing wildcard.

Apply:

-You can even combines prefixes with wildcard routes to create more complex patterns:
+You can even combine prefixes with wildcard routes to create more complex patterns:
@@
-// src/routes/on-disk/storage-{$}
-export const Route = createFileRoute('/on-disk/storage-{$postId}/$')({
+// src/routes/on-disk/storage-{$postId}/$.tsx
+export const Route = createFileRoute('/on-disk/storage-{$postId}/$')({
@@
-function StorageComponent() {
-  const { _splat } = Route.useParams()
-  // _splat, will be value after 'storage-'
-  // i.e. my-drive/documents/foo.txt
-  return <div>Storage Location: /{_splat}</div>
-}
+function StorageComponent() {
+  const { postId, _splat } = Route.useParams()
+  // postId is the value after 'storage-'
+  // _splat is the remaining path (e.g. my-drive/documents/foo.txt)
+  return (
+    <div>
+      <div>Drive: {postId}</div>
+      <div>Storage Location: /{_splat}</div>
+    </div>
+  )
+}

155-168: Tighten suffix example wording and fix file path comment

  • Grammar: “match a URL a filename” → “match a URL for a filename”
  • The comment path should include the dot to match the code.
-Suffixes are defined by placing the suffix text outside the curly braces after the variable name. For example, if you want to match a URL a filename that ends with `txt`, you can define it like this:
+Suffixes are defined by placing the suffix text outside the curly braces after the variable name. For example, if you want to match a URL for a filename that ends with `.txt`, you can define it like this:
@@
-// src/routes/files/{$fileName}txt
+// src/routes/files/{$fileName}.txt

173-182: Make wildcard + suffix example consistent

Use the same fileName param in the comment as in the code.

-// src/routes/files/{$}[.]txt
+// src/routes/files/{$fileName}[.]txt

187-200: Align “prefix + suffix” example with description

The text mentions suffix .json but the example uses person. Prefer .json for consistency.

-// src/routes/users/user-{$userId}person
-export const Route = createFileRoute('/users/user-{$userId}person')({
+// src/routes/users/user-{$userId}.json
+export const Route = createFileRoute('/users/user-{$userId}.json')({
@@
-  // userId will be the value between 'user-' and 'person'
+  // userId will be the value between 'user-' and '.json'

342-351: Avoid duplicate hook calls

Destructure both values from a single useParams() call.

-function DocsComponent() {
-  const { version } = Route.useParams()
-  const { _splat } = Route.useParams()
+function DocsComponent() {
+  const { version, _splat } = Route.useParams()
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1)

10-12: Invalid list semantics:

    has non-
  • direct children

    <div> and <hr> under <ul> violate HTML semantics and can hinder accessibility. Either wrap those sections in <li> or replace <ul> with a non-list container.

    Minimal change (keeps layout, reduces a11y impact):

    -      <ul class="grid mb-2">
    +      <div role="list" class="grid mb-2">
    @@
    -        <hr />
    +        <hr />
    @@
    -        <hr />
    +        <hr />
    @@
    -        <hr />
    +        <hr />
    @@
    -      </ul>
    +      </div>

    If you want full semantic correctness, convert headings and section wrappers into <li> items instead.

    Also applies to: 53-54, 105-107, 149-151, 193-197, 238-241

e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)

10-12: Same list semantics issue as Solid

<ul> has <div> and <hr> as direct children. Mirror the Solid fix here.

-      <ul className="grid mb-2">
+      <div role="list" className="grid mb-2">
@@
-        <hr />
+        <hr />
@@
-        <hr />
+        <hr />
@@
-        <hr />
+        <hr />
@@
-      </ul>
+      </div>

Also applies to: 53-54, 105-107, 149-151, 193-197, 238-241

e2e/solid-router/basic-file-based/tests/optionalParams.spec.ts (2)

34-41: Prefer expect(page).toHaveURL(...) over waitForURL(...)

toHaveURL is more idiomatic, includes built-in waiting, and yields clearer errors. Same applies to other URL checks in this file.

Example:

-    await page.waitForURL('/optional-params/simple')
-    pagePathname = new URL(page.url()).pathname
-    expect(pagePathname).toBe('/optional-params/simple')
+    await expect(page).toHaveURL('/optional-params/simple')

Also applies to: 43-51, 52-60, 61-69


56-58: Stabilize assertions and reduce flake

  • Consider toBeVisible() instead of toBeInViewport() for reliability across CI environments.
  • You can DRY repeated patterns with small helpers (e.g., assertUrlAndHeading(url, headingTestId)).

I can provide a tiny helper module to de-duplicate these patterns if you want.

Also applies to: 111-115, 129-132, 205-216, 404-414

e2e/react-router/basic-file-based/tests/optionalParams.spec.ts (2)

34-41: Use toHaveURL in place of waitForURL

Same reasoning as the Solid suite; simplifies and clarifies expectations.

-    await page.waitForURL('/optional-params/simple')
-    pagePathname = new URL(page.url()).pathname
-    expect(pagePathname).toBe('/optional-params/simple')
+    await expect(page).toHaveURL('/optional-params/simple')

Also applies to: 43-51, 52-60, 61-69


56-58: Visibility assertion and helper extraction

Prefer toBeVisible() over toBeInViewport() for headings; consider extracting repeated steps.

Happy to draft a small test util with navigateAndAssert(url, headingTestId, paramsTestId, expectedParams) to cut repetition.

Also applies to: 111-115, 129-132, 205-216, 404-414

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d00f28 and afd99a9.

📒 Files selected for processing (23)
  • docs/router/framework/react/guide/path-params.md (1 hunks)
  • e2e/react-router/basic-file-based/src/routeTree.gen.ts (16 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (1 hunks)
  • e2e/react-router/basic-file-based/tests/optionalParams.spec.ts (1 hunks)
  • e2e/solid-router/basic-file-based/src/routeTree.gen.ts (16 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/tests/optionalParams.spec.ts (1 hunks)
  • packages/router-core/src/path.ts (3 hunks)
  • packages/router-core/tests/match-by-path.test.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (18)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (2)
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (3)
e2e/solid-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (7)
e2e/solid-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (2)
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (7)
e2e/react-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (2)
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (2)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (4)
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (7)
e2e/react-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (4)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (2)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
packages/router-core/tests/match-by-path.test.ts (1)
packages/router-core/src/path.ts (1)
  • matchByPath (563-603)
e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (7)
e2e/solid-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (1)
  • Route (3-7)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (2)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.path.index.tsx (1)
  • Route (3-7)
e2e/solid-router/basic-file-based/src/routeTree.gen.ts (1)
e2e/react-router/basic-file-based/src/routeTree.gen.ts (5)
  • FileRoutesByFullPath (525-601)
  • FileRoutesByTo (602-675)
  • FileRoutesById (676-757)
  • FileRouteTypes (758-992)
  • RootRouteChildren (993-1030)
e2e/react-router/basic-file-based/src/routeTree.gen.ts (1)
e2e/solid-router/basic-file-based/src/routeTree.gen.ts (5)
  • FileRoutesByFullPath (465-532)
  • FileRoutesByTo (533-597)
  • FileRoutesById (598-670)
  • FileRouteTypes (671-878)
  • RootRouteChildren (879-907)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (2)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)
  • Route (3-5)
⏰ 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). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (25)
e2e/solid-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)

3-7: Solid route and params usage look good

Correct path shape, and using Route.useParams() as an accessor is correct in Solid. Test IDs/readout are consistent with the e2e tests.

Also applies to: 9-24

packages/router-core/tests/match-by-path.test.ts (1)

51-66: Broad additions look solid

New cases comprehensively exercise optional-after-required, consecutive optionals, and trailing slash handling. These should catch regressions introduced by the lookahead logic.

Please run the updated suite against the patched isMatch to ensure no expectation changes are required.

Also applies to: 83-239, 241-288, 321-335, 348-417

e2e/react-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)

3-7: React route is correct; params usage matches React API

Path shape and index route (trailing slash) look right. Using Route.useParams() as an object is correct in React.

Also applies to: 9-17

e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)

3-5: Solid simple/path route LGTM

Correct accessor usage and test IDs align with the suite.

Also applies to: 7-15

e2e/solid-router/basic-file-based/src/routes/optional-params/withIndex/{-$id}.$category.index.tsx (1)

9-17: LGTM (Solid index route): params accessor usage is correct.

Route.useParams() is used as an accessor and the test IDs look consistent with the pattern.

e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)

7-14: LGTM (React index route): params object usage is correct.

Hook usage and test IDs look consistent with the simple optional pattern.

e2e/solid-router/basic-file-based/src/routes/optional-params/simple/{-$id}.index.tsx (1)

7-14: LGTM (Solid index route): params accessor usage is correct.

Accessor call and test IDs are consistent with the simple pattern.

e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredInBetween/{-$id}.$category.path.{-$slug}.tsx (1)

3-7: Solid route component and params usage look correct

Route definition, test IDs, and Solid’s Route.useParams() accessor usage (params()) align with the e2e tests.

Also applies to: 9-26

e2e/react-router/basic-file-based/src/routes/optional-params/consecutive/{-$id}.{-$slug}.$category.info.tsx (1)

3-7: React route wiring and params rendering LGTM

Correct createFileRoute path, test IDs, and Route.useParams() usage (object, not accessor) match the tests.

Also applies to: 9-24

e2e/react-router/basic-file-based/src/routes/optional-params/simple/{-$id}.path.tsx (1)

3-15: Route and test IDs are correct

Matches e2e expectations; Route.useParams() (object) usage is appropriate.

docs/router/framework/react/guide/path-params.md (1)

414-420: Type note — numeric params are allowed (auto‑stringified)

Confirmed: Link/To accept params: Record<string, unknown>, so params={{ category: 123 }} type‑checks and will be auto‑stringified for the path; parsed URL params remain strings.

e2e/react-router/basic-file-based/src/routeTree.gen.ts (7)

24-24: Optional-params imports wired correctly.

All new route imports resolve to expected paths under routes/optional-params/.

Also applies to: 77-77, 79-79, 86-86, 88-92


149-153: Registering the optional-params route group looks correct.

id, path, and parent match the new group.


431-437: Child routes for optional-param patterns are consistent and parented correctly.

Paths/ids map to the described patterns; trailing-slash conventions match the generator’s norms.

Also applies to: 442-447, 483-488, 494-499, 500-507, 508-515, 516-523


1111-1117: Module augmentation entries added for the group and all children.

preLoaderRoute and parentRoute bindings look correct.

Also applies to: 1482-1488, 1496-1502, 1545-1551, 1559-1565, 1566-1572, 1573-1579, 1580-1586


1616-1645: Route children map is complete for the new group.

All declared children are exported via OptionalParamsRouteRouteWithChildren.


1852-1854: Root children expose OptionalParams group.

Group is now directly navigable from root.


528-528: Verification blocked — permission denied reading generated route files.

Automated check couldn’t confirm optional-params across FileRoutesByFullPath/FileRoutesByTo/FileRoutesById in both routers because the script failed with: "/bin/bash: line 57: e2e/react-router/basic-file-based/src/routeTree.gen.ts: Permission denied". Grant read access and re-run the provided script, or paste the three export interface blocks (FileRoutesByFullPath, FileRoutesByTo, FileRoutesById) from these files so I can re-verify: e2e/react-router/basic-file-based/src/routeTree.gen.ts and e2e/solid-router/basic-file-based/src/routeTree.gen.ts.

e2e/solid-router/basic-file-based/src/routeTree.gen.ts (7)

23-23: Optional-params imports added.

Imports mirror the React variant and match on-disk routes.

Also applies to: 68-83


134-138: Optional-params group registration is correct.

Proper id/path under root.


371-376: Child routes for optional-params added with correct parenting and paths.

Patterns and trailing-slash handling align with generator behavior.

Also applies to: 382-387, 423-429, 434-439, 440-447, 448-455, 456-463


468-468: Type surfaces (FullPath/To/Id) reflect the new optional-params routes.

Coverage looks complete and consistent.

Also applies to: 513-516, 523-524, 528-531, 534-537, 578-581, 588-589, 593-596, 599-600, 602-603, 651-654, 661-669, 672-677


981-987: Module augmentation for @tanstack/solid-router updated.

All optional-params entries are present with correct preLoaderRoute bindings.

Also applies to: 1296-1302, 1310-1316, 1359-1365, 1373-1379, 1380-1386, 1387-1393, 1394-1400


1430-1455: Children map and with-children export added for the group.

Matches the route constants declared above.

Also applies to: 1457-1459


1664-1667: Root children now include OptionalParams group.

Expected for top-level navigation and typing.

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: 2

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/path.ts (1)

759-770: Skipping optionals on missing base or slash should bump the counters.

When !baseSegment or baseSegment.value === '/', you continue without updating processedOptionals/skippedOptionals. This desynchronizes later lookahead math.

-        if (!baseSegment) {
-          // No base segment for optional param - skip this route segment
-          routeIndex++
-          continue
-        }
+        if (!baseSegment) {
+          // No base segment for optional param - skip this route segment
+          skippedOptionals++
+          processedOptionals++
+          routeIndex++
+          continue
+        }

-        if (baseSegment.value === '/') {
-          // Skip slash segments for optional params
-          routeIndex++
-          continue
-        }
+        if (baseSegment.value === '/') {
+          // Skip slash segments for optional params
+          skippedOptionals++
+          processedOptionals++
+          routeIndex++
+          continue
+        }
🧹 Nitpick comments (1)
packages/router-core/src/path.ts (1)

614-618: Stateful optional counters add footguns; prefer deriving remaining counts per-iteration.

processedOptionals, skippedOptionals, and a global optionalCount are easy to desynchronize and are later used with total lengths (see comments below). Consider deriving remaining optionals from routeSegments.slice(routeIndex) when needed to avoid global state coupling.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afd99a9 and 1d76023.

📒 Files selected for processing (6)
  • e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx (1 hunks)
  • e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx (1 hunks)
  • packages/router-core/src/path.ts (3 hunks)
  • packages/router-core/tests/match-by-path.test.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx
  • e2e/solid-router/basic-file-based/src/routes/optional-params/route.tsx
  • e2e/solid-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx
  • e2e/react-router/basic-file-based/src/routes/optional-params/withRequiredParam/{-$id}.$category.{-$slug}.info.tsx
  • packages/router-core/tests/match-by-path.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). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (2)
packages/router-core/src/path.ts (2)

821-836: Good early disambiguation on PATHNAME lookahead.

Skipping the current optional when the next pathname equals the current base segment is correct and prevents over-greedy optional matches. Ensure both case-sensitive and insensitive test variants exist.


919-924: Counters update order/coverage.

Incrementing skippedOptionals and then processedOptionals here is fine for the branch that attempted a match. After applying the earlier fix for the early-continue cases, the invariants will hold across all branches.

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: 1

🧹 Nitpick comments (2)
packages/router-core/src/path.ts (2)

829-839: Comment/behavior mismatch for PATHNAME lookahead.

Comment says “invalid url ⇒ return undefined,” but code just breaks. Either adjust the comment or implement the early failure.

-              if (
+              const futureMatchesBase =
                 caseSensitive
                   ? futureRouteSegment.value === baseSegment.value
                   : futureRouteSegment.value.toLowerCase() ===
                     baseSegment.value.toLowerCase()
-              ) {
-                // so we should skip this optional parameter
-                shouldMatchOptional = false
-              }
-              break
+              if (futureMatchesBase) {
+                // skip this optional parameter
+                shouldMatchOptional = false
+                break
+              } else {
+                // no match ahead and a required pathname follows
+                return false
+              }

842-852: Consecutive optionals: code contradicts the “prefer first optional” note.

The condition sets shouldMatchOptional = false, effectively preferring a later optional. Either invert the condition or reword the comment to reflect actual intent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d76023 and dc6e771.

📒 Files selected for processing (1)
  • packages/router-core/src/path.ts (4 hunks)
🔇 Additional comments (1)
packages/router-core/src/path.ts (1)

883-895: Good: speculative recursion now slices base correctly and clones params.

baseSegments.slice(baseIndex + 1) and { ...params } avoid index mixups and side-effects. Matches earlier feedback.

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 (5)
packages/router-core/src/path.ts (5)

614-619: Remove unused skippedOptionals bookkeeping.

skippedOptionals is never read after being incremented. Drop the variable and its increments to simplify control flow.

Apply:

-  let processedOptionals = 0
-  let skippedOptionals = 0
+  let processedOptionals = 0

929-934: Delete dead increment of skippedOptionals.

Since skippedOptionals is not used, remove this branch to reduce noise.

-        } else {
-          skippedOptionals++
-        }
-
-        processedOptionals++
+        }
+        processedOptionals++

808-816: Precompute remaining required segments for clarity; current math is correct.

The routeIndex-based remaining length fixes prior mid-route skew. For readability, derive “remaining required” once and reuse.

-          const remainingOptionals = optionalCount - processedOptionals - 1 > 0
-          // consider last route segment might be index route and any prior optionals that was not matched
-          const remainingRouteSegmentLength =
+          const remainingOptionals = optionalCount - processedOptionals - 1 > 0
+          // consider last route segment might be index route and any prior optionals that were not matched
+          const remainingRouteSegmentLength =
             (routeSegments.slice(-1)[0]?.value === '/'
               ? routeSegments.length - 1
               : routeSegments.length) - routeIndex
+          const remainingRequiredSegmentsFromHere =
+            remainingRouteSegmentLength - (optionalCount - processedOptionals)

842-854: Reuse the derived “remaining required” metric.

Logic is sound; switching to the precomputed variable reduces cognitive load.

-              if (
-                remainingRouteSegmentLength -
-                  optionalCount +
-                  processedOptionals >=
-                baseSegments.length - baseIndex
-              ) {
+              if (remainingRequiredSegmentsFromHere >= (baseSegments.length - baseIndex)) {
                 shouldMatchOptional = false
               }

880-885: Avoid shadowing remainingRouteSegmentLength — rename the inner declaration and update its local uses.

packages/router-core/src/path.ts contains two const declarations named remainingRouteSegmentLength (decls at ~line 812 and ~line 880). The inner one shadows the outer and is used nearby (comparisons around lines 888, 902–904, 910–911). Rename the inner variable (e.g. remainingRouteSegmentLengthAfterFollowing) and update those local comparisons.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4f784c and 91cb686.

📒 Files selected for processing (1)
  • packages/router-core/src/path.ts (4 hunks)
⏰ 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). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (3)
packages/router-core/src/path.ts (3)

758-766: Early-exit handling for missing// base segments looks correct.

Incrementing processedOptionals and advancing routeIndex here avoids infinite loops and correctly treats absent or slash segments as “skipped optional.”

Also applies to: 768-774


825-839: Good: prefer skipping optional when next static segment matches current base.

Case sensitivity is honored; early break prevents overconsumption by the optional.


856-897: Nice fixes: safe speculative recursion and correct base slicing.

Cloning params and using baseIndex + 1 for remainingBaseSegments address the prior mutation and index bugs.

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/src/path.ts (1)

900-912: Avoid shadowing “remainingRouteSegmentLength” — rename to prevent misreads.

The inner block defines a remainingRouteSegmentLength that shadows the outer one; later checks (Line 908) refer to the outer value. Rename for clarity to prevent future regressions.

Apply:

-          const remainingRouteSegmentLength =
+          const remainingRouteLenFromHere =
             (routeSegments.slice(-1)[0]?.value === '/'
               ? routeSegments.length - 1
               : routeSegments.length) - routeIndex

-          const remainingRequiredRouteSegmentCount =
-            remainingRouteSegmentLength - (optionalCount - processedOptionals)
+          const remainingRequiredRouteCount =
+            remainingRouteLenFromHere - (optionalCount - processedOptionals)
           const remainingBaseSegmentCount = baseSegments.length - baseIndex

And below:

-              if (
-                remainingRequiredRouteSegmentCount >= remainingBaseSegmentCount
-              ) {
+              if (remainingRequiredRouteCount >= remainingBaseSegmentCount) {
                 shouldMatchOptional = false
               }

Keep the inner-scope var distinct too:

-                const remainingRouteSegmentLength =
+                const followingRemainingRouteLen =
                   remainingRouteSegments.slice(-1)[0]?.value === '/'
                     ? remainingRouteSegments.length - 1
                     : remainingRouteSegments.length
...
-                isMatchedFurtherDown =
-                  remainingRouteSegmentLength ===
+                isMatchedFurtherDown =
+                  followingRemainingRouteLen ===
                     remainingBaseSegments.length &&
                   isMatch(

And finally:

-                if (remainingBaseSegmentCount < remainingRouteSegmentLength) {
+                if (remainingBaseSegmentCount < remainingRouteLenFromHere) {
                   shouldMatchOptional = false
                 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91cb686 and 96e5190.

📒 Files selected for processing (1)
  • packages/router-core/src/path.ts (4 hunks)
🔇 Additional comments (5)
packages/router-core/src/path.ts (5)

614-619: Optional-segment bookkeeping added — sane approach.

Tracking processedOptionals and the precomputed optionalCount is clear and avoids repeated scans. Looks good.


758-766: Skip paths correctly advance optional counters.

Incrementing processedOptionals before continue preserves invariants when base is exhausted or a / is encountered. Good.


809-821: Remaining-length math now scoped to current indices — resolves prior miscounts.

Using (routeSegments.length - routeIndex) and (baseSegments.length - baseIndex) (with index-route adjustment) fixes the earlier global-length bug and makes optional decisions correct mid-route. Also like the early break on static PATHNAME and the consecutive-optionals guard.

Also applies to: 829-855


857-899: Speculative probe no longer mutates caller and slices base correctly.

Cloning params and using baseSegments.slice(baseIndex + 1) address the prior side-effects/off-by-index issues in lookahead recursion. Nice.


928-929: Post‑processing increment ensures consistent optional accounting.

Incrementing processedOptionals after match-or-skip keeps the remaining-optionals math correct for subsequent lookahead. LGTM.

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

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/tests/match-by-path.test.ts (1)

644-658: Replace stray '/a/{-$id}/b_/{-$other}' with '/a/{-$id}/b/{-$other}_' (packages/router-core/tests/match-by-path.test.ts:651)

Single occurrence at line 651; apply patch:

-    ['/a/b', '/a/_{-$id}_/b_/{-$other}_', {}],
+    ['/a/b', '/a_/{-$id}_/b_/{-$other}_', {}],
🧹 Nitpick comments (2)
packages/router-core/tests/match-by-path.test.ts (2)

55-66: Add reciprocal 'from' trailing-slash strictness cases for optionals

Good additions for trailing slashes on the pattern. Add the inverse (trailing slash on the URL but not on the pattern) to lock strict behavior.

Apply this diff inside the same dataset:

+    ['/a/1/b/', '/a/{-$id}/b', undefined],
+    ['/a/b/', '/a/{-$id}/b', undefined],
+    ['/a/1/b/2/', '/a/{-$id}/b/{-$other}', undefined],
+    ['/a/b/2/', '/a/{-$id}/b/{-$other}', undefined],

83-239: Broad coverage looks solid; consider DRYing trailing-slash twins

The permutations are thorough. To reduce repetition/typos, consider a small helper to auto-generate to + to/ variants for each case.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96e5190 and b9337d8.

📒 Files selected for processing (1)
  • packages/router-core/tests/match-by-path.test.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/router-core/tests/match-by-path.test.ts (1)
packages/router-core/src/path.ts (1)
  • matchByPath (563-603)
⏰ 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
🔇 Additional comments (4)
packages/router-core/tests/match-by-path.test.ts (4)

241-288: Consecutive optionals: coverage LGTM

These cases accurately exercise presence/absence across multiple optionals and adjacent literals.


321-331: Case-insensitive optionals: parity with strict mode looks correct

The added cases align with expectations in case-insensitive mode.


348-417: Case-insensitive complex optionals: thorough and consistent

Good mix of optional + required + trailing-slash variants; expectations read correctly.


594-594: Non-nested, case-insensitive repeated param override: OK

Confirms later $id wins; matches behavior in other suites.

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: 1

🧹 Nitpick comments (5)
e2e/react-router/basic-file-based/src/routes/optional-params/single/{-$id}.tsx (1)

11-13: Minor: align heading literal with route syntax

Show the parameter with braces for consistency with file-based pattern.

-        Hello "/optional-params/single/-$id"!
+        Hello "/optional-params/single/{-$id}"!
e2e/react-router/basic-file-based/tests/optionalParams.spec.ts (4)

320-323: Stability: prefer visibility over viewport checks

toBeInViewport can be flaky; toBeVisible is sufficient here.

-    await expect(
-      page.getByTestId('withRequiredInBetween-heading'),
-    ).toBeInViewport()
+    await expect(
+      page.getByTestId('withRequiredInBetween-heading'),
+    ).toBeVisible()

337-340: Same as above: swap viewport assertion

-    await expect(
-      page.getByTestId('withRequiredInBetween-heading'),
-    ).toBeInViewport()
+    await expect(
+      page.getByTestId('withRequiredInBetween-heading'),
+    ).toBeVisible()

355-357: Same as above: swap viewport assertion

-    ).toBeInViewport()
+    ).toBeVisible()

371-374: Same as above: swap viewport assertion

-    ).toBeInViewport()
+    ).toBeVisible()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9337d8 and a3e3c38.

📒 Files selected for processing (5)
  • e2e/react-router/basic-file-based/src/routeTree.gen.ts (0 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/single/path.tsx (1 hunks)
  • e2e/react-router/basic-file-based/src/routes/optional-params/single/{-$id}.tsx (1 hunks)
  • e2e/react-router/basic-file-based/tests/optionalParams.spec.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • e2e/react-router/basic-file-based/src/routeTree.gen.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
e2e/react-router/basic-file-based/src/routes/optional-params/single/{-$id}.tsx (2)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/single/path.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/single/path.tsx (2)
e2e/react-router/basic-file-based/src/routes/optional-params/route.tsx (1)
  • Route (3-5)
e2e/react-router/basic-file-based/src/routes/optional-params/single/{-$id}.tsx (1)
  • Route (3-5)
⏰ 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
🔇 Additional comments (2)
e2e/react-router/basic-file-based/src/routes/optional-params/single/path.tsx (1)

3-5: LGTM: route export and wiring look correct

Matches the test usage and naming conventions.

e2e/react-router/basic-file-based/src/routes/optional-params/single/{-$id}.tsx (1)

3-5: LGTM: file-route definition

Route creation and params usage are correct.


// consider last route segment might be index route and any prior optionals that was not matched
const remainingRouteSegmentLength =
(routeSegments.slice(-1)[0]?.value === '/'
Copy link
Contributor

Choose a reason for hiding this comment

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

isMatch is a critical part of the router, we should be careful about performance. Maybe we can use the last() utility function to access the last item in an array instead of this?

Suggested change
(routeSegments.slice(-1)[0]?.value === '/'
(last(routeSegments)?.value === '/'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks. applied this. agreed performance here is paramount hence added extra early exits from the for loop running the isMatch only in specific circumstances

isMatch(
remainingBaseSegments,
remainingRouteSegments,
{ ...params },
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it needs to inherit params

Suggested change
{ ...params },
{},

Copy link
Contributor Author

Choose a reason for hiding this comment

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

params gets modified in isMatch when the match is found, for this here we only want to confirm a match exists and don't want it to mutate the params, hence the spread.

Copy link
Contributor

Choose a reason for hiding this comment

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

but it's never reading from it either right? So if we're not gonna use it, and it doesn't need it, why spread at all? Why not just pass in an empty object?

@amosbastian
Copy link

I also encountered this problem today

@nlynzaad
Copy link
Contributor Author

nlynzaad commented Oct 2, 2025

The added tests has highlighted additional issues with optional params, unrelated to this PR, that we need to address.

Unfortunately I have been busy dealing with other issues on non-nested paths that has taken my attention away from this PR. I hope to return to this as soon as we have resolved those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Everything documentation related package: router-core
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Router does not render index.tsx in a dynamic path parameter route that comes after an optional path parameter
3 participants