Appendix B: Module Loading

This chapter is outdated and basically kept for historical reasons.

B.1. Dependency Management

There exist several types of dependencies between modules, distinguishable by the time when the dependency is relevant. We first define these dependencies lazily to give an impression of the problem, at more rigorously later on.

Dependency needed at compile time. These type of dependency is removed by the compiler. These are basically type references used in variable or function declarations.

Runtime dependencies are to be handled at runtime in general. We distinguish two special types of runtime dependencies:

A loadtime dependency is a special runtime dependency that needs to be resolved before a module is initialized, that is, when all top-level statements of a module, containing class declarations, are executed. This usually is a types super type (e.g., super class), or a call to a function (defined in a different module) in a static initializer or module top level statement.

An execution time dependency is a non-initialization runtime dependency. That is, when a method is called (from another module), this is execution time.

Of course, before a module can be loaded, it needs to be fetched (i.e., the actual code has to be retrieved by the browser).

We can define sets containing modules which a given module depends on. Note that these sets contain each other, as shown in Euler Dependencies. However, we define disjoint sets in which a dependency to another type is only contained in one of the sets.

euler dependencies
Figure 65. Euler diagram of dependency sets

Given a code sequence , we define the set of accessed modules in it as .

describes all function calls happening in code block , i.e. . In case  calls on functions , we define a function’s body code sequence as .

The complete set of accessed modules for a particular code sequence is then defined as ]

We explicitly allow a function to be excluded from being incorporated in the above algorithm by annotating it.

The set of load-time-dependencies for a module with initializer code is then defined as math:[\[\begin{aligned} load-time-deps := AccessdModules( SuperClass(M) ) + AccessdModules( IC(M) ) \end{aligned}\]]

B.2. ECMAScript Modules

B.2.1. ES5 Modules Systems

Before ES6, Javascript had no built in support for modules. To overcome this hurdle, the two widely accepted formats have been :

  1. CommonJS : Primarily aimed at synchronous module loading. The main implementation of this format is seen in Node.js

    var value = 100;
    function inc() {
        value++;
    }
    
    module.exports = {
        value : value,
        inc : inc
    }

    Import via require.

  2. AMD : Primarily aimed at asynchronous module loading in browsers. The main implementation of this format is seen in RequireJS.

    define('myModule', ['mod1', 'mod2'], function (mod1, mod2) {
            return {
                myFunc: function(x, y) {
                    ..
                }
            };
        };
    });

    passive format

B.2.2. ES6 Modules

The ES6 spec introduces modules. ES6 modules resemble CommonJS syntax with AMD like asynchronous loading support.

Apart from the syntactic details, the highlights of ES6 modules are :

  1. ES6 modules support (multiple) named exports.

    export const VERSION = "1.0.1";
    export function inc(x) {
        return x + 1;
    }
  2. ES6 modules support default exports.

    export default function (x) {
        return x+1;
    }
  3. As specified here, ES6 modules export live immutable bindings (instead of values).

    This behaviour is different from that of CommonJS and AMD modules where a snapshot of the value is exported.

    An example demonstrating the behavioural difference :

    //-------------src.js------------
    var value = 100;
    function inc() {
        value++;
    }
    
    module.exports = {
        value : value,
        inc : inc
    }
    //-------------main.js------------
    var src = require("./src"); //import src
    
    console.log(src.value); //prints 100
    src.inc();
    
    console.log(src.value); //prints 100 <--- The value does not update.
    
    src.value = 65;
    console.log(src.value); //prints 65 <--- The imported value is mutable.

    The same example with ES6 modules :

    //-------------src.js------------
    export var value = 100; // <--- ES6 syntax
    export function inc() { // <--- ES6 syntax
        value++;
    }
    
    //-------------main.js------------
    import {value, inc} from "src" // <--- ES6 syntax
    
    console.log(value); //prints 100
    inc();
    
    console.log(value); //prints 101 <--- The value is a live binding.
    
    value = 65; // <--- throws an Error implying the binding is immutable.
  4. ES6 modules impose a static module structure i.e. the imports and exports can be determined at compile time (statically).

B.3. ECMAScript Module Loaders

For resolving module dependencies and loading modules, the JS landscape provides a few different module loaders.

  1. RequireJS is the loader of choice for in browser, AMD style modules. We currently transpile our code into an AMD-style format to allow it running in both Browser and Node.js environments.

  2. Node.js provides a native loader implementation for CommonJS style modules.

  3. For browsers (primarily), tools like Webpack and Browserify exist. These tools analyse the dependency graph of the entire project and then bundle up all the dependencies in a single file. Browserify only supports CommonJS modules where as Webpack works with both CommonJS & AMD style modules.

  4. At the time of writing this document (August 2015), there does not exist any native implementation for ES6 modules by any Javascript host environments i.e. ES6 modules are not natively supported by browsers or Node.js, as of now.

[fig:moduelLoader] shows an overview.

moduleLoader
Figure 66. Module Loader and Transpilers, Overview

B.3.1. ES6 Module Loaders

The ES6 spec started out with ES6 Module Loader details as part of the spec. However the Working Group later decided to not proceed with it. The specification for ES6 Module Loader is now a separate specification [WhatWGLoader].

The aim of this specification is:

This specification describes the behavior of loading JavaScript modules from a JavaScript host environment. It also provides APIs for intercepting the module loading process and customizing loading behavior.

The Implementation status of the spec states :

It is too early to know about the Loader, first we need ES2015 modules implemented by the various engines.

B.3.2. Polyfills for ES6 Module Loaders

Although there is no native support for ES6 module loading, there are a few attempts to polyfill this gap.

B.3.2.1. es6-module-loader

The es6-module-loader project provides a polyfill for the ES6 Module Loader implementation. It dynamically loads ES6 modules in browsers and Node.js with support for loading existing and custom module formats through loader hooks.

B.3.2.2. SystemJS

Building upon es6-module-loader, SystemJS supports loading ES6 modules along with AMD, CommonJS and global scripts in the browser and Node.js.

In order to use ES6 modules (written in ES6 syntax) the code first needs to be transpiled to ES5. For this purpose, SystemJS provides an option to use Traceur compiler or Babel.
B.3.2.3. Demo

A demonstration of how to how to use ES6 modules with Babel and SystemJS in Node.js as of today (August 2015).

  1. Create an ES6 module as shown:

    export var value = 100; // <--- named export of a variable
    export function inc() { // <--- named export of a function
        value++;
    }
  2. Import the bindings from the module as shown:

    import {value, inc} from "src"
    
    var importedValue = value; // <--- using the imported value
    inc(); // <--- using the imported function
  3. Transpile these two files using Babel to ES5 with the target module format as system, as shown:

    $ babel <inputdir> --out-dir <outputdir> --modules system
  4. The transpiled output should be resemble the following:

    System.register([], function (_export) {
        "use strict";
    
        var value;
    
        _export("inc", inc);
    
        function inc() {
            _export("value", value += 1);
        }
    
        return {
            setters: [],
            execute: function () {
                value = 100;
    
                _export("value", value);
            }
        };
    });
    System.register(["src"], function (_export) {
      "use strict";
    
      var value, inc, importedValue;
      return {
        setters: [function (_src) {
          value = _src.value;
          inc = _src.inc;
        }],
        execute: function () {
          importedValue = value;
          inc();
        }
      };
    });
  5. Finally run the above transpiled files, as shown:

    var System = require('systemjs'); // <--- Require SystemJS
    System.transpiler = 'babel'; // <--- Configure SystemJS
    
    System.import('main'); // <--- Import the transpiled "main" module.

B.4. Case Study : TypeScript

This section is NOT an exhaustive introduction to Microsoft’s TypeScript, but a narrowed down analysis of certain aspects of TypeScript.

B.4.1. ES6 Modules Support

TypeScript language has recently added support for ES6 modules. From the wiki :

TypeScript 1.5 supports ECMAScript 6 (ES6) modules. ES6 modules are effectively TypeScript external modules with a new syntax: ES6 modules are separately loaded source files that possibly import other modules and provide a number of externally accessible exports. ES6 modules feature several new export and import declarations.

B.4.2. TypeScript and Module Loading

TypeScript does not concern itself with providing a module loader. It is the responsibility of the host environment. However TypeScript’s compiler provides options to transpile the modules to different formats like AMD, CommonJS, ES6 etc. It is the developer’s responsibility to choose an appropriate format and then use the modules with a correct module loader.

From the wiki again :

TypeScript supports down-level compilation of external modules using the new ES6 syntax. When compiling with -t ES3 or -t ES5 a module format must be chosen using -m CommonJS or -m AMD. When compiling with -t ES6 the module format is implicitly assumed to be ECMAScript 6 and the compiler simply emits the original code with type annotations removed. When compiling down-level for CommonJS or AMD, named exports are emitted as properties on the loader supplied exports instance. This includes default exports which are emitted as assignments to exports.default.

Consider the following module src.ts :

export var value = 100; //<--- ES6 syntax

export function inc() {  //<--- ES6 syntax
    value++;
}

Compiling it to SystemJS format produces :

System.register([], function(exports_1) {
    var value;
    function inc() {
        (exports_1("value", ++value) - 1);
    }
    exports_1("inc", inc);
    return {
        setters:[],
        execute: function() {
            exports_1("value", value = 100); //<--- ES6 syntax
        }
    }
});

Compiling it to CommonJS format produces :

exports.value = 100; //<--- ES6 syntax
function inc() {
    exports.value++;
}
exports.inc = inc;

Compiling it to AMD format produces :

define(["require", "exports"], function (require, exports) {
    exports.value = 100; //<--- ES6 syntax
    function inc() {
        exports.value++;
    }
    exports.inc = inc;
});

Compiling it to UMD format produces :

(function (deps, factory) {
    if (typeof module === 'object' && typeof module.exports === 'object') {
        var v = factory(require, exports); if (v !== undefined) module.exports = v;
    }
    else if (typeof define === 'function' && define.amd) {
        define(deps, factory);
    }
})(["require", "exports"], function (require, exports) {
    exports.value = 100; //<--- ES6 syntax
    function inc() {
        exports.value++;
    }
    exports.inc = inc;
});

NOTE :

  1. Visual Studio 2015 does not support ES6 modules at this time.

  2. SystemJS supports TypeScript as a compiler. This implies TypeScript modules can be transpiled to be used with SystemJS as the module loader.

B.5. Cyclic Dependencies

To better analyse and evaluate SystemJS module loader and different module formats, let’s look at a cyclic dependency example from a (extremely simplified) stdlib task FixedPoint6. The outline for the example is :

  1. Prepare 2 ES6 modules with a circular dependency.

  2. Then transpile these modules to different module formats (e.g. AMD, & SystemJS).

  3. With SystemJS as the module loader, execute the test for every transpiled module format.

