Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

January 7, 2024

Last updated January 7, 2024

How to use Bun with Ruby on Rails

In the Ruby on Rails world, we often leverage tools like node.js and yarn as a build step for front-end tooling like Tailwind CSS, Stimulus.js, and many other front-end technologies.

As more hype grows about removing the “build” from the equation, I, for one, don’t necessarily see the additional dependencies and tooling as a bad thing. It's made so much of what I do way more efficient.

A new approach called Bun has entered the scene and offers those same benefits and more. Most promising is the massive leap in build speed. I heard about this addition to Rails and have meant to dive in to try it out for some time. This blog post and video is an answer to that desire.

What is Bun?

Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called bun​. You can compare it to npm and yarn.

On the marketing site there's a section that reads:

Replace yarn with bun install to get 30x faster package installs.

So you can see why I might be intrigued. To be 100% fair, I don’t leverage a lot of JavaScript. Ruby on Rails is my go-to for more features. In the event I need more interactivity, I reach out to Stimulus.js. That being said, if I stand to benefit from quicker installs of packages and build times, I will gravitate toward that route.

Here are the design goals as quoted from Bun’s website

Bun is designed from the ground up with today's JavaScript ecosystem in mind.

  • Speed. Bun processes start 4x faster than Node.js currently (try it yourself!)
  • TypeScript & JSX support. You can directly execute .jsx, .ts, and .tsx files; Bun's transpiler converts these to vanilla JavaScript before execution.
  • ESM & CommonJS compatibility. The world is moving towards ES modules (ESM), but millions of packages on npm still require CommonJS. Bun recommends ES modules, but supports CommonJS.
  • Web-standard APIs. Bun implements standard Web APIs like fetch, WebSocket, and ReadableStream. Bun is powered by the JavaScriptCore engine, which is developed by Apple for Safari, so some APIs like Headers and URL directly use Safari's implementation.
  • Node.js compatibility. In addition to supporting Node-style module resolution, Bun aims for full compatibility with built-in Node.js globals (process, Buffer) and modules (path, fs, http, etc.) This is an ongoing effort that is not complete. Refer to the compatibility page for the current status.

Bun is more than a runtime. The long-term goal is to be a cohesive, infrastructural toolkit for building apps with JavaScript/TypeScript, including a package manager, transpiler, bundler, script runner, test runner, and more.

Bun with Rails

In a recent pull request to the jsbundling-rails gem, support for Bun was added to Rails as a JavaScript runtime.

You can install it like other options, including build, rollup, and webpack. I usually reach for esbuild but wanted to try bun

Create a demo Rails app using bun

Let’s create a simple app and give bun a shot

rails new bun_demo -c tailwind -j bun

This installs a vanilla Rails application with Tailwind CSS and Bun for the front end.

What about existing apps?

Do you have an existing app and want to use Bun? Assuming you’ve installed the jsbundling-rails gem, you can run the following task:

./bin/rails javascript:install:bun

Preview the bun.config.js file

When you run the new rails app command above in the latest version of Rails (7.1), a new bun.config.js file is created in the project's root. Inside is the following code.

import path from 'path';
import fs from 'fs';

const config = {
  sourcemap: "external",
  entrypoints: ["app/javascript/application.js"],
  outdir: path.join(process.cwd(), "app/assets/builds"),
};

const build = async (config) => {
  const result = await Bun.build(config);

  if (!result.success) {
    if (process.argv.includes('--watch')) {
      console.error("Build failed");
      for (const message of result.logs) {
        console.error(message);
      }
      return;
    } else {
      throw new AggregateError(result.logs, "Build failed");
    }
  }
};

(async () => {
  await build(config);

  if (process.argv.includes('--watch')) {
    fs.watch(path.join(process.cwd(), "app/javascript"), { recursive: true }, (eventType, filename) => {
      console.log(`File changed: ${filename}. Rebuilding...`);
      build(config);
    });
  } else {
    process.exit(0);
  }
})();

Here, we have a general configuration for bun, which declares the entry point of the app and the output points.

The configuration also watches for changes within the app/javascript directory, so you needn't refresh the page as you code. This was a massive milestone in front-end web development, and now we all seem to take it for granted :).

Notice Stimulus.js is installed by default and preconfigured to “Just Work". Remember the motto “Convention Over Configuration”? that’s at play here, and I love this stuff!

Much like esbuild, a file builds to the app/assets/builds directory in real-time. In this case, it would be called application.js.

This configuration file gets processed with a script inside the package.json file. There I have the following scripts:

// package.json

"scripts": {
    "build": "bun bun.config.js",
    "build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify"
  },

Because I installed Tailwind CSS and Bun, we see both scripts present.

Finally, in the current version of Rails (7.1) there’s a Procfile.dev file that gets sourced to a ruby gem called foreman.

That file bundles all the scripts we need to run into one set of processes giving us the ability to run one command to boot the rails app and build the assets.

bin/dev

Lovely, although a spiderweb of stuff, right? Luckily, it works out of the box, and you don’t have to pre-configure anything to install.

Not so fast!

If you’re following along and you ran bin/dev in the previous step, you might see an error like this in your logs:

unknown command: bun run build --watch

Bun still needs to be installed on your system if you see this. There are several ways to go about installing it. I will reach for homebrew, given I’m on a Mac. Read the docs for a different setup.

brew tap oven-sh/bun 
brew install bun

You can check if it’s installed by running

bun --version

Usage

Bun is super similar to yarn in syntax and key commands. You can use it like the following:

bun add -d tailwindcss
bun add  @hotwired/turbo-rails @hotwired/stimulus

To run bin/dev we need to install dependencies using bun.

bun install
# bun install v1.0.20 (09d51486)
# 12 packages installed [1.66s]

This happens quickly, but I noticed that while Rails created the Stimulus and Hotwire files inside app/javascript, the dependencies we need aren't installed.

bun add @hotwired/stimulus @hotwired/turbo-rails

Now running bin/dev, we should be set up for success!

What now?

Well, build cool stuff like normal! With bun under the hood for JavaScript, you get the added benefits of speed and compatibility without changing too much “muscle memory”.

Personally, I have used yarn and esbuild exclusively since jsbundiling-rails and cssbundling-rails entered the scene. I’m excited to give Bun a run for its money and will likely switch to it.

Link this article
Est. reading time: 7 minutes
Stats: 1,314 views

Categories

Collection

Part of the Ruby on Rails collection