ECMAScript modules in Node.js: the new plan

[2018-12-20] dev, javascript, nodejs, jsmodules
(Ad, please don’t block)

Status of ECMAScript module (ESM) support in Node.js:

  • Experimental support for ESM was added in Node.js 8.5.0 (2017-09-12).
  • Afterwards, the Node.js Foundation Technical Steering Committee formed the Modules Team to help design missing pieces and to guide the implementation towards its (non-experimental) release. The Modules Team is comprised of people from several different web development areas (frontend, backend, JS engines, etc.).

In October, the Modules Team published the document “Plan for New Modules Implementation”. This blog post explains what it contains.

The phases  

The work of the Modules Team is split into phases:

  • Phase 1: Establish a “minimal kernel” – a foundational set of rules and features that is both minimal and as uncontroversial as possible.
  • Phase 2 and later: Build on the kernel and tackle the design of more complicated features.

The minimal kernel is a foundation for further work. The new design will replace the current experimental implementation, once it has similar capabilities.

Phase 1: Minimal kernel of ESM support in Node.js  

Simplifying module specifiers  

One of the goals of the Modules Team is “browser equivalence”: Node.js should stay as close to browsers as possible. The kernel achieves that by making the resolution of module specifiers (the URLs pointing to modules) simpler:

  • Each module specifier must end with a file name extension. That is:
    • No file name extension is added automatically.
    • Importing a directory dir is not supported (neither via dir/index.mjs nor via the field main in package.json).
  • ES modules can import built-in Node.js modules (path etc.). All other bare paths must have file name extensions.
  • By default, only .mjs files are supported (check Phase 2 if you are interested in other file name extensions). That is, the following kinds of files cannot be imported via import: CommonJS modules, JSON files, native modules.

Bringing important CommonJS features to ES modules  

  • URL of current module (CommonJS: __filename): import.meta.url
  • Dynamically importing ES modules (always possible via require() in CommonJS): import()

Interoperability:  

  • ES modules can import CommonJS modules via createRequireFromPath(). This works as follows (with plans to make it more concise, e.g. via a function createRequireFromUrl()):
    import {createRequireFromPath as createRequire} from 'module';
    import {fileURLToPath as fromPath} from 'url';
    const require = createRequire(fromPath(import.meta.url));
    
    const cjsModule = require('./cjs-module.js');
    
  • CommonJS modules can import ES modules via import().

Phase 2 and later  

  • Phase 2:
    • Support for “bare” specifiers such as 'lodash'. This will probably involve some kind of mapping from such specifiers to real paths.
    • Support for file name extensions other than .mjs. That includes support for ES modules in .js files.
  • Phase 3 will probably focus on module loaders with extension points that user code can hook into.

When can I use ES modules on Node.js?  

  • Via a library: esm by John-David Dalton. Also supports older versions of Node.js.
  • Behind a flag: available now.
    • Caveat: Not yet updated to Phase 1+ (as of Node.js 11.5.0).
  • No flag and based on Phase 1+: The goal of the Modules Team is to make this happen as soon as possible. Hopefully, it will be unflagged in Node.js 14 (April 2020) and backported as far back as possible.

Frequently asked questions  

  • Why the different file name extension .mjs?
    • Every solution for distinguishing ESM and CJS has pros and cons. Using a different extension is a reasonable compromise (more information).
  • Why should Node.js stay close to the browser?

    • That makes it possible to reuse code. For example, to write libraries that work both on browsers and on Node.js.
    • It also makes it easier to switch between frontend and backend when coding.
  • Why all these restrictions w.r.t. interoperability?

    • Structure (static vs. dynamic) and loading process (async vs. sync) of ES modules and CommonJS modules are quite different. The restrictions keep things simple – considering the long-term future where ESM will be dominant.
  • Why does all of this take so long?

    • There are many stakeholders and many different platforms involved (Node.js, npm, browsers, JS engines, TypeScript, TC39, etc.). If we end up with ES modules working well everywhere then it’s worth the wait, IMO.

Acknowledgement  

  • Thanks to Myles Borins for his feedback on this blog post.

Further reading and sources of this blog post