Skip to content

Commit

Permalink
Adapt superflare from remix → react-router
Browse files Browse the repository at this point in the history
this required duplicating node-adapter.js and maintaining it within superflare-remix, because it’s no longer included in the published package’s dist/ directory. here’s a PR that exposes the (from|to)NodeRequest utilities that we depend on in the superflareDevProxyVitePlugin: remix-run/react-router#12774

if it gets merged, we can remove superflare-remix/node.adapter.ts and import those utils alongside cloudflareDevProxy

note that i also needed to update superflare-remix’s tsconfig.json module and moduleResolution settings to get it to build without error (presumably due to changes between the published remix vs react-router packages
  • Loading branch information
acusti committed Jan 22, 2025
1 parent d63b444 commit baca18b
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 2,882 deletions.
16 changes: 5 additions & 11 deletions packages/superflare-remix/dev.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { cloudflareDevProxyVitePlugin } from "@remix-run/dev";
import {
fromNodeRequest,
toNodeRequest,
} from "@remix-run/dev/dist/vite/node-adapter.js";
import {
createRequestHandler,
type ServerBuild,
} from "@remix-run/server-runtime";
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { createRequestHandler, type ServerBuild } from "react-router";
import { type Plugin, type ViteDevServer } from "vite";
import { type GetPlatformProxyOptions } from "wrangler";
import { type Cloudflare, getLoadContext } from "./load-context";
import { fromNodeRequest, toNodeRequest } from "./node-adapter";

/**
* This is copied from the workers-sdk repo (used for wrangler’s getPlatformProxy).
Expand Down Expand Up @@ -41,7 +35,7 @@ export function superflareDevProxyVitePlugin<Env extends { APP_KEY: string }>(
options: GetPlatformProxyOptions = {}
): Plugin {
const ctx = new ExecutionContext();
const remixVitePlugin = cloudflareDevProxyVitePlugin(options);
const remixVitePlugin = cloudflareDevProxy(options);

return {
...remixVitePlugin,
Expand All @@ -61,7 +55,7 @@ export function superflareDevProxyVitePlugin<Env extends { APP_KEY: string }>(
// the same instance of the Config singleton class as in app code.
const superflare = await server.ssrLoadModule("superflare");
const build = (await server.ssrLoadModule(
"virtual:remix/server-build"
"virtual:react-router/server-build"
)) as ServerBuild;

const handler = createRequestHandler(build, "development");
Expand Down
6 changes: 3 additions & 3 deletions packages/superflare-remix/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type AppLoadContext } from "@remix-run/cloudflare";
import { type AppLoadContext } from "react-router";
import {
type DefineConfigReturn,
handleFetch as superflareHandleFetch,
Expand All @@ -10,7 +10,7 @@ import { type Cloudflare, getLoadContext } from "./load-context";

export { type Cloudflare, getLoadContext } from "./load-context";

declare module "@remix-run/cloudflare" {
declare module "react-router" {
interface AppLoadContext {
auth: InstanceType<typeof SuperflareAuth>;
session: InstanceType<typeof SuperflareSession>;
Expand Down Expand Up @@ -39,7 +39,7 @@ export async function handleFetch<Env extends { APP_KEY: string }>(
// `getPlatformProxy` used during development via Remix's
// `cloudflareDevProxyVitePlugin`:
// https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy
cloudflare: { caches, ctx, env, cf: request.cf },
cloudflare: { caches, ctx, env, cf: request.cf as Cloudflare<Env>["cf"] },
},
SuperflareAuth,
SuperflareSession,
Expand Down
8 changes: 2 additions & 6 deletions packages/superflare-remix/load-context.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import {
type AppLoadContext,
createCookieSessionStorage,
} from "@remix-run/cloudflare";
import { type AppLoadContext, createCookieSessionStorage } from "react-router";
import type { SuperflareAuth, SuperflareSession } from "superflare";
import { type PlatformProxy } from "wrangler";

// NOTE: PlatformProxy’s caches property is incompatible with the caches global
// https://github.com/cloudflare/workers-sdk/blob/main/packages/wrangler/src/api/integrations/platform/caches.ts
export type Cloudflare<Env extends { APP_KEY: string }> = Omit<
PlatformProxy<Env>,
"dispose" | "caches" | "cf"
"dispose" | "caches"
> & {
caches: CacheStorage;
cf: Request["cf"];
};

// Shared implementation compatible with Vite, Wrangler, and Workers
Expand Down
106 changes: 106 additions & 0 deletions packages/superflare-remix/node-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/vite/node-adapter.ts
declare module "set-cookie-parser";
import type { IncomingHttpHeaders, ServerResponse } from "node:http";
import { once } from "node:events";
import { Readable } from "node:stream";
import { splitCookiesString } from "set-cookie-parser";
import { createReadableStreamFromReadable } from "@react-router/node";
import type * as Vite from "vite";

export type NodeRequestHandler = (
req: Vite.Connect.IncomingMessage,
res: ServerResponse
) => Promise<void>;

function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
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 origin =
nodeReq.headers.origin && "null" !== nodeReq.headers.origin
? nodeReq.headers.origin
: `http://${nodeReq.headers.host}`;
// Use `req.originalUrl` so React Router is aware of the full path
invariant(
nodeReq.originalUrl,
"Expected `nodeReq.originalUrl` to be defined"
);
// @ts-expect-error this is a @react-router/dev file
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.headers),
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;
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);
}

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();
}
}

function invariant(value: any, message?: string) {
if (value === false || value == null) {
throw new Error(message);
}
}
14 changes: 8 additions & 6 deletions packages/superflare-remix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,24 @@
"license": "MIT",
"devDependencies": {
"@cloudflare/workers-types": "^4.20250109.0",
"@remix-run/cloudflare": "^2.12.1",
"@remix-run/dev": "^2.12.1",
"@remix-run/server-runtime": "^2.12.1",
"@react-router/dev": "^7",
"@react-router/node": "^7",
"@types/set-cookie-parser": "^2.4.10",
"react-router": "^7",
"tsconfig": "workspace:*",
"tsup": "^8.3.5",
"typescript": "^5",
"vite": "^5",
"wrangler": "^3.91.0"
},
"peerDependencies": {
"@remix-run/cloudflare": "^2.12.1",
"@remix-run/dev": "^2.12.1",
"@remix-run/server-runtime": "^2.12.1",
"@react-router/dev": "^7",
"@react-router/node": "^7",
"react-router": "^7",
"wrangler": "^3.91.0"
},
"dependencies": {
"set-cookie-parser": "^2.7.1",
"superflare": "workspace:*"
}
}
3 changes: 2 additions & 1 deletion packages/superflare-remix/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"module": "commonjs",
"module": "ESNext",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"isolatedModules": false,
"rootDir": ".",
Expand Down
2 changes: 1 addition & 1 deletion packages/superflare/cli/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export async function devHandler(
env: process.env,
});

spawn("remix", ["vite:dev"], {
spawn("react-router", ["dev"], {
stdio: "inherit",
shell: true,
env: process.env,
Expand Down
Loading

0 comments on commit baca18b

Please sign in to comment.