Setting up ES6
Setting up ES6
Axel Rauschmayer
Buy on Leanpub

1. About this book

ES6 (whose official name is ECMAScript 2015) is the latest version of JavaScript.

Topics covered in this book:

  • Configuring Babel.
  • Setting up ES6 projects that are compiled to ES5 via Babel:
    • Deploying ES6 in browsers via Babel and webpack.
    • Deploying ES6 in Node.js, by statically or dynamically compiling it via Babel.

Versions used in this book:

  • Babel 6
  • webpack 1.x
  • Node.js 5

More information on this book:

  • Required knowledge: You should already know ES6. If you don’t, consult my book “Exploring ES6” which you can read online for free.
  • Feedback, submitting errata: consult the home page of this book.

More information on Babel:

Acknowledgements. Many thanks go to the Babel team (especially Logan Smyth, James Kyle and Sebastian McKenzie) for answering questions I had while writing this book.

2. Deploying ECMAScript 6

This chapter describes the options you have for deploying ECMAScript 6 in current JavaScript environments. It is selective w.r.t. the amount of tools it covers. If you want a comprehensive list of tools, I suggest you look at Addy Osmani’s “ECMAScript 6 Tools”.

2.1 Using ECMAScript 6 today

What options do you have for using ECMAScript 6 today?

  • Current JavaScript engines (browsers, Node.js, …) already support much of ES6. You can look up which features are supported where:
  • ES6 REPLs (interactive command lines) let you try out smaller pieces of code. A section in this chapter lists available options.
  • Compiling from ES6 to ES5: Especially if you take support for legacy engines into consideration, compiling ES6 to ES5 will be the only viable option for using ES6 for quite a while. Compiling from source code to source code is also called transpiling. You can transpile ES6 either before deployment (statically) or at runtime (dynamically). This chapter explains how that works and what other ES6 tools and libraries there are.

The nice thing about ES6 is that it is a superset of ES5, which means that all of your ES5 code bases are already valid ES6. This helps with adopting ES6-specific features, because you can do so incrementally.

2.2 ES6 REPLs

There are many REPLs (command lines) out there for interactively playing with ES6. The obvious choices are the interactive online playgrounds of the following projects:

Additionally, Babel brings ES6 support to the Node.js REPL via its babel-node tool.

2.3 Transpilation tools

There are three essential choices that you have to make for transpilation:

  • A transpiler (for your code)
  • A package manager (to install existing libraries)
  • A module system (for the complete app)

Note that the choices are not completely independent; not every module system works with every package manager etc. The next sections explain each of these choices in more detail.

2.3.1 Choosing a transpiler

A transpiler compiles your ES6 code to ES5. Popular choices are:

  • Microsoft TypeScript: Is basically ECMAScript 6 plus optional type annotations.
  • Google Traceur: is the first popular ES6 transpiler. Pronounced French, /tʁa.sœʁ/; an English approximation is “truh-SIR” (source, listen to native French speakers pronounce this word).
  • Babel: is a newer ES6 transpiler that has become the de-facto standard. Babel supports React’s JSX syntax in addition to ES6. Pronounced “babble” (think Australian accent – Babel’s creator, Sebastian McKenzie is Australian).
  • Closure Compiler: can be used as a static transpiler from, e.g., ECMAScript 6 to ECMAScript 5, if you use the following two command line options:
    • Specify the input language: --language ECMASCRIPT6_STRICT
    • Specify the output language: --language_out ECMASCRIPT5

In principle, transpilation can be done either:

  • statically (before deployment) or
  • dynamically (at runtime)
2.3.1.1 Static transpilation

As a build step, TypeScript, Traceur, Babel and Closure Compiler let you produce ES5 code in the following module formats. You can either invoke them directly or use a build tool (grunt, gulp, broccoli, etc.).

  • AMD
  • CommonJS
  • ES6 module loader API: The ES6 code is transpiled to ES5 code that uses this API via a polyfill.

In browsers, such ES5 modules are loaded via one of the module systems described later. On Node.js, you can use the built-in module system (other options exist, e.g. webpack and the ES6 Module Loader Polyfill).

2.3.1.2 Dynamic transpilation

In browsers, you transpile dynamically via a library plus a custom <script type="...">. This option exists for Traceur and Babel.

For Node.js, Babel has tools for on-the-fly compilation. These are described in another section.

2.3.2 Choosing a package manager

You need a package manager for installing third-party libraries. These are three popular ones:

  • npm: package manager that was originally created for Node.js, but has grown in popularity for client-side development thanks to module packaging and loading tools such as browserify and webpack. Packages contain CommonJS modules and/or other content such as command line tools.
  • Bower: package manager for client-side code. The most popular packages are AMD modules, but CommonJS modules, CSS, HTML and other artifacts can also be managed via it.
  • jspm: package manager for SystemJS (see next bullet list). It can install modules from a variety of sources, including GitHub and npm. One key feature of jspm is that external modules can also be written in ES6 (and will be transpiled), not just your own modules.

2.3.3 Choosing a module system

Module systems bring support for modules to ES5 browsers (Node.js has a built-in module system). That way, you can build your app out of modules – your own and library modules. Popular module systems are:

  • RequireJS: is a loader for AMD modules, which can be statically created via TypeScript, Traceur, Babel and Closure Compiler. Loader plugins (based on Traceur and Babel) enable it to load ES6 modules.
  • Browserify: packages CommonJS modules (including ones installed via npm) so that they can be loaded in browsers. Supports ES6 modules via transforms (plugins) based on Traceur and Babel.
  • webpack: a packager and loader for either CommonJS modules (including ones installed via npm) or AMD modules (including ones installed via Bower). Supports ES6 modules via custom loaders (plugins) based on Traceur and Babel.
  • SystemJS: A module system based on the ES6 Module Loader Polyfill that supports ES6 modules and the ES5 module formats CommonJS, AMD and “ES6 module loader API”.

2.4 Other useful ES6 tools and libraries

2.4.1 Linters and checkers

Linters and checkers analyze source code statically and report problems related to style, typing, etc.

The following linters all support ES6, but to varying degrees:

  • JSLint (focus: enforcing coding practices)
  • JSHint (focus: enforcing coding practices)
  • ESLint (focus: letting people implement their own style rules)
  • JSCS (focus: enforcing code style)

The following type checkers all support ES6:

  • Flow: type-checks code. It receives type information from three sources:
    • Type annotation syntax (non-standard)
    • Type annotations in comments
    • Type inference (which deduces types by statically analyzing source code, making Flow useful even for completely unannotated code)
  • TypeScript: In addition to being a transpiler, the TypeScript compiler also works similarly to Flow and warns about type problems.
  • Closure Compiler: type-checks code and understands type information stored in JSDoc tags.

2.4.2 Test tools

Many test tools (such as Jasmine and mocha) can mostly be used as they are, because they work with the transpiled code and don’t have to understand the original ES6 code. Babel’s documention has information on how to use it with various test tools.

2.4.3 Shims/polyfills

Shims/polyfills enable you to use much of the ECMAScript 6 standard library in ES5 code:

2.4.4 ES6 parsers

These are a few parsers can handle ES6:

  • Esprima
  • Acorn
  • Babel has also recently become more of a framework for statically analyzing and transforming JavaScript source code. The “Babel Plugin Handbook” (by James Kyle) provides information on how that part of Babel works.

2.4.5 Documentation tools

These are a few documentation tools that understand ES6:

2.5 The future: native ES6

As soon as the first engine fully supports ES6 and until all non-ES6 engines go away, a hybrid approach could be used for client-side apps:

  • The server has two versions of each file: the native ES6 version and its transpilation, an ES5 version.
  • When the web app starts, feature detection is used to check whether ES6 is fully supported. If it is, the ES6 version of the app is used. Otherwise, the ES5 version is used.

Detecting ECMAScript versions is difficult, because many engines support parts of versions before they support them completely. For example, this is how you’d check whether an engine supports ECMAScript 6’s for-of loop – but that may well be the only ES6 feature it supports:

function isForOfSupported() {
    try {
        eval("for (var e of ['a']) {}");
        return true;
    } catch (e) {
        // Possibly: check if e instanceof SyntaxError
    }
    return false;
}

Kyle Simpson’s library ES Feature Tests lets you detect whether an engine supports ES6:

var ReflectSupports = require("es-feature-tests");

ReflectSupports("all", function (results) {
    if (results.letConst && results.arrow) {
        // Use ES6 natively
    } else {
        // Use transpiled ES6
    }
});

npm may eventually support two versions of the same module, which would enable you to deliver libraries as both ES5 and ES6 for Node.js and client-side module setups that are based on npm.

2.6 Are there ES6 features that can’t be transpiled to ES5?

Some ECMAScript 6 features cannot be transpiled (compiled) to ES5. ES6 has three kinds of features:

  • Better syntax for existing features
  • New functionality in the standard library
  • Completely new features

The next sections explain for each kind of feature how difficult it is to transpile to ES5.

2.6.1 Better syntax for existing features

ES6 provides better syntax for features that are already available via libraries. Two examples:

  • Classes
  • Modules

Both can be relatively easily compiled to ES5.

2.6.2 New functionality in the standard library

ES6 has a more comprehensive standard library than ES5. Additions include:

  • New methods for strings, arrays
  • Promises
  • Maps, Sets

These can be provided via a library. Much of that functionality (such as String.prototype.repeat()) is even useful for ES5. A later section lists a few such libraries.

2.6.3 Completely new features

Some ES6 features are completely new and unrelated to existing features. Such features can never be transpiled completely faithfully. But some of them have reasonable simulations, for example:

  • let and const: are transpiled to var plus renaming of identifiers to avoid name clashes where necessary. That produces fast code and should work well in practice, but you don’t get the immutable bindings that const creates in native ES6.
  • Symbols: an be transpiled to objects with unique IDs. These objects can be used as property keys, because the bracket operator coerces them to strings. Additionally, some property-enumerating functions (such as Object.keys()) have to be patched to ignore property keys coming from transpiled symbols.
  • Generators: are compiled to state machines, which is a complex transformation, but works remarkably well. For example, this generator function:
      function* gen() {
          for(let i=0; i < 3; i++) {
              yield i;
          }
      }
    

    is translated to the following ES5 code:

      var marked0$0 = [gen].map(regeneratorRuntime.mark);
      function gen() {
          var i;
          return regeneratorRuntime.wrap(function gen$(context$1$0) {
              while (1) switch (context$1$0.prev = context$1$0.next) {
                  case 0:
                      i = 0;
    
                  case 1:
                      if (!(i < 3)) {
                          context$1$0.next = 7;
                          break;
                      }
    
                      context$1$0.next = 4;
                      return i;
    
                  case 4:
                      i++;
                      context$1$0.next = 1;
                      break;
    
                  case 7:
                  case "end":
                      return context$1$0.stop();
              }
          }, marked0$0[0], this);
      }
    

    You can see the state machine in the code, the next state is stored in context$1$0.next.

    Check out Facebook’s regenerator library for more information.

  • WeakMaps: The entries of a WeakMap are key-value pairs. Keys are always objects. The main feature of WeakMaps is that keys are weakly held: if an object is a key and there is no other (strong) reference to it then it can be garbage-collected. An ES5 simulation of a WeakMap is an object that contains a unique ID (a string such as 'weakmap_072c-4328-af75'), but is otherwise empty. Each of its key-value entries is managed by storing the value in the key, via a property whose name is the WeakMap ID. That is quite a hack: It only works if the keys are mutable. And, if a simulated WeakMap is garbage-collected, its properties in the keys are not removed, resulting in wasted memory. On the plus side, keys are held weakly by simulated WeakMaps. The simulation works because all of the WeakMap operations (get, set, has, delete) require a key. There is no way to clear WeakMaps or to enumerate their entries, keys or values.

Other features are impossible to transpile (in a straightforward manner):

  • Proxies: intercepting proxy operations is only possible if you make all operations on objects interceptable. And that would cause a tremendous performance penalty.
  • Subclassable built-in constructors (e.g. Error and Array). This feature is enabled by a new subclassing protocol (new.target etc.) that ES5 built-ins do not support. Replacing them with implementations that do is not a simple solution, either, because there are many places that use built-in constructors, but don’t refer to them via global variables.
  • Tail call optimization: implementing tail call optimization universally in ES5 would require a radical transformation of the ES6 code (e.g. trampolining). The resulting code would be quite slow.

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)

4. Configuring Babel 6

Babel 6 is much more configurable than Babel 5, but also more difficult to configure. This chapter gives tips.

4.1 Installing Babel 6

The following are a few important npm packages. All Babel packages reside in a single repository on GitHub. Browsing their source code and their package.json files is instructive.

  • babel-core: the core compilation machinery and plugin infrastructure for Babel. You will rarely need to install this package, because other packages such as babel-cli have it as a dependency, meaning that it will be automatically installed when they are installed.
  • babel-cli: a command line interface to Babel. It includes the following commands:
    • babel-doctor detects common problems with your Babel installation.
    • babel transpiles files or stdin via Babel.
    • babel-node a version of the Node.js executable node that transpiles everything via Babel.
    • babel-external-helpers prints all of Babel’s helper functions (such as inherits for subclassing) to the console.
  • babel-register: lets you switch on Babel transpilation from within Node.js. After you do, all modules you require (minus code you want to ignore, e.g. packages installed via npm) are automatically transpiled.

4.2 Configuration data

Babel is often about compiling an input file, e.g. in the following two scenarios:

  • Compiling a file via the command line tool babel:
      babel input-es6.js --out-file output-es5.js
    
  • Running a Node.js script written in ES6:
      babel-node input-es6.js
    

The configuration data is an object of JSON data that is assembled from various sources (which are described later). Two configuration options have much influence on how the output is produced: plugins and presets. These are explained next. The remaining configuration options are explained in the Babel documentation.

4.2.1 Plugins

Roughly, plugins are functions that are applied to the input during compilation. Two important categories of plugins are:

If you want to compile something that isn’t part of the base syntax, you need both a syntax plugin and a corresponding transform plugin. However, each transform plugin that depends on a syntax plugin automatically activates that plugin.

Plugins are installed via npm. Their package names are their names plus the prefix babel-plugin-:

  • Plugin syntax-jsx: npm install babel-plugin-syntax-jsx
  • Plugin transform-react-jsx: npm install babel-plugin-transform-react-jsx

4.2.2 Presets

In order to configure Babel’s output to your liking, you need to specify what plugins it should use. You can specify:

  • Individual plugins
  • Presets, sets of plugins that support various compilation scenarios.

The following are useful presets:

  • es2015: compiles ES6 (as described by the ECMAScript spec) to ES5
  • stage-3: compiles stage 3 ECMAScript proposals to ES5
  • react: compiles JSX to JavaScript and removes Flow type annotations
  • es2015-node5: Contains just those plugins that are needed to upgrade Node.js 5 to full ES6. Therefore, a lot less is transpiled than with the es2015 preset. Especially generators not being transpiled helps with debugging.

Presets are installed via npm. Their package names are their names plus the prefix babel-preset-. For example, this is how to install the preset es2015:

npm install babel-preset-es2015

4.3 Sources of configuration data

This section explains where configuration data can come from.

4.3.1 Primary source of configuration data: .babelrc

The main source of configuration data is a file .babelrc that should be located close to the input file that you want to compile. Babel first looks for .babelrc in the same directory as the input file, then in that directory’s parent directory, etc. The contents of .babelrc are interpreted as JSON and used as Babel options. For example:

{
  "presets": [
    "es2015"
  ]
}

The file .babelignore complements .babelrc:

  • It is searched for independently of .babelrc, but in the same manner (by going through the ancestor directories of the input file).
  • It does not replace .babelrc; the two sets of data are merged.

.babelignore only specifies the option ignore: its lines are turned into an Array and interpreted as that option.

4.3.2 Alternative to .babelrc: package.json

Each npm package has a file package.json. Babel allows you to use that file for configuration, as an alternative to .babelrc. Then it must have a property babel (otherwise Babel ignores package.json files). The value of that property is an object with configuration data. For example:

