Upgrader.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. const fs = require('fs')
  2. const path = require('path')
  3. const {
  4. chalk,
  5. semver,
  6. log,
  7. done,
  8. logWithSpinner,
  9. stopSpinner,
  10. isPlugin,
  11. resolvePluginId,
  12. loadModule
  13. } = require('@vue/cli-shared-utils')
  14. const tryGetNewerRange = require('./util/tryGetNewerRange')
  15. const getPkg = require('./util/getPkg')
  16. const PackageManager = require('./util/ProjectPackageManager')
  17. const { runMigrator } = require('./migrate')
  18. function clearRequireCache () {
  19. Object.keys(require.cache).forEach(key => delete require.cache[key])
  20. }
  21. module.exports = class Upgrader {
  22. constructor (context = process.cwd()) {
  23. this.context = context
  24. this.pkg = getPkg(this.context)
  25. this.pm = new PackageManager({ context })
  26. }
  27. async upgradeAll (includeNext) {
  28. // TODO: should confirm for major version upgrades
  29. // for patch & minor versions, upgrade directly
  30. // for major versions, prompt before upgrading
  31. const upgradable = await this.getUpgradable(includeNext)
  32. if (!upgradable.length) {
  33. done('Seems all plugins are up to date. Good work!')
  34. return
  35. }
  36. for (const p of upgradable) {
  37. // reread to avoid accidentally writing outdated package.json back
  38. this.pkg = getPkg(this.context)
  39. await this.upgrade(p.name, { to: p.latest })
  40. }
  41. done('All plugins are up to date!')
  42. }
  43. async upgrade (pluginId, options) {
  44. const packageName = resolvePluginId(pluginId)
  45. let depEntry, required
  46. for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) {
  47. if (this.pkg[depType] && this.pkg[depType][packageName]) {
  48. depEntry = depType
  49. required = this.pkg[depType][packageName]
  50. break
  51. }
  52. }
  53. if (!required) {
  54. throw new Error(`Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('package.json')}`)
  55. }
  56. const installed = options.from || this.pm.getInstalledVersion(packageName)
  57. if (!installed) {
  58. throw new Error(
  59. `Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('node_modules')}. Please install the dependencies first.\n` +
  60. `Or to force upgrade, you can specify your current plugin version with the ${chalk.cyan('--from')} option`
  61. )
  62. }
  63. let targetVersion = options.to || 'latest'
  64. // if the targetVersion is not an exact version
  65. if (!/\d+\.\d+\.\d+/.test(targetVersion)) {
  66. if (targetVersion === 'latest') {
  67. logWithSpinner(`Getting latest version of ${packageName}`)
  68. } else {
  69. logWithSpinner(`Getting max satisfying version of ${packageName}@${options.to}`)
  70. }
  71. targetVersion = await this.pm.getRemoteVersion(packageName, targetVersion)
  72. if (!options.to && options.next) {
  73. const next = await this.pm.getRemoteVersion(packageName, 'next')
  74. if (next) {
  75. targetVersion = semver.gte(targetVersion, next) ? targetVersion : next
  76. }
  77. }
  78. stopSpinner()
  79. }
  80. if (targetVersion === installed) {
  81. log(`Already installed ${packageName}@${targetVersion}`)
  82. const newRange = tryGetNewerRange(`~${targetVersion}`, required)
  83. if (newRange !== required) {
  84. this.pkg[depEntry][packageName] = newRange
  85. fs.writeFileSync(path.resolve(this.context, 'package.json'), JSON.stringify(this.pkg, null, 2))
  86. log(`${chalk.green('✔')} Updated version range in ${chalk.yellow('package.json')}`)
  87. }
  88. return
  89. }
  90. log(`Upgrading ${packageName} from ${installed} to ${targetVersion}`)
  91. await this.pm.upgrade(`${packageName}@~${targetVersion}`)
  92. // as the dependencies have now changed, the require cache must be invalidated
  93. // otherwise it may affect the behavior of the migrator
  94. clearRequireCache()
  95. // The cached `pkg` field won't automatically update after running `this.pm.upgrade`.
  96. // Also, `npm install pkg@~version` won't replace the original `"pkg": "^version"` field.
  97. // So we have to manually update `this.pkg` and write to the file system in `runMigrator`
  98. this.pkg[depEntry][packageName] = `~${targetVersion}`
  99. const noop = () => {}
  100. const pluginMigrator =
  101. loadModule(`${packageName}/migrator`, this.context) || noop
  102. await runMigrator(
  103. this.context,
  104. {
  105. id: packageName,
  106. apply: pluginMigrator,
  107. baseVersion: installed
  108. },
  109. this.pkg
  110. )
  111. }
  112. async getUpgradable (includeNext) {
  113. const upgradable = []
  114. // get current deps
  115. // filter @vue/cli-service, @vue/cli-plugin-* & vue-cli-plugin-*
  116. for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) {
  117. for (const [name, range] of Object.entries(this.pkg[depType] || {})) {
  118. if (name !== '@vue/cli-service' && !isPlugin(name)) {
  119. continue
  120. }
  121. const installed = await this.pm.getInstalledVersion(name)
  122. const wanted = await this.pm.getRemoteVersion(name, range)
  123. if (!installed) {
  124. throw new Error(`At least one dependency can't be found. Please install the dependencies before trying to upgrade`)
  125. }
  126. let latest = await this.pm.getRemoteVersion(name)
  127. if (includeNext) {
  128. const next = await this.pm.getRemoteVersion(name, 'next')
  129. if (next) {
  130. latest = semver.gte(latest, next) ? latest : next
  131. }
  132. }
  133. if (semver.lt(installed, latest)) {
  134. // always list @vue/cli-service as the first one
  135. // as it's depended by all other plugins
  136. if (name === '@vue/cli-service') {
  137. upgradable.unshift({ name, installed, wanted, latest })
  138. } else {
  139. upgradable.push({ name, installed, wanted, latest })
  140. }
  141. }
  142. }
  143. }
  144. return upgradable
  145. }
  146. async checkForUpdates (includeNext) {
  147. logWithSpinner('Gathering package information...')
  148. const upgradable = await this.getUpgradable(includeNext)
  149. stopSpinner()
  150. if (!upgradable.length) {
  151. done('Seems all plugins are up to date. Good work!')
  152. return
  153. }
  154. // format the output
  155. // adapted from @angular/cli
  156. const names = upgradable.map(dep => dep.name)
  157. let namePad = Math.max(...names.map(x => x.length)) + 2
  158. if (!Number.isFinite(namePad)) {
  159. namePad = 30
  160. }
  161. const pads = [namePad, 16, 16, 16, 0]
  162. console.log(
  163. ' ' +
  164. ['Name', 'Installed', 'Wanted', 'Latest', 'Command to upgrade'].map(
  165. (x, i) => chalk.underline(x.padEnd(pads[i]))
  166. ).join('')
  167. )
  168. for (const p of upgradable) {
  169. const fields = [
  170. p.name,
  171. p.installed || 'N/A',
  172. p.wanted,
  173. p.latest,
  174. `vue upgrade ${p.name}${includeNext ? ' --next' : ''}`
  175. ]
  176. // TODO: highlight the diff part, like in `yarn outdated`
  177. console.log(' ' + fields.map((x, i) => x.padEnd(pads[i])).join(''))
  178. }
  179. return upgradable
  180. }
  181. }