How to bundle assets in a Rails engine
Tags: rails, ruby, assets, management
Published: 2023-01-10 18:29:35

During Rails' lifetime, we had a lot of ways to load, parse, and process assets. The "recommended" way is to hook into the asset pipeline (whichever that is) and, on deployment, let the `assets:precompile` task to take over and... compile them so they can be used in your app.

But, if you built an engine that you want to distribute to others, there might be a few things that might get in the way.

### Asset pipeline cons

Let's say you're using `jsbundling-rails` and/or `tailwindcss-rails` with your distributable engine. To you, it's common to have all you need to compile those assets.
You'll have Node.js and all other external dependencies installed and ready to compile everything together, but the parent app that's using your gem might not. They might use `importmaps,` and they might not have all that Node.js infrastructure.

When the user pushes the Rails app with your gem installed, they might get the gem without any assets (because they haven't been built yet), or they might even get a crash that sprockets (or propshaft) can't find those assets.

But there's a better way!

## Compile assets on publish-time

One approach we had with [avo-hq/avo](https://github.com/avo-hq/avo) was to precompile the assets before we publish a new version and serve them as static assets to the parent app.

### How do we do that?

The overview is this:

1. Set up build commands
1. Provide a way for the parent app to reach those assets
1. Run all the compilation commands
1. Package the gem up with those static assets


## 1. Set up build commands

You first install your asset handlers as you need them for your project. They can be anything from [rails/jsbundling-rails](https://github.com/rails/jsbundling-rails) and [rails/tailwindcss-rails](https://github.com/rails/tailwindcss-rails) to webpacker or something custom.

Add some commands to build up those assets. 
We have the following scripts inside the `package.json` file:

```json
  "scripts": {
    "prod:build:js": "esbuild app/javascript/*.js --bundle --sourcemap --minify --outdir=public/avo-assets",
    "prod:build:css": "tailwindcss -i ./app/assets/stylesheets/avo.base.css -o ./public/avo-assets/avo.base.css --postcss",
  }
```

They take the source JS and CSS files, compile them, and move them to a `public/assets` directory.

## 2. Provide a way for the parent app to reach those assets

When you ship your gem, that `public` directory will not be public at all. It will be hosted somewhere hidden on that machine, so the browser will not have a way to reach them.

Let's use a `Rack::Static` middleware to the `engine.rb` file.

```ruby
module Avo
  class Engine < ::Rails::Engine
    config.app_middleware.use(
      Rack::Static,
      urls: ["/avo-assets"],
      root: Avo::Engine.root.join("public")
    )
  end
end
```

Now, the parent app will re-route all the traffic from `/avo-assets` to that "hidden" directory where our compiled assets are.

## 3. Run all the compilation commands

When we're ready to ship our gem to RubyGems (or any other gems server), we should run the compilation commands to ensure all the JS and CSS files are compiled, minified, and packaged up how we need them to be in production.

```bash
$ yarn prod:build:js
$ yarn prod:build:css
```

## 4. Package the gem up with those static assets

We need to instruct the `gem` utility to add the compiled files to the packaged gem.

```ruby
Gem::Specification.new do |spec|
  spec.name = "avo"
  spec.version = Avo::VERSION
  # more spec properties

  spec.files = Dir["{bin,app,config,db,lib,public}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "avo.gemspec", "Gemfile", "Gemfile.lock"]

  spec.add_dependency "activerecord", ">= 6.0"
  # other dependencies here
end
```

The `spec.files` property knows which files should be bundled up. Finally, you'll see the `Dir["{public}/**/*"]` will add the whole `public` directory to the gem, including the `avo-assets` one.

Run `bundle exec rails build` to package everything up and profit 🙌


### All you need to know about Rails engines

This post is part of a series I'm writing to share some of my learnings while building my own distributed engine, Avo.

[All you need to know about Rails Engines](https://avohq.io/rails-engines)