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?

ECMAScript 6 features are continually appearing in engines. You can look up which ones are supported where in Kangax’ “ECMAScript 6 compatibility table”.

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.1.1 Using ECMAScript 6 natively

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.2 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.2.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.2.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.2.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.2.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.2.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.3 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.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 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.5.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.5.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.5.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.