Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 2 additions & 3 deletions packages/react-router-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
}
},
"dependencies": {
"isbot": "^5.1.11",
"@babel/core": "^7.27.7",
"@babel/generator": "^7.27.5",
"@babel/parser": "^7.27.7",
Expand All @@ -78,20 +77,21 @@
"@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",
"picocolors": "^1.1.1",
"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"
Expand All @@ -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",
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 "@remix-run/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 } from "./vite";
import { type ResolvedReactRouterConfig, loadConfig } from "../config/config";

Expand Down Expand Up @@ -144,7 +145,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 "@remix-run/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 "@remix-run/node-fetch-server";
import {
unstable_setDevServerHooks as setDevServerHooks,
createRequestHandler,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
47 changes: 21 additions & 26 deletions pnpm-lock.yaml

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

Loading