Guide to Webpack 5

Back in the days when modularity in Javascript was introduced, there was no way to support modules within web browsers. To fix this issue, module bundlers such as Parcel, Webpack, and Rollup were developed. These helped to optimize multiple modules into a production-ready bundle, which can be executed by browsers.

Today, when it comes to the ecosystem of module bundling, the tool that resides the most on everyone’s lips is Webpack.

For me, webpack used to be quite intimidating to deal with, I would rather run to tools like vue-create if I needed to set up a Vue project or create-react-app to set up a React app, just to avoid all the complexities I felt while working with webpack.

If you are just the way I used to be, that is, you feel like setting up webpack from scratch with Babel, SASS, Vue, or TypeScript can be pretty confusing, then this guide is for you!

In this guide, you will learn how to create a frontend environment using Webpack, and like all things of life, the moment you give yourself the chance to learn and make sure to understand a concept thoroughly, you will realize that it is not too scary after all.

With lots of moving parts to it, we will learn various fundamental concepts to get you familiar with webpack and build a webpack boilerplate to help you get up and running when setting up any project from scratch with the widely popular module bundler.

What is Webpack?

Regardless of how complex webpack might come off to be, its core goal remains very straightforward; it takes a bunch of different assets, different files - of different types; JavaScript, images, SASS, PostCSS, whichever file it may be, and it combines them altogether (bundles them) into a smaller group of files, perhaps one file, or it could be one file for your JavaScript, one file for stylesheets, one for third-party JavaScript, and one for the index JavaScript file.

It is very configurable, and this is where it turns out to be a bit tedious to people who are new to it, however, the idea behind it is very simple. In addition to just bundling things together, it also manages dependencies of an application, thus, making sure that codes that need to load first - load first, in the same way, if for example, you write a file that depends on two other files, it means that those two other files need to be loaded first.

As per the official GitHub page:

"A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff."

There is just so much to Webpack, and at this point in this guide, we are about to dive into getting familiar with core Webpack concepts.

Project Setup

In your preferred directory for this tutorial, create a new folder: webpack-app, for housing the webpack project.

$ mkdir webpack-app

Next, let us initialize Node.js in the project folder by running the following commands in the terminal, from the project directory:

$ cd webpack-app
$ npm init --yes

This will create a package.json file which makes it possible and easy to track application dependencies.

Now that Node is initialized in our project, we can begin to install the various packages that we will need. To proceed, let us install the core packages: webpack and webpack-cli.

$ yarn add --dev webpack webpack-cli
# Or
$ npm install --save-dev webpack webpack-cli

The webpack package would allow us to produce bundled JavaScript modules for usage in a browser, while the webpack-cli is a tool that provides a set of commands that make it easy for developers to set up a custom webpack project quickly.

To begin, let us create two new folders in the root directory src and dist. In the src folder, create an index.js file that would serve as the root file for our application.

Note: Out of the box, webpack doesn’t require a config file. It automatically assumes that the entry point of a project is the src/index.js and it will output a minified and optimized result for production in the dst/main.js file.

Configuring the Project

Next, in the root of our project directory, let us set up the configuration file webpack.config.js to define how bundling should take shape within the project:

$ touch webpack.config.js

Mode

The mode defines the environment where the project currently operates. Its property has a default of none, but can be set to either production or development, but you’d never actually use none as a mode. In fact, what happens when the mode remains unset, webpack uses production as a fallback option for optimization.

With development set as the mode, we easily use source mapping to spot errors and know where the original files were.

With production set as the mode, the priority is to shrink, optimized, and minify files:

const path = require("path");

module.exports = {
  mode: "development", // It can also be production
};

Entry

Another configuration to set is the entry object. This defines what file we want our webpack to start building our application bundle from. If it is a Single Page Application, then it should have one entry point, while if it is a Multi-Page Application, it should have multiple entry points.

Of course, in a Single Page Application, that would be the index JavaScript file inside the src folder, and in our case, it is the src/index.js file:

const path = require("path");

module.exports = {
  /* ... */
  entry: {
    main: path.resolve(__dirname, "src/index.js"),
  },
};

Output

