Skip to content

Conversation

posva
Copy link
Member

@posva posva commented Dec 5, 2024

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

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

Summary by CodeRabbit

  • New Features

    • Interactive Experiments Playground app for exploring routing and inspecting route state.
    • Public experimental routing API and resolver-driven routing primitives.
  • Improvements

    • Documentation sidebars now load API navigation dynamically for English and Chinese.
    • Refined URL parsing/serialization and more robust parameter parsing/building.
  • Tests

    • Extensive new and updated tests covering experimental router, matchers, param parsers, and guards.
  • Chores

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

Copy link

netlify bot commented Dec 5, 2024

Deploy Preview for vue-router canceled.

Name Link
🔨 Latest commit 6481cf6
🔍 Latest deploy log https://app.netlify.com/projects/vue-router/deploys/68cc24d3c61db8000803ac2d

Copy link

pkg-pr-new bot commented Dec 5, 2024

Open in StackBlitz

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

commit: 6481cf6

Copy link

codecov bot commented Dec 6, 2024

Codecov Report

❌ Patch coverage is 77.08688% with 269 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.61%. Comparing base (6a11243) to head (6481cf6).
⚠️ Report is 196 commits behind head on main.

Files with missing lines Patch % Lines
packages/router/src/experimental/router.ts 61.22% 226 Missing and 2 partials ⚠️
.../src/experimental/route-resolver/resolver-fixed.ts 86.52% 19 Missing ⚠️
...imental/route-resolver/matchers/matcher-pattern.ts 92.10% 9 Missing ⚠️
...tal/route-resolver/matchers/param-parsers/index.ts 75.00% 8 Missing ⚠️
...l/route-resolver/matchers/matcher-pattern-query.ts 93.24% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2415      +/-   ##
==========================================
- Coverage   94.90%   89.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.
📢 Have feedback on the report? Share it here.

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

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

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

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

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

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

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

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

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

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

18-19: Doc nit: clarify sentence.

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

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

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

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

Applied to files:

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

Applied to files:

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

96-113: JSDoc references are correct now

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


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

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

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

20-20: Split type and value imports

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

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

47-47: Import App as a type

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

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

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

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

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

897-901: Initialize ready to a boolean

Avoids relying on undefined truthiness.

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

112-114: Avoid as any on set call

Use a typed helper to keep strong typing without any.

Apply:

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

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


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

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

I can push a tiny doc tweak if you prefer.


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

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

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

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

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

Applied to files:

  • packages/router/src/router.ts
🧬 Code graph analysis (2)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (5)
  • MatcherParamsFormatted (307-307)
  • MatcherPattern (20-43)
  • MatcherQueryParams (323-323)
  • MatcherQueryParamsValue (317-321)
  • EmptyParams (312-312)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-22)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)
  • ParamParser (34-34)
  • PARAM_PARSER_DEFAULTS (13-21)
packages/router/src/router.ts (2)
packages/router/src/experimental/router.ts (3)
  • EXPERIMENTAL_RouterOptions_Base (105-182)
  • EXPERIMENTAL_Router_Base (398-558)
  • _OnReadyCallback (94-94)
