Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Jacob Ebey <jacob.ebey@live.com> Co-authored-by: Mark Dalgleish <mark.john.dalgleish@gmail.com>
- Loading branch information
1 parent
db45174
commit f44419e
Showing
10 changed files
with
295 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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([]); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
Oops, something went wrong.