Editor’s Note: Today’s post is a guest post from John-David Dalton, a Program Manager on the Microsoft Edge team and creator of the popular Lodash JavaScript library, sharing the news of a new community project to bring ECMAScript modules to Node.
I’m excited to announce the release of @std/esm (standard/esm), an opt-in, spec-compliant, ECMAScript (ES) module loader that enables a smooth transition between Node and ES module formats with near built-in performance! This fast, small, zero dependency package is all you need to enable ES modules in Node 4+ today!

A tale of two module formats
With ESM landing in browsers, attention is turning to Node’s future ESM support. Unlike browsers, which have an out-of-band parse goal signal and no prior module format, support for ESM in Node is a bit more…prickly. Node’s legacy module format, a CommonJS (CJS) variant, is a big reason for Node’s popularity, but CJS also complicates Node’s future ESM support. As a refresher, let’s look at an example of both module syntaxes.
CJS:
[code language=”javascript”]const a = require(“./a”)
module.exports = { a, b: 2 }[/code]
ESM:
[code language=”javascript”]import a from “./a”
export default { a, b: 2 }[/code]
Note: For more in-depth comparisons see Nicolás Bevacqua’s excellent post.
Because CJS is not compatible with ESM, a distinction must be made. After much discussion, Node has settled on using the “.mjs” (modular JavaScript) file extension to signal the “module” parse goal. Node has a history of processing resources by file extension. For example, if you require a .jsonfile, Node will happily load and JSON.parse the result.
ESM support is slated to land, unflagged, in Node v10 anytime between October 2018 and April 2020. This puts developers, esp. package authors, in a tough spot. They could choose to:
- Go all in, shipping only ESM, and alienate users of older Node versions
- Wait until Jan 1, 2020, the day after Node 8 support ends, to go all in
- Ship both transpiled CJS and ESM sources, inflating package size and shouldering the responsibility for ensuring 1:1 behavior
None of those choices seem super appealing. The ecosystem needs something that meets it where it is to span the CJS to ESM gap.
Bridge building
Enter the <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> loader, a user-land package designed to bridge the module gap. Since Node now supports most ES2015 features, <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> is free to focus solely on enabling ESM.
The loader stays out of your way and tries to be a good neighbor by:
- Not polluting stack traces
- Working with your existing tools like Babel and webpack.
- Playing well with other loaders like babel-register
(using .babelrc<a href="https://medium.com/r/?url=https%3A%2F%2Fbabeljs.io%2Fdocs%2Fplugins%2Fpreset-env%2F%23optionsmodules" target="_blank" rel="noopener nofollow"><em>"modules":false</em></a>) - Only processing files of packages that explicitly opt-in with a
@std/esmconfiguration object or having@std/esmas a dependency, dev dependency, or peer dependency - Supporting versioning
(i.e. package “A” can depend on one version of<em>@std/esm</em>and package “B” on another)
Unlike existing ESM solutions which require shipping transpiled CJS, <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> performs minimal source transformations on demand, processing and caching files at runtime. Processing files at runtime has a number of advantages.
- Only process what is used, when it’s used
- The same code is executed in all Node versions
- Features are configurable by module consumers
(e.g. module “A” consumes module “C” with the default<a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener"><em>@std/esm</em></a>config while module “B” consumes module “C” with<em>cjs</em>compat rules enabled) - More spec-compliance opportunities
(i.e.<a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener"><em>@std/esm</em></a>can enforce Node’s ESM rules for environment variables, error codes, path protocol and resolution, etc.)
Standard features
Defaults are important. The <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> loader strives to be as spec-compliant as possible while following Node’s planned built-in behaviors. This means, by default, ESM requires the use of the .mjs extension.
Out of the box, <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> just works, no configuration necessary, and supports:
<a href="https://ponyfoo.com/articles/es6-modules-in-depth#import" target="_blank" rel="nofollow noopener">import</a>/<a href="https://ponyfoo.com/articles/es6-modules-in-depth#export" target="_blank" rel="nofollow noopener">export</a>- Dynamic
<a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Ftc39%2Fproposal-dynamic-import" target="_blank" rel="nofollow noopener">import()</a> - The file URI scheme
- Live bindings
- Loading
<a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Fnodejs%2Fnode-eps%2Fblob%2Fmaster%2F002-es-modules.md%2332-determining-if-source-is-an-es-module" target="_blank" rel="nofollow noopener">.mjs</a>files as ESM
Unlockables
Developers have strong opinions on just about everything. To accommodate, @std/esm allows unlocking extra features with the "@std/esm" package.json field. Options include:
- Enabling unambiguous module support (i.e. files with at least an
<em>import</em>,<em>export</em>, or<em>"use module"</em>pragma are treated as ESM) - Supporting named exports of CJS modules
- Top-level
awaitin main modules - Loading gzipped modules
Performance
Before I continue, let me qualify the following section:
It’s still super early, mileage may vary, and results may be hand wavey!
Testing was done using Node 9 compiled from PR #14369, which enables built-in ESM support. I measured the time taken to load the 643 modules of lodash-es, converted to .mjs, against a baseline run loading nothing. Keep in mind the <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> cache is good for the lifetime of the unmodified file. Ideally, that means you’ll only have a single non-cached load in production.
- Loading CJS equivs was ~0.28 milliseconds per module
- Loading built-in ESM was ~0.51 milliseconds per module
- First
<a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a>no cache run was ~1.6 milliseconds per module - Secondary
<a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a>cached runs were ~0.54 milliseconds per module
Initial results look very promising, with cached <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> loads achieving near built-in performance! I’m sure, with your help, parse and runtime performance will continue to improve.
Getting started
- Run
npm i --save @std/esmin your app or package directory. - Call
require("@std/esm")before importing ES modules.
index.js:
require("@std/esm")
module.exports = require("./main.mjs").default
For package authors with sub modules:
// Have "foo" require only "<code>@std/esm". require("foo") // Sub modules work! const bar = require("foo/bar").default
Enable ESM in the Node CLI by loading <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> with the <a href="https://medium.com/r/?url=https%3A%2F%2Fnodejs.org%2Fapi%2Fcli.html%23cli_r_require_module" target="_blank" rel="nofollow noopener">-r</a> option:
node -r @std/esm file.mjs
Enable ESM in the Node REPL by loading <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> upon entering:
$ node
> require("@std/esm")
@std/esm enabled
> import p from "path"
undefined
> p.join("hello", "world")
'hello/world'
Meteor’s might
The <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> loader wouldn’t exist without Ben Newman, creator of the Reify compiler from which <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> is forked. He’s proven the loader implementation in production at Meteor, since May 2016, in tens of thousands of Meteor apps!
All green thumbs
Even though <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> has just been released, it’s already had a positive impact on several related projects:
- Fixing Acorn’s strict mode pragma detection and aligning parser APIs
- Improving dynamic import support of Babel and Acorn plugin
(the dynamic import Acorn plugin is used by webpack for code splitting) - Improving property iteration order in ChakraCore
- Improving the parse, load time, and spec compliance of Reify
- Inspiring a fast top-level parser proof of concept
- Spurred championing of
<a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Ftc39%2Fproposal-export-ns-from" target="_blank" rel="nofollow noopener">export * as ns from "mod"</a>and<a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Ftc39%2Fproposal-export-default-from" target="_blank" rel="nofollow noopener">export default from "mod"</a>proposals
What’s next
Like many developers, I want ES modules yesterday. I plan to use <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="noopener nofollow">@std/esm</a> in Lodash v5 to not only transition to ESM but also leverage features like gzip module support to greatly reduce its package size.
The <a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40std%2Fesm" target="_blank" rel="nofollow noopener">@std/esm</a> loader is available on GitHub. It’s my hope that others are as excited and as energized as I am. ES modules are here! This is just the start. What’s next is up to you. I look forward to seeing where you take it.
Final Thought
While this is not a Microsoft release, we’re proud to have a growing number of core contributors to fundamental JavaScript frameworks, libraries, and utilities at Microsoft. Contributors like Maggie Pint of Moment.js, Matthew Podwysocki of ReactiveX, Nolan Lawson of PouchDB, Patrick Kettner of Modernizr, Rob Eisenberg of Aurelia, Sean Larkin of webpack, and Tom Dale of Ember, to name a few, who in addition to their roles at Microsoft, are helping shape the future of JavaScript and the web at large through standards engagement and ecosystem outreach. I’m happy to share this news on the Microsoft Edge blog to share our enthusiasm with the community!
― John-David Dalton, Program Manager, Microsoft Edge
