Skip to content

fix(resolve): exclude empty optional arguments from params#2434

Merged
posva merged 6 commits into
vuejs:mainfrom
babu-ch:feat/router_to_active
Apr 22, 2026
Merged

fix(resolve): exclude empty optional arguments from params#2434
posva merged 6 commits into
vuejs:mainfrom
babu-ch:feat/router_to_active

Conversation

@babu-ch

@babu-ch babu-ch commented Jan 7, 2025

Copy link
Copy Markdown
Contributor

fix #2419

Removed if optional parameter is empty
Now the param will match if :to is string and if it is object

In the case of string path, it should not be an empty string parameter(Right?)
, so I thought there would be no problem with excluding it.

However, this may be a breaking change if you have users who were using this

In addition, I added a router-link to Playgound to check the operation.

Summary by CodeRabbit

  • New Features

    • Added navigation links and a route to access feature pages, including nested feature paths.
  • Bug Fixes

    • Fixed resolution so missing optional route parameters are omitted instead of returning empty placeholders.
  • Tests

    • Added tests covering optional-parameter resolution, including catch-all nested paths and differences when resolving by path vs name.

@netlify

netlify Bot commented Jan 7, 2025

Copy link
Copy Markdown

Deploy Preview for vue-router canceled.

Name Link
🔨 Latest commit 66f824f
🔍 Latest deploy log https://app.netlify.com/sites/vue-router/deploys/677e37044abd7e000807f3dd

Comment thread packages/router/src/matcher/index.ts Outdated
params = matcher.parse(path)!
name = matcher.record.name