{
  "dependencies": {
    ···
  },
  "babel": {
    "presets": [
      "es2015"
    ]
  }
}

Babel searches for package.json in the same way that it searches for .babelrc, by going through the ancestor directories of the input file. If there is both a .babelrc and a package.json in the same directory then the former file wins.

4.3.3 Mixing in additional configuration data

Two properties in configuration data specify additional configuration data:

  • Property env of maps the names of environments ('development', 'production', etc.) to objects with more configuration data. If env exists, the object corresponding to the current environment is merged with the configuration data that has already been assembled. Consult the Babel documentation for more information on environments.
  • Property extends contains a path pointing to a file with more configuration data.

4.3.4 Secondary sources of configuration data

4.3.4.1 babel-node

If you are using babel-node, you can specify the following options (and a few others) via the command line:

  • --presets
  • --plugins
  • --ignore: by default, any file that has the segment 'node_modules' in its path is not transpiled.

The following command runs my-script.js via babel-node, with the presets es2015-node5 and react, and the plugin transform-async-to-generator enabled.

babel-node \
--presets es2015-node5,react \
--plugins transform-async-to-generator \
my-script.js
4.3.4.2 webpack

The following is an excerpt of a webpack configuration file webpack.config.js:

var path = require('path');
···
module.exports = {
    ···
    module: {
        loaders: [
            {
                loader: 'babel-loader',
                test: path.resolve(__dirname, 'js'),
                query: {
                    presets: ['es2015'],
                },
            }
        ]
    },
    ···
};

As you can see, babel-loader supports the property query for specifying Babel options.

4.4 More information

5. Babel: configuring standard library and helpers

This chapter explains how to configure how Babel 6 accesses its own helper functions and the ES6 standard library.

The following GitHub repo lets you play with what’s explained here: babel-config-demo

5.1 Overview

The code produced by Babel usually has two kinds of external dependencies:

  • Helper functions (e.g. for subclassing)
  • Standard library functionality (e.g. Map or ES6 string methods)

There are two ways of fulfilling these dependencies: by installing functionality globally or via modules. In both cases the functionality is delivered as npm packages.

5.1.1 External dependencies via global variables

The following npm packages install their functionality globally and let you access it via global variables:

  • (P) babel-plugin-external-helpers and (I) generated file: global helpers
  • (I) babel-polyfill: global standard library
    • ES5, ES6+, Regenerator runtime
  • (I) core-js: global standard library
    • core-js/shim: ES5, ES6+
    • core-js/es6: ES6

Installation:

  • (P) Plugin: install the npm package as a dev dependency and switch on the plugin in the Babel configuration data (see Chap. “Configuring Babel 6”).
  • (I) Import: install the npm package as a runtime dependency and import it when the program starts.

5.1.2 External dependencies via module imports

The following npm packages enable dependencies via modules:

  • (P) babel-plugin-transform-runtime, (M) babel-runtime: helpers and standard library via imports (plugin generates imports).
    • Babel helpers (mandatory)
    • Standard library (can be switched off)
    • Regenerator API (can be switched off)
  • (M) core-js: standard library via modules.
    • Single entities:
        import _repeat from 'core-js/library/fn/string/repeat';
      
    • Namespace object (ES5, ES6+):
        import * as core from 'core-js/library';
        const myStr = core.String.repeat('*', 10);
      
    • Namespace object (ES6):
        import * as core from 'core-js/library/es6';
        const myStr = core.String.repeat('*', 10);
      

Installation:

  • (P) Plugin: install the npm package as a dev dependency and switch on the plugin in the Babel configuration data (see Chap. “Configuring Babel 6”).
  • (M) Module: install the npm package as a runtime dependency and import entities at runtime (as needed).

5.2 External dependencies of transpiled code

The code generated by Babel usually has two kinds of external dependencies that need to be fulfilled.

First, most code invokes functionality of the ES6 standard library. By default, you access this functionality via global variables:

let m = new Map();
if (str.startsWith('/')) ···

Second, Babel has helper functions (e.g. for subclassing) that are called from the transpiled code. By default, the code of helper functions is inlined, inserted into each file. For example (_classCallCheck is a helper):

//---------- Input: ES6 code
class Person {}

//---------- Output: ES5 code that uses helpers
"use strict";

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Person = function Person() {
    _classCallCheck(this, Person);
};

Inlined helpers lead to code duplication whenever the same helper is used in several files.

There are two ways in which you can get the standard library and non-inlined helpers: via global variables and via module imports. How is explained in the next sections.

5.3 External dependencies via global variables

5.3.1 Helpers via a global variable: babel-plugin-external-helpers

There are two things you need to install:

  • Plugin babel-plugin-external-helpers:
    • Install as development dependency:
        npm install --save-dev babel-plugin-external-helpers
      
    • Switch on in Babel configuration data:
        "plugins": ["external-helpers"]
      
  • File that sets up global variable babelHelpers:
    • Must be loaded or imported at runtime, as early as possible. E.g.:
        import 'babelHelpers';
      
    • Generated via command line tool babel-external-helpers (how is explained in the next section).
    • Install babel-external-helpers as a development dependency:
        npm install --save-dev babel-cli
      

The plugin ensures that all helpers are invoked via methods of the global object babelHelpers. In this section, I’ll explain how that works. In the next section, I’ll explain how to set up babelHelpers.

As an example, consider the following ES6 code, before transpilation:

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

If you transpile it with the es2015 preset and without external-helpers, you get:

