systemjs/lib/register.js
2015-09-06 10:08:27 +02:00

581 lines
18 KiB
JavaScript

/*
* 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 = false;
function doRegister(loader, name, register) {
calledRegister = true;
// named register
if (name) {
// ideally wouldn't apply map config to bundle names but
// dependencies go through map regardless so we can't restrict
// could reconsider in shift to new spec
name = (loader.normalizeSync || loader.normalize).call(loader, name);
register.name = name;
if (!(name in loader.defined))
loader.defined[name] = register;
}
// anonymous register
else {
if (anonRegister)
throw new TypeError('Invalid anonymous System.register module load. If loading a single module, ensure anonymous System.register is loaded via System.import. If loading a bundle, ensure all the System.register calls are named.');
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
* - esmExports whether to extend the esModule with named exports
*
* 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);
if (calledRegister) {
// anonymous define
if (anonRegister)
load.metadata.entry = anonRegister;
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 Module() {}
defineProperty(Module, 'toString', {
value: function() {
return 'Module';
}
});
function getOrCreateModuleRecord(name, moduleRecords) {
return moduleRecords[name] || (moduleRecords[name] = {
name: name,
dependencies: [],
exports: new Module(), // 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 setters for all entries with the matching dependency name
var originalIndices = entry.originalIndices[i];
for (var j = 0, len = originalIndices.length; j < len; ++j) {
var index = originalIndices[j];
if (module.setters[index]) {
module.setters[index](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;
// __esModule flag treats as already-named
if (exports && exports.__esModule)
entry.esModule = exports;
// set module as 'default' export, then fake named exports by iterating properties
else if (entry.esmExports)
entry.esModule = getESModule(exports);
// just use the 'default' export
else
entry.esModule = { 'default': exports };
}
/*
* 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 leadingCommentAndMetaRegEx = /^\s*(\/\*[^\*]*(\*(?!\/)[^\*]*)*\*\/|\s*\/\/[^\n]*|\s*"[^"]+"\s*;?|\s*'[^']+'\s*;?)*\s*/;
function detectRegisterFormat(source) {
var leadingCommentAndMeta = source.match(leadingCommentAndMetaRegEx);
return leadingCommentAndMeta && source.substr(leadingCommentAndMeta[0].length, 15) == 'System.register';
}
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.authorization)
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.bundle || !load.metadata.format && detectRegisterFormat(load.source))
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;
if (typeof __exec != 'undefined')
__exec.call(loader, load);
if (!calledRegister && !load.metadata.registered)
throw new TypeError(load.name + ' detected as System.register but didn\'t execute.');
if (anonRegister)
entry = anonRegister;
else
load.metadata.bundle = true;
if (!entry && loader.defined[load.name])
entry = loader.defined[load.name];
anonRegister = null;
calledRegister = false;
}
// 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;
var grouped = group(entry.deps);
entry.deps = grouped.names;
entry.originalIndices = grouped.indices;
entry.name = load.name;
entry.esmExports = load.metadata.esmExports !== false;
// 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);
}
};
});
};
});
})();