B.5.1. Setup

Consider the following ES6 listings:

  1. RoundingMode

    export default {
        FLOOR : "FLOOR",
        CEILING : "CEILING"
    }
  2. MathContext

    import { default as FixedPoint6 } from "FixedPoint6";
    import { default as RoundingMode } from "RoundingMode";
    
    let MathContext = class {
        constructor(mode) {
            this.mode = mode;
        }
    
        divide(fp1, fp2) {
            var quotient = FixedPoint6.getQuotient(fp1, fp2);
    
            if(this.mode === RoundingMode.CEILING) {
                return new FixedPoint6(Math.ceil(quotient));
            } else if(this.mode === RoundingMode.FLOOR) {
                return new FixedPoint6(Math.floor(quotient));
            } else {
                throw new Error("Incorrect RoundingMode");
            }
        }
    }
    
    MathContext.FLOOR = new MathContext(RoundingMode.FLOOR);
    MathContext.CEILING = new MathContext(RoundingMode.CEILING);
    
    export default MathContext;
  3. FixedPoint6

    import { default as MathContext } from "MathContext";
    
    export default class FixedPoint6 {
        constructor(number) {
            this.value = number;
        }
    
        static getQuotient(fp1, fp2) {
            return fp1.value/fp2.value;
        }
    
        divide(fp) {
            return FixedPoint6.defaultContext.divide(this, fp);
        }
    }
    
    FixedPoint6.defaultContext = MathContext.FLOOR;
  4. Test

    import { default as FixedPoint6 } from "FixedPoint6";
    import { default as MathContext } from "MathContext";
    import { default as RoundingMode } from 'RoundingMode';
    
    var fp1 = new FixedPoint6(20.5);
    var fp2 = new FixedPoint6(10);
    
    var fp3 = fp1.divide(fp2);
    console.log(fp1, fp2, fp3);
  5. Runner : This is the runner file to execute the test (after transpilation).

    var System = require('systemjs');
    System.transpiler = 'babel';
    
    System.config({
        baseURL: './build',
        "paths": {
            "*": "*.js"
        }
    });
    
    System.import('test').catch(function(e) {
        console.log(e);
    })

Clearly MathContext & FixedPoint6 have a circular dependency upon each other.

B.5.2. Transpile and Execute

Transpile the above setup to different formats and execute the code using SystemJS module loader :

B.5.2.1. Module Format = AMD
  1. Compile the previous set up using babel as the transpiler with AMD modules :

    babel src -w --out-dir build --modules amd
  2. Run the transpiled test.js with :

    node runner.js
  3. The execution would fail with an error like the following :

    Error: _FixedPoint62.default.getQuotient is not a function
B.5.2.2. Module Format = CommonJS
  1. Compile the previous set up using babel as the transpiler with CommonJS modules :

    babel src -w --out-dir build --modules common
  2. Run the transpiled test.js with :

    node runner.js
  3. The execution is successful and logs the following results :

    { value: 20.5 } { value: 10 } { value: 2 }
B.5.2.3. Module Format = SystemJS
  1. Compile the previous set up using babel as the transpiler with SystemJS modules :

    babel src -w --out-dir build --modules system
  2. Run the transpiled test.js with :

    node runner.js
  3. The execution is successful and logs the following results :

    { value: 20.5 } { value: 10 } { value: 2 }

B.5.3. Conclusion

As observed, the test is executed successfully with CommonJS & SystemJS module formats. It however fails with AMD format (due to the circular dependency).

B.6. System.register as transpilation target

In order to integrate SystemJS as the module loader, the recommended module format is System.register. This section serves as a guide (& implementation hint) to transpile N4JS modules with System.register as the module format.

B.6.1. Introduction

This format is best explained from its documentation :

System.register can be considered as a new module format designed to support the exact semantics of ES6 modules within ES5. It is a format that was developed out of collaboration and is supported as a module output in Traceur (as instantiate), Babel and TypeScript (as system). All dynamic binding and circular reference behaviors supported by ES6 modules are supported by this format. In this way it acts as a safe and comprehensive target format for the polyfill path into ES6 modules.

To run the format, a suitable loader implementation needs to be used that understands how to execute it. Currently these include SystemJS, SystemJS Self-Executing Bundles and ES6 Micro Loader. The ES6 Module Loader polyfill also uses this format internally when transpiling and executing ES6.

The System.register format is not very well documented. However, this format is supported by all major transpilers out there i.e. BabelJS, Traceur & TypeScript transpilers. In fact, the primary resource of this documentation has been the outputs generated by these transpilers.

B.6.1.1. External Transpilers

In order to follow along, it will be best to try out different ES6 syntax being transpiled to System.register format by these transpilers.

The following instructions will be useful :

  1. Transpile with Traceur

    traceur --dir <SOURCE_DIR> <OUTPUT_DIR> --experimental --modules=instantiate
  2. Transpile with Babel

    babel <SOURCE_DIR> --out-dir <OUTPUT_DIR> --modules system
  3. Transpile with TypeScript

    Create a file by the name of tsconfig.json in the project folder, with the following contents :

    {
        "compilerOptions": {
            "module": "system",
            "target": "ES5",
            "outDir": <OUTPUT_DIR>,
            "rootDir": <SOURCE_DIR>
        }
    }

    Then transpile with :

    tsc
B.6.1.2. Example of a System.register module

For the following ES6 code :

import { p as q } from './dep';

