module.exports =
and ES6 Module Interop in Node.js
module.exports =
and ES6 Module Interop in Node.jsThe 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:
export
syntax, without breaking consumers that do require("function-module")()
?import
syntax, while not demanding that the module author rewrites his code to ES6 export
?@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.
require
+ ES6 export
syntaxGiven 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 at logFoo/package.json
and sees an "es6": true
entry.logFoo/index.js
with the ES6 module loader API.distinguishedName
property and returns that to the caller of require
.This means require("logFoo")()
will work, since require
retrieves the distinguishedName
export of logFoo/index.js
.
import
+ Node.js module.exports =
syntaxGiven 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:
logFoo/package.json
, and sees no entry of the form "es6": true
.logFoo/index.js
into memory, and executes it in a special context.module.exports
now has a value in this context.distinguishedName
, whose value is filled out by pulling module.exports
out of this context.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.