Embed in Go · Self-host Workers · npm CLI

A JS/TS runtime with soundness-gated AOT native compilation.

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.

install · quickstart
MIT · npm · Go 1.26+ · TypeScript 7 Beta (tsgo)
01
Run TS from CLI
npm i -g @ramunejs/cli → ramune run hello.ts. One binary with run · test · compile · check · fmt · lint · repl, --sandbox, and --docker for untrusted code. Go toolchain optional.
02
Sandbox & permissions
Layered: language-level deny-all + allow-lists on every backend, plus WASM VM isolation via wazero when built with -tags qjswasm. Docker is an optional extra layer.
03
Embed in Go
Call any Go lib from JS. Same API across all three backends, zero Cgo at build time.
04
Self-host Workers
export default { fetch }. Compile handler + runtime into a single Go binary; run on your VM, bare metal, or scratch container.
05
Terminal UI
Ramune.tui: Bubbletea + Lipgloss + glamour, authored in TSX. Elm-style init/update/view; components, block layout, chord keymap; runs locally or over SSH via wish.
§ IQuickstart

Try it from the CLI
in one line.

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.

-tags qjswasm
Build-time: JS runs inside a wazero WASM VM — no host syscalls by default. Complements (doesn't replace) --sandbox.
--sandbox
Runtime: deny read / write / net / env / subprocess by default.
--allow-{read,write,net,env,run}
Comma-separated allowlists; empty value grants all.
ramune.toml
[permissions] — commit the policy next to your handler.
Go options
WithPermissions · WithResourceLimits · NewSandboxRuntime — identical knobs from Go.
--docker · --docker-memory
Optional extra layer: wrap the script in a Docker container.
# 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.ts
§ IIThree backends, one API

Pick your tradeoff at build time.

Handler 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.

# CLI install
$ go install -tags qjswasm github.com/i2y/ramune/cmd/ramune@latest
# single-binary compile
$ ramune compile app.ts --tags qjswasm -o myapp
# embed in your Go program
$ go build -tags qjswasm ./...
Backend
JSC
default
qjswasm
-tags qjswasm
goja
-tags goja
EngineJavaScriptCore (purego)QuickJS-NG on wazerodop251/goja
JIT
PlatformsmacOS · LinuxmacOS · Linux · WindowsmacOS · Linux · Windows
System depsmac: none · linux: libjscnonenone
Spec coverageES2023ES2023ES2023 eff.
Best forthroughput · HTTPportability · scratch imgdrop-in · pure Go
§ IIIHybrid compilation

Extract typed
functions to 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).

Picker
Soundness-gated: signature + body AST must be statically equivalent to Go. No runtime profiling.
Codegen
.go files emitted alongside app.ts; go build links them into the compiled binary
Rejected
Keep running on the JS floor at the same speed as a JS-only build
Biggest wins
qjswasm / goja (no JS JIT): typically 10×–350× on extractable kernels
fib(30) on qjswasm: 186 ms → 2 ms · 92×
// 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 skipped
§ IVPerformance

60× over goja on CPU. 62k req/s on multi-runtime pool.

Apple 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.

Fibonacci(35) · ms, lower better
Ramune JSC+JIT
35 ms
Ramune qjswasm
1,987 ms
Ramune goja
2,400 ms
otto
26,413 ms
RuntimePool JSC · req/s, higher better
1 worker
40,511
2 workers
54,500
3 workers
58,401
4 workers
59,706
5 workers
60,913
6 workers
62,407

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.

Hybrid extraction · ramune compile --hybrid
fib(30) · ms, lower better (examples/hybrid/)
qjswasm · JS-only
185.8 ms
qjswasm · --hybrid92×
2.0 ms
JSC+JIT · JS-only
3.1 ms
JSC+JIT · --hybrid1.55×
2.0 ms

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.

Fibonacci(40) wall-clock vs Bun / Node — Apple M4 Max, hyperfine (bench/run.sh)
node fib.js
535 ms
bun run fib.js
364 ms
compile (JS)
358 ms
compile --hybrid1.5×
243 ms

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.

TinyGo standalone WASM · ramune compile --target wasm-wasi

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
Built on
JavaScriptCore · QuickJS-NG · goja · wazero · fastschema/qjs · purego · esbuild · typescript-go · rslint