Finally, for a start, we will also be needing an output to declare where we want to output the files, assets, bundles, and other application resources after the application bundling processes have been completed. In our case, we set the path for output to be the dist folder, but you can rename it if you wish, perhaps to deploy, build, or whatever name you prefer.

We also need to set the filename to define the name of the output bundle. A common name convention is [name].bundle.js:

const path = require("path");

module.exports = {
  /* ... */

  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.bundle.js", // using static name
    // OR
    // using the entry name, sets the name to main, as specific in the entry object.
    filename: "[name].bundle.js", 
  },
};

We have now set up the minimum configuration required to bundle an application, that we can run through package.json

To achieve that, we need to update the predefined script object in package.json. Let us create a new command called build, which would have webpack as its value. With this command saved, we can now run webpack.

But before we run the build command, let us quickly include a few lines of JavaScript code in the src/index.js file:

// src/index.js
console.log("Hello world! My name is Uche Azubuko.");

Next, we run webpack with the following command, from the terminal:

$ yarn build
## OR
$ npm run build

This outputs:

$ webpack
asset main.bundle.js 1.24 KiB [emitted] (name: main)
./src/index.js 54 bytes [built] [code generated]
webpack 5.75.0 compiled successfully in 98 ms
Done in 0.94s.

Essentially, when we run the build command, webpack is the command that is run under the hood; the configuration settings we defined would be read and the entry point, that is app.js gets bundled, read through, and output as main.bundle.js in the dist folder.

If we keep building various points, we would get various names, but if we set the filename to [name].[contenthash].js, we would be sure to get a unique name for the compiled file, each time our application bundle is completed.

Now, when we build app again, we get a unique name for the file that is output into the dist folder, such as main.96f31f8d4c5ec9be4552.js:

$ yarn build
# Or
$ npm run build

This outputs:

$ webpack
asset main.96f31f8d4c5ec9be4552.js 1.24 KiB [emitted] [immutable] (name: main)
./src/index.js 54 bytes [built] [code generated]
webpack 5.75.0 compiled successfully in 88 ms
Done in 0.92s.

This way, the browser is able to cache application resources per build time, since there are unique names each time the app was built (more like version management for each build file).
This makes it possible for web applications and websites to have faster load time and unnecessary network traffic, however, when changes are made to the content, it could cause some difficulties since webpack doesn’t now know which file is needed or not needed.

Thus, the best thing to do is to clean up the dist directory each time an application bundling process gets completed.

The clean-up can be achieved by setting the clean option in the output object to true, so that webpack.config.js is updated:

const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    main: path.resolve(__dirname, "src/index.js"),
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].[contenthash].js",
    clean: true,
  },
};

Now, every time we run the build command, the dist directory is first cleaned up before a new file gets emitted into it.

So that is about entry and output; where to get the files for bundling, and where to put the files after bundling.

There are two other main options with the configuration file that we can include: loaders, and plugins.

Plugins

To see plugins in action, we would make use of the HTMLWebpackPlugin by which makes it possible to dynamically create HTML files, by installing the html-webpack-plugin package:

$ yarn add --dev html-webpack-plugin
# Or
$ npm install --save-dev html-webpack-plugin

Then in the plugins sections of the config file, we pass in the title of the HTML file to be built, and a filename:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: {
    main: path.resolve(__dirname, "src/index.js"),
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].[contenthash].js",
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "Testing html file",
      filename: "index.html",
    }),
  ],
};

Now, when we run the build command , a HTML file is created in the dist folder. And in the index.html file, you will notice that the unique js file that had been bundled has been automatically appended:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Testing html file</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"><script defer src="main.b908ea8ce529b415a20a.js"></script></head>
  <body>
  </body>
</html>

If your wish is to generate a template to use as your index.html page in the dist folder, you can achieve this by attaching a template option into the plugins option of the config file, then define a template file in the src folder which would be the value of the template option:

module.exports = {
  /* ... */

  plugins: [
    new HtmlWebpackPlugin({
      title: "Testing html file",
      filename: "index.html",
      template: path.resolve(__dirname, "src/template.html"),
    }),
  ],
};

Then in the template.html file, we can define a few markups. One thing to note is that this template file has access to all that we have defined in the new HtmlWebpackPlugin object, such as the title, filename, which we can import into the file:

// src/template.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <header>
      <h1>Template File of <%= htmlWebpackPlugin.options.title %></h1>
    </header>
  </body>
</html>

Now, when we build our app again, we get the title in the generated dist/index.html to be “Testing html file”:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Testing html file</title>
  <script defer src="main.b908ea8ce529b415a20a.js"></script></head>
  <body>
    <header>
      <h1>Template File of Testing html file</h1>
    </header>
  </body>
</html>

Loaders

By default, webpack understands JavaScript and JSON (JavaScript Object Notation), but it does not know what an image file is, or HTML, CSS, SASS, or SVG files are, including other types of files; it does not know how to handle them. Because of this challenge, loaders come in.

If you have got HTML, CSV, SVG, CSS, SASS, or image files, you can have loaders for all these different types of files that would look into the source folder, find them, and then turn them into modules that can be imported by JavaScript.

Let us have a typical example, by attaching this SVG file into the src folder and a CSS file too. If we wanted to bring that in and have it well bundled, we would use a loader - our index.js file, two things ought to done here; the index.js file acts as a loader to load the type of file, then this loader gets imported to the dist directory when the app is bundled, so that it becomes part of the application’s dependency graph.

Loading CSS files with Webpack

We can create a main.css in the src folder:

// src/main.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

Then update index.js:

// src/index.js

import style from "./main.css";

console.log("Hello world! My name is Uche Azubuko!");

In the loaders option in the config file, we can now set rules, for loading various types of files as modules during the bundling process:

module.exports = {
  /* ... */

  module: {
    rules: [{ test: /\.css$/, use: ["style-loader", "css-loader"] }],
  },
};

Here, we have created a rule that provides an array of loaders for loading any CSS files in the src folder; being read from right to left. The CSS-loader looks for the file, turns it into a module and gives it to JavaScript, while the style-loader takes the imported module, and injects it into the dist/index.html file.

They are dev dependencies, thus, we have to also install them in the application:

$ yarn add --dev style-loader css-loader
# Or
$ npm install --save-dev style-loader css-loader

When we run the build command, the resulting JavaScript file from the bundling process now knows about the CSS file and injects it into the dist app.

Suppose you wish to launch the development version of your application on the browser. First, we add another option to the scripts object in the package.json file:

"dev": webpack serve

The command serve is added along with webpack. It compiles everything and holds it in memory. In the config file, we then add the two options: devtool and devServer. These are options that help to make working on the server optimal.

Devtools

The devtool config option controls how and if source maps are generated during compilation. Source mapping keeps track of where all content came from, and makes the browser know where any potential error is coming from:

module.exports = {
  /* ... */

  devtool: "inline-source-map",
};

DevServer

This defines the setting for the server that we are setting up, and we define where the base of the app should run from. We also define hot module reloading (HMR) using the hot option, set the port number wherein the app would run, watch for changes using the watchFiles by setting its value to the src folder, use the open option to immediately open the browser when the compilation process for launching the development version is completed, and set the port number of your choice using the port option:

module.exports = {
  /* ... */

  devServer: {
    contentBase: path.resolve(__dirname, "dist"),
    port: 5001, // default is often 8080
    open: true,
    hot: true,
    watchFiles: [path.resolve(__dirname, 'src')],
  },
};

In addition to the defined devServer option, we also need to install the webpack-dev-server package as a dev dependency, which is to help.

Now when we run the dev command, the app is rendered on the browser at localhost:5001, showing all content that has been injected into the dist/index.html during the bundling process:

With hot module replacement being enabled, if you make a change during development, it immediately reflects on the browser when you save the changes.

Loading Images

Another thing that is made easier in version 5 of webpack is the built-in asset/resource loader, which we can use to check for images in order to inject them into the app during the bundling process:

module.exports = {
  /* ... */

  module: {
    /* ... */
    { test: /\.(svg|ico|png|webp|jpg|gif|jpeg)$/, type: "asset/resource" },
  },
};

Here, we have written a check to find all files that are associated with the following file type and inject them into the dist folder during bundling: svg, ico, png, webp, jpg, gif, jpeg.

Update src/index.html to include the SVG file you downloaded earlier:

// src/index.js

import style from "./main.css";
import logo from "./logo.svg";

console.log("Hello world! My name is Uche Azubuko!");

Now, run the dev command, and the SVG file should now be compiled and the bundling process should be successful.

Typically, we would want to make use of the SVG within a component file. For that, let us create a component.js file in the src folder. Therein, we would create a module that would use to create content on the page:

// src/component.js

import logo from "./logo.svg";

function component() {
  let main = document.createElement("main");
  let para = document.createElement("p");
  let img = document.createElement("img");
  main.append(para);
  para.append(img);
  img.src = logo;
  img.alt = "logo for the app";
  return main;
}

export default component;

For us to make use of the image, the first step would be to import it into the file. Then we also need to import the component.js file in index.js so that it can be bundled along and rendered on the DOM. The component is a function that is called and returns the main element which was returned in the component.js file, and is then injected into the body of the DOM:

// src/index.js

import style from "./main.css";
import logo from "./logo.svg";
import component from "./component";

console.log("Hello world! My name is Uche Azubuko!");

document.body.append(component());

Now, when we run the dev server again, we should have the SVG file now rendered on the DOM:

If we wish for our bundled images to maintain their original names after bundling is completed, in webpack.config.js, we can set assetModuleFilename to be "[name][ext]" in the output option:

output: {
  path: path.resolve(__dirname, "dist"),
  filename: "[name].[contenthash].js",
  clean: true,
  assetModuleFilename: "[name][ext]",
},

Now, when we restart the development server the name of the image should be the same as it was before bundling occurred.

Transpiling

It is often a good idea to transpile your code into older ES5 versions so that older browsers can make sense of your code, as well as newer browsers that use ES6 and ES7, can. To achieve that, we use the Babel tool.

Now, we include a rule that looks for all JavaScript files in the src folder excluding the node_modules folder, then transpile them using the babel-loader which uses @babel/core and would be installed as a dev dependency, along with setting the presets options to @babel/preset-env.

First, let us install the required packages:

$ yarn add --dev babel-loader @babel/core @babel/preset-env
# Or
$ npm install --save-dev babel-loader @babel/core @babel/preset-env
module.exports = {
  /* ... */

  module: {
    /* ... */
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: "babel-loader",
        options: { presets: ["@babel/preset-env"] },
      },
    },
  },
};

Save the file, then re-run the development server. This time, our application now has support for older browsers that use ES5 modules and is more backward compatible now

Conclusion

The aim of this guide was to get you to see how things work under the hood; how to build JavaScript modules for cross-browser support, how to transpile with Babel, create source maps, and make your frontend environment as simple and advanced as you would like.

Even if you make use of tools like vue-create to set up a Vue project or create-react-app to set up a React app, the truth is that they use webpack behind the scenes.

Webpack is really not that hard to understand, and from this guide, it would be awesome for you to extend your learning to see how to work with SASS, PostCSS in webpack, and production optimization. If you find it challenging, feel free to reach out to me, I would be glad to help.

Once again, you can make use of the project we built in this guide as a boilerplate for applications that you might want to build using plain ol’ webpack, and you can easily set up anything else that you might like.

You can refer to all the source code used in this guide on GitHub.

Additional Sources

Last Updated: April 20th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Uchechukwu AzubukoAuthor

Uchechukwu Azubuko is a Software Engineer and STEM Educator passionate about making things that propel sustainable impact and an advocate for women in tech.

He enjoys having specific pursuits and teaching people better ways to live and work - to give them the confidence to succeed and the curiosity required to make the most of life.

Project

React State Management with Redux and Redux-Toolkit

# javascript# React

Coordinating state and keeping components in sync can be tricky. If components rely on the same data but do not communicate with each other when...

David Landup
Uchechukwu Azubuko
Details

Getting Started with AWS in Node.js

Build the foundation you'll need to provision, deploy, and run Node.js applications in the AWS cloud. Learn Lambda, EC2, S3, SQS, and more!

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms