JavaScript does not provide any package management features. There is no standard way to specify that one module of code depends upon other modules being available. All external JavaScript (.js) files required for an application must be loaded directly by the host web page. Unless we do something clever, this means that any changes to the dependenices of a .js file requires us to change all of its users!
Fortunately, JavaScript does allow us to be clever and build a package system. This crucially depends upon JavaScript's support for higher-order and anonymous functions, as well as its ability to interact with the host HTML document via the HTML DOM (Document Object Model).
Here is an example of a couple of packages we may wish to specify:
// Code in SomePackage.js: var someFunction; _package_('SomePackage', [], function () { // This should be package-local function helperFunction(x, y, z) { return x*y + y*z; } // This should be exported in the global namespace someFunction = function(x, y) { return helperFunction(x, y, 5); } }); // Code in OtherPackage.js: var otherObject; _package_('OtherPackage', ['SomePackage'], function () { // SomePackage should be loaded before this code runs. // This should be package-local var store = { }; // This should be exported in the global namespace otherObject = { put: function (name, x, y) { // Uses a required import store[name] = someFunction(x, y); }, get: function (name) { return store[name]; } }; });
Before we get to packages with dependencies, let's see how to load external scripts into a JavaScript program running on a web page.
// Exports var include; // Avoid global namespace pollution (function () { // We will insert additional scripts into the <head> section of the HTML document. var head = document.getElementsByTagName('head')[0]; // Load an external script into the document. include = function (jsUrl, k) { // Create a tag to load the specified script. // It will have the form <script type="text/javascript" src="jsUrl">...</script> var scriptTag = document.createElement('script'); scriptTag.type = 'text/javascript'; scriptTag.src = jsUrl; scriptTag.onload = k; // Add the script to the document, causing it to load and run asynchronously. head.appendChild(scriptTag); }; })();
This program has a few interesting features:
A surprising feature of (3) is that we can load external scripts from any site—not just the same origin as the HTML document! This means JavaScript can send information to any web server (via parameters within the requested script's jsUrl), and retrieve information back (via the effects of evaluating the script)!
Now that we know how to load an arbitrary script, the next step is a mechanism to keep track of who required a script to be loaded. We would like to be able to call:
requirePackage("PackageName", "http://www.somehost.com/js/some-script.js", function () { ... })
and have the code ... execute after the package called PackageName has been loaded from the file some-script.js
The basic idea is to queue up the continuations provided as the third argument to requirePackage(), and only evaluate them once the package has been loaded from the specified script file. There are three possible states when we call requirePackage():
Here is the code of requirePackage(). It assumes include() is available.
// Exports var requirePackage; // Avoid namespace pollution (function () { // Keep track of the packages that have been loaded. var includedPackages = { }; // Call k() after loading the named package from the given url. requirePackage = function (name, url, k) { if (includedPackages[name] == true) { // The package has already been loaded. if (k != null) { // Proceed immediately. k(); } } else if (includedPackages[name]) { // The package is waiting on its dependencies. if (k != null) { // Ask the package to call us when it is ready. includedPackages[name].push(k); } } else { // The package has not been visited yet, so start processing it. includedPackages[name] = []; if (k != null) { // Ask the package to call us when it is ready. includedPackages[name].push(k); } // Load the script at the given url. // When it runs, if it contains the requested package, evaluation will continue. include(url); } } })();
Finally, we need a way to define packages. A package must automatically load all its dependencies and wait for them (via requirePackage()), and it must notify all packages that depend upon it once it is done loading. Here is the code. Again, this code fragment assumes requirePackage() and include() have already been defined.
// Exports var _package_; // Avoid namespace pollution (function () { _package_ = function (name, deps, k) { var kprime = join(deps.length, function () { // Run this code when all our dependencies are loaded. // Evaluate the package contents. if (k != null) { k(); } // Notify all packages which depend on us that we're done loading. var clients = includedPackages[name]; includedPackages[name] = true; if (clients && clients != true) { for (var i = 0; i < clients.length; i++) { clients[i](); } } }); // Begin loading dependencies. for (var i = 0; i < deps.length; i++) { // Expand syntactic shortcuts if (typeof(deps[i]) == 'string') { deps[i] = [deps[i]]; } if (deps[i].length == 1) { deps[i].push(deps[i][0]+'.js'); } // General case if (deps[i].length == 2) { requirePackage(deps[i][0], deps[i][1], kprime); } } }; })();
The above code fragments stored include() and requirePackage() in the global namespace, although they are really just internal implementation details used by the _package_ construct. We can combine all of the code together as follows. You can also download this code as package.js.
// Exports var _package_; // Avoid namespace pollution (function () { // We will insert additional scripts into the <head> section of the HTML document. var head = document.getElementsByTagName('head')[0]; // Load an external script into the document. function include(jsUrl, k) { // Create a tag to load the specified script. // It will have the form <script type="text/javascript" src="jsUrl">...</script> var scriptTag = document.createElement('script'); scriptTag.type = 'text/javascript'; scriptTag.src = jsUrl; scriptTag.onload = k; // Add the script to the document, causing it to load and run asynchronously. head.appendChild(scriptTag); }; // Keep track of the packages that have been loaded. var includedPackages = { }; // Call k() after loading the named package from the given url. function requirePackage(name, url, k) { if (includedPackages[name] == true) { // The package has already been processed. if (k != null) { // Proceed immediately. k(); } } else if (includedPackages[name]) { // The package is waiting on its dependencies. if (k != null) { // Ask the package to call us when it is ready. includedPackages[name].push(k); } } else { // The package has not been visited yet, so start processing it. includedPackages[name] = []; if (k != null) { // Ask the package to call us when it is ready. includedPackages[name].push(k); } // Load the script at the given url. // When it runs, if it contains the requested package, evaluation will continue. include(url); } } // Declare a new package and its dependencies. _package_ = function (name, deps, k) { var kprime = join(deps.length, function () { // Run this code when all our dependencies are loaded. // Evaluate the package contents. if (k != null) { k(); } // Notify all packages which depend on us that we're done loading. var clients = includedPackages[name]; includedPackages[name] = true; if (clients && clients != true) { for (var i = 0; i < clients.length; i++) { clients[i](); } } }); // Begin loading dependencies. for (var i = 0; i < deps.length; i++) { // Expand syntactic shortcuts if (typeof(deps[i]) == 'string') { deps[i] = [deps[i]]; } if (deps[i].length == 1) { deps[i].push(deps[i][0]+'.js'); } // General case if (deps[i].length == 2) { requirePackage(deps[i][0], deps[i][1], kprime); } } }; })();