[PR #1039] [MERGED] fix(routing): mirror inherited parallel slots to descendant sub-pages #1042

Closed
opened 2026-05-06 13:11:42 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/1039
Author: @james-elicx
Created: 5/3/2026
Status: Merged
Merged: 5/3/2026
Merged by: @james-elicx

Base: mainHead: claude/cranky-heyrovsky-5f9148


📝 Commits (3)

  • 04cf20f fix(routing): mirror inherited parallel slots to descendant sub-pages
  • 7c5e732 fix(routing): extract slot-specific params for inherited mirrors
  • 761c07c fix(routing): address bonk review on inherited parallel slot mirror

📊 Changes

15 files changed (+473 additions, -21 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-manifest.ts (+2 -0)
📝 packages/vinext/src/routing/app-route-graph.ts (+173 -10)
📝 packages/vinext/src/server/app-page-element-builder.ts (+69 -10)
📝 packages/vinext/src/server/app-page-route-wiring.tsx (+15 -1)
📝 tests/app-route-graph.test.ts (+126 -0)
tests/e2e/app-router/inherited-parallel-slots.spec.ts (+34 -0)
tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/[...path]/page.tsx (+8 -0)
tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/about/page.tsx (+3 -0)
tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/default.tsx (+3 -0)
tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/distinct/[name]/page.tsx (+8 -0)
tests/fixtures/app-basic/app/inherited-slot/[...path]/page.tsx (+8 -0)
tests/fixtures/app-basic/app/inherited-slot/about/page.tsx (+3 -0)
tests/fixtures/app-basic/app/inherited-slot/distinct/[id]/page.tsx (+4 -0)
tests/fixtures/app-basic/app/inherited-slot/layout.tsx (+14 -0)
tests/fixtures/app-basic/app/inherited-slot/page.tsx (+3 -0)

📄 Description

Summary

Fixes two related bugs in inherited parallel-slot resolution where slots silently rendered default.tsx instead of their mirrored sub-page.

1. Inherited slots never mirrored sub-pages

discoverInheritedParallelSlots was hard-coding pagePath: null for any ancestor slot, so a slot defined at the layout level only ever rendered default.tsx — even when a clear mirror existed. With the fix, each ancestor's segment index is tracked in dirsToCheck and the mirror is looked up through three tiers, each more permissive than the last:

  1. Literal filesystem path — fast path when route and slot share shape.
  2. URL-parts equality — handles route groups appearing on only one side, e.g. (marketing)/about@breadcrumbs/about.
  3. Pattern compatibility (with specificity scoring) — accepts slot patterns whose dynamic markers have different names than the route's, or whose catch-alls subsume the route. Most-specific compatible sub-page wins (literal > single dynamic > optional catch-all > catch-all).

Falls back to default.tsx when no mirror matches.

2. Slot params used the route's pattern, not the slot's

When the slot's mirror declares dynamic markers under different names than the route (e.g. slot @breadcrumbs/distinct/[name] for route distinct/[id]), the runtime was passing the route's matchedParams through unchanged — so the slot page received {id} instead of {name} and rendered with empty values.

Three changes plumb slot-specific params:

  • Each ParallelSlot carries slotPatternParts and slotParamNames (full URL pattern + ordered param names), serialized through the manifest.
  • buildPageElements extracts a per-request slotOverrides[key].params by matching the request URL against slotPatternParts whenever a slot's params aren't a subset of the route's.
  • AppPageSlotOverride.pageModule becomes optional so a params-only override doesn't have to swap the slot's page.

Tests

Unit (tests/app-route-graph.test.ts):

  • Literal segment mirroring
  • Catch-all segment mirroring
  • Multi-level inheritance
  • Route-group asymmetry (route has (marketing)/about, slot has plain about)
  • Fallback to default when no mirror exists

E2E (tests/e2e/app-router/inherited-parallel-slots.spec.ts) with a fixture under app/inherited-slot/:

  • Literal mirror renders for /inherited-slot/about
  • Catch-all mirror renders for /inherited-slot/products/42 (route and slot both use [...path] so params share names)
  • Default fallback renders for /inherited-slot
  • Distinct param names — visits /inherited-slot/distinct/foo (route [id], slot [name]); the page renders id:foo and the slot renders name:foo. Verified to fail before the runtime fix (slot rendered with empty name).

Test plan

  • pnpm check passes
  • pnpm test:unit (3480 tests)
  • Playwright app-router project (344 passed; only pre-existing flakes in intercepting/instrumentation tests)

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/1039 **Author:** [@james-elicx](https://github.com/james-elicx) **Created:** 5/3/2026 **Status:** ✅ Merged **Merged:** 5/3/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `claude/cranky-heyrovsky-5f9148` --- ### 📝 Commits (3) - [`04cf20f`](https://github.com/cloudflare/vinext/commit/04cf20f4906c0fd35d67e8af7fc0dce34f0944da) fix(routing): mirror inherited parallel slots to descendant sub-pages - [`7c5e732`](https://github.com/cloudflare/vinext/commit/7c5e732700a63030836166856a0a2095c92546fd) fix(routing): extract slot-specific params for inherited mirrors - [`761c07c`](https://github.com/cloudflare/vinext/commit/761c07c66c66552d31ed41b9bcfd56c875c4ff67) fix(routing): address bonk review on inherited parallel slot mirror ### 📊 Changes **15 files changed** (+473 additions, -21 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-manifest.ts` (+2 -0) 📝 `packages/vinext/src/routing/app-route-graph.ts` (+173 -10) 📝 `packages/vinext/src/server/app-page-element-builder.ts` (+69 -10) 📝 `packages/vinext/src/server/app-page-route-wiring.tsx` (+15 -1) 📝 `tests/app-route-graph.test.ts` (+126 -0) ➕ `tests/e2e/app-router/inherited-parallel-slots.spec.ts` (+34 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/[...path]/page.tsx` (+8 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/about/page.tsx` (+3 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/default.tsx` (+3 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/@breadcrumbs/distinct/[name]/page.tsx` (+8 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/[...path]/page.tsx` (+8 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/about/page.tsx` (+3 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/distinct/[id]/page.tsx` (+4 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/layout.tsx` (+14 -0) ➕ `tests/fixtures/app-basic/app/inherited-slot/page.tsx` (+3 -0) </details> ### 📄 Description ## Summary Fixes two related bugs in inherited parallel-slot resolution where slots silently rendered `default.tsx` instead of their mirrored sub-page. ### 1. Inherited slots never mirrored sub-pages `discoverInheritedParallelSlots` was hard-coding `pagePath: null` for any ancestor slot, so a slot defined at the layout level only ever rendered `default.tsx` — even when a clear mirror existed. With the fix, each ancestor's segment index is tracked in `dirsToCheck` and the mirror is looked up through three tiers, each more permissive than the last: 1. **Literal filesystem path** — fast path when route and slot share shape. 2. **URL-parts equality** — handles route groups appearing on only one side, e.g. `(marketing)/about` ↔ `@breadcrumbs/about`. 3. **Pattern compatibility (with specificity scoring)** — accepts slot patterns whose dynamic markers have different names than the route's, or whose catch-alls subsume the route. Most-specific compatible sub-page wins (literal > single dynamic > optional catch-all > catch-all). Falls back to `default.tsx` when no mirror matches. ### 2. Slot params used the route's pattern, not the slot's When the slot's mirror declares dynamic markers under different names than the route (e.g. slot `@breadcrumbs/distinct/[name]` for route `distinct/[id]`), the runtime was passing the route's `matchedParams` through unchanged — so the slot page received `{id}` instead of `{name}` and rendered with empty values. Three changes plumb slot-specific params: - Each `ParallelSlot` carries `slotPatternParts` and `slotParamNames` (full URL pattern + ordered param names), serialized through the manifest. - `buildPageElements` extracts a per-request `slotOverrides[key].params` by matching the request URL against `slotPatternParts` whenever a slot's params aren't a subset of the route's. - `AppPageSlotOverride.pageModule` becomes optional so a params-only override doesn't have to swap the slot's page. ## Tests Unit (`tests/app-route-graph.test.ts`): - Literal segment mirroring - Catch-all segment mirroring - Multi-level inheritance - Route-group asymmetry (route has `(marketing)/about`, slot has plain `about`) - Fallback to default when no mirror exists E2E (`tests/e2e/app-router/inherited-parallel-slots.spec.ts`) with a fixture under `app/inherited-slot/`: - Literal mirror renders for `/inherited-slot/about` - Catch-all mirror renders for `/inherited-slot/products/42` (route and slot both use `[...path]` so params share names) - Default fallback renders for `/inherited-slot` - **Distinct param names** — visits `/inherited-slot/distinct/foo` (route `[id]`, slot `[name]`); the page renders `id:foo` and the slot renders `name:foo`. Verified to fail before the runtime fix (slot rendered with empty `name`). ## Test plan - [x] `pnpm check` passes - [x] `pnpm test:unit` (3480 tests) - [x] Playwright app-router project (344 passed; only pre-existing flakes in intercepting/instrumentation tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#1042
No description provided.