diff --git a/contributors.yml b/contributors.yml
index 98f300e6d0..58131406b9 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -115,6 +115,7 @@
- dokeet
- doytch
- Drishtantr
+- edmundhung
- edwin177
- eiffelwong1
- ek9
diff --git a/integration/vite-plugin-cloudflare-test.ts b/integration/vite-plugin-cloudflare-test.ts
index 3bacaeab7d..d6794c7d41 100644
--- a/integration/vite-plugin-cloudflare-test.ts
+++ b/integration/vite-plugin-cloudflare-test.ts
@@ -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;
@@ -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);
+ });
});
diff --git a/packages/react-router-dev/cli/commands.ts b/packages/react-router-dev/cli/commands.ts
index 3e4d615932..45aa60240d 100644
--- a/packages/react-router-dev/cli/commands.ts
+++ b/packages/react-router-dev/cli/commands.ts
@@ -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",
@@ -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";
diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts
index 0f54b23466..40b9174238 100644
--- a/packages/react-router-dev/config/config.ts
+++ b/packages/react-router-dev/config/config.ts
@@ -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",
@@ -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
diff --git a/packages/react-router-dev/config/defaults/entry.server.node.tsx b/packages/react-router-dev/config/defaults/entry.server.node.tsx
index 657d19c76f..93c6099f8a 100644
--- a/packages/react-router-dev/config/defaults/entry.server.node.tsx
+++ b/packages/react-router-dev/config/defaults/entry.server.node.tsx
@@ -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.`,
+ );
+}
export const streamTimeout = 5_000;
@@ -44,7 +53,7 @@ export default function handleRequest(
streamTimeout + 1000,
);
- const { pipe, abort } = renderToPipeableStream(
+ const { pipe, abort } = ReactDOMServer.renderToPipeableStream(
,
{
[readyOption]() {
diff --git a/integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx b/packages/react-router-dev/config/defaults/entry.server.web.tsx
similarity index 55%
rename from integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx
rename to packages/react-router-dev/config/defaults/entry.server.web.tsx
index 113e32c94e..0c873ccc0c 100644
--- a/integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx
+++ b/packages/react-router-dev/config/defaults/entry.server.web.tsx
@@ -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.`,
+ );
+}
+
+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(
,
{
+ 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) {