-
Notifications
You must be signed in to change notification settings - Fork 0
/
moduleLoader.js
267 lines (238 loc) · 9.34 KB
/
moduleLoader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2017-2019 (original work) Open Assessment Technologies SA ;
*/
/**
* Loads modules
*
* It provides 2 distinct way of loading modules :
* 1. The "required" modules that are necessary. Should be already loaded.
* 2. The "dynamic" modules that are loaded on demand, they are provided as AMD modules. The module is loaded using the load method.
*
* @author Bertrand Chevrier <bertrand@taotesting.com>
* @author Jean-Sébastien Conan <jean-sebastien@taotesting.com>
*/
import _ from 'lodash';
import Promise from 'core/promise';
/**
* The data required by the modules loader
*
* @typedef {Object} moduleDefinition
* @property {String} module - AMD module name
* @property {String} bundle - AMD module name of the bundle that should contain the module
* @property {String} category - the module category
* @property {String} name - the module name
* @property {String|Number} [position = 'append'] - append, prepend or arbitrary position within the category
*/
/**
* Creates a loader with the list of required modules
* @param {Object} requiredModules - A collection of mandatory modules, where the key is the category and the value are an array of loaded modules
* @param {Function} [validate] - A validator function, by default the module should be an object
* @param {Object} [specs] - Some extra methods to assign to the loader instance
* @returns {loader} the provider loader
* @throws TypeError if something is not well formatted
*/
export default function moduleLoaderFactory(requiredModules, validate, specs) {
/**
* The list of loaded modules
*/
const loaded = {};
/**
* Retains the AMD modules to load
*/
const modules = {};
/**
* The modules to exclude
*/
const excludes = [];
/**
* Bundles to require
*/
const bundles = [];
/**
* The module loader
* @typedef {loader}
*/
const loader = {
/**
* Adds a list of dynamic modules to load
* @param {moduleDefinition[]} moduleList - the modules to add
* @returns {loader} chains
* @throws {TypeError} misuse
*/
addList(moduleList) {
_.forEach(moduleList, this.add, this);
return this;
},
/**
* Adds a dynamic module to load
* @param {moduleDefinition} def - the module to add
* @returns {loader} chains
* @throws {TypeError} misuse
*/
add(def) {
if (!_.isPlainObject(def)) {
throw new TypeError('The module definition module must be an object');
}
if (_.isEmpty(def.module) || !_.isString(def.module)) {
throw new TypeError('An AMD module must be defined');
}
if (_.isEmpty(def.category) || !_.isString(def.category)) {
const identifyProvider = def.id || def.name || def.module;
throw new TypeError(`The provider '${identifyProvider}' must belong to a category`);
}
modules[def.category] = modules[def.category] || [];
if (_.isNumber(def.position)) {
modules[def.category][def.position] = def.module;
} else if (def.position === 'prepend' || def.position === 'before') {
modules[def.category].unshift(def.module);
} else {
modules[def.category].push(def.module);
}
if (def.bundle && !_.includes(bundles, def.bundle)) {
bundles.push(def.bundle);
}
return this;
},
/**
* Appends a dynamic module
* @param {moduleDefinition} def - the module to add
* @returns {loader} chains
* @throws {TypeError} misuse
*/
append(def) {
return this.add(_.merge({ position: 'append' }, def));
},
/**
* Prepends a dynamic module to a category
* @param {moduleDefinition} def - the module to add
* @returns {loader} chains
* @throws {TypeError} misuse
*/
prepend(def) {
return this.add(_.merge({ position: 'prepend' }, def));
},
/**
* Removes a module from the loading stack
* @param {String} module - the module's module
* @returns {loader} chains
* @throws {TypeError} misuse
*/
remove(module) {
excludes.push(module);
return this;
},
/**
* Loads the dynamic modules : trigger the dependency resolution
* @param {Boolean} [loadBundles=false] - does load the bundles
* @returns {Promise}
*/
load(loadBundles) {
//compute the providers dependencies
const dependencies = _(modules).values().flatten().uniq().difference(excludes).value();
/**
* Loads AMD modules and wrap then into a Promise
* @param {String[]} amdModules - the list of modules to require
* @returns {Promise} resolves with the loaded modules
*/
const loadModules = (amdModules = []) => {
if (_.isArray(amdModules) && amdModules.length) {
if (typeof window.define === 'function' && window.define.amd) {
return new Promise((resolve, reject) => {
window.require(
amdModules,
(...loadedModules) => resolve(loadedModules),
err => {
reject(err);
}
);
});
} else {
return Promise.all(
amdModules.map(module => import(/* webpackIgnore: true */ `${module}`))
).then(loadedModules => loadedModules.map(module => module.default));
}
}
return Promise.resolve();
};
// 1. load bundles
// 2. load dependencies
// 3. add them to the modules list
return loadModules(loadBundles ? bundles : [])
.then(() => loadModules(dependencies))
.then(loadedModules => {
_.forEach(dependencies, (dependency, index) => {
const module = loadedModules[index];
const category = _.findKey(modules, val => _.includes(val, dependency));
if (typeof validate === 'function' && !validate(module)) {
throw new TypeError(`The module '${dependency}' is not valid`);
}
if (_.isString(category)) {
loaded[category] = loaded[category] || [];
loaded[category].push(module);
}
});
return this.getModules();
});
},
/**
* Get the resolved list of modules.
* Load needs to be called before to have the dynamic modules.
* @param {String} [category] - to get the modules for a given category, if not set, we get everything
* @returns {Object[]} the modules
*/
getModules(category) {
if (_.isString(category)) {
return loaded[category] || [];
}
return _(loaded).values().flatten().uniq().value();
},
/**
* Get the module categories
* @returns {String[]} the categories
*/
getCategories() {
return _.keys(loaded);
}
};
validate = _.isFunction(validate) ? validate : _.isPlainObject;
//verify and add the required modules
_.forEach(requiredModules, function (moduleList, category) {
if (_.isEmpty(category) || !_.isString(category)) {
throw new TypeError('Modules must belong to a category');
}
if (!_.isArray(moduleList)) {
throw new TypeError('A list of modules must be an array');
}
if (!_.every(moduleList, validate)) {
throw new TypeError('The list does not contain valid modules');
}
if (loaded[category]) {
loaded[category] = loaded[category].concat(moduleList);
} else {
loaded[category] = moduleList;
}
});
// let's extend the instance with extra methods
if (specs) {
_(specs)
.functions()
.forEach(function (method) {
loader[method] = (...args) => specs[method].apply(loader, args);
});
}
return loader;
}