Skip to content

Commit 3de66bf

Browse files
authored
chore: workerd environment prototype (#14)
* chore: init * chore: lockfile * chore: setup script * wip * chore: durable objects websocket example * chore: setup tsup * chore: statefull example * feat: vite ModuleRunner in durable objects * chore: cleanup * feat: prototype vitePluginWorkerd * wip: plugin * wip * refactor: simplify runner options * fix: setup __viteFetchModule service binding * fix: add vitePluginVirtualIndexHtml * ci: fix build * fix: fix vitePluginVirtualIndexHtml on build * chore: readme * fix: fix vitePluginVirtualIndexHtml extension * test: test-e2e-workerd * chore: remove old * chore: comment * wip: hmr * wip: implement hot * chore: vite v6.0.0-alpha.1 * refactor: async createEnvironment * refactor: createWorkerdDevEnvironment * refactor: minor * refactor: move code * feat: vitest-pool-workers like options * refactor: move off entry selection * feat: catch error * refactor: get/setRunnerFetchOptions * feat: export createWorkerdDevEnvironment * feat: createSimpleHMRChannel * wip: kv demo * chore: setup example * chore: kv demo * ci: add e2e * chore: use wrangler.toml * chore: readme * chore: readme * refactor: minor * chore: readme
1 parent fb90476 commit 3de66bf

32 files changed

+2043
-27
lines changed

.github/workflows/ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ jobs:
1212
- run: corepack enable
1313
- run: pnpm i
1414
- run: pnpm lint-check
15+
- run: pnpm -C examples/workerd build
1516
- run: pnpm tsc
1617
- run: npx playwright install chromium
1718
- run: pnpm -C examples/react-ssr test-e2e
1819
- run: pnpm -C examples/react-ssr build
1920
- run: pnpm -C examples/react-ssr test-e2e-preview
21+
- run: pnpm -C examples/react-ssr test-e2e-workerd
22+
- run: pnpm -C examples/react-ssr-workerd test-e2e
2023
- run: pnpm -C examples/react-server test-e2e
2124
- run: pnpm -C examples/react-server build
2225
- run: pnpm -C examples/react-server test-e2e-preview

examples/react-ssr-workerd/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# react-ssr-workerd
2+
3+
```sh
4+
pnpm dev
5+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test("basic", async ({ page }) => {
4+
await page.goto("/");
5+
await expect(page.locator("#root")).toContainText("hydrated: true");
6+
await expect(page.locator("#root")).toContainText("Count: 0");
7+
await page.getByRole("button", { name: "+" }).click();
8+
await expect(page.locator("#root")).toContainText("Count: 1");
9+
});

examples/react-ssr-workerd/index.html

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>react-ssr-workerd</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, height=device-height, initial-scale=1.0"
9+
/>
10+
</head>
11+
<body>
12+
<script src="/src/entry-client" type="module"></script>
13+
</body>
14+
</html>
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@hiogawa/vite-environment-examples-react-ssr-workerd",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"test-e2e": "playwright test"
8+
},
9+
"dependencies": {
10+
"react": "18.3.0-canary-6c3b8dbfe-20240226",
11+
"react-dom": "18.3.0-canary-6c3b8dbfe-20240226"
12+
},
13+
"devDependencies": {
14+
"@cloudflare/workers-types": "^4.20240405.0",
15+
"@hiogawa/vite-plugin-workerd": "workspace:*",
16+
"@types/react": "18.2.72",
17+
"@types/react-dom": "18.2.22"
18+
},
19+
"volta": {
20+
"extends": "../../package.json"
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
3+
const port = Number(process.env["E2E_PORT"] || 6174);
4+
const command = process.env["E2E_PREVIEW"]
5+
? `pnpm preview --port ${port} --strict-port`
6+
: process.env["E2E_WORKERD"]
7+
? `pnpm dev-workerd --port ${port} --strict-port`
8+
: `pnpm dev --port ${port} --strict-port`;
9+
10+
export default defineConfig({
11+
testDir: "e2e",
12+
use: {
13+
trace: "on-first-retry",
14+
},
15+
projects: [
16+
{
17+
name: "chromium",
18+
use: devices["Desktop Chrome"],
19+
},
20+
],
21+
webServer: {
22+
command,
23+
port,
24+
},
25+
forbidOnly: !!process.env["CI"],
26+
retries: process.env["CI"] ? 2 : 0,
27+
reporter: "list",
28+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createMiddleware } from "@hattip/adapter-node/native-fetch";
2+
import { handler } from "../entry-server";
3+
4+
export default createMiddleware((ctx) => handler(ctx.request));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { handler } from "../entry-server";
2+
3+
export default {
4+
fetch(request: Request, env: unknown) {
5+
Object.assign(globalThis, { env });
6+
return handler(request);
7+
},
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { tinyassert } from "@hiogawa/utils";
2+
import ReactDomClient from "react-dom/client";
3+
import Page from "./routes/page";
4+
import React from "react";
5+
6+
async function main() {
7+
const el = document.getElementById("root");
8+
tinyassert(el);
9+
React.startTransition(() => {
10+
ReactDomClient.hydrateRoot(el, <Page />);
11+
});
12+
}
13+
14+
main();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import ReactDomServer from "react-dom/server.edge";
2+
import Page from "./routes/page";
3+
4+
export async function handler(request: Request) {
5+
const url = new URL(request.url);
6+
if (url.pathname === "/api") {
7+
return apiHandler(request);
8+
}
9+
if (url.pathname === "/nodejs-compat") {
10+
const util = await import("node:util");
11+
return new Response(util.format("hello %s", "world"));
12+
}
13+
14+
const ssrHtml = ReactDomServer.renderToString(<Page />);
15+
let html = (await import("virtual:index-html")).default;
16+
html = html.replace(/<body>/, `<body><div id="root">${ssrHtml}</div>`);
17+
return new Response(html, { headers: { "content-type": "text/html" } });
18+
}
19+
20+
async function apiHandler(request: Request) {
21+
let count = Number(await env.kv.get("count"));
22+
if (request.method === "POST") {
23+
const { delta } = await request.json();
24+
count += delta;
25+
await env.kv.put("count", String(count));
26+
}
27+
return new Response(JSON.stringify({ count }));
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
3+
export default function Page() {
4+
const [count, setCount] = React.useState<number>();
5+
6+
const [hydrated, setHydrated] = React.useState(false);
7+
React.useEffect(() => {
8+
setHydrated(true);
9+
getCount().then(setCount);
10+
}, []);
11+
12+
return (
13+
<div>
14+
<div>hydrated: {String(hydrated)}</div>
15+
<div>Count: {count ?? "..."}</div>
16+
<button onClick={async () => changeCount(-1).then(setCount)}>-1</button>
17+
<button onClick={async () => changeCount(+1).then(setCount)}>+1</button>
18+
</div>
19+
);
20+
}
21+
22+
async function getCount() {
23+
const res = await fetch("/api");
24+
const { count } = await res.json();
25+
return count as number;
26+
}
27+
28+
async function changeCount(delta: number) {
29+
const res = await fetch("/api", {
30+
method: "POST",
31+
body: JSON.stringify({ delta }),
32+
});
33+
const { count } = await res.json();
34+
return count as number;
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module "react-dom/server.edge" {
2+
export * from "react-dom/server";
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "virtual:index-html" {
2+
const src: string;
3+
export default src;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare const env: {
2+
kv: import("@cloudflare/workers-types").KVNamespace;
3+
};
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"extends": "@tsconfig/strictest/tsconfig.json",
3+
"include": ["src", "vite.config.ts", "e2e", "playwright.config.ts"],
4+
"compilerOptions": {
5+
"exactOptionalPropertyTypes": false,
6+
"verbatimModuleSyntax": true,
7+
"noEmit": true,
8+
"moduleResolution": "Bundler",
9+
"module": "ESNext",
10+
"target": "ESNext",
11+
"lib": ["ESNext", "DOM"],
12+
"types": ["vite/client"],
13+
"jsx": "react-jsx"
14+
}
15+
}
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { defineConfig } from "vite";
2+
import react from "@vitejs/plugin-react";
3+
import { vitePluginWorkerd } from "@hiogawa/vite-plugin-workerd";
4+
import { vitePluginVirtualIndexHtml } from "../react-ssr/vite.config";
5+
import { Log } from "miniflare";
6+
7+
export default defineConfig((_env) => ({
8+
clearScreen: false,
9+
appType: "custom",
10+
plugins: [
11+
react(),
12+
vitePluginWorkerd({
13+
entry: "/src/adapters/workerd.ts",
14+
miniflare: {
15+
log: new Log(),
16+
},
17+
wrangler: {
18+
configPath: "./wrangler.toml",
19+
},
20+
}),
21+
vitePluginVirtualIndexHtml(),
22+
],
23+
environments: {
24+
workerd: {
25+
// [feedback] how to prevent deps optimization to inject this? still `ssr.target: "webworker"` needed?
26+
// import { createRequire } from 'module';const require = createRequire(import.meta.url);
27+
nodeCompatible: false,
28+
webCompatible: true,
29+
resolve: {
30+
noExternal: true,
31+
},
32+
dev: {
33+
optimizeDeps: {
34+
include: [
35+
"react",
36+
"react/jsx-runtime",
37+
"react/jsx-dev-runtime",
38+
"react-dom/server.edge",
39+
],
40+
},
41+
},
42+
},
43+
},
44+
ssr: {
45+
target: "webworker",
46+
},
47+
}));
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
compatibility_date = "2024-01-01"
2+
compatibility_flags = ["nodejs_compat"]
3+
kv_namespaces = [
4+
{ binding = "kv", id = "test-namespace" }
5+
]

examples/react-ssr/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
"type": "module",
55
"scripts": {
66
"dev": "vite",
7+
"dev-workerd": "vite --config vite.config.workerd.ts",
78
"build": "vite build --all",
89
"preview": "vite preview",
910
"test-e2e": "playwright test",
1011
"test-e2e-preview": "E2E_PREVIEW=1 playwright test",
12+
"test-e2e-workerd": "E2E_WORKERD=1 playwright test",
1113
"vc-build": "SERVER_ENTRY=/src/adapters/vercel-edge.ts pnpm build && bash misc/vercel-edge/build.sh",
1214
"vc-release": "vercel deploy --prebuilt misc/vercel-edge --prod"
1315
},
@@ -16,6 +18,7 @@
1618
"react-dom": "18.3.0-canary-6c3b8dbfe-20240226"
1719
},
1820
"devDependencies": {
21+
"@hiogawa/vite-plugin-workerd": "workspace:*",
1922
"@types/react": "18.2.72",
2023
"@types/react-dom": "18.2.22"
2124
},

examples/react-ssr/playwright.config.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { defineConfig, devices } from "@playwright/test";
22

33
const port = Number(process.env["E2E_PORT"] || 6174);
4-
const isPreview = Boolean(process.env["E2E_PREVIEW"]);
5-
const command = isPreview
4+
const command = process.env["E2E_PREVIEW"]
65
? `pnpm preview --port ${port} --strict-port`
7-
: `pnpm dev --port ${port} --strict-port`;
6+
: process.env["E2E_WORKERD"]
7+
? `pnpm dev-workerd --port ${port} --strict-port`
8+
: `pnpm dev --port ${port} --strict-port`;
89

910
export default defineConfig({
1011
testDir: "e2e",
@@ -21,7 +22,6 @@ export default defineConfig({
2122
command,
2223
port,
2324
},
24-
grepInvert: isPreview ? /@dev/ : /@build/,
2525
forbidOnly: !!process.env["CI"],
2626
retries: process.env["CI"] ? 2 : 0,
2727
reporter: "list",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { handler } from "../entry-server";
2+
3+
export default {
4+
fetch: handler,
5+
};

examples/react-ssr/tsconfig.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
{
22
"extends": "@tsconfig/strictest/tsconfig.json",
3-
"include": ["src", "vite.config.ts", "e2e", "playwright.config.ts"],
3+
"include": [
4+
"src",
5+
"vite.config.ts",
6+
"vite.config.workerd.ts",
7+
"e2e",
8+
"playwright.config.ts"
9+
],
410
"compilerOptions": {
511
"exactOptionalPropertyTypes": false,
612
"verbatimModuleSyntax": true,
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { defineConfig } from "vite";
2+
import react from "@vitejs/plugin-react";
3+
import { vitePluginWorkerd } from "@hiogawa/vite-plugin-workerd";
4+
import { vitePluginVirtualIndexHtml } from "./vite.config";
5+
import { Log } from "miniflare";
6+
7+
export default defineConfig((_env) => ({
8+
clearScreen: false,
9+
appType: "custom",
10+
plugins: [
11+
react(),
12+
vitePluginWorkerd({
13+
entry: "/src/adapters/workerd.ts",
14+
miniflare: {
15+
log: new Log(),
16+
},
17+
}),
18+
vitePluginVirtualIndexHtml(),
19+
],
20+
environments: {
21+
workerd: {
22+
// [feedback] how to prevent deps optimization to inject this? still `ssr.target: "webworker"` needed?
23+
// import { createRequire } from 'module';const require = createRequire(import.meta.url);
24+
nodeCompatible: false,
25+
webCompatible: true,
26+
resolve: {
27+
noExternal: true,
28+
},
29+
dev: {
30+
optimizeDeps: {
31+
include: [
32+
"react",
33+
"react/jsx-runtime",
34+
"react/jsx-dev-runtime",
35+
"react-dom/server.edge",
36+
],
37+
},
38+
},
39+
},
40+
},
41+
ssr: {
42+
target: "webworker",
43+
},
44+
}));

examples/workerd/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# workerd
2+
3+
Vite module runner on Durable Objects.
4+
5+
See [`examples/react-ssr-workerd`](../react-ssr-workerd).
6+
7+
## references
8+
9+
- https://github.com/cloudflare/workers-sdk/blame/2789f26a87c769fc6177b0bdc79a839a15f4ced1/packages/miniflare/test/plugins/do/index.spec.ts
10+
- https://github.com/cloudflare/workers-sdk/blob/d994066f255f6851759a055eac3b52a4aa4b83c3/packages/vitest-pool-workers/src/worker/index.ts#L174
11+
- https://github.com/cloudflare/workers-sdk/blob/2789f26a87c769fc6177b0bdc79a839a15f4ced1/packages/vitest-pool-workers/src/pool/index.ts#L630
12+
- https://github.com/cloudflare/workers-sdk/pull/5530

0 commit comments

Comments
 (0)