1 | const fs = require('fs')
|
2 | const path = require('path')
|
3 | const {
|
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 |
|
20 | const tryGetNewerRange = require('./util/tryGetNewerRange')
|
21 | const getPkg = require('./util/getPkg')
|
22 | const PackageManager = require('./util/ProjectPackageManager')
|
23 |
|
24 | function clearRequireCache () {
|
25 | Object.keys(require.cache).forEach(key => delete require.cache[key])
|
26 | }
|
27 | module.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 |
|
36 |
|
37 |
|
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 |
|
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 |
|
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 |
|
112 |
|
113 |
|
114 | this.pkg[depEntry][packageName] = `~${targetVersion}`
|
115 |
|
116 | const resolvedPluginMigrator =
|
117 | resolveModule(`${packageName}/migrator`, this.context)
|
118 |
|
119 | if (resolvedPluginMigrator) {
|
120 |
|
121 |
|
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 |
|
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 |
|
149 |
|
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 |
|
173 |
|
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 |
|
197 |
|
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 |
|
219 | console.log(' ' + fields.map((x, i) => x.padEnd(pads[i])).join(''))
|
220 | }
|
221 |
|
222 | return upgradable
|
223 | }
|
224 | }
|