Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

posva
Copy link
Member

@posva posva commented Dec 5, 2024

New version of the internal matcher (renamed as resolver). With more responsibilities and allowing it to be overridden:

  • custom parsing /serializing of params (numbers, dates, classes, etc)
  • matching on the query

Summary by CodeRabbit

  • New Features

    • Added an interactive Experiments Playground app for exploring routing behaviors and inspecting route state.
    • Exposed an experimental routing API and resolver-driven routing primitives for advanced use.
  • Improvements

    • Documentation sidebars now load API navigation dynamically for English and Chinese locales.
    • Improved URL parsing/serialization (query/hash/path) and richer parameter handling.
  • Tests

    • Large suite of new tests covering experimental router, matchers, and param parsers.
  • Chores

    • Dependency/tooling upgrades, updated ignore rules, and build/size-check adjustments.

Copy link

netlify bot commented Dec 5, 2024

Deploy Preview for vue-router canceled.

Name Link
🔨 Latest commit 7d6164a
🔍 Latest deploy log https://app.netlify.com/projects/vue-router/deploys/68b43f302215eb0008856b3a

Copy link

pkg-pr-new bot commented Dec 5, 2024

Open in StackBlitz

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

commit: 7d6164a

Copy link

codecov bot commented Dec 6, 2024

Codecov Report

❌ Patch coverage is 76.86441% with 273 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.55%. Comparing base (6a11243) to head (7d6164a).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
packages/router/src/experimental/router.ts 60.94% 230 Missing and 2 partials ⚠️
.../src/experimental/route-resolver/resolver-fixed.ts 86.52% 19 Missing ⚠️
...imental/route-resolver/matchers/matcher-pattern.ts 92.10% 9 Missing ⚠️
...tal/route-resolver/matchers/param-parsers/index.ts 75.00% 8 Missing ⚠️
...l/route-resolver/matchers/matcher-pattern-query.ts 93.24% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2415      +/-   ##
==========================================
- Coverage   94.90%   89.55%   -5.35%     
==========================================
  Files          34       46      +12     
  Lines        3002     4109    +1107     
  Branches      846     1091     +245     
==========================================
+ Hits         2849     3680     +831     
- Misses        150      424     +274     
- Partials        3        5       +2     

☔ 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.

@posva posva changed the title New Matcher with param parsing Custom Route Resolvers Mar 20, 2025
@github-project-automation github-project-automation bot moved this to 🆕 Triaging in Vue Router Roadmap Jul 15, 2025
@posva posva moved this from 🆕 Triaging to 🧑‍💻 In progress in Vue Router Roadmap Jul 15, 2025
@posva posva mentioned this pull request Jul 15, 2025
Copy link

@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 (15)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (4)

124-127: Add a symmetry check for double-slash build output

You verify star.build() preserves double slashes; also assert that the produced path re-matches and yields the expected pathMatch.

   it('keep paths as is', () => {
     const pattern = new MatcherPatternPathStar('/team/')
     expect(pattern.build({ pathMatch: '/hey' })).toBe('/team//hey')
+    // roundtrip: built path should match back to the same suffix
+    expect(pattern.match('/team//hey')).toEqual({ pathMatch: '/hey' })
   })

242-253: Add build() assertions for catch-all route

Cover the build side for empty, null, and non-empty pathMatch when trailingSlash is null (splat). This guards the “add trailing slash when empty” behavior.

   it('catch all route', () => {
     const pattern = new MatcherPatternPathDynamic(
       /^\/(.*)$/,
       { pathMatch: [] },
       [0],
       null
     )

     expect(pattern.match('/ok')).toEqual({ pathMatch: 'ok' })
     expect(pattern.match('/ok/ok/ok')).toEqual({ pathMatch: 'ok/ok/ok' })
     expect(pattern.match('/')).toEqual({ pathMatch: '' })
+    // build side
+    expect(pattern.build({ pathMatch: 'ok' })).toBe('/ok')
+    expect(pattern.build({ pathMatch: '' })).toBe('/')
+    expect(pattern.build({ pathMatch: null })).toBe('/')
   })

376-383: Keep the sparse tuple — add a linter-ignore to document intent

Acknowledged from prior context: the sparse tuple is intentional to validate support for sparse arrays. Add an inline Biome ignore to prevent false-positive lint errors and to explain intent for future readers.

   const pattern = new MatcherPatternPathDynamic(
     /^\/teams\/(.+?)\/$/,
     {
-      teamId: [, true],
+      // biome-ignore lint/suspicious/noSparseArray: intentionally testing sparse array support
+      teamId: [, true],
     },
     ['teams', 1],
     true
   )

433-441: Unreachable branch for v === undefined in parser.get

In match(), captures are normalized with ?? null, so get() never receives undefined. Either drop that branch or add a brief comment stating it’s unreachable with current matcher semantics.

