Over the weekend I cobbled together ruby-syntax-tree.github.io, and I thought I’d share a quick post about what it is, how it works, and what I learned while I built it.
What is it?
.wasm file that you can execute natively in the browser or through a polyfill. (You can actually execute it on any WebAssembly runtime, but for my purposes the browser will do.) For more information on the WASI Ruby project, check out the final report.
So, to get to the titular question of this section. ruby-syntax-tree.github.io is a website that uses the new WASI ABI functionality of Ruby to compile a
.wasm file containing both the Ruby runtime and the source for the Syntax Tree gem. It then boots a virtual machine within the browser and uses it to transpile your Ruby into equivalent s-expressions.
How it works
Let’s start from the ground up. The first part of building the site was to build the
.wasm file containing the Ruby runtime and the Ruby files necessary to run Syntax Tree. Following instructions from the ruby/ruby.wasm README, I ran a bunch of commands locally to get my own machine up and running. Once I verified that I had everything I needed, I replicated that process in a Rakefile.
One of the trickier parts was including Syntax Tree itself. I briefly considering including it as a git submodule so that it could be mounted as part of the wasi-vfs build process. I ended up scrapping that solution since dependabot wouldn’t be able to automatically update it, and I realized that if I ever wanted any other gems loaded I wanted a reproducable solution.
Instead, I ended up using bundler as normal to install the dependencies. Once they were installed, I knew they existed somewhere on the system. I also knew that
require "bundler/setup" sets up the load paths so that you can require gems my name. So I decided to piggy-back on this functionality to copy the gem contents into the mounted directory. I found the right directory based on the
$: load path global variable.
With everything in place, I used wasi-vfs to build the file. For packaging this file into the built web application, I used
esbuild. They don’t have built-in support for
.wasm files, but adding support isn’t hard. You can write your own plugin by mostly copy-pasting from their docs. That resulted in the esbuild plugin here. That makes it so that you can import
.wasm files as you would normally import modules. The default export is a function that accepts the imports for the module, and it asynchronously returns the module. You can then use the
ruby-head-wasm-wasi npm package that Ruby now ships to wrap up the module and provide an
eval function to evaluate RUby code.
Once the module is imported, it’s a matter of requiring the correct files at the top of the file. That’s accomplished by requiring the native gems that we need, then adding
lib directory we put the Syntax Tree gem into early to the load path, then requiring it. All of that is encapsulated in the createRuby.ts file. The actual web application is a relatively standard React/TypeScript application. Since it’s not the novelty of this post, I won’t cover it, but you can check out the source here.
What I learned
I learned a bunch of stuff with this experiment! Here are a couple of things that I found useful that I feel are worth sharing:
- Rake proxies all of the
FileUtilsclass methods as instance methods, so you can call things like
cp_rfrom within Rake tasks and it will just work.
- You can reflect on load paths to find out where gems are housed by looking at
- There’s a new-ish
AwaitedTypeScript generic type that will return the type encapsulated by a
esbuildis really well documented and plugins are not too hard to write.
- You can deploy directly to GitHub pages through GitHub actions even if you have to invoke something like rake. (See here)
At some point I’d like to add the ability to format the source, add a better editor, and general improve the styling and UX. But for now, the current state is up at ruby-syntax-tree.github.io.← Back to home