Maximo Martinez Soria
Published on

Let's create a Webpack configuration from scratch

Even though It's not that difficult, webpack could be so intimidating.

We just need to understand a couple of things.

At the end of the post you will be able to fully understand how to write a webpack.config.js file.

This is the full repo: https://github.com/maximomartinezsoria/webpack-boilerplate

Feel free to check it out if you feel lost at any point.

Let's get started.

Setup

First of all, we'll need to create a new folder and initialize git and npm.

We'll also create a src folder with a javascript file where our code is going to live.

# create a new folder and move into it
$ mkdir webpack-boilerplate
$ cd webpack-boilerplate

# Initialize git and npm
$ git init
$ npm init

# create src folder, move into and create a file
$ mkdir src
$ cd src
$ touch index.js

Then, we'll install webpack and webpack-cli. The first one, is the core library and the other will help us interact with webpack.

# i: install
# -D: development dependencies
$ npm i -D webpack webpack-cli

Finally, let's create a configuration file at the same level of package.json.

$ touch webpack.config.js

You should finish with somthing like this:

- webpack-boilerplate
  - node_modules
    - ....
  - src
    - index.js
  - package.json
  - package.lock.json
  - webpack.config.js

Getting into Webpack

Let's work a little bit in webpack.config.js file.

// webpack.config.js
const path = require('path')

module.exports = {
	entry: path.resolve(__dirname, 'src/index.js'),
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'js/[name].js',
	},
}

This is a node file, so we are using commonjs instead of es6 import / export syntax.

Entry, is where Webpack is going to look for the code.

From now on, if you want a code to pass through Webpack, you'll need to import it in that file.

CSS, preprocessors, and other sort of files could be imported as well. Just need the proper loader.

Output, is where Webpack is going to drop the resultant files.

Notice that we are using a placeholder. This one will be replaced with the name of the file by Webpack.

Giving it a shot

We need to create some commands in order to start using webpack.

// package.json

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

As simple as that.

$ npm run dev

The command will throw you a warning message saying that you should specify the mode option.

Go ahead and add a flag to the command specifying either development or production.

We are going to create specific configurations for both modes later, so it's not important right now.

Loaders and Plugins

Loaders allow us to load and pre-process different kinds of files, and plugins extends their possibilities.

I know that this doesn't make much sense right now, but you'll understand soon. Let's go to the code.

Handle HTML

As you now know, the entry point and main file of the app is index.js.

As you also know, browsers need html files. At least one.

The first thing that we are going to do, is to export a html file.

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
	entry: path.resolve(__dirname, 'src/index.js'),
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'js/[name].js',
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: path.resolve(__dirname, 'public/index.html'),
		}),
	],
}

Remember to install the plugin.

$ npm i -D html-webpack-plugin

Babel for modern JavaScript

Not all people use modern browsers. That's why we need Babel to transpile our modern JavaScript into ES5.

First, we need to install some dependencies:

$ npm i -D babel-loader @babel/core @babel/preset-env

As I told you, loaders allow us to load different kinds of files.

This time we have babel-loader, which loads JavaScript files into @babel-core, which transpiles our modern JavaScript into ES5 using the rules defined in @babel/preset-env.

After that little explanation, let's go to the code.

In order to use a loader, we need to use the key module and set an array of rules inside it.

Each rule has some configurations. In this case we are using the followings:

  • test: tells Webpack which kind of files is this rule for.
  • use: which loader will be applied in this rule.
  • exclude: we are excluding the node_modules folder to improve performance.
// webpack.config.js
module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
    ]
  },
  plugins: [
    ...
  ],
}

Ok. We are asking Webpack to use babel. Now we need to tell babel what to do.

All babel configurations need to be set in a specific file called .babelrc in the root folder.

In addition to presets, we can use plugins to increase presets functionality.

In this case, lets install @babel-plugin-transform-runtime, which allows us to use async/await.

