Modularizer

A component which encapsulates basic modularization using the AMD API but in a self contained package rather than on the global object

View project on GitHub

Modularizer Build Status

Modularizer was born out of a need for a self contained module packager which could co exist with other package managers.It is a component which encapsulates basic modularization using the standard AMD API, as implemented by RequireJS, but in self contained "packages" of modules, rather than one package which resides on the global object.

To learn more about the history behind Modularizer please take a look at the following article by the author of the Modularizer library: Why on earth would you create yet another package manager for Javascript?.

Defining a package

Modularizer packages provide the ability to keep your modules a sandboxed environment to exist, without the danger of multiple modules from different sources clashing due to having identical names. Think of them as a namespace, if that helps.

Creating a package

Creating a new package couldn't be simpler. It is done by creating a new instance of the Modularizer Constructor function, like so:

  
    var myModularizer = new Modularizer();
  

From that moment you have a package, accessible through the myModularizer variable, into which you can define modules and resources.

Configuring a package

The Modularizer has a default configuration which dictates the behaviour of a package, but all of these can be overridden when the package is created.

The following is the default config:

  
  // default config
  Modularizer.config = {
    /**
     *  Should we use console to log messages?
     *  True/False
     *  */
    debug: false,
    /**
     * Cache Buster value to add to all JS scripts added by the loader.
     * To allow maximum flexibility the Cache buster makes no assumptions other than it's location being
     * at the end of the URI.
     * This allows you to make it a parameter, like '?cb=XXX', or any other postfix you wish on the URL.
     * We found it comfortable to use the hash of the file's last Git commit, which promised us a clear cache
     * every time the file was changed, rather than every deployment.
     * */
    cb: false,
    /**
     * A loader function is provided by default, but we do try and detect jQuery first and use that instead if possible.
     * The *loader* function takes either a JS function or a Boolean.
     * True - This value tells Modularizer that it should use a loader, but doesn't tell it which. If the *detectJQuery* config
     *        is set to True, then Modularizer will first try and find jQuery and use that, otherwise it uses an internal file loader.
     * False - Don't try and load missing files. If the required Module isn't defined manually - Modularizer will treat it as a lost
     *        cause and won't try and fetch it on it's own.
     * func() - If a function is provided Modularizer will call it every time a file needs to be fetched for a module's definition.
     *        The syntax for this function is thus: function(filePath, callback)
     *        filePath : The URL of the file that needs to be loaded
     *        callback : A callback for the loader to call when it has fetched the file
     * */
    loader: true,
    detectJQuery: true,
    /**
     *  Should timeouts be narrowed down?
     *  When the modularizer is told to fetch a module definition it injects the resource which, it believes,
     *  contains the module and waits for the module definition to actually take place.
     *  If it waits too long, it triggers a "timeout" and throws an exception.
     *  How long it waits is defined by the *timeout* config, which by default, sets a 3 second timeout.
     **/
    timeout: 3000,
    /**
     *  Once a timeout is triggered we can simply throw an error, for the system to deal with, or we can go digging in
     *  the definitions to figure out why the timeout took place.
     *  There are several different reasons which can cause a timeout - a missing dependancy, or a circular reference causing
     *  a deadlock.
     *  Figuring out these reasons can take time and be a complex analysis, so for critical environments, or when a missing module
     *  can be ignored with an appropriate fallback, it may be beneficial to skip this analysis.
     *  Hence, by default, we set this option to false, which means a generic error will be thrown specifying the module's name
     *  but not the missing dependancy or circular reference.
     *  It is recommended to set this configuration to True for Dev environments where it more important to identify the actual
     *  problem.
     *  */
    narrowDownTimeouts: false,
    /***
     * Tag base is the base URL which is used for all resources.
     * When a resource is defined, for example, at 'view/homepage/header.js' and the base configuration is set as 'http://www.gidi.io/static/',
     * the Modularizer loader will specify the whole URL, 'http://www.gidi.io/static/view/homepage/header.js', as the resource's
     * location.
     */
    base: '',
    /**
     * How should requirements which are defined for a module which is unknown be treated? Strictly or Leniently?
     * True - yes, treat strictly and hence throw an error when an unknown module is required.
     * False - no, treat leniently, and hence only throw an error when an actual requirement is made, rather than defined.
     *
     * This is very comfortable for situations where you have dynamically defined modules, which are created at runtime.
     * We found this very useful for injecting Mustache Template definitions which are fetched externally from Modularizer,
     * but required by modules inside of our Modularizer packages.
     * */
    strictRequirment: true,
    /***
     * A string or array of strings with regex matches which will be treated as exceptions for the strictRequirment mode.
     * This means that if these values are matched with a required resource, then they will be treated in the opposite to the specified mode.
     * So if strictRequirment is set to true, the matched modules will be treated leniently and visa versa.
     */
    requirementExceptions: null,
    /***
     * A string or array of strings with regex matches which will be treated as additional exceptions for the strictRequirment mode.
     * This pattern is different than the requirementExceptions only in one aspect - that it applies to the timeout in addition to the requirment.
     * This means that, unlike the requirementExceptions patterns, these modules will be ignored in the timeout as well as having lenient requirment,
     * so when the timeout occurs, their absence will not cause the package to be deemed invalid.
     */
    optionalRequirment: null
  };

