lfr-amd-loader
Version:
AMD Loader with support for combo URL and conditional loading
734 lines (605 loc) • 27.3 kB
JavaScript
(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);
global.define.amd = {};
}(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 = {};
}
Loader.prototype = Object.create(global.EventEmitter.prototype);
Loader.prototype.constructor = Loader;
Loader.superclass = global.EventEmitter.prototype;
var LoaderProtoMethods = {
/**
* Adds a module to the configuration. See {@link ConfigParser#addModule} for more details.
*
* @memberof! Loader#
* @param {Object} module The module which should be added to the configuration. See {@link ConfigParser#addModule} for more details.
* @return {Object} Returns the added module to the configuration.
*/
addModule: function(module) {
return this._getConfigParser().addModule(module);
},
/**
* 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>
* <li>exports - If the module does not expose a "define" function, then you can specify an "exports" property.
* The value of this property should be a string, which represents the value, which this module exports to
* the global namespace. For example: exports: '_'. This will mean the module exports an underscore to the
* global namespace. In this way you can load legacy modules.
* </li>
* </ul>
* @return {Object} The constructed module.
*/
define: function(name, dependencies, implementation, config) {
console.log('DEFINE', name, dependencies);
var passedArgsCount = arguments.length;
if (passedArgsCount < 2) {
console.log('DEFINE, module with one param only, returning');
// we don't support modules with implementation only
return;
} else if (passedArgsCount === 2) {
if (typeof name === 'string') {
console.log('DEFINE, module with two params only, name and implementation', name);
// there are two parameters, but the first one is not an array with dependencies,
// this is a module name
implementation = dependencies;
dependencies = ['module', 'exports'];
} else {
// anonymous module, we don't support this
return;
}
}
// Create a new module by merging the provided config with the passed name,
// dependencies and implementation.
var module = config || {};
var configParser = this._getConfigParser();
var pathResolver = this._getPathResolver();
// Resolve the path according to the parent module. Example:
// define('metal/src/component/component', ['../array/array']) will become:
// define('metal/src/component/component', ['metal/src/array/array'])
dependencies = dependencies.map(function(dependency) {
return pathResolver.resolvePath(name, dependency);
});
module.name = name;
module.dependencies = dependencies;
module.pendingImplementation = implementation;
configParser.addModule(module);
if (!this._modulesMap[module.name]) {
this._modulesMap[module.name] = true;
}
this.emit('moduleRegister', name);
},
/**
* 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;
console.log('REQUIRE CALLED');
var failureCallback;
var i;
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)
if (Array.isArray(arguments[0])) {
modules = arguments[0];
successCallback = typeof arguments[1] === 'function' ? arguments[1] : null;
failureCallback = typeof arguments[2] === 'function' ? arguments[2] : null;
} else {
modules = [];
for (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;
}
}
}
console.log('REQUIRE called with', modules);
var configParser = self._getConfigParser();
// Map the required modules so we start with clean idea what the hell we should load.
var mappedModules = configParser.mapModule(modules);
console.log('REQUIRE modules mapped to', mappedModules);
var rejectTimeout;
new Promise(function(resolve, reject) {
// Resolve the dependencies of the requested modules,
// then load them and resolve the Promise
self._resolveDependencies(mappedModules).then(function(dependencies) {
console.log('REQUIRE dependencies resolved to', dependencies);
var config = configParser.getConfig();
// Establish a load timeout and reject the Promise in case of Error
if (config.waitTimeout !== 0) {
rejectTimeout = setTimeout(function() {
var registeredModules = configParser.getModules();
var error = new Error('Load timeout for modules: ' + modules);
error.dependecies = dependencies;
error.mappedModules = mappedModules;
error.missingDependencies = dependencies.filter(function(dep) {
return !registeredModules[dep].implementation;
});
error.modules = modules;
console.log('REQUIRE timeout', error);
reject(error);
}, config.waitTimeout || 7000);
}
// Load the dependencies, then resolve the Promise
self._loadModules(dependencies).then(resolve, reject);
}, reject);
}).then(function(loadedModules) {
console.log('REQUIRE promise success');
clearTimeout(rejectTimeout);
/* istanbul ignore else */
if (successCallback) {
var moduleImplementations = self._getModuleImplementations(mappedModules);
successCallback.apply(successCallback, moduleImplementations);
}
}, function(error) {
console.log('REQUIRE promise failure');
clearTimeout(rejectTimeout);
/* 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 registeredModules = self._getConfigParser().getModules();
// Check if this is a module, which exports something
// If so, check if the exported value is available
var module = registeredModules[moduleName];
if (module.exports) {
var exportedValue = self._getValueGlobalNS(module.exports);
if (!!exportedValue) {
resolve(exportedValue);
} else {
reject(new Error('Module ' + moduleName + ' does not export the specified value: ' + module.exports));
}
} else {
var onModuleRegister = function(registeredModuleName) {
if (registeredModuleName === moduleName) {
self.off('moduleRegister', onModuleRegister);
// Overwrite the promise entry in the modules map with a 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.
*
* @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.
*
* @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 a value from the global namespace.
*
* @memberof! Loader#
* @protected
* @param {String} exports The key which should be used to retrieve the value.
* @return {Any} The retrieved value.
*/
_getValueGlobalNS: function(exports) {
return (0, eval)('this')[exports];
},
/**
* Returns an array of all missing dependencies of the passed modules.
* A missing dependency is a dependency, which does not have pending implementation yet.
*
* @memberof! Loader#
* @protected
* @param {array} moduleNames List of module names to be checked for missing dependencies.
* @return {Array<string>} A list with all missing dependencies.
*/
_getMissingDepenencies: function(moduleNames) {
var configParser = this._getConfigParser();
var registeredModules = configParser.getModules();
var missingDependencies = Object.create(null);
for (var i = 0; i < moduleNames.length; i++) {
var module = registeredModules[moduleNames[i]];
var mappedDependencies = configParser.mapModule(module.dependencies);
for (var j = 0; j < mappedDependencies.length; j++) {
var dependency = mappedDependencies[j];
var dependencyModule = registeredModules[dependency];
if (dependency !== 'exports' && dependency !== 'module' && (!dependencyModule || !dependencyModule.pendingImplementation)) {
missingDependencies[dependency] = 1;
}
}
}
return Object.keys(missingDependencies);
},
/**
* 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 an instance of {@link PathResolver} class.
*
* @memberof! Loader#
* @protected
* @return {PathResolver} Instance of {@link PathResolver} class.
*/
_getPathResolver: function() {
if (!this._pathResolver) {
this._pathResolver = new global.PathResolver();
}
return this._pathResolver;
},
/**
* Returns instance of {@link URLBuilder} class.
*
* @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.
// We exclude "exports" and "module" modules, which are part of AMD spec.
if ((registeredModule !== 'exports' && registeredModule !== 'module') && (!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 not yet requested modules
var notRequestedModules = self._filterNotRequestedModules(moduleNames);
if (notRequestedModules.length) {
// If there are not yet requested 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
console.log('SCRIPTS', urls);
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(loadedModules) {
resolve(loadedModules);
})
// 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(loadedModules) {
resolve(loadedModules);
})
// 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 dependencies = self._getDependencyBuilder().resolveDependencies(modules);
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;
} else if (module.exports) {
module.pendingImplementation = module.implementation = this._getValueGlobalNS(module.exports);
continue;
}
var dependencyImplementations = [];
// Leave exports implementation undefined by default
var exportsImpl;
var configParser = this._getConfigParser();
for (var j = 0; j < module.dependencies.length; j++) {
var dependency = module.dependencies[j];
// 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 if (dependency === 'module') {
exportsImpl = {exports: {}};
dependencyImplementations.push(exportsImpl);
} else {
// otherwise set as value the implementation of the registered module
var dependencyModule = registeredModules[configParser.mapModule(dependency)];
var impl = dependencyModule.implementation;
dependencyImplementations.push(impl);
}
}
var result;
if (typeof module.pendingImplementation === 'function') {
result = module.pendingImplementation.apply(module.pendingImplementation, dependencyImplementations);
} else {
result = module.pendingImplementation;
}
// Store as implementation either the returned value from the function's invocation,
// or one of these:
// 1. If the passed object has 'exports' property (in case of 'module' dependency), get this one.
// 2. Otherwise, get the passed object itself (in case of 'exports' dependency)
//
// The final implementation of this module may be undefined if there is no
// returned value, or the object does not have 'exports' or 'module' dependency.
if (result) {
module.implementation = result;
} else if (exportsImpl) {
module.implementation = exportsImpl.exports || 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]));
}
// Wait until all modules were loaded and their Promises resolved
Promise.all(modulesPromises).then(function(uselessPromises) {
// The modules were loaded. However, we have to check their dependencies
// one more time, because some dependencies might have not been registered in the configuration.
// In this case we have to load them too, otherwise we won't be able to properly
// get the implementation from the module.
var registeredModules = self._getConfigParser().getModules();
var defineModules = function () {
var definedModules = [];
for (var i = 0; i < moduleNames.length; i++) {
definedModules.push(registeredModules[moduleNames[i]]);
}
self._setModuleImplementation(definedModules);
resolve(definedModules);
};
var missingDependencies = self._getMissingDepenencies(moduleNames);
if (missingDependencies.length) {
console.log('MISSING DEPENDENCIES', 'requested', moduleNames, 'missing', missingDependencies);
self.require(missingDependencies, defineModules, reject);
} else {
defineModules();
}
}, reject);
});
}
/**
* Indicates that a module has been registered.
*
* @event Loader#moduleRegister
* @param {Object} module - The registered module.
*/
};
Object.keys(LoaderProtoMethods).forEach(function(key) {
Loader.prototype[key] = LoaderProtoMethods[key];
});
return Loader;
}));