Skip to content

Commit 80751f7

Browse files
authored
feat: fail the build for dynamic fs-routes without generateStaticParams (#136)
1 parent 16736af commit 80751f7

5 files changed

Lines changed: 20 additions & 25 deletions

File tree

packages/docs/src/pages/learn/FileSystemRouting.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ export default function BlogPost({ params }: { params: { slug: string } }) {
120120

121121
This generates `blog/hello.html` and `blog/world.html`. Each page component receives the resolved `params` as a prop.
122122

123-
A dynamic route without `generateStaticParams` is **not** pre-rendered (a warning is logged); it still resolves on the client via the SPA fallback.
123+
A dynamic route **must** export `generateStaticParams`; the build fails otherwise. A static site can only serve pages that were enumerated at build time, so a dynamic route without it would produce no output.
124124

125-
> **Note:** Because static hosting serves one pre-rendered RSC payload per page, soft client-side navigation between different values of the _same_ dynamic route reflects the params of the initially-loaded page. Loading a dynamic URL directly (or via the SPA fallback) always renders the correct params. Static routes and layouts navigate fully on the client.
125+
> **Note:** Because static hosting serves one pre-rendered RSC payload per page, soft client-side navigation between different values of the _same_ dynamic route reflects the params of the initially-loaded page. Loading a dynamic URL directly always renders the correct params. Static routes and layouts navigate fully on the client.
126126
127127
## Custom Conventions (Adapters)
128128

packages/static/src/fs-routes/runtime.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export function createFsRoutesEntries(
115115
};
116116
const files = modulesToRouteFiles(modules, warn);
117117
const tree = adapter.buildRoutes(files);
118-
const pages = await collectStaticPaths(tree, warn);
118+
const pages = await collectStaticPaths(tree);
119119
for (const { urlPath, params } of pages) {
120120
yield {
121121
path: urlPathToFilePath(urlPath),

packages/static/src/fs-routes/tree.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,13 @@ describe("collectStaticPaths", () => {
9494
]);
9595
});
9696

97-
it("warns and skips a dynamic route without generateStaticParams", async () => {
97+
it("throws for a dynamic route without generateStaticParams", async () => {
9898
const tree: FsRouteTreeNode[] = [
9999
{ path: "/blog/:slug", page: true, module: component },
100100
];
101-
const warn = vi.fn();
102-
const pages = await collectStaticPaths(tree, warn);
103-
expect(pages).toEqual([]);
104-
expect(warn).toHaveBeenCalledTimes(1);
105-
expect(warn.mock.calls[0]?.[0]).toContain("/blog/:slug");
101+
await expect(collectStaticPaths(tree)).rejects.toThrow(
102+
/"\/blog\/:slug".*generateStaticParams/,
103+
);
106104
});
107105

108106
it("throws when generateStaticParams is missing a param value", async () => {

packages/static/src/fs-routes/tree.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ async function addPagesForLeaf(
8787
segments: string[],
8888
module: FsRouteModule,
8989
pages: StaticPage[],
90-
onWarn?: (message: string) => void,
9190
): Promise<void> {
9291
const dynamicSegments = segments.filter(isDynamicSegment);
9392

@@ -98,11 +97,11 @@ async function addPagesForLeaf(
9897

9998
const generate = module.generateStaticParams;
10099
if (typeof generate !== "function") {
101-
onWarn?.(
102-
`Dynamic route "${segmentsToUrl(segments)}" has no generateStaticParams() export; skipping static generation. ` +
103-
`It will still work on the client via the SPA fallback.`,
100+
throw new Error(
101+
`Dynamic route "${segmentsToUrl(segments)}" has no generateStaticParams() export. ` +
102+
`Every page of a static site must be enumerated at build time; ` +
103+
`export generateStaticParams() from the page module to list the params to pre-render.`,
104104
);
105-
return;
106105
}
107106

108107
const paramSets = await generate();
@@ -126,17 +125,16 @@ async function walk(
126125
nodes: FsRouteTreeNode[],
127126
prefixSegments: string[],
128127
pages: StaticPage[],
129-
onWarn?: (message: string) => void,
130128
): Promise<void> {
131129
for (const node of nodes) {
132130
const ownSegments =
133131
node.path !== undefined ? splitRoutePath(node.path) : [];
134132
const segments = [...prefixSegments, ...ownSegments];
135133
if (node.page) {
136-
await addPagesForLeaf(segments, node.module, pages, onWarn);
134+
await addPagesForLeaf(segments, node.module, pages);
137135
}
138136
if (node.children) {
139-
await walk(node.children, segments, pages, onWarn);
137+
await walk(node.children, segments, pages);
140138
}
141139
}
142140
}
@@ -146,15 +144,15 @@ async function walk(
146144
*
147145
* Static routes are emitted directly. Dynamic routes (with `:param` or
148146
* catch-all segments) are expanded using each page module's
149-
* `generateStaticParams()`; if absent, the route is skipped and `onWarn` is
150-
* invoked.
147+
* `generateStaticParams()`; a dynamic route without that export fails the
148+
* build, since a static site cannot serve pages that were not enumerated at
149+
* build time.
151150
*/
152151
export async function collectStaticPaths(
153152
tree: FsRouteTreeNode[],
154-
onWarn?: (message: string) => void,
155153
): Promise<StaticPage[]> {
156154
const pages: StaticPage[] = [];
157-
await walk(tree, [], pages, onWarn);
155+
await walk(tree, [], pages);
158156
return pages;
159157
}
160158

packages/static/src/fs-routes/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ export interface FsRouteModule {
1313
/** The component for this page or layout. */
1414
default?: ComponentType<{ params: Record<string, string> }> | ComponentType;
1515
/**
16-
* Optional function used to statically generate a dynamic route.
16+
* Function used to statically generate a dynamic route. Required for pages
17+
* whose route contains a dynamic segment; the build fails without it, since
18+
* a static site cannot serve pages that were not enumerated at build time.
1719
*
1820
* Returns the list of concrete params to pre-render. Each entry maps every
1921
* dynamic param name in the route's path to a concrete string value. For a
2022
* catch-all segment, the value may contain slashes.
21-
*
22-
* Without this export, a dynamic route is not pre-rendered to HTML (it still
23-
* works on the client via the SPA fallback).
2423
*/
2524
generateStaticParams?: () => MaybePromise<Array<Record<string, string>>>;
2625
[key: string]: unknown;

0 commit comments

Comments
 (0)