Mozilla Rhino + CommonJS - making a smart modules

Monday, October 15, 2012 Posted by Ruslan Matveev
Hi all, in my last post about Mozilla Rhino CommonJS support I've described the basics. In this post I'll explain you how to organize your code so it can work when there is no CommonJS sandbox enabled. Imagine that we have decided to organize all our Mozilla Rhino libraries as CommonJS modules, but what if there is an old project that has no knowledge of this change, and it still relies on old good load() function that is importing everything into a global namespace? The question is: is that possible to make a library that can work same way with and without CommonJS support?
For example, we have a library myLibrary, which is organized as CommonJS module:

// export variable
exports.PI = 3.14;

// export function
exports.log = function(value) {
    print('log:', value);
};

To load this library we would normally use this code:

// load "myLibrary" module
var myLibrary = require('./myLibrary.js');
// make use of it's interface
myLibrary.log(myLibrary.PI);

Okay cool, now what is going to happen if we will use load() function instead of require() to load our myLibrary.js? Can we still use our module somehow? ... of couse not. If we run our example without enabling CommonJS support, we'll get a runtime exception, because myLibrary uses exports object which is not available in non - CommonJS environment. In order to solve this problem we need a module that can adjust itself, depends on CommonJS availability:

(function(namespace) {

    /**
     * depending on CommonJS support
     * namespace argument will contain
     * a reference to exports or to global
     */

    // export variable
    namespace.PI = 3.14;

    // export function
    namespace.log = function(value) {
        print('log:', value);
    };

})(
    // check if we have exports object available
    // means that CommonJS support is enabled
    typeof(exports) !== 'undefined' && exports ||
    // no CommonJS support, use global namespace
    function() { return this; }.call(null)
);

Now we can call our module as plain - javascript module (java -jar js.jar example.js):

// load "myLibrary" module
load('./myLibrary.js');
// make use of it's interface
log(PI);

And as CommonJS module (java -jar  js.jar -main example.js):

// load "myLibrary" module
var myLibrary = require('./myLibrary.js');
// make use of it's interface
myLibrary.log(myLibrary.PI);

Okay, now what if we try to load our module with CommonJS enabled, but using load() function (java -jar  js.jar -main example.js):

// load "myLibrary" module
load('./myLibrary.js');
// make use of it's interface
log(PI);

It won't work, because in our wrapper if we have exports object available, we assume that module is requested as CommonJS module and we should export everything into exports namespace. In order to react on this situation, we have to adjust our wrapper so it can check whether or not our module was requested using require() or load(), and this is actually tricky:

(function(namespace) {

    /**
     * depending on CommonJS support
     * namespace argument will contain
     * a reference to exports or to global
     */

    // export variable
    namespace.PI = 3.14;

    // export function
    namespace.log = function(value) {
        print('log:', value);
    };

})(function() {

    // check if we have exports object available
    // means that CommonJS support is enabled
    if (typeof(exports) !== 'undefined' &&
        // check if current module id is the same as global module id
        // means that exports point us to the current module namespace
        // but not to the global module namespace
        this.module.id !== module.id) {
        return exports;
    }

    // no CommonJS support, or someone trying to load() our module
    // in the environment where CommonJS is available, use global namespace
    return this;

}.call(null));

Now our module can be used in all the cases which is described above: load, CommonJS + load, CommonJS + require. In real life, I'm using this pattern for wrapping JavaScript version of Histone template engine, which can be used as CommonJS module (Mozilla Rhino and Node.js), RequireJS module or plain javascript - library, all in one single distribution.

Post a Comment