Call JavaScript from Ruby. No bundling, no eval strings, no V8.
Powered by Boa, a JavaScript engine written in pure Rust.
require 'boax'
intl = Boax.import('Intl')
nf = intl.NumberFormat.new('en-US', { style: 'currency', currency: 'USD' })
nf.format(1234.56) # => "$1,234.56"
Boax.init(root: __dir__)
_ = Boax.import('lodash-es')
_.chunk([1, 2, 3, 4, 5, 6], 2).to_ruby # => [[1, 2], [3, 4], [5, 6]]
_.uniq([1, 1, 2, 3, 3]).to_ruby # => [1, 2, 3]Proof of concept. This is an early exploration of embedding the Boa JS engine in Ruby via Rust. It works, but the API may change, performance hasn't been tuned, and Boa itself is pre-1.0.
Boax.eval(code)— evaluate JS expressions, returns native Ruby typesBoax.import(name)— import JS globals (Math,JSON,Date) or ES module packages (lodash-es)Boax.require(name)— load CommonJS packages (minimist, etc.)- Proxy objects —
method_missingforwards Ruby calls to JS:math.sqrt(144),_.chunk([1,2,3], 2) - Constructors —
Date.new(2024, 0, 15)callsnew Date(2024, 0, 15)in JS - Type conversion — nil, bool, integer, float, string, symbol, array, hash (both directions)
to_ruby— deep conversion of JS arrays/objects to Ruby arrays/hashes- npm packages — ES module resolution via oxc_resolver against
node_modules/ - Node API modules —
path,util,events,fs,process,os,querystring,string_decoder,assert,url,buffer,crypto,stream - Web API globals —
URL,URLSearchParams,console,setTimeout/setInterval,TextEncoder/TextDecoder,structuredClone
- Intl — polyfilled
NumberFormat(currency, percent) andDateTimeFormatfor common locales; not full ICU coverage - Performance — Boa has no JIT; compute-heavy JS will be slower than V8
- Streams — MVP without backpressure; no real async I/O
- HTTP, child_process — not yet implemented
| mini_racer | boax | |
|---|---|---|
| Engine | V8 (~45MB binary) | Boa (~5-10MB, pure Rust) |
| Interface | ctx.eval("...") |
Boax.import('lodash-es').uniq([1,1,2]).to_ruby |
| ES modules | No | Yes |
| npm packages | Manual bundling required | Boax.import('package-name') |
| Node APIs | None | 13 built-in modules |
| Platforms | No Windows, fork-safety issues | Everywhere Rust compiles |
Requirements: Ruby 3.1+, Rust 1.70+, npm (for package imports).
git clone https://github.com/rubys/boax
cd boax
bundle install
bundle exec rake compile
# Run the tests
npm install # installs lodash-es and minimist for integration tests
bundle exec rspecrequire 'boax'
# Evaluate JS
Boax.eval("1 + 2") # => 3
Boax.eval("'hello'.repeat(3)") # => "hellohellohello"
# Import JS globals
json = Boax.import('JSON')
json.stringify({ a: 1, b: [2, 3] }) # => '{"a":1,"b":[2,3]}'
# Import npm packages (requires Boax.init and npm install)
Boax.init(root: __dir__)
_ = Boax.import('lodash-es')
_.camelCase('foo-bar') # => "fooBar"
# Node built-in modules
path = Boax.import('path')
path.join('/foo', 'bar', 'baz') # => "/foo/bar/baz"
fs = Boax.import('fs')
fs.writeFileSync('/tmp/test.txt', 'hello')
fs.readFileSync('/tmp/test.txt') # => "hello"
# CommonJS packages (use require instead of import)
minimist = Boax.require('minimist')
minimist.call(['--foo', 'bar', 'hello']).to_ruby
# => {"_" => ["hello"], "foo" => "bar"}
# Deep conversion to Ruby types
Boax.eval("({a: [1, {b: 2}]})").to_ruby
# => {"a" => [1, {"b" => 2}]}Ruby method calls on Boax::JsObject are proxied to JavaScript via method_missing. When a Ruby built-in method shadows a JS method name, append ! to bypass Ruby and call the JS method directly.
The most common case is Promise.then() — Ruby's Kernel#then intercepts the call before method_missing gets it:
promise = fs["promises"].readFile("data.txt")
# Calls Ruby's Kernel#then, not JS Promise.then()
promise.then(callback)
# Strips the !, calls JS promise.then(callback)
promise.then!(callback)This works for any JS method name that conflicts with a Ruby method. The ! is stripped before the JS property lookup, so obj.then!(cb) calls obj.then(cb) on the JS side.
This project is licensed under the Unlicense or MIT licenses, at your option.