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