We are also installing @babel/runtime. It's needed by the plugin and it's highly recommended to install it as a production dependency.

$ npm i -D @babel/plugin-transform-runtime
$ npm i @babel/runtime
// .babelrc
{
    "plugins": [
        "@babel/plugin-transform-runtime"
    ],
    "presets": [
        "@babel/preset-env"
    ]
}

Styles

Every website or app needs styles.

As you already know, loaders allow us to import different kinds of files into webpack.

Does that mean that we can import css into JavaScript? Yes, it does.

It sounds weird and a little bit crazy, but importing css in JS is how we include our styles into the bundle.

$ npm i -D style-loader css-loader

Importing css into JavaScript is actually weird. That's why we need a couple of things.

css-loader is the one who handle the importing, and style-loader injects css in the html file that we've already generated with HtmlWebpackPlugin.

If you want to use a preprocessor, you need one more loader to handle that file and the specific preprocessor library itself.

SASS / SCSS

$ npm i -D sass-loader node-sass

STYLUS

$ npm i -D stylus-loader stylus

LESS

$ npm i -D less-loader less

The code is quite similar than the last time. We are just adding more rules.

// webpack.config.js
...
module: {
	rules: [
		{
		  test: /\.css$/,
		  use: [
		    'style-loader',
		    'css-loader',
		  ],
		},
		{
		  test: /\.scss$/,
		  use: [
		    'style-loader',
		    'css-loader',
		    'sass-loader'
		  ],
		},
		{
	    test: /\.less$/,
	    use: [
	      'style-loader',
	      'css-loader',
	      'less-loader'
	    ],
	  },
	  {
	    test: /\.styl$/,
	    use: [
	      'style-loader',
	      'css-loader',
	      'stylus-loader'
	    ],
	  },
		...
	]
}
...

Let's recap.

First, we've used a specific loader for a preprocessor, which turns the code into css. Then, css-loader handles that code and finally style-loader injects css into html.

Remember to import these files into your main JS file.

// index.js
import './index.css'
import './index.scss'
import './index.less'
import './index.styl'

Images, videos and fonts

In order to use images, videos or fonts in our css, we need to use a specific loader.

In this case, we are using an object in the use key. This allows us to use additional configurations like the output path.

$ npm i -D file-loader
// webpack.config.js
...
module: {
	rules: [
		...
		{
			// '?' means that 'e' is optional. So we can use jpg or jpeg
      test: /\.jpe?g|png|gif|woff|eot|ttf|svg|mp4|webm$/,
      use: {
        loader: 'file-loader',
        options: {
          outputPath: 'assets/'
        }
      },
    },
		...
	]
}
...

Development vs Production

We've already learned a lot about Webpack. We are able to use lots of files as well as modern Javascript and CSS preprocessors.

But the real power of Webpack is to make a better developer experience in development mode and a better user experience in production mode. That's why we are going to make two different configurations from now on.

Development

Let's create a new configuration file for development purposes.

The standard name is usually webpack.dev.config.js, but you can use anyone.

We need all the things that we've done before. So I'm going to rename the file that I was using, as oposed to create a new one.

Since we are working in development mode, let's get Webpack to know that, using the mode key.

//webpack.config.js
module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js',
  },
  mode: 'development',
	...
}

It's also time to change our package.json script.

// package.json
...
"scripts": {
  "dev": "webpack-dev-server --config ./webpack.dev.config.js"
},
...

The config option defines the path to the config file.

Let's talk about webpack-dev-server.

Development server

The first and one of the most important things that a developer needs is a local server.

That's what we use webpack-dev-server for.

$ npm i -D webpack-dev-server

We also need to write the configuration for the server.

//webpack.config.js
module.exports = {
  ...
  mode: 'development',
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    open: true,
  },
	...
}

contentBase defines the directory where is going to look for the files and open in true, means that when we run the command Webpack is going to open a browser automatically.

From now on, Webpack is going to create a new bundle every time you save a file.

Production

Our development configuration is done.

