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!
@std/esm used in the Node REPL
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:
const a = require("./a")
module.exports = { a, b: 2 }ESM:
import a from "./a"
export default { a, b: 2 }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 around April 2018. This puts developers, esp. package authors, in a tough spot. They could choose to:
None of those choices seem super appealing. The ecosystem needs something that meets it where it is to span the CJS to ESM gap.
Enter the @std/esm loader, a user-land package designed
to bridge the module gap. Since Node now
supports most ES2015 features, @std/esm is free to
focus solely on enabling ESM.
The loader stays out of your way and tries to be a good neighbor by:
"modules":false)@std/esm configuration object or having
@std/esm as a dependency, dev dependency, or peer
dependency@std/esm and package “B” on another)Unlike existing ESM solutions which require shipping transpiled
CJS, @std/esm performs minimal source transformations on
demand, processing and caching files at runtime. Processing files at
runtime has a number of advantages.
@std/esm config while
module “B” consumes module “C” with cjs compat rules
enabled)@std/esm can
enforce Node’s
ESM rules for environment variables, error codes, path protocol and
resolution, etc.)Defaults are important. The @std/esm 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, @std/esm just works, no configuration
necessary, and supports:
import / exportimport().mjs files
as ESMDevelopers have strong opinions on just about everything. To
accommodate, @std/esm allows unlocking
extra features with the "@std/esm" package.json field.
Options include:
import, export,
or "use module" pragma are treated as ESM)await in main modulesBefore 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 @std/esm cache is good for the lifetime of
the unmodified file. Ideally, that means you’ll only have a single
non-cached load in production.
@std/esm no cache run was ~1.6 milliseconds per
module@std/esm cached runs were ~0.54 milliseconds
per moduleInitial results look very promising, with
cached @std/esm loads achieving near built-in performance!
I’m sure, with your help, parse and runtime performance will continue to
improve.
npm i --save @std/esm in your app or package
directory.require("@std/esm") before importing ES
modules.index.js:
require("@std/esm")
module.exports = require("./main.mjs").defaultFor package authors with sub modules:
// Have "foo" require only "@std/esm". require("foo") // Sub modules work!
const bar = require("foo/bar").defaultEnable ESM in the Node CLI by loading @std/esm with the -r option:
node -r @std/esm file.mjs.
Enable ESM in the Node REPL by loading @std/esm upon
entering:
$ node
> require("@std/esm")
@std/esm enabled
> import p from "path"
undefined
> p.join("hello", "world")
'hello/world'The @std/esm loader wouldn’t exist without Ben
Newman, creator of the Reify compiler
from which @std/esm is forked. He’s proven the loader
implementation in
production at Meteor, since May 2016, in tens of thousands of Meteor
apps!
Even though @std/esm has just been released, it’s
already had a positive impact on several related projects:
export as ns from "mod" and export default from "mod" proposalsLike many developers, I want ES modules yesterday. I plan to
use @std/esm in Lodash v5 to not only transition to ESM but
also leverage features like gzip module support to greatly reduce its
package size.
The @std/esm 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.
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!