The question: how can we use ES6 modules in Node.js, where modules-as-functions is very common? That is, given a future in which V8 supports ES6 modules:
- How can authors of function-modules convert to ES6
export
syntax, without breaking consumers that dorequire("function-module")()
? - How can consumers of function-modules use ES6
import
syntax, while not demanding that the module author rewrites his code to ES6export
?
@wycats showed me a solution. It involves hooking into the loader API to do some rewriting, and using a distinguished name for the single export.
This is me eating crow for lots of false statements I've made all over Twitter today. Here it goes.
Given this on the consumer side:
require("logFoo")();
and this ES5 on the producer side:
module.exports = function () {
console.log("foo");
};
how can the producer switch to ES6 export
syntax, while not breaking the consumer?
The producer rewrites logFoo
's main file, call it logFoo/index.js
, to look like this:
export function distinguishedName() {
console.log("foo");
};
Then, the following hypothetical changes in Node.js make it work:
require
is rewritten to look atlogFoo/package.json
and sees an"es6": true
entry.- It then switches to loading
logFoo/index.js
with the ES6 module loader API. - Once the ES6 module loader API has given the results back, it plucks off the
distinguishedName
property and returns that to the caller ofrequire
.
This means require("logFoo")()
will work, since require
retrieves the distinguishedName
export of logFoo/index.js
.
Given this ES5 on the producer side:
module.exports = function () {
console.log("foo");
};
and this ES5 on the consumer side:
require("logFoo")();
how can the consumer switch to ES6 import
syntax, while not demanding that the consumer rewrite his code to accomodate yet-another-module-system?
The consumer rewrites his code as
import { distinguishedName: logFoo } from "logFoo";
logFoo();
Then, the following hypothetical changes in Node.js make it work:
- The default ES6 module loader API is overriden to intercept any module loads
- It sees the module identifier string "logFoo", goes to look at
logFoo/package.json
, and sees no entry of the form"es6": true
. - It reads
logFoo/index.js
into memory, and executes it in a special context. - Once execution is done, it sees that
module.exports
now has a value in this context. - It creates a new module object with a single property,
distinguishedName
, whose value is filled out by pullingmodule.exports
out of this context. - It returns this new module object back as the imported module.
This means import { distinguishedName: logFoo } from "logFoo"
will work, since the module loader API ensures distinguishedName
exists before importing.
Elegant? No. Considerate of Node idioms? No. But does it work? Yes.
With a solution like this, you can interoperably use require
on ES6 modules and import
on ES5 modules, even in the function-module case. And the burden is entirely on the ES6 user to twist his code into awkward shapes, which is as it should be: updating 22K+ and growing packages is not an acceptable path forward.
Don't be so quick to swallow the crow.
Making this work requires several non-trivial changes to a section of node where stability is much more valuable than just about anything else. It's very easy to break every node program by making changes to the module loader. That's why we don't do it. What we have works great. Until and unless very significant improvements can be made to Node (in performance or other radically desirable features), we're not going to make any changes to node-core for any new module system that comes down as part of ES6.
Even when it is implemented in V8, what will be the benefit for us?
It's probably not performance. The module loading step is already rather slow, blocking, and only happens once at program startup. So who cares? But if the speed of the overall program is reduced by wrapping modules in a different sort of boilerplate, well, then that's a very very big deal. (This is why, for example, we cannot use the
with(knownObject){$code}
trick to provide access to the local activation context -- it prevent V8 from optimizing anything fully.) How long until V8 actually optimizes new ES6 modules effectively? (My guess: at least years.)As for features, we clearly don't need them, and this can be explored in userland anyway.
So, really what's in it for node to perform this kind of surgery? I don't really see the benefit.
Maybe in a few years, when ES6 actually has modules in some sort of a spec, and they're implemented in V8, and they're already popular enough in the browser world for V8 to optimize them fully, some descendent of node can build something interesting using them. But at least in the near term, none of this is realistic.