A JavaScript/TypeScript runtime for Go — powered by JavaScriptCore, no Cgo required. Pure Go except for JSC: type checker and formatter (typescript-go), linter (rslint), bundler (esbuild), and all Node.js polyfills are built in with zero external tool dependencies.
Named after Ramune, a Japanese carbonated soft drink served in a Codd-neck bottle.
ramune run server.ts # Run TypeScript
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 binaryRamune is two things:
- A JS/TS runtime like Bun or Deno, but built in Go
- An embeddable JS engine for Go applications
It loads Apple's JavaScriptCore dynamically via purego — no C compiler, no Cgo, just go build.
JavaScriptCore is built into macOS — no extra dependencies.
go install github.com/i2y/ramune/cmd/ramune@latest
ramune setup-jit # enable JIT (~10x faster, recommended)sudo apt install libjavascriptcoregtk-4.1-dev # JSC runtime (required)
go install github.com/i2y/ramune/cmd/ramune@latestMulti-runtime (RuntimePool, worker_threads) works out of the box on x86_64. On arm64, gcc is required for cgo signal forwarding (apt install gcc).
go install -tags nosqlite -ldflags="-s -w" github.com/i2y/ramune/cmd/ramune@latest-tags nosqlite excludes bun:sqlite. -ldflags="-s -w" strips debug info.
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 scriptramune eval "1 + 2"
ramune eval "require('crypto').randomUUID()"
ramune eval "const x: number = 42; x" # TypeScript worksramune replPackages 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.
ramune testFinds *.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);
});ramune compile server.ts -o myserver --http --minify
./myserver # self-contained binary with embedded JSThe 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 ~21MB (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).
ramune check app.ts # check files
ramune check src/ # check directory
ramune run --check app.ts # check then runUses typescript-go (TypeScript 7.0-dev, backward-compatible with TS 5.x) built into Ramune — no external tools required.
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-fixThe 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.
ramune init # create package.json
ramune add lodash dayjs # add dependencies
ramune remove lodash # remove
ramune install # install allramune build app.ts --outdir=dist --bundle --minifyramune run app.ts # default: all allowed
ramune run --sandbox app.ts # deny all
ramune run --sandbox --allow-read=/tmp app.ts # selective accessFlags: --allow-read, --allow-write, --allow-net, --allow-env, --allow-run.
.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.tsRun scripts defined in package.json:
ramune run dev # runs "scripts.dev" from package.json
ramune run build # runs "scripts.build"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
}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
})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"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")rt, _ := ramune.New(
ramune.NodeCompat(),
ramune.Dependencies("lodash@4"),
)
val, _ := rt.Eval(`lodash.chunk([1,2,3,4,5,6], 2)`)val, _ := rt.EvalAsync(`
new Promise(resolve => setTimeout(() => resolve(42), 100))
`)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 });
});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("computeHeavy()") // round-robin dispatch
pool.Broadcast("globalThis.config = {debug: true}") // run on all
// Multi-worker HTTP server
pool.ListenAndServe(":3000", `
globalThis.__poolHandle = function(req) {
return { status: 200, body: "Hello from worker!" };
};
`)With CPU-heavy handlers, 3 workers achieve 2.7x throughput vs single-threaded Bun.
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 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, 101K req/s) |
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 |
bun:sqlite |
Supported (pure Go, modernc.org/sqlite) |
Bun.* |
Alias for Ramune.* (partial Bun compatibility) |
Go's garbage collector can interfere with JavaScriptCore under high load. Ramune provides tunable GC settings:
rt, _ := ramune.New(ramune.NodeCompat(), ramune.WithGC(ramune.GCConfig{
DisableAutoGC: true, // disable Go's auto GC during HTTP serving
GCInterval: 2000, // manual GC every N requests
GCPercent: 100, // Go GC target %
}))For most use cases (CLI, scripting, SDK), defaults work fine. Tuning is only needed for high-throughput HTTP servers.
rt, _ := ramune.New(
ramune.NodeCompat(),
ramune.WithPermissions(&ramune.Permissions{
Read: ramune.PermGranted,
ReadPaths: []string{"/tmp", "/var/data"},
Write: ramune.PermDenied,
Net: ramune.PermDenied,
}),
)Benchmarks on Apple M4 Max (macOS, JIT enabled):
| Test | Ramune | Bun | Node.js |
|---|---|---|---|
| Hello World startup | 14.2ms | 7.1ms | 18.0ms |
| Fibonacci(35) | 46.2ms | 40.0ms | 64.7ms |
| JSON 10K objects | 17.6ms | 9.7ms | 22.9ms |
| Crypto SHA256 x1000 | 19.8ms | 11.0ms | 20.4ms |
| File I/O x100 | 20.7ms | 13.3ms | 24.2ms |
| HTTP req/s (single) | 101K | 156K | 112K |
Ramune runs multiple JSC VMs in parallel on separate OS threads (Bun/Node are single-threaded):
| Workers | req/s | Scaling |
|---|---|---|
| 1 | 44K | 1.0x |
| 2 | 65K | 1.48x |
| 3 | 68K | 1.56x |
Measured with a JSON generate/filter/map handler (200 objects per request).
| Test | Ramune (JSC+JIT) | goja | otto |
|---|---|---|---|
| Fibonacci(35) | 31ms | 1,964ms (64x slower) | 26,203ms (852x slower) |
| JSON 10K objects | 0.9ms | 11ms (13x slower) | 27ms (31x slower) |
Ramune uses Apple's JavaScriptCore with JIT compilation. goja and otto are pure Go interpreters.
Run make bench to reproduce.
On macOS, JIT requires a code signing entitlement:
# After go install:
ramune setup-jit
# Or when building from source:
make build-cliLinux does not need JIT setup.
| 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 | 70% | Buffer | 60% | |
| http/https | 70% | assert | 80% | |
| net/tls | 60% | dns | basic | |
| worker_threads | 70% | readline | 70% | |
| vm | 70% | querystring | 80% | |
| timers/promises | 70% | perf_hooks | basic | |
| util | 80% (types, promisify, format) | process | 85% (signals, exit, env) |
| API | Status |
|---|---|
fetch |
Supported (Go net/http backend) |
ReadableStream / WritableStream / TransformStream |
Supported (pipeTo, pipeThrough, tee, async iterator) |
crypto.subtle |
Supported (digest, sign/verify, encrypt/decrypt, importKey/exportKey, deriveBits/deriveKey) |
crypto.getRandomValues / randomUUID |
Supported |
Blob / File |
Supported |
FormData |
Supported |
Headers / Request / Response |
Supported (ReadableStream body) |
TextEncoder / TextDecoder |
Supported (UTF-8) |
AbortController / AbortSignal |
Supported |
URL / URLSearchParams |
Supported |
WebSocket |
Supported (server-side via Ramune.serve) |
performance.now / mark / measure |
Supported |
structuredClone |
Supported (circular refs, Map, Set, Date, RegExp, TypedArray) |
setTimeout / setInterval |
Supported |
navigator |
Supported (userAgent, platform, hardwareConcurrency) |
console.time / table / trace |
Supported |
Ramune also supports package.json "exports" field resolution (conditional exports with require/import/default and subpath exports).
- N-API / Native addons: Not supported. Packages that require
.nodenative 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 JSC context deadlock).
- Windows: No JavaScriptCore available.
- Linux multi-runtime: Architecture-dependent signal handling. On arm64,
CGO_ENABLED=1and 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 limit: 2-3 workers recommended for sustained high-throughput; 4+ may trigger JSC JIT contention.
| Dependency | Required | Purpose |
|---|---|---|
| Go 1.26+ | Yes | Build and install |
| macOS or Linux | Yes | macOS: JSC built-in. Linux: apt install libjavascriptcoregtk-4.1-dev |
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.
make ci # fmt + build + vet + test
make build-cli # build with JIT entitlement (macOS)
make bench # benchmark vs Bun/Node
make sync # sync typescript-go & rslint from submodulesMIT
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/) |
| evanw/esbuild | MIT | TypeScript transpilation, bundling | Go module dependency |
License texts for source-copied projects are in internal/tsgo/LICENSE and internal/rslint/LICENSE.
The Ramune logo includes the Go Gopher, originally designed by Renée French, licensed under Creative Commons Attribution 4.0.
