Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
8 changes: 6 additions & 2 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ class API {
} = {}) {
// cache state of plugins, as these will be wiped
const plugins = (await new Project({ cwd }).getInstallTargets())
.map(p => p.isLocalSource ? p.sourcePath : `${p.name}@${p.requestedVersion}`)
.map(p => {
if (p.isLocalSource) return p.sourcePath
if (p.isGitSource) return p.gitUrl + (p.gitRef ? '#' + p.gitRef : '')
return `${p.name}@${p.requestedVersion}`
})

await this.installFramework({ version, repository, cwd, logger })
// restore plugins
Expand Down Expand Up @@ -271,7 +275,7 @@ class API {
.filter(Boolean)
await async.eachOfLimit(filteredPlugins, 8, async plugin => {
await plugin.fetchProjectInfo()
await plugin.fetchBowerInfo()
await plugin.fetchSourceInfo()
await plugin.findCompatibleVersion(frameworkVersion)
})
return filteredPlugins
Expand Down
13 changes: 2 additions & 11 deletions lib/integration/AdaptFramework/clone.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import chalk from 'chalk'
import { exec } from 'child_process'
import { ADAPT_FRAMEWORK } from '../../util/constants.js'
import path from 'path'
import gitClone from '../../util/gitClone.js'

export default async function clone ({
repository = ADAPT_FRAMEWORK,
Expand All @@ -13,15 +13,6 @@ export default async function clone ({
cwd = path.resolve(process.cwd(), cwd)
if (!branch && !repository) throw new Error('Repository details are required.')
logger?.write(chalk.cyan('cloning framework to', cwd, '\t'))
await new Promise(function (resolve, reject) {
const child = exec(`git clone ${repository} "${cwd}"`)
child.addListener('error', reject)
child.addListener('exit', resolve)
})
await new Promise(function (resolve, reject) {
const child = exec(`git checkout ${branch}`)
child.addListener('error', reject)
child.addListener('exit', resolve)
})
await gitClone({ url: repository, dir: cwd, branch })
logger?.log(' ', 'done!')
}
58 changes: 50 additions & 8 deletions lib/integration/Plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import endpointParser from 'bower-endpoint-parser'
import semver from 'semver'
import fs from 'fs-extra'
import path from 'path'
import os from 'os'
import gitClone from '../util/gitClone.js'
import getBowerRegistryConfig from './getBowerRegistryConfig.js'
import { ADAPT_ALLOW_PRERELEASE, PLUGIN_TYPES, PLUGIN_TYPE_FOLDERS, PLUGIN_DEFAULT_TYPE } from '../util/constants.js'
/** @typedef {import("./Project.js").default} Project */
Expand Down Expand Up @@ -38,13 +40,23 @@ export default class Plugin {
this.project = project
this.cwd = cwd
this.BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd: this.cwd })
const endpoint = name + '#' + (isCompatibleEnabled ? '*' : requestedVersion)
const ep = endpointParser.decompose(endpoint)
this.sourcePath = null
this.name = ep.name || ep.source
this.packageName = (/^adapt-/i.test(this.name) ? '' : 'adapt-') + (!isContrib ? '' : 'contrib-') + slug(this.name, { maintainCase: true })
// the constraint given by the user
this.requestedVersion = requestedVersion

const isGitUrl = /^https?:\/\//.test(name)
if (isGitUrl) {
this.gitUrl = name
this.gitRef = (requestedVersion && requestedVersion !== '*') ? requestedVersion : null
this.name = ''
this.packageName = ''
this.requestedVersion = '*'
} else {
const endpoint = name + '#' + (isCompatibleEnabled ? '*' : requestedVersion)
const ep = endpointParser.decompose(endpoint)
this.name = ep.name || ep.source
this.packageName = (/^adapt-/i.test(this.name) ? '' : 'adapt-') + (!isContrib ? '' : 'contrib-') + slug(this.name, { maintainCase: true })
this.requestedVersion = requestedVersion
}

// the most recent version of the plugin compatible with the given framework
this.latestCompatibleSourceVersion = null
// a non-wildcard constraint resolved to the highest version of the plugin that satisfies the requestedVersion and is compatible with the framework
Expand Down Expand Up @@ -128,6 +140,14 @@ export default class Plugin {
return Boolean(this.sourcePath || this?._projectInfo?._wasInstalledFromPath)
}

/**
* plugin will be or was installed from a git URL
* @returns {boolean}
*/
get isGitSource () {
return Boolean(this.gitUrl || this._projectInfo?._wasInstalledFromGitRepo)
}

/**
* check if source path is a zip
* @returns {boolean}
Expand Down Expand Up @@ -185,10 +205,28 @@ export default class Plugin {
}

async fetchSourceInfo () {
if (this.isGitSource) return await this.fetchGitSourceInfo()
if (this.isLocalSource) return await this.fetchLocalSourceInfo()
await this.fetchBowerInfo()
}

async fetchGitSourceInfo () {
if (this._sourceInfo) return this._sourceInfo
this._sourceInfo = null
const tmpDir = path.join(os.tmpdir(), `adapt-git-${Date.now()}`)
try {
await gitClone({ url: this.gitUrl, dir: tmpDir, branch: this.gitRef, shallow: true })
const bowerJSONPath = path.join(tmpDir, 'bower.json')
if (!fs.existsSync(bowerJSONPath)) return
this._sourceInfo = await fs.readJSON(bowerJSONPath)
this.name = this._sourceInfo.name
this.packageName = this.name
this.matchedVersion = this._sourceInfo.version
} finally {
await fs.rm(tmpDir, { recursive: true, force: true })
}
}

async fetchLocalSourceInfo () {
if (this._sourceInfo) return this._sourceInfo
this._sourceInfo = null
Expand Down Expand Up @@ -269,6 +307,10 @@ export default class Plugin {
if (!this._projectInfo) return
this.name = this._projectInfo.name
this.packageName = this.name
if (this._projectInfo._wasInstalledFromGitRepo) {
this.gitUrl = this._projectInfo._gitUrl
this.gitRef = this._projectInfo._gitRef || null
}
}

async findCompatibleVersion (framework) {
Expand All @@ -291,7 +333,7 @@ export default class Plugin {
const getMatchingVersion = async () => {
if (!this.isPresent) return null

if (this.isLocalSource) {
if (this.isLocalSource || this.isGitSource) {
const info = this.projectVersion ? this._projectInfo : this._sourceInfo
const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(info.version, this.requestedVersion, semverOptions)
const satisfiesFramework = semver.satisfies(framework, info.framework)
Expand Down Expand Up @@ -360,7 +402,7 @@ export default class Plugin {

async getRepositoryUrl () {
if (this._repositoryUrl) return this._repositoryUrl
if (this.isLocalSource) return
if (this.isLocalSource || this.isGitSource) return
const url = await new Promise((resolve, reject) => {
bower.commands.lookup(this.packageName, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
.on('end', resolve)
Expand Down
27 changes: 27 additions & 0 deletions lib/integration/PluginManagement/clone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import fs from 'fs-extra'
import path from 'path'
import gitClone from '../../util/gitClone.js'

/**
* Clone a plugin from a git URL and write .bower.json metadata
* @param {Object} options
* @param {string} options.url The git repository URL
* @param {string} options.destPath The target directory for the plugin
* @param {string} [options.branch] Optional branch, tag, or ref
* @returns {Object} The bower.json contents (with git metadata)
*/
export default async function clonePlugin ({
url,
destPath,
branch = null
} = {}) {
await fs.ensureDir(path.dirname(destPath))
await fs.rm(destPath, { recursive: true, force: true })
await gitClone({ url, dir: destPath, branch })
const bowerJSON = await fs.readJSON(path.join(destPath, 'bower.json'))
bowerJSON._gitUrl = url
bowerJSON._gitRef = branch || undefined
bowerJSON._wasInstalledFromGitRepo = true
await fs.writeJSON(path.join(destPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null })
return bowerJSON
}
21 changes: 15 additions & 6 deletions lib/integration/PluginManagement/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,28 @@ async function getInstallTargets ({ logger, project, plugins, isCompatibleEnable
const itinerary = isEmpty
? await project.getManifestDependencies()
: plugins.reduce((itinerary, arg) => {
const [name, version = '*'] = arg.split(/[#@]/)
let name, version
if (/^https?:\/\//.test(arg)) {

@oliverfoster oliverfoster Jun 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code change seems wholly unnecessary.

It splits the arg over the # sign and assigns index 0 to name and index 1 to version, it defaults version to * when index 1 == undefined. Which is what the original line did, but for both # and @

Everything else looks good.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, have reverted that whole if

const hashIndex = arg.lastIndexOf('#')
if (hashIndex !== -1) {
name = arg.substring(0, hashIndex)
version = arg.substring(hashIndex + 1)
} else {
name = arg
version = '*'
}
} else {
[name, version = '*'] = arg.split(/[#@]/)
}
// Duplicates are removed by assigning to object properties
itinerary[name] = version
return itinerary
}, {})
const pluginNames = Object.entries(itinerary).map(([name, version]) => `${name}#${version}`)

/**
* @type {[Target]}
*/
const targets = pluginNames.length
? pluginNames.map(nameVersion => {
const [name, requestedVersion] = nameVersion.split(/[#@]/)
const targets = Object.keys(itinerary).length
? Object.entries(itinerary).map(([name, requestedVersion]) => {
return new Target({ name, requestedVersion, isCompatibleEnabled, project, logger })
})
: await project.getInstallTargets()
Expand Down
6 changes: 4 additions & 2 deletions lib/integration/PluginManagement/print.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ export function versionPrinter (plugin, logger) {
versionToApply,
latestCompatibleSourceVersion
} = plugin
const sourceLabel = plugin.isLocalSource ? ' (local)' : plugin.isGitSource ? ' (git)' : ` (latest compatible version is ${greenIfEqual(versionToApply, latestCompatibleSourceVersion)})`
logger?.log(highlight(plugin.packageName), latestCompatibleSourceVersion === null
? '(no version information)'
: `${chalk.greenBright(versionToApply)}${plugin.isLocalSource ? ' (local)' : ` (latest compatible version is ${greenIfEqual(versionToApply, latestCompatibleSourceVersion)})`}`
: `${chalk.greenBright(versionToApply)}${sourceLabel}`
)
}

Expand All @@ -37,9 +38,10 @@ export function existingVersionPrinter (plugin, logger) {
const fromTo = preUpdateProjectVersion !== null
? `from ${chalk.greenBright(preUpdateProjectVersion)} to ${chalk.greenBright(projectVersion)}`
: `${chalk.greenBright(projectVersion)}`
const sourceLabel = plugin.isLocalSource ? ' (local)' : plugin.isGitSource ? ' (git)' : ` (latest compatible version is ${greenIfEqual(projectVersion, latestCompatibleSourceVersion)})`
logger?.log(highlight(plugin.packageName), latestCompatibleSourceVersion === null
? fromTo
: `${fromTo}${plugin.isLocalSource ? ' (local)' : ` (latest compatible version is ${greenIfEqual(projectVersion, latestCompatibleSourceVersion)})`}`
: `${fromTo}${sourceLabel}`
)
}

Expand Down
15 changes: 9 additions & 6 deletions lib/integration/PluginManagement/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ async function conflictResolution ({ logger, targets, isInteractive }) {
prompt
}
}
const preFilteredPlugins = targets.filter(target => !target.isLocalSource)
const preFilteredPlugins = targets.filter(target => !target.isLocalSource && !target.isGitSource)
const allQuestions = [
add(preFilteredPlugins.filter(target => !target.hasFrameworkCompatibleVersion && target.latestSourceVersion), 'There is no compatible version of the following plugins:', checkVersion),
add(preFilteredPlugins.filter(target => target.hasFrameworkCompatibleVersion && !target.hasValidRequestVersion), 'The version requested is invalid, there are newer compatible versions of the following plugins:', checkVersion),
Expand All @@ -181,28 +181,31 @@ async function conflictResolution ({ logger, targets, isInteractive }) {
* @param {[Target]} options.targets
*/
function summariseDryRun ({ logger, targets }) {
const preFilteredPlugins = targets.filter(target => !target.isLocalSource)
const preFilteredPlugins = targets.filter(target => !target.isLocalSource && !target.isGitSource)
const localSources = targets.filter(target => target.isLocalSource)
const gitSources = targets.filter(target => target.isGitSource && target.isToBeUpdated)
const toBeInstalled = preFilteredPlugins.filter(target => target.isToBeUpdated)
const toBeSkipped = preFilteredPlugins.filter(target => !target.isToBeUpdated || target.isSkipped)
const missing = preFilteredPlugins.filter(target => target.isMissing)
summarise(logger, localSources, packageNamePrinter, 'The following plugins were installed from a local source and cannot be updated:')
summarise(logger, toBeSkipped, packageNamePrinter, 'The following plugins will be skipped:')
summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
summarise(logger, toBeInstalled, existingVersionPrinter, 'The following plugins will be updated:')
summarise(logger, [...toBeInstalled, ...gitSources], existingVersionPrinter, 'The following plugins will be updated:')
}

/**
* @param {Object} options
* @param {[Target]} options.targets
*/
function summariseUpdates ({ logger, targets }) {
const preFilteredPlugins = targets.filter(target => !target.isLocalSource)
const preFilteredPlugins = targets.filter(target => !target.isLocalSource && !target.isGitSource)
const localSources = targets.filter(target => target.isLocalSource)
const installSucceeded = preFilteredPlugins.filter(target => target.isUpdateSuccessful)
const gitSucceeded = targets.filter(target => target.isGitSource && target.isUpdateSuccessful)
const gitErrored = targets.filter(target => target.isGitSource && target.isUpdateFailure)
const installSucceeded = [...preFilteredPlugins.filter(target => target.isUpdateSuccessful), ...gitSucceeded]
const installSkipped = preFilteredPlugins.filter(target => target.isSkipped)
const noUpdateAvailable = preFilteredPlugins.filter(target => !target.isToBeUpdated && !target.isSkipped)
const installErrored = preFilteredPlugins.filter(target => target.isUpdateFailure)
const installErrored = [...preFilteredPlugins.filter(target => target.isUpdateFailure), ...gitErrored]
const missing = preFilteredPlugins.filter(target => target.isMissing)
const noneInstalled = (installSucceeded.length === 0)
const allInstalledSuccessfully = (installErrored.length === 0 && missing.length === 0)
Expand Down
15 changes: 13 additions & 2 deletions lib/integration/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,16 @@ export default class Project {

/** @returns {[Target]} */
async getInstallTargets () {
return Object.entries(await this.getManifestDependencies()).map(([name, requestedVersion]) => new Target({ name, requestedVersion, project: this, logger: this.logger }))
return Object.entries(await this.getManifestDependencies()).map(([name, requestedVersion]) => {
if (/^https?:\/\//.test(requestedVersion)) {
const hashIndex = requestedVersion.lastIndexOf('#')
if (hashIndex !== -1) {
return new Target({ name: requestedVersion.substring(0, hashIndex), requestedVersion: requestedVersion.substring(hashIndex + 1), project: this, logger: this.logger })
}
return new Target({ name: requestedVersion, project: this, logger: this.logger })
}
return new Target({ name, requestedVersion, project: this, logger: this.logger })
})
}

/** @returns {[string]} */
Expand Down Expand Up @@ -128,7 +137,9 @@ export default class Project {
if (this.containsManifestFile) {
manifest = readValidateJSONSync(this.manifestFilePath)
}
manifest.dependencies[plugin.packageName] = plugin.sourcePath || plugin.requestedVersion || plugin.version
manifest.dependencies[plugin.packageName] = plugin.gitUrl
? (plugin.gitUrl + (plugin.gitRef ? '#' + plugin.gitRef : ''))
: plugin.sourcePath || plugin.requestedVersion || plugin.version
fs.writeJSONSync(this.manifestFilePath, manifest, { spaces: 2, replacer: null })
}

Expand Down
Loading