diff --git a/crates/rubyrs/src/bin/wasm_worker.rs b/crates/rubyrs/src/bin/wasm_worker.rs new file mode 100644 index 000000000..5a2ac285b --- /dev/null +++ b/crates/rubyrs/src/bin/wasm_worker.rs @@ -0,0 +1,56 @@ +// PoC entry point for the Cloudflare Workers PoC. +// +// Shape: read Ruby source from stdin, evaluate, write result / +// runtime stdout to stdout, exit non-zero on trap. The Worker +// pipes the HTTP request body as stdin and captures stdout as +// the HTTP response — see `poc/cf-worker/`. +// +// Why a separate bin (not main.rs): the CLI reads a path from +// argv, which is awkward to coordinate from workers-wasi since +// its public API does not expose pre-populating the in-isolate +// FS. Stdin is a `ReadableStream` in workers-wasi's option +// shape, which IS easy to drive from a Worker. +// +// Intentionally NOT a feature flag — keeping it as a separate +// bin avoids adding any conditional compilation to the +// well-trodden CLI / library paths. Build with: +// cargo build --release --target wasm32-wasip1 \ +// --bin wasm_worker --no-default-features -p rubyrs + +use std::io::Read; + +use rubyrs::{Config, Runtime}; + +fn main() { + let mut src = String::new(); + if let Err(e) = std::io::stdin().read_to_string(&mut src) { + eprintln!("wasm_worker: stdin read failed: {e}"); + std::process::exit(2); + } + // PoC: defaults only. Once cold-start + execution numbers + // land, the right per-request caps (RUBYRS_DEADLINE_MS to + // back-stop Workers' 30s CPU cap, max_value_bytes to keep a + // runaway response from filling the 128MB isolate budget) + // are an obvious follow-up. + let cfg = Config { + // wasi has no PID concept; CLI uses None for the same + // reason. `$$` surfaces as 0 in Ruby-land. + pid: None, + ..Config::default() + }; + // `take_wizer_runtime` only exists under `target_os = "wasi"` + // (see lib.rs); on host targets we skip the fast path so this + // bin still `cargo check`s without `--target wasm32-wasip1`. + #[cfg(target_os = "wasi")] + let mut rt = match rubyrs::take_wizer_runtime() { + Some(mut rt) => { rt.apply_config(cfg); rt } + None => Runtime::with_config(cfg), + }; + #[cfg(not(target_os = "wasi"))] + let mut rt = Runtime::with_config(cfg); + rt.set_stdout(Box::new(std::io::stdout())); + if let Err(trap) = rt.eval(&src, "(worker)") { + eprint!("{}", rt.format_trap(&trap)); + std::process::exit(1); + } +} diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 8c41e6c85..bcff470e8 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -88,6 +88,107 @@ or native MRI wins on throughput (see "Throughput" below where rubyrs still trails CRuby's interpreter ~1.76× on a 1M-iteration loop). The two niches don't overlap. +## Edge runtimes: cross-host portability + +Validates the "one wasm artifact, many edge runtimes" thesis. Same +`src/rubyrs_worker.wasm` (1.68 MB after Wizer pre-init, no +wasm-opt — see [PoC details](#wasm-opt-vs-wizer-notes) below) +runs unchanged under three different V8-based runtimes and one +non-V8 baseline. Spike branch: +[`spike/cf-worker-poc`](../poc/cf-worker/). + +`puts 1+1` workload, n=5 each, Apple M-series: + +| Runtime | Engine | Self-host? | Cold-start | Warm tiny | Warm smoke.rb | 1M `each` | +|---------|:------:|:---------:|:----------:|:---------:|:-------------:|:---------:| +| **Deno** 2.8 + browser_wasi_shim | V8 14.9 | ✅ | 25 ms | **1.5 ms** | **1.7 ms** | 124 ms | +| **workerd** 2026-05-26 + workers-wasi | V8 | ✅ | **18 ms** | 2.5 ms | 4.0 ms | 135 ms | +| **CF Workers edge** (managed) | V8 (= workerd) | ❌ | ~149 ms wall | 7 ms cpu | 7 ms cpu | 173 ms cpu | +| wasmtime 45 (CLI, no HTTP) | wasmtime | ✅ | 12.7 ms (raw) / ~7 ms (AOT) | — | — | — | + +Notes: + +- **CF edge numbers are CPU time from `wrangler tail`** bucketed + by per-isolate invocation count (a header `x-rubyrs-invocation` + emitted by [worker.js](../poc/cf-worker/src/worker.js)). Cold + isolate (invocation == 1) wall is 149 ms / cpu ~80 ms; warm + (invocation > 1) settles to 7 ms cpu p50, p90 12 ms, max 13 ms. + The earlier-reading "wizer regresses edge perf" turned out to + be deploy-then-immediately-measure pool-warming noise, not a + real regression — [Pyodide-on-Workers' published 1027 ms + mean](https://blog.cloudflare.com/python-workers-advancements/) + is similarly a pool-hit + pool-miss blend. +- **Deno beats workerd on warm by ~40 %** (1.5 vs 2.5 ms tiny) + despite trailing on cold (25 vs 18 ms). Plausible reasons: (1) + `browser_wasi_shim`'s stdin/stdout is a pure-JS callback on a + single `Uint8Array`, vs `workers-wasi`'s extra `memfs.wasm` + proxy step; (2) `Deno.serve` is hyper-based Rust HTTP cutting + out workerd's JS-shim ↔ kj layer. Heavy compute converges to + within ~10 % because V8's wasm engine dominates that regime. +- **wasmtime cold-start (7-13 ms)** beats every V8 host on + cold but provides no HTTP layer of its own — listed for + baseline only; HTTP-serving wasmtime would require either + wasi-http (component model, not Preview 1) or a custom Rust + HTTP loop. Not part of the V8-host comparison. + +#### wasm-opt vs Wizer notes + +Counter-intuitive PoC finding: **`wasm-opt` is consistently +net-negative on V8 cold-start at every optimisation level**, even +when its size reductions are large. Smaller wasm doesn't translate +into faster instantiate; the V8 wasm parser appears to bottleneck +on IR construction / module setup rather than byte count. Wizer +pre-init is the win, n=5 each on workerd local: + +| Build pipeline | Wasm size | Cold-start (median) | +|----------------|----------:|--------------------:| +| baseline (raw cargo output) | 1.54 MB | 57 ms | +| wasm-opt -Oz only | 1.22 MB (−21 %) | 53 ms (−7 %) | +| wasm-opt -Oz + Wizer | 1.37 MB | 27 ms (−53 %) | +| wasm-opt -O2 + Wizer | 1.42 MB | 23 ms (−60 %) | +| **Wizer only** (no wasm-opt) | **1.68 MB** | **18 ms (−69 %)** | + +The Wizer win matches what +[`workerd/src/pyodide/make_snapshots.py`](https://github.com/cloudflare/workerd/tree/main/src/pyodide) +does for Python Workers — snapshot the post-init linear memory +so cold-start skips re-running the interpreter's bootstrap. We +cannot match CF's *baseline-preloaded-in-isolate-pool* trick +(that requires the runtime to be linked into workerd itself), +but the per-Worker snapshot equivalent is exactly what the PoC's +`build.sh` produces. + +#### Cold-start floor — two negative experiments + +Above ~18 ms (workerd local) the marginal cost of further wasm +shrinkage is zero. Two independent attempts confirmed: + +1. **Lazy-loading the Tier 1 stdlib preambles** (Random + SecureRandom, + `src/lib.rs::load_preamble` calls these unconditionally today). + Cuts ~40 KB from the Wizer'd wasm. Cold-start n=5: 17.7, 19.5, + 22.1, 22.6, 19.3 ms — **median 19.5 ms, marginally SLOWER than + the 18.2 ms baseline**, well within variance. + +2. **`opt-level = "z"` + LTO=fat + codegen-units=1**. Repo's own + `[profile.release-min]` history note records this combination as + **3–19 % SLOWER at cold start despite producing a 56 %-smaller + binary**, measured on three hosts (macOS arm64, Linux arm64, + Linux x86_64). The reason is the same one wasm-opt -Oz hits: + aggressive size shrinkage suppresses inlining and substitutes + shorter call sequences, which V8's wasm tier-up engine takes + longer to fix up than it would have to compile the original. + +Two independent angles, same negative result — the 18 ms floor +is V8's wasm parser + module-instantiate fixed cost, NOT a +function of our byte count. To reduce further the project would +have to either (a) reduce the function count (1798 today) so V8 +has fewer IRs to build, requiring rubyrs-internal refactoring; (b) +move to component model + AOT (wasmtime serve), bypassing V8's +parse path entirely; or (c) get CF to expose a generic +`--save-wasm-snapshot` user-wasm equivalent of their privileged +Python preload (currently not on offer). The 18 ms cold-start +is best treated as the public-API floor for this build shape and +the PoC is now operating at that floor. + ## Throughput 1M iteration loop computing fizzbuzz string lengths. diff --git a/poc/cf-worker/.gitignore b/poc/cf-worker/.gitignore new file mode 100644 index 000000000..ab328c412 --- /dev/null +++ b/poc/cf-worker/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.wrangler/ +src/*.wasm +workerd/dist/ diff --git a/poc/cf-worker/README.md b/poc/cf-worker/README.md new file mode 100644 index 000000000..b5f7f8e08 --- /dev/null +++ b/poc/cf-worker/README.md @@ -0,0 +1,94 @@ +# rubyrs on Cloudflare Workers — PoC + +Goal: prove rubyrs.wasm runs on Cloudflare Workers (V8 isolate + +WASI Preview 1 polyfill), end-to-end, locally via `wrangler dev`. + +## Shape + +``` +HTTP POST body=Ruby source + ↓ + Worker fetch handler (src/worker.js) + ↓ pipes body as stdin + @cloudflare/workers-wasi (WASI preview1 shim in JS) + ↓ + rubyrs_worker.wasm (wasm32-wasip1 bin reading stdin via Runtime::eval) + ↓ captures stdout +HTTP 200 body=Ruby script output +``` + +The worker bin (`crates/rubyrs/src/bin/wasm_worker.rs`) reads +stdin → `Runtime::eval` → stdout. The Worker pipes +`request.body` straight in; it does not touch the in-isolate +filesystem (workers-wasi's littlefs has no public pre-population +API, see [research notes](#research-notes)). + +## Prerequisites + +- Rust toolchain matching `rust-toolchain.toml` +- `rustup target add wasm32-wasip1` +- `WASI_SDK_PATH` pointing at a wasi-sdk install (same as + `tests/wasm/smoke.sh` — needed for the wasi_stub.c compile in + build.rs). Download from + https://github.com/WebAssembly/wasi-sdk/releases. +- `node` + `npm` +- `wizer` is optional; included in the build path when present + (`cargo install wizer-cli`). + +## Quick start + +```sh +# From this directory. +npm install # @cloudflare/workers-wasi + wrangler +./build.sh # cargo → (optional) wizer → wasm/rubyrs_worker.wasm +npx wrangler dev # local V8 (workerd) on http://localhost:8787 + +# In another terminal: +curl -X POST --data-binary 'puts (1..5).sum' http://localhost:8787 +# → 15 +``` + +## Layout + +``` +poc/cf-worker/ +├── wrangler.toml # Worker config + CompiledWasm rule +├── package.json # workers-wasi + wrangler +├── build.sh # cargo build → wizer → copy artifact +├── src/worker.js # fetch handler +├── wasm/ # build.sh writes rubyrs_worker.wasm here +└── README.md +``` + +## Knobs / next steps + +- **Streaming response**: replace the buffered stdout capture in + `worker.js` with a `TransformStream` whose readable side is + the `Response` body. Lets long-running Ruby see incremental + output. +- **CPU / memory caps**: surface `RUBYRS_DEADLINE_MS` etc. via + WASI `env`. Worker fetch handler can set a deadline below the + Worker's own 30 s CPU cap so traps come from rubyrs with + context rather than from the edge with `Error 1102`. +- **Wizer cold-start measurement**: only meaningful on the real + edge — Miniflare/`wrangler dev` does not reproduce isolate + cold-start. Deploy + `wrangler tail` to measure. +- **Static-script mode**: for a fixed-DSL deployment, replace + `request.body` with an embedded `include_str!`'d script and + pin the wasm at build time. Removes the per-request stdin + plumbing and lets the response be streamed. + +## Research notes + +- `@cloudflare/workers-wasi` does not expose a way to write into + the FS before instantiation; `preopens` is a `string[]` of + names only. Stdin is the documented input channel for + command-shape wasm — hence the bin reads from stdin. +- Local dev (`wrangler dev`) uses Miniflare v3 → `workerd`, the + same runtime as production. Module loading and the + `wasi_snapshot_preview1` shim behave identically. Cold-start + timing and the 10 ms / 30 s CPU caps are **not** enforced + locally — only on the deployed edge. +- `_start` is the entry; workers-wasi's `wasi.start(instance)` + drives it. Re-instantiating per request is the documented + pattern; V8 caches the compiled `WebAssembly.Module`. diff --git a/poc/cf-worker/build.sh b/poc/cf-worker/build.sh new file mode 100755 index 000000000..dc5c71d41 --- /dev/null +++ b/poc/cf-worker/build.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Build rubyrs.wasm for the CF Workers PoC. +# +# Pipeline: +# 1. cargo build wasm_worker bin for wasm32-wasip1, --no-default-features +# (cext requires dlopen which wasi has no equivalent for). +# 2. (Optional) wizer pre-init pass: snapshots classes + preamble +# bytecode into the wasm so cold-start on Workers doesn't burn +# the 1s top-level CPU budget re-doing that work. Skipped when +# `wizer` is not on PATH so first-time PoC contributors don't +# need to install it before seeing the round-trip work. +# 3. Copy the artifact to poc/cf-worker/wasm/ so wrangler picks +# it up via the [[rules]] CompiledWasm glob. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$WORKSPACE_ROOT" + +if [ -z "${WASI_SDK_PATH:-}" ]; then + echo "build.sh: WASI_SDK_PATH not set (needed for wasi_stub.c compile in build.rs)." >&2 + echo " Install wasi-sdk from https://github.com/WebAssembly/wasi-sdk/releases" >&2 + echo " and export WASI_SDK_PATH=/path/to/wasi-sdk-XX.0" >&2 + exit 1 +fi + +# Prefer rustup's shim over any Homebrew (or other) rustc that may +# shadow PATH — those distributions usually lack the wasm32-wasip1 +# rust-std component, and cargo errors with a misleading "target +# may not be installed" even when `rustup target add wasm32-wasip1` +# succeeded for the rustup toolchain. +if [ -x "$HOME/.cargo/bin/cargo" ]; then + export PATH="$HOME/.cargo/bin:$PATH" +fi + +if ! rustup target list --installed | grep -qx wasm32-wasip1; then + echo "build.sh: wasm32-wasip1 target missing — \`rustup target add wasm32-wasip1\`" >&2 + exit 1 +fi + +echo "[build.sh] cargo build --release --target wasm32-wasip1 --bin wasm_worker --no-default-features" +cargo build --release --target wasm32-wasip1 \ + --bin wasm_worker -p rubyrs --no-default-features + +RAW="$WORKSPACE_ROOT/target/wasm32-wasip1/release/wasm_worker.wasm" +# Final artifact lands NEXT TO src/worker.js so both wrangler and +# workerd can resolve `import "./rubyrs_worker.wasm"`. A historical +# poc/cf-worker/wasm/ location worked for wrangler (default +# CompiledWasm glob walks the project) but not for workerd, which +# rejects `..`-containing module specifiers. Co-locating is the +# minimum-friction shape that satisfies both runtimes. +OUT_DIR="$SCRIPT_DIR/src" +mkdir -p "$OUT_DIR" +OUT="$OUT_DIR/rubyrs_worker.wasm" + +# Optional wasm-opt pass. Pick the level via `WASM_OPT_LEVEL`: +# skip → no wasm-opt (default — see note below) +# -O2 → balanced speed/size +# -O3 → aggressive speed +# -Oz → aggressive size +# +# Why default = skip: rubyrs PoC measurement found that `-Oz` on +# the wasm32-wasip1 binary improves *workerd local* cold-start +# (57→27 ms with wizer) but REGRESSES V8 execution perf on +# Cloudflare Workers' edge (heavy loop 173 ms → 416 ms, +# `puts 1+1` 8 ms → 60 ms). Working hypothesis: `-Oz`'s +# aggressive size shrinks (function-deduplication, inlining +# inhibition, instruction substitution) break V8's wasm +# tier-up heuristics. Until that's debugged the conservative +# default is no opt; the env var lets benchmarks opt in. +# +# Order: wasm-opt FIRST, then wizer. wasm-opt restructures code +# (function indices, instruction layout); wizer snapshots linear +# memory at init time AFTER seeing the final code shape, so +# running it the other way around would have wasm-opt +# invalidate the snapshot's function-index references. +WIZER_IN="$RAW" +WASM_OPT_LEVEL="${WASM_OPT_LEVEL:-skip}" +if [ "$WASM_OPT_LEVEL" = "skip" ]; then + echo "[build.sh] wasm-opt skipped (WASM_OPT_LEVEL=skip)" +elif command -v wasm-opt >/dev/null 2>&1; then + OPT="$WORKSPACE_ROOT/target/wasm32-wasip1/release/wasm_worker.opt.wasm" + echo "[build.sh] wasm-opt $WASM_OPT_LEVEL" + wasm-opt "$WASM_OPT_LEVEL" --enable-bulk-memory "$RAW" -o "$OPT" + WIZER_IN="$OPT" + echo "[build.sh] $(wc -c < "$RAW") → $(wc -c < "$OPT") bytes" +else + echo "[build.sh] wasm-opt not on PATH — skipping size pass (\`brew install binaryen\`)" +fi + +if command -v wizer >/dev/null 2>&1; then + # Wizer needs --allow-wasi --wasm-bulk-memory + the binary's + # `wizer.initialize` export (lib.rs exports this). Skip if the + # export is absent so we don't fail on bins without it. + # + # Stage the objdump output to a tempfile rather than piping + # straight into `grep -q`. `grep -q` closes its stdin after + # the first match, which sends SIGPIPE upstream — under + # `set -o pipefail` (which we want everywhere else in this + # script) that turns the successful detection into a + # failure-coded pipe, and we'd silently fall through to the + # "wizer skipped" branch even when the export is present. + DUMP="$(mktemp -t rubyrs-wasm-dump.XXXXXX)" + trap 'rm -f "$DUMP"' EXIT + wasm-objdump -x "$WIZER_IN" > "$DUMP" 2>/dev/null || true + if grep -q "wizer.initialize" "$DUMP"; then + echo "[build.sh] wizer pre-init pass" + wizer --allow-wasi --wasm-bulk-memory true "$WIZER_IN" -o "$OUT" + else + echo "[build.sh] wizer skipped (no wizer.initialize export in this bin)" + cp "$WIZER_IN" "$OUT" + fi +else + echo "[build.sh] wizer not on PATH — skipping pre-init pass (\`cargo install wizer-cli\`)" + cp "$WIZER_IN" "$OUT" +fi + +echo "[build.sh] $OUT ($(wc -c < "$OUT") bytes)" diff --git a/poc/cf-worker/deno/deno.json b/poc/cf-worker/deno/deno.json new file mode 100644 index 000000000..07e4c94b8 --- /dev/null +++ b/poc/cf-worker/deno/deno.json @@ -0,0 +1,14 @@ +{ + "_comment": [ + "Scoped Deno config so deno commands run from this subdir do not", + "pick up the parent package.json (workerd, wrangler, esbuild, ...).", + "The parent's npm tree pulls a vitest/turbo/rolldown forest that", + "deno tries to materialise under --node-modules-dir=auto. An empty", + "imports block plus the local nodeModulesDir setting keeps Deno", + "self-contained to this subdir." + ], + "imports": { + "@bjorn3/browser_wasi_shim": "npm:@bjorn3/browser_wasi_shim@^0.4.2" + }, + "nodeModulesDir": "auto" +} diff --git a/poc/cf-worker/deno/deno.lock b/poc/cf-worker/deno/deno.lock new file mode 100644 index 000000000..d035fea82 --- /dev/null +++ b/poc/cf-worker/deno/deno.lock @@ -0,0 +1,16 @@ +{ + "version": "5", + "specifiers": { + "npm:@bjorn3/browser_wasi_shim@~0.4.2": "0.4.2" + }, + "npm": { + "@bjorn3/browser_wasi_shim@0.4.2": { + "integrity": "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==" + } + }, + "workspace": { + "dependencies": [ + "npm:@bjorn3/browser_wasi_shim@~0.4.2" + ] + } +} diff --git a/poc/cf-worker/deno/server.ts b/poc/cf-worker/deno/server.ts new file mode 100644 index 000000000..477426ee4 --- /dev/null +++ b/poc/cf-worker/deno/server.ts @@ -0,0 +1,139 @@ +// Deno self-host of rubyrs.wasm — third deployment target after +// CF Workers (managed) and workerd (self-host). Demonstrates that +// the same `wasm32-wasip1` artifact runs unchanged on a different +// V8 host as long as it provides a Preview 1 WASI shim. +// +// Why this exists: the broader thesis of the PoC is "one +// rubyrs.wasm bytes, run on any wasm-host edge runtime, no vendor +// lock-in". Deno is the reference example on the JS-runtime side — +// Deno Deploy : Deno :: CF Workers : workerd (managed/self-host +// duality with the same engine on both ends). +// +// Why NOT @cloudflare/workers-wasi here: it bundles `memfs.wasm` +// inside the npm package and imports it via `import wasm from +// "./memfs.wasm"`. Deno's module loader eagerly walks the wasm +// module's import section and treats `wasi_snapshot_preview1` as +// an unresolvable JS package (it's actually a wasm import to be +// supplied at instantiate time). That works under workerd's +// loader but not Deno's; the symptom is a startup ModuleNotFound +// error before the server ever listens. +// +// Why NOT jsr:@std/wasi: that module was deprecated (Oct 2023) and +// removed (Nov 2023) from Deno's std library before the JSR cut- +// over; it never shipped to JSR. The URL 404s today. +// +// We use `@bjorn3/browser_wasi_shim` — pure-JS Preview 1 shim with +// no internal wasm dep, designed for buffer-shaped stdin/stdout +// via `File`/`ConsoleStdout` fds. Same logical role as workers-wasi +// in worker.js; the differences live in the small wiring section +// below. +// +// Run: +// ./build.sh # produce src/rubyrs_worker.wasm +// deno run --allow-net --allow-read deno/server.ts +// curl -X POST --data-binary 'puts 1+1' http://localhost:8000 + +import { + ConsoleStdout, + File, + OpenFile, + WASI, +} from "@bjorn3/browser_wasi_shim"; + +// Load + compile the module once at boot. Deno's V8 caches the +// compiled `WebAssembly.Module` for the lifetime of the process +// (same shape workerd uses), so per-request cost is the +// `instantiate` + run, not the parse. +const wasmBytes = await Deno.readFile( + new URL("../src/rubyrs_worker.wasm", import.meta.url), +); +const wasmModule = await WebAssembly.compile(wasmBytes); + +// Per-isolate hit counter (parallels worker.js). A Deno process +// has one long-lived isolate, so this is effectively a request +// counter — but the header name stays consistent with the CF / +// workerd surface so the cold/warm bucketing harness works +// unchanged across all three deployment targets. +let invocations = 0; + +Deno.serve({ port: 8000 }, async (request) => { + invocations += 1; + const invocation = invocations; + + if (request.method !== "POST") { + return new Response( + "POST Ruby source as the request body (text/plain).\n", + { status: 405, headers: { "content-type": "text/plain" } }, + ); + } + + // Buffer the request body. browser_wasi_shim's `File` takes a + // Uint8Array; it does not accept a ReadableStream. For our PoC + // (small Ruby source per request) buffering is fine; for large + // / streaming inputs we'd need a custom Fd subclass that + // implements `fd_read` against the live stream. + const bodyBytes = new Uint8Array(await request.arrayBuffer()); + + // Capture stdout / stderr via ConsoleStdout's callback hook. + // Same buffer-then-respond pattern as worker.js — we need the + // exit code before deciding the HTTP status. + const stdoutChunks: Uint8Array[] = []; + const stderrChunks: Uint8Array[] = []; + + // fds array maps to WASI file descriptors in order: 0=stdin, + // 1=stdout, 2=stderr, then anything else (preopens, etc.). We + // wrap stdin as a read-only OpenFile over the request body so + // rubyrs's `io::stdin().read_to_string(...)` pulls the Ruby + // source out byte-for-byte. + const wasi = new WASI( + ["rubyrs_worker"], // argv + [], // env + [ + new OpenFile(new File(bodyBytes)), // fd 0: stdin + ConsoleStdout.lineBuffered((line: string) => { // fd 1: stdout + stdoutChunks.push(new TextEncoder().encode(line + "\n")); + }), + ConsoleStdout.lineBuffered((line: string) => { // fd 2: stderr + stderrChunks.push(new TextEncoder().encode(line + "\n")); + }), + ], + ); + + // browser_wasi_shim exposes the import object as `wasiImport`, + // same name as workers-wasi. + const instance = await WebAssembly.instantiate(wasmModule, { + wasi_snapshot_preview1: wasi.wasiImport, + }) as WebAssembly.Instance; + + // command-shape wasm exports `_start`. browser_wasi_shim's + // `start()` throws `WASIProcExit` on `proc_exit`; catch it so a + // Ruby trap (exit 1) doesn't crash the server. CF / Node also + // route exits through a thrown sentinel (different class names), + // but we don't need the host's class — just `code`-shaped exit. + let exitCode = 0; + try { + wasi.start(instance); + } catch (e) { + if (e && typeof e === "object" && "code" in e) { + exitCode = (e as { code: number }).code; + } else { + throw e; + } + } + + const decoder = new TextDecoder(); + const stdout = stdoutChunks.map((c) => decoder.decode(c)).join(""); + const stderr = stderrChunks.map((c) => decoder.decode(c)).join(""); + + const headers = { + "content-type": "text/plain", + "x-rubyrs-invocation": String(invocation), + }; + if (exitCode !== 0) { + return new Response( + `rubyrs exited ${exitCode}\n\n${stderr || stdout}`, + { status: 500, headers }, + ); + } + return new Response(stdout, { status: 200, headers }); +}); diff --git a/poc/cf-worker/package-lock.json b/poc/cf-worker/package-lock.json new file mode 100644 index 000000000..72c7ec6f9 --- /dev/null +++ b/poc/cf-worker/package-lock.json @@ -0,0 +1,1805 @@ +{ + "name": "rubyrs-cf-poc", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rubyrs-cf-poc", + "version": "0.0.0", + "dependencies": { + "@cloudflare/workers-wasi": "^0.0.5" + }, + "devDependencies": { + "workerd": "^1.20260526.1", + "wrangler": "^3.0.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260526.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260526.1.tgz", + "integrity": "sha512-/pR3GH3gfv0PUp7DjI8v0aAIDOqFwibq4bg5xT7TZgcVdBV/cJQWckdXCMqiRtHiawLwogUX00EIOINkYJ1Zqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260526.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260526.1.tgz", + "integrity": "sha512-rcyu0iANYfaiezKh3Mcao1O4IIgVfQldxduiL5TZT1sP0NIeRY4YReSTrzPxNnXxSYaIqaqRHMcHbUM/ic4knA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260526.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260526.1.tgz", + "integrity": "sha512-5EZAEnlLwa9oGJRo8Nd3iY5Wcd9ROGNNG90xNIGp8MEjj8v2jTn42NC47fCZKFdnLj3+S+vWEhu1x0GVJnALjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260526.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260526.1.tgz", + "integrity": "sha512-X/YBQXeXFeCN7QTStoWrATEBc9WKl7PIqkw/dQkjyJ72gh3rkLe0+Xkzp3wO7gtxTDQMa7NPGy1W4+sdMf8q1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260526.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260526.1.tgz", + "integrity": "sha512-R+tqpFFdcfZIljx8fIW9rj9fRTtDgfoA2yonsfAGa6e8snrmr+38mdFHtkRC0D3UyZpn/hOtmXiUBfdX2gMR7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-wasi": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-wasi/-/workers-wasi-0.0.5.tgz", + "integrity": "sha512-Gxu2tt2YY8tRgN7vfY8mSW0Md5wUj5+gb5eYrqsGRM+qJn9jx+ButL6BteLluDe5vlEkxQ69LagEMHjE58O7iQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "3.20250718.3", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", + "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250718.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stacktracey": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", + "ufo": "^1.5.4" + } + }, + "node_modules/workerd": { + "version": "1.20260526.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260526.1.tgz", + "integrity": "sha512-IHzymht98p10JH1zzwdCpbViAqw97HrwKl7+KfZeASFMsYSrIsAULWdPn0LRC5FTUzBpamLNyKCCKxbgXHgRHQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260526.1", + "@cloudflare/workerd-darwin-arm64": "1.20260526.1", + "@cloudflare/workerd-linux-64": "1.20260526.1", + "@cloudflare/workerd-linux-arm64": "1.20260526.1", + "@cloudflare/workerd-windows-64": "1.20260526.1" + } + }, + "node_modules/wrangler": { + "version": "3.114.17", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", + "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@cloudflare/unenv-preset": "2.0.2", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.17.19", + "miniflare": "3.20250718.3", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.14", + "workerd": "1.20250718.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250408.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/poc/cf-worker/package.json b/poc/cf-worker/package.json new file mode 100644 index 000000000..b7ea19e4b --- /dev/null +++ b/poc/cf-worker/package.json @@ -0,0 +1,18 @@ +{ + "name": "rubyrs-cf-poc", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "./build.sh", + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "dependencies": { + "@cloudflare/workers-wasi": "^0.0.5" + }, + "devDependencies": { + "workerd": "^1.20260526.1", + "wrangler": "^3.0.0" + } +} diff --git a/poc/cf-worker/src/worker.js b/poc/cf-worker/src/worker.js new file mode 100644 index 000000000..b3ad708d7 --- /dev/null +++ b/poc/cf-worker/src/worker.js @@ -0,0 +1,105 @@ +// PoC Worker: pipe HTTP request body → wasm stdin → HTTP response. +// +// Shape: +// POST / with `text/plain` body containing Ruby source +// → 200 with the Ruby script's stdout as the response body +// → 500 with the trap message on Ruby-side error +// +// V8 caches the compiled `WebAssembly.Module` across requests in +// the same isolate (Cloudflare confirms this in the runtime-apis +// docs), so the per-request cost is `WebAssembly.instantiate` + +// stdio plumbing, not parse/compile. The first request in a cold +// isolate pays the parse cost — that's the only number that has +// to be measured on the real edge (Miniflare/`wrangler dev` does +// not faithfully simulate isolate cold-starts). +// +// References: +// - @cloudflare/workers-wasi: https://github.com/cloudflare/workers-wasi +// - WASM module bindings: https://developers.cloudflare.com/workers/runtime-apis/webassembly/ +import { WASI } from "@cloudflare/workers-wasi"; +// Module specifier resolved by: +// - wrangler: walks `./` from worker.js's location (src/) at +// bundle time, so the wasm must sit alongside this +// file in src/. build.sh copies it there. +// - workerd: capnp `modules` entry uses the same `./` name +// verbatim. workerd accepts `./` (same-directory +// relative) but rejects `..` (breakout) and bare +// specifiers (wrangler resolves those via the +// default CompiledWasm rule that workerd doesn't +// replicate). +import wasmModule from "./rubyrs_worker.wasm"; + +// Per-isolate hit counter used to distinguish "first request after +// V8 isolate spawn" (cold) from subsequent reuses (warm). The +// distinction is invisible from outside — Cloudflare's edge +// load-balances opaquely across isolates and the public docs only +// give a blended mean cold-start. Burying a counter in module +// scope is the lightest-touch way to recover the partition: this +// variable is initialised exactly once per V8 isolate (when +// workerd first instantiates the JS module) and lives until that +// isolate is evicted. Wrap the response with the counter via the +// `x-rubyrs-invocation` header so the measurement harness can +// bucket requests as invocation==1 vs >1. +let isolateInvocations = 0; + +export default { + async fetch(request) { + isolateInvocations += 1; + const invocation = isolateInvocations; + + if (request.method !== "POST") { + return new Response( + "POST Ruby source as the request body (text/plain).", + { status: 405, headers: { "content-type": "text/plain" } }, + ); + } + + // Capture stdout / stderr via a WritableStream sink. We can't + // hand the Worker's outgoing Response stream directly to wasi + // here because we need to await wasi.start() to know whether + // the script trapped before deciding on the status code; the + // straightforward shape is "buffer, then respond". For + // streaming responses, swap to a TransformStream and pipe its + // readable side into the Response — that's a follow-up once + // the round-trip works. + const stdoutChunks = []; + const stderrChunks = []; + const collect = (sink) => new WritableStream({ + write(chunk) { sink.push(chunk); }, + }); + + const wasi = new WASI({ + args: ["rubyrs_worker"], + env: {}, + stdin: request.body, // pipe request body straight in + stdout: collect(stdoutChunks), + stderr: collect(stderrChunks), + returnOnExit: true, // don't throw ProcessExit + }); + + const instance = await WebAssembly.instantiate(wasmModule, { + wasi_snapshot_preview1: wasi.wasiImport, + }); + + const exitCode = await wasi.start(instance); + + const decoder = new TextDecoder(); + const stdout = stdoutChunks.map((c) => decoder.decode(c)).join(""); + const stderr = stderrChunks.map((c) => decoder.decode(c)).join(""); + + // Attach the per-isolate invocation count to every response so + // the harness can bucket cold (1) vs warm (>1) without needing + // wrangler tail. Header is safe for both 200 and 500 paths. + const headers = { + "content-type": "text/plain", + "x-rubyrs-invocation": String(invocation), + }; + if (exitCode && exitCode !== 0) { + return new Response( + `rubyrs exited ${exitCode}\n\n${stderr || stdout}`, + { status: 500, headers }, + ); + } + return new Response(stdout, { status: 200, headers }); + }, +}; diff --git a/poc/cf-worker/workerd/bench-js/config.capnp b/poc/cf-worker/workerd/bench-js/config.capnp new file mode 100644 index 000000000..a05eafd0c --- /dev/null +++ b/poc/cf-worker/workerd/bench-js/config.capnp @@ -0,0 +1,17 @@ +# JS-only baseline — measures workerd process startup + V8 +# isolate spawn cost. Used to attribute the rubyrs worker's +# cold-start time to wasm/Ruby overhead vs workerd's own +# inherent setup. +using Workerd = import "/workerd/workerd.capnp"; + +const config :Workerd.Config = ( + services = [ ( name = "main", worker = .helloWorker ) ], + sockets = [ ( name = "http", address = "*:8081", http = (), service = "main" ) ] +); + +const helloWorker :Workerd.Worker = ( + modules = [ + ( name = "hello.mjs", esModule = embed "./hello.mjs" ), + ], + compatibilityDate = "2025-11-01", +); diff --git a/poc/cf-worker/workerd/bench-js/hello.mjs b/poc/cf-worker/workerd/bench-js/hello.mjs new file mode 100644 index 000000000..71db3a04b --- /dev/null +++ b/poc/cf-worker/workerd/bench-js/hello.mjs @@ -0,0 +1,10 @@ +// Minimal JS-only Worker — baselines pure workerd process +// startup + V8 isolate spawn + fetch-handler entry cost. No +// wasm, no WASI shim, no rubyrs. Difference vs the rubyrs +// worker's first-request wall-time attributes the gap to +// wasm parse/compile + Runtime construction. +export default { + async fetch() { + return new Response("hi\n", { status: 200 }); + }, +}; diff --git a/poc/cf-worker/workerd/bundle.sh b/poc/cf-worker/workerd/bundle.sh new file mode 100755 index 000000000..c445421af --- /dev/null +++ b/poc/cf-worker/workerd/bundle.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Bundle src/worker.js + its @cloudflare/workers-wasi dependency +# into a single ES module that workerd's capnp module list can +# `embed` directly. Wrangler does this implicitly; workerd +# standalone needs every dep declared, and bundling sidesteps +# the need to enumerate workers-wasi's internal modules. +# +# `--external:*.wasm` keeps the `import wasmModule from +# "../wasm/rubyrs_worker.wasm"` line as a bare specifier so +# workerd's capnp `wasm =` entry resolves it instead of esbuild +# trying to inline a 1.4MB Uint8Array literal into the .mjs. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +POC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$POC_DIR" + +# workers-wasi pulls in its OWN memfs.wasm via a bare `import wasm +# from "./memfs.wasm"`. We mark wasm imports external so esbuild +# emits the same shape; the capnp config lists both .wasm modules. +echo "[bundle.sh] esbuild src/worker.js → workerd/dist/worker.mjs" +node_modules/.bin/esbuild src/worker.js \ + --bundle \ + --format=esm \ + --target=esnext \ + --external:*.wasm \ + --outfile=workerd/dist/worker.mjs \ + --log-level=warning + +echo "[bundle.sh] $(wc -c < workerd/dist/worker.mjs) bytes" diff --git a/poc/cf-worker/workerd/config.capnp b/poc/cf-worker/workerd/config.capnp new file mode 100644 index 000000000..ff59acc70 --- /dev/null +++ b/poc/cf-worker/workerd/config.capnp @@ -0,0 +1,48 @@ +# workerd standalone config — runs the rubyrs Worker without +# Cloudflare's edge, without an account, and without CPU/memory +# caps. Same V8 + wasm engine as the CF edge runtime, so a +# `workerd serve` smoke is an apples-to-apples comparison +# against `wrangler dev` (which itself wraps workerd via +# Miniflare). +# +# Run from the poc/cf-worker/ directory: +# ./workerd/bundle.sh # esbuild → workerd/dist/worker.mjs +# ./build.sh # cargo + wizer → wasm/rubyrs_worker.wasm +# npx workerd serve workerd/config.capnp +# +# Module-name design: esbuild leaves the two `import` specifiers +# bare (`./memfs.wasm` for workers-wasi's internal littlefs, and +# `../wasm/rubyrs_worker.wasm` for ours, both relative to the +# bundled `worker.mjs`'s nominal location at `src/`). The +# `modules` entries below carry exactly those names so workerd's +# import resolver finds them at instantiation time. + +using Workerd = import "/workerd/workerd.capnp"; + +const config :Workerd.Config = ( + services = [ ( name = "main", worker = .rubyrsWorker ) ], + sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ] +); + +const rubyrsWorker :Workerd.Worker = ( + modules = [ + ( name = "worker.mjs", + esModule = embed "./dist/worker.mjs" ), + + # workers-wasi's bundled littlefs implementation. The bundled + # worker.mjs imports it as `"./memfs.wasm"` — esbuild kept + # the specifier bare because of `--external:*.wasm`. + ( name = "./memfs.wasm", + wasm = embed "../node_modules/@cloudflare/workers-wasi/dist/memfs.wasm" ), + + # rubyrs_worker (wasm32-wasip1, --no-default-features). Built + # by ../build.sh from crates/rubyrs/src/bin/wasm_worker.rs. + # `./` prefix matches what `worker.js` imports and is allowed + # by workerd's directory-breakout sanity check; the embed + # path is host-side and is allowed to traverse upward to + # reach the build output. + ( name = "./rubyrs_worker.wasm", + wasm = embed "../src/rubyrs_worker.wasm" ), + ], + compatibilityDate = "2025-11-01", +); diff --git a/poc/cf-worker/wrangler.toml b/poc/cf-worker/wrangler.toml new file mode 100644 index 000000000..e115dfeee --- /dev/null +++ b/poc/cf-worker/wrangler.toml @@ -0,0 +1,16 @@ +# PoC: rubyrs.wasm on Cloudflare Workers. +# +# Pin compatibility_date — Workers behaviour can shift on flag- +# gated features (notably wasm-related ones); pinning makes the +# `wrangler dev` ↔ `wrangler deploy` shape reproducible. +name = "rubyrs-poc" +main = "src/worker.js" +compatibility_date = "2025-11-01" + +# No `[[rules]]` block needed: wrangler v3+ ships a default +# CompiledWasm rule matching `**/*.wasm` (and `**/*.wasm?module`) +# with `fallthrough = false`, so adding our own re-declaration +# fails the build with "ignored because a previous rule with the +# same type was not marked as `fallthrough = true`". Workers-wasi +# also imports a `memfs.wasm` from inside its own package; the +# default rule handles both that and our `wasm/rubyrs_worker.wasm`.