var s = 'local';

export function func() {
    return q;
}

export class C {}

The Babel transpiler generates the following code (w/ System.register format):

System.register(['./dep'], function (_export) {
    'use strict';

    var q, s, C;

    _export('func', func);

    function _classCallCheck(instance, Constructor) { .. }

    function func() {
        return q;
    }

    return {
        setters: [function (_dep) {
            q = _dep.p;
        }],
        execute: function () {
            s = 'local';

            C = function C() {
                _classCallCheck(this, C);
            };

            _export('C', C);
        }
    };
});

B.6.2. Structure of a System.register module

Broadly speaking, a System.register module has the following structure :

System.register(<<DEPENDENCIES ARRAY>>, function(<<exportFn>>) {
    <<DECLARATION SCOPE>>

    return {
        setters: <<SETTERS ARRAY>>,
        execute: function() {
            <<EXECUTABLES>>
        }
    };
});

Highlights :

  • System.register(…​) is called with 2 arguments :

    • <<DEPENDENCIES ARRAY>> : an array of dependencies (similar to AMD) extracted from the ES6 import statements.

    • a function (FN) :

      • accepts a parameter called <<exportFn>>. This <<exportFn>> (provided by SystemJS) keeps a track of all the exports of this module & will inform all the importing modules of any changes.

      • contains a <<DECLARATION SCOPE>> where all the functions and variables (from the source code) get hoisted to.

      • returns an object with 2 properties :

        • setters : The <<SETTERS ARRAY>> is simply an array of functions. Each of these functions represents the imported-bindings from the <<DEPENDENCIES ARRAY>>. The <<SETTERS ARRAY>> and <<DEPENDENCIES ARRAY>> follow the same order.

        • execute : <<EXECUTABLES>> is the rest of the body of the source code.

B.6.3. Transpilation Hints

By observing the existing transpilers’ output, this sub-section provides insights into the process of transpiling to this format :

B.6.3.1. Handling Imports

