Ramblings of a degenerate coder

This blog post contains useful information if you're interested in the latest developments on ECMAScript 6 Harmony modules and they current state of their implementation. The original post can be found here.

At the bottom you'll find a list of useful related links.

This document covers a number of use-cases covered by existing module implementations in JavaScript, and how those use-cases will be handled by ES6 modules.

It will also cover some additional use-cases unique to the ES6 module system.

Terminology

For those unfamiliar with the current ES6 module proposal, here is some terminology you should understand:

module: a unit of source code with optional imports and exports.

export: a module can export a value with a name.

imports: a module can import a value exported by another module by its name.

module instance object: an instance of the Module constructor that represents a module. Its property names and values come from the module's exports.

Loader: an object that defines how modules are fetched, translated, and compiled into a module instance object. Each JavaScript environment (the browser, node.js) defines a default Loader that defines the semantics for that environment.

This illustrates the basic syntax of ES6 modules. A module can export named values, and other modules can import those values.

Avoiding Scope Pollution

When working with a module with a large number of exports, you may want to avoid adding each of them as top-level names of another module that wants to import it.

For example, consider an API like Node.js fs module. This module has a large number of exports, like rename, chown, chmod, stat and others. With the ES6 module API, it is possible to bring in the module as a single top-level name that contains all of the module's exports.

Importing From Multiple Non-ES6 Modules

To import from all three of these external module systems together, you would write a resolve hook that would store off the type of module in the context, and then use that information to evaluate the source appropriately in the link hook.

To make this process easier, a JavaScript library like require.js, built for the ES6 loader, could provide conveniences for registering the type of external modules and assimilation code for link.

Import a "Single Export" From a Non-ES6 Module

Some external module systems support modules that have a single export, rather than a number of named exports.

The techniques described above could be used to register that single export under a conventionally known name.

Consider the following "single export" module using node-style modules:

Importing an AMD Module With Asynchronous Dependencies

In the above examples, we assumed that all dependencies in external modules are available synchronously, so we could use System.get in the link hook.

AMD modules can have asynchronous dependencies that can be determined without having to execute the module.

For this use-case, you can return (from link) a list of dependencies and a callback to call once the Loader has loaded the dependencies. The callback will receive the list of dependencies as parameters and must return a Module instance.

Returning the imports and a callback from link allows the link hook to participate in the same two-phase loading process of ES6 modules, but using the AMD definition to separate the phases instead of ES6 syntax.

Importing a Node Module By Processing requires

Because node modules use a dynamic expression for imports, there is no perfectly reliable way to ensure that all dependencies are loaded before evaluating the module.

The approach used by Browserify is to statically analyze the file first for require statements and use them as the dependencies. The AMD CommonJS wrapper uses a similar approach.

The link hook could be used to analyze Node-style packages for require lines, and return them as imports.

By the time the execute callback was called, all modules would be synchronously available, and aliasing require to System.get would continue to work.

This would cause the Loader to attempt to load 'string-utils', before it would call back the provided execute callback.

The Loader would fetch string-utils and evaluate it using the Node-style link hook.

Once this is done, the provided execute callback would run, receiving the string-utils Module as a parameter.

The execute callback would then return a Module.

This is just an illustrative example; any combination of module systems could freely interoperate through the Loader.

A Note on "Single Export" Interoperability

Many of the existing module systems support mechanisms for exporting a single value instead of a number of named values from a module.

At the current time, ES6 modules do not provide explicit support for this feature, but it can be emulated using the Loader. One specific strategy would be to export the single value as a well-known name (for example, exports).

Let's take a look at how a Loader could support a Node-style module using require to import the "single export" of another Node-style module.

This same approach would support interoperability between module systems that support importing and exporting of single values.

We'll need to enhance the previous solution we provided for this scenario:

Here, we create a new unique Symbol that we will use to tag a module as containing a single export. This will avoid conflicts with Node-style modules that export the name exports explicitly.

Next, we will need to enhance the code that we have been using for Node-style require. Until now, we have simply aliased it to System.get. Now, we will check for the isSingle symbol and give it special treatment in that case.

This same approach, using a shared isSingle symbol, could be used to support interoperability between AMD and Node single exports.

As described earlier, ES6 modules would use import { exports: underscore } from 'string-utils/underscore'.

Configuration of Existing Loaders

The requirejs loader has a number of useful configuration options that its users can use to control the loader.

This section covers a sampling of those options and how they map onto the semantics of the ES6 Loader. In general, the compilation pipeline provides hooks that can be used to implement these configuration options.

Base URL

The requirejs loader allows the user to configure a base URL for resolving relative paths.

In the default browser loader, the base URL will default to the page's base URL. The default System.resolve will prefix that base URL and append .js to the end of the module name (if not already present).

The browser's default Loader (window.System) will also include a baseURL configuration option that controls the base URL for its implementation of resolve.

JavaScript code could also configure the Loader's resolve hook to provide any policy they like:

Referencing Modules in HTML

Here, the app is asking Ember.js to render some HTML defined in an App.FancyButton constructor. Note that Ember encourages the use of a global namespace for coordination between JavaScript and HTML templates.

Here, the app is asking Angular.js to replace the <button> with some content defined in a globally registered fancy-button directive.

Both Angular and Ember both use globally registered names to define controller objects to attach to parts of the HTML controlled by the framework.

<!-- ember -->
{{control "fancy-button"}}

Here, the app is asking Ember.js to render some HTML defined in an App.FancyButtonView and use an instance of the App.FancyButtonController as its controller. Again, Ember is relying on a globally rooted namespace for coordination.

Here, the app is asking Angular to use a globally rooted object called TodoCtrl as the controller for this part of the HTML. In Angular, this controller is used to control the scope for data-bound content nested inside of its element.

To handle the kind of situation where a module is referenced by a String and needs to be looked up dynamically, ES6 modules provide an API for looking up a module at runtime.

System.get('controllers/fancy-button');

Systems like Ember or Angular could use this API to allow their users to reference a module's exports in HTML.

In the first Ember example, instead of referencing a globally rooted constructor, the HTML would reference a module name:

Originally posted by wycats as a gist on GitHub, but I felt it deserved to get more public attention. Resources describing ES6 modules are scarce, and rarely this detailed. Head over to the gist for an interesting discussion on the state of ES6 modules.

Comments(1)

erik_isaksen

Fantastick! I've followed all the complaining over JavaScript modules in the past year and I'm actually pretty happy with how they are turning out. It's great to see how they can be used with Browserify as well. Thanks for a great article!