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.