453 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			453 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {Object<string, ComponentCategory>} Components
 | 
						|
 * @typedef {Object<string, ComponentEntry | string>} ComponentCategory
 | 
						|
 *
 | 
						|
 * @typedef ComponentEntry
 | 
						|
 * @property {string} [title] The title of the component.
 | 
						|
 * @property {string} [owner] The GitHub user name of the owner.
 | 
						|
 * @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded.
 | 
						|
 * @property {string | string[]} [alias] An optional list of aliases for the id of the component.
 | 
						|
 * @property {Object<string, string>} [aliasTitles] An optional map from an alias to its title.
 | 
						|
 *
 | 
						|
 * Aliases which are not in this map will the get title of the component.
 | 
						|
 * @property {string | string[]} [optional]
 | 
						|
 * @property {string | string[]} [require]
 | 
						|
 * @property {string | string[]} [modify]
 | 
						|
 */
 | 
						|
 | 
						|
var getLoader = (function () {
 | 
						|
 | 
						|
	/**
 | 
						|
	 * A function which does absolutely nothing.
 | 
						|
	 *
 | 
						|
	 * @type {any}
 | 
						|
	 */
 | 
						|
	var noop = function () { };
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Invokes the given callback for all elements of the given value.
 | 
						|
	 *
 | 
						|
	 * If the given value is an array, the callback will be invokes for all elements. If the given value is `null` or
 | 
						|
	 * `undefined`, the callback will not be invoked. In all other cases, the callback will be invoked with the given
 | 
						|
	 * value as parameter.
 | 
						|
	 *
 | 
						|
	 * @param {null | undefined | T | T[]} value
 | 
						|
	 * @param {(value: T, index: number) => void} callbackFn
 | 
						|
	 * @returns {void}
 | 
						|
	 * @template T
 | 
						|
	 */
 | 
						|
	function forEach(value, callbackFn) {
 | 
						|
		if (Array.isArray(value)) {
 | 
						|
			value.forEach(callbackFn);
 | 
						|
		} else if (value != null) {
 | 
						|
			callbackFn(value, 0);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Returns a new set for the given string array.
 | 
						|
	 *
 | 
						|
	 * @param {string[]} array
 | 
						|
	 * @returns {StringSet}
 | 
						|
	 *
 | 
						|
	 * @typedef {Object<string, true>} StringSet
 | 
						|
	 */
 | 
						|
	function toSet(array) {
 | 
						|
		/** @type {StringSet} */
 | 
						|
		var set = {};
 | 
						|
		for (var i = 0, l = array.length; i < l; i++) {
 | 
						|
			set[array[i]] = true;
 | 
						|
		}
 | 
						|
		return set;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Creates a map of every components id to its entry.
 | 
						|
	 *
 | 
						|
	 * @param {Components} components
 | 
						|
	 * @returns {EntryMap}
 | 
						|
	 *
 | 
						|
	 * @typedef {{ readonly [id: string]: Readonly<ComponentEntry> | undefined }} EntryMap
 | 
						|
	 */
 | 
						|
	function createEntryMap(components) {
 | 
						|
		/** @type {Object<string, Readonly<ComponentEntry>>} */
 | 
						|
		var map = {};
 | 
						|
 | 
						|
		for (var categoryName in components) {
 | 
						|
			var category = components[categoryName];
 | 
						|
			for (var id in category) {
 | 
						|
				if (id != 'meta') {
 | 
						|
					/** @type {ComponentEntry | string} */
 | 
						|
					var entry = category[id];
 | 
						|
					map[id] = typeof entry == 'string' ? { title: entry } : entry;
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return map;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Creates a full dependencies map which includes all types of dependencies and their transitive dependencies.
 | 
						|
	 *
 | 
						|
	 * @param {EntryMap} entryMap
 | 
						|
	 * @returns {DependencyResolver}
 | 
						|
	 *
 | 
						|
	 * @typedef {(id: string) => StringSet} DependencyResolver
 | 
						|
	 */
 | 
						|
	function createDependencyResolver(entryMap) {
 | 
						|
		/** @type {Object<string, StringSet>} */
 | 
						|
		var map = {};
 | 
						|
		var _stackArray = [];
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Adds the dependencies of the given component to the dependency map.
 | 
						|
		 *
 | 
						|
		 * @param {string} id
 | 
						|
		 * @param {string[]} stack
 | 
						|
		 */
 | 
						|
		function addToMap(id, stack) {
 | 
						|
			if (id in map) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
 | 
						|
			stack.push(id);
 | 
						|
 | 
						|
			// check for circular dependencies
 | 
						|
			var firstIndex = stack.indexOf(id);
 | 
						|
			if (firstIndex < stack.length - 1) {
 | 
						|
				throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> '));
 | 
						|
			}
 | 
						|
 | 
						|
			/** @type {StringSet} */
 | 
						|
			var dependencies = {};
 | 
						|
 | 
						|
			var entry = entryMap[id];
 | 
						|
			if (entry) {
 | 
						|
				/**
 | 
						|
				 * This will add the direct dependency and all of its transitive dependencies to the set of
 | 
						|
				 * dependencies of `entry`.
 | 
						|
				 *
 | 
						|
				 * @param {string} depId
 | 
						|
				 * @returns {void}
 | 
						|
				 */
 | 
						|
				function handleDirectDependency(depId) {
 | 
						|
					if (!(depId in entryMap)) {
 | 
						|
						throw new Error(id + ' depends on an unknown component ' + depId);
 | 
						|
					}
 | 
						|
					if (depId in dependencies) {
 | 
						|
						// if the given dependency is already in the set of deps, then so are its transitive deps
 | 
						|
						return;
 | 
						|
					}
 | 
						|
 | 
						|
					addToMap(depId, stack);
 | 
						|
					dependencies[depId] = true;
 | 
						|
					for (var transitiveDepId in map[depId]) {
 | 
						|
						dependencies[transitiveDepId] = true;
 | 
						|
					}
 | 
						|
				}
 | 
						|
 | 
						|
				forEach(entry.require, handleDirectDependency);
 | 
						|
				forEach(entry.optional, handleDirectDependency);
 | 
						|
				forEach(entry.modify, handleDirectDependency);
 | 
						|
			}
 | 
						|
 | 
						|
			map[id] = dependencies;
 | 
						|
 | 
						|
			stack.pop();
 | 
						|
		}
 | 
						|
 | 
						|
		return function (id) {
 | 
						|
			var deps = map[id];
 | 
						|
			if (!deps) {
 | 
						|
				addToMap(id, _stackArray);
 | 
						|
				deps = map[id];
 | 
						|
			}
 | 
						|
			return deps;
 | 
						|
		};
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Returns a function which resolves the aliases of its given id of alias.
 | 
						|
	 *
 | 
						|
	 * @param {EntryMap} entryMap
 | 
						|
	 * @returns {(idOrAlias: string) => string}
 | 
						|
	 */
 | 
						|
	function createAliasResolver(entryMap) {
 | 
						|
		/** @type {Object<string, string> | undefined} */
 | 
						|
		var map;
 | 
						|
 | 
						|
		return function (idOrAlias) {
 | 
						|
			if (idOrAlias in entryMap) {
 | 
						|
				return idOrAlias;
 | 
						|
			} else {
 | 
						|
				// only create the alias map if necessary
 | 
						|
				if (!map) {
 | 
						|
					map = {};
 | 
						|
 | 
						|
					for (var id in entryMap) {
 | 
						|
						var entry = entryMap[id];
 | 
						|
						forEach(entry && entry.alias, function (alias) {
 | 
						|
							if (alias in map) {
 | 
						|
								throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]);
 | 
						|
							}
 | 
						|
							if (alias in entryMap) {
 | 
						|
								throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.');
 | 
						|
							}
 | 
						|
							map[alias] = id;
 | 
						|
						});
 | 
						|
					}
 | 
						|
				}
 | 
						|
				return map[idOrAlias] || idOrAlias;
 | 
						|
			}
 | 
						|
		};
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @typedef LoadChainer
 | 
						|
	 * @property {(before: T, after: () => T) => T} series
 | 
						|
	 * @property {(values: T[]) => T} parallel
 | 
						|
	 * @template T
 | 
						|
	 */
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each
 | 
						|
	 * component in topological order.
 | 
						|
	 *
 | 
						|
	 * @param {DependencyResolver} dependencyResolver
 | 
						|
	 * @param {StringSet} ids
 | 
						|
	 * @param {(id: string) => T} loadComponent
 | 
						|
	 * @param {LoadChainer<T>} [chainer]
 | 
						|
	 * @returns {T}
 | 
						|
	 * @template T
 | 
						|
	 */
 | 
						|
	function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) {
 | 
						|
		var series = chainer ? chainer.series : undefined;
 | 
						|
		var parallel = chainer ? chainer.parallel : noop;
 | 
						|
 | 
						|
		/** @type {Object<string, T>} */
 | 
						|
		var cache = {};
 | 
						|
 | 
						|
		/**
 | 
						|
		 * A set of ids of nodes which are not depended upon by any other node in the graph.
 | 
						|
		 *
 | 
						|
		 * @type {StringSet}
 | 
						|
		 */
 | 
						|
		var ends = {};
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Loads the given component and its dependencies or returns the cached value.
 | 
						|
		 *
 | 
						|
		 * @param {string} id
 | 
						|
		 * @returns {T}
 | 
						|
		 */
 | 
						|
		function handleId(id) {
 | 
						|
			if (id in cache) {
 | 
						|
				return cache[id];
 | 
						|
			}
 | 
						|
 | 
						|
			// assume that it's an end
 | 
						|
			// if it isn't, it will be removed later
 | 
						|
			ends[id] = true;
 | 
						|
 | 
						|
			// all dependencies of the component in the given ids
 | 
						|
			var dependsOn = [];
 | 
						|
			for (var depId in dependencyResolver(id)) {
 | 
						|
				if (depId in ids) {
 | 
						|
					dependsOn.push(depId);
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			/**
 | 
						|
			 * The value to be returned.
 | 
						|
			 *
 | 
						|
			 * @type {T}
 | 
						|
			 */
 | 
						|
			var value;
 | 
						|
 | 
						|
			if (dependsOn.length === 0) {
 | 
						|
				value = loadComponent(id);
 | 
						|
			} else {
 | 
						|
				var depsValue = parallel(dependsOn.map(function (depId) {
 | 
						|
					var value = handleId(depId);
 | 
						|
					// none of the dependencies can be ends
 | 
						|
					delete ends[depId];
 | 
						|
					return value;
 | 
						|
				}));
 | 
						|
				if (series) {
 | 
						|
					// the chainer will be responsibly for calling the function calling loadComponent
 | 
						|
					value = series(depsValue, function () { return loadComponent(id); });
 | 
						|
				} else {
 | 
						|
					// we don't have a chainer, so we call loadComponent ourselves
 | 
						|
					loadComponent(id);
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// cache and return
 | 
						|
			return cache[id] = value;
 | 
						|
		}
 | 
						|
 | 
						|
		for (var id in ids) {
 | 
						|
			handleId(id);
 | 
						|
		}
 | 
						|
 | 
						|
		/** @type {T[]} */
 | 
						|
		var endValues = [];
 | 
						|
		for (var endId in ends) {
 | 
						|
			endValues.push(cache[endId]);
 | 
						|
		}
 | 
						|
		return parallel(endValues);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Returns whether the given object has any keys.
 | 
						|
	 *
 | 
						|
	 * @param {object} obj
 | 
						|
	 */
 | 
						|
	function hasKeys(obj) {
 | 
						|
		for (var key in obj) {
 | 
						|
			return true;
 | 
						|
		}
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and
 | 
						|
	 * a way to efficiently load them in synchronously and asynchronous contexts (`load`).
 | 
						|
	 *
 | 
						|
	 * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding
 | 
						|
	 * components will have to reloaded.
 | 
						|
	 *
 | 
						|
	 * The ids in `load` and `loaded` may be in any order and can contain duplicates.
 | 
						|
	 *
 | 
						|
	 * @param {Components} components
 | 
						|
	 * @param {string[]} load
 | 
						|
	 * @param {string[]} [loaded=[]] A list of already loaded components.
 | 
						|
	 *
 | 
						|
	 * If a component is in this list, then all of its requirements will also be assumed to be in the list.
 | 
						|
	 * @returns {Loader}
 | 
						|
	 *
 | 
						|
	 * @typedef Loader
 | 
						|
	 * @property {() => string[]} getIds A function to get all ids of the components to load.
 | 
						|
	 *
 | 
						|
	 * The returned ids will be duplicate-free, alias-free and in load order.
 | 
						|
	 * @property {LoadFunction} load A functional interface to load components.
 | 
						|
	 *
 | 
						|
	 * @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction
 | 
						|
	 * A functional interface to load components.
 | 
						|
	 *
 | 
						|
	 * The `loadComponent` function will be called for every component in the order in which they have to be loaded.
 | 
						|
	 *
 | 
						|
	 * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as
 | 
						|
	 * `Promise#then` and `Promise.all`.
 | 
						|
	 *
 | 
						|
	 * @example
 | 
						|
	 * load(id => { loadComponent(id); }); // returns undefined
 | 
						|
	 *
 | 
						|
	 * await load(
 | 
						|
	 *     id => loadComponentAsync(id), // returns a Promise for each id
 | 
						|
	 *     {
 | 
						|
	 *         series: async (before, after) => {
 | 
						|
	 *             await before;
 | 
						|
	 *             await after();
 | 
						|
	 *         },
 | 
						|
	 *         parallel: async (values) => {
 | 
						|
	 *             await Promise.all(values);
 | 
						|
	 *         }
 | 
						|
	 *     }
 | 
						|
	 * );
 | 
						|
	 */
 | 
						|
	function getLoader(components, load, loaded) {
 | 
						|
		var entryMap = createEntryMap(components);
 | 
						|
		var resolveAlias = createAliasResolver(entryMap);
 | 
						|
 | 
						|
		load = load.map(resolveAlias);
 | 
						|
		loaded = (loaded || []).map(resolveAlias);
 | 
						|
 | 
						|
		var loadSet = toSet(load);
 | 
						|
		var loadedSet = toSet(loaded);
 | 
						|
 | 
						|
		// add requirements
 | 
						|
 | 
						|
		load.forEach(addRequirements);
 | 
						|
		function addRequirements(id) {
 | 
						|
			var entry = entryMap[id];
 | 
						|
			forEach(entry && entry.require, function (reqId) {
 | 
						|
				if (!(reqId in loadedSet)) {
 | 
						|
					loadSet[reqId] = true;
 | 
						|
					addRequirements(reqId);
 | 
						|
				}
 | 
						|
			});
 | 
						|
		}
 | 
						|
 | 
						|
		// add components to reload
 | 
						|
 | 
						|
		// A component x in `loaded` has to be reloaded if
 | 
						|
		//  1) a component in `load` modifies x.
 | 
						|
		//  2) x depends on a component in `load`.
 | 
						|
		// The above two condition have to be applied until nothing changes anymore.
 | 
						|
 | 
						|
		var dependencyResolver = createDependencyResolver(entryMap);
 | 
						|
 | 
						|
		/** @type {StringSet} */
 | 
						|
		var loadAdditions = loadSet;
 | 
						|
		/** @type {StringSet} */
 | 
						|
		var newIds;
 | 
						|
		while (hasKeys(loadAdditions)) {
 | 
						|
			newIds = {};
 | 
						|
 | 
						|
			// condition 1)
 | 
						|
			for (var loadId in loadAdditions) {
 | 
						|
				var entry = entryMap[loadId];
 | 
						|
				forEach(entry && entry.modify, function (modId) {
 | 
						|
					if (modId in loadedSet) {
 | 
						|
						newIds[modId] = true;
 | 
						|
					}
 | 
						|
				});
 | 
						|
			}
 | 
						|
 | 
						|
			// condition 2)
 | 
						|
			for (var loadedId in loadedSet) {
 | 
						|
				if (!(loadedId in loadSet)) {
 | 
						|
					for (var depId in dependencyResolver(loadedId)) {
 | 
						|
						if (depId in loadSet) {
 | 
						|
							newIds[loadedId] = true;
 | 
						|
							break;
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			loadAdditions = newIds;
 | 
						|
			for (var newId in loadAdditions) {
 | 
						|
				loadSet[newId] = true;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		/** @type {Loader} */
 | 
						|
		var loader = {
 | 
						|
			getIds: function () {
 | 
						|
				var ids = [];
 | 
						|
				loader.load(function (id) {
 | 
						|
					ids.push(id);
 | 
						|
				});
 | 
						|
				return ids;
 | 
						|
			},
 | 
						|
			load: function (loadComponent, chainer) {
 | 
						|
				return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer);
 | 
						|
			}
 | 
						|
		};
 | 
						|
 | 
						|
		return loader;
 | 
						|
	}
 | 
						|
 | 
						|
	return getLoader;
 | 
						|
 | 
						|
}());
 | 
						|
 | 
						|
if (typeof module !== 'undefined') {
 | 
						|
	module.exports = getLoader;
 | 
						|
}
 |