adapt-authoring-core 3.1.0 → 3.2.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.
@@ -7,17 +7,21 @@
7
7
  "sourceIndex": "docs/index-backend.md",
8
8
  "manualPages": {
9
9
  "binscripts.md": "reference",
10
+ "configure-environment.md": "getting-started",
10
11
  "contributing.md": "contributing",
11
12
  "contributing-code.md": "contributing",
12
13
  "coremodules.md": "reference",
13
14
  "customising.md": "development",
15
+ "developer-workflow.md": "contributing",
16
+ "error-handling.md": "concepts",
14
17
  "folder-structure.md": "getting-started",
15
18
  "hooks.md": "concepts",
16
19
  "licensing.md": "reference",
17
20
  "releasing.md": "contributing",
18
21
  "request-response.md": "concepts",
19
22
  "run.md": "getting-started",
20
- "writing-a-module.md": "development"
23
+ "writing-a-module.md": "development",
24
+ "writing-tests.md": "development"
21
25
  },
22
26
  "manualPlugins": [
23
27
  "docs/plugins/binscripts.js",
@@ -98,6 +98,17 @@ export default class MyModule extends AbstractModule {
98
98
  }
99
99
  ```
100
100
 
101
+ `waitForModule` rejects if the module isn't installed, so it's for **required** dependencies. For an **optional** integration, probe first with `App#isModuleAvailable` (which never throws) and only wait when it's present — don't `try/catch` `waitForModule`, as that conflates "not installed" with "installed but failed to load":
102
+
103
+ ```javascript
104
+ if (this.app.isModuleAvailable('websocket')) {
105
+ const websocket = await this.app.waitForModule('websocket');
106
+ // wire up the optional integration
107
+ }
108
+ ```
109
+
110
+ Both accept short names (without the `adapt-authoring-` prefix).
111
+
101
112
  ### _Optional task: add a configuration schema_
102
113
 
103
114
  If you plan to add user-configurable settings to your module, you can add a `config.schema.json` to define which settings users need to add. See [this page](defining-config) for more information.
@@ -1,4 +1,5 @@
1
1
  import Hook from './Hook.js'
2
+ import { toShortName } from './utils/toShortName.js'
2
3
  /**
3
4
  * Abstract class for authoring tool modules. All custom modules must extend this class.
4
5
  * @memberof core
@@ -114,7 +115,7 @@ class AbstractModule {
114
115
  * @param {...*} rest Arguments to log
115
116
  */
116
117
  log (level, ...rest) {
117
- this.app.logger?.log(level, this.name.replace(/^adapt-authoring-/, ''), ...rest)
118
+ this.app.logger?.log(level, toShortName(this.name), ...rest)
118
119
  }
119
120
  }
120
121
 
package/lib/App.js CHANGED
@@ -146,6 +146,15 @@ class App extends AbstractModule {
146
146
  const results = await Promise.all(modNames.map(m => this.dependencyloader.waitForModule(m)))
147
147
  return results.length > 1 ? results : results[0]
148
148
  }
149
+
150
+ /**
151
+ * Whether a dependency is installed and loadable as an Adapt module. Use to guard optional integrations without throwing (`waitForModule` rejects when a module is missing).
152
+ * @param {string} modName Name of the module (short or full)
153
+ * @return {boolean}
154
+ */
155
+ isModuleAvailable (modName) {
156
+ return this.dependencyloader.isAvailable(modName)
157
+ }
149
158
  }
150
159
 
151
160
  export default App
@@ -1,7 +1,7 @@
1
1
  import { glob } from 'glob'
2
2
  import path from 'path'
3
3
  import Hook from './Hook.js'
4
- import { metadataFileName, packageFileName, stripScope, readJson } from './Utils.js'
4
+ import { metadataFileName, packageFileName, stripScope, toShortName, readJson } from './Utils.js'
5
5
 
