Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/stale-bats-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-router/dev": patch
"react-router": patch
---

Adjust approach for Prerendering/SPA Mode via headers
22 changes: 17 additions & 5 deletions integration/vite-prerender-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ test.describe("Prerendering", () => {
"app/routes/about.tsx": js`
import { useLoaderData } from 'react-router';
export function loader({ request }) {
return "ABOUT-" + request.headers.has('X-React-Router-Prerender');
return "ABOUT-" + Boolean(process.env.IS_RR_BUILD_REQUEST);
}

export default function Comp() {
Expand All @@ -613,7 +613,7 @@ test.describe("Prerendering", () => {
"app/routes/not-prerendered.tsx": js`
import { useLoaderData } from 'react-router';
export function loader({ request }) {
return "NOT-PRERENDERED-" + request.headers.has('X-React-Router-Prerender');
return "NOT-PRERENDERED-" + Boolean(process.env.IS_RR_BUILD_REQUEST);
}

export default function Comp() {
Expand Down Expand Up @@ -659,7 +659,7 @@ test.describe("Prerendering", () => {
import { useLoaderData } from 'react-router';
export function loader({ request }) {
return {
prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
// 24999 characters
data: new Array(5000).fill('test').join('-'),
};
Expand Down Expand Up @@ -712,7 +712,7 @@ test.describe("Prerendering", () => {
import { useLoaderData } from 'react-router';
export function loader({ request }) {
return {
prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
data: "한글 데이터 - UTF-8 문자",
};
}
Expand All @@ -732,7 +732,7 @@ test.describe("Prerendering", () => {
import { useLoaderData } from 'react-router';
export function loader({ request }) {
return {
prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
data: "非プリレンダリングデータ - UTF-8文字",
};
}
Expand Down Expand Up @@ -837,6 +837,18 @@ test.describe("Prerendering", () => {
await page.waitForSelector("[data-mounted]");
expect(await app.getHtml()).toMatch("Index: INDEX");
});

test("Ignores build-time headers at runtime", async () => {
fixture = await createFixture({ files });
let res = await fixture.requestSingleFetchData("/_root.data", {
headers: {
"X-React-Router-Prerender-Data": encodeURI(
'[{"_1":2},"routes/_index",{"_3":4},"data","Hello World!"]'
),
},
});
expect((res.data as any)["routes/_index"].data).toBe("Index Loader Data");
});
});

test.describe("ssr: false", () => {
Expand Down
31 changes: 31 additions & 0 deletions integration/vite-spa-mode-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,37 @@ test.describe("SPA Mode", () => {
expect(await res.text()).toMatch(/^<!DOCTYPE html><html lang="en">/);
});

test("Ignores build-time headers at runtime", async () => {
let fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({
splitRouteModules,
}),
"app/root.tsx": js`
import { Outlet, Scripts } from "react-router";

export default function Root() {
return (
<html lang="en">
<head></head>
<body>
<h1 data-root>Root</h1>
<Scripts />
</body>
</html>
);
}
`,
},
});
let res = await fixture.requestDocument("/", {
headers: { "X-React-Router-SPA-Mode": "yes" },
});
let html = await res.text();
expect(html).toMatch('"isSpaMode":false');
expect(html).toMatch('<h1 data-root="true">Root</h1>');
});

test("works when combined with a basename", async ({ page }) => {
fixture = await createFixture({
spaMode: true,
Expand Down
27 changes: 11 additions & 16 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
);
}

// Set an environment variable we can look for in the handler to
// enable some build-time-only logic
process.env.IS_RR_BUILD_REQUEST = "yes";

if (isPrerenderingEnabled(ctx.reactRouterConfig)) {
// If we have prerender routes, that takes precedence over SPA mode
// which is ssr:false and only the root route being rendered
Expand Down Expand Up @@ -2623,11 +2627,6 @@ async function handlePrerender(
}

let buildRoutes = createPrerenderRoutes(build.routes);
let headers = {
// Header that can be used in the loader to know if you're running at
// build time or runtime
"X-React-Router-Prerender": "yes",
};
for (let path of build.prerender) {
// Ensure we have a leading slash for matching
let matches = matchRoutes(buildRoutes, `/${path}/`.replace(/^\/\/+/, "/"));
Expand Down Expand Up @@ -2655,17 +2654,15 @@ async function handlePrerender(
[leafRoute.id],
clientBuildDirectory,
reactRouterConfig,
viteConfig,
{ headers }
viteConfig
);
// Prerender a raw file for external consumption
await prerenderResourceRoute(
handler,
path,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
{ headers }
viteConfig
);
} else {
viteConfig.logger.warn(
Expand All @@ -2684,8 +2681,7 @@ async function handlePrerender(
null,
clientBuildDirectory,
reactRouterConfig,
viteConfig,
{ headers }
viteConfig
);
}

Expand All @@ -2698,11 +2694,10 @@ async function handlePrerender(
data
? {
headers: {
...headers,
"X-React-Router-Prerender-Data": encodeURI(data),
},
}
: { headers }
: undefined
);
}
}
Expand Down Expand Up @@ -2746,7 +2741,7 @@ async function prerenderData(
clientBuildDirectory: string,
reactRouterConfig: ResolvedReactRouterConfig,
viteConfig: Vite.ResolvedConfig,
requestInit: RequestInit
requestInit?: RequestInit
) {
let normalizedPath = `${reactRouterConfig.basename}${
prerenderPath === "/"
Expand Down Expand Up @@ -2789,7 +2784,7 @@ async function prerenderRoute(
clientBuildDirectory: string,
reactRouterConfig: ResolvedReactRouterConfig,
viteConfig: Vite.ResolvedConfig,
requestInit: RequestInit
requestInit?: RequestInit
) {
let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(
/\/\/+/g,
Expand Down Expand Up @@ -2845,7 +2840,7 @@ async function prerenderResourceRoute(
clientBuildDirectory: string,
reactRouterConfig: ResolvedReactRouterConfig,
viteConfig: Vite.ResolvedConfig,
requestInit: RequestInit
requestInit?: RequestInit
) {
let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`
.replace(/\/\/+/g, "/")
Expand Down
12 changes: 12 additions & 0 deletions packages/react-router/lib/server-runtime/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,15 @@ export function getDevServerHooks(): DevServerHooks | undefined {
// @ts-expect-error
return globalThis[globalDevServerHooksKey];
}

// Guarded access to build-time-only headers
export function getBuildTimeHeader(request: Request, headerName: string) {
if (typeof process !== "undefined") {
try {
if (process.env?.IS_RR_BUILD_REQUEST === "yes") {
return request.headers.get(headerName);
}
} catch (e) {}
}
return null;
}
10 changes: 6 additions & 4 deletions packages/react-router/lib/server-runtime/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "../dom/ssr/single-fetch";
import invariant from "./invariant";
import type { ServerRouteModule } from "../dom/ssr/routeModules";
import { getBuildTimeHeader } from "./dev";

export type ServerRouteManifest = RouteManifest<Omit<ServerRoute, "children">>;

Expand Down Expand Up @@ -86,10 +87,11 @@ export function createStaticHandlerDataRoutes(
? async (args: RRLoaderFunctionArgs) => {
// If we're prerendering, use the data passed in from prerendering
// the .data route so we don't call loaders twice
if (args.request.headers.has("X-React-Router-Prerender-Data")) {
const preRenderedData = args.request.headers.get(
"X-React-Router-Prerender-Data"
);
let preRenderedData = getBuildTimeHeader(
args.request,
"X-React-Router-Prerender-Data"
);
if (preRenderedData != null) {
let encoded = preRenderedData
? decodeURI(preRenderedData)
: preRenderedData;
Expand Down
18 changes: 12 additions & 6 deletions packages/react-router/lib/server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { matchServerRoutes } from "./routeMatching";
import type { ServerRoute } from "./routes";
import { createStaticHandlerDataRoutes, createRoutes } from "./routes";
import { createServerHandoffString } from "./serverHandoff";
import { getDevServerHooks } from "./dev";
import { getBuildTimeHeader, getDevServerHooks } from "./dev";
import {
encodeViaTurboStream,
getSingleFetchRedirect,
Expand Down Expand Up @@ -164,12 +164,17 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
normalizedPath = normalizedPath.slice(0, -1);
}

let isSpaMode =
getBuildTimeHeader(request, "X-React-Router-SPA-Mode") === "yes";

// When runtime SSR is disabled, make our dev server behave like the deployed
// pre-rendered site would
if (!_build.ssr) {
// When SSR is disabled this, file can only ever run during dev because we
// delete the server build at the end of the build
if (_build.prerender.length === 0) {
// Add the header if we're in SPA mode
request.headers.set("X-React-Router-SPA-Mode", "yes");
// ssr:false and no prerender config indicates "SPA Mode"
isSpaMode = true;
} else if (
!_build.prerender.includes(normalizedPath) &&
!_build.prerender.includes(normalizedPath + "/")
Expand All @@ -194,7 +199,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
});
} else {
// Serve a SPA fallback for non-pre-rendered document requests
request.headers.set("X-React-Router-SPA-Mode", "yes");
isSpaMode = true;
}
}
}
Expand Down Expand Up @@ -275,7 +280,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
}
}
} else if (
!request.headers.has("X-React-Router-SPA-Mode") &&
!isSpaMode &&
matches &&
matches[matches.length - 1].route.module.default == null &&
matches[matches.length - 1].route.module.ErrorBoundary == null
Expand Down Expand Up @@ -309,6 +314,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
request,
loadContext,
handleError,
isSpaMode,
criticalCss
);
}
Expand Down Expand Up @@ -426,9 +432,9 @@ async function handleDocumentRequest(
request: Request,
loadContext: AppLoadContext | unstable_RouterContextProvider,
handleError: (err: unknown) => void,
isSpaMode: boolean,
criticalCss?: CriticalCss
) {
let isSpaMode = request.headers.has("X-React-Router-SPA-Mode");
try {
let response = await staticHandler.query(request, {
requestContext: loadContext,
Expand Down