123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- 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
- }
- }
|