intertwingly

It’s just data

Ruby2JS Self-Hosting Update


Four commands and you have a Ruby-to-JavaScript converter running in Node.js:

wget https://www.ruby2js.com/demo/selfhost/ruby2js.mjs
chmod +x ruby2js.mjs
npm install @ruby/prism
./ruby2js.mjs -e 'puts "hello"'

That's it. No Ruby installation required.

Before running downloaded scripts, review them. The ruby2js.mjs file is generated JavaScript -- comments may be slightly relocated, but otherwise the code is readable, idiomatic, and nearly hand-coded. You can also browse the Ruby source that produced it.


What Changed Since November

In my previous post, I had a proof of concept: the core converter running in the browser at ~250KB. Since then, the self-hosting work has matured significantly.

All Converters Now Self-Hosted

The original post mentioned that basic literals, variables, and control flow worked. Now everything is transpiled:

The unified ruby2js.mjs bundle is about 200KB uncompressed and unminified (excluding Prism WASM). It works in Node.js CLI, browser imports, and CI tests -- same code everywhere.

245 Tests Passing

The transliteration test suite has been transpiled from Ruby to JavaScript. The same tests that validate the Ruby converter now validate the JavaScript version:

Ready specs:
  transliteration_spec: 245 passed, 2 skipped

The two skipped tests involve Proc#source_location -- a Ruby introspection feature that doesn't translate to JavaScript. Everything else passes.

New Documentation

The Ruby2JS User's Guide is new, with live demos on every page:

New Filters

Polyfill Filter -- Adds JavaScript prototype polyfills for Ruby methods that don't have direct equivalents. Instead of transforming .first to [0], it preserves the Ruby method name and adds a polyfill definition:

Object.defineProperty(Array.prototype, "first", {
  get() { return this[0] },
  configurable: true
});

Pragma Filter -- Extracted from the selfhost work into a general-purpose filter. Pragmas are comments that control transpilation:

x ||= default           # Standard: may become ??= or ||= depending on context
y ||= default # Pragma: ??      # Force nullish coalescing
z ||= default # Pragma: logical # Force logical OR

Since pragmas are comments, they have no effect when code runs in Ruby -- making them perfect for dual-target development.


The Architecture

Ruby Source Code
       |
       v
@ruby/prism (WebAssembly, ~2.7MB)
       |
       v
Prism AST (JavaScript objects)
       |
       v
PrismWalker (transpiled from Ruby)
       |
       v
Parser-compatible AST
       |
       v
Converter + Handlers (transpiled from Ruby)
       |
       v
Serializer (transpiled from Ruby)
       |
       v
JavaScript Output

The key take-away: a "dual-target" approach that lets you maintain a single codebase that works in both environments is viable. This is the foundation for offline-first scoring in my showcase dance competition app -- sharing validation and business logic between Rails and the browser.


What's Not Done Yet

Filters aren't transpiled yet. The self-hosted version does basic transliteration -- Ruby syntax to JavaScript syntax. The filters that transform Ruby idioms to JavaScript idioms (functions, camelCase, esm, etc.) still require the Ruby version. But this is just a matter of time. I hope to be complete around the time that Ruby 4.0 is released (in about two weeks).


Try It

CLI (Node.js):

wget https://www.ruby2js.com/demo/selfhost/ruby2js.mjs
chmod +x ruby2js.mjs
npm install @ruby/prism
echo 'class Foo; def bar; 42; end; end' | ./ruby2js.mjs

Browser:

<script type="module">
  import { convert } from 'https://www.ruby2js.com/demo/selfhost/ruby2js.mjs';
  console.log(convert('puts "hello"'));
</script>

Or visit the self-hosted demo directly.


What This Enables

Self-hosting isn't just a technical curiosity. It proves that Ruby2JS can handle substantial, real-world Ruby code -- its own implementation. Patterns discovered during self-hosting have been extracted into general-purpose filters that benefit all users.

More practically, it means that "dual target" and offline-first functionality in Rails applications is achievable -- run the same Ruby code on the server and in the browser, with no runtime overhead.


Ruby2JS is open source: github.com/ruby2js/ruby2js