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 …
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
.
method = function () { };
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.
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,
id: 'package/lib'
};
F.call(exports,
require,
exports,
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.
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.
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.
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”.
var module = "module.js";
this.method = function () {
return 'Hi, I am the load("' + module + '") result';
};
load('module.js');
module;
method();
Accordingly, if we’d like to use load
in order to bring in modules, we might consider writing them in the following way:
(function (exports) {'use strict';
var module = "module.js";
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.
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.
var module = "module.js";
this.method = function () {
return 'Hi, I am the imports.' + module + ' result';
};
imports.searchPath.push('.');
print(imports.module.method());
print(imports.module.module);
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.
Playing around with GJS code and modules, I’ve found this approach way cleaner and superior than every module I’ve read so far.
const {
methodA,
propertyB,
methodC
} = (function () {
var Gtk = imports.gi.Gtk;
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.
(function (exports) {'use strict';
var module = "module.js";
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.
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.
(function (context) {'use strict';
if ('require' in context) return;
var
cache = Object.create(null),
executeAndCache = function (file) {
var
exports = {},
module = {exports: exports, id: file}
;
Function(
'require',
'exports',
'module',
read(file)
).call(
exports,
require,
exports,
module
);
cache[file] = module.exports;
return cache[file];
},
read = (context.java &&
function (file) {
return new java.lang.String(
java.nio.file.Files.readAllBytes(
java.nio.file.Paths.get(file)
)
);
}) ||
context.readFile ||
context.read ||
(typeof imports === 'object' &&
function(file){
return imports.gi.GLib.file_get_contents(file)[1];
}) ||
function (file) {
var xhr = new XMLHttpRequest;
xhr.open('GET', file, false);
xhr.send(null);
return xhr.responseText;
}
;
context.require = require;
function require(file) {
if (file.slice(-3) !== '.js') file += '.js';
return cache[file] || executeAndCache(file);
}
}(this));
All we need to do in non-CommonJS environments is to import or load such file at the very beginning.
this.method = function () {
return 'Hi, I am require("' + module.id + '").method()';
};
load('require.js');
require('./module').method();
imports.searchPath.push('.');
var require = imports.require.require;
print(require('./module').method());
require('./module').method()
Good, we can now use any npm module that is based on JS and not node.js specific tasks!
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.
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); }
};
}));
}
export function require(file) {
if (file.slice(-3) !== '.js') file += '.js';
return cache[file] || loadInCache(file);
};
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.
import * from 'async';
import * from 'require';
async(function *() {
var module = yield require('./module');
alert(module.method());
});
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 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!