UNPKG

amdld

Version:

AMD module loader with deterministic and just-in time module initialization

350 lines (314 loc) 9.49 kB
(function(exports, require){ "use strict"; const CHECK_CYCLES = 1; const DEBUG = 0; // = 1 in .g.js builds function emptyFunction(){} /** @const {Function} */ const assert = DEBUG ? function(cond) { if (!cond) { throw new Error('assertion failure'); } } : emptyFunction; /** @const {Function} */ const logdebug = DEBUG ? function(args) { if (define['debug']) { (console.debug || console.log).apply( console, ['[define]'].concat(Array.prototype.slice.call(arguments)) ); } } : emptyFunction; /** @private {Map<string|symbol,Module>} */ let modules = new Map; /** @private {Map<string|symbol,Set<string|symbol>>} */ let waiting = new Map; function _require(id) { let m = modules.get(id); if (!m) { if (require) { return require(id); } throw new Error(`unknown module "${id}"`); } return m.init ? undefined : m['exports']; } /** * @final * @constructor * @param {string|symbol|null} id * @param {Object|null} exports * @param {Array<Object>|null} deps * @param {Function|Object} fn */ function Module(id, exports, deps, fn) { this['id'] = id; this['exports'] = exports; this.deps = deps; this.fn = fn; this.init = null; this.waitdeps = null; } // Return the path to dependency 'id' starting at m. // Returns null if m does not depend on id. // Note: This is not a generic function but only works during initialization // and is currently only used for cyclic-dependency check. /** * @param {Module} m * @param {string} id */ function deppath(m, id) { // : Array<string> | null if (m.waitdeps) { for (let wdepid of m.waitdeps) { if (wdepid == id) { return [m['id']]; } let wdepm = modules.get(wdepid); if (wdepm) { let path = deppath(wdepm, id); if (path) { return [m['id']].concat(path); } } } } return null; } /** * @param {Module} m */ function mfinalize(m) { // clear init to signal that the module has been initialized m.init = null; // get dependants that are waiting let /** Set<symbol|string> */ waitingDependants = waiting.get(m['id']); waiting.delete(m['id']); // clear this module from `waiting` if (m.fn) { // execute module function let res = m.fn.apply(m['exports'], m.deps); if (res) { m['exports'] = res; } m.fn = null; } // clear module properties to free up memory since m will live forever because // it's owned by modules which is bound to the define's closure. m.deps = null; m.waitdeps = null; if (waitingDependants) { // check in on dependants for (let depid of waitingDependants) { let depm = modules.get(depid); if (depm.init) { if (depm.waitdeps.size == 1) { // The just-initialized module is the last dependency. // Resume initialization of depm. depm.init(); } else { // The just-initialized module is one of many dependencies. // Simply clear this module from depm's waitdeps depm.waitdeps.delete(m['id']); } } } assert(typeof m['id'] != 'symbol'); } else if (typeof m['id'] == 'symbol') { // remove anonymous module reference as it was only needed while // resoling its dependencies. Note that typeof=='symbol' is only available in // environments with native Symbols, so we will not be able to clean up // anon modules when running in older JS environments. It's an okay trade-off // as checking for "shimmed" symbol type is quite complicated. modules.delete(m['id']); } } /** * @param {Module} m */ function* minitg(m, deps) { while (true) { for (let i = 0, L = deps.length; i != L; ++i) { let depid = deps[i]; if (m.deps[i] !== undefined) { continue; } if (depid == 'require') { m.deps[i] = _require; } else if (depid == 'exports') { m.deps[i] = m['exports']; } else if (depid == 'module') { m.deps[i] = m; } else { let depm = modules.get(depid); if (depm && !depm.init) { // dependency is initialized m.deps[i] = depm['exports']; if (m.waitdeps) { m.waitdeps.delete(depid); } } else { // latent dependency — add to waitdeps if (!m.waitdeps) { m.waitdeps = new Set([depid]); } else if (!m.waitdeps.has(depid)) { m.waitdeps.add(depid); } else { continue; } // check for cyclic dependencies when depm.init is still pending if (CHECK_CYCLES && depm) { let cycle = deppath(depm, m['id']); if (cycle) { if (cycle[cycle.length-1] != m['id']) { cycle.push(m['id']); } throw new Error( `Cyclic module dependency: ${m['id']} -> ${cycle.join(' -> ')}` ); } } } } } if (!m.waitdeps || m.waitdeps.size == 0) { // no outstanding dependencies break; } yield m.waitdeps; } mfinalize(m); } // Creates a resumable init function for module m with dependencies deps /** * @param {Module} m * @param {Array<string>|null} deps */ function minit(m, deps) { let initg = minitg(m, deps); return function init() { logdebug('attempting to resolve dependencies for', m['id']); let v = initg.next(); if (v.done) { // module initialized logdebug('completed initialization of', m['id']); return true; } // add outstanding dependencies to waitset for (let depid of v.value) { let waitset = waiting.get(depid); if (waitset) { waitset.add(m['id']); } else { waiting.set(depid, new Set([m['id']])); } } return false; }; } // if define.timeout is set, the `timeout` function is called to check for // modules that has not yet loaded, and if any are found throws an error. let timeoutTimer = null; let timeoutReached = false; function timeout() { clearTimeout(timeoutTimer); timeoutTimer = null; timeoutReached = true; if (waiting && waiting.size > 0) { let v = []; for (let id of waiting.keys()) { if (!modules.has(id)) { v.push(id); } } if (v.length) { throw new Error(`Module load timeout -- still waiting on "${v.join('", "')}"`) } } } // define(id?, deps?, fn) function define(id, deps, fn) { logdebug('define', id, deps, typeof fn); if (define.timeout && define.timeout > 0) { if (timeoutReached) { logdebug('define bailing out since timeout has been reached'); return; } clearTimeout(timeoutTimer); timeoutTimer = setTimeout(timeout, define.timeout); } let objfact = 1; // 0=no, 1=?, 2=yes switch (typeof id) { case 'function': { // define(factory) fn = id; id = null; deps = []; objfact = 0; break; } case 'object': { // define([...], factory) fn = deps; deps = id; id = null; if (typeof fn != 'function') { // define([...], {...}) throw new Error('object module without id'); } break; } default: { objfact = 0; if (typeof deps == 'function') { // define(id, factory) fn = deps; deps = []; } else if (!fn) { // define(id, obj) fn = deps; deps = []; objfact = 2; } // else: define(id, [...], factory) break; } } if (!deps || deps.length == 0) { // no dependencies logdebug('taking a shortcut becase', id, 'has no dependencies'); objfact = (objfact == 1 && typeof fn != 'function') ? 2 : objfact; let m = new Module(id, objfact ? fn : {}, null, objfact ? null : fn); if (id) { modules.set(id, m); mfinalize(m); } else { // Note: intentionally ignoring return value as a module w/o an id // is never imported by anything. fn.apply(m['exports']); m.fn = null; } return true; } if (typeof fn != 'function') { // define('id', [...], {...}) throw new Error('object module with dependencies'); } // resolve dependencies let m = new Module( id || Symbol(''), {}, new Array(deps.length), fn ); modules.set(m['id'], m); m.init = minit(m, deps); return m.init(); } // Set to a number larger than zero to enable timeout. // Whenever define() is called, the timeout is reset and when the timer expires // an error is thrown if there are still undefined modules. /** @export {number} */ define['timeout'] = 0; define['require'] = _require; define['amd'] = {}; if (DEBUG) { define['debug'] = false; } exports['define'] = define; })(this, typeof require == 'function' ? require : null);