Skip to content

Commit bf2bc05

Browse files
Fix usage of prerender option with serverBundles (#13082)
1 parent b3ad2a2 commit bf2bc05

File tree

3 files changed

+98
-8
lines changed

3 files changed

+98
-8
lines changed

.changeset/dry-impalas-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
Fix usage of `prerender` option when `serverBundles` option has been configured or provided by a preset, e.g. `vercelPreset` from `@vercel/react-router`

integration/vite-prerender-test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,60 @@ test.describe("Prerendering", () => {
285285
expect(html).toMatch('<p data-loader-data="true">About Loader Data</p>');
286286
});
287287

288+
test("Prerenders a static array of routes with server bundles", async () => {
289+
fixture = await createFixture({
290+
prerender: true,
291+
files: {
292+
...files,
293+
"react-router.config.ts": js`
294+
let counter = 1;
295+
export default {
296+
serverBundles: () => "server" + counter++,
297+
async prerender() {
298+
await new Promise(r => setTimeout(r, 1));
299+
return ['/', '/about'];
300+
},
301+
}
302+
`,
303+
"vite.config.ts": js`
304+
import { defineConfig } from "vite";
305+
import { reactRouter } from "@react-router/dev/vite";
306+
307+
export default defineConfig({
308+
build: { manifest: true },
309+
plugins: [
310+
reactRouter()
311+
],
312+
});
313+
`,
314+
},
315+
});
316+
appFixture = await createAppFixture(fixture);
317+
318+
let clientDir = path.join(fixture.projectDir, "build", "client");
319+
expect(listAllFiles(clientDir).sort()).toEqual([
320+
"_root.data",
321+
"about.data",
322+
"about/index.html",
323+
"favicon.ico",
324+
"index.html",
325+
]);
326+
327+
let res = await fixture.requestDocument("/");
328+
let html = await res.text();
329+
expect(html).toMatch("<title>Index Title: Index Loader Data</title>");
330+
expect(html).toMatch("<h1>Root</h1>");
331+
expect(html).toMatch('<h2 data-route="true">Index</h2>');
332+
expect(html).toMatch('<p data-loader-data="true">Index Loader Data</p>');
333+
334+
res = await fixture.requestDocument("/about");
335+
html = await res.text();
336+
expect(html).toMatch("<title>About Title: About Loader Data</title>");
337+
expect(html).toMatch("<h1>Root</h1>");
338+
expect(html).toMatch('<h2 data-route="true">About</h2>');
339+
expect(html).toMatch('<p data-loader-data="true">About Loader Data</p>');
340+
});
341+
288342
test("Prerenders a dynamic array of routes based on the static routes", async () => {
289343
fixture = await createFixture({
290344
files: {

packages/react-router-dev/vite/plugin.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,28 @@ const isRouteVirtualModule = (id: string): boolean => {
199199
return isRouteEntryModuleId(id) || isRouteChunkModuleId(id);
200200
};
201201

202+
const isServerBuildVirtualModuleId = (id: string): boolean => {
203+
return id.split("?")[0] === virtual.serverBuild.id;
204+
};
205+
206+
const getServerBuildFile = (viteManifest: Vite.Manifest): string => {
207+
let serverBuildIds = Object.keys(viteManifest).filter(
208+
isServerBuildVirtualModuleId
209+
);
210+
211+
invariant(
212+
serverBuildIds.length <= 1,
213+
"Multiple server build files found in manifest"
214+
);
215+
216+
invariant(
217+
serverBuildIds.length === 1,
218+
"Server build file not found in manifest"
219+
);
220+
221+
return viteManifest[serverBuildIds[0]].file;
222+
};
223+
202224
export type ServerBundleBuildConfig = {
203225
routes: RouteManifest;
204226
serverBundleId: string;
@@ -1518,7 +1540,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
15181540
viteConfig,
15191541
ctx.reactRouterConfig,
15201542
serverBuildDirectory,
1521-
ssrViteManifest[virtual.serverBuild.id].file,
1543+
getServerBuildFile(ssrViteManifest),
15221544
clientBuildDirectory
15231545
);
15241546
}
@@ -1531,7 +1553,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
15311553
viteConfig,
15321554
ctx.reactRouterConfig,
15331555
serverBuildDirectory,
1534-
ssrViteManifest[virtual.serverBuild.id].file,
1556+
getServerBuildFile(ssrViteManifest),
15351557
clientBuildDirectory
15361558
);
15371559
}
@@ -2406,19 +2428,28 @@ async function handlePrerender(
24062428
serverBuildPath
24072429
);
24082430

2409-
let routes = createPrerenderRoutes(build.routes);
2431+
let routes = createPrerenderRoutes(reactRouterConfig.routes);
2432+
for (let path of build.prerender) {
2433+
let matches = matchRoutes(routes, `/${path}/`.replace(/^\/\/+/, "/"));
2434+
if (!matches) {
2435+
throw new Error(
2436+
`Unable to prerender path because it does not match any routes: ${path}`
2437+
);
2438+
}
2439+
}
2440+
2441+
let buildRoutes = createPrerenderRoutes(build.routes);
24102442
let headers = {
24112443
// Header that can be used in the loader to know if you're running at
24122444
// build time or runtime
24132445
"X-React-Router-Prerender": "yes",
24142446
};
24152447
for (let path of build.prerender) {
24162448
// Ensure we have a leading slash for matching
2417-
let matches = matchRoutes(routes, `/${path}/`.replace(/^\/\/+/, "/"));
2418-
invariant(
2419-
matches,
2420-
`Unable to prerender path because it does not match any routes: ${path}`
2421-
);
2449+
let matches = matchRoutes(buildRoutes, `/${path}/`.replace(/^\/\/+/, "/"));
2450+
if (!matches) {
2451+
continue;
2452+
}
24222453
// When prerendering a resource route, we don't want to pass along the
24232454
// `.data` file since we want to prerender the raw Response returned from
24242455
// the loader. Presumably this is for routes where a file extension is

0 commit comments

Comments
 (0)