From 0d743c19f0fce4313fcc10134fd750c3cee30064 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 29 Jan 2026 20:59:59 +0000 Subject: [PATCH 1/5] feat: add default server entry for non-Node runtimes --- integration/vite-plugin-cloudflare-test.ts | 19 +++++++++++- packages/react-router-dev/cli/commands.ts | 9 ++---- packages/react-router-dev/config/config.ts | 10 ++---- .../config/defaults/entry.server.web.tsx | 31 ++++++++++++++++--- 4 files changed, 51 insertions(+), 18 deletions(-) rename integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx => packages/react-router-dev/config/defaults/entry.server.web.tsx (58%) diff --git a/integration/vite-plugin-cloudflare-test.ts b/integration/vite-plugin-cloudflare-test.ts index 3bacaeab7d..6aca7d8a47 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 web 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/integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx b/packages/react-router-dev/config/defaults/entry.server.web.tsx similarity index 58% 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..e9e914d18d 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,47 @@ 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.renderToPipeableStream is only available in Node.js +if (typeof ReactDOMServer.renderToPipeableStream === "function") { + throw new Error( + `The default server entry for non-Node runtimes is being used on Node.js. ` + + `Please install @react-router/node, or provide a custom entry.server.tsx/jsx file in your app directory.`, + ); +} + +const { renderToReadableStream } = ReactDOMServer; + +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( , { + 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) { From a599bddb90a314ae6b8fc33ba744b5073f908bd0 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 29 Jan 2026 23:47:50 +0000 Subject: [PATCH 2/5] remove node runtime check from default server entry --- .../config/defaults/entry.server.web.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/react-router-dev/config/defaults/entry.server.web.tsx b/packages/react-router-dev/config/defaults/entry.server.web.tsx index e9e914d18d..c351f57c63 100644 --- a/packages/react-router-dev/config/defaults/entry.server.web.tsx +++ b/packages/react-router-dev/config/defaults/entry.server.web.tsx @@ -1,17 +1,7 @@ import type { AppLoadContext, EntryContext } from "react-router"; import { ServerRouter } from "react-router"; import { isbot } from "isbot"; -import * as ReactDOMServer from "react-dom/server"; - -// ReactDOMServer.renderToPipeableStream is only available in Node.js -if (typeof ReactDOMServer.renderToPipeableStream === "function") { - throw new Error( - `The default server entry for non-Node runtimes is being used on Node.js. ` + - `Please install @react-router/node, or provide a custom entry.server.tsx/jsx file in your app directory.`, - ); -} - -const { renderToReadableStream } = ReactDOMServer; +import { renderToReadableStream } from "react-dom/server"; export const streamTimeout = 5_000; From fb934d2284bcf4bcba14a1a49f334c68e12bf160 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Fri, 30 Jan 2026 00:03:46 +0000 Subject: [PATCH 3/5] update contributors.yml --- contributors.yml | 1 + 1 file changed, 1 insertion(+) 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 From 188d4a3ed77c56705bf89dc3f3c58ee0e6347baf Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Fri, 30 Jan 2026 12:45:01 +0000 Subject: [PATCH 4/5] improve runtime detection in default server entries --- .../config/defaults/entry.server.node.tsx | 13 +++++++++++-- .../config/defaults/entry.server.web.tsx | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) 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/packages/react-router-dev/config/defaults/entry.server.web.tsx b/packages/react-router-dev/config/defaults/entry.server.web.tsx index c351f57c63..0c873ccc0c 100644 --- a/packages/react-router-dev/config/defaults/entry.server.web.tsx +++ b/packages/react-router-dev/config/defaults/entry.server.web.tsx @@ -1,7 +1,16 @@ 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; @@ -25,7 +34,7 @@ export default async function handleRequest( let shellRendered = false; let userAgent = request.headers.get("user-agent"); - const body = await renderToReadableStream( + const body = await ReactDOMServer.renderToReadableStream( , { signal: AbortSignal.timeout(streamTimeout + 1000), From 4af667de9f18e048aefb72f4d54b0bb191377a5c Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Fri, 30 Jan 2026 13:16:37 +0000 Subject: [PATCH 5/5] improve the name of the added test --- integration/vite-plugin-cloudflare-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/vite-plugin-cloudflare-test.ts b/integration/vite-plugin-cloudflare-test.ts index 6aca7d8a47..d6794c7d41 100644 --- a/integration/vite-plugin-cloudflare-test.ts +++ b/integration/vite-plugin-cloudflare-test.ts @@ -128,7 +128,7 @@ test.describe("vite-plugin-cloudflare", () => { ); }); - test("builds project with default web entry", async () => { + test("builds project with default server entry", async () => { const files = defineFiles(); const cwd = await createProject( await files({ port: 0 }),