A tale of Webpack 4 and how to finally configure it in the right way

2018/4/28 posted in  Node.js

https://hackernoon.com/a-tale-of-webpack-4-and-how-to-finally-configure-it-in-the-right-way-4e94c8e7e5c1

There are a million tutorials online, so you probably have seen a thousand different ways to configure Webpack file. And all of them will be working examples. Why is it so? Webpack itself has been evolving really fast and a lot of loaders and plugins have to keep up. This is a major reason why the configuration files are so different: with a different version combination of the same tools things might work, or break.

Let me just say one thing, and this is my sincere opinion: a lot of people have been complaining about webpack and how cumbersome it is. This is true in many ways, although I have to say with my experience of working with gulp and grunt, you stumble upon the same type of errors there too, meaning that when you use npm modules, it’s inevitable that some versions would be incompatible.

Webpack 4 so far is the popular module bundler that has just undergone a massive update. There is a lot of new things it has to offer, such as zero configuration, reasonable defaults, performance improvement, optimisation tools out of the box.

If you are completely new to webpack, a great way to start would be to read the docs. Webpack has a pretty nice documentation with many parts explained, so I will go through them very briefly.

Zero config: webpack 4 does not require a configuration file, this is new for the version 4. Webpack kinda grows step by step, so there is no need to do a monstrous configuration from the start.

Performance improvement: webpack 4 is the fastest version of webpack so far.

Reasonable defaults: webpack 4main concepts are entry, output, loaders, plugins. I will not cover these in details, although the difference between loaders and plugins is very vague. It all depends on how library author has implemented it.

Core concepts

Entry

This should be your _.js_ file. Now you will probably see a few configurations where people include _.scss_ or _.css_ file there. This is a major hack and can lead to a lot of unexpected errors. Also sometimes you see an entry with a few _.js_ files. While some solutions allow you to do so, I would say it usually adds more complexity and only do it when you really know why you are doing it.

Output

This is your build/ or dist/ or wateveryounameit/ folder where your end js file will be hosted. This is your end result comprised of modules.

Loaders

They mostly compile or transpile your code, like postcss-loader will go through different plugins. You will see it later.

Plugins

Plugins play a vital role in outputting your code into files.

Quickstart

Create a new directory and move into it:

mkdir webpack-4-tutorial
cd webpack-4-tutorial

Initialize a package.json :

npm init

We need to download webpack v4 as a module and webpack-cli to run it from your terminal.

npm install webpack webpack-cli --save-dev

Make sure you have version 4 installed, if not, you can explicitly specify it in your package.json file. Now open up package.json and add a build script:

"scripts": {
  "dev": "webpack"
}

You will most likely see a warning:

WARNING in configuration

The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.

