diff --git a/.changeset/sour-crews-destroy.md b/.changeset/sour-crews-destroy.md new file mode 100644 index 0000000000..4d14c2945a --- /dev/null +++ b/.changeset/sour-crews-destroy.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Switch internal vite plugin Response logic to use `@remix-run/node-fetch-server` diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 3d6e09934b..37703a96eb 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -68,7 +68,6 @@ } }, "dependencies": { - "isbot": "^5.1.11", "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", @@ -78,12 +77,14 @@ "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "workspace:*", + "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", + "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "pathe": "^1.1.2", @@ -91,7 +92,6 @@ "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", - "set-cookie-parser": "^2.6.0", "tinyglobby": "^0.2.14", "valibot": "^0.41.0", "vite-node": "^3.2.2" @@ -107,7 +107,6 @@ "@types/lodash": "^4.14.182", "@types/node": "^20.0.0", "@types/npmcli__package-json": "^4.0.0", - "@types/set-cookie-parser": "^2.4.1", "@types/semver": "^7.7.0", "@vitejs/plugin-rsc": "0.4.30", "esbuild-register": "^3.6.0", diff --git a/packages/react-router-dev/vite/cloudflare-dev-proxy.ts b/packages/react-router-dev/vite/cloudflare-dev-proxy.ts index 8f36a829c3..243a093d22 100644 --- a/packages/react-router-dev/vite/cloudflare-dev-proxy.ts +++ b/packages/react-router-dev/vite/cloudflare-dev-proxy.ts @@ -1,3 +1,4 @@ +import { sendResponse } from "@remix-run/node-fetch-server"; import { createRequestHandler } from "react-router"; import { type AppLoadContext, @@ -8,7 +9,7 @@ import { import { type Plugin } from "vite"; import { type GetPlatformProxyOptions, type PlatformProxy } from "wrangler"; -import { fromNodeRequest, toNodeRequest } from "./node-adapter"; +import { fromNodeRequest } from "./node-adapter"; import { preloadVite } from "./vite"; import { type ResolvedReactRouterConfig, loadConfig } from "../config/config"; @@ -144,7 +145,7 @@ export const cloudflareDevProxyVitePlugin = ( ? await getLoadContext({ request: req, context }) : context; let res = await handler(req, loadContext); - await toNodeRequest(res, nodeRes); + await sendResponse(nodeRes, res); } catch (error) { next(error); } diff --git a/packages/react-router-dev/vite/node-adapter.ts b/packages/react-router-dev/vite/node-adapter.ts index 26e3147d51..5a71c9c212 100644 --- a/packages/react-router-dev/vite/node-adapter.ts +++ b/packages/react-router-dev/vite/node-adapter.ts @@ -1,9 +1,6 @@ -import { once } from "node:events"; -import type { IncomingMessage, ServerResponse } from "node:http"; -import { TLSSocket } from "node:tls"; -import { Readable } from "node:stream"; -import { splitCookiesString } from "set-cookie-parser"; -import { createReadableStreamFromReadable } from "@react-router/node"; +import type { ServerResponse } from "node:http"; + +import { createRequest } from "@remix-run/node-fetch-server"; import type * as Vite from "vite"; import invariant from "../invariant"; @@ -13,110 +10,16 @@ export type NodeRequestHandler = ( res: ServerResponse, ) => Promise; -function fromNodeHeaders(nodeReq: IncomingMessage): Headers { - let nodeHeaders = nodeReq.headers; - - if (nodeReq.httpVersionMajor >= 2) { - nodeHeaders = { ...nodeHeaders }; - if (nodeHeaders[":authority"]) { - nodeHeaders.host = nodeHeaders[":authority"] as string; - } - delete nodeHeaders[":authority"]; - delete nodeHeaders[":method"]; - delete nodeHeaders[":path"]; - delete nodeHeaders[":scheme"]; - } - - let headers = new Headers(); - - for (let [key, values] of Object.entries(nodeHeaders)) { - if (values) { - if (Array.isArray(values)) { - for (let value of values) { - headers.append(key, value); - } - } else { - headers.set(key, values); - } - } - } - - return headers; -} - -// Based on `createRemixRequest` in packages/react-router-express/server.ts export function fromNodeRequest( nodeReq: Vite.Connect.IncomingMessage, nodeRes: ServerResponse, ): Request { - let protocol = - nodeReq.socket instanceof TLSSocket && nodeReq.socket.encrypted - ? "https" - : "http"; - let origin = - nodeReq.headers.origin && "null" !== nodeReq.headers.origin - ? nodeReq.headers.origin - : `${protocol}://${nodeReq.headers.host}`; // Use `req.originalUrl` so React Router is aware of the full path invariant( nodeReq.originalUrl, "Expected `nodeReq.originalUrl` to be defined", ); - let url = new URL(nodeReq.originalUrl, origin); - - // Abort action/loaders once we can no longer write a response - let controller: AbortController | null = new AbortController(); - let init: RequestInit = { - method: nodeReq.method, - headers: fromNodeHeaders(nodeReq), - signal: controller.signal, - }; - - // Abort action/loaders once we can no longer write a response iff we have - // not yet sent a response (i.e., `close` without `finish`) - // `finish` -> done rendering the response - // `close` -> response can no longer be written to - nodeRes.on("finish", () => (controller = null)); - nodeRes.on("close", () => controller?.abort()); - - if (nodeReq.method !== "GET" && nodeReq.method !== "HEAD") { - init.body = createReadableStreamFromReadable(nodeReq); - (init as { duplex: "half" }).duplex = "half"; - } - - return new Request(url.href, init); -} - -// Adapted from solid-start's `handleNodeResponse`: -// https://github.com/solidjs/solid-start/blob/7398163869b489cce503c167e284891cf51a6613/packages/start/node/fetch.js#L162-L185 -export async function toNodeRequest(res: Response, nodeRes: ServerResponse) { - nodeRes.statusCode = res.status; - - // HTTP/2 doesn't support status messages - // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.4 - if (!nodeRes.req || nodeRes.req.httpVersionMajor < 2) { - nodeRes.statusMessage = res.statusText; - } - - let cookiesStrings = []; - - for (let [name, value] of res.headers) { - if (name === "set-cookie") { - cookiesStrings.push(...splitCookiesString(value)); - } else nodeRes.setHeader(name, value); - } - - if (cookiesStrings.length) { - nodeRes.setHeader("set-cookie", cookiesStrings); - } + nodeReq.url = nodeReq.originalUrl; - if (res.body) { - // https://github.com/microsoft/TypeScript/issues/29867 - let responseBody = res.body as unknown as AsyncIterable; - let readable = Readable.from(responseBody); - readable.pipe(nodeRes); - await once(readable, "end"); - } else { - nodeRes.end(); - } + return createRequest(nodeReq, nodeRes); } diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 4f86b5b3f3..d409f808c0 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -15,6 +15,7 @@ import { import * as path from "node:path"; import * as url from "node:url"; import * as babel from "@babel/core"; +import { sendResponse } from "@remix-run/node-fetch-server"; import { unstable_setDevServerHooks as setDevServerHooks, createRequestHandler, @@ -46,7 +47,7 @@ import invariant from "../invariant"; import type { Cache } from "./cache"; import { generate, parse } from "./babel"; import type { NodeRequestHandler } from "./node-adapter"; -import { fromNodeRequest, toNodeRequest } from "./node-adapter"; +import { fromNodeRequest } from "./node-adapter"; import { getCssStringFromViteDevModuleCode, getStylesForPathname, @@ -1602,7 +1603,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { req, await reactRouterDevLoadContext(req), ); - await toNodeRequest(res, nodeRes); + await sendResponse(nodeRes, res); }; await nodeHandler(req, res); } catch (error) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7e40f5b85..182ceea34b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,7 +113,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 6.1.0-canary-67a44bcd-20250915(eslint@8.57.0) + version: 6.1.0-canary-128abcfa-20250917(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -598,7 +598,7 @@ importers: version: 3.0.1(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0)) + version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)) integration/helpers/vite-6-template: dependencies: @@ -1068,6 +1068,9 @@ importers: '@react-router/node': specifier: workspace:* version: link:../react-router-node + '@remix-run/node-fetch-server': + specifier: ^0.9.0 + version: 0.9.0 arg: specifier: ^5.0.1 version: 5.0.2 @@ -1110,9 +1113,6 @@ importers: semver: specifier: ^7.3.7 version: 7.7.2 - set-cookie-parser: - specifier: ^2.6.0 - version: 2.6.0 tinyglobby: specifier: ^0.2.14 version: 0.2.14 @@ -1156,15 +1156,12 @@ importers: '@types/semver': specifier: ^7.7.0 version: 7.7.0 - '@types/set-cookie-parser': - specifier: ^2.4.1 - version: 2.4.7 '@vitejs/plugin-rsc': specifier: 0.4.30 version: 0.4.30(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) esbuild-register: specifier: ^3.6.0 - version: 3.6.0(esbuild@0.25.4) + version: 3.6.0(esbuild@0.25.0) execa: specifier: 5.1.1 version: 5.1.1 @@ -1576,7 +1573,7 @@ importers: version: 5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0)) + version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)) playground/framework-vite-7-beta: dependencies: @@ -4298,8 +4295,11 @@ packages: '@remix-run/changelog-github@0.0.5': resolution: {integrity: sha512-43tqwUqWqirbv6D9uzo55ASPsCJ61Ein1k/M8qn+Qpros0MmbmuzjLVPmtaxfxfe2ANX0LefLvCD0pAgr1tp4g==} - '@remix-run/node-fetch-server@0.8.0': - resolution: {integrity: sha512-8/sKegb4HrM6IdcQeU0KPhj9VOHm5SUqswJDHuMCS3mwbr/NRx078QDbySmn0xslahvvZoOENd7EnK40kWKxkg==} + '@remix-run/node-fetch-server@0.8.1': + resolution: {integrity: sha512-J1dev372wtJqmqn9U/qbpbZxbJSQrogNN2+Qv1lKlpATpe/WQ9aCZfl/xSb9d2Rgh1IyLSvNxZAXPZxruO6Xig==} + + '@remix-run/node-fetch-server@0.9.0': + resolution: {integrity: sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==} '@remix-run/web-blob@3.1.0': resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} @@ -6252,8 +6252,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - eslint-plugin-react-hooks@6.1.0-canary-67a44bcd-20250915: - resolution: {integrity: sha512-8qekpMzs36M7AOcq+AVqPpaWrpjRdEHUQBbcO/soxGXVHjHw2cW/A7FdAturo/7fyFEGekZBXwNIDhUdsjlTzg==} + eslint-plugin-react-hooks@6.1.0-canary-128abcfa-20250917: + resolution: {integrity: sha512-cR/EftrsVDqCbmfq6IEsLaPqMhkLFgoiJvnSF6nArECbchE8ZQJyGQv7sXGwsf1sKYXr7N9vaB45iDmZAx4Ecw==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -12691,7 +12691,9 @@ snapshots: transitivePeerDependencies: - encoding - '@remix-run/node-fetch-server@0.8.0': {} + '@remix-run/node-fetch-server@0.8.1': {} + + '@remix-run/node-fetch-server@0.9.0': {} '@remix-run/web-blob@3.1.0': dependencies: @@ -13649,7 +13651,7 @@ snapshots: '@vitejs/plugin-rsc@0.4.30(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))': dependencies: - '@remix-run/node-fetch-server': 0.8.0 + '@remix-run/node-fetch-server': 0.8.1 es-module-lexer: 1.7.0 estree-walker: 3.0.3 magic-string: 0.30.18 @@ -13662,7 +13664,7 @@ snapshots: '@vitejs/plugin-rsc@0.4.30(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))': dependencies: - '@remix-run/node-fetch-server': 0.8.0 + '@remix-run/node-fetch-server': 0.8.1 es-module-lexer: 1.7.0 estree-walker: 3.0.3 magic-string: 0.30.18 @@ -14782,13 +14784,6 @@ snapshots: transitivePeerDependencies: - supports-color - esbuild-register@3.6.0(esbuild@0.25.4): - dependencies: - debug: 4.4.1 - esbuild: 0.25.4 - transitivePeerDependencies: - - supports-color - esbuild@0.19.12: optionalDependencies: '@esbuild/aix-ppc64': 0.19.12 @@ -15070,7 +15065,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@6.1.0-canary-67a44bcd-20250915(eslint@8.57.0): + eslint-plugin-react-hooks@6.1.0-canary-128abcfa-20250917(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7 @@ -19367,7 +19362,7 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0)): + vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)): dependencies: debug: 4.4.1 globrex: 0.1.2