"use strict";

var _createClass = (function () {
    ···
})();

function _classCallCheck(instance, Constructor) {
    ···
}

var Point = (function () {
    function Point(x, y) {
        _classCallCheck(this, Point);

        this.x = x;
        this.y = y;
    }

    _createClass(Point, [{
        key: "toString",
        value: function toString() {
            return "(" + this.x + ", " + this.y + ")";
        }
    }]);

    return Point;
})();

Note the two helper functions _createClass and _classCallCheck.

If you switch on the plugin external-helpers, you get this output:

"use strict";

var Point = (function () {
    function Point(x, y) {
        babelHelpers.classCallCheck(this, Point);

        this.x = x;
        this.y = y;
    }

    babelHelpers.createClass(Point, [{
        key: "toString",
        value: function toString() {
            return "(" + this.x + ", " + this.y + ")";
        }
    }]);
    return Point;
})();
5.3.1.1 Installing the global Babel helpers

How do you install the object with the helpers into the global variable babelHelpers? Via a file generated by the command line tool babel-external-helpers. The tool is part of the npm package babel-cli. The file is available in three formats:

  • babel-external-helpers -t global
    prints a Node.js module that puts the helpers into global.babelHelpers.
  • babel-external-helpers -t var
    prints a script file (browser code) that var-declares babelHelpers in global scope and assigns it an object with the helpers.
  • babel-external-helpers -t umd
    prints a Universal Module Definition (UMD) that works as CommonJS module, AMD module and as a script (via a global variable).

This invocation prints usage information:

babel-external-helpers --help

5.3.2 Standard library and more via global variables: babel-polyfill

The package babel-polyfill contains a module that installs several things into global variables:

The polyfills are provided by core-js, which you can see if you look at the two import statements making up this module:

import "core-js/shim";
import "babel-regenerator-runtime";
5.3.2.1 Installation

Install babel-polyfill via npm as a runtime dependency if you find that any of the aforementioned functionality is missing in your transpiled code. In current Node.js versions, you may be able to get by without using it, because those versions come with much of the ES6 standard library and native generators.

The module is installed via:

npm install --save babel-polyfill

You must import it before you use the standard library:

import 'babel-polyfill';

The module will check global variables and only install missing functionality. The downside of the polyfill is that you always deliver and load all of the functionality, independently of how much you use.

5.3.3 Just the standard library via global variables: core-js

If you don’t need the Regenerator runtime, you can use the core-js polyfill directly.

You install it like this:

npm install --save

There are several ways in which you can install the polyfill functionality. Two common ones are:

  • import 'core-js/shim';
    Polyfills ES5, ES6 and some post-ES6 functionality.
  • import 'core-js/es6';
    Polyfills just ES6 functionality.

The import statement needs to happen before you access the runtime library. It installs the polyfills globally.

5.4 External dependencies via module imports

5.4.1 Helpers and standard library via imports: babel-plugin-transform-runtime

Two pieces are needed for this approach:

  • A module that contains helpers and standard library. Install as a runtime dependency:
      npm install --save babel-runtime
    
  • A plugin that changes Babel output so that helpers and standard library are imported from babel-runtime.
    • Install as a development dependency:
        npm install --save-dev babel-plugin-transform-runtime
      
    • Switch on in Babel configuration data:
        "plugins": ["transform-runtime"]
      

The plugin redirects three kinds of operations to imports from babel-runtime:

  1. Babel helpers (mandatory)
  2. Standard library (can be switched off)
  3. Regenerator API (can be switched off): used by babel-plugin-transform-regenerator to transpile ES6 generators to ES5 code.

babel-runtime becomes a runtime dependency, but the import statements are created by the plugin – you do not need to import anything.

The plugin lets you switch off #2 and #3 as follows:

"plugins": [
    ["transform-runtime",
        { "polyfill": false, "regenerator": false }],    
]

The next two sections explore how transform-runtime works for helpers and for the standard library.

5.4.2 transform-runtime and Babel helpers

transform-runtime works well for the helpers. Consider the following ES6 code:

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

With the plugin, this is transpiled to:

"use strict";

var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck"); // (A)

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

var _createClass2 = require("babel-runtime/helpers/createClass"); // (B)

var _createClass3 = _interopRequireDefault(_createClass2);

function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : { default: obj };
}

var Point = (function () {
    function Point(x, y) {
        (0, _classCallCheck3.default)(this, Point);

        this.x = x;
        this.y = y;
    }

    (0, _createClass3.default)(Point, [{
        key: "toString",
        value: function toString() {
            return "(" + this.x + ", " + this.y + ")";
        }
    }]);
    return Point;
})();

The helpers classCallCheck (line A) and createClass (line B) are now imported from babel-runtime. Note that each function sits in its own module, which ensures that only functions you actually use end up in bundled code.

The helper function _interopRequireDefault ensures that either plain CommonJS modules or transpiled ES6 modules can be used.

5.4.3 transform-runtime and the standard library

The plugin transform-runtime handles helpers automatically, but for much of the standard library, extra work is required. Let’s examine the support for the following ways of accessing the standard library:

  • Accessing functions (supported)
  • Accessing methods normally (not supported)
  • Accessing methods via custom utility functions (supported)
5.4.3.1 Accessing functions (supported)

transform-runtime does properly detect function invocations: namespaced functions (such as Object.assign and Math.sign) and constructors (such as Map and Promise). Take, for example, the following ES6 code:

