adapt-authoring-contentplugin 1.2.3 → 1.3.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/lib/ContentPluginModule.js +21 -133
- package/lib/utils/backupPluginVersion.js +43 -0
- package/lib/utils/cleanupOldPluginBackups.js +28 -0
- package/lib/utils/getMostRecentBackup.js +37 -0
- package/lib/utils/processPluginFiles.js +53 -0
- package/lib/utils/restorePluginFromBackup.js +45 -0
- package/lib/utils.js +5 -0
- package/package.json +5 -5
- package/tests/ContentPluginUtils.spec.js +513 -0
|
@@ -3,6 +3,14 @@ import apidefs from './apidefs.js'
|
|
|
3
3
|
import fs from 'fs/promises'
|
|
4
4
|
import { glob } from 'glob'
|
|
5
5
|
import path from 'path'
|
|
6
|
+
import { readJson } from 'adapt-authoring-core'
|
|
7
|
+
import {
|
|
8
|
+
backupPluginVersion,
|
|
9
|
+
getMostRecentBackup,
|
|
10
|
+
cleanupOldPluginBackups,
|
|
11
|
+
restorePluginFromBackup,
|
|
12
|
+
processPluginFiles
|
|
13
|
+
} from './utils.js'
|
|
6
14
|
import semver from 'semver'
|
|
7
15
|
/**
|
|
8
16
|
* Abstract module which handles framework plugins
|
|
@@ -107,7 +115,7 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
async readJson (filepath) {
|
|
110
|
-
return
|
|
118
|
+
return readJson(filepath)
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
/**
|
|
@@ -360,30 +368,7 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
360
368
|
* @returns {Promise<String|null>} Path to the backup directory, or null if no backup was created
|
|
361
369
|
*/
|
|
362
370
|
async backupPluginVersion (pluginPath, pluginName) {
|
|
363
|
-
|
|
364
|
-
await fs.access(pluginPath)
|
|
365
|
-
} catch (e) { // No plugin, no backup needed
|
|
366
|
-
return null
|
|
367
|
-
}
|
|
368
|
-
let existingVersion
|
|
369
|
-
try {
|
|
370
|
-
const pkgPath = path.join(pluginPath, 'package.json')
|
|
371
|
-
const pkg = await this.readJson(pkgPath)
|
|
372
|
-
existingVersion = pkg.version
|
|
373
|
-
} catch (e) {
|
|
374
|
-
try {
|
|
375
|
-
const bowerPath = path.join(pluginPath, 'bower.json')
|
|
376
|
-
const bower = await this.readJson(bowerPath)
|
|
377
|
-
existingVersion = bower.version
|
|
378
|
-
} catch (e2) {
|
|
379
|
-
this.log('warn', `Could not read version for backup of ${pluginName}`)
|
|
380
|
-
existingVersion = `unknown-${Date.now()}`
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
const backupDir = `${pluginPath}-v${existingVersion}`
|
|
384
|
-
await fs.rename(pluginPath, backupDir)
|
|
385
|
-
this.log('info', `Backed up ${pluginName}@${existingVersion}`)
|
|
386
|
-
return backupDir
|
|
371
|
+
return backupPluginVersion(pluginPath, pluginName, this.log.bind(this))
|
|
387
372
|
}
|
|
388
373
|
|
|
389
374
|
/**
|
|
@@ -393,54 +378,11 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
393
378
|
* @returns {Promise<String|null>} Path to the most recent backup, or null if none found
|
|
394
379
|
*/
|
|
395
380
|
async getMostRecentBackup (pluginDir, pluginName) {
|
|
396
|
-
|
|
397
|
-
const backups = await glob(pattern, { cwd: pluginDir, absolute: true })
|
|
398
|
-
|
|
399
|
-
if (backups.length === 0) {
|
|
400
|
-
return null
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Sort by version (newest first)
|
|
404
|
-
// Extract version from backup path (format: pluginName-vX.Y.Z)
|
|
405
|
-
backups.sort((a, b) => {
|
|
406
|
-
const versionA = path.basename(a).replace(`${pluginName}-v`, '')
|
|
407
|
-
const versionB = path.basename(b).replace(`${pluginName}-v`, '')
|
|
408
|
-
|
|
409
|
-
// Try to compare as semver versions
|
|
410
|
-
if (semver.valid(versionA) && semver.valid(versionB)) {
|
|
411
|
-
return semver.rcompare(versionA, versionB) // descending order
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Fall back to alphabetical comparison for non-semver versions (e.g., unknown-timestamp)
|
|
415
|
-
return b.localeCompare(a)
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
return backups[0]
|
|
381
|
+
return getMostRecentBackup(pluginDir, pluginName)
|
|
419
382
|
}
|
|
420
383
|
|
|
421
|
-
/**
|
|
422
|
-
* Cleans up old plugin version backups, keeping only the most recent one
|
|
423
|
-
* @param {String} pluginDir Base directory containing plugins
|
|
424
|
-
* @param {String} pluginName Name of the plugin
|
|
425
|
-
* @returns {Promise<void>}
|
|
426
|
-
*/
|
|
427
384
|
async cleanupOldPluginBackups (pluginDir, pluginName) {
|
|
428
|
-
|
|
429
|
-
const backups = await glob(pattern, { cwd: pluginDir, absolute: true })
|
|
430
|
-
|
|
431
|
-
if (backups.length <= 1) {
|
|
432
|
-
return
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Get the most recent backup using the helper
|
|
436
|
-
const mostRecent = await this.getMostRecentBackup(pluginDir, pluginName)
|
|
437
|
-
|
|
438
|
-
// Remove all backups except the most recent
|
|
439
|
-
const backupsToRemove = backups.filter(backup => backup !== mostRecent)
|
|
440
|
-
for (const backup of backupsToRemove) {
|
|
441
|
-
await fs.rm(backup, { recursive: true })
|
|
442
|
-
this.log('info', `Removed old backup: ${backup}`)
|
|
443
|
-
}
|
|
385
|
+
return cleanupOldPluginBackups(pluginDir, pluginName, this.log.bind(this))
|
|
444
386
|
}
|
|
445
387
|
|
|
446
388
|
/**
|
|
@@ -450,77 +392,23 @@ class ContentPluginModule extends AbstractApiModule {
|
|
|
450
392
|
*/
|
|
451
393
|
async restorePluginFromBackup (pluginName) {
|
|
452
394
|
const pluginDir = this.getConfig('pluginDir')
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (!mostRecentBackup) {
|
|
395
|
+
const result = await restorePluginFromBackup(pluginDir, pluginName, this.log.bind(this))
|
|
396
|
+
if (!result) {
|
|
457
397
|
throw this.app.errors.NOT_FOUND
|
|
458
398
|
.setData({ type: 'backup', id: pluginName })
|
|
459
399
|
}
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
await fs.access(pluginPath)
|
|
463
|
-
await fs.rm(pluginPath, { recursive: true })
|
|
464
|
-
} catch (e) {
|
|
465
|
-
// Current version doesn't exist, that's fine
|
|
466
|
-
}
|
|
467
|
-
// Restore the backup
|
|
468
|
-
await fs.rename(mostRecentBackup, pluginPath)
|
|
469
|
-
this.log('info', `Restored ${pluginName} from backup`)
|
|
470
|
-
let pkg
|
|
471
|
-
try {
|
|
472
|
-
pkg = await this.readJson(path.join(pluginPath, 'package.json'))
|
|
473
|
-
} catch (e) {
|
|
474
|
-
try {
|
|
475
|
-
pkg = await this.readJson(path.join(pluginPath, 'bower.json'))
|
|
476
|
-
} catch (e2) {
|
|
477
|
-
throw this.app.errors.CONTENTPLUGIN_INVALID_ZIP
|
|
478
|
-
.setData({ pluginName, message: 'Could not read package.json or bower.json from backup' })
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return pkg
|
|
400
|
+
return result
|
|
482
401
|
}
|
|
483
402
|
|
|
484
|
-
/**
|
|
485
|
-
* Ensures local plugin source files are stored in the correct location and structured in an expected way
|
|
486
|
-
* @param {Object} pluginData Plugin metadata
|
|
487
|
-
* @param {String} sourcePath The path to the plugin source files
|
|
488
|
-
* @returns Resolves with package data
|
|
489
|
-
*/
|
|
490
403
|
async processPluginFiles (pluginData) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return { name: pluginData.name, version: sourcePath, isLocalInstall: false }
|
|
494
|
-
}
|
|
495
|
-
const contents = await fs.readdir(sourcePath)
|
|
496
|
-
if (contents.length === 1) { // deal with a nested root folder
|
|
497
|
-
sourcePath = path.join(pluginData.sourcePath, contents[0])
|
|
498
|
-
}
|
|
499
|
-
let pkg
|
|
500
|
-
try { // load package data, with fall-back to bower
|
|
501
|
-
try {
|
|
502
|
-
pkg = await this.readJson(path.join(sourcePath, 'package.json'))
|
|
503
|
-
} catch (e) {
|
|
504
|
-
pkg = await this.readJson(path.join(sourcePath, 'bower.json'))
|
|
505
|
-
}
|
|
506
|
-
pkg.sourcePath = path.join(this.getConfig('pluginDir'), pkg.name)
|
|
507
|
-
pkg.isLocalInstall = true
|
|
404
|
+
try {
|
|
405
|
+
return await processPluginFiles(pluginData, this.getConfig('pluginDir'), this.log.bind(this))
|
|
508
406
|
} catch (e) {
|
|
509
|
-
|
|
407
|
+
if (e.message?.startsWith('Invalid plugin zip')) {
|
|
408
|
+
throw this.app.errors.CONTENTPLUGIN_INVALID_ZIP
|
|
409
|
+
}
|
|
410
|
+
throw e
|
|
510
411
|
}
|
|
511
|
-
|
|
512
|
-
const pluginDir = this.getConfig('pluginDir')
|
|
513
|
-
|
|
514
|
-
// Back up the existing version if it exists
|
|
515
|
-
await this.backupPluginVersion(pkg.sourcePath, pkg.name)
|
|
516
|
-
|
|
517
|
-
// Clean up old backups (keep only 1 previous version)
|
|
518
|
-
await this.cleanupOldPluginBackups(pluginDir, pkg.name)
|
|
519
|
-
|
|
520
|
-
// move the files into the persistent location
|
|
521
|
-
await fs.cp(sourcePath, pkg.sourcePath, { recursive: true })
|
|
522
|
-
await fs.rm(sourcePath, { recursive: true })
|
|
523
|
-
return pkg
|
|
524
412
|
}
|
|
525
413
|
|
|
526
414
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { readJson } from 'adapt-authoring-core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a versioned backup of an existing plugin directory.
|
|
7
|
+
*
|
|
8
|
+
* Reads the plugin version from package.json (or bower.json as fallback),
|
|
9
|
+
* removes any stale backup at the target path, then renames the plugin
|
|
10
|
+
* directory to `<pluginPath>-v<version>`.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} pluginPath - Absolute path to the plugin directory
|
|
13
|
+
* @param {string} pluginName - Human-readable plugin name (for logging)
|
|
14
|
+
* @param {Function} [log] - Optional logging callback `(level, msg) => void`
|
|
15
|
+
* @returns {Promise<string|null>} Path to the backup directory, or null if no backup was needed
|
|
16
|
+
*/
|
|
17
|
+
export async function backupPluginVersion (pluginPath, pluginName, log) {
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(pluginPath)
|
|
20
|
+
} catch (e) { // No plugin, no backup needed
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
let existingVersion
|
|
24
|
+
try {
|
|
25
|
+
const pkg = await readJson(path.join(pluginPath, 'package.json'))
|
|
26
|
+
existingVersion = pkg.version
|
|
27
|
+
} catch (e) {
|
|
28
|
+
try {
|
|
29
|
+
const bower = await readJson(path.join(pluginPath, 'bower.json'))
|
|
30
|
+
existingVersion = bower.version
|
|
31
|
+
} catch (e2) {
|
|
32
|
+
if (log) log('warn', `Could not read version for backup of ${pluginName}`)
|
|
33
|
+
existingVersion = `unknown-${Date.now()}`
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const backupDir = `${pluginPath}-v${existingVersion}`
|
|
37
|
+
try {
|
|
38
|
+
await fs.rm(backupDir, { recursive: true, force: true })
|
|
39
|
+
} catch (e) {} // ignore if doesn't exist
|
|
40
|
+
await fs.rename(pluginPath, backupDir)
|
|
41
|
+
if (log) log('info', `Backed up ${pluginName}@${existingVersion}`)
|
|
42
|
+
return backupDir
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import { glob } from 'glob'
|
|
3
|
+
import { getMostRecentBackup } from './getMostRecentBackup.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cleans up old plugin version backups, keeping only the most recent one.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} pluginDir - Base directory containing plugins
|
|
9
|
+
* @param {string} pluginName - Name of the plugin
|
|
10
|
+
* @param {Function} [log] - Optional logging callback `(level, msg) => void`
|
|
11
|
+
* @returns {Promise<void>}
|
|
12
|
+
*/
|
|
13
|
+
export async function cleanupOldPluginBackups (pluginDir, pluginName, log) {
|
|
14
|
+
const pattern = `${pluginName}-v*`
|
|
15
|
+
const backups = await glob(pattern, { cwd: pluginDir, absolute: true })
|
|
16
|
+
|
|
17
|
+
if (backups.length <= 1) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mostRecent = await getMostRecentBackup(pluginDir, pluginName)
|
|
22
|
+
|
|
23
|
+
const backupsToRemove = backups.filter(backup => backup !== mostRecent)
|
|
24
|
+
for (const backup of backupsToRemove) {
|
|
25
|
+
await fs.rm(backup, { recursive: true })
|
|
26
|
+
if (log) log('info', `Removed old backup: ${backup}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { glob } from 'glob'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import semver from 'semver'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the most recent backup for a plugin based on version sorting.
|
|
7
|
+
*
|
|
8
|
+
* Scans for directories matching `<pluginName>-v*` in the given directory,
|
|
9
|
+
* sorts by semver (falling back to alphabetical for non-semver), and
|
|
10
|
+
* returns the most recent.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} pluginDir - Base directory containing plugins
|
|
13
|
+
* @param {string} pluginName - Name of the plugin
|
|
14
|
+
* @returns {Promise<string|null>} Absolute path to the most recent backup, or null if none found
|
|
15
|
+
*/
|
|
16
|
+
export async function getMostRecentBackup (pluginDir, pluginName) {
|
|
17
|
+
const pattern = `${pluginName}-v*`
|
|
18
|
+
const backups = await glob(pattern, { cwd: pluginDir, absolute: true })
|
|
19
|
+
|
|
20
|
+
if (backups.length === 0) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Sort by version (newest first)
|
|
25
|
+
backups.sort((a, b) => {
|
|
26
|
+
const versionA = path.basename(a).replace(`${pluginName}-v`, '')
|
|
27
|
+
const versionB = path.basename(b).replace(`${pluginName}-v`, '')
|
|
28
|
+
|
|
29
|
+
if (semver.valid(versionA) && semver.valid(versionB)) {
|
|
30
|
+
return semver.rcompare(versionA, versionB)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return b.localeCompare(a)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return backups[0]
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { readJson } from 'adapt-authoring-core'
|
|
4
|
+
import { backupPluginVersion } from './backupPluginVersion.js'
|
|
5
|
+
import { cleanupOldPluginBackups } from './cleanupOldPluginBackups.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Processes local plugin source files for installation.
|
|
9
|
+
*
|
|
10
|
+
* If `sourcePath` is just a filename (no directory component), returns a
|
|
11
|
+
* non-local install descriptor. Otherwise reads the package metadata from
|
|
12
|
+
* the source, backs up any existing version, cleans old backups, and copies
|
|
13
|
+
* the source files into their persistent location under `pluginDir`.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} pluginData - Plugin metadata (must include `name` and `sourcePath`)
|
|
16
|
+
* @param {string} pluginDir - Persistent plugin storage directory
|
|
17
|
+
* @param {Function} [log] - Optional logging callback `(level, msg) => void`
|
|
18
|
+
* @returns {Promise<Object>} Package metadata with `sourcePath` and `isLocalInstall` fields
|
|
19
|
+
* @throws {Error} If the source contains no valid package.json or bower.json
|
|
20
|
+
*/
|
|
21
|
+
export async function processPluginFiles (pluginData, pluginDir, log) {
|
|
22
|
+
let sourcePath = pluginData.sourcePath
|
|
23
|
+
if (sourcePath === path.basename(sourcePath)) { // no local files
|
|
24
|
+
return { name: pluginData.name, version: sourcePath, isLocalInstall: false }
|
|
25
|
+
}
|
|
26
|
+
const contents = await fs.readdir(sourcePath)
|
|
27
|
+
if (contents.length === 1) { // deal with a nested root folder
|
|
28
|
+
sourcePath = path.join(pluginData.sourcePath, contents[0])
|
|
29
|
+
}
|
|
30
|
+
let pkg
|
|
31
|
+
try {
|
|
32
|
+
try {
|
|
33
|
+
pkg = await readJson(path.join(sourcePath, 'package.json'))
|
|
34
|
+
} catch (e) {
|
|
35
|
+
pkg = await readJson(path.join(sourcePath, 'bower.json'))
|
|
36
|
+
}
|
|
37
|
+
pkg.sourcePath = path.join(pluginDir, pkg.name)
|
|
38
|
+
pkg.isLocalInstall = true
|
|
39
|
+
} catch (e) {
|
|
40
|
+
throw new Error(`Invalid plugin zip: no package.json or bower.json found in ${sourcePath}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Back up the existing version if it exists
|
|
44
|
+
await backupPluginVersion(pkg.sourcePath, pkg.name, log)
|
|
45
|
+
|
|
46
|
+
// Clean up old backups (keep only 1 previous version)
|
|
47
|
+
await cleanupOldPluginBackups(pluginDir, pkg.name, log)
|
|
48
|
+
|
|
49
|
+
// move the files into the persistent location
|
|
50
|
+
await fs.cp(sourcePath, pkg.sourcePath, { recursive: true })
|
|
51
|
+
await fs.rm(sourcePath, { recursive: true })
|
|
52
|
+
return pkg
|
|
53
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { getMostRecentBackup } from './getMostRecentBackup.js'
|
|
4
|
+
import { readJson } from 'adapt-authoring-core'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Restores a plugin from its most recent versioned backup.
|
|
8
|
+
*
|
|
9
|
+
* Finds the newest backup directory, removes the current plugin directory
|
|
10
|
+
* (if present), renames the backup into place, and reads the package metadata.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} pluginDir - Base directory containing plugins
|
|
13
|
+
* @param {string} pluginName - Name of the plugin to restore
|
|
14
|
+
* @param {Function} [log] - Optional logging callback `(level, msg) => void`
|
|
15
|
+
* @returns {Promise<Object|null>} Package metadata from the restored backup,
|
|
16
|
+
* or null if no backup was found
|
|
17
|
+
* @throws {Error} If the restored backup contains no package.json or bower.json
|
|
18
|
+
*/
|
|
19
|
+
export async function restorePluginFromBackup (pluginDir, pluginName, log) {
|
|
20
|
+
const pluginPath = path.join(pluginDir, pluginName)
|
|
21
|
+
const mostRecentBackup = await getMostRecentBackup(pluginDir, pluginName)
|
|
22
|
+
|
|
23
|
+
if (!mostRecentBackup) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
// Remove current version if it exists
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(pluginPath)
|
|
29
|
+
await fs.rm(pluginPath, { recursive: true })
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Current version doesn't exist, that's fine
|
|
32
|
+
}
|
|
33
|
+
// Restore the backup
|
|
34
|
+
await fs.rename(mostRecentBackup, pluginPath)
|
|
35
|
+
if (log) log('info', `Restored ${pluginName} from backup`)
|
|
36
|
+
try {
|
|
37
|
+
return await readJson(path.join(pluginPath, 'package.json'))
|
|
38
|
+
} catch (e) {
|
|
39
|
+
try {
|
|
40
|
+
return await readJson(path.join(pluginPath, 'bower.json'))
|
|
41
|
+
} catch (e2) {
|
|
42
|
+
throw new Error(`Could not read package.json or bower.json from backup of ${pluginName}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { backupPluginVersion } from './utils/backupPluginVersion.js'
|
|
2
|
+
export { getMostRecentBackup } from './utils/getMostRecentBackup.js'
|
|
3
|
+
export { cleanupOldPluginBackups } from './utils/cleanupOldPluginBackups.js'
|
|
4
|
+
export { restorePluginFromBackup } from './utils/restorePluginFromBackup.js'
|
|
5
|
+
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.3.0",
|
|
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",
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"adapt-authoring-core": "^2.0.0",
|
|
13
14
|
"adapt-cli": "^3.3.3",
|
|
14
15
|
"glob": "^13.0.0",
|
|
15
16
|
"semver": "^7.6.0"
|
|
16
17
|
},
|
|
17
18
|
"peerDependencies": {
|
|
18
|
-
"adapt-authoring-adaptframework": "^
|
|
19
|
-
"adapt-authoring-content": "^
|
|
20
|
-
"adapt-authoring-core": "^1.7.0",
|
|
19
|
+
"adapt-authoring-adaptframework": "^2.0.0",
|
|
20
|
+
"adapt-authoring-content": "^2.0.0",
|
|
21
21
|
"adapt-authoring-jsonschema": "^1.2.0",
|
|
22
22
|
"adapt-authoring-middleware": "^1.0.2",
|
|
23
|
-
"adapt-authoring-mongodb": "^
|
|
23
|
+
"adapt-authoring-mongodb": "^2.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
import { readJson } from 'adapt-authoring-core'
|
|
8
|
+
import {
|
|
9
|
+
backupPluginVersion,
|
|
10
|
+
getMostRecentBackup,
|
|
11
|
+
cleanupOldPluginBackups,
|
|
12
|
+
restorePluginFromBackup,
|
|
13
|
+
processPluginFiles
|
|
14
|
+
} from '../lib/utils.js'
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// readJson
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
describe('readJson()', () => {
|
|
20
|
+
let tmpDir
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cpu-test-'))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should parse a valid JSON file', async () => {
|
|
31
|
+
const filePath = path.join(tmpDir, 'data.json')
|
|
32
|
+
await fs.writeFile(filePath, JSON.stringify({ name: 'test', version: '1.0.0' }))
|
|
33
|
+
const result = await readJson(filePath)
|
|
34
|
+
assert.deepEqual(result, { name: 'test', version: '1.0.0' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should throw on invalid JSON', async () => {
|
|
38
|
+
const filePath = path.join(tmpDir, 'bad.json')
|
|
39
|
+
await fs.writeFile(filePath, '{ not valid }')
|
|
40
|
+
await assert.rejects(() => readJson(filePath), SyntaxError)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should throw ENOENT when file does not exist', async () => {
|
|
44
|
+
await assert.rejects(
|
|
45
|
+
() => readJson(path.join(tmpDir, 'nope.json')),
|
|
46
|
+
(err) => err.code === 'ENOENT'
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// backupPluginVersion
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
describe('backupPluginVersion()', () => {
|
|
55
|
+
let tmpDir
|
|
56
|
+
let log
|
|
57
|
+
|
|
58
|
+
beforeEach(async () => {
|
|
59
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cpu-test-'))
|
|
60
|
+
log = mock.fn()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Helper: create a fake plugin directory with a package.json.
|
|
69
|
+
*/
|
|
70
|
+
async function createPlugin (name, version, { useBower = false } = {}) {
|
|
71
|
+
const pluginPath = path.join(tmpDir, name)
|
|
72
|
+
await fs.mkdir(pluginPath, { recursive: true })
|
|
73
|
+
const filename = useBower ? 'bower.json' : 'package.json'
|
|
74
|
+
await fs.writeFile(
|
|
75
|
+
path.join(pluginPath, filename),
|
|
76
|
+
JSON.stringify({ name, version })
|
|
77
|
+
)
|
|
78
|
+
return pluginPath
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
it('should return null when the plugin directory does not exist', async () => {
|
|
82
|
+
const result = await backupPluginVersion(
|
|
83
|
+
path.join(tmpDir, 'nonexistent'),
|
|
84
|
+
'nonexistent',
|
|
85
|
+
log
|
|
86
|
+
)
|
|
87
|
+
assert.equal(result, null)
|
|
88
|
+
assert.equal(log.mock.callCount(), 0)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should rename the plugin directory to a versioned backup', async () => {
|
|
92
|
+
const pluginPath = await createPlugin('adapt-hotgrid', '4.3.5')
|
|
93
|
+
|
|
94
|
+
const result = await backupPluginVersion(pluginPath, 'adapt-hotgrid', log)
|
|
95
|
+
|
|
96
|
+
const expectedBackup = `${pluginPath}-v4.3.5`
|
|
97
|
+
assert.equal(result, expectedBackup)
|
|
98
|
+
|
|
99
|
+
// Original should be gone, backup should exist
|
|
100
|
+
await assert.rejects(() => fs.access(pluginPath), { code: 'ENOENT' })
|
|
101
|
+
await fs.access(expectedBackup) // should not throw
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should read version from bower.json when package.json is absent', async () => {
|
|
105
|
+
const pluginPath = await createPlugin('adapt-vanilla', '2.1.0', { useBower: true })
|
|
106
|
+
|
|
107
|
+
const result = await backupPluginVersion(pluginPath, 'adapt-vanilla', log)
|
|
108
|
+
|
|
109
|
+
assert.equal(result, `${pluginPath}-v2.1.0`)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should use unknown-<timestamp> when neither package.json nor bower.json exist', async () => {
|
|
113
|
+
const pluginPath = path.join(tmpDir, 'adapt-empty')
|
|
114
|
+
await fs.mkdir(pluginPath)
|
|
115
|
+
|
|
116
|
+
const result = await backupPluginVersion(pluginPath, 'adapt-empty', log)
|
|
117
|
+
|
|
118
|
+
assert.ok(result.startsWith(`${pluginPath}-vunknown-`))
|
|
119
|
+
// Should have logged a warning
|
|
120
|
+
const warns = log.mock.calls.filter(c => c.arguments[0] === 'warn')
|
|
121
|
+
assert.equal(warns.length, 1)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should succeed when a backup directory already exists (bug #2 regression)', async () => {
|
|
125
|
+
const pluginPath = await createPlugin('adapt-hotgrid', '4.3.5')
|
|
126
|
+
const backupDir = `${pluginPath}-v4.3.5`
|
|
127
|
+
|
|
128
|
+
// Pre-create a stale backup with content (simulates previous test run)
|
|
129
|
+
await fs.mkdir(backupDir, { recursive: true })
|
|
130
|
+
await fs.writeFile(path.join(backupDir, 'stale-file.txt'), 'old data')
|
|
131
|
+
|
|
132
|
+
// This would fail with ENOTEMPTY before the fix
|
|
133
|
+
const result = await backupPluginVersion(pluginPath, 'adapt-hotgrid', log)
|
|
134
|
+
|
|
135
|
+
assert.equal(result, backupDir)
|
|
136
|
+
// Original should be gone
|
|
137
|
+
await assert.rejects(() => fs.access(pluginPath), { code: 'ENOENT' })
|
|
138
|
+
// Backup should exist and contain the new package.json, not the stale file
|
|
139
|
+
const pkg = JSON.parse(await fs.readFile(path.join(backupDir, 'package.json'), 'utf8'))
|
|
140
|
+
assert.equal(pkg.version, '4.3.5')
|
|
141
|
+
await assert.rejects(
|
|
142
|
+
() => fs.access(path.join(backupDir, 'stale-file.txt')),
|
|
143
|
+
{ code: 'ENOENT' }
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should log an info message on success', async () => {
|
|
148
|
+
const pluginPath = await createPlugin('adapt-text', '1.0.0')
|
|
149
|
+
|
|
150
|
+
await backupPluginVersion(pluginPath, 'adapt-text', log)
|
|
151
|
+
|
|
152
|
+
const infoCalls = log.mock.calls.filter(c => c.arguments[0] === 'info')
|
|
153
|
+
assert.equal(infoCalls.length, 1)
|
|
154
|
+
assert.ok(infoCalls[0].arguments[1].includes('adapt-text'))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should work without a log callback', async () => {
|
|
158
|
+
const pluginPath = await createPlugin('adapt-nolog', '1.0.0')
|
|
159
|
+
|
|
160
|
+
// Should not throw even though log is undefined
|
|
161
|
+
const result = await backupPluginVersion(pluginPath, 'adapt-nolog')
|
|
162
|
+
|
|
163
|
+
assert.equal(result, `${pluginPath}-v1.0.0`)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// getMostRecentBackup
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
describe('getMostRecentBackup()', () => {
|
|
171
|
+
let tmpDir
|
|
172
|
+
|
|
173
|
+
beforeEach(async () => {
|
|
174
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cpu-test-'))
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
afterEach(async () => {
|
|
178
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should return null when no backups exist', async () => {
|
|
182
|
+
const result = await getMostRecentBackup(tmpDir, 'adapt-hotgrid')
|
|
183
|
+
assert.equal(result, null)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should return the only backup when there is one', async () => {
|
|
187
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
188
|
+
|
|
189
|
+
const result = await getMostRecentBackup(tmpDir, 'adapt-hotgrid')
|
|
190
|
+
|
|
191
|
+
assert.equal(result, path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should return the highest semver version', async () => {
|
|
195
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
196
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v2.3.1'))
|
|
197
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v1.9.9'))
|
|
198
|
+
|
|
199
|
+
const result = await getMostRecentBackup(tmpDir, 'adapt-hotgrid')
|
|
200
|
+
|
|
201
|
+
assert.equal(result, path.join(tmpDir, 'adapt-hotgrid-v2.3.1'))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should handle non-semver versions with alphabetical fallback', async () => {
|
|
205
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-vunknown-100'))
|
|
206
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-vunknown-200'))
|
|
207
|
+
|
|
208
|
+
const result = await getMostRecentBackup(tmpDir, 'adapt-hotgrid')
|
|
209
|
+
|
|
210
|
+
assert.equal(result, path.join(tmpDir, 'adapt-hotgrid-vunknown-200'))
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should not match backups for other plugins', async () => {
|
|
214
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-text-v1.0.0'))
|
|
215
|
+
|
|
216
|
+
const result = await getMostRecentBackup(tmpDir, 'adapt-hotgrid')
|
|
217
|
+
|
|
218
|
+
assert.equal(result, null)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// cleanupOldPluginBackups
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
describe('cleanupOldPluginBackups()', () => {
|
|
226
|
+
let tmpDir
|
|
227
|
+
let log
|
|
228
|
+
|
|
229
|
+
beforeEach(async () => {
|
|
230
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cpu-test-'))
|
|
231
|
+
log = mock.fn()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
afterEach(async () => {
|
|
235
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should do nothing when no backups exist', async () => {
|
|
239
|
+
await cleanupOldPluginBackups(tmpDir, 'adapt-hotgrid', log)
|
|
240
|
+
assert.equal(log.mock.callCount(), 0)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should do nothing when only one backup exists', async () => {
|
|
244
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
245
|
+
|
|
246
|
+
await cleanupOldPluginBackups(tmpDir, 'adapt-hotgrid', log)
|
|
247
|
+
|
|
248
|
+
// Backup should still exist
|
|
249
|
+
await fs.access(path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
250
|
+
assert.equal(log.mock.callCount(), 0)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should remove all but the most recent backup', async () => {
|
|
254
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
255
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v2.0.0'))
|
|
256
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v3.0.0'))
|
|
257
|
+
|
|
258
|
+
await cleanupOldPluginBackups(tmpDir, 'adapt-hotgrid', log)
|
|
259
|
+
|
|
260
|
+
// v3.0.0 should survive, the others should be gone
|
|
261
|
+
await fs.access(path.join(tmpDir, 'adapt-hotgrid-v3.0.0'))
|
|
262
|
+
await assert.rejects(() => fs.access(path.join(tmpDir, 'adapt-hotgrid-v1.0.0')), { code: 'ENOENT' })
|
|
263
|
+
await assert.rejects(() => fs.access(path.join(tmpDir, 'adapt-hotgrid-v2.0.0')), { code: 'ENOENT' })
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should log info for each removed backup', async () => {
|
|
267
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
268
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v2.0.0'))
|
|
269
|
+
|
|
270
|
+
await cleanupOldPluginBackups(tmpDir, 'adapt-hotgrid', log)
|
|
271
|
+
|
|
272
|
+
const infoCalls = log.mock.calls.filter(c => c.arguments[0] === 'info')
|
|
273
|
+
assert.equal(infoCalls.length, 1)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should not affect backups from other plugins', async () => {
|
|
277
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v1.0.0'))
|
|
278
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-hotgrid-v2.0.0'))
|
|
279
|
+
await fs.mkdir(path.join(tmpDir, 'adapt-text-v1.0.0'))
|
|
280
|
+
|
|
281
|
+
await cleanupOldPluginBackups(tmpDir, 'adapt-hotgrid', log)
|
|
282
|
+
|
|
283
|
+
// adapt-text backup should be untouched
|
|
284
|
+
await fs.access(path.join(tmpDir, 'adapt-text-v1.0.0'))
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// restorePluginFromBackup
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
describe('restorePluginFromBackup()', () => {
|
|
292
|
+
let tmpDir
|
|
293
|
+
let log
|
|
294
|
+
|
|
295
|
+
beforeEach(async () => {
|
|
296
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cpu-test-'))
|
|
297
|
+
log = mock.fn()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
afterEach(async () => {
|
|
301
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('should return null when no backup exists', async () => {
|
|
305
|
+
const result = await restorePluginFromBackup(tmpDir, 'adapt-hotgrid', log)
|
|
306
|
+
assert.equal(result, null)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('should restore the most recent backup into the plugin directory', async () => {
|
|
310
|
+
const backupDir = path.join(tmpDir, 'adapt-hotgrid-v2.0.0')
|
|
311
|
+
await fs.mkdir(backupDir)
|
|
312
|
+
await fs.writeFile(path.join(backupDir, 'package.json'), JSON.stringify({ name: 'adapt-hotgrid', version: '2.0.0' }))
|
|
313
|
+
|
|
314
|
+
const result = await restorePluginFromBackup(tmpDir, 'adapt-hotgrid', log)
|
|
315
|
+
|
|
316
|
+
assert.equal(result.name, 'adapt-hotgrid')
|
|
317
|
+
assert.equal(result.version, '2.0.0')
|
|
318
|
+
// Plugin dir should now exist, backup dir should be gone
|
|
319
|
+
await fs.access(path.join(tmpDir, 'adapt-hotgrid'))
|
|
320
|
+
await assert.rejects(() => fs.access(backupDir), { code: 'ENOENT' })
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('should remove the current plugin directory before restoring', async () => {
|
|
324
|
+
// Create current version
|
|
325
|
+
const pluginPath = path.join(tmpDir, 'adapt-hotgrid')
|
|
326
|
+
await fs.mkdir(pluginPath)
|
|
327
|
+
await fs.writeFile(path.join(pluginPath, 'package.json'), JSON.stringify({ name: 'adapt-hotgrid', version: '3.0.0' }))
|
|
328
|
+
// Create backup
|
|
329
|
+
const backupDir = path.join(tmpDir, 'adapt-hotgrid-v2.0.0')
|
|
330
|
+
await fs.mkdir(backupDir)
|
|
331
|
+
await fs.writeFile(path.join(backupDir, 'package.json'), JSON.stringify({ name: 'adapt-hotgrid', version: '2.0.0' }))
|
|
332
|
+
|
|
333
|
+
const result = await restorePluginFromBackup(tmpDir, 'adapt-hotgrid', log)
|
|
334
|
+
|
|
335
|
+
assert.equal(result.version, '2.0.0')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should read version from bower.json if no package.json', async () => {
|
|
339
|
+
const backupDir = path.join(tmpDir, 'adapt-hotgrid-v1.0.0')
|
|
340
|
+
await fs.mkdir(backupDir)
|
|
341
|
+
await fs.writeFile(path.join(backupDir, 'bower.json'), JSON.stringify({ name: 'adapt-hotgrid', version: '1.0.0' }))
|
|
342
|
+
|
|
343
|
+
const result = await restorePluginFromBackup(tmpDir, 'adapt-hotgrid', log)
|
|
344
|
+
|
|
345
|
+
assert.equal(result.version, '1.0.0')
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('should throw when backup has neither package.json nor bower.json', async () => {
|
|
349
|
+
const backupDir = path.join(tmpDir, 'adapt-hotgrid-v1.0.0')
|
|
350
|
+
await fs.mkdir(backupDir)
|
|
351
|
+
await fs.writeFile(path.join(backupDir, 'readme.txt'), 'no metadata')
|
|
352
|
+
|
|
353
|
+
await assert.rejects(
|
|
354
|
+
() => restorePluginFromBackup(tmpDir, 'adapt-hotgrid', log),
|
|
355
|
+
(err) => err.message.includes('Could not read package.json or bower.json')
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('should log an info message on success', async () => {
|
|
360
|
+
const backupDir = path.join(tmpDir, 'adapt-hotgrid-v1.0.0')
|
|
361
|
+
await fs.mkdir(backupDir)
|
|
362
|
+
await fs.writeFile(path.join(backupDir, 'package.json'), JSON.stringify({ name: 'adapt-hotgrid', version: '1.0.0' }))
|
|
363
|
+
|
|
364
|
+
await restorePluginFromBackup(tmpDir, 'adapt-hotgrid', log)
|
|
365
|
+
|
|
366
|
+
const infoCalls = log.mock.calls.filter(c => c.arguments[0] === 'info')
|
|
367
|
+
assert.equal(infoCalls.length, 1)
|
|
368
|
+
assert.ok(infoCalls[0].arguments[1].includes('adapt-hotgrid'))
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// processPluginFiles
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
describe('processPluginFiles()', () => {
|
|
376
|
+
let tmpDir
|
|
377
|
+
let pluginDir
|
|
378
|
+
let log
|
|
379
|
+
|
|
380
|
+
beforeEach(async () => {
|
|
381
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cpu-test-'))
|
|
382
|
+
pluginDir = path.join(tmpDir, 'plugins')
|
|
383
|
+
await fs.mkdir(pluginDir)
|
|
384
|
+
log = mock.fn()
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
afterEach(async () => {
|
|
388
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should return a non-local install when sourcePath has no directory component', async () => {
|
|
392
|
+
const result = await processPluginFiles(
|
|
393
|
+
{ name: 'adapt-hotgrid', sourcePath: '4.3.5' },
|
|
394
|
+
pluginDir,
|
|
395
|
+
log
|
|
396
|
+
)
|
|
397
|
+
assert.deepEqual(result, { name: 'adapt-hotgrid', version: '4.3.5', isLocalInstall: false })
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('should read package.json and copy source to pluginDir', async () => {
|
|
401
|
+
// Create source directory with plugin files
|
|
402
|
+
const sourcePath = path.join(tmpDir, 'source', 'adapt-hotgrid')
|
|
403
|
+
await fs.mkdir(sourcePath, { recursive: true })
|
|
404
|
+
await fs.writeFile(
|
|
405
|
+
path.join(sourcePath, 'package.json'),
|
|
406
|
+
JSON.stringify({ name: 'adapt-hotgrid', version: '4.3.5' })
|
|
407
|
+
)
|
|
408
|
+
await fs.writeFile(path.join(sourcePath, 'index.js'), 'module.exports = {}')
|
|
409
|
+
|
|
410
|
+
const result = await processPluginFiles(
|
|
411
|
+
{ name: 'adapt-hotgrid', sourcePath },
|
|
412
|
+
pluginDir,
|
|
413
|
+
log
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
assert.equal(result.name, 'adapt-hotgrid')
|
|
417
|
+
assert.equal(result.version, '4.3.5')
|
|
418
|
+
assert.equal(result.isLocalInstall, true)
|
|
419
|
+
assert.equal(result.sourcePath, path.join(pluginDir, 'adapt-hotgrid'))
|
|
420
|
+
|
|
421
|
+
// Plugin should now be in pluginDir
|
|
422
|
+
const copiedPkg = await readJson(path.join(pluginDir, 'adapt-hotgrid', 'package.json'))
|
|
423
|
+
assert.equal(copiedPkg.version, '4.3.5')
|
|
424
|
+
|
|
425
|
+
// Source should be removed
|
|
426
|
+
await assert.rejects(() => fs.access(sourcePath), { code: 'ENOENT' })
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should fall back to bower.json when package.json is absent', async () => {
|
|
430
|
+
const sourcePath = path.join(tmpDir, 'source', 'adapt-vanilla')
|
|
431
|
+
await fs.mkdir(sourcePath, { recursive: true })
|
|
432
|
+
await fs.writeFile(
|
|
433
|
+
path.join(sourcePath, 'bower.json'),
|
|
434
|
+
JSON.stringify({ name: 'adapt-vanilla', version: '2.0.0' })
|
|
435
|
+
)
|
|
436
|
+
await fs.writeFile(path.join(sourcePath, 'index.js'), '')
|
|
437
|
+
|
|
438
|
+
const result = await processPluginFiles(
|
|
439
|
+
{ name: 'adapt-vanilla', sourcePath },
|
|
440
|
+
pluginDir,
|
|
441
|
+
log
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
assert.equal(result.name, 'adapt-vanilla')
|
|
445
|
+
assert.equal(result.isLocalInstall, true)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('should handle a nested root folder in the source', async () => {
|
|
449
|
+
// Source has a single subfolder containing the actual plugin
|
|
450
|
+
const sourcePath = path.join(tmpDir, 'source')
|
|
451
|
+
await fs.mkdir(path.join(sourcePath, 'adapt-hotgrid'), { recursive: true })
|
|
452
|
+
await fs.writeFile(
|
|
453
|
+
path.join(sourcePath, 'adapt-hotgrid', 'package.json'),
|
|
454
|
+
JSON.stringify({ name: 'adapt-hotgrid', version: '1.0.0' })
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
const result = await processPluginFiles(
|
|
458
|
+
{ name: 'adapt-hotgrid', sourcePath },
|
|
459
|
+
pluginDir,
|
|
460
|
+
log
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
assert.equal(result.name, 'adapt-hotgrid')
|
|
464
|
+
assert.equal(result.isLocalInstall, true)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('should throw when source has no package.json or bower.json', async () => {
|
|
468
|
+
const sourcePath = path.join(tmpDir, 'source', 'bad-plugin')
|
|
469
|
+
await fs.mkdir(sourcePath, { recursive: true })
|
|
470
|
+
await fs.writeFile(path.join(sourcePath, 'readme.txt'), 'no metadata')
|
|
471
|
+
|
|
472
|
+
await assert.rejects(
|
|
473
|
+
() => processPluginFiles(
|
|
474
|
+
{ name: 'bad-plugin', sourcePath },
|
|
475
|
+
pluginDir,
|
|
476
|
+
log
|
|
477
|
+
),
|
|
478
|
+
(err) => err.message.startsWith('Invalid plugin zip')
|
|
479
|
+
)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('should back up existing plugin before copying new version', async () => {
|
|
483
|
+
// Pre-existing plugin in pluginDir
|
|
484
|
+
const existingPath = path.join(pluginDir, 'adapt-hotgrid')
|
|
485
|
+
await fs.mkdir(existingPath)
|
|
486
|
+
await fs.writeFile(
|
|
487
|
+
path.join(existingPath, 'package.json'),
|
|
488
|
+
JSON.stringify({ name: 'adapt-hotgrid', version: '3.0.0' })
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
// New source
|
|
492
|
+
const sourcePath = path.join(tmpDir, 'source', 'adapt-hotgrid')
|
|
493
|
+
await fs.mkdir(sourcePath, { recursive: true })
|
|
494
|
+
await fs.writeFile(
|
|
495
|
+
path.join(sourcePath, 'package.json'),
|
|
496
|
+
JSON.stringify({ name: 'adapt-hotgrid', version: '4.0.0' })
|
|
497
|
+
)
|
|
498
|
+
await fs.writeFile(path.join(sourcePath, 'index.js'), '')
|
|
499
|
+
|
|
500
|
+
const result = await processPluginFiles(
|
|
501
|
+
{ name: 'adapt-hotgrid', sourcePath },
|
|
502
|
+
pluginDir,
|
|
503
|
+
log
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
assert.equal(result.version, '4.0.0')
|
|
507
|
+
// Backup should exist
|
|
508
|
+
await fs.access(path.join(pluginDir, 'adapt-hotgrid-v3.0.0'))
|
|
509
|
+
// New version should be in place
|
|
510
|
+
const pkg = await readJson(path.join(pluginDir, 'adapt-hotgrid', 'package.json'))
|
|
511
|
+
assert.equal(pkg.version, '4.0.0')
|
|
512
|
+
})
|
|
513
|
+
})
|