Skip to content

fix(app-router): scope layout params and layout error boundaries#938

Open
NathanDrake2406 wants to merge 2 commits intocloudflare:mainfrom
NathanDrake2406:nathan/app-layout-boundaries-params
Open

fix(app-router): scope layout params and layout error boundaries#938
NathanDrake2406 wants to merge 2 commits intocloudflare:mainfrom
NathanDrake2406:nathan/app-layout-boundaries-params

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

@NathanDrake2406 NathanDrake2406 commented Apr 28, 2026

What this changes

App Router layouts now receive only the params that apply at their segment depth during both page tree wiring and layout probing. Layout metadata and viewport resolution use the same segment-scoped params, while page metadata still receives full route params and searchParams.

Layout generateMetadata() and generateViewport() failures now propagate to the normal App Router error boundary path instead of being swallowed. Layout-thrown forbidden() and unauthorized() now select the matching parent HTTP access boundary instead of falling through the not-found-only path.

The generated RSC entry stays thin: it serializes route imports and delegates param slicing, head resolution, search param collection, and parent access-boundary selection to typed server helpers.

Why

Next.js scopes layout params by walking accumulated parent params per segment, not by passing the full route params object to every layout. Relevant references:

Next.js also routes metadata and HTTP access fallback failures through the relevant error/access boundary paths:

Approach

Add small functional helpers under server/ for segment param slicing, search param collection, head resolution, and parent access-boundary selection. The RSC entry now imports those helpers and keeps the route-specific imperative work in place.

The scanner now keeps forbidden and unauthorized boundary arrays aligned with layout levels, matching the existing not-found array shape.

Validation

  • vp test run tests/app-page-params.test.ts tests/app-page-head.test.ts tests/app-page-boundary.test.ts tests/app-page-route-wiring.test.ts tests/entry-templates.test.ts tests/routing.test.ts
  • vp test run tests/nextjs-compat/global-error.test.ts
  • vp test run tests/app-router.test.ts -t "thrown from a layout uses|layout generateMetadata\(\) does not receive searchParams"
  • vp check tests/app-page-params.test.ts tests/app-page-head.test.ts tests/app-page-boundary.test.ts tests/app-page-route-wiring.test.ts tests/entry-templates.test.ts tests/routing.test.ts tests/nextjs-compat/global-error.test.ts tests/app-router.test.ts
  • vp check tests/nextjs-compat/global-error.test.ts
  • git diff --check

Risks / follow-ups

Checked open PRs before publishing. Closest adjacent work is #891, #735, and #822; this PR should not overlap their behavior directly.

This PR intentionally does not address remaining App Router opportunities like unknown Server Action IDs, non-action method handling, route-handler NextResponse.next() validation, or route-handler cookie precedence.

Layouts received full route params, layout generateMetadata failures were swallowed, and layout-thrown forbidden()/unauthorized() fell back through the not-found boundary path. Those diverge from Next.js when nested layouts depend on segment params, when head generation fails before page render, or when HTTP access APIs are thrown from a layout.

The generated RSC entry now delegates param slicing, head resolution, and parent access-boundary selection to typed runtime helpers. The helpers keep route-specific imports in the entry while moving the behavioral core into unit-tested modules.

Tests port Next.js layout params, global-error, forbidden, and unauthorized coverage into focused helper and integration regressions.
Copilot AI review requested due to automatic review settings April 28, 2026 16:04
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@938

commit: 9d9e98e

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves App Router Next.js compatibility by scoping layout params to the segment depth (instead of passing full route params everywhere) and by routing layout metadata/access-fallback failures through the correct boundary paths. It also keeps the generated RSC entry slimmer by delegating param slicing, head resolution, and parent access-boundary selection to server helpers.

Changes:

  • Add server helpers for segment-scoped params, search param collection, and unified head (metadata/viewport) resolution.
  • Route layout generateMetadata() errors through normal error boundaries; route layout forbidden() / unauthorized() throws to the correct parent HTTP access boundary.
  • Update router scanning/entry generation to carry per-layout forbidden/unauthorized boundary arrays and to use the new helpers; add/extend tests and fixtures for Next.js parity.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/vinext/src/server/app-page-params.ts New helper to slice matched route params by segment depth for layouts.
