-
-
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
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.61% -5.29%
==========================================
Files 34 46 +12
Lines 3002 4103 +1101
Branches 846 1090 +244
==========================================
+ Hits 2849 3677 +828
- Misses 150 421 +271
- 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: 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.
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
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
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.
improve build compat for non unix
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/router/__tests__/guards/beforeEnter.spec.ts (2)
111-116
: Update expectations to the new guard signature (no next callback).Tests still expect a third
next
argument. With return-based guards, only(to, from)
are passed.- expect(beforeEnters[0]).toHaveBeenCalledWith( - expect.objectContaining({ path: '/multiple' }), - expect.objectContaining({ path: '/' }), - expect.any(Function) - ) + expect(beforeEnters[0]).toHaveBeenCalledWith( + expect.objectContaining({ path: '/multiple' }), + expect.objectContaining({ path: '/' }) + )
127-136
: Same here: remove the thirdnext
expectation.- expect(nested.nestedNested).toHaveBeenCalledWith( - expect.objectContaining({ path: '/nested/nested/foo' }), - expect.objectContaining({ path: '/nested/a' }), - expect.any(Function) - ) - expect(nested.nestedNestedFoo).toHaveBeenCalledWith( - expect.objectContaining({ path: '/nested/nested/foo' }), - expect.objectContaining({ path: '/nested/a' }), - expect.any(Function) - ) + expect(nested.nestedNested).toHaveBeenCalledWith( + expect.objectContaining({ path: '/nested/nested/foo' }), + expect.objectContaining({ path: '/nested/a' }) + ) + expect(nested.nestedNestedFoo).toHaveBeenCalledWith( + expect.objectContaining({ path: '/nested/nested/foo' }), + expect.objectContaining({ path: '/nested/a' }) + )packages/router/__tests__/guards/beforeRouteLeave.spec.ts (1)
143-154
: Update expectations to remove the legacynext
argument.The assertions still expect a third parameter. With return-based guards, only
(to, from)
are passed.expect(nested.nestedA).toHaveBeenCalledWith( expect.objectContaining({ name: 'nested-path-b', fullPath: '/nested/b', }), expect.objectContaining({ name: 'nested-path', fullPath: '/nested/a', - }), - expect.any(Function) + }) )
♻️ Duplicate comments (4)
packages/router/src/experimental/route-resolver/resolver-fixed.ts (4)
169-176
: LGTM! The flatMap usage correctly filters undefined values.The code properly leverages JavaScript's
flatMap
behavior which automatically excludesundefined
values whenrecord.query
is missing. The spread operator safely receives only valid query matcher results.
240-248
: LGTM! Query merging correctly handles all cases.The query resolution properly merges current location query, user-provided query, and record-defined query builders. The flatMap usage is safe due to its undefined-filtering behavior.
279-287
: Use resolved path for fullPath consistencyFor object-relative navigations, the
fullPath
is currently built using the rawto.path
instead of the resolved absolute path. This creates inconsistency with the string path branch and could expose relative paths in the final location.Apply this diff to use the resolved path:
} else { const query = normalizeQuery(to.query) const path = resolveRelativePath(to.path, currentLocation?.path || '/') url = { - fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash), + fullPath: NEW_stringifyURL(stringifyQuery, path, query, to.hash), path, query, hash: to.hash || '', } }
237-238
: Hash resolution needs refinement for object navigationsThe hash resolution logic has two issues:
- When
to.hash
is explicitly provided, it should take precedence overrecord.hash?.build(params)
- For relative-by-name navigations (when
to.name
is undefined), the current location's hash should be preserved as per the docstringThe current implementation builds hash from the record even when
to.hash
is provided, and doesn't distinguish between named vs relative-by-name navigations.Apply this diff to fix the hash resolution priority:
- const hash = - record.hash?.build(params) ?? to.hash ?? currentLocation?.hash ?? '' + const hash = + to.hash ?? + (to.name == null && currentLocation?.hash) ?? + record.hash?.build(params) ?? + ''Consider adding tests for:
- Named navigation with explicit
to.hash
- Relative-by-name navigation that should preserve
currentLocation.hash
🧹 Nitpick comments (20)
packages/router/src/experimental/route-resolver/resolver-fixed.ts (1)
298-306
: Consider using more specific error handlingThe catch-all error handling during route matching silently swallows all errors. Consider catching only expected validation errors to avoid masking unexpected bugs.
You could make error handling more specific:
try { ;[matched, parsedParams] = validateMatch(record, url) // validate throws if no match, so we should break here break - } catch (e) { - // for debugging tests - // console.log('❌ ERROR matching', e) + } catch (e) { + // Expected when match fails - continue to next record + if (__DEV__ && !(e instanceof MatcherError)) { + console.warn('Unexpected error during route matching:', e) + } }This assumes you'd create a specific
MatcherError
class for expected matching failures.packages/router/__tests__/multipleApps.spec.ts (2)
37-39
: Return-based guard spy is correct; consider a simpler no-op.
vi.fn(() => {})
is enough here and avoids any accidental implicit returns. If you want type coverage, you can also type it as aNavigationGuard
in tests.
46-46
: Avoid time-based sleeps; wait on navigation instead.
await delay(5)
can be flaky on slow CI. Prefer an event-driven wait (e.g.,await nextNavigation(router)
or a tick-based helper) to make the test deterministic.Also applies to: 52-52
packages/router/__tests__/guards/extractComponentsGuards.spec.ts (2)
43-45
: LGTM on migrating to return-based guards; add typing for safety.Type the spy as a
NavigationGuard
to catch accidental API regressions.-import type { RouteRecordRaw, RouteRecordNormalized } from '../../src' +import type { RouteRecordRaw, RouteRecordNormalized, NavigationGuard } from '../../src' @@ -const beforeRouteEnter = vi.fn() +const beforeRouteEnter = vi.fn<NavigationGuard>()
64-65
: Make the assertion explicit.
expect(await guard())
doesn’t assert anything. Prefer asserting the resolved value.- expect(await guard()) + expect(await guard()).toBeUndefined()packages/router/__tests__/guards/beforeRouteUpdate.spec.ts (2)
71-74
: LGTM; you can simplify by returning the promise.Returning the promise directly is slightly clearer.
-beforeRouteUpdate.mockImplementationOnce(async (to, from) => { - await promise - return -}) +beforeRouteUpdate.mockImplementationOnce(() => promise)
75-81
: Ensure the component is “mounted” so beforeRouteUpdate actually runs.This test relies on the update guard delaying navigation. Explicitly simulate an instance after the first push to guarantee the guard is invoked.
await router.push('/guard/one') +// simulate a mounted route component so beforeRouteUpdate runs +router.currentRoute.value.matched[0].instances.default = {} as any const p = router.push('/guard/foo')packages/router/__tests__/guards/beforeRouteEnter.spec.ts (2)
207-210
: LGTM; minor simplification possible.You can return the promise directly instead of using an async wrapper.
-beforeRouteEnter.mockImplementationOnce(async (to, from) => { - await promise - return -}) +beforeRouteEnter.mockImplementationOnce(() => promise)
136-153
: Duplicate test block with identical description.There are two “calls beforeRouteEnter guards on navigation for nested views” tests. Please remove or rename the duplicate to avoid confusion.
packages/router/__tests__/initialNavigation.spec.ts (1)
22-24
: LGTM; return-based redirect is correct.Optionally, return a location object for clarity:
return { path: '/' }
.packages/router/__tests__/guards/beforeRouteLeave.spec.ts (1)
97-100
: Fix path comparison for clarity.The route path includes the leading slash; compare against
'/foo'
to match the navigation below.-beforeRouteLeave.mockImplementationOnce((to, from) => { - if (to.path === 'foo') return false - else return -}) +beforeRouteLeave.mockImplementationOnce((to, from) => { + if (to.path === '/foo') return false + return +})packages/router/__tests__/errors.spec.ts (2)
112-118
: Trim redundant branches in async guardReturning
void
is implicit; simplify the flow and avoid an unnecessaryelse
.- router.beforeEach(async (to, from) => { - // let it hang otherwise - if (to.path === '/') return - else { - await promise - return - } - }) + router.beforeEach(async to => { + // let it hang otherwise + if (to.path !== '/') await promise + })
240-247
: Use concise guard for history testSame simplification as above; ternary reads cleaner.
- await testHistoryNavigation( - ((to, from) => { - if (to.path === '/location') return - else return '/location' - }) as NavigationGuard, - undefined - ) + await testHistoryNavigation( + ((to, from) => (to.path === '/location' ? undefined : '/location')) as NavigationGuard, + undefined + )packages/router/__tests__/guards/beforeEach.spec.ts (4)
90-94
: Prefer a concise return for redirect guardEquivalent behavior with less branching.
- spy.mockImplementation((to, from) => { - // only allow going to /other - if (to.fullPath !== '/other') return '/other' - else return - }) + spy.mockImplementation(to => + to.fullPath !== '/other' ? '/other' : undefined + )
163-168
: Tighten multi‑step redirect guardKeep the intent, reduce branches.
- spy.mockImplementation((to, from) => { - // only allow going to /other - const i = Number(to.params.i) - if (i >= 3) return - else return redirectFn(String(i + 1)) - }) + spy.mockImplementation(to => { + const i = Number(to.params.i) + return i >= 3 ? undefined : redirectFn(String(i + 1)) + })
213-216
: Drop explicitreturn
in async no‑op guardImplicit
void
is fine.- router.beforeEach(async (to, from) => { - await promise - return - }) + router.beforeEach(async () => { + await promise + })
231-234
: Guard stubs: remove unused params and explicit returnsKeep guards minimal; no behavior change.
- guard1.mockImplementationOnce(async (to, from) => { + guard1.mockImplementationOnce(async () => { expect(order++).toBe(0) await p1 - return }) @@ - guard2.mockImplementationOnce(async (to, from) => { + guard2.mockImplementationOnce(async () => { expect(order++).toBe(1) await p2 - return })Also applies to: 237-241
packages/router/__tests__/router.spec.ts (3)
563-571
: Return‑based cancellation test reads wellGood migration; tiny cleanup: omit explicit
return
in the else branch.- router.beforeEach(async (to, from) => { + router.beforeEach(async (to, from) => { if (to.name !== 'Param') return // the first navigation gets passed target if (to.params.p === 'a') { await p1 return target || undefined - } else { - // the second one just passes - return - } + } + // the second one just passes
616-627
: Minor branch pruning in popstate test guardSame cleanup pattern as above.
- router.beforeEach(async (to, from) => { + router.beforeEach(async (to, from) => { if (to.name !== 'Param') return if (to.fullPath === '/foo') { await p1 - return + return } else if (from.fullPath === '/p/b') { await p2 // @ts-ignore: same as function above return target - } else { - return - } + } })
977-986
: Dynamic beforeEnter: redirect by returningto.fullPath
Logic is sound and matches the new pattern. Consider a tiny readability tweak.
- beforeEnter(to, from) { + beforeEnter(to) { if (!removeRoute) { removeRoute = router.addRoute('dynamic parent', { path: 'child', name: 'dynamic child', component: components.Foo, }) return to.fullPath - } else return + } + return },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (18)
packages/router/__tests__/errors.spec.ts
(3 hunks)packages/router/__tests__/guards/beforeEach.spec.ts
(4 hunks)packages/router/__tests__/guards/beforeEnter.spec.ts
(2 hunks)packages/router/__tests__/guards/beforeRouteEnter.spec.ts
(3 hunks)packages/router/__tests__/guards/beforeRouteLeave.spec.ts
(4 hunks)packages/router/__tests__/guards/beforeRouteUpdate.spec.ts
(1 hunks)packages/router/__tests__/guards/extractComponentsGuards.spec.ts
(1 hunks)packages/router/__tests__/initialNavigation.spec.ts
(1 hunks)packages/router/__tests__/lazyLoading.spec.ts
(5 hunks)packages/router/__tests__/multipleApps.spec.ts
(1 hunks)packages/router/__tests__/router.spec.ts
(10 hunks)packages/router/package.json
(6 hunks)packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts
(1 hunks)packages/router/src/experimental/route-resolver/matchers/param-parsers/types.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)packages/router/src/experimental/router.spec.ts
(1 hunks)packages/router/src/experimental/router.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- packages/router/src/experimental/router.spec.ts
- packages/router/src/experimental/route-resolver/resolver-abstract.ts
- packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts
- packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts
- packages/router/src/experimental/router.ts
🧰 Additional context used
🧠 Learnings (2)
📚 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
📚 Learning: 2025-08-31T16:03:10.087Z
Learnt from: posva
PR: vuejs/router#2415
File: packages/router/src/experimental/route-resolver/resolver-fixed.ts:236-244
Timestamp: 2025-08-31T16:03:10.087Z
Learning: The `flatMap` method in JavaScript automatically filters out `undefined` values returned by the callback function. When using `array.flatMap(item => item.property?.map(...))`, if `property` is undefined, the `?.map(...)` returns undefined, and `flatMap` excludes it from the result. This makes the spread operator safe to use with the result of `flatMap`.
Applied to files:
packages/router/src/experimental/route-resolver/resolver-fixed.ts
🧬 Code graph analysis (4)
packages/router/__tests__/guards/beforeRouteEnter.spec.ts (1)
packages/router/__tests__/guards/beforeRouteEnterCallback.spec.ts (2)
beforeRouteEnter
(14-16)beforeRouteEnter
(24-26)
packages/router/__tests__/guards/extractComponentsGuards.spec.ts (2)
packages/router/__tests__/guards/beforeRouteEnterCallback.spec.ts (2)
beforeRouteEnter
(14-16)beforeRouteEnter
(24-26)packages/router/e2e/guards-instances/index.ts (1)
beforeRouteEnter
(66-73)
packages/router/__tests__/router.spec.ts (2)
packages/router/src/query.ts (1)
parseQuery
(54-81)packages/router/src/index.ts (1)
parseQuery
(7-7)
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-90)ResolverLocationResolved
(107-124)ResolverLocationAsPathAbsolute
(195-198)ResolverLocationAsPathRelative
(169-182)ResolverLocationAsNamed
(146-157)ResolverLocationAsRelative
(211-224)NO_MATCH_LOCATION
(130-134)packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (4)
MatcherPatternPath
(52-56)MatcherPatternHash
(303-305)MatcherQueryParams
(328-328)MatcherParamsFormatted
(312-312)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)
🔇 Additional comments (14)
packages/router/package.json (1)
129-151
: LGTM! Comprehensive dev dependency updates.The updates to development dependencies including tsdown, Vue 3.5+ tooling, and related packages are consistent with the build system migration and peer dependency bump.
packages/router/src/experimental/route-resolver/resolver-fixed.ts (4)
1-334
: Overall implementation looks solid with robust type safety!The experimental fixed resolver implementation provides a clean, type-safe approach to route resolution with support for custom matchers and flexible location resolution. The code properly handles various navigation patterns (named, path-based, relative) and includes appropriate dev-mode validation.
224-228
: Hash validation logic is correctThe warning correctly triggers when the hash does not start with '#', helping developers catch malformed hash values early.
163-184
: Extract common validation logic to reduce duplicationThe
validateMatch
function logic is duplicated inline at line 263. Consider always using the extracted function for consistency.Line 263 already calls
validateMatch
, so this comment about duplication appears to be incorrect upon closer inspection. The code is properly reusing the function.
106-116
: Efficient parent traversal implementationThe
buildMatched
function efficiently builds the matched chain from leaf to root using unshift. Good implementation!packages/router/__tests__/guards/beforeRouteEnter.spec.ts (2)
114-117
: Abort via boolean return looks good.
186-188
: Named-view abort via boolean return looks good.packages/router/__tests__/guards/beforeEnter.spec.ts (1)
160-163
: LGTM on return-based guard implementations.Also applies to: 175-178, 179-182
packages/router/__tests__/lazyLoading.spec.ts (1)
162-169
: Return-based guard migration looks consistent throughout.Abort with
false
and proceed by returningvoid
are applied correctly.Also applies to: 190-193, 202-205, 218-221, 243-246
packages/router/__tests__/guards/beforeRouteLeave.spec.ts (1)
113-115
: LGTM on return-based leave guards and ordering checks.Also applies to: 161-172, 187-189
packages/router/__tests__/router.spec.ts (3)
23-25
: beforeEnter: return‑based redirect looks goodSwitch to
return '/'
aligns with the updated guard pattern.
170-175
: Correct expectation for parseQuery input — verified
parseQuery is defined in packages/router/src/query.ts and only invoked from packages/router/src/location.ts (line 71), so the leading '?' contract is centralized and consistent; approve.
279-283
: Keep the 3rdnext
in expectations — NavigationGuard still supports itbeforeEach is typed as NavigationGuardWithThis (the guard type includes the optional third
next
), so spies declared as (to, from) will still receive anext
argument; no change required to the tests.packages/router/__tests__/errors.spec.ts (1)
87-94
: Rename test and simplify return-based guard — other tests still assert a 3rdnext
argRename the test title and simplify the guard to a ternary; suggested diff:
- it('next("/location") triggers afterEach', async () => { + it('redirect (return "/location") triggers afterEach', async () => { await testNavigation( - ((to, from) => { - if (to.path === '/location') return - else return '/location' - }) as NavigationGuard, + ((to, from) => (to.path === '/location' ? undefined : '/location')) as NavigationGuard, undefined ) })rg found remaining uses of expect.any(Function) in tests (update these if you intend to remove the 3rd
next
arg globally):
- packages/router/tests/router.spec.ts:707
- packages/router/tests/guards/guardToPromiseFn.spec.ts:21
- packages/router/tests/guards/beforeRouteLeave.spec.ts:152
- packages/router/tests/guards/beforeEnter.spec.ts:114
- packages/router/tests/guards/beforeEnter.spec.ts:130
- packages/router/tests/guards/beforeEnter.spec.ts:135
- packages/router/tests/guards/beforeEach.spec.ts:75
- packages/router/tests/guards/beforeEach.spec.ts:81
- packages/router/tests/guards/beforeEach.spec.ts:104
- packages/router/tests/guards/beforeEach.spec.ts:110
- packages/router/tests/guards/beforeEach.spec.ts:269
If the long‑term goal is dropping the 3rd
next
arg, update those tests or add a 2‑arg assertion variant; otherwise this single-test change is fine.
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