DEV Community

Cover image for Supporting SASS in your TS React project using TSC and esbuild
Matti Bar-Zeev
Matti Bar-Zeev

Posted on

Supporting SASS in your TS React project using TSC and esbuild

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:

Image description

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",
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

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';
        ╵        ~~~~~~~~~~~~~~
Enter fullscreen mode Exit fullscreen mode

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)};
       });
   },
};
Enter fullscreen mode Exit fullscreen mode

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';
        ╵        ~~~~~~~~~~~~~~
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

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

Oldest comments (2)

Collapse
 
ussaarchekkan profile image
SabzWorld

Can you upgrade the react and react-dom version to to 18+ here?

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

I assume there shouldn't be any trouble with that.