let m = new Map();
Math.sign(-1);

This code is transpiled to:

"use strict";

var _sign = require("babel-runtime/core-js/math/sign"); // (A)

var _sign2 = _interopRequireDefault(_sign);

var _map = require("babel-runtime/core-js/map"); // (B)

var _map2 = _interopRequireDefault(_map);

function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : { default: obj };
}

var m = new _map2.default();
(0, _sign2.default)(-1);

Note the imports in line A and line B.

5.4.3.2 Accessing methods normally (not supported)

However, transform-runtime does not detect method calls like those in the following ES6 code:

console.log('a'.repeat(3));
console.log(String.prototype.repeat.call('b', 3));

This is transpiled to:

'use strict';

console.log('a'.repeat(3));
console.log(String.prototype.repeat.call('b', 3));

There are no imports – the input code in basically untouched. The first method call is dynamically dispatched, so it’s not surprising that transform-runtime doesn’t catch it. However, the second method call is direct and ignored, too.

5.4.3.3 Accessing methods via custom utility functions (supported)

transform-runtime provides a work-around for method calls – utility functions attached to constructors. You can use them to replace method calls such as this one:

'c'.repeat(3);

With an invocation of a utility function:

String.repeat('c', 3);

Transpiled, the code looks like this (note the import in line A):

'use strict';

var _repeat = require('babel-runtime/core-js/string/repeat'); // (A)

var _repeat2 = _interopRequireDefault(_repeat);

function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : { default: obj };
}

(0, _repeat2.default)('c', 3);
5.4.3.4 Babel’s polyfilling is based on core-js

If you look back, all imports generated by transform-runtime use module IDs that start with babel-runtime/core-js. That’s because Babel’s polyfilling is based on the library core-js. You can look up how transform-runtime maps identifiers and chained property accesses to core-js modules in the repository file definitions.js. This is an excerpt:

module.exports = {
  builtins: {
    ···
    Set: "set",
    ···
  },

  methods: {
    Array: {
      ···
      from: "array/from",
      ···
    },
    ···
    String: {
      ···
      repeat: "string/repeat",
      ···
    },
    ···
  }
};

That means that transform-runtime provides Set, Array.from(), String.repeat() and more.

5.4.4 core-js: standard library via modules

Given that transform-runtime requires you to access properties of built-in constructors if you want to make a method call, using core-js directly is a useful alternative to that plugin.

Install it as a runtime dependency:

npm install --save core-js

One way of accessing standard library functionality non-globally is via a single library object:

import * as core from 'core-js/library';

const mySet = new core.Set([1, 2, 3, 2, 1]);
const myArr = core.Array.from(mySet);
const myStr = core.String.repeat('*', 10);

As for the content of the library object, you have a choice between:

  • core-js/library: polyfills ES5, ES6 and a few proposed features.
  • core-js/library/es6: polyfills just ES6.

Another way of accessing standard library functionality non-globally is to import entities individually:

import _Set from 'core-js/library/fn/set';
import _from from 'core-js/library/fn/array/from';
import _repeat from 'core-js/library/fn/string/repeat';

const mySet = new _Set([1, 2, 3, 2, 1]);
const myArr = _from(mySet);
const myStr = _repeat('*', 10);

5.5 What should I use when?

For libraries, you must not touch global variables, which is why everything must come from imports:

  • Babel helpers: babel-plugin-transform-runtime and babel-runtime work well.
  • Regenerator runtime: The previous combo also provides the regenerator runtime via imports. If you don’t use or transpile generators then you can switch off this part of babel-plugin-transform-runtime.
  • Standard library: I don’t like that babel-plugin-transform-runtime enables ES6 methods (e.g. Array.prototype.repeat()) via non-standard global methods (e.g. Array.repeat()). It’d switch this part of the plugin off and use core-js directly.

For apps, you can use a mixed approach:

  • Use Babel helpers and – if necessary – the Regenerator runtime via module imports, as explained for libraries.
  • Install the ES6 standard library globally. Modules have the advantage that a bundler will only deploy the functionality that is actually used by the app. But a global polyfill lets you write code that will eventually run natively without any changes. I find that more important.

    Given that Regenerator is already taken care of, you don’t need babel-polyfill; core-js is enough.

Non-redundant Babel helpers are always just an optimization, but especially for large code bases, it is worth it because of space savings.

Acknowledgements. Thanks to Denis Pushkarev (@zloirock) and Paul Klimashkin for their feedback on this content.

6. Babel’s loose mode

Babel’s loose mode transpiles ES6 code to ES5 code that is less faithful to ES6 semantics. This chapter explains how that works and what the pros and cons are (spoiler: normally not recommended).

6.1 Two modes

Many plugins in Babel have two modes:

  • A normal mode follows the semantics of ECMAScript 6 as closely as possible.
  • A loose mode produces simpler ES5 code.

Normally, it is recommended to not use loose mode. The pros and cons are:

  • Pros: The generated code is potentially faster and more compatible with older engines. It also tends to be cleaner, more “ES5-style”.
  • Con: You risk getting problems later on, when you switch from transpiled ES6 to native ES6. That is rarely a risk worth taking.

6.1.1 Switching on loose mode

The preset es2015-loose is the loose version of the standard ES6 preset, es2015. The preset’s code provides a good overview of what plugins have a loose mode and how to switch it on. This is an excerpt:

module.exports = {
  plugins: [
    ···
    [require("babel-plugin-transform-es2015-classes"), {loose: true}],
    require("babel-plugin-transform-es2015-object-super"),
    ···
  ]
};