The following are ES6 code snippets with some import statements :

  1. Simple Import Statement

    import {b1} from 'B';

    A valid System.register output for this snippet would look like :

    System.register(['B'], function (<<exportFn>>) { //(1.)
    
      var b1;  //(2.)
    
      return {
    
        //(3.)
        setters: [function (_B) {
          b1 = _B.b1; //(4.)
        }],
    
        execute: function () {}
      };
    });
    highlights
    1. The <<DEPENDENCIES ARRAY>> is just [’B’].

    2. The <<DECLARATION SCOPE>> simply declares the imported binding v1 as a variable.

    3. The <<SETTERS ARRAY>> has 1 function. This function corresponds to the single dependency (’B’) from (1.)

    4. The setter function accepts one argument (the exported object from ’B’ as _B . It then sets the local binding (i.e. local variable v1) to _B.b1.

    Takeaway
    • An import statement is broken down into <<DEPENDENCIES ARRAY>> & <<SETTERS ARRAY>>.

    • Whenever the value of b1 inside B is changed, SystemJS will execute the corresponding setter function in this module i.e. the 1st function in this case.

  2. Multiple Import Statements

    import { a1 as a0 } from 'A';
    import {b1} from 'B';
    import { c1 as c0 } from 'C';
    import {b2, b3} from 'B';
    import {default} from 'C';
    import {a2} from 'A';

    A valid System.register output for this snippet would look like :

    System.register(['A', 'B', 'C'], function (<<exportFn>>) { //(1.)
    
      var a0, a2, b1, b2, b3, c0, default;  //(2.)
    
      return {
    
        //(3.)
        setters: [function (_A) {
          a0 = _A.a1;         //(4.1.)
          a2 = _A.a2;         //(4.2.)
        }, function (_B) {
          b1 = _B.b1;
          b2 = _B.b2;
          b3 = _B.b3;
        }, function (_C) {
          c0 = _C.c1;
          default = _C['default'];
        }],
    
    
        execute: function () {}
      };
    });
    highlights
    1. The <<DEPENDENCIES ARRAY>> is now a unique array [’A’, ’B’, ’C’]. Note that there are no duplicates.

    2. The <<DECLARATION SCOPE>> simply declares all the imported bindings as variables.

    3. The <<SETTERS ARRAY>> now has 3 functions. These 3 functions match the ordering of the <<DEPENDENCIES ARRAY>>.

    4. The setter function accepts one argument (the exported object from the dependency) It then sets the local bindings (i.e. local variables) from the exported value of the dependency.

    Takeaway
    • Whenever an exported value from A is changed, SystemJS will execute the first setter function in this module.

    • Whenever an exported value from B is changed, SystemJS will execute the second setter function in this module.

    • Whenever an exported value from C is changed, SystemJS will execute the third setter function in this module.

B.6.3.2. <<exportFn>>

Before moving on to handling exports, let’s focus on the SystemJS provided <<exportFn>>.

This function looks similar to the following :

function (name, value) {     //(1.)
      module.locked = true;

      if (typeof name == 'object') {
        for (var p in name)
          exports[p] = name[p];      //(2.1.)
      }
      else {
        exports[name] = value;      //(2.2.)
      }

      for (var i = 0, l = module.importers.length; i < l; i++) {
        var importerModule = module.importers[i];
        if (!importerModule.locked) {
          var importerIndex = indexOf.call(importerModule.dependencies, module);
          importerModule.setters[importerIndex](exports);     //(3.)
        }
      }

      module.locked = false;
      return value; //(4.)
}
highlights
  1. The <<exportFn>> takes 2 arguments : name & value.

  2. It maintains an exports object with name & value.

  3. For every module which imports the current module, it executes the corresponding setter function.

  4. It returns the value.

Takeaway

This <<exportFn>> is responsible for pushing the changes from a module to every importing module thereby implementing the live binding.

B.6.3.3. Handling Exports

Now let’s focus on handling export statements.

  1. Simple Exports Statement

    export var v =1;
    export function f(){}

    A valid System.register output for this snippet would look like :

    System.register([], function (_export) {  //(1.)
    
       //(2.)
      var v;
      function f() {}
    
      _export("f", f); //(4.1)
    
      return {
        setters: [],
    
        //(3.)
        execute: function () {
          v = 1;  //(3.1.)
    
          _export("v", v); //(4.2.)
        }
      };
    });
    highlights
    1. The <<exportFn>> is named to as _export. (This is an implementation decision by Babel.) The name should be unique to not conflict with any user-defined variable/function names.

    2. The <<DECLARATION SCOPE>> hoists the exported variable v and the function f.

    3. The <<EXECUTABLES>> zone now contains the executable code from the source module.

    4. Initialise the variable v1 with the value extracted from the source. This essentially is the executable part of the module.

    5. The export function expression results in a call to the _exports function as: _export(f, f)

    6. The export statement results in a call to the _export function as: _export(v, v)

    Takeaway
    • The module’s exports statements are separated from the hoistable and executable statements.

    • All the exported bindings are tracked by wrapping them inside the <<exportFn>>.

  2. Tracking Exported Bindings

    To maintain live bindings, SystemJS needs to track any changes to exported bindings in order to call the setter functions of importing modules. Let’s look at an example for that :

    export var v1 = 1;
    export var v2 = 2;
    export var v3 = 3;
    export function f() {}
    
    v1++; //(1.)
    ++v2; //(2.)
    v3 += 5; //(3.)
    f = null; //(4.)

    Babel output for this snippet looks like :

    System.register([], function (_export) {
    
      var v1, v2, v3;
    
      _export("f", f);
    
      function f() {}
    
      return {
        setters: [],
    
        execute: function () {
          v1 = 1;
    
          _export("v1", v1);
    
          v2 = 2;
    
          _export("v2", v2);
    
          v3 = 3;
    
          _export("v3", v3);
    
          _export("v1", v1 += 1); //(1.)
          _export("v2", v2 += 1); //(2.)
          _export("v3", v3 += 5); //(3.)
          _export("f", f = null); //(4.)
        }
      };
    });

    Traceur output for this snippet looks like :

    System.register([], function($__export) {
    
      var v1, v2, v3;
      function f() {}
    
      $__export("f", f);
    
      return {
        setters: [],
    
        execute: function() {
          v1 = 1;
          $__export("v1", v1);
          v2 = 2;
          $__export("v2", v2);
          v3 = 3;
          $__export("v3", v3);
    
          ($__export("v1", v1 + 1), v1++); //(1.)
          $__export("v2", ++v2); //(2.)
          $__export("v3", v3 += 5); //(3.)
          $__export("f", f = null); //(4.)
        }
      };
    });

    TypeScript output for this snippet looks like :

    System.register([], function(exports_1) {
        var v1, v2, v3;
        function f() { }
    
        exports_1("f", f);
    
        return {
            setters:[],
    
            execute: function() {
                exports_1("v1", v1 = 1);
                exports_1("v2", v2 = 2);
                exports_1("v3", v3 = 3);
    
                (exports_1("v1", ++v1) - 1); //(1.)
                exports_1("v2", ++v2); //(2.)
                exports_1("v3", v3 += 5); //(3.)
                f = null; //(4.)
            }
        }
    });
    highlights
    • The re-assignment of v1, v2, v3 and f is wrapped inside a call to the <<exportFn>> with the updated value.

    Takeaway
    • While transpiling we need to detect if any exported binding is reassigned. In that case invoke the <<exportFn>> immediately with the new value.

    • Different transpilers perform different optimization tricks, which may be worth looking at.

  3. Exporting a Class extending an imported Class.

    Let’s look at the following class declaration :

    import {A} from "A"; //<-- import class A
    
    export class C extends A {}

    Babel output for this snippet looks like :

    System.register(["A"], function (_export) {
    
      var A, C;
    
      var _get = function get(_x, _x2, _x3) { ... };
    
      function _classCallCheck(instance, Constructor) { ... }
    
      function _inherits(subClass, superClass) { ... }
    
      return {
        setters: [function (_A2) {
          A = _A2.A;
        }],
    
        execute: function () { //(1.)
          C = (function (_A) {
            _inherits(C, _A);
    
            function C() {
              _classCallCheck(this, C);
    
              _get(Object.getPrototypeOf(C.prototype), "constructor", this).apply(this, arguments);
            }
    
            return C;
          })(A);
    
          _export("C", C);
        }
      };
    });

    Traceur output for this snippet looks like :

    System.register(["A"], function($__export) {
      var A, C;
    
      return {
        setters: [function($__m) {
          A = $__m.A;
        }],
    
        execute: function() { //(1.)
          C = $traceurRuntime.initTailRecursiveFunction(function($__super) {
            return $traceurRuntime.call(function($__super) {
              function C() {
                $traceurRuntime.superConstructor(C).apply(this, arguments);
              }
              return $traceurRuntime.continuation($traceurRuntime.createClass, $traceurRuntime, [C, {}, {}, $__super]);
            }, this, arguments);
          })(A);
          $__export("C", C);
        }
      };
    });

    TypeScript output for this snippet looks like :

    System.register(["A"], function(exports_1) {
        var __extends = function(){ ... }
        var A_1;
        var C;
    
        return {
            setters:[
                function (A_1_1) {
                    A_1 = A_1_1;
                }],
    
            execute: function() { //(1.)
                C = (function (_super) {
                    __extends(C, _super);
                    function C() {
                        _super.apply(this, arguments);
                    }
                    return C;
                })(A_1.A);
                exports_1("C", C);
            }
        }
    });
    highlights
    1. Notice how the construction of class C has now been deferred to the <<EXECUTABLES>> zone. It is because C depends on A being imported first.

    Takeaway
    • The <<DECLARATION SCOPE>> is for hoisting only independent entities i.e. the ones that do not depend upon any imports. Everything else is moved to the <<EXECUTABLES>> region.

B.6.4. Examples w/ Circular Dependencies

This section focuses on circular dependencies. The goal is to see how the transpiled output looks like and if the execution is possible.

Source files:

import B from "B";

export default class A {}
A.b = new B(); //<---
import A from "A";

export default class B {}
B.a = new A(); //<---

Transpiled Outputs (w/ Babel):

System.register(["B"], function (_export) {
  "use strict";

  var B, A;

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

  return {
    setters: [function (_B) {
      B = _B["default"];
    }],
    execute: function () {
      A = function A() {
        _classCallCheck(this, A);
      };

      _export("default", A);

      A.b = new B();
    }
  };
});
System.register(["A"], function (_export) {
  "use strict";

  var A, B;

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

  return {
    setters: [function (_A) {
      A = _A["default"];
    }],
    execute: function () {
      B = function B() {
        _classCallCheck(this, B);
      };

      _export("default", B);

      B.a = new A();
    }
  };
});

Execution Result

var System = require('systemjs');

System.import('A', 'B').then(function(resp) {
    var a = new A();
    var b = new B();
}).catch(function(e) {
    console.log(e);
});
Babel : [Error: undefined is not a function]

Source files:

import {B} from "B";

export class A extends B{}
import {A} from "A";

export class B{}
class C extends A{}

Transpiled Outputs (w/ Babel) :

System.register(["B"], function (_export) {
  "use strict";

  var B, A;

  var _get = function get(_x, _x2, _x3) { ... };

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

  function _inherits(subClass, superClass) {...}

  return {
    setters: [function (_B2) {
      B = _B2.B;
    }],
    execute: function () {
      A = (function (_B) {
        _inherits(A, _B);

        function A() {
          _classCallCheck(this, A);

          _get(Object.getPrototypeOf(A.prototype), "constructor", this).apply(this, arguments);
        }

        return A;
      })(B);

      _export("A", A);
    }
  };
});
System.register(["A"], function (_export) {
  "use strict";

  var A, B, C;

  var _get = function get(_x, _x2, _x3) { ... };

  function _inherits(subClass, superClass) { ... }

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

  return {
    setters: [function (_A2) {
      A = _A2.A;
    }],
    execute: function () {
      B = function B() {
        _classCallCheck(this, B);
      };

      _export("B", B);

      C = (function (_A) {
        _inherits(C, _A);

        function C() {
          _classCallCheck(this, C);

          _get(Object.getPrototypeOf(C.prototype), "constructor", this).apply(this, arguments);
        }

        return C;
      })(A);
    }
  };
});

Execution Result

var System = require('systemjs');

System.import('A','B').then(function(resp) {
    var a = new A();
}).catch(function(e) {
    console.log(e);
});
TypeScript : [Error: Cannot read property 'prototype' of undefined]
Babel : [Error: Super expression must either be null or a function, not undefined]

Source files:

import B from "B";

class A extends B {}
export default class X {}
import X from "A";

export default class B {}
class Y extends X {}

Transpiled Outputs (w/ Babel):

System.register(["B"], function (_export) {
  "use strict";

  var B, A, X;

  var _get = function get(_x, _x2, _x3) { ... };

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

  function _inherits(subClass, superClass) { ... }

  return {
    setters: [function (_B2) {
      B = _B2["default"];
    }],
    execute: function () {
      A = (function (_B) {
        _inherits(A, _B);

        function A() {
          _classCallCheck(this, A);

          _get(Object.getPrototypeOf(A.prototype), "constructor", this).apply(this, arguments);
        }

        return A;
      })(B);

      X = function X() {
        _classCallCheck(this, X);
      };

      _export("default", X);
    }
  };
});
System.register(["A"], function (_export) {
  "use strict";

  var X, B, Y;

  var _get = function get(_x, _x2, _x3) { ... };

  function _inherits(subClass, superClass) { ... }

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

  return {
    setters: [function (_A) {
      X = _A["default"];
    }],
    execute: function () {
      B = function B() {
        _classCallCheck(this, B);
      };

      _export("default", B);

      Y = (function (_X) {
        _inherits(Y, _X);

        function Y() {
          _classCallCheck(this, Y);

          _get(Object.getPrototypeOf(Y.prototype), "constructor", this).apply(this, arguments);
        }

        return Y;
      })(X);
    }
  };
});

Execution Result

var System = require('systemjs');

System.import('A').then(function(resp) {
    var a = new A();
}).catch(function(e) {
    console.log(e);
});
TypeScript : [Error: Cannot read property 'prototype' of undefined]
Babel : [[Error: Super expression must either be null or a function, not undefined]]

B.6.5. N4JS Examples w/ Circular Dependencies

In order to improve our precision in conversing and discussing about different kinds of circular dependencies, this section provides the most basic examples of different kinds.

B.6.5.1. Unresolved Cyclic Dependencies

Below examples demonstrate cases when cyclic dependency cannot be resolved at all and will cause runtime errors.

Example 5. Circular dependency resolution 1
import b from "B"

export public var number a = 1;
export public var number a2 = b + 1;
import a from "A"

export public var number b = a + 1;
import a2 from "A"
console.log(a2); //<-- should be 3. not NaN.
Example 6. Circular dependency resolution 2
import B from "B"

export public class A {
    static a = B.b + 1;
}
import A from "A"
export public class B {
    static b = 1;
}
export public class B2 {
    static b2 = A.a;
}
import B2 from "B"
console.log(B2.b2); //should log 2
Example 7. Circular dependency resolution 3
import B from "B"
export public class A {
    B b = new B();
}
import A from "A"
export public class B {
    A a = new A();
}
import A from "A"
new A(); // should not cause a runtime error.
B.6.5.2. Examples with Variables & Functions
Example 8. Circular dependency resolution 4
import b_fun from "B"

export public var a2 = b_fun();
export public var a = 1;
import a from "A"

export public function b_fun() {
    return a + 1;
}
import a2 from "A"
console.log(a2); //<-- should be 2. not NaN.
B.6.5.3. Examples with Classes
Example 9. Circular dependency resolution 5
import B from "B"
export public class A {
    static a1 = 1;
    static a2 = B.b1;
}
import A from "A"
export public class B {
    static b1 = A.a1;
}
import A from "A"
console.log(A.a1); //should log 1. not an error.
Example 10. Circular dependency resolution 6
import B from "B"
export public class A {
    static a1 = 1;
    static a2 = B.b1;
}
import A from "A"
export public class B {
    static b1 = -1;
    static b2 = A.a1;
}
import A from "A"
console.log(A.a1);//should log 1. not an error.
Example 11. Circular dependency resolution 7
import B from "B"
export public class A {
    static a = new B();
}
import A from "A"
export public class B {
    static b = new A();
}
import A from "A"
new A(); //should succeed.
B.6.5.4. Examples with SubClassing
Example 12. Circular dependency resolution 8
import B from "B"
export public class A {}
export public class C extends B {}
import A from "A"
export public class B extends A{}
import C from "A"
new C();//should succeed.
Example 13. Circular dependency resolution 9
import B from "B"
export public class A {}
export public class C {
    c = new B();
}
import A from "A"
export public class B extends A{}
import C from "A"
new C(); //should succeed.
B.6.5.5. Miscellaneous
Example 14. Circular dependency resolution 10
import B from "B"
export public class A {}
new B();
import A from "A"
export public class B {}
new A();
import A from "A"
new A() //should succeed.
Example 15. Circular dependency resolution 11
import B from "B"
export public class A {}
B.b1;
import A from "A"
export public class B {
    static b1;
}
new A();
import A from "A"
new A() //should succeed.

B.7. CommonJS as transpilation target

To provide better compatibility with npm eco-system, we want to transpile N4JS code to CommonJS module format.

B.7.1. Introduction

A sample CommonJS module :

var lib1 = require("/lib1"); //<-- require
var lib2 = require("/lib2"); //<-- require

function fn() {
    //...something using 'lib1' & 'lib2'
}

exports.usefulFn = fn; //<--exports
exports.uselessValue = 42; //<--exports

The CommonJS spec describes the salient features of module format as (quoted verbatim) :

Module Context

  1. In a module, there is a free variable "require", that is a function.

    1. The "require" function accepts a module identifier.

    2. "require" returns the exported API of the foreign module.

    3. If there is a dependency cycle, the foreign module may not have finished executing at the time it is required by one of its transitive dependencies; in this case, the object returned by "require" must contain at least the exports that the foreign module has prepared before the call to require that led to the current module’s execution.

    4. If the requested module cannot be returned, "require" must throw an error.

  2. In a module, there is a free variable called "exports", that is an object that the module may add its API to as it executes.

  3. modules must use the "exports" object as the only means of exporting.

Module Identifiers

  1. A module identifier is a String of "terms" delimited by forward slashes.

  2. A term must be a camelCase identifier, ".", or "..".

  3. Module identifiers may not have file-name extensions like ".js".

  4. Module identifiers may be "relative" or "top-level". A module identifier is "relative" if the first term is "." or "..".

  5. Top-level identifiers are resolved off the conceptual module name space root.

  6. Relative identifiers are resolved relative to the identifier of the module in which "require" is written and called.

B.7.2. Transpilation Hints

This section examines how Babel transpiles ES6 modules to CommonJS format. By observing the transpiled output from Babel, we can gather insights for transpiling N4JS modules to CommonJS format.

B.7.2.1. Import Statements
Example 16. Import an entire module (for side effects only)
import "B";
console.log(B);
"use strict";

require("B");
console.log(B);
Example 17. Import single member of a module
import {b1} from "B";
b1;
"use strict";

var _B = require("B");
_B.b1;
Example 18. Import multiple members of a module
import {b1, b2} from "B";
b1;
b2;
"use strict";

var _B = require("B");

_B.b1;
_B.b2;
Example 19. Import a single member of a module w/ an alias
import {b3 as b4} from "B";
b4 + 1;
"use strict";

var _B = require("B");

_B.b3 + 1;
Example 20. Import multiple members of a module w/ aliases
import {b3 as b4, b5 as b6} from "B";
b4 + 1;
b6 + 1;
"use strict";

var _B = require("B");

_B.b3 + 1;
_B.b5 + 1;
Example 21. Import ALL the bindings of a module
import * as B from "B";
console.log(B);
"use strict";

function _interopRequireWildcard(obj) {
    //Babel internally tracks ES6 modules using a flag "__esModule".
    if (obj && obj.__esModule) {
        return obj;
    } else {
        //Copy over all the exported members.
        var newObj = {};
        if (obj != null) {
            for (var key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
            }
        }

        //Set the "default" as the obj itself (ES6 default export)
        newObj["default"] = obj;
        return newObj;
    }
}

var _B = require("B");

var B = _interopRequireWildcard(_B);

console.log(B);
Example 22. Import the default export of a module
import B from "B";
console.log(B);
"use strict";

//For importing a default export,
//Babel checks if the obj is an ES6 module or not.
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : { "default": obj };
}

