Practical Workflows for ES6 Modules

Guy Bedford
⋅
6 April 2014 (revised 20 July 2014)

ES6 modules are set to become the future replacement for the AMD and CommonJS module formats. They are defined by the current ES6 Specification
and will be implemented in JavaScript engines in the future in both the browser and the server.
This article covers a variety of practical workflows for implementing ES6 modules. These approaches are very new,
and there will still be edge cases, but these principles provide a path forward towards ES6 for use in both new and existing projects today.

Why Use ES6 Modules

Module syntax
The way we write modules in our code with the new import, export and module keywords.

The module loader
The module loading pipeline instructing the JavaScript engine how to handle loading modules. It comprehensively specifies
the entire loading algorithm through a module loader class.

This module loader instance can then be provided as a global variable in the environment, called System in the browser,
which can then be called directly allowing dynamic loads in the browser with System.import('module-name').

The theory behind why we might want to start write our code using ES6 module syntax today is because it means we are writing
our code based on what is a module specification for the future versions of the language.

Many web applications need to dynamically load scripts after the initial page load. In order to do this, we need a dynamic
module loader. We can in fact use the System dynamic loader in browsers today in production using the
ES6 Module Loader polyfill with the SystemJS loader extension.

The reason we would use the System browser loader with a polyfill is exactly the same reason we'd use ES6 module syntax -
to be able to build our applications on top of a spec-compliant module system.

Future ES6 Loader Bundling Scenarios

There has been some complaint about the fact that the ES6 Module Loader provides no native bundling format. But this
becomes clear when understood in terms of the time scales of adoption for the spec.

The way bundling will be enabled is through improvements at the protocol level such as SPDY and HTTP/2 which allow lots of small modules to be
sent with similar performance to sending an entire file bundle.

There are also proposals at the specification level for other bundling options at the protocol level, so this is very much the focus
of the problem.

The workflows shown here give us some bundling workflows that can work in browsers today, but the real solutions for bundling in the future will be
for these approaches not to be necessary at all.

Tooling

These approaches are based on using Google's Traceur project for compilation, the benefit
over the similar ES6 Module Transpiler build methods being that it also allows the use of other ES6 syntax features such
as classes, arrow functions, generators, desctructuring etc.

The workflows below all use Traceur directly. The following tools provide compilation from ES6 to AMD or CommonJS with Traceur for existing build systems:

If we don't use other syntax, including the Traceur runtime isn't necessary. These differences in workflow are described below between these two cases.

Static Workflow 1: Running the app in NodeJS

To run the above in NodeJS, we need to first install Traceur, Google's ES6 to ES5 JavaScript transpiler. This
converts the new ES6 module syntax into something existing JavaScript engines can understand.

To install Traceur, we do:

npm install -g traceur

Now that we have Traceur installed, we can run our application directly from the directory root of our project with:

traceur app/app

You should then see the incredibly rewarding console output, es6!.

Static Workflow 2: Compiling into CommonJS

If we want to publish our project to npm or use Browserify, what we can do is transpile our entire application into CommonJS first, and then provide that to users.

This can be done with the --dir option in Traceur:

traceur --dir app app-build --modules=commonjs

The above tells Traceur to run through each ES6 module in the app directory and individually compile it
into a corresponding CommonJS module in the app-build directory.

We can now run our entire application with NodeJS directly:

node app-build/app

And again we should see the midly tantilising output, es6.

With Additional ES6 Features

If we had used ES6 classes, or another feature, it isn't enough to simple run the app.

Instead we first install Traceur as a local dependency for our project:

npm install traceur --save

Then we create a new entry point, index.js, and load the runtime first:

require('traceur/bin/traceur-runtime');
require('./app-build/app');

Static Workflow 3: Browser Single File Build

So that's the server, now we want to make this remarkable application work in the browser.

Traceur makes it very easy to build for the browser with the out option:

traceur --out app-build.js app/app

So it will read app/app.js, trace all the module dependencies, and then build them into a single file app-build.js.

Then we just load this into the browser:

<!doctype html>
<script src="app-build.js"></script>

And we're done, the text es6 appearing in the browser console.

With Additional ES6 Features

traceur-runtime.js works equivalently in the browser. We copy the file from node_modules/traceur/bin/traceur-runtime.js or from GitHub (ensuring to use the correct version tag) and then include it
with a <script> tag before loading anything else.

The great thing about this workflow is we can now use ES6 code alongside existing AMD code and get flexibile bundling.

With Additional ES6 Features

Just like workflow 3, this can take advantage of ES6 features by including the Traceur runtime script before requiring the modules.

Dynamic Module Loader

In the static workflows we saw how to create builds using Traceur that can run in the browser and NodeJS, but the problem with these workflows in the browser
is that we have no way to load new modules after the initial page load unless we use an AMD loader.

The ES6 Module Specification defines a System loader for the browser (supported in IE8+, and IE9+ if using additional ES6 features), that we can actually polyfill to behave just like the spec using the
ES6 Module Loader Polyfill, coming to 7.4KB minified and gzipped, suitable for production.

With some extension libraries, we can make this loader behave just like an AMD loader including support for loading AMD, CommonJS and global scripts as
well as other features such as map config and plugins.

Dynamic Workflow 1: Loading ES6 Dynamically in the Browser

We begin by downloading the ES6 Module Loader polyfill and Traceur (es6-module-loader.js, traceur.js see the Getting Started section for the links)
and including them in our page.

Say we have a directory of ES6 module files app (our same example has app/app and app/module where app/app imports from ./module),
we can import the ES6 and transpile it in the browser dyanmically with:

The System loader uses ES6 promises to get the module value. Then that is all there is to it. Now the console log statements would match the previous example.

Loading modules separately and transpiling ES6 in the browser is not suitable for production, that is why we have dynamic workflows 2, 3 and 4.

Dynamic Workflow 2: Loading with SystemJS

The 4.6KB (minified and gzipped) SystemJS loader extension library provides compatibility layers to load any module format (ES6, AMD, CommonJS, global scripts) dynamically in the browser.
It also comes with map config, and a plugin system like RequireJS as well as various other features.

To use SystemJS, we include both system.js (see the getting started guide for links) and es6-module-loader.js.

In this example, we'll just load an AMD module:

module.js:

define(function() {
return 'This is AMD';
});

We could equally have written a CommonJS or global module, SystemJS detects the format automatically.

Summary

New ES6 projects can use dynamic ES6 module loaders that load multiple module formats, like the SystemJS loader.

It is possible to upgrade an AMD project to use the SystemJS loader, which will support loading AMD as well as ES6,
and then use that with ES6 modules compiled into AMD for production.

The Traceur instantiate output is a specially designed ES6 compile target that will soon support circular
references. Bundling techniques to work alongside this output are the current focus of active development for new ES6 build workflows.

Feedback

Feedback on these workflows is very welcome. Feel free to leave a comment, or get involved in the issue queues of the appropriate projects.