Sprokets remains an important part of the Rails ecosystem, despite the move to webpacker for Javascript assets. In this blog post I look at a particular case where the interplay of Sprockets require-directives and CSS @import rules can cause confusion.

Introduction

The sprockets gem has been a cornerstone of the Rails asset pipeline for over a decade. Recent Rails versions have made a shift to webpacker, but the default tool for bundling CSS and images has remained sprockets. Furthermore, Rails 7 has taken the noteworthy step of removing webpacker from the default project (see here). So, whilst it may have been around a while, it seems like sprockets is going to be a significant part of the Rails ecosystem for some time to come.

I recently encountered a problem with CSS changes not being correctly propagated to the production environment. The problem turned out to be related to differences in how sprockets require directives and CSS @import rules were handled during the compilation of our CSS assets. Of course, if I had read the docs this effect would not have taken me by surprise. Nonetheless, I found it instructive to walk through the behaviour in baby steps, so I decided to share it in this post.

Setup

We can start with a stripped back setup to investigate the behaviour.

We are interested in the result of running asset precompilation on our CSS files. To do this in a non-intrusive way on our development enviroment, let's start by updating the environment config at config/environments/development.rb, like so:


    Rails.application.configure do
      …
      # Write dev precompiled assets to different location
      config.assets.prefix = "/dev-assets"
      …
    end

    
If we don't write these precompiled files to a separate location we run the risk that we forget about them, and the precompiled files get served in development even as you change the underlying assests - very confusing!

In the same vein, we can set up a separate manifest file for this experiment. The file will reside at app/assets/stylesheets/dummy-manifest.css, so we will need to add this path to our list of assets for precompilation at config/initializers/assets.rb:


  Rails.application.config.assets.precompile += %w(
    …
    dummy-manifest.css
    …
  )

    
This lets sprockets know about any non-default entry points that must be bundled when we run the precompilation step, i.e. bundle exec rake assets:precompile.

The Styles

The contents of our CSS entry-point file, app/assets/stylesheets/dummy-manifest.css, will be as follows:


  //= require shared/dummy

    
So the file has a single sprockets require-directive, which is telling the sprockets pre-processor to include the styles stored in file app/assets/stylesheets/shared/dummy.scss:

  @import './dummy-import';

  body {
    color: red;
  }

    

And, in turn, this file @imports the SCSS file at app/assets/stylesheets/shared/dummy.scss:


  $color: #00FF00;
  p {
    color: $color;
  }

    
This file just sets up a variable, $color, using SCSS syntax and uses this colour in the subsequent style. OK so there are a few files to keep track of here, but the figure below (Fig. 1) might help to visualise the simple linear dependency between these files.

Asset compilation

When we run sprockets asset compilation we expect these files to be bundled together into a single package, so let's try it. We run asset precompilation as follows:


  RAILS_ENV=development bundle exec rake assets:precompile 

    
This will lead to the following concatenated output at /public/dev-assets/dummy-manifest-[some-hash].css:

  /* line 3, app/assets/stylesheets/shared/dummy-import.scss */
  p {
    color: #00FF00;
  }

  /* line 3, app/assets/stylesheets/shared/dummy.scss */
  body {
    color: red;
  }

    
So the output includes the styles from dummy-import.scss and dummy.scss, and everything looks as expected. The whole processed is summarised as follows:
Fig. 1: CSS compilation with require and @import statements

Let's replace the @import in dummy.scss with a Sprockets require. In this way the file at shared/dummy.scss becomes:


  //require './dummy-import'

  body {
    color: red;
  }

    
After precompilation the resulting file is effectively identical to before:

  /* line 3, app/assets/stylesheets/shared/dummy-import.scss */
  p {
    color: #00FF00;
  }
  /* line 2, app/assets/stylesheets/shared/dummy.scss */
  body {
    color: red;
  }

    
So on the surface, replacing the @import with a require preprocessing directive has no effect.

We will update the file shared/dummy.scss to reference the $color variable from the shared/dummy-import.scss file:


  @import './dummy-import';

  body {
    color: $color;
  }

    
Running asset compilation results in the following output

/* line 3, app/assets/stylesheets/shared/dummy-import.scss */
p {
  color: #00FF00;
}

/* line 3, app/assets/stylesheets/shared/dummy.scss */
body {
  color: #00FF00;
}

    
We can see that the $color variable is used to define both the body and p styles, i.e. it is available across both shared/dummy-import.scss and the importing file, shared/dummy.scss.

Let's try the same thing using a require directive in place of the @import statement. The shared/dummy.scss, file becomes:


  //= require './dummy-import'

  body {
    color: $color;
  }

    
If we now attempt to run the asset precompilation step the following happens:

  SassC::SyntaxError: Error: Undefined variable: "$color".
        on line 3:10 of app/assets/stylesheets/shared/dummy.scss

    
Boom. Computer says no. The $color SCSS variable is not available in the scope of the shared/dummy.scss file, despite the fact that the file in which the variable is declared has been require-d.

Of course, the astute developer who takes the time to read the Rails documentation will have seen this effect highlighted therein:

If you want to use multiple Sass files, you should generally use the Sass @import rule instead of these Sprockets directives. When using Sprockets directives, Sass files exist within their own scope, making variables or mixins only available within the document they were defined in.
For me, I learned this one the hard way.

Summary

Sprockets remains an important part of the Rails ecosystem for the preprocessing and bundling of CSS files. We ran the precompilation step locally to investigate the effect of switching Sprockets require preprocessor statements and CSS @import rules. For a graph of simple CSS files the two approaches were effectively equivalent. However, problems were encountered when we wanted to share SCSS variables. If the file defining the variable is require-d into another file the SCSS variable will not be available in the requiring file. For this reason alone it is probably safter to using @import to include styles from other SCSS files.

Comments

There are no existing comments

Got your own view or feedback? Share it with us below …

×

Subscribe

Join our mailing list to hear when new content is published to the VectorLogic blog.
We promise not to spam you, and you can unsubscribe at any time.