-    const nullAwareParser = definePathParamParser({
-      get: (v: string | null) => {
-        if (v === null) return 'was-null'
-        if (v === undefined) return 'was-undefined'
-        return `processed-${v}`
-      },
+    const nullAwareParser = definePathParamParser({
+      get: (v: string | null) => {
+        if (v === null) return 'was-null'
+        // Note: `undefined` is not produced by the matcher (normalized to null)
+        return `processed-${v}`
+      },
       set: (v: string | null) =>
         v === 'was-null' ? null : String(v).replace('processed-', ''),
     })
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts (4)

28-33: Add boundary tests for safe integers.

Guard against regressions on limits accepted by Number.isSafeInteger:

Apply this diff to extend coverage:

     it('parses valid scientific notation as integers', () => {
       expect(PARAM_PARSER_INT.get('1e5')).toBe(100000)
       expect(PARAM_PARSER_INT.get('1e2')).toBe(100)
       expect(PARAM_PARSER_INT.get('2.5e10')).toBe(25000000000)
       expect(PARAM_PARSER_INT.get('1.5e2')).toBe(150)
     })
+
+    it('respects MAX_SAFE_INTEGER boundaries', () => {
+      expect(PARAM_PARSER_INT.get('9007199254740991')).toBe(9007199254740991) // Number.MAX_SAFE_INTEGER
+      expect(() => PARAM_PARSER_INT.get('9007199254740992')).toThrow()
+    })
+
+    it('accepts explicit plus sign and -0', () => {
+      expect(PARAM_PARSER_INT.get('+42')).toBe(42)
+      expect(PARAM_PARSER_INT.get('-0')).toBe(0)
+    })

39-49: Decide policy for non-decimal numeric literals (0x, 0b, 0o).

Number() accepts hex/binary/octal (e.g., '0x2A' → 42). If you want to reject these for predictability, add tests (and a small guard in the parser). If you prefer to allow them, add tests to lock the behavior.

Possible tests to add (choose one direction and keep):

+    it('rejects non-decimal literal prefixes', () => {
+      expect(() => PARAM_PARSER_INT.get('0x2A')).toThrow()
+      expect(() => PARAM_PARSER_INT.get('0b1010')).toThrow()
+      expect(() => PARAM_PARSER_INT.get('0o52')).toThrow()
+    })
+    // or, to lock-in acceptance instead:
+    // expect(PARAM_PARSER_INT.get('0x2A')).toBe(42)
+    // expect(PARAM_PARSER_INT.get('0b1010')).toBe(10)
+    // expect(PARAM_PARSER_INT.get('0o52')).toBe(42)

67-76: Array cases are good; add whitespace-array coverage.

Single values accept whitespace as zero; mirror that in arrays to make the behavior explicit.

Apply this diff:

     it('handles empty arrays', () => {
       expect(PARAM_PARSER_INT.get([])).toEqual([])
     })
+
+    it('parses whitespace strings in arrays as zeros', () => {
+      expect(PARAM_PARSER_INT.get([' ', '\t', '\n'])).toEqual([0, 0, 0])
+    })

Also applies to: 79-99


102-115: Serialization tests look good; consider a negative-zero check.

String(-0) becomes '0'. Add a quick assertion to lock this behavior if desired.

Apply this diff:

     it('converts integers to strings', () => {
       expect(PARAM_PARSER_INT.set(0)).toBe('0')
       expect(PARAM_PARSER_INT.set(1)).toBe('1')
       expect(PARAM_PARSER_INT.set(42)).toBe('42')
       expect(PARAM_PARSER_INT.set(-1)).toBe('-1')
       expect(PARAM_PARSER_INT.set(-999)).toBe('-999')
       expect(PARAM_PARSER_INT.set(2147483647)).toBe('2147483647')
+      // -0 stringifies to '0'
+      // @ts-expect-error ensure runtime behavior is locked
+      expect(PARAM_PARSER_INT.set(-0 as any)).toBe('0')
     })

Also applies to: 117-132

packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts (5)

89-125: Build() coverage is solid; fix a minor typo.

The test title has a split word.

Apply this diff:

-      it('strips off und efined values', () => {
+      it('strips off undefined values', () => {

Also applies to: 127-150


152-209: Default handling scenarios are well covered; fix a small comment typo.

Minor spelling nit.

Apply this diff:

-          // this leavs the value as null
+          // this leaves the value as null

211-231: Parser integration looks correct; add array-error fallback and BOOL array roundtrip tests.

Strengthen guarantees for arrays when a parser throws and default exists; also ensure array boolean build retains shape.

Apply this diff:

   describe('parser integration', () => {
@@
     it('throws on error without default', () => {
@@
     })
+
+    it('array: falls back to default on parser error', () => {
+      const matcher = new MatcherPatternQueryParam(
+        'ids',
+        'id',
+        'array',
+        PARAM_PARSER_INT,
+        [0]
+      )
+      expect(() => matcher.match({ id: ['ok', 'bad'] })).not.toThrow()
+      expect(matcher.match({ id: ['ok', 'bad'] })).toEqual({ ids: [0] })
+    })
@@
     it('can use PARAM_PARSER_BOOL for booleans', () => {
@@
       expect(matcher.build({ enabled: true })).toEqual({ e: 'true' })
     })
+
+    it('can handle boolean arrays end-to-end', () => {
+      const matcher = new MatcherPatternQueryParam(
+        'flags',
+        'f',
+        'array',
+        PARAM_PARSER_BOOL
+      )
+      expect(matcher.match({ f: ['true', 'false'] })).toEqual({
+        flags: [true, false],
+      })
+      expect(matcher.build({ flags: [true, false] })).toEqual({
+        f: ['true', 'false'],
+      })
+    })
   })

Also applies to: 233-257


404-476: Add tests for 'format: both' to document intended behavior.

Currently, 'both' is treated like 'array' in match(). Make it explicit via tests to avoid confusion and future regressions.

Apply this diff:

   describe('parser fallback', () => {
+    it('format: both behaves like array for match/build', () => {
+      const matcher = new MatcherPatternQueryParam(
+        'items',
+        'i',
+        'both',
+        PARAM_PARSER_DEFAULTS
+      )
+      expect(matcher.match({ i: 'one' })).toEqual({ items: ['one'] })
+      expect(matcher.match({ i: ['one', 'two'] })).toEqual({
+        items: ['one', 'two'],
+      })
+      expect(matcher.build({ items: ['x', 'y'] })).toEqual({ i: ['x', 'y'] })
+    })

439-476: Consider adding a build() test for arrays containing null with default serializer.

Locks behavior that nulls are preserved (not coerced into 'null').

Apply this diff:

       it('should handle array values with missing set method', () => {
@@
       })
+
+      it('build: preserves nulls in arrays with default serializer', () => {
+        const matcher = new MatcherPatternQueryParam(
+          'vals',
+          'v',
+          'array',
+          PARAM_PARSER_DEFAULTS
+        )
+        expect(matcher.build({ vals: ['a', null, 'b'] })).toEqual({
+          v: ['a', null, 'b'],
+        })
+      })
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts (2)

4-13: Optional: Reject non-decimal literal prefixes (0x/0b/0o) while preserving whitespace→0.

Number() accepts hex/binary/octal; this can be surprising for query params. If you want only decimal/scientific forms, add a small guard that doesn’t break the current whitespace-to-zero behavior.

Apply this diff:

 const PARAM_INTEGER_SINGLE = {
   get: (value: string | null) => {
-    const num = Number(value)
+    // Forbid non-decimal literal prefixes after trimming, but keep Number(value)
+    // so that whitespace-only strings still parse to 0.
+    const trimmed = typeof value === 'string' ? value.trim() : value
+    if (typeof trimmed === 'string' && /^(0[xX]|0[bB]|0[oO])/.test(trimmed)) {
+      throw miss()
+    }
+    const num = Number(value)
     if (value && Number.isSafeInteger(num)) {
       return num
     }
     throw miss()
   },
   set: (value: number) => String(value),
 } satisfies ParamParser<number, string | null>

12-13: Optional: Validate on set() to maintain integer invariants.

Helps catch accidental non-integer writes (e.g., from loose typing in user code).

Apply this diff:

-  set: (value: number) => String(value),
+  set: (value: number) => {
+    if (!Number.isSafeInteger(value)) throw miss()
+    return String(value)
+  },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d9c63f1 and ecd1303.

📒 Files selected for processing (8)
  • packages/router/src/experimental/index.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.spec.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.spec.ts
  • packages/router/src/experimental/index.ts
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-26T20:28:25.688Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts:378-383
Timestamp: 2025-08-26T20:28:25.688Z
Learning: In Vue Router experimental code, sparse arrays like `[, true]` in matcher parameter options are intentionally used to test system support for sparse array syntax, not bugs to be fixed.

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts
🧬 Code graph analysis (4)
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts (2)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (1)
  • PARAM_PARSER_INT (37-37)
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts (1)
  • PARAM_PARSER_INT (26-39)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (2)
  • MatcherPatternPathStatic (66-89)
  • MatcherPatternPathDynamic (149-289)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-path-star.ts (1)
  • MatcherPatternPathStar (18-38)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • definePathParamParser (32-41)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts (4)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1)
  • MatcherPatternQueryParam (21-111)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (3)
  • PARAM_PARSER_DEFAULTS (14-22)
  • PARAM_PARSER_BOOL (38-38)
  • PARAM_PARSER_INT (37-37)
packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.ts (1)
  • PARAM_PARSER_BOOL (44-53)
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts (1)
  • PARAM_PARSER_INT (26-39)
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts (1)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-21)
🪛 Biome (2.1.2)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts

[error] 380-380: This array contains an empty slots..

The presences of empty slots may cause incorrect information and might be a typo.
Unsafe fix: Replace hole with undefined

(lint/suspicious/noSparseArray)

🔇 Additional comments (7)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (1)

131-204: Solid coverage for dynamic params (single/decoded/optional)

Good breadth across match and build, including URL decoding and optional handling. No issues spotted.

packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts (1)

4-13: Solid baseline coverage for single-value parsing (LGTM).

Covers signs, whitespace-to-zero, scientific notation, null propagation, and error paths. Nicely aligned with current parser semantics.

Also applies to: 15-27, 28-33, 35-37, 39-49, 51-65

packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts (3)

10-21: Comprehensive coverage for match() with value/array formats (LGTM).

Covers first-item extraction, null handling, coercion, and missing-query defaults. Good alignment with constructor semantics.

Also applies to: 22-33, 34-43, 45-87


259-315: Edge cases are thoughtful and match new null-passing policy (LGTM).

Particularly good: integer parser filtering nulls vs. default/boolean passing nulls to the parser.

Also applies to: 317-391


393-402: Constructor without parser parameter behaves as expected (fallback).

Nice coverage of default get/set fallback.

packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts (2)

15-19: Repeatable parser behavior (filtering nulls) matches tests (LGTM).

Nulls are dropped for ints; errors bubble for invalid items. Consistent with spec tests.


26-39: Dispatcher logic is correct and null-safe (LGTM).

Array vs single routing, null passthrough, and satisfies typing all look good.

Copy link

@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

♻️ Duplicate comments (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (1)

401-401: Keep sparse array test, but suppress linter noise with an inline directive

Per the learning from earlier discussion, the sparse array is intentional for support testing. Add a Biome ignore comment to prevent false positives.

Apply:

-        teamId: [, true],
+        // biome-ignore lint/suspicious/noSparseArray: intentional sparse array to test support
+        teamId: [, true],
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)

7-7: Empty PATH_PARAM_SINGLE_DEFAULT will crash at call sites — provide identity get/set or remove export

An empty object violates callers expecting parser.get/set. Provide safe no-ops.

Apply:

-export const PATH_PARAM_SINGLE_DEFAULT: ParamParser<string, string> = {}
+export const PATH_PARAM_SINGLE_DEFAULT: ParamParser<string, string> = {
+  get: (value: string) => value,
+  set: (value: string) => value,
+}

22-31: PATH_PARAM_PARSER_DEFAULTS: don’t stringify nullish array entries to 'null'

Comment says “doesn't allow null values in arrays” but set() maps them to 'null'. Reject nullish entries to avoid corrupt URLs.

Apply:

 export const PATH_PARAM_PARSER_DEFAULTS = {
   get: value => value ?? null,
-  set: value =>
-    value == null
-      ? null
-      : Array.isArray(value)
-        ? value.map(String)
-        : String(value),
+  set: value => {
+    if (value == null) return null
+    if (Array.isArray(value)) {
+      if (value.some(v => v == null)) {
+        throw new TypeError('Path param arrays cannot contain null/undefined entries')
+      }
+      return value.map(String)
+    }
+    return String(value)
+  },
   // differently from PARAM_PARSER_DEFAULTS, this doesn't allow null values in arrays
 } satisfies ParamParser<string | string[] | null, string | string[] | null>
🧹 Nitpick comments (2)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)

27-34: Clarify doc: this “Generic” alias models path params (no nullish array entries)

The wording implies universal usage; suggest scoping it to path params to avoid confusion with query parsers that may accept nulls.

Apply:

-/**
- * Generic type for a param parser that can handle both single and repeatable params.
- *
- * @see ParamParser
- */
+/**
+ * Generic type for a path param parser that can handle both single and repeatable params.
+ * Note: path params don't allow nullish entries in arrays; for query params use defineQueryParamParser.
+ *
+ * @see ParamParser
+ */
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (1)

49-50: Doc typo: “extend / infering” → “extends / inferring”

Minor polish.

Apply:

-  // path params are parsed by the router as these
-  // we use extend to allow infering a more specific type
+  // path params are parsed by the router as these
+  // we use extends to allow inferring a more specific type
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ecd1303 and 9c8268c.

📒 Files selected for processing (6)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.test-d.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/resolver-fixed.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.test-d.ts
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts
  • packages/router/src/experimental/route-resolver/resolver-fixed.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-14T13:58:50.446Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts:0-0
Timestamp: 2025-08-14T13:58:50.446Z
Learning: The `encodeParam` function in vue-router handles nullish values (null/undefined) by returning an empty string, rather than stringifying them to "null" or "undefined". This means missing parameters in route building result in empty path segments rather than literal "undefined" in URLs.

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts
📚 Learning: 2025-08-14T13:58:50.446Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts:0-0
Timestamp: 2025-08-14T13:58:50.446Z
Learning: The `encodeParam` function in vue-router has the signature `(text: string | number | null | undefined): string` and specifically handles nullish values by returning an empty string via `text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F')`. This means missing or null parameters in route building result in empty path segments rather than literal "null"/"undefined" strings in URLs.

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts
📚 Learning: 2025-08-26T20:28:25.698Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts:378-383
Timestamp: 2025-08-26T20:28:25.698Z
Learning: In Vue Router experimental code, sparse arrays like `[, true]` in matcher parameter options are intentionally used to test system support for sparse array syntax, not bugs to be fixed.

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts
🧬 Code graph analysis (3)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1)
  • MatcherQueryParamsValue (314-318)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-21)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1)
  • MatcherQueryParamsValue (314-318)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (2)
  • MatcherPatternPathStatic (66-89)
  • MatcherPatternPathDynamic (149-289)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-path-star.ts (1)
  • MatcherPatternPathStar (18-38)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (1)
  • definePathParamParser (45-54)
🪛 Biome (2.1.2)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts

[error] 401-401: This array contains an empty slots..

The presences of empty slots may cause incorrect information and might be a typo.
Unsafe fix: Replace hole with undefined

(lint/suspicious/noSparseArray)

🔇 Additional comments (8)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (2)

9-21: ParamParser shape and generics look good

The API is flexible without over-constraining implementers. Default generics and optional get/set are sensible.


19-20: NoInfer is available natively in TypeScript 5.4+ (we’re on ~5.8.3), no import needed

TypeScript 5.4 introduced the built-in utility type NoInfer<T>, and since our packages specify "typescript": "~5.8.3" (and ~5.8.0 in the playground), NoInfer is already provided by the compiler. There is no need to declare or import it in param-parsers/types.ts.

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (3)

1-129: Strong coverage across static/star patterns

Good breadth of cases (case-insensitivity, prefixes, build symmetry). Assertions read cleanly.


363-380: Dynamic + trailing-slash cases are well covered

Edge conditions and build() symmetry checks are solid.


442-511: Custom parsers: nice real-world scenarios

The double and null-aware parsers exercise both match() and build(); good guardrails via miss().

packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (3)

45-54: definePathParamParser: API surface looks correct and tree-shake friendly

Required plus the no-side-effects pragma is good.


67-73: defineQueryParamParser: good generic defaults

Using MatcherQueryParamsValue for the URL-side type is appropriate.


84-84: Single source of truth for defineParamParser alias

Alias to defineQueryParamParser here is fine, given removal of the earlier duplicate export elsewhere.

Copy link

@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

♻️ Duplicate comments (2)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)

8-8: Provide safe no-op implementations to avoid runtime crashes.

An empty object will cause parser.get!/parser.set! call-sites to crash.

-export const PATH_PARAM_SINGLE_DEFAULT: ParamParser<string, string> = {}
+export const PATH_PARAM_SINGLE_DEFAULT: ParamParser<string, string> = {
+  get: (value: string) => value,
+  set: (value: string) => value,
+}

23-32: Do not stringify nullish array entries for path params.

map(String) turns null into the literal "null", contradicting the comment that arrays don’t allow nulls.

 export const PATH_PARAM_PARSER_DEFAULTS = {
   get: value => value ?? null,
   set: value =>
     value == null
       ? null
       : Array.isArray(value)
-        ? value.map(String)
+        ? value.filter((v): v is string => v != null).map(String)
         : String(value),
   // differently from PARAM_PARSER_DEFAULTS, this doesn't allow null values in arrays
 } satisfies ParamParser<string | string[] | null, string | string[] | null>

If you prefer, we can throw on nullish entries instead of filtering.

🧹 Nitpick comments (1)
packages/router/src/experimental/route-resolver/matchers/param-parsers/strings.ts (1)

18-19: Doc nit: clarify sentence.

- * params. It doesn't make much sense to use it for path params will be `null |
- * string | string[]` (all cases combined).
+ * params. It doesn't make much sense to use it for path params, which will be
+ * `null | string | string[]` (all cases combined).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 37c2cf1 and ad12e09.

📒 Files selected for processing (5)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/strings.spec.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/strings.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-14T13:58:50.446Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts:0-0
Timestamp: 2025-08-14T13:58:50.446Z
Learning: The `encodeParam` function in vue-router handles nullish values (null/undefined) by returning an empty string, rather than stringifying them to "null" or "undefined". This means missing parameters in route building result in empty path segments rather than literal "undefined" in URLs.

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts
📚 Learning: 2025-08-14T13:58:50.446Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts:0-0
Timestamp: 2025-08-14T13:58:50.446Z
Learning: The `encodeParam` function in vue-router has the signature `(text: string | number | null | undefined): string` and specifically handles nullish values by returning an empty string via `text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F')`. This means missing or null parameters in route building result in empty path segments rather than literal "null"/"undefined" strings in URLs.

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts
🧬 Code graph analysis (1)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-22)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1)
  • MatcherQueryParamsValue (317-321)
