lfr-amd-loader
Version:
AMD Loader with support for combo URL and conditional loading
1,520 lines (1,279 loc) • 48.5 kB
JavaScript
/*!
* @overview es6-promise - a tiny implementation of Promises/A+.
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
* @license Licensed under MIT license
* See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE
* @version 2.0.0
*/
(function () {
function r(a, b) {
n[l] = a;
n[l + 1] = b;
l += 2;
2 === l && A()
}
function s(a) {
return "function" === typeof a
}
function F() {
return function () {
process.nextTick(t)
}
}
function G() {
var a = 0,
b = new B(t),
c = document.createTextNode("");
b.observe(c, {
characterData: !0
});
return function () {
c.data = a = ++a % 2
}
}
function H() {
var a = new MessageChannel;
a.port1.onmessage = t;
return function () {
a.port2.postMessage(0)
}
}
function I() {
return function () {
setTimeout(t, 1)
}
}
function t() {
for (var a = 0; a < l; a += 2)(0, n[a])(n[a + 1]), n[a] = void 0, n[a + 1] = void 0;
l = 0
}
function p() {}
function J(a, b, c, d) {
try {
a.call(b, c, d)
} catch (e) {
return e
}
}
function K(a, b, c) {
r(function (a) {
var e = !1,
f = J(c, b, function (c) {
e || (e = !0, b !== c ? q(a, c) : m(a, c))
}, function (b) {
e || (e = !0, g(a, b))
});
!e && f && (e = !0, g(a, f))
}, a)
}
function L(a, b) {
1 === b.a ? m(a, b.b) : 2 === a.a ? g(a, b.b) : u(b, void 0, function (b) {
q(a, b)
}, function (b) {
g(a, b)
})
}
function q(a, b) {
if (a === b) g(a, new TypeError("You cannot resolve a promise with itself"));
else if ("function" === typeof b || "object" === typeof b && null !== b) if (b.constructor === a.constructor) L(a, b);
else {
var c;
try {
c = b.then
} catch (d) {
v.error = d, c = v
}
c === v ? g(a, v.error) : void 0 === c ? m(a, b) : s(c) ? K(a, b, c) : m(a, b)
} else m(a, b)
}
function M(a) {
a.f && a.f(a.b);
x(a)
}
function m(a, b) {
void 0 === a.a && (a.b = b, a.a = 1, 0 !== a.e.length && r(x, a))
}
function g(a, b) {
void 0 === a.a && (a.a = 2, a.b = b, r(M, a))
}
function u(a, b, c, d) {
var e = a.e,
f = e.length;
a.f = null;
e[f] = b;
e[f + 1] = c;
e[f + 2] = d;
0 === f && a.a && r(x, a)
}
function x(a) {
var b = a.e,
c = a.a;
if (0 !== b.length) {
for (var d, e, f = a.b, g = 0; g < b.length; g += 3) d = b[g], e = b[g + c], d ? C(c, d, e, f) : e(f);
a.e.length = 0
}
}
function D() {
this.error =
null
}
function C(a, b, c, d) {
var e = s(c),
f, k, h, l;
if (e) {
try {
f = c(d)
} catch (n) {
y.error = n, f = y
}
f === y ? (l = !0, k = f.error, f = null) : h = !0;
if (b === f) {
g(b, new TypeError("A promises callback cannot return that same promise."));
return
}
} else f = d, h = !0;
void 0 === b.a && (e && h ? q(b, f) : l ? g(b, k) : 1 === a ? m(b, f) : 2 === a && g(b, f))
}
function N(a, b) {
try {
b(function (b) {
q(a, b)
}, function (b) {
g(a, b)
})
} catch (c) {
g(a, c)
}
}
function k(a, b, c, d) {
this.n = a;
this.c = new a(p, d);
this.i = c;
this.o(b) ? (this.m = b, this.d = this.length = b.length, this.l(), 0 === this.length ? m(this.c, this.b) : (this.length = this.length || 0, this.k(), 0 === this.d && m(this.c, this.b))) : g(this.c, this.p())
}
function h(a) {
O++;
this.b = this.a = void 0;
this.e = [];
if (p !== a) {
if (!s(a)) throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");
if (!(this instanceof h)) throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
N(this, a)
}
}
var E = Array.isArray ? Array.isArray : function (a) {
return "[object Array]" === Object.prototype.toString.call(a)
},
l = 0,
w = "undefined" !== typeof window ? window : {},
B = w.MutationObserver || w.WebKitMutationObserver,
w = "undefined" !== typeof Uint8ClampedArray && "undefined" !== typeof importScripts && "undefined" !== typeof MessageChannel,
n = Array(1E3),
A;
A = "undefined" !== typeof process && "[object process]" === {}.toString.call(process) ? F() : B ? G() : w ? H() : I();
var v = new D,
y = new D;
k.prototype.o = function (a) {
return E(a)
};
k.prototype.p = function () {
return Error("Array Methods must be provided an Array")
};
k.prototype.l =
function () {
this.b = Array(this.length)
};
k.prototype.k = function () {
for (var a = this.length, b = this.c, c = this.m, d = 0; void 0 === b.a && d < a; d++) this.j(c[d], d)
};
k.prototype.j = function (a, b) {
var c = this.n;
"object" === typeof a && null !== a ? a.constructor === c && void 0 !== a.a ? (a.f = null, this.g(a.a, b, a.b)) : this.q(c.resolve(a), b) : (this.d--, this.b[b] = this.h(a))
};
k.prototype.g = function (a, b, c) {
var d = this.c;
void 0 === d.a && (this.d--, this.i && 2 === a ? g(d, c) : this.b[b] = this.h(c));
0 === this.d && m(d, this.b)
};
k.prototype.h = function (a) {
return a
};
k.prototype.q = function (a, b) {
var c = this;
u(a, void 0, function (a) {
c.g(1, b, a)
}, function (a) {
c.g(2, b, a)
})
};
var O = 0;
h.all = function (a, b) {
return (new k(this, a, !0, b)).c
};
h.race = function (a, b) {
function c(a) {
q(e, a)
}
function d(a) {
g(e, a)
}
var e = new this(p, b);
if (!E(a)) return (g(e, new TypeError("You must pass an array to race.")), e);
for (var f = a.length, h = 0; void 0 === e.a && h < f; h++) u(this.resolve(a[h]), void 0, c, d);
return e
};
h.resolve = function (a, b) {
if (a && "object" === typeof a && a.constructor === this) return a;
var c = new this(p, b);
q(c, a);
return c
};
h.reject = function (a, b) {
var c = new this(p, b);
g(c, a);
return c
};
h.prototype = {
constructor: h,
then: function (a, b) {
var c = this.a;
if (1 === c && !a || 2 === c && !b) return this;
var d = new this.constructor(p),
e = this.b;
if (c) {
var f = arguments[c - 1];
r(function () {
C(c, d, f, e)
})
} else u(this, d, a, b);
return d
},
"catch": function (a) {
return this.then(null, a)
}
};
var z = {
Promise: h,
polyfill: function () {
var a;
a = "undefined" !== typeof global ? global : "undefined" !== typeof window && window.document ? window : self;
"Promise" in a && "resolve" in a.Promise && "reject" in a.Promise && "all" in a.Promise && "race" in a.Promise &&
function () {
var b;
new a.Promise(function (a) {
b = a
});
return s(b)
}() || (a.Promise = h)
}
};
"function" === typeof define && define.amd ? define(function () {
return z
}) : "undefined" !== typeof module && module.exports ? module.exports = z : "undefined" !== typeof this && (this.ES6Promise = z)
}).call(this);
(function() {
var global = {};
global.__CONFIG__ = window.__CONFIG__;
(function (global, factory) {
'use strict';
var built = factory(global);
/* istanbul ignore else */
if (typeof module === 'object' && module) {
module.exports = built;
}
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define(factory);
}
global.EventEmitter = built;
}(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this, function (global) {
'use strict';
/**
* Creates an instance of EventEmitter class.
*
* @constructor
*/
function EventEmitter() {
this._events = {};
}
EventEmitter.prototype = {
constructor: EventEmitter,
/**
* Adds event listener to an event.
*
* @param {string} event The name of the event.
* @param {Function} callback Callback method to be invoked when event is being emitted.
*/
on: function (event, callback) {
var listeners = this._events[event] = this._events[event] || [];
listeners.push(callback);
},
/**
* Removes an event from the list of event listeners to some event.
*
* @param {string} event The name of the event.
* @param {function} callback Callback method to be removed from the list of listeners.
*/
off: function (event, callback) {
var listeners = this._events[event];
if (listeners) {
var callbackIndex = listeners.indexOf(callback);
if (callbackIndex > -1) {
listeners.splice(callbackIndex, 1);
} else {
console.warn('Off: callback was not removed: ' + callback.toString());
}
} else {
console.warn('Off: there are no listeners for event: ' + event);
}
},
/**
* Emits an event. The function calls all registered listeners in the order they have been added. The provided args
* param will be passed to each listener of the event.
*
* @param {string} event The name of the event.
* @param {object} args Object, which will be passed to the listener as only argument.
*/
emit: function (event, args) {
var listeners = this._events[event];
if (listeners) {
// Slicing is needed to prevent the following situation:
// A listener is being invoked. During its execution, it may
// remove itself from the list. In this case, for loop will
// be damaged, since i will be out of sync.
listeners = listeners.slice(0);
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
listener.call(listener, args);
}
} else {
console.warn('No listeners for event: ' + event);
}
}
};
return EventEmitter;
}));
(function (global, factory) {
'use strict';
var built = factory(global);
/* istanbul ignore else */
if (typeof module === 'object' && module) {
module.exports = built;
}
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define(factory);
}
global.ConfigParser = built;
}(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this, function (global) {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Creates an instance of ConfigurationParser class.
*
* @constructor
* @param {object=} - The configuration object to be parsed.
*/
function ConfigParser(config) {
this._config = {};
this._modules = {};
this._conditionalModules = {};
this._parseConfig(config);
}
ConfigParser.prototype = {
constructor: ConfigParser,
/**
* Adds a module to the configuration.
*
* @param {object} module The module which should be added to the configuration. Should have the following
* properties:
* <ul>
* <strong>Obligatory properties</strong>:
* <li>name (String) The name of the module</li>
* <li>dependencies (Array) The modules from which it depends</li>
* </ul>
*
* <strong>Optional parameters:</strong>
* The same as those which config parameter of {@link Loader#define} method accepts.
*/
addModule: function (module) {
// Module might be added via configuration or when it arrives from the server.
// If it arrives from the server, it will have already a definition. In this case,
// we will overwrite the existing properties with those, provided from the module definition.
// Otherwise, we will just add it to the map.
var moduleDefinition = this._modules[module.name];
if (moduleDefinition) {
for (var key in module) {
if (hasOwnProperty.call(module, key)) {
moduleDefinition[key] = module[key];
}
}
} else {
this._modules[module.name] = module;
}
this._registerConditionalModule(module);
},
/**
* Returns the current configuration.
*
* @return {object} The current configuration.
*/
getConfig: function () {
return this._config;
},
/**
* Returns map with all currently registered conditional modules and their triggers.
*
* @return {object} Map with all currently registered conditional modules.
*/
getConditionalModules: function () {
return this._conditionalModules;
},
/**
* Returns map with all currently registered modules.
*
* @return {object} Map with all currently registered modules.
*/
getModules: function () {
return this._modules;
},
/**
* Maps module names to their aliases. Example:
* __CONFIG__.maps = {
* liferay: 'liferay@1.0.0'
* }
*
* When someone does require('liferay/html/js/ac.es',...),
* if the module 'liferay/html/js/ac.es' is not defined,
* then a corresponding alias will be searched. If found, the name will be replaced,
* so it will look like user did require('liferay@1.0.0/html/js/ac.es',...).
*
* @protected
* @param {array|string} module The module which have to be mapped or array of modules.
* @return {array|string} The mapped module or array of mapped modules.
*/
mapModule: function(module) {
var modules;
if (Array.isArray(module)) {
modules = module;
} else {
modules = [module];
}
for (var i = 0; i < modules.length; i++) {
var tmpModule = modules[i];
for (var alias in this._config.maps) {
/* istanbul ignore else */
if (hasOwnProperty.call(this._config.maps, alias)) {
if (tmpModule === alias || tmpModule.indexOf(alias + '/') === 0) {
tmpModule = this._config.maps[alias] + tmpModule.substring(alias.length);
modules[i] = tmpModule;
break;
}
}
}
}
return Array.isArray(module) ? modules : modules[0];
},
/**
* Parses configuration object.
*
* @protected
* @param {object} config Configuration object to be parsed.
* @return {object} The created configuration
*/
_parseConfig: function (config) {
for (var key in config) { /* istanbul ignore else */
if (hasOwnProperty.call(config, key)) {
if (key === 'modules') {
this._parseModules(config[key]);
} else {
this._config[key] = config[key];
}
}
}
return this._config;
},
/**
* Parses a provided modules configuration.
*
* @protected
* @param {object} modules Map of modules to be parsed.
* @return {object} Map of parsed modules.
*/
_parseModules: function (modules) {
for (var key in modules) { /* istanbul ignore else */
if (hasOwnProperty.call(modules, key)) {
var module = modules[key];
module.name = key;
this.addModule(module);
}
}
return this._modules;
},
/**
* Registers conditional module to the configuration.
*
* @protected
* @param {object} module Module object
*/
_registerConditionalModule: function (module) {
// Create HashMap of all modules, which have conditional modules, as an Array.
if (module.condition) {
var existingModules = this._conditionalModules[module.condition.trigger];
if (!existingModules) {
this._conditionalModules[module.condition.trigger] = existingModules = [];
}
existingModules.push(module.name);
}
}
};
return ConfigParser;
}));
(function (global, factory) {
'use strict';
var built = factory(global);
/* istanbul ignore else */
if (typeof module === 'object' && module) {
module.exports = built;
}
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define(factory);
}
global.DependencyBuilder = built;
}(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this, function (global) {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Creates an instance of DependencyBuilder class.
*
* @constructor
* @param {object} - instance of {@link ConfigParser} object.
*/
function DependencyBuilder(configParser) {
this._configParser = configParser;
this._result = [];
}
DependencyBuilder.prototype = {
constructor: DependencyBuilder,
/**
* Resolves modules dependencies.
*
* @param {array} modules List of modules which dependencies should be resolved.
* @return {array} List of module names, representing module dependencies. Module name itself is being returned too.
*/
resolveDependencies: function (modules) {
// Copy the passed modules to a resolving modules queue.
// Modules may be added there during the process of resolving.
this._queue = modules.slice(0);
var result;
try {
this._resolveDependencies();
// Reorder the modules list so the modules without dependencies will
// be moved upfront
result = this._result.reverse().slice(0);
}
finally {
this._cleanup();
}
return result;
},
/**
* Clears the used resources during the process of resolving dependencies.
*
* @protected
*/
_cleanup: function () {
var modules = this._configParser.getModules();
// Set to false all temporary markers which were set during the process of
// dependencies resolving.
for (var key in modules) { /* istanbul ignore else */
if (hasOwnProperty.call(modules, key)) {
var module = modules[key];
module.conditionalMark = false;
module.mark = false;
module.tmpMark = false;
}
}
this._queue.length = 0;
this._result.length = 0;
},
/**
* Processes conditional modules. If a module has conditional module as dependency, this module will be added to
* the list of modules, which dependencies should be resolved.
*
* @protected
* @param {object} module Module, which will be checked for conditional modules as dependencies.
*/
_processConditionalModules: function (module) {
var conditionalModules = this._configParser.getConditionalModules()[module.name];
// If the current module has conditional modules as dependencies,
// add them to the list (queue) of modules, which have to be resolved.
if (conditionalModules && !module.conditionalMark) {
var modules = this._configParser.getModules();
for (var i = 0; i < conditionalModules.length; i++) {
var conditionalModule = modules[conditionalModules[i]];
if (this._queue.indexOf(conditionalModule.name) === -1 && this._testConditionalModule(conditionalModule.condition.test)) {
this._queue.push(conditionalModule.name);
}
}
module.conditionalMark = true;
}
},
/**
* Processes all modules in the {@link DependencyBuilder#_queue} and resolves their dependencies. The function
* implements standard
* [topological sorting based on depth-first search]{@link http://en.wikipedia.org/wiki/Topological_sorting}.
*
* @protected
*/
_resolveDependencies: function () {
// Process all modules in the queue.
// Note: modules may be added to the queue during the process of evaluating.
var modules = this._configParser.getModules();
for (var i = 0; i < this._queue.length; i++) {
var module = modules[this._queue[i]];
if (!module.mark) {
this._visit(module);
}
}
},
/**
* Executes the test function of an conditional module and adds it to the list of module dependencies if the
* function returns true.
*
* @param {function|string} testFunction The function which have to be executed. May be Function object or string.
* @return {boolean} The result of the execution of the test function.
*/
_testConditionalModule: function (testFunction) {
if (typeof testFunction === 'function') {
return testFunction();
} else {
return eval('false || ' + testFunction)();
}
},
/**
* Visits a module during the process of resolving dependencies. The function will throw exception in case of
* circular dependencies among modules.
*
* @protected
* @param {object} module The module which have to be visited.
*/
_visit: function (module) {
// Directed Acyclic Graph is supported only, throw exception if there are circular dependencies.
if (module.tmpMark) {
throw new Error('Error processing module: ' + module.name + '. ' + 'The provided configuration is not Directed Acyclic Graph.');
}
// Check if this module has conditional modules and add them to the queue if so.
this._processConditionalModules(module);
if (!module.mark) {
module.tmpMark = true;
var modules = this._configParser.getModules();
for (var i = 0; i < module.dependencies.length; i++) {
var dependencyName = module.dependencies[i];
if (dependencyName === 'exports') {
continue;
}
// Map the modules to their aliases
dependencyName = this._configParser.mapModule(dependencyName);
var moduleDependency = modules[dependencyName];
if (!moduleDependency) {
throw new Error('Cannot resolve module: ' + module.name + ' ' + 'due to not yet registered or wrongly specified dependency: ' + dependencyName);
}
this._visit(moduleDependency, modules);
}
module.mark = true;
module.tmpMark = false;
this._result.unshift(module.name);
}
},
/**
* @property {array} _queue List of modules, which dependencies should be resolved. Initially, it is copy of
* the array of modules, passed for resolving; during the process more modules may be added to the queue. For
* example, these might be conditional modules.
*
* @protected
* @memberof! DependencyBuilder#
* @default []
*/
_queue: []
};
return DependencyBuilder;
}));
(function (global, factory) {
'use strict';
var built = factory(global);
/* istanbul ignore else */
if (typeof module === 'object' && module) {
module.exports = built;
}
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define(factory);
}
global.URLBuilder = built;
}(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this, function (global) {
'use strict';
// External protocols regex, supports:
// "http", "https", "//" and "www."
var REGEX_EXTERNAL_PROTOCOLS = /^https?:\/\/|\/\/|www\./;
/**
* Creates an instance of URLBuilder class.
*
* @constructor
* @param {object} - instance of {@link ConfigParser} object.
*/
function URLBuilder(configParser) {
this._configParser = configParser;
}
URLBuilder.prototype = {
constructor: URLBuilder,
/**
* Returns a list of URLs from provided list of modules.
*
* @param {array} modules List of modules for which URLs should be created.
* @return {array} List of URLs.
*/
build: function (modules) {
var buffer = [];
var result = [];
var registeredModules = this._configParser.getModules();
var config = this._configParser.getConfig();
var basePath = config.basePath;
/* istanbul ignore else */
if (basePath.charAt(basePath.length - 1) !== '/') {
basePath += '/';
}
for (var i = 0; i < modules.length; i++) {
var module = registeredModules[modules[i]];
// If module has fullPath, individual URL have to be created.
if (module.fullPath) {
result.push(module.fullPath);
} else {
var path = this._getModulePath(module);
// If the URL starts with external protocol, individual URL shall be created.
if (REGEX_EXTERNAL_PROTOCOLS.test(path)) {
result.push(path);
// If combine is disabled, create individual URL based on config URL and module path.
} else if (!config.combine) {
result.push(config.url + basePath + path);
} else {
// If combine is true and module does not have full path, it will be collected
// in a buffer to be loaded among with other modules from combo loader.
buffer.push(path);
}
}
module.requested = true;
}
// Add to the result all modules, which have to be combined.
if (buffer.length) {
result.push(config.url + basePath + buffer.join('&' + basePath));
buffer.length = 0;
}
return result;
},
/**
* Returns the path for a module. If module has property path, it will be returned directly. Otherwise,
* the name of module will be used and extension .js will be added to module name if omitted.
*
* @protected
* @param {object} module The module which path should be returned.
* @return {string} Module path.
*/
_getModulePath: function (module) {
var path = module.path || module.name;
var paths = this._configParser.getConfig().paths;
for (var key in paths) {
/* istanbul ignore else */
if (Object.prototype.hasOwnProperty.call(paths, key)) {
if (path === key || path.indexOf(key + '/') === 0) {
path = paths[key] + path.substring(key.length);
}
}
}
if (!REGEX_EXTERNAL_PROTOCOLS.test(path) && path.indexOf('.js') !== path.length - 3) {
path += '.js';
}
return path;
}
};
return URLBuilder;
}));
(function (global, factory) {
'use strict';
var built = factory(global);
/* istanbul ignore else */
if (typeof module === 'object' && module) {
module.exports = built;
}
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define(factory);
}
/* jshint newcap:false */
global.Loader = new built();
global.require = global.Loader.require.bind(global.Loader);
global.define = global.Loader.define.bind(global.Loader);
}(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this, function (global) {
'use strict';
/**
* Creates an instance of Loader class.
*
* @namespace Loader
* @extends EventEmitter
* @constructor
*/
function Loader(config) {
Loader.superclass.constructor.apply(this, arguments);
this._config = config || global.__CONFIG__;
this._modulesMap = {};
}
extend(Loader, global.EventEmitter, {
/**
* Defines a module in the system and fires {@link Loader#event:moduleRegister} event with the registered module as param.
*
* @memberof! Loader#
* @param {string} name The name of the module.
* @param {array} dependencies List of module dependencies.
* @param {function} implementation The implementation of the module.
* @param {object=} config Object configuration:
* <ul>
* <strong>Optional properties</strong>:
* <li>path (String) - Explicitly set path of the module. If omitted, module name will be used as path</li>
* <li>condition (Object) Object which represents if the module should be added automatically after another
* module.
* It should have the following properties:</li>
* <ul>
* <li>trigger - the module, which should trigger the loading of the current module</li>
* <li>test - function, which should return true if module should be loaded</li>
* </ul>
* </ul>
* @return {Object} The constructed module.
*/
define: function(name, dependencies, implementation, config) {
// Create new module by merging the provided config with the passed name,
// dependencies and the implementation.
var module = config || {};
name = this._getConfigParser().mapModule(name);
module.name = name;
module.dependencies = dependencies;
module.pendingImplementation = implementation;
var configParser = this._getConfigParser();
configParser.addModule(module);
if (!this._modulesMap[module.name]) {
this._modulesMap[module.name] = true;
}
this.emit('moduleRegister', module);
},
/**
* Returns list of currently registered conditional modules.
*
* @memberof! Loader#
* @return {array} List of currently registered conditional modules.
*/
getConditionalModules: function() {
return this._getConfigParser().getConditionalModules();
},
/**
* Returns list of currently registered modules.
*
* @memberof! Loader#
* @return {array} List of currently registered modules.
*/
getModules: function() {
return this._getConfigParser().getModules();
},
/**
* Requires list of modules. If a module is not yet registered, it will be ignored and its implementation
* in the provided success callback will be left undefined.<br>
*
* @memberof! Loader#
* @param {array|string[]} modules Modules can be specified as an array of strings or provided as
* multiple string parameters.
* @param {function} success Callback, which will be invoked in case of success. The provided parameters will
* be implementations of all required modules.
* @param {function} failure Callback, which will be invoked in case of failure. One parameter with
* information about the error will be provided.
*/
require: function() {
var self = this;
var failureCallback;
var modules;
var successCallback;
// Modules can be specified by as an array, or just as parameters to the function
// We do not slice or leak arguments to not cause V8 performance penalties
// TODO: This could be extracted as an inline function (hint)
var isArgsArray = Array.isArray ? Array.isArray(arguments[0]) : /* istanbul ignore next */
Object.prototype.toString.call(arguments[0]) === '[object Array]';
if (isArgsArray) {
modules = arguments[0];
successCallback = typeof arguments[1] === 'function' ? arguments[1] : null;
failureCallback = typeof arguments[2] === 'function' ? arguments[2] : null;
} else {
modules = [];
for (var i = 0; i < arguments.length; ++i) {
if (typeof arguments[i] === 'string') {
modules[i] = arguments[i];
/* istanbul ignore else */
} else if (typeof arguments[i] === 'function') {
successCallback = arguments[i];
failureCallback = typeof arguments[++i] === 'function' ? arguments[i] : /* istanbul ignore next */ null;
break;
}
}
}
modules = this._getConfigParser().mapModule(modules);
// Resolve the dependencies of the specified modules by the user
// then load their JS scripts
self._resolveDependencies(modules).then(function(dependencies) {
return self._loadModules(dependencies);
}).then(function(loadedModules) {
var moduleImplementations = self._getModuleImplementations(modules);
/* istanbul ignore else */
if (successCallback) {
successCallback.apply(successCallback, moduleImplementations);
}
}, function(error) { /* istanbul ignore else */
if (failureCallback) {
failureCallback.call(failureCallback, error);
}
});
},
/**
* Creates Promise for module. It will be resolved as soon as module is being loaded from server.
*
* @memberof! Loader#
* @protected
* @param {string} moduleName The name of module for which Promise should be created.
* @return {Promise} Promise, which will be resolved as soon as the requested module is being loaded.
*/
_createModulePromise: function(moduleName) {
var self = this;
return new Promise(function(resolve, reject) {
var onModuleRegister = function(registeredModule) {
if (registeredModule.name === moduleName) {
self.off('moduleRegister', onModuleRegister);
// Overwrite the promise entry in modules map with simple true value.
// Hopefully GC will remove this promise from the memory.
self._modulesMap[moduleName] = true;
resolve(moduleName);
}
};
self.on('moduleRegister', onModuleRegister);
});
},
/**
* Returns instance of {@link ConfigParser} class currently used.
*
* @memberof! Loader#
* @protected
* @return {ConfigParser} Instance of {@link ConfigParser} class.
*/
_getConfigParser: function() { /* istanbul ignore else */
if (!this._configParser) {
this._configParser = new global.ConfigParser(this._config);
}
return this._configParser;
},
/**
* Returns instance of {@link DependencyBuilder} class currently used.
*
* @memberof! Loader#
* @protected
* @return {DependencyBuilder} Instance of {@link DependencyBuilder} class.
*/
_getDependencyBuilder: function() {
if (!this._dependencyBuilder) {
this._dependencyBuilder = new global.DependencyBuilder(this._getConfigParser());
}
return this._dependencyBuilder;
},
/**
* Retrieves module implementations to an array.
*
* @memberof! Loader#
* @protected
* @param {array} requiredModules Lit of modules, which implementations will be added to an array.
* @return {array} List of modules implementations.
*/
_getModuleImplementations: function(requiredModules) {
var moduleImplementations = [];
var modules = this._getConfigParser().getModules();
for (var i = 0; i < requiredModules.length; i++) {
var requiredModule = modules[requiredModules[i]];
moduleImplementations.push(requiredModule ? requiredModule.implementation : undefined);
}
return moduleImplementations;
},
/**
* Returns instance of {@link URLBuilder} class currently used.
*
* @memberof! Loader#
* @protected
* @return {URLBuilder} Instance of {@link URLBuilder} class.
*/
_getURLBuilder: function() { /* istanbul ignore else */
if (!this._urlBuilder) {
this._urlBuilder = new global.URLBuilder(this._getConfigParser());
}
return this._urlBuilder;
},
/**
* Filters a list of modules and returns only these which have been not yet requested for delivery via network.
*
* @memberof! Loader#
* @protected
* @param {array} modules List of modules which which will be filtered.
* @return {array} List of modules not yet requested for delivery via network.
*/
_filterNotRequestedModules: function(modules) {
var missingModules = [];
var registeredModules = this._getConfigParser().getModules();
for (var i = 0; i < modules.length; i++) {
var registeredModule = registeredModules[modules[i]];
// Get all modules which are not yet requested from the server.
if (registeredModule !== 'exports' && (!registeredModule || !registeredModule.requested)) {
missingModules.push(modules[i]);
}
}
return missingModules;
},
/**
* Loads list of modules.
*
* @memberof! Loader#
* @protected
* @param {array} modules List of modules to be loaded.
* @return {Promise} Promise, which will be resolved as soon as all module a being loaded.
*/
_loadModules: function(moduleNames) {
var self = this;
return new Promise(function(resolve, reject) {
// First, detect any still unloaded modules
var notRequestedModules = self._filterNotRequestedModules(moduleNames);
if (notRequestedModules.length) {
// If there are unloaded modules, construct their URLs
var urls = self._getURLBuilder().build(notRequestedModules);
var pendingScripts = [];
// Create promises for each of the scripts, which should be loaded
for (var i = 0; i < urls.length; i++) {
pendingScripts.push(self._loadScript(urls[i]));
}
// Wait for resolving all script Promises
// As soon as that happens, wait for each module to define itself
Promise.all(pendingScripts).then(function(loadedScripts) {
return self._waitForModules(moduleNames);
})
// As soon as all scripts were loaded and all dependencies have been resolved,
// resolve the main Promise
.then(function(modules) {
resolve(modules);
})
// If any script fails to load or other error happens,
// reject the main Promise
.catch(function(error) {
reject(error);
});
} else {
// If there are no any missing modules, just wait for modules dependencies
// to be resolved and then resolve the main promise
self._waitForModules(moduleNames).then(function(modules) {
resolve(modules);
})
// If some error happens, for example if some module implementation
// throws error, reject the main Promise
.
catch(function(error) {
reject(error);
});
}
});
},
/**
* Loads a <script> element on the page.
*
* @memberof! Loader#
* @protected
* @param {string} url The src of the script.
* @return {Promise} Promise which will be resolved as soon as the script is being loaded.
*/
_loadScript: function(url) {
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = url;
// On ready state change is needed for IE < 9, not sure if that is needed anymore,
// it depends which browsers will we support at the end
script.onload = script.onreadystatechange = function() { /* istanbul ignore else */
if (!this.readyState || /* istanbul ignore next */ this.readyState === 'complete' || /* istanbul ignore next */ this.readyState === 'load') {
script.onload = script.onreadystatechange = null;
resolve(script);
}
};
// If some script fails to load, reject the main Promise
script.onerror = function() {
document.body.removeChild(script);
reject(script);
};
document.body.appendChild(script);
});
},
/**
* Resolves modules dependencies.
*
* @memberof! Loader#
* @protected
* @param {array} modules List of modules which dependencies should be resolved.
* @return {Promise} Promise which will be resolved as soon as all dependencies are being resolved.
*/
_resolveDependencies: function(modules) {
var self = this;
return new Promise(function(resolve, reject) {
try {
var registeredModules = self._getConfigParser().getModules();
var finalModules = [];
// Ignore wrongly specified byt the user (misspelled) modules
for (var i = 0; i < modules.length; i++) {
if (registeredModules[modules[i]]) {
finalModules.push(modules[i]);
}
}
var dependencies = self._getDependencyBuilder().resolveDependencies(finalModules);
resolve(dependencies);
} catch (error) {
reject(error);
}
});
},
/**
* Invokes the implementation method of list of modules passing the implementations of its dependencies.
*
* @memberof! Loader#
* @protected
* @param {array} modules List of modules to which implementation should be set.
*/
_setModuleImplementation: function(modules) {
var registeredModules = this._getConfigParser().getModules();
for (var i = 0; i < modules.length; i++) {
var module = modules[i];
if (module.implementation) {
continue;
}
var dependencyImplementations = [];
// Leave exports implementation undefined by default
var exportsImpl;
for (var j = 0; j < module.dependencies.length; j++) {
var dependency = module.dependencies[j];
var impl;
// If the current dependency of this module is 'exports',
// create an empty object and pass it as implementation of
// 'exports' module
if (dependency === 'exports') {
exportsImpl = {};
dependencyImplementations.push(exportsImpl);
} else {
// otherwise set as value the implementation of the
// registered module
dependency = this._getConfigParser().mapModule(dependency);
var dependencyModule = registeredModules[dependency];
impl = dependencyModule.implementation;
dependencyImplementations.push(impl);
}
}
var result = module.pendingImplementation.apply(module.pendingImplementation, dependencyImplementations);
// Store as implementation either the returned value from function invocation
// or the implementation of the 'exports' object.
// The final implementation of this module may be undefined if there is no
// returned value, or the object does not have 'exports' dependency
module.implementation = result || exportsImpl;
}
},
/**
* Resolves a Promise as soon as all module dependencies are being resolved or it has implementation already.
*
* @memberof! Loader#
* @protected
* @param {object} module The module for which this function should wait.
* @return {Promise}
*/
_waitForModule: function(moduleName) {
var self = this;
// Check if there is already a promise for this module.
// If there is not - create one and store it to module promises map.
var modulePromise = self._modulesMap[moduleName];
if (!modulePromise) {
modulePromise = self._createModulePromise(moduleName);
self._modulesMap[moduleName] = modulePromise;
}
return modulePromise;
},
/**
* Resolves a Promise as soon as all dependencies of all provided modules are being resolved and modules have
* implementations.
*
* @memberof! Loader#
* @protected
* @param {array} modules List of modules for which implementations this function should wait.
* @return {Promise}
*/
_waitForModules: function(moduleNames) {
var self = this;
return new Promise(function(resolve, reject) {
var modulesPromises = [];
for (var i = 0; i < moduleNames.length; i++) {
modulesPromises.push(self._waitForModule(moduleNames[i]));
}
Promise.all(modulesPromises).then(function(uselessPromises) {
var registeredModules = self._getConfigParser().getModules();
var definedModules = [];
for (var i = 0; i < moduleNames.length; i++) {
definedModules.push(registeredModules[moduleNames[i]]);
}
self._setModuleImplementation(definedModules);
resolve(definedModules);
});
});
}
/**
* Indicates that a module has been registered.
*
* @event Loader#moduleRegister
* @param {object} module - The registered module.
*/
});
// Utilities methods
function extend(r, s, px) { /* istanbul ignore if else */
if (!s || !r) {
throw ('extend failed, verify dependencies');
}
var sp = s.prototype,
rp = Object.create(sp);
r.prototype = rp;
rp.constructor = r;
r.superclass = sp;
/* istanbul ignore if else */
// assign constructor property
if (s != Object && sp.constructor == Object.prototype.constructor) {
sp.constructor = s;
}
/* istanbul ignore else */
// add prototype overrides
if (px) {
mix(rp, px);
}
return r;
}
function mix(destination, source) {
var hasOwnProperty = Object.prototype.hasOwnProperty;
for (var k in source) { /* istanbul ignore else */
if (hasOwnProperty.call(source, k)) {
destination[k] = source[k];
}
}
return destination;
}
return Loader;
}));
window.Loader = global.Loader;
window.require = global.require;
window.define = global.define;
}());