Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions .changeset/big-drinks-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@react-router/dev": patch
---

Introduce a `prerender.unstable_concurrency` option, to support running the prerendering concurrently, potentially speeding up the build.

RFC https://github.com/remix-run/react-router/discussions/14080
fixes https://github.com/remix-run/react-router/issues/14383
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
23 changes: 18 additions & 5 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
211 changes: 121 additions & 90 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ import {
createConfigLoader,
resolveEntryFiles,
configRouteToBranchRoute,
PrerenderPaths,
} from "../config/config";
import { getOptimizeDepsEntries } from "./optimize-deps-entries";
import { decorateComponentExportsWithProps } from "./with-props";
import { loadDotenv } from "./load-dotenv";
import { validatePluginOrder } from "./plugins/validate-plugin-order";
import { warnOnClientSourceMaps } from "./plugins/warn-on-client-source-maps";
import { pMap } from "./pmap";

export type LoadCssContents = (
viteDevServer: Vite.ViteDevServer,
Expand Down Expand Up @@ -2658,80 +2660,93 @@ async function handlePrerender(
}

let buildRoutes = createPrerenderRoutes(build.routes);
for (let path of build.prerender) {
// Ensure we have a leading slash for matching
let matches = matchRoutes(buildRoutes, `/${path}/`.replace(/^\/\/+/, "/"));
if (!matches) {
continue;
}
// When prerendering a resource route, we don't want to pass along the
// `.data` file since we want to prerender the raw Response returned from
// the loader. Presumably this is for routes where a file extension is
// already included, such as `app/routes/items[.json].tsx` that will
// render into `/items.json`
let leafRoute = matches ? matches[matches.length - 1].route : null;
let manifestRoute = leafRoute ? build.routes[leafRoute.id]?.module : null;
let isResourceRoute =
manifestRoute && !manifestRoute.default && !manifestRoute.ErrorBoundary;

if (isResourceRoute) {
invariant(leafRoute);
invariant(manifestRoute);
if (manifestRoute.loader) {
// Prerender a .data file for turbo-stream consumption
await prerenderData(
handler,
path,
[leafRoute.id],
clientBuildDirectory,
reactRouterConfig,
viteConfig,
);
// Prerender a raw file for external consumption
await prerenderResourceRoute(
handler,
path,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
);
await pMap(
build.prerender,
async (path) => {
// Ensure we have a leading slash for matching
let matches = matchRoutes(
buildRoutes,
`/${path}/`.replace(/^\/\/+/, "/"),
);
if (!matches) {
return;
}
// When prerendering a resource route, we don't want to pass along the
// `.data` file since we want to prerender the raw Response returned from
// the loader. Presumably this is for routes where a file extension is
// already included, such as `app/routes/items[.json].tsx` that will
// render into `/items.json`
let leafRoute = matches ? matches[matches.length - 1].route : null;
let manifestRoute = leafRoute ? build.routes[leafRoute.id]?.module : null;
let isResourceRoute =
manifestRoute && !manifestRoute.default && !manifestRoute.ErrorBoundary;

if (isResourceRoute) {
invariant(leafRoute);
invariant(manifestRoute);
if (manifestRoute.loader) {
// Prerender a .data file for turbo-stream consumption
await prerenderData(
handler,
path,
[leafRoute.id],
clientBuildDirectory,
reactRouterConfig,
viteConfig,
);
// Prerender a raw file for external consumption
await prerenderResourceRoute(
handler,
path,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
);
} else {
viteConfig.logger.warn(
`⚠️ Skipping prerendering for resource route without a loader: ${leafRoute?.id}`,
);
}
} else {
viteConfig.logger.warn(
`⚠️ Skipping prerendering for resource route without a loader: ${leafRoute?.id}`,
let hasLoaders = matches.some(
(m) => build.assets.routes[m.route.id]?.hasLoader,
);
}
} else {
let hasLoaders = matches.some(
(m) => build.assets.routes[m.route.id]?.hasLoader,
);
let data: string | undefined;
if (!isResourceRoute && hasLoaders) {
data = await prerenderData(
let data: string | undefined;
if (!isResourceRoute && hasLoaders) {
data = await prerenderData(
handler,
path,
null,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
);
}

await prerenderRoute(
handler,
path,
null,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
data
? {
headers: {
"X-React-Router-Prerender-Data": encodeURI(data),
},
}
: undefined,
);
}

await prerenderRoute(
handler,
path,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
data
? {
headers: {
"X-React-Router-Prerender-Data": encodeURI(data),
},
}
: undefined,
);
}
}
},
{
concurrency:
typeof reactRouterConfig.prerender === "object" &&
"paths" in reactRouterConfig.prerender
? reactRouterConfig.prerender.unstable_concurrency || 1
: 1,
},
);
}

function getStaticPrerenderPaths(routes: DataRouteObject[]) {
Expand Down Expand Up @@ -2916,33 +2931,49 @@ export async function getPrerenderPaths(
routes: GenericRouteManifest,
logWarning = false,
): Promise<string[]> {
let prerenderPaths: string[] = [];
if (prerender != null && prerender !== false) {
let prerenderRoutes = createPrerenderRoutes(routes);
if (prerender === true) {
let { paths, paramRoutes } = getStaticPrerenderPaths(prerenderRoutes);
if (logWarning && !ssr && paramRoutes.length > 0) {
console.warn(
colors.yellow(
[
"⚠️ Paths with dynamic/splat params cannot be prerendered when " +
"using `prerender: true`. You may want to use the `prerender()` " +
"API to prerender the following paths:",
...paramRoutes.map((p) => " - " + p),
].join("\n"),
),
);
}
prerenderPaths = paths;
} else if (typeof prerender === "function") {
prerenderPaths = await prerender({
getStaticPaths: () => getStaticPrerenderPaths(prerenderRoutes).paths,
});
} else {
prerenderPaths = prerender || ["/"];
if (prerender == null || prerender === false) {
return [];
}

let pathsConfig: PrerenderPaths;

if (typeof prerender === "object" && "paths" in prerender) {
pathsConfig = prerender.paths;
} else {
pathsConfig = prerender;
}

if (pathsConfig === false) {
return [];
}

let prerenderRoutes = createPrerenderRoutes(routes);

if (pathsConfig === true) {
let { paths, paramRoutes } = getStaticPrerenderPaths(prerenderRoutes);
if (logWarning && !ssr && paramRoutes.length > 0) {
console.warn(
colors.yellow(
[
"⚠️ Paths with dynamic/splat params cannot be prerendered when " +
"using `prerender: true`. You may want to use the `prerender()` " +
"API to prerender the following paths:",
...paramRoutes.map((p) => " - " + p),
].join("\n"),
),
);
}
return paths;
}
return prerenderPaths;

if (typeof pathsConfig === "function") {
let paths = await pathsConfig({
getStaticPaths: () => getStaticPrerenderPaths(prerenderRoutes).paths,
});
return paths;
}

return pathsConfig;
}

// Note: Duplicated from react-router/lib/server-runtime
Expand Down
Loading