packages/router/src/utils/callbacks.ts (1)
  • useCallbacks (4-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
packages/router/src/router.ts (2)

55-59: Guard helpers extraction looks solid

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


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

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

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

Also applies to: 440-441, 772-775

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

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

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

I can adjust the normalization based on the desired contract.


88-96: Good missing-value behavior

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


104-110: Empty params return path is correct

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


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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

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

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

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

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

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

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

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

Optional per-item approach:

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

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

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

Apply:

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

Add after imports:

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

Replace usages:

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

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

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

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

packages/router/package.json (1)

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

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

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

Outside this hunk:

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

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

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

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

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

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

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

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

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

95-98: Make build produce typings deterministically

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

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

Optional:

+    "prepublishOnly": "pnpm run build"

123-123: Remove unused API Extractor devDependency

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

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

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

Applied to files:

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

14-16: ESM node entry looks consistent

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


35-37: Auto-routes node ESM mapping LGTM


52-54: Auto node ESM mapping LGTM


83-83: Include root node build in publish list

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


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

Looks consistent with compiler/server-renderer pins.


131-135: Tooling versions acknowledged

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

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


137-137: Chromedriver bump

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


142-142: Geckodriver bump

ACK.


150-154: Build tool upgrades

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

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

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

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

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

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

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

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

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

Apply:

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

Also applies to: 56-59, 97-98

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

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

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

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

131-135: Make NO_MATCH_LOCATION immutable

Prevent accidental mutation of the sentinel at runtime.

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

146-158: Clarify params optionality for named locations

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

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

190-205: Message nit: missing space in warning

Small readability fix.

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

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

Mirror the DEV warning used in the named branch.

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

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

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

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


102-111: Guard against cycles in parent chain

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

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

125-130: Duplicate record names silently overwrite earlier entries

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

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

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

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

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

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

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

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

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

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

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

Applied to files:

  • packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
🧬 Code graph analysis (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (3)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (5)
  • MatcherParamsFormatted (309-309)
  • MatcherPattern (20-43)
  • MatcherQueryParams (325-325)
  • MatcherQueryParamsValue (319-323)
  • EmptyParams (314-314)
packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts (1)
  • ParamParser (9-22)
packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts (2)
  • ParamParser (34-34)
  • PARAM_PARSER_DEFAULTS (13-21)
packages/router/src/experimental/route-resolver/resolver-fixed.ts (6)
packages/router/src/experimental/route-resolver/resolver-abstract.ts (8)
  • RecordName (10-10)
  • EXPERIMENTAL_Resolver_Base (19-91)
  • ResolverLocationResolved (108-125)
  • ResolverLocationAsPathAbsolute (196-199)
  • ResolverLocationAsPathRelative (170-183)
  • ResolverLocationAsNamed (147-158)
  • ResolverLocationAsRelative (212-225)
  • NO_MATCH_LOCATION (131-135)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (4)
  • MatcherPatternPath (49-53)
  • MatcherPatternHash (300-302)
  • MatcherQueryParams (325-325)
  • MatcherParamsFormatted (309-309)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts (1)
  • MatcherPatternQuery (16-18)
packages/router/src/location.ts (4)
  • LocationNormalized (13-18)
  • NEW_stringifyURL (120-128)
  • parseURL (44-97)
  • resolveRelativePath (241-287)
packages/router/src/warning.ts (1)
  • warn (2-9)
packages/router/src/query.ts (1)
  • normalizeQuery (131-148)
packages/router/src/experimental/route-resolver/resolver-abstract.ts (2)
packages/router/src/location.ts (1)
  • LocationNormalized (13-18)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts (1)
  • MatcherParamsFormatted (309-309)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
package.json (2)

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


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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

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

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

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

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

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

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

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

Apply:

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

Quick checks:

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

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

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

Apply:

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

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

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

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

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


36-36: Use frozen lockfile for deterministic installs

Fail fast on lockfile drift in CI.

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

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

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

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

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

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

📒 Files selected for processing (2)
  • .github/workflows/test.yml (1 hunks)
  • package.json (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
.github/workflows/test.yml (1)

22-22: actions/checkout v5 bump looks good

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

package.json (2)

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


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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (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 third next 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 legacy next 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 excludes undefined values when record.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 consistency

For object-relative navigations, the fullPath is currently built using the raw to.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 navigations

The hash resolution logic has two issues:

  1. When to.hash is explicitly provided, it should take precedence over record.hash?.build(params)
  2. For relative-by-name navigations (when to.name is undefined), the current location's hash should be preserved as per the docstring

The 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 handling

The 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 a NavigationGuard 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 guard

Returning void is implicit; simplify the flow and avoid an unnecessary else.

-    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 test

Same 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 guard

Equivalent 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 guard

Keep 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 explicit return in async no‑op guard

Implicit 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 returns

Keep 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 well

Good 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 guard

Same 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 returning to.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

📥 Commits

Reviewing files that changed from the base of the PR and between 7d6164a and 2938bff.

⛔ 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 correct

The 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 duplication

The 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 implementation

The 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 returning void 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 good

Switch 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 3rd next in expectations — NavigationGuard still supports it

beforeEach is typed as NavigationGuardWithThis (the guard type includes the optional third next), so spies declared as (to, from) will still receive a next 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 3rd next arg

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

@posva posva merged commit 1885046 into main Sep 18, 2025
8 of 9 checks passed
@posva posva deleted the feat/new-matcher branch September 18, 2025 15:30
@github-project-automation github-project-automation bot moved this from 🧑‍💻 In progress to ✅ Done in Vue Router Roadmap Sep 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

1 participant