DEV Community

Jason Meller for Kolide

Posted on

How to Migrate a Rails 6 App From sass-rails to cssbundling-rails

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.

GitHub logo 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 )
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.

  2. Rename each file to match this format <name>.sass.scss (so application.scss would become application.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
Enter fullscreen mode Exit fullscreen mode

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 \
  $@
Enter fullscreen mode Exit fullscreen mode

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"
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Oh, and we're hiring!

Discussion (0)