Skip to content

Instantly share code, notes, and snippets.

@nazomikan
Forked from domenic/interop.md
Created February 10, 2013 07:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nazomikan/4748702 to your computer and use it in GitHub Desktop.
Save nazomikan/4748702 to your computer and use it in GitHub Desktop.

module.exports = and ES6 Module Interop in Node.js

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 do require("function-module")()?
  • How can consumers of function-modules use ES6 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.

Case 1: require + ES6 export syntax

Problem

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?

Answer

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.
  • 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 of require.

This means require("logFoo")() will work, since require retrieves the distinguishedName export of logFoo/index.js.

Case 2: import + Node.js module.exports = syntax

Problem

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?

Solution

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, but before executing it, rewrites it by replacing module.exports = function () { ... } with export function distinguishedName() { ... }.
  • Once this is done, it loads the rewritten module in the standard way, passing control back to the calling program.

This means import { distinguishedName: logFoo } from "logFoo" will work, since the module loader API ensures distinguishedName exists before importing.

Conclusion

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment