How to export a JavaScript module
Few days ago I wrote a tweet that surprised many developers not fully aware on how CommonJS modules work. The same tweet also gave birth to some usual discussion about anti-patterns, confusion, what why and how …
after all these years developers still write module.exports.stuff = 123;
instead of this.stuff = 123;
since module.exports === this
— Andrea Giammarchi (@WebReflection) November 26, 2015
Not only node.js
When people talk about isomorphic, or better “universal JS”, they usually refer to code that runs on both node.js and the browser. There are at least 7 others major engines in the server-side and micro-controller scenario, and these might or might not have a CommonJS built-in modules system:
- Nashorn, the JavaScript for the JVM which has functions to load files without implementing CommonJS
- SpiderMonkey, which is very popular in some linux distro, used as example extensively as
js17
orjs24
dependency in GNOME - JSC, the WebKit JavaScript Core, usable as well on OSX like any other command line tool
- GJS, JavaScript Gtk3 Bindings for GNOME, the PythonGtk3 exact equivalent in JavaScript, a SpiderMonkey based runtime with its own module system (explained leter on)
- the Espruino JavaScript interpreter, a very actively developed engine targeting embedded devices with as little as 128kB Flash and 8kB RAM
- Duktape, an embeddable JS engine that follows CommonJS modules
- KinomaJS, also based on CommonJS, claiming to be the engine than more than any other fulfill the ES2015 specifications
Ultimately, we have old and modern browsers, where there’s no CommonJS support but, in most updated cases, an explicit export
based syntax which is not compatible with CommonJS.
Have you ever wondered what all these JavaScript engines have in common since about ever?
The top level this
context
If an engine doesn’t implement CommonJS module
and exports
, the only way to attach a property, a method, or an utility, is using this
.
// generic module.js file
// how to export a method?
// the following throws an Error under "use strict"
method = function () { /* ... */ };
// even under "use strict" we can always do the following
this.method = function () { /* ... */ };
If you think using this
inside a module is confusing, think again ‘cause in JavaScript there shouldn’t ever be confusion about the execution context. In a module system there are only 3 possible cases for the this
reference:
- the context used to export, since in a module system there’s no way you want/expect to pollute the global scope and context defining a property to
this
. In CommonJS environments such node.js, Duktape, KinomaJS, or others, theexports
reference is exactly thethis
context indeed. - the global context, meaning the code is running in a non CommonJS based environment, yet it is capable to somehow export its functionality instead of throwing or breaking.
this.EventEmitter = function () { ... };
would exportEventEmitter
in the global context and make it available from that time on everywhere. Simply definingvar EventEmitter = function () {};
won’t be as portable because the module cannot be used on CommonJS systems. - undefined if used directly as ES2015 module, since each module is virtually evaluated under a
"use strict"
closure without an explicit execution context. This is actually a good thing because CommonJS modules are not compatible with ES6/ES2015 modules. Latter one need to use the very specific syntax suchexport EventEmitter = function () {};
which would brake in every non ES2015 capable engine. Failing early is crucial so that we are sure the ES2015 code needs to be transpiled or our module needs to be modified in order to use explicitly the new syntax.
The good news about ES2015 modules, is that we could bring CommonJS in there with ease, finally making our modules truly portable cross environments. The counter bad news is that it’s not possible, without a transpiler, to bring ES2015 modules to CommonJS based environments.
As of today, CommonJS is the most de-facto standard when it comes to modules and modules related tools.
Quick recap about CommonJS
As explained in the Duktape documentation, whenever we require a module its content will be executed within a closure in the following way:
var exports = {};
var module = {
exports: exports, /* initial value, may be replaced by user */
id: 'package/lib'
};
// F is the closure with the content of the module
F.call(exports, /* exports also used as 'this' binding */
require, /* require method */
exports, /* exports */
module); /* module */
It’s that simple: whenever we need to export a property, a method, an utility, or a class, we can either use this
or simply the exports
object. In some developer opinion, using exports
somehow implicitly flags the module as CommonJS compatible, and I kinda agree the explicit intent works quite well and is very welcome.
// how to export in CommonJS modules
// simulating the JS Math object
exports.PI = 3.14;
exports.abs = function (n) {
return (n | 0) * (n < 0 ? -1 : 1);
};
exports.max = function () {
// ...
};
The only reason we might need to access the exports
property trough the module
reference is actually to fully overwrite the export, allowing us to export directly a function or any other object.
// math.js as exports override
module.exports = {
PI: 3.14,
abs: function (n) {
return (n | 0) * (n < 0 ? -1 : 1);
},
max: function () {
// ...
}
};
If your habit is to pass through the module in order to export anything like in module.exports.foo = "bar"
, you might reconsider your verbosity and do directly exports.foo = "bar"
instead, which is the exact equivalent action.
Bear in mind, if your habit is to overwrite the default exports
object you still need to do that through the module.exports = {...};
way ‘cause reassigning the exports = {}
won’t actually work.
Loading files in Nashorn, SpiderMonkey, and JSC
There is another de-facto standard in most common CLI oriented JS environments, the load(fileName)
function. Not so difficult to imagine, the load
function simply find synchronously a file and execute its content in a top level global context.
It’s basically the equivalent of a <script src="module.js"></script>
on the web, there’s no guard about global scope/context pollution, so whatever is there, will be somehow “exported”.
// module.js
// will pollute the global scope
var module = "module.js";
// will pollute the global context
this.method = function () {
return 'Hi, I am the load("' + module + '") result';
};
// common way to load files in
// Nashorn, SpiderMonkey, or JSC
load('module.js');
module; // "module.js"
method(); // 'Hi, I am the load("module.js") result'
Accordingly, if we’d like to use load
in order to bring in modules, we might consider writing them in the following way:
// module exported on the global context
// without exceeding with variable pollution
(function (exports) {'use strict';
// will not pollute the global context
var module = "module.js";
// will export everywhere the `method` function
exports.method = function () {
return 'Hi, I am the load("' + module + '") result';
};
}(this));
It is not by accident that I’ve called the inline invoked function parameter exports
because that module will be automatically compatible with any CommonJS based environment too.
The curious case of GJS modules
When developers like JS simplicity but are not fully familiar with the JavaScript world, things like GJS happen. Please don’t get me wrong, I’ve written my ArchLinux and GNOME based operating system updater via GJS and I think it’s an amazing project, however, the fact the API uses Python naming convention, instead of a JS one, and the fact you apparently need to throw errors in order to know in which file you are running, make this environment quite hostile for regular client/server JS developers.
On top of that, there is a NON-CommonJS like module system which basically creates a Proxy object per each imported module so that you don’t accidentally pollute the global context, you actually pollute the module itself with undesired exports.
Every demo I’ve seen, even directly from the source, apparently doesn’t care about undesired exports per module, and there’s not a single word about that in the related documentation. I’m kinda new to GJS so I don’t feel that confident about filing bugs, but the TL;DR version of its weird behavior is the following.
// file module.js
// will pollute the module export
var module = "module.js";
// will pollute the module export too
this.method = function () {
return 'Hi, I am the imports.' + module + ' result';
};
// file main.js
imports.searchPath.push('.');
// the method is exported
print(imports.module.method());
// and so is the variable
print(imports.module.module);
// "module.js"
The problem is that every GJS module imports Gtk and other modules, so that every module accidentally exports every single constant or variable defined within them. While this is practically not such huge issue, unless you are creating a module introspection library that will be inevitably full of false-positives, I think is a very dirty approach to modules in general.
A better GJS module style
Playing around with GJS code and modules, I’ve found this approach way cleaner and superior than every module I’ve read so far.
// destructuring a sandboxed module
const {
methodA,
propertyB,
methodC
} = (function () {
// one or many imports
var Gtk = imports.gi.Gtk;
// the export that will be destructured
return {
methodA: function () { Gtk.init(null, 0); },
propertyB: 12345,
methodC: function () { Gtk.main(); }
};
}());
No accidental module pollution, a clean way to group the export, a closure to import or do whatever we need in there. Anyway, this is a very specific GJS issue I hope they will solve or put a word in the documentation soon.
Alternatively, the same approach used with Nashorn and others would also work quite well, granting compatibility with CommonJS.
// module exported witohut accidental pollution
(function (exports) {'use strict';
// will not pollute the module context
var module = "module.js";
// will export everywhere the `method` function
exports.method = function () {
return 'Hi, I am the load("' + module + '") result';
};
}(this));
As we can see, once again using this
to export wins in terms of portability.
Bringing in CommonJS
Having to deal with all these different ways to import/export modules is a real mess.
Having an incompatible ES2015 specification that doesn’t work in engines already based on CommonJS surely doesn’t help neither.
I personally wish “ES.future“ will bring in CommonJS too, beside current export
syntax, so that every environment can finally converge without breaking current modules ecosystem.
Meanwhile, we can at least normalize the CommonJS approach pretty much everywhere. How? Using this
to export at least one and one only utility: the require
one! Please note this is a simplified script for this post purpose only.
// require.js
(function (context) {'use strict';
// avoid redefinition of the require function
// in case this is a global context
if ('require' in context) return;
var
// will meomize every required module
cache = Object.create(null),
// in charge of actually evaluating modules
executeAndCache = function (file) {
var
// by default, each module has an exports
// reference object: it's there to export stuff!
exports = {},
// however, there is also a module reference
// which has an exports property that points
// to the very same object
module = {exports: exports, id: file}
;
// the file will be evaluated on the global scope
// using `exports` reference as top level context
Function( // each module has 3 references
'require', // the require utility
'exports', // the exports object
'module', // and the module one
read(file) // the content to evaluate "sandboxed"
// eventually in charge of using
// "use strict" at its very beginning
// in order to avoid accidental
// global scope/context pollution
).call(
exports, // exports will be top level context
require, // require will be passed as utility
exports, // there is an exports reference too
module // and there is also a module reference
);
// cache the exported object
// we don't cache directly exports
// because if someone has redefined it
// within the module content, we gonna
// trash the exports object and use
// whatever was exported instead
// e.g. module.exports = function () {};
// to export directly a class or a function
cache[file] = module.exports;
// return the module and from next time on
// simply serve the cached one
return cache[file];
},
// used to read a file content
read = (context.java && // available in Nashorn
function (file) {
return new java.lang.String(
java.nio.file.Files.readAllBytes(
java.nio.file.Paths.get(file)
)
);
}) ||
context.readFile || // available in JSC
context.read || // available in SpiderMonkey
(typeof imports === 'object' &&
function(file){ // available in GJS
return imports.gi.GLib.file_get_contents(file)[1];
}) ||
function (file) { // browsers (sync) fallback
var xhr = new XMLHttpRequest;
xhr.open('GET', file, false);
xhr.send(null);
return xhr.responseText;
}
// read can be eventually normalized
// for pretty much every other environment
;
// the exported require function
context.require = require;
function require(file) {
if (file.slice(-3) !== '.js') file += '.js';
// optional logic to path-normalize the module
// then return the cached result
// or execute the module in a "sandbox"
// and store it in the cache
return cache[file] || executeAndCache(file);
}
// in case you'd like to use require in ES2015 too
// try { eval('export default context.require'); }
// catch (notES2015) { /*\_(ツ)_*/ }
}(this)); // <= we are going to use `this` context
// in order to export the require()
All we need to do in non-CommonJS environments is to import or load such file at the very beginning.
// content of module.js
this.method = function () {
return 'Hi, I am require("' + module.id + '").method()';
};
// Nashorn, SpiderMonkey, JSC
load('require.js');
require('./module').method();
// Hi, I am require("./module.js").method()
// GJS as main.js file (gjs main.js)
imports.searchPath.push('.');
var require = imports.require.require;
print(require('./module').method());
// Hi, I am require("./module.js").method()
// well, of course node.js
require('./module').method()
// Hi, I am require("./module.js").method()
Good, we can now use any npm module that is based on JS and not node.js specific tasks!
Normalizing Browsers compatible with ES2015
While we all dream about one/universal JS, there are many differences between a trusted environment, the server, and a non trusted one, which is usually the browser. In the latter case, the network status is also a major concern, and blocking while loading, as opposite of bundling all modules, doesn’t seem like a good idea, unless done in an intra-net environment or to quickly prototype some project.
Accordingly, the only reasonable way to load modules in a CommonJS way on browsers would be running main applications within an async function or, generally speaking, a generator.
// require.js for ES2015 compatible browsers
var cache = Object.create(null);
function loadInCache(file) {
return (cache[file] = new Promise(function (res, rej) {
var xhr = new XMLHttpRequest;
xhr.open('GET', file, true);
xhr.send(null);
xhr.onerror = rej;
xhr.onload = function () {
var
exports = {},
module = {exports: exports, id: file}
;
try {
Function(
'require', 'exports', 'module',
xhr.responseText
).call(
exports,
require, exports, module
);
res(module.exports);
} catch(o_O) { rej(o_O); }
};
}));
}
// require as explicit export name
export function require(file) {
if (file.slice(-3) !== '.js') file += '.js';
return cache[file] || loadInCache(file);
};
// also as default
export default require;
At this point, everything we need to import in a non blocking way should be in place. The only missing bit is a generator handler like the classic async
one, as shown and described in this page.
// assuming there is an async module
import * from 'async';
// and there is a require one
import * from 'require';
async(function *() {
var module = yield require('./module');
alert(module.method());
});
// eventually, enabling parallel downloads
async(function *() {
let [
moduleA,
moduleB,
moduleC
] = yield Promise.all([
require('./moduleA'),
require('./moduleB'),
require('./moduleC')
]);
});
We have now the ability to use npm
modules even through most updated browsers and in a non blocking way … cool uh?
As Summary
As we’ve seen, the most de-facto cross environment way to export a library is through the this
reference, but having a common way to define modules through an exports
reference in every environment is definitively a better option and we should probably go for it.
The npm
registry in this case becomes virtually the source for any sort of JavaScript based project, and we could start publishing even Desktop based Applications and UIs via GJS and Gtk3 based JavaScript modules … and that would be absolutely marvelous!