UNPKG

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