modules
Version:
CommonJS modules for the browser
179 lines (152 loc) • 6.22 kB
JavaScript
(function(global, undefined) {
var doc = global.document, modules = {}, waits = {}, factories = {}, urls = {},
link = doc.createElement('a'),
main = doc.querySelector('#modules-define,script[data-main]'),
path = main ? (main.getAttribute('data-path') || main.src) : '',
uids = JSON.parse(main ? main.getAttribute('data-urls') : '{}') || {};
// a poor-man's Array.prototype.map
function map(arr, fn) {
for (var arr2 = [], a = 0, aa = arr.length; a < aa; ++a) { arr2[a] = fn(arr[a]); }
return arr2;
}
// run all functions waiting for the module
function fireWaits(module) {
map(waits[module.id], function(fn) { fn(module); });
waits[module.id] = [];
}
// make a url absolute
function canonicalize(url) {
link.href = url; // Ensures that the href is properly escaped
return link.href;
}
// look for all of the modules at the given url
function mapUrlToIds(url, ids) {
map(ids, function(id) { urls[id] = url; });
}
// define a new module
function define(id, deps, exp) {
if (!exp) { exp = deps; deps = [ 'require', 'exports', 'module' ]; }
var module = getModule(undefined, id);
module.children = map(deps, function(dep) { return getModule(module, dep); });
module.loaded = true;
factories[id] = exp;
fireWaits(module);
}
// build out the require function specific to the module scope
function makeRequire(module) {
var mrequire = function(id, fn) { return require(module, id, fn); };
mrequire.resolve = function(id) { return getModule(module, id).uri; };
mrequire.toUrl = function(id) {
return getModule(module, id.replace(/\.[^.\\\/]+$/, ''))
.uri.replace(/\.js$/i, id.match(/\.[^.\\\/]+$/));
};
mrequire.cache = modules;
mrequire.main = main;
mrequire.map = mapUrlToIds; // allow mapping multiple module ids to a url
return mrequire;
}
// build or retrieve a specific module object
function getModule(parent, id) {
if ('require' === id || 'exports' === id || 'module' === id) {
return { id:id, loaded:true, exports:parent[id] || parent, children:[] };
}
id = resolve(parent, id).replace(/\.js$/i, '');
if (modules[id]) { return modules[id]; }
var uri = canonicalize(urls[id] ? urls[id] : path.replace(/[^\/]*$/, id + '.js')),
module = (modules[id] = { id:id, filename:uri, uri:uri, loaded:false, children:[] });
module.require = makeRequire(module);
waits[id] = [];
return module;
}
// execute a module's code, and return its exports
function require(parent, id, fn) {
if (fn) { return ensure(parent, id, fn); }
var module = getModule(parent, id);
if (!module.loaded) { throw new Error(id + ' not found'); }
if (!('exports' in module)) {
module.parent = parent; // first module to actually require this one is parent
// if define was passed a non-function, just assign it to exports
if ('function' !== typeof factories[id = module.id]) { module.exports = factories[id]; }
else {
module.exports = {};
fn = map(module.children.slice(0, factories[id].length), // don't require prematurely on wrapped commonjs
function(child) { return require(module, child.id); });
if ((fn = factories[id].apply(global, fn))) { module.exports = fn; } // if something was returned, export it instead
}
}
return module.exports;
}
// resolve a module id relative to the parent scope
function resolve(parent, id) {
if (/^\.\.?\//.test(id) && parent && parent.id) { // is a relative id
id = parent.id.replace(/[^\/]+$/, id); // prepend parent's dirname
}
var terms = [];
map(id.split('/'), function(term) {
if ('..' === term) { terms.pop(); } // remove previous, don't add ..
else if ('.' !== term) { terms.push(term); } // add term
// else if ('.' === term) // ignore .
});
return terms.join('/');
}
// make sure the modules are loaded
function ensure(parent, ids, fn) {
ids = [].concat(ids);
var visited = {}, wait = 0;
function done() {
if (fn) { fn.apply(global, map(ids, function(id) { return require(parent, id); })); }
fn = undefined;
}
function visit(module) {
if (module.id in visited) { return; }
if ((visited[module.id] = module.loaded)) { return map(module.children, visit); }
++wait;
waits[module.id].push(function() {
map(module.children, visit);
if (--wait <= 0) { setTimeout(done, 0); }
});
var script;
map(doc.querySelectorAll('script'), function(node) {
if (canonicalize(node.src) === module.uri) { script = node; }
});
if (!script) {
script = doc.createElement('script');
script.onload = script.onerror = script.onreadystatechange = function() {
if ('loading' === script.readyState) { return; }
script.onload = script.onerror = script.onreadystatechange = null;
fireWaits(module);
};
script.defer = true;
script.src = module.uri;
doc.querySelector('head').appendChild(script);
}
}
map(ids, function(id) { visit(getModule(parent, id)); });
if (wait <= 0) { setTimeout(done, 0); } // always async
}
// if there were modules shim defined before now, really define them now
if (global.define && global.define.amd) {
if (!global.define.amd.d) { return; } // bail if not define.shim, defer to other loader
map(global.define.amd.d, function(args) { define.apply(undefined, args); });
}
// this define function is AMD compatible
define.amd = {};
// make sure we map all of the urls in the attribute
for (var url in uids) { mapUrlToIds(url, uids[url]); }
// if there is a main module, require it when the page is ready
function run() { require(undefined, main.id, function(){}); }
if ((main = main && main.getAttribute('data-main') || undefined)) {
(main = getModule(undefined, main)).require.main = main;
if ('loading' !== doc.readyState) { setTimeout(run, 0); }
else if (doc.addEventListener) {
doc.addEventListener('DOMContentLoaded', run, false);
global.addEventListener('load', run, false);
} else { // if (global.attachEvent) assume if addEventListener wasn't found
global.attachEvent('onload', run);
}
}
// export `define`, `require`, and `global` to global object
global.global = global; // for greater nodejs compat
global.define = define;
global.require = makeRequire();
}(this));