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
5 changes: 5 additions & 0 deletions .changeset/big-drinks-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Introduce a `prerender.unstable_concurrency` option, to support running the prerendering concurrently, potentially speeding up the build.
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
- kigawas
- kilavvy
- kiliman
- kirillgroshkov
- kkirsche
- kno-raziel
- knownasilya
Expand Down
52 changes: 52 additions & 0 deletions integration/vite-prerender-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,58 @@ test.describe("Prerendering", () => {
expect(html).toMatch('<h2 data-route="true">About</h2>');
expect(html).toMatch('<p data-loader-data="true">About Loader Data</p>');
});

test("Permits a concurrency option", async () => {
fixture = await createFixture({
prerender: true,
files: {
...files,
"react-router.config.ts": js`
export default {
prerender: {
paths: ['/', '/about'],
unstable_concurrency: 2,
},
}
`,
"vite.config.ts": js`
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";

export default defineConfig({
build: { manifest: true },
plugins: [
reactRouter()
],
});
`,
},
});
appFixture = await createAppFixture(fixture);

let clientDir = path.join(fixture.projectDir, "build", "client");
expect(listAllFiles(clientDir).sort()).toEqual([
"_root.data",
"about.data",
"about/index.html",
"favicon.ico",
"index.html",
]);

let res = await fixture.requestDocument("/");
let html = await res.text();
expect(html).toMatch("<title>Index Title: Index Loader Data</title>");
expect(html).toMatch("<h1>Root</h1>");
expect(html).toMatch('<h2 data-route="true">Index</h2>');
expect(html).toMatch('<p data-loader-data="true">Index Loader Data</p>');

res = await fixture.requestDocument("/about");
html = await res.text();
expect(html).toMatch("<title>About Title: About Loader Data</title>");
expect(html).toMatch("<h1>Root</h1>");
expect(html).toMatch('<h2 data-route="true">About</h2>');
expect(html).toMatch('<p data-loader-data="true">About Loader Data</p>');
});
});

test.describe("ssr: true", () => {
Expand Down
61 changes: 46 additions & 15 deletions packages/react-router-dev/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ type BuildEndHook = (args: {
viteConfig: Vite.ResolvedConfig;
}) => void | Promise<void>;

export type PrerenderPaths =
| boolean
| Array<string>
| ((args: {
getStaticPaths: () => string[];
}) => Array<string> | Promise<Array<string>>);

/**
* Config to be exported via the default export from `react-router.config.ts`.
*/
Expand Down Expand Up @@ -149,13 +156,19 @@ export type ReactRouterConfig = {
/**
* An array of URLs to prerender to HTML files at build time. Can also be a
* function returning an array to dynamically generate URLs.
*
* `unstable_concurrency` defaults to 1, which means "no concurrency" - fully serial execution.
* Setting it to a value more than 1 enables concurrent prerendering.
* Setting it to a value higher than one can increase the speed of the build,
* but may consume more resources, and send more concurrent requests to the
* server/CMS.
*/
prerender?:
| boolean
| Array<string>
| ((args: {
getStaticPaths: () => string[];
}) => Array<string> | Promise<Array<string>>);
| PrerenderPaths
| {
paths: PrerenderPaths;
unstable_concurrency?: number;
};
/**
* An array of React Router plugin config presets to ease integration with
* other platforms and tools.
Expand Down Expand Up @@ -462,17 +475,35 @@ async function resolveConfig({
serverBundles = undefined;
}

let isValidPrerenderConfig =
prerender == null ||
typeof prerender === "boolean" ||
Array.isArray(prerender) ||
typeof prerender === "function";
if (prerender) {
let isValidPrerenderPathsConfig = (p: unknown) =>
typeof p === "boolean" || typeof p === "function" || Array.isArray(p);

if (!isValidPrerenderConfig) {
return err(
"The `prerender` config must be a boolean, an array of string paths, " +
"or a function returning a boolean or array of string paths",
);
let isValidPrerenderConfig =
isValidPrerenderPathsConfig(prerender) ||
(typeof prerender === "object" &&
"paths" in prerender &&
isValidPrerenderPathsConfig(prerender.paths));

if (!isValidPrerenderConfig) {
return err(
"The `prerender`/`prerender.paths` config must be a boolean, an array " +
"of string paths, or a function returning a boolean or array of string paths.",
);
}

let isValidConcurrencyConfig =
typeof prerender != "object" ||
!("unstable_concurrency" in prerender) ||
(typeof prerender.unstable_concurrency === "number" &&
Number.isInteger(prerender.unstable_concurrency) &&
prerender.unstable_concurrency > 0);

if (!isValidConcurrencyConfig) {
return err(
"The `prerender.unstable_concurrency` config must be a positive integer if specified.",
);
}
}

let routeDiscovery: ResolvedReactRouterConfig["routeDiscovery"];
Expand Down
Loading