adapt-authoring-core 1.4.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/conf/config.schema.json +6 -0
- package/docs/plugins/index-manual.js +1 -1
- package/lib/AbstractModule.js +3 -4
- package/lib/App.js +3 -1
- package/lib/DependencyLoader.js +107 -39
- package/package.json +1 -1
- package/.github/dependabot.yml +0 -11
package/conf/config.schema.json
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
"type": "boolean",
|
|
8
8
|
"default": true
|
|
9
9
|
},
|
|
10
|
+
"moduleLoadTimeout": {
|
|
11
|
+
"description": "Amount of time to wait for a module to load before erroring",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"isTimeMs": true,
|
|
14
|
+
"default": "1m"
|
|
15
|
+
},
|
|
10
16
|
"dataDir": {
|
|
11
17
|
"description": "Directory for persistant application data",
|
|
12
18
|
"type": "string",
|
|
@@ -18,7 +18,7 @@ export default class Contributors {
|
|
|
18
18
|
this.tiers = [
|
|
19
19
|
{ name: 'gold', count: 3, border: '#FFD700' },
|
|
20
20
|
{ name: 'silver', count: 6, border: '#C0C0C0' },
|
|
21
|
-
{ name: 'bronze', count:
|
|
21
|
+
{ name: 'bronze', count: 9, border: '#CD7F32' },
|
|
22
22
|
{ name: 'contributor' }
|
|
23
23
|
]
|
|
24
24
|
}
|
package/lib/AbstractModule.js
CHANGED
|
@@ -71,13 +71,12 @@ class AbstractModule {
|
|
|
71
71
|
if (this._isReady) {
|
|
72
72
|
return
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return
|
|
74
|
+
if (!error) {
|
|
75
|
+
this._isReady = true
|
|
77
76
|
}
|
|
78
|
-
this._isReady = true
|
|
79
77
|
this.initTime = Math.round((Date.now() - this._startTime))
|
|
80
78
|
this.log('verbose', AbstractModule.MODULE_READY, this.initTime)
|
|
79
|
+
await this.readyHook.invoke(error)
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
/**
|
package/lib/App.js
CHANGED
|
@@ -70,7 +70,9 @@ class App extends AbstractModule {
|
|
|
70
70
|
if (failedMods.length) this.log('warn', `${failedMods.length} module${failedMods.length === 1 ? '' : 's'} failed to load: ${failedMods}. See above for details`)
|
|
71
71
|
if (startError) {
|
|
72
72
|
process.exitCode = 1
|
|
73
|
-
|
|
73
|
+
const e = new Error('Failed to start App')
|
|
74
|
+
e.cause = startError
|
|
75
|
+
throw e
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
|
package/lib/DependencyLoader.js
CHANGED
|
@@ -11,14 +11,15 @@ import Utils from './Utils.js'
|
|
|
11
11
|
*/
|
|
12
12
|
class DependencyLoader {
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Creates a new DependencyLoader instance
|
|
15
|
+
* @param {App} app The main app instance
|
|
15
16
|
*/
|
|
16
17
|
constructor (app) {
|
|
17
18
|
/**
|
|
18
|
-
* Name of the class (
|
|
19
|
-
* @type {
|
|
19
|
+
* Name of the class (convenience function to stay consistent with other classes)
|
|
20
|
+
* @type {string}
|
|
20
21
|
*/
|
|
21
|
-
this.name = this.constructor.name
|
|
22
|
+
this.name = this.constructor.name.toLowerCase()
|
|
22
23
|
/**
|
|
23
24
|
* Reference to the main app
|
|
24
25
|
* @type {App}
|
|
@@ -26,22 +27,22 @@ class DependencyLoader {
|
|
|
26
27
|
this.app = app
|
|
27
28
|
/**
|
|
28
29
|
* Key/value store of all the Adapt dependencies' configs. Note this includes dependencies which are not loaded as Adapt modules (i.e. `module: false`).
|
|
29
|
-
* @type {Object}
|
|
30
|
+
* @type {Object<string, Object>}
|
|
30
31
|
*/
|
|
31
32
|
this.configs = {}
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
* @type {
|
|
34
|
+
* Map of module names to their loaded instances
|
|
35
|
+
* @type {Object<string, Object>}
|
|
35
36
|
*/
|
|
36
37
|
this.instances = {}
|
|
37
38
|
/**
|
|
38
|
-
*
|
|
39
|
-
* @type {
|
|
39
|
+
* Map of module names to arrays of modules that depend on them as peer dependencies
|
|
40
|
+
* @type {Object<string, Array<string>>}
|
|
40
41
|
*/
|
|
41
42
|
this.peerDependencies = {}
|
|
42
43
|
/**
|
|
43
|
-
* List of
|
|
44
|
-
* @type {Array}
|
|
44
|
+
* List of module names which have failed to load
|
|
45
|
+
* @type {Array<string>}
|
|
45
46
|
*/
|
|
46
47
|
this.failedModules = []
|
|
47
48
|
/**
|
|
@@ -54,11 +55,14 @@ class DependencyLoader {
|
|
|
54
55
|
* @type {Hook}
|
|
55
56
|
*/
|
|
56
57
|
this.moduleLoadedHook = new Hook()
|
|
58
|
+
|
|
59
|
+
this.moduleLoadedHook.tap(this.logProgress, this)
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
* Loads all Adapt module dependencies
|
|
61
|
-
* @return {Promise}
|
|
63
|
+
* Loads all Adapt module dependencies. Essential modules are loaded first, then non-essential modules (with force mode).
|
|
64
|
+
* @return {Promise<void>}
|
|
65
|
+
* @throws {Error} When any essential module fails to load
|
|
62
66
|
*/
|
|
63
67
|
async load () {
|
|
64
68
|
await this.loadConfigs()
|
|
@@ -71,9 +75,7 @@ class DependencyLoader {
|
|
|
71
75
|
}, { essential: [], theRest: [] })
|
|
72
76
|
// load each set of deps
|
|
73
77
|
await this.loadModules(essential)
|
|
74
|
-
|
|
75
|
-
await this.loadModules(theRest)
|
|
76
|
-
} catch (e) {} // not a problem if non-essential module fails to load
|
|
78
|
+
await this.loadModules(theRest, { force: true })
|
|
77
79
|
|
|
78
80
|
if (this.failedModules.length) {
|
|
79
81
|
throw new Error(`Failed to load modules ${this.failedModules.join(', ')}`)
|
|
@@ -81,8 +83,8 @@ class DependencyLoader {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
/**
|
|
84
|
-
* Loads
|
|
85
|
-
* @return {Promise}
|
|
86
|
+
* Loads configuration files for all Adapt dependencies found in node_modules.
|
|
87
|
+
* @return {Promise<void>}
|
|
86
88
|
*/
|
|
87
89
|
async loadConfigs () {
|
|
88
90
|
/** @ignore */ this._configsLoaded = false
|
|
@@ -117,9 +119,9 @@ class DependencyLoader {
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
/**
|
|
120
|
-
* Loads the relevant configuration files for an Adapt module
|
|
121
|
-
* @param {
|
|
122
|
-
* @return {Promise}
|
|
122
|
+
* Loads the relevant configuration files for an Adapt module by reading and merging package.json and adapt.json
|
|
123
|
+
* @param {string} modDir Absolute path to the module directory
|
|
124
|
+
* @return {Promise<Object>} Resolves with configuration object
|
|
123
125
|
*/
|
|
124
126
|
async loadModuleConfig (modDir) {
|
|
125
127
|
return {
|
|
@@ -130,9 +132,10 @@ class DependencyLoader {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
/**
|
|
133
|
-
* Loads a single Adapt module. Should not need to be called directly.
|
|
134
|
-
* @param {
|
|
135
|
-
* @return {Promise} Resolves with module instance
|
|
135
|
+
* Loads a single Adapt module by dynamically importing it, instantiating it, and waiting for its onReady promise. Should not need to be called directly.
|
|
136
|
+
* @param {string} modName Name of the module to load (e.g., 'adapt-authoring-core')
|
|
137
|
+
* @return {Promise<Object>} Resolves with module instance when module.onReady completes
|
|
138
|
+
* @throws {Error} When module already exists, is in an unknown format or cannot be initialised (or initialisation exceeds 60 second timeout)
|
|
136
139
|
*/
|
|
137
140
|
async loadModule (modName) {
|
|
138
141
|
if (this.instances[modName]) {
|
|
@@ -154,9 +157,11 @@ class DependencyLoader {
|
|
|
154
157
|
throw new Error('Module must define onReady function')
|
|
155
158
|
}
|
|
156
159
|
try {
|
|
160
|
+
// all essential modules will use hard-coded value, as config won't be loaded yet
|
|
161
|
+
const timeout = this.getConfig('moduleLoadTimeout') ?? 10000
|
|
157
162
|
await Promise.race([
|
|
158
163
|
instance.onReady(),
|
|
159
|
-
new Promise((resolve, reject) => setTimeout(() => reject(new Error(`${modName} load exceeded timeout (
|
|
164
|
+
new Promise((resolve, reject) => setTimeout(() => reject(new Error(`${modName} load exceeded timeout (${timeout})`)), timeout))
|
|
160
165
|
])
|
|
161
166
|
this.instances[modName] = instance
|
|
162
167
|
await this.moduleLoadedHook.invoke(null, instance)
|
|
@@ -169,14 +174,23 @@ class DependencyLoader {
|
|
|
169
174
|
|
|
170
175
|
/**
|
|
171
176
|
* Loads a list of Adapt modules. Should not need to be called directly.
|
|
172
|
-
* @param {Array} modules Module names
|
|
173
|
-
* @
|
|
177
|
+
* @param {Array<string>} modules Module names to load
|
|
178
|
+
* @param {Object} [options] Loading options
|
|
179
|
+
* @param {boolean} [options.force=false] If true, logs errors and continues loading other modules when a module fails. If false, throws a DependencyError on first failure.
|
|
180
|
+
* @return {Promise<void>} Resolves when all modules have loaded (or failed to load in force mode)
|
|
181
|
+
* @throws {DependencyError} When a module fails to load and options.force is not true
|
|
174
182
|
*/
|
|
175
|
-
async loadModules (modules) {
|
|
176
|
-
await Promise.
|
|
183
|
+
async loadModules (modules, options = {}) {
|
|
184
|
+
await Promise.all(modules.map(async m => {
|
|
177
185
|
try {
|
|
178
186
|
await this.loadModule(m)
|
|
179
187
|
} catch (e) {
|
|
188
|
+
if (options.force !== true) {
|
|
189
|
+
const error = new Error(`Failed to load '${m}'`)
|
|
190
|
+
error.name = 'DependencyError'
|
|
191
|
+
error.cause = e
|
|
192
|
+
throw error
|
|
193
|
+
}
|
|
180
194
|
this.logError(`Failed to load '${m}',`, e)
|
|
181
195
|
const deps = this.peerDependencies[m]
|
|
182
196
|
if (deps && deps.length) {
|
|
@@ -189,9 +203,10 @@ class DependencyLoader {
|
|
|
189
203
|
}
|
|
190
204
|
|
|
191
205
|
/**
|
|
192
|
-
* Waits for a single module to load
|
|
193
|
-
* @param {
|
|
194
|
-
* @return {Promise} Resolves with module instance
|
|
206
|
+
* Waits for a single module to load. Returns the instance (if loaded), or hooks into moduleLoadedHook to wait for it.
|
|
207
|
+
* @param {string} modName Name of module to wait for (accepts short names without 'adapt-authoring-' prefix)
|
|
208
|
+
* @return {Promise<Object>} Resolves with module instance when module.onReady completes
|
|
209
|
+
* @throws {Error} When module is missing from configs or has failed to load
|
|
195
210
|
*/
|
|
196
211
|
async waitForModule (modName) {
|
|
197
212
|
if (!this._configsLoaded) {
|
|
@@ -202,8 +217,9 @@ class DependencyLoader {
|
|
|
202
217
|
if (!this.configs[modName]) {
|
|
203
218
|
throw new Error(`Missing required module '${modName}'`)
|
|
204
219
|
}
|
|
220
|
+
const DependencyError = new Error(`Dependency '${modName}' failed to load`)
|
|
205
221
|
if (this.failedModules.includes(modName)) {
|
|
206
|
-
throw
|
|
222
|
+
throw DependencyError
|
|
207
223
|
}
|
|
208
224
|
const instance = this.instances[modName]
|
|
209
225
|
if (instance) {
|
|
@@ -211,23 +227,75 @@ class DependencyLoader {
|
|
|
211
227
|
}
|
|
212
228
|
return new Promise((resolve, reject) => {
|
|
213
229
|
this.moduleLoadedHook.tap((error, instance) => {
|
|
214
|
-
if (error) return reject(
|
|
230
|
+
if (error) return reject(DependencyError)
|
|
215
231
|
if (instance?.name === modName) resolve(instance)
|
|
216
232
|
})
|
|
217
233
|
})
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
/**
|
|
221
|
-
* Logs
|
|
222
|
-
* @param {
|
|
237
|
+
* Logs load progress
|
|
238
|
+
* @param {AbstractModule} instance The last loaded instance
|
|
223
239
|
*/
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
240
|
+
logProgress (error, instance) {
|
|
241
|
+
if (error) {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
const toShort = names => names.map(n => n.replace('adapt-authoring-', '')).join(', ')
|
|
245
|
+
const loaded = []
|
|
246
|
+
const notLoaded = []
|
|
247
|
+
let totalCount = 0
|
|
248
|
+
Object.keys(this.configs).forEach(key => {
|
|
249
|
+
if (this.configs[key].module === false) return
|
|
250
|
+
this.instances[key]?._isReady || key === instance.name ? loaded.push(key) : notLoaded.push(key)
|
|
251
|
+
totalCount++
|
|
252
|
+
})
|
|
253
|
+
const progress = Math.round((loaded.length / totalCount) * 100)
|
|
254
|
+
this.log('verbose', 'LOAD', [
|
|
255
|
+
toShort([instance.name]),
|
|
256
|
+
`${loaded.length}/${totalCount} (${progress}%)`,
|
|
257
|
+
notLoaded.length && `awaiting: ${toShort(notLoaded)}`,
|
|
258
|
+
this.failedModules.length && `failed: ${toShort(this.failedModules)}`
|
|
259
|
+
].filter(Boolean).join(', '))
|
|
260
|
+
|
|
261
|
+
if (progress === 100) {
|
|
262
|
+
const initTimes = Object.entries(this.instances)
|
|
263
|
+
.sort((a, b) => a[1].initTime < b[1].initTime ? -1 : a[1].initTime > b[1].initTime ? 1 : 0)
|
|
264
|
+
.reduce((memo, [modName, instance]) => Object.assign(memo, { [modName]: instance.initTime }), {})
|
|
265
|
+
this.log('verbose', initTimes)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Logs a message using the app logger if available, otherwise falls back to console.log
|
|
271
|
+
* @param {...*} args Arguments to be logged
|
|
272
|
+
*/
|
|
273
|
+
log (level, ...args) {
|
|
274
|
+
if (this.app.logger?._isReady) {
|
|
275
|
+
this.app.logger.log(level, this.name, ...args)
|
|
227
276
|
} else {
|
|
228
277
|
console.log(...args)
|
|
229
278
|
}
|
|
230
279
|
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Logs an error message using the app logger if available, otherwise falls back to console.log
|
|
283
|
+
* @param {...*} args Arguments to be logged
|
|
284
|
+
*/
|
|
285
|
+
logError (...args) {
|
|
286
|
+
this.log('error', ...args)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Retrieves a configuration value from this module's config
|
|
291
|
+
* @param {string} key - The configuration key to retrieve
|
|
292
|
+
* @returns {*|undefined} The configuration value if config is ready, undefined otherwise
|
|
293
|
+
*/
|
|
294
|
+
getConfig (key) {
|
|
295
|
+
if (this.app.config?._isReady) {
|
|
296
|
+
return this.app.config.get(`adapt-authoring-core.${key}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
231
299
|
}
|
|
232
300
|
|
|
233
301
|
export default DependencyLoader
|
package/package.json
CHANGED
package/.github/dependabot.yml
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
-
# package ecosystems to update and where the package manifests are located.
|
|
3
|
-
# Please see the documentation for all configuration options:
|
|
4
|
-
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
-
|
|
6
|
-
version: 2
|
|
7
|
-
updates:
|
|
8
|
-
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
-
directory: "/" # Location of package manifests
|
|
10
|
-
schedule:
|
|
11
|
-
interval: "weekly"
|