Modules/SimpleAsynchronous

From CommonJS Spec Wiki
Jump to: navigation, search

STATUS: PROPOSAL

This Modules proposal attempts to define a simple asynchronous module system roughly similar to (Modules/AsynchronousDefinition) and the modules part of Modules 2.0, but without any relationship to Modules 1.1. You can also read this as developer guides lines for a Modules/2.0: a set of rules that regularize and simplify module use similar to the way jslint constrains JS code.

In a departure from the AMD spec, but similar to Modules 2.0, we'll describe both the module format and the mechanism for loading the modules: a complete but simple solution.

Synoposis

  1. ModuleLoader is a function
    1. ModuleLoader(config) accepts a configuration object
    2. new ModuleLoader(config) constructions a system of modules
    3. The configuration object has a baseUrl property, a URL.
  2. var modules = new ModuleLoader(config) has two methods,
    1. modules.declare(id, deps, initializer) declares a module for the module system
      1. id is a URL, deps is an array of URLs, initializer is a function,
      2. Every module listed in the deps array is loaded before the initializer function is called,
      3. The initializer function returns an object, called the exports object,
      4. When a dependency lookup causes a load, the id of the declare must match the path of the unit loaded,
    2. modules.lookup(id) returns the exports object for the module with id,
      1. The id is a URL

ModuleLoader model

To simplify the description we define a model for the ModuleLoader object and some terms of action on the model. The model itself is not part of any specification.

The ModuleLoader model is a look-up table with one row for every module, keyed by the module id. The remaining columns in the table are an array of module ids called the dependencies, a function called the module initializer, and the result of calling that function, called an exports object.

Id Deps initializer exports
"foo.js" ["bar.js"] function fooInitializer() {} foo
"bar.js" ["foo.js"] function barInitializer() {} bar

We start with an empty table. When we add a row we say we are declaring a module. The row begins with the exports object being undefined. When we pick up one module id from the Deps array and find the values that allow us to declare the module we say we resolved the id. When the initializer function is called, we say the exports object is initialized.

The basic game of module loading is to fill in the table such that the first entry's exports object can be initialized. The Deps from the first row are converted to absolute URLs and resolved in to source objects containing modules.declare() calls; these calls declare new modules that add to the table. The goal of the specification is to allow as many developers to win the game as possible with the least amount of effort and greatest amount of reliability.

ModuleLoader constructor

The specification defines a single function "ModuleLoader" that is available as a property of the global scope. The function is to be used as a constructor:

 var modules = new ModuleLoader(configuration);

The only parameter of the this function is a optional configuration object. Some of the properties of the configuration object will (eventually) be part of the specification; others will be available and meaningful only in some environments. All properties are optional and the default values are designed to give a working module system.

ModuleLoader objects

Object created from ModuleLoader have a method to declare modules:

modules.declare(id, dependencies, initializer);

The first argument, id, specifies the id of the dependent module being declared. The module id MUST be an relative id ( absolute ids are not allowed).

The second argument, dependencies, is an array of the dependency identifiers. Every dependency identifier must be resolved (associated with a module initializer function in memory) prior to execution of the dependent module initializer function. The dependencies ids should be relative to the ModuleLoader baseUrl if the dependency is part of the same logical system.

The third argument, initializer, is a function, stored for later execution to initialize the exports object.

Note: this method is similar to module.provide in [2]; it is not similar to define() in [1]. By the recent show of hands this method should be called module.load; for now it's declare to maximize simplicity. There are no optional or variable parameters. The id is relative, opposite of AMD [2].

Module declaration

One of the main uses of modules is to combine source from independent units, typically files. We'll use the word 'files' here. The developer writes the source in the files, organizes the files into directories, then joins the source to create a program using the ModuleLoader object.

The source in modules files compiles in a global scope with a single property called modules. All of the other properties needed for the code to run come through the dependencies or the JavaScript runtime. Since we can't do very much without accessing the dependencies, a typical module files will begin with a declaration:

 modules.declare(id, deps, initializer);

