|
| 1 | +const fs = require('fs') |
| 2 | +const path = require('path') |
| 3 | +const chalk = require('chalk') |
| 4 | +const execa = require('execa') |
| 5 | +const { |
| 6 | + log, |
| 7 | + done, |
| 8 | + |
| 9 | + logWithSpinner, |
| 10 | + stopSpinner, |
| 11 | + |
| 12 | + isPlugin, |
| 13 | + resolvePluginId, |
| 14 | + loadModule, |
| 15 | + |
| 16 | + hasProjectGit |
| 17 | +} = require('@vue/cli-shared-utils') |
| 18 | + |
| 19 | +const Migrator = require('./Migrator') |
| 20 | +const tryGetNewerRange = require('./util/tryGetNewerRange') |
| 21 | +const readFiles = require('./util/readFiles') |
| 22 | + |
| 23 | +const getPackageJson = require('./util/getPackageJson') |
| 24 | +const PackageManager = require('./util/ProjectPackageManager') |
| 25 | + |
| 26 | +const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG |
| 27 | + |
| 28 | +module.exports = class Upgrader { |
| 29 | + constructor (context = process.cwd()) { |
| 30 | + this.context = context |
| 31 | + this.pkg = getPackageJson(this.context) |
| 32 | + this.pm = new PackageManager({ context }) |
| 33 | + } |
| 34 | + |
| 35 | + async upgradeAll () { |
| 36 | + // TODO: should confirm for major version upgrades |
| 37 | + // for patch & minor versions, upgrade directly |
| 38 | + // for major versions, prompt before upgrading |
| 39 | + const upgradable = await this.getUpgradable() |
| 40 | + |
| 41 | + if (!upgradable.length) { |
| 42 | + done('Seems all plugins are up to date. Good work!') |
| 43 | + return |
| 44 | + } |
| 45 | + |
| 46 | + for (const p of upgradable) { |
| 47 | + await this.upgrade(p.name, { to: p.latest }) |
| 48 | + } |
| 49 | + |
| 50 | + done('All plugins are up to date!') |
| 51 | + } |
| 52 | + |
| 53 | + async upgrade (pluginId, options) { |
| 54 | + const packageName = resolvePluginId(pluginId) |
| 55 | + |
| 56 | + let depEntry, required |
| 57 | + for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) { |
| 58 | + if (this.pkg[depType] && this.pkg[depType][packageName]) { |
| 59 | + depEntry = depType |
| 60 | + required = this.pkg[depType][packageName] |
| 61 | + break |
| 62 | + } |
| 63 | + } |
| 64 | + if (!required) { |
| 65 | + throw new Error(`Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('package.json')}`) |
| 66 | + } |
| 67 | + |
| 68 | + let targetVersion = options.to || 'latest' |
| 69 | + // if the targetVersion is not an exact version |
| 70 | + if (!/\d+\.\d+\.\d+/.test(targetVersion)) { |
| 71 | + if (targetVersion === 'latest') { |
| 72 | + logWithSpinner(`Getting latest version of ${packageName}`) |
| 73 | + } else { |
| 74 | + logWithSpinner(`Getting max satisfying version of ${packageName}@${options.to}`) |
| 75 | + } |
| 76 | + |
| 77 | + targetVersion = await this.pm.getRemoteVersion(packageName, targetVersion) |
| 78 | + stopSpinner() |
| 79 | + } |
| 80 | + |
| 81 | + const installed = this.pm.getInstalledVersion(packageName) |
| 82 | + if (targetVersion === installed) { |
| 83 | + log(`Already installed ${packageName}@${targetVersion}`) |
| 84 | + |
| 85 | + const newRange = tryGetNewerRange(`^${targetVersion}`, required) |
| 86 | + if (newRange !== required) { |
| 87 | + this.pkg[depEntry][packageName] = newRange |
| 88 | + fs.writeFileSync(path.resolve(this.context, 'package.json'), JSON.stringify(this.pkg, null, 2)) |
| 89 | + log(`${chalk.green('✔')} Updated version range in ${chalk.yellow('package.json')}`) |
| 90 | + } |
| 91 | + return |
| 92 | + } |
| 93 | + |
| 94 | + log(`Upgrading ${packageName} from ${installed} to ${targetVersion}`) |
| 95 | + await this.pm.upgrade(`${packageName}@^${targetVersion}`) |
| 96 | + |
| 97 | + await this.runMigrator(packageName, { installed }) |
| 98 | + } |
| 99 | + |
| 100 | + async runMigrator (packageName, options) { |
| 101 | + const pluginMigrator = loadModule(`${packageName}/migrator`, this.context) |
| 102 | + if (!pluginMigrator) { return } |
| 103 | + |
| 104 | + const plugin = { |
| 105 | + id: packageName, |
| 106 | + apply: pluginMigrator, |
| 107 | + installed: options.installed |
| 108 | + } |
| 109 | + |
| 110 | + const createCompleteCbs = [] |
| 111 | + const migrator = new Migrator(this.context, { |
| 112 | + plugin: plugin, |
| 113 | + |
| 114 | + pkg: this.pkg, |
| 115 | + files: await readFiles(this.context), |
| 116 | + completeCbs: createCompleteCbs, |
| 117 | + invoking: true |
| 118 | + }) |
| 119 | + |
| 120 | + log(`🚀 Running migrator of ${packageName}`) |
| 121 | + await migrator.generate({ |
| 122 | + extractConfigFiles: true, |
| 123 | + checkExisting: true |
| 124 | + }) |
| 125 | + |
| 126 | + const newDeps = migrator.pkg.dependencies |
| 127 | + const newDevDeps = migrator.pkg.devDependencies |
| 128 | + const depsChanged = |
| 129 | + JSON.stringify(newDeps) !== JSON.stringify(this.pkg.dependencies) || |
| 130 | + JSON.stringify(newDevDeps) !== JSON.stringify(this.pkg.devDependencies) |
| 131 | + |
| 132 | + if (!isTestOrDebug && depsChanged) { |
| 133 | + log(`📦 Installing additional dependencies...`) |
| 134 | + log() |
| 135 | + await this.pm.install() |
| 136 | + } |
| 137 | + |
| 138 | + if (createCompleteCbs.length) { |
| 139 | + logWithSpinner('⚓', `Running completion hooks...`) |
| 140 | + for (const cb of createCompleteCbs) { |
| 141 | + await cb() |
| 142 | + } |
| 143 | + stopSpinner() |
| 144 | + log() |
| 145 | + } |
| 146 | + |
| 147 | + log(`${chalk.green('✔')} Successfully invoked migrator for plugin: ${chalk.cyan(plugin.id)}`) |
| 148 | + if (!process.env.VUE_CLI_TEST && hasProjectGit(this.context)) { |
| 149 | + const { stdout } = await execa('git', [ |
| 150 | + 'ls-files', |
| 151 | + '--exclude-standard', |
| 152 | + '--modified', |
| 153 | + '--others' |
| 154 | + ], { |
| 155 | + cwd: this.context |
| 156 | + }) |
| 157 | + if (stdout.trim()) { |
| 158 | + log(` The following files have been updated / added:\n`) |
| 159 | + log( |
| 160 | + chalk.red( |
| 161 | + stdout |
| 162 | + .split(/\r?\n/g) |
| 163 | + .map(line => ` ${line}`) |
| 164 | + .join('\n') |
| 165 | + ) |
| 166 | + ) |
| 167 | + log() |
| 168 | + log( |
| 169 | + ` You should review these changes with ${chalk.cyan( |
| 170 | + `git diff` |
| 171 | + )} and commit them.` |
| 172 | + ) |
| 173 | + log() |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + migrator.printExitLogs() |
| 178 | + } |
| 179 | + |
| 180 | + async getUpgradable () { |
| 181 | + const upgradable = [] |
| 182 | + |
| 183 | + // get current deps |
| 184 | + // filter @vue/cli-service, @vue/cli-plugin-* & vue-cli-plugin-* |
| 185 | + for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) { |
| 186 | + for (const [name, range] of Object.entries(this.pkg[depType] || {})) { |
| 187 | + if (name !== '@vue/cli-service' && !isPlugin(name)) { |
| 188 | + continue |
| 189 | + } |
| 190 | + |
| 191 | + const installed = await this.pm.getInstalledVersion(name) |
| 192 | + const wanted = await this.pm.getRemoteVersion(name, range) |
| 193 | + |
| 194 | + const latest = await this.pm.getRemoteVersion(name) |
| 195 | + |
| 196 | + if (installed !== latest) { |
| 197 | + // always list @vue/cli-service as the first one |
| 198 | + // as it's depended by all other plugins |
| 199 | + if (name === '@vue/cli-service') { |
| 200 | + upgradable.unshift({ name, installed, wanted, latest }) |
| 201 | + } else { |
| 202 | + upgradable.push({ name, installed, wanted, latest }) |
| 203 | + } |
| 204 | + } |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + return upgradable |
| 209 | + } |
| 210 | + |
| 211 | + async checkForUpdates () { |
| 212 | + logWithSpinner('Gathering package information...') |
| 213 | + const upgradable = await this.getUpgradable() |
| 214 | + stopSpinner() |
| 215 | + |
| 216 | + if (!upgradable.length) { |
| 217 | + done('Seems all plugins are up to date. Good work!') |
| 218 | + return |
| 219 | + } |
| 220 | + |
| 221 | + // format the output |
| 222 | + // adapted from @angular/cli |
| 223 | + const names = upgradable.map(dep => dep.name) |
| 224 | + let namePad = Math.max(...names.map(x => x.length)) + 2 |
| 225 | + if (!Number.isFinite(namePad)) { |
| 226 | + namePad = 30 |
| 227 | + } |
| 228 | + const pads = [namePad, 12, 12, 12, 0] |
| 229 | + console.log( |
| 230 | + ' ' + |
| 231 | + ['Name', 'Installed', 'Wanted', 'Latest', 'Command to upgrade'].map( |
| 232 | + (x, i) => chalk.underline(x.padEnd(pads[i])) |
| 233 | + ).join('') |
| 234 | + ) |
| 235 | + for (const p of upgradable) { |
| 236 | + const fields = [p.name, p.installed, p.wanted, p.latest, `vue upgrade ${p.name}`] |
| 237 | + // TODO: highlight the diff part, like in `yarn outdated` |
| 238 | + console.log(' ' + fields.map((x, i) => x.padEnd(pads[i])).join('')) |
| 239 | + } |
| 240 | + |
| 241 | + console.log(`Run ${chalk.yellow('vue upgrade --all')} to upgrade all the above plugins`) |
| 242 | + |
| 243 | + return upgradable |
| 244 | + } |
| 245 | +} |
0 commit comments