This is an experiment in achieving 'modular CSS' by use of postCSS as a build-step, invoked
by appropriately constructed npm scripts. We start out with an example project (see
./index.html
/ ./src
) which uses a BEM-ish approach to enforce modular styles. We then
gradually tweak it until we get to the point where we can get rid of the unique prefixes and
the relevant maintenance hassle.
There are 5 stages to this, 0 to 4, which are laid out below. For each stage you can
git checkout
the relevant tag (0basics
to 3usefulScripts
) to force the example project's
source to the state which is relevant to the stage.
npm run serve
: Run a local dev server on port 8888. (Depends on local-web-server, a simple web-server for productive front-end development.)js:bundle
: Build a JS bundle. (Depends on requirejs, the node adapter for RequireJS, for loading AMD modules. Includes the RequireJS optimizer.)css:bundle
: Build a CSS bundle. (Depends on node-sass, the node bindings for the Sass stylesheet preprocessor.) This will transpile all the*.scss
files in our project and output abundle.css
at the root of our app - which is the (one-and-only) CSS file referenced inindex.html
. Note thatcss:bundle
relies onstyle/style.scss
to 'know' which.scss
files to bundle. We need to maintainstyle/style.scss
as we add/remove/relocate.scss
files.css:bundle-when-styles-change
: Watch for style changes and rebuild CSS bundle in response. (Depends on node-sass and onchange. The latter module facilitates the use of glob patterns to watch file sets and run commands when anything is added, changed or deleted.)
Install postcss. Also install postcss-cli, the command line for postcss.
Let's create an experimental css:prefix
script based on
autoprefixer (yes, install that too). Take a
look at browserslist for a way to configure autoprefixer
(go ahead and add a browserslist file). Our script should look like:
{
"css:prefix": "postcss -u autoprefixer"
}
try npm run css:prefix -- ./src/views/ui/users/user-plain/user-plain.scss
.
(The --
token allows passing custom arguments to invoked scripts, courtecy of
npm's run-script
.)
Okay, we're now ready to add a css:create-module
npm script. Let's install
postcss-modules and create a new script based on
the previous css:prefix
:
{
"css:create-module": "postcss -u autoprefixer -u postcss-modules"
}
Using autoprefixer is not really relevant at this point but it's a nice-to-have so let's leave it
in there. Try out our new script with
npm run css:create-module -- ./src/views/ui/users/user-plain/user-plain.scss
. Notice the
generated .scss.json
. Revel in its glory.
We now have a method of uniquefying the class names per module without having to manually prefix
them. Let's pick a module and remove all prefixes, applying our create-module
script and making use
of the generated JSON files to get the unique class names into our templates.
We'll have to run css:create-module
script with the -o
switch to generate a scss.module
file:
npm run css:create-module -- -o ./src/views/.../foo.scss.module ./src/views/.../foo.scss
Having generated the .scss.module
file we'll have to @import
it in styles/modules.scss
in
place of the original .scss
file: It is the .scss.module
file that contains the uniquefied
class names that we want in our stylesheets. On the JS side, we'll have to swap direct references to
class names for references to attributes in the generated .scss.json
file. Something along the
lines of:
var classNames = JSON.parse(require('./foo.scss.json'));
// .. and then, further down
$(classNames.title).text('War and Peace'); // Instead of $('.title').text('War and Peace')
We can go over each and every one of our defined modules applying step 2. We will have to, given that we need to change all our scripts to make use of generated JSON files when referencing class names. However, we naturally want to automate the creation of CSS-modules. So here's the scripts that we'll be using:
We already have that. We can run it as described in step 2 above to generate the scss.module
and
scss.json
files per module. (Remember to run css:bundle
afterwards, in order to get the updated
styles into bundle.css
.)
Notice the s
at the end. This is a script to go over all defined modules in our code and run
css:create-module
on them. This is a necessity as we don't want to be manually iterating our
codebase looking for modules and re-running css:create-module
all the time. A user checking out
our repo will start out without any scss.module
or scss.json
files (these are generated and
as such not versioned). To generate all of them it should be enough to run this script. The
implementation will be based on bash find:
{
"css:create-modules":
"find ./src -iname '*.scss' -exec npm run css:create-module -- -o '{}.module' '{}' \\;"
}
(The {}
token is the path of the found file, available courtecy of find
.)
Watch .scss
files and whenever they change, re-create scss.module
/ scss.json
. So that we
don't have to do it manually. We can use onchange for the
watching and delegate back to css:create-module
:
"css:create-module-when-styles-change":
"onchange 'src/**/*.scss' -v -- npm run css:create-module -- -o '{{changed}}.module' '{{changed}}'",
(The {{changed}}
token is the path of the changed file, available courtecy of onchange
.)
We've had this script all along, even before we introduced any of the modular CSS stuff. It'll always
have to be run after a css:create-module
to get the new styles into bundle.css
. As was previously
mentioned, the bundle script relies on the presence of a styles/modules.scss
file which enumerates
(@import
s) each and every scss.module
file in our project. This is obviously a maintenance
hassle - idealy, we'd like to automate this as well. This brings us to
Collect all scss.module
files into the styles/modules.scss
'import-list'. This is yet another
application of find
:
"css:collect-modules":
"find ./src -iname '*.scss.module' -exec echo '@import \"{}\";' > ./style/modules.scss \\;"
Collect all modules and then (re)create the CSS bundle. This is just a sequential combination of
other scripts we've already put together. It's quite typical however so it's worth a dedicated
script of its own. We'd also like to be able to run this automatically, watching the project's files
and responding to any CSS module changing (or being added). This, again, can be done with onchange
:
"css:collect-modules-and-bundle": "npm run css:collect-modules && npm run css:bundle",
"css:collect-modules-and-bundle-when-module-changes":
"onchange 'src/**/*.scss.module' -v -- npm run css:collect-modules-and-bundle"
This is our
finishing move. Watch
the project's files and a) (re)create CSS modules when any style changes, b) (re)create entire CSS
bundle when any module changes (or is added). We already have these scripts - we just combine them
into the handy css:watch
that we can run and forget:
"css:watch":
"npm run css:create-module-when-styles-change & npm run css:collect-modules-and-build-when-module-changes &"