lfr-amd-loader
Version:
AMD Loader with support for combo URL and conditional loading
561 lines (456 loc) • 19.3 kB
JavaScript
/**
* 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;
}
;