🛠 Customize CSS Loader options in Next.js
- Published on
Recently when working on a project migrating the code from Gatsby to Next.js, I faced a very interesting problem.
Next.js supports CSS Modules using the [name].module.css
file naming convention. Really cool stuff. I can now import class names using JS syntax like import styles from './styles'
and use object notations to reference those styles.
The problem however, Gatsby code had styles referencing HTML tags like a
and section
.
section {
display: flex;
flex-direction: column;
align-items: center;
width: 480px;
margin: 20px 0;
scroll-margin-top: 74px;
&.full-width {
width: 100%;
margin: 20px auto;
}
}
This unfortunately doesn't work with the opinions of the creators of Next.js. And there isn't an easy way to override the config of css-loader
plugin using next.config.js
12. I was not in the mood to update the CSS files, (honestly, there were a lot of them).
So after browsing the internet for a couple of hours, I found a hack3, which works well for my particular use case.
/**
* Stolen from https://stackoverflow.com/questions/10776600/testing-for-equality-of-regular-expressions
*/
const regexEqual = (x, y) => {
return (
x instanceof RegExp &&
y instanceof RegExp &&
x.source === y.source &&
x.global === y.global &&
x.ignoreCase === y.ignoreCase &&
x.multiline === y.multiline
)
}
// Overrides for css-loader plugin
function cssLoaderOptions(modules) {
const { getLocalIdent, ...others } = modules // Need to delete getLocalIdent else localIdentName doesn't work
return {
...others,
localIdentName: '[hash:base64:6]',
exportLocalsConvention: 'camelCaseOnly',
mode: 'local',
}
}
module.exports = {
webpack: (config) => {
const oneOf = config.module.rules.find((rule) => typeof rule.oneOf === 'object')
if (oneOf) {
// Find the module which targets *.scss|*.sass files
const moduleSassRule = oneOf.oneOf.find((rule) =>
regexEqual(rule.test, /\.module\.(scss|sass)$/)
)
if (moduleSassRule) {
// Get the config object for css-loader plugin
const cssLoader = moduleSassRule.use.find(({ loader }) => loader.includes('css-loader'))
if (cssLoader) {
cssLoader.options = {
...cssLoader.options,
modules: cssLoaderOptions(cssLoader.options.modules),
}
}
}
}
return config
},
}
cssLoaderOptions
method returns the css-loader
options which I want to override. L#36, L#42 and L#47 is where the magic happens. Looping through all the modules Next.js uses, we find using a regex module which targets *.module.scss|*.module.sass
files, and get the object for css-loader
plugin in L#41-42.
If you want to target *.css
files instead, change the regex on L#36.
And et voilà, problem solved! I do hope the RFC2 moves forward, and we get a built-in option from Next.js to override the config, but until then, this hack remains.
Update on Next.js 12 and above.
The above implementation breaks, because in this PR, the Next.js team has switched to a custom fixed version of css-loader
that has no defaultGetLocalIdent
, nor does it check if getLocalIndent
is null or undefined.
Therefore, removing getLocalIdent
from the cssLoaderOptions
configuration will lead to TypeError: getLocalIdent is not a function
thrown with the above implementation. It has now become necessary to provide a function to getLocalIdent
.
Below is the updated implementation for cssLoaderOptions
from the above config:
const loaderUtils = require('loader-utils')
/**
* Generate context-aware class names when developing locally
*/
const localIdent = (loaderContext, localIdentName, localName, options) => {
return (
loaderUtils
.interpolateName(loaderContext, `[folder]_[name]__${localName}`, options)
// Webpack name interpolation returns `about_about.module__root` for
// `.root {}` inside a file named `about/about.module.css`. Let's simplify
// this.
.replace(/\.module_/, '_')
// Replace invalid symbols with underscores instead of escaping
// https://mathiasbynens.be/notes/css-escapes#identifiers-strings
.replace(/[^a-zA-Z0-9-_]/g, '_')
// "they cannot start with a digit, two hyphens, or a hyphen followed by a digit [sic]"
// https://www.w3.org/TR/CSS21/syndata.html#characters
.replace(/^(\d|--|-\d)/, '__$1')
)
}
// Overrides for css-loader plugin
function cssLoaderOptions(modules) {
const { getLocalIdent, ...others } = modules
return {
...others,
getLocalIdent: isProd ? getLocalIdent : localIdent,
exportLocalsConvention: 'camelCaseOnly',
mode: 'local',
}
}
I use the localIdent
to keep the classNames readable when developing locally. When building a production version, I defer to the default implementation of generating the classNames. The rest of the config remains the same as before.
The newer implementation is again a hack, and it works for my use-case. YMMV. Until Next.js provides an official way to override the config, I will continue to use this hack.