Babel and CommonJS modules

[2015-12-13] dev, javascript, esnext, babel, jstools, jsmodules
(Ad, please don’t block)

This blog post 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.

Starting point for this series of posts on Babel:Configuring Babel 6

ES6 modules vs. CommonJS modules  

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.

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;

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”.

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?

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' }

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.

How Babel imports CommonJS modules  

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).

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.

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.

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).