adapt-authoring-core 2.5.0 → 3.0.1
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/.github/workflows/releases.yml +9 -24
- package/conf/config.schema.json +16 -0
- package/conf/deprecated-lang.json +4 -0
- package/conf/deprecated-logger.json +7 -0
- package/docs/configure-environment.md +78 -0
- package/docs/developer-workflow.md +649 -0
- package/docs/error-handling.md +49 -0
- package/docs/plugins/configuration.js +75 -0
- package/docs/plugins/configuration.md +14 -0
- package/docs/plugins/errors.js +22 -0
- package/docs/plugins/errorsref.md +9 -0
- package/errors/errors.json +87 -0
- package/errors/node-core.json +39 -0
- package/index.js +5 -0
- package/lib/AbstractModule.js +3 -13
- package/lib/AdaptError.js +57 -0
- package/lib/App.js +66 -51
- package/lib/Config.js +226 -0
- package/lib/DataCache.js +6 -0
- package/lib/DependencyLoader.js +46 -103
- package/lib/Errors.js +50 -0
- package/lib/Hook.js +2 -3
- package/lib/Lang.js +126 -0
- package/lib/Logger.js +149 -0
- package/lib/utils/getArgs.js +4 -6
- package/migrations/3.0.0-conf-migrate-lang-config.js +7 -0
- package/migrations/3.0.0-conf-migrate-logger-config.js +9 -0
- package/package.json +8 -25
- package/tests/AbstractModule.spec.js +4 -54
- package/tests/AdaptError.spec.js +62 -0
- package/tests/Config.spec.js +122 -0
- package/tests/DataCache.spec.js +84 -1
- package/tests/DependencyLoader.spec.js +61 -146
- package/tests/Errors.spec.js +91 -0
- package/tests/Lang.spec.js +116 -0
- package/tests/Logger.spec.js +187 -0
- package/tests/utils-getArgs.spec.js +2 -8
- package/tests/App.spec.js +0 -160
package/lib/Config.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Schemas } from 'adapt-schemas'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads, validates, and provides access to application configuration.
|
|
7
|
+
* Configuration is sourced from user settings files, environment variables, and module schema defaults.
|
|
8
|
+
* @memberof core
|
|
9
|
+
*/
|
|
10
|
+
class Config {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {String} options.rootDir Application root directory
|
|
14
|
+
* @param {String} options.configFilePath Path to the user configuration file
|
|
15
|
+
* @param {Object} options.dependencies Key/value map of dependency configs
|
|
16
|
+
* @param {String} options.appName The core module name (for sorting)
|
|
17
|
+
* @param {Function} options.log Logging function (level, id, ...args)
|
|
18
|
+
*/
|
|
19
|
+
constructor ({ rootDir, configFilePath, dependencies = {}, appName = '', log = () => {} } = {}) {
|
|
20
|
+
/** @ignore */
|
|
21
|
+
this._config = {}
|
|
22
|
+
/**
|
|
23
|
+
* Application root directory
|
|
24
|
+
* @type {String}
|
|
25
|
+
*/
|
|
26
|
+
this.rootDir = rootDir
|
|
27
|
+
/**
|
|
28
|
+
* Path to the user configuration file
|
|
29
|
+
* @type {String}
|
|
30
|
+
*/
|
|
31
|
+
this.configFilePath = configFilePath
|
|
32
|
+
/**
|
|
33
|
+
* The keys for all attributes marked as public
|
|
34
|
+
* @type {Array<String>}
|
|
35
|
+
*/
|
|
36
|
+
this.publicAttributes = []
|
|
37
|
+
/** @ignore */
|
|
38
|
+
this._dependencies = dependencies
|
|
39
|
+
/** @ignore */
|
|
40
|
+
this._appName = appName
|
|
41
|
+
/** @ignore */
|
|
42
|
+
this.log = log
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loads configuration from all sources
|
|
47
|
+
* @returns {Promise}
|
|
48
|
+
*/
|
|
49
|
+
async load () {
|
|
50
|
+
await this.storeUserSettings()
|
|
51
|
+
this.storeEnvSettings()
|
|
52
|
+
this.storeSchemaSettings(this._dependencies, this._appName)
|
|
53
|
+
this.log('info', 'config', `using config at ${this.configFilePath}`)
|
|
54
|
+
return this
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Determines whether an attribute has a set value
|
|
59
|
+
* @param {String} attr Attribute key name
|
|
60
|
+
* @return {Boolean}
|
|
61
|
+
*/
|
|
62
|
+
has (attr) {
|
|
63
|
+
return Object.hasOwn(this._config, attr)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns a value for a given attribute
|
|
68
|
+
* @param {String} attr Attribute key name
|
|
69
|
+
* @return {*}
|
|
70
|
+
*/
|
|
71
|
+
get (attr) {
|
|
72
|
+
return this._config[attr]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Retrieves all config options marked as 'public'
|
|
77
|
+
* @return {Object}
|
|
78
|
+
*/
|
|
79
|
+
getPublicConfig () {
|
|
80
|
+
return this.publicAttributes.reduce((m, a) => {
|
|
81
|
+
m[a] = this.get(a)
|
|
82
|
+
return m
|
|
83
|
+
}, {})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Loads the relevant config file into memory
|
|
88
|
+
* @return {Promise}
|
|
89
|
+
*/
|
|
90
|
+
async storeUserSettings () {
|
|
91
|
+
let config
|
|
92
|
+
try {
|
|
93
|
+
await fs.readFile(this.configFilePath)
|
|
94
|
+
config = (await import(this.configFilePath)).default
|
|
95
|
+
} catch (e) {
|
|
96
|
+
this.log('warn', 'config', `Failed to load config at ${this.configFilePath}: ${e}. Will attempt to run with defaults.`)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
Object.entries(config).forEach(([name, c]) => {
|
|
100
|
+
Object.entries(c).forEach(([key, val]) => {
|
|
101
|
+
this._config[`${name}.${key}`] = val
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Copy env values to config
|
|
108
|
+
*/
|
|
109
|
+
storeEnvSettings () {
|
|
110
|
+
Object.entries(process.env).forEach(([key, val]) => {
|
|
111
|
+
try {
|
|
112
|
+
val = JSON.parse(val)
|
|
113
|
+
} catch {} // ignore parse errors for non-JSON values
|
|
114
|
+
this._config[Config.envVarToConfigKey(key)] = val
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Processes all module config schema files
|
|
120
|
+
* @param {Object} dependencies Key/value map of dependency configs
|
|
121
|
+
* @param {String} appName The core module name (for sorting)
|
|
122
|
+
*/
|
|
123
|
+
storeSchemaSettings (dependencies, appName) {
|
|
124
|
+
const schemas = new Schemas().init()
|
|
125
|
+
const isCore = d => d.name === appName
|
|
126
|
+
const deps = Object.values(dependencies).sort((a, b) => {
|
|
127
|
+
if (isCore(a)) return -1
|
|
128
|
+
if (isCore(b)) return 1
|
|
129
|
+
return a.name.localeCompare(b.name)
|
|
130
|
+
})
|
|
131
|
+
const coreDep = deps.find(d => isCore(d))
|
|
132
|
+
if (coreDep) this.processModuleSchema(coreDep, schemas)
|
|
133
|
+
|
|
134
|
+
const errors = []
|
|
135
|
+
for (const d of deps.filter(d => !isCore(d))) {
|
|
136
|
+
try {
|
|
137
|
+
this.processModuleSchema(d, schemas)
|
|
138
|
+
} catch (e) {
|
|
139
|
+
errors.push(e?.data?.errors ? { modName: e.modName, message: e.data.errors } : { message: String(e) })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (errors.length) {
|
|
143
|
+
errors.forEach(e => {
|
|
144
|
+
this.log('error', 'config', `${e.modName ? e.modName + ': ' : ''}${e.message}`)
|
|
145
|
+
})
|
|
146
|
+
throw new Error('Config validation failed')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Processes and validates a single module config schema
|
|
152
|
+
* @param {Object} pkg Package.json data
|
|
153
|
+
* @param {Schemas} schemas Schemas library instance
|
|
154
|
+
*/
|
|
155
|
+
processModuleSchema (pkg, schemas) {
|
|
156
|
+
if (!pkg.name || !pkg.rootDir) return
|
|
157
|
+
const schemaPath = path.resolve(pkg.rootDir, 'conf/config.schema.json')
|
|
158
|
+
let schema
|
|
159
|
+
try {
|
|
160
|
+
// TODO config schemas should define $id, remove this workaround once they do
|
|
161
|
+
schema = schemas.createSchema(schemaPath)
|
|
162
|
+
schema.raw.$id = pkg.name
|
|
163
|
+
schema.build({ compile: false })
|
|
164
|
+
schema.built.$id = pkg.name
|
|
165
|
+
schema.compiledWithDefaults = schemas.validatorWithDefaults.compile(schema.built)
|
|
166
|
+
schema.compiled = schemas.validator.compile(schema.built)
|
|
167
|
+
} catch (e) {
|
|
168
|
+
if (e.code !== 'SCHEMA_LOAD_FAILED') {
|
|
169
|
+
this.log('warn', 'config', `${pkg.name}: ${e.message}`)
|
|
170
|
+
}
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
const dirKeys = new Set()
|
|
174
|
+
let data = Object.entries(schema.raw.properties).reduce((m, [k, v]) => {
|
|
175
|
+
if (v?._adapt?.isPublic) this.publicAttributes.push(`${pkg.name}.${k}`)
|
|
176
|
+
if (v?.isDirectory) dirKeys.add(k)
|
|
177
|
+
return { ...m, [k]: this.get(`${pkg.name}.${k}`) }
|
|
178
|
+
}, {})
|
|
179
|
+
try {
|
|
180
|
+
data = schema.validate(data)
|
|
181
|
+
} catch (e) {
|
|
182
|
+
e.modName = pkg.name
|
|
183
|
+
throw e
|
|
184
|
+
}
|
|
185
|
+
Object.entries(data).forEach(([key, val]) => {
|
|
186
|
+
if (dirKeys.has(key) && typeof val === 'string') {
|
|
187
|
+
val = this.resolveDirectory(val)
|
|
188
|
+
}
|
|
189
|
+
this._config[`${pkg.name}.${key}`] = val
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolves directory path variables ($ROOT, $DATA, $TEMP)
|
|
195
|
+
* @param {String} value The path string to resolve
|
|
196
|
+
* @return {String}
|
|
197
|
+
*/
|
|
198
|
+
resolveDirectory (value) {
|
|
199
|
+
const vars = [
|
|
200
|
+
['$ROOT', this.rootDir],
|
|
201
|
+
['$DATA', this._config['adapt-authoring-core.dataDir']],
|
|
202
|
+
['$TEMP', this._config['adapt-authoring-core.tempDir']]
|
|
203
|
+
]
|
|
204
|
+
for (const [key, replacement] of vars) {
|
|
205
|
+
if (value.startsWith(key) && replacement && !replacement.startsWith('$')) {
|
|
206
|
+
return path.resolve(replacement, value.replace(key, '').slice(1))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return value
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parses an environment variable key into a format expected by Config
|
|
214
|
+
* @param {String} envVar
|
|
215
|
+
* @return {String}
|
|
216
|
+
*/
|
|
217
|
+
static envVarToConfigKey (envVar) {
|
|
218
|
+
if (envVar.startsWith('ADAPT_AUTHORING_')) {
|
|
219
|
+
const [modPrefix, key] = envVar.split('__')
|
|
220
|
+
return `${modPrefix.replace(/_/g, '-').toLowerCase()}.${key}`
|
|
221
|
+
}
|
|
222
|
+
return `env.${envVar}`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default Config
|
package/lib/DataCache.js
CHANGED
|
@@ -13,6 +13,8 @@ class DataCache {
|
|
|
13
13
|
this.isEnabled = enable === true
|
|
14
14
|
this.lifespan = lifespan
|
|
15
15
|
this.cache = {}
|
|
16
|
+
this.hits = 0
|
|
17
|
+
this.misses = 0
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -26,8 +28,12 @@ class DataCache {
|
|
|
26
28
|
const key = JSON.stringify(query) + JSON.stringify(options) + JSON.stringify(mongoOptions)
|
|
27
29
|
this.prune()
|
|
28
30
|
if (this.isEnabled && this.cache[key]) {
|
|
31
|
+
this.hits++
|
|
32
|
+
App.instance.logger?.log('verbose', 'datacache', 'hit', options.collectionName, query)
|
|
29
33
|
return this.cache[key].data
|
|
30
34
|
}
|
|
35
|
+
this.misses++
|
|
36
|
+
App.instance.logger?.log('verbose', 'datacache', 'miss', options.collectionName, query)
|
|
31
37
|
const mongodb = await App.instance.waitForModule('mongodb')
|
|
32
38
|
const data = await mongodb.find(options.collectionName, query, mongoOptions)
|
|
33
39
|
this.cache[key] = { data, timestamp: Date.now() }
|
package/lib/DependencyLoader.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
/* eslint no-console: 0 */
|
|
2
|
-
import _ from 'lodash'
|
|
3
|
-
import fs from 'fs-extra'
|
|
4
1
|
import { glob } from 'glob'
|
|
5
2
|
import path from 'path'
|
|
6
3
|
import Hook from './Hook.js'
|
|
7
|
-
import { metadataFileName, packageFileName, stripScope } from './Utils.js'
|
|
4
|
+
import { metadataFileName, packageFileName, stripScope, readJson } from './Utils.js'
|
|
5
|
+
|
|
8
6
|
/**
|
|
9
7
|
* Handles the loading of Adapt authoring tool module dependencies.
|
|
10
8
|
* @memberof core
|
|
@@ -59,47 +57,21 @@ class DependencyLoader {
|
|
|
59
57
|
this.moduleLoadedHook.tap(this.logProgress, this)
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
/**
|
|
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
|
|
66
|
-
*/
|
|
67
|
-
async load () {
|
|
68
|
-
await this.loadConfigs()
|
|
69
|
-
|
|
70
|
-
const configValues = Object.values(this.configs)
|
|
71
|
-
// sort dependencies into priority
|
|
72
|
-
const { essential, theRest } = configValues.reduce((m, c) => {
|
|
73
|
-
this.app.pkg.essentialApis.includes(c.essentialType) ? m.essential.push(c.name) : m.theRest.push(c.name)
|
|
74
|
-
return m
|
|
75
|
-
}, { essential: [], theRest: [] })
|
|
76
|
-
// load each set of deps
|
|
77
|
-
await this.loadModules(essential)
|
|
78
|
-
await this.loadModules(theRest, { force: true })
|
|
79
|
-
|
|
80
|
-
if (this.failedModules.length) {
|
|
81
|
-
throw new Error(`Failed to load modules ${this.failedModules.join(', ')}`)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
60
|
/**
|
|
86
61
|
* Loads configuration files for all Adapt dependencies found in node_modules.
|
|
87
62
|
* @return {Promise<void>}
|
|
88
63
|
*/
|
|
89
64
|
async loadConfigs () {
|
|
90
65
|
/** @ignore */ this._configsLoaded = false
|
|
66
|
+
const corePathSegment = `/${this.app.name}/`
|
|
91
67
|
const files = await glob(`${this.app.rootDir}/node_modules/**/${metadataFileName}`)
|
|
92
68
|
const deps = files
|
|
93
69
|
.map(d => d.replace(`${metadataFileName}`, ''))
|
|
94
|
-
.sort((a, b) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (a.endsWith(corePathSegment)) return -1
|
|
100
|
-
if (b.endsWith(corePathSegment)) return 1
|
|
101
|
-
return 0
|
|
102
|
-
})
|
|
70
|
+
.sort((a, b) => {
|
|
71
|
+
if (a.endsWith(corePathSegment)) return -1
|
|
72
|
+
if (b.endsWith(corePathSegment)) return 1
|
|
73
|
+
return a.length - b.length
|
|
74
|
+
})
|
|
103
75
|
for (const d of deps) {
|
|
104
76
|
try {
|
|
105
77
|
const c = await this.loadModuleConfig(d)
|
|
@@ -112,8 +84,8 @@ class DependencyLoader {
|
|
|
112
84
|
}
|
|
113
85
|
}
|
|
114
86
|
} catch (e) {
|
|
115
|
-
this.
|
|
116
|
-
this.
|
|
87
|
+
this.log('error', `Failed to load config for '${d}', module will not be loaded`)
|
|
88
|
+
this.log('error', e)
|
|
117
89
|
}
|
|
118
90
|
}
|
|
119
91
|
this._configsLoaded = true
|
|
@@ -126,10 +98,10 @@ class DependencyLoader {
|
|
|
126
98
|
* @return {Promise<Object>} Resolves with configuration object
|
|
127
99
|
*/
|
|
128
100
|
async loadModuleConfig (modDir) {
|
|
129
|
-
const pkg = await
|
|
101
|
+
const pkg = await readJson(path.join(modDir, packageFileName))
|
|
130
102
|
return {
|
|
131
103
|
...pkg,
|
|
132
|
-
...await
|
|
104
|
+
...await readJson(path.join(modDir, metadataFileName)),
|
|
133
105
|
name: stripScope(pkg.name),
|
|
134
106
|
packageName: pkg.name,
|
|
135
107
|
rootDir: modDir
|
|
@@ -144,7 +116,7 @@ class DependencyLoader {
|
|
|
144
116
|
*/
|
|
145
117
|
async loadModule (modName) {
|
|
146
118
|
if (this.instances[modName]) {
|
|
147
|
-
throw
|
|
119
|
+
throw this.app.errors.DEP_ALREADY_LOADED.setData({ module: modName })
|
|
148
120
|
}
|
|
149
121
|
const config = this.configs[modName]
|
|
150
122
|
|
|
@@ -153,54 +125,48 @@ class DependencyLoader {
|
|
|
153
125
|
}
|
|
154
126
|
const { default: ModClass } = await import(config.packageName)
|
|
155
127
|
|
|
156
|
-
if (
|
|
157
|
-
throw
|
|
128
|
+
if (typeof ModClass !== 'function') {
|
|
129
|
+
throw this.app.errors.DEP_INVALID_EXPORT.setData({ module: modName })
|
|
158
130
|
}
|
|
159
131
|
const instance = new ModClass(this.app, config)
|
|
160
132
|
|
|
161
|
-
if (
|
|
162
|
-
throw
|
|
133
|
+
if (typeof instance.onReady !== 'function') {
|
|
134
|
+
throw this.app.errors.DEP_NO_ONREADY.setData({ module: modName })
|
|
163
135
|
}
|
|
164
136
|
try {
|
|
165
|
-
|
|
166
|
-
const timeout = this.getConfig('moduleLoadTimeout') ?? 10000
|
|
137
|
+
const timeout = this.app.getConfig('moduleLoadTimeout') ?? 10000
|
|
167
138
|
await Promise.race([
|
|
168
139
|
instance.onReady(),
|
|
169
|
-
new Promise((resolve, reject) => setTimeout(() => reject(
|
|
140
|
+
new Promise((resolve, reject) => setTimeout(() => reject(this.app.errors.DEP_TIMEOUT.setData({ module: modName, timeout })), timeout))
|
|
170
141
|
])
|
|
171
142
|
this.instances[modName] = instance
|
|
172
143
|
await this.moduleLoadedHook.invoke(null, instance)
|
|
173
144
|
return instance
|
|
174
145
|
} catch (e) {
|
|
175
|
-
await this.moduleLoadedHook.invoke(e)
|
|
146
|
+
await this.moduleLoadedHook.invoke(e, { name: modName })
|
|
176
147
|
throw e
|
|
177
148
|
}
|
|
178
149
|
}
|
|
179
150
|
|
|
180
151
|
/**
|
|
181
|
-
* Loads
|
|
182
|
-
* @param {Array<string>} modules Module names to load
|
|
183
|
-
* @
|
|
184
|
-
* @
|
|
185
|
-
* @return {Promise<void>} Resolves when all modules have loaded (or failed to load in force mode)
|
|
186
|
-
* @throws {DependencyError} When a module fails to load and options.force is not true
|
|
152
|
+
* Loads Adapt modules. If no list is provided, loads all configured dependencies.
|
|
153
|
+
* @param {Array<string>} [modules] Module names to load (defaults to all dependencies)
|
|
154
|
+
* @return {Promise<void>} Resolves when all modules have loaded or failed
|
|
155
|
+
* @throws {Error} When any module throws a fatal error (error.isFatal or error.cause.isFatal)
|
|
187
156
|
*/
|
|
188
|
-
async loadModules (modules
|
|
157
|
+
async loadModules (modules = Object.values(this.configs).map(c => c.name)) {
|
|
189
158
|
await Promise.all(modules.map(async m => {
|
|
190
159
|
try {
|
|
191
160
|
await this.loadModule(m)
|
|
192
161
|
} catch (e) {
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
error.name = 'DependencyError'
|
|
196
|
-
error.cause = e
|
|
197
|
-
throw error
|
|
162
|
+
if (e.isFatal || e.cause?.isFatal) {
|
|
163
|
+
throw e
|
|
198
164
|
}
|
|
199
|
-
this.
|
|
165
|
+
this.log('error', `Failed to load '${m}',`, e)
|
|
200
166
|
const deps = this.peerDependencies[m]
|
|
201
|
-
if (deps
|
|
202
|
-
this.
|
|
203
|
-
deps.forEach(d => this.
|
|
167
|
+
if (deps?.length) {
|
|
168
|
+
this.log('error', 'The following modules are peer dependencies, and may not work:')
|
|
169
|
+
deps.forEach(d => this.log('error', `- ${d}`))
|
|
204
170
|
}
|
|
205
171
|
this.failedModules.push(m)
|
|
206
172
|
}
|
|
@@ -217,14 +183,12 @@ class DependencyLoader {
|
|
|
217
183
|
if (!this._configsLoaded) {
|
|
218
184
|
await this.configsLoadedHook.onInvoke()
|
|
219
185
|
}
|
|
220
|
-
|
|
221
|
-
if (!modName.startsWith(longPrefix)) modName = `adapt-authoring-${modName}`
|
|
186
|
+
if (!modName.startsWith('adapt-authoring-')) modName = `adapt-authoring-${modName}`
|
|
222
187
|
if (!this.configs[modName]) {
|
|
223
|
-
throw
|
|
188
|
+
throw this.app.errors.DEP_MISSING.setData({ module: modName })
|
|
224
189
|
}
|
|
225
|
-
const DependencyError = new Error(`Dependency '${modName}' failed to load`)
|
|
226
190
|
if (this.failedModules.includes(modName)) {
|
|
227
|
-
throw
|
|
191
|
+
throw this.app.errors.DEP_FAILED.setData({ module: modName })
|
|
228
192
|
}
|
|
229
193
|
const instance = this.instances[modName]
|
|
230
194
|
if (instance) {
|
|
@@ -232,8 +196,9 @@ class DependencyLoader {
|
|
|
232
196
|
}
|
|
233
197
|
return new Promise((resolve, reject) => {
|
|
234
198
|
this.moduleLoadedHook.tap((error, instance) => {
|
|
235
|
-
if (
|
|
236
|
-
if (
|
|
199
|
+
if (instance?.name !== modName) return
|
|
200
|
+
if (error) return reject(this.app.errors.DEP_FAILED.setData({ module: modName }))
|
|
201
|
+
resolve(instance)
|
|
237
202
|
})
|
|
238
203
|
})
|
|
239
204
|
}
|
|
@@ -243,9 +208,8 @@ class DependencyLoader {
|
|
|
243
208
|
* @param {AbstractModule} instance The last loaded instance
|
|
244
209
|
*/
|
|
245
210
|
logProgress (error, instance) {
|
|
246
|
-
if (error)
|
|
247
|
-
|
|
248
|
-
}
|
|
211
|
+
if (error) return
|
|
212
|
+
|
|
249
213
|
const toShort = names => names.map(n => n.replace('adapt-authoring-', '')).join(', ')
|
|
250
214
|
const loaded = []
|
|
251
215
|
const notLoaded = []
|
|
@@ -264,42 +228,21 @@ class DependencyLoader {
|
|
|
264
228
|
].filter(Boolean).join(', '))
|
|
265
229
|
|
|
266
230
|
if (progress === 100) {
|
|
267
|
-
const initTimes = Object.
|
|
268
|
-
.
|
|
269
|
-
|
|
231
|
+
const initTimes = Object.fromEntries(
|
|
232
|
+
Object.entries(this.instances)
|
|
233
|
+
.sort(([, a], [, b]) => a.initTime - b.initTime)
|
|
234
|
+
.map(([name, inst]) => [name, inst.initTime])
|
|
235
|
+
)
|
|
270
236
|
this.log('verbose', initTimes)
|
|
271
237
|
}
|
|
272
238
|
}
|
|
273
239
|
|
|
274
240
|
/**
|
|
275
|
-
* Logs a message using the app logger
|
|
241
|
+
* Logs a message using the app logger
|
|
276
242
|
* @param {...*} args Arguments to be logged
|
|
277
243
|
*/
|
|
278
244
|
log (level, ...args) {
|
|
279
|
-
|
|
280
|
-
this.app.logger.log(level, this.name, ...args)
|
|
281
|
-
} else {
|
|
282
|
-
console.log(...args)
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Logs an error message using the app logger if available, otherwise falls back to console.log
|
|
288
|
-
* @param {...*} args Arguments to be logged
|
|
289
|
-
*/
|
|
290
|
-
logError (...args) {
|
|
291
|
-
this.log('error', ...args)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Retrieves a configuration value from this module's config
|
|
296
|
-
* @param {string} key - The configuration key to retrieve
|
|
297
|
-
* @returns {*|undefined} The configuration value if config is ready, undefined otherwise
|
|
298
|
-
*/
|
|
299
|
-
getConfig (key) {
|
|
300
|
-
if (this.app.config?._isReady) {
|
|
301
|
-
return this.app.config.get(`adapt-authoring-core.${key}`)
|
|
302
|
-
}
|
|
245
|
+
this.app.logger?.log(level, this.name, ...args)
|
|
303
246
|
}
|
|
304
247
|
}
|
|
305
248
|
|
package/lib/Errors.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import AdaptError from './AdaptError.js'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { globSync } from 'glob'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads and stores all error definitions for the application. Errors are accessed via human-readable error codes for better readability when thrown in code.
|
|
7
|
+
* @memberof core
|
|
8
|
+
*/
|
|
9
|
+
class Errors {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {Object} options.dependencies Key/value map of dependency configs (each with a rootDir)
|
|
13
|
+
* @param {Function} [options.log] Optional logging function (level, id, ...args)
|
|
14
|
+
*/
|
|
15
|
+
constructor ({ dependencies, log } = {}) {
|
|
16
|
+
const errorDefs = {}
|
|
17
|
+
for (const d of Object.values(dependencies)) {
|
|
18
|
+
const files = globSync('errors/*.json', { cwd: d.rootDir, absolute: true })
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
try {
|
|
21
|
+
const contents = JSON.parse(fs.readFileSync(f))
|
|
22
|
+
Object.entries(contents).forEach(([k, v]) => {
|
|
23
|
+
if (errorDefs[k]) {
|
|
24
|
+
log?.('warn', 'errors', `error code '${k}' already defined`)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
errorDefs[k] = v
|
|
28
|
+
})
|
|
29
|
+
} catch (e) {
|
|
30
|
+
log?.('warn', 'errors', e.message)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
Object.entries(errorDefs)
|
|
35
|
+
.sort()
|
|
36
|
+
.forEach(([k, { description, statusCode, isFatal, data }]) => {
|
|
37
|
+
Object.defineProperty(this, k, {
|
|
38
|
+
get: () => {
|
|
39
|
+
const metadata = { description }
|
|
40
|
+
if (isFatal) metadata.isFatal = true
|
|
41
|
+
if (data) metadata.data = data
|
|
42
|
+
return new AdaptError(k, statusCode, metadata)
|
|
43
|
+
},
|
|
44
|
+
enumerable: true
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default Errors
|
package/lib/Hook.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
1
|
/**
|
|
3
2
|
* Allows observers to tap into to a specific piece of code, and execute their own arbitrary code
|
|
4
3
|
* @memberof core
|
|
@@ -43,7 +42,7 @@ class Hook {
|
|
|
43
42
|
* @param {*} scope Sets the scope of the observer
|
|
44
43
|
*/
|
|
45
44
|
tap (observer, scope) {
|
|
46
|
-
if (
|
|
45
|
+
if (typeof observer === 'function') this._hookObservers.push(observer.bind(scope))
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
/**
|
|
@@ -78,7 +77,7 @@ class Hook {
|
|
|
78
77
|
data = await Promise.all(this._hookObservers.map(o => o(...args)))
|
|
79
78
|
} else {
|
|
80
79
|
// if not mutable, send a deep copy of the args to avoid any meddling
|
|
81
|
-
for (const o of this._hookObservers) data = await o(...this._options.mutable ? args : args.map(a =>
|
|
80
|
+
for (const o of this._hookObservers) data = await o(...this._options.mutable ? args : args.map(a => structuredClone(a)))
|
|
82
81
|
}
|
|
83
82
|
} catch (e) {
|
|
84
83
|
error = e
|