How to bundle assets in a Rails engine
All you need to know about Rails Engines
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 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:
Set up build commands
Provide a way for the parent app to reach those assets
Run all the compilation commands
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 and 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:
"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.
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.
$ 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.
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.