Using ES modules natively in Node.js

[2017-09-12] dev, javascript, esnext, jsmodules, nodejs
(Ad, please don’t block)
  • Update 2018-12-20: Warning: This blog post is outdated! Consult “ECMAScript modules in Node.js: the new plan” for the latest information.
  • Update 2017-09-14: Major rewrite of Sect. “Checklist: things to watch out for”. New FAQ entries.

Starting with version 8.5.0, Node.js supports ES modules natively, behind a command line option. Most of the credit for this new functionality goes to Bradley Farias.

This blog post explains the details.

Demo  

The demo repository has the following structure:

esm-demo/
    lib.mjs
    main.mjs

lib.mjs:

export function add(x, y) {
    return x + y;
}

main.mjs:

import {add} from './lib.mjs';

console.log('Result: '+add(2, 3));

Running the demo:

$ node --experimental-modules main.mjs
Result: 5

Checklist: things to watch out for  

Module specifiers of ES modules:

  • All module specifiers are now URLs – which is new for Node.js.
  • Peer files: are best referred to via relative paths with the file extension .mjs. That way, specifiers remain compatible with the web. If you omit the extension, path resolution works similarly to CJS modules; if the same file exists as both .mjs and .js, the former wins.
    • Example: '../util/tools.mjs'
  • Libraries: are best referred to via bare paths without file extensions. That is most compatible with how libraries are delivered at the moment.
    • Example: 'lodash'
    • How to best make npm-installed libraries available in browsers (without using a bundler) remains to be seen. One possibility is to introduce RequireJS-style configuration data mapping bare paths to real paths. At the moment, bare paths as module specifiers are illegal in browsers.

Features of ES modules:

  • No dynamic importing of modules. But the dynamic import() operator is being worked on and should be available relatively soon.

  • No metavariables such as __dirname and __filename. However, there is a proposal to bring similar functionality to ES modules – “import.meta” by Domenic Denicola:

    console.log(import.meta.url);
    

Interoperability with CJS modules:

  • ES modules can import CJS modules, but they always only have a default export – the value of module.exports. Letting a CJS module make named exports (e.g. via a pragma at the beginning of the file) is on the roadmap, but may take a while. If you can help, please do so.

    import fs1 from 'fs';
    console.log(Object.keys(fs1).length); // 86
    
    import * as fs2 from 'fs';
    console.log(Object.keys(fs2)); // ['default']
    
  • You can’t use require() inside ES modules. The main reasons for this are:

    • Path resolution works slightly differently: ESM does not support NODE_PATH and require.extensions. And its specifiers always being URLs also leads to a few minor differences.
    • ES modules are always loaded asynchronously, which ensures maximal compatibility with the web. This style of loading doesn’t mix well with synchronously loading CJS modules via require().
    • Forbidding sync module loading also keeps the door open for top-level await in ES modules (a feature that is currently under consideration).
  • CJS modules can’t require() ES modules. Similarly to what was mentioned in the previous item, synchronously loading asynchronous modules is problematic. You’ll probably be able to use the Promise-based import() operator to load ES modules, though.

ES modules on older versions of Node.js  

If you want to use ES modules on Node.js versions prior to 8.5.0, take a look at @std/esm by John-David Dalton.

Tip: if you don’t switch on any of the unlockables (extra features), you’ll stay 100% compatible with native ES modules on Node.js.

FAQ  

When will ES modules be available without the command line option?  

The current plan is to make ES modules available by default in Node.js 10 LTS.

Do .mjs files work with browsers?  

They do, but they have to be served with the correct Media Type (text/javascript or application/javascript). Work on standardizing .mjs and upgrading tools and libraries is ongoing.

Is the file extension .mjs required for ES modules?  

Yes it is – on Node.js. Browsers don’t care about file extensions, only about Media Types (see previous question).

Why is the file extension .mjs required on Node.js?  

Node.js has to be able to detect whether a file contains a CJS module or an ES module. Several alternatives were considered before one of them was chosen. You can read about the alternatives in a separate blog post. Each one has pros and cons. In my opinion, .mjs was the right choice (but you can make up your own mind if you read the linked blog post).

Further reading  

More information on ES modules in Node.js and browsers:

Upcoming ECMAScript proposals: