Migrate Your Asset Pipeline to Rails 7.x
After another confusing migration of a Rails app’s asset stack into I finally found a stack that I’m happy with for Rails 7.x.
Before…
I had some Rails 6 and Rails 5 apps. One of them was using webpacker
, sprockets 3
, bootstrap-sass 3
, react
, typescript
, jquery-ui
and jquery-ujs
. Two others were using sprockets 3
and bootstrap-sass 3
.
The Concept
- Use JS-based tools like
node-sass
andesbuild
to compile CSS and JS files and output the compiled files toapp/assets/builds
@import
any CSS files required by your JS libraries (like date pickers or charts) in your CSS files instead of JS files-
Make a list of those built files in
app/config/manifest.js
sosprockets-4
will let you reference them withjavascript_include_tag
etc… - Remove webpack
The Solution
1. cssbundling-rails
This gives you a nice bin/build-css
command. It’s a bash script that compiles all your CSS/SCSS using node-sass
, which you will install in your package.json
.
You can configure multiple CSS entrypoints in here if you need. Mine looks something like this:
./node_modules/sass/sass.js \
./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css \
./app/assets/stylesheets/admin/application.sass.scss:./app/assets/builds/admin/application.css --no-source-map \
--load-path=node_modules \
--load-path=vendor/assets/stylesheets \
$@
The outputs will all be sent to app/assets/builds
.
Some of the JS libraries we use come with CSS dependencies. Previously I was import
-ing them in JS files (which always felt weird in a Rails app). Now I can import them into my SCSS files like this:
@import "../../../../node_modules/react-date-range/dist/styles";
@import "../../../../node_modules/@melloware/coloris/dist/coloris";
@import "../../../../vendor/assets/stylesheets/medium-editor";
Note: be sure to remove the .css
extension when importing CSS from node_modules
. That’s the only way the will import properly.
2. jsbundling-rails
with esbuild
This will auto-create an esbuild.config.js
where you can set your JS entrypoints. Mine looks something like this:
require("esbuild")
.build({
entryPoints: [
"app/javascript/dashboard.js",
"app/javascript/frontend.js",
"app/javascript/onboarding.js",
],
bundle: true,
sourcemap: true,
watch: process.argv.includes("--watch"),
outdir: "app/assets/builds",
})
.catch(() => process.exit(1));
Like with CSS your outputs will all be sent to app/assets/builds
.
3. sprockets 4
I’d avoided sprocket upgrades for years. Anytime I tried it just got messy and their warnings in the upgrade guide definitely didn’t inspire confidence.
In the end sprockets-4 was simple to get running. I created the sprockets app/config/manifest.js
file like this:
//= link_tree ../builds
//= link_tree ../images
//= link_tree ../fonts
The first line references all those files we built in the previous steps. The next two let me use my image_url
and font_url
helpers as usual.
The sprockets manifest replaces any Rails.application.config.assets.precompile
you had before. Get rid of that entirely.
4. Global jQuery
Old Bootstrap, jQuery UI and some old plugins require a global jQuery object. At first I put this at the top of each of my entrypoints:
import jquery from "jquery";
window.$ = window.jQuery = jquery;
…but that was not enough. I needed to move that to a separate file and then import that file. So like this:
// in app/javascript/jquery.js
import jquery from "jquery";
window.$ = window.jQuery = jquery;
// in your other JS files
import "./jquery";
import {} from "jquery-ui";
import {} from "jquery-ujs";
import {} from "bootstrap-sass";
5. Bonus Tips
Ensure your app/assets/builds
exists by adding a .keep
file or similar. Sprockets won’t work in production without it.
esbuild
doesn’t minify by default. Here’s how you can conditionally minify your JS in production.
When importing your local JS files, make sure you import them without extensions so Sprockets can do its thing. So instead of
import "my/forms.js";
remove the extension…
import "my/forms";