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) {