var _B = require("B");

var _B2 = _interopRequireDefault(_B);

console.log(_B2["default"]);
B.7.2.2. Export Statements
Example 23. Export a member
let a = 1;
export {a};
"use strict";

//Babel makes a note that this is as an ES6 module.
//This information is later used when this module is imported.
Object.defineProperty(exports, "__esModule", {
  value: true
});
var a = 1;

exports.a = a;
Example 24. Export multiple members
let a = 1;
let b = true;

export {a, b};
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var a = 1;
var b = true;

exports.a = a;
exports.b = b;
Example 25. Export using alias
let a =1;
export {a as b};
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var a = 1;
exports.b = a;
Example 26. Multiple exports using alias
let a = 1, b = 2;
export {a as A, b as B};
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var a = 1,
    b = 2;
exports.A = a;
exports.B = b;
Example 27. Simple default export
export default 42;
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = 42; //<-- default export is treated as a special named export
module.exports = exports["default"]; //<-- IMPORTANT
Example 28. Default export using an alias
let x =10;
export {x as default};
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var x = 10;
exports["default"] = x;
module.exports = exports["default"];
Example 29. Default export w/ named export
let a = 1;
export {a};
export default 42;
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var a = 1;
exports.a = a;
exports["default"] = 42;
Example 30. Default export a class
export default class A  {}
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});

