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.
- package/adapt-authoring.json +5 -1
- package/conf/config.schema.json +38 -0
- package/docs/writing-a-module.md +41 -1
- package/lib/AbstractModule.js +2 -1
- package/lib/App.js +9 -0
- package/lib/DependencyLoader.js +22 -3
- package/lib/Utils.js +1 -0
- package/lib/utils/toShortName.js +9 -0
- package/package.json +1 -1
- package/tests/DependencyLoader.spec.js +32 -0
- package/tests/utils-toShortName.spec.js +30 -0
package/adapt-authoring.json
CHANGED
|
@@ -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",
|
package/conf/config.schema.json
CHANGED
|
@@ -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
|
}
|
package/docs/writing-a-module.md
CHANGED
|
@@ -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.
|
package/lib/AbstractModule.js
CHANGED
|
@@ -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
|
|
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
|
package/lib/DependencyLoader.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
@@ -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
|
+
})
|