Skip to content

feat(vue): add @modular-vue/runtime zones and route data (PR-23) - #66

Open
kibertoad wants to merge 2 commits into
mainfrom
feat/pr-23-vue-runtime-zones-route-data
Open

feat(vue): add @modular-vue/runtime zones and route data (PR-23)#66
kibertoad wants to merge 2 commits into
mainfrom
feat/pr-23-vue-runtime-zones-route-data

Conversation

@kibertoad

@kibertoad kibertoad commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Part 3 of the @modular-vue/runtime port: the zone and route-data composables. Next row in the Vue initiative after PR-22 (#65).

What

Adds three composables to @modular-vue/runtime, each the Vue analog of the same-named file in react-router-runtime:

  • zones.tsuseZones<TZones>(). React source: react-router-runtime/src/zones.ts.
  • active-zones.tsuseActiveZones<TZones>(activeModuleId?). React source: active-zones.ts.
  • route-data.tsuseRouteData<TRouteData>(). React source: route-data.ts.

All three walk useRoute().matched, read zones/static data off each record's meta (vue-router's analog of React Router's handle, per the PR-20 route-meta.ts convention), and merge deepest-wins through core's mergeRouteStaticData with a createRouteDataOverrideWarner dev warning. useActiveZones layers the active module's descriptor zones over the route zones (module-wins). The runtime index now exports all three.

Core change

Two additive changes in @modular-frontend/core, both anticipated by the warner's own "new runtimes should add their package name here" note:

  • RouteDataRuntimeLabel gains "@modular-vue/runtime" and RouteDataFieldLabel gains "meta", so the Vue composables pass the same compile-time-checked labels the React/TanStack runtimes do.
  • readMatchId now falls back to a vue-router record's name, then path, after id/routeId (vue-router matched records carry no id). Covered by a new frontend-core test.

Deviations from the React source (all forced by the framework)

  • The React hooks return a plain object recomputed each render; the Vue composables return a ComputedRef driven by the reactive useRoute(), so the merged map recomputes on navigation. useActiveZones takes activeModuleId as a MaybeRefOrGetter (read via toValue) so a tab switcher's changing id re-drives the merge.
  • Tests mock vue-router's useRoute (mirroring the React suites mocking useMatches) and assert on .value. slots.test.ts pulls the pure helpers from @modular-frontend/core and createSlotsSignal from @modular-vue/vue, matching the React source case-for-case.

Tests

46 new tests across zones.test.ts (7), active-zones.test.ts (7), route-data.test.ts (9), slots.test.ts (23); package total 108. Two added reactivity tests cover route-change recompute (useZones) and reactive activeModuleId re-merge (useActiveZones). Full workspace typecheck (120 tasks) and vite build (JS + dts) pass; React and TanStack runtime suites stay green.

Tracker (docs/vue-support-tracker.md) updated: PR-23 marked done with the full write-up.

Summary by CodeRabbit

  • New Features

    • Added Vue Router composables for reading route zones and route data from matched routes.
    • Added support for combining route-derived zones with active module zones, with module values taking precedence.
    • Improved override warnings so conflicting route data is clearer in Vue Router setups.
  • Bug Fixes

    • Better handling for routes without stable IDs, including fallback matching and clearer duplicate-match warnings.
    • Preserved parent values when deeper matches don’t provide an override.

Add zones.ts, active-zones.ts, and route-data.ts to
@modular-vue/runtime, the Vue analogs of the same-named files in
react-router-runtime. All three read useRoute().matched, take
zones/static data off each record's meta (vue-router's analog of
React Router's handle, per the PR-20 route-meta convention), and
funnel through core's mergeRouteStaticData with a
createRouteDataOverrideWarner dev warning. useActiveZones layers the
active module's descriptor zones over route zones, module-wins. The
runtime index now exports useZones, useActiveZones, useRouteData.

Widen the warner contract in @modular-frontend/core, as its own
"new runtimes should add their package name here" note anticipates:
RouteDataRuntimeLabel gains "@modular-vue/runtime", RouteDataFieldLabel
gains "meta", and readMatchId falls back to a vue-router record's name
then path (vue-router matched records carry no id).

The composables return a ComputedRef driven by the reactive
useRoute(), so the merged map recomputes on navigation;
useActiveZones takes activeModuleId as a MaybeRefOrGetter so a tab
switcher's changing id re-drives the merge. Tests mock useRoute
(mirroring the React suites mocking useMatches) and assert on .value.

46 new tests across zones (7), active-zones (7), route-data (9),
slots (23); package total 108. Full workspace typecheck (120 tasks)
and vite build pass; React/TanStack runtime suites stay green.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extends the shared route-data override-warning and merge utilities in frontend-core to support vue-router semantics (match id/name/path fallbacks, index-based dedup disambiguation), adds new vue-router-runtime composables (useZones, useActiveZones, useRouteData), wires their exports, adds extensive test coverage, and updates tracker documentation.

Changes

Vue-router runtime support

Layer / File(s) Summary
Route-data warner types and dedup logic
packages/frontend-core/src/route-data-warn.ts, packages/frontend-core/src/route-data-warn.test.ts
RouteDataRuntimeLabel and RouteDataFieldLabel unions extended for @modular-vue/runtime and "meta"; dedup key and message labeling now incorporate match index; readMatchId adds name/path fallbacks; tests cover new warning content and dedup behavior.
Override index reporting in mergeRouteStaticData
packages/frontend-core/src/route-data.ts, packages/frontend-core/src/route-data.test.ts
RouteStaticDataOverrideInfo gains previousIndex/nextIndex; merge loop tracks match position and reports it via onOverride; tests updated and extended for index reporting including skipped non-contributing matches.
useZones composable
packages/vue-router-runtime/src/zones.ts, zones.test.ts
New composable merges meta across useRoute().matched hierarchy via mergeRouteStaticData, with deepest-match-wins semantics and dev-time override warnings; fully tested.
useActiveZones composable
packages/vue-router-runtime/src/active-zones.ts, active-zones.test.ts
New composable merges route-derived zones with active module zones, module zones taking precedence, reactive to activeModuleId; fully tested.
useRouteData composable and exports
packages/vue-router-runtime/src/route-data.ts, route-data.test.ts, index.ts
New composable merges route meta values across matches with override warnings; useZones, useActiveZones, useRouteData re-exported from package index; tested including type-level and interoperability behavior.
Slots utility tests
packages/vue-router-runtime/src/slots.test.ts
New test suite covering existing buildSlotsManifest, collectDynamicSlotFactories, evaluateDynamicSlots, and createSlotsSignal behavior.
Vue support tracker docs
docs/vue-support-tracker.md
Status text, PR-23 documentation section, and status board updated to reflect landed work.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • kibertoad/modular-react#27: Extends the same createRouteDataOverrideWarner/readMatchId dedup and onOverride reporting infrastructure to add vue-router match identifier and match-position disambiguation support.

Suggested reviewers: diogomiguel, casamitjana

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the Vue runtime addition of zones and route data and is specific enough.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pr-23-vue-runtime-zones-route-data

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

❤️ Share

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

…h position

Two fixes to the shared override warner surfaced while reviewing the
@modular-vue/runtime port (PR-23):

- readMatchId falls back to a vue-router record's `path`, which is not
  unique: nameless index routes report the same `path` as their parent.
  Two override sites that share a path pair collapsed to one dedup key, so
  the second real clobber was silently suppressed, and the message read as
  a route overriding itself. mergeRouteStaticData now threads each match's
  position in the matched hierarchy into the override info, and the warner
  folds it into the dedup key and (when the ids would otherwise collide)
  the message. React Router / TanStack output is unchanged since their ids
  are already unique.

- Update stale JSDoc in route-data-warn.ts that still described two
  runtimes and omitted the `meta` field label after @modular-vue/runtime
  and "meta" were added.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/frontend-core/src/route-data-warn.ts (1)

85-90: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

ambiguous only guards previousIndex, not nextIndex.

If a future caller ever supplies previousIndex without nextIndex (or vice versa), ambiguous would still evaluate true and the label with the missing index would render as "... (match undefined)". Currently unreachable since mergeRouteStaticData always sets both together, but worth hardening for future callers of this exported helper.

🛡️ Suggested defensive check
-    const ambiguous = prevId === nextId && info.previousIndex !== undefined;
+    const ambiguous =
+      prevId === nextId && info.previousIndex !== undefined && info.nextIndex !== undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend-core/src/route-data-warn.ts` around lines 85 - 90, The
route warning label logic in route-data-warn.ts should defensively require both
indices before treating a route as ambiguous. Update the ambiguous check in the
helper that builds prevLabel and nextLabel so it only becomes true when prevId
equals nextId and both info.previousIndex and info.nextIndex are defined,
preventing “match undefined” from appearing for partial callers. Keep the
existing behavior for mergeRouteStaticData unchanged, but harden the exported
helper against future inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/vue-support-tracker.md`:
- Line 205: The test-count summary in the Vue support tracker has a
running-total mismatch: the per-file counts in the PR-23 summary add up
correctly, but the stated package total does not match the prior total from
PR-22. Update the summary text in docs/vue-support-tracker.md so the cumulative
package total is arithmetically consistent with the earlier “package total 58”
and the 46 new tests, and keep the existing per-file counts and feature notes
unchanged.

In `@packages/frontend-core/src/route-data-warn.ts`:
- Around line 33-38: Update the JSDoc in route-data-warn so it matches the
actual dedup logic in the route matcher warning helper: the hierarchy position
is always included in the dedup key via the prevPart/nextPart handling, not only
when ids collide. Keep the collision-specific wording only for the message label
behavior tied to the ambiguous case, and adjust the text near the route match/id
description so future readers understand the difference between the dedup key
and the displayed message.

In `@packages/vue-router-runtime/src/route-data.ts`:
- Around line 5-84: The JSDoc for useRouteData is currently attached to the
internal onOverride constant instead of the exported composable, so move that
doc block to immediately precede export function useRouteData and keep
onOverride undocumented; this will ensure tooling associates the documentation
with the public API symbol rather than the helper constant.

In `@packages/vue-router-runtime/src/zones.ts`:
- Around line 6-61: The JSDoc intended for useZones is attached to the internal
onOverride constant because it immediately precedes that declaration. Move the
createRouteDataOverrideWarner("`@modular-vue/runtime`", "useZones", "meta") const
above the doc block, or place the doc directly above export function useZones so
the language service and docs generators associate it with useZones.

---

Nitpick comments:
In `@packages/frontend-core/src/route-data-warn.ts`:
- Around line 85-90: The route warning label logic in route-data-warn.ts should
defensively require both indices before treating a route as ambiguous. Update
the ambiguous check in the helper that builds prevLabel and nextLabel so it only
becomes true when prevId equals nextId and both info.previousIndex and
info.nextIndex are defined, preventing “match undefined” from appearing for
partial callers. Keep the existing behavior for mergeRouteStaticData unchanged,
but harden the exported helper against future inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 176a34cc-c782-4ebc-a725-95b47a7189ec

📥 Commits

Reviewing files that changed from the base of the PR and between de7bb6a and c0b6d0a.

📒 Files selected for processing (13)
  • docs/vue-support-tracker.md
  • packages/frontend-core/src/route-data-warn.test.ts
  • packages/frontend-core/src/route-data-warn.ts
  • packages/frontend-core/src/route-data.test.ts
  • packages/frontend-core/src/route-data.ts
  • packages/vue-router-runtime/src/active-zones.test.ts
  • packages/vue-router-runtime/src/active-zones.ts
  • packages/vue-router-runtime/src/index.ts
  • packages/vue-router-runtime/src/route-data.test.ts
  • packages/vue-router-runtime/src/route-data.ts
  • packages/vue-router-runtime/src/slots.test.ts
  • packages/vue-router-runtime/src/zones.test.ts
  • packages/vue-router-runtime/src/zones.ts

- The React hooks return a plain object recomputed each render; the Vue composables return a `ComputedRef` driven by the reactive `useRoute()`, so the merged map recomputes on navigation. Consumers read `.value` (or let the template auto-unwrap). `useActiveZones` additionally accepts `activeModuleId` as a `MaybeRefOrGetter` and reads it via `toValue`, so a tab switcher's changing id re-drives the merge — the reactive analog of React reading the argument each render.
- The zones/active-zones/route-data tests mock `vue-router`'s `useRoute` (mirroring the React suites mocking `useMatches`) rather than booting a real router, and assert on `.value`. The `slots.test.ts` port pulls `buildSlotsManifest` / `collectDynamicSlotFactories` / `evaluateDynamicSlots` from `@modular-frontend/core` and `createSlotsSignal` from `@modular-vue/vue` (the runtime re-export barrel), matching the React source case-for-case.

Error-message prefixes are `[@modular-vue/runtime]`. Acceptance: met. Deepest-wins merge, undefined-skips-inherit, and module-over-route precedence match the React suites; two added reactivity tests cover route-change recompute (`useZones`) and reactive `activeModuleId` re-merge (`useActiveZones`). 46 new tests across `zones.test.ts` (7), `active-zones.test.ts` (7), `route-data.test.ts` (9), `slots.test.ts` (23); package total 108. Full workspace typecheck (120 tasks) and `vite build` (JS + dts) pass; externals (`vue`, `vue-router`, `@modular-frontend/core`, `@modular-vue/vue`, `@modular-vue/core`) stay unbundled.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test-count arithmetic doesn't add up.

PR-22 states a "package total 58" and PR-23 says "46 new tests ... package total 108". 58 + 46 = 104, not 108 — a 4-test discrepancy. The individual per-file counts (7+7+9+23=46) are internally consistent, so the "108" running total looks like the error.

🔧 Proposed fix
-Error-message prefixes are `[`@modular-vue/runtime`]`. Acceptance: met. Deepest-wins merge, undefined-skips-inherit, and module-over-route precedence match the React suites; two added reactivity tests cover route-change recompute (`useZones`) and reactive `activeModuleId` re-merge (`useActiveZones`). 46 new tests across `zones.test.ts` (7), `active-zones.test.ts` (7), `route-data.test.ts` (9), `slots.test.ts` (23); package total 108. Full workspace typecheck (120 tasks) and `vite build` (JS + dts) pass; externals (`vue`, `vue-router`, `@modular-frontend/core`, `@modular-vue/vue`, `@modular-vue/core`) stay unbundled.
+Error-message prefixes are `[`@modular-vue/runtime`]`. Acceptance: met. Deepest-wins merge, undefined-skips-inherit, and module-over-route precedence match the React suites; two added reactivity tests cover route-change recompute (`useZones`) and reactive `activeModuleId` re-merge (`useActiveZones`). 46 new tests across `zones.test.ts` (7), `active-zones.test.ts` (7), `route-data.test.ts` (9), `slots.test.ts` (23); package total 104. Full workspace typecheck (120 tasks) and `vite build` (JS + dts) pass; externals (`vue`, `vue-router`, `@modular-frontend/core`, `@modular-vue/vue`, `@modular-vue/core`) stay unbundled.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Error-message prefixes are `[@modular-vue/runtime]`. Acceptance: met. Deepest-wins merge, undefined-skips-inherit, and module-over-route precedence match the React suites; two added reactivity tests cover route-change recompute (`useZones`) and reactive `activeModuleId` re-merge (`useActiveZones`). 46 new tests across `zones.test.ts` (7), `active-zones.test.ts` (7), `route-data.test.ts` (9), `slots.test.ts` (23); package total 108. Full workspace typecheck (120 tasks) and `vite build` (JS + dts) pass; externals (`vue`, `vue-router`, `@modular-frontend/core`, `@modular-vue/vue`, `@modular-vue/core`) stay unbundled.
Error-message prefixes are `[`@modular-vue/runtime`]`. Acceptance: met. Deepest-wins merge, undefined-skips-inherit, and module-over-route precedence match the React suites; two added reactivity tests cover route-change recompute (`useZones`) and reactive `activeModuleId` re-merge (`useActiveZones`). 46 new tests across `zones.test.ts` (7), `active-zones.test.ts` (7), `route-data.test.ts` (9), `slots.test.ts` (23); package total 104. Full workspace typecheck (120 tasks) and `vite build` (JS + dts) pass; externals (`vue`, `vue-router`, `@modular-frontend/core`, `@modular-vue/vue`, `@modular-vue/core`) stay unbundled.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/vue-support-tracker.md` at line 205, The test-count summary in the Vue
support tracker has a running-total mismatch: the per-file counts in the PR-23
summary add up correctly, but the stated package total does not match the prior
total from PR-22. Update the summary text in docs/vue-support-tracker.md so the
cumulative package total is arithmetically consistent with the earlier “package
total 58” and the 46 new tests, and keep the existing per-file counts and
feature notes unchanged.

Comment on lines +33 to +38
* are read off the match object at warn time: React Router and TanStack
* Router expose a stable `id`/`routeId` on `useMatches()` entries, and
* vue-router's matched records fall back to their `name` or `path`. Because
* a vue-router `path` is not unique (nameless index routes share their
* parent's `path`), the match's position in the hierarchy is folded into
* both the dedup key and the message when the ids would otherwise collide.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

JSDoc overstates when position-folding applies to the dedup key.

The docstring says the match position is folded into both the dedup key and the message when the ids would otherwise collide, but in the implementation the dedup key always folds in the index whenever it's defined (prevPart/nextPart), regardless of whether prevId === nextId. Only the message label folding is conditioned on collision (ambiguous). This could mislead a future reader into assuming the dedup key stays untouched for non-colliding ids.

✏️ Suggested wording fix
- * a vue-router `path` is not unique (nameless index routes share their
- * parent's `path`), the match's position in the hierarchy is folded into
- * both the dedup key and the message when the ids would otherwise collide.
+ * a vue-router `path` is not unique (nameless index routes share their
+ * parent's `path`), the match's position is always folded into the dedup
+ * key (when known), and is additionally surfaced in the message only when
+ * the ids would otherwise collide.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* are read off the match object at warn time: React Router and TanStack
* Router expose a stable `id`/`routeId` on `useMatches()` entries, and
* vue-router's matched records fall back to their `name` or `path`. Because
* a vue-router `path` is not unique (nameless index routes share their
* parent's `path`), the match's position in the hierarchy is folded into
* both the dedup key and the message when the ids would otherwise collide.
* are read off the match object at warn time: React Router and TanStack
* Router expose a stable `id`/`routeId` on `useMatches()` entries, and
* vue-router's matched records fall back to their `name` or `path`. Because
* a vue-router `path` is not unique (nameless index routes share their
* parent's `path`), the match's position is always folded into the dedup
* key (when known), and is additionally surfaced in the message only when
* the ids would otherwise collide.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend-core/src/route-data-warn.ts` around lines 33 - 38, Update
the JSDoc in route-data-warn so it matches the actual dedup logic in the route
matcher warning helper: the hierarchy position is always included in the dedup
key via the prevPart/nextPart handling, not only when ids collide. Keep the
collision-specific wording only for the message label behavior tied to the
ambiguous case, and adjust the text near the route match/id description so
future readers understand the difference between the dedup key and the displayed
message.

Comment on lines +5 to +84
/**
* Read merged `meta` values from the currently matched route hierarchy —
* the "non-component zone" escape hatch.
*
* `useZones` is the component-typed channel: each value must be a
* `UiComponent | undefined` so the shell can render it in a layout
* region. That constraint is a useful rail 95% of the time, but it gets in
* the way for non-component metadata the module wants to attach to a route —
* a header variant enum, a page title string, an analytics event name, a
* per-route feature flag. `useRouteData` is the relaxed-typing counterpart:
* same deepest-wins merge over `route.meta`, no constraint on values.
*
* Two composables, two channels: keep components in `meta` fields consumed by
* `useZones`, keep metadata in fields consumed by `useRouteData`. They can
* co-exist in the same `meta` object because they read the same match
* values; each composable only surfaces the keys you've declared in its type.
*
* @example
* ```ts
* // Declare both shapes explicitly — zones for renderable components,
* // route data for everything else.
* interface AppZones {
* HeaderActions?: UiComponent
* DetailPanel?: UiComponent
* }
* interface AppRouteData {
* headerVariant?: "portal" | "project" | "setup"
* pageTitle?: string
* }
*
* // A route can contribute to both:
* meta: {
* HeaderActions: ProjectActions, // → useZones<AppZones>()
* headerVariant: "project" as const, // → useRouteData<AppRouteData>()
* }
*
* // Layout reads each channel with its own typing:
* const zones = useZones<AppZones>()
* const routeData = useRouteData<AppRouteData>()
* // routeData.value.headerVariant, routeData.value.pageTitle
* ```
*
* ## Merge semantics
*
* Walks matched records root-to-leaf, deepest match wins per key.
* `undefined` values at a deeper level don't override an ancestor —
* **omit the key (or set it to `undefined`) to inherit**. Set the key
* to `null` to **explicitly clear** an ancestor's value; the consuming
* shell decides how to render `null` (typically: as if the field was
* never set, but distinct from "still loading").
*
* In dev (NODE_ENV !== "production"), this composable logs a deduped
* `console.warn` whenever a deeper match overrides a key already set by
* an ancestor. The warning is intended to catch accidental clobbers of
* shell-owned route data (e.g. `headerVariant`); ignore it when the
* override is intentional.
*
* ## Returned object contains all meta keys, not just declared ones
*
* The merged value is the raw merged `meta` — TypeScript narrows what you
* can *access* via `TRouteData`, but every key present across matches is
* still there at runtime. If a route declared a component zone (e.g.
* `HeaderActions`) on the same `meta` object, it appears here too.
*
* This is intentional: the two composables (`useZones` / `useRouteData`)
* don't have to coordinate on key sets, so a migration can split meta fields
* between them incrementally. The consequence is that code that iterates the
* merged value (`Object.keys(useRouteData().value)`, `JSON.stringify`, etc.)
* will see component entries mixed with data entries — read by declared key,
* not by iteration.
*
* ## Return value
*
* Returns a `ComputedRef` driven by `useRoute()`, so the merged data
* recomputes when navigation changes the matched hierarchy. Read
* `routeData.value` in script, or let the template auto-unwrap it.
*/
const onOverride = createRouteDataOverrideWarner("@modular-vue/runtime", "useRouteData", "meta");

export function useRouteData<TRouteData extends object>(): ComputedRef<Partial<TRouteData>> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Same JSDoc-misattachment issue as zones.ts.

The doc block (Lines 5-81) is directly above const onOverride = ... (Line 82) instead of export function useRouteData (Line 84), so tooling attaches it to the internal const rather than the exported composable.

🔧 Proposed fix
-/**
- * Read merged `meta` values from the currently matched route hierarchy —
- * ...
- */
-const onOverride = createRouteDataOverrideWarner("`@modular-vue/runtime`", "useRouteData", "meta");
-
-export function useRouteData<TRouteData extends object>(): ComputedRef<Partial<TRouteData>> {
+const onOverride = createRouteDataOverrideWarner("`@modular-vue/runtime`", "useRouteData", "meta");
+
+/**
+ * Read merged `meta` values from the currently matched route hierarchy —
+ * ...
+ */
+export function useRouteData<TRouteData extends object>(): ComputedRef<Partial<TRouteData>> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Read merged `meta` values from the currently matched route hierarchy
* the "non-component zone" escape hatch.
*
* `useZones` is the component-typed channel: each value must be a
* `UiComponent | undefined` so the shell can render it in a layout
* region. That constraint is a useful rail 95% of the time, but it gets in
* the way for non-component metadata the module wants to attach to a route
* a header variant enum, a page title string, an analytics event name, a
* per-route feature flag. `useRouteData` is the relaxed-typing counterpart:
* same deepest-wins merge over `route.meta`, no constraint on values.
*
* Two composables, two channels: keep components in `meta` fields consumed by
* `useZones`, keep metadata in fields consumed by `useRouteData`. They can
* co-exist in the same `meta` object because they read the same match
* values; each composable only surfaces the keys you've declared in its type.
*
* @example
* ```ts
* // Declare both shapes explicitly — zones for renderable components,
* // route data for everything else.
* interface AppZones {
* HeaderActions?: UiComponent
* DetailPanel?: UiComponent
* }
* interface AppRouteData {
* headerVariant?: "portal" | "project" | "setup"
* pageTitle?: string
* }
*
* // A route can contribute to both:
* meta: {
* HeaderActions: ProjectActions, // → useZones<AppZones>()
* headerVariant: "project" as const, // → useRouteData<AppRouteData>()
* }
*
* // Layout reads each channel with its own typing:
* const zones = useZones<AppZones>()
* const routeData = useRouteData<AppRouteData>()
* // routeData.value.headerVariant, routeData.value.pageTitle
* ```
*
* ## Merge semantics
*
* Walks matched records root-to-leaf, deepest match wins per key.
* `undefined` values at a deeper level don't override an ancestor —
* **omit the key (or set it to `undefined`) to inherit**. Set the key
* to `null` to **explicitly clear** an ancestor's value; the consuming
* shell decides how to render `null` (typically: as if the field was
* never set, but distinct from "still loading").
*
* In dev (NODE_ENV !== "production"), this composable logs a deduped
* `console.warn` whenever a deeper match overrides a key already set by
* an ancestor. The warning is intended to catch accidental clobbers of
* shell-owned route data (e.g. `headerVariant`); ignore it when the
* override is intentional.
*
* ## Returned object contains all meta keys, not just declared ones
*
* The merged value is the raw merged `meta` TypeScript narrows what you
* can *access* via `TRouteData`, but every key present across matches is
* still there at runtime. If a route declared a component zone (e.g.
* `HeaderActions`) on the same `meta` object, it appears here too.
*
* This is intentional: the two composables (`useZones` / `useRouteData`)
* don't have to coordinate on key sets, so a migration can split meta fields
* between them incrementally. The consequence is that code that iterates the
* merged value (`Object.keys(useRouteData().value)`, `JSON.stringify`, etc.)
* will see component entries mixed with data entries read by declared key,
* not by iteration.
*
* ## Return value
*
* Returns a `ComputedRef` driven by `useRoute()`, so the merged data
* recomputes when navigation changes the matched hierarchy. Read
* `routeData.value` in script, or let the template auto-unwrap it.
*/
const onOverride = createRouteDataOverrideWarner("@modular-vue/runtime", "useRouteData", "meta");
export function useRouteData<TRouteData extends object>(): ComputedRef<Partial<TRouteData>> {
const onOverride = createRouteDataOverrideWarner("`@modular-vue/runtime`", "useRouteData", "meta");
/**
* Read merged `meta` values from the currently matched route hierarchy
* the "non-component zone" escape hatch.
*
* `useZones` is the component-typed channel: each value must be a
* `UiComponent | undefined` so the shell can render it in a layout
* region. That constraint is a useful rail 95% of the time, but it gets in
* the way for non-component metadata the module wants to attach to a route
* a header variant enum, a page title string, an analytics event name, a
* per-route feature flag. `useRouteData` is the relaxed-typing counterpart:
* same deepest-wins merge over `route.meta`, no constraint on values.
*
* Two composables, two channels: keep components in `meta` fields consumed by
* `useZones`, keep metadata in fields consumed by `useRouteData`. They can
* co-exist in the same `meta` object because they read the same match
* values; each composable only surfaces the keys you've declared in its type.
*
* `@example`
*
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router-runtime/src/route-data.ts` around lines 5 - 84, The JSDoc
for useRouteData is currently attached to the internal onOverride constant
instead of the exported composable, so move that doc block to immediately
precede export function useRouteData and keep onOverride undocumented; this will
ensure tooling associates the documentation with the public API symbol rather
than the helper constant.

Comment on lines +6 to +61
/**
* Read zone components contributed by the currently matched route hierarchy.
*
* Zones ride on vue-router's `meta` (the analog of React Router's `handle`)
* on individual routes. This composable walks all matched records from root
* to leaf via `useRoute().matched` and returns a merged map where the deepest
* match wins for each zone key.
*
* @example
* // In the shell layout's <script setup>:
* const zones = useZones<AppZones>()
*
* // In the template:
* // <main><router-view /></main>
* // <aside><component :is="zones.detailPanel" v-if="zones.detailPanel" /></aside>
*
* @example
* // In a module's createRoutes():
* {
* path: ':userId',
* component: UserDetailPage,
* meta: {
* detailPanel: UserDetailSidebar,
* } satisfies ModuleRouteMeta<AppZones>,
* }
*
* ## Ownership and overrides
*
* Zones merge with deepest-wins semantics: a descendant route that declares
* the same zone key as an ancestor silently replaces it. That is the
* intended escape hatch for "this section overrides the default panel" —
* but it is also the failure mode when a descendant route accidentally
* declares a zone key the shell layout owns (e.g. `HeaderTitle`,
* `HeaderActions`).
*
* In dev (NODE_ENV !== "production"), this composable logs a deduped
* `console.warn` whenever a deeper match overrides a zone already set by
* an ancestor. Use that warning to catch unintended clobbers; if the
* override is intentional you can ignore it.
*
* To **inherit** an ancestor's zone, omit the key (or set to `undefined`).
* To **explicitly clear** an ancestor's zone at a deeper route, set the
* key to `null`. Don't redeclare a shell-owned zone "just to be safe" —
* that's exactly the pattern the override warning is designed to flag.
*
* ## Return value
*
* Returns a `ComputedRef` driven by `useRoute()`, so the merged map
* recomputes when navigation changes the matched hierarchy. The React
* binding returns a plain object per render; Vue's reactive route makes a
* `computed` the faithful analog. Read `zones.value` in script, or let the
* template auto-unwrap it.
*/
const onOverride = createRouteDataOverrideWarner("@modular-vue/runtime", "useZones", "meta");

export function useZones<TZones extends ZoneMapOf<TZones>>(): ComputedRef<Partial<TZones>> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

JSDoc for useZones is misattached to the internal onOverride const.

The large doc block (Lines 6-58) sits directly above const onOverride = ... (Line 59), not above export function useZones (Line 61). Editors/TS language service and doc generators (TypeDoc) associate a comment with the immediately following declaration, so this doc currently documents the unexported onOverride constant — hovering useZones in an IDE will show nothing, defeating the purpose of this extensive doc.

🔧 Proposed fix: move the const above the JSDoc
-/**
- * Read zone components contributed by the currently matched route hierarchy.
- * ...
- */
-const onOverride = createRouteDataOverrideWarner("`@modular-vue/runtime`", "useZones", "meta");
-
-export function useZones<TZones extends ZoneMapOf<TZones>>(): ComputedRef<Partial<TZones>> {
+const onOverride = createRouteDataOverrideWarner("`@modular-vue/runtime`", "useZones", "meta");
+
+/**
+ * Read zone components contributed by the currently matched route hierarchy.
+ * ...
+ */
+export function useZones<TZones extends ZoneMapOf<TZones>>(): ComputedRef<Partial<TZones>> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Read zone components contributed by the currently matched route hierarchy.
*
* Zones ride on vue-router's `meta` (the analog of React Router's `handle`)
* on individual routes. This composable walks all matched records from root
* to leaf via `useRoute().matched` and returns a merged map where the deepest
* match wins for each zone key.
*
* @example
* // In the shell layout's <script setup>:
* const zones = useZones<AppZones>()
*
* // In the template:
* // <main><router-view /></main>
* // <aside><component :is="zones.detailPanel" v-if="zones.detailPanel" /></aside>
*
* @example
* // In a module's createRoutes():
* {
* path: ':userId',
* component: UserDetailPage,
* meta: {
* detailPanel: UserDetailSidebar,
* } satisfies ModuleRouteMeta<AppZones>,
* }
*
* ## Ownership and overrides
*
* Zones merge with deepest-wins semantics: a descendant route that declares
* the same zone key as an ancestor silently replaces it. That is the
* intended escape hatch for "this section overrides the default panel"
* but it is also the failure mode when a descendant route accidentally
* declares a zone key the shell layout owns (e.g. `HeaderTitle`,
* `HeaderActions`).
*
* In dev (NODE_ENV !== "production"), this composable logs a deduped
* `console.warn` whenever a deeper match overrides a zone already set by
* an ancestor. Use that warning to catch unintended clobbers; if the
* override is intentional you can ignore it.
*
* To **inherit** an ancestor's zone, omit the key (or set to `undefined`).
* To **explicitly clear** an ancestor's zone at a deeper route, set the
* key to `null`. Don't redeclare a shell-owned zone "just to be safe" —
* that's exactly the pattern the override warning is designed to flag.
*
* ## Return value
*
* Returns a `ComputedRef` driven by `useRoute()`, so the merged map
* recomputes when navigation changes the matched hierarchy. The React
* binding returns a plain object per render; Vue's reactive route makes a
* `computed` the faithful analog. Read `zones.value` in script, or let the
* template auto-unwrap it.
*/
const onOverride = createRouteDataOverrideWarner("@modular-vue/runtime", "useZones", "meta");
export function useZones<TZones extends ZoneMapOf<TZones>>(): ComputedRef<Partial<TZones>> {
const onOverride = createRouteDataOverrideWarner("`@modular-vue/runtime`", "useZones", "meta");
/**
* Read zone components contributed by the currently matched route hierarchy.
*
* Zones ride on vue-router's `meta` (the analog of React Router's `handle`)
* on individual routes. This composable walks all matched records from root
* to leaf via `useRoute().matched` and returns a merged map where the deepest
* match wins for each zone key.
*
* `@example`
* // In the shell layout's <script setup>:
* const zones = useZones<AppZones>()
*
* // In the template:
* // <main><router-view /></main>
* // <aside><component :is="zones.detailPanel" v-if="zones.detailPanel" /></aside>
*
* `@example`
* // In a module's createRoutes():
* {
* path: ':userId',
* component: UserDetailPage,
* meta: {
* detailPanel: UserDetailSidebar,
* } satisfies ModuleRouteMeta<AppZones>,
* }
*
* ## Ownership and overrides
*
* Zones merge with deepest-wins semantics: a descendant route that declares
* the same zone key as an ancestor silently replaces it. That is the
* intended escape hatch for "this section overrides the default panel"
* but it is also the failure mode when a descendant route accidentally
* declares a zone key the shell layout owns (e.g. `HeaderTitle`,
* `HeaderActions`).
*
* In dev (NODE_ENV !== "production"), this composable logs a deduped
* `console.warn` whenever a deeper match overrides a zone already set by
* an ancestor. Use that warning to catch unintended clobbers; if the
* override is intentional you can ignore it.
*
* To **inherit** an ancestor's zone, omit the key (or set to `undefined`).
* To **explicitly clear** an ancestor's zone at a deeper route, set the
* key to `null`. Don't redeclare a shell-owned zone "just to be safe" —
* that's exactly the pattern the override warning is designed to flag.
*
* ## Return value
*
* Returns a `ComputedRef` driven by `useRoute()`, so the merged map
* recomputes when navigation changes the matched hierarchy. The React
* binding returns a plain object per render; Vue's reactive route makes a
* `computed` the faithful analog. Read `zones.value` in script, or let the
* template auto-unwrap it.
*/
export function useZones<TZones extends ZoneMapOf<TZones>>(): ComputedRef<Partial<TZones>> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router-runtime/src/zones.ts` around lines 6 - 61, The JSDoc
intended for useZones is attached to the internal onOverride constant because it
immediately precedes that declaration. Move the
createRouteDataOverrideWarner("`@modular-vue/runtime`", "useZones", "meta") const
above the doc block, or place the doc directly above export function useZones so
the language service and docs generators associate it with useZones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant