Skip to content

Commit

Permalink
feat(cli): upgrade plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Chau committed Jan 21, 2019
1 parent 906b0b2 commit f75db0d
Show file tree
Hide file tree
Showing 11 changed files with 438 additions and 49 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@types/inquirer": "^0.0.43",
"@types/node": "^10.12.5",
"@types/ora": "^1.3.4",
"@types/lru-cache": "^4.1.1",
"@types/semver": "^5.5.0",
"@types/systeminformation": "^3.23.1",
"@types/webpack-chain": "^5.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/@nodepack/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"fs-extra": "^7.0.1",
"inquirer": "^6.2.1",
"lodash.clonedeep": "^4.5.0",
"semver": "^5.6.0",
"validate-npm-package-name": "^3.0.0"
}
}
23 changes: 23 additions & 0 deletions packages/@nodepack/cli/src/bin/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,29 @@ program
require('../commands/add')(pluginName, options)
})

program
.command('upgrade [plugins...]')
.description(`upgrade one or more plugins`)
// Version
.option('-w, --wanted', 'Use wanted versions')
.option('-l, --latest', 'Use latest versions (may inclide breaking changes!)')
.option('-y, --yes', 'Skip asking for upgrade confirmation')
// Install
.option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
.option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
.option('-x, --proxy', 'Use specified proxy when creating project')
// Git
.option('-g, --git [message]', 'Force git commit with message before maintenance')
.option('-n, --no-git', 'Skip git commit before maintenance')
.action((plugins, cmd) => {
const options = cleanArgs(cmd)
// --no-git makes commander to default git to true
if (process.argv.includes('-g') || process.argv.includes('--git')) {
options.forceGit = true
}
require('../commands/upgrade')(plugins, options)
})

program
.command('build')
.description('build your project using `nodepack-service build` in a project')
Expand Down
24 changes: 24 additions & 0 deletions packages/@nodepack/cli/src/commands/upgrade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { error, stopSpinner } = require('@nodepack/utils')
const PluginUpgradeJob = require('../lib/PluginUpgradeJob')

async function upgrade (plugins, options) {
if (options.proxy) {
process.env.HTTP_PROXY = options.proxy
}

const cwd = options.cwd || process.cwd()

const job = new PluginUpgradeJob(plugins, cwd)
await job.upgrade(options)
}

module.exports = (...args) => {
// @ts-ignore
return upgrade(...args).catch(err => {
stopSpinner(false) // do not persist
error(err)
if (!process.env.NODEPACK_TEST) {
process.exit(1)
}
})
}
8 changes: 2 additions & 6 deletions packages/@nodepack/cli/src/lib/PluginAddJob.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ const {
getPkgCommand,
installPackage,
getPackageTaggedVersion,
writePkg,
} = require('@nodepack/utils')
const fs = require('fs-extra')
const path = require('path')
const officialPluginShorthands = require('../util/officialPluginShorthands')

