Skip to content

refactor(dev/vite): use @mjackson/node-fetch-server #13927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/react-router-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions packages/react-router-dev/vite/cloudflare-dev-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sendResponse } from "@mjackson/node-fetch-server";
import { createRequestHandler } from "react-router";
import {
type AppLoadContext,
Expand All @@ -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";

Expand Down Expand Up @@ -142,7 +143,7 @@ export const cloudflareDevProxyVitePlugin = <Env, Cf extends CfProperties>(
? await getLoadContext({ request: req, context })
: context;
let res = await handler(req, loadContext);
await toNodeRequest(res, nodeRes);
await sendResponse(nodeRes, res);
} catch (error) {
next(error);
}
Expand Down
107 changes: 5 additions & 102 deletions packages/react-router-dev/vite/node-adapter.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,110 +10,16 @@ export type NodeRequestHandler = (
res: ServerResponse
) => Promise<void>;

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<Vite.Connect.IncomingMessage>
): 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<Uint8Array>;
let readable = Readable.from(responseBody);
readable.pipe(nodeRes);
await once(readable, "end");
} else {
nodeRes.end();
}
return createRequest(nodeReq, nodeRes);
}
5 changes: 3 additions & 2 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 8 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.