🪛 GitHub Actions: test
packages/router/src/experimental/route-resolver/matchers/param-parsers/strings.spec.ts

[error] 3-3: TypeScript error TS6133: 'MatchMiss' is declared but its value is never read. (During 'pnpm run -r test:types' -> tsc --build tsconfig.json)

Copy link

@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: 3

♻️ Duplicate comments (2)
packages/router/src/router.ts (2)

96-113: JSDoc references are correct now

The links target RouteRecordRaw (not EXPERIMENTAL_*). This resolves prior feedback.


71-74: Make this a type-only import to avoid runtime circular deps

These are types only; using a value import risks a runtime cycle with experimental/router.

-import {
-  EXPERIMENTAL_RouterOptions_Base,
-  EXPERIMENTAL_Router_Base,
-  _OnReadyCallback,
-} from './experimental/router'
+import type {
+  EXPERIMENTAL_RouterOptions_Base,
+  EXPERIMENTAL_Router_Base,
+  _OnReadyCallback,
+} from './experimental/router'
🧹 Nitpick comments (7)
packages/router/src/router.ts (4)

20-20: Split type and value imports

HistoryState is a type; make it type-only for clarity and cleaner emits.

-import { HistoryState, NavigationType } from './history/common'
+import type { HistoryState } from './history/common'
+import { NavigationType } from './history/common'

