From 6f7db38f018add1dc0e4c94b1038d9cf12b0b6f7 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 4 Mar 2026 22:58:53 -0800 Subject: [PATCH 1/2] fix(web): move react-router deps to devDependencies Add a custom entry.server.tsx (based on the default react-router template) so that the @react-router/dev build plugin no longer requires @react-router/node and isbot to be in the dependencies field of package.json. This allows all react-router related packages to be devDependencies since they are fully bundled at build time, leaving express as the sole production dependency. --- .changeset/fix-web-missing-peer-deps.md | 5 ++ packages/web/app/entry.server.tsx | 85 +++++++++++++++++++++++++ packages/web/package.json | 4 +- pnpm-lock.yaml | 12 ++-- 4 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-web-missing-peer-deps.md create mode 100644 packages/web/app/entry.server.tsx diff --git a/.changeset/fix-web-missing-peer-deps.md b/.changeset/fix-web-missing-peer-deps.md new file mode 100644 index 0000000000..d1d1c94c8b --- /dev/null +++ b/.changeset/fix-web-missing-peer-deps.md @@ -0,0 +1,5 @@ +--- +"@workflow/web": patch +--- + +Add custom `entry.server.tsx` and move `@react-router/node`, `isbot`, `react-router`, and `@react-router/express` to devDependencies since the build process bundles them entirely at build time diff --git a/packages/web/app/entry.server.tsx b/packages/web/app/entry.server.tsx new file mode 100644 index 0000000000..91c8aff385 --- /dev/null +++ b/packages/web/app/entry.server.tsx @@ -0,0 +1,85 @@ +import { PassThrough } from 'node:stream'; +import { createReadableStreamFromReadable } from '@react-router/node'; +import { isbot } from 'isbot'; +import type { RenderToPipeableStreamOptions } from 'react-dom/server'; +import { renderToPipeableStream } from 'react-dom/server'; +import type { AppLoadContext, EntryContext } from 'react-router'; +import { ServerRouter } from 'react-router'; + +export const streamTimeout = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + loadContext: AppLoadContext +) { + // https://httpwg.org/specs/rfc9110.html#HEAD + if (request.method.toUpperCase() === 'HEAD') { + return new Response(null, { + status: responseStatusCode, + headers: responseHeaders, + }); + } + + return new Promise((resolve, reject) => { + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + const readyOption: keyof RenderToPipeableStreamOptions = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode + ? 'onAllReady' + : 'onShellReady'; + + // Abort the rendering stream after the `streamTimeout` so it has time to + // flush down the rejected boundaries + let timeoutId: ReturnType | undefined = setTimeout( + () => abort(), + streamTimeout + 1000 + ); + + const { pipe, abort } = renderToPipeableStream( + , + { + [readyOption]() { + shellRendered = true; + const body = new PassThrough({ + final(callback) { + // Clear the timeout to prevent retaining the closure and memory leak + clearTimeout(timeoutId); + timeoutId = undefined; + callback(); + }, + }); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set('Content-Type', 'text/html'); + + pipe(body); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // 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) { + console.error(error); + } + }, + } + ); + }); +} diff --git a/packages/web/package.json b/packages/web/package.json index 331e9d7a87..90b2d74805 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -36,9 +36,7 @@ "test": "vitest run" }, "dependencies": { - "@react-router/node": "7.13.1", - "express": "^4.21.0", - "isbot": "^5" + "express": "^4.21.0" }, "devDependencies": { "@testing-library/react": "^16.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43b88b7653..b7a32c3d14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -955,15 +955,9 @@ importers: packages/web: dependencies: - '@react-router/node': - specifier: 7.13.1 - version: 7.13.1(react-router@7.13.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3) express: specifier: ^4.21.0 version: 4.22.1 - isbot: - specifier: ^5 - version: 5.1.35 devDependencies: '@biomejs/biome': specifier: 'catalog:' @@ -1004,6 +998,9 @@ importers: '@react-router/express': specifier: 7.13.1 version: 7.13.1(express@4.22.1)(react-router@7.13.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3) + '@react-router/node': + specifier: 7.13.1 + version: 7.13.1(react-router@7.13.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.9.3) '@tailwindcss/vite': specifier: '4' version: 4.1.18(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -1061,6 +1058,9 @@ importers: geist: specifier: ^1.7.0 version: 1.7.0(next@16.2.0-canary.65(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + isbot: + specifier: ^5 + version: 5.1.35 jsdom: specifier: ^26 version: 26.1.0 From de1c8b862c21ca3fa1a1d95dcb5aa84c133a2824 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 10 Mar 2026 18:32:08 -0700 Subject: [PATCH 2/2] Update packages/web/app/entry.server.tsx Co-authored-by: Peter Wielander Signed-off-by: Nathan Rajlich --- packages/web/app/entry.server.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/web/app/entry.server.tsx b/packages/web/app/entry.server.tsx index 91c8aff385..1dacf6b1bb 100644 --- a/packages/web/app/entry.server.tsx +++ b/packages/web/app/entry.server.tsx @@ -8,6 +8,9 @@ import { ServerRouter } from 'react-router'; export const streamTimeout = 5_000; +/** + * This custom base request handler is based on the default react-router template, modified so that the `@react-router/dev` build plugin no longer requires `@react-router/node` and `isbot` in the dependencies field of `package.json`, allowing us to extract them as peer dependencies. + */ export default function handleRequest( request: Request, responseStatusCode: number,