Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(workerd): remote eval api #23

Merged
merged 8 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
- run: pnpm -C examples/react-ssr test-e2e-preview
- run: pnpm -C examples/react-ssr test-e2e-workerd
- run: pnpm -C examples/react-ssr-workerd test-e2e
- run: pnpm -C examples/workerd-cli test
- run: pnpm -C examples/react-server test-e2e
- run: pnpm -C examples/react-server build
- run: pnpm -C examples/react-server test-e2e-preview
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pnpm build
pnpm -C examples/react-ssr dev
pnpm -C examples/react-ssr-workerd dev
pnpm -C examples/react-server dev
pnpm -C examples/workerd-cli cli
```
24 changes: 24 additions & 0 deletions examples/workerd-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# workerd-cli

```sh
$ pnpm cli
[mf:inf] Ready on http://127.0.0.1:44031

> env.kv.list()
{ keys: [], list_complete: true, cacheStatus: null }

> env.kv.put("hello", "world")

> env.kv.list()
{ keys: [ { name: 'hello' } ], list_complete: true, cacheStatus: null }

> env.kv.get("hello")
world

> (await import("/wrangler.toml?raw")).default
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]
kv_namespaces = [
{ binding = "kv", id = "test-namespace" }
]
```
43 changes: 43 additions & 0 deletions examples/workerd-cli/e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import test from "node:test";
import childProcess from "node:child_process";

test("basic", async () => {
const proc = childProcess.spawn("pnpm", ["cli"]);
using _ = proc;
const helper = createProcessHelper(proc);
await helper.waitFor((out) => out.includes("[mf:inf] Ready"));
proc.stdin.write(`env.kv.list()\n`);
await helper.waitFor((out) => out.includes("{ keys: []"));
});

function createProcessHelper(
proc: childProcess.ChildProcessWithoutNullStreams,
) {
let stdout = "";
const listeners = new Set<() => void>();
proc.stdout.on("data", (data) => {
stdout += String(data);
for (const f of listeners) {
f();
}
});

async function waitFor(predicate: (stdout: string) => boolean) {
return new Promise<void>((resolve) => {
const listener = () => {
if (predicate(stdout)) {
resolve();
listeners.delete(listener);
}
};
listeners.add(listener);
});
}

return {
get stdout() {
return stdout;
},
waitFor,
};
}
16 changes: 16 additions & 0 deletions examples/workerd-cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@hiogawa/vite-environment-examples-workerd-cli",
"private": true,
"type": "module",
"scripts": {
"cli": "tsx src/cli.ts",
"test": "node --import tsx/esm --test e2e/*.test.ts"
},
"dependencies": {},
"devDependencies": {
"@hiogawa/vite-plugin-workerd": "workspace:*"
},
"volta": {
"extends": "../../package.json"
}
}
85 changes: 85 additions & 0 deletions examples/workerd-cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { createServer } from "vite";
import {
createWorkerdDevEnvironment,
type WorkerdDevEnvironment,
} from "@hiogawa/vite-plugin-workerd";
import { Log } from "miniflare";
import repl from "node:repl";

async function main() {
const server = await createServer({
clearScreen: false,
plugins: [
{
name: "virtual-repl",
resolveId(source, _importer, _options) {
if (source.startsWith("virtual:repl/")) {
return "\0" + source;
}
return;
},
load(id, _options) {
if (id.startsWith("\0virtual:repl/")) {
const cmd = id.slice("\0virtual:repl/".length);
return decodeURI(cmd);
}
return;
},
},
],
environments: {
workerd: {
resolve: {
noExternal: true,
},
dev: {
createEnvironment: (server, name) =>
createWorkerdDevEnvironment(server, name, {
miniflare: {
log: new Log(),
},
wrangler: {
configPath: "./wrangler.toml",
},
}),
},
},
},
});
const devEnv = server.environments["workerd"] as WorkerdDevEnvironment;

async function evaluate(cmd: string) {
if (!cmd.includes("return")) {
cmd = `return ${cmd}`;
}
const entrySource = `export default async function({ env }) { ${cmd} }`;
const entry = "virtual:repl/" + encodeURI(entrySource);
await devEnv.api.eval(
entry,
async function (this: any, mod: any) {
const result = await mod.default({ env: this.env });
if (typeof result !== "undefined") {
console.log(result);
}
},
[],
);
}

const replServer = repl.start({
eval: async (cmd, _context, _filename, callback) => {
try {
await evaluate(cmd);
(callback as any)(null);
} catch (e) {
callback(e as Error, null);
}
},
});

replServer.on("close", () => {
server.close();
});
}

main();
15 changes: 15 additions & 0 deletions examples/workerd-cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"include": ["src", "vite.config.ts"],
"compilerOptions": {
"exactOptionalPropertyTypes": false,
"verbatimModuleSyntax": true,
"noEmit": true,
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ESNext",
"lib": ["ESNext", "DOM"],
"types": ["vite/client"],
"jsx": "react-jsx"
}
}
3 changes: 3 additions & 0 deletions examples/workerd-cli/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "vite";

export default defineConfig({});
5 changes: 5 additions & 0 deletions examples/workerd-cli/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]
kv_namespaces = [
{ binding = "kv", id = "test-namespace" }
]
40 changes: 34 additions & 6 deletions packages/workerd/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import {
} from "miniflare";
import { fileURLToPath } from "url";
import { DefaultMap, tinyassert } from "@hiogawa/utils";
import { ANY_URL, RUNNER_INIT_PATH, setRunnerFetchOptions } from "./shared";
import {
ANY_URL,
RUNNER_INIT_PATH,
setRunnerFetchOptions,
type RunnerEvalOptions,
RUNNER_EVAL_PATH,
} from "./shared";
import {
DevEnvironment,
type CustomPayload,
Expand All @@ -20,7 +26,7 @@ import { createMiddleware } from "@hattip/adapter-node/native-fetch";
import type { SourcelessWorkerOptions } from "wrangler";

interface WorkerdPluginOptions extends WorkerdEnvironmentOptions {
entry: string;
entry?: string;
}

interface WorkerdEnvironmentOptions {
Expand All @@ -47,12 +53,14 @@ export function vitePluginWorkerd(pluginOptions: WorkerdPluginOptions): Plugin {
},

configureServer(server) {
const entry = pluginOptions.entry;
if (!entry) {
return;
}
const devEnv = server.environments["workerd"] as WorkerdDevEnvironment;
const nodeMiddleware = createMiddleware(
(ctx) => devEnv.api.dispatchFetch(pluginOptions.entry, ctx.request),
{
alwaysCallNext: false,
},
(ctx) => devEnv.api.dispatchFetch(entry, ctx.request),
{ alwaysCallNext: false },
);
return () => {
server.middlewares.use(nodeMiddleware);
Expand Down Expand Up @@ -161,6 +169,7 @@ export async function createWorkerdDevEnvironment(

// custom environment api
const api = {
// fetch proxy
async dispatchFetch(entry: string, request: Request) {
const req = new MiniflareRequest(request.url, {
method: request.method,
Expand All @@ -178,6 +187,25 @@ export async function createWorkerdDevEnvironment(
headers: res.headers as any,
});
},

// playwright-like eval interface https://playwright.dev/docs/evaluating
// (de)serialization can be customized (currently JSON.stringify/parse)
async eval<T>(
entry: string,
fn: (mod: unknown, ...args: unknown[]) => T,
...args: unknown[]
): Promise<Awaited<T>> {
const res = await runnerObject.fetch(ANY_URL + RUNNER_EVAL_PATH, {
method: "POST",
body: JSON.stringify({
entry,
fnString: fn.toString(),
args,
} satisfies RunnerEvalOptions),
});
tinyassert(res.ok);
return (await res.json()) as any;
},
};

// workaround for tsup dts?
Expand Down
9 changes: 8 additions & 1 deletion packages/workerd/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { tinyassert } from "@hiogawa/utils";

export const RUNNER_INIT_PATH = "/__viteInit";
export const RUNNER_EVAL_PATH = "/__viteEval";
export const ANY_URL = "https://any.local";

export type RunnerEnv = {
__viteRoot: string;
__viteUnsafeEval: {
eval: (code: string, filename: string) => any;
eval: (code: string, filename?: string) => any;
};
__viteFetchModule: {
fetch: (request: Request) => Promise<Response>;
Expand All @@ -32,3 +33,9 @@ export function getRunnerFetchOptions(headers: Headers): RunnerFetchOptions {
tinyassert(raw);
return JSON.parse(decodeURIComponent(raw));
}

export type RunnerEvalOptions = {
entry: string;
fnString: string;
args: unknown[];
};
11 changes: 11 additions & 0 deletions packages/workerd/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
RUNNER_INIT_PATH,
getRunnerFetchOptions,
type RunnerEnv,
RUNNER_EVAL_PATH,
type RunnerEvalOptions,
} from "./shared";
import { ModuleRunner } from "vite/module-runner";

Expand Down Expand Up @@ -39,6 +41,15 @@ export class RunnerObject implements DurableObject {
return new Response(null, { status: 101, webSocket: pair[1] });
}

if (url.pathname === RUNNER_EVAL_PATH) {
tinyassert(this.#runner);
const options = await request.json<RunnerEvalOptions>();
const fn = this.#env.__viteUnsafeEval.eval(`() => ${options.fnString}`)();
const mod = await this.#runner.import(options.entry);
const result = await fn.apply({ env: this.#env }, [mod, ...options.args]);
return new Response(JSON.stringify(result ?? null));
}

tinyassert(this.#runner);
const options = getRunnerFetchOptions(request.headers);
const mod = await this.#runner.import(options.entry);
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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