Compiling Frontend Assets with RSpec and Rails 7

Rails 7 ships with a lot of really great improvements to the frontend the biggest of which is removing webpacker in favor of a more generic approach to allow developers to use whatever they'd like to use to compile their assets like esbuild or import maps or even webpacker if that's still your thing. This works really great during development because there are some commands that get run to recompile your assets on the fly. Let's take a look at what I'm talking about.

Rails 7 comes with a couple of gems that better handle asset compilation. They are cssbundling-rails and jsbundling-rails. When installed, these add a new executable called bin/dev which uses the foreman gem to run multiple processes with one command. Those processes can be found in the Procfile.dev and handle compilation of the CSS and JS assets as they change in real time while running in development.

RAILS_ENV=development

bin/dev

This script checks if you have foreman installed and installs it if you don't. Then it starts running all the processes found in your Procfile.dev.

#!/usr/bin/env bash

if ! foreman version &> /dev/null
then
  echo "Installing foreman..."
  gem install foreman
fi

foreman start -f Procfile.dev "$@"

Procfile.dev

Here's an example of the Procfile.dev with processes for web, js, and css. You'll notice the --watch argument passed to the build commands. This "watches" your files and as you save changes they will be recompiled automatically. This is an important thing to note because your files while in development mode will always be recompiled into the latest version but we don't run foreman or these commands in test by default.

If you're using a PaaS like Heroku you can also have a regular Procfile that is used in production environments to outline what processes should be run there.

web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch

The Gotcha

The asset "application.js" is not present in the asset pipeline.

Great, now our assets are compiling as fast as we can change them and we are writing code like crazy. With all that new code we definitely need some tests to make sure things are working as expected. System tests are a great way to do that because it runs a full web server and a headless browser to click around pages, fill in forms, etc. But when we run our system tests we get this error:

ActionView::Template::Error:
   The asset "application.js" is not present in the asset pipeline.

RSpec Support

Huh, well that's not good. This is because while our bin/dev runs the asset compilation commands for our development environemtn our test environment does not. To fix this I added a support file to my RSpec setup that looks like this:

# spec/support/generate_assets.rb
# frozen_string_literal: true

RSpec.configure do |config|
  config.before(:suite) do
    system("yarn build", out: File::NULL)
    system("yarn build:css", out: File::NULL)
  end
end

Now before our tests are run we compile the assets using the same commands that are used during development. I added the out: File::NULL to quiet the output of those asset compilations from the test logs. Now the application.js is present and correct before every test run.

CI Check

This works great when running locally but I found that our CI was doubling up efforts because the CI builds everything on its own so these commands were not relevant there. Here is the full version of the file I use with a check if we are in a CI environment or not.

# spec/support/generate_assets.rb
# frozen_string_literal: true

unless ENV.fetch("CI", false)
  RSpec.configure do |config|
    config.before(:suite) do
      system("yarn build", out: File::NULL)
      system("yarn build:css", out: File::NULL)
    end
  end
end

Conclusion

That's it! Now our development environment constantly recompiles assets while we edit them thanks to the --watch argument and our test assets are compiled before every run.

I don't really love this patch though. It seems to me that this should already be handled through the gems to better facilitate the Rails Magic™ and have things work out of the box. I understand that not everyone uses RSpec but I would think the gems could do a simple check like the bin/dev file does to see which testing framework is in use and trigger compilation accordingly.

I hope this was helpful and feel free to leave comments if you know of a better way to do things or if I've missed anything.