packages/vinext/src/server/app-page-head.ts New helper to collect search params and resolve layout+page metadata/viewport with segment-scoped layout params.
packages/vinext/src/server/app-page-route-wiring.tsx Pass segment-scoped params to each layout during element wiring.
packages/vinext/src/server/app-page-boundary.ts Scope params during boundary/layout wrapping; add parent HTTP access boundary selection helper.
packages/vinext/src/server/app-page-boundary-render.ts Use shared head resolution helper for fallback pages; remove duplicated head-resolution logic.
packages/vinext/src/routing/app-router.ts Track forbidden/unauthorized boundary paths per layout level (aligned with layouts).
packages/vinext/src/entries/app-rsc-entry.ts Keep generated entry thin by importing helpers for head resolution, param slicing, and parent access-boundary selection; serialize per-layout boundary arrays.
packages/vinext/src/shims/metadata.tsx Broaden searchParams typing to support repeated query keys (`string
tests/app-page-params.test.ts New unit tests for segment param scoping (including catch-all/optional catch-all behavior).
tests/app-page-head.test.ts New unit tests for search param collection and scoped layout metadata vs full page metadata inputs.
tests/app-page-route-wiring.test.ts Assert layout probing/wiring uses only segment-applicable params.
tests/app-page-boundary.test.ts Update expectations to reflect new segment/param scoping behavior.
tests/app-router.test.ts New integration assertions for layout-thrown forbidden/unauthorized selecting correct boundaries.
tests/nextjs-compat/global-error.test.ts Add coverage for layout generateMetadata() errors flowing to local/global error boundaries.
tests/fixtures/app-basic/app/nextjs-compat/* New fixtures for layout metadata error + layout access fallback scenarios.
tests/__snapshots__/entry-templates.test.ts.snap Snapshot updates reflecting new generated entry imports/behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/vinext/src/server/app-page-head.ts
Layout generateViewport() failures now intentionally share the same boundary path as layout generateMetadata() failures. Copilot flagged that this behavior change needed direct coverage.

Add dev and production-preview compat assertions for co-located error.tsx handling and global-error escalation, with fixtures that throw from layout generateViewport().
@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Solid PR. The refactoring is well-structured: extracted helpers are small, single-purpose, and properly tested. The entry stays thin (matching the repo's architecture guidance), and the boundary arrays for forbidden/unauthorized are properly aligned with the existing not-found pattern.

The behavior changes are well-justified by the Next.js references, and the test coverage is thorough — unit tests for the new helpers, integration tests for both dev and production preview, and the fixtures are minimal and focused.

A few minor observations:

1. Metadata cascade on layout rejection (app-page-head.ts:97-103)

When metadataPromise rejects, the .then() chain on line 97 also rejects. The void accumulatedMetadata.catch(() => null) on line 103 prevents unhandled rejections. This means if layout N throws, layouts N+1..M will also have their parentForLayout reject and their own promises will be rejected too. The Promise.all on line 109 surfaces the first rejection, which is the correct one. The logic is sound.

2. Naming consistency (app-rsc-entry.ts)

forbiddens and unauthorizeds as property names read awkwardly, but consistency with the existing notFounds pattern is more important. If there's ever a rename pass, forbiddenModules/unauthorizedModules would be clearer.

3. Dot exclusion in param parsing (app-page-params.ts:15)

The !segment.includes(".") check correctly prevents matching segments with dots (e.g. route groups like (...)) after catch-all patterns are handled. This matches Next.js behavior where dynamic segments can't contain dots.

4. searchParams type widening (metadata.tsx:357)

The searchParams type change from Record<string, string> to Record<string, string | string[]> is correct — repeated query keys produce arrays in collectAppPageSearchParams, and this was a latent type mismatch.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 28, 2026

The review has been submitted. Here's a summary of my findings:

Approved — this is a well-structured PR that improves Next.js parity in two areas:

  1. Scoped layout params: Layouts now receive only the params applicable at their segment depth (matching Next.js's create-component-tree.tsx behavior), rather than the full route params. This is enforced consistently in page tree wiring, layout probing, metadata resolution, and boundary rendering.

  2. Layout error/access boundary propagation: Layout generateMetadata() and generateViewport() errors now propagate through the normal error boundary path instead of being swallowed. Layout forbidden()/unauthorized() throws now select the correct parent HTTP access boundary instead of always falling through the not-found path.

The implementation follows the repo's architecture guidelines well — the generated RSC entry stays thin, with real behavior extracted into typed helpers under server/ (app-page-params.ts, app-page-head.ts, app-page-boundary.ts). Test coverage is thorough with unit tests for the new helpers, integration tests in both dev and production preview modes, and fixtures ported from the Next.js test suite.

github run

Comment on lines +424 to +430
params: options.makeThenableParams(
resolveAppPageSegmentParams(
options.route.routeSegments,
layoutEntry.treePosition,
options.matchedParams,
),
),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i think we may also need to align the params passed to the page as well, like we've done for the layout here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants