Stages of Learning Webpack, Pt. 3 - SourceMaps, Loaders, & Plugins
Tags: webpack, javascript, typescript
Published: 2017-06-18 05:59:58

---
title: Stages of Learning Webpack, Pt. 3 - SourceMaps, Loaders, & Plugins
published: true
description: Simplified bundle debugging, the fanciest of JavaScript, & more!
tags: webpack, javascript, typescript
---


## Intro

This article is a continuation of the [Stages of Learning Webpack](https://dev.to/nitishdayal/stages-of-learning-webpack) series. The repository containing the source code has been updated since the last article to include the latest versions of all dependencies as well as some configuration improvements (read: I might've made a lot of goofs the first time around).

[Github Repo](https://github.com/nitishdayal/webpack-stages-example)


### Step 2, Cont.

At some point, you'll need to debug your application. Most modern browsers provide intuitive _developer tools_ which can assist in debugging and optimizing your application.

In your text editor, change the first line in `src/app/sayHello.js` to the following:

```javascript
export default name => alet(`Hello ${name}`);
```

We've placed an intentional error in our application by misspelling `alert` as `alet`. From the command line, run `npm run build`/`yarn build`. The build should still succeed; Webpack is not responsible for maintaining the accuracy of our JavaScript code. Open the `index.html` file in your browser and open your browser's developer tools. There will be an error message along the lines of `ReferenceError: alet is not defined`.

Clicking on the filename to the right of the error message will navigate us to the line & column of the file in which the error occurred. Since our `index.html` file is using the generated Webpack bundle to load our JavaScript code, we'll be pointed to the line in the generated Webpack bundle at which the error occurred. From there, it's up to us to work our way backwards from the point of error in the _bundle_ to the point of error in our actual source code.

If that's what's involved in debugging, then I'm not debugging any more. There must be an easier way. We can do better. **_We have the technology._**

### Step 3

_Branch: sourceMaps_

Let's start by looking at the differences between this branch (sourceMaps) and the previous (init):


---
#### Changes to `webpack.config.js`:

We've added a new key to the Webpack configuration object; the `devtool` key. The value associated with this key depends on the value of the _argument_ `env`. 

```javascript
module.exports = env => ({
  devtool: (env && env.production) ? "source-map" : "cheap-eval-source-map",
  entry: "./src/index.js",
  output: { filename: "./build/bundle.js" },
  resolve: { extensions: [".js"] }
});
```

We can rewrite the file in ES5 as:

```javascript
module.exports = function(env) {
  var devtool;

  if (env !== undefined && env.production === true) {
      devtool = "source-map";
  } else {
      devtool = "cheap-eval-source-map";
  };

  return {
    devtool: devtool,
    entry: "./src/index.js",
    output: { filename: "./build/bundle.js" },
    resolve: { extensions: [".js"] }
  };
};
```

First, we declare a variable `devtool`. Then, _if_ the `env` argument isn't `undefined` **and** `env` is an object with a key/value pair `{ production: true }`, then define the value of `devtool` as a string `"source-map"`. Otherwise, define `devtool` as `"cheap-eval-source-map"`. The meaning associated with these values will be explained later; for now, I want to be clear that all we've done is create a variable, and define that variable as a string. The value of that string is dependent on a conditional statement (the _if/else_ block).

Finally, we return an object with a set of key/value pairs that Webpack can use to create our bundle. The `entry`, `output`, and `resolve` key/value pairs have been carried over from the `init` branch.

#### Changes to `package.json`:

We've updated the `scripts` section of the `package.json` file.

**Before:**

```javascript
/*...*/
  "scripts": {
    "build": "webpack"
  },
/*...*/
```

**After:**

```javascript
/*...*/
  "scripts": {
    "dev": "webpack",
    "prod": "webpack --env.production"
  },
/*...*/
```

The name of the command which calls Webpack has been changed from `build` to `dev`. The naming convention implies that this will create a _development_ version of the bundle, and this is true. We're not having Webpack run any sort of optimization when it creates the bundle. Our configuration just says 'take this entry file (`src/index.js`) and every file it imports, bundle them all together, and output that bundle as a file (`./build/bundle.js`).

There is also a new key, `prod`. Again, the naming convention implies that this will create a _production_ version of the bundle. It doesn't. Yet. But it will! Right now, the only difference between the `prod` script and the `dev` script is that we're now passing an argument to the exported function in `webpack.config.js` as the `env` argument, which the function then uses to create and return the Webpack configuration object. To see this in action, you can place a `console.log(env)` statement inside the function exported from `webpack.config.js`.

```javascript
// webpack.config.js
module.exports = env => {
  console.log(env);

  return {
    devtool: env && env.production ? "source-map" : "cheap-eval-source-map",
    entry: "./src/index.js",
    output: { filename: "./build/bundle.js" },
    resolve: { extensions: [".js"] }
  }
};
```

From the command line, run the command `npm run dev`/`yarn dev`.

```bash
> webpack

undefined
Hash: 9d81a1b766e4629aec0c
Version: webpack 2.6.1
Time: 82ms
            Asset     Size  Chunks             Chunk Names
./build/bundle.js  5.75 kB       0  [emitted]  main
   [0] ./src/app/sayHello.js 233 bytes {0} [built]
   [1] ./src/index.js 453 bytes {0} [built]
```

That `undefined` right after `> webpack` is our `console.log(env)` statement. It's undefined because we didn't pass any additional arguments to Webpack in our `dev` command. Now, let's run the `npm run prod`/`yarn prod` command from the command line.

```bash
> webpack --env.production

{ production: true }
Hash: cbc8e27e9f167ab0bc36
Version: webpack 2.6.1
Time: 90ms
                Asset     Size  Chunks             Chunk Names
    ./build/bundle.js  3.79 kB       0  [emitted]  main
./build/bundle.js.map  3.81 kB       0  [emitted]  main
   [0] ./src/app/sayHello.js 233 bytes {0} [built]
   [1] ./src/index.js 453 bytes {0} [built]
```

Instead of seeing `undefined`, we're seeing an object with one key/value pair `{ production: true }`. These values match up with the conditional statement in our Webpack configuration; our conditional statement ensures that the argument `env` isn't undefined, and that it is an object with a key/value pair `{ production: true }`. You might have noticed that the generated bundles from the commands are different as well. The bundle generated with the `dev` command is larger than bundle generated by `prod`, however the `prod` command generated an additional file `bundle.js.map`.

---

Open the file `src/app/sayHello.js`. Since this is a different branch of the Git repository, the error we previously placed in this file might not carry over if the changes were made in the `init` branch. If that's the case, change the first line so that the `alert` call is misspelled as `alet`. Save your changes, then run `npm run dev/yarn dev` from the command line again. Open `index.html` in your browser, then open the browser's devtools. You should have an error in the console stating `alet is not defined`.

If the console claims that this error is being generated in the `index.html` file, refresh the page. You should see something along the lines of:

```bash
ReferenceError: alet is not defined          sayHello.js?7eb0:1
```

Clicking on this error should take you to the line & file in which the error occurred, but you'll notice that the entire line is highlighted as an error. In this case, that's not entirely inaccurate. But let's say we change the `src/app/sayHello.js` file around again. This time, we'll change the reference to `name` inside of the `alert` call to be `namen`:

```javascript
export default name => alert(`Hello ${namen}`);

export const donut = "I WANT YOUR DONUTS";

/**
 * Same code, ES5 style:
 * 
 * function sayName(name){
 *    return alert('Hello ' + name);
 * }
 * 
 * export default sayName;
 * 
 */
```

Run `npm run dev/yarn dev` from the command line again, and refresh the `index.html` file that's open in your browser. The console in your devtools should display a similar error message; `namen is not defined`. Clicking on the error message will, again, take us to the line in which the error occurred.

Now, run `npm run prod`/`yarn prod` from the command line, and refresh the `index.html` file in your browser. Open your devtools and look at the error in your console, the filename is now just `sayHello.js`. Clicking on the error navigates us not only to the file & line in which the error occurred, but also the column in which it occurred. The error underline is more specific as well; it begins at `namen` as opposed to underlining the whole first line.

And that's the difference between the two commands; the accuracy of the source maps they generate. The reason we use a less accurate version of source maps for development purposes is because they are faster to generate than to have Webpack generate full source map files each time we create a build. You can learn about the different options for source mapping with Webpack here: [Webpack Devtool Configuration.](https://webpack.js.org/configuration/devtool/)

### Step 4

_Branch: loader_

Notice that the generated bundles maintain all ES2015 syntax used in the source files; `let` & `const`, arrow functions, newer object literal syntax, etc. If we tried to run our application in an older browser which didn't have support for these features, the application would fail. This is where we'd usually take advantage of a _transpiler_ such as Babel, TypeScript, CoffeeScript, etc. to run through our code and translate it to a version with better cross-browser support. The _loader_ branch covers how to integrate TypeScript into our Webpack build process in order to transpile our application code down to ES3 syntax. Note that we don't introduce any TypeScript-specific features; I even leave the files as .js files. We'll be using TypeScript as an ESNext --> ES3 transpiler.

**Strap in folks; this one's gonna be bumpy.**

#### Dependencies

Looking at the `package.json` file, we've added two new _developer dependencies_.

- TypeScript: As stated earlier, we'll use TypeScript as our transpiler.
- TS-Loader: Loaders allow Webpack to understand _more_ than JavaScript. In this case, TS-Loader allows Webpack to use TypeScript to load TypeScript (and JavaScript) files and transpile them based on your TypeScript configuration before generating a browser-friendly bundle.

To install these dependencies, run `npm install` from the command line. NPM should read the `package.json` file and install the dependencies as listed. In general, to install additional developer dependencies, you can run `npm i -D <package-name>`, where `<package-name>` is the package you want to install, ie: `npm i -D typescript`. The -D flag tells NPM to save the installed package as a developer dependency.

The `prod` command has been updated as well; it now includes the flag `-p`. The `-p` flag is an option that can be provided to the Webpack CLI (command line interface, the tool that NPM calls on when a `script` in the `package.json` file uses `webpack`) which provides optimizations for a production environment. We'll take a deeper look at this shortly.

#### TypeScript Configuration

The `tsconfig.json` file provides information for TypeScript to utilize when transpiling our code.

```javascript
{
  "compilerOptions": {
    "allowJs": true,
    "module": "es2015",
    "target": "es3",
    "sourceMap": true,
    "strict": true
  },
  "include": [
    "./src/"
  ],
  "exclude": [
    "node_modules/",
    "./build/"
  ]
}
```

This configuration object tells TypeScript a few things:

- TypeScript is generally used to transpile TypeScript files (`.ts`) into JavaScript. By setting `allowJs` to `true`, we're allowing TypeScript to transpile .js files.
- TypeScript is capable of transpiling JavaScript to work with a variety of module systems. We're telling TypeScript to use the ES2015 module system because Webpack is able to apply some pretty nifty optimizations when applications are created using this variation.
- We can target most JavaScript versions from ES3 to ESNext. Given that we're aiming for _BETTER_ browser support, not horrendously worse, we go with ES3.
- Generate source maps for each transpiled file.
- Use all the `strict` type-checking features that TypeScript offers.

#### Webpack Configuration Updates

```javascript
module: {
  devtool: env && env.production ? "source-map" : "inline-source-map",
  /* ... */
  rules: [
    {
      test: /\.js(x)?/,
      loader: "ts-loader",
      options: {
        transpileOnly: true,
        entryFileIsJs: true
      }
    }
  ]
}
```

We've introduced a new key to the Webpack configuration object; `module`. The `module` section provides information to Webpack regarding how to work with certain files that are utilized throughout the application. We've provided one rule, which can be read as such:

> When Webpack comes across a file ending in `.js` or `.jsx`, it will first pass the files to TS-Loader before generating a the requested bundle. TS-Loader is responsible only for transpiling the application; we are not using any of the type-checking features provided by TypeScript.

The type of source map used for development environments has been changed from "cheap-eval-source-map" to "inline-source-map". The differences between these two options are covered in the Webpack documentation:  here: [Webpack Devtool Configuration.](https://webpack.js.org/configuration/devtool/)

Run `npm run dev`/`yarn dev` from the command line and open the `index.html` file in your browser. Everything should work as expected. Look at lines 73-105 in the generated bundle:

```javascript
"use strict";
/* unused harmony export donut */
/* harmony default export */ __webpack_exports__["a"] = (function (name) { return alert("Hello " + name); });;
var donut = "I WANT YOUR DONUTS";
/**
 * Same code, ES5 style:
 *
 * function sayName(name){
 *    return alert('Hello ' + name);
 * }
 *
 * export default sayName;
 *
 */


/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__app_sayHello__ = __webpack_require__(0);

// Import whatever the default export is from /app/sayHello
// and refer to it in this file as 'Hello'
var name = "Nitish";
// Reference to the <div id="root"> element in
var root = document.getElementById("root");
// Call the function that was imported from /app/sayHello, passing in
// `const name` that was created on line 5.
__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__app_sayHello__["a" /* default */])(name);
root.textContent = "Helllloooo " + name + "!";
```

All `const` and `let` declarations have been converted to `var`. The template strings used in the alert message and for `root.textContent` have been replaced with string concatenation. Our bundle was created using the _transpiled_ code generated by TypeScript.

If we remember from earlier, `src/app/sayHello.js` exports two items; a function as a _default export_, and a constant `donut` as a _named export_.

```javascript
export default name => alert(`Hello ${name}`);

export const donut = "I WANT YOUR DONUTS";
```

The second export isn't used anywhere in the application, but it's still included in the bundle. However, if we run `npm run prod`/`yarn prod` and take a look at our bundle then...

It's a hot mess! Here's a (nicer, formatted) look at the bundle:

```javascript
!(function(t) {
  function e(r) {
    if (n[r]) return n[r].exports;
    var o = (n[r] = { i: r, l: !1, exports: {} });
    return t[r].call(o.exports, o, o.exports, e), (o.l = !0), o.exports;
  }
  var n = {};
  (e.m = t), (e.c = n), (e.i = function(t) {
    return t;
  }), (e.d = function(t, n, r) {
    e.o(t, n) ||
      Object.defineProperty(t, n, { configurable: !1, enumerable: !0, get: r });
  }), (e.n = function(t) {
    var n = t && t.__esModule
      ? function() {
          return t.default;
        }
      : function() {
          return t;
        };
    return e.d(n, "a", n), n;
  }), (e.o = function(t, e) {
    return Object.prototype.hasOwnProperty.call(t, e);
  }), (e.p = ""), e((e.s = 1));
})([
  function(t, e, n) {
    "use strict";
    e.a = function(t) {
      return alert("Hello " + t);
    };
  },
  function(t, e, n) {
    "use strict";
    Object.defineProperty(e, "__esModule", { value: !0 });
    var r = n(0), o = document.getElementById("root");
    n.i(r.a)("Nitish"), (o.textContent = "Helllloooo Nitish!");
  }
]);
//# sourceMappingURL=bundle.js.map
```

It's still a hot mess! There isn't much of a need to manually parse through this; it's 38 lines of IIFE goodness, so it's doable, but there's no obligation and it won't assist with the rest of this guide. What I'm trying to show here is that the generated _production_ bundle has no reference to the line `const donut = "I WANT YOUR DONUTS!";`. It's completely dropped from the bundle. Along with the minification, uglification, and handful of other out-of-the-box production optimizations Webpack is capable of implementing when provided the `-p` flag, **tree-shaking** is part of that list. I didn't have to do anything to enable tree-shaking; it Just Works™.

Excellent! We're transpiling our ES2015+ code down to ES3, removing any unused code along the way, and generating a production(ish)-quality bundle that can be loaded by most modern browsers with errors and warnings pointing back to our source code for simplified debugging.

### Step 5

_Branch: plugin_

Plugins do exactly what they say on the tin; they plug into the build process to introduce extra functionality. In this example, we'll get introduced to [HTMLWebpackPlugin](https://github.com/jantimon/html-webpack-plugin#configuration), a plugin for generating HTML documents which can serve our Webpack bundles.

As it stands, we created an HTML file which points to the expected bundle. In simple situations, a setup like this would work fine. As the application grows, the bundle could get split into more than one file, The filenames might be randomly generated, etc. If we were to try and manually maintain the list of files that need to be loaded into our HTML file...well, we're kind of back to square A, right? We'll use HTMLWebpackPlugin to automate the process of loading our bundles into our HTML document.

#### File Changes

1. Introduced a new developer dependency to the `package.json` file; `HTMLWebpackPlugin`. Make sure to run `npm install`/`yarn` when you've switched to this branch to get the necessary dependencies.

```javascript
    "devDependencies": {
      "html-webpack-plugin": "^2.28.0",
      "ts-loader": "^2.1.0",
      "typescript": "^2.3.4",
      "webpack": "^2.6.1"
    }
```

2. The `index.html` file no longer loads the `build/bundle.js` file.

3. `webpack.config.js` has been updated to include a CommonJS style import statement (`const HTMLWebpackPlugin = require("html-webpack-plugin");`) at the top of the file, and has a new section, `plugins`:

```javascript
    //webpack.config.js
    const HTMLWebpackPlugin = require("html-webpack-plugin");

    module.exports = env => {
      /*...*/
      plugins: [
        new HTMLWebpackPlugin({
          filename: "./build/index.html",
          inject: "body",
          template: "./index.html"
        })
      ]
    }
```

We're telling Webpack that we'll be using HTMLWebpackPlugin to generate an HTML file named `index.html` inside of the `build` folder. HTMLWebpackPlugin is to take any generated bundles and `inject` them into the body of the HTML file in script tags. It will use the existing `index.html` found in our application root as a template.

If we call on `npm run dev`/`yarn dev` or `npm run prod`/`yard prod`, we should see something similar to:

```bash
$ npm run dev

> webpack -p --env.production

ts-loader: Using typescript@2.3.4 and /Projects/dev_to/webpack_configs/example/tsconfig.json
Hash: 693b4a366ee89bdb9cde
Version: webpack 2.6.1
Time: 2233ms
             Asset       Size  Chunks             Chunk Names
 ./build/bundle.js    8.96 kB       0  [emitted]  main
./build/index.html  352 bytes          [emitted]
```

Based on the configuration provided, Webpack generated the requested bundle along with an `index.html` file. The generated `index.html` file looks very similar to our existing template, but carries a reference to the generated Webpack bundle inside of the document body.

Open the **new** index.html file (`./build/index.html`) in your browser to make sure everything works as expected.

Now stand back, rejoice in your work, and soak it all in. You're on your way to Webpacking the world, amigos.