47-47: Import App as a type

App is only used in type positions; import it as type.

-import { shallowRef, nextTick, App, unref, shallowReactive } from 'vue'
+import type { App } from 'vue'
+import { shallowRef, nextTick, unref, shallowReactive } from 'vue'

48-48: Type-only deep import (and good call on deep path per learnings)

RouteRecordNormalized is a type; import it as type. The deep path aligns with the repo’s dependency rules.

-import { RouteRecordNormalized } from './matcher/types'
+import type { RouteRecordNormalized } from './matcher/types'

897-901: Initialize ready to a boolean

Avoids relying on undefined truthiness.

-let ready: boolean
+let ready = false
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (3)

112-114: Avoid as any on set call

Use a typed helper to keep strong typing without any.

Apply:

-    return {
-      [this.queryKey]: (this.parser.set ?? PARAM_PARSER_DEFAULTS.set)(
-        paramValue as any
-      ),
-    }
+    const set: (v: T) => MatcherQueryParamsValue =
+      (this.parser.set ?? PARAM_PARSER_DEFAULTS.set) as (v: T) => MatcherQueryParamsValue
+    return {
+      [this.queryKey]: set(paramValue),
+    }

Longer-term, consider parameterizing TRaw on the class (e.g., ParamParser<T, MatcherQueryParamsValue, TRaw>) so build() can accept raw values without casts.


32-38: Doc update nit: comment mentions “value => keep the last value”

After the fix, please clarify the comment to note shape-preserving behavior for 'both', and that empty arrays arise only under 'array' normalization.

I can push a tiny doc tweak if you prefer.


21-31: Constructor surface looks good; consider documenting defaultValue ambiguity when T is a function

Make it explicit in the JSDoc that if T is a function/class, defaultValue should be provided as a zero-arg thunk (() => T) to avoid accidental invocation.

I can add the JSDoc line in a follow-up commit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8930dab and ecbbcd9.

📒 Files selected for processing (4)
  • packages/router/rollup.config.mjs (0 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1 hunks)
  • packages/router/src/router.ts (8 hunks)
💤 Files with no reviewable changes (1)
  • packages/router/rollup.config.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T15:22:32.526Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/index.ts:43-44
Timestamp: 2025-08-11T15:22:32.526Z
Learning: In the Vue Router codebase, files within the src directory should not import from src/index to avoid circular dependencies. Deep imports like `../matcher/types` are intentional and necessary for maintaining proper dependency hierarchy.

Applied to files:

  • packages/router/src/router.ts
🧬 Code graph analysis (2)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (5)
  • MatcherParamsFormatted (307-307)
  • MatcherPattern (20-43)
  • MatcherQueryParams (323-323)
  • MatcherQueryParamsValue (317-321)
  • EmptyParams (312-312)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-22)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)
  • ParamParser (34-34)
  • PARAM_PARSER_DEFAULTS (13-21)
packages/router/src/router.ts (2)
packages/router/src/experimental/router.ts (3)
  • EXPERIMENTAL_RouterOptions_Base (105-182)
  • EXPERIMENTAL_Router_Base (398-558)
  • _OnReadyCallback (94-94)
packages/router/src/utils/callbacks.ts (1)
  • useCallbacks (4-24)
⏰ 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: build
🔇 Additional comments (6)
packages/router/src/router.ts (2)

55-59: Guard helpers extraction looks solid

Moving these helpers out reduces router.ts surface and clarifies responsibilities.


379-388: Redirect handler now receives from — add tests to lock behavior

Great API: redirect(to, from). Please add/confirm tests for:

  • function redirects using from (e.g., query-dependent redirects)
  • redirect loops detection (guard-based self-redirects)

Also applies to: 440-441, 772-775

packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (4)

44-48: Null-to-empty-array coercion changes meaning for array format

Converting null to [] discards explicit nulls (presence vs absence). Ensure this is intentional for array semantics; otherwise keep [null] to preserve signal and let parsers decide.

I can adjust the normalization based on the desired contract.


88-96: Good missing-value behavior

Throwing miss() when no value and no default prevents silent matches. The later default application is clear.


104-110: Empty params return path is correct

Returning {} as EmptyParams when paramValue is undefined aligns with build contract.


62-67: I’ve added a script to verify the existence and contents of the errors.ts file in the matchers folder, and to check for any existing miss exports or an isMatchMiss guard. Please review the output.

Copy link

@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

♻️ Duplicate comments (5)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (4)

39-43: Avoid Array.prototype.at for compat; use index math.

-        ? Array.isArray(queryValue)
-          ? queryValue.at(-1)
+        ? Array.isArray(queryValue)
+          ? (queryValue.length ? queryValue[queryValue.length - 1] : undefined)

33-38: Early-return on missing query key instead of flowing through parsing.

Short-circuit reduces try/catch paths and ensures parser.get is never called with absent data.

   match(query: MatcherQueryParams): Record<ParamName, T> {
     const queryValue: MatcherQueryParamsValue | undefined = query[this.queryKey]
 
+    // Absent key → use default or miss immediately.
+    if (queryValue === undefined) {
+      const def = resolveDefault(this.defaultValue)
+      if (def === undefined) throw miss()
+      return { [this.paramName]: def } as Record<ParamName, T>
+    }
+
     // normalize the value coming from the query based on the expected format

52-71: Clarify/align array parsing contract (whole-array vs per-item).

You pass the entire array to parser.get. If custom parsers expect string inputs, this will throw for any multi-value query. Either document that get must accept arrays or map per-item and skip invalids.

Optional per-item approach:

-      } else {
-        try {
-          value = (this.parser.get ?? PARAM_PARSER_DEFAULTS.get)(
-            valueBeforeParse
-          ) as T
-        } catch (error) {
-          if (this.defaultValue === undefined) {
-            throw error
-          }
-          value = undefined
-        }
-      }
+      } else {
+        const get = this.parser.get ?? PARAM_PARSER_DEFAULTS.get
+        try {
+          // Map each element; skip invalids unless no default provided.
+          const out: unknown[] = []
+          for (const v of valueBeforeParse) {
+            if (v != null) {
+              try { out.push(get(v as any)) } catch (e) { if (this.defaultValue == null) throw e }
+            }
+          }
+          value = (out.length ? (out as any) : undefined) as T | undefined
+        } catch (error) {
+          if (this.defaultValue === undefined) throw error
+          value = undefined
+        }
+      }

1-1: Replace vue.toValue with a local default resolver (avoids Vue 3.3+ peer bump and accidental calls).

toValue() forces a Vue ≥3.3 peer and will eagerly invoke function/class defaults. Use a tiny local helper and drop the import.

Apply:

- import { toValue } from 'vue'
+ // (toValue import removed; using local resolver)

Add after imports:

@@
 import { miss } from './errors'
 
+// Resolve defaults without pulling Vue and with explicit () => T semantics.
+function resolveDefault<T>(def: T | (() => T) | undefined): T | undefined {
+  return def === undefined ? undefined : (typeof def === 'function' ? (def as () => T)() : def)
+}

Replace usages:

-        value = toValue(this.defaultValue)
+        value = resolveDefault(this.defaultValue)!
-      value = toValue(this.defaultValue)
+      value = resolveDefault(this.defaultValue)!

