Loading an ES6 module in a CommonJS module using require()

; Date: February 16, 2018

Tags: Node.JS »»»» JavaScript

ES6 modules are a nifty new way to write modules. They're rather similar to the CommonJS module format traditionally used by Node.js, but there are significant differences. At the current time (Node.js 9.5) an ES6 module can load a CommonJS module using import, but a CommonJS module cannot load an ES6 module. The core problem is one of semantics and capabilities - ES6 modules behave and work differently enough that the Node.js require function can't use them directly. Which raises a big problem with integrating ES6 modules into the Node.js ecosystem that is dominated by CommonJS modules.

First, how do you use an ES6 module in Node.js? The current release (Node.js 9.5) makes it easy. Simply run the Node.js program with the --experimental-modules flag as so:

$ node --experimental-modules ./module-name
(node:5813) ExperimentalWarning: The ESM module loader is experimental.
... module output

With that option enabled you can use the .mjs extension (e.g. module-name.mjs) to identify ES6 Modules. You may see that extension referred to as Michael Jackson Script (.mjs). Such files are interpreted by the Node.js runtime as ES6 Modules, and regular .js modules are interpreted by the CommonJS semantics we've used with Node.js all along.

The default rules for loading modules are:

  • An ES6 Module can load other ES6 modules using import
  • An ES6 Module can load CommonJS using import
  • A CommonJS module can load other CommonJS modules using require
  • A CommonJS module CANNOT load an ES6 Module

That's the gap we're looking to solve - that CommonJS modules cannot load ES6 Modules.

The example I faced recently had to do with writing a Mocha test suite for an application written using ES6 modules. Mocha tests must be written using CommonJS semantics. Therefore testing ES6 modules requires somehow loading those modules in a CommonJS module. Well, that's if we want to keep using Mocha. Maybe it's time to write a new test framework based on ES6 modules?

Generally speaking the Node.js community has a large body of code written with CommonJS modules. It's not feasible to do what I just flippantly said and toss all that CommonJS code and immediately rewrite the world in the ES6 Module format. Instead what's needed is a solution to load ES6 modules in a CommonJS/Node.js module.

Module loading review

To review CommonJS ... a module is loaded as so:

const TheModule = require('module-specifier');

There are well-known rules for the module-specifier in the Node.js documentation. For exammple, ./foo loads a module in the current directory, whereas foogrep loads a module from the node_modules directory.

ES6 modules load other ES6 modules as so:

import * as TheModule from `es6-module-specifier`;

That's the basic mode, there are several alternatives available.

And an ES6 module loads CommonJS modules as so:

import TheModule from `module-specifier`;

In both cases the module-specifier uses the same rules for locating the module as Node.js supports in the require function.

The reason for using .mjs versus .js for different module formats is that the semantics are different. ES6 modules have a structure that's known by the language - export and import are now keywords in JavaScript. Further, their content is loaded asynchronously. Contrarily, Node.js modules are not baked into the language but instead require is a function provided by Node.js. And, exporting values from a Node.js/CommonJS module is similarly a code convention rather than a language feature.

Those differences create a situation where - at the current stage of things - Node.js could not automatically account for ES6 modules in a file with the .js extension. That resulted in the two extensions, .mjs and .js, as well as the rules matrix above.

Problem identified - look for solution

Enabling ES6 module support in Node.js CommonJS modules is as simple as putting this at the top of each module:

require = require("@std/esm")(module,{"esm":"js"});

The (www.npmjs.com) @std/esm supports using ES6 modules in Node.js separately from the --experimental-modules flag. Unlike that flag which was introduced in Node.js 9.x, the @std/esm team says their module supports Node.js 4.x and later.

You'll notice that what happens is the require function is being replaced. Therefore this line must appear BEFORE all other module loading activities, AND it has to be loaded in every module which desires to load ES6 modules.

Once you've done this, a CommonJS/Node.js module can load an ES6 module as so:

const TheModule = require(`es6-module-specifier`);

The object assigned to TheModule is the default export of the ES6 module. Other exports of the module are available as TheModule.exportName as you might expect.

That the @std/esm module makes this possible indicates it's within the power of the Node.js team to fix the problem. It seems they are working on the issue but it won't be ready for the upcoming Node.js 10.x release. It will be a minor miracle just getting ES6 module support into the platform.