browser-modules
The browser-modules library is a synchronous, in-browser implementation of CommonJs. If you've landed here by chance, then it's likely your needs might be better served by browserify.
Installing
browser-modules provides a realm
object, having define()
and require()
methods that you can use to define and require things with, for example:
realm.define('stuff', {});
var stuff = realm.require('stuff');
Typically however, and to allow browser-modules to be CommonJs compliant, you will install the realm
first:
realm.install();
define('stuff', {});
var stuff = require('stuff');
Warning: Having a global define()
method may be enough to trigger AMD definitions in some buggy UMD modules — they should really check for define.AMD
, but not all of them do.
Defining Modules
In addition to being able to define and require stuff, you can also define and require JavaScript modules, for example:
define('acme/module1', function(require, exports, module) {
exports.stuff = 'more stuff';
});
While browser-module's define()
method looks a little like the CommonJs Wrapper described in the RequireJS documentation, it's synchronous, so that everything that will be required must be pre-loaded, and pre-defined.
One module can require another module internally, for example:
define('acme/module2', function(require, exports, module) {
var acmeModule1 = require('acme/module1');
exports.stuff = acmeModule1.stuff + ', etc';
});
and this can also be written using a relative require-path since, logically, both modules reside within 'acme', for example:
define('acme/module2', function(require, exports, module) {
var acmeModule1 = require('./module');
exports.stuff = acmeModule1.stuff + ', etc';
});
Node.js Modules
browser-modules supports Node.js style modules too, so we could instead define our two modules like this:
define('acme/module1', function(require, exports, module) {
module.exports = 'more stuff';
});
define('acme/module2', function(require, exports, module) {
var moreStuff = require('./module1');
module.exports = moreStuff + ', etc';
});
Module Transpilation
The browser-modules library is designed to be paired with an out-of-process (e.g. file-watcher or web-server) module transpiler. So that, a CommonJs module that looks like this:
var acmeModule1 = require('./module');
exports.stuff = acmeModule1.stuff + ', etc';
can trivially be transpiled into this:
define('acme/module2', function(require, exports, module) {
var acmeModule1 = require('./module');
exports.stuff = acmeModule1.stuff + ', etc';
});
where the line numbers in the transpiled code are only one out, compared with the original code. Specifically, we created browser-modules for BladeRunnerJS, but it can equally well be used in other systems too.
Circular Dependency Detection
The browser-modules library provides a circular dependency feedback mechanism that would not be possible with a 100% CommonJs compliant implementation. Consequently, while this mechanism does not affect Node.js modules, it may prevent what would otherwise be valid CommonJs modules from loading, instead leading to circular dependency errors being thrown.
Specifically, browser-modules will throw a circular dependency error if a CommonJs module is loaded that forms part of a circle, if that module has not exported any values before it requires the next module within the circle.
For example, given the following two modules:
define('module1', function(require, exports, module) {
exports.stuff = 'stuff';
require('module2');
});
define('module2', function(require, exports, module) {
require('module1');
exports.stuff = 'stuff';
});
then in a fully CommonJs compliant system these modules could be loaded in any order, but with browser-modules it will only work provided 'module1' is required first. If 'module2' is required first then the following error will be generated:
Circular dependency detected: module2 => module1 -> module2
Although relaxing CommonJs compliance may seem strange, we believe there are good grounds for doing it. The rationale is as follows:
- We prefer using Node.js style modules to CommonJs modules, and for Node.js modules this change is wholly positive, since it makes it debugging circular dependency issues trivial.
- It's trivial to 'fix' a CommonJs module so that it works again when an error is encountered.
- It complements BladeRunnerJS, which exports libraries as a bag-of-classes rather than as a single encapsulated module, so that circular dependencies can be happened upon in third-party libraries.
Fixing Circular Dependencies
Although these aren't officially endorsed classifications, we find it useful to consider that there are three distinct types of inter-module dependency:
- Pre-export define-time dependencies.
- Post-export define-time dependencies.
- use-time dependencies.
A module's define-time dependencies are any dependencies that are expressed (via require()
invocations) at the point the module is exporting whatever it exports, whereas use-time dependencies are the remaining dependencies that aren't required until the module is actually used.
Additionally, if a dependency is required before the module has exported anything then we consider it as being a pre-export dependency, whereas if it's required after a module has exported then it's a post-export dependency.
The following example hopefully makes these distinctions clear:
define('some-module', function(require, exports, module) {
require('pre-export-dependency');
exports.stuff = function() {
require('use-time-dependency');
return 'stuff';
};
require('post-export-dependency');
});
When a circular dependency error occurs, you will see an error message that looks like this:
Circular dependency detected: moduleA => moduleB -> moduleC => moduleD -> moduleA
Regarding these error messages:
- pre-export define-time dependencies are represented by double-bar arrows.
- post-export define-time dependencies are represented by single-bar arrows.
- use-time dependencies will never appear, since they prevent circular dependencies from becoming problematic.
Additionally, although there may be a number of pre-export dependencies within the circle (i.e. =>
), the first dependency will always be a pre-export dependency, since if it was either a post-export dependency or a use-time dependency then it would cease to be problematic.
Therefore, you can fix circular dependencies by either:
- Introducing a use-time dependency anywhere within the circle.
- Ensuring that the first module required within the circle has a post-export dependency.
Sub-Realms
To aid testing, browser-modules supports hierarchical realms, providing comparable functionality to what mockery provides to Node.js developers. This allows developers to install a sub-realm for the duration of a test, where arbitrary definitions within the realm can be replaced for the duration of that test, leaving the original realm unchanged and unaffected.
Specifically, sub-realms must be used exactly as follows:
- Install the sub-realm.
- Override any definitions within the realm.
- Start requiring things.
- Un-install the sub-realm.
Consider the following test for example, that tests the 'acme/module2' module defined earlier within the Defining Modules section:
describe('acme/module2', function() {
var subrealm;
beforeEach(function() {
subrealm = realm.subrealm();
subrealm.install();
});
afterEach(function() {
subrealm.uninstall();
});
it('appends ", etc" to the value in module1', function() {
define('acme/module1', function(require, exports, module) {
module.exports = 'less stuff';
});
expect(require('acme/module2')).toBe('less stuff, etc');
});
});
If any of the steps are done out of order, then things will start to go wrong.
For example, if you start requiring modules before you've finished overriding all the definitions, then the transitive dependencies of the modules you've required may well cause you to end up using the original definitions of modules you planned to override.
Augmenting Definitions WIthin Sub-Realms
If you need to augment a module within a sub-realm, rather than completely replace it, then you need some way to get a reference the original definition in the sub-realm, even though you've overridden it. This can be done using the subrealm.recast()
method, as follows:
define('acme/module1', function(require, exports, module) {
var moreStuff = subrealm.recast('acme/module1');
module.exports = moreStuff + '?';
});