module.exports = class PluginAddJob {
Expand Down Expand Up @@ -49,10 +48,7 @@ module.exports = class PluginAddJob {
if (isTestOrDebug) {
pkg.devDependencies = pkg.devDependencies || {}
pkg.devDependencies[packageName] = await getPackageTaggedVersion(packageName).then(version => version && `^${version}`) || 'latest'
const pkgFile = path.resolve(cwd, 'package.json')
await fs.writeJson(pkgFile, pkg, {
spaces: 2,
})
writePkg(cwd, pkg)
if (!alreadyInPkg) {
plugins.push(packageName)
}
Expand Down
257 changes: 257 additions & 0 deletions packages/@nodepack/cli/src/lib/PluginUpgradeJob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/** @typedef {import('inquirer').Questions} Questions */
/** @typedef {import('inquirer').Question} Question */
/** @typedef {import('@nodepack/utils/src/deps').PackageVersionsInfo} PackageVersionsInfo */

/**
* @typedef UpdateInfo
* @prop {string} id
* @prop {string?} link
* @prop {string} versionRange
* @prop {PackageVersionsInfo} versionsInfo
* @prop {boolean} canUpdateWanted
* @prop {boolean} canUpdateLatest
* @prop {'dependencies' | 'devDependencies'} dependencyType
*/

/**
* @typedef QueuedUpdate
* @prop {string} version
* @prop {UpdateInfo} info
*/

const { Maintenance } = require('@nodepack/maintenance')
const {
resolvePluginId,
log,
logWithSpinner,
stopSpinner,
chalk,
getPackageMetadata,
getPackageVersionsInfo,
writePkg,
} = require('@nodepack/utils')
const officialPluginShorthands = require('../util/officialPluginShorthands')
const inquirer = require('inquirer')
const semver = require('semver')

module.exports = class PluginUpgradeJob {
/**
* @param {string[]} pluginNames
* @param {string} cwd
*/
constructor (pluginNames, cwd) {
this.packageNames = pluginNames.map(pluginName => {
if (officialPluginShorthands.includes(pluginName)) {
pluginName = `@nodepack/plugin-${pluginName}`
}
return resolvePluginId(pluginName)
})
this.cwd = cwd
}

/**
* @param {any} cliOptions Additional options
*/
async upgrade (cliOptions) {
const { packageNames, cwd } = this

/** @type {QueuedUpdate []} */
let queuedUpdates = []

const maintenance = new Maintenance({
cwd,
cliOptions,
skipPreInstall: true,
before: async ({ pkg, shouldCommitState, installDeps, isTestOrDebug }) => {
logWithSpinner(`🔄`, `Checking for plugin updates...`)
const { updateInfos, wantedUpgrades, latestUpgrades, totalUpgrades } = await this.resolveUpdates(pkg, packageNames)
stopSpinner()

// No updates
if (wantedUpgrades === 0 && latestUpgrades === 0) {
log(`${chalk.green('✔')} No plugin updates available.`)
process.exit()
}

if (!cliOptions.wanted && !cliOptions.latest) {
// Main action prompt
const mainChoices = []
if (wantedUpgrades) {
mainChoices.push({
name: chalk.green(`Update ${chalk.bold(wantedUpgrades.toString())} plugins to their wanted version`),
value: 'updateAllWanted',
})
}
mainChoices.push({
name: chalk.green(`Manually select for each plugin`),
value: 'manual',
})
if (latestUpgrades) {
mainChoices.push({
name: chalk.yellow(`Update ${chalk.bold(latestUpgrades.toString())} plugins to their latest version`),
value: 'updateAllLatest',
})
}
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `${chalk.bold(totalUpgrades.toString())} plugin updates available`,
choices: mainChoices,
},
])

if (action === 'manual') {
queuedUpdates = await this.selectUpdates(updateInfos)
} else if (action === 'updateAllWanted') {
queuedUpdates = updateInfos.filter(i => i.canUpdateWanted).map(i => ({
info: i,
version: this.getUpdatedVersionRange(i, i.versionsInfo.wanted) || i.versionRange,
}))
} else if (action === 'updateAllLatest') {
queuedUpdates = updateInfos.filter(i => i.canUpdateLatest).map(i => ({
info: i,
version: this.getUpdatedVersionRange(i, i.versionsInfo.latest) || 'latest',
}))
}

if (queuedUpdates.length) {
if (!cliOptions.yes) {
const { confirm } = await inquirer.prompt([{
name: 'confirm',
type: 'confirm',
message: `Confirm ${queuedUpdates.length} plugin update${queuedUpdates.length > 1 ? 's' : ''}?`,
}])
if (!confirm) process.exit()
}

await shouldCommitState()

for (const update of queuedUpdates) {
pkg[update.info.dependencyType][update.info.id] = update.version
}
writePkg(cwd, pkg)

if (!isTestOrDebug) {
await installDeps(`📦 Updating packages...`)
}
}
}
},
after: async maintenance => {
const count = queuedUpdates.length
log(`🎉 Successfully upgraded ${chalk.yellow(`${count} plugin${count > 1 ? 's' : ''}`)}.`)
},
})

await maintenance.run()
}

/**
* @param {any} pkg
* @param {string []} plugins
*/
async resolveUpdates (pkg, plugins) {
/** @type {UpdateInfo []} */
const updateInfos = []
let wantedUpgrades = 0
let latestUpgrades = 0
let totalUpgrades = 0
for (const id of plugins) {
const versionRange = pkg.dependencies[id] || pkg.devDependencies[id]
const versionsInfo = await getPackageVersionsInfo(this.cwd, id, versionRange)
let canUpdateWanted = false
let canUpdateLatest = false
if (versionsInfo.current !== null) {
canUpdateWanted = versionsInfo.current !== versionsInfo.wanted
canUpdateLatest = versionsInfo.current !== versionsInfo.latest
// Count
if (canUpdateWanted) wantedUpgrades++
if (canUpdateLatest) latestUpgrades++
if (canUpdateWanted || canUpdateLatest) totalUpgrades++
}
let link = null
const medata = await getPackageMetadata(id)
if (medata) {
link = medata.body.homepage
}
updateInfos.push({
id,
link,
versionRange,
versionsInfo,
canUpdateWanted,
canUpdateLatest,
dependencyType: id in pkg.dependencies ? 'dependencies' : 'devDependencies',
})
}
return {
updateInfos,
wantedUpgrades,
latestUpgrades,
totalUpgrades,
}
}

/**
* @param {UpdateInfo []} updateInfos
* @returns {Promise.<QueuedUpdate []>}
*/
async selectUpdates (updateInfos) {
/** @type {Question []} */
const prompts = []
for (const updateInfo of updateInfos) {
if (updateInfo.canUpdateWanted || updateInfo.canUpdateLatest) {
const choices = []
if (updateInfo.canUpdateWanted) {
choices.push({
name: chalk.green(`Update to wanted (${updateInfo.versionsInfo.wanted})`),
value: this.getUpdatedVersionRange(updateInfo, updateInfo.versionsInfo.wanted),
})
} if (updateInfo.canUpdateLatest) {
choices.push({
name: chalk.yellow(`Update to latest (${updateInfo.versionsInfo.latest})`),
value: this.getUpdatedVersionRange(updateInfo, updateInfo.versionsInfo.latest),
})
}
choices.push({ name: 'Skip', value: null })
prompts.push({
name: updateInfo.id,
type: 'list',
message: `${updateInfo.id} ${updateInfo.link ? `(${updateInfo.link}) ` : ''} ${chalk.grey(updateInfo.dependencyType)}`,
choices: [],
})
}
}
const answers = await inquirer.prompt(prompts)
const result = []
for (const id in answers) {
const version = answers[id]
const info = updateInfos.find(i => i.id === id)
if (version && info) {
result.push({
info,
version,
})
}
}
return result
}

/**
* @param {UpdateInfo} updateInfo
* @param {string?} version
*/
getUpdatedVersionRange (updateInfo, version) {
const { versionRange } = updateInfo
if (version && semver.validRange(versionRange)) {
const match = versionRange.match(/\d+/)
if (match) {
return `${versionRange.substr(0, match.index)}${version}`
} else {
return version
}
}
return versionRange
}
}
26 changes: 15 additions & 11 deletions packages/@nodepack/maintenance/src/lib/Maintenance.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,11 @@ class Maintenance {
await this.beforeHook(this)
}

const { cwd, cliOptions, plugins, packageManager } = this
const { cwd, plugins } = this

if (!this.skipPreInstall && !this.isTestOrDebug) {
// pre-run install to be sure everything is up-to-date
log(`📦 Checking dependencies installation...`)
if (!this.isTestOrDebug) {
await installDeps(cwd, packageManager, cliOptions.registry)
}
// pre-run install to be sure everything is up-to-date
if (!this.skipPreInstall) {
await this.installDeps(`📦 Checking dependencies installation...`)
}

// Run app migrations
Expand All @@ -112,10 +109,7 @@ class Maintenance {
this.results.appMigrationAllOptions = allOptions

// install additional deps (injected by migrations)
log(`📦 Installing additional dependencies...`)
if (!this.isTestOrDebug) {
await installDeps(cwd, packageManager, cliOptions.registry)
}
await this.installDeps(`📦 Installing additional dependencies...`)
}

// TODO Env Migrations
Expand Down Expand Up @@ -156,6 +150,16 @@ class Maintenance {
}
this.preCommitAttempted = true
}

/**
* @param {string?} message
*/
async installDeps (message = null) {
if (!this.isTestOrDebug) {
if (message) log(message)
await installDeps(this.cwd, this.packageManager, this.cliOptions.registry)
}
}
}

module.exports = Maintenance
Loading

0 comments on commit f75db0d

Please sign in to comment.