function _classCallCheck(...) { ... }

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

exports["default"] = A;
module.exports = exports["default"];
Example 31. Wildcard re-export
export * from "A"
"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true
});

function _interopExportWildcard(obj, defaults) {
    var newObj = defaults({}, obj);
    delete newObj["default"]; //<-- A module's default export can not be re-exported.
    return newObj;
}

function _defaults(obj, defaults) {
    var keys = Object.getOwnPropertyNames(defaults);
    for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        var value = Object.getOwnPropertyDescriptor(defaults, key);
        if (value && value.configurable && obj[key] === undefined) {
            Object.defineProperty(obj, key, value);
        }
    }
    return obj;
}

var _A = require("A");

_defaults(exports, _interopExportWildcard(_A, _defaults));
Example 32. Specific member re-export
export {a1, a2} from "A";
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _A = require("A");

Object.defineProperty(exports, "a1", {
  enumerable: true,
  get: function get() {
    return _A.a1;
  }
});
Object.defineProperty(exports, "a2", {
  enumerable: true,
  get: function get() {
    return _A.a2;
  }
});
Example 33. Specific member re-export using alias
export {a1 as A1, a2 as A2} from "A";
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _A = require("A");

Object.defineProperty(exports, "A1", {
  enumerable: true,
  get: function get() {
    return _A.a1;
  }
});
Object.defineProperty(exports, "A2", {
  enumerable: true,
  get: function get() {
    return _A.a2;
  }
});
B.7.2.3. Tracking Live Bindings

