Skip to content

BladeRunnerJS CommonJs Compliance

thecapdan edited this page Oct 22, 2014 · 9 revisions

BladeRunnerJS CommonJs Compliance

Differences from CommonJs

BladeRunnerJS is broadly CommonJs compliant, but with some notable additions and one incompatible difference.

The additions:

  1. It supports Node.js style module.exports assignments, making it easier to work with modules that export only a single item.
  2. It includes circular dependency detection code that provide the developer a descriptive error message when invalid circular dependencies are created, rather than leaving the developer to debug the cause of the problem.
  3. It automatically globalises the contents of all NamespacedJs modules; although NamespacedJs source modules automatically globalise their dependent source modules, there are a number of other advantages to doing this:
    1. It allows any aspect and workbench index pages to continue referring to these classes using their namespaced names.
    2. Some legacy NamespacedJs classes rely on being able to access all of the classes within a 'package', which require() doesn't support.
    3. For simplicity reasons, the NamespacedJs class to CommonJs module transpiler promotes all module dependencies (including use-time dependencies) to define-time dependencies, and having a globalisation-block enables us change the order that these modules are first actualised, enabling us to avoid circular dependency issues that end-developers would otherwise have to work around.

The one incompatible difference:

  1. Although the circular dependency detection code is wholly beneficial for Node.js style modules, for pure CommonJs modules it can lead to circular dependency errors being incorrectly thrown for code that would otherwise be valid.

Why Break Compatibility?

The decision to relax strict adherence to the CommonJs specification was taken for the following reasons:

  1. Whereas Node.Js libraries are typically exported as a single module, where the classes within each library are encapsulated by the module, and where any order-dependendent circular dependency issues are hidden from the outside world, BladeRunnerJS libraries are typically 'exported' as a bag-of-classes, and so order dependent circular dependencies can be discovered by external developers depending on the order they make use of a libraries' classes, and so it therefore needs to be trivial to diagnose and work around any such issues.
  2. Practically all of the code written for BladeRunnerJS so far has used the Node.Js style, rather than the pure CommonJs style, and so developers are unlikely to be affected by this decision.
  3. Developers that do wish to use CommonJs style modules and that run into this error, will find it easy to work-around any such issues.

Fixing CommonJs Style Circular Dependencies

Provided that items within imported modules aren't de-referenced until use-time, it's impossible for CommonJs programs to have circular dependency issues when run in a CommonJs compliant system. In BladeRunnerJS, however, it is possible to experience a circular dependency error even when this is the case.

For example, consider the following two CommonJs modules, 'ping':

var pong = require('pong');
exports.log = function() {
  console.log('ping');
  pong.log();
};

and 'pong':

var ping = require('ping');
exports.log = function() {
  console.log('pong');
  ping.log();
};

and the program:

require('ping').log();

This produces constant ping, pong, ... log output in CommonJs compliant systems, but in BladeRunnerJS leads to an error with following message:

Circular dependency detected: ping => pong => ping

This can be fixed by modifying the 'ping' module so that it requires 'pong' after it has exported something, as follows:

exports.log = function() {
  console.log('ping');
  pong.log();
};
var pong = require('pong');

If we instead change the program to produce pong, ping, ... log output instead, for example:

require('pong').log();

then we would again see an error message, this time as follows:

Circular dependency detected: pong => ping -> pong

Notice how the second arrow has only a single line, whereas the first arrow has a double line. Circular dependencies that involve use-time dependencies will never lead to an error, and so the single line arrow is actually being used to denote a variation of the two types of define-time dependency that are possible:

  • pre-export define-time dependencies are required before the module has exported its value (indicated as =>).
  • post-export define-time dependencies are required after the module has exported its value (indicated as ->).

A mixed circular dependency involving both pre-export and post-export define-time dependencies may or may not work, depending on which module is first required within the circle. So that, in essence, mixed circular dependencies are essentially latent failures waiting to happen.

It is for this reason that we clearly indicate the nature of all problematic circular dependencies, making them trivial to diagnose and remedy.

Fixing Node.js Style Circular Dependencies

Node.js trades cast-iron circular-dependency support for single-export convenience and tidier looking code. Node.js users often advocate moving require() statements after module.exports to deal with issues as they are encountered. However, unless this is done consistently for all nodes in the circle, then latent errors will remain.

Due to the idiomatic style libraries are written in with Node.js, these latent errors don't pose any real risk since they can usually only be happened up on by the library's author, given that external access to a library is usually via an encapsulated module.

Unlike with pure CommonJs where there can be valid code that doesn't work on BladeRunnerJS, any Node.js style code whose dependencies are correctly resolved when running on Node.js would also work correctly in BladeRunnerJS. However, for code that does have issues, BladeRunnerJS affords much clearer error messages.

Fixing NamespacedJs Style Circular Dependencies

NamespacedJs style code pre-dates both AMD and CommonJs, and is another way of preventing libraries from clashing with each other by having classes define themselves on a unique namespace, in much the same way as is done in Java and .NET.

Consider the following class for example:

pkg.SubClass = function() {
  this.obj = new pkg.subpkg.Class();
};
pkg.SubClass.prototype = Object.create(pkg.BaseClass.prototype);
pkg.SubClass.prototype.constructor = pkg.SubClass;

To make classes like this compatible with code-bases that also contain CommonJs modules, such classes are automatically transpiled into CommonJs modules by BladeRunnerJS.

For example, the class above would be transpiled by BladeRunnerJS into a CommonJs module that effectively looks like this:

pkg.BaseClass = require('pkg/BaseClass');

pkg.SubClass = function() {
  this.obj = new pkg.subpkg.Class();
};
pkg.SubClass.prototype = Object.create(pkg.BaseClass.prototype);
pkg.SubClass.prototype.constructor = pkg.SubClass;

module.exports = pkg.SubClass;

pkg.subpkg.Class = require('pkg/subpkg/Class');

Dependency Promotion

One of the shortcomings of the NamespacedJs to CommonJs transpiler is that it promotes use-time dependencies (for example pkg/subpkg/Class) into post-export define-time dependencies. This means that dependencies that should never be able to create a circular dependency error could now cause such an error.

To prevent this from actually occurring in practice, we include a globalisation-block that requires all modules in an order that prevents such errors.

Singleton Pattern

A second shortcoming with the NamespacedJs transpiler is that it can't determine whether any of a classes methods will be invoked at define-time, and so it may incorrectly decide that some dependencies are post-export define-time when in practice they should actually be pre-export define-time. The typical use case for this are modules that return a singleton instance. To resolve this issue, a singleton-pattern (defined below) should always be used for modules that return an instance object.

Given a module like this:

pkg.Singleton = function() {
  this.obj = new pkg.subpkg.Class();
};
pkg.Singleton = new pkg.Singleton();

it should be re-factored into a 'pkg/SingletonClass' module:

pkg.Singleton = function() {
  this.obj = new pkg.subpkg.Class();
};

and a corresponding 'pkg/Singleton' module:

pkg.Singleton = new pkg.SingletonClass();

This singleton-pattern is a good thing anyway since it makes singleton objects easier to unit test as it now becomes possible to construct new instances of the underlying class, but it also causes all dependencies to be handled correctly.

Curiously, the singleton-pattern can only correctly solve the second NamespacedJs transpiler deficiency (the inability to recognise method invocation) due to the presence of the first NamespacedJs transpiler deficiency (the incorrect promotion of use-time dependencies).

Clone this wiki locally