Mozilla Rhino CommonJS module support

Friday, September 30, 2011 Posted by Ruslan Matveev
Good news, last version of Mozilla Rhino (1.7R3) introduces a fully compliant CommonJS module implementation, and in this blog post I'm going to give you a brief introduction on how to use it in your projects. Okay, so how do we normally manage dependencies in the JavaScript code that is running under Mozilla Rhino? Normally when you want to include the contents of another JavaScript file (for example some library), you call load() function (provided by Rhino shell) and pass the name of the file that you want to include as an argument:

// include XSLTProcessor library
load('XLSTProcessor.js');
// use XSLTProcessor class
var xsltProcessor = new XSLTProcessor('test.xsl');

This code will work as you expect, however it has couple of problems: load() function always resolves it's argument against the current working directory (i. e. directory of the java process), that means that no matter what kind of directory structure you have in your project, you'll always have to keep in mind that the dependency path is relative to the working directory, which is not very convenient when you want to create something more or less complex.

Second problem is that load() function doesn't return anything, it simply executes provided JavaScript file in the global context (very similar to including a JavaScript file using the script tag in the web browser), so the only way how the external library can expose it's public interface, is to attach it to the global object, which is again imposes certain limitations (allows two conflicting libraries to overwrite each other in case if they have same name).

In one of my previous blog posts I was looking into RequireJS dependency manager that solves similar problems for the JavaScript code that runs in the web - browser. So my first idea was to use RequireJS instead of built - in load() function, but the problem is that RequireJS was designed to work in the web browser and to load the dependencies asynchronously, so it didn't work for me (at the end I've got it working using Envjs environment simulator, but that wasn't quite as fast as I would expect it to be), and I had to look for some other solution.

I've spent about a day of hacking it from both JavaScript or Java side but either way it was very far from ideal. At one moment I've made a typo in the arguments list that I use for invoking Rhino shell and if this happens Rhino prints out the help which contains the list of all possible arguments. This is how I found out that Mozilla Rhino has some CommonJS support. I went to check Rhino Shell documentation and I couldn't find anything that is mentioning CommonJS in there, but I knew that the RequireJS module pattern was inspired by CommonJS module pattern, so I've decided that is worth to check everything myself.

Introducing CommonJS

So if you type "java -jar js.jar -help" in your console, then you'll see that there are 3 arguments that is responsible for handling CommonJS modules (the rest of the arguments is not important in this particular case):

-main [module]         Set CommonJS main module id or file name.
-modules [path-or-url] Add path or URL to the CommonJS modules search path.

Normally when you want to run some JavaScript, you have to invoke Rhino like this: "java -jar js.jar myScript.js", but now there is another way of doing that and that is using "main" argument to pass CommonJS main module id or file name. The good thing is that you don't have to make any special changes to your script in order to run it as a CommonJS main module, so in order to run "myScript.js" as a CommonJS main module, you have to invoke Rhino like this: "java -jar js.jar -main myScript.js". Okay it works absolutely the same way, so what is the point? The point is that now "myScript.js" can use additional functionality provided by the CommonJS module API and there are couple of interesting things that you might wanna look at.

CommonJS introduces function called "require" that is used for handling the dependencies in the new way, at the beginning of this article I've described the limitations imposed by using standard "load" function. First of all, require() resolves it's arguments relative to the caller, instead of the current directory of the java process (note that it only happens if you prepend your file name with "./"), which solves all the problems with the dependency paths, so now you can get rid of stupid "semi absolute" paths:

// contents of lib/js/main.js
require('./myLibrary.js');

additionally you can specify the search path for all your modules using "-modules" argument while invoking Rhino, like this: "java -jar js.jar -modules my/modules/path -main main.js", which will let you to forget about the dependency paths once and forever.

Second interesting feature is that now you can determine the path to the current JavaScript file using "uri" property of the "module" object (part of the CommonJS modules standard):

// print current script file name
print(module.uri);

which makes it even more cool than I've expected, so at this point I have realized that I want to use all those features provided by CommonJS API for all my modules and libraries that runs under Mozilla Rhino. The only thing left is... well what is CommonJS module and how to define it?

CommonJS module pattern

CommonsJS modules are executed in so called "hermetic eval" which means that there is no global object available in the module scope, so if you will try to use your old code as it is, you will soon realize, that functions, objects and variables that is defined in your code will not be available to the caller. In order to make something available to the caller you'll have to put it into "exports" namespace:

// export variable
exports.PI = 3.14;

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

This module makes one variable (PI) and a function (print) available to the outside world. It can be loaded using require:

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

Conclusion

CommonJS module support in Mozilla Rhino brings it to the new level, allowing you to create complex, easily maintainable solutions. I'm really surprised that some parts of Rhino documentation is out of date and do not describe this amazing feature at all. I hope that someone from Mozilla Rhino project will read it and update do something about it. In the mean time you can experiment with it yourself. Find more information on CommonJS website and in this presentation.

Post a Comment