Skip to content

feat(prerender): support layout-level generateStaticParams#735

Open
raed04 wants to merge 1 commit intocloudflare:mainfrom
raed04:feat/layout-generate-static-params-567
Open

feat(prerender): support layout-level generateStaticParams#735
raed04 wants to merge 1 commit intocloudflare:mainfrom
raed04:feat/layout-generate-static-params-567

Conversation

@raed04
Copy link
Copy Markdown
Contributor

@raed04 raed04 commented Apr 1, 2026

Summary

Closes #567

Layout files at dynamic segments (e.g. app/[category]/layout.tsx) can export generateStaticParams to provide parent params for nested child routes. Previously only page-level exports were scanned, silently skipping layout-only dynamic segments.

  • Extend generateStaticParamsMap code-gen to emit layout-level entries by walking each route's layouts[] and layoutTreePositions[], with deduplication and page-entry priority
  • Remove routeIndex guard in resolveParentParams — check staticParamsMap directly so layout-only prefixes are resolved
  • Handle null results as pass-through (CF Workers Proxy compatibility) instead of collapsing the param accumulator to []
  • Use decodeRouteSegment for static segments to match convertSegmentsToRouteParts behavior
  • Remove dead routeIndex parameter and Map construction from prerenderApp
  • Update entry-templates snapshots (removed stale TODO comments from 6 snapshots)
  • Add 10 new resolveParentParams tests covering layout-only parents, mixed page/layout, null pass-through, catch-all parents, error propagation, and three-level nesting

Changed files

File What changed
packages/vinext/src/entries/app-rsc-entry.ts Two-pass IIFE for generateStaticParamsMap: page entries then layout entries
packages/vinext/src/build/prerender.ts resolveParentParams simplified signature, null pass-through fix
tests/__snapshots__/entry-templates.test.ts.snap Removed stale TODO comment from 6 snapshots
tests/prerender.test.ts 19 total tests (was 9), removed unused routeIndexFrom helper

Test plan

  • All 19 resolveParentParams unit tests pass
  • All 20 entry-templates snapshot tests pass
  • vp check passes on all changed files (lint + types + format)
  • No test regressions vs base branch (same pre-existing failures)
  • Verify with a real fixture that has layout-level generateStaticParams

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 1, 2026

Open in StackBlitz

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

commit: a4e39da

…e#567)

Layout files at dynamic segments (e.g. app/[category]/layout.tsx) can
export generateStaticParams to provide parent params for nested child
routes. Previously only page-level exports were scanned, silently
skipping layout-only dynamic segments.

Changes:
- Extend generateStaticParamsMap code-gen to emit layout-level entries
  by walking each route's layouts[] and layoutTreePositions[]
- Remove routeIndex guard in resolveParentParams; check staticParamsMap
  directly so layout-only prefixes are resolved
- Handle null results as pass-through (CF Workers Proxy compatibility)
  instead of collapsing the param accumulator
- Use decodeRouteSegment for static segments to match the authoritative
  convertSegmentsToRouteParts behavior
- Remove dead routeIndex parameter and Map construction
- Update entry-templates snapshots (removed stale TODO comments)
- Add 10 new resolveParentParams tests covering layout-only parents,
  mixed page/layout, null pass-through, catch-all parents, error
  propagation, and three-level nesting

Closes cloudflare#567
@raed04 raed04 force-pushed the feat/layout-generate-static-params-567 branch from 0362287 to a4e39da Compare April 1, 2026 03:51
@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.

Review: layout-level generateStaticParams

Good feature — this closes a real gap where layout-only dynamic segments were silently skipped during prerendering. The approach of extending generateStaticParamsMap code-gen and simplifying resolveParentParams is sound. A few items to address before merging:

Issues

  1. Type unsoundness for null returnStaticParamsMap (line 249-256 of prerender.ts) declares the function return type as Promise<Record<string, string | string[]>[]>, which does not include null. The new null pass-through logic in resolveParentParams works at runtime but the tests have to cast with as unknown as ... to work around this. The type should be updated to Promise<Record<string, string | string[]>[] | null> so this contract is explicit.

  2. Duplicate tests — Two of the new tests are exact duplicates of pre-existing ones (identical mock inputs, identical assertions, only different description strings). See inline comments.

  3. Missing fixture coverage — The PR description calls out that [ ] Verify with a real fixture that has layout-level generateStaticParams is unchecked. The unit tests for resolveParentParams are good but they only test the resolver in isolation — the code-gen IIFE in app-rsc-entry.ts (the part that walks r.layouts / r.layoutTreePositions to build prefix patterns) has no test coverage. A fixture with app/[category]/layout.tsx exporting generateStaticParams (and no page at that level) would close the loop end-to-end and test the code-gen path.

Minor observations

  1. The code-gen IIFE duplicates convertSegmentsToRouteParts logic inline. This is noted in comments but worth tracking — if convertSegmentsToRouteParts changes, this copy will silently diverge.

  2. The snapshot diffs are purely mechanical (removing TODO comments) and look correct.

};
const result = await resolveParentParams(child, staticParamsMap);
expect(result).toEqual([{ category: "electronics" }, { category: "clothing" }]);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test is an exact duplicate of "resolves single parent dynamic segment" at line 776. The inputs, function call, and assertions are character-for-character identical — only the description string differs.

Since resolveParentParams no longer takes a routeIndex and doesn't inspect pagePath at all, there's no distinct code path being exercised here. The "layout-level" vs "page-level" distinction only matters in the code-gen (which builds the staticParamsMap), not in the resolver itself.

Consider removing this test or making it genuinely different — e.g., by passing mockRoute("/shop/:category/:item", { pagePath: null }) and verifying the behavior is the same, to document the intent that pagePath no longer matters.

{ b: "2", d: "y" },
{ b: "2", d: "z" },
]);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same issue — this is an exact duplicate of "resolves two levels of parent dynamic segments" at line 785. Same mockRoute, same staticParamsMap, same expected output.

const child = mockRoute("/a/:b/c/:d/:e");
const staticParamsMap: StaticParamsMap = {
// /a/:b has no generateStaticParams — simulates CF Workers Proxy returning null
"/a/:b": async () => null as unknown as Record<string, string | string[]>[],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The as unknown as Record<string, string | string[]>[] cast is a type smell. It's needed because StaticParamsMap's function return type doesn't allow null. Rather than casting in every null-related test, update the StaticParamsMap type definition (line 253) to:

    }) => Promise<Record<string, string | string[]>[] | null>)

Then these casts become unnecessary and the null pass-through behavior is part of the documented contract.

for (const parentParams of currentParams) {
const results = await generateStaticParams({ params: parentParams });
if (Array.isArray(results)) {
if (results === null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The null check here is correct behavior-wise, but the type system doesn't know generateStaticParams can return null — the StaticParamsMap function signature only allows Promise<Record<string, string | string[]>[]>. This means results === null is technically unreachable according to the types.

Update the return type in the StaticParamsMap type definition (line 253) to include | null so this branch is type-sound.

Comment on lines +1345 to +1374
// Convert filesystem segments to URL pattern parts (same logic as convertSegmentsToRouteParts)
const urlParts: string[] = [];
let hasDynamic = false;
for (const seg of prefixSegments) {
// Skip invisible segments
if (seg === "." || (seg.startsWith("(") && seg.endsWith(")")) || seg.startsWith("@"))
continue;
// Catch-all
const catchAll = seg.match(/^\[\.\.\.([\w-]+)\]$/);
if (catchAll) {
urlParts.push(`:${catchAll[1]}+`);
hasDynamic = true;
continue;
}
// Optional catch-all
const optCatchAll = seg.match(/^\[\[\.\.\.([\w-]+)\]\]$/);
if (optCatchAll) {
urlParts.push(`:${optCatchAll[1]}*`);
hasDynamic = true;
continue;
}
// Dynamic segment
const dyn = seg.match(/^\[([\w-]+)\]$/);
if (dyn) {
urlParts.push(`:${dyn[1]}`);
hasDynamic = true;
continue;
}
// Static segment — decode percent-encoded chars to match convertSegmentsToRouteParts
urlParts.push(decodeRouteSegment(seg));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This block replicates the segment-to-URL-pattern conversion logic from convertSegmentsToRouteParts() in routing/app-router.ts. The comment acknowledges the duplication, which is good. Worth tracking as tech debt — if convertSegmentsToRouteParts gains handling for new segment types (e.g., intercepting route conventions), this copy won't pick it up.

Longer-term, consider extracting the shared segment→URL-part conversion into a reusable helper in routing/utils.ts. Not a blocker for this PR.


// 2. Layout-level entries (layouts at dynamic segments that may export generateStaticParams)
for (const r of routes) {
if (!r.isDynamic) continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: since the for loop is a no-op when r.layouts.length === 0, you could fold it into the guard:

Suggested change
if (!r.isDynamic) continue;
if (!r.isDynamic || r.layouts.length === 0) continue;

Makes the intent clearer — skip routes that have no layouts to scan.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 1, 2026

Review submitted successfully. Here's a summary of what I posted:

3 issues identified:

  1. Type unsoundnessStaticParamsMap's function return type doesn't include null, but the new null pass-through logic in resolveParentParams explicitly handles null returns. Tests use as unknown as ... casts to work around this. The type should be updated to Promise<... | null>.

  2. Duplicate tests — Two new tests ("resolves parent params from layout-level generateStaticParams" and "resolves two levels of layout-only parent segments") are exact duplicates of existing tests with different names. Since resolveParentParams no longer inspects pagePath or routeIndex, the "layout-level" distinction only matters in code-gen, not the resolver.

  3. Missing fixture coverage — The PR's own checklist marks fixture verification as incomplete. The code-gen IIFE that walks r.layouts/r.layoutTreePositions has no test coverage.

2 minor observations:

  1. The code-gen duplicates convertSegmentsToRouteParts logic inline — worth tracking as tech debt.
  2. A minor nit to fold r.layouts.length === 0 into the existing !r.isDynamic guard for clarity.

github run

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.

Support layout-level generateStaticParams for pre-rendering

2 participants