When you create your package you can override these configurations like so:

  
    var myModularizer = new Modularizer({
      // Very harsh cache buster, every file request will break the cache ;)
      cb : '?cb=' + (new Date).getTime(), 
      loader: function(filePath, callback){ ... },
      // Ignore jQuery
      detectJQuery: false
      // 5 seconds
      timeout: 5000  
    });
  

Defining a resource

A resource is an external file that contains a module or a several modules. We can tell our package to expect a specific file to contain a specific module, like so:

  
    myModularizer.register("view/homepage/header.js").defines('View.Header');
  

Which means Modularizer will fetch the file header.js when someone requires the module View.Header.

A resource can also contain several modules, such as:

  
    myModularizer.register("app/booking.js")
      .defines('Model.BookingBasket')
      .defines('Model.PaymentMethod')
      .defines('Collection.Products')
      .defines('Controller.API.Booking')
      .defines('View.Booking.Header')
      .defines('View.Booking.CardDetails')
      .defines('View.Booking.Submission');
  

This tells Modularizer that once someone requires any one of the modules in that resource, it should load the file app/booking.js which will result in all those modules being defined in the package.

Defining a module

In the same way that calling RequireJS's define() function defined a module in Require's registry, so will calling the define() method of your package define that module within that package, and will only be visible to other modules within that package.

Defining a module is the same in Modularizer as in RequireJS, with one caveat - it needs to know what package to define the module into. This caveat is why Modularizer is more suited to application architecture, than to generic package management. The beauty of RequireJS is that it is a generic, all around, package manager. On the other hand, Modularizer, is designed to allow sand boxed module packages for an application architecture.

The definition is done like so:

  
    myModularizer.define('View.HelloMessage',function(){
      return React.createClass({
        render: function() {
          return <div>Hello {this.props.name}</div>;
        }
      });
    });
  

The above example defines a module, identified as View.HelloMessage, which is actually a React View. Whenever the application needs to render this type of view component, it will require the 'View.HelloMessage' module and in return it will receive the React class which can be used to create instances of that module.

A definition uses the following signature: function(module, dependancies, callback)

  1. module The name of the module. Must be a String.
  2. dependancies Optional. The dependancies this module has. An array of strings, each string a module name, which will be fetched before this module definition can be executed.
  3. callback A function which defines the module. This function is executed only once - the first time the module is required, and it's return value will be passed to anyone requiring the module throughout the system.

Requiring a module

When you need a module you can simply require it from the Package. If the module has already been instantiated, it is simply returned. Otherwise, if the module has already been defined, it is instantiated and returned to the function. Otherwise, if the module hasn't even been defined, it's resource is loaded. If the module is unknown Modularizer will either throw an error or oush a callback into a queue and wait for the module to be defined (depends on the configuration of the strictRequirment config).

  
    myModularizer.require('App.Init',function(ApplicationInitializer){
     //...
    });
  

The signature for require() is thus: function(dependancies, callback, context, synchronous)

  1. dependancies Either String or Array of Strings. The modules to require, which will be passed as arguments to the callback
  2. callback Function. The callback which will receive the module instances.
  3. context Object. The context in which the callback will be executed.
  4. synchronous Boolean. Should the execution be synchronous or not (by default, not, hence False). If synchronous is required (True) then an error will be thrown if a required module has not yet been defined, even if the Package knows which resources contains the module, it won't try and load the file, as that would require an asynchronous procedure.

Special features

  1. Timeout. Often we will want to track the health of our application and report an error to our Front End analytics when it take our clients too long to load files. To this effect we can set a timeout period which will throw an error when too long a period has passed since a module was required and the definition failed to reach the package. This could happen due to either a resource failing to contain a module's definition, a resource failing to load or a module's definition throwing an error and hence failing to provide an instance for the package.
  2. requirementExceptions. Sometimes you want to dynamically create a module. An example for this is when you inject Templates into your page (such as Mustache templates) and want to generate modules which will wrap the rendering of these functions. You don't want to type up an actual JS module for every template (we created a generic component which auto defined a module for each template fetched onto the page) but you still want to be able to define these template modules as requirements for your actual modules. In order to prevent these dynamically defined modules from throwing an error due to them being required but undefined, we can use a configuration called requirementExceptions to set a regex which will tell modularizer to avoid throwing an error when these modules are encountered. For example, our generic template component, defines these templates with the prefix Template. and hence every view module can require it's templates freely and when the template is injected it's module requirments will be fulfilled by modularizer and execution will continue freely.
  
  var myModularizer = new Modularizer({
        requirementExceptions: ['^Template\\..*']
  });

  //...

  myModularizer.define('View.HelloMessage',['Template.HelloTemplate'], function(Template){
        return React.createClass({
          render: function() {
            return Template;
          }
        });
  });