diff --git a/lib/cli.js b/lib/cli.js index d232ea1..634bf42 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -12,6 +12,7 @@ import unregister from './commands/unregister.js' import update from './commands/update.js' import version from './commands/version.js' import logger from './logger.js' +import { isNPM } from './integration/PluginManagement/npm.js' import './econnreset.js' const commands = { @@ -53,6 +54,7 @@ class CLI { async execute () { try { + logger.info(`using ${await isNPM() ? 'NPM' : 'BOWER'} registry...`) if (!commands[this.command.name]) { const e = new Error(`Unknown command "${this.command.name}", please check the documentation.`) logger?.log(e.message) diff --git a/lib/integration/Plugin.js b/lib/integration/Plugin.js index b69ff3b..15fd554 100644 --- a/lib/integration/Plugin.js +++ b/lib/integration/Plugin.js @@ -5,6 +5,7 @@ import endpointParser from 'bower-endpoint-parser' import semver from 'semver' import fs from 'fs-extra' import path from 'path' +import { fetchAllInfo, fetchVersionInfo, fetchRepoUrl } from './PluginManagement/npm.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 */ @@ -57,7 +58,7 @@ export default class Plugin { const isLocalPath = (isNameAPath || isVersionAPath) if (isLocalPath) { // wait to name the plugin until the local config file is loaded - this.sourcePath = isNameAPath ? this.name : this.requestedVersion + this.sourcePath = path.resolve(this.cwd, isNameAPath ? this.name : this.requestedVersion) this.name = isVersionAPath ? this.packageName : '' this.packageName = isNameAPath ? '' : this.packageName this.requestedVersion = '*' @@ -79,7 +80,7 @@ export default class Plugin { * @returns {boolean|null} */ get isUpToDate () { - if (!this.hasFrameworkCompatibleVersion) return true; + if (!this.hasFrameworkCompatibleVersion) return true const canCheckSourceAgainstProject = (this.latestSourceVersion && this.projectVersion) if (!canCheckSourceAgainstProject) return null const isLatestVersion = (this.projectVersion === this.latestSourceVersion) @@ -178,7 +179,7 @@ export default class Plugin { async fetchSourceInfo () { if (this.isLocalSource) return await this.fetchLocalSourceInfo() - await this.fetchBowerInfo() + await this.fetchRegistryInfo() } async fetchLocalSourceInfo () { @@ -186,9 +187,11 @@ export default class Plugin { this._sourceInfo = null if (!this.isLocalSource) throw new Error('Plugin name or version must be a path to the source') if (this.isLocalSourceZip) throw new Error('Cannot install from zip files') + // TODO: sourcePath for adapt devinstall modules this._sourceInfo = await new Promise((resolve, reject) => { - // get bower.json data + // get package.json or bower.json data const paths = [ + path.resolve(this.cwd, `${this.sourcePath}/package.json`), path.resolve(this.cwd, `${this.sourcePath}/bower.json`) ] const bowerJSON = paths.reduce((bowerJSON, bowerJSONPath) => { @@ -196,6 +199,10 @@ export default class Plugin { if (!fs.existsSync(bowerJSONPath)) return null return fs.readJSONSync(bowerJSONPath) }, null) + const hasPackageJSON = fs.existsSync(paths[0]) + if (this.project.isNPM && !hasPackageJSON) { + fs.copySync(paths[1], paths[0]) + } resolve(bowerJSON) }) if (!this._sourceInfo) return @@ -204,12 +211,20 @@ export default class Plugin { this.packageName = this.name } - async fetchBowerInfo () { + async fetchRegistryInfo () { if (this._sourceInfo) return this._sourceInfo this._sourceInfo = null if (this.isLocalSource) return + const isNPM = await this.project.isNPM() const perform = async (attemptCount = 0) => { try { + if (isNPM) { + return await fetchAllInfo({ + logger: this.logger, + cwd: this.cwd, + packageName: this.packageName + }) + } return await new Promise((resolve, reject) => { bower.commands.info(`${this.packageName}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG }) .on('end', resolve) @@ -227,12 +242,19 @@ export default class Plugin { this._versionsInfo = info.versions.filter(version => semverOptions.includePrerelease ? true : !semver.prerelease(version)) } + async refetchProjectInfo () { + this._projectInfo = null + return this.fetchProjectInfo() + } + async fetchProjectInfo () { if (this._projectInfo) return this._projectInfo this._projectInfo = null this._projectInfo = await new Promise((resolve, reject) => { - // get bower.json data + // get package.json or bower.json data globs([ + `${this.cwd.replace(/\\/g, '/')}/src/node_modules/${this.packageName}/.package.json`, + `${this.cwd.replace(/\\/g, '/')}/src/node_modules/${this.packageName}/package.json`, `${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/.bower.json`, `${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/bower.json` ], (err, matches) => { @@ -242,8 +264,10 @@ export default class Plugin { if (!match) { // widen the search globs([ - `${this.cwd.replace(/\\/g, '/')}/src/**/.bower.json`, - `${this.cwd.replace(/\\/g, '/')}/src/**/bower.json` + `${this.cwd.replace(/\\/g, '/')}/src/node_modules/adapt-*/.package.json`, + `${this.cwd.replace(/\\/g, '/')}/src/node_modules/adapt-*/package.json`, + `${this.cwd.replace(/\\/g, '/')}/src/*/adapt-*/.bower.json`, + `${this.cwd.replace(/\\/g, '/')}/src/*/adapt-*/bower.json` ], (err, matches) => { if (err) return resolve(null) const tester = new RegExp(`/${this.packageName}/`, 'i') @@ -264,9 +288,18 @@ export default class Plugin { } async findCompatibleVersion (framework) { + const isNPM = await this.project.isNPM() const getBowerVersionInfo = async (version) => { const perform = async (attemptCount = 0) => { try { + if (isNPM) { + return await fetchVersionInfo({ + logger: this.logger, + cwd: this.cwd, + packageName: this.packageName, + version + }) + } return await new Promise((resolve, reject) => { bower.commands.info(`${this.packageName}@${version}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG }) .on('end', resolve) @@ -352,6 +385,15 @@ export default class Plugin { async getRepositoryUrl () { if (this._repositoryUrl) return this._repositoryUrl if (this.isLocalSource) return + const isNPM = await this.project.isNPM() + if (isNPM) { + const url = await fetchRepoUrl({ + logger: this.logger, + cwd: this.cwd, + packageName: this.packageName + }) + return (this._repositoryUrl = url) + } 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/bower.js b/lib/integration/PluginManagement/bower.js new file mode 100644 index 0000000..6037e45 --- /dev/null +++ b/lib/integration/PluginManagement/bower.js @@ -0,0 +1,20 @@ +import nodeFetch from 'node-fetch' +import getBowerRegistryConfig from '../getBowerRegistryConfig.js' + +export async function searchInfo ({ + logger, + cwd, + term +}) { + const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) + for (const url of BOWER_REGISTRY_CONFIG.search) { + try { + const pluginUrl = `${url}packages/search/${term}` + const req = await nodeFetch(pluginUrl) + const data = await req.json() + return data ?? [] + } catch (err) { + } + } + return [] +} diff --git a/lib/integration/PluginManagement/install.js b/lib/integration/PluginManagement/install.js index 1098bfd..de21ab2 100644 --- a/lib/integration/PluginManagement/install.js +++ b/lib/integration/PluginManagement/install.js @@ -8,6 +8,7 @@ import Target from '../Target.js' import bower from 'bower' import { difference } from 'lodash-es' import path from 'path' +import { install as npmInstall } from './npm.js' export default async function install ({ plugins, @@ -20,10 +21,15 @@ export default async function install ({ logger = null }) { cwd = path.resolve(process.cwd(), cwd) - isClean && await new Promise(resolve => bower.commands.cache.clean().on('end', resolve)) const project = new Project({ cwd, logger }) project.tryThrowInvalidPath() + const isNPM = await project.isNPM() + + !isNPM && isClean && await new Promise(resolve => { + bower.commands.cache.clean().on('end', resolve) + }) + logger?.log(chalk.cyan(`${dev ? 'cloning' : 'installing'} adapt dependencies...`)) const targets = await getInstallTargets({ logger, project, plugins, isCompatibleEnabled }) @@ -40,8 +46,26 @@ export default async function install ({ await eachOfSeriesProgress( installTargetsToBeInstalled, target => target.install({ clone: dev }), - percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Installing plugins ${percentage}% complete`) + percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Installing plugins ${(percentage / (isNPM ? 2 : 1))}% complete`) ) + if (isNPM) { + // Batch install npm plugins as it's faster + const installArgs = installTargetsToBeInstalled + .filter(target => target.isNPMInstall) + .map(target => { + if (target.isLocalSource) { + return `${target.sourcePath}` + } + return `${target.packageName}@${target.versionToApply}` + }) + const outputPath = path.join(cwd, 'src') + await npmInstall({ logger, cwd: outputPath, args: installArgs }) + await eachOfSeriesProgress( + installTargetsToBeInstalled, + target => target.postInstall(), + percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Installing plugins ${50 + (percentage / 2)}% complete`) + ) + } logger?.log(`${chalk.bold.cyan('')} Installing plugins 100% complete`) const manifestDependencies = await project.getManifestDependencies() await updateManifest({ logger, project, targets, manifestDependencies, isInteractive }) diff --git a/lib/integration/PluginManagement/npm.js b/lib/integration/PluginManagement/npm.js new file mode 100644 index 0000000..de06ff4 --- /dev/null +++ b/lib/integration/PluginManagement/npm.js @@ -0,0 +1,166 @@ +import { exec } from 'child_process' +import path from 'path' +import nodeFetch from 'node-fetch' +import getBowerRegistryConfig from '../getBowerRegistryConfig.js' +import semver from 'semver' + +const pluginCache = {} +let isNPMCache = null + +export function correctVersionNumbers (versionObjectHash) { + versionObjectHash = { ...versionObjectHash } + for (const version in versionObjectHash) { + if (semver.valid(version)) continue + delete versionObjectHash[version] + } + return versionObjectHash +} + +export async function isNPM ({ cwd = process.cwd() } = {}) { + if (isNPMCache !== null) return isNPMCache + const packageName = 'adapt-contrib-core' + const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) + for (const url of BOWER_REGISTRY_CONFIG.search) { + try { + const pluginUrl = `${url}npm/${packageName}` + const req = await nodeFetch(pluginUrl) + const data = await req.json() + isNPMCache = Boolean(typeof data === 'object' && data.name === packageName) + return isNPMCache + } catch (err) { + } + } + return (isNPMCache = false) +} + +export async function execute ({ + logger, + command, + cwd, + args = [] +} = {}) { + cwd = path.resolve(process.cwd(), cwd) + await new Promise((resolve, reject) => { + exec([(process.platform === 'win32' ? 'npm.cmd' : 'npm'), '--unsafe-perm', command, ...args].join(' '), { + cwd + }, (err, stdout, stderr) => { + if (!err) return resolve() + reject(stderr) + }) + }) +} + +export async function install ({ + logger, + cwd, + args = [] +} = {}) { + await execute({ logger, command: 'install', cwd, args: ['--omit=dev'].concat(args) }) +} + +export async function update ({ + logger, + cwd, + args = [] +} = {}) { + await execute({ logger, command: 'update', cwd, args }) +} + +export async function uninstall ({ + logger, + cwd, + args = [] +} = {}) { + await execute({ logger, command: 'uninstall', cwd, args }) +} + +export async function fetchAllInfo ({ + logger, + cwd, + packageName +}) { + const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) + let json + for (const url of BOWER_REGISTRY_CONFIG.search) { + try { + let data = pluginCache[packageName] + if (!data) { + const pluginUrl = `${url}npm/${packageName}` + const req = await nodeFetch(pluginUrl) + data = await req.json() + } + data.versions = correctVersionNumbers(data.versions) + const versions = Object.values(data.versions).map(item => item.version) + versions.sort((a, b) => semver.compare(a, b) * -1) + json = { + name: data.name, + versions, + latest: data.versions[data['dist-tags'].latest] + } + } catch (err) { + } + } + return json +} + +export async function fetchVersionInfo ({ + logger, + cwd, + packageName, + version +}) { + const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) + for (const url of BOWER_REGISTRY_CONFIG.search) { + try { + let data = pluginCache[packageName] + if (!data) { + const pluginUrl = `${url}npm/${packageName}` + const req = await nodeFetch(pluginUrl) + data = await req.json() + } + return data.versions[version] + } catch (err) { + } + } + return [] +} + +export async function fetchRepoUrl ({ + logger, + cwd, + packageName, + version +}) { + const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) + for (const url of BOWER_REGISTRY_CONFIG.search) { + try { + let data = pluginCache[packageName] + if (!data) { + const pluginUrl = `${url}npm/${packageName}` + const req = await nodeFetch(pluginUrl) + data = await req.json() + } + return data.repository + } catch (err) { + } + } + return null +} + +export async function searchInfo ({ + logger, + cwd, + term +}) { + const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) + for (const url of BOWER_REGISTRY_CONFIG.search) { + try { + const pluginUrl = `${url}npm/-/v1/search?text=${term}&size=100` + const req = await nodeFetch(pluginUrl) + const data = await req.json() + return data?.objects ?? [] + } catch (err) { + } + } + return [] +} diff --git a/lib/integration/PluginManagement/register.js b/lib/integration/PluginManagement/register.js index 879e639..1a5daa0 100644 --- a/lib/integration/PluginManagement/register.js +++ b/lib/integration/PluginManagement/register.js @@ -3,12 +3,15 @@ import getBowerRegistryConfig from '../getBowerRegistryConfig.js' import fs from 'fs-extra' import path from 'path' import bower from 'bower' +import fetch from 'node-fetch' import chalk from 'chalk' import inquirer from 'inquirer' +import globs from 'globs' import { readValidateJSON } from '../../util/JSONReadValidate.js' import Plugin from '../Plugin.js' import semver from 'semver' import { ADAPT_ALLOW_PRERELEASE } from '../../util/constants.js' +import { searchInfo, isNPM } from '../PluginManagement/npm.js' const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE } export default async function register ({ @@ -16,20 +19,26 @@ export default async function register ({ cwd = process.cwd() } = {}) { cwd = path.resolve(process.cwd(), cwd) + const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) logger?.warn('Using registry at', BOWER_REGISTRY_CONFIG.register) try { - const bowerJSONPath = path.join(cwd, 'bower.json') - const hasBowerJSON = fs.existsSync(bowerJSONPath) + const pluginJSONPath = await new Promise((resolve, reject) => { + globs(['package.json', 'bower.json'], { cwd }, (err, matches) => { + if (err) return reject(err) + resolve(matches?.[0]) + }) + }) + const hasPluginJSON = fs.existsSync(pluginJSONPath) - const bowerJSON = { + const pluginJSON = { name: undefined, repository: undefined, framework: undefined, - ...(hasBowerJSON ? await readValidateJSON(bowerJSONPath) : {}) + ...(hasPluginJSON ? await readValidateJSON(pluginJSONPath) : {}) } - const properties = await confirm(bowerJSON) - hasBowerJSON && await fs.writeJSON(bowerJSONPath, properties, { spaces: 2, replacer: null }) + const properties = await confirm(pluginJSON) + hasPluginJSON && await fs.writeJSON(pluginJSONPath, properties, { spaces: 2, replacer: null }) // given a package name, create two Plugin representations // if supplied name is adapt-contrib-myPackageName do a check against this name only @@ -37,15 +46,15 @@ export default async function register ({ // becase we don't want to allow adapt-myPackageName if adapt-contrib-myPackageName exists const plugin = new Plugin({ name: properties.name, logger }) const contribPlugin = new Plugin({ name: properties.name, isContrib: true, logger }) - const contribExists = await exists(BOWER_REGISTRY_CONFIG, contribPlugin) - const pluginExists = await exists(BOWER_REGISTRY_CONFIG, plugin) + const contribExists = await exists({ cwd, BOWER_REGISTRY_CONFIG, pluginName: contribPlugin }) + const pluginExists = await exists({ cwd, BOWER_REGISTRY_CONFIG, pluginName: plugin }) if (contribExists || pluginExists) { logger?.warn(plugin.toString(), 'has been previously registered. Plugin names must be unique. Try again with a different name.') return } - const result = await registerWithBowerRepo(BOWER_REGISTRY_CONFIG, plugin, properties.repository) + const result = await registerWithBowerRepo({ BOWER_REGISTRY_CONFIG, plugin, repository: properties.repository }) if (!result) throw new Error('The plugin was unable to register.') logger?.log(chalk.green(plugin.packageName), 'has been registered successfully.') } catch (err) { @@ -105,8 +114,15 @@ async function confirm (properties) { * @param {Plugin} plugin * @returns {boolean} */ -async function exists (BOWER_REGISTRY_CONFIG, plugin) { - const pluginName = plugin.toString().toLowerCase() +async function exists ({ cwd, BOWER_REGISTRY_CONFIG, pluginName }) { + pluginName = pluginName.toString().toLowerCase() + if (await isNPM()) { + return Boolean(await searchInfo({ + cwd, + registry: BOWER_REGISTRY_CONFIG.register, + term: pluginName + }).length) + } return new Promise((resolve, reject) => { bower.commands.search(pluginName, { registry: BOWER_REGISTRY_CONFIG.register @@ -119,7 +135,24 @@ async function exists (BOWER_REGISTRY_CONFIG, plugin) { }) } -async function registerWithBowerRepo (BOWER_REGISTRY_CONFIG, plugin, repository) { +async function registerWithBowerRepo ({ BOWER_REGISTRY_CONFIG, plugin, repository }) { + if (await isNPM()) { + const path = 'packages' + const response = await fetch(BOWER_REGISTRY_CONFIG.register + path, { + method: 'POST', + headers: { + 'User-Agent': 'adapt-cli', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: plugin.toString(), + url: repository.url + }), + followRedirect: false + }) + if (response.status === 201) return true + throw new Error(`The server responded with ${response.status}`) + } return new Promise((resolve, reject) => { bower.commands.register(plugin.toString(), repository.url || repository, { registry: BOWER_REGISTRY_CONFIG diff --git a/lib/integration/PluginManagement/rename.js b/lib/integration/PluginManagement/rename.js index 1ae33d5..9c216d5 100644 --- a/lib/integration/PluginManagement/rename.js +++ b/lib/integration/PluginManagement/rename.js @@ -1,11 +1,11 @@ import getBowerRegistryConfig from '../getBowerRegistryConfig.js' import authenticate from './autenticate.js' -import bower from 'bower' import chalk from 'chalk' import inquirer from 'inquirer' import fetch from 'node-fetch' import path from 'path' import Plugin from '../Plugin.js' +import { searchInfo } from './bower.js' export default async function rename ({ logger, @@ -25,9 +25,9 @@ export default async function rename ({ logger?.warn('Using registry at', BOWER_REGISTRY_CONFIG.register) logger?.warn(`Plugin will be renamed from ${oldName} to ${newName}`) try { - const oldExists = await exists(BOWER_REGISTRY_CONFIG, oldName) + const oldExists = await exists({ cwd, BOWER_REGISTRY_CONFIG, pluginName: oldName }) if (!oldExists) throw new Error(`Plugin "${oldName}" does not exist`) - const newExists = await exists(BOWER_REGISTRY_CONFIG, newName) + const newExists = await exists({ cwd, BOWER_REGISTRY_CONFIG, pluginName: newName }) if (newExists) throw new Error(`Name "${newName}" already exists`) const { username, token, type } = await authenticate({ pluginName: oldName }) logger?.log(`${username} authenticated as ${type}`) @@ -80,16 +80,13 @@ async function renameInBowerRepo ({ * @param {Plugin} plugin * @returns {boolean} */ -async function exists (BOWER_REGISTRY_CONFIG, plugin) { - const pluginName = plugin.toString().toLowerCase() - return new Promise((resolve, reject) => { - bower.commands.search(pluginName, { - registry: BOWER_REGISTRY_CONFIG.register - }) - .on('end', result => { - const matches = result.filter(({ name }) => name.toLowerCase() === pluginName) - resolve(Boolean(matches.length)) - }) - .on('error', reject) +async function exists ({ cwd, BOWER_REGISTRY_CONFIG, pluginName }) { + pluginName = pluginName.toString().toLowerCase() + const matches = await searchInfo({ + cwd, + registry: BOWER_REGISTRY_CONFIG.register, + term: pluginName }) + console.log(matches) + return Boolean(matches.length) } diff --git a/lib/integration/PluginManagement/uninstall.js b/lib/integration/PluginManagement/uninstall.js index 69a6bb8..c7d3435 100644 --- a/lib/integration/PluginManagement/uninstall.js +++ b/lib/integration/PluginManagement/uninstall.js @@ -7,6 +7,7 @@ import { createPromptTask } from '../../util/createPromptTask.js' import { errorPrinter, packageNamePrinter } from './print.js' import { intersection } from 'lodash-es' import path from 'path' +import { uninstall as npmUninstall } from './npm.js' export default async function uninstall ({ plugins, @@ -18,17 +19,33 @@ export default async function uninstall ({ const project = new Project({ cwd, logger }) project.tryThrowInvalidPath() + const isNPM = await project.isNPM() + logger?.log(chalk.cyan('uninstalling adapt dependencies...')) const targets = await getUninstallTargets({ logger, project, plugins, isInteractive }) if (!targets?.length) return targets await loadPluginData({ logger, targets }) + const uninstallTargetsToBeUninstalled = targets.filter(target => target.isToBeUninstalled) await eachOfLimitProgress( - targets.filter(target => target.isToBeUninstalled), + uninstallTargetsToBeUninstalled, target => target.uninstall(), - percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Uninstalling plugins ${percentage}% complete`) + percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Uninstalling plugins ${percentage / (isNPM ? 2 : 1)}% complete`) ) + if (isNPM) { + // Batch uninstall npm plugins as it's faster + const installArgs = uninstallTargetsToBeUninstalled + .filter(target => target.isNPMUninstall) + .map(target => `${target.packageName}`) + const outputPath = path.join(cwd, 'src') + await npmUninstall({ logger, cwd: outputPath, args: installArgs }) + await eachOfLimitProgress( + uninstallTargetsToBeUninstalled, + target => target.postUninstall(), + percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Uninstalling plugins ${50 + (percentage / 2)}% complete`) + ) + } logger?.log(`${chalk.bold.cyan('')} Uninstalling plugins 100% complete`) const installedDependencies = await project.getInstalledDependencies() await updateManifest({ project, targets, installedDependencies, isInteractive }) diff --git a/lib/integration/PluginManagement/unregister.js b/lib/integration/PluginManagement/unregister.js index 3170207..00234b0 100644 --- a/lib/integration/PluginManagement/unregister.js +++ b/lib/integration/PluginManagement/unregister.js @@ -7,6 +7,7 @@ import inquirer from 'inquirer' import { readValidateJSON } from '../../util/JSONReadValidate.js' import Plugin from '../Plugin.js' import fetch from 'node-fetch' +import globs from 'globs' export default async function unregister ({ logger, @@ -17,11 +18,17 @@ export default async function unregister ({ const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd }) logger?.warn('Using registry at', BOWER_REGISTRY_CONFIG.register) try { - const bowerJSONPath = path.join(cwd, 'bower.json') - const hasBowerJSON = fs.existsSync(bowerJSONPath) - const bowerJSON = hasBowerJSON ? await readValidateJSON(bowerJSONPath) : {} - if (pluginName) bowerJSON.name = pluginName - const props = await confirm(bowerJSON) + const pluginJSONPath = await new Promise((resolve, reject) => { + globs(['package.json', 'bower.json'], { cwd }, (err, matches) => { + if (err) return reject(err) + resolve(matches?.[0]) + }) + }) + + const hasPluginJSON = fs.existsSync(pluginJSONPath) + const pluginJSON = hasPluginJSON ? await readValidateJSON(pluginJSONPath) : {} + if (pluginName) pluginJSON.name = pluginName + const props = await confirm(pluginJSON) pluginName = props.pluginName const repository = props.repository const { username, token, type } = await authenticate({ repository, pluginName }) @@ -84,7 +91,9 @@ async function unregisterInBowerRepo ({ const uri = `${BOWER_REGISTRY_CONFIG.register}packages/${username}/${pluginName}?access_token=${token}` const response = await fetch(uri, { method: 'DELETE', - headers: { 'User-Agent': 'adapt-cli' }, + headers: { + 'User-Agent': 'adapt-cli' + }, followRedirect: false }) if (response.status !== 204) throw new Error(`The server responded with ${response.status}`) diff --git a/lib/integration/PluginManagement/update.js b/lib/integration/PluginManagement/update.js index c89df22..332054e 100644 --- a/lib/integration/PluginManagement/update.js +++ b/lib/integration/PluginManagement/update.js @@ -5,6 +5,7 @@ import Project from '../Project.js' import { createPromptTask } from '../../util/createPromptTask.js' import { errorPrinter, packageNamePrinter, versionPrinter, existingVersionPrinter } from './print.js' import { eachOfLimitProgress, eachOfSeriesProgress } from '../../util/promises.js' +import { update as npmUpdate } from './npm.js' /** @typedef {import("../Target.js").default} Target */ export default async function update ({ @@ -19,6 +20,8 @@ export default async function update ({ const project = new Project({ cwd, logger }) project.tryThrowInvalidPath() + const isNPM = await project.isNPM() + logger?.log(chalk.cyan('update adapt dependencies...')) const targets = await getUpdateTargets({ logger, project, plugins, isDryRun, isInteractive }) @@ -35,8 +38,21 @@ export default async function update ({ await eachOfSeriesProgress( updateTargetsToBeUpdated, target => target.update(), - percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Updating plugins ${percentage}% complete`) + percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Updating plugins ${percentage / (isNPM ? 2 : 1)}% complete`) ) + if (isNPM) { + // Batch update npm plugins as it's faster + const installArgs = updateTargetsToBeUpdated + .filter(target => target.isNPMUpdate) + .map(target => `${target.packageName}`) + const outputPath = path.join(cwd, 'src') + await npmUpdate({ logger, cwd: outputPath, args: installArgs }) + await eachOfSeriesProgress( + updateTargetsToBeUpdated, + target => target.postUpdate(), + percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Updating plugins ${50 + (percentage / 2)}% complete`) + ) + } logger?.log(`${chalk.bold.cyan('')} Updating plugins 100% complete`) } await summariseUpdates({ logger, targets }) diff --git a/lib/integration/Project.js b/lib/integration/Project.js index 528f4e0..cd111fe 100644 --- a/lib/integration/Project.js +++ b/lib/integration/Project.js @@ -4,6 +4,7 @@ import globs from 'globs' import { readValidateJSON, readValidateJSONSync } from '../util/JSONReadValidate.js' import Plugin from './Plugin.js' import Target from './Target.js' +import { isNPM } from './PluginManagement/npm.js' export const MANIFEST_FILENAME = 'adapt.json' export const FRAMEWORK_FILENAME = 'package.json' @@ -48,6 +49,16 @@ export default class Project { } } + async isNPM () { + if (this._isNPM !== undefined) return this._isNPM + const isProjectNPM = fs.existsSync(path.join(this.cwd, 'src/package.json')) + const isServerNPM = await isNPM() + if (isProjectNPM && !isServerNPM) { + throw new Error('Project is NPM, registry server is BOWER') + } + return (this._isNPM = isProjectNPM) + } + tryThrowInvalidPath () { if (this.containsManifestFile) return this.logger?.error('Fatal error: please run above commands in adapt course directory.') @@ -82,9 +93,12 @@ export default class Project { async getInstalledDependencies () { const getDependencyBowerJSONs = async () => { - const glob = `${this.cwd.replace(/\\/g, '/')}/src/**/bower.json` + const paths = [ + this.isNPM && `${this.cwd.replace(/\\/g, '/')}/src/node_modules/adapt-*/package.json`, + !this.isNPM && `${this.cwd.replace(/\\/g, '/')}/src/*/adapt-*/bower.json` + ].filter(Boolean) const bowerJSONPaths = await new Promise((resolve, reject) => { - globs(glob, (err, matches) => { + globs(paths, (err, matches) => { if (err) return reject(err) resolve(matches) }) diff --git a/lib/integration/Target.js b/lib/integration/Target.js index 1bea515..f542bff 100644 --- a/lib/integration/Target.js +++ b/lib/integration/Target.js @@ -158,26 +158,83 @@ export default class Target extends Plugin { async install ({ clone = false } = {}) { const logger = this.logger - const pluginTypeFolder = await this.getTypeFolder() - if (this.isLocalSource) { - await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder)) - const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.name) - await fs.rm(pluginPath, { recursive: true, force: true }) - await fs.copy(this.sourcePath, pluginPath, { recursive: true }) - const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) - bowerJSON._source = this.sourcePath - bowerJSON._wasInstalledFromPath = true - await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) - this._projectInfo = null - await this.fetchProjectInfo() + const isNPM = await this.project.isNPM() + if (!isNPM) { + const pluginTypeFolder = await this.getTypeFolder() + if (this.isLocalSource) { + await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder)) + const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.name) + await fs.rm(pluginPath, { recursive: true, force: true }) + await fs.copy(this.sourcePath, pluginPath, { recursive: true }) + const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) + bowerJSON._source = this.sourcePath + bowerJSON._wasInstalledFromPath = true + await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) + await this.refetchProjectInfo() + return + } + if (clone) { + // clone install + const repoDetails = await this.getRepositoryUrl() + if (!repoDetails) throw new Error('Error: Plugin repository url could not be found.') + await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder)) + const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.name) + await fs.rm(pluginPath, { recursive: true, force: true }) + const url = repoDetails.url.replace(/^git:\/\//, 'https://') + 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}`) + } 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}".`) + } + } + await this.refetchProjectInfo() + return + } + // bower install + const outputPath = path.join(this.cwd, 'src', pluginTypeFolder) + const pluginPath = path.join(outputPath, this.name) + try { + await fs.rm(pluginPath, { recursive: true, force: true }) + } catch (err) { + throw new Error(`There was a problem writing to the target directory ${pluginPath}`) + } + await new Promise((resolve, reject) => { + const pluginNameVersion = `${this.packageName}@${this.versionToApply}` + bower.commands.install([pluginNameVersion], null, { + directory: outputPath, + cwd: this.cwd, + registry: this.BOWER_REGISTRY_CONFIG + }) + .on('end', resolve) + .on('error', err => { + err = new Error(`Bower reported ${err}`) + this._error = err + reject(err) + }) + }) + await this.postInstall() return } - if (clone) { - // clone install + if (isNPM && clone) { + // npm devinstall const repoDetails = await this.getRepositoryUrl() if (!repoDetails) throw new Error('Error: Plugin repository url could not be found.') - await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder)) - const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.name) + await fs.ensureDir(path.resolve(this.cwd, 'src/custom')) + const pluginPath = path.resolve(this.cwd, 'src/custom', this.name) await fs.rm(pluginPath, { recursive: true, force: true }) const url = repoDetails.url.replace(/^git:\/\//, 'https://') try { @@ -200,77 +257,93 @@ export default class Target extends Plugin { throw new Error(chalk.yellow(this.packageName), `could not checkout branch "${this.versionToApply}".`) } } - this._projectInfo = null - await this.fetchProjectInfo() + await this.refetchProjectInfo() + this.sourcePath = pluginPath + } + this.isNPMInstall = true + } + + async postInstall () { + const isNPM = await this.project.isNPM() + if (!isNPM) { + const pluginTypeFolder = await this.getTypeFolder() + const outputPath = path.join(this.cwd, 'src', pluginTypeFolder) + const pluginPath = path.join(outputPath, this.name) + const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) + bowerJSON.version = bowerJSON.version ?? this.versionToApply + await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) + await this.refetchProjectInfo() return } - // bower install - const outputPath = path.join(this.cwd, 'src', pluginTypeFolder) - const pluginPath = path.join(outputPath, this.name) - try { - await fs.rm(pluginPath, { recursive: true, force: true }) - } catch (err) { - throw new Error(`There was a problem writing to the target directory ${pluginPath}`) + const outputPath = path.join(this.cwd, 'src') + const pluginPath = path.join(outputPath, 'node_modules', this.name) + const bowerJSON = await fs.readJSON(path.join(pluginPath, 'package.json')) + bowerJSON.version = bowerJSON.version ?? this.versionToApply + if (this.isLocalSource) { + bowerJSON._source = this.sourcePath + bowerJSON._wasInstalledFromPath = true } - await new Promise((resolve, reject) => { - const pluginNameVersion = `${this.packageName}@${this.versionToApply}` - bower.commands.install([pluginNameVersion], null, { - directory: outputPath, - cwd: this.cwd, - registry: this.BOWER_REGISTRY_CONFIG - }) - .on('end', resolve) - .on('error', err => { - err = new Error(`Bower reported ${err}`) - this._error = err - reject(err) - }) - }) - const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json')) - bowerJSON.version = bowerJSON.version ?? this.versionToApply; - await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null }) - this._projectInfo = null - await this.fetchProjectInfo() + await fs.writeJSON(path.join(pluginPath, '.package.json'), bowerJSON, { spaces: 2, replacer: null }) + await this.refetchProjectInfo() } async update () { if (!this.isToBeUpdated) throw new Error() - const typeFolder = await this.getTypeFolder() - const outputPath = path.join(this.cwd, 'src', typeFolder) - const pluginPath = path.join(outputPath, this.name) - try { - await fs.rm(pluginPath, { recursive: true, force: true }) - } catch (err) { - throw new Error(`There was a problem writing to the target directory ${pluginPath}`) - } - await new Promise((resolve, reject) => { - const pluginNameVersion = `${this.packageName}@${this.matchedVersion}` - bower.commands.install([pluginNameVersion], null, { - directory: outputPath, - cwd: this.cwd, - registry: this.BOWER_REGISTRY_CONFIG - }) - .on('end', resolve) - .on('error', err => { - err = new Error(`Bower reported ${err}`) - this._error = err - reject(err) + const isNPM = await this.project.isNPM() + if (!isNPM) { + // bower update + const typeFolder = await this.getTypeFolder() + const outputPath = path.join(this.cwd, 'src', typeFolder) + const pluginPath = path.join(outputPath, this.name) + try { + await fs.rm(pluginPath, { recursive: true, force: true }) + } catch (err) { + throw new Error(`There was a problem writing to the target directory ${pluginPath}`) + } + await new Promise((resolve, reject) => { + const pluginNameVersion = `${this.packageName}@${this.matchedVersion}` + bower.commands.install([pluginNameVersion], null, { + directory: outputPath, + cwd: this.cwd, + registry: this.BOWER_REGISTRY_CONFIG }) - }) + .on('end', resolve) + .on('error', err => { + err = new Error(`Bower reported ${err}`) + this._error = err + reject(err) + }) + }) + await this.postUpdate() + return + } + this.isNPMUpdate = true + } + + async postUpdate () { this.preUpdateProjectVersion = this.projectVersion - this._projectInfo = null - await this.fetchProjectInfo() + await this.refetchProjectInfo() } async uninstall () { - try { - if (!this.isToBeUninstalled) throw new Error() - await fs.rm(this.projectPath, { recursive: true, force: true }) - this._wasUninstalled = true - } catch (err) { - this._wasUninstalled = false - throw new Error(`There was a problem writing to the target directory ${this.projectPath}`) + const isNPM = await this.project.isNPM() + if (!isNPM) { + // bower uninstall + try { + if (!this.isToBeUninstalled) throw new Error() + await fs.rm(this.projectPath, { recursive: true, force: true }) + this._wasUninstalled = true + } catch (err) { + this._wasUninstalled = false + throw new Error(`There was a problem writing to the target directory ${this.projectPath}`) + } + return } + this.isNPMUninstall = true + } + + async postUninstall () { + this._wasUninstalled = true } isNameMatch (name) { diff --git a/lib/logger.js b/lib/logger.js index ace74ca..150503d 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -9,10 +9,13 @@ export default { this.write(args.join(' ')) }, warn (...args) { - chalk.yellow(...args) + console.log(chalk.yellow(...args)) }, error (...args) { - chalk.red(...args) + console.log(chalk.red(...args)) + }, + info (...args) { + console.log(chalk.cyan(...args)) }, log (...args) { if (this.isLoggingProgress) {