adapt-authoring-core 3.0.1 → 3.1.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
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
|
@@ -122,4 +122,33 @@ If you don't want to publish your module to npm, you can simply provide the URL
|
|
|
122
122
|
"adapt-authoring-mymodule": "https://github.com/MY_GITHUB_ACCOUNT/GITHUB_REPO_NAME.git"
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
```
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Conventions
|
|
128
|
+
|
|
129
|
+
Beyond structure and code, modules follow a few project-wide conventions.
|
|
130
|
+
|
|
131
|
+
### Linting
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
### Testing
|
|
136
|
+
|
|
137
|
+
Write tests with `node:test` and `node:assert/strict` (see [Writing tests](writing-tests)). The project's testable-code conventions:
|
|
138
|
+
|
|
139
|
+
- 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.
|
|
140
|
+
- Keep logic in the class only when it needs instance state, orchestrates side effects, or is a trivial one-liner delegating to a utility.
|
|
141
|
+
- Each utility gets a matching `tests/utils-<name>.spec.js`, importing the file directly.
|
|
142
|
+
- Use `mock.module()` (before the dynamic `import()`) when the function imports app modules; use table-driven tests for mappers and lookups.
|
|
143
|
+
- Run unit tests with `node --experimental-test-module-mocks --test 'tests/**/*.spec.js'`.
|
|
144
|
+
|
|
145
|
+
### Workflow and releases
|
|
146
|
+
|
|
147
|
+
- Start from a clean `master` (`git checkout master && git pull`), then branch.
|
|
148
|
+
- Commit with `Tag: description (fixes #N)` — the tag determines the release type (see [Contributing code](contributing-code)).
|
|
149
|
+
- Merging to `master` triggers an **immediate** semantic-release publish; there is no staging step between merge and publish (see [Developer workflow](developer-workflow)).
|
|
150
|
+
|
|
151
|
+
### Documentation
|
|
152
|
+
|
|
153
|
+
- 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.
|
|
154
|
+
- 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/DependencyLoader.js
CHANGED
|
@@ -68,8 +68,9 @@ class DependencyLoader {
|
|
|
68
68
|
const deps = files
|
|
69
69
|
.map(d => d.replace(`${metadataFileName}`, ''))
|
|
70
70
|
.sort((a, b) => {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const aIsCore = a.endsWith(corePathSegment)
|
|
72
|
+
const bIsCore = b.endsWith(corePathSegment)
|
|
73
|
+
if (aIsCore !== bIsCore) return aIsCore ? -1 : 1
|
|
73
74
|
return a.length - b.length
|
|
74
75
|
})
|
|
75
76
|
for (const d of deps) {
|
|
@@ -133,11 +134,14 @@ class DependencyLoader {
|
|
|
133
134
|
if (typeof instance.onReady !== 'function') {
|
|
134
135
|
throw this.app.errors.DEP_NO_ONREADY.setData({ module: modName })
|
|
135
136
|
}
|
|
137
|
+
let timer
|
|
136
138
|
try {
|
|
137
139
|
const timeout = this.app.getConfig('moduleLoadTimeout') ?? 10000
|
|
138
140
|
await Promise.race([
|
|
139
141
|
instance.onReady(),
|
|
140
|
-
new Promise((resolve, reject) =>
|
|
142
|
+
new Promise((resolve, reject) => {
|
|
143
|
+
timer = setTimeout(() => reject(this.app.errors.DEP_TIMEOUT.setData({ module: modName, timeout })), timeout)
|
|
144
|
+
})
|
|
141
145
|
])
|
|
142
146
|
this.instances[modName] = instance
|
|
143
147
|
await this.moduleLoadedHook.invoke(null, instance)
|
|
@@ -145,6 +149,8 @@ class DependencyLoader {
|
|
|
145
149
|
} catch (e) {
|
|
146
150
|
await this.moduleLoadedHook.invoke(e, { name: modName })
|
|
147
151
|
throw e
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(timer)
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
|
package/package.json
CHANGED
|
@@ -4,7 +4,8 @@ import AdaptError from '../lib/AdaptError.js'
|
|
|
4
4
|
import DependencyLoader from '../lib/DependencyLoader.js'
|
|
5
5
|
import fs from 'fs-extra'
|
|
6
6
|
import path from 'path'
|
|
7
|
-
import { fileURLToPath } from 'url'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
8
|
+
import { setTimeout as wait } from 'timers/promises'
|
|
8
9
|
|
|
9
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
11
|
|
|
@@ -132,6 +133,39 @@ describe('DependencyLoader', () => {
|
|
|
132
133
|
})
|
|
133
134
|
})
|
|
134
135
|
|
|
136
|
+
describe('#loadConfigs() with nested duplicate', () => {
|
|
137
|
+
let testRootDir
|
|
138
|
+
|
|
139
|
+
before(async () => {
|
|
140
|
+
testRootDir = path.join(__dirname, 'data', 'loadconfigs-nested-root')
|
|
141
|
+
const topCoreDir = path.join(testRootDir, 'node_modules', 'adapt-authoring-core')
|
|
142
|
+
const otherDir = path.join(testRootDir, 'node_modules', 'adapt-authoring-other')
|
|
143
|
+
const nestedCoreDir = path.join(otherDir, 'node_modules', 'adapt-authoring-core')
|
|
144
|
+
await fs.ensureDir(topCoreDir)
|
|
145
|
+
await fs.ensureDir(nestedCoreDir)
|
|
146
|
+
await fs.writeJson(path.join(topCoreDir, 'package.json'), { name: 'adapt-authoring-core', version: '3.0.0' })
|
|
147
|
+
await fs.writeJson(path.join(topCoreDir, 'adapt-authoring.json'), { module: false })
|
|
148
|
+
await fs.writeJson(path.join(otherDir, 'package.json'), { name: 'adapt-authoring-other' })
|
|
149
|
+
await fs.writeJson(path.join(otherDir, 'adapt-authoring.json'), { module: true })
|
|
150
|
+
await fs.writeJson(path.join(nestedCoreDir, 'package.json'), { name: 'adapt-authoring-core', version: '1.9.2' })
|
|
151
|
+
await fs.writeJson(path.join(nestedCoreDir, 'adapt-authoring.json'), { module: false })
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
after(async () => {
|
|
155
|
+
await fs.remove(testRootDir)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should prefer the top-level core over a nested duplicate', async () => {
|
|
159
|
+
const mockApp = { rootDir: testRootDir, name: 'adapt-authoring-core' }
|
|
160
|
+
const loader = new DependencyLoader(mockApp)
|
|
161
|
+
await loader.loadConfigs()
|
|
162
|
+
const winningPath = loader.configs['adapt-authoring-core'].rootDir
|
|
163
|
+
const expected = path.join(testRootDir, 'node_modules', 'adapt-authoring-core') + path.sep
|
|
164
|
+
assert.equal(winningPath, expected)
|
|
165
|
+
assert.equal(loader.configs['adapt-authoring-core'].version, '3.0.0')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
135
169
|
describe('#loadModuleConfig()', () => {
|
|
136
170
|
let testModuleDir
|
|
137
171
|
|
|
@@ -346,6 +380,26 @@ describe('DependencyLoader', () => {
|
|
|
346
380
|
|
|
347
381
|
assert.equal(loader.instances['non-module'], undefined)
|
|
348
382
|
})
|
|
383
|
+
|
|
384
|
+
it('should clear the load-timeout timer once onReady resolves', async () => {
|
|
385
|
+
let timeoutAccessed = false
|
|
386
|
+
const mockApp = {
|
|
387
|
+
rootDir: '/test',
|
|
388
|
+
getConfig: () => 25,
|
|
389
|
+
errors: {
|
|
390
|
+
get DEP_TIMEOUT () { timeoutAccessed = true; return new AdaptError('DEP_TIMEOUT') }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const loader = new DependencyLoader(mockApp)
|
|
394
|
+
const packageName = pathToFileURL(path.join(__dirname, 'data', 'fast-module.js')).href
|
|
395
|
+
loader.configs = { 'fast-module': { name: 'fast-module', module: true, packageName } }
|
|
396
|
+
|
|
397
|
+
await loader.loadModule('fast-module')
|
|
398
|
+
await wait(75)
|
|
399
|
+
|
|
400
|
+
assert.equal(timeoutAccessed, false, 'DEP_TIMEOUT should never be accessed after a successful load')
|
|
401
|
+
assert.ok(loader.instances['fast-module'])
|
|
402
|
+
})
|
|
349
403
|
})
|
|
350
404
|
|
|
351
405
|
describe('#waitForModule()', () => {
|