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.
@@ -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
  }
@@ -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.
@@ -68,8 +68,9 @@ class DependencyLoader {
68
68
  const deps = files
69
69
  .map(d => d.replace(`${metadataFileName}`, ''))
70
70
  .sort((a, b) => {
71
- if (a.endsWith(corePathSegment)) return -1
72
- if (b.endsWith(corePathSegment)) return 1
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) => setTimeout(() => reject(this.app.errors.DEP_TIMEOUT.setData({ module: modName, timeout })), timeout))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-core",
3
- "version": "3.0.1",
3
+ "version": "3.1.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",
@@ -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()', () => {
@@ -0,0 +1,13 @@
1
+ export default class FastModule {
2
+ constructor (app, config) {
3
+ this.app = app
4
+ this.config = config
5
+ this.name = config.name
6
+ this._isReady = false
7
+ }
8
+
9
+ async onReady () {
10
+ this._isReady = true
11
+ return this
12
+ }
13
+ }