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:

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 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!

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor