adapt-authoring-contentplugin 1.7.1 → 1.8.1
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/README.md +9 -0
- package/adapt-authoring.json +4 -1
- package/docs/content-plugins.md +183 -0
- package/lib/ContentPluginModule.js +47 -15
- package/lib/utils/getPluginReadmes.js +21 -0
- package/lib/utils.js +1 -0
- package/package.json +1 -1
- package/routes.json +21 -0
- package/tests/utils-getPluginReadmes.spec.js +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# adapt-authoring-contentplugin
|
|
2
|
+
|
|
3
|
+
Manages Adapt framework plugins — the components, extensions, menus and themes that course content is built from. Handles installing, updating and querying the plugins available to the authoring tool.
|
|
4
|
+
|
|
5
|
+
Extends `AbstractApiModule` from [adapt-authoring-core](../adapt-authoring-core) and exposes plugins under the `contentplugins` collection.
|
|
6
|
+
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
- [Content plugins](docs/content-plugins.md) — how framework plugins are managed
|
package/adapt-authoring.json
CHANGED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Content plugins
|
|
2
|
+
|
|
3
|
+
`adapt-authoring-contentplugin` manages the **Adapt framework plugins** that ship
|
|
4
|
+
inside built courses: components, extensions, menus and themes. These are the
|
|
5
|
+
packages the Adapt framework installs and bundles at build time — *not* the
|
|
6
|
+
authoring tool's front-end "UI plugins" (those live in a module's `ui/plugin.js`;
|
|
7
|
+
content plugins are Adapt framework packages installed into the framework copy on
|
|
8
|
+
disk).
|
|
9
|
+
|
|
10
|
+
The module extends `AbstractApiModule`, so it inherits standard REST CRUD plus the
|
|
11
|
+
access/hook machinery, and layers on framework install/update/uninstall, schema
|
|
12
|
+
registration and on-disk backups.
|
|
13
|
+
|
|
14
|
+
Source: `lib/ContentPluginModule.js`, `lib/utils/*`.
|
|
15
|
+
|
|
16
|
+
## The model
|
|
17
|
+
|
|
18
|
+
Each installed plugin is one document in the `contentplugins` collection
|
|
19
|
+
(`schemaName = 'contentplugin'`, root `contentplugins`). Schema:
|
|
20
|
+
`schema/contentplugin.schema.json`.
|
|
21
|
+
|
|
22
|
+
| Field | Notes |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `name` | Unique plugin name, e.g. `adapt-contrib-text` (unique index) |
|
|
25
|
+
| `displayName` | User-friendly name (unique index) |
|
|
26
|
+
| `version` | Installed semver |
|
|
27
|
+
| `framework` | Compatible framework version range |
|
|
28
|
+
| `type` | Plugin type, e.g. `component` / `extension` / `menu` / `theme` (indexed) |
|
|
29
|
+
| `targetAttribute` | The content attribute the plugin binds to (required at install — see `CONTENTPLUGIN_ATTR_MISSING`) |
|
|
30
|
+
| `isLocalInstall` | `true` when installed from an uploaded zip rather than the registry |
|
|
31
|
+
| `isEnabled` | Default `true` |
|
|
32
|
+
| `isAddedByDefault` | If `true`, auto-added to every new course's `_enabledPlugins` |
|
|
33
|
+
| `pluginDependencies` | Plugin-to-version map |
|
|
34
|
+
| `canBeUpdated`, `latestCompatibleVersion` | Read-only; populated on demand (see below) |
|
|
35
|
+
|
|
36
|
+
`required`: `framework`, `name`, `type`, `version`, `isLocalInstall`.
|
|
37
|
+
|
|
38
|
+
## Where plugins come from
|
|
39
|
+
|
|
40
|
+
Two sources, distinguished by `isLocalInstall`:
|
|
41
|
+
|
|
42
|
+
- **Registry / source string** — `versionOrPath` is a bare version or name
|
|
43
|
+
(no directory component), resolved by `adapt-cli` from the Adapt plugin
|
|
44
|
+
registry. `processPluginFiles` returns `isLocalInstall: false`.
|
|
45
|
+
- **Local zip upload** — `versionOrPath` is a filesystem path to an unzipped
|
|
46
|
+
upload. `processPluginFiles` reads `package.json` (falling back to
|
|
47
|
+
`bower.json`), copies the files into the persistent `pluginDir`, and marks
|
|
48
|
+
`isLocalInstall: true`. A zip with neither manifest throws
|
|
49
|
+
`CONTENTPLUGIN_INVALID_ZIP`.
|
|
50
|
+
|
|
51
|
+
The actual framework install/uninstall/update is always delegated to `adapt-cli`
|
|
52
|
+
via `this.framework.runCliCommand(...)` (`installPlugins`, `uninstallPlugins`,
|
|
53
|
+
`updatePlugins`, `getPluginUpdateInfos`). `init()` forces
|
|
54
|
+
`ADAPT_ALLOW_PRERELEASE=true` for the CLI if unset.
|
|
55
|
+
|
|
56
|
+
## Storage & versioning
|
|
57
|
+
|
|
58
|
+
- Registry installs live inside the framework copy (managed by the CLI).
|
|
59
|
+
- Local installs are copied into `pluginDir` (config `pluginDir`, default
|
|
60
|
+
`$DATA/contentplugins`).
|
|
61
|
+
- Before a local install overwrites an existing plugin dir, the old one is
|
|
62
|
+
renamed to `<pluginPath>-v<version>` (`backupPluginVersion`). Only the single
|
|
63
|
+
most-recent backup is kept (`cleanupOldPluginBackups`); `getMostRecentBackup`
|
|
64
|
+
sorts `<name>-v*` dirs by semver.
|
|
65
|
+
- DB version is kept in step with the framework copy by `syncPluginData`, which
|
|
66
|
+
is run on init and tapped into the framework's `postInstallHook` /
|
|
67
|
+
`postUpdateHook`. If a plugin recorded in the DB is missing from disk on boot,
|
|
68
|
+
`getMissingPlugins` re-installs it (from registry, or from the most recent
|
|
69
|
+
on-disk backup for local installs).
|
|
70
|
+
|
|
71
|
+
## Schemas
|
|
72
|
+
|
|
73
|
+
Content-plugin schemas (`$patch` extensions to content schemas) are registered
|
|
74
|
+
with the `jsonschema` module by `processPluginSchemas`. Because `jsonschema`
|
|
75
|
+
resets its registry on app-ready and only re-registers schemas owned by
|
|
76
|
+
`app.dependencies`, this module tracks plugin schema paths in `this.pluginSchemas`
|
|
77
|
+
and re-registers them via `jsonschema.registerSchemasHook`. `serveSchema` returns
|
|
78
|
+
the built schema for a plugin via `GET /api/contentplugins/schema?type=<name>`.
|
|
79
|
+
|
|
80
|
+
## The `_enabledPlugins` relationship
|
|
81
|
+
|
|
82
|
+
A plugin is "used" by a course through the course's **config** document:
|
|
83
|
+
`config._enabledPlugins` is an array of plugin `name`s. There is no per-course
|
|
84
|
+
copy of the plugin — courses reference plugins by name.
|
|
85
|
+
|
|
86
|
+
- New courses: `addDefaultPlugins` taps the content module's `preInsertHook`;
|
|
87
|
+
on a `config` insert it appends every plugin with `isAddedByDefault: true`
|
|
88
|
+
to `_enabledPlugins`.
|
|
89
|
+
- `getPluginUses(_id)` aggregates the `content` collection for `config` docs
|
|
90
|
+
whose `_enabledPlugins` contains the plugin name, returning the owning courses
|
|
91
|
+
(title + creator email). It gates deletion and drives the `/uses` endpoint.
|
|
92
|
+
|
|
93
|
+
## Endpoints
|
|
94
|
+
|
|
95
|
+
Routes are declared explicitly (`routes.json`) to omit the default `POST /` and
|
|
96
|
+
`PUT /:_id` — plugins are not created/edited as plain documents. Root:
|
|
97
|
+
`/api/contentplugins`.
|
|
98
|
+
|
|
99
|
+
| Method & route | Handler | Permission |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `GET /` | `requestHandler` | `read:contentplugins` |
|
|
102
|
+
| `GET /:_id` | `requestHandler` | `read:contentplugins` |
|
|
103
|
+
| `PATCH /:_id` | `requestHandler` | `write:contentplugins` |
|
|
104
|
+
| `DELETE /:_id` | `requestHandler` (uninstall) | `write:contentplugins` |
|
|
105
|
+
| `POST /query` | `queryHandler` | `read:contentplugins` |
|
|
106
|
+
| `GET /schema` | `serveSchema` | `read:schema` |
|
|
107
|
+
| `GET /readme` | `readmesHandler` | `read:contentplugins` |
|
|
108
|
+
| `POST /install` | `installHandler` | `install:contentplugins` |
|
|
109
|
+
| `POST /:_id/update` | `updateHandler` | `update:contentplugins` |
|
|
110
|
+
| `GET /:_id/uses` | `usesHandler` | `read:contentplugins` |
|
|
111
|
+
| `GET /:_id/readme` | `readmeHandler` | `read:contentplugins` |
|
|
112
|
+
|
|
113
|
+
### Install
|
|
114
|
+
|
|
115
|
+
`POST /api/contentplugins/install` accepts either JSON (`name`, `version`,
|
|
116
|
+
`force`) or a multipart zip upload (parsed by the `middleware` module's
|
|
117
|
+
`fileUploadParser`; the uploaded file path becomes `versionOrPath`). Install is
|
|
118
|
+
`strict: true` from the HTTP path, so failures throw.
|
|
119
|
+
|
|
120
|
+
```http
|
|
121
|
+
POST /api/contentplugins/install
|
|
122
|
+
Content-Type: application/json
|
|
123
|
+
|
|
124
|
+
{ "name": "adapt-contrib-text", "version": "7.5.1", "force": false }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`installPlugin` flow: resolve/process the source files → if the plugin already
|
|
128
|
+
exists and the new version is `<=` existing and `force` is false, throw
|
|
129
|
+
`CONTENTPLUGIN_ALREADY_EXISTS` → CLI `installPlugins` → `insertOrUpdate` the DB
|
|
130
|
+
record → register its schemas. Missing `targetAttribute` throws
|
|
131
|
+
`CONTENTPLUGIN_ATTR_MISSING`.
|
|
132
|
+
|
|
133
|
+
### Update
|
|
134
|
+
|
|
135
|
+
`GET /?includeUpdateInfo=true` enriches results with `canBeUpdated` and
|
|
136
|
+
`latestCompatibleVersion` (from the CLI `getPluginUpdateInfos`).
|
|
137
|
+
`POST /api/contentplugins/:_id/update` runs the CLI `updatePlugins`, updates the
|
|
138
|
+
DB record and schemas, then — if any courses use the plugin — calls
|
|
139
|
+
`framework.migrateCourses({ fromPlugins, toPlugins, courseIds })` to migrate
|
|
140
|
+
affected course content between the old and new plugin versions.
|
|
141
|
+
|
|
142
|
+
### Uninstall
|
|
143
|
+
|
|
144
|
+
`DELETE /api/contentplugins/:_id`. Blocked with `CONTENTPLUGIN_IN_USE` (listing
|
|
145
|
+
the offending courses) if any course's `_enabledPlugins` references it.
|
|
146
|
+
Otherwise deregisters the plugin's schemas, runs CLI `uninstallPlugins`, then
|
|
147
|
+
deletes the DB record.
|
|
148
|
+
|
|
149
|
+
### READMEs
|
|
150
|
+
|
|
151
|
+
Each plugin ships a `README.md` in its framework source directory
|
|
152
|
+
(`<frameworkDir>/src/{components,extensions,menu,theme}/<name>/README.md`).
|
|
153
|
+
`getReadmes(name?)` reads them via `getPluginReadmes` and returns a
|
|
154
|
+
`{ <pluginName>: <markdown> }` map.
|
|
155
|
+
|
|
156
|
+
- `GET /api/contentplugins/readme` returns the map for every installed plugin
|
|
157
|
+
(plugins with no `README.md` are omitted).
|
|
158
|
+
- `GET /api/contentplugins/:_id/readme` looks the plugin up by `_id` and returns
|
|
159
|
+
`{ name, readme }`, throwing `NOT_FOUND` if the plugin or its README is absent.
|
|
160
|
+
|
|
161
|
+
## Backup & restore
|
|
162
|
+
|
|
163
|
+
Backups are created automatically around local installs (above). Restore is
|
|
164
|
+
available programmatically via `restorePluginFromBackup(pluginName)` — it renames
|
|
165
|
+
the most-recent `<name>-v*` backup back into place and returns its manifest, or
|
|
166
|
+
throws `NOT_FOUND` if no backup exists. There is no HTTP endpoint for restore; it
|
|
167
|
+
is also used implicitly by `getMissingPlugins` on boot to recover a local plugin
|
|
168
|
+
whose directory has gone missing.
|
|
169
|
+
|
|
170
|
+
## Config
|
|
171
|
+
|
|
172
|
+
| Option | Default | Notes |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| `pluginDir` | `$DATA/contentplugins` | Location of locally installed plugins and their version backups |
|
|
175
|
+
|
|
176
|
+
## Errors
|
|
177
|
+
|
|
178
|
+
Defined in `errors/errors.json`. Notable: `CONTENTPLUGIN_ALREADY_EXISTS`,
|
|
179
|
+
`CONTENTPLUGIN_IN_USE`, `CONTENTPLUGIN_INVALID_ZIP`, `CONTENTPLUGIN_ATTR_MISSING`,
|
|
180
|
+
`CONTENTPLUGIN_CLI_INSTALL_FAILED`, `CONTENTPLUGIN_INSTALL_FAILED`,
|
|
181
|
+
`CONTENTPLUGIN_INCOMPAT_FW`. (`CONTENTPLUGIN_ATTR_CLASH`,
|
|
182
|
+
`CONTENTPLUGIN_NEWER_INSTALLED` and `CONTENTPLUGIN_VERSION_MISMATCH` are declared
|
|
183
|
+
but not thrown from this module's code.)
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
getMostRecentBackup,
|
|
10
10
|
cleanupOldPluginBackups,
|
|
11
11
|
restorePluginFromBackup,
|
|
12
|
-
processPluginFiles
|
|
12
|
+
processPluginFiles,
|
|
13
|
+
getPluginReadmes
|
|
13
14
|
} from './utils.js'
|
|
14
15
|
import semver from 'semver'
|
|
15
16
|
/**
|
|
@@ -293,6 +294,15 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
293
294
|
])).toArray()
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Retrieves the README contents of installed content plugins
|
|
299
|
+
* @param {String} [name] Limit the result to a single named plugin
|
|
300
|
+
* @returns {Promise<Object<string,string>>} Map of plugin name to README contents
|
|
301
|
+
*/
|
|
302
|
+
async getReadmes (name) {
|
|
303
|
+
return getPluginReadmes(path.join(this.framework.path, 'src'), name)
|
|
304
|
+
}
|
|
305
|
+
|
|
296
306
|
/**
|
|
297
307
|
* Installs new plugins
|
|
298
308
|
* @param {Array[]} plugins 2D array of strings in the format [pluginName, versionOrPath]
|
|
@@ -433,20 +443,6 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
433
443
|
return p
|
|
434
444
|
}
|
|
435
445
|
|
|
436
|
-
/** @override */
|
|
437
|
-
async serveSchema (req, res, next) {
|
|
438
|
-
try {
|
|
439
|
-
const plugin = await this.get({ name: req.apiData.query.type }) || {}
|
|
440
|
-
const schema = await this.getSchema(plugin.schemaName)
|
|
441
|
-
if (!schema) {
|
|
442
|
-
return next(this.app.errors.NO_SCHEMA_DEF)
|
|
443
|
-
}
|
|
444
|
-
res.type('application/schema+json').json(schema.built)
|
|
445
|
-
} catch (e) {
|
|
446
|
-
return next(e)
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
446
|
/**
|
|
451
447
|
* Express request handler for installing a plugin (also used for updating via zip upload).
|
|
452
448
|
* @param {external:ExpressRequest} req
|
|
@@ -504,6 +500,42 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
504
500
|
return next(error)
|
|
505
501
|
}
|
|
506
502
|
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Express request handler for retrieving the README of every content plugin
|
|
506
|
+
* @param {external:ExpressRequest} req
|
|
507
|
+
* @param {external:ExpressResponse} res
|
|
508
|
+
* @param {Function} next
|
|
509
|
+
*/
|
|
510
|
+
async readmesHandler (req, res, next) {
|
|
511
|
+
try {
|
|
512
|
+
res.send(await this.getReadmes())
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return next(error)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Express request handler for retrieving the README of a single content plugin
|
|
520
|
+
* @param {external:ExpressRequest} req
|
|
521
|
+
* @param {external:ExpressResponse} res
|
|
522
|
+
* @param {Function} next
|
|
523
|
+
*/
|
|
524
|
+
async readmeHandler (req, res, next) {
|
|
525
|
+
try {
|
|
526
|
+
const plugin = await this.findOne({ _id: req.params._id }, { strict: false })
|
|
527
|
+
if (!plugin) {
|
|
528
|
+
throw this.app.errors.NOT_FOUND.setData({ type: this.schemaName, id: req.params._id })
|
|
529
|
+
}
|
|
530
|
+
const readme = (await this.getReadmes(plugin.name))[plugin.name]
|
|
531
|
+
if (readme === undefined) {
|
|
532
|
+
throw this.app.errors.NOT_FOUND.setData({ type: 'README', id: plugin.name })
|
|
533
|
+
}
|
|
534
|
+
res.send({ name: plugin.name, readme })
|
|
535
|
+
} catch (error) {
|
|
536
|
+
return next(error)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
507
539
|
}
|
|
508
540
|
|
|
509
541
|
export default ContentPluginModule
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { globAbsolute } from './globAbsolute.js'
|
|
4
|
+
|
|
5
|
+
const README_PATTERN = '{components,extensions,menu,theme}/*/README.md'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reads the README.md of installed content plugins from the framework src directory
|
|
9
|
+
* @param {String} srcDir The framework's src directory
|
|
10
|
+
* @param {String} [name] Limit the result to a single named plugin
|
|
11
|
+
* @returns {Promise<Object<string,string>>} Map of plugin name to README contents
|
|
12
|
+
*/
|
|
13
|
+
export async function getPluginReadmes (srcDir, name) {
|
|
14
|
+
const pattern = name ? `{components,extensions,menu,theme}/${name}/README.md` : README_PATTERN
|
|
15
|
+
const readmePaths = await globAbsolute(pattern, srcDir)
|
|
16
|
+
const entries = await Promise.all(readmePaths.map(async p => [
|
|
17
|
+
path.basename(path.dirname(p)),
|
|
18
|
+
await fs.readFile(p, 'utf8')
|
|
19
|
+
]))
|
|
20
|
+
return Object.fromEntries(entries)
|
|
21
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { addDefaultPlugins } from './utils/addDefaultPlugins.js'
|
|
2
2
|
export { backupPluginVersion } from './utils/backupPluginVersion.js'
|
|
3
3
|
export { getMostRecentBackup } from './utils/getMostRecentBackup.js'
|
|
4
|
+
export { getPluginReadmes } from './utils/getPluginReadmes.js'
|
|
4
5
|
export { cleanupOldPluginBackups } from './utils/cleanupOldPluginBackups.js'
|
|
5
6
|
export { restorePluginFromBackup } from './utils/restorePluginFromBackup.js'
|
|
6
7
|
export { processPluginFiles } from './utils/processPluginFiles.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-contentplugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Module for managing framework plugins",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-contentplugin",
|
|
6
6
|
"repository": "github:adapt-security/adapt-authoring-contentplugin",
|
package/routes.json
CHANGED
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
"handlers": { "get": "serveSchema" },
|
|
13
13
|
"permissions": { "get": ["read:schema"] }
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
"route": "/readme",
|
|
17
|
+
"handlers": { "get": "readmesHandler" },
|
|
18
|
+
"permissions": { "get": ["read:${scope}"] },
|
|
19
|
+
"meta": {
|
|
20
|
+
"get": {
|
|
21
|
+
"summary": "Return the README of every content plugin, keyed by plugin name"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
15
25
|
{
|
|
16
26
|
"route": "/:_id",
|
|
17
27
|
"handlers": { "get": "requestHandler", "patch": "requestHandler", "delete": "requestHandler" },
|
|
@@ -71,6 +81,17 @@
|
|
|
71
81
|
"parameters": [{ "name": "_id", "in": "path", "description": "Content plugin _id", "required": true }]
|
|
72
82
|
}
|
|
73
83
|
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"route": "/:_id/readme",
|
|
87
|
+
"handlers": { "get": "readmeHandler" },
|
|
88
|
+
"permissions": { "get": ["read:${scope}"] },
|
|
89
|
+
"meta": {
|
|
90
|
+
"get": {
|
|
91
|
+
"summary": "Return the README of a single content plugin",
|
|
92
|
+
"parameters": [{ "name": "_id", "in": "path", "description": "Content plugin _id", "required": true }]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
]
|
|
76
97
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
|
|
7
|
+
import { getPluginReadmes } from '../lib/utils/getPluginReadmes.js'
|
|
8
|
+
|
|
9
|
+
describe('getPluginReadmes()', () => {
|
|
10
|
+
let srcDir
|
|
11
|
+
|
|
12
|
+
const writePlugin = async (category, name, readme) => {
|
|
13
|
+
const dir = path.join(srcDir, category, name)
|
|
14
|
+
await fs.mkdir(dir, { recursive: true })
|
|
15
|
+
if (readme !== undefined) await fs.writeFile(path.join(dir, 'README.md'), readme)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
srcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'readme-test-'))
|
|
20
|
+
await writePlugin('components', 'adapt-contrib-text', '# Text')
|
|
21
|
+
await writePlugin('extensions', 'adapt-contrib-spoor', '# Spoor')
|
|
22
|
+
await writePlugin('menu', 'adapt-contrib-boxMenu', '# Box Menu')
|
|
23
|
+
await writePlugin('theme', 'adapt-contrib-vanilla', '# Vanilla')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await fs.rm(srcDir, { recursive: true, force: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return every plugin README keyed by plugin name', async () => {
|
|
31
|
+
const result = await getPluginReadmes(srcDir)
|
|
32
|
+
assert.deepEqual(result, {
|
|
33
|
+
'adapt-contrib-text': '# Text',
|
|
34
|
+
'adapt-contrib-spoor': '# Spoor',
|
|
35
|
+
'adapt-contrib-boxMenu': '# Box Menu',
|
|
36
|
+
'adapt-contrib-vanilla': '# Vanilla'
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should limit the result to a single named plugin', async () => {
|
|
41
|
+
const result = await getPluginReadmes(srcDir, 'adapt-contrib-spoor')
|
|
42
|
+
assert.deepEqual(result, { 'adapt-contrib-spoor': '# Spoor' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should return an empty object for an unknown plugin', async () => {
|
|
46
|
+
assert.deepEqual(await getPluginReadmes(srcDir, 'does-not-exist'), {})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should ignore plugins without a README and non-plugin directories', async () => {
|
|
50
|
+
await writePlugin('components', 'adapt-no-readme')
|
|
51
|
+
await fs.mkdir(path.join(srcDir, 'core'), { recursive: true })
|
|
52
|
+
await fs.writeFile(path.join(srcDir, 'core', 'README.md'), '# Core')
|
|
53
|
+
|
|
54
|
+
const result = await getPluginReadmes(srcDir)
|
|
55
|
+
assert.ok(!('adapt-no-readme' in result))
|
|
56
|
+
assert.ok(!('core' in result))
|
|
57
|
+
})
|
|
58
|
+
})
|