diff --git a/packages/@vue/cli-upgrade/bin/vue-cli-upgrade.js b/packages/@vue/cli-upgrade/bin/vue-cli-upgrade.js new file mode 100644 index 0000000000..796786c303 --- /dev/null +++ b/packages/@vue/cli-upgrade/bin/vue-cli-upgrade.js @@ -0,0 +1,3 @@ +const vueCliUpgrade = require('../index') + +vueCliUpgrade() diff --git a/packages/@vue/cli-upgrade/get-installed-version.js b/packages/@vue/cli-upgrade/get-installed-version.js new file mode 100644 index 0000000000..ea6307f760 --- /dev/null +++ b/packages/@vue/cli-upgrade/get-installed-version.js @@ -0,0 +1,14 @@ +const path = require('path') +const getPackageJson = require('./get-package-json') + +module.exports = function getInstalledVersion (packageName) { + // for first level deps, read package.json directly is way faster than `npm list` + try { + const packageJson = getPackageJson( + path.resolve(process.cwd(), 'node_modules', packageName) + ) + return packageJson.version + } catch (e) { + return 'N/A' + } +} diff --git a/packages/@vue/cli-upgrade/get-package-json.js b/packages/@vue/cli-upgrade/get-package-json.js new file mode 100644 index 0000000000..7049cc39e5 --- /dev/null +++ b/packages/@vue/cli-upgrade/get-package-json.js @@ -0,0 +1,21 @@ +const fs = require('fs') +const path = require('path') + +module.exports = function getPackageJson (projectPath) { + const packagePath = path.join(projectPath, 'package.json') + + let packageJson + try { + packageJson = fs.readFileSync(packagePath, 'utf-8') + } catch (err) { + throw new Error(`${packagePath} not exist`) + } + + try { + packageJson = JSON.parse(packageJson) + } catch (err) { + throw new Error('The package.json is malformed') + } + + return packageJson +} diff --git a/packages/@vue/cli-upgrade/get-upgradable-version.js b/packages/@vue/cli-upgrade/get-upgradable-version.js new file mode 100644 index 0000000000..dc1166a4c0 --- /dev/null +++ b/packages/@vue/cli-upgrade/get-upgradable-version.js @@ -0,0 +1,38 @@ +const execa = require('execa') + +function getMaxSatisfying (packageName, range) { + let version = JSON.parse( + execa.shellSync(`npm view ${packageName}@${range} version --json`).stdout + ) + + if (typeof version !== 'string') { + version = version[0] + } + + return version +} + +module.exports = function getUpgradableVersion ( + packageName, + currRange, + semverLevel +) { + let newRange + if (semverLevel === 'patch') { + const currMaxVersion = getMaxSatisfying(packageName, currRange) + newRange = `~${currMaxVersion}` + const newMaxVersion = getMaxSatisfying(packageName, newRange) + newRange = `~${newMaxVersion}` + } else if (semverLevel === 'minor') { + const currMaxVersion = getMaxSatisfying(packageName, currRange) + newRange = `^${currMaxVersion}` + const newMaxVersion = getMaxSatisfying(packageName, newRange) + newRange = `^${newMaxVersion}` + } else if (semverLevel === 'major') { + newRange = `^${getMaxSatisfying(packageName, 'latest')}` + } else { + throw new Error('Release type must be one of patch | minor | major!') + } + + return newRange +} diff --git a/packages/@vue/cli-upgrade/index.js b/packages/@vue/cli-upgrade/index.js new file mode 100644 index 0000000000..373941200b --- /dev/null +++ b/packages/@vue/cli-upgrade/index.js @@ -0,0 +1,153 @@ +const fs = require('fs') +const path = require('path') + +const chalk = require('chalk') +const Table = require('cli-table') +const inquirer = require('inquirer') + +/* eslint-disable node/no-extraneous-require */ +const { + hasYarn, + logWithSpinner, + stopSpinner +} = require('@vue/cli-shared-utils') +const { loadOptions } = require('@vue/cli/lib/options') +const { installDeps } = require('@vue/cli/lib/util/installDeps') +/* eslint-enable node/no-extraneous-require */ + +const getPackageJson = require('./get-package-json') +const getInstalledVersion = require('./get-installed-version') +const getUpgradableVersion = require('./get-upgradable-version') + +const projectPath = process.cwd() + +// - Resolve the version to upgrade to. +// - `vue upgrade [patch|minor|major]`: defaults to minor +// - If already latest, print message and exit +// - Otherwise, confirm via prompt + +function isCorePackage (packageName) { + return ( + packageName === '@vue/cli-service' || + packageName.startsWith('@vue/cli-plugin-') + ) +} + +function shouldUseYarn () { + // infer from lockfiles first + if (fs.existsSync(path.resolve(projectPath, 'package-lock.json'))) { + return false + } + + if (fs.existsSync(path.resolve(projectPath, 'yarn.lock')) && hasYarn()) { + return true + } + + // fallback to packageManager field in ~/.vuerc + const { packageManager } = loadOptions() + if (packageManager) { + return packageManager === 'yarn' + } + + return hasYarn() +} + +module.exports = async function vueCliUpgrade (semverLevel = 'minor') { + // get current deps + // filter @vue/cli-service & @vue/cli-plugin-* + const pkg = getPackageJson(projectPath) + const upgradableDepMaps = new Map([ + ['dependencies', new Map()], + ['devDependencies', new Map()], + ['optionalDependencies', new Map()] + ]) + + logWithSpinner('Gathering update information...') + for (const depType of upgradableDepMaps.keys()) { + for (const [packageName, currRange] of Object.entries(pkg[depType] || {})) { + if (!isCorePackage(packageName)) { + continue + } + + const upgradable = getUpgradableVersion( + packageName, + currRange, + semverLevel + ) + if (upgradable !== currRange) { + upgradableDepMaps.get(depType).set(packageName, upgradable) + } + } + } + + const table = new Table({ + head: ['package', 'installed', '', 'upgraded'], + colAligns: ['left', 'right', 'right', 'right'], + chars: { + top: '', + 'top-mid': '', + 'top-left': '', + 'top-right': '', + bottom: '', + 'bottom-mid': '', + 'bottom-left': '', + 'bottom-right': '', + left: '', + 'left-mid': '', + mid: '', + 'mid-mid': '', + right: '', + 'right-mid': '', + middle: '' + } + }) + + for (const [depType, depMap] of upgradableDepMaps.entries()) { + for (const packageName of depMap.keys()) { + const installedVersion = getInstalledVersion(packageName) + const upgradedVersion = depMap.get(packageName) + table.push([packageName, installedVersion, '→', upgradedVersion]) + + pkg[depType][packageName] = upgradedVersion + } + } + + stopSpinner() + + if ([...upgradableDepMaps.values()].every(depMap => depMap.size === 0)) { + console.log('Already up-to-date.') + return + } + + console.log('These packages can be upgraded:\n') + console.log(table.toString()) + console.log( + `\nView complete changelog at ${chalk.blue( + 'https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md' + )}\n` + ) + + const useYarn = shouldUseYarn() + const { confirmed } = await inquirer.prompt([ + { + name: 'confirmed', + type: 'confirm', + message: `Upgrade ${chalk.yellow('package.json')} and run ${chalk.blue( + useYarn ? 'yarn install' : 'npm install' + )}?` + } + ]) + + if (!confirmed) { + return + } + + fs.writeFileSync(path.resolve(projectPath, 'package.json'), JSON.stringify(pkg, null, 2)) + console.log() + console.log(`${chalk.yellow('package.json')} saved`) + if (useYarn) { + await installDeps(projectPath, 'yarn') + } else { + await installDeps(projectPath, 'npm') + } +} diff --git a/packages/@vue/cli-upgrade/package.json b/packages/@vue/cli-upgrade/package.json new file mode 100644 index 0000000000..3a56743b22 --- /dev/null +++ b/packages/@vue/cli-upgrade/package.json @@ -0,0 +1,28 @@ +{ + "name": "@vue/cli-upgrade", + "version": "3.0.1", + "description": "utility to upgrade vue cli service / plugins in vue apps", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/vue-cli.git" + }, + "keywords": [ + "vue", + "cli", + "upgrade", + "update" + ], + "author": "Haoqun Jiang ", + "license": "MIT", + "bugs": { + "url": "https://github.com/vuejs/vue-cli/issues" + }, + "homepage": "https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-upgrade#readme", + "dependencies": { + "chalk": "^2.4.1", + "cli-table": "^0.3.1", + "execa": "^0.10.0", + "inquirer": "^6.0.0" + } +} diff --git a/packages/@vue/cli/bin/vue.js b/packages/@vue/cli/bin/vue.js index 6ee000686d..ecb9e0730f 100755 --- a/packages/@vue/cli/bin/vue.js +++ b/packages/@vue/cli/bin/vue.js @@ -148,6 +148,13 @@ program require('../lib/config')(value, cleanArgs(cmd)) }) +program + .command('upgrade [semverLevel]') + .description('upgrade vue cli service / plugins (default semverLevel: minor)') + .action((semverLevel, cmd) => { + loadCommand('upgrade', '@vue/cli-upgrade')(semverLevel, cleanArgs(cmd)) + }) + // output help information on unknown commands program .arguments('') @@ -189,12 +196,16 @@ if (!process.argv.slice(2).length) { program.outputHelp() } +function camelize (str) { + return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '') +} + // commander passes the Command object itself as options, // extract only actual options into a fresh object. function cleanArgs (cmd) { const args = {} cmd.options.forEach(o => { - const key = o.long.replace(/^--/, '') + const key = camelize(o.long.replace(/^--/, '')) // if an option is not present and Command has a method with the same name // it should not be copied if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') { diff --git a/yarn.lock b/yarn.lock index a983e273d8..e9136587cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3601,6 +3601,12 @@ cli-spinners@^1.0.1, cli-spinners@^1.1.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg== +cli-table@^0.3.1: + version "0.3.1" + resolved "http://registry.npm.taobao.org/cli-table/download/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" + dependencies: + colors "1.0.3" + cli-truncate@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"