[d24f17c] | 1 | 'use strict';
|
---|
| 2 |
|
---|
| 3 | /**
|
---|
| 4 | * @typedef {Object<string, ComponentCategory>} Components
|
---|
| 5 | * @typedef {Object<string, ComponentEntry | string>} ComponentCategory
|
---|
| 6 | *
|
---|
| 7 | * @typedef ComponentEntry
|
---|
| 8 | * @property {string} [title] The title of the component.
|
---|
| 9 | * @property {string} [owner] The GitHub user name of the owner.
|
---|
| 10 | * @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded.
|
---|
| 11 | * @property {string | string[]} [alias] An optional list of aliases for the id of the component.
|
---|
| 12 | * @property {Object<string, string>} [aliasTitles] An optional map from an alias to its title.
|
---|
| 13 | *
|
---|
| 14 | * Aliases which are not in this map will the get title of the component.
|
---|
| 15 | * @property {string | string[]} [optional]
|
---|
| 16 | * @property {string | string[]} [require]
|
---|
| 17 | * @property {string | string[]} [modify]
|
---|
| 18 | */
|
---|
| 19 |
|
---|
| 20 | var getLoader = (function () {
|
---|
| 21 |
|
---|
| 22 | /**
|
---|
| 23 | * A function which does absolutely nothing.
|
---|
| 24 | *
|
---|
| 25 | * @type {any}
|
---|
| 26 | */
|
---|
| 27 | var noop = function () { };
|
---|
| 28 |
|
---|
| 29 | /**
|
---|
| 30 | * Invokes the given callback for all elements of the given value.
|
---|
| 31 | *
|
---|
| 32 | * If the given value is an array, the callback will be invokes for all elements. If the given value is `null` or
|
---|
| 33 | * `undefined`, the callback will not be invoked. In all other cases, the callback will be invoked with the given
|
---|
| 34 | * value as parameter.
|
---|
| 35 | *
|
---|
| 36 | * @param {null | undefined | T | T[]} value
|
---|
| 37 | * @param {(value: T, index: number) => void} callbackFn
|
---|
| 38 | * @returns {void}
|
---|
| 39 | * @template T
|
---|
| 40 | */
|
---|
| 41 | function forEach(value, callbackFn) {
|
---|
| 42 | if (Array.isArray(value)) {
|
---|
| 43 | value.forEach(callbackFn);
|
---|
| 44 | } else if (value != null) {
|
---|
| 45 | callbackFn(value, 0);
|
---|
| 46 | }
|
---|
| 47 | }
|
---|
| 48 |
|
---|
| 49 | /**
|
---|
| 50 | * Returns a new set for the given string array.
|
---|
| 51 | *
|
---|
| 52 | * @param {string[]} array
|
---|
| 53 | * @returns {StringSet}
|
---|
| 54 | *
|
---|
| 55 | * @typedef {Object<string, true>} StringSet
|
---|
| 56 | */
|
---|
| 57 | function toSet(array) {
|
---|
| 58 | /** @type {StringSet} */
|
---|
| 59 | var set = {};
|
---|
| 60 | for (var i = 0, l = array.length; i < l; i++) {
|
---|
| 61 | set[array[i]] = true;
|
---|
| 62 | }
|
---|
| 63 | return set;
|
---|
| 64 | }
|
---|
| 65 |
|
---|
| 66 | /**
|
---|
| 67 | * Creates a map of every components id to its entry.
|
---|
| 68 | *
|
---|
| 69 | * @param {Components} components
|
---|
| 70 | * @returns {EntryMap}
|
---|
| 71 | *
|
---|
| 72 | * @typedef {{ readonly [id: string]: Readonly<ComponentEntry> | undefined }} EntryMap
|
---|
| 73 | */
|
---|
| 74 | function createEntryMap(components) {
|
---|
| 75 | /** @type {Object<string, Readonly<ComponentEntry>>} */
|
---|
| 76 | var map = {};
|
---|
| 77 |
|
---|
| 78 | for (var categoryName in components) {
|
---|
| 79 | var category = components[categoryName];
|
---|
| 80 | for (var id in category) {
|
---|
| 81 | if (id != 'meta') {
|
---|
| 82 | /** @type {ComponentEntry | string} */
|
---|
| 83 | var entry = category[id];
|
---|
| 84 | map[id] = typeof entry == 'string' ? { title: entry } : entry;
|
---|
| 85 | }
|
---|
| 86 | }
|
---|
| 87 | }
|
---|
| 88 |
|
---|
| 89 | return map;
|
---|
| 90 | }
|
---|
| 91 |
|
---|
| 92 | /**
|
---|
| 93 | * Creates a full dependencies map which includes all types of dependencies and their transitive dependencies.
|
---|
| 94 | *
|
---|
| 95 | * @param {EntryMap} entryMap
|
---|
| 96 | * @returns {DependencyResolver}
|
---|
| 97 | *
|
---|
| 98 | * @typedef {(id: string) => StringSet} DependencyResolver
|
---|
| 99 | */
|
---|
| 100 | function createDependencyResolver(entryMap) {
|
---|
| 101 | /** @type {Object<string, StringSet>} */
|
---|
| 102 | var map = {};
|
---|
| 103 | var _stackArray = [];
|
---|
| 104 |
|
---|
| 105 | /**
|
---|
| 106 | * Adds the dependencies of the given component to the dependency map.
|
---|
| 107 | *
|
---|
| 108 | * @param {string} id
|
---|
| 109 | * @param {string[]} stack
|
---|
| 110 | */
|
---|
| 111 | function addToMap(id, stack) {
|
---|
| 112 | if (id in map) {
|
---|
| 113 | return;
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | stack.push(id);
|
---|
| 117 |
|
---|
| 118 | // check for circular dependencies
|
---|
| 119 | var firstIndex = stack.indexOf(id);
|
---|
| 120 | if (firstIndex < stack.length - 1) {
|
---|
| 121 | throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> '));
|
---|
| 122 | }
|
---|
| 123 |
|
---|
| 124 | /** @type {StringSet} */
|
---|
| 125 | var dependencies = {};
|
---|
| 126 |
|
---|
| 127 | var entry = entryMap[id];
|
---|
| 128 | if (entry) {
|
---|
| 129 | /**
|
---|
| 130 | * This will add the direct dependency and all of its transitive dependencies to the set of
|
---|
| 131 | * dependencies of `entry`.
|
---|
| 132 | *
|
---|
| 133 | * @param {string} depId
|
---|
| 134 | * @returns {void}
|
---|
| 135 | */
|
---|
| 136 | function handleDirectDependency(depId) {
|
---|
| 137 | if (!(depId in entryMap)) {
|
---|
| 138 | throw new Error(id + ' depends on an unknown component ' + depId);
|
---|
| 139 | }
|
---|
| 140 | if (depId in dependencies) {
|
---|
| 141 | // if the given dependency is already in the set of deps, then so are its transitive deps
|
---|
| 142 | return;
|
---|
| 143 | }
|
---|
| 144 |
|
---|
| 145 | addToMap(depId, stack);
|
---|
| 146 | dependencies[depId] = true;
|
---|
| 147 | for (var transitiveDepId in map[depId]) {
|
---|
| 148 | dependencies[transitiveDepId] = true;
|
---|
| 149 | }
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | forEach(entry.require, handleDirectDependency);
|
---|
| 153 | forEach(entry.optional, handleDirectDependency);
|
---|
| 154 | forEach(entry.modify, handleDirectDependency);
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | map[id] = dependencies;
|
---|
| 158 |
|
---|
| 159 | stack.pop();
|
---|
| 160 | }
|
---|
| 161 |
|
---|
| 162 | return function (id) {
|
---|
| 163 | var deps = map[id];
|
---|
| 164 | if (!deps) {
|
---|
| 165 | addToMap(id, _stackArray);
|
---|
| 166 | deps = map[id];
|
---|
| 167 | }
|
---|
| 168 | return deps;
|
---|
| 169 | };
|
---|
| 170 | }
|
---|
| 171 |
|
---|
| 172 | /**
|
---|
| 173 | * Returns a function which resolves the aliases of its given id of alias.
|
---|
| 174 | *
|
---|
| 175 | * @param {EntryMap} entryMap
|
---|
| 176 | * @returns {(idOrAlias: string) => string}
|
---|
| 177 | */
|
---|
| 178 | function createAliasResolver(entryMap) {
|
---|
| 179 | /** @type {Object<string, string> | undefined} */
|
---|
| 180 | var map;
|
---|
| 181 |
|
---|
| 182 | return function (idOrAlias) {
|
---|
| 183 | if (idOrAlias in entryMap) {
|
---|
| 184 | return idOrAlias;
|
---|
| 185 | } else {
|
---|
| 186 | // only create the alias map if necessary
|
---|
| 187 | if (!map) {
|
---|
| 188 | map = {};
|
---|
| 189 |
|
---|
| 190 | for (var id in entryMap) {
|
---|
| 191 | var entry = entryMap[id];
|
---|
| 192 | forEach(entry && entry.alias, function (alias) {
|
---|
| 193 | if (alias in map) {
|
---|
| 194 | throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]);
|
---|
| 195 | }
|
---|
| 196 | if (alias in entryMap) {
|
---|
| 197 | throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.');
|
---|
| 198 | }
|
---|
| 199 | map[alias] = id;
|
---|
| 200 | });
|
---|
| 201 | }
|
---|
| 202 | }
|
---|
| 203 | return map[idOrAlias] || idOrAlias;
|
---|
| 204 | }
|
---|
| 205 | };
|
---|
| 206 | }
|
---|
| 207 |
|
---|
| 208 | /**
|
---|
| 209 | * @typedef LoadChainer
|
---|
| 210 | * @property {(before: T, after: () => T) => T} series
|
---|
| 211 | * @property {(values: T[]) => T} parallel
|
---|
| 212 | * @template T
|
---|
| 213 | */
|
---|
| 214 |
|
---|
| 215 | /**
|
---|
| 216 | * Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each
|
---|
| 217 | * component in topological order.
|
---|
| 218 | *
|
---|
| 219 | * @param {DependencyResolver} dependencyResolver
|
---|
| 220 | * @param {StringSet} ids
|
---|
| 221 | * @param {(id: string) => T} loadComponent
|
---|
| 222 | * @param {LoadChainer<T>} [chainer]
|
---|
| 223 | * @returns {T}
|
---|
| 224 | * @template T
|
---|
| 225 | */
|
---|
| 226 | function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) {
|
---|
| 227 | var series = chainer ? chainer.series : undefined;
|
---|
| 228 | var parallel = chainer ? chainer.parallel : noop;
|
---|
| 229 |
|
---|
| 230 | /** @type {Object<string, T>} */
|
---|
| 231 | var cache = {};
|
---|
| 232 |
|
---|
| 233 | /**
|
---|
| 234 | * A set of ids of nodes which are not depended upon by any other node in the graph.
|
---|
| 235 | *
|
---|
| 236 | * @type {StringSet}
|
---|
| 237 | */
|
---|
| 238 | var ends = {};
|
---|
| 239 |
|
---|
| 240 | /**
|
---|
| 241 | * Loads the given component and its dependencies or returns the cached value.
|
---|
| 242 | *
|
---|
| 243 | * @param {string} id
|
---|
| 244 | * @returns {T}
|
---|
| 245 | */
|
---|
| 246 | function handleId(id) {
|
---|
| 247 | if (id in cache) {
|
---|
| 248 | return cache[id];
|
---|
| 249 | }
|
---|
| 250 |
|
---|
| 251 | // assume that it's an end
|
---|
| 252 | // if it isn't, it will be removed later
|
---|
| 253 | ends[id] = true;
|
---|
| 254 |
|
---|
| 255 | // all dependencies of the component in the given ids
|
---|
| 256 | var dependsOn = [];
|
---|
| 257 | for (var depId in dependencyResolver(id)) {
|
---|
| 258 | if (depId in ids) {
|
---|
| 259 | dependsOn.push(depId);
|
---|
| 260 | }
|
---|
| 261 | }
|
---|
| 262 |
|
---|
| 263 | /**
|
---|
| 264 | * The value to be returned.
|
---|
| 265 | *
|
---|
| 266 | * @type {T}
|
---|
| 267 | */
|
---|
| 268 | var value;
|
---|
| 269 |
|
---|
| 270 | if (dependsOn.length === 0) {
|
---|
| 271 | value = loadComponent(id);
|
---|
| 272 | } else {
|
---|
| 273 | var depsValue = parallel(dependsOn.map(function (depId) {
|
---|
| 274 | var value = handleId(depId);
|
---|
| 275 | // none of the dependencies can be ends
|
---|
| 276 | delete ends[depId];
|
---|
| 277 | return value;
|
---|
| 278 | }));
|
---|
| 279 | if (series) {
|
---|
| 280 | // the chainer will be responsibly for calling the function calling loadComponent
|
---|
| 281 | value = series(depsValue, function () { return loadComponent(id); });
|
---|
| 282 | } else {
|
---|
| 283 | // we don't have a chainer, so we call loadComponent ourselves
|
---|
| 284 | loadComponent(id);
|
---|
| 285 | }
|
---|
| 286 | }
|
---|
| 287 |
|
---|
| 288 | // cache and return
|
---|
| 289 | return cache[id] = value;
|
---|
| 290 | }
|
---|
| 291 |
|
---|
| 292 | for (var id in ids) {
|
---|
| 293 | handleId(id);
|
---|
| 294 | }
|
---|
| 295 |
|
---|
| 296 | /** @type {T[]} */
|
---|
| 297 | var endValues = [];
|
---|
| 298 | for (var endId in ends) {
|
---|
| 299 | endValues.push(cache[endId]);
|
---|
| 300 | }
|
---|
| 301 | return parallel(endValues);
|
---|
| 302 | }
|
---|
| 303 |
|
---|
| 304 | /**
|
---|
| 305 | * Returns whether the given object has any keys.
|
---|
| 306 | *
|
---|
| 307 | * @param {object} obj
|
---|
| 308 | */
|
---|
| 309 | function hasKeys(obj) {
|
---|
| 310 | for (var key in obj) {
|
---|
| 311 | return true;
|
---|
| 312 | }
|
---|
| 313 | return false;
|
---|
| 314 | }
|
---|
| 315 |
|
---|
| 316 | /**
|
---|
| 317 | * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and
|
---|
| 318 | * a way to efficiently load them in synchronously and asynchronous contexts (`load`).
|
---|
| 319 | *
|
---|
| 320 | * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding
|
---|
| 321 | * components will have to reloaded.
|
---|
| 322 | *
|
---|
| 323 | * The ids in `load` and `loaded` may be in any order and can contain duplicates.
|
---|
| 324 | *
|
---|
| 325 | * @param {Components} components
|
---|
| 326 | * @param {string[]} load
|
---|
| 327 | * @param {string[]} [loaded=[]] A list of already loaded components.
|
---|
| 328 | *
|
---|
| 329 | * If a component is in this list, then all of its requirements will also be assumed to be in the list.
|
---|
| 330 | * @returns {Loader}
|
---|
| 331 | *
|
---|
| 332 | * @typedef Loader
|
---|
| 333 | * @property {() => string[]} getIds A function to get all ids of the components to load.
|
---|
| 334 | *
|
---|
| 335 | * The returned ids will be duplicate-free, alias-free and in load order.
|
---|
| 336 | * @property {LoadFunction} load A functional interface to load components.
|
---|
| 337 | *
|
---|
| 338 | * @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction
|
---|
| 339 | * A functional interface to load components.
|
---|
| 340 | *
|
---|
| 341 | * The `loadComponent` function will be called for every component in the order in which they have to be loaded.
|
---|
| 342 | *
|
---|
| 343 | * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as
|
---|
| 344 | * `Promise#then` and `Promise.all`.
|
---|
| 345 | *
|
---|
| 346 | * @example
|
---|
| 347 | * load(id => { loadComponent(id); }); // returns undefined
|
---|
| 348 | *
|
---|
| 349 | * await load(
|
---|
| 350 | * id => loadComponentAsync(id), // returns a Promise for each id
|
---|
| 351 | * {
|
---|
| 352 | * series: async (before, after) => {
|
---|
| 353 | * await before;
|
---|
| 354 | * await after();
|
---|
| 355 | * },
|
---|
| 356 | * parallel: async (values) => {
|
---|
| 357 | * await Promise.all(values);
|
---|
| 358 | * }
|
---|
| 359 | * }
|
---|
| 360 | * );
|
---|
| 361 | */
|
---|
| 362 | function getLoader(components, load, loaded) {
|
---|
| 363 | var entryMap = createEntryMap(components);
|
---|
| 364 | var resolveAlias = createAliasResolver(entryMap);
|
---|
| 365 |
|
---|
| 366 | load = load.map(resolveAlias);
|
---|
| 367 | loaded = (loaded || []).map(resolveAlias);
|
---|
| 368 |
|
---|
| 369 | var loadSet = toSet(load);
|
---|
| 370 | var loadedSet = toSet(loaded);
|
---|
| 371 |
|
---|
| 372 | // add requirements
|
---|
| 373 |
|
---|
| 374 | load.forEach(addRequirements);
|
---|
| 375 | function addRequirements(id) {
|
---|
| 376 | var entry = entryMap[id];
|
---|
| 377 | forEach(entry && entry.require, function (reqId) {
|
---|
| 378 | if (!(reqId in loadedSet)) {
|
---|
| 379 | loadSet[reqId] = true;
|
---|
| 380 | addRequirements(reqId);
|
---|
| 381 | }
|
---|
| 382 | });
|
---|
| 383 | }
|
---|
| 384 |
|
---|
| 385 | // add components to reload
|
---|
| 386 |
|
---|
| 387 | // A component x in `loaded` has to be reloaded if
|
---|
| 388 | // 1) a component in `load` modifies x.
|
---|
| 389 | // 2) x depends on a component in `load`.
|
---|
| 390 | // The above two condition have to be applied until nothing changes anymore.
|
---|
| 391 |
|
---|
| 392 | var dependencyResolver = createDependencyResolver(entryMap);
|
---|
| 393 |
|
---|
| 394 | /** @type {StringSet} */
|
---|
| 395 | var loadAdditions = loadSet;
|
---|
| 396 | /** @type {StringSet} */
|
---|
| 397 | var newIds;
|
---|
| 398 | while (hasKeys(loadAdditions)) {
|
---|
| 399 | newIds = {};
|
---|
| 400 |
|
---|
| 401 | // condition 1)
|
---|
| 402 | for (var loadId in loadAdditions) {
|
---|
| 403 | var entry = entryMap[loadId];
|
---|
| 404 | forEach(entry && entry.modify, function (modId) {
|
---|
| 405 | if (modId in loadedSet) {
|
---|
| 406 | newIds[modId] = true;
|
---|
| 407 | }
|
---|
| 408 | });
|
---|
| 409 | }
|
---|
| 410 |
|
---|
| 411 | // condition 2)
|
---|
| 412 | for (var loadedId in loadedSet) {
|
---|
| 413 | if (!(loadedId in loadSet)) {
|
---|
| 414 | for (var depId in dependencyResolver(loadedId)) {
|
---|
| 415 | if (depId in loadSet) {
|
---|
| 416 | newIds[loadedId] = true;
|
---|
| 417 | break;
|
---|
| 418 | }
|
---|
| 419 | }
|
---|
| 420 | }
|
---|
| 421 | }
|
---|
| 422 |
|
---|
| 423 | loadAdditions = newIds;
|
---|
| 424 | for (var newId in loadAdditions) {
|
---|
| 425 | loadSet[newId] = true;
|
---|
| 426 | }
|
---|
| 427 | }
|
---|
| 428 |
|
---|
| 429 | /** @type {Loader} */
|
---|
| 430 | var loader = {
|
---|
| 431 | getIds: function () {
|
---|
| 432 | var ids = [];
|
---|
| 433 | loader.load(function (id) {
|
---|
| 434 | ids.push(id);
|
---|
| 435 | });
|
---|
| 436 | return ids;
|
---|
| 437 | },
|
---|
| 438 | load: function (loadComponent, chainer) {
|
---|
| 439 | return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer);
|
---|
| 440 | }
|
---|
| 441 | };
|
---|
| 442 |
|
---|
| 443 | return loader;
|
---|
| 444 | }
|
---|
| 445 |
|
---|
| 446 | return getLoader;
|
---|
| 447 |
|
---|
| 448 | }());
|
---|
| 449 |
|
---|
| 450 | if (typeof module !== 'undefined') {
|
---|
| 451 | module.exports = getLoader;
|
---|
| 452 | }
|
---|