Home

Awesome

Montage Require

This is a CommonJS module system, highly compatible with NodeJS, intended for front-end development of web applications using NPM style packages. It is designed to be automatically replaced by the Montage Optimizer with a smaller, faster, and bundled production module system.

To use, install the module system in your application package with NPM.

npm install mr

Then, incorporate the Montage Require bootstrapping script in an HTML document.

<script
    src="node_modules/mr/bootstrap.js"
    data-module="index"
></script>
<script
    src="node_modules/mr/bootstrap.js"
    data-auto-package
    data-module="index"
></script>
<script
    src="node_modules/mr/bootstrap.js"
    data-package="."
    data-module="index"
></script>

Node and NPM Compatibility

Montage fully supports CommonJS Modules and Packages. It also supports some of the extensions from NodeJS and NPM.

Extensions:

Not supported:

The Montage modules debug-mode run-time loads modules asynchronously and calculates their transitive dependencies heuristically—by statically scanning for require calls using a simple regular expression. Montage can load cross-origin scripts in debug-mode if the CORS headers are set on the remote server.

Take a look at the Montage Optimizer to optimize applications for production. The optimizer can bundle packages with all of the dependent modules, can preload bundles of progressive enhancements in phases, and can generate HTML5 application cache manifests.

Optimizer Script Attributes

The Montage Optimizer, mop, does not yet handle stand-alone Montage Require. However, when it does, the optimizer can convert entire packages to production ready versions without manual alteration. The optimizer rewrites HTML, particularly replacing the bootstrapping script with a bundle. As such, the run-time supports some additional options.

The optimizer can convert all resources into script-injection form, by changing .js modules to .load.js scripts with define(hash, id, descriptor) boilerplate. This permits packages to be loaded cross-origin and with content security policies that forbid eval. The hash is a consistent hash for each package. The bootstrapper needs to know these hashes so it can recognize incoming package.json.load.js definitions.

Among other things, the optimizer is also responsible for processing package.json files to include the hash of each dependency.

Cross-browser Compatibility

At present, Montage Require depends on document.querySelector and probably several other recent EcmaScript methods that might not be available in legacy browsers. With your help, I intend to isolate and fix these bugs.

At time of writing, tests pass in Chrome 21, Safari 5.1.5, and Firefox 13 on Mac OS 10.6.

How It Works

In broad strokes, Montage Require uses so-called "XML" HTTP requests to fetch modules, then uses a regular expression to scan for require calls within each JavaScript module, then executes the module with some variation of eval. Then, with the Montage Optimizer, mop, Montage Require can also serve as the runtime for loading modules with bundled script-injection with no alteration to the source code of an application. With script-injection, XHR and eval are not necessary, so applications are suitable for production, cross-domain, and with content security policies (CSP) that forbid eval.

In slightly thinner strokes, Montage Require has an asynchronous phase and a synchronous phase. In the asynchronous "loading" phase, Montage Require fetches every module that it will need in the synchronous phase. It then passes into the synchronous "execution" phase, where require calls actually occur. The asynchronous portion includes require.async, require.load, and require.deepLoad, which return Q promises. The synchronous phase employs require calls directly to transitively instantiate modules on demand. The system must be kicked off with require.async since no modules are loaded initially.

Some alternatives to Montage Require use a full JavaScript parser to cull the false positives you will occasionally see when using regular expressions to scan for static require calls. This is a trade-off between weight and accuracy. Montage Require does not block execution when it is unable to load these false-positive modules, but instead continues to the execution to "wait and see" whether the module can run to completion without the module that failed to load. Also, Montage Require can be configured to use an alternate dependency parser.

Around this system, Montage Require supports packages. This entails asynchronously loading and parsing package.json files, then configuring and connecting the module systems of each package in the "load" phase. Package dependencies are loaded on demand.

Each package has an isolated module identifier name space. The package.json dictates how that name space forwards to other packages through the dependencies property, as well as internal aliases from the package's name, main, and redirects properties.

Additionally, Montage Require is very configurable and pluggable. Montage itself vastly extends the capabilities of Montage Require so that it can load HTML templates. Montage's internal configuration includes middleware stacks for loading and compiling. The loader middleware stack can be overridden with config.makeLoader or config.load. The compiler middleware can be overridden with config.makeCompiler or config.compile. The makers are called to create loaders or compilers per package, each receiving the configuration for their particular package.

The signature of loader middleware is makeLoader(config, nextLoader) which must return a function of the form load(id, module). The signature of compiler middleware if makeCompiler(config, nextCompiler) which must return a function of the form compile(module).

As part of the bootstrapping process, configuration begins with a call to Require.loadPackage(dependency, config) that returns a promise for the require function of the package.

config is an optional base configuration that can contain alternate makeLoader, makeCompiler, and parseDependencies functions. Montage Require then takes ownership of the config object and uses it to store information shared by all packages like the registries of known packages by name and location, and memoized promises for each package while they load.

dependency declares the location of the package, and can also inform the module system of the consistent hash of the package. Dependency can be a location string for short, but gets internally normalized to an object with a location property. The hash is only necessary for optimized packages since they use script-injection. The injected scripts call define for each module, identifying the module by the containing package hash and module id.

The require function for any package has a similar loadPackage function that can take a dependency argument. That dependency may have a name instead of location. In that case, Montage Require infers the location based on the known locations of packages with that name, or assumes the package exists within the node_modules directory of the dependent package. This is a relatively safe assumption if the application was installed with NPM.

Montage Require also supports a form of dependency injection. These features were implemented because bootstrap.js (and in Montage proper, montage.js) would need to load and instantiate certain resources before being able to instantiate a module system. To avoid reloading these already-instantiated resources, the bootstrapper would inject them into the packages before handing control over to the application.

require.inject(id, exports) adds the exports for a given module to a package.

require.injectPackageDescription(location, description) allows the module system to read the content of a package.json for the package at location without fetching the corresponding file.

require.injectPackageDescriptionLocation(location, descriptionLocation) instructs the module system to look in an alternate location for the package.json for a particular package.

Interface

require

A require function stands for a package. Specialized require functions exist within each module. Calling require from outside a module will return the exports of the module with the given top-level identifier. Calling require within a module will resolve the given identifier relative to the current module and return the exports of the corresponding module. require will throw an exception if a needed module has not yet been loaded.

module

The module object is available within a module, returned by require.getModuleDescriptor(id), and passed to loader and compiler middleware for decoration.

config

Require.loadPackage accepts the following configuration options for all packages in a fresh module system.

Montage Require then adds shared state for all packages to the config.

Then, for each package, Montage Require creates a config that prototypically inherits from the master config and expands on that configuration with details synthesized from the content of the package description, package.json. This is the config that gets passed to Require.makeRequire(config).

Within Require.makeRequire(config), Montage Require uses makeLoader and makeConfig with its own config to produce config.load and config.compile properties. The config.load in particular is distinct and used internally by require.load, which memoizes and compiles modules.

package.json (package description)

Montage Require configures each package based on the contents of package.json, the package description, and the shared configuration. These properties are meaningful to Montage Require.

Maintenance

Tests are in the spec directory. All of the CommonJS module tests exist in there as well as tests for packaging and extensions.

Open spec/run.html in a browser to verify the specs.

This implementation is a part from Motorola Mobility’s Montage web application framework. The module system was written by Tom Robinson and Kris Kowal. Motorola holds the copyright on much of the original content, and provided it as open source under the permissive BSD 3-Clause license. This project is maintained by Kris Kowal, continuing with that license.