matcher?.keys.forEach(key => {

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.

Can the ?. be changed to . instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@skirtles-code
Thank you for your comment.
Oh, it was a remnant from writing outside of this block.
Fixed!
66f824f

@skirtles-code

Copy link
Copy Markdown
Contributor

I was wondering about some other edge cases. I'm not sure whether these necessarily impact this PR, but I thought they were worth considered before it's merged.

I put together this Playground to demonstrate:

These are are to relevant values:

  1. '/features'
  2. { name: 'features' }
  3. { name: 'features', params: { id: undefined } }
  4. { name: 'features', params: { id: null } }
  5. { name: 'features', params: { id: '' } }

All of them lead to the same path.

Clicking around between them, it seems that 2, 3 and 4 are considered equivalent, which makes sense. It's 5 that I found most interesting. It seems to behave like it's a child of 2, 3 and 4.

I also tried with { name: 'features', params: { id: '123' } }, which seems to behave similarly. When that route is active it shows 2, 3 and 4 as active, though not exact active.

Not sure if that's intentional or not, but it doesn't seem to match what's described at https://router.vuejs.org/guide/essentials/active-links.html. I wrote that page, so I may be to blame for describing these classes incorrectly. It seems that there's some special handling for optional params that I didn't take into account in my description.

@posva

posva commented Oct 9, 2025

Copy link
Copy Markdown
Member

FYI in the upcoming custom resolvers I'm trying to introduce a consistent behaviour. Right now I'm going for this: optional params are always null if missing (not passed, empty string, undefined, null). It's working well and the only issue I can see is that the empty string could also be simply rejected rather than transformed to null but it sounded less convenient. Open to suggestions!

@posva posva force-pushed the feat/router_to_active branch from 66f824f to 012ddc7 Compare April 22, 2026 14:53
@netlify

netlify Bot commented Apr 22, 2026

Copy link
Copy Markdown

Deploy Preview for vue-router canceled.

Name Link
🔨 Latest commit ab403a9
🔍 Latest deploy log https://app.netlify.com/projects/vue-router/deploys/69e8e8032df2ec00087eb04e

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a catch-all /features/:pathMatch(.*)* route, adds three RouterLink examples in the playground, and updates path-based resolution to drop optional params parsed as empty/falsy and omit undefined param keys in resolved locations. Tests updated accordingly.

Changes

Cohort / File(s) Summary
Playground Navigation
packages/playground/src/App.vue
Appended three <router-link> entries: :to="{ name: 'features' }", to="/features", and to="/features/one". No reactive/script logic changed.
Router Configuration
packages/playground/src/router.ts
Added route: { path: '/features/:pathMatch(.*)*', name: 'features', component: User } to handle catch-all /features/*.
Matcher Logic
packages/router/src/matcher/index.ts
In createRouterMatcher().resolve() (path branch), remove keys for optional params when parsed value is falsy so optional params (e.g., '') are not retained.
Tests
packages/router/__tests__/matcher/resolve.spec.ts
Updated expectations for resolving optional params (missing optional → key omitted) and added cases covering optional splat behavior for /features and /features/one.

Sequence Diagram(s)

sequenceDiagram
participant Client as RouterLink
participant Router as Router
participant Matcher as Matcher.resolve()
participant View as User
Client->>Router: navigate(to: name or path)
Router->>Matcher: resolve(location)
Matcher-->>Router: matched route + params (omit falsy optional / drop undefined keys)
Router->>View: render component with sanitized params
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • edison1105

Poem

🐰 I hopped through links both named and plain,
Found paths that catch the stray and the spry,
I brushed off empty shadows from each lane,
Now params are tidy as I bounce by,
Hop in, the features burrow nearby! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately describes the main change: excluding empty optional parameters from the params object during route resolution.
Linked Issues check ✅ Passed Changes implement the core objective from #2419 by excluding empty optional params from resolved params, making param resolution consistent between string and name-based route navigation.
Out of Scope Changes check ✅ Passed All changes directly address issue #2419: router core fix, test coverage for the fix, and a playground example to demonstrate the corrected behavior.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pkg-pr-new

pkg-pr-new Bot commented Apr 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: ab403a9

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/playground/src/router.ts (1)

162-166: Nit: User component expects an id prop, not pathMatch.

User.vue destructures defineProps<{ id: string }>() and renders User: {{ id }}. With this route, the view will always show User: undefined, which makes the playground less useful for eyeballing the #2419 repro. Consider a small inline component (or the Generic/NotFound component already imported) that renders route.params.pathMatch so the active-link behavior is easier to verify.

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

In `@packages/playground/src/router.ts` around lines 162 - 166, The route for path
'/features/:pathMatch(.*)*' is using the User component which expects an id prop
(defineProps<{ id: string }>()), causing the view to show undefined; update the
route definition (the object with name 'features' and component: User) to
instead use a component that reads and renders route.params.pathMatch (either
replace with the already-imported Generic or NotFound component, or provide a
small inline/view component that returns route.params.pathMatch) so the page
displays the pathMatch value and preserves active-link verification.
packages/router/__tests__/matcher/resolve.spec.ts (1)

697-714: Consider tightening coverage around the new behavior.

Two additions would make the regression harder to undo silently:

  1. A case where the same URL is resolved both ways (by path /features and by name: 'features' with no params or with params: { pathMatch: '' }) and asserts that the resulting params objects are deep-equal — that is ultimately what RouterLink active-class comparison relies on for #2419.
  2. A repeatable non-splat optional (e.g. /a/:p*) resolved at /a, to confirm the params[key.name] === '' check also drops repeatable optionals and doesn’t leak through as '' or [].
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router/__tests__/matcher/resolve.spec.ts` around lines 697 - 714,
Add two tighter test cases using assertRecordMatch: (1) resolve the same URL
both by path '/features' and by name 'features' with no params and also with
params: { pathMatch: '' }, and assert the resulting route objects (especially
params) are deep-equal to ensure RouterLink active-class comparisons remain
consistent; (2) add a case for a repeatable optional route like { path:
'/a/:p*', name: 'a' } resolved at '/a' and assert that params.p is omitted (not
'' or []) — use the existing assertRecordMatch helper and the same-style test
block as the 'drops missing optional splat when resolving by path' spec to add
both assertions.
🤖 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/playground/src/App.vue`:
- Around line 161-169: The CI failure is due to formatting issues in the
router-link JSX/HTML in App.vue (the <router-link> elements for "Go to Features
(name)", "Go to Features (string)" and "Go to Feature one"); run the formatter
(oxfmt --write) or your project format script (e.g., pnpm format), re-check with
oxfmt --check, and commit the updated formatting changes so the <router-link>
blocks are properly formatted.

In `@packages/router/src/matcher/index.ts`:
- Around line 328-334: The name-based resolution branch is not stripping empty
optional params like the path-based branch does (see matcher.keys.forEach check
that deletes params[key.name] when key.optional and params[key.name] === ''),
causing divergence between path vs name resolution; update the name-based branch
(the code around the existing name-resolution logic) to run the same
empty-optional stripping over its params — iterate matcher.keys and delete
params[key.name] when key.optional && params[key.name] === '' — so both
resolution paths normalize optional empty-string params the same way and keep
isActive comparisons consistent.

---

Nitpick comments:
In `@packages/playground/src/router.ts`:
- Around line 162-166: The route for path '/features/:pathMatch(.*)*' is using
the User component which expects an id prop (defineProps<{ id: string }>()),
causing the view to show undefined; update the route definition (the object with
name 'features' and component: User) to instead use a component that reads and
renders route.params.pathMatch (either replace with the already-imported Generic
or NotFound component, or provide a small inline/view component that returns
route.params.pathMatch) so the page displays the pathMatch value and preserves
active-link verification.

In `@packages/router/__tests__/matcher/resolve.spec.ts`:
- Around line 697-714: Add two tighter test cases using assertRecordMatch: (1)
resolve the same URL both by path '/features' and by name 'features' with no
params and also with params: { pathMatch: '' }, and assert the resulting route
objects (especially params) are deep-equal to ensure RouterLink active-class
comparisons remain consistent; (2) add a case for a repeatable optional route
like { path: '/a/:p*', name: 'a' } resolved at '/a' and assert that params.p is
omitted (not '' or []) — use the existing assertRecordMatch helper and the
same-style test block as the 'drops missing optional splat when resolving by
path' spec to add both assertions.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3c0c4051-e11f-4077-bdcf-1ac3c6db1847

📥 Commits

Reviewing files that changed from the base of the PR and between bbd5349 and 012ddc7.

📒 Files selected for processing (4)
  • packages/playground/src/App.vue
  • packages/playground/src/router.ts
  • packages/router/__tests__/matcher/resolve.spec.ts
  • packages/router/src/matcher/index.ts

Comment thread packages/playground/src/App.vue
posva added a commit to babu-ch/router that referenced this pull request Apr 22, 2026
Previously passing `{ params: { id: undefined } }` to a route with an
optional param produced a malformed path containing the literal string
"undefined". Align the behavior with omitting the key entirely.

Covers vuejs#2434 (comment).
@posva posva force-pushed the feat/router_to_active branch from 8f7df00 to 2f1b929 Compare April 22, 2026 15:06

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
packages/router/__tests__/matcher/resolve.spec.ts (1)

864-877: ⚠️ Potential issue | 🟠 Major

Update tests to treat empty strings and null as absent optional params, consistent with undefined behavior.

The test "keeps optional params passed as empty strings" (line 864) expects empty string params to be retained in the resolved location, but this conflicts with the path-based resolution logic which explicitly deletes empty string optional params (see matcher resolve line 294-299). To align with undefined behavior and the PR's intent, empty strings should be removed from resolved params.

Additionally, null is currently commented out as unsupported despite being allowed by RouteParamValueRaw types and handled by encodeParam (which returns an empty string for null, identical to undefined behavior). Either test that null is supported and works like undefined, or add an explicit test asserting it's rejected at runtime.

Suggested test alignment
-    it('keeps optional params passed as empty strings', () => {
+    it('treats optional params passed as empty strings as absent', () => {
       assertRecordMatch(
         { path: '/:a/:b?', name: 'p', components },
         { name: 'p', params: { a: 'b', b: '' } },
-        { name: 'p', path: '/b', params: { a: 'b', b: '' } },
+        { name: 'p', path: '/b', params: { a: 'b' } },
         {
-          params: { a: 'a', b: '' },
+          params: { a: 'a' },
           path: '/a',
           matched: [],
           meta: {},
@@ -906,12 +906,12 @@ describe('LocationAsName', () => {
     // `#2419` (skirtles-code's comment): `{ params: { id: undefined } }`
     // should resolve the same as omitting the key entirely so active-link
     // comparisons stay consistent.
     it('treats explicit undefined optional params as absent', () => {
       const record = {
         path: '/features/:id?',
         name: 'features',
         components,
       }
       assertRecordMatch(
         record,
         { name: 'features', params: { id: undefined } },
         { name: 'features', path: '/features', params: {} }
       )
-      // null is rejected by the types and not supported at runtime:
-      // assertRecordMatch(
-      //   record,
-      //   { name: 'features', params: { id: null } },
-      //   { name: 'features', path: '/features', params: {} }
-      // )
+      assertRecordMatch(
+        record,
+        { name: 'features', params: { id: null } },
+        { name: 'features', path: '/features', params: {} }
+      )
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router/__tests__/matcher/resolve.spec.ts` around lines 864 - 877,
The test "keeps optional params passed as empty strings" contradicts the matcher
resolve behavior that treats empty strings (and encodeParam(null)) as absent
optional params; update the test in resolve.spec.ts to expect empty string and
null to be removed from resolved params (i.e., optional params treated as
absent/undefined), or alternatively add a new test that asserts null is handled
like undefined via encodeParam/RouteParamValueRaw; specifically adjust
assertions referencing the route record for '/:a/:b?' and the resolved locations
so that params { b: '' } (and null) are not preserved, matching the deletion
logic in matcher.resolve (and encodeParam behavior).
🤖 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/__tests__/matcher/resolve.spec.ts`:
- Around line 864-877: The test "keeps optional params passed as empty strings"
contradicts the matcher resolve behavior that treats empty strings (and
encodeParam(null)) as absent optional params; update the test in resolve.spec.ts
to expect empty string and null to be removed from resolved params (i.e.,
optional params treated as absent/undefined), or alternatively add a new test
that asserts null is handled like undefined via encodeParam/RouteParamValueRaw;
specifically adjust assertions referencing the route record for '/:a/:b?' and
the resolved locations so that params { b: '' } (and null) are not preserved,
matching the deletion logic in matcher.resolve (and encodeParam behavior).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bb1ddc13-96ab-4f3c-bd8d-42f919532bbb

📥 Commits

Reviewing files that changed from the base of the PR and between 8f7df00 and 2f1b929.

📒 Files selected for processing (2)
  • packages/router/__tests__/matcher/resolve.spec.ts
  • packages/router/src/matcher/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/router/src/matcher/index.ts

assertRecordMatch(
{ path: '/features/:pathMatch(.*)*', components, name: 'features' },
{ path: '/features' },
{ path: '/features', params: {}, name: 'features' }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

still unsure if params should be { pathMatch: [] } for convenience since it's repeatable. This is probably not worth changing in the current version but worth changing in the upcoming route resolvers

@codecov

codecov Bot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.93%. Comparing base (20521b0) to head (ab403a9).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2434   +/-   ##
=======================================
  Coverage   85.92%   85.93%           
=======================================
  Files          88       88           
  Lines       10076    10081    +5     
  Branches     2315     2317    +2     
=======================================
+ Hits         8658     8663    +5     
  Misses       1407     1407           
  Partials       11       11           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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/src/matcher/index.ts`:
- Around line 328-336: Fix the spelling typo in the comment inside the
matcher.keys.forEach block: change "noramlizes" to "normalizes" in the comment
that explains deleting optional params for falsy values; keep the existing logic
referencing params[key.name], key.optional, and parse() unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fc75b79c-939c-48d5-a0d3-26cf2a09dfd5

📥 Commits

Reviewing files that changed from the base of the PR and between 2f1b929 and ab403a9.

📒 Files selected for processing (3)
  • packages/playground/src/App.vue
  • packages/router/__tests__/matcher/resolve.spec.ts
  • packages/router/src/matcher/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/playground/src/App.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router/tests/matcher/resolve.spec.ts

Comment on lines +328 to 336

// delete all optional params that have falsy values
// noramlizes '', null, and undefined into deleting the key
matcher.keys.forEach(key => {
if (key.optional && !params[key.name]) {
delete params[key.name]
}
})
}

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm what parse() returns for a missing repeatable optional segment,
# to validate whether `!params[key.name]` is sufficient today.
sed -n '220,285p' packages/router/src/matcher/pathParserRanker.ts

Repository: vuejs/router

Length of output: 2351


Fix typo in comment.

Line 330 has a typo: noramlizes should be normalizes.

The falsy check !params[key.name] is correct as-is. The parse() function returns an empty string '' for missing optional repeatable parameters (never an empty array), so the check safely deletes all three cases ('', null, undefined).

The asymmetry between the path-based and name-based branches for empty optional params (where name-based would keep pathMatch: '') remains a separate concern already noted in prior feedback.

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

In `@packages/router/src/matcher/index.ts` around lines 328 - 336, Fix the
spelling typo in the comment inside the matcher.keys.forEach block: change
"noramlizes" to "normalizes" in the comment that explains deleting optional
params for falsy values; keep the existing logic referencing params[key.name],
key.optional, and parse() unchanged.

@posva posva merged commit 1ef0964 into vuejs:main Apr 22, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from 🆕 Triaging to ✅ Done in Vue Router Roadmap Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Active class not applied on RouterLink when using to with string or object with path key

3 participants