ramune

package module
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 22, 2026 License: MIT Imports: 73 Imported by: 3

README

Ramune

Ramune

A JavaScript/TypeScript runtime you embed in Go. Cloudflare Workers-style fetch handlers that run on your own infrastructure.

Ramune solves four concrete problems:

  • "I'm building a Go service and I want my users to write custom logic in JS/TS." Until now the options were goja (ES2017-ish, reflection-based) or otto (an order of magnitude slower). Ramune is a drop-in with the same import-once ergonomics, choosing between JIT-accelerated JSC, pure-Go QuickJS-NG on wazero, or goja — all behind one API.
  • "I want my customers to upload JS code that my SaaS runs on their behalf." Ramune is a self-ownable substrate for this shape: WithPermissions(SandboxPermissions()) denies I/O by default, WithResourceLimits caps JS memory/stack/GC on the qjswasm backend, DBBackend / KVBackend / WithExtraEnvJS define what env.* each tenant can reach, and qjswasm under sandbox additionally closes its WASI FS mount so VM escapes can't pivot to host files. Direct comparables — Cloudflare Workers for Platforms, Deno Subhosting — are managed SaaS. Ramune runs in your process, on your hardware, with your env design.
  • "I like the Cloudflare Workers model but I don't want vendor lock-in — or I need to run air-gapped." Handlers written in the Cloudflare Workers shape — export default { fetch, scheduled } — run on your VM, bare metal, or FROM scratch container. env.KV / env.DB are Go interfaces; swap for Redis / Postgres / DynamoDB / anything. (Shape only — Durable Objects, R2, and other Cloudflare-runtime-specific bindings are not implemented.)
  • "I just want a fast JS/TS runtime." Use Bun or Deno — that's not our main battlefield. Ramune ships run / test / repl / check / fmt / lint / compile and is competitive in single-process benchmarks (Node-equivalent HTTP, 1.3× faster than Node on CPU-fib on our M4 Max), but raw CLI speed is not where Ramune's value lives. Use it here if (1)-(3) are your primary reason and you want one less binary to install.
// Embed in Go — user JS calls existing Go services
rt, _ := ramune.New()
defer rt.Close()
rt.RegisterFunc("queryDB", func(args []any) (any, error) {
    return myDB.Query(args[0].(string))
})
rt.Eval(`queryDB("SELECT 1")`)
// Workers-style handler, self-hosted
export default {
  async fetch(request, env, ctx) {
    const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?")
      .bind(request.headers.get("x-user-id")).first();
    return Response.json(user);
  },
};
ramune serve worker.ts                 # dev / production serve
ramune compile worker.ts -o myworker   # bundle handler + runtime into one Go binary

Who Ramune is for

Five use cases, five audiences. Jump to the section that matches your motivation.

1. Embed a JS engine in a Go program. Competitors: goja, otto. Ramune's goja backend (-tags goja) is a drop-in replacement for existing goja users, with esbuild auto-lowering so modern JS syntax works. Switch to -tags qjswasm (pure-Go QuickJS-NG on wazero, ES2023) or the default JSC backend (JIT, 60×+ faster than goja on CPU-bound JS). Same API across all three — swap at build time to trade off startup vs throughput vs platform reach. Call any Go library from JS via RegisterFunc; expose typed Go functions as require()-able modules via NativeModuleFromFuncs. → see Embed in Go

2. Build a platform where customers run JS on your service. The self-ownable counterpart to Cloudflare Workers for Platforms / Deno Subhosting. Multi-tenant isolation via RuntimePool (N independent JS VMs per process, round-robined at HTTP layer), layered defense-in-depth sandbox via qjswasm's WASM linear memory + SandboxPermissions() (auto-disables WASI FS mount) + WithResourceLimits (memory/stack/GC caps at the QuickJS-NG level) + permission-gated Go bridges, per-tenant env.* via pluggable KVBackend / DBBackend. Your customers write code in the Workers fetch-handler shape. You own the data plane. → see In-process Sandbox for Untrusted JS

3. Self-host Cloudflare Workers-style handlers. You have export default { fetch, scheduled } handlers and want them running on your VM, bare metal, or FROM scratch Docker. ramune serve worker.ts or ramune compile worker.ts -o myworker — single binary, no Wrangler, no Dockerfile, no Node. Default surface covers fetch / env.KV / env.DB / env.SECRETS / ctx.waitUntil / scheduled / cron / WinterCG; see the Workers serve section for the full scope (what ships, what's user-supplied, what's still partial).

4. Run JS/TS from the command line. Not our main battlefield — Bun and Deno are faster for pure CLI use. But Ramune ships ramune run / test / check / fmt / lint / repl / compile with tsgo + rslint + esbuild built in, and is competitive (Node-equivalent HTTP, 1.3× faster than Node on CPU-fib). → see Quick Start

5. You don't know Go. npm install -g @ramunejs/cli ships prebuilt binaries — no Go toolchain, no go install. Self-host Workers handlers (#3), run / test / type-check / fmt / lint / REPL TS/JS (#4), ramune compile to a single binary — all accessible from the npm install. Only the embed-in-Go API (#1) and custom build tags still want the Go toolchain. → see Install via npm

Key capabilities

  • Design your own env. env.KV / env.DB are Go interfaces (KVBackend, DBBackend) — plug Redis, Postgres, DynamoDB, in-memory, anything. Invent new bindings (env.QUEUE / env.EMAIL / env.AI / env.R2 …) by registering a Go callback plus a tiny JS facade via WithExtraEnvJS. Walkthrough and runnable example: workers/BINDINGS.md, examples/workers/custom-binding/.
  • Auto-compile typed hot paths to native Go. ramune compile --hybrid walks every user TS file in the app and extracts any function or pure class whose signature and body are provably semantics-equivalent to Go — the rest stays on the JS floor. No hand-fixes, no silent failures; functions that don't qualify just keep running as JS. Biggest wins land on the no-JIT backends (qjswasm, goja) where extractable CPU-heavy kernels typically go 10×-350× faster than JS-only. On JSC+JIT the gains are narrower and pattern-dependent (recursion / control-flow wins; tight integer-modulo loops and array-arg marshalling can regress). See examples/hybrid/, examples/hybrid-hono/, and the --hybrid section below.
  • Single-binary deploy. ramune compile worker.ts -o myworker bundles handler + runtime into one Go executable. No Kubernetes, no Wrangler, no Dockerfile required — scp ./myworker prod: and run. qjswasm path is fully self-contained; JSC path still resolves the system JSC at run time (see next bullet).
  • No Cgo at build; honest about runtime. go build cross-compiles to any GOOS/GOARCH without a C toolchain. JSC backend loads JavaScriptCore dynamically via purego — zero install on macOS, libjavascriptcoregtk-4.1 on Linux. qjswasm is pure Go with zero runtime dependencies (QuickJS-NG compiled to WebAssembly, embedded into the Go binary, driven by wazero) and runs on FROM scratch Docker.

Tri-backend: JavaScriptCore (JIT, macOS/Linux) via purego, qjswasm (pure Go, cross-platform incl. Windows — QuickJS-NG compiled to WebAssembly and driven by wazero) via fastschema/qjs, and goja (pure Go, reflect-based, ~94% ECMAScript) via github.com/dop251/goja — no Cgo required for any of them. Type checker and formatter (typescript-go), linter (rslint), bundler (esbuild), and all Node.js polyfills are built in with zero external tool dependencies.

ramune serve worker.ts        # Serve Workers-style module
ramune run server.ts          # Run TypeScript (classic)
ramune test                   # Run tests
ramune check app.ts           # Type-check
ramune fmt .                  # Format
ramune lint .                 # Lint
ramune compile app.ts -o app  # Compile to standalone binary
ramune transpile main.ts -o out  # Transpile TS to Go source
ramune typegen go:fmt go:net/http -o go.d.ts  # Generate .d.ts for Go packages
ramune skills install         # Install Agent Skills for AI agents

Three backends, same API:

JSC (default) qjswasm (-tags qjswasm) goja (-tags goja)
Engine Apple JavaScriptCore via purego fastschema/qjs — QuickJS-NG on wazero (pure Go) dop251/goja (pure Go, reflect-based)
JIT Yes No No
Platforms macOS, Linux macOS, Linux, Windows macOS, Linux, Windows
System deps macOS: none. Linux: libjavascriptcoregtk None None
Spec coverage ES2023 ES2023 ES2023 effective (goja native ~ES2017; esbuild lowers newer syntax transparently on Eval)
Best for Performance, HTTP servers Embedding, scripting, portability Pure-Go embedding, Windows-native, no cgo signal forwarding

All three are pure Go at build time: go build needs no C toolchain. Runtime deps differ: JSC resolves the system JavaScriptCore (zero install on macOS, libjavascriptcoregtk-4.1 on Linux); qjswasm and goja have none.

For AI coding agents: ramune skills install adds an Agent Skill that teaches Claude Code, GitHub Copilot, and similar tools how to use Ramune's APIs and CLI.

Install

Via npm

No Go toolchain required.

npm install -g @ramunejs/cli

Prebuilt binaries for macOS arm64, Linux x64/arm64, and Windows x64/arm64; the correct one is resolved via optionalDependencies. macOS ships JavaScriptCore with JIT enabled (ad-hoc codesigned with the required entitlement in CI); if a JIT error ever shows up, run ramune setup-jit. Linux and Windows use the QuickJS-NG (qjswasm) backend — zero host dependencies and FROM scratch compatibility, at the cost of JSC-level CPU throughput. For JavaScriptCore throughput on Linux, use go install (see below).

Via go install

Use go install for JavaScriptCore throughput on Linux, non-default build tags, a smaller binary, or to embed the Go API directly.

macOS

JavaScriptCore is built into macOS — no extra dependencies.

go install github.com/i2y/ramune/cmd/ramune@latest
go install github.com/i2y/ramune/cmd/ramune-toolchain@latest  # for check / fmt / lint / compile / transpile / typegen
ramune setup-jit   # enable JIT (~10x faster, recommended)
Linux
sudo apt install libjavascriptcoregtk-4.1-dev   # JSC runtime (required)
go install github.com/i2y/ramune/cmd/ramune@latest
go install github.com/i2y/ramune/cmd/ramune-toolchain@latest  # for check / fmt / lint / compile / transpile / typegen

Multi-runtime (RuntimePool, worker_threads) works out of the box on x86_64. On arm64, gcc is required for cgo signal forwarding (apt install gcc).

Windows / Zero-dependency (qjswasm backend)
go install -tags qjswasm github.com/i2y/ramune/cmd/ramune@latest
go install -tags qjswasm github.com/i2y/ramune/cmd/ramune-toolchain@latest  # optional: check / fmt / lint / compile

The qjswasm backend uses fastschema/qjs — QuickJS-NG compiled to WebAssembly and driven by wazero's compiler-mode JIT (AOT WASM→native). Pure Go, ES2023, no shared libraries — works on Windows, macOS, and Linux. Trade-off: no JS JIT, so CPU-bound code is slower than JSC (see Performance).

Goja backend (-tags goja, pure Go, reflect-based)
go install -tags goja github.com/i2y/ramune/cmd/ramune@latest

The goja backend wraps dop251/goja unchanged, so it's a drop-in for existing goja users: scripts and Go interop code that run on goja directly run on Ramune with -tags goja with no behavioral change, and can later switch to -tags qjswasm or the default JSC build to gain throughput without touching the handler code. goja is a reflection-based Go JS interpreter with ~94% ECMAScript coverage. Appropriate when you want pure-Go embedding on Windows without any shared libraries and want to avoid the cgo signal-forwarding requirement that JSC needs on Linux/arm64. Modern JS syntax that goja's parser rejects (private class fields, top-level await, Object.hasOwn, logical assignment, etc.) is transparently lowered to ES2017 via esbuild on first-encounter parse failure in Runtime.Eval / Runtime.Exec — both CLI and library paths see the same effective ES2023 surface, and the lowered result is cached so repeated source is amortized.

Smaller binary
go install -tags nosqlite -ldflags="-s -w" github.com/i2y/ramune/cmd/ramune@latest

The main ramune binary above is ~30MB and holds the runtime; ramune-toolchain (~60MB) is a separate development-only binary for check / fmt / lint / compile / transpile / typegen. If you only need ramune run / serve / eval / repl / test, you can skip installing ramune-toolchain entirely.

-tags nosqlite excludes bun:sqlite. -ldflags="-s -w" strips debug info. Combine with -tags qjswasm,nosqlite for the smallest possible binary.

Quick Start

Run JavaScript/TypeScript
ramune run app.ts
ramune run -p lodash -p dayjs app.ts   # with npm packages
ramune run                              # reads package.json
ramune run -w server.ts                 # watch mode
ramune run --workers 4 server.ts        # multi-worker HTTP server
ramune run --env-file .env.prod app.ts  # load env file
ramune run dev                          # run package.json script
Evaluate Expressions
ramune eval "1 + 2"
ramune eval "require('crypto').randomUUID()"
ramune eval "const x: number = 42; x"   # TypeScript works
REPL
ramune repl

Packages from package.json are automatically available:

ramune add lodash
ramune repl
> lodash.chunk([1,2,3,4,5,6], 2)
[[1,2],[3,4],[5,6]]

Features: history, tab completion, TypeScript, colors, multiline.

Test Runner
ramune test

Finds *.test.ts, *.spec.js, etc. Jest/Bun-compatible API:

describe("math", () => {
  test("addition", () => {
    expect(1 + 2).toBe(3);
  });
});

Mocking is supported via jest.fn() and jest.spyOn():

test("mock", () => {
  const fn = jest.fn().mockReturnValue(42);
  expect(fn()).toBe(42);
  expect(fn).toHaveBeenCalledTimes(1);
});
Compile to Standalone Binary
ramune compile server.ts -o myserver --http --minify
./myserver    # self-contained binary with embedded JS

The compiled binary embeds the bundled JS via go:embed. On macOS, it is automatically codesigned with the JIT entitlement.

Options: --http (Ramune.serve event loop), --minify (esbuild minification). Output binary is ~28MB (linter/formatter/checker are not included — only the runtime).

Note: The compiled binary loads JavaScriptCore dynamically at runtime. The target machine must have JSC available (macOS: built-in, Linux: libjavascriptcoregtk). Use -tags qjswasm for cross-platform builds.

Native Extension Modules (Experimental)

Note: This workflow uses the TypeScript-to-Go transpiler under the hood and inherits its experimental status. Simple typed functions (primitives, structs, typed slices) work reliably; generics, decorators, and deep class inheritance may produce Go code that needs manual fixes.

Compile performance-critical TypeScript functions to native Go code and call them from JavaScript at full compiled speed:

ramune compile app.js --native math.ts -o myapp

The --native flag transpiles TypeScript to Go, making exported functions available via require('native:modulename'):

// math.ts — transpiled to Go
export function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
// app.js — runs in JS, calls native Go code
const { fibonacci } = require('native:math');
console.log(fibonacci(35)); // runs as compiled Go, not interpreted JS

Multiple native files and inter-file imports are supported:

ramune compile app.js --native math.ts --native geometry.ts -o myapp

Native functions support rich type interop — structs, typed arrays, and class-like instances with live properties:

// counter.ts
export class Counter {
  count: number = 0;
  name: string;
  constructor(name: string) { this.name = name; }
  increment(): number { return ++this.count; }
}
export function newCounter(name: string): Counter {
  return new Counter(name);
}
const { newCounter } = require('native:counter');
const c = newCounter("hits");
c.increment();
c.increment();
console.log(c.count); // 2 — live property, reads Go struct field
c.count = 100;        // setter, writes Go struct field
c.increment();
console.log(c.count); // 101
Automatic Native Extraction (--hybrid)

An alternative to --native that walks your app automatically: top-level functions and pure classes whose signature and body fall inside a statically-verified subset get compiled to Go; everything else stays on the JS floor. Rejected functions are not silently skipped — each is reported with a reason code when --hybrid-report is set.

Multi-file projects are supported transparently — the picker walks every user TS file reachable from the entry's import graph (excluding .d.ts and node_modules), so functions declared in a separate kernel.ts or lib/math.ts are eligible for extraction, and cross-file calls between them are accepted.

ramune compile app.ts --hybrid -o myapp                  # auto-extract typed hot paths
ramune compile app.ts --hybrid --hybrid-report -o myapp  # print per-function report to stderr

The picker is soundness-gated: it accepts a function only when every signature type and every body AST node is statically provable to behave identically in Go. The extracted Go never needs hand-fixes, and rejected functions keep running on the JS floor unchanged — so adding --hybrid to an existing ramune compile never breaks correctness; at worst nothing gets extracted.

Soundness is proven, speed is not. Each extracted call pays a fixed JS↔Go bridge cost, so extraction can regress when the body does trivial per-call work, marshals a large array on every invocation, or hammers a method in a tight loop — examples/hybrid/ shows one case where sumSquares(xs) over a 1000-element array is ~86× slower than plain JS on JSC + JIT because array marshalling dominates the ~1 µs of body work. Rule of thumb: extract recursive or loop-heavy kernels with primitive arguments; leave frequently-called tight methods and array/object-arg APIs on the JS floor. The picker doesn't see these costs — it only proves semantic equivalence — so use --hybrid-report to pick targets and then measure.

Accepts: primitive / T[] / Promise<primitive> signatures; pure classes (primitive fields, constructor-initialized, this-method bodies); arithmetic, comparison, %, switch, for / while / for-of, template literals, await-on-extractable; Math.*, Number.*, string / array method safelists, callback-driven map / filter / forEach / some / every; named-interface struct params; same-file *JSFunc callback params.

Rejects: reduce / find / findIndex, inline object literal params, Map / Set / Date / RegExp, try / catch / throw, generics, generators, closure capture beyond params/locals, parameter mutation, class inheritance / static / #private / decorators / getters-setters, str.charAt / charCodeAt, and more. Full reason-code list in the extraction report.

vs --native:

  • --hybrid (this section): soundness-gated auto-extraction. Narrower per-function surface but no manual-fix failure mode. Good for "most of my code is plain TS but I have a few typed hot loops."
  • --native file.ts (previous section, experimental): transpile the whole designated file. Wider per-function surface (generics, inheritance) but may need hand-fixes for complex patterns. Good for "I have a specific module that's all math/data processing and I want all of it in Go."

Runnable examples with bench numbers:

  • examples/hybrid/ — minimal single-file bench (fib / primes / array-marshal) comparing JS-only vs hybrid on JSC and qjswasm; shows both the wins and the bridge-cost pitfalls.
  • examples/hybrid-hono/ — Hono web app with extractable route handlers; wrk bench across both backends.
  • examples/hybrid-multifile/ — kernels split across lib/math.ts / lib/format.ts imported by the entry, demonstrating cross-file extraction.

Pair with --tags qjswasm / --tags goja to pick the backend that the compiled binary will embed; hybrid's biggest wins show up on the no-JIT backends (qjswasm is typically 10×-350× faster on extractable kernels vs JS-only when the JIT is out of play).

Transpile TypeScript to Go (Experimental)

Note: The TypeScript-to-Go transpiler is experimental and under active development. Generated code may require manual adjustments for complex codebases.

Transpile TypeScript source code directly to Go:

ramune transpile main.ts -o out/                     # single file
ramune transpile main.ts utils.ts -o out/ --module myapp  # multi-file project
ramune transpile main.ts --compile -o myapp           # transpile + build binary

The transpiler converts TypeScript types, classes, interfaces, generics, async/await, and more to idiomatic Go. See TRANSPILER.md for supported features and limitations.

Type Checking
ramune check app.ts              # check files
ramune check src/                # check directory
ramune run --check app.ts        # check then run

Uses typescript-go — the Go-native compiler behind TypeScript 7.0 Beta (@typescript/native-preview / tsgo), backward-compatible with TS 5.x — built into Ramune. No external tools required.

Format & Lint
ramune fmt .                     # format all JS/TS files
ramune fmt --check .             # check formatting (CI)
ramune lint .                    # lint all JS/TS files
ramune lint --fix .              # lint with auto-fix

The formatter uses typescript-go's built-in formatter. The linter uses rslint (Go-based, 20-40x faster than ESLint). Both are built into Ramune — no external tools required.

If rslint.json or rslint.jsonc exists, ramune lint uses that configuration. Otherwise, all recommended rules are enabled by default.

Note: TypeScript transpilation (ramune run app.ts) uses esbuild which is also built into Ramune.

Package Manager
ramune init                      # create package.json
ramune add lodash dayjs          # add dependencies
ramune remove lodash             # remove
ramune install                   # install all
Build (esbuild)
ramune build app.ts --outdir=dist --bundle --minify
Permissions (Sandbox)
ramune run app.ts                              # default: all allowed
ramune run --sandbox app.ts                    # deny all
ramune run --sandbox --allow-read=/tmp app.ts  # selective access

Flags: --allow-read, --allow-write, --allow-net, --allow-env, --allow-run.

Docker Sandbox

Run scripts in isolated Docker containers:

ramune run --docker app.ts                          # run in default ubuntu:24.04
ramune run --docker --docker-image node:22 app.ts   # custom image
ramune run --docker --docker-memory 512 app.ts      # 512MB memory limit
ramune run --docker --docker-no-network app.ts      # no network access
ramune run --docker --docker-network mynet app.ts   # specific Docker network

The host binary is automatically mounted into the container. On macOS/Windows, a Linux binary is cross-compiled and cached. Go functions registered via SandboxRuntime.RegisterFunc are available inside the container (they are compiled into the binary).

Environment Variables

.env and .env.local files are automatically loaded (like Bun/Deno). Use --env-file to specify a custom file:

ramune run --env-file .env.production app.ts
Package.json Scripts

Run scripts defined in package.json:

ramune run dev     # runs "scripts.dev" from package.json
ramune run build   # runs "scripts.build"
Workers-style Modules (ramune serve)

The flagship command. Serve a Cloudflare Workers-style ES-module handler — export a default object with fetch(request, env, ctx) and the CLI wires it up.

Scope. Three categories so you know what's on the critical path:

  • Ships today: fetch, env.KV, env.SECRETS, ctx.waitUntil, scheduled, cron, WinterCG basics, plus env.DB (D1-style API subset: prepare / bind / all / first / run / exec).
  • Partial / API shape only: env.DB .batch and .raw not yet implemented, .all() meta fields not populated. "D1-style" and "Workers-KV-like" describe the handler-side API shape only — the defaults are a single-node local SQLite file, not Cloudflare's edge-replicated D1 or globally eventually-consistent Workers KV. Swap in Postgres / Planetscale / Redis / DynamoDB via DBBackend / KVBackend when you need distributed scaling.
  • User-supplied env.* bindings (not core): Durable Objects, Queues, R2, AI Gateway, Service Bindings, Hyperdrive. Implement them as Go callbacks + tiny JS facades — walkthrough in workers/BINDINGS.md. The core stays small so you aren't blocked waiting on us to ship a Cloudflare-equivalent.
// worker.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const url = new URL(request.url);
    return Response.json({ hello: url.searchParams.get("name") ?? "world" });
  },
} satisfies WorkersHandler;
ramune serve worker.ts                    # listens on :3000
ramune serve --port 8080 worker.ts        # custom port
ramune serve --workers 4 worker.ts        # N runtimes, round-robined
ramune serve --sqlite :memory:  worker.ts # non-persistent env.DB/env.KV

ctx.waitUntil(promise) keeps the executor alive after the response goes out. env.SECRETS reads RAMUNE_SECRET_* env vars. env.DB (D1-style API subset, see scope above) and env.KV (Workers-KV-like API) are backed by a single-node local SQLite file at .ramune/data.db by default — this matches the D1/Workers-KV handler surface, not their distributed storage profile. For production horizontal scaling, implement DBBackend / KVBackend against your actual database (Postgres, Redis, etc.) and bind via WithDBBackend / WithKVBackend. Works with Hono directly (export default app).

An optional ramune.toml next to the entry declares dependencies, permissions, and named KV bindings:

[dependencies]
hono = "*"

[permissions]
net = "granted"
read = "denied"

[[kv_namespaces]]
binding = "SESSIONS"
namespace = "sessions"

See examples/workers/ for hello, SSE, Hono, and a full HTML guestbook. Type declarations live in workers/workers.d.ts. For non-SQLite backends (Redis, Postgres, …) and user-defined bindings (env.QUEUE, env.EMAIL, …), see the embed API below and the custom binding guide.

Embed in Go

Ramune is also a Go library. Embed JavaScript in your Go application and expose any Go library to JS — database drivers, image processing, gRPC clients, ML inference, etc.

package main

import (
    "fmt"
    "log"

    "github.com/i2y/ramune"
)

func main() {
    rt, err := ramune.New()
    if err != nil {
        log.Fatal(err)
    }
    defer rt.Close()

    val, _ := rt.Eval("1 + 2")
    defer val.Close()
    fmt.Println(val.Float64()) // 3
}
Call Go from JavaScript
rt.RegisterFunc("greet", func(args []any) (any, error) {
    return fmt.Sprintf("Hello, %s!", args[0]), nil
})

val, _ := rt.Eval(`greet("World")`) // "Hello, World!"

Go functions registered via RegisterFunc can safely access Value methods (Attr, Call, SetAttr, etc.) — no deadlock. For typed callbacks, use Register with generics:

ramune.Register(rt, "add", func(a, b float64) float64 {
    return a + b
})
Receive JS Functions in Go

JS functions passed to Go callbacks are wrapped as *JSFunc, callable from Go:

rt.RegisterFunc("forEach", func(args []any) (any, error) {
    items := args[0].([]any)
    fn := args[1].(*ramune.JSFunc)
    defer fn.Close()
    for _, item := range items {
        fn.Call(item)
    }
    return nil, nil
})
forEach(["a", "b", "c"], function(item) { console.log(item); });
// → a, b, c
Struct Binding

Expose Go structs to JavaScript:

type User struct {
    Name string `js:"name"`
    Age  int    `js:"age"`
}
func (u *User) Greet() string { return "Hello, " + u.Name }

rt.Bind("user", &User{Name: "Alice", Age: 30})
// JS: user.name → "Alice", user.greet() → "Hello, Alice"
Plugin System

Register custom modules available via require():

rt, _ := ramune.New(ramune.NodeCompat(), ramune.WithModule(ramune.Module{
    Name: "mydb",
    Exports: map[string]ramune.GoFunc{
        "query": func(args []any) (any, error) {
            return db.Query(args[0].(string))
        },
    },
}))
// JS: const db = require('mydb'); db.query("SELECT 1")
Native Module (Go Library API)

NativeModuleFromFuncs creates a require()-able module from typed Go functions — no manual argument parsing needed:

mod := ramune.NativeModuleFromFuncs("native:math", map[string]any{
    "add":       func(a, b float64) float64 { return a + b },
    "isPrime":   func(n float64) bool { /* ... */ },
    "fibonacci": mymath.Fibonacci,  // any typed Go function
})

rt, _ := ramune.New(ramune.NodeCompat(), ramune.WithModule(mod))
rt.Eval(`require('native:math').add(3, 4)`) // 7

Supports struct parameters, struct returns with live properties, typed slices, error handling, and panic recovery:

type Point struct {
    X float64 `json:"x"`
    Y float64 `json:"y"`
}

mod := ramune.NativeModuleFromFuncs("native:geo", map[string]any{
    "distance": func(a, b Point) float64 {
        dx, dy := a.X-b.X, a.Y-b.Y
        return math.Sqrt(dx*dx + dy*dy)
    },
})
// JS: require('native:geo').distance({x:0, y:0}, {x:3, y:4}) → 5

When a function returns a struct pointer, the JS object has live getter/setter properties and callable methods — mutations in JS are reflected in Go and vice versa.

Use npm Packages
rt, _ := ramune.New(
    ramune.NodeCompat(),
    ramune.Dependencies("lodash@4"),
)
val, _ := rt.Eval(`lodash.chunk([1,2,3,4,5,6], 2)`)

Subpath imports are supported (e.g., "react-dom/server"). Use PreloadJS to inject polyfills that bundled packages may require:

rt, _ := ramune.New(
    ramune.NodeCompat(),
    ramune.PreloadJS(`globalThis.MessageChannel = class { constructor() { this.port1 = {}; this.port2 = {}; } };`),
    ramune.Dependencies("react@18", "react-dom@18", "react-dom/server"),
)
Async / Promises
val, _ := rt.EvalAsync(`
    new Promise(resolve => setTimeout(() => resolve(42), 100))
`)
HTTP Server
rt, _ := ramune.New(ramune.NodeCompat())

rt.Exec(`
    Ramune.serve({
        port: 3000,
        fetch(req) {
            return new Response("Hello!");
        }
    })
`)
rt.RunEventLoop()

Works with Hono and other frameworks. Async handlers with setTimeout/await are supported:

app.get('/slow', async (c) => {
    await new Promise(r => setTimeout(r, 100));
    return c.json({ ok: true });
});
Workers-style Modules (ramune/workers)

The Go embed API for Workers-style handlers — returns an http.Handler that slots into any Go HTTP server (net/http, chi, Echo, gRPC gateway, …):

import "github.com/i2y/ramune/workers"

rt, _ := ramune.New(ramune.NodeCompat(), ramune.WithFetch())
defer rt.Close()

src, _ := os.ReadFile("worker.ts")
handler, err := workers.Register(rt, "worker.ts", string(src),
    workers.WithSQLite(".ramune/data.db"),
    workers.WithWaitUntilTimeout(30*time.Second),
)
http.ListenAndServe(":3000", handler)

ctx.waitUntil(promise) is honoured — the HTTP response ships immediately while the executor drains pending promises. For multi-VM setups, workers.Prepare runs esbuild once and workers.AttachPrepared binds to each Runtime.

Swap env.KV / env.DB for anything. The built-in SQLite path is built on KVBackend / DBBackend Go interfaces; implement them with Redis, Postgres, DynamoDB, or an in-memory map:

// type KVBackend interface { Get/Put/Delete/List ... }
// type DBBackend interface { Query/Exec ... }
workers.Register(rt, "w.ts", src,
    workers.WithKVBackend(myRedisKV),
    workers.WithDBBackend(myPostgres),
)

Invent your own bindings. Anything a CF Workers binding does — env.QUEUE.send, env.EMAIL.send, env.R2.put, env.AI.run, env.DURABLE.get — is a few lines of Go. Register a callback with RegisterFunc, inject a small JS facade via WithExtraEnvJS, and handler code uses env.FOO naturally:

rt.RegisterFunc("__env_email_send", func(args []any) (any, error) {
    // wire up SMTP / SendGrid / SES here
    return nil, myMailer.Send(opts)
})

handler, _ := workers.Register(rt, "w.ts", src,
    workers.WithExtraEnvJS(`
        globalThis.__extraEnvBindings = function(env) {
            env.EMAIL = { send: opts => __env_email_send(opts) };
        };
    `),
)

Full walkthrough with TypeScript types and the composition pattern for stacking multiple bindings: workers/BINDINGS.md. Runnable example: examples/workers/custom-binding/.

workers.LoadRamuneTOML parses the same ramune.toml schema the CLI uses. The CLI-side wrapper is ramune serve (above).

Multi-core Parallelism

Unlike Bun/Node (single-threaded), Ramune runs multiple JSC VMs in parallel on separate OS threads:

pool, _ := ramune.NewPool(4, ramune.NodeCompat())
defer pool.Close()

pool.Eval("Math.PI * 2")                        // round-robin to one VM
pool.Broadcast("globalThis.config = {debug: true}")  // run on every VM

// Multi-worker HTTP server
pool.ListenAndServe(":3000", `
    globalThis.__poolHandle = function(req) {
        return { status: 200, body: "Hello from worker!" };
    };
`)

Worker threads are also supported:

const { Worker } = require('worker_threads');
const w = new Worker('./worker.js', { workerData: { n: 42 } });
w.on('message', msg => console.log(msg));
Ramune APIs & Bun Compatibility

Ramune provides its own API namespace. Bun.* is available as an alias for backward compatibility with existing Bun code, though compatibility is partial and will be improved over time.

API Status
Ramune.serve({port, fetch, websocket}) Supported (Go net/http backend, see Performance)
Ramune.file(path) Supported (text, json, exists, size)
Ramune.write(path, data) Supported
Ramune.password.hash/verify Supported (bcrypt)
Ramune.sleep(ms) Supported
Ramune.plugin({setup}) Supported (onLoad filters, virtual modules)
Request / Response Polyfilled with ReadableStream body
Ramune.build({entrypoints, outdir, ...}) Supported (esbuild backend, minify, splitting, sourcemap)
bun:sqlite Supported (transactions, WAL, prepared stmt cache, pure Go)
Bun.* Alias for Ramune.* (partial Bun compatibility)
WebView (Desktop)

Open native desktop webview windows from JavaScript (macOS, via glaze + purego):

// Go setup — must run on main thread (macOS requirement)
ramune.InitWebViewMain()
rt, _ := ramune.New(ramune.NodeCompat())
done := make(chan struct{})
go func() {
    rt.Exec(`
        var wv = new Ramune.WebView({ title: "My App", width: 800, height: 600 });
        wv.navigate("https://example.com");
        // or: wv.setHtml("<h1>Hello</h1>");
    `)
    rt.RunEventLoop()
    close(done)
}()
ramune.DrainWebViewMain(done)

API: navigate(url), setHtml(html), eval(js), setTitle(title), setSize(w, h), init(js), destroy(), onclose(fn).

WebView (Headless / Bun.WebView)

Headless browser automation via Chrome DevTools Protocol, compatible with Bun.WebView:

const wv = new Bun.WebView({ headless: true });
await wv.navigate("https://example.com");
console.log(await wv.evaluate("document.title")); // "Example Domain"
const screenshot = await wv.screenshot(); // PNG buffer
await wv.click(100, 200);
await wv.type("hello");
wv.close();

Requires Chrome or Chromium installed (set CHROME_PATH to override detection).

API: navigate(url), evaluate(expr), screenshot(opts), click(x, y), type(text), press(key), scroll(dx, dy), resize(w, h), back(), forward(), reload(), cdp(method, params), close(). Properties: url, title, loading.

Console Output

console.log/error/warn work out of the box in both CLI and library mode. Output goes to os.Stdout/os.Stderr by default. Use WithStdout/WithStderr to redirect:

var buf bytes.Buffer
rt, _ := ramune.New(ramune.WithStdout(&buf))
rt.Exec(`console.log("captured")`)
fmt.Println(buf.String()) // "captured\n"
GC Configuration

Ramune provides tunable GC settings for high-throughput HTTP servers:

rt, _ := ramune.New(ramune.NodeCompat(), ramune.WithGC(ramune.GCConfig{
    GCInterval: 2000,   // manual JSC GC every N requests
    GCPercent:  100,    // Go GC target % (GOGC)
}))

For most use cases (CLI, scripting, SDK), defaults work fine.