You can also set it to ‘none’ to disable any default behavior. Learn more: [https://webpack.js.org/concepts/mode/](https://webpack.js.org/concepts/mode/)

Webpack 4 modes

You need to edit your script to contain mode flag:

"scripts": {
 "dev": "webpack --mode development"
}

ERROR in Entry module not found: Error: Can’t resolve ‘./src’ in ‘~/webpack-4-quickstart’

This means webpack is looking for a folder _.src/_ with an index.js file. This is a default behaviour for webpack 4 since it requires zero configuration.

Let`s go create a directory with a _.js_ file like this ./src/index.js and put some code there.

console.log("hello, world");

Now run the dev script:

npm run dev

Now you have a ./dist/main.js directory. This is great since we know our code compiled. But what did just happen?

By default, webpack requires zero configuration meaning you do not have to fiddle with webpack.config.js to get started using it. Because of that, it had to assume some default behaviour, such that it will always look for ./src folder by default and index.js in it and output to ./dist/main.js main.js is your compiled file with dependencies.

Having 2 configuration files is a common practice in webpack, especially in big projects. Usually you would have one file for development and one for production. In webpack 4 you have modes: production and development. That eliminates the need for having two files (for medium-sized projects).

"scripts": {
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
}

If you paid close attention, you have checked your main.js file and saw it was not minified.

I will use build script in this example since it provides a lot of optimisation out of the box, but feel free to use any of them from now on. The core difference between build and dev scripts is how they output files. Build is created for production code. Dev is created for development, meaning that it supports hot module replacement, dev server, and a lot of things that assist your dev work.

You can override defaults in npm scripts easily, just use flags:

"scripts": {
  "dev": "webpack --mode development ./src/index.js --output ./dist/main.js",
  "build": "webpack --mode production ./src/index.js --output ./dist/main.js"
}

This will override the default option without having to configure anything yet.

As an exercise, try also these flags:

  • — watch flag for enabling watch mode. It will watch your file changes and recompile every time some file has been updated.
"scripts": {
  "dev": "webpack --mode development ./src/index.js --output ./dist/main.js --watch", 
  "build": "webpack --mode production ./src/index.js --output ./dist/main.js --watch"
}
  • — entry flag. Works exactly like output, but rewrites the entry path.

Transpile your .js code

Modern JS code is mostly written is ES6, and ES6 is not supported by all the browsers. So you need to transpile it — a fancy word for turn your ES6 code into ES5. You can use babel for that — the most popular tool to transpile things now. Of course, we do not only do it for ES6 code, but for many JS implementations such as TypeScript, React, etc.

npm install babel-core babel-loader babel-preset-env --save-dev

This is the part when you need to create a config file for babel.

nano .babelrc

paste there:

{  
  "presets": ["env"]
}

We have two options for configuring babel-loader:

  • using a configuration file webpack.config.js
  • using --module-bind in your npm scripts

You can technically do a lot with new flags webpack introduces but I would prefer webpack.config.js for simplicity reasons.

Configuration file

Although webpack advertises itself as a zero-configuration platform, it mostly applies to general defaults such as entry and output.

At this point we will create webpack.config.js with the following content:

// webpack v4

const path = require('path');

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  }
};

also we will remove flags from our npm scripts now.

"scripts": {
  "build": "webpack --mode production",
  "dev": "webpack --mode development"
},

Now when we run npm run devit should output us a nice minified _.js_ file into _./dist/main.js_ If not, try re-installing babel-loader.

The most common pattern of webpack is to use it to compile react application. While this is true, we will not concentrate on React part in this tutorial since I want it to be framework agnostic. Instead, I will show you how to proceed and create your .html and .css configuration.

HTML and CSS imports

Lets create a small index.html file first in our _./dist_ folder

<html>
  <head>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div>Hello, world!</div>
    <script src="main.js"></script>
  </body>
</html>

As you can see, we are importing here style.css Lets configure it! As we agreed, we ca only have one entry point for webpack. Sow were do we put our css to?

Create a style.css in our _./src_ folder

div {
  color: red;
}

Do not forget to include it into your .js file:

import "./style.css";
console.log("hello, world");

In webpack create a new rule for css files:

// webpack v4
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract(
          {
            fallback: 'style-loader',
            use: ['css-loader']
          })
      }
    ]
  }
};

in terminal run

npm install extract-text-webpack-plugin --save-dev
npm install style-loader css-loader --save-dev

We need yo use extract text plugin to compile our .css. As you can see, we also added a new rule for .css.

A quick description of how rules usually work:

{
  test: /\.YOUR_FILE_EXTENSION$/,
  exclude: /SOMETHING THAT IS THAT EXTENSION BUT SHOULD NOT BE PROCESSED/,
  use: { loader: "loader for your file extension  or a group of loaders" }
}

We need to use ExtractTextPlugin because webpack be default only understands _.js_ format. ExtractTextPlugin gets your_ .css_ and extracts it into a separate _.css_ file in your _./dist_ directory.

Spoiler: in certain articles, you will hear that ExtractTextPlugin does not work with webpack 4 but it worked for me :) It proves my point of modules ambiguity in set-up and if it absolutely does not work for you, you can switch to MiniCssExtractPlugin. I will show you how to configure another one later in this article.

// webpack v4
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract(
          {
            fallback: 'style-loader',
            use: ['css-loader']
          })
      }
    ]
  },
  plugins: [ 
    new ExtractTextPlugin({filename: 'style.css'})
  ]
};;

Since version 4, Webpack 4 has problems with this plugin, so you might run into this error:

Webpack 4 compatibility · Issue #701 · webpack-contrib/extract-text-webpack-plugin
I'm trying to use this plugin with webpack 4 alpha 5 and getting the following error: Error: Chunk.entrypoints: Use…github.com

To fix it, you can run

npm install -D extract-text-webpack-plugin@next

Pro tip: google errors you get and try to find similar question in Github issues or just ask a question on StackOverflow.

After that, your css code should compile to _./dist/style.css_

At this point in my package.json my dev dependencies look like this:

"devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.4",
    "babel-preset-env": "^1.6.1",
    "css-loader": "^0.28.11",
    "extract-text-webpack-plugin": "^4.0.0-beta.0",
    "style-loader": "^0.20.3",
    "webpack": "^4.4.1",
    "webpack-cli": "^2.0.12"
  }

Please, note that another combination might not work since even updating webpack-cli v2.0.12 to 2.0.13 can break it. #justwebpackthings

So now it should output your style.css into _./dist_ folder.

Configure support for SCSS

It is very common to develop websites with SASS and POSTCSS, they are very helpful. So we will include support for SASS first. Let`s rename our _./src/style.css_ and create another folder to store _.scss_ files in there. Now we need to add support for _.scss_ formatting.

npm install node-sass sass-loader --save-dev

replace style.scss with _./scss/main.scss_ in your _.js_ file.

HTML template

Now lets create _.html_ file template. Add index.html to _./src_ file with exactly the same structure.

<html>
  <head>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div>Hello, world!</div>
    <script src="main.js"></script>
  </body>
</html>

We will need to use html plugin for this file in order to use it as a template.

npm install html-webpack-plugin --save-dev

Add it to your webpack file:

// webpack v4
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract(
          {
            fallback: 'style-loader',
            use: ['css-loader', 'sass-loader']
          })
      }
    ]
  },
  plugins: [ 
    new ExtractTextPlugin(
      {filename: 'style.css'}
    ),
    new HtmlWebpackPlugin({
      inject: false,
      hash: true,
      template: './src/index.html',
      filename: 'index.html'
    })
  ]
};