This is a CommonJS module where you can use all of ECMAScript 5. If you configure Babel via .babelrc or package.json (details), you need to use JSON. You can either include the whole preset:

···
"presets": [
  ···
  "es2015-loose",
  ···
],
···

Or you can include plugins individually:

···
"plugins": [
  ···
  ["transform-es2015-classes", {loose: true}],
  "transform-es2015-object-super",
  ···
],
···

6.2 Example: the output of normal mode and loose mode

Let’s see how the modes affect the transpilation of the following code.

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

6.2.1 Normal mode

In normal mode, the prototype methods of the class are added via Object.defineProperty (line A), to ensure that they are non-enumerable, as required by the ES6 spec.

"use strict";

var _createClass = (function () {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor); // (A)
        }
    }
    return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
})();

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Point = (function () {
    function Point(x, y) {
        _classCallCheck(this, Point);

        this.x = x;
        this.y = y;
    }

    _createClass(Point, [{
        key: "toString",
        value: function toString() {
            return "(" + this.x + ", " + this.y + ")";
        }
    }]);

    return Point;
})();

6.2.2 Loose mode

In loose mode, normal assignment is used to add methods (line A). This style is more like you’d hand-write code in ES5.

"use strict";

function _classCallCheck(instance, Constructor) { ··· }

var Point = (function () {
    function Point(x, y) {
        _classCallCheck(this, Point);

        this.x = x;
        this.y = y;
    }

    Point.prototype.toString = function toString() { // (A)
        return "(" + this.x + ", " + this.y + ")";
    };

    return Point;
})();

7. Babel and CommonJS modules

This chapter examines how Babel ensures that code it transpiles interoperates properly with normal CommonJS modules. Consult chapter “Modules” in “Exploring ES6” for more information on ES6 modules.

7.1 ES6 modules vs. CommonJS modules

7.1.1 ECMAScript 6 modules

Default export (single export):

// lib.js
export default function () {}

// main.js
import lib from './lib';

Named exports (multiple exports):

// lib.js
export function foo() {}
export function bar() {}

// main1.js
import * as lib from './lib';
lib.foo();
lib.bar();
// main2.js
import {foo, bar} from './lib';
foo();
bar();

It is possible to combine both styles of exports, they don’t conflict with each other.

7.1.2 CommonJS modules

Single export:

// lib.js
module.exports = function () {};

// main.js
var lib = require('./lib');

Multiple exports:

// lib.js
exports.foo = function () {};
exports.bar = function () {};

// main1.js
var lib = require('./lib');
lib.foo();
lib.bar();
// main2.js
var foo = require('./lib').foo;
var bar = require('./lib').bar;
foo();
bar();

Single exports and multiple exports are mutually exclusive. You have to use either one the two styles. Some modules combine both styles as follows:

function defaultExport() {}
defaultExport.foo = function () {};
defaultExport.bar = function () {};

module.exports = defaultExport;

7.1.3 Comparing the two modules formats

ES6 modules have two advantages over CommonJS modules.

First, their rigid structure makes them statically analyzable. That enables, e.g., tree shaking (dead code elimination) which can significantly reduce the size of bundled modules.

Second, imports are never accessed directly, which means that cyclic dependencies are always supported. In CommonJS, you must code like this, so that the exported entity foo can be filled in later:

var lib = require('./lib');
lib.foo();

In contrast, this style of importing does not work (neither do single exports via module.exports):

var foo = require('./lib').foo;
foo();

More information on cyclic dependencies: Section “Support for cyclic dependencies” in “Exploring ES6”.

7.2 How Babel compiles ES6 modules to CommonJS

As an example, consider the following ES6 module.

export function foo() {};
export default 123;

Babel transpiles this to the following CommonJS code:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.foo = foo;
function foo() {};
exports.default = 123;

The following subsections answer questions you may have about this code:

  • Why isn’t the default export done like a CommonJS single export?
  • Why mark transpiled ES6 modules with the flag __esModule?

7.2.1 Why isn’t the default export done like a CommonJS single export?

Answer: There are three reasons for doing so.

First, it is closer to ES6 semantics.

Second, you prevent scenarios like the following.

// lib.js
export default {
    foo: () => {},
    bar: () => {},
};
// main.js
import {foo,bar} from './lib';

This is illegal in native ES6 and Babel shouldn’t let you do that.

Third, you want to support doing a default export and named exports at the same time. You could treat a module with just a default export like a single-export CommonJS module:

// lib_es6.js
export default 'abc';
// main_cjs.js
var lib = require('./lib_es6');
    // 'abc'

However, then the exports would change completely if you add a named export:

// lib_es6.js
export default 'abc';
export var foo = 123;
// main_cjs.js
var lib = require('./lib_es6');
    // { foo: 123, default: 'abc' }

7.2.2 Why mark transpiled ES6 modules with the flag __esModule?

The flag enables Babel to treat non-ES6 CommonJS modules that have single exports as if they were ES6 modules with default exports. How that is done is examined in the next section.

7.3 How Babel imports CommonJS modules

7.3.1 Default imports

This ES6 code:

import assert from 'assert';
assert(true);

is compiled to this ES5 code:

'use strict';

function _interopRequireDefault(obj) {
    return obj && obj.__esModule
        ? obj
        : { 'default': obj };
}

var _assert = require('assert');
var _assert2 = _interopRequireDefault(_assert);