But for a better user experience we can change some things for production.

As we are going to change lots of things, let's create another file and make a new script. I'll call it webpack.config.js.

// package.json
...
"scripts": {
  "dev": "webpack-dev-server --config ./webpack.dev.config.js",
  "build": "webpack"
},
...

As long as you use the default name (webpack.config.js), config option is not required.

We can reuse some options that we've already written, so my new file it's just a duplication from webpack.dev.config.js.

Some important changes that we need before getting started are:

  • mode: set to production.
  • devServer: remove the entire object. We don't need a local server in production.
  • output: is a good practice to use hash in production files in order to avoid cache problems.
...
entry: path.resolve(__dirname, 'src/index.js'),
output: {
  path: path.resolve(__dirname, 'dist'),
  filename: 'js/[name].[hash].js',
},
mode: 'production',
..

Now we are ready to start.

Extracting styles

The first difference between development and production is how styles are handled.

In development, we inject the css in order to accelerate things. But in production we need a css file. That's why, we are using a new loader that will extract the css into a new file.

$ npm i -D mini-css-extract-plugin

So, is it a plugin or a loader? Actually, both. You'll see.

We need to pass it as a plugin to define both the output filename and the chunkFilename.

The hash placeholder, allows us to avoid cache problems.

We need to pass it as a loader also. And, as you can see, we are not using style-loader any more.

// webpack.config.js
const MiniCSSExtractPlugin = require('mini-css-extract-plugin')
...

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCSSExtractPlugin.loader
          },
          'css-loader',
        ],
      },
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCSSExtractPlugin.loader
          },
          'css-loader',
          'sass-loader'
        ],
      },
      {
        test: /\.less$/,
        use: [
          {
            loader: MiniCSSExtractPlugin.loader
          },
          'css-loader',
          'less-loader'
        ],
      },
      {
        test: /\.styl$/,
        use: [
          {
            loader: MiniCSSExtractPlugin.loader
          },
          'css-loader',
          'stylus-loader'
        ],
      },
      ...
    ]
  },
  plugins: [
    ...
    new MiniCSSExtractPlugin({
      filename: 'css/[name].[hash].css',
      chunkFilename: 'css/[id].[hash].css'
    })
  ],
}

So, let's recall.

We are using css-loader or the specific loaders for preprocessors to allow importing those files into JavaScript. Then, we use MiniCSSExtractPlugin to extract css and create a file with it. Finally, that new file will be linked to the html automatically.

Cleaning

We are almost done. Let's add two more fixes.

The first one is related with hashes. As you know, we are using hashes everywhere to avoid cache problems. But now we have a problem with hashes. It's a bug or a feature?

The problem is that we are creating new files every time we run Webpack, but we are just using the last one. So, if you run a couple of times the command and see the css or js folder, you are going to find lots of garbage files.

It would be great to erase all of those files before each run of Webpack. Let's do it.

$ npm i -D clean-webpack-plugin
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

...
	plugins: [
		...
		new CleanWebpackPlugin({
		  cleanOnceBeforeBuildPatterns: ['**/*']
	  })
	]
...

That does exactly what we want. It cleans the whole dist folder before each bundle creation.

Note that you can modify the pattern (**/*) to match just the things you want. This might be useful when you are using dll for example.

Minify CSS

The second fix is that css is not being minified.

Minification process removes all unnecessary spaces in the file so it becomes smaller.

$ npm i -D optimize-css-assets-webpack-plugin
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

...
	output: {
    ...
  },
	optimization: {
    minimizer: [ new OptimizeCSSAssetsPlugin() ]
  },
...

optimization is a new key that we didn't use before. I recommend you to take a look at the docs to know more about this key.

The perfect configuration

since this is a very general configuration, we're done.

But this isn't the perfect Webpack configuration. Actually, that doesn't exists.

Each project has its own configuration so you can do whatever you need.

I'm preparing another example of Webpack configuration for React. Stay tuned.