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 | }
|
---|