diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index d1745d3ee5..ed5cb566d9 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -74,6 +74,7 @@ "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "workspace:*", + "@remix-run/node-fetch-server": "^0.8.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", @@ -87,7 +88,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" @@ -103,7 +103,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", "esbuild-register": "^3.6.0", "execa": "5.1.1", diff --git a/packages/react-router-dev/vite/cloudflare-dev-proxy.ts b/packages/react-router-dev/vite/cloudflare-dev-proxy.ts index e7a4f38d5d..739905ea47 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, getVite } 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 67b467eb65..82808fc673 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, @@ -47,7 +48,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, @@ -1688,7 +1689,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 3ddfb3f895..d67f9daf01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1056,6 +1056,9 @@ importers: '@react-router/node': specifier: workspace:* version: link:../react-router-node + '@remix-run/node-fetch-server': + specifier: ^0.8.0 + version: 0.8.0 arg: specifier: ^5.0.1 version: 5.0.2 @@ -1098,9 +1101,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 @@ -1144,9 +1144,6 @@ importers: '@types/semver': specifier: ^7.7.0 version: 7.7.0 - '@types/set-cookie-parser': - specifier: ^2.4.1 - version: 2.4.7 esbuild-register: specifier: ^3.6.0 version: 3.6.0(esbuild@0.25.4) @@ -4277,6 +4274,9 @@ 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/web-blob@3.1.0': resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} @@ -12721,6 +12721,8 @@ snapshots: transitivePeerDependencies: - encoding + '@remix-run/node-fetch-server@0.8.0': {} + '@remix-run/web-blob@3.1.0': dependencies: '@remix-run/web-stream': 1.1.0