Releases: i2y/ramune
v0.21.0
Full Changelog: v0.20.0...v0.21.0
v0.20.0
Full Changelog: v0.19.0...v0.20.0
v0.19.0
Full Changelog: v0.17.0...v0.19.0
v0.17.0
Highlights
- TypeScript to JavaScript transpile now runs on typescript-go (TS 7 Beta).
ramune run/eval/repl/ workers dispatch / nodecompatrequire('./*.ts')all go through the same Go-native tsc that backscheck/fmt/lint/ gotranspiler. One compiler across every TS surface, no subprocess. esbuild is retained for bundling and the goja parse-failure lowerer. - HTTP throughput regression fixed. A missing
Wake()call inbunServerStatemade everyBun.serverequest wait up to 10ms under the3d95009event-loop poll cap. Benchmarks on macOS 26.2 / M4 Max (wrk 2x50, 10s,bench/http_ramune.js): 7.6K to 96.6K req/s, 6.59ms to 483us latency. WebSocket had the same gap and is also fixed.
What landed
feat(transpile): adopt typescript-go for TS to JS emit
- New
internal/tsgotranspilepackage that wrapscompiler.Program.Emitwith a statefulTranspilercaching parsedlib.d.tsASTs across calls (~33x speedup on warm reuse vs. a freshProgramper call). - Six previous
esbuild.Transformcall sites now route through it:cmd/ramunerun / eval / repl,workers/dispatch.gomodule wrap, andnodecompat_esm.gorequire('./*.ts'). - The 30-line regex rewriter that post-processed esbuild output into CommonJS shape is gone: tsgo emits
exports.foo = foo/module.exports = ...natively. - Sharp edge:
.mjs/.cjsinputs force their module kind in tsgo regardless of theModuleoption, so nodecompat renames those to.jsbefore feeding tsgo when CommonJS emit is requested. - esbuild stays for bundling (
ramune build,Ramune.build, npm-dep cache, ESM fallback) and the goja parse-failure lowerer (lowerForGoja): tsc does not lower top-level await in CJS emit, so esbuild's IIFE wrap is the only semantics that lets TLA-containing source run under goja.
fix(eventloop): wake JS loop on bun serve request and websocket event push
Commit 3d95009 added a 10ms poll cap when hasPendingLocked() && nextDelayLocked() <= 0, on the assumption that every async manager calls r.Wake() after enqueuing work. bunServerState and wsManager did not, so Bun.serve and WebSocket sat on the cap per event.
buncompat.go: twos.rt.Wake()calls afters.requests <- req(WebSocket upgrade path + normal fetch path).websocket.go: newwsManager.wakeFnplumbed throughinstallWebSocket, called after each of the fourevents = append(...)sites (open / close / error / message).- The
3d95009busy-loop cap stays in place for paths that already call Wake correctly (fetch in flight, async sockets, native Promise bridge).
A/B measurement on macOS 26.2 / M4 Max (wrk 2x50, 10s):
| Binary | req/s | latency |
|---|---|---|
Pre-3d95009 (2f72297) |
96,683 | 483us |
| HEAD without fix | 7,603 | 6.59ms |
| HEAD with fix | 96,581 | 483us |
docs
- Tagline refresh around "soundness-gated AOT native compilation" across README, npm package, landing site, and SKILL.md.
- Standalone
TRANSPILER.mdremoved (content reachable from README). - TS 7 Beta (tsgo) surfaced in the site hero caption (
MIT · npm · Go 1.26+ · TypeScript 7 Beta (tsgo)) andllms.txt.
Full Changelog: v0.16.0...v0.17.0
v0.16.0
Full Changelog: v0.15.1...v0.16.0
v0.15.0 — --hybrid graduates
--hybrid graduates: automatic TS→native-Go compilation
ramune compile --hybrid app.ts walks your TypeScript, extracts every top-level function / pure class whose signature and body are provably semantics-equivalent to Go, and links them as native code in the compiled binary. Rejected functions keep running on the JS floor — no hand-fixes, no silent failures.
What the picker accepts now (since v0.13.2)
Primitive / T[] / Promise<primitive> signatures · pure classes (primitive fields, constructor-initialised, this-method bodies) · await on same-file async chains · Math.* · Number.* · string / array method safelists · callback-driven map / filter / forEach / some / every via the *JSFunc bridge · named-interface struct params · multi-file projects (the picker walks every user TS file reachable from the entry's import graph, excluding .d.ts and node_modules).
Performance
Biggest wins on the no-JIT backends (qjswasm, goja): extractable CPU-heavy kernels typically go 10×-350× faster than JS-only. On JSC+JIT the gains are narrower and pattern-dependent — recursion-heavy kernels win, tight integer-modulo loops and array-arg marshalling can regress. Three runnable examples with wrk / bench numbers:
examples/hybrid/— minimal fib / primes / array-marshal benchexamples/hybrid-hono/— Hono web appwrkbench across both backendsexamples/hybrid-multifile/— cross-file extraction demo
Soundness fixes in the emitter
- Mixed
int/float64comparisons widen tofloat64instead of truncating withint(n)(fixesn < 0for negative fractionals,n === 0for non-integers). - Mixed
%usesmath.Modfor IEEE-754 semantics instead ofint(n) % int(m). emitBlockno longer injects the function-level default return into nestedfor/ifbodies (the bug that causedcountPrimes(10000)to return 0).
--tags on compile
ramune compile app.ts --tags qjswasm picks the backend the compiled binary embeds — ship the same source as JSC on macOS/Linux and FROM scratch-compatible qjswasm elsewhere. Combined with --hybrid, the qjswasm path is where native extraction has the biggest lift.
Other runtime fixes
- Promise bridge:
hasPendingLockednow tracks in-flight native Promise bridges so the event loop doesn't exit before a resolve lands (3-backend fix). - qjswasm:
js_std_awaitdeadlock in the promise bridge fixed by routing results through a globalThis slot. - Native struct instances: zero-arg
voidmutator methods (e.g.counter.increment()) are now exposed. Bun.WebViewno longer clobbersRamune.WebViewat polyfill install.- Event loop: busy-loop capped when pending work exists without a JS timer.
Housekeeping
Legacy goir IR pipeline gated behind -tags legacy_goir (default build/test is noticeably faster).
v0.13.2
What's Fixed
-tags qjswasm now builds from downstream projects. v0.13.1 leaked replace github.com/fastschema/qjs => ./third_party/qjs as an internal build contract — Ramune's own CI was green, but any project depending on github.com/i2y/ramune@v0.13.1 failed with opt.DisableFS undefined (type qjs.Option has no field or method DisableFS) when built with -tags qjswasm, because replace directives don't propagate through go.mod to downstream consumers.
Fix: inline-vendor the qjs fork into Ramune's module. third_party/qjs/ no longer has its own go.mod (sub-module boundary removed), so it's just a regular package of the Ramune module. Imported as github.com/i2y/ramune/third_party/qjs. Downstream projects that depend on Ramune get the fork transparently — no replace directive required in their own go.mod.
No behavior changes. The DisableFS patch, embedded qjs.wasm, wazero integration, and all sandbox semantics are identical to v0.13.1. This is purely a Go-module plumbing fix.
Migration
If you tried -tags qjswasm on v0.13.1 and hit the DisableFS error, just bump:
go get github.com/i2y/ramune@v0.13.2
go build -tags qjswasm ./...
No source changes needed. If you worked around the bug by adding replace github.com/fastschema/qjs => ... to your own go.mod, you can now remove it.
Upstream Reconciliation (future)
The Option.DisableFS patch in third_party/qjs/ is small (~10 LoC) and security-meaningful (blocks ambient WASI CWD mount under SandboxPermissions()). A future release may submit it upstream to fastschema/qjs; if merged, Ramune will switch back to the public module path. No user-visible change when that happens.
v0.13.1
What's New
qjswasm backend (new, replaces modernc/quickjs): QuickJS-NG compiled to WebAssembly and driven by wazero's compiler-mode JIT via fastschema/qjs. 1.7-2.3× faster than the removed modernc/quickjs backend at every worker count, scales 5.72× across 6 workers (vs modernc's 4.14×), monotonic and linear. Pure Go, no Cgo, works on Windows / macOS / Linux / FreeBSD / FROM scratch Docker.
In-process sandbox for untrusted JS: layered defense-in-depth on the qjswasm backend — wazero's WASM linear memory isolates the VM, SandboxPermissions() auto-disables the WASI filesystem mount (so a QuickJS-NG VM escape can't pivot to host files via WASI), WithResourceLimits caps JS heap / stack / GC at the QuickJS-NG level, and the existing permission-gated Go bridges gate all I/O. See README "In-process Sandbox for Untrusted JS".
ResourceLimits option: new WithResourceLimits(ramune.ResourceLimits{MaxMemoryBytes, MaxStackBytes, GCThresholdBytes}) option. Honored by qjswasm (maps to QuickJS-NG's JS_SetMemoryLimit / JS_SetMaxStackSize / JS_SetGCThreshold); other backends silently ignore. MaxExecutionTime accepted but not yet enforced (C-shim interrupt handler wiring pending).
Vendored fastschema/qjs fork (third_party/qjs/): 2-line patch adds Option.DisableFS so the ambient WASI CWD mount can be skipped under SandboxPermissions(). Prebuilt qjs.wasm (1 MB) embedded into the Go binary — no wasi-sdk required. QuickJS-NG LICENSE text preserved at third_party/qjs/qjswasm/quickjs/LICENSE for MIT compliance on binary distribution.
Breaking Changes
-tags quickjsremoved. Themodernc.org/quickjsbackend was removed after qjswasm beat it on every bench axis (CPU, HTTP req/s, multi-worker scaling). Migration: use-tags qjswasmfor the pure-Go path. A tripwire file makes-tags quickjsbuilds fail loudly (undefined: modernc_quickjs_backend_removed_use_tags_qjswasm) rather than silently falling through to JSC.third_party/libc/removed. The per-TLS allocator + page-owner routing fork ofmodernc.org/libcwas needed to scale the modernc/quickjs backend past 1 worker. With qjswasm's per-runtime wasm linear memory, the global-allocator contention is gone, so the fork is no longer needed.replace modernc.org/libcdirective deleted fromgo.mod; upstream libc resumed (still used indirectly bymodernc.org/sqlite).
Docs
- README restructured around 4 audience personas ((1) Go embedding, (2) SaaS platform foundation for running untrusted JS, (3) Cloudflare Workers self-hosting, (4) CLI runtime).
- Performance section reordered: vs Go-embedded JS runtimes (primary) → Multi-Runtime Pool → vs Bun/Node (secondary).
- New "In-process Sandbox for Untrusted JS" section with layered defense-in-depth explanation.
CLAUDE.md,llms.txt,llms-full.txt,SKILL.mdupdated for the new backend matrix.
Note on v0.13.0
v0.13.0 was tagged but its CI run failed on a gofmt check (trailing blank lines in callback_qjswasm.go). v0.13.1 is the actual first green release in the 0.13 series — use v0.13.1.
v0.7.0
What's New
- Buffer (60% -> 90%): Float/Double read/write, signed variants, encoding support, ArrayBuffer/TypedArray interop
- HTTP Streaming: Response ReadableStream, chunked transfer, gzip/deflate/brotli compression, configurable timeout
- fetch Streaming: Response.body ReadableStream, AbortController, redirect handling
- Stream Backpressure: Go io.Reader/Writer bridge, byte stream support
- SQLite (basic -> full): db.transaction(), WAL mode, prepared statement cache (LRU), PRAGMA defaults, db.query()
- net.createServer(): TCP server with connection/listening/close/error events
- Bun.build(): esbuild-backed bundler API (entrypoints, outdir, target, minify, splitting, sourcemap, external)
- SharedArrayBuffer + Atomics: Go []byte shared memory, load/store/add/sub/and/or/xor/exchange/compareExchange, wait/notify, worker transfer