If you prefer keeping toValue(), bump peerDependencies.vue to ^3.3.0 and verify.

#!/usr/bin/env bash
# Check peer dependency and remaining toValue imports
jq -r '.peerDependencies.vue // empty' packages/router/package.json
rg -n "toValue\\(" -n packages/router

Also applies to: 55-57, 95-96, 11-16

packages/router/package.json (1)

8-11: Ship CJS-aware typings (.d.cts) and wire them in exports

Only .d.mts is emitted. CJS/NodeNext consumers may miss types. Add a CJS declaration and point the require branch to it; also include it in published files.

   "types": "dist/vue-router.d.mts",
   "exports": {
     ".": {
-      "types": "./dist/vue-router.d.mts",
+      "types": "./dist/vue-router.d.mts",
       "node": {
         "import": {
           "production": "./vue-router.node.mjs",
           "development": "./vue-router.node.mjs",
           "default": "./vue-router.node.mjs"
         },
         "require": {
+          "types": "./dist/vue-router.d.cts",
           "production": "./dist/vue-router.prod.cjs",
           "development": "./dist/vue-router.cjs",
           "default": "./index.js"
         }
       },
       "import": "./dist/vue-router.mjs",
       "require": "./index.js"
     },

Outside this hunk:

-    "dist/**/*.d.{ts,mts}",
+    "dist/**/*.d.{ts,mts,cts}",

And ensure the file exists (see comment on Lines 95-98 for build step).

🧹 Nitpick comments (4)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1)

111-115: Don’t emit an empty array in query; omit the key instead.

Serializing [] usually yields no meaningful URL state; returning EmptyParams is cleaner.

-    return {
-      [this.queryKey]: (this.parser.set ?? PARAM_PARSER_DEFAULTS.set)(
-        paramValue as any
-      ),
-    }
+    const serialized = (this.parser.set ?? PARAM_PARSER_DEFAULTS.set)(paramValue as any)
+    if (Array.isArray(serialized) && serialized.length === 0) {
+      return {} as EmptyParams
+    }
+    return { [this.queryKey]: serialized }
packages/router/package.json (3)

30-30: Expose types for "./experimental" subpath

Add a types entry so TS resolves subpath declarations without relying on root d.mts re-exports.

-    "./experimental": "./dist/experimental/index.mjs",
+    "./experimental": {
+      "types": "./dist/experimental/index.d.mts",
+      "default": "./dist/experimental/index.mjs"
+    },

95-98: Make build produce typings deterministically

Ensure build always emits declarations (including .d.cts) so “pnpm publish” doesn’t miss appended bits.

-    "build": "tsdown",
+    "build": "tsdown && pnpm run build:dts",
-    "build:dts": "tail -n +10 src/globalExtensions.ts >> dist/vue-router.d.mts",
+    "build:dts": "tail -n +10 src/globalExtensions.ts >> dist/vue-router.d.mts && cp dist/vue-router.d.mts dist/vue-router.d.cts",

Optional:

+    "prepublishOnly": "pnpm run build"

123-123: Remove unused API Extractor devDependency

No api-extractor invocation found in packages/router/package.json and the shared config isn’t used here; drop "@microsoft/api-extractor": "^7.52.8" from devDependencies to slim installs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ecbbcd9 and 7ff9507.

📒 Files selected for processing (4)
  • package.json (3 hunks)
  • packages/experiments-playground/src/router/index.ts (1 hunks)
  • packages/router/package.json (6 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/experiments-playground/src/router/index.ts
  • package.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T15:22:32.526Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/index.ts:43-44
Timestamp: 2025-08-11T15:22:32.526Z
Learning: In the Vue Router codebase, files within the src directory should not import from src/index to avoid circular dependencies. Deep imports like `../matcher/types` are intentional and necessary for maintaining proper dependency hierarchy.

Applied to files:

  • packages/router/package.json
🧬 Code graph analysis (1)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (5)
  • MatcherParamsFormatted (307-307)
  • MatcherPattern (20-43)
  • MatcherQueryParams (323-323)
  • MatcherQueryParamsValue (317-321)
  • EmptyParams (312-312)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-22)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)
  • ParamParser (34-34)
  • PARAM_PARSER_DEFAULTS (13-21)
🔇 Additional comments (9)
packages/router/package.json (9)

14-16: ESM node entry looks consistent

Single ESM file for all node conditions is fine and simplifies resolution.


35-37: Auto-routes node ESM mapping LGTM


52-54: Auto node ESM mapping LGTM


83-83: Include root node build in publish list

Good to see vue-router.node.mjs explicitly included.


113-113: Peer to Vue ^3.5.0 aligns with devDeps

Looks consistent with compiler/server-renderer pins.


131-135: Tooling versions acknowledged

Dev-only bumps (TS native preview, Vue toolchain) seem scoped; just ensure CI matrix pins matching Node/TS.

Would you like a CI job snippet to assert TypeScript NodeNext/Node16 resolution for .d.mts/.d.cts across TS 5.4–5.8?


137-137: Chromedriver bump

Looks fine; confirm e2e runner locks Chrome version to avoid drift.


142-142: Geckodriver bump

ACK.


150-154: Build tool upgrades

tsdown/tsup/vite updates acknowledged; no concerns from package.json side.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
packages/router/src/experimental/route-resolver/resolver-fixed.ts (1)

233-235: Respect explicit hash and keep current hash for relative-by-name

Explicit to.hash should win. For relative-by-name (no to.name), reuse currentLocation.hash. Otherwise fall back to record default. Current order ignores to.hash.

-      const hash =
-        record.hash?.build(params) ?? to.hash ?? currentLocation?.hash ?? ''
+      const hash =
+        // Prefer explicit hash; for relative-by-name keep current hash; else use record default
+        to.hash ??
+        (to.name == null ? currentLocation?.hash : record.hash?.build(params)) ??
+        record.hash?.build(params) ??
+        ''
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (2)

41-44: Avoid Array.prototype.at for broader runtime compatibility.

Replace .at(-1) with index math to support older browsers without polyfills.

-        ? Array.isArray(queryValue)
-          ? queryValue.at(-1)
+        ? Array.isArray(queryValue)
+          ? (queryValue.length ? queryValue[queryValue.length - 1] : undefined)

1-1: Remove Vue dependency for defaults; add local resolver (or bump Vue peer dep to ≥3.3).

Using toValue ties the router to Vue ≥3.3 and can eagerly invoke function/class defaults. Prefer a local resolveDefault() and drop the import, or explicitly bump the vue peerDependency to ^3.3.0.

