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 @import
s 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:
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 …