Skip to content

Commit 29b9383

Browse files
committed
refactor: extract Upgrader to a standalone file for easier testing
1 parent 1b0562a commit 29b9383

File tree

4 files changed

+253
-251
lines changed

4 files changed

+253
-251
lines changed

packages/@vue/cli/__tests__/upgrade.spec.js renamed to packages/@vue/cli/__tests__/Upgrader.spec.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const fs = require('fs')
22
const path = require('path')
33
const create = require('@vue/cli-test-utils/createTestProject')
4-
// const { logs } = require('@vue/cli-shared-utils')
4+
const { logs } = require('@vue/cli-shared-utils')
5+
6+
const Upgrader = require('../lib/Upgrader')
57

68
jest.setTimeout(300000)
79

@@ -26,13 +28,12 @@ test('upgrade: plugin-babel v3.5', async () => {
2628
const pkg = JSON.parse(await project.read('package.json'))
2729
expect(pkg.dependencies).not.toHaveProperty('core-js')
2830

29-
await project.run(`${require.resolve('../bin/vue')} upgrade @vue/babel`)
31+
await (new Upgrader(project.dir)).upgrade('@vue/babel', {})
3032

3133
const updatedPkg = JSON.parse(await project.read('package.json'))
3234
expect(updatedPkg.dependencies).toHaveProperty('core-js')
3335

34-
// TODO: run upgrade in the same process so that we can access logs
35-
// expect(logs.log.some(([msg]) => msg.match('core-js has been upgraded'))).toBe(true)
36+
expect(logs.log.some(([msg]) => msg.match('core-js has been upgraded'))).toBe(true)
3637
})
3738

3839
test('upgrade: plugin-babel with core-js 2', async () => {
@@ -48,7 +49,7 @@ test('upgrade: plugin-babel with core-js 2', async () => {
4849
const pkg = JSON.parse(await project.read('package.json'))
4950
expect(pkg.dependencies['core-js']).toMatch('^2')
5051

51-
await project.run(`${require.resolve('../bin/vue')} upgrade @vue/babel --to next`)
52+
await (new Upgrader(project.dir)).upgrade('@vue/babel', {})
5253

5354
const updatedPkg = JSON.parse(await project.read('package.json'))
5455
expect(updatedPkg.dependencies['core-js']).toMatch('^3')

packages/@vue/cli/lib/Upgrader.js

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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

Comments
 (0)