Apply:

-import { toValue } from 'vue'
+// Local default resolver to avoid Vue peer dependency and accidental invocation of classes.
+function resolveDefault<T>(def: T | (() => T) | undefined): T | undefined {
+  if (def === undefined) return undefined
+  return typeof def === 'function' ? (def as () => T)() : def
+}
-        value = toValue(this.defaultValue)
+        value = resolveDefault(this.defaultValue)!
-      value = toValue(this.defaultValue)
+      value = resolveDefault(this.defaultValue)!

Also applies to: 56-59, 97-98

🧹 Nitpick comments (10)
packages/router/src/experimental/route-resolver/resolver-abstract.ts (3)

119-124: Fix minor doc typo (“the the”)

“the the one that matched the location” → “the one that matched the location”.

-   * Chain of route records that lead to the matched one. The last record is
-   * the the one that matched the location. Each previous record is the parent
+   * Chain of route records that lead to the matched one. The last record is
+   * the one that matched the location. Each previous record is the parent

131-135: Make NO_MATCH_LOCATION immutable

Prevent accidental mutation of the sentinel at runtime.

-export const NO_MATCH_LOCATION = {
+export const NO_MATCH_LOCATION = Object.freeze({
   name: __DEV__ ? Symbol('no-match') : Symbol(),
   params: {},
   matched: [],
-} satisfies Omit<ResolverLocationResolved<never>, keyof LocationNormalized>
+}) as Omit<ResolverLocationResolved<never>, keyof LocationNormalized>

146-158: Clarify params optionality for named locations

Type currently requires params. If unresolved routes can have no params, consider params?: MatcherParamsFormatted for DX consistency with classic router.

packages/router/src/experimental/route-resolver/resolver-fixed.ts (5)

190-205: Message nit: missing space in warning

Small readability fix.

-          `Cannot resolve relative location "${JSON.stringify(to)}"without a "name" or a current location. This will crash in production.`,
+          `Cannot resolve relative location "${JSON.stringify(to)}" without a "name" or a current location. This will crash in production.`,

270-282: Warn on malformed hash for object-relative path too (parity with named branch)

Mirror the DEV warning used in the named branch.

-      } else {
-        const query = normalizeQuery(to.query)
+      } else {
+        if (__DEV__ && to.hash && !to.hash.startsWith('#')) {
+          warn(
+            `A "hash" should start with "#". Replace "${to.hash}" with "#${to.hash}".`
+          )
+        }
+        const query = normalizeQuery(to.query)
         const path = resolveRelativePath(to.path, currentLocation?.path || '/')
         url = {
           fullPath: NEW_stringifyURL(stringifyQuery, path, query, to.hash),
           path,
           query,
           hash: to.hash || '',
         }
       }

246-256: Ensure LocationNormalized.hash has a consistent leading “#”

parseURL returns a hash that includes “#”; object branches may set url.hash to a value without it. Consider normalizing hash (e.g., always storing with leading “#” while letting NEW_stringifyURL handle encoding).

Would you like me to add a small helper (e.g., normalizeHash(str?: string): string) and update both branches plus a test?


102-111: Guard against cycles in parent chain

A malformed record graph with cycles will loop forever. Add a DEV-only visited set to break and warn.

 export function buildMatched<T extends EXPERIMENTAL_ResolverRecord>(
   record: T
 ): T[] {
   const matched: T[] = []
-  let node: T | undefined = record
-  while (node) {
+  const seen = __DEV__ ? new Set<T>() : undefined
+  let node: T | undefined = record
+  while (node) {
+    if (__DEV__ && seen!.has(node)) {
+      warn(`Cycle detected in record.parent chain for "${String((node as any).name)}".`)
+      break
+    }
+    __DEV__ && seen!.add(node)
     matched.unshift(node)
     node = node.parent as T
   }
   return matched
 }

125-130: Duplicate record names silently overwrite earlier entries

If two records share the same name, the later one wins with no signal. In DEV, detect duplicates and warn/throw.

-  for (const record of records) {
-    recordMap.set(record.name, record)
-  }
+  for (const record of records) {
+    if (__DEV__ && recordMap.has(record.name)) {
+      warn(`Duplicate resolver record name "${String(record.name)}"; the last one wins.`)
+    }
+    recordMap.set(record.name, record)
+  }
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (2)

34-36: Optional: short-circuit early when key is absent and a default exists.

Tiny simplification: if queryValue === undefined and there is a default, return immediately and skip normalization/parsing branches.

   match(query: MatcherQueryParams): Record<ParamName, T> {
     const queryValue: MatcherQueryParamsValue | undefined = query[this.queryKey]
+
+    if (queryValue === undefined && this.defaultValue !== undefined) {
+      return {
+        [this.paramName]: resolveDefault(this.defaultValue)!,
+      } as Record<ParamName, T>
+    }

40-51: Nit: type the intermediate for clarity.

Annotate valueBeforeParse as MatcherQueryParamsValue to make intent explicit and keep inference stable across refactors.

-    let valueBeforeParse =
+    let valueBeforeParse: MatcherQueryParamsValue =
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff9507 and 39a823c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • package.json (3 hunks)
  • packages/experiments-playground/package.json (1 hunks)
  • packages/playground/package.json (1 hunks)
  • packages/router/package.json (6 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/resolver-abstract.ts (1 hunks)
  • packages/router/src/experimental/route-resolver/resolver-fixed.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/experiments-playground/package.json
  • packages/playground/package.json
  • packages/router/package.json
  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T15:22:32.526Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/index.ts:43-44
Timestamp: 2025-08-11T15:22:32.526Z
Learning: In the Vue Router codebase, files within the src directory should not import from src/index to avoid circular dependencies. Deep imports like `../matcher/types` are intentional and necessary for maintaining proper dependency hierarchy.

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
🧬 Code graph analysis (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (5)
  • MatcherParamsFormatted (309-309)
  • MatcherPattern (20-43)
  • MatcherQueryParams (325-325)
  • MatcherQueryParamsValue (319-323)
  • EmptyParams (314-314)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-22)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)
  • ParamParser (34-34)
  • PARAM_PARSER_DEFAULTS (13-21)
packages/router/src/experimental/route-resolver/resolver-fixed.ts (6)
packages/router/src/experimental/route-resolver/resolver-abstract.ts (8)
  • RecordName (10-10)
  • EXPERIMENTAL_Resolver_Base (19-91)
  • ResolverLocationResolved (108-125)
  • ResolverLocationAsPathAbsolute (196-199)
  • ResolverLocationAsPathRelative (170-183)
  • ResolverLocationAsNamed (147-158)
  • ResolverLocationAsRelative (212-225)
  • NO_MATCH_LOCATION (131-135)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (4)
  • MatcherPatternPath (49-53)
  • MatcherPatternHash (300-302)
  • MatcherQueryParams (325-325)
  • MatcherParamsFormatted (309-309)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1)
  • MatcherPatternQuery (16-18)
packages/router/src/location.ts (4)
  • LocationNormalized (13-18)
  • NEW_stringifyURL (120-128)
  • parseURL (44-97)
  • resolveRelativePath (241-287)
packages/router/src/warning.ts (1)
  • warn (2-9)
packages/router/src/query.ts (1)
  • normalizeQuery (131-148)
packages/router/src/experimental/route-resolver/resolver-abstract.ts (2)
packages/router/src/location.ts (1)
  • LocationNormalized (13-18)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1)
  • MatcherParamsFormatted (309-309)
