-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Custom Route Resolvers #2415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Custom Route Resolvers #2415
Conversation
✅ Deploy Preview for vue-router canceled.
|
commit: |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
[skip ci]
There was a problem hiding this 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 outputYou 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 routeCover 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 intentAcknowledged 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 forv === undefined
in parser.getIn match(), captures are normalized with
?? null
, soget()
never receivesundefined
. 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.
📒 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.
There was a problem hiding this 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 directivePer 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 exportAn 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.
📒 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 goodThe 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 neededTypeScript 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 inparam-parsers/types.ts
.packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts (3)
1-129
: Strong coverage across static/star patternsGood breadth of cases (case-insensitivity, prefixes, build symmetry). Assertions read cleanly.
363-380
: Dynamic + trailing-slash cases are well coveredEdge conditions and build() symmetry checks are solid.
442-511
: Custom parsers: nice real-world scenariosThe 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 friendlyRequired plus the no-side-effects pragma is good.
67-73
: defineQueryParamParser: good generic defaultsUsing MatcherQueryParamsValue for the URL-side type is appropriate.
84-84
: Single source of truth for defineParamParser aliasAlias to defineQueryParamParser here is fine, given removal of the earlier duplicate export elsewhere.
There was a problem hiding this 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)
turnsnull
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.
📒 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)
packages/router/src/experimental/route-resolver/matchers/param-parsers/strings.spec.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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 nowThe links target RouteRecordRaw (not EXPERIMENTAL_*). This resolves prior feedback.
71-74
: Make this a type-only import to avoid runtime circular depsThese 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 importsHistoryState 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 typeApp 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 booleanAvoids relying on undefined truthiness.
-let ready: boolean +let ready = falsepackages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (3)
112-114
: Avoidas any
onset
callUse 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 functionMake 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.
📒 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 solidMoving these helpers out reduces router.ts surface and clarifies responsibilities.
379-388
: Redirect handler now receives from — add tests to lock behaviorGreat 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 formatConverting 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 behaviorThrowing miss() when no value and no default prevents silent matches. The later default application is clear.
104-110
: Empty params return path is correctReturning {} as EmptyParams when paramValue is undefined aligns with build contract.
62-67
: I’ve added a script to verify the existence and contents of theerrors.ts
file in thematchers
folder, and to check for any existingmiss
exports or anisMatchMiss
guard. Please review the output.
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
Show resolved
Hide resolved
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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/routerAlso 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 exportsOnly .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" subpathAdd 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 deterministicallyEnsure 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 devDependencyNo
api-extractor
invocation found inpackages/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.
📒 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 consistentSingle 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 listGood to see vue-router.node.mjs explicitly included.
113-113
: Peer to Vue ^3.5.0 aligns with devDepsLooks consistent with compiler/server-renderer pins.
131-135
: Tooling versions acknowledgedDev-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 bumpLooks fine; confirm e2e runner locks Chrome version to avoid drift.
142-142
: Geckodriver bumpACK.
150-154
: Build tool upgradestsdown/tsup/vite updates acknowledged; no concerns from package.json side.
There was a problem hiding this 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-nameExplicit
to.hash
should win. For relative-by-name (noto.name
), reusecurrentLocation.hash
. Otherwise fall back to record default. Current order ignoresto.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 localresolveDefault()
and drop the import, or explicitly bump thevue
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 immutablePrevent 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 locationsType currently requires
params
. If unresolved routes can have no params, considerparams?: MatcherParamsFormatted
for DX consistency with classic router.packages/router/src/experimental/route-resolver/resolver-fixed.ts (5)
190-205
: Message nit: missing space in warningSmall 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 ahash
that includes “#”; object branches may seturl.hash
to a value without it. Consider normalizinghash
(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 chainA 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 entriesIf 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
asMatcherQueryParamsValue
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.
⛔ 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 markedrequiresBuild: true
inpnpm-lock.yaml
beyondchromedriver
,esbuild
, andgeckodriver
.
37-50
: TypeScript 5.8 + TypeDoc stack compatibility verified
TypeDoc 0.28.x officially supports TypeScript 5.8 cite12, and typedoc-plugin-markdown 4.7.x targets TypeDoc 0.28.x cite34; no doc-gen changes required.
export interface MatcherPatternQuery< | ||
TParams extends MatcherParamsFormatted = MatcherParamsFormatted, | ||
> extends MatcherPattern<MatcherQueryParams, TParams> {} |
There was a problem hiding this comment.
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
There was a problem hiding this 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 fetchPrevent 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 jobExplicit permissions help reduce blast radius; this job only reads code.
runs-on: ubuntu-latest + permissions: + contents: readIf 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 installsFail fast on lockfile drift in CI.
- - run: pnpm install + - run: pnpm install --frozen-lockfile
46-48
: Fail CI if Codecov upload errors, and consider pinningMake 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.
📒 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 goodNode 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; norequiresBuild: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 invokingtypedoc
ordocs:api
; add or enable a CI job that runsnpm run docs:api
(or equivalent) against these updated dependencies to confirm the pipeline still succeeds.
New version of the internal matcher (renamed as resolver). With more responsibilities and allowing it to be overridden:
Summary by CodeRabbit
New Features
Improvements
Tests
Chores