How to Migrate a Rails 6 App From sass-rails to cssbundling-rails
Tags: rails, sass, ruby, tutorial
Published: 2021-10-21 02:59:07

At Kolide ([btw we're hiring](https://angel.co/company/kolideco/jobs/1638302-rails-engineer)), 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](https://weblog.rubyonrails.org/2017/2/23/Rails-5-1-beta1/), 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. 

{% twitter 808349072734027776 %}

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](https://sass-lang.com) has deprecated both its original ruby-based version of `sass`, and more recently, the `libsass/sassc` library in favor of [dart-sass](https://sass-lang.com/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](https://github.com/sass/sassc-ruby/issues/197) and [inefficiencies](https://github.com/sass/sassc-ruby/issues/200). With no fixes on the horizon, it's time to move on.

This is where the newly minted [`cssbundling-rails`](https://github.com/rails/cssbundling-rails) comes in. Inspired by the also new [`jsbundling-rails`](https://github.com/rails/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 rails/cssbundling-rails %}

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:

```ruby
# 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:

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:

```js
//= 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:

```bash
#!/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:

```json
  "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](https://github.com/rails/sprockets-rails/pull/476), we arrived at a workaround that seems to do the trick:

```ruby
# 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](https://github.com/alxjrvs) 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](https://twitter.com/jmeller). 

[Oh, and we're hiring!](https://angel.co/company/kolideco/jobs/1638302-rails-engineer)