JS.Packages

JS.Class comes with a package manager that makes it really easy to load libraries into your application on demand. You can tell it which file each module in your application lives in, and what other modules it depends on, and it takes care of resolving dependencies and loading your code for you. This means your application code only needs to specify which objects it needs, rather than which scripts to download in order to run.

For example, say I want to do something with YUI’s CSS selector. I just need to require it, and supply a function to run once all the requisite code has loaded:

    JS.require('YAHOO.util.Selector', function() {
        var myDivs = YAHOO.util.Selector.query('div.mine');
        // .. more code (elided)
    });

The package system only loads what is needed to get the objects you want, and it makes sure that each file is only downloaded once. Where possible, files are downloaded in parallel to improve performance, but it makes sure interdependent scripts run in the right order.

As well as making it easy to improve the load time of your web applications, JS.Packages works great as a dependency manager for server-side JavaScript projects. It currently supports the following platforms.

Setting up

Before you can use the require statement, you need the following loaded into the page:

The first two are dealt with with a couple of script tags (or load() calls if using a server-side platform). The third needs to be written out to describe all the other files in your application and how they relate to each other.

Alternatively, use the loader.js file, described in more detail below.

The package listing

To describe your packages, you list the external script files used by your application, stating which JavaScript objects are provided by the file and which objects it depends on. For example, here’s a typical package definition for YUI:

    JS.Packages(function() { with(this) {
        var cdn  = 'http://yui.yahooapis.com/';
        var yui  = cdn + '2.7.0/build/';

        file(yui + 'yahoo-dom-event/yahoo-dom-event.js')
            .provides('YAHOO',
                      'YAHOO.lang',
                      'YAHOO.util.Dom',
                      'YAHOO.util.Event');

        file(yui + 'selector/selector-min.js')
            .provides('YAHOO.util.Selector')
            .requires('YAHOO');

        file(yui + 'animation/animation-min.js')
            .provides('YAHOO.util.Anim',
                      'YAHOO.util.ColorAnim')
            .requires('YAHOO');

        file(yui + 'dragdrop/dragdrop-min.js')
            .provides('YAHOO.util.DD')
            .requires('YAHOO',
                      'YAHOO.util.Dom');

        file(yui + 'slider/slider-min.js')
            .provides('YAHOO.widget.Slider')
            .requires('YAHOO',
                      'YAHOO.util.Anim',
                      'YAHOO.util.DD');
    }});

(Yes, YUI has its own loader utility, I’m just using this as a well-known example.)

Notice how two of the stated dependencies for YAHOO.util.DD are provided by the same file – YAHOO and YAHOO.util.Dom. The package system will spot this and only loads each file once. Object detection is used to figure out whether each file has been loaded and a file is not requested unless some of its objects appear to be missing.

Where possible, the package system will attempt to load scripts in parallel where it spots that execution order doesn’t matter. If the load order of a set of scripts is important, you must make sure you make this clear using the requires() statement. A file will not be requested until all the objects it requires are present.

In addition to requires(), there is a statement called uses() that specifies a ‘soft dependency’, i.e. an object the package needs but that does not necessarily need to be loaded first. For example, HashSet uses a Hash for storage but you could load Hash after the Set package just fine. On the other hand, HashSet mixes in Enumerable and this must be loaded before HashSet is defined. And, Hash is itself based on Enumerable. So the package config for this might look like:

    JS.Packages(function() { with(this) {

        file('/js-class/enumerable.js') .provides('JS.Enumerable');

        file('/js-class/hash.js')       .provides('JS.Hash')
                                        .requires('JS.Enumerable');

        file('/js-class/set.js')        .provides('JS.Set',
                                                  'JS.SortedSet',
                                                  'JS.HashSet')
                                        .requires('JS.Enumerable')
                                        .uses(    'JS.Hash');
    });

The advantage of using uses() is that it helps the package system optimise the downloading of packages, since if the load order does not matter the packages can be downloaded in parallel.

Short-hand setup using autoload

As your application grows you may find that your package configuration becomes repetitive. For example, you may have a set of test scripts that mirror the set of classes in your application:

    JS.Packages(function() { with(this) {

        file('tests/widget_spec.js')        .provides('WidgetSpec')
                                            .requires('MyApp.Widget');

        file('tests/blog_post_spec.js')     .provides('BlogPostSpec')
                                            .requires('MyApp.BlogPost');

        file('tests/users/profile_spec.js') .provides('Users.ProfileSpec')
                                            .requires('MyApp.Users.Profile');
    });

If you run into this situation you can use the autoload() function to set up packages for objects whose name matches a certain pattern. For example you could compress the above configuration like this:

    JS.Packages(function() { with(this) {
        autoload(/^(.*)Spec$/, {from: 'tests', require: 'MyApp.$1'});
    });

autoload() expects three parameters. The first is a regex that is used to match package names. The from option is a directory path where packages with that name pattern live, for example this rule would make the package loader look in tests/users/profile_spec.js to find the Users.ProfileSpec module. The require option lets you specify an object the package depends on, using match results from the regex. The above rule would mean that Users.ProfileSpec has a dependency on MyApp.Users.Profile.

If you require() a package that doesn’t have an explicit configuration, the autoloader will try to figure out where to load it from by matching its name against the set of patterns it knows about. A naming convention is adopted for converting object names to paths: dots convert to path separators, and camelcase names are converted to underscored style. Thus Users.ProfileSpec becomes users/profile_spec.js.

Custom loader functions

Some libraries, such as the Google Ajax APIs, have their own systems for loading code on demand that involve more than simply knowing the path to a script file. Our package system allows you to specify packages that use a loader function rather than a path to load themselves; the function should take a callback and call it when the library in question is done loading. For example, here’s how you’d incorporate Google Maps into your library:

    JS.Packages(function() { with(this) {

        file('http://www.google.com/jsapi?key=MY_GOOGLE_KEY')
            .provides('google.load');

        loader(function(cb) { google.load('maps', '2.x', {callback: cb}) })
            .provides('GMap2', 'GClientGeocoder')
            .requires('google.load');
    }});

The callback (cb) is a function generated by the package system that continues to load and run dependent code once the custom loader has finished its work. If you don’t call cb (or pass it to a function that will call it for you as above), code that depends on this library will not run.

JS.Packages also provides post-load setup hooks that let you run some code after a file loads. For example, a strategy for loading YUI3 might involve loading the seed file, creating a new global instance of the library, then using YUI’s own loader functions to load further modules. Some sample code:

    JS.Packages(function() { with(this) {

        file('http://yui.yahooapis.com/3.0.0pr2/build/yui/yui-min.js')
            .setup(function() { window.yui3 = YUI() })
            .provides('YUI', 'yui3');

        loader(function(cb) { yui3.use('node', cb) })
            .provides('yui3.Node')
            .requires('yui3');
    }});

Loader functions can also be used to generate library objects that are expensive to create without necessarily loading code from external files. Just remember to call cb yourself when the generated object is ready:

    JS.Packages(function() { with(this) {

        loader(function(cb) {
            window.ChocolateFactory = new WonkaVenture();
            // Perform other expensive setup operations
            cb();
        })
        .provides('ChocolateFactory');
    }});

The JS.Class loader.js file

The JS.Class distribution comes with a file called loader.js. This contains the package system and dependency information for all JS.Class components. This lets you load a small file initially and download the rest of JS.Class into your app on demand. You must host the library on your servers, but it can figure out where it’s being served from automatically to load other packages. (Note: keep all the JS.Class files in one directory, as they appear in the download, otherwise the loader will not be able to find them.) A simple example:

    <!-- In HEAD -->
    <script src="http://example.com/js-class/loader.js" type="text/javascript">
    </script>

    <!-- Elsewhere in your app -->
    <script type="text/javascript">
        JS.require('JS.SortedSet', function() {
            // ...
        });
    </script>

You can also use this file on server-side platforms (see the list of supported platforms above) but you must tell the library the path where it is stored as this information cannot be determined automatically on these platforms. You do this using the JSCLASS_PATH variable, for example:

    JSCLASS_PATH = 'path/to/js.class/';
    load(JSCLASS_PATH + 'loader.js');

    JS.require('JS.SortedSet', function() {
        // do something with SortedSet
    });

Note that as of version 2.1.4, loader.js does not contain the JS.Class core. If you need JS.Class, JS.Module, JS.Singleton or JS.Interface you must specify these as requirements using the JS.require() function or by adding them as dependencies in your JS.Packages configuration.