diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 07984a977c..622434be96 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -71,6 +71,7 @@ "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", + "@mjackson/node-fetch-server": "^0.7.0", "@npmcli/package-json": "^4.0.1", "@react-router/node": "workspace:*", "arg": "^5.0.1", @@ -86,7 +87,6 @@ "prettier": "^2.7.1", "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/node": "^20.0.0", "@types/npmcli__package-json": "^4.0.0", "@types/prettier": "^2.7.3", - "@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 9d37690bf1..936eed175e 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 "@mjackson/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"; @@ -142,7 +143,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 3068348104..66806c057d 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 "@mjackson/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 3905414211..aad722a1f3 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 "@mjackson/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, @@ -1644,7 +1645,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 c7d64999c7..93e07f3994 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1032,6 +1032,9 @@ importers: '@babel/types': specifier: ^7.27.7 version: 7.27.7 + '@mjackson/node-fetch-server': + specifier: ^0.7.0 + version: 0.7.0 '@npmcli/package-json': specifier: ^4.0.1 version: 4.0.1 @@ -1077,9 +1080,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 @@ -1126,9 +1126,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.0) @@ -3694,6 +3691,9 @@ packages: '@mjackson/node-fetch-server@0.6.1': resolution: {integrity: sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg==} + '@mjackson/node-fetch-server@0.7.0': + resolution: {integrity: sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -11874,6 +11874,8 @@ snapshots: '@mjackson/node-fetch-server@0.6.1': {} + '@mjackson/node-fetch-server@0.7.0': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true