At Kolide (btw we're hiring), we move swiftly to adopt to new versions of Ruby, Rails, and other major dependencies within a few months of them becoming available. We are at our happiest when we get to use the latest language and framework features. Additionally, forging ahead to uncharted waters allows us to contribute back bug reports, PRs, and guides for other rubyists also interested on being on the bleeding edge.
To that end, I wanted to share our recent experience with upgrading our production Rails 6.1 app from sprockets/sass-rails to the brand-new cssbundling-rails. If you are considering an eventual transition to Rails 7, this is a great first step in that direction.
Why cssbundling-rails
?
Or, more precisely, why move aways from sass-rails
?
When the webpacker gem was released as part of Rails 5.1 release in 2017, DHH made it clear that while webpacker could be used to bundle CSS, he highly recommended to keep it simple and continue to use the Rails asset pipeline.
Heeding this advice, many Rails projects (like ours) continue to dutifully serve their SCSS files via sprockets and the sass-rails
gem, the same way it's been doing circa Rails 3.1 in 2011.
This sprockets setup has always worked great, but lately some serious bit-rot has set in. Over the last few years, the Sass Team has deprecated both its original ruby-based version of sass
, and more recently, the libsass/sassc
library in favor of dart-sass. As of this writing, I could not find any sprockets compatible versions of dart-sass
. Further, as time marches on, the sassc gem is beginning to accumulate some pretty nasty bugs and inefficiencies. With no fixes on the horizon, it's time to move on.
This is where the newly minted cssbundling-rails
comes in. Inspired by the also new jsbundling-rails
library, it allows folks to leverage yarn/npm
to build a much simpler and more canonical CSS processing pipeline with a setup that will be familiar to JS developers.
rails / cssbundling-rails
Bundle and process CSS in Rails with Tailwind, PostCSS, and Sass via Node.js.
CSS Bundling for Rails
Use Tailwind CSS, Bootstrap, Bulma, PostCSS, or Dart Sass to bundle and process your CSS, then deliver it via the asset pipeline in Rails. This gem provides installers to get you going with the bundler of your choice in a new Rails application, and a convention to use app/assets/builds
to hold your bundled output as artifacts that are not checked into source control (the installer adds this directory to .gitignore
by default).
You develop using this approach by running the bundler in watch mode in a terminal with yarn build:css --watch
(and your Rails server in another, if you're not using something like puma-dev). You can also use ./bin/dev
, which will start both the Rails server and the CSS build watcher (along with a JS build watcher, if you're also using jsbundling-rails
).
Whenever the bundler detects changes to any…
With our mission set, let's roll up our sleeves and get started.
Upgrading a Rails 6.x App
According to its gemspec, cssbundling-rails
is not just for new Rails 7 apps, it's also compatible with 6.0.
Step 1 - Prepare Your Gemfile
Our goal is to not just transition to cssbundling-rails
, but to also remove sass-rails
gem. To get started, remove sass-rails
and any other potential references to sass like sass-ruby
and sassc
(if defined).
Next, add gem cssbundling-rails, '>= 0.2.4'
(the version at the time of this writing) and run bundle install
.
Step 2 - Prepare Your SCSS Files
First, let's take some inventory. Open up config/initializers/assets.rb
and at the bottom of that file you will see something like the following:
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
Rails.application.config.assets.precompile += %w( sessions.css staff.css marketing.css )
If the Rails.application.config.assets.precompile
line is uncommented, take note of the .css
files referenced in the array. In addition to application.css
these are all separate top-level CSS files that we will need to convert to the new format.
For each file, (or just application.[s]css
) you should do the following:
If you haven't already, convert the files to use SCSS's
@import
syntax instead of the sprockets magic comments like*= require_self
or*= require_tree
.Rename each file to match this format
<name>.sass.scss
(soapplication.scss
would becomeapplication.sass.scss
).
Step 3 - Run the Installation (And Then Fix What It Broke)
Run ./bin/rails css:install:sass
.
If you receive an overwrite warning for app/assets/stylesheets/application.sass.scss
, you can respond with N
when prompted.
After the installation completes, we need to clean up a few things.
First, in our case, the installation inserted an extra stylesheet_link_tag
at the bottom of app/views/layout/application.html.erb
. You should delete this extra line.
Second, while the installation command updates the app/assets/config/manifest.js
file with a few new lines, it often doesn't remove explicit references to any sass files. After the upgrade, ours looks like this:
//= link_tree ../images
//= link_tree ../fonts
//= link_tree ../builds
Note: We added the line for fonts
as we use the font-url
helper in our SCSS files. These fonts didn't need to be explicitly included in the manifest before because sprockets would include them dynamically as they were referenced in the source CSS file. After this upgrade sprockets isn't processing the file so it's important that we ensure it's in the manifest.
Finally, the build:css
script the installation creates in package.json
is only sufficient if you only have one main application.scss
, if you have other files you need to output, you are going to need to modify the script's contents. If this is the case, my suggestion is to create a new file called bin/build-css
and do something like the following:
#!/usr/bin/env bash
./node_modules/sass/sass.js \
./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css \
./app/assets/stylesheets/sessions.sass.scss:./app/assets/builds/sessions.css \
./app/assets/stylesheets/staff.sass.scss:./app/assets/builds/staff.css \
./app/assets/stylesheets/marketing.sass.scss:./app/assets/builds/marketing.css \
--no-source-map \
--load-path=node_modules \
$@
The $@
at the bottom ensures we pass along any additional arguments like --watch
when this is invoked via bin/dev
(more on that later).
Now in the package.json
file do the following:
"scripts": {
"build:css": "./bin/build-css"
}
Don't forget to also run chmod 755 ./bin/build-css
in your terminal before moving on to the next step.
Step 5 - Handle asset-url
And Friends
Often in Rails, you need to reference files from the app/assets/images
or app/asset/fonts
folders directly in CSS. Since sprockets computes hashes for each asset, you can't just hard-code the name of the asset in there. To work around this, sprockets introduced helper functions like asset-url
, font-url
, and image-url
that resolve the relative path to the asset correctly.
dart-sass
has no knowledge of sprockets, so before we finalize the build we need sprockets to run through each file quickly and add these assets paths in. While official support for this is pending, we arrived at a workaround that seems to do the trick:
# config/initializers/asset_url_processor.rb
class AssetUrlProcessor
def self.call(input)
context = input[:environment].context_class.new(input)
data = input[:data].gsub(/(\w*)-url\(\s*["']?(?!(?:\#|data|http))([^"'\s)]+)\s*["']?\)/) do |_match|
"url(#{context.asset_path($2, type: $1)})"
end
{data: data}
end
end
Sprockets.register_postprocessor "text/css", AssetUrlProcessor
This regex will match these url functions and convert their contents to the appropriate location of the asset on disk in both development and production.
Step 6 - Test It Out With bin/dev
Before trying this out, you'll likely want to clear out any sprockets cache in tmp/
. To do that, you can simply run bin/rake tmp:clear
.
As part of the earlier ./bin/rails css:install:sass
command, a new file called bin/dev
was created. Additionally, a new gem dependency called foreman
and its associated config file Procfile.dev
was installed.
By running bin/dev
you are now invoking foreman
which will read the Profile
and simultaneously run the rails server
and the yarn build:css --watch
commands. This should give you a very similar development experience to the original setup where you can make changes to CSS files and, after a refresh, those changes will be immediately reflected in the browser.
If all went well, bin/dev
should start right up and a visit to your app locally should "just work."
Credits & Closing Thoughts
A big thank you to Alex Jarvis for leading the charge on this upgrade at Kolide and collaborating with me.
I hope you found this guide useful. If you found any errors in this guide or suggestions to improve it, please reach out in the comments or hit me up on twitter @jmeller.
Top comments (4)
For whatever reason I didn't have
in my Gemfile and was running into issues when I removed sass-rails. Adding it explicitly solved my issue
If I comment gem 'sassc-rails', app stops working with:
Started GET "/javascripts/application.js" for ::1 at 2022-08-03 19:26:31 -0400
Processing by ApplicationController#routing_error as JS
Parameters: {"path"=>"javascripts/application"}
Security warning: an embedded tag on another site requested protected JavaScript. If you know what you're doing, go ahead and disable forgery protection on this action to permit cross-origin JavaScript embedding.<br> Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms | Allocations: 668)</p> <p>ActionController::InvalidCrossOriginRequest (Security warning: an embedded <script> tag on another site requested protected JavaScript. If you know what you're doing, go ahead and disable forgery protection on this action to permit cross-origin JavaScript embedding.):</p>
Amazing post! thanks for sharing 🤘 I have a question, with this new cssbundling gem is there a way to combine Tailwindcss and SASS? for example to create some nested classes using apply, use mixins, etc.
thanks for sharing! how would one go about using both dart-sass and tailwind? I have some sass in my app and I would like my yielded application.css to have both my tailwind styles and preprocessed sass.