---
title: "Modern JavaScript: Webpack and Babel"
date: 2018-09-29
description: How to set up and configure Webpack and Babel for writing cross-compatible modern ES2015+ JavaScript code.
tags: [javascript]
---

## Introduction

This post will explain how to set-up and configure the various tooling necessary in order to be able to write cross-compatible modern (ES2015+) JavaScript code.

> [!TIP]
> if you're unsure of what 'modern' JavaScript looks like, then I'll refer you to [these compatibility tables](http://kangax.github.io/compat-table/es6/).

The tools we'll be using:

- [Babel](https://babeljs.io/): transpiler of modern JS into ES5 compatible code.
- [Webpack](https://webpack.github.io/): a js module bundler.

> [!INFO]
> webpack is actually capable of transforming, bundling, packaging just about anything (as we'll see shortly).

To clarify, you don't _need_ 'webpack', as babel handles transpiling modern JS code into ES5 compatible code, but it makes sense to use webpack still as a way to help with the performance of your client-side services.

With that in mind the configuration I'll be describing will be using webpack primarily, and under that I'll be using webpack 'loaders' to utilise the babel transpiler.

If you weren't using webpack, the configuration would be different as you'd be configuring babel directly instead of webpack.

## Example Project

We're going to create a very basic project. It's so basic, it doesn't really do anything. It's the bare minimum required in order to demonstrate the setup and configuration of webpack and babel (I purposely did this because learning new tech can be confusing enough without needing to understand a real-world application at the same time).

One thing you'll need upfront though, is [Node](https://nodejs.org/) and [NPM](https://www.npmjs.com/) installed, as we'll be installing webpack and babel from existing NPM packages.

Let's begin by creating our project directory:

```
mkdir modern-js && cd modern-js
```

> [!INFO]
> I will be working exclusively from the terminal.

Now create an empty `package.json` file:

```
npm init -y
```

Next, we'll install all the relevant packages we'll be needing...

```
npm install --save-dev webpack \
                       webpack-cli \
                       webpack-dev-server \
                       @babel/core \
                       @babel/preset-env \
                       "babel-loader@^8.0.0-beta" \
                       style-loader \
                       css-loader \
                       sass-loader \
                       node-sass \
                       eslint@4.x babel-eslint@8

npm install --save @babel/polyfill
```

> [!WARNING]
> the dev dependencies need to be installed in the order they're specified above, otherwise npm will complain/fail.

Your `package.json` should now have the following content:

```
{
  "name": "modern-js",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.1.2",
    "@babel/preset-env": "^7.1.0",
    "babel-eslint": "^8.2.6",
    "babel-loader": "^8.0.4",
    "css-loader": "^1.0.0",
    "eslint": "^4.19.1",
    "node-sass": "^4.9.3",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.0",
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.9"
  },
  "dependencies": {
    "@babel/polyfill": "^7.0.0"
  }
}
```

There are two important Babel related packages to pay attention to...

1. `@babel/polyfill`
1. `@babel/present-env`

The [@babel/polyfill](https://babeljs.io/docs/en/babel-polyfill) dependency will help emulate a full ES2015+ environment, and this means you can use new built-ins like `Promise` or `WeakMap`, static methods like `Array.from` or `Object.assign`, instance methods like `Array.prototype.includes` (amongst others, see the documentation for more information).

The [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env) helps manage the browser environment for you, so it'll handle determining what additional scripts/polyfills you need. By default it'll setup everything to support ES5 environments, but you can configure it for very specific browsers if you don't need to worry about older browsers.

> [!INFO]
> `@babel/polyfill` also provides a `useBuiltIns` flag which allows Babel to selectively polyfill built-in features that were introduced as part of ES6+. Because it filters polyfills to include only the ones required by the environment, we mitigate the cost of shipping with babel-polyfill in its entirety.

Now let's create all the files we need to build out our example application:

```
mkdir src dist
touch .eslintrc src/index.js src/component.js src/styles.scss dist/index.html
```

This should result in the following tree structure...

```
tree -I node_modules

.
├── .eslintrc
├── dist
│   └── index.html
├── src
│   ├── component.js
│   ├── index.js
│   └── styles.scss
```

We can see we have one [Sass](https://sass-lang.com/) file and two JavaScript files, as well as a single HTML page that will load our scripts/css.

Let's now look at the contents of each of these files...

### .eslintrc

```
{
  "parser": "babel-eslint",
  "globals": {
    "console": true,
    "document": true,
    "window": true
  },
  "rules": {
    'brace-style': [2, '1tbs', {'allowSingleLine': true}],
    'camelcase': 2,
    'comma-spacing': 2,
    'comma-style': 2,
    'curly': 2,
    'eol-last': 2,
    'indent': [2, 2],
    'key-spacing': 2,
    'new-cap': 2,
    'new-parens': 2,
    'no-lonely-if': 2,
    'no-multi-spaces': 2,
    'no-multiple-empty-lines': [2, {'max': 2}],
    'func-call-spacing': 2,
    'no-trailing-spaces': 2,
    'quotes': [2, 'single', {'allowTemplateLiterals': true}],
    'semi': 2,
    'semi-spacing': 2,
    'space-before-blocks': 2,
    'space-in-parens': 2,
    'space-infix-ops': 2,
    'space-unary-ops': 2,
    'array-callback-return': 2,
    'block-scoped-var': 2,
    'consistent-return': 2,
    'eqeqeq': 2,
    'guard-for-in': 2,
    'no-array-constructor': 2,
    'no-caller': 2,
    'no-cond-assign': 2,
    'no-const-assign': 2,
    'no-control-regex': 2,
    'no-delete-var': 2,
    'no-dupe-args': 2,
    'no-dupe-class-members': 2,
    'no-dupe-keys': 2,
    'no-duplicate-case': 2,
    'no-empty-character-class': 2,
    'no-empty-pattern': 2,
    'no-eval': 2,
    'no-ex-assign': 2,
    'no-extend-native': 2,
    'no-extra-bind': 2,
    'no-fallthrough': 2,
    'no-func-assign': 2,
    'no-implied-eval': 2,
    'no-invalid-regexp': 2,
    'no-iterator': 2,
    'no-lone-blocks': 2,
    'no-loop-func': 2,
    'no-mixed-operators': [2, {
      groups: [
        ['&', '|', '^', '~', '<<', '>>', '>>>'],
        ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
        ['&&', '||'],
        ['in', 'instanceof']
      ],
      allowSamePrecedence: false
    }],
    'no-multi-str': 2,
    'no-native-reassign': 2,
    'no-unneeded-ternary': 2,
    'no-unsafe-negation': 2,
    'no-new-func': 2,
    'no-new-object': 2,
    'no-new-symbol': 2,
    'no-new-wrappers': 2,
    'no-obj-calls': 2,
    'no-octal': 2,
    'no-octal-escape': 2,
    'no-redeclare': 2,
    'no-regex-spaces': 2,
    'no-script-url': 2,
    'no-self-assign': 2,
    'no-self-compare': 2,
    'no-sequences': 2,
    'no-shadow-restricted-names': 2,
    'no-shadow': 2,
    'no-sparse-arrays': 2,
    'no-template-curly-in-string': 2,
    'no-this-before-super': 2,
    'no-throw-literal': 2,
    'no-undef': 2,
    'no-unexpected-multiline': 2,
    'no-unreachable': 2,
    'no-unused-expressions': [2, {
      'allowShortCircuit': true,
      'allowTernary': true
    }],
    'no-unused-vars': 2,
    'no-use-before-define': [2, 'nofunc'],
    'no-useless-computed-key': 2,
    'no-useless-concat': 2,
    'no-useless-constructor': 2,
    'no-useless-escape': 2,
    'no-useless-rename': 2,
    'no-with': 2,
    'radix': 2,
    'require-yield': 2,
    'use-isnan': 2,
    'valid-typeof': 2,
    'wrap-iife': [2, 'any']
  }
}
```

You don't have to worry too much about the contents of this file, as it's just the configuration I'm using to define what is 'good' JavaScript syntax.

In other words, if your code editor is configured properly, then any JS code you write that violates any of these ES linter values, will be flagged as an error.

### dist/index.html

```
<!doctype html>
<html>
  <head>
    <title>Hello Webpack</title>
  </head>
  <body>
    <script src="bundle.js"></script>
  </body>
</html>
```

This is simple enough, a HTML page that loads a `bundle.js` script (which currently doesn't exist, and will be generated by webpack, _via_ babel).

### src/component.js

```
const c = ['x', 'y', 'z'];

export default c;
```

A simple script that defines an Array of items and then exports them (using modern JS module syntax, familiar to people who may have written Node applications before).

### src/index.js

```
/*eslint no-undef: "error"*/
/*eslint-env browser*/

import '@babel/polyfill';
import './styles.scss';
import c from './component.js';

console.log(c);

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a);
console.log(b);

const root = document.createElement('div');
root.innerHTML = `<p>Hello Webpack!</p>`;
document.body.appendChild(root);
```

A simple script that imports from our `component.js` and then logs it (to prove the import code works), it then demonstrates `let` variables and another modern JS feature known as '[destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)' before finally creating a HTML `<div>` element and populating it with some text and inserting it into the DOM of the HTML page.

At the top of the script you'll notice some code comments. These are used to tell our ES linter package what context this script is running in, and so global references such as `document` or `console` won't trigger a linting error as we've told the linter that the context the script will run is the browser environment, where those globals are expected to exist. The code comment for `eslint-env` could be replaced by adding individual references into the `.eslintrc` file (and I have done that, take a look at the `globals` field in the file contents shown earlier), but I prefer the code comment as it can be a much clearer indicator of the expectations of the file's scope.

You'll also notice that we import the `@babel/polyfill` at the top of the file. This module must _always_ be the first import in the file.

Lastly, you'll notice we also import a Sass file, which admittedly is a bit strange considering we're dealing with a JS file, but we'll dig into this a little bit more later on and why we do that.

### src/styles.scss

```
$color: blue;

body {
  background-color: $color;
}
```

This is a simple Sass file that demonstrates how to use a 'variable' to generate dynamic CSS output (in this case, the body element should have a blue background).

## Webpack Configuration

OK, at this point we have a set of JS files that can't be used in some browsers due to the fact that they use features not supported by most web browsers. So we want to compile this code down into something that _is_ understandable to most browsers (i.e. ES5 standardized code).

To do that we'll be using Babel (as our transpiler) and Webpack as our module bundler. Webpack will take the separate JS files and combine them into a single `bundle.js` file, which our HTML will attempt to load.

So let's create a `webpack.config.js` file:

```
touch webpack.config.js
```

We'll then add the following contents to that file:

```
/*eslint-env node*/

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
        }
      },
      {
        test: /\.scss$/,
        use: [
          {loader: 'style-loader'},
          {loader: 'css-loader'},
          {loader: 'sass-loader'}
        ]
      }
    ]
  }
};
```

You'll notice a similar code comment at the top of the file, which again is for the benefit of our ES linter. In this case it won't complain when it sees certain global references such as `require` or `module` (as we've told the linter that the context the script will be run in is the Node environment, where those globals are expected to exist).

We see the `entry` field of the configuration is telling webpack that the main application JavaScript file is going to be found at `./src/index.js`. Webpack will look at this file and then traverse back up its dependency tree looking for imports and it will combine each of those separate files into one single file.

The file that is generated (and where) is determined by the `output` field. We can see we want the file to be called `bundle.js` and we want the file to be saved to the `./dist` directory (this is indicated by the `path` field).

The `publicPath` field is a bit different, in that it tells the `webpack-dev-server` package where to find the `bundle.js` file that the HTML is attempting to load. What won't be clear yet is the fact that when running our code locally (in dev) we'll be using `webpack-dev-server` because it allows us to utilise a web server for running our code as well as 'hot reloading' (which means, if we're dealing with a complex single-page application with lots of nested state, that a change in code doesn't cause us to lose the state the page is in).

For production we'll statically generate our final bundle file using the standard `webpack` command, and so you'll see shortly that we need to update our `package.json` to include two `npm run ...` commands that let us use either `webpack-dev-server` or `webpack` depending on where our code needs to run.

The `module` field tells webpack, that for every file it finds, before adding it to the final `bundle.js`, to run it through a 'loader' script for additional processing. In this case, all `.js` files are passed through babel and so their modern JS code is transpiled into ES5 code first before being added to `bundle.js`.

Finally, we look for any Sass `.scss` files and tell webpack to pass those files through multiple 'loaders':

- **sass-loader**: transforms Sass into CSS.
- **css-loader**: parses the CSS into JavaScript and resolves any dependencies.
- **style-loader**: outputs our CSS into a `<style>` tag in the document.

Loaders are executed in a nested fashion, meaning the above order of loaders would evaluate to something like:

```
styleLoader(cssLoader(sassLoader("source")))
```

Where the source file is passed into the Sass loader, and so transforming the Sass into CSS. That CSS is then passed into the CSS loader, which allows it to be parsed by JavaScript. Finally, the Style loader places our CSS into a `<style>` tag within our HTML page.

## Package.json Update

As mentioned earlier, we want to modify the `package.json` so that we have two `npm run ...` commands for letting us run our code in development mode (i.e. locally) or compile our code ready for use in production.

The following snippet shows the changes needed to be made:

```
{
  ...

  "scripts": {
    "build": "webpack --mode=production",
    "dev": "webpack-dev-server --mode=development --config webpack.config.js",

    ...
  },
  
  ...
}
```

Now we can run either `npm run dev` (for local dev) or `npm run build` (to compile our bundle for production).

You'll notice that we pass a specific `--mode` flag to both `webpack` and `webpack-dev-server`, and this indicates to both tools what to do to the files it's configured to interact with.

In the case of `--mode=production` the `webpack` tool will make additional modifications that means the `bundle.js` output is as efficient as possible (such as minifiying and obfuscating the code).

Whereas `--mode=development` will allow for the generation of webpack source map files (to aid you in debugging).

## Conclusion

This should be the basics covered of how to use babel with webpack, and hopefully is enough to help you kickstart your exploration of new JavaScript features.
