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
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
- dokeet
- doytch
- Drishtantr
- edmundhung
- edwin177
- eiffelwong1
- ek9
Expand Down
19 changes: 18 additions & 1 deletion integration/vite-plugin-cloudflare-test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { expect } from "@playwright/test";
import dedent from "dedent";

import { type Files, test, viteConfig } from "./helpers/vite.js";
import {
type Files,
test,
viteConfig,
createProject,
build,
} from "./helpers/vite.js";

const tsx = dedent;
const css = dedent;
Expand Down Expand Up @@ -121,4 +127,15 @@ test.describe("vite-plugin-cloudflare", () => {
"20px",
);
});

test("builds project with default server entry", async () => {
const files = defineFiles();
const cwd = await createProject(
await files({ port: 0 }),
"vite-plugin-cloudflare-template",
);
const buildResult = build({ cwd });

expect(buildResult.status).toBe(0);
});
});
9 changes: 3 additions & 6 deletions packages/react-router-dev/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,6 @@ export async function generateEntry(
let pkgJson = await readPackageJSON(rootDirectory);
let deps = pkgJson.dependencies ?? {};

if (!deps["@react-router/node"]) {
console.error(colors.red(`No default server entry detected.`));
return;
}

let defaultsDirectory = path.resolve(
path.dirname(require.resolve("@react-router/dev/package.json")),
"dist",
Expand All @@ -157,7 +152,9 @@ export async function generateEntry(

let defaultEntryServer = path.resolve(
defaultsDirectory,
`entry.server.node.tsx`,
deps["@react-router/node"]
? `entry.server.node.tsx`
: `entry.server.web.tsx`,
);

let isServerEntry = entry === "entry.server";
Expand Down
10 changes: 3 additions & 7 deletions packages/react-router-dev/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -994,12 +994,6 @@ export async function resolveEntryFiles({
let pkgJson = await readPackageJSON(packageJsonDirectory);
let deps = pkgJson.dependencies ?? {};

if (!deps["@react-router/node"]) {
throw new Error(
`Could not determine server runtime. Please install @react-router/node, or provide a custom entry.server.tsx/jsx file in your app directory.`,
);
}

if (!deps["isbot"]) {
console.log(
"adding `isbot@5` to your package.json, you should commit this change",
Expand All @@ -1019,7 +1013,9 @@ export async function resolveEntryFiles({
});
}

entryServerFile = `entry.server.node.tsx`;
entryServerFile = deps["@react-router/node"]
? `entry.server.node.tsx`
: `entry.server.web.tsx`;
}

let entryClientFilePath = userEntryClientFile
Expand Down
13 changes: 11 additions & 2 deletions packages/react-router-dev/config/defaults/entry.server.node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ import { createReadableStreamFromReadable } from "@react-router/node";
import { ServerRouter } from "react-router";
import { isbot } from "isbot";
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import * as ReactDOMServer from "react-dom/server";

// ReactDOMServer.renderToPipeableStream is only available in Node.js
if (typeof ReactDOMServer.renderToPipeableStream !== "function") {
throw new Error(
`Running the Node.js server entry on a non-Node runtime. ` +
`React Router uses this when @react-router/node is listed in your dependencies. ` +
`Remove it, or provide a custom entry.server.tsx/jsx file in your app directory.`,
);
}
Comment on lines +10 to +17
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helps users migrating from Node.js to non-Node runtimes. If they forget to remove @react-router/node, React Router selects this entry but renderToPipeableStream won't be available.

The check provides a clear error message instead of a cryptic failure.


export const streamTimeout = 5_000;

Expand Down Expand Up @@ -44,7 +53,7 @@ export default function handleRequest(
streamTimeout + 1000,
);

const { pipe, abort } = renderToPipeableStream(
const { pipe, abort } = ReactDOMServer.renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
[readyOption]() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import type { AppLoadContext, EntryContext } from "react-router";
import { ServerRouter } from "react-router";
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";
import * as ReactDOMServer from "react-dom/server";

// ReactDOMServer.renderToReadableStream is not available in Node.js until React 19.2.0+
if (typeof ReactDOMServer.renderToReadableStream !== "function") {
throw new Error(
`ReactDOMServer.renderToReadableStream() is not available. ` +
`React Router uses this API when @react-router/node is not installed. ` +
`Please install @react-router/node, or provide a custom entry.server.tsx/jsx file in your app directory.`,
);
}
Comment on lines +6 to +13
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check is still needed because renderToReadableStream is only available in Node.js from React 19.2.0+.

Without this check, users on React 18.x or React 19.0.0-19.1.x running on Node.js would get a cryptic error because renderToReadableStream is undefined.


export const streamTimeout = 5_000;

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
_loadContext: AppLoadContext,
loadContext: AppLoadContext,
// If you have middleware enabled:
// loadContext: RouterContextProvider
) {
// https://httpwg.org/specs/rfc9110.html#HEAD
if (request.method.toUpperCase() === "HEAD") {
return new Response(null, {
status: responseStatusCode,
headers: responseHeaders,
});
}

let shellRendered = false;
const userAgent = request.headers.get("user-agent");
let userAgent = request.headers.get("user-agent");

const body = await renderToReadableStream(
const body = await ReactDOMServer.renderToReadableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
signal: AbortSignal.timeout(streamTimeout + 1000),
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
Expand Down