From 33fbc7993bb62105842fe1f29a9736d6a9bc17e5 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 25 Feb 2026 11:18:19 +0000 Subject: [PATCH 1/4] New: Support installing and updating plugins from git URLs (fixes #237) Allow users to install plugins directly from HTTPS git URLs instead of requiring them to be registered in the bower registry. Supports optional branch/tag refs via URL#ref syntax. --- lib/integration/Plugin.js | 64 ++++++++++++++++++--- lib/integration/PluginManagement/install.js | 21 +++++-- lib/integration/PluginManagement/print.js | 6 +- lib/integration/PluginManagement/update.js | 15 +++-- lib/integration/Project.js | 11 +++- lib/integration/Target.js | 54 +++++++++++++++++ 6 files changed, 147 insertions(+), 24 deletions(-) diff --git a/lib/integration/Plugin.js b/lib/integration/Plugin.js index c35ed2c..515c7b4 100644 --- a/lib/integration/Plugin.js +++ b/lib/integration/Plugin.js @@ -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 { exec } from 'child_process' 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 */ @@ -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 @@ -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} @@ -185,10 +205,34 @@ 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 { + const branchFlag = this.gitRef ? ` --branch ${this.gitRef}` : '' + await new Promise((resolve, reject) => { + exec(`git clone --depth 1${branchFlag} ${this.gitUrl} "${tmpDir}"`, (err) => { + if (err) return reject(err) + resolve() + }) + }) + 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 @@ -269,6 +313,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) { @@ -291,7 +339,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) @@ -360,7 +408,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) diff --git a/lib/integration/PluginManagement/install.js b/lib/integration/PluginManagement/install.js index 1098bfd..d782b20 100644 --- a/lib/integration/PluginManagement/install.js +++ b/lib/integration/PluginManagement/install.js @@ -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)) { + 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() diff --git a/lib/integration/PluginManagement/print.js b/lib/integration/PluginManagement/print.js index b2ace82..53d7d9b 100644 --- a/lib/integration/PluginManagement/print.js +++ b/lib/integration/PluginManagement/print.js @@ -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}` ) } @@ -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}` ) } diff --git a/lib/integration/PluginManagement/update.js b/lib/integration/PluginManagement/update.js index 985b85c..70ad051 100644 --- a/lib/integration/PluginManagement/update.js +++ b/lib/integration/PluginManagement/update.js @@ -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), @@ -181,15 +181,16 @@ 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:') } /** @@ -197,12 +198,14 @@ function summariseDryRun ({ logger, targets }) { * @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) diff --git a/lib/integration/Project.js b/lib/integration/Project.js index 528f4e0..e896b1e 100644 --- a/lib/integration/Project.js +++ b/lib/integration/Project.js @@ -56,7 +56,12 @@ 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)) { + return new Target({ name: requestedVersion, project: this, logger: this.logger }) + } + return new Target({ name, requestedVersion, project: this, logger: this.logger }) + }) } /** @returns {[string]} */ @@ -128,7 +133,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 }) } diff --git a/lib/integration/Target.js b/lib/integration/Target.js index 5bf271c..607382a 100644 --- a/lib/integration/Target.js +++ b/lib/integration/Target.js @@ -121,6 +121,10 @@ export default class Target extends Plugin { } markInstallable () { + if (this.isGitSource && this.matchedVersion) { + this.versionToApply = this.matchedVersion + return + } if (!this.isApplyLatestCompatibleVersion && !(this.isLocalSource && this.latestSourceVersion)) return this.versionToApply = this.matchedVersion } @@ -172,6 +176,30 @@ export default class Target extends Plugin { await this.fetchProjectInfo() return } + if (this.isGitSource) { + await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder)) + const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.packageName) + await fs.rm(pluginPath, { recursive: true, force: true }) + const branchFlag = this.gitRef ? ` --branch ${this.gitRef}` : '' + try { + await new Promise((resolve, reject) => { + exec(`git clone${branchFlag} ${this.gitUrl} "${pluginPath}"`, (err) => { + if (err) return reject(err) + resolve() + }) + }) + } catch (error) { + throw new Error(`The plugin was found but failed to clone from ${this.gitUrl}. Error ${error}`) + } + const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) + bowerJSON._gitUrl = this.gitUrl + bowerJSON._gitRef = this.gitRef || undefined + bowerJSON._wasInstalledFromGitRepo = true + await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) + this._projectInfo = null + await this.fetchProjectInfo() + return + } if (clone) { // clone install const repoDetails = await this.getRepositoryUrl() @@ -238,6 +266,32 @@ export default class Target extends Plugin { const typeFolder = await this.getTypeFolder() const outputPath = path.join(this.cwd, 'src', typeFolder) const pluginPath = path.join(outputPath, this.name) + if (this.isGitSource) { + this.preUpdateProjectVersion = this.projectVersion + try { + await fs.rm(pluginPath, { recursive: true, force: true }) + } catch (err) { + throw new Error(`There was a problem writing to the target directory ${pluginPath}`) + } + try { + await new Promise((resolve, reject) => { + exec(`git clone ${this.gitUrl} "${pluginPath}"`, (err) => { + if (err) return reject(err) + resolve() + }) + }) + } catch (error) { + throw new Error(`The plugin was found but failed to clone from ${this.gitUrl}. Error ${error}`) + } + const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) + bowerJSON._gitUrl = this.gitUrl + bowerJSON._gitRef = this.gitRef || undefined + bowerJSON._wasInstalledFromGitRepo = true + await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) + this._projectInfo = null + await this.fetchProjectInfo() + return + } try { await fs.rm(pluginPath, { recursive: true, force: true }) } catch (err) { From 223c1e36c656ed978ff9e6cf7e2c9b303ece87bc Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 25 Feb 2026 11:23:39 +0000 Subject: [PATCH 2/4] Fix: Handle git plugins in node API (updateFramework, getPluginUpdateInfos) Ensure git-sourced plugins survive framework updates by correctly serialising them as URLs when caching. Split URL#ref when reading manifest entries. Route git plugins through fetchSourceInfo instead of fetchBowerInfo in getPluginUpdateInfos. --- lib/api.js | 8 ++++++-- lib/integration/Project.js | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/api.js b/lib/api.js index 345fcf0..1b248b3 100644 --- a/lib/api.js +++ b/lib/api.js @@ -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 @@ -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 diff --git a/lib/integration/Project.js b/lib/integration/Project.js index e896b1e..835d741 100644 --- a/lib/integration/Project.js +++ b/lib/integration/Project.js @@ -58,6 +58,10 @@ export default class Project { async getInstallTargets () { 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 }) From ab9ad1056667d5a442ed5737cf495da2c3b4be80 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 25 Feb 2026 11:30:48 +0000 Subject: [PATCH 3/4] Chore: Extract git clone operations into reusable utilities Add lib/util/gitClone.js as a low-level promise wrapper around git clone, and lib/integration/PluginManagement/clone.js for the higher-level "clone plugin and write .bower.json metadata" operation. Refactor Plugin.js and Target.js to use these instead of inline exec calls. --- lib/integration/Plugin.js | 10 ++----- lib/integration/PluginManagement/clone.js | 27 +++++++++++++++++++ lib/integration/Target.js | 33 +++-------------------- lib/util/gitClone.js | 28 +++++++++++++++++++ 4 files changed, 60 insertions(+), 38 deletions(-) create mode 100644 lib/integration/PluginManagement/clone.js create mode 100644 lib/util/gitClone.js diff --git a/lib/integration/Plugin.js b/lib/integration/Plugin.js index 515c7b4..fc9a37f 100644 --- a/lib/integration/Plugin.js +++ b/lib/integration/Plugin.js @@ -6,7 +6,7 @@ import semver from 'semver' import fs from 'fs-extra' import path from 'path' import os from 'os' -import { exec } from 'child_process' +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 */ @@ -215,13 +215,7 @@ export default class Plugin { this._sourceInfo = null const tmpDir = path.join(os.tmpdir(), `adapt-git-${Date.now()}`) try { - const branchFlag = this.gitRef ? ` --branch ${this.gitRef}` : '' - await new Promise((resolve, reject) => { - exec(`git clone --depth 1${branchFlag} ${this.gitUrl} "${tmpDir}"`, (err) => { - if (err) return reject(err) - resolve() - }) - }) + 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) diff --git a/lib/integration/PluginManagement/clone.js b/lib/integration/PluginManagement/clone.js new file mode 100644 index 0000000..0ff67df --- /dev/null +++ b/lib/integration/PluginManagement/clone.js @@ -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 +} diff --git a/lib/integration/Target.js b/lib/integration/Target.js index 607382a..5c750f4 100644 --- a/lib/integration/Target.js +++ b/lib/integration/Target.js @@ -5,6 +5,7 @@ import semver from 'semver' import fs from 'fs-extra' import path from 'path' import { ADAPT_ALLOW_PRERELEASE } from '../util/constants.js' +import clonePlugin from './PluginManagement/clone.js' import Plugin from './Plugin.js' /** @typedef {import("./Project.js").default} Project */ const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE } @@ -177,25 +178,12 @@ export default class Target extends Plugin { return } if (this.isGitSource) { - await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder)) const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.packageName) - await fs.rm(pluginPath, { recursive: true, force: true }) - const branchFlag = this.gitRef ? ` --branch ${this.gitRef}` : '' try { - await new Promise((resolve, reject) => { - exec(`git clone${branchFlag} ${this.gitUrl} "${pluginPath}"`, (err) => { - if (err) return reject(err) - resolve() - }) - }) + await clonePlugin({ url: this.gitUrl, destPath: pluginPath, branch: this.gitRef }) } catch (error) { throw new Error(`The plugin was found but failed to clone from ${this.gitUrl}. Error ${error}`) } - const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) - bowerJSON._gitUrl = this.gitUrl - bowerJSON._gitRef = this.gitRef || undefined - bowerJSON._wasInstalledFromGitRepo = true - await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) this._projectInfo = null await this.fetchProjectInfo() return @@ -269,25 +257,10 @@ export default class Target extends Plugin { if (this.isGitSource) { this.preUpdateProjectVersion = this.projectVersion try { - await fs.rm(pluginPath, { recursive: true, force: true }) - } catch (err) { - throw new Error(`There was a problem writing to the target directory ${pluginPath}`) - } - try { - await new Promise((resolve, reject) => { - exec(`git clone ${this.gitUrl} "${pluginPath}"`, (err) => { - if (err) return reject(err) - resolve() - }) - }) + await clonePlugin({ url: this.gitUrl, destPath: pluginPath }) } catch (error) { throw new Error(`The plugin was found but failed to clone from ${this.gitUrl}. Error ${error}`) } - const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) - bowerJSON._gitUrl = this.gitUrl - bowerJSON._gitRef = this.gitRef || undefined - bowerJSON._wasInstalledFromGitRepo = true - await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) this._projectInfo = null await this.fetchProjectInfo() return diff --git a/lib/util/gitClone.js b/lib/util/gitClone.js new file mode 100644 index 0000000..3ce0a1e --- /dev/null +++ b/lib/util/gitClone.js @@ -0,0 +1,28 @@ +import { exec } from 'child_process' + +/** + * Clone a git repository + * @param {Object} options + * @param {string} options.url The repository URL + * @param {string} options.dir The target directory + * @param {string} [options.branch] Optional branch, tag, or ref to clone + * @param {boolean} [options.shallow=false] Whether to use --depth 1 + */ +export default async function gitClone ({ + url, + dir, + branch = null, + shallow = false +} = {}) { + const flags = [ + shallow && '--depth 1', + branch && `--branch ${branch}` + ].filter(Boolean).join(' ') + const cmd = `git clone${flags ? ' ' + flags : ''} ${url} "${dir}"` + await new Promise((resolve, reject) => { + exec(cmd, (err) => { + if (err) return reject(err) + resolve() + }) + }) +} From fa620b51f9f49ab3f502b04b68050af4a6022649 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 25 Feb 2026 11:35:48 +0000 Subject: [PATCH 4/4] Chore: Migrate remaining git clone calls to gitClone utility Update the dev-mode clone install in Target.js and the framework clone in AdaptFramework/clone.js to use the shared gitClone utility, eliminating all inline exec('git clone ...') calls. --- lib/integration/AdaptFramework/clone.js | 13 ++----------- lib/integration/Target.js | 21 ++++----------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/lib/integration/AdaptFramework/clone.js b/lib/integration/AdaptFramework/clone.js index 1fb1004..2c76d8b 100644 --- a/lib/integration/AdaptFramework/clone.js +++ b/lib/integration/AdaptFramework/clone.js @@ -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, @@ -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!') } diff --git a/lib/integration/Target.js b/lib/integration/Target.js index 5c750f4..fef7276 100644 --- a/lib/integration/Target.js +++ b/lib/integration/Target.js @@ -1,10 +1,10 @@ import chalk from 'chalk' import bower from 'bower' -import { exec } from 'child_process' import semver from 'semver' import fs from 'fs-extra' import path from 'path' import { ADAPT_ALLOW_PRERELEASE } from '../util/constants.js' +import gitClone from '../util/gitClone.js' import clonePlugin from './PluginManagement/clone.js' import Plugin from './Plugin.js' /** @typedef {import("./Project.js").default} Project */ @@ -196,26 +196,13 @@ export default class Target extends Plugin { const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.packageName) await fs.rm(pluginPath, { recursive: true, force: true }) const url = repoDetails.url.replace(/^git:\/\//, 'https://') + const branch = this.versionToApply !== '*' ? `v${this.versionToApply}` : null try { - const exitCode = await new Promise((resolve, reject) => { - try { - exec(`git clone ${url} "${pluginPath}"`, resolve) - } catch (err) { - reject(err) - } - }) - if (exitCode) throw new Error(`The plugin was found but failed to download and install. Exit code ${exitCode}`) + await gitClone({ url, dir: pluginPath, branch }) } catch (error) { throw new Error(`The plugin was found but failed to download and install. Error ${error}`) } - if (this.versionToApply !== '*') { - try { - await new Promise(resolve => exec(`git -C "${pluginPath}" checkout v${this.versionToApply}`, resolve)) - logger?.log(chalk.green(this.packageName), `is on branch "${this.versionToApply}".`) - } catch (err) { - throw new Error(chalk.yellow(this.packageName), `could not checkout branch "${this.versionToApply}".`) - } - } + if (branch) logger?.log(chalk.green(this.packageName), `is on branch "${this.versionToApply}".`) this._projectInfo = null await this.fetchProjectInfo() return