JavaScript, webpack

Should you use WebPack for building SCSS?

In a previous post, I outlined how you could use WebPack to have different build outputs for development and production.

If you read that post, it should be clear that bundling and uglifying your JavaScript with webpack is very straightforward. However, getting webpack to do other front end build operations, such as compile SCSS before uglifying and bundling the generated CSS, is not a simple task. In fact, it’s something I feel I put too much time into.

Since that post, I’ve updated that project so that webpack does one thing – bundle JavaScript.

All of the other build operations (compile SCSS, etc) are now being done in the simplest way possible – Yarn / NPM scripts.

These were previously jobs we’d leave to a task runner, like gulp. These worked and were reliable, but they had their downsides:

  • More gulp specific packages would be needed to act as a glue between the task runner and the task (e.g. gulp-sass)
  • Debugging a massive set of piped streams was not easy, and often needed more packaged installed to do so (see gulp-debug)
  • Managing the parallel nature of these tasks was often tricky, and sometimes required even more packages installed to help you orchestrate the order

You don’t need a task runner

Instead of using gulp or webpack for our entire font end build, we’re going to use yarn / NPM scripts. So, let’s start with our SCSS. We can use node-sass package to compile our SCSS into CSS files:

yarn add node-scss --dev

Now we just need to add a command into the “scripts” section of our package.json file that will call the node-scss package:

...
  "scripts": {
    "build-scss": "node-sass --omit-source-map-url styles/appstyles.scss dist/css/appstyles.css"
  }

It’s basically a macro command. Calling “build-scss” is a shortcut for the longer command that we’ve entered into our package.json file. Here’s how we call it:

yarn run build-scss

Now let’s add another script to call webpack so that it can do what it’s good at – bundling our JavaScript modules:

...
  "scripts": {
    "build-scss": "node-sass --omit-source-map-url styles/appstyles.scss dist/css/appstyles.css",
    "build-js": "webpack --mode=development"
  }

Which now means that we can run:

yarn run build-js

To build our JavaScript.

Bringing it all together

We’ve now got two different yarn script commands. I don’t want to run two commands every time I need to run a front-end build, so wouldn’t it be great if I could run these two commands with a single “build” command? Yes it would!

...
  "scripts": {
    "build-scss": "node-sass --omit-source-map-url styles/appstyles.scss dist/css/appstyles.css",
    "build-js": "webpack --mode=development",
    "build": "yarn run build-scss && yarn run build-js"
  }

All we need to do now is run

yarn run build

And our CSS and JavaScript will be generated. You could add more steps and more yarn scripts as needed – for example, a step to uglify your generated CSS.

Technical, webpack

webpack 4 – How to have separate build outputs for production and development

A pretty common use case is wanting to have different configs for development and production for front end builds. For example, when running in development mode, I want sourcemaps generated, and I don’t want any of my JavaScript or CSS to be minified to help with my debugging.

Webpack 4 introduced a new build flag that is supposed to make this easier – “mode”. Here’s how it’s supposed to be used:

webpack --mode production

This actually won’t be much help if you want to get different build outputs. Whilst this flag will be picked up by Webpack’s internals and will produced minified JavaScript, it won’t help if you want to do something like conditionally include a plugin.

For example, just say I have a plugin to build my SCSS, if I want to minify and bundle my generated CSS into a single file, the best way to do it is to use a plugin – OptimizeCssAssetsPlugin. It would be great if I could detect this mode flag at build time, and conditionally include this plugin if I’m building in production mode. The goal is that in production mode, my generated CSS gets bundled and minified, and in development mode, it doesn’t.

In your webpack.config.js file, it’s not possible to detect this mode flag, and to then conditionally add the plugin based on this flag. This is because the mode flag can only be used in the DefinePlugin phase, where it is mapped to the NODE_ENV variable. The configuration below will make a global variable “ENV” available to my JavaScript code, but not to any of my Webpack configuration code:

module.exports = {
  ...
  plugins: [
    new webpack.DefinePlugin({
      ENV: JSON.stringify(process.env.NODE_ENV),
      VERSION: JSON.stringify('5fa3b9')
    })
  ]
}

Tying to access process.env.NODE_ENV outside of the DefinePlugin phase will return undefined, so we can’t use it. In my application JavaScript, I can use the “ENV” and the “VERSION” global variables, but not in my webpack config files themselves.

The Solution

The best solution even with webpack 4, is to split your config files. I now have 3:

  • webpack.common.js – contains common webpack configs for all environments
  • webpack.dev.js – contains only the dev config settings (e.g. source map gens)
  • webpack.prod.js – contains only prod config, like the OptimizeCssAssetsPlugin

The configs are merged together using the webpack-merge package. For example, here’s my webpack.prod.js file:

const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = merge(common, {
  plugins: [
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano'),
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
      canPrint: true
    })
  ]
});

You can then specify which config file to use when you call webpack. You can do something like this in your package.json file:

{
  ...
  "scripts": {
    "build": "webpack --mode development --config webpack.dev.js",
    "build-dist": "webpack --mode production --config webpack.prod.js"
  }
}

Some further information on using the config splitting approach can be found on the production page in the webpack documentation.