3. Babel setups for browsers and Node.js

This chapter covers example transpilation setups for Babel:

For these setups it helps to be roughly familiar with how Babel is configured. Consult the following two sources if you aren’t:

Two technologies are used by the setups, which is why I’ll explain them first:

  • Local installs via npm
  • Source maps for debugging transpiled code via the original ES6 code

3.1 npm and local installs

npm can help you to install everything you need to manage a project locally. To see how, take a look at the following package.json file inside an npm-managed package:

{
  "dependencies": {
    ···
  },
  "devDependencies": {
    "mocha": "^2.2.5",
    ···
  },
  "bin": {
    "foo": "./bin/foo.js"
  },
  "scripts": {
    "foo": "./bin/foo.js",
    "test": "mocha --ui qunit --compilers"
  }
}

Explanations:

  • dependencies, devDependencies: The packages used by the current package. Installed into node_modules by npm install. The unit test tool mocha is included as a development-only dependency.
  • bin: The entries listed here are available as shell commands (if installed globally) and to scripts of packages that have the current package as a dependency.
  • scripts: Every entry in this object has two parts – the key defines the name of a script, the value defines how the script works. A script whose name is foo can be executed via npm run foo.
    • Additional arguments provided after npm run foo are passed on to the script.
    • The bin executables provided by the dependencies of the package can be used in the definitions of scripts. That’s why the command mocha is available here, even though nothing was installed globally. Alas, the bin entries of the current package.json are not available here.

Several scripts have shortcuts:

  • npm test and npm t is a shorter version of npm run test. The shorter version has the additional benefit of not showing an error message if execution fails (as it does for mocha if a test fails).
  • npm start is (roughly) a shorter version of npm run start.

3.2 Source maps

Source maps help whenever a programming language is compiled to JavaScript. Compiling source code to source code is also called transpiling. Examples of transpilation are:

  • Minification (normal JavaScript to minified JavaScript)
  • CoffeeScript
  • ECMAScript 6 (ES6 to ES5)

A source map is a file that accompanies the transpilation output and maps the lines of the output to lines in the input files. This information can be used for error messages and debugging, to refer to the original instead of the transpiled code. There are two ways to let tools know about a source map: Either the transpilation output refers to the source map file in the last line or it embeds that file’s contents in the last line.

3.3 Browser setup: ES6 via webpack and Babel

The setup described in this section enables client-side ES6 via the following technologies:

  • webpack as a client-side module builder and module loader.
  • npm as a package manager.
  • Babel as a transpiler from ES6 to ES5 (we’ll be using Babel 6).

3.3.1 webpack features

Notable webpack features include:

  • Supported module formats: AMD, CommonJS
    • Via loader (plug-in): ES6
  • Supported package managers: Bower, npm
  • Loaders for non-code: CSS, templates, …
  • On-demand loading (chunked transfer)
  • Built-in development server that supports automatic browser refreshes and hot module reloading (which are both useful during development)
  • Dead code elimination (also known as tree shaking)

3.3.2 Installing the demo project

This section examines a demo project. If you want to, you can install it, like this:

  • Download or clone the GitHub repository webpack-babel-demo.
  • cd webpack-babel-demo/
  • npm install

Everything is installed locally.

3.3.3 The structure of the demo project

The demo project has the following basic structure:

build/
    bundle.js
    bundle.js.map
    index.html
html/
    index.html
js/
    main.js
    world.js
node_modules/
package.json
webpack.config.js

Configuration files and libraries:

  • package.json configures npm so that everything can be installed and run properly. For this project, we only need webpack and Babel.
  • node_modules/ is the directory where npm installs packages.
  • webpack.config.js configures webpack, which, in this case, serves as both transpiler and build tool.

Input directories:

  • js/ is the directory that contains the ES6 code that webpack will transpile to ES5.
  • html/ contains files that will appear in the output unchanged.

Output directory:

  • build/ is the directory where webpack stores its output. You’ll get plain HTML and ES5 code, without any dependencies on webpack, Babel or npm. The contents I’ve listed here are created by a build step:
    • bundle.js is all of the JavaScript, bundled into a single file.
    • bundle.js.map is a source map which enables browsers to run ES5 code, but to debug ES6 code.
    • index.html is simply copied over from html/

3.3.4 File package.json

The file package.json configures npm:

{
  "devDependencies": {
    "babel-loader": "^6.1.0",
    "babel-preset-es2015": "^6.1.18",
    "babel-polyfill": "^6.3.14",
    "webpack": "^1.12.6",
    "webpack-dev-server": "^1.12.1",
    "copy-webpack-plugin": "^0.2.0"
  },
  "scripts": {
    "build": "webpack",
    "watch": "webpack --watch",
    "start": "webpack-dev-server --hot --inline"
  },
  "babel": {
    "presets": ["es2015"]
  }
}