Permissions (Library API)
rt, _ := ramune.New(
    ramune.NodeCompat(),
    ramune.WithPermissions(&ramune.Permissions{
        Read:      ramune.PermGranted,
        ReadPaths: []string{"/tmp", "/var/data"},
        Write:     ramune.PermDenied,
        Net:       ramune.PermDenied,
    }),
)
In-process Sandbox for Untrusted JS (qjswasm + permissions)

For platforms that let customers upload JS code (persona 2 above), the qjswasm backend + SandboxPermissions() + WithResourceLimits gives you a layered defense-in-depth sandbox in one process, no Docker required:

rt, err := ramune.New(
    ramune.NodeCompat(),
    ramune.WithPermissions(ramune.SandboxPermissions()),   // deny all I/O by default
    ramune.WithResourceLimits(ramune.ResourceLimits{
        MaxMemoryBytes:   64 << 20,    // 64 MiB JS heap cap
        MaxStackBytes:    1 << 20,     // 1 MiB stack cap
        GCThresholdBytes: 16 << 20,    // trigger GC at 16 MiB
    }),
)

This stacks four independent layers:

  1. WASM linear memory isolation (qjswasm only). QuickJS-NG runs inside wazero's linear memory. A memory-safety bug in the VM can only corrupt the wasm sandbox's own memory — it cannot read or write Go heap, host memory, or make arbitrary syscalls. Bounds-checked by wazero at compile time (compiler mode AOT-compiles WASM → native while preserving WASM's memory-safety semantics).
  2. No ambient syscalls (qjswasm only). wazero only exposes what host imports are explicitly registered. SandboxPermissions() triggers Ramune's fork of fastschema/qjs to pass DisableFS: true, which skips the default wazero.NewFSConfig().WithDirMount(CWD, "/") — so even WASI fd_read / path_open have no filesystem to reach. A VM escape still can't pivot to host files.
  3. Permission-gated Go bridges. The only path from JS to the host OS is through Ramune's registered Go callbacks (fs.readFile, fetch, child_process.spawn, etc.). Each checks perms.CheckRead / CheckWrite / CheckNet / CheckRun / CheckEnv at the Go side before doing anything. This gate is shared across all three backends.
  4. Resource caps. WithResourceLimits maps to QuickJS-NG's JS_SetMemoryLimit / JS_SetMaxStackSize / JS_SetGCThreshold. OOM and stack-overflow in JS are recoverable errors, not process crashes. Per-runtime caps survive multiple tenants sharing one Ramune process via RuntimePool.

For comparison:

  • JSC and goja honor permissions (layer 3) and can be used for trusted-by-default scenarios, but they lack the VM-boundary isolation (layer 1-2). JSC has a JIT with RWX pages that's generally well-audited but a larger attack surface; goja runs Go reflection code with full process privileges.
  • Docker Sandbox (SandboxRuntime, below) is an outer OS-level layer on top of all of this — use it when you need kernel-level isolation (namespaces, cgroups, seccomp) or when you're about to run a binary you don't control. For JS code you do control but want to sandbox from your own Go code, in-process qjswasm is usually sufficient and much lighter.

Known gaps:

  • ResourceLimits.MaxExecutionTime is accepted but currently not enforced by the C shim (the QuickJS interrupt handler hook is wired in Go but the C path that would register it is commented out). CPU-bound DoS isn't caught in-band yet; use an out-of-band timeout (context.WithTimeout + RunEventLoopFor) until this lands.
  • Multi-tenant fairness across workers in a RuntimePool is best-effort — one tenant can starve others if they burn CPU, since workers aren't preempted.
Docker Sandbox (Library API)

Execute untrusted JS in Docker containers with Go function passthrough:

rt := ramune.NewSandboxRuntime(ramune.NodeCompat())

// Go functions are available inside the container
rt.RegisterFunc("multiply", func(args []any) (any, error) {
    return args[0].(float64) * args[1].(float64), nil
})

// Must be called first — handles re-exec as sandbox worker
if ramune.HandleSandboxWorker(rt) {
    return
}

result, err := rt.SandboxRun("script.js", ramune.SandboxConfig{
    Image:     "ubuntu:24.04",
    MemoryMB:  512,
    NoNetwork: true,
    Timeout:   30 * time.Second,
})
fmt.Println(result.Stdout)

SandboxEval evaluates code strings instead of files. SandboxAvailable() checks if Docker is reachable.

Docker API (dockerode)

Access Docker from JavaScript via the DockerModule() option:

rt, _ := ramune.New(ramune.NodeCompat(), ramune.DockerModule())
const Docker = require('dockerode');
const docker = new Docker();
await docker.ping();

const container = await docker.createContainer({ Image: 'alpine:latest', Cmd: ['echo', 'hello'] });
await container.start();
const { StatusCode } = await container.wait();

Supports: ping, pull, createContainer, start/stop/remove/wait/inspect/logs, createNetwork/removeNetwork.

Performance

TL;DR. Ramune's primary competition is goja / otto (Go-embedded JS runtimes) and "no tool at all" (self-hosting Workers-style handlers). Pick JSC for raw single-worker throughput (JIT, 60× faster than goja on CPU-bound code), qjswasm for pure-Go multi-worker scaling (5.72× across 6 workers), or goja for the smallest pure-Go footprint. All three share the same Ramune API.

vs Go-embedded JS runtimes (primary comparison)

Absolute ms per workload, lower is better. Apple M4 Max. Reproduce with make bench-go.

Test Ramune (JSC+JIT) Ramune (qjswasm) Ramune (goja) otto
Fibonacci(35) 35 ms 1,987 ms 2,400 ms 26,413 ms
JSON 10K objects 0.98 ms 19.6 ms 12.3 ms 27 ms

JSC with JIT is the fastest Go-embedded JS runtime by 1-2 orders of magnitude on CPU-heavy code. qjswasm (QuickJS-NG on wazero's AOT WASM→native JIT) is faster than goja on CPU-heavy integer code and slightly slower on pure-JSON workloads. otto is an order of magnitude slower across the board.

Multi-Runtime Pool (Ramune's differentiator)

Ramune runs N JS VMs in parallel on separate OS threads within one process. Bun and Node are single-threaded; their equivalents (cluster, worker_threads) require separate processes or message passing.

Apple M4 Max, bench/pool/pool_bench.go (JSON generate/filter/map handler, 200 objects per request, wrk -t4 -c100 -d10s), median of 3 runs per backend. Reproduce with go build [-tags qjswasm|-tags goja] -o pool bench/pool/pool_bench.go && ./pool 6.

JSC (default)
Workers req/s Scaling
1 40,511 1.0x
2 54,500 1.35x
3 58,401 1.44x
4 59,706 1.47x
5 60,913 1.50x
6 62,407 1.54x

JSC wins on absolute throughput by a wide margin thanks to the JIT. Multi-worker scaling is shallow but monotonic — the single-worker JIT throughput is already close to saturating what the handler can generate, so additional workers add modest headroom (~54% over 1-worker at 6 workers). For latency-sensitive workloads, 1-3 workers is usually optimal; past 3 workers the curve is close to flat.

qjswasm (-tags qjswasm)
Workers req/s Scaling
1 2,348 1.0x
2 4,666 1.99x
3 6,782 2.89x
4 9,152 3.90x
5 11,331 4.83x
6 13,435 5.72x

Monotonic out to 6 workers (and still linear). QuickJS-NG compiled to WASM and driven by wazero sidesteps the global-allocator contention that hobbled the previous modernc/quickjs backend — the wasm linear memory is per-runtime with no shared-allocator mutex to fight over.

goja (-tags goja)
Workers req/s Scaling
1 3,518 1.0x
2 5,833 1.66x
3 7,404 2.10x
4 8,374 2.38x
5 9,351 2.66x
6 10,173 2.89x

Pure-Go reflection interpreter. Faster than qjswasm at 1-3 workers (lower setup cost, no wazero compile) but qjswasm pulls ahead from 4 workers on.

Backend selection by shape. JSC wins by a wide margin on absolute throughput at every worker count (~40k single-worker, ~62k at 6 workers). qjswasm has the best multiplicative scaling (5.72× at 6 workers) and the highest absolute throughput among pure-Go backends past 3 workers. goja is the simplest pure-Go option and is the fastest pure-Go at 1-3 workers (no wasm compile cost).

vs Bun / Node.js (single-process, secondary)

Not Ramune's primary framing — Ramune's value lives in embedding and multi-core scaling, not raw CLI speed. But for readers evaluating CLI use anyway, here are the numbers on Apple M4 Max with JSC+JIT; run make bench for numbers on your machine.

Workload Ramune vs Node.js Ramune vs Bun
Hello World startup ~1.1x faster ~2.3x slower
Fibonacci(35) CPU ~1.3x faster ~1.2x slower
JSON 10K objects ~1.2x faster ~2x slower
Crypto SHA256 x1000 comparable ~2x slower
File I/O x100 comparable ~1.7x slower
HTTP req/s (single) ~equal ~1.2x slower

Ramune is Node-equivalent on HTTP and faster on CPU-fib. Bun is faster on most axes but the gap has narrowed from ~1.7× to ~1.2× on single-process HTTP. If raw single-process throughput is all you need, Bun is still the right answer. If you need Go embedding, multi-core scaling in one process, or self-hosted Workers, those aren't offered by Bun or Node and that's where Ramune's value lives.

qjswasm backend (no JS JIT, pure-Go)
Workload (Apple M4 Max) qjswasm Ratio vs JSC
Fibonacci(35) 1.99 B ns/op ~58x slower
JSON 10K objects 19.6 M ns/op ~20x slower

wazero AOT-compiles the WASM to native but QuickJS-NG itself runs as an interpreter inside, so CPU-bound JS is materially slower than JSC. Best for zero-dependency deployments (FROM scratch Docker, Windows-native) and multi-worker scaling where absolute single-worker speed matters less than scaling factor.

JIT Setup

On macOS, JIT requires a code signing entitlement:

# After go install:
ramune setup-jit

# Or when building from source:
make build-cli

Linux does not need JIT setup.

Node.js Compatibility

Module Coverage Module Coverage
path 100% zlib 75% (gzip, deflate, brotli)
fs 90% (async + sync + watch) os 85%
child_process 80% events 85%
crypto 85% (+ crypto.subtle) url 80%
stream 85% (class extends, asyncIterator, backpressure, cork/uncork) Buffer 90%
http/https 80% (createServer with streaming response) assert 80%
http2 75% (connect, createServer, trailers, multiplexing) dns basic
net/tls 70% (+ net.createServer) readline 70%
worker_threads 80% (+ SharedArrayBuffer, Atomics.waitAsync) querystring 80%
vm 70% perf_hooks basic
timers/promises 70% process 85% (stdin, signals, exit, env, tty)
util 80% (types, promisify, format, debuglog) tty 70% (isatty, WriteStream)
dgram 70% (UDP) async_hooks basic (AsyncLocalStorage)
module createRequire, file-based require with ESM-to-CJS
Stream Classes

Stream classes (Readable, Writable, Duplex, Transform, PassThrough) are ES6 classes that support class extends:

const { Transform } = require('stream');

class Upper extends Transform {
  _transform(chunk, encoding, cb) {
    this.push(String(chunk).toUpperCase());
    cb();
  }
}

Key features: Symbol.asyncIterator (for await...of), backpressure-aware pipe(), cork()/uncork(), unshift(), unpipe(), objectMode, readableFlowing/readableEnded/writableEnded/writableFinished properties, stream.pipeline(), stream.finished().

http.IncomingMessage extends Readable, so request bodies support pipe(), read(), and for await...of.

HTTP/2
const http2 = require('http2');

// Client
const session = http2.connect('http://localhost:8080');
session.on('connect', () => {
  const req = session.request({ ':method': 'POST', ':path': '/api' });
  req.on('response', (headers) => { /* ... */ });
  req.on('data', (chunk) => { /* ... */ });
  req.on('trailers', (trailers) => { /* grpc-status, grpc-message */ });
  req.end(body);
});

// Server (cleartext h2c)
const server = http2.createServer((stream, headers) => {
  stream.respond({ ':status': 200, 'content-type': 'text/plain' });
  stream.end('hello');
});
server.listen(3000);

Supports http2.connect(), createServer(), createSecureServer(), stream multiplexing, trailers (for gRPC), and http2.constants.

Web Platform APIs

Ramune implements the WinterTC Minimum Common Web API (ECMA-429), the standard API surface shared across non-browser JS runtimes (Deno, Cloudflare Workers, Bun, Node.js). The implementation is Go-side, so the Web API surface is consistent across all three backends (JSC, qjswasm, goja); only WebAssembly is JSC-only, and all other APIs below behave identically regardless of the backend.

API Status
fetch / Headers / Request / Response Supported (Go net/http backend, ReadableStream body)
ReadableStream / WritableStream / TransformStream Supported (pipeTo, pipeThrough, tee, BYOB, async iterator)
ReadableStreamBYOBReader / ReadableByteStreamController Supported
CompressionStream / DecompressionStream Supported (gzip, deflate, deflate-raw, brotli)
TextEncoder / TextDecoder Supported (UTF-8)
TextEncoderStream / TextDecoderStream Supported
crypto.subtle Supported (digest, sign/verify, encrypt/decrypt, importKey/exportKey, deriveBits/deriveKey)
crypto.getRandomValues / randomUUID Supported
Blob / File / FormData Supported (stream, bytes, slice, MIME normalization)
AbortController / AbortSignal Supported (timeout, abort, any)
EventTarget / Event / CustomEvent Supported (addEventListener, once, signal, handleEvent)
ErrorEvent / PromiseRejectionEvent / MessageEvent Supported
MessageChannel / MessagePort Supported
DOMException Supported (all legacy error codes)
URL / URLSearchParams / URLPattern Supported
WebSocket Supported (server-side via Ramune.serve)
Performance / performance.now Supported (mark, measure, timeOrigin)
SharedArrayBuffer / Atomics Supported (Go []byte backed, wait/notify/waitAsync)
structuredClone Supported (circular refs, Map, Set, Date, RegExp, TypedArray)
atob / btoa Supported
setTimeout / setInterval / queueMicrotask Supported
navigator.userAgent Supported
reportError / onerror / onunhandledrejection Supported
CountQueuingStrategy / ByteLengthQueuingStrategy Supported
WebAssembly Supported (JSC backend; compile, instantiate, validate, streaming)
console Supported (log, error, warn, info, debug, time, table, trace)
WPT Conformance

Ramune's Web API implementations are validated against the Web Platform Tests (WPT) suite:

make test-wpt   # run WPT conformance tests

Pass rates measure coverage against the full WPT subtest corpus, which includes browser-only edge cases irrelevant to a non-browser runtime (layout, DOM mutation, cross-window postMessage, etc.). Mainstream Workers patterns like fetch, Response.json, basic streaming readers/writers, and compression round-trips work reliably in practice; the unmet percentages are dominated by those browser-specific subtests and a handful of obscure stream-controller races. Run make test-wpt for the exact failing subtests per category.

Most categories pass at identical rates across all three backends because the Web APIs are Go-side polyfills:

Category Pass Rate
timers 100%
atob/btoa 99%
hr-time 86%
FileAPI/blob 82%
microtask-queuing 80%
dom/abort 61%

Backend-sensitive categories (differ because the underlying JS engine affects behavior or the polyfill interacts with engine-specific scheduling):

Category JSC qjswasm goja
compression 63% 55% 53%
streams 53% 53% 51%
dom/events 46% 53%† 53%†
webmessaging 33% 18% 33%

†qjswasm and goja skip AddEventListenerOptions-signal.any.js which hangs on both engines; goja additionally skips gb18030-decoder.any.js which triggers a goja parser panic on \u{10FFFF} string literals. Skip list lives in wpt_test.go.

WPT checkout is required (test/wpt/). See make test-wpt output for setup instructions.

File-based require() with ESM Support

require() loads files from the filesystem with automatic ESM-to-CJS conversion:

const { hello } = require('./lib.mjs');     // ESM -> CJS transform
const data = require('./config.json');       // JSON parsing
const utils = require('./utils.ts');         // TypeScript stripping
const pkg = require('some-package');         // node_modules resolution

ESM detection: .mjs extension, package.json "type": "module", or import/export keywords. TypeScript ESM files (.ts with import/export) are processed in a single esbuild pass. Modules are cached by resolved absolute path. Per-module require functions ensure correct relative path resolution in nested imports.

Ramune also supports package.json "exports" field resolution (conditional exports with require/import/default and subpath exports).

TypeScript-to-Go Transpiler

Ramune ships a built-in TypeScript-to-Go transpiler that converts TS source to idiomatic Go: numberfloat64, classes → structs with methods, Promise<T>*promise.Promise[T], generics, enums, discriminated unions. A go: prefix lets you import any Go package from TypeScript. Two integration paths with different maturity profiles:

  • ramune compile --hybrid app.ts — soundness-gated auto-extraction: the picker walks the app and extracts only functions / pure classes whose signature and body are statically provable to behave identically in Go; everything else stays on the JS floor. The extracted Go never needs hand-fixes. See Automatic Native Extraction above.
  • ramune compile --native file.ts (experimental) — transpile a designated TS file wholesale and expose it as a require('native:name') module. Wider per-function surface (generics, inheritance) but may need hand-fixes for complex patterns.

Full feature list, type mapping, native module workflow, go: imports, and limitations: TRANSPILER.md.

Known Limitations

  • N-API / Native addons: Not supported. Packages that require .node native binaries (e.g., bcrypt, sharp, better-sqlite3) will not work. Use pure JS alternatives instead.
  • HTTP self-fetch: Ramune.serve() handlers cannot fetch their own server (same JS context deadlock).
  • Windows: JSC backend not available. Use -tags qjswasm for Windows support.
  • Linux multi-runtime (JSC): Architecture-dependent signal handling. On arm64, CGO_ENABLED=1 and gcc are required for multi-runtime (cgo's signal forwarding is needed for JSC's GC). On x86_64, multi-runtime works without cgo (CGO_ENABLED=0).
  • Multi-worker scaling (JSC): Scaling flattens around 3-4 workers on macOS due to JSC JIT contention and purego FFI overhead. Linux (libjavascriptcoregtk) may differ.
  • qjswasm backend: No JS JIT (wazero AOT-compiles the WASM but QuickJS-NG itself runs as an interpreter inside). CPU-bound JS is slower than JSC (see Performance). Error stack traces not yet round-tripped. ResourceLimits.MaxExecutionTime silently ignored until the C-shim interrupt handler is wired (memory/stack/GC limits work).
  • goja backend: No WebAssembly (JSC-only). Post-ES2017 syntax (private class fields, top-level await, etc.) is auto-lowered to ES2017 via esbuild at Runtime.Eval/Exec time on first parse failure and cached, so user code rarely hits goja's native parser limits in practice. Some subsystems are stubbed (WebSocket upgrade path). No JS error stack traces exposed to Go.
  • Native module instance lifecycle: Struct instances returned to JS are not automatically freed when the JS object is garbage collected. Instances are cleaned up when Runtime.Close() is called. For long-running servers creating many short-lived struct instances, this may cause increased memory usage.

Requirements

JSC backend (default) qjswasm backend (-tags qjswasm) goja backend (-tags goja)
Go 1.26+ 1.26+ 1.26+
Platforms macOS, Linux macOS, Linux, Windows macOS, Linux, Windows
System deps macOS: none. Linux: apt install libjavascriptcoregtk-4.1-dev None None

All tools are built in — no external dependencies needed for check, fmt, lint, or TypeScript transpilation. npm packages are fetched directly from the npm registry — no npm or bun CLI required.

Developing Ramune

Make targets for working on Ramune itself (contributor setup):

make ci          # fmt + build + vet + test
make test-wpt    # WPT conformance tests (requires test/wpt checkout)
make build-cli   # build with JIT entitlement (macOS)
make bench       # CLI benchmarks vs Bun/Node (hyperfine + wrk)
make bench-go    # compare vs goja / otto (Go-embedded runtimes)
make sync        # sync typescript-go & rslint from submodules

About the name

Named after Ramune, a Japanese carbonated soft drink served in a Codd-neck bottle — the one with the marble you have to press down into the neck to open. Hoping for the same "fizz" inside a Go binary.

  • Soda — A drop-in Ramune-backed replacement for PocketBase's JSVM plugin. Lets PocketBase hooks be written as Workers-style export default { fetch } modules.
  • Dark — Go SSR web framework (Preact/React templates, htmx, Islands hydration) built on net/http. Uses Ramune internally for SSR, and ships as a native desktop app via WebView with Go↔JS bindings.

License

MIT

Third-Party Licenses

Ramune includes code from the following projects:

Project License Usage Inclusion
microsoft/typescript-go Apache-2.0 Type checker, formatter (TS 7.0-dev) Source copy (internal/tsgo/)
web-infra-dev/rslint MIT Linter Source copy (internal/rslint/)
dop251/goja MIT goja backend (-tags goja) Go module dependency
fastschema/qjs MIT qjswasm backend (-tags qjswasm) — QuickJS-NG on wazero, fork adds a DisableFS option for sandboxed use Inline vendored (third_party/qjs/)
QuickJS-NG MIT Baked into the prebuilt third_party/qjs/qjs.wasm that the qjswasm backend embeds; license text at third_party/qjs/qjswasm/quickjs/LICENSE Compiled-binary inclusion
tetratelabs/wazero Apache-2.0 WebAssembly runtime that drives qjs.wasm for the qjswasm backend Go module dependency
modernc.org/sqlite BSD-3-Clause Pure-Go SQLite for bun:sqlite and the Workers-style env.DB default Go module dependency
evanw/esbuild MIT TypeScript transpilation, bundling Go module dependency

License texts for source-copied projects are in internal/tsgo/LICENSE, internal/rslint/LICENSE, internal/rslint/tsgo_pinned/LICENSE (a separate tsgo copy pinned to rslint's version for its shim bindings), and third_party/qjs/LICENSE + third_party/qjs/qjswasm/quickjs/LICENSE (see third_party/qjs/NOTICES.md).

The Ramune logo includes the Go Gopher, originally designed by Renée French, licensed under Creative Commons Attribution 4.0.

Documentation

Overview

Package ramune provides Go bindings for JavaScriptCore via purego — no Cgo required. The JSC runtime is dynamically loaded at startup and works with the system framework on macOS and libjavascriptcoregtk on Linux.

Basic Usage

Evaluate JavaScript and read results:

rt, err := ramune.New()
if err != nil {
    log.Fatal(err)
}
defer rt.Close()

val, err := rt.Eval("1 + 2")
if err != nil {
    log.Fatal(err)
}
defer val.Close()
fmt.Println(val.Float64()) // 3

Constructing Objects

Create JS objects and arrays directly from Go:

obj, _ := rt.NewObject(map[string]any{"name": "Alice", "age": 30})
arr, _ := rt.NewArray(1, "two", true)
obj.SetAttr("tags", arr)

Go Callbacks

Register Go functions callable from JavaScript:

rt.RegisterFunc("add", func(args []any) (any, error) {
    return args[0].(float64) + args[1].(float64), nil
})
val, _ := rt.Eval("add(3, 4)") // 7

npm Packages

Use npm packages via automatic esbuild bundling:

rt, _ := ramune.New(
    ramune.NodeCompat(),
    ramune.Dependencies("lodash@4"),
)
val, _ := rt.Eval(`lodash.chunk([1,2,3,4,5,6], 2)`)

Event Loop and Async

setTimeout, setInterval, and Promises work with the built-in event loop:

val, _ := rt.EvalAsync(`
    new Promise(resolve => setTimeout(() => resolve(42), 100))
`)
fmt.Println(val.Float64()) // 42

Fetch

HTTP requests via globalThis.fetch backed by Go's net/http:

rt, _ := ramune.New(ramune.WithFetch())
val, _ := rt.EvalAsync(`
    fetch("https://api.example.com/data").then(r => r.json())
`)

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyClosed = errors.New("ramune: runtime already closed")

ErrAlreadyClosed is returned when operations are attempted on a closed Runtime.

View Source
var ErrJSCNotFound = errors.New("ramune: shared library not found")

ErrJSCNotFound is returned when no suitable JavaScriptCore shared library can be located on the system.

View Source
var ErrNilValue = errors.New("ramune: operation on nil Value")

ErrNilValue is returned when an operation is attempted on a nil Value.

View Source
var NodeBuiltins = []string{
	"child_process", "fs", "path", "os", "net", "http", "https", "tls",
	"stream", "events", "util", "buffer", "crypto", "url", "querystring",
	"zlib", "string_decoder", "assert", "readline", "dns", "worker_threads",
	"dgram", "module", "process", "tty", "timers", "timers/promises",
	"perf_hooks", "node:*",
}

NodeBuiltins lists Node.js built-in modules that are provided by the NodeCompat polyfill layer and should be marked as external during bundling.

Functions

func ClearCache

func ClearCache() error

ClearCache removes all cached JS bundles created by Dependencies().

func DrainWebViewMain added in v0.8.0

func DrainWebViewMain(done <-chan struct{})

DrainWebViewMain processes WebView operations on the main thread. Blocks until done is closed or all work is complete. Must be called from the main goroutine (pinned to thread 0 via runtime.LockOSThread in init).

func HandleSandboxWorker added in v0.8.0

func HandleSandboxWorker(s *SandboxRuntime) bool

HandleSandboxWorker checks if the current process is a sandbox worker and returns true if so. The caller should pass the SandboxRuntime that was set up with RegisterFunc calls, then exit.

Usage:

rt := ramune.NewSandboxRuntime(ramune.NodeCompat())
rt.RegisterFunc("add", func(a, b float64) float64 { return a + b })
if ramune.HandleSandboxWorker(rt) {
    return
}

func InitWebViewMain added in v0.8.0

func InitWebViewMain()

InitWebViewMain enables WebView support by creating the main-thread dispatch channel. Must be called before any WebView is created. The caller must drain the channel on the main goroutine, e.g.:

ramune.InitWebViewMain()
go func() { /* run engine */ }()
ramune.DrainWebViewMain(done)

func InstallNpmPackages added in v0.5.0

func InstallNpmPackages(specs []string, destDir string) error

InstallNpmPackages downloads npm packages to destDir/node_modules/. Packages are specified as "name" or "name@version" (e.g., "preact", "lodash@4"). Packages are fetched directly from the npm registry — no npm or bun required.

func Register

func Register[F any](rt *Runtime, name string, fn F) error

Register registers a typed Go function as a global JavaScript function. Unlike RegisterFunc, which requires manual args[]any casting, Register uses reflection to automatically convert JS arguments to the function's parameter types and convert return values back.

Supported parameter types: float64, float32, int, int8/16/32/64, uint, uint8/16/32/64, string, bool, map[string]any, []any.

Supported return signatures:

  • func(...) → no return value
  • func(...) T → single return value (non-error)
  • func(...) error → single error return
  • func(...) (T, error) → value + error

Example:

ramune.Register(rt, "add", func(a, b float64) float64 {
    return a + b
})

func SandboxAvailable added in v0.8.0

func SandboxAvailable() bool

SandboxAvailable checks whether Docker is reachable.

Types

type CallbackContext

type CallbackContext struct {
	// contains filtered or unexported fields
}

CallbackContext provides safe access to the JS engine from within a GoFunc. Value methods like Attr() and Call() dispatch to the engine goroutine, which deadlocks inside a callback (already on the engine goroutine). CallbackContext calls engine functions directly and returns Go values.

func (*CallbackContext) Eval

func (cc *CallbackContext) Eval(code string) (any, error)

Eval evaluates JS code and returns the result as any (Go value).

func (*CallbackContext) EvalBool

func (cc *CallbackContext) EvalBool(code string) (bool, error)

EvalBool evaluates JS code and returns the result as bool.

func (*CallbackContext) EvalFloat64

func (cc *CallbackContext) EvalFloat64(code string) (float64, error)

EvalFloat64 evaluates JS code and returns the result as float64.

func (*CallbackContext) EvalString

func (cc *CallbackContext) EvalString(code string) (string, error)

EvalString evaluates JS code and returns the result as string.

func (*CallbackContext) Exec

func (cc *CallbackContext) Exec(code string) error

Exec executes JavaScript code, discarding the result.

func (*CallbackContext) GetProperty

func (cc *CallbackContext) GetProperty(name string) (any, error)

GetProperty reads a property from the global object.

func (*CallbackContext) SetProperty

func (cc *CallbackContext) SetProperty(name string, value any) error

SetProperty sets a property on the global object.

type GCConfig

type GCConfig struct {
	// GCInterval is the number of HTTP requests between manual JSC GC cycles.
	// Lower values use more CPU but prevent JS memory growth.
	// Default: 2000. Set to 0 to disable manual JSC GC.
	GCInterval int

	// GCPercent sets the Go GC target percentage (same as GOGC env var).
	// Applied when the HTTP server starts. Default: 100 (Go's default).
	GCPercent int
}

GCConfig configures garbage collection behavior for a Runtime.

func DefaultGCConfig

func DefaultGCConfig() GCConfig

DefaultGCConfig returns the default GC configuration.

type GoFunc

type GoFunc func(args []any) (any, error)

GoFunc is a Go function that can be called from JavaScript. Arguments are converted to Go types: bool, float64, string, nil, map[string]any (for objects), []any (for arrays), or *JSFunc (for functions).

type GoFuncWithContext

type GoFuncWithContext func(ctx *CallbackContext, args []any) (any, error)

GoFuncWithContext is a callback that receives a CallbackContext for safe engine access. Use RegisterFuncWithContext to register these.

type JSError

type JSError struct {
	Context string
	Message string
	Stack   string // JavaScript stack trace, if available
}

JSError represents an error that originated in the JavaScript runtime.

func (*JSError) Error

func (e *JSError) Error() string

type JSFunc added in v0.4.0

type JSFunc struct {
	// contains filtered or unexported fields
}

JSFunc wraps a JavaScript function reference, allowing it to be called from Go. Created automatically when a JS function is passed as an argument to a GoFunc callback. Call Close() when done to release the JS reference.

func (*JSFunc) Call added in v0.4.0

func (f *JSFunc) Call(args ...any) (any, error)

Call invokes the JavaScript function with the given arguments. Returns the result converted to a Go value (bool, float64, string, nil, map[string]any, or []any). Safe to call from any goroutine and from within GoFunc callbacks.

func (*JSFunc) Close added in v0.4.0

func (f *JSFunc) Close() error

Close releases the JavaScript function reference. Safe to call multiple times or on nil.

type LibraryNotFoundError

type LibraryNotFoundError struct {
	Searched []string
}

LibraryNotFoundError provides detailed information about which paths were searched when JavaScriptCore could not be found.

func (*LibraryNotFoundError) Error

func (e *LibraryNotFoundError) Error() string

func (*LibraryNotFoundError) Unwrap

func (e *LibraryNotFoundError) Unwrap() error

type Module

type Module struct {
	// Name is the module name used with require('name').
	Name string

	// Exports maps JS property names to Go functions.
	// Each function becomes a property on the module object.
	Exports map[string]GoFunc

	// Init is called after exports are registered, allowing
	// additional setup such as evaluating JS code.
	// The Runtime's JSC lock is held — use execLocked/evalLocked only.
	Init func(rt *Runtime) error
}

Module defines a custom module that can be loaded via require() in JS.

func NativeModuleFromFuncs added in v0.4.0

func NativeModuleFromFuncs(name string, funcs map[string]any) Module

NativeModuleFromFuncs creates a Module from a map of typed Go functions. Each function is automatically wrapped with reflection-based argument conversion, similar to Register[F]. This is used to expose transpiled Go code as a JS module.

Example:

mod := ramune.NativeModuleFromFuncs("native:math", map[string]any{
    "fibonacci": mymath.Fibonacci,  // func(float64) float64
    "isPrime":   mymath.IsPrime,    // func(float64) bool
})
rt, _ := ramune.New(ramune.WithModule(mod))
rt.Eval(`require('native:math').fibonacci(10)`) // 55

type Option

type Option func(*config)

Option configures a Runtime.

func Dependencies

func Dependencies(pkgs ...string) Option

Dependencies declares npm package dependencies that are automatically installed, bundled with esbuild, and evaluated in the JSC context. Packages are specified as "name" or "name@version" (e.g., "lodash@4"). Subpath imports are also supported (e.g., "react-dom/server"); the base package is installed and the subpath is imported separately in the bundle. The bundle is cached in ~/.cache/ramune/jsbundles/<hash>/. Packages are fetched directly from the npm registry — no npm or bun required.

func DockerModule added in v0.8.0

func DockerModule() Option

DockerModule returns an Option that installs the Docker native module. The module is lazy-initialized on first require('dockerode').

func NodeCompat

func NodeCompat() Option

NodeCompat installs a minimal Node.js compatibility layer into the JSC context. This enables npm packages that depend on Node.js built-ins to run in JSC with Go providing the native functionality.

Supported polyfills:

  • require() — returns polyfilled modules
  • process.env, process.cwd(), process.platform, process.arch
  • child_process.spawnSync / execSync (synchronous only)
  • fs.readFileSync, fs.existsSync, fs.writeFileSync, fs.mkdirSync
  • path.join, path.resolve, path.dirname, path.basename, path.sep
  • Buffer.from (basic)
  • console.log, console.error, console.warn
  • setTimeout (immediate execution, no real delay)

func PreloadJS added in v0.6.0

func PreloadJS(code string) Option

PreloadJS sets JavaScript code that will be executed before loading dependency bundles. This is useful for providing polyfills required by bundled packages (e.g., MessageChannel for React).

func WithFetch

func WithFetch() Option

WithFetch installs a globalThis.fetch polyfill backed by Go's net/http. This is also automatically enabled when NodeCompat() is used.

func WithGC

func WithGC(gc GCConfig) Option

WithGC configures garbage collection behavior. See GCConfig for details on each setting.

func WithLibraryPath

func WithLibraryPath(path string) Option

WithLibraryPath sets an explicit path to the JavaScriptCore shared library. Ignored when using the QuickJS backend.

func WithModule

func WithModule(m Module) Option

WithModule returns an Option that registers a module during Runtime creation. The module is available via require('name') if NodeCompat is enabled.

func WithPermissions

func WithPermissions(p *Permissions) Option

WithPermissions sets the permission policy for the Runtime.

func WithResourceLimits added in v0.13.0

func WithResourceLimits(l ResourceLimits) Option

WithResourceLimits caps runtime resources. See ResourceLimits for the per-backend applicability matrix.

func WithStderr added in v0.8.0

func WithStderr(w io.Writer) Option

WithStderr sets the writer for console.error/warn output. Defaults to os.Stderr if not set.

func WithStdout added in v0.8.0

func WithStdout(w io.Writer) Option

WithStdout sets the writer for console.log/info/debug output. Defaults to os.Stdout if not set.

func WithTickManager added in v0.8.0

func WithTickManager(m TickManager) Option

WithTickManager registers a custom event loop manager. The manager's ProcessEvents method is called during each event loop tick, and HasActive is checked to determine if the event loop should keep running.

func WithWinterTC added in v0.10.0

func WithWinterTC() Option

WithWinterTC installs the WinterTC (ECMA-429) Minimum Common Web API surface. This includes CompressionStream, DecompressionStream, MessageChannel, MessagePort, MessageEvent, ErrorEvent, PromiseRejectionEvent, and URLPattern.

When used with NodeCompat(), these APIs are installed automatically. Use WithWinterTC() for standalone WinterTC compliance without the full Node.js compatibility layer.

type PermissionState

type PermissionState int

PermissionState represents the state of a permission.

const (
	PermGranted PermissionState = iota
	PermDenied
)

type Permissions

type Permissions struct {
	Read  PermissionState
	Write PermissionState
	Net   PermissionState
	Env   PermissionState
	Run   PermissionState

	ReadPaths  []string // allowed read paths (empty = all if granted)
	WritePaths []string // allowed write paths
	NetHosts   []string // allowed network hosts
	EnvVars    []string // allowed env var names
	RunCmds    []string // allowed commands
}

Permissions controls access to system resources. Default is all granted (Bun-compatible). Use WithSandbox() to deny by default.

func AllPermissions

func AllPermissions() *Permissions

AllPermissions returns permissions with everything granted.

func SandboxPermissions

func SandboxPermissions() *Permissions

SandboxPermissions returns permissions with everything denied.

func (*Permissions) CheckEnv

func (p *Permissions) CheckEnv(name string) error

CheckEnv checks if accessing the given env var is allowed.

func (*Permissions) CheckNet

func (p *Permissions) CheckNet(host string) error

CheckNet checks if network access to the given host is allowed.

func (*Permissions) CheckRead

func (p *Permissions) CheckRead(path string) error

CheckRead checks if reading the given path is allowed.

func (*Permissions) CheckRun

func (p *Permissions) CheckRun(cmd string) error

CheckRun checks if running the given command is allowed.

func (*Permissions) CheckWrite

func (p *Permissions) CheckWrite(path string) error

CheckWrite checks if writing to the given path is allowed.

func (*Permissions) DeniesFS added in v0.13.0

func (p *Permissions) DeniesFS() bool

DeniesFS reports whether the policy forbids filesystem access in either direction. Callers use this to decide whether to close ambient FS surfaces that bypass CheckRead/CheckWrite (e.g. WASI FS mounts on the qjswasm backend). WASI mounts can't be read-only without read-only being granular at the mount layer, so denying either side closes the whole mount.

type ResourceLimits added in v0.13.0

type ResourceLimits struct {
	// MaxMemoryBytes caps total JS heap in bytes. When exceeded, the engine
	// aborts the current operation with an OOM-like error.
	MaxMemoryBytes int64
	// MaxStackBytes caps the JS stack in bytes. When exceeded, the engine
	// throws a stack overflow error (recoverable).
	MaxStackBytes int64
	// GCThresholdBytes tunes when the GC kicks in. Lower = more aggressive.
	GCThresholdBytes int64
}

ResourceLimits caps the resources a Runtime may consume. Only qjswasm honors these today (mapping to QuickJS-NG's JS_SetMemoryLimit / JS_SetMaxStackSize / JS_SetGCThreshold); other backends silently ignore them. Zero means "unlimited" (backend default).

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime holds a loaded JavaScriptCore library and a global JS context. Multiple Runtimes can coexist in the same process — each gets a dedicated OS thread for JSC access. All JSC operations are dispatched to this thread via a channel, ensuring thread identity across calls (required by JSC). The Runtime is safe for concurrent use from multiple goroutines.

func New

func New(opts ...Option) (*Runtime, error)

New creates a new JavaScriptCore runtime with a fresh global context. Each Runtime gets a dedicated OS thread for all JSC operations, ensuring thread identity across calls. Multiple Runtimes can coexist safely.

func (*Runtime) Bind

func (r *Runtime) Bind(name string, v any) error

Bind exposes a Go struct as a JavaScript object on globalThis. Exported struct fields become JS properties (read from current Go state), and methods on the pointer receiver become callable JS functions.

Field naming: if a field has a `js:"name"` tag, that name is used; otherwise the first letter is lowercased (e.g. Name → name).

Method naming: Go method names are converted to camelCase (e.g. Greet → greet, SetAge → setAge).

Fields are backed by the Go struct: reading a property in JS always returns the current Go value. Setting a property in JS updates the Go struct. Methods that mutate the struct are reflected on subsequent property reads.

func (*Runtime) Close

func (r *Runtime) Close() error

Close releases the JS global context and stops the dedicated JSC goroutine.

func (*Runtime) Engine added in v0.2.0

func (r *Runtime) Engine() string

Engine returns the name of the JS engine backend.

func (*Runtime) Eval

func (r *Runtime) Eval(code string) (*Value, error)

Eval evaluates JavaScript code and returns the result.

func (*Runtime) EvalAsync

func (r *Runtime) EvalAsync(code string) (*Value, error)

EvalAsync evaluates JavaScript code that may return a Promise, runs the event loop until the Promise resolves, and returns the result.

func (*Runtime) EvalAsyncWithContext

func (r *Runtime) EvalAsyncWithContext(ctx context.Context, code string) (*Value, error)

EvalAsyncWithContext evaluates JavaScript code that may return a Promise, runs the event loop until the Promise resolves or the context is cancelled/expired, and returns the result.

func (*Runtime) EvalWithContext

func (r *Runtime) EvalWithContext(ctx context.Context, code string) (*Value, error)

EvalWithContext evaluates JavaScript code and returns the result, respecting the provided context for cancellation and deadlines. Since JSC cannot be interrupted mid-execution, the context is checked before evaluation begins. For timeout control over async operations, use EvalAsyncWithContext instead.

func (*Runtime) Exec

func (r *Runtime) Exec(code string) error

Exec executes JavaScript code, discarding the result.

func (*Runtime) GlobalObject

func (r *Runtime) GlobalObject() *Value

GlobalObject returns the global object for this context.

func (*Runtime) LoadModule

func (r *Runtime) LoadModule(m Module) error

LoadModule registers a module on an existing Runtime.

func (*Runtime) NativeInstanceCount added in v0.4.0

func (r *Runtime) NativeInstanceCount() int

NativeInstanceCount returns the number of live native struct instances. Useful for testing and debugging instance lifecycle.

func (*Runtime) NewArray

func (r *Runtime) NewArray(items ...any) (*Value, error)

NewArray creates a new JavaScript array with the given items.

func (*Runtime) NewObject

func (r *Runtime) NewObject(props map[string]any) (*Value, error)

NewObject creates a new JavaScript object with the given properties.

func (*Runtime) NewUint8Array

func (r *Runtime) NewUint8Array(data []byte) (*Value, error)

NewUint8Array creates a JavaScript Uint8Array containing a copy of the given bytes.

func (*Runtime) RegisterFunc

func (r *Runtime) RegisterFunc(name string, fn GoFunc) error

RegisterFunc registers a Go function as a global JavaScript function.

func (*Runtime) RegisterFuncWithContext

func (r *Runtime) RegisterFuncWithContext(name string, fn GoFuncWithContext) error

RegisterFuncWithContext registers a Go function that receives a CallbackContext, allowing safe JSC access from within the callback without deadlocking.

func (*Runtime) RunEventLoop

func (r *Runtime) RunEventLoop() error

RunEventLoop processes the event loop until all pending operations complete. For short-lived scripts (timers, promises), the default timeout is 30 seconds. If an HTTP server (Ramune.serve) is active, the loop runs indefinitely.

func (*Runtime) RunEventLoopFor

func (r *Runtime) RunEventLoopFor(timeout time.Duration) error

RunEventLoopFor processes the event loop until all timers complete or the timeout is reached.

func (*Runtime) RunEventLoopWithContext

func (r *Runtime) RunEventLoopWithContext(ctx context.Context) error

RunEventLoopWithContext processes the event loop until all timers complete or the context is cancelled/expired.

func (*Runtime) Tick

func (r *Runtime) Tick() (bool, error)

Tick processes one round of the event loop (immediates + ready timers). Returns true if there are still pending timers or immediates.

func (*Runtime) Wake

func (r *Runtime) Wake()

Wake signals the event loop to process events immediately. Safe to call from any goroutine. Non-blocking.

type RuntimePool

type RuntimePool struct {
	// contains filtered or unexported fields
}

RuntimePool manages multiple Runtimes for parallel JS execution. Each Runtime has its own dedicated OS thread and independent JSC VM.

func NewPool

func NewPool(n int, opts ...Option) (*RuntimePool, error)

NewPool creates a pool of n Runtimes, each configured with the given options.

func (*RuntimePool) Addr

func (p *RuntimePool) Addr() string

Addr returns the listener address, or "" if not listening.

func (*RuntimePool) Broadcast

func (p *RuntimePool) Broadcast(code string) error

Broadcast executes JavaScript code on all workers in parallel.

func (*RuntimePool) Close

func (p *RuntimePool) Close() error

Close stops the HTTP server (if running) and closes all Runtimes.

func (*RuntimePool) Eval

func (p *RuntimePool) Eval(code string) (*Value, error)

Eval evaluates JavaScript code on the next available worker.

func (*RuntimePool) Exec

func (p *RuntimePool) Exec(code string) error

Exec executes JavaScript code on the next available worker, discarding the result.

func (*RuntimePool) ListenAndServe

func (p *RuntimePool) ListenAndServe(addr string, jsHandler string) error

ListenAndServe starts an HTTP server that dispatches requests round-robin across pool workers via per-worker channels.

func (*RuntimePool) Size

func (p *RuntimePool) Size() int

Size returns the number of workers in the pool.

func (*RuntimePool) StopHTTP

func (p *RuntimePool) StopHTTP()

StopHTTP stops the HTTP server and drains in-flight requests.

type SandboxConfig added in v0.8.0

type SandboxConfig struct {
	// Image is the Docker image to use. Default: "ubuntu:24.04".
	Image string
	// Mounts are additional bind mounts in "host:container[:ro]" format.
	Mounts []string
	// Env is additional environment variables for the container.
	Env map[string]string
	// Timeout is the maximum execution time. Default: 60s.
	Timeout time.Duration
	// SocketPath overrides the Docker socket path.
	SocketPath string
	// Network is the Docker network to connect the container to.
	// Use this to give the sandbox access to other services on the same network.
	Network string
	// ExtraHosts adds custom host-to-IP mappings (like --add-host).
	// Format: "hostname:ip" (e.g. "api-server:192.168.1.100").
	ExtraHosts []string
	// MemoryMB sets the container memory limit in megabytes. 0 = unlimited.
	MemoryMB int
	// CPUs sets the CPU limit (e.g. 1.5 = 1.5 cores). 0 = unlimited.
	CPUs float64
	// NoNetwork disables all network access from the container.
	NoNetwork bool
}

SandboxConfig configures Docker sandbox execution.

type SandboxResult added in v0.8.0

type SandboxResult struct {
	ExitCode int
	Stdout   string
	Stderr   string
}

SandboxResult holds the result of a sandboxed execution.

func SandboxEval added in v0.8.0

func SandboxEval(code string, cfg SandboxConfig) (*SandboxResult, error)

SandboxEval evaluates a JS expression inside a Docker container.

func SandboxRun added in v0.8.0

func SandboxRun(scriptPath string, cfg SandboxConfig) (*SandboxResult, error)

SandboxRun executes a ramune script inside a Docker container.

type SandboxRuntime added in v0.8.0

type SandboxRuntime struct {
	// contains filtered or unexported fields
}

SandboxRuntime holds Go function registrations for sandbox execution. When the binary is re-invoked inside Docker, the same functions are available because they are compiled into the binary.

func NewSandboxRuntime added in v0.8.0

func NewSandboxRuntime(opts ...Option) *SandboxRuntime

NewSandboxRuntime creates a new SandboxRuntime with the given options.

func (*SandboxRuntime) RegisterFunc added in v0.8.0

func (s *SandboxRuntime) RegisterFunc(name string, fn GoFunc)

RegisterFunc registers a Go function that will be available to JS code in the sandbox. The function is compiled into the binary, so it works both on the host and inside the Docker container.

func (*SandboxRuntime) SandboxEval added in v0.8.0

func (s *SandboxRuntime) SandboxEval(code string, cfg SandboxConfig) (*SandboxResult, error)

SandboxEval evaluates JS code inside a Docker container.

func (*SandboxRuntime) SandboxRun added in v0.8.0

func (s *SandboxRuntime) SandboxRun(scriptPath string, cfg SandboxConfig) (*SandboxResult, error)

SandboxRun executes a ramune script inside a Docker container.

type TickManager added in v0.8.0

type TickManager interface {
	// ProcessEvents drains pending events and delivers them to JavaScript.
	// Called on the dedicated engine goroutine during each event loop tick.
	ProcessEvents(r *Runtime)

	// HasActive returns true if there are pending operations that should
	// keep the event loop running.
	HasActive() bool

	// Close releases resources held by the manager.
	Close()
}

TickManager is the interface for custom event loop managers. External packages implement this to integrate async I/O with ramune's event loop.

type Value

type Value struct {
	// contains filtered or unexported fields
}

Value wraps a JavaScriptCore JSValueRef with lifecycle management. Call Close() to unprotect the value.

func (*Value) Attr

func (v *Value) Attr(name string) *Value

Attr returns a property by name from the JS object. Returns nil if this is not an object or the property doesn't exist.

func (*Value) AttrErr

func (v *Value) AttrErr(name string) (*Value, error)

AttrErr returns a property by name or an error.

func (*Value) Bool

func (v *Value) Bool() (bool, error)

Bool returns the value as a bool.

func (*Value) Bytes

func (v *Value) Bytes() ([]byte, error)

Bytes returns the contents of a TypedArray or ArrayBuffer as a Go byte slice.

func (*Value) Call

func (v *Value) Call(args ...any) (*Value, error)

Call calls this value as a function with the given arguments.

func (*Value) Close

func (v *Value) Close() error

Close queues the JSValueRef for unprotection at the next safe point. Uses the unprotectQueue to avoid deadlock when called from GoFunc callbacks (which already run on the dedicated JSC goroutine). Safe to call multiple times or on nil.

func (*Value) Delete

func (v *Value) Delete(name string) error

Delete removes a property from the JS object.

func (*Value) Float64

func (v *Value) Float64() (float64, error)

Float64 returns the value as a float64.

func (*Value) GoString

func (v *Value) GoString() (string, error)

GoString returns the JavaScript string value as a Go string.

func (*Value) Has

func (v *Value) Has(name string) bool

Has reports whether the JS object has the named property.

func (*Value) Index

func (v *Value) Index(i int) *Value

Index returns the value at the given array index.

func (*Value) Int64

func (v *Value) Int64() (int64, error)

Int64 returns the value as an int64 (truncated from float64, since JS has no integer type).

func (*Value) IsArray

func (v *Value) IsArray() bool

IsArray reports whether this value is a JavaScript array.

func (*Value) IsFunction

func (v *Value) IsFunction() bool

IsFunction reports whether this value is a JavaScript function.

func (*Value) IsNull

func (v *Value) IsNull() bool

IsNull reports whether this value is JavaScript null.

func (*Value) IsUndefined

func (v *Value) IsUndefined() bool

IsUndefined reports whether this value is JavaScript undefined.

func (*Value) Keys

func (v *Value) Keys() ([]string, error)

Keys returns all enumerable property names of the JS object.

func (*Value) Len

func (v *Value) Len() (int, error)

Len returns the "length" property as an integer. Works for arrays, strings, and any object with a length property.

func (*Value) Ptr

func (v *Value) Ptr() uintptr

Ptr returns the raw JSValueRef pointer.

func (*Value) SetAttr

func (v *Value) SetAttr(name string, val any) error

SetAttr sets a property on the JS object.

func (*Value) String

func (v *Value) String() string

String returns the JavaScript string representation.

func (*Value) ToMap

func (v *Value) ToMap() (map[string]any, error)

ToMap converts a JS object to a Go map via JSON serialization. Returns an error if the value is not a JSON-serializable object.

func (*Value) ToSlice

func (v *Value) ToSlice() ([]any, error)

ToSlice converts a JS array to a Go slice via JSON serialization.

Directories

Path Synopsis
bench
pool command
cmd
ramune command
Command ramune is a JavaScript runtime powered by JavaScriptCore via purego.
Command ramune is a JavaScript runtime powered by JavaScriptCore via purego.
ramune-toolchain command
Command ramune-toolchain is the development toolchain for ramune: check, fmt, lint, transpile, typegen, compile.
Command ramune-toolchain is the development toolchain for ramune: check, fmt, lint, transpile, typegen, compile.
examples
workers/custom-binding command
A runnable demonstration of a custom env binding.
A runnable demonstration of a custom env binding.
internal
gotranspiler
Package gotranspiler implements TypeScript to Go source code transpilation.
Package gotranspiler implements TypeScript to Go source code transpilation.
gotranspiler/composer
Package composer orchestrates the hybrid TS→Go extraction pipeline.
Package composer orchestrates the hybrid TS→Go extraction pipeline.
gotranspiler/picker
Package picker decides which top-level TypeScript declarations are safe to extract as native Go code in the hybrid TS→Go transpile pipeline.
Package picker decides which top-level TypeScript declarations are safe to extract as native Go code in the hybrid TS→Go transpile pipeline.
rslint/plugins/react/rules/no_unknown_property
cspell:disable — this file enumerates HTML / SVG / ARIA attribute names verbatim from React and the WHATWG / W3C specs, so it contains many attribute-name tokens (aria-*, SVG presentation attributes, popover / shadowroot attrs, …) that are not in a general English dictionary.
cspell:disable — this file enumerates HTML / SVG / ARIA attribute names verbatim from React and the WHATWG / W3C specs, so it contains many attribute-name tokens (aria-*, SVG presentation attributes, popover / shadowroot attrs, …) that are not in a general English dictionary.
rslint/rules/no_implied_eval
cspell:ignore sctx
cspell:ignore sctx
rslint/rules/no_misleading_character_class
Package no_misleading_character_class implements ESLint's no-misleading-character-class rule on top of the layered regex / JS string utilities in `internal/utils`.
Package no_misleading_character_class implements ESLint's no-misleading-character-class rule on top of the layered regex / JS string utilities in `internal/utils`.
rslint/tsgo_pinned/bundled
Package bundled provides access to files bundled with TypeScript.
Package bundled provides access to files bundled with TypeScript.
rslint/tsgo_pinned/compiler
Package compiler implements the TypeScript compiler.
Package compiler implements the TypeScript compiler.
rslint/tsgo_pinned/diagnostics
Package diagnostics contains generated localizable diagnostic messages.
Package diagnostics contains generated localizable diagnostic messages.
rslint/tsgo_pinned/jsnum
Package jsnum provides JS-like number handling.
Package jsnum provides JS-like number handling.
rslint/tsgo_pinned/nodebuilder
Exports interfaces and types defining the node builder - concrete implementations are on top of the checker, but these types and interfaces are used by the emit resolver in the printer
Exports interfaces and types defining the node builder - concrete implementations are on top of the checker, but these types and interfaces are used by the emit resolver in the printer
rslint/tsgo_pinned/printer
Package printer exports a Printer for pretty-printing TS ASTs and writer interfaces and implementations for using them Intended ultimate usage:
Package printer exports a Printer for pretty-printing TS ASTs and writer interfaces and implementations for using them Intended ultimate usage:
rslint/tsgo_pinned/stringutil
Package stringutil Exports common rune utilities for parsing and emitting javascript
Package stringutil Exports common rune utilities for parsing and emitting javascript
rslint/utils
cspell:ignore octals
cspell:ignore octals
tsgo/bundled
Package bundled provides access to files bundled with TypeScript.
Package bundled provides access to files bundled with TypeScript.
tsgo/compiler
Package compiler implements the TypeScript compiler.
Package compiler implements the TypeScript compiler.
tsgo/diagnostics
Package diagnostics contains generated localizable diagnostic messages.
Package diagnostics contains generated localizable diagnostic messages.
tsgo/jsnum
Package jsnum provides JS-like number handling.
Package jsnum provides JS-like number handling.
tsgo/nodebuilder
Exports interfaces and types defining the node builder - concrete implementations are on top of the checker, but these types and interfaces are used by the emit resolver in the printer
Exports interfaces and types defining the node builder - concrete implementations are on top of the checker, but these types and interfaces are used by the emit resolver in the printer
tsgo/printer
Package printer exports a Printer for pretty-printing TS ASTs and writer interfaces and implementations for using them Intended ultimate usage:
Package printer exports a Printer for pretty-printing TS ASTs and writer interfaces and implementations for using them Intended ultimate usage:
tsgo/pseudochecker
pseudochecker is a limited "checker" that returns pseudo-"types" of expressions - mostly those which trivially have type nodes
pseudochecker is a limited "checker" that returns pseudo-"types" of expressions - mostly those which trivially have type nodes
tsgo/stringutil
Package stringutil Exports common rune utilities for parsing and emitting javascript
Package stringutil Exports common rune utilities for parsing and emitting javascript
Package jsrt provides runtime support for TypeScript-to-Go transpiled code.
Package jsrt provides runtime support for TypeScript-to-Go transpiled code.
array
Package array provides JavaScript Array.prototype method equivalents using Go generics.
Package array provides JavaScript Array.prototype method equivalents using Go generics.
compat/lodash
Package lodash provides Go adapters for commonly used lodash/lodash-es functions.
Package lodash provides Go adapters for commonly used lodash/lodash-es functions.
compat/uuid
Package uuid provides a Go adapter for the npm "uuid" package.
Package uuid provides a Go adapter for the npm "uuid" package.
compat/zod
Package zod provides a Go adapter for the npm "zod" schema validation library.
Package zod provides a Go adapter for the npm "zod" schema validation library.
console
Package console provides console.log/error/warn for transpiled TypeScript code.
Package console provides console.log/error/warn for transpiled TypeScript code.
fetch
Package fetch provides the fetch() API for transpiled TypeScript code.
Package fetch provides the fetch() API for transpiled TypeScript code.
node/crypto
Package crypto provides Node.js crypto module equivalents for transpiled TypeScript code.
Package crypto provides Node.js crypto module equivalents for transpiled TypeScript code.
node/fs
Package fs provides Node.js fs module equivalents for transpiled TypeScript code.
Package fs provides Node.js fs module equivalents for transpiled TypeScript code.
node/http
Package http provides Node.js http module equivalents for transpiled TypeScript code.
Package http provides Node.js http module equivalents for transpiled TypeScript code.
node/path
Package path provides Node.js path module equivalents for transpiled TypeScript code.
Package path provides Node.js path module equivalents for transpiled TypeScript code.
promise
Package promise provides a Promise[T] type for transpiled async/await TypeScript code.
Package promise provides a Promise[T] type for transpiled async/await TypeScript code.
web
Package web provides Go struct definitions for Web API types used by transpiled TypeScript code.
Package web provides Go struct definitions for Web API types used by transpiled TypeScript code.
third_party
qjs
Package workers provides a Cloudflare-Workers-style handler runtime on top of Ramune.
Package workers provides a Cloudflare-Workers-style handler runtime on top of Ramune.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL