UNPKG

systemjs

Version:

System loader extension for flexible AMD & CommonJS support

559 lines (465 loc) 17.3 kB
/* * Instantiate registry extension * * Supports Traceur System.register 'instantiate' output for loading ES6 as ES5. * * - Creates the loader.register function * - Also supports metadata.format = 'register' in instantiate for anonymous register modules * - Also supports metadata.deps, metadata.execute and metadata.executingRequire * for handling dynamic modules alongside register-transformed ES6 modules * * * The code here replicates the ES6 linking groups algorithm to ensure that * circular ES6 compiled into System.register can work alongside circular AMD * and CommonJS, identically to the actual ES6 loader. * */ (function() { /* * There are two variations of System.register: * 1. System.register for ES6 conversion (2-3 params) - System.register([name, ]deps, declare) * see https://github.com/ModuleLoader/es6-module-loader/wiki/System.register-Explained * * 2. System.registerDynamic for dynamic modules (3-4 params) - System.registerDynamic([name, ]deps, executingRequire, execute) * the true or false statement * * this extension implements the linking algorithm for the two variations identical to the spec * allowing compiled ES6 circular references to work alongside AMD and CJS circular references. * */ var anonRegister; var calledRegister; function doRegister(loader, name, register) { calledRegister = true; // named register if (name) { name = loader.normalizeSync(name); register.name = name; if (!(name in loader.defined)) loader.defined[name] = register; } // anonymous register else if (register.declarative) { if (anonRegister) throw new TypeError('Multiple anonymous System.register calls in the same module file.'); anonRegister = register; } } SystemJSLoader.prototype.register = function(name, deps, declare) { if (typeof name != 'string') { declare = deps; deps = name; name = null; } // dynamic backwards-compatibility // can be deprecated eventually if (typeof declare == 'boolean') return this.registerDynamic.apply(this, arguments); doRegister(this, name, { declarative: true, deps: deps, declare: declare }); }; SystemJSLoader.prototype.registerDynamic = function(name, deps, declare, execute) { if (typeof name != 'string') { execute = declare; declare = deps; deps = name; name = null; } // dynamic doRegister(this, name, { declarative: false, deps: deps, execute: execute, executingRequire: declare }); }; /* * Registry side table - loader.defined * Registry Entry Contains: * - name * - deps * - declare for declarative modules * - execute for dynamic modules, different to declarative execute on module * - executingRequire indicates require drives execution for circularity of dynamic modules * - declarative optional boolean indicating which of the above * * Can preload modules directly on System.defined['my/module'] = { deps, execute, executingRequire } * * Then the entry gets populated with derived information during processing: * - normalizedDeps derived from deps, created in instantiate * - groupIndex used by group linking algorithm * - evaluated indicating whether evaluation has happend * - module the module record object, containing: * - exports actual module exports * * For dynamic we track the es module with: * - esModule actual es module value * * Then for declarative only we track dynamic bindings with the 'module' records: * - name * - exports * - setters declarative setter functions * - dependencies, module records of dependencies * - importers, module records of dependents * * After linked and evaluated, entries are removed, declarative module records remain in separate * module binding table * */ hookConstructor(function(constructor) { return function() { constructor.call(this); this.defined = {}; this._loader.moduleRecords = {}; }; }); // script injection mode calls this function synchronously on load hook('onScriptLoad', function(onScriptLoad) { return function(load) { onScriptLoad.call(this, load); // anonymous define if (anonRegister) load.metadata.entry = anonRegister; if (calledRegister) { load.metadata.format = load.metadata.format || 'defined'; load.metadata.registered = true; calledRegister = false; anonRegister = null; } }; }); function buildGroups(entry, loader, groups) { groups[entry.groupIndex] = groups[entry.groupIndex] || []; if (indexOf.call(groups[entry.groupIndex], entry) != -1) return; groups[entry.groupIndex].push(entry); for (var i = 0, l = entry.normalizedDeps.length; i < l; i++) { var depName = entry.normalizedDeps[i]; var depEntry = loader.defined[depName]; // not in the registry means already linked / ES6 if (!depEntry || depEntry.evaluated) continue; // now we know the entry is in our unlinked linkage group var depGroupIndex = entry.groupIndex + (depEntry.declarative != entry.declarative); // the group index of an entry is always the maximum if (depEntry.groupIndex === undefined || depEntry.groupIndex < depGroupIndex) { // if already in a group, remove from the old group if (depEntry.groupIndex !== undefined) { groups[depEntry.groupIndex].splice(indexOf.call(groups[depEntry.groupIndex], depEntry), 1); // if the old group is empty, then we have a mixed depndency cycle if (groups[depEntry.groupIndex].length == 0) throw new TypeError("Mixed dependency cycle detected"); } depEntry.groupIndex = depGroupIndex; } buildGroups(depEntry, loader, groups); } } function link(name, loader) { var startEntry = loader.defined[name]; // skip if already linked if (startEntry.module) return; startEntry.groupIndex = 0; var groups = []; buildGroups(startEntry, loader, groups); var curGroupDeclarative = !!startEntry.declarative == groups.length % 2; for (var i = groups.length - 1; i >= 0; i--) { var group = groups[i]; for (var j = 0; j < group.length; j++) { var entry = group[j]; // link each group if (curGroupDeclarative) linkDeclarativeModule(entry, loader); else linkDynamicModule(entry, loader); } curGroupDeclarative = !curGroupDeclarative; } } // module binding records function getOrCreateModuleRecord(name, moduleRecords) { return moduleRecords[name] || (moduleRecords[name] = { name: name, dependencies: [], exports: {}, // start from an empty module and extend importers: [] }); } function linkDeclarativeModule(entry, loader) { // only link if already not already started linking (stops at circular) if (entry.module) return; var moduleRecords = loader._loader.moduleRecords; var module = entry.module = getOrCreateModuleRecord(entry.name, moduleRecords); var exports = entry.module.exports; var declaration = entry.declare.call(__global, function(name, value) { module.locked = true; if (typeof name == 'object') { for (var p in name) exports[p] = name[p]; } else { exports[name] = value; } for (var i = 0, l = module.importers.length; i < l; i++) { var importerModule = module.importers[i]; if (!importerModule.locked) { var importerIndex = indexOf.call(importerModule.dependencies, module); importerModule.setters[importerIndex](exports); } } module.locked = false; return value; }); module.setters = declaration.setters; module.execute = declaration.execute; if (!module.setters || !module.execute) { throw new TypeError('Invalid System.register form for ' + entry.name); } // now link all the module dependencies for (var i = 0, l = entry.normalizedDeps.length; i < l; i++) { var depName = entry.normalizedDeps[i]; var depEntry = loader.defined[depName]; var depModule = moduleRecords[depName]; // work out how to set depExports based on scenarios... var depExports; if (depModule) { depExports = depModule.exports; } // dynamic, already linked in our registry else if (depEntry && !depEntry.declarative) { depExports = depEntry.esModule; } // in the loader registry else if (!depEntry) { depExports = loader.get(depName); } // we have an entry -> link else { linkDeclarativeModule(depEntry, loader); depModule = depEntry.module; depExports = depModule.exports; } // only declarative modules have dynamic bindings if (depModule && depModule.importers) { depModule.importers.push(module); module.dependencies.push(depModule); } else { module.dependencies.push(null); } // run the setter for this dependency if (module.setters[i]) module.setters[i](depExports); } } // An analog to loader.get covering execution of all three layers (real declarative, simulated declarative, simulated dynamic) function getModule(name, loader) { var exports; var entry = loader.defined[name]; if (!entry) { exports = loader.get(name); if (!exports) throw new Error('Unable to load dependency ' + name + '.'); } else { if (entry.declarative) ensureEvaluated(name, [], loader); else if (!entry.evaluated) linkDynamicModule(entry, loader); exports = entry.module.exports; } if ((!entry || entry.declarative) && exports && exports.__useDefault) return exports['default']; return exports; } function linkDynamicModule(entry, loader) { if (entry.module) return; var exports = {}; var module = entry.module = { exports: exports, id: entry.name }; // AMD requires execute the tree first if (!entry.executingRequire) { for (var i = 0, l = entry.normalizedDeps.length; i < l; i++) { var depName = entry.normalizedDeps[i]; // we know we only need to link dynamic due to linking algorithm var depEntry = loader.defined[depName]; if (depEntry) linkDynamicModule(depEntry, loader); } } // now execute entry.evaluated = true; var output = entry.execute.call(__global, function(name) { for (var i = 0, l = entry.deps.length; i < l; i++) { if (entry.deps[i] != name) continue; return getModule(entry.normalizedDeps[i], loader); } throw new TypeError('Module ' + name + ' not declared as a dependency.'); }, exports, module); if (output) module.exports = output; // create the esModule object, which allows ES6 named imports of dynamics exports = module.exports; if (exports && exports.__esModule) { entry.esModule = exports; } else { var hasOwnProperty = exports && exports.hasOwnProperty; entry.esModule = {}; for (var p in exports) { if (!hasOwnProperty || exports.hasOwnProperty(p)) entry.esModule[p] = exports[p]; } entry.esModule['default'] = exports; defineProperty(entry.esModule, '__useDefault', { value: true }); } } /* * Given a module, and the list of modules for this current branch, * ensure that each of the dependencies of this module is evaluated * (unless one is a circular dependency already in the list of seen * modules, in which case we execute it) * * Then we evaluate the module itself depth-first left to right * execution to match ES6 modules */ function ensureEvaluated(moduleName, seen, loader) { var entry = loader.defined[moduleName]; // if already seen, that means it's an already-evaluated non circular dependency if (!entry || entry.evaluated || !entry.declarative) return; // this only applies to declarative modules which late-execute seen.push(moduleName); for (var i = 0, l = entry.normalizedDeps.length; i < l; i++) { var depName = entry.normalizedDeps[i]; if (indexOf.call(seen, depName) == -1) { if (!loader.defined[depName]) loader.get(depName); else ensureEvaluated(depName, seen, loader); } } if (entry.evaluated) return; entry.evaluated = true; entry.module.execute.call(__global); } // override the delete method to also clear the register caches hook('delete', function(del) { return function(name) { delete this._loader.moduleRecords[name]; delete this.defined[name]; return del.call(this, name); }; }); var registerRegEx = /^\s*(\/\*.*\*\/\s*|\/\/[^\n]*\s*)*System\.register(Dyanmic)?\s*\(/; hook('fetch', function(fetch) { return function(load) { if (this.defined[load.name]) { load.metadata.format = 'defined'; return ''; } // this is the synchronous chain for onScriptLoad anonRegister = null; calledRegister = false; if (load.metadata.format == 'register') load.metadata.scriptLoad = true; // NB remove when "deps " is deprecated load.metadata.deps = load.metadata.deps || []; return fetch.call(this, load); }; }); hook('translate', function(translate) { // we run the meta detection here (register is after meta) return function(load) { return Promise.resolve(translate.call(this, load)).then(function(source) { if (typeof load.metadata.deps === 'string') load.metadata.deps = load.metadata.deps.split(','); load.metadata.deps = load.metadata.deps || []; // run detection for register format if (load.metadata.format == 'register' || !load.metadata.format && load.source.match(registerRegEx)) load.metadata.format = 'register'; return source; }); }; }); hook('instantiate', function(instantiate) { return function(load) { var loader = this; var entry; // first we check if this module has already been defined in the registry if (loader.defined[load.name]) { entry = loader.defined[load.name]; entry.deps = entry.deps.concat(load.metadata.deps); } // picked up already by a script injection else if (load.metadata.entry) entry = load.metadata.entry; // otherwise check if it is dynamic else if (load.metadata.execute) { entry = { declarative: false, deps: load.metadata.deps || [], execute: load.metadata.execute, executingRequire: load.metadata.executingRequire // NodeJS-style requires or not }; } // Contains System.register calls else if (load.metadata.format == 'register' || load.metadata.format == 'esm' || load.metadata.format == 'es6') { anonRegister = null; calledRegister = false; __exec.call(loader, load); if (anonRegister) entry = anonRegister; else load.metadata.bundle = true; if (!entry && loader.defined[load.name]) entry = loader.defined[load.name]; if (!calledRegister && !load.metadata.registered) throw new TypeError(load.name + ' detected as System.register but didn\'t execute.'); } // named bundles are just an empty module if (!entry) entry = { declarative: false, deps: load.metadata.deps, execute: function() { return loader.newModule({}); } }; // place this module onto defined for circular references loader.defined[load.name] = entry; entry.deps = dedupe(entry.deps); entry.name = load.name; // first, normalize all dependencies var normalizePromises = []; for (var i = 0, l = entry.deps.length; i < l; i++) normalizePromises.push(Promise.resolve(loader.normalize(entry.deps[i], load.name))); return Promise.all(normalizePromises).then(function(normalizedDeps) { entry.normalizedDeps = normalizedDeps; return { deps: entry.deps, execute: function() { // recursively ensure that the module and all its // dependencies are linked (with dependency group handling) link(load.name, loader); // now handle dependency execution in correct order ensureEvaluated(load.name, [], loader); // remove from the registry loader.defined[load.name] = undefined; // return the defined module object return loader.newModule(entry.declarative ? entry.module.exports : entry.esModule); } }; }); }; }); })();