// Copyright (c) 2012, Srikumar K. S. // Licensed for use and redistribution under the MIT license. // See http://www.opensource.org/licenses/mit-license.php // // Simple global namespace package manager. // Java style packages. // Usage: // Package names are in the reverse dns form // - ex: "com.nishabdam.sample-manager". // // Functions: // package(name) -> // Returns the currently loaded package of the given name. // package(name, definition) -> // "definition" is a function that is called. The // return value of the function gives the package object. If definition itself // is an object, then it becomes the package value directly. // package(name, dependencies, definition) -> // "dependencies" is an array. This causes all the dependencies to // be loaded first. Once that is done, the definition function is // called with an argument list corresponding to the given array // of dependencies. // // Utilities: // package.aliases({ name1: "com.blah.bling.SophisticatedName1", ...}); // Defines aliases for long package names. The aliases are global. // // package.config({ // "com.where.packageName": { url: "where/dir/file.js", alias: "name" }, // ... // }) // The url can refer to a relative path or an absolute url. // If the 'url' key is specified instead of 'path', then the package // is fetched using http, even in Node.js environment. (function (package) { this["package"] = package; }((function () { var the_global_object = this; var packages = {'#global': the_global_object}; // Maps package names of the form "com.blah.bling" to package objects a.k.a. "modules". // The global object pseudo-package '#global' come "pre-installed". var loading = {}; // Every package that has started loading, but hasn't finished yet // will have its package name registered here. var onloads = {}; // Maps complete package names to an array of callbacks to be called when the // package finished loading. The callbacks all take the package object as a // single parameter. var config = {}; // Maps full package names to objects of the form - // { path: "path/to/package/file.js" // , url: "http://somewhere.com/somefile.js" // Overrides 'path' // , alias: "shortname" // } // If the configuration is a function, then it is passed the // components of the required subpackage as an array and the return // value is expected to be an object with the above structure, // or undefined if the subpackage is invalid. var loadOrder = {}; // Maps package name to number indicating when it was loaded. var aliases = {}; // Maps short names to full package names. // Valid package names are those that are not any of // the builtin members of Function objects in JS, // which includes raw objects as well. This excludes // stuff like 'constructor', 'toString', etc. as package // names. It returns the name if valid, throws an exception // if not valid. var validPkgName = (function () { function checkComponent(n) { if (checkComponent[n]) { throw new Error('Invalid package component name [' + n + ']'); } else { return n; } } return function (n) { return n.split('.').map(checkComponent).join('.'); }; }()); // Search through the package hierarchy for a // configuration. A configuration can be either an object // with 'url' or 'path' fields, or a function which will // return such an object when passed the subpackage // components as an array. function findConfig(pkgname) { var i, N, part, partArr, cfg; var components = pkgname.split('.'); for (i = 0, N = components.length; i < N; ++i) { partArr = components.slice(0, components.length - i); if (i > 0) { partArr.push('*'); // We're searching parent packages. } part = partArr.join('.'); cfg = config[part]; if (!cfg) { continue; } if (cfg.constructor === Function) { cfg = cfg(components.slice(components.length - i)); if (cfg) { break; } } else if (i === 0 && cfg.constructor === Object) { break; } } if (cfg) { config[pkgname] = cfg; // Cache the config. } return cfg; } // Gets path specified in config, or derives a path // from the package name by replacing '.' with '/'. function packagePath(name) { var cfg = findConfig(name); if (cfg) { return cfg.path; } else { return name.replace(/\./g, '/') + '.js'; } } // Returns url if absolute one is specified. function packageURL(name) { var cfg = findConfig(name); if (cfg && cfg.url && /^https?:\/\//.test(cfg.url)) { return cfg.url; } else { return null; } } function knownPackage(name) { return packages[name]; } function trueName(name) { if (/^\./.test(name)) { name = package.__parent + name; } return name in aliases ? aliases[name] : name; } function pseudoPackage(name) { return name.charAt(0) === '#'; } function definePackageFromSource(name, source) { loadConfig(name); var closure = eval('(function (package, __pkgname__) {\n' + source + ';\n})'); closure(package, name); return packages[name]; } // Inside a package definition function, "this" // refers to the current package object so you can // setup exports by assigning properties to the // "this" object. If you don't have a return statement // in the package definition function, or you return // 'undefined', the this object will be used as the // package definition. Otherwise the return value will // be used. function defWithFallback(pkg, definition, dependencies) { var p = definition.apply(pkg, dependencies); return p === undefined ? pkg : p; } function definePackage(name, definition, dependencies) { package.__parent = name.replace(/\.[^\.]+$/, ''); var p = packages[name] || {}; packages[name] = p; packages[name] = (definition.constructor === Function ? defWithFallback(p, definition, dependencies) : definition); return onPackageLoaded(name); } function addOnLoad(name, callback) { if (name in onloads) { onloads[name].push(callback); } else { onloads[name] = [callback]; } } var delay = (function () { try { return process.nextTick; } catch (e) { return function (proc) { setTimeout(proc, 0); }; } }()); function with_package_in_browser(name, callback) { // Expected to be loaded. var p = knownPackage(name); if (p) { // Package already loaded. delay(function () { callback(p); }); } else if (loading[name]) { // Package started loading already. addOnLoad(name, callback); } else { // Need to load package. loading[name] = true; addOnLoad(name, callback); if (!pseudoPackage(name)) { var cfg = findConfig(name); if (cfg && cfg.external) { loadExternalModuleFromURL(name, cfg.external.url, cfg.external.dependsOn, cfg.external.depNames, cfg.external.name); } else { var script = document.createElement('script'); script.setAttribute('src', packagePath(name)); document.head.insertAdjacentElement('beforeend', script); } } } } function with_package_in_fs(name, callback) { var p = knownPackage(name); var source, closure, where; if (p) { // Package loaded already. delay(function () { callback(p); }); } else if (loading[name]) { // Package started loading already. addOnLoad(name, callback); } else { // Need to load package. loading[name] = true; addOnLoad(name, callback); if (!pseudoPackage(name)) { var cfg = findConfig(name); if (cfg && cfg.external) { loadExternalModuleFromURL(name, cfg.external.url, cfg.external.dependsOn, cfg.external.depNames, cfg.external.name); } else { where = packageURL(name); if (where) { loadPackageFromURL(name, where); } else { where = packagePath(name); loadPackageFromDisk(name, where); } } } } } function fetch_url_async_in_browser(url, callback, errback) { var req = new XMLHttpRequest(); req.open('GET', url, true); req.onload = function () { if (req.status === 200) { callback(package, url, req.responseText); } else if (errback) { errback("Module path [" + url + "] not found."); } }; req.send(); } function fetch_url_async(url, callback, errback) { var urlp = require('url').parse(url); if (urlp.protocol === 'http:' || urlp.protocol === 'https:') { urlp.headers = {'Accept-Encoding': 'identity'}; require(urlp.protocol.split(':')[0]).get(urlp, function (res) { var source = ""; res.setEncoding('utf8'); res.on('data', function (chunk) { source += chunk; }); res.on('end', function () { callback(package, url, source); }); res.on('error', function (err) { if (errback) { errback(err); } }); }).on('error', function (err) { if (errback) { errback(err); } }); } else { return require('fs').readFile(url, 'utf8', function (err, data) { if (err) { if (errback) { errback(err); } } else { callback(package, url, data); } }); } } var with_package, fetch, loadExternalModuleFromURL; if (the_global_object.navigator) { // In browser with_package = with_package_in_browser; fetch = fetch_url_async_in_browser; loadExternalModuleFromURL = loadExternalModuleFromURL_browser; } else { // In Node.js with_package = with_package_in_fs; fetch = fetch_url_async; loadExternalModuleFromURL = loadExternalModuleFromURL_node; } function loadPackageFromURL(name, url) { fetch_url_async(url, function (package, url, source) { package.__CONFIG__ = {url: url}; definePackageFromSource(name, source); }, function (err) { console.error(err); }); } var listingPkgSuffix = /\.__listing__$/; var listingFileSuffix = /__listing__\.js$/; function loadPackageFromDisk(name, where) { var fs = require('fs'); var source, dirLoc, dirContents, parentPkg, subDirs, subDirCount; try { source = fs.readFileSync(where, 'utf8'); package.__CONFIG__ = {path: where}; definePackageFromSource(name, source); } catch (e) { if (listingPkgSuffix.test(name)) { // A listing entry failed. In this case, we can find // out the directory contents automatically. So do that. dirLoc = where.replace(listingFileSuffix, ''); dirContents = fs.readdirSync(dirLoc); parentPkg = name.replace(listingPkgSuffix, ''); packages[name] = dirContents.filter(function (f) { return /\.js$/.test(f); }) .map(function (f) { var fname = f.replace(/\.js$/, ''); var cfg = {}; cfg[parentPkg + '.' + fname] = {path: dirLoc + f}; package.config(cfg); return fname; }); subDirs = dirContents.filter(function (f) { return fs.statSync(where.replace(listingFileSuffix, f)).isDirectory(); }); if (subDirs.length === 0) { onPackageLoaded(name); } else { subDirCount = 0; // Recursively load sub directories. subDirs.forEach(function (f) { var fname = name.replace(/__listing__$/, f) + '.__listing__'; delay(function () { with_package(fname, function (p) { ++subDirCount; if (subDirCount === subDirs.length) { onPackageLoaded(name); } }); }); }); } } else { console.error("Failed to load package [" + name + "] from [" + where + "]"); console.error("Current configuration = "); console.error(config); } } } // An external module is something that doesn't use package to wrap it. // This includes libraries such as jquery, backbone, underscore and any // other that wishes to be directly used in an app using a '); }); document.write(''); } // If you load a package named 'blah.bling.meow', // then you can get the package in a number of ways - // package('blah.bling.meow') // package('blah.bling.*').meow // package('blah.*').bling.meow // package('*').blah.bling.meow // This function sets up all those alternative paths. function setPackagePatterns(components, p) { var pattern, prefix; if (components.length > 1) { prefix = components.slice(0, components.length - 1); pattern = prefix.join('.') + '.*'; } else { prefix = null; pattern = '*'; } if (packages[pattern]) { packages[pattern][components[components.length - 1]] = p; } else { (packages[pattern] = {})[components[components.length - 1]] = p; if (prefix) { setPackagePatterns(prefix, packages[pattern]); } } } function onPackageLoaded(name) { var p = packages[name]; var callbacks = onloads[name]; // Add the package to pattern packages as well. var components = name.split('.'); components[0] = trueName(components[0]); setPackagePatterns(components, p); delete loading[name]; // Store the load order so that we can optimize package load // sequence. loadOrder[name] = package.loadOrder++; console.log("package " + name + " loaded"); if (callbacks && callbacks.length > 0) { delete onloads[name]; callbacks.forEach(function (cb) { delay(function () { cb(p); }); }); } return p; } function relativePackagePath(path, pkg) { var components = path.split('/'); // The last component of pkg after the final period is taken // as the name of the file, with a js suffix. For example, // if pkg is "canine.dog.bowow", then the right hand side // of the assignment below will evaluate to "bowow.js". components[components.length - 1] = pkg.match(/\.([^\.]+)$/)[1] + '.js'; return components.join('/'); } function loadConfig(pname) { if (!config[pname] && package.__CONFIG__) { var cfg = {}; cfg[pname] = {url: package.__CONFIG__.url, path: package.__CONFIG__.path}; package.config(cfg); } } function package3(name, dependencies, definition) { var depPackages = []; var count = 0; var pname = trueName(validPkgName(name)); loadConfig(pname); var pnamecfg = findConfig(pname); loading[pname] = true; if (dependencies.length > 0) { dependencies.forEach(function (dep, i) { var tname = trueName(dep); var tnamecfg; function onePkgLoaded(p) { depPackages[i] = p; ++count; if (count === dependencies.length) { definePackage(pname, definition, depPackages); } } if (/^\./.test(dep)) { // Relative package name starting with a period. // Auto expand it. dep = pname.replace(/\.[^\.]+$/, dep); tname = trueName(dep); tnamecfg = findConfig(tname); // IMPORTANT: // If pname has a config and this one doesn't, then // assume it is going to be served up from the same location. // This is an important simplification that lets you omit // parent package prefixes of dependencies. if (pnamecfg && !tnamecfg) { config[tname] = {}; if (pnamecfg.path) { config[tname].path = relativePackagePath(pnamecfg.path, dep); } if (pnamecfg.url) { config[tname].url = relativePackagePath(pnamecfg.url, dep); } } } if (/\.\*$/.test(tname)) { var listing = tname.replace(/\*$/, '__listing__'); with_package(listing, function (p) { var subPkgs = {}; var subPkgCount = 0; p.forEach(function (subPkgName) { with_package(tname.replace(/\*$/, subPkgName), function (sp) { subPkgs[subPkgName] = sp; ++subPkgCount; if (subPkgCount === p.length) { onePkgLoaded(subPkgs); } }); }); }); } else { with_package(tname, onePkgLoaded); } }); return undefined; } else { return definePackage(pname, definition, []); } } function package2(name, definition) { var tname = trueName(validPkgName(name)); loadConfig(tname); return definePackage(tname, definition, []); } function package1(name) { name = trueName(validPkgName(name)); return packages[name]; } function package() { switch (arguments.length) { case 1: return package1(arguments[0]); case 2: return package2(arguments[0], arguments[1]); case 3: return package3(arguments[0], arguments[1], arguments[2]); default: throw "Invalid number of arguments."; } } function defAlias(name, p) { validPkgName(name); validPkgName(p); aliases[name] = p; var pobj = packages[p]; if (pobj) { packages[name] = pobj; onPackageLoaded(name); } else { addOnLoad(p, function (pobj) { packages[name] = pobj; onPackageLoaded(name); }); } } package.config = function (setupInfo) { var i; for (var p in setupInfo) { i = config[p] = setupInfo[p]; i.alias && defAlias(i.alias, p); } }; package.aliases = function (name2package) { for (var a in name2package) { defAlias(a, name2package[a]); } }; package.fetch = fetch; package.declare = function (packagesThatWillBeDefined) { packagesThatWillBeDefined.forEach(function (pname) { var pnameres = trueName(pname); if (!knownPackage(pnameres)) { loading[pname] = true; loading[pnameres] = true; } }); }; package.external = function (pkgname, exportedName, url, dependsOn, depNames) { var cfg = {}; cfg[pkgname] = { external: { url: url, dependsOn: dependsOn || [], depNames: depNames || dependsOn || [], name: exportedName }}; package.config(cfg); }; package.loadOrder = 1; function loadKnownPackageConfig() { var cacheFile = './.packages.js'; var fs = require('fs'); fs.stat(cacheFile, function (err, stat) { function loadSource(source) { eval('(function (package) {\n' + source + '\n})')(package); } if (err) { console.error("Run 'configure -r' to get known configurations."); throw new Error("Known package config not downloaded yet."); } else { loadSource(fs.readFileSync(cacheFile, 'utf8')); } }); } if (the_global_object.navigator && the_global_object.document && the_global_object.document.write) { // TODO: Figure out a way to auto-add the package registry before // the other code loads. the_global_object.document.write(''); } else { // In node.js loadKnownPackageConfig(); } return package; }())));