Skip to content

Commit

Permalink
Cloudflare support for Vite (#8531)
Browse files Browse the repository at this point in the history
Co-authored-by: Jacob Ebey <jacob.ebey@live.com>
Co-authored-by: Mark Dalgleish <mark.john.dalgleish@gmail.com>
  • Loading branch information
3 people committed Jan 25, 2024
1 parent db45174 commit f44419e
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 112 deletions.
3 changes: 2 additions & 1 deletion integration/package.json
Expand Up @@ -36,6 +36,7 @@
"type-fest": "^4.0.0",
"typescript": "^5.1.0",
"vite-env-only": "^2.0.0",
"vite-tsconfig-paths": "^4.2.2"
"vite-tsconfig-paths": "^4.2.2",
"wrangler": "^3.24.0"
}
}
153 changes: 153 additions & 0 deletions integration/vite-cloudflare-test.ts
@@ -0,0 +1,153 @@
import { test, expect } from "@playwright/test";
import getPort from "get-port";

import { VITE_CONFIG, createProject, using, viteDev } from "./helpers/vite.js";

test.describe("Vite / cloudflare", async () => {
let port: number;
let cwd: string;

test.beforeAll(async () => {
port = await getPort();
cwd = await createProject({
"package.json": JSON.stringify(
{
private: true,
sideEffects: false,
type: "module",
scripts: {
dev: "remix vite:dev",
build: "remix vite:build",
start: "wrangler pages dev ./build/client",
deploy: "wrangler pages deploy ./build/client",
typecheck: "tsc",
},
dependencies: {
"@remix-run/cloudflare": "*",
"@remix-run/cloudflare-pages": "*",
"@remix-run/react": "*",
isbot: "^4.1.0",
miniflare: "^3.20231030.4",
react: "^18.2.0",
"react-dom": "^18.2.0",
},
devDependencies: {
"@cloudflare/workers-types": "^4.20230518.0",
"@remix-run/dev": "*",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"node-fetch": "^3.3.2",
typescript: "^5.1.6",
vite: "^5.0.0",
"vite-tsconfig-paths": "^4.2.1",
wrangler: "^3.24.0",
},
engines: {
node: ">=18.0.0",
},
},
null,
2
),
"vite.config.ts": await VITE_CONFIG({
port,
pluginOptions: `{ adapter: (await import("@remix-run/dev")).unstable_vitePluginAdapterCloudflare() }`,
}),
"functions/[[page]].ts": `
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
// @ts-ignore - the server build file is generated by \`remix vite:build\`
import * as build from "../build/server";
export const onRequest = createPagesFunctionHandler({
build,
getLoadContext: (context) => ({ env: context.env }),
});
`,
"wrangler.toml": `
kv_namespaces = [
{ id = "abc123", binding="MY_KV" }
]
`,
"app/routes/_index.tsx": `
import {
json,
type LoaderFunctionArgs,
type ActionFunctionArgs,
} from "@remix-run/cloudflare";
import { Form, useLoaderData } from "@remix-run/react";
const key = "__my-key__";
export async function loader({ context }: LoaderFunctionArgs) {
const { MY_KV } = context.env;
const value = await MY_KV.get(key);
return json({ value });
}
export async function action({ request, context }: ActionFunctionArgs) {
const { MY_KV: myKv } = context.env;
if (request.method === "POST") {
const formData = await request.formData();
const value = formData.get("value") as string;
await myKv.put(key, value);
return null;
}
if (request.method === "DELETE") {
await myKv.delete(key);
return null;
}
throw new Error(\`Method not supported: "\${request.method}"\`);
}
export default function Index() {
const { value } = useLoaderData<typeof loader>();
return (
<div>
<h1>Welcome to Remix</h1>
{value ? (
<>
<p data-text>Value: {value}</p>
<Form method="DELETE">
<button>Delete</button>
</Form>
</>
) : (
<>
<p data-text>No value</p>
<Form method="POST">
<label htmlFor="value">Set value:</label>
<input type="text" name="value" id="value" required />
<br />
<button>Save</button>
</Form>
</>
)}
</div>
);
}
`,
});
});

test("vite dev", async ({ page }) => {
await using(await viteDev({ cwd, port }), async () => {
let pageErrors: Error[] = [];
page.on("pageerror", (error) => pageErrors.push(error));

await page.goto(`http://localhost:${port}/`, {
waitUntil: "networkidle",
});
await expect(page.locator("[data-text]")).toHaveText("No value");

await page.getByLabel("Set value:").fill("my-value");
await page.getByRole("button").click();
await expect(page.locator("[data-text]")).toHaveText("Value: my-value");

expect(pageErrors).toEqual([]);
});
});
});
5 changes: 4 additions & 1 deletion packages/remix-dev/index.ts
Expand Up @@ -10,4 +10,7 @@ export type {
Unstable_BuildManifest,
Unstable_VitePluginAdapter,
} from "./vite";
export { unstable_vitePlugin } from "./vite";
export {
unstable_vitePlugin,
unstable_vitePluginAdapterCloudflare,
} from "./vite";
9 changes: 7 additions & 2 deletions packages/remix-dev/package.json
Expand Up @@ -91,12 +91,14 @@
"msw": "^1.2.3",
"strip-ansi": "^6.0.1",
"tiny-invariant": "^1.2.0",
"vite": "^5.0.0"
"vite": "^5.0.0",
"wrangler": "^3.24.0"
},
"peerDependencies": {
"@remix-run/serve": "^2.5.1",
"typescript": "^5.1.0",
"vite": "^5.0.0"
"vite": "^5.0.0",
"wrangler": "^3.24.0"
},
"peerDependenciesMeta": {
"@remix-run/serve": {
Expand All @@ -107,6 +109,9 @@
},
"vite": {
"optional": true
},
"wrangler": {
"optional": true
}
},
"engines": {
Expand Down
13 changes: 13 additions & 0 deletions packages/remix-dev/vite/adapters/cloudflare.ts
@@ -0,0 +1,13 @@
export const adapter = () => async () => {
let { getBindingsProxy } = await import("wrangler");
let { bindings } = await getBindingsProxy();
let loadContext = bindings && { env: bindings };
let viteConfig = {
ssr: {
resolve: {
externalConditions: ["workerd", "worker"],
},
},
};
return { viteConfig, loadContext };
};
2 changes: 2 additions & 0 deletions packages/remix-dev/vite/index.ts
Expand Up @@ -12,3 +12,5 @@ export const unstable_vitePlugin: RemixVitePlugin = (...args) => {
let { remixVitePlugin } = require("./plugin") as typeof import("./plugin");
return remixVitePlugin(...args);
};

export { adapter as unstable_vitePluginAdapterCloudflare } from "./adapters/cloudflare";
85 changes: 85 additions & 0 deletions packages/remix-dev/vite/node-adapter.ts
@@ -0,0 +1,85 @@
import type {
IncomingHttpHeaders,
IncomingMessage,
ServerResponse,
} from "node:http";
import { once } from "node:events";
import { Readable } from "node:stream";
import { splitCookiesString } from "set-cookie-parser";
import { createReadableStreamFromReadable } from "@remix-run/node";

import invariant from "../invariant";

export type NodeRequestHandler = (
req: 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/remix-express/server.ts
export function fromNodeRequest(nodeReq: IncomingMessage): Request {
let origin =
nodeReq.headers.origin && "null" !== nodeReq.headers.origin
? nodeReq.headers.origin
: `http://${nodeReq.headers.host}`;
invariant(nodeReq.url, 'Expected "req.url" to be defined');
let url = new URL(nodeReq.url, origin);

let init: RequestInit = {
method: nodeReq.method,
headers: fromNodeHeaders(nodeReq.headers),
};

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

0 comments on commit f44419e

Please sign in to comment.