Now your file from _./src/index.html_ is a template for your final index.html file. To check that everything works, delete every file from _./dist_ folder and the folder itself.

rm -rf /dist
npm run dev

You will see that _./dist_ folder was created on its own and there are three files: index.html, style.css, script.js.

Caching and Hashing

One of the most common problems in development is implementing caching. It is very important to understand how it works since you want your users to always have the best latest version of your code.

Since this blogpost is mainly about webpack configuration, we will not concentrate on how caching works in details. I will just say that one of the most popular ways to solve caching problems is adding a hash number to asset files, such style.css and script.js. You can read about it here. Hashing is needed to teach our browser to only request changed files.

Webpack 4 has a prebuilt functionality for it implemented via chunkhash. It can be done with:

// webpack v4
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract(
          {
            fallback: 'style-loader',
            use: ['css-loader', 'sass-loader']
          })
      }
    ]
  },
  plugins: [ 
    new ExtractTextPlugin(
      {filename: 'style.[chunkhash].css', disable: false, allChunks: true}
    ),
    new HtmlWebpackPlugin({
      inject: false,
      hash: true,
      template: './src/index.html',
      filename: 'index.html'
    }),
  ]
};

In your_./src/index.html_ file add

<html>
  <head>
    <link rel="stylesheet" href="<%=htmlWebpackPlugin.files.chunks.main.css %>">
  </head>
  <body>
    <div>Hello, world!</div>
    <script src="<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>
  </body>
</html>

This syntax will teach your template to use hashed files. This is a new feature implemented after this issue:

Support for .css and .manifest files and cache busting by jantimon · Pull Request #14 ·…
This pull request deprecates the usage of {%=o.htmlWebpackPlugin.assets%} and creates a new representation called…github.com

We will use htmlWebpackPlugin.files.chunks.main pattern described there.

now in our _./dist_ file index.html

Now if we do not change anything in our _.js_ and. css file and run

npm run dev

no matter how many times you run it, the numbers in hashes should be identical to each other in both files.

Problem with hashing and how to solve it

Although we have the working implementation here, it is not perfect yet. What if we change some code in our _.scss_ file? Go ahead, change some scss there and run dev script again. Now the new file hash is not generated

What if we add a new console.log to our _.js_ file like this:

import "./style.css";
console.log("hello, world");
console.log("Hello, world 2");

If you run a dev script again, you will see that hash number has been updated in both files.

This issue is known and there is even a stack overflow question about it:

Updating chunkhash in both css and js file in webpack
I have only got the JS file in the output whereas i have used the ExtractTextPlugin to extract the Css file.Both have…stackoverflow.com

Now how to fix that?

After trying a lot of plugins that claim they solve this problem I have finally came to two types of solution:

Solution 1

Replace [chukhash] with just [hash] in _.css_ extract plugin. This was one of the solutions to the issue. This appears to be a conflict with webpack 4.3 which introduced a [contenthash] variable of its own. In conjunction, use this plugin: webpack-md5-hash

Now if you make changes to your main.scss file and run dev script, only a new style.css should be generated with a new hash.

// webpack v4
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackMd5Hash = require('webpack-md5-hash');

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract(
          {
            fallback: 'style-loader',
            use: ['css-loader', 'sass-loader']
          })
      }
    ]
  },
  plugins: [ 
    new ExtractTextPlugin(
      {filename: 'style.[hash].css', disable: false, allChunks: true}
    ),
    new HtmlWebpackPlugin({
      inject: false,
      hash: true,
      template: './src/index.html',
      filename: 'index.html'
    }),
    new WebpackMd5Hash()
  ]
};

Now lets test our _.js_ files. Now both files change hash.

Solution 2

There might also be some conflicts still, so now lets try mini-css-extract plugin.

Mini-CSS plugin

The Mini CSS plugin is meant to replace extract-text plugin and provide you with better future compatibility.

I have restructured my webpack file to compile style.css with /mini-css-extract-plugin and it works for me.

// webpack v4
const path = require('path');
// const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackMd5Hash = require('webpack-md5-hash');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.scss$/,
        use:  [  'style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [
    // new ExtractTextPlugin(
    //   {filename: 'style.[hash].css', disable: false, allChunks: true }
    // ),
    new MiniCssExtractPlugin({
      filename: 'style.[contenthash].css',
    }),
    new HtmlWebpackPlugin({
      inject: false,
      hash: true,
      template: './src/index.html',
      filename: 'index.html'
    }),
    new WebpackMd5Hash()
  ]
};

Now when I edit main.scss a new hash for style_.css_ is generated. And when I edit css only css hash changes and when I edit _./src/script.js_ only script.js hash changes!