6
6
  /**
7
7
  * Handles the loading of Adapt authoring tool module dependencies.
@@ -179,6 +179,25 @@ class DependencyLoader {
179
179
  }))
180
180
  }
181
181
 
182
+ /**
183
+ * Resolves a module name to the canonical key used in {@link DependencyLoader#configs}: short names (without the `adapt-authoring-` prefix) are expanded to the full form. Config keys are scope-stripped at load time, so a scoped package is reached by its unscoped name.
184
+ * @param {string} modName Module name (short or full)
185
+ * @return {string} The canonical config key
186
+ */
187
+ resolveModuleName (modName) {
188
+ return modName.startsWith('adapt-authoring-') ? modName : `adapt-authoring-${modName}`
189
+ }
190
+
191
+ /**
192
+ * Whether a dependency is installed and loadable as an Adapt module. Lets callers guard optional integrations without throwing — unlike {@link DependencyLoader#waitForModule}, which rejects when a module is missing.
193
+ * @param {string} modName Module name (short or full)
194
+ * @return {boolean} `true` when the module is present and not declared `module: false`
195
+ */
196
+ isAvailable (modName) {
197
+ const config = this.configs[this.resolveModuleName(modName)]
198
+ return Boolean(config) && config.module !== false
199
+ }
200
+
182
201
  /**
183
202
  * Waits for a single module to load. Returns the instance (if loaded), or hooks into moduleLoadedHook to wait for it.
184
203
  * @param {string} modName Name of module to wait for (accepts short names without 'adapt-authoring-' prefix)
@@ -189,7 +208,7 @@ class DependencyLoader {
189
208
  if (!this._configsLoaded) {
190
209
  await this.configsLoadedHook.onInvoke()
191
210
  }
192
- if (!modName.startsWith('adapt-authoring-')) modName = `adapt-authoring-${modName}`
211
+ modName = this.resolveModuleName(modName)
193
212
  if (!this.configs[modName]) {
194
213
  throw this.app.errors.DEP_MISSING.setData({ module: modName })
195
214
  }
@@ -216,7 +235,7 @@ class DependencyLoader {
216
235
  logProgress (error, instance) {
217
236
  if (error) return
218
237
 
219
- const toShort = names => names.map(n => n.replace('adapt-authoring-', '')).join(', ')
238
+ const toShort = names => names.map(toShortName).join(', ')
220
239
  const loaded = []
221
240
  const notLoaded = []
222
241
  let totalCount = 0
package/lib/Utils.js CHANGED
@@ -9,4 +9,5 @@ export { ensureDir } from './utils/ensureDir.js'
9
9
  export { escapeRegExp } from './utils/escapeRegExp.js'
10
10
  export { stringifyValues } from './utils/stringifyValues.js'
11
11
  export { stripScope } from './utils/stripScope.js'
12
+ export { toShortName } from './utils/toShortName.js'
12
13
  export { loadDependencyFiles } from './utils/loadDependencyFiles.js'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Strips the `adapt-authoring-` prefix from a module name to give its short, display form (e.g. 'adapt-authoring-server' becomes 'server'). Inverse of the canonicalisation in {@link DependencyLoader#resolveModuleName}.
3
+ * @param {string} name - The module name
4
+ * @returns {string} The name without the prefix
5
+ */
6
+ export function toShortName (name) {
7
+ if (typeof name !== 'string') return name
8
+ return name.replace(/^adapt-authoring-/, '')
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-core",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "A bundle of reusable 'core' functionality",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-core",
6
6
  "license": "GPL-3.0",
@@ -506,6 +506,38 @@ describe('DependencyLoader', () => {
506
506
  })
507
507
  })
508
508
 
509
+ describe('#resolveModuleName()', () => {
510
+ it('should expand a short name to the canonical key', () => {
511
+ const loader = new DependencyLoader({ rootDir: '/test' })
512
+ assert.equal(loader.resolveModuleName('server'), 'adapt-authoring-server')
513
+ })
514
+
515
+ it('should leave a full name unchanged', () => {
516
+ const loader = new DependencyLoader({ rootDir: '/test' })
517
+ assert.equal(loader.resolveModuleName('adapt-authoring-server'), 'adapt-authoring-server')
518
+ })
519
+ })
520
+
521
+ describe('#isAvailable()', () => {
522
+ it('should return true for a present loadable module (short name)', () => {
523
+ const loader = new DependencyLoader({ rootDir: '/test' })
524
+ loader.configs = { 'adapt-authoring-server': { name: 'adapt-authoring-server', module: true } }
525
+ assert.equal(loader.isAvailable('server'), true)
526
+ })
527
+
528
+ it('should return false for a missing module', () => {
529
+ const loader = new DependencyLoader({ rootDir: '/test' })
530
+ loader.configs = {}
531
+ assert.equal(loader.isAvailable('server'), false)
532
+ })
533
+
534
+ it('should return false for a dependency declared module: false', () => {
535
+ const loader = new DependencyLoader({ rootDir: '/test' })
536
+ loader.configs = { 'adapt-authoring-docs': { name: 'adapt-authoring-docs', module: false } }
537
+ assert.equal(loader.isAvailable('docs'), false)
538
+ })
539
+ })
540
+
509
541
  describe('#logProgress()', () => {
510
542
  it('should not throw when called with valid instance', () => {
511
543
  const mockApp = { rootDir: '/test' }
@@ -0,0 +1,30 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { toShortName } from '../lib/utils/toShortName.js'
5
+
6
+ describe('toShortName()', () => {
7
+ it('should strip the adapt-authoring- prefix', () => {
8
+ assert.equal(toShortName('adapt-authoring-server'), 'server')
9
+ })
10
+
11
+ it('should only strip an anchored prefix', () => {
12
+ assert.equal(toShortName('x-adapt-authoring-y'), 'x-adapt-authoring-y')
13
+ })
14
+
15
+ it('should return a name without the prefix unchanged', () => {
16
+ assert.equal(toShortName('server'), 'server')
17
+ })
18
+
19
+ it('should return an empty string unchanged', () => {
20
+ assert.equal(toShortName(''), '')
21
+ })
22
+
23
+ it('should return undefined unchanged', () => {
24
+ assert.equal(toShortName(undefined), undefined)
25
+ })
26
+
27
+ it('should return null unchanged', () => {
28
+ assert.equal(toShortName(null), null)
29
+ })
30
+ })