⏰ 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: build
🔇 Additional comments (2)
package.json (2)

74-79: onlyBuiltDependencies whitelist is complete
Verification found no additional packages marked requiresBuild: true in pnpm-lock.yaml beyond chromedriver, esbuild, and geckodriver.


37-50: TypeScript 5.8 + TypeDoc stack compatibility verified
TypeDoc 0.28.x officially supports TypeScript 5.8 cite12, and typedoc-plugin-markdown 4.7.x targets TypeDoc 0.28.x cite34; no doc-gen changes required.

Comment on lines +16 to +18
export interface MatcherPatternQuery<
TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
> extends MatcherPattern<MatcherQueryParams, TParams> {}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Preserve TParamRaw in the public types; eliminate as any in build().

MatcherPattern supports a distinct TParamsRaw (e.g., allowing nullish values on navigation), but MatcherPatternQuery (and this class) collapse raw to parsed, forcing as any. Forward the third generic and thread it through the class.

-export interface MatcherPatternQuery<
-  TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
-> extends MatcherPattern<MatcherQueryParams, TParams> {}
+export interface MatcherPatternQuery<
+  TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
+  TParamsRaw extends MatcherParamsFormatted = TParams,
+> extends MatcherPattern<MatcherQueryParams, TParams, TParamsRaw> {}
-export class MatcherPatternQueryParam<T, ParamName extends string>
-  implements MatcherPatternQuery<Record<ParamName, T>>
+export class MatcherPatternQueryParam<
+  T,
+  ParamName extends string,
+  TRaw = T
+> implements
+  MatcherPatternQuery<Record<ParamName, T>, Record<ParamName, TRaw>>
 {
   constructor(
     private paramName: ParamName,
     private queryKey: string,
     private format: 'value' | 'array',
-    private parser: ParamParser<T> = {},
+    private parser: ParamParser<T, MatcherQueryParamsValue, TRaw> = {},
     private defaultValue?: (() => T) | T
   ) {}
-  build(params: Record<ParamName, T>): MatcherQueryParams {
+  build(params: Record<ParamName, TRaw>): MatcherQueryParams {
     const paramValue = params[this.paramName]

     if (paramValue === undefined) {
       return {} as EmptyParams
     }

     return {
-      [this.queryKey]: (this.parser.set ?? PARAM_PARSER_DEFAULTS.set)(
-        paramValue as any
-      ),
+      [this.queryKey]: (this.parser.set ?? PARAM_PARSER_DEFAULTS.set)(paramValue),
     }
   }

Also applies to: 23-33, 106-118

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
.github/workflows/test.yml (1)

29-35: Pin BrowserStack action to a tag or commit SHA (avoid @master)

Using a moving branch is a supply-chain risk. Pin to a release tag or commit.

-        uses: 'browserstack/github-actions/setup-env@master'
+        uses: 'browserstack/github-actions/setup-env@<pinned-commit-sha>'

(Same for the commented setup-local steps below when re-enabling.)

♻️ Duplicate comments (1)
package.json (1)

4-4: Pin pnpm in Volta and confirm lockfile upgrade.

Add pnpm to Volta to avoid local/CI drift and ensure pnpm-lock.yaml was regenerated by pnpm 10.

Apply:

 "volta": {
-  "node": "24.7.0"
+  "node": "24.7.0",
+  "pnpm": "10.15.0"
 }

Quick checks:

#!/bin/bash
# Expect lockfileVersion compatible with pnpm@10 and packageManager matching
rg -n '^lockfileVersion:' pnpm-lock.yaml || true
rg -n '^  "packageManager":' package.json
🧹 Nitpick comments (4)
.github/workflows/test.yml (4)

22-22: Harden checkout step: drop token, keep shallow fetch

Prevent write access to the repo in this job and make the intent explicit.

Apply:

-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v5
+        with:
+          persist-credentials: false
+          fetch-depth: 1

18-21: Set least-privilege GITHUB_TOKEN for the job

Explicit permissions help reduce blast radius; this job only reads code.

     runs-on: ubuntu-latest
+    permissions:
+      contents: read

If you want fully deterministic runners, consider pinning the OS image (e.g., ubuntu-24.04) instead of ubuntu-latest.


36-36: Use frozen lockfile for deterministic installs

Fail fast on lockfile drift in CI.

-      - run: pnpm install
+      - run: pnpm install --frozen-lockfile

46-48: Fail CI if Codecov upload errors, and consider pinning

Make coverage upload issues visible; also consider pinning to a commit SHA.

-      - uses: codecov/codecov-action@v4
+      - uses: codecov/codecov-action@v4
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
+          fail_ci_if_error: true
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 39a823c and 7d6164a.

📒 Files selected for processing (2)
  • .github/workflows/test.yml (1 hunks)
  • package.json (3 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). (1)
  • GitHub Check: build
🔇 Additional comments (3)
.github/workflows/test.yml (1)

22-22: actions/checkout v5 bump looks good

Node 20-based v5 aligns with the rest of the toolchain here. No functional changes expected.

package.json (2)

74-79: onlyBuiltDependencies allowlist verified—no missing build deps Verified lockfileVersion=9.0; no requiresBuild:true entries in pnpm-lock.yaml; no install/prepare/postinstall scripts in local packages; chromedriver, esbuild, and geckodriver are present and correctly whitelisted.


37-50: Ensure docs generation is validated in CI
I didn’t find any GitHub Actions steps invoking typedoc or docs:api; add or enable a CI job that runs npm run docs:api (or equivalent) against these updated dependencies to confirm the pipeline still succeeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 🧑‍💻 In progress
Development

Successfully merging this pull request may close these issues.

1 participant