(0, _assert2['default'])(true); // (A)

Explanations:

  • _interopRequireDefault(): An ES6 CommonJS module is used as is (if it has a default export then it has a property named default). A normal CommonJS module becomes the value of the property default. In other words, in the later case, the module’s exports become the default export.
  • Note that the default export is always accessed via the exports object _assert2 (line A), never directly, like this:
      var assert = _assert2.default;
    

    The reason for that is support for cyclic dependencies.

  • (0, _assert2['default']) is done so that the invocation in line A is a function call, not a method call (with this === _assert2).

7.3.2 Namespace imports

This ES6 code:

import * as assert from 'assert';
assert.ok(true);

is compiled to this ES5 code:

'use strict';

function _interopRequireWildcard(obj) {
    if (obj && obj.__esModule) {
        return obj;
    }
    else {
        var newObj = {}; // (A)
        if (obj != null) {
            for (var key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key))
                    newObj[key] = obj[key];
            }
        }
        newObj.default = obj;
        return newObj;
    }
}

var _assert = require('assert');
var assert = _interopRequireWildcard(_assert);

assert.ok(true);

Explanations:

  • _interopRequireWildcard(): CommonJS exports are translated to an object where the named exports are the properties of the exports objects and the default exports is (yet again) the exports object. The module assert is an example of where a normal CommonJS module mixes a single export with multiple exports and the Babel work-around translates such a module to the world of ES6:
      import assert, {ok} from `assert`;
    

    assert accesses a default export, {ok} accesses a named export.

  • Babel creates a new object (line A), because it must not modify the original exports object.

7.3.3 Named imports

This ES6 code:

import {ok} from 'assert';

ok();

is compiled to this ES5 code:

'use strict';

var _assert = require('assert');

(0, _assert.ok)();

Again, you can see that ok() is never accessed directly, always via _assert, which ensures that cyclic dependencies work.

7.4 Recommendations

You need to look very closely at what a module exports and then choose the appropriate way of importing. For example, conceptually, the Node.js module fs is clearly a collection of named exports, not a single export (an object). Therefore, while both of the following two ways of importing this module work, the second one is the better choice.

import fs from 'fs'; // no
import * as fs from 'fs'; // recommended

If you want to future-proof your normal CommonJS module, you should opt for either a single export or multiple named exports, but not for mixing styles (attaching named exports as properties of a single export).

8. The future of bundling JavaScript modules

This chapter examines how the bundling of modules is affected by two future developments: HTTP/2 and native modules.

8.1 Why we bundle modules

Bundling modules means combining several files with modules into a single file. That is done for three reasons:

  1. Fewer files need to be retrieved in order to load all modules.
  2. Compressing the bundled file is slightly more efficient than compressing separate files.
  3. During bundling, unused exports can be removed, potentially resulting in significant space savings.

8.2 JavaScript modules

With ECMAScript 6, JavaScript finally got built-in modules (I’m calling them JavaScript modules for the remainder of this chapter). However, that feature is currently in a strange position:

On one hand, ES6 fully standardized their syntax and much of their semantics. They have become a popular format for writing modules and their static structure enables the automatic omission of unused exports (also known as “tree-shaking” in the JavaScript world).

On the other hand, standardizing how to load JavaScript modules is ongoing and no JavaScript engine supports them natively, yet. That means that, at the moment, the only way of using JavaScript modules is by compiling them to a non-native format. Popular solutions are: browserify, webpack, jspm and Rollup.

8.3 Future developments and bundling

Let’s look at two future developments and how they affect the bundling of JavaScript modules.

8.3.1 Future development: HTTP/2

HTTP/2 is slowly being rolled out. It mainly affects reason #1 for bundling: With HTTP/2, the cost per request has decreased considerably compared to HTTP/1, which means that there are practically no performance gains if you download a single file instead of multiple ones. That enables smaller, more incremental updates: With bundling, you always need to download the complete bundle. Without bundling, you only need to download the parts that have changed (while the other parts are often still in the browser cache).

However, reasons #2 and #3 for bundling are not negated by HTTP/2. Therefore, mixed approaches may be adopted in the future, to optimize for both incremental updates and minimal total download size.

8.3.2 Future development: native JavaScript modules

Once engines support native JavaScript modules, will that affect bundling? Even AMD modules – which run natively in browsers – have a custom bundle format (along with a minimal loader). Will native JS modules be different? It looks like they will. Rollup lets you bundle multiple JS modules into a single JS module.

Take, for example, these two JS modules:

// lib.js
export function foo() {}
export function bar() {}

// main.js
import {foo} from './lib.js';
console.log(foo());

Rollup can bundle these two JS modules into the following single JS module (note the eliminated unused export bar):

function foo() {}

console.log(foo());

Initially, it wasn’t a given that JavaScript modules would work as a bundle format – quoting Rollup’s creator Rich Harris:

When I started writing Rollup, it was an experiment that I wasn’t certain would succeed.

The way imports are handled by JS modules helps with bundling: they are not copies of exports, they are read-only views on them.

Rollup’s site has a nice interactive playground where you can try it out.

8.4 Further reading

  • Building for HTTP/2” by Rebecca Murphey (explains how best practices change – often radically – with this new version of HTTP)
  • Chap. “Modules” in “Exploring ES6” (explains how ES6 modules work)
  • Chap. “Babel and CommonJS modules” in this book (explains how Babel ensures that transpiled ES6 modules interoperate properly with CommonJS modules)