Not long ago, I was tripped over a bug: I updated a dependency package by modifying some css rules. However, the new rule was missing even when it was there when I tested it locally. After a close investigation, I found that I wasn’t using css modules as it should be! I also needed to configure some post- processors which is quite an interesting exploration. So here we go — onwards to Webpack build with CSS modules and post-processor!
But before anything else, let’s get the concept right: What is CSS modules, and what is a css post-processor?
CSS modules
“A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.”
So you can refer that a CSS module is generated during a build step (with WebPack or any other build tool) that makes each rule locally scoped. This solved the pain-point of globally scoped CSS.
How is this working?
We need to import the stylesheet to the JS file, and write our classname like styles[“classname”]
:
//JS
import styles from "./styles.css";<div className={styles.classnameRandom}></div>//Styles.css
.classnameRandom {
...
}
When our files go through the build step, the builder tool will check that styles.css
file imported, and then check the corresponding JavaScript, and make the .classnameRandom
class accessible via styles.classnameRandom
.
A new classname is then generated with a new string of characters replacing both the HTML class and the CSS selector class. Our generated HTML might look like this:
<div className="_53NJ"></div>
As you can see it is very unique and will be 100% locally scoped as a result. There are some good resources online that explains why CSS module is so good, with its compose
syntax and more. This will not be the focus of this blog, but I highly recommend you to have a check, especially if you like using SASS like me.
So how we make the magic happen in Webpack?
First I’d recommend to install the following loaders/plugins :
sass-loader
is a loader for Webpack for compiling SCSS/Sass files.style-loader
injects our styles into our DOM. This will put styles into the HTML file, so no separate stylesheet.css-loader
interprets@import
and@url()
and resolves them. For example, if you reference some images like .jpg in css, you need this to resolve it.mini-css-extract-plugin
extracts our CSS from themain.js
JavaScript bundle into a separate file, essential for production builds.
So here is the webpack.config.js
, be noted of the bold text part.
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = env => {
const buildRoot = path.resolve(__dirname, "dist");
const srcRoot = path.resolve(__dirname, "src"); const isDev = env === "dev";
const sourceMap = isDev;
const minimize = !isDev; //we only minimise in production env
return {
mode: isDev ? "development" : "production",
entry: "./src/index.ts",
output: {
filename: "index.js",
libraryTarget: "commonjs2", // module.exports = [library]
path: buildRoot,
publicPath: publicAssetRoot
},
devtool: sourceMap ? "source-map" : false,
// Don't bundle any node_modules dependencies.
// This leaves the require("foo") statement in,
// so your application will bundle dependencies instead.
externals: [/^[^.]/],
module: {
rules: [
{
test: /\.(ts|tsx)$/,
include: [srcRoot],
loader: "ts-loader", // for typescript!
options: {
transpileOnly: true
}
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
],
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader, //1
{
loader: "css-loader", //2
options: {
sourceMap,
import: false,
modules: true,
camelCase: true,
localIdentName: isDev ? '[local]' : '[sha1:hash:hex:4]'
}
},
"sass-loader" //3
]
},
{
test: /\.(jpe?g|png|gif|svg)$/,
include: [srcRoot],
use: [
{
loader: "file-loader",
options: {
name: "[name].[hash:base64:8].[ext]"
}
},
{
loader: "img-loader",
options: {
enabled: minimize
}
}
]
}
]
},
resolve: {
extensions: [".ts", ".tsx"]
},
plugins: [
new MiniCssExtractPlugin({ filename: "style.css" })
]
};
};
We can see that first we use MiniCssExtractPlugin.loader to externalise the CSS files, and then we use css-loader to resolve @import()
or url()
rule. Here we are resolving the css modules by defining:
options: {
sourceMap,
import: false,
modules: true,
camelCase: true,
localIdentName: isDev ? '[local]' : '[sha1:hash:hex:4]'
}
By setting modules: true
we tells css-loader
to enable CSS modules. And
import: false
we disable the css modules on @imported()
resources. In some place you can see it’s written as importLoaders: 2
instead (1 is true).
And the localIdentName: isDev ? ‘[local]’ : ‘[sha1:hash:hex:4]’
defines how the classname will be resolved. In the dev env, it is how it is, but in prod, it will be encoded per defined, like _26de a1cd
. This is for efficient cache busting.
And finally we apply sass-loader to convert scss into css.
So now there’s a final question: what if you want to use global css and css module at the same time?
It’s easy — with the help of some regex and the include & exclude
! Let’s say all your css modules are put in files ending with .module.css
and global css just in .css
.
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
import: false,
modules: true
}
}
],
include: /\.module\.css$/
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
],
exclude: /\.module\.css$/
}
]
}
So that’s pretty much it! Happy Reading!