Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip action-only resource routes with prerender:true #13004

Merged
merged 2 commits into from
Feb 12, 2025
Merged
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/late-nails-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Skip action-only resource routes when using `prerender:true`
5 changes: 5 additions & 0 deletions .changeset/rotten-numbers-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Limit prerendered resource route `.data` files to only the target route
42 changes: 36 additions & 6 deletions integration/vite-prerender-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,42 @@ test.describe("Prerendering", () => {
expect(html).toMatch('<p data-loader-data="true">About Loader Data</p>');
});

test("Skips action-only resource routes prerender:true", async () => {
let buildStdio = new PassThrough();
fixture = await createFixture({
buildStdio,
files: {
"react-router.config.ts": reactRouterConfig({
prerender: true,
}),
"vite.config.ts": files["vite.config.ts"],
"app/root.tsx": files["app/root.tsx"],
"app/routes/_index.tsx": files["app/routes/_index.tsx"],
"app/routes/action.tsx": js`
export function action() {
return null
}
`,
},
});

let buildOutput: string;
let chunks: Buffer[] = [];
buildOutput = await new Promise<string>((resolve, reject) => {
buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
buildStdio.on("error", (err) => reject(err));
buildStdio.on("end", () =>
resolve(Buffer.concat(chunks).toString("utf8"))
);
});

expect(buildOutput).toContain(
"⚠️ Skipping prerendering for resource route without a loader: routes/action"
);
// Only logs once
expect(buildOutput.match(/routes\/action/g)?.length).toBe(1);
});

test("Pre-renders resource routes with file extensions", async () => {
fixture = await createFixture({
prerender: true,
Expand Down Expand Up @@ -385,9 +421,6 @@ test.describe("Prerendering", () => {

let dataRes = await fixture.requestSingleFetchData("/json.json.data");
expect(dataRes.data).toEqual({
root: {
data: null,
},
"routes/json[.json]": {
data: {
hello: "world",
Expand All @@ -400,9 +433,6 @@ test.describe("Prerendering", () => {

dataRes = await fixture.requestSingleFetchData("/text.txt.data");
expect(dataRes.data).toEqual({
root: {
data: null,
},
"routes/text[.txt]": {
data: "Hello, world",
},
Expand Down
78 changes: 50 additions & 28 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2317,21 +2317,6 @@ async function handlePrerender(
matches,
`Unable to prerender path because it does not match any routes: ${path}`
);
let hasLoaders = matches.some(
(m) => build.assets.routes[m.route.id]?.hasLoader
);
let data: string | undefined;
if (hasLoaders) {
data = await prerenderData(
handler,
path,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
{ headers }
);
}

// 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
Expand All @@ -2340,21 +2325,53 @@ async function handlePrerender(
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 &&
manifestRoute.loader;
manifestRoute && !manifestRoute.default && !manifestRoute.ErrorBoundary;

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

await prerenderRoute(
handler,
path,
Expand Down Expand Up @@ -2408,6 +2425,7 @@ function getStaticPrerenderPaths(routes: DataRouteObject[]) {
async function prerenderData(
handler: RequestHandler,
prerenderPath: string,
onlyRoutes: string[] | null,
clientBuildDirectory: string,
reactRouterConfig: ResolvedReactRouterConfig,
viteConfig: Vite.ResolvedConfig,
Expand All @@ -2418,7 +2436,11 @@ async function prerenderData(
? "/_root.data"
: `${prerenderPath.replace(/\/$/, "")}.data`
}`.replace(/\/\/+/g, "/");
let request = new Request(`http://localhost${normalizedPath}`, requestInit);
let url = new URL(`http://localhost${normalizedPath}`);
if (onlyRoutes?.length) {
url.searchParams.set("_routes", onlyRoutes.join(","));
}
let request = new Request(url, requestInit);
let response = await handler(request);
let data = await response.text();

Expand Down