mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-01-21 06:39:56 -06:00
* fix: run migrator in a separator process, fix require cache issues during upgrade * fix: fix cwd * test: fix test mocks * fix: early return
225 lines
7.1 KiB
JavaScript
225 lines
7.1 KiB
JavaScript
const fs = require('fs')
|
|
const path = require('path')
|
|
const {
|
|
chalk,
|
|
execa,
|
|
semver,
|
|
|
|
log,
|
|
done,
|
|
logWithSpinner,
|
|
stopSpinner,
|
|
|
|
isPlugin,
|
|
resolvePluginId,
|
|
|
|
loadModule,
|
|
resolveModule
|
|
} = require('@vue/cli-shared-utils')
|
|
|
|
const tryGetNewerRange = require('./util/tryGetNewerRange')
|
|
const getPkg = require('./util/getPkg')
|
|
const PackageManager = require('./util/ProjectPackageManager')
|
|
|
|
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}`)
|
|
|
|
// 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 resolvedPluginMigrator =
|
|
resolveModule(`${packageName}/migrator`, this.context)
|
|
|
|
if (resolvedPluginMigrator) {
|
|
// for unit tests, need to run migrator in the same process for mocks to work
|
|
// TODO: fix the tests and remove this special case
|
|
if (process.env.VUE_CLI_TEST) {
|
|
clearRequireCache()
|
|
await require('./migrate').runMigrator(
|
|
this.context,
|
|
{
|
|
id: packageName,
|
|
apply: loadModule(`${packageName}/migrator`, this.context),
|
|
baseVersion: installed
|
|
},
|
|
this.pkg
|
|
)
|
|
return
|
|
}
|
|
|
|
const cliBin = path.resolve(__dirname, '../bin/vue.js')
|
|
// Run migrator in a separate process to avoid all kinds of require cache issues
|
|
await execa('node', [cliBin, 'migrate', packageName, '--from', installed], {
|
|
cwd: this.context,
|
|
stdio: 'inherit'
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|