Three backends behind one API — JSC+JIT on macOS/Linux, pure-Go portability to Windows (QuickJS-NG on wazero, goja). Embed in Go, or self-host Cloudflare Workers-style handlers on your own infrastructure.
One go install and you have run · test · repl · check · fmt · lint · compile · serve. Sandboxing is layered — language-level deny-all / allow-lists on every backend, plus WASM VM isolation via wazero when you build with -tags qjswasm (JS has no host syscalls by default). Same knobs from CLI flags, ramune.toml, or Go options.
# easiest: prebuilt binaries via npm, no Go toolchain required
$ npm install -g @ramunejs/cli
# or via Go, for JSC+JIT on Linux or custom build tags
$ go install github.com/i2y/ramune/cmd/ramune@latest
$ go install github.com/i2y/ramune/cmd/ramune-toolchain@latest
$ ramune setup-jit # macOS: enable JIT (~10× faster)
# stronger isolation: build with -tags qjswasm → JS runs
# inside a wazero WASM VM (no host syscalls by default)
$ go install -tags qjswasm github.com/i2y/ramune/cmd/ramune@latest
# run a .ts file
$ ramune run hello.ts
# untrusted code: language-level deny-all + allowlists
$ ramune run --sandbox \
--allow-net example.com:443 \
--allow-read ./data \
untrusted.ts
# commit the policy next to your handler
$ cat ramune.toml
[permissions]
net = "allow"
net_hosts = ["example.com:443"]
read = "allow"
read_paths = ["./data"]
# optional extra layer: Docker
$ ramune run --docker --docker-memory 256 untrusted.tsHandler code and Go interop stay identical. The same build tag picks the backend everywhere — install the CLI, compile a single-binary app, or embed the runtime in your own Go program.
| Backend | JSC default | qjswasm -tags qjswasm | goja -tags goja |
|---|---|---|---|
| Engine | JavaScriptCore (purego) | QuickJS-NG on wazero | dop251/goja |
| JIT | ✓ | — | — |
| Platforms | macOS · Linux | macOS · Linux · Windows | macOS · Linux · Windows |
| System deps | mac: none · linux: libjsc | none | none |
| Spec coverage | ES2023 | ES2023 | ES2023 eff. |
| Best for | throughput · HTTP | portability · scratch img | drop-in · pure Go |
ramune compile --hybrid statically picks every function whose signature and body are provably semantically equivalent in Go, emits the Go source, and links it into the same binary. Rejected functions keep running on the JS floor — adding --hybrid never breaks correctness. Biggest wins on no-JIT pure-Go backends (qjswasm, goja).
// app.ts — no annotations, no new syntax
export function fib(n: number): number {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
// $ ramune compile app.ts --hybrid -o myapp
// extracted function fib
// skipped function bucketSign [object-type]
// 1 extracted, 1 skippedApple M4 Max, JSC backend (the default). Fib(35) vs other Go-embedded JS runtimes and RuntimePool req/s scaling on a JSON handler (wrk -t4 -c100 -d10s). Reproduce with make bench-go.
Pure-Go scaling story: qjswasm goes 2,348 → 13,435 req/s from 1 → 6 workers (5.72×, near-linear), the best multiplicative scaling among pure-Go backends. JSC wins on absolute throughput; qjswasm wins on scaling factor and Windows/scratch-image portability.
The picker proves semantic equivalence, not speed. On qjswasm (no JS JIT), recursion-heavy kernels like fib go near-native — 92× here. On JSC+JIT the JIT already specialises integer-typed JS aggressively, so hybrid is a targeted tool: some kernels regress (countPrimes 60× slower when integer modulo dominates; sumSquares over a 1000-element array 103× slower when marshalling dominates). Rule of thumb: extract recursive / loop kernels with primitive arguments; leave array-arg APIs and tight methods on the JS floor. Run with --hybrid-report to see what got extracted and measure.
Wall-clock CLI bench against the Bun and Node binaries on the same source — fib(40) recursive numerics. ramune compile --hybrid lands at 243 ms vs 358 ms for the JS-only compile, 364 ms for Bun, 535 ms for Node. Compute-only (subtract ~16 ms binary startup): 227 ms vs 357 ms (Bun) and 519 ms (Node) — Bun's startup advantage from the Zig launcher fades once the workload dominates. The extracted Go is what the picker emits verbatim; this is the same win available to any TS app whose hot kernel is recursive or loop-heavy with primitive arguments.
The same picker pipeline can target TinyGo instead of go, producing a self-contained WASI reactor (.wasm, ~110-320 KB) of the extracted Go alone — no JS runtime, no ramune host bundled. Numerics-only signatures get //go:wasmexport wrappers callable from any wasm host (wazero, wasmtime, browser-via-wasi-shim) after _initialize. Verified: add(2.5, 3.25)=5.75 and fib(10)=55 callable from a Go host through wazero's ExportedFunction lookup.
$ ramune compile --target wasm-wasi -o app.wasm app.ts
# → 320 KB self-contained WASI reactor, no JS runtime
# → exports (call after _initialize):
# fib(float64) float64