From 04f06f070583b76252b361baf8def410d5cab308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Thu, 3 Jul 2025 02:57:04 +0200 Subject: [PATCH] refactor(dev/vite): use `@remix-run/node-fetch-server` --- packages/react-router-dev/package.json | 3 +- .../vite/cloudflare-dev-proxy.ts | 5 +- .../react-router-dev/vite/node-adapter.ts | 107 +----------------- packages/react-router-dev/vite/plugin.ts | 5 +- pnpm-lock.yaml | 27 ++--- 5 files changed, 24 insertions(+), 123 deletions(-) diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index a5207b72db..5a5ffa4b6c 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -78,6 +78,7 @@ "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "workspace:*", + "@remix-run/node-fetch-server": "^0.8.1", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", @@ -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.26", "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 c426afd7ca..6208f543a2 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-b1b0955f-20250901(eslint@8.57.0) + version: 6.1.0-canary-f3a80361-20250911(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -1065,6 +1065,9 @@ importers: '@react-router/node': specifier: workspace:* version: link:../react-router-node + '@remix-run/node-fetch-server': + specifier: ^0.8.1 + version: 0.8.1 arg: specifier: ^5.0.1 version: 5.0.2 @@ -1107,9 +1110,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 @@ -1153,9 +1153,6 @@ 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.26 version: 0.4.26(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)) @@ -4289,8 +4286,8 @@ 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/web-blob@3.1.0': resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} @@ -6243,8 +6240,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-b1b0955f-20250901: - resolution: {integrity: sha512-T6+EUTSU6FeRntFJ0tGyFWuHnkAWgBlTFPzPMu8B+ny6zUEWrApVfMdtBjCjszAN775OzBM1jCKr29Z/SbwGRg==} + eslint-plugin-react-hooks@6.1.0-canary-f3a80361-20250911: + resolution: {integrity: sha512-QctnrvG6O5Ha5qfm4nq0pAsaJrv58OHArsY9IpmtPdLg0R/yZpFs0arRnHfoLtpYef3yt/o7lOhESEPxhgLipg==} 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 @@ -12682,7 +12679,7 @@ snapshots: transitivePeerDependencies: - encoding - '@remix-run/node-fetch-server@0.8.0': {} + '@remix-run/node-fetch-server@0.8.1': {} '@remix-run/web-blob@3.1.0': dependencies: @@ -13640,7 +13637,7 @@ snapshots: '@vitejs/plugin-rsc@0.4.26(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 @@ -13653,7 +13650,7 @@ snapshots: '@vitejs/plugin-rsc@0.4.26(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 @@ -15061,7 +15058,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@6.1.0-canary-b1b0955f-20250901(eslint@8.57.0): + eslint-plugin-react-hooks@6.1.0-canary-f3a80361-20250911(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7