devDependencies are the npm packages that are needed during development:

  • webpack leads to webpack being installed locally.
  • webpack-dev-server adds a hot-reloading development web server to webpack.
  • copy-webpack-plugin is a webpack plugin that copies files to the build directory.
  • babel-loader enables webpack to transpile JavaScript via Babel. This package internally imports a few core Babel packages (babel-core etc.).
  • babel-preset-es2015 is a Babel preset for compiling ES6 to plain ES5.

scripts specifies several ways in which you can run webpack:

  • Build once:
    • npm run build
    • Open build/index.html in a web browser
  • Watch files continuously, rebuild incrementally whenever one of them changes:
    • npm run watch
    • Open build/index.html in a web browser, manually reload page whenever there was a change
  • Hot reloading via the webpack development server:
    • npm start (a shortcut for npm run start)
    • Go to http://localhost:8080/. The page reloads automatically when there are changes.

babel tells Babel to use the preset es2015 (which we have installed via the npm package babel-preset-es2015).

3.3.5 Directory js/

Two small files contain all the JavaScript code in this project.

First, js/main.js

import 'babel-polyfill';
import world from './world';

document.getElementById('output').innerHTML = `Hello ${world}!`;

Importing babel-polyfill in the first line adds whatever is missing from the ES6 standard library to global variables (Object.assign(), the new ES6 string methods, etc.).

Second, js/world.js

export default 'WORLD';

During building, webpack bundles these files into the single output file build/bundle.js and puts a source map for that file into build.js.map.

3.3.6 File html/index.html

The HTML file loads and executes the bundle via a <script> element.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>webpack Babel demo</title>
</head>
<body>

<h1>webpack Babel demo</h1>

<div id="output"></div>

<script src="bundle.js"></script>
</body>
</html>

3.3.7 File webpack.config.js

This is an excerpt of the configuration file for webpack (I’ve omitted the imports and a few other details):

var dir_js = path.resolve(__dirname, 'js');
var dir_html = path.resolve(__dirname, 'html');
var dir_build = path.resolve(__dirname, 'build');

module.exports = {
    entry: path.resolve(dir_js, 'main.js'),
    output: {
        path: dir_build,
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            {
                loader: 'babel-loader',
                test: dir_js,
            }
        ]
    },
    plugins: [
        // Simply copies the files over
        new CopyWebpackPlugin([
            { from: dir_html } // to: output.path
        ]),
    ],
    // Create Sourcemaps for the bundle
    devtool: 'source-map',
    devServer: {
        contentBase: dir_build,
    },
};

The file is a native Node.js module that exports an object with the configuration data. It uses the special Node.js variable __dirname that contains the path of the parent directory of the currently executed module. The configuration data has the following properties:

  • entry: This is where the execution of JavaScript code starts. webpack starts compiling here and continues by compiling its dependencies (imported modules), then the dependencies of the dependencies, etc.
  • output: webpack bundles the entry file and everything it depends on into the output file bundle.js.
  • module.loaders: are preprocessors for imported files. Support for ES6 is enabled via a the module loader babel-loader.
    • Property test specifies what files the loader should transpile. You can specify a single test or multiple tests:
      • Single test: match an absolute path via a regular expression or a string
      • Multiple tests: array of single tests (logical “and”)
  • plugins extend webpack in various ways. I use CopyWebpackPlugin to copy everything in html/ over to build/. That means that webpack performs a task here that is traditionally handled by a build tool such as grunt or gulp.
  • The option devtool switches on and configures source maps.
  • devServer tells the webpack development server what files to serve.

3.3.8 Installing client-side libraries via npm

You can install packages via npm and use them from your client-side ES6 code, seamlessly. For example: First install lodash and save that dependency as a runtime dependency via --save:

npm install --save lodash

Then use it anywhere in your ES6 code:

import { zip } from 'lodash';
console.log(zip(['1', '2'], ['a', 'b']));

3.4 Node.js setup: Dynamically transpiled ES6 via Babel

In this section, we run ES6 on Node.js. We transpile ES6 code dynamically (at runtime), via Babel (version 6). We run unit tests via mocha, which we configure so that it transpiles the tests dynamically, again via Babel.

3.4.1 Installing the demo project

This section describes a demo project, which you can install like this:

Everything is installed only locally.

3.4.2 The structure of the demo project

The project has the following structure:

node_modules/
package.json
src/
    point.js
    require-hook.js
test/
    point_test.js

Explanations:

  • node_modules/ contains the packages installed via npm.
  • package.json configures npm.
  • src/ contains ES6 source code.
  • test/ contains tests for the ES6 source code.