As specified in the section about ES6 Modules (ES6 Modules), ES6 Modules export live immutable bindings. The following listings demonstrate how Babel achieves this.

Example 34. Tracking Live Binding
export var a = 1;
a++;
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var a = 1;
exports.a = a;
exports.a = a += 1; //<-- Exported value is tracked.
B.7.2.4. A complete example

The following listings present a simple but complete example of ES6 export, import and live-binding concepts. It uses 3 simple ES6 modules called A.js, B.js and Main.js. The modules are listed alongside their CommonJS versions generated by Babel.

export var a = 1; //<-- exports a number

export function incA() { //<-- exports a function
    a++;
}
"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true
});

exports.incA = incA;
var a = 1;

exports.a = a;

function incA() {
    exports.a = a += 1;
}
import {incA} from "./A"; //<-- Imports the function from A.js

export function incB() { //<-- Exports a function that calls the imported function from A.js
    incA();
}
"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.incB = incB;

var _A = require("./A");

function incB() {
    _A.incA();
}
import {a} from "./A"; //<-- Imports the exported number from A.js
import {incB} from "./B"; //<-- Imports the exported function from B.js

console.log(a); //<-- Prints "1"
incB(); //<-- This will call the "incA" function of A.js
console.log(a); //<--Prints "2". The imported value "a" is updated.
"use strict";

var _A = require("./A");

var _B = require("./B");

console.log(_A.a);
_B.incB();
console.log(_A.a);

Quick Links