Skip to content

Releases: i2y/ramune

v0.21.0

24 May 22:39

Choose a tag to compare

v0.20.0

28 Apr 23:56

Choose a tag to compare

Full Changelog: v0.19.0...v0.20.0

v0.19.0

26 Apr 14:56

Choose a tag to compare

Full Changelog: v0.17.0...v0.19.0

v0.17.0

23 Apr 04:03

Choose a tag to compare

Highlights

  • TypeScript to JavaScript transpile now runs on typescript-go (TS 7 Beta). ramune run / eval / repl / workers dispatch / nodecompat require('./*.ts') all go through the same Go-native tsc that backs check / 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 in bunServerState made every Bun.serve request wait up to 10ms under the 3d95009 event-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/tsgotranspile package that wraps compiler.Program.Emit with a stateful Transpiler caching parsed lib.d.ts ASTs across calls (~33x speedup on warm reuse vs. a fresh Program per call).
  • Six previous esbuild.Transform call sites now route through it: cmd/ramune run / eval / repl, workers/dispatch.go module wrap, and nodecompat_esm.go require('./*.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 / .cjs inputs force their module kind in tsgo regardless of the Module option, so nodecompat renames those to .js before 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: two s.rt.Wake() calls after s.requests <- req (WebSocket upgrade path + normal fetch path).
  • websocket.go: new wsManager.wakeFn plumbed through installWebSocket, called after each of the four events = append(...) sites (open / close / error / message).
  • The 3d95009 busy-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.md removed (content reachable from README).
  • TS 7 Beta (tsgo) surfaced in the site hero caption (MIT · npm · Go 1.26+ · TypeScript 7 Beta (tsgo)) and llms.txt.

Full Changelog: v0.16.0...v0.17.0

v0.16.0

22 Apr 13:28

Choose a tag to compare

Full Changelog: v0.15.1...v0.16.0

v0.15.0 — --hybrid graduates

21 Apr 21:32
@i2y i2y

Choose a tag to compare

--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:

Soundness fixes in the emitter

  • Mixed int / float64 comparisons widen to float64 instead of truncating with int(n) (fixes n < 0 for negative fractionals, n === 0 for non-integers).
  • Mixed % uses math.Mod for IEEE-754 semantics instead of int(n) % int(m).
  • emitBlock no longer injects the function-level default return into nested for / if bodies (the bug that caused countPrimes(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: hasPendingLocked now tracks in-flight native Promise bridges so the event loop doesn't exit before a resolve lands (3-backend fix).
  • qjswasm: js_std_await deadlock in the promise bridge fixed by routing results through a globalThis slot.
  • Native struct instances: zero-arg void mutator methods (e.g. counter.increment()) are now exposed.
  • Bun.WebView no longer clobbers Ramune.WebView at 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

19 Apr 19:57
@i2y i2y

Choose a tag to compare

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

19 Apr 08:45
@i2y i2y

Choose a tag to compare

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 quickjs removed. The modernc.org/quickjs backend was removed after qjswasm beat it on every bench axis (CPU, HTTP req/s, multi-worker scaling). Migration: use -tags qjswasm for the pure-Go path. A tripwire file makes -tags quickjs builds 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 of modernc.org/libc was 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/libc directive deleted from go.mod; upstream libc resumed (still used indirectly by modernc.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.md updated 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

07 Apr 23:35
@i2y i2y

Choose a tag to compare

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