3.4.3 Running ES6 code via babel-node

babel-node is an executable for running code via Babel that otherwise works like the node executable. It is installed via the npm package babel-cli.

The important parts for setting up babel-node in package.json are:

{
  "dependencies": {
    "babel-cli": "^6.2.4",
    "babel-preset-es2015-node5": "^1.1.0"
  },
  "bin": {
    "point": "./src/point.js"
  },
  "scripts": {
    "point": "babel-node ./src/point.js",
    "b": "babel-node"
  },
  "babel": {
    "presets": [
      "es2015-node5"
    ]
  }
}

Explanations:

  • dependencies: We need the npm package babel-cli for the babel-node executable and the babel-preset-es2015-node5 so that Babel can transpile ES6. The preset configures Babel so that only ES6 constructs are transpiled that are missing from Node.js 5 (which supports quite a bit of ES6; e.g., classes and generators).
  • bin: If you install the demo project globally then you’ll get the shell command point.
  • scripts: enables us to run src/point.js via npm run point. Even though we have installed babel-node only locally, we can use it here, because npm adds all executables in the dependencies to the shell path. The script b is explained later.
  • babel: tells Babel to use the aforementioned preset for transpilation.

The file point.js looks as follows:

#!/usr/bin/env babel-node

export class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `new Point(${this.x}, ${this.y})`;
    }
}

if (require.main === module) {
    let pt = new Point(7,4);
    console.log(`My point: ${pt}`);
}

