Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions lib/ContentPluginModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ class ContentPluginModule extends AbstractApiModule {
for (const i of await this.framework.runCliCommand('getPluginUpdateInfos')) {
if (dbInfo[i.name]?.version !== i.matchedVersion) {
this.log('debug', 'SYNC', i.name, 'local:', dbInfo[i.name]?.version, 'fw:', i.matchedVersion)
await this.insertOrUpdate({ ...(await i.getInfo()), type: await i.getType(), isLocalInstall: i.isLocalSource })
const pluginInfo = { ...(await i.getInfo()), type: await i.getType(), isLocalInstall: i.isLocalSource }
if (i.isGitSource) pluginInfo.gitUrl = i.gitUrl
await this.insertOrUpdate(pluginInfo)
}
}
}
Expand All @@ -180,6 +182,7 @@ class ContentPluginModule extends AbstractApiModule {
// For local installs, check if backup exists if main plugin directory doesn't
const pluginsWithPaths = await Promise.all(missingPlugins.map(async (p) => {
if (!p.isLocalInstall) {
if (p.gitUrl) return p.gitUrl
return `${p.name}@${p.version}`
}
const pluginDir = this.getConfig('pluginDir')
Expand Down Expand Up @@ -330,22 +333,36 @@ class ContentPluginModule extends AbstractApiModule {
* @returns Resolves with plugin DB data
*/
async installPlugin (pluginName, versionOrPath, options = { strict: false, force: false }) {
const pluginData = await this.findOne({ name: String(pluginName) }, { includeUpdateInfo: true, strict: false })
const { name, version, sourcePath, isLocalInstall } = await this.processPluginFiles({ ...pluginData, sourcePath: versionOrPath })
const isGitUrl = /^https?:\/\//.test(versionOrPath)

Comment thread
taylortom marked this conversation as resolved.
let name, version, sourcePath, isLocalInstall, gitUrl

if (isGitUrl) {
name = pluginName
sourcePath = null
isLocalInstall = false
gitUrl = versionOrPath.split('#')[0]
Comment thread
taylortom marked this conversation as resolved.
} else {
const pluginData = await this.findOne({ name: String(pluginName) }, { includeUpdateInfo: true, strict: false })
Comment thread
taylortom marked this conversation as resolved.
Outdated
;({ name, version, sourcePath, isLocalInstall } = await this.processPluginFiles({ ...pluginData, sourcePath: versionOrPath }))
}
const existingPlugin = await this.findOne({ name }, { strict: false })
Comment thread
taylortom marked this conversation as resolved.
Outdated

if (existingPlugin) {
if (!options.force && semver.lte(version, existingPlugin.version)) {
if (!isGitUrl && !options.force && semver.lte(version, existingPlugin.version)) {
throw this.app.errors.CONTENTPLUGIN_ALREADY_EXISTS
.setData({ name: existingPlugin.name, version: existingPlugin.version })
}
}
const [data] = await this.framework.runCliCommand('installPlugins', { plugins: [`${name}@${sourcePath ?? version}`] })
const info = await this.insertOrUpdate({
const pluginArg = isGitUrl ? versionOrPath : `${name}@${sourcePath ?? version}`
const [data] = await this.framework.runCliCommand('installPlugins', { plugins: [pluginArg] })
const dbData = {
...(await data.getInfo()),
type: await data.getType(),
isLocalInstall
})
}
if (isGitUrl) dbData.gitUrl = gitUrl
const info = await this.insertOrUpdate(dbData)
Comment thread
taylortom marked this conversation as resolved.
Outdated
if (!data.isInstallSuccessful) {
throw this.app.errors.CONTENTPLUGIN_CLI_INSTALL_FAILED
.setData({ name })
Comment thread
taylortom marked this conversation as resolved.
Outdated
Expand Down
4 changes: 4 additions & 0 deletions schema/contentplugin.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"description": "Whether the plugin has been installed locally (as opposed to with the CLI)",
"type": "boolean"
},
"gitUrl": {
Comment thread
taylortom marked this conversation as resolved.
"description": "The HTTPS git URL this plugin was installed from, if applicable",
"type": "string"
},
"isEnabled": {
"description": "",
"type": "boolean",
Expand Down
94 changes: 94 additions & 0 deletions tests/ContentPluginModule.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, mock } from 'node:test'
import assert from 'node:assert/strict'
import ContentPluginModule from '../lib/ContentPluginModule.js'

describe('ContentPluginModule.installPlugin()', () => {
it('should install git URLs directly and persist gitUrl', async () => {
const runCliCommand = mock.fn(async () => [{
isInstallSuccessful: true,
getInfo: async () => ({ name: 'adapt-hotgrid', version: '2.0.0', targetAttribute: '_component' }),
getType: async () => 'component'
}])
const insertOrUpdate = mock.fn(async (data) => data)
const processPluginFiles = mock.fn(async () => {
throw new Error('processPluginFiles should not be called for git installs')
})
const context = {
framework: { runCliCommand },
processPluginFiles,
insertOrUpdate,
findOne: mock.fn(async () => ({ name: 'adapt-hotgrid', version: '999.0.0' })),
processPluginSchemas: mock.fn(async () => {}),
app: {
errors: {
CONTENTPLUGIN_ALREADY_EXISTS: { setData: (data) => Object.assign(new Error('already exists'), { data }) },
CONTENTPLUGIN_CLI_INSTALL_FAILED: { setData: (data) => Object.assign(new Error('cli failed'), { data }) },
CONTENTPLUGIN_ATTR_MISSING: { setData: (data) => Object.assign(new Error('attr missing'), { data }) }
}
}
}

const result = await ContentPluginModule.prototype.installPlugin.call(
context,
'',
'https://github.com/org/adapt-hotgrid.git#v2.0.0',
{ force: false }
)

assert.equal(runCliCommand.mock.calls[0].arguments[0], 'installPlugins')
assert.deepEqual(runCliCommand.mock.calls[0].arguments[1], {
plugins: ['https://github.com/org/adapt-hotgrid.git#v2.0.0']
})
assert.equal(processPluginFiles.mock.callCount(), 0)
assert.equal(insertOrUpdate.mock.calls[0].arguments[0].gitUrl, 'https://github.com/org/adapt-hotgrid.git')
assert.equal(result.name, 'adapt-hotgrid')
Comment thread
taylortom marked this conversation as resolved.
})
})

describe('ContentPluginModule.getMissingPlugins()', () => {
it('should return gitUrl for missing git-installed plugins', async () => {
const context = {
find: async () => ([
{ name: 'adapt-hotgrid', version: '2.0.0', isLocalInstall: false, gitUrl: 'https://github.com/org/adapt-hotgrid.git' },
{ name: 'adapt-text', version: '1.0.0', isLocalInstall: false }
]),
framework: {
getManifestPlugins: async () => [],
getInstalledPlugins: async () => []
}
}
const result = await ContentPluginModule.prototype.getMissingPlugins.call(context)
assert.deepEqual(result, [
'https://github.com/org/adapt-hotgrid.git',
'adapt-text@1.0.0'
])
})
})

describe('ContentPluginModule.syncPluginData()', () => {
it('should persist gitUrl for git sources', async () => {
const insertOrUpdate = mock.fn(async () => {})
const context = {
log: mock.fn(),
find: async () => [],
insertOrUpdate,
framework: {
runCliCommand: async () => ([
{
name: 'adapt-hotgrid',
matchedVersion: '2.0.0',
isLocalSource: false,
isGitSource: true,
gitUrl: 'https://github.com/org/adapt-hotgrid.git',
getInfo: async () => ({ name: 'adapt-hotgrid', version: '2.0.0' }),
getType: async () => 'component'
}
])
}
}

await ContentPluginModule.prototype.syncPluginData.call(context)

assert.equal(insertOrUpdate.mock.calls[0].arguments[0].gitUrl, 'https://github.com/org/adapt-hotgrid.git')
})
})