adapt-authoring-core 3.0.2 → 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",
@@ -40,6 +40,44 @@
40
40
  "description": "The default language used by the server",
41
41
  "type": "string",
42
42
  "default": "en"
43
+ },
44
+ "appName": {
45
+ "description": "Application name displayed in the UI header and browser tab",
46
+ "type": "string",
47
+ "default": "Adapt",
48
+ "_adapt": { "isPublic": true }
49
+ },
50
+ "primaryColour": {
51
+ "description": "Primary palette colour for the UI theme (hex format)",
52
+ "type": "string",
53
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
54
+ "default": "#1ec0d9",
55
+ "_adapt": { "isPublic": true }
56
+ },
57
+ "chromeColour": {
58
+ "description": "Chrome colour (navy): icon rail, app bar, headings (hex format)",
59
+ "type": "string",
60
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
61
+ "default": "#263944",
62
+ "_adapt": { "isPublic": true }
63
+ },
64
+ "commitColour": {
65
+ "description": "Commit colour (mint): the single affirmative action per view, e.g. Save/Create (hex format)",
66
+ "type": "string",
67
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
68
+ "default": "#00dd95",
69
+ "_adapt": { "isPublic": true }
70
+ },
71
+ "accentColour": {
72
+ "description": "Accent colour (expressive) and the default decorative-icon ink; never a button fill (hex format)",
73
+ "type": "string",
74
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
75
+ "default": "#ec4899",
76
+ "_adapt": { "isPublic": true }
77
+ },
78
+ "logo": {
79
+ "description": "Absolute path to a custom logo image (PNG recommended — email-safe) to bake into the UI build (overrides the built-in mark) and reuse in branded emails. Leave unset to use the built-in logo.",
80
+ "type": "string"
43
81
  }
44
82
  }
45
83
  }
@@ -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.
@@ -122,4 +133,33 @@ If you don't want to publish your module to npm, you can simply provide the URL
122
133
  "adapt-authoring-mymodule": "https://github.com/MY_GITHUB_ACCOUNT/GITHUB_REPO_NAME.git"
123
134
  }
124
135
  }
125
- ```
136
+ ```
137
+
138
+ ## Conventions
139
+
140
+ Beyond structure and code, modules follow a few project-wide conventions.
141
+
142
+ ### Linting
143
+
144
+ All code must pass [Standard.js](https://standardjs.com/) — no config file is needed. Run `npx standard` in the module root; it must pass before opening a PR.
145
+
146
+ ### Testing
147
+
148
+ Write tests with `node:test` and `node:assert/strict` (see [Writing tests](writing-tests)). The project's testable-code conventions:
149
+
150
+ - Extract discrete logic (transformations, mappers, predicates, validators) into one function per file under `lib/utils/<fn>.js`, re-exported via a `lib/utils.js` barrel.
151
+ - Keep logic in the class only when it needs instance state, orchestrates side effects, or is a trivial one-liner delegating to a utility.
152
+ - Each utility gets a matching `tests/utils-<name>.spec.js`, importing the file directly.
153
+ - Use `mock.module()` (before the dynamic `import()`) when the function imports app modules; use table-driven tests for mappers and lookups.
154
+ - Run unit tests with `node --experimental-test-module-mocks --test 'tests/**/*.spec.js'`.
155
+
156
+ ### Workflow and releases
157
+
158
+ - Start from a clean `master` (`git checkout master && git pull`), then branch.
159
+ - Commit with `Tag: description (fixes #N)` — the tag determines the release type (see [Contributing code](contributing-code)).
160
+ - Merging to `master` triggers an **immediate** semantic-release publish; there is no staging step between merge and publish (see [Developer workflow](developer-workflow)).
161
+
162
+ ### Documentation
163
+
164
+ - Document non-obvious behaviour in `docs/*.md` guides (built into the documentation site), not just inline comments. Cover what the module does, the APIs/seams it exposes, and any gotchas.
165
+ - Keep the guides current: when a change alters documented behaviour, update the relevant `docs/*.md` in the **same PR**. Treat stale docs as a bug.
@@ -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.0.2",
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
+ })