Explanations:

  • The first line lets us run this file as an executable on Unix (it’s not needed if you use npm run to execute it). It starts with a so-called hashbang (#!).
  • Next, class Point is declared and exported.
  • At the end, we check whether point.js is run as a script (and not just imported as a module). If it is, we create an instance of Point and log it to the console.

There are several ways of running point.js:

  • On Unix, you can run the file as follows (if it is executable and has the first line shown in the previous code):
      ./src/point.js
    
  • You can execute point.js via npm run (as configured in scripts):
      npm run point
    
  • If you install babel-node globally (via npm install -g babel-cli) then you can execute point.js like this:
      babel-node src/point.js
    
  • The helper script b lets us run babel-node locally. The local version of the previous invocation is:
      npm run b src/point.js
    
  • If you install the demo project globally, you get a shell command point (as specified via bin).
  • If you install the demo project as a dependency of another package, you can execute point via the scripts of that package, as if it were a globally installed command.

This is what the output of point.js looks like if you execute it via npm run point:

> @ point node-babel-dynamic-demo
> ./src/point.js

My point: new Point(7, 4)

3.4.4 Running ES6 scripts via a require hook

The idea of a require hook is as follows:

  • The first script of an app is a native (untranspiled) module, that you can start via the normal node binary.
  • That script hooks Babel into Node’s require.
  • All modules the script requires are than transpiled from ES6 to ES5.

The inital script looks like this:

#!/usr/bin/env node

// The advantage of a require hook is that
// you can start via normal Node.js

require("babel-register")({
    presets: [
        "es2015-node5"
    ],
});

var point = require('./point');

console.log('Case in point: ' + new point.Point(8, 2));

Again, the first line (starting with #!) lets us run this file as a script on Unix (but it must be executable).

If you use babel-register then that package becomes a runtime dependency!

3.4.5 Standard library and source maps

Whenever you transpile code dynamically on Node.js, Babel automatically includes babel-polyfill (which polyfills what’s missing from the ES6 standard library) and switches on support for source maps.

3.4.6 Getting an ES6 REPL via babel-node

babel-node also gives you a REPL, via the following shell command:

babel-node

In the REPL, you can use ES6:

> [1,2,3].map(x => x * x)
[ 1, 4, 9 ]

3.4.7 Running mocha unit tests via Babel

You can run your unit tests in ES6 via mocha. The relevant bits of package.json are:

{
  "devDependencies": {
    "mocha": "^2.2.5",
    "babel-register": "^6.2.0"
  },
  "scripts": {
    "test": "mocha --ui qunit --compilers js:babel-core/register"
  },
  "babel": {
    "presets": [
      "es2015-node5"
    ]
  }
}

Explanations:

  • devDependencies: mocha is needed for running unit tests, babel-register is how we enable mocha to use Babel.
  • scripts: defines a shortcut for invoking mocha.
    • You can use that shortcut via npm run test or via npm test or via npm t.
    • I prefer a QUnit-style “frame” around the assertions (the mocha docs call this kind of frame an “interface”). This is switched on via --ui qunit.
  • babel: sets up Babel so that it transpiles ES6.

The unit test itself in test/point_test.js looks as follows.

/* global suite */
/* global test */

import assert from 'assert';

import { Point } from '../src/point';

suite('Point');

test('Instance properties', () => { // (A)
    let p = new Point(1, 5);
    assert.strictEqual(p.x, 1);
    assert.strictEqual(p.y, 5);
});
  • As previously mentioned, the mocha “UI” is QUnit (suite() and test(), flat at the top level).
  • The assertions API is the built-in Node.js module assert.
  • The unit tests profit from ES6’s arrow functions (line A).

3.5 Node.js setup: Statically transpiled ES6 via Babel

This section explains how to use ES6 on Node.js by statically transpiling it to ES5 via Babel (version 6).

The previous section showed how to dynamically transpile ES6 at runtime (also via Babel). That is more convenient and should work for many projects, but occasionally you may want a simpler and faster setup for your runtime environment.

3.5.1 Installing the demo project

You don’t need to do so in order to follow this section, but here is how to install the demo project that we are examining in this section:

Everything is installed locally.

3.5.2 Structure of the demo project

The repo has the following structure:

es5/
    myapp.js
    myapp.js.map
es6/
    myapp.js
node_modules/
package.json

Explanations:

  • es6/: contains the source code of the Node.js app, written in ES6.
  • es5/: contains the ES5 that the ES6 code is transpiled to. And a source map so that the ES5 code can be debugged via the ES6 code. The files in this directory don’t exist, initially. They are created by a build step.
  • node_modules/: contains the packages installed via npm.
  • package.json: contains the configuration data for npm.

3.5.3 File package.json

{
  "devDependencies": {
    "babel-cli": "^6.2.0",
    "babel-preset-es2015-node5": "^1.1.1"
  },
  "dependencies": {
    "babel-polyfill": "^6.2.0",
    "source-map-support": "^0.4.0"
  },
  "scripts": {
    "build": "babel es6 --out-dir es5 --source-maps",
    "watch": "babel es6 --out-dir es5 --source-maps --watch",
    "start": "node es5/myapp.js"
  },
  "babel": {
    "presets": [
      "es2015-node5"
    ]
  }
}

Explanations:

  • devDependencies: We need babel-cli to get the executable babel for transpiling on the command line. And we need the preset babel-preset-es2015-node5 so that Babel can transpile full ES6 to the partial ES6 supported by Node.js version 5.
  • dependencies: If you import babel-polyfill, it adds whatever is missing from the ES6 standard library, globally. Package source-map-support provides support for source maps on Node.js.
  • scripts: lets us use npm to build and run the Node.js app:
    • Build once: npm run build
    • Watch files, build whenever there are changes: npm run watch
    • Run the app (as ES5 code, via the normal Node.js binary): npm start (or npm run start)
  • babel: is for configuring Babel.

3.5.4 Transpilation

The file es6/myapp.js contains the ES6 code of the Node.js application:

import { install } from 'source-map-support';
install();

import 'babel-polyfill';

console.log([1,2,3].map(x => x * x));

throw new Error('Test!'); // line 8

Alas, Node.js does not come with built-in support for source maps. But it can be enabled via a library, e.g. the npm package source-map-support. That library needs to be called at least once in an app. The first two lines in the previous code take care of that. They also demonstrate that you can use any npm-installed package via ES6 syntax.

Afterwards, importing babel-polyfill ensures that all of the ES6 standard library is present globally.

The following npm invocation transpiles myapp.js.

npm run build

Alternatively, you can use npm run watch to continuously watch the ES6 files and transpile them whenever they change.

The results of the transpilation are in the directory es5/:

es5/
    myapp.js
    myapp.js.map

You can see the ES5 version of es6/myapp.js and the source map file myapp.js.map. The contents of the former file are:

'use strict';

var _sourceMapSupport = require('source-map-support');

require('babel-polyfill');

(0, _sourceMapSupport.install)();

console.log([1, 2, 3].map(x => x * x));

throw new Error('Test!'); // line 8
//# sourceMappingURL=myapp.js.map

3.5.5 Running the transpiled code

The transpiled code is a normal ES5 Node.js app. You can run it as usual:

node es5/myapp.js

Or via npm:

npm start

The latter invocation produces the following output. Note that, thanks to the source map, the stack trace reports that the exception is thrown where it is in the ES6 file (not where it is thrown in the ES5 file). The source code that is quoted is from the ES6 file, too.

> @ start /tmp/node-babel-static-demo
> node es5/myapp.js

[ 1, 4, 9 ]

/tmp/node-babel-static-demo/es6/myapp.js:8
throw new Error('Test!'); // line 8
      ^
Error: Test!
    at Object.<anonymous> (/tmp/node-babel-static-demo/es6/myapp.js:8:7)