What a mouthful, right? “Supporting SASS in your TS React project using TSC and esbuild”... and this is not even the full title which tells what will soon happen here. It’s because it’s a little bit complicated, though the implementation is rather simple. Let me explain…
We all know SASS and its benefits, and until “native” CSS will support nesting, to say the least, I don’t see it going anywhere (and SASS has a lot more to offer).
My Components package currently supports regular plain-old CSS (not that there’s anything wrong with it), and I thought it was a good time to introduce SASS to it, but the package is not a ordinary “Webpack-build-that-s#!t-for-me”. I’m using TSC (TypeScript Compiler) to generate the artifacts -
What it means is that TSC is compiling 2 versions of the component, ESM and CJS. Once we have these, we’re taking the ESM outcome and bundling it using esbuild. You can read more about it here, but if to put it visually:
You can argue that this does not make much sense for a components package and you'll be right, but we can take this package as an example for a React project written in TS with a build process which results in a JS bundle and a CSS file.
The build script looks like this:
"build": "tsc --project tsconfig.esm.json & tsc --project tsconfig.cjs.json && yarn bundle",
"bundle": "node ../../esbuild.config.js",
And the esbuild.config.js
looks like this:
const cssPlugin = require('./esbuild.css.plugin');
require('esbuild')
.build({
entryPoints: ['dist/esm/index.js'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/main/index.js',
plugins: [cssPlugin],
})
.catch(() => process.exit(1));
What is this “cssPlugin”?
The CSS plugin here helps us “convert” the CSS files paths, since TSC does not deal with them and therefore they are not to be found under the dist/esm
directory, so what this custom plugin does is identifying the import and redirect it to where the file is at. This saves us the "copy" of .css
files to the dist/esm
directory, but come to think of it, I realize I need to copy these anyhow. not relevant for now ;)
It’s always good to define our goals before jumping into coding, and in this case:
- Have our component’s style as a SASS file
- Make sure that Storybook (yes, Storybook) is still working as it used to
- Have the esbuild bundling process create the final CSS file from the SASS file, and place it with the JS bundle under the
dist/main
directory
Before we start, know that you can find all the code under this GitHub repo.
Here we go :)
First we rename the index.css
file into a index.scss
one. We also change the import in the component accordingly:
. . .
import './index.scss';
Storybook
Because I like tormenting myself, I run Storybook to make sure that we’re still getting our CSS styles and sure enough we’re getting an error:
ModuleParseError: Module parse failed: Unexpected token (8:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| */
|
> .selected {
Why? Because Storybook does not “know” how to handle this SASS file. In order for it to handle SASS we need to use a plugin called “storybook-addon-sass-postcss”. You can read more details on how to install and use it here. Once installed and set in the Storybook addons, it appears that Storybook is working as expected.
Now what about the bundling?
esbuild bundling
For bundling we need to have esbuild take care of our SASS file. If I run the build now, this is the result I’m getting:
✘ [ERROR] Could not resolve "./index.scss"
dist/esm/src/Pagination/index.js:4:7:
4 │ import './index.scss';
╵ ~~~~~~~~~~~~~~
The plugin I’ve mentioned earlier, which takes care of redirecting the paths for imported CSS, is only looking for .css
, but we would like it to look for .scss
as well. I will change the regex for that. Here is the result plugin:
const path = require('path');
module.exports = {
name: 'css',
setup(build) {
// Redirect all paths css, scss or sass
build.onResolve({filter: /.\.s[ac]ss$/}, (args) => {
const path1 = args.resolveDir.replace('/dist/esm', '');
return {path: path.join(path1, args.path)};
});
},
};
Running the “build” again and we’re getting another error, but this time it’s encouraging. The error claims that there is no “Loader” to deal with the .scss
file:
✘ [ERROR] No loader is configured for ".scss" files: src/Pagination/index.scss
dist/esm/src/Pagination/index.js:4:7:
4 │ import './index.scss';
╵ ~~~~~~~~~~~~~~
Great news :)
For that we need to install a loader for esbuild that can take care of the .scss
file. We’re going to use the esbuild-sass-plugin to do that. I’m installing it with yarn add -D esbuild-sass-plugin
and add it to my esbuild.config.js
like so:
const cssPlugin = require('./esbuild.css.plugin');
const {sassPlugin} = require('esbuild-sass-plugin');
require('esbuild')
.build({
entryPoints: ['dist/esm/index.js'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/main/index.js',
plugins: [cssPlugin, sassPlugin()],
})
.catch(() => process.exit(1));
Running the build now and Boom! We have it - the dist/main
directory holds a processed CSS file generated from the .scss
file. Nice :)
Not that complicated after all. I’m messing a bit with the SASS file, just to make sure the nesting works as expected and indeed it works well:
.pagination {
font-size: large;
.selected {
font-weight: bolder;
color: blue;
}
button {
border: none;
background-color: aqua;
border-radius: 5px;
padding: 10px;
&:hover {
background-color: darkmagenta;
}
&:active {
background-color: aqua;
}
&:disabled {
background-color: lightgray;
}
}
}
As mentioned before, you can find all the code under this GitHub repo, and as always, if you have any questions or comments please leave them in the comments section below so that we can all learn from them :)
Hey! for more content like the one you've just read check out @mattibarzeev on Twitter 🍻
Photo by Karim MANJRA on Unsplash
Top comments (2)
Can you upgrade the react and react-dom version to to 18+ here?
I assume there shouldn't be any trouble with that.