UNPKG

lfr-amd-loader

Version:

AMD Loader with support for combo URL and conditional loading

1,463 lines (1,197 loc) 51.3 kB
(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'; /** * 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 properties:</strong> * The same as those which config parameter of {@link Loader#define} method accepts. * @return {Object} The added module */ 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 (Object.prototype.hasOwnProperty.call(module, key)) { moduleDefinition[key] = module[key]; } } } else { this._modules[module.name] = module; } this._registerConditionalModule(module); return this._modules[module.name]; }, /** * 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 (Object.prototype.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 (Object.prototype.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 (Object.prototype.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._pathResolver = new global.PathResolver(); 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. * If the module is not registered to the configuration, it will be automatically added there with no * dependencies. The function implements a 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 not registered, add the module on the fly with no dependencies. // Note: the module name (this._queue[i]) is expected to be already mapped. if (!module) { module = this._configParser.addModule({ name: this._queue[i], dependencies: [] }); } 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. If a dependency is not registered, it will be added to the configuration * as a module without dependencies. * * @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' || dependencyName === 'module') { continue; } // Resolve relative path and map the dependency to its alias dependencyName = this._pathResolver.resolvePath(module.name, dependencyName); // A module may have many dependencies so we should map them. var mappedDependencyName = this._configParser.mapModule(dependencyName); var moduleDependency = modules[mappedDependencyName]; // Register on the fly all unregistered in the configuration dependencies as modules without dependencies. if (!moduleDependency) { moduleDependency = this._configParser.addModule({ name: mappedDependencyName, dependencies: [] }); } this._visit(moduleDependency); } 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 bufferAbsoluteURL = []; var bufferRelativeURL = []; var result = []; var config = this._configParser.getConfig(); var basePath = config.basePath || ''; var registeredModules = this._configParser.getModules(); /* istanbul ignore else */ if (basePath.length && 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); var absolutePath = path.indexOf('/') === 0; // 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. // If the module path starts with "/", do not include basePath in the URL. } else if (!config.combine) { result.push(config.url + (absolutePath ? '' : 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. // We will put the path in different buffer depending on the fact if it is absolute URL or not. if (absolutePath) { bufferAbsoluteURL.push(path); } else { bufferRelativeURL.push(path); } } } module.requested = true; } // Add to the result all modules, which have to be combined. if (bufferRelativeURL.length) { result.push(config.url + basePath + bufferRelativeURL.join('&' + basePath)); bufferRelativeURL.length = 0; } if (bufferAbsoluteURL.length) { result.push(config.url + bufferAbsoluteURL.join('&')); bufferAbsoluteURL.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); } global.PathResolver = built; }(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this, function (global) { 'use strict'; /** * Creates an instance of PathResolver class. * * @constructor */ function PathResolver() {} PathResolver.prototype = { constructor: PathResolver, /** * Resolves the path of module. * * @param {string} root Root path which will be used as reference to resolve the path of the dependency. * @param {string} dependency The dependency path, which have to be resolved. * @return {string} The resolved dependency path. */ resolvePath: function(root, dependency) { if (dependency === 'exports' || dependency === 'module' || !(dependency.indexOf('.') === 0 || dependency.indexOf('..') === 0)) { return dependency; } // Split module directories var moduleParts = root.split('/'); // Remove module name moduleParts.splice(-1, 1); // Split dependency directories var dependencyParts = dependency.split('/'); // Extract dependecy name var dependencyName = dependencyParts.splice(-1, 1); for (var i = 0; i < dependencyParts.length; i++) { var dependencyPart = dependencyParts[i]; if (dependencyPart === '.') { continue; } else if (dependencyPart === '..') { if (moduleParts.length) { moduleParts.splice(-1, 1); } else { moduleParts = moduleParts.concat(dependencyParts.slice(i)); break; } } else { moduleParts.push(dependencyPart); } } moduleParts.push(dependencyName); return moduleParts.join('/'); } }; return PathResolver; })); (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 &ltscript&gt 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 registeredModul