The function call here looks like the one from the modules object. There is only one difference:

  1. Within a module file, the initializer is compiled in a highly restricted scope: only our dependencies and modules are available.

Developers are free to name the object reference returned by new ModuleLoader() something other than modules; in this case the two calls will have a second difference.

Note: this corresponds roughly to AMD define() [1] or module.declare() [2].

Exports Initialization

Every module declaration that is not created by a dependency resolution results in exports initialization or an exception. Before exports initialization, every dependency is resolved and the modules declared, recursively through dependencies of dependencies. The module initializer function must only be executed once. If the initializer function returns a value (an object, function, or any value that coerces to true), and then that value should be assigned as the exports object for the module.

Declared modules created by dependency resolution have their exports object initialized one of two ways:

  1. binding to an argument, or
  2. modules.lookup

Exports Initialization by Binding to an Argument

Immediately before the the initializer function is called, the arity of the initializer function is determined and the corresponding dependent modules are initialized. The export objects must be passed as arguments to the initializer function with argument positions corresponding to index in the dependencies array.

modules.lookup

The exports object can be initialized by calling

 var theExport = modules.lookup(id);

The argument is a module id; the return value refers to the exports object of the module. If the id is not resolved, the function throws an exception. If the exports object has not been initialized, the initializer function is called before the lookup function returns.

Notes This differs from require(id) in [3] in being a property (in a namespace) not a global and explicitly it does not cause declaration or dependency resolution; the call fails if the id is not in the system. It could called modules.require(id); it could be aliased as var require = modules.lookup;

Module Identifiers

  • A module identifier is a URL [4]
  • Module identifiers may be "relative" or "top-level" URLs
  • Top-level identifiers are resolved off the base URL configured into the ModuleLoader object
  • Relative identifiers are resolved relative to the identifier of the module being declared.

Notes: This does not match [3], and I am sure a lot of discussion went to those decisions. One important argument against URLs is the freedom to declare multiple modules in one file. The file becomes a "module directory", with a base URL equal to the path to the file and the module ids in the declarations are relative to that base.

Module Id Resolution

Dependency ids are resolved into resources by:

  1. Conversion from a relative to an absolute url:
    1. The first declaration uses the ModuleLoader base URL
    2. Declarations called in by dependencies use the file as a baseURL
  2. If the resulting absolute URL does not match a resource, the last segment is replaced by index.


Notes: The funky last rule allows developers to combine any number of declarations in to one file. These declarations use relative ids, creating an internal directory of declarations.

ModuleLoader Configuration

The configuration object passed to the ModuleLoader function has these properties:

  • baseUrl: same as [5] baseUrl

The configuration object may have other properties that affect the results of module loading.

Design Rationale

  • Minimal specification for asynchronous module loading: a thin base that can be clearly understood,
  • Minimal global property addition: organize impact in one property,
  • Minimal optional and variable arguments: easy for developers to remember, better error messages,
    • By requiring id values, we can give good error messages during module loading,
    • Allowing only an array of dependencies simplifies the API and regularizes the code,
  • Maximum reuse: URLs a have proven track record, excellent documentation, and large base of tools,

Examples

declaring a module

Sets up the module with ID of "alpha", that uses a module with ID of "beta":

modules.declare("alpha", ["beta"], function alphaInitializer(beta) {
    var alpha = {};
    alpha.verb = function() {
        return beta.verb();
    }
    return alpha
});

If the beta module exports object is not yet initialized, its initializer function will be called immediately before the call to the function alphaInitializer.

An anonymous module could be defined (module id derived from filename) that returns an object literal:

modules.declare(["alpha"], function (alpha) {
    return {
      verb: function(){
        return alpha.verb() + 2;
      }
    };
});

A dependency-free module can declare a exports object literal:

 modules.declare({
  add: function(x, y){
    return x + y;
  }
});

References

[1] Modules/AsynchronousDefinition

[2] Wes Garland's Modules 2.0

[3] Modules/1.1.1

[4] Wikipedia URL (should be the RFC)

[5] RequireJS

[6] RFC 1738 Uniform Resource Locators (URL)