Files
vue-cli/packages/@vue/cli/lib/Upgrader.js
2020-02-06 19:02:35 +08:00

215 lines
6.7 KiB
JavaScript

const fs = require('fs')
const path = require('path')
const {
chalk,
semver,
log,
done,
logWithSpinner,
stopSpinner,
isPlugin,
resolvePluginId,
loadModule
} = require('@vue/cli-shared-utils')
const tryGetNewerRange = require('./util/tryGetNewerRange')
const getPkg = require('./util/getPkg')
const PackageManager = require('./util/ProjectPackageManager')
const { runMigrator } = require('./migrate')
function clearRequireCache () {
Object.keys(require.cache).forEach(key => delete require.cache[key])
}
module.exports = class Upgrader {
constructor (context = process.cwd()) {
this.context = context
this.pkg = getPkg(this.context)
this.pm = new PackageManager({ context })
}
async upgradeAll (includeNext) {
// TODO: should confirm for major version upgrades
// for patch & minor versions, upgrade directly
// for major versions, prompt before upgrading
const upgradable = await this.getUpgradable(includeNext)
if (!upgradable.length) {
done('Seems all plugins are up to date. Good work!')
return
}
for (const p of upgradable) {
// reread to avoid accidentally writing outdated package.json back
this.pkg = getPkg(this.context)
await this.upgrade(p.name, { to: p.latest })
}
done('All plugins are up to date!')
}
async upgrade (pluginId, options) {
const packageName = resolvePluginId(pluginId)
let depEntry, required
for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) {
if (this.pkg[depType] && this.pkg[depType][packageName]) {
depEntry = depType
required = this.pkg[depType][packageName]
break
}
}
if (!required) {
throw new Error(`Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('package.json')}`)
}
const installed = options.from || this.pm.getInstalledVersion(packageName)
if (!installed) {
throw new Error(
`Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('node_modules')}. Please install the dependencies first.\n` +
`Or to force upgrade, you can specify your current plugin version with the ${chalk.cyan('--from')} option`
)
}
let targetVersion = options.to || 'latest'
// if the targetVersion is not an exact version
if (!/\d+\.\d+\.\d+/.test(targetVersion)) {
if (targetVersion === 'latest') {
logWithSpinner(`Getting latest version of ${packageName}`)
} else {
logWithSpinner(`Getting max satisfying version of ${packageName}@${options.to}`)
}
targetVersion = await this.pm.getRemoteVersion(packageName, targetVersion)
if (!options.to && options.next) {
const next = await this.pm.getRemoteVersion(packageName, 'next')
if (next) {
targetVersion = semver.gte(targetVersion, next) ? targetVersion : next
}
}
stopSpinner()
}
if (targetVersion === installed) {
log(`Already installed ${packageName}@${targetVersion}`)
const newRange = tryGetNewerRange(`~${targetVersion}`, required)
if (newRange !== required) {
this.pkg[depEntry][packageName] = newRange
fs.writeFileSync(path.resolve(this.context, 'package.json'), JSON.stringify(this.pkg, null, 2))
log(`${chalk.green('✔')} Updated version range in ${chalk.yellow('package.json')}`)
}
return
}
log(`Upgrading ${packageName} from ${installed} to ${targetVersion}`)
await this.pm.upgrade(`${packageName}@~${targetVersion}`)
// as the dependencies have now changed, the require cache must be invalidated
// otherwise it may affect the behavior of the migrator
clearRequireCache()
// The cached `pkg` field won't automatically update after running `this.pm.upgrade`.
// Also, `npm install pkg@~version` won't replace the original `"pkg": "^version"` field.
// So we have to manually update `this.pkg` and write to the file system in `runMigrator`
this.pkg[depEntry][packageName] = `~${targetVersion}`
const noop = () => {}
const pluginMigrator =
loadModule(`${packageName}/migrator`, this.context) || noop
await runMigrator(
this.context,
{
id: packageName,
apply: pluginMigrator,
baseVersion: installed
},
this.pkg
)
}
async getUpgradable (includeNext) {
const upgradable = []
// get current deps
// filter @vue/cli-service, @vue/cli-plugin-* & vue-cli-plugin-*
for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) {
for (const [name, range] of Object.entries(this.pkg[depType] || {})) {
if (name !== '@vue/cli-service' && !isPlugin(name)) {
continue
}
const installed = await this.pm.getInstalledVersion(name)
const wanted = await this.pm.getRemoteVersion(name, range)
if (!installed) {
throw new Error(`At least one dependency can't be found. Please install the dependencies before trying to upgrade`)
}
let latest = await this.pm.getRemoteVersion(name)
if (includeNext) {
const next = await this.pm.getRemoteVersion(name, 'next')
if (next) {
latest = semver.gte(latest, next) ? latest : next
}
}
if (semver.lt(installed, latest)) {
// always list @vue/cli-service as the first one
// as it's depended by all other plugins
if (name === '@vue/cli-service') {
upgradable.unshift({ name, installed, wanted, latest })
} else {
upgradable.push({ name, installed, wanted, latest })
}
}
}
}
return upgradable
}
async checkForUpdates (includeNext) {
logWithSpinner('Gathering package information...')
const upgradable = await this.getUpgradable(includeNext)
stopSpinner()
if (!upgradable.length) {
done('Seems all plugins are up to date. Good work!')
return
}
// format the output
// adapted from @angular/cli
const names = upgradable.map(dep => dep.name)
let namePad = Math.max(...names.map(x => x.length)) + 2
if (!Number.isFinite(namePad)) {
namePad = 30
}
const pads = [namePad, 16, 16, 16, 0]
console.log(
' ' +
['Name', 'Installed', 'Wanted', 'Latest', 'Command to upgrade'].map(
(x, i) => chalk.underline(x.padEnd(pads[i]))
).join('')
)
for (const p of upgradable) {
const fields = [
p.name,
p.installed || 'N/A',
p.wanted,
p.latest,
`vue upgrade ${p.name}${includeNext ? ' --next' : ''}`
]
// TODO: highlight the diff part, like in `yarn outdated`
console.log(' ' + fields.map((x, i) => x.padEnd(pads[i])).join(''))
}
return upgradable
}
}