Modular JavaScript in the browser with CommonJS Compiler
JavaScript was designed as a script language that is easy to embed in a larger host system and meant to manipulate the objects of the host system. With the advance of HTML5 formerly mostly static web-pages are turning into sophisticated web-applications. Now we expect JavaScript code to be scalable and modular. But how when JavaScript has no built-in facilities to combine distinct scripts?
Surely, you can insert a script element into the DOM and therefore have outer scripts loaded dynamically. Then you will have to deal with asynchronously loaded scripts and resolve all the required dependencies prior to running any depended code. In fact there are plenty of libraries that can do it for you (http://wiki.commonjs.org/wiki/Implementations). While going AMD you will quickly realize that every module makes a separate HTTP request that badly affect the application performance. Well you can use the tools like r.js to combine modules in a single build. Still it brings an entire library into the resulting code. With compiled the modules we hardly need so much of extra complexity. After the compiler enclosures the modules in unique scopes, we only need a function to manage access to these contexts. Let’s take CommonJS module format. I do love how it is implemented in NodeJS – pretty concise, but incredibly capable. It introduces module object representing a module and available in every module scope. So the module scope can look like that:
_require.def( "module-id", function( require, exports, module ){
// Module constructor
var moduleObj = {};
module.exports = moduleObj;
return module;
});
Bow we need the require function to give access from module to module. Following NodeJS when require is called the first time the module is being constructed. Every second call takes the exported object from the cache. That can be implemented with the following code:
/**
*
* Define scope for `require`
*/
var _require = (function(){
var /**
* Store modules (types assigned to module.exports)
* @type {module[]}
*/
imports = [],
/**
* Store the code that constructs a module (and assigns to exports)
* @type {*[]}
*/
factories = [],
/**
* @type {module}
*/
module = {},
/**
* Implement CommonJS `require`
* http://wiki.commonjs.org/wiki/Modules/1.1.1
* @param {string} filename
* @returns {*}
*/
__require = function( filename ) {
if ( typeof imports[ filename ] !== "undefined" ) {
return imports[ filename ].exports;
}
module = {
id: filename,
filename: filename,
parent: module,
children: [],
exports: {},
loaded: false
};
if ( typeof factories[ filename ] === "undefined" ) {
throw new Error( "The factory of " + filename + " module not found" );
}
// Called first time, so let's run code constructing (exporting) the module
imports[ filename ] = factories[ filename ]( _require, module.exports, module );
imports[ filename ].loaded = true;
if ( imports[ filename ].parent.children ) {
imports[ filename ].parent.children.push( imports[ filename ] );
}
return imports[ filename ].exports;
};
/**
* Register module
* @param {string} filename
* @param {function(module, *)} moduleFactory
*/
__require.def = function( filename, moduleFactory ) {
factories[ filename ] = moduleFactory;
};
return __require;
}());
// Must run for UMD, but under CommonJS do not conflict with global require
if ( typeof require === "undefined" ) {
require = _require;
}
This design expects the compiler to replace ids given in require calls with the fully resolved file names (relative to the project directory). So the file names become unique identifiers for the modules. Besides, compiler must detect the require calls combinations causing infinite loops.
Using RequireJS Compiler
I released the compiler on GitHub https://github.com/dsheiko/cjsc . It is a NodeJS package that can be used as easy as it:
./cjsc main-module.js build.js
Let’s write a few modules to see what it does.
./main.js
console.log( "main.js running..." );
console.log( "Imported name in main.js is `%s`", require( "./lib/dep1" ).name );
console.log( "Getting imported object from the cache:" );
console.log( " imported name in main.js is still `%s`", require( "./lib/dep1" ).name );
./lib/dep1.js
console.log( "dep1.js running..." );
console.log( "Imported name in dep1.js is `%s`", require( "./dep2" ).name );
module.exports.name = "dep1";
./lib/dep2.js
console.log( "dep2.js running..." );
module.exports.name = "dep2";
After we compile the main.js module and fire up the derived build in the browser, we get the following output:
main.js running...
dep1.js running...
dep2.js running...
Imported name in dep1.js is `dep2`
Imported name in main.js is `dep1`
Getting imported object from the cache:
imported name in main.js is still `dep1`
Well, the dependencies resolved by given ids based on relative paths, module constructors ran when required and only once – everything went as under NodeJS.
Supporting RequireJS modules
What if we call for a UMD module? Let’s try:
./main.js
console.log( "%s is running...", module.id );
console.log( "%s imports %s", module.id, require( "./umd/module1.js" ).id );
./umd/module1.js
// UMD boilerplate according to https://github.com/umdjs/umd
if ( typeof module === "object" %26%26 typeof define !== "function" ) {
/**
* Override AMD `define` function for RequireJS
* @param {function( function, Object, Object )} factory
*/
var define = function ( factory ) {
module.exports = factory( require, exports, module );
};
}
define(function( require, exports, module ) {
console.log( "%s is running...", module.id );
return { id: module.id };
});
The build output:
./umd.js is running...
./umd/module1.js is running...
./umd.js imports ./umd/module1.js
Everything is fine.
Automating build process
On the development environment we can have the fooling Grunt configuration:
Gruntfile.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-cjsc');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.initConfig({
pkg: grunt.file.readJSON( "package.json" ),
cjsc: {
debug: {
options: {
sourceMap: "*.map",
sourceMapUrl: "/build/js/",
minify: false
},
files: {
"build/js/main.js": "sources/js/main.js"
}
},
build: {
options: {
minify: true,
banner: "/*! <%= pkg.name %> - v<%= pkg.version %> - " +
"<%= grunt.template.today(\"yyyy-mm-dd\") %> */"
},
files: {
"build/js/main.js": "sources/js/main.js"
}
}
},
watch: {
options: {
livereload: false
},
build: {
files: ['sources/js/**/**/*.js'],
tasks: ['cjsc']
}
}
});
grunt.registerTask( "default", [ "cjsc" ]);
};
And development dependencies in package.json
"devDependencies": {
//..
"grunt-contrib-watch": "~0.4.4",
"grunt-contrib-cjsc": "*"
}
Now when we are working on the development environment we can run
grunt debug
Since we configured cjsc to generate source map we are going to have all the breakpoints and console messages mapped to the original sources.
When we need a build for production environment we do:
grunt build
Now we also can make Grunt compiling the build.js automatically every time any modules change:
grunt watch
So as you see you can write CommonJS modules and have them running in the browser without any additional library. If you ask me about Browserify – it looks awesome. Honestly I wasn’t simply aware about that solution while building RequireJS compiler.