Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 13 additions & 17 deletions packages/vinext/src/build/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ export type StaticParamsMap = Record<
*/
export async function resolveParentParams(
childRoute: AppRoute,
routeIndex: ReadonlyMap<string, AppRoute>,
staticParamsMap: StaticParamsMap,
): Promise<Record<string, string | string[]>[]> {
const { patternParts } = childRoute;
Expand Down Expand Up @@ -292,18 +291,12 @@ export async function resolveParentParams(
prefixPattern += "/" + part;
if (!part.startsWith(":")) continue;

const parentRoute = routeIndex.get(prefixPattern);
// TODO: layout-level generateStaticParams — a layout segment can define
// generateStaticParams without a corresponding page file, so parentRoute
// may be undefined here even though the layout exports generateStaticParams.
// resolveParentParams currently only looks up routes that have a pagePath
// (i.e. leaf pages), missing layout-level providers. Fix requires scanning
// layout files in addition to page files during route collection.
if (parentRoute?.pagePath) {
const fn = staticParamsMap[prefixPattern];
if (typeof fn === "function") {
parentSegments.push(fn);
}
// Check staticParamsMap directly — it now includes both page-level and
// layout-level generateStaticParams entries, so we don't need to verify
// that a page route exists at this prefix pattern.
const fn = staticParamsMap[prefixPattern];
if (typeof fn === "function") {
parentSegments.push(fn);
}
}

Expand All @@ -314,7 +307,12 @@ export async function resolveParentParams(
const nextParams: Record<string, string | string[]>[] = [];
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.

// No generateStaticParams at this level (e.g. CF Workers Proxy returned
// null for a prefix with no layout/page). Pass parent params through
// unchanged so deeper parent segments can still be resolved.
nextParams.push(parentParams);
} else if (Array.isArray(results)) {
for (const result of results) {
nextParams.push({ ...parentParams, ...result });
}
Expand Down Expand Up @@ -802,8 +800,6 @@ export async function prerenderApp({
},
});

const routeIndex = new Map(routes.map((r) => [r.pattern, r]));

// ── Collect URLs to render ────────────────────────────────────────────────
type UrlToRender = {
urlPath: string;
Expand Down Expand Up @@ -887,7 +883,7 @@ export async function prerenderApp({
continue;
}

const parentParamSets = await resolveParentParams(route, routeIndex, staticParamsMap);
const parentParamSets = await resolveParentParams(route, staticParamsMap);
let paramSets: Record<string, string | string[]>[] | null;

if (parentParamSets.length > 0) {
Expand Down
77 changes: 65 additions & 12 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
NextRewrite,
} from "../config/next-config.js";
import type { AppRoute } from "../routing/app-router.js";
import { decodeRouteSegment } from "../routing/utils.js";
import { generateDevOriginCheckCode } from "../server/dev-origin-check.js";
import type { MetadataFileRoute } from "../server/metadata-routes.js";
import {
Expand Down Expand Up @@ -1320,18 +1321,70 @@ async function __readFormDataWithLimit(request, maxBytes) {
// Used by the prerender phase to enumerate dynamic route URLs without
// loading route modules via the dev server.
export const generateStaticParamsMap = {
// TODO: layout-level generateStaticParams — this map only includes routes that
// have a pagePath (leaf pages). Layout segments can also export generateStaticParams
// to provide parent params for nested dynamic routes, but they don't have a pagePath
// so they are excluded here. Supporting layout-level generateStaticParams requires
// scanning layout.tsx files separately and including them in this map.
${routes
.filter((r) => r.isDynamic && r.pagePath)
.map(
(r) =>
` ${JSON.stringify(r.pattern)}: ${getImportVar(r.pagePath!)}?.generateStaticParams ?? null,`,
)
.join("\n")}
${(() => {
const entries: string[] = [];
const emittedPatterns = new Set<string>();

// 1. Page-level entries (leaf pages with generateStaticParams)
for (const r of routes) {
if (r.isDynamic && r.pagePath) {
entries.push(
` ${JSON.stringify(r.pattern)}: ${getImportVar(r.pagePath)}?.generateStaticParams ?? null,`,
);
emittedPatterns.add(r.pattern);
}
}

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

for (let li = 0; li < r.layouts.length; li++) {
const layoutDepth = r.layoutTreePositions[li];
// Build the prefix pattern from routeSegments up to this layout's depth
const prefixSegments = r.routeSegments.slice(0, layoutDepth);
// 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));
Comment on lines +1345 to +1374
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.

}
if (!hasDynamic) continue;
const prefixPattern = "/" + urlParts.join("/");
if (emittedPatterns.has(prefixPattern)) continue;
emittedPatterns.add(prefixPattern);
entries.push(
` ${JSON.stringify(prefixPattern)}: ${getImportVar(r.layouts[li])}?.generateStaticParams ?? null,`,
);
}
}

return entries.join("\n");
})()}
};

export default async function handler(request, ctx) {
Expand Down
30 changes: 0 additions & 30 deletions tests/__snapshots__/entry-templates.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1246,11 +1246,6 @@ async function __readFormDataWithLimit(request, maxBytes) {
// Used by the prerender phase to enumerate dynamic route URLs without
// loading route modules via the dev server.
export const generateStaticParamsMap = {
// TODO: layout-level generateStaticParams — this map only includes routes that
// have a pagePath (leaf pages). Layout segments can also export generateStaticParams
// to provide parent params for nested dynamic routes, but they don't have a pagePath
// so they are excluded here. Supporting layout-level generateStaticParams requires
// scanning layout.tsx files separately and including them in this map.
"/blog/:slug": mod_3?.generateStaticParams ?? null,
};

Expand Down Expand Up @@ -3440,11 +3435,6 @@ async function __readFormDataWithLimit(request, maxBytes) {
// Used by the prerender phase to enumerate dynamic route URLs without
// loading route modules via the dev server.
export const generateStaticParamsMap = {
// TODO: layout-level generateStaticParams — this map only includes routes that
// have a pagePath (leaf pages). Layout segments can also export generateStaticParams
// to provide parent params for nested dynamic routes, but they don't have a pagePath
// so they are excluded here. Supporting layout-level generateStaticParams requires
// scanning layout.tsx files separately and including them in this map.
"/blog/:slug": mod_3?.generateStaticParams ?? null,
};

Expand Down Expand Up @@ -5646,11 +5636,6 @@ async function __readFormDataWithLimit(request, maxBytes) {
// Used by the prerender phase to enumerate dynamic route URLs without
// loading route modules via the dev server.
export const generateStaticParamsMap = {
// TODO: layout-level generateStaticParams — this map only includes routes that
// have a pagePath (leaf pages). Layout segments can also export generateStaticParams
// to provide parent params for nested dynamic routes, but they don't have a pagePath
// so they are excluded here. Supporting layout-level generateStaticParams requires
// scanning layout.tsx files separately and including them in this map.
"/blog/:slug": mod_3?.generateStaticParams ?? null,
};

Expand Down Expand Up @@ -7870,11 +7855,6 @@ async function __readFormDataWithLimit(request, maxBytes) {
// Used by the prerender phase to enumerate dynamic route URLs without
// loading route modules via the dev server.
export const generateStaticParamsMap = {
// TODO: layout-level generateStaticParams — this map only includes routes that
// have a pagePath (leaf pages). Layout segments can also export generateStaticParams
// to provide parent params for nested dynamic routes, but they don't have a pagePath
// so they are excluded here. Supporting layout-level generateStaticParams requires
// scanning layout.tsx files separately and including them in this map.
"/blog/:slug": mod_3?.generateStaticParams ?? null,
};

Expand Down Expand Up @@ -10074,11 +10054,6 @@ async function __readFormDataWithLimit(request, maxBytes) {
// Used by the prerender phase to enumerate dynamic route URLs without
// loading route modules via the dev server.
export const generateStaticParamsMap = {
// TODO: layout-level generateStaticParams — this map only includes routes that
// have a pagePath (leaf pages). Layout segments can also export generateStaticParams
// to provide parent params for nested dynamic routes, but they don't have a pagePath
// so they are excluded here. Supporting layout-level generateStaticParams requires
// scanning layout.tsx files separately and including them in this map.
"/blog/:slug": mod_3?.generateStaticParams ?? null,
};

Expand Down Expand Up @@ -12497,11 +12472,6 @@ async function __readFormDataWithLimit(request, maxBytes) {
// Used by the prerender phase to enumerate dynamic route URLs without
// loading route modules via the dev server.
export const generateStaticParamsMap = {
// TODO: layout-level generateStaticParams — this map only includes routes that
// have a pagePath (leaf pages). Layout segments can also export generateStaticParams
// to provide parent params for nested dynamic routes, but they don't have a pagePath
// so they are excluded here. Supporting layout-level generateStaticParams requires
// scanning layout.tsx files separately and including them in this map.
"/blog/:slug": mod_3?.generateStaticParams ?? null,
};

Expand Down
Loading
Loading