diff --git a/packages/@vue/cli-plugin-babel/migrator/index.js b/packages/@vue/cli-plugin-babel/migrator/index.js new file mode 100644 index 0000000000..ea74209881 --- /dev/null +++ b/packages/@vue/cli-plugin-babel/migrator/index.js @@ -0,0 +1,26 @@ +const { chalk } = require('@vue/cli-shared-utils') + +module.exports = (api) => { + // TODO: backport this part to v3 + // if (api.fromVersion('<=3.5.3')) { + // // add core-js@2 as dependency + // api.extendPackage({ + // dependencies: { + // 'core-js': '^2.6.5' + // } + // }) + // } + + if (api.fromVersion('^3')) { + api.extendPackage({ + dependencies: { + 'core-js': '^3.1.2' + } + }, true) + + // TODO: implement a codemod to migrate polyfills + api.exitLog(`core-js has been upgraded from v2 to v3. +If you have any custom polyfills defined in ${chalk.yellow('babael.config.js')}, please be aware their names may have been changed. +For more complete changelog, see https://github.com/zloirock/core-js/blob/master/CHANGELOG.md#300---20190319`) + } +} diff --git a/packages/@vue/cli-plugin-typescript/generator/index.js b/packages/@vue/cli-plugin-typescript/generator/index.js index 55f77531cc..c42b6e410d 100644 --- a/packages/@vue/cli-plugin-typescript/generator/index.js +++ b/packages/@vue/cli-plugin-typescript/generator/index.js @@ -3,6 +3,7 @@ module.exports = (api, { tsLint, lintOn = [] }, _, invoking) => { + debugger if (typeof lintOn === 'string') { lintOn = lintOn.split(',') } diff --git a/packages/@vue/cli-shared-utils/index.js b/packages/@vue/cli-shared-utils/index.js index c5df8ba483..ab1213441d 100644 --- a/packages/@vue/cli-shared-utils/index.js +++ b/packages/@vue/cli-shared-utils/index.js @@ -17,3 +17,4 @@ exports.chalk = require('chalk') exports.execa = require('execa') +exports.semver = require('semver') diff --git a/packages/@vue/cli-shared-utils/lib/spinner.js b/packages/@vue/cli-shared-utils/lib/spinner.js index 7bc13d91fe..84f44d677e 100644 --- a/packages/@vue/cli-shared-utils/lib/spinner.js +++ b/packages/@vue/cli-shared-utils/lib/spinner.js @@ -43,6 +43,10 @@ exports.resumeSpinner = () => { spinner.start() } +exports.failSpinner = (text) => { + spinner.fail(text) +} + // silent all logs except errors during tests and keep record if (process.env.VUE_CLI_TEST) { require('./_silence')('spinner', exports) diff --git a/packages/@vue/cli-ui/apollo-server/connectors/dependencies.js b/packages/@vue/cli-ui/apollo-server/connectors/dependencies.js index e148cb0c76..d8190c1b10 100644 --- a/packages/@vue/cli-ui/apollo-server/connectors/dependencies.js +++ b/packages/@vue/cli-ui/apollo-server/connectors/dependencies.js @@ -211,7 +211,7 @@ function install ({ id, type, range }, context) { arg = id } - await installPackage(cwd.get(), getCommand(cwd.get()), null, arg, type === 'devDependencies') + await installPackage(cwd.get(), getCommand(cwd.get()), arg, type === 'devDependencies') logs.add({ message: `Dependency ${id} installed`, @@ -239,7 +239,7 @@ function uninstall ({ id }, context) { const dep = findOne(id, context) - await uninstallPackage(cwd.get(), getCommand(cwd.get()), null, id) + await uninstallPackage(cwd.get(), getCommand(cwd.get()), id) logs.add({ message: `Dependency ${id} uninstalled`, @@ -265,7 +265,7 @@ function update ({ id }, context) { const dep = findOne(id, context) const { current, wanted } = await getVersion(dep, context) - await updatePackage(cwd.get(), getCommand(cwd.get()), null, id) + await updatePackage(cwd.get(), getCommand(cwd.get()), id) logs.add({ message: `Dependency ${id} updated from ${current} to ${wanted}`, @@ -310,7 +310,7 @@ function updateAll (context) { args: [updatedDeps.length] }) - await updatePackage(cwd.get(), getCommand(cwd.get()), null, updatedDeps.map( + await updatePackage(cwd.get(), getCommand(cwd.get()), updatedDeps.map( p => p.id ).join(' ')) diff --git a/packages/@vue/cli-ui/apollo-server/connectors/plugins.js b/packages/@vue/cli-ui/apollo-server/connectors/plugins.js index b33085f8c3..c41a6991db 100644 --- a/packages/@vue/cli-ui/apollo-server/connectors/plugins.js +++ b/packages/@vue/cli-ui/apollo-server/connectors/plugins.js @@ -334,7 +334,7 @@ function install (id, context) { if (process.env.VUE_CLI_DEBUG && isOfficialPlugin(id)) { mockInstall(id, context) } else { - await installPackage(cwd.get(), getCommand(cwd.get()), null, id) + await installPackage(cwd.get(), getCommand(cwd.get()), id) } await initPrompts(id, context) installationStep = 'config' @@ -412,7 +412,7 @@ function uninstall (id, context) { if (process.env.VUE_CLI_DEBUG && isOfficialPlugin(id)) { mockUninstall(id, context) } else { - await uninstallPackage(cwd.get(), getCommand(cwd.get()), null, id) + await uninstallPackage(cwd.get(), getCommand(cwd.get()), id) } currentPluginId = null installationStep = null @@ -520,7 +520,7 @@ function update ({ id, full }, context) { if (localPath) { await updateLocalPackage({ cwd: cwd.get(), id, localPath, full }, context) } else { - await updatePackage(cwd.get(), getCommand(cwd.get()), null, id) + await updatePackage(cwd.get(), getCommand(cwd.get()), id) } logs.add({ @@ -583,7 +583,7 @@ async function updateAll (context) { args: [updatedPlugins.length] }) - await updatePackage(cwd.get(), getCommand(cwd.get()), null, updatedPlugins.map( + await updatePackage(cwd.get(), getCommand(cwd.get()), updatedPlugins.map( p => p.id ).join(' ')) diff --git a/packages/@vue/cli-upgrade/bin/vue-cli-upgrade.js b/packages/@vue/cli-upgrade/bin/vue-cli-upgrade.js deleted file mode 100644 index 796786c303..0000000000 --- a/packages/@vue/cli-upgrade/bin/vue-cli-upgrade.js +++ /dev/null @@ -1,3 +0,0 @@ -const vueCliUpgrade = require('../index') - -vueCliUpgrade() diff --git a/packages/@vue/cli-upgrade/get-upgradable-version.js b/packages/@vue/cli-upgrade/get-upgradable-version.js deleted file mode 100644 index dc1166a4c0..0000000000 --- a/packages/@vue/cli-upgrade/get-upgradable-version.js +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 373941200b..0000000000 --- a/packages/@vue/cli-upgrade/index.js +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index 813d5906a5..0000000000 --- a/packages/@vue/cli-upgrade/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@vue/cli-upgrade", - "version": "4.0.0-alpha.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", - "directory": "packages/@vue/cli-upgrade" - }, - "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": { - "@vue/cli-shared-utils": "^4.0.0-alpha.1", - "chalk": "^2.4.1", - "cli-table": "^0.3.1", - "execa": "^1.0.0", - "inquirer": "^6.3.1" - } -} diff --git a/packages/@vue/cli/__tests__/upgrade.spec.js b/packages/@vue/cli/__tests__/upgrade.spec.js new file mode 100644 index 0000000000..26b3a576cf --- /dev/null +++ b/packages/@vue/cli/__tests__/upgrade.spec.js @@ -0,0 +1,55 @@ +const fs = require('fs') +const path = require('path') +const create = require('@vue/cli-test-utils/createTestProject') +// const { logs } = require('@vue/cli-shared-utils') + +jest.setTimeout(200000) + +const outsideTestFolder = path.resolve(__dirname, '../../../../../vue-upgrade-tests') + +beforeAll(() => { + if (!fs.existsSync(outsideTestFolder)) { + fs.mkdirSync(outsideTestFolder) + } +}) + +test('upgrade: plugin-babel v3.5', async () => { + process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN = true + const project = await create('plugin-babel-legacy', { + plugins: { + '@vue/cli-plugin-babel': { + version: '3.5.3' + } + } + }, outsideTestFolder) + + const pkg = JSON.parse(await project.read('package.json')) + expect(pkg.dependencies).not.toHaveProperty('core-js') + + await project.run(`${require.resolve('../bin/vue')} upgrade @vue/babel`) + + const updatedPkg = JSON.parse(await project.read('package.json')) + expect(updatedPkg.dependencies).toHaveProperty('core-js') + + // TODO: run upgrade in the same process so that we can access logs + // expect(logs.log.some(([msg]) => msg.match('core-js has been upgraded'))).toBe(true) +}) + +test('upgrade: plugin-babel with core-js 2', async () => { + process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN = true + const project = await create('plugin-babel-v3', { + plugins: { + '@vue/cli-plugin-babel': { + version: '3.8.0' + } + } + }, outsideTestFolder) + + const pkg = JSON.parse(await project.read('package.json')) + expect(pkg.dependencies['core-js']).toMatch('^2') + + await project.run(`${require.resolve('../bin/vue')} upgrade @vue/babel --to next`) + + const updatedPkg = JSON.parse(await project.read('package.json')) + expect(updatedPkg.dependencies['core-js']).toMatch('^3') +}) diff --git a/packages/@vue/cli/bin/vue.js b/packages/@vue/cli/bin/vue.js index 9d88addbbf..8161aad511 100755 --- a/packages/@vue/cli/bin/vue.js +++ b/packages/@vue/cli/bin/vue.js @@ -166,10 +166,12 @@ program }) program - .command('upgrade [semverLevel]') - .description('upgrade vue cli service / plugins (default semverLevel: minor)') - .action((semverLevel, cmd) => { - loadCommand('upgrade', '@vue/cli-upgrade')(semverLevel, cleanArgs(cmd)) + .command('upgrade [package-name]') + .description('(experimental) upgrade vue cli service / plugins') + .option('-t, --to ', 'upgrade to a version that is not latest') + .option('-r, --registry ', 'Use specified npm registry when installing dependencies') + .action((packageName, cmd) => { + require('../lib/upgrade')(packageName, cleanArgs(cmd)) }) program diff --git a/packages/@vue/cli/lib/Creator.js b/packages/@vue/cli/lib/Creator.js index 250bfbd83c..7782b59cf6 100644 --- a/packages/@vue/cli/lib/Creator.js +++ b/packages/@vue/cli/lib/Creator.js @@ -112,7 +112,7 @@ module.exports = class Creator extends EventEmitter { let latestMinor = `${semver.major(latest)}.${semver.minor(latest)}.0` // if using `next` branch of cli - if (semver.gt(current, latest) && semver.prerelease(current)) { + if (semver.gte(current, latest) && semver.prerelease(current)) { latestMinor = current } // generate package.json with plugin dependencies @@ -136,6 +136,7 @@ module.exports = class Creator extends EventEmitter { ((/^@vue/.test(dep)) ? `^${latestMinor}` : `latest`) ) }) + // write package.json await writeFileTree(context, { 'package.json': JSON.stringify(pkg, null, 2) @@ -155,11 +156,12 @@ module.exports = class Creator extends EventEmitter { log(`⚙ Installing CLI plugins. This might take a while...`) log() this.emit('creation', { event: 'plugins-install' }) - if (isTestOrDebug) { + + if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) { // in development, avoid installation process await require('./util/setupDevProject')(context) } else { - await installDeps(context, packageManager, cliOptions.registry) + await installDeps(context, packageManager) } // run generator @@ -180,7 +182,7 @@ module.exports = class Creator extends EventEmitter { this.emit('creation', { event: 'deps-install' }) log() if (!isTestOrDebug) { - await installDeps(context, packageManager, cliOptions.registry) + await installDeps(context, packageManager) } // run complete cbs if any (injected by generators) diff --git a/packages/@vue/cli/lib/GeneratorAPI.js b/packages/@vue/cli/lib/GeneratorAPI.js index cc0f87077e..ec7ffa566b 100644 --- a/packages/@vue/cli/lib/GeneratorAPI.js +++ b/packages/@vue/cli/lib/GeneratorAPI.js @@ -173,8 +173,9 @@ class GeneratorAPI { * files are written to disk. * * @param {object | () => object} fields - Fields to merge. + * @param {boolean} forceNewVersion - Ignore version conflicts when updating dependency version */ - extendPackage (fields) { + extendPackage (fields, forceNewVersion) { const pkg = this.generator.pkg const toMerge = isFunction(fields) ? fields(pkg) : fields for (const key in toMerge) { @@ -186,7 +187,8 @@ class GeneratorAPI { this.id, existing || {}, value, - this.generator.depSources + this.generator.depSources, + forceNewVersion ) } else if (!(key in pkg)) { pkg[key] = value diff --git a/packages/@vue/cli/lib/Migrator.js b/packages/@vue/cli/lib/Migrator.js new file mode 100644 index 0000000000..ccdf2918a2 --- /dev/null +++ b/packages/@vue/cli/lib/Migrator.js @@ -0,0 +1,29 @@ +const Generator = require('./Generator') +const MigratorAPI = require('./MigratorAPI') + +const inferRootOptions = require('./util/inferRootOptions') + +module.exports = class Migrator extends Generator { + constructor (context, { + plugin, + + pkg = {}, + completeCbs = [], + files = {}, + invoking = false + } = {}) { + super(context, { + pkg, + plugins: [], + completeCbs, + files, + invoking + }) + this.plugins = [plugin] + + const rootOptions = inferRootOptions(pkg) + // apply migrators from plugins + const api = new MigratorAPI(plugin.id, plugin.installed, this, plugin.options, rootOptions) + plugin.apply(api, plugin.options, rootOptions, invoking) + } +} diff --git a/packages/@vue/cli/lib/MigratorAPI.js b/packages/@vue/cli/lib/MigratorAPI.js new file mode 100644 index 0000000000..1c6dd45226 --- /dev/null +++ b/packages/@vue/cli/lib/MigratorAPI.js @@ -0,0 +1,23 @@ +const semver = require('semver') +const GeneratorAPI = require('./GeneratorAPI') + +class MigratorAPI extends GeneratorAPI { + /** + * @param {string} id - Id of the owner plugin + * @param {Migrator} migrator - The invoking Migrator instance + * @param {object} options - options passed to this plugin + * @param {object} rootOptions - root options (the entire preset) + */ + constructor (id, installedVersion, migrator, options, rootOptions) { + super(id, migrator, options, rootOptions) + + this.installedVersion = installedVersion + this.migrator = this.generator + } + + fromVersion (range) { + return semver.satisfies(this.installedVersion, range) + } +} + +module.exports = MigratorAPI diff --git a/packages/@vue/cli/lib/add.js b/packages/@vue/cli/lib/add.js index 07640da6c3..848a843cd2 100644 --- a/packages/@vue/cli/lib/add.js +++ b/packages/@vue/cli/lib/add.js @@ -28,7 +28,7 @@ async function add (pluginName, options = {}, context = process.cwd()) { log() const packageManager = loadOptions().packageManager || (hasProjectYarn(context) ? 'yarn' : hasProjectPnpm(context) ? 'pnpm' : 'npm') - await installPackage(context, packageManager, options.registry, packageName) + await installPackage(context, packageManager, packageName) log(`${chalk.green('✔')} Successfully installed plugin: ${chalk.cyan(packageName)}`) log() diff --git a/packages/@vue/cli/lib/invoke.js b/packages/@vue/cli/lib/invoke.js index b822f08dd1..e9120e8ae1 100644 --- a/packages/@vue/cli/lib/invoke.js +++ b/packages/@vue/cli/lib/invoke.js @@ -2,13 +2,7 @@ const fs = require('fs-extra') const path = require('path') const execa = require('execa') const chalk = require('chalk') -const globby = require('globby') const inquirer = require('inquirer') -const { isBinaryFileSync } = require('isbinaryfile') -const Generator = require('./Generator') -const { loadOptions } = require('./options') -const { installDeps } = require('./util/installDeps') -const normalizeFilePaths = require('./util/normalizeFilePaths') const { log, error, @@ -21,23 +15,10 @@ const { loadModule } = require('@vue/cli-shared-utils') -async function readFiles (context) { - const files = await globby(['**'], { - cwd: context, - onlyFiles: true, - gitignore: true, - ignore: ['**/node_modules/**', '**/.git/**'], - dot: true - }) - const res = {} - for (const file of files) { - const name = path.resolve(context, file) - res[file] = isBinaryFileSync(name) - ? fs.readFileSync(name) - : fs.readFileSync(name, 'utf-8') - } - return normalizeFilePaths(res) -} +const Generator = require('./Generator') +const { loadOptions } = require('./options') +const { installDeps } = require('./util/installDeps') +const readFiles = require('./util/readFiles') function getPkg (context) { const pkgPath = path.resolve(context, 'package.json') @@ -146,7 +127,7 @@ async function runGenerator (context, plugin, pkg = getPkg(context)) { log() const packageManager = loadOptions().packageManager || (hasProjectYarn(context) ? 'yarn' : hasProjectPnpm(context) ? 'pnpm' : 'npm') - await installDeps(context, packageManager, plugin.options && plugin.options.registry) + await installDeps(context, packageManager) } if (createCompleteCbs.length) { diff --git a/packages/@vue/cli/lib/upgrade.js b/packages/@vue/cli/lib/upgrade.js new file mode 100644 index 0000000000..cc90bc9164 --- /dev/null +++ b/packages/@vue/cli/lib/upgrade.js @@ -0,0 +1,271 @@ +const fs = require('fs') +const path = require('path') +const chalk = require('chalk') +const execa = require('execa') +const { + log, + error, + done, + + logWithSpinner, + stopSpinner, + + isPlugin, + resolvePluginId, + loadModule, + + hasProjectGit +} = require('@vue/cli-shared-utils') + +const Migrator = require('./Migrator') + +const { getCommand, getVersion } = require('./util/packageManager') +const { installDeps, updatePackage } = require('./util/installDeps') +const { linkPackage } = require('./util/linkBin') + +const getPackageJson = require('./util/getPackageJson') +const getInstalledVersion = require('./util/getInstalledVersion') +const tryGetNewerRange = require('./util/tryGetNewerRange') +const readFiles = require('./util/readFiles') + +const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG + +async function runMigrator (packageName, options, context) { + const pluginMigrator = loadModule(`${packageName}/migrator`, context) + if (!pluginMigrator) { return } + + const plugin = { + id: packageName, + apply: pluginMigrator, + installed: options.installed + } + + const pkg = getPackageJson(context) + const createCompleteCbs = [] + const migrator = new Migrator(context, { + plugin: plugin, + + pkg, + files: await readFiles(context), + completeCbs: createCompleteCbs, + invoking: true + }) + + log(`🚀 Running migrator of ${packageName}`) + await migrator.generate({ + extractConfigFiles: true, + checkExisting: true + }) + + const newDeps = migrator.pkg.dependencies + const newDevDeps = migrator.pkg.devDependencies + const depsChanged = + JSON.stringify(newDeps) !== JSON.stringify(pkg.dependencies) || + JSON.stringify(newDevDeps) !== JSON.stringify(pkg.devDependencies) + + if (!isTestOrDebug && depsChanged) { + log(`📦 Installing additional dependencies...`) + log() + const packageManager = getCommand(context) + await installDeps(context, packageManager) + } + + if (createCompleteCbs.length) { + logWithSpinner('⚓', `Running completion hooks...`) + for (const cb of createCompleteCbs) { + await cb() + } + stopSpinner() + log() + } + + log(`${chalk.green('✔')} Successfully invoked migrator for plugin: ${chalk.cyan(plugin.id)}`) + if (!process.env.VUE_CLI_TEST && hasProjectGit(context)) { + const { stdout } = await execa('git', [ + 'ls-files', + '--exclude-standard', + '--modified', + '--others' + ], { + cwd: context + }) + if (stdout.trim()) { + log(` The following files have been updated / added:\n`) + log( + chalk.red( + stdout + .split(/\r?\n/g) + .map(line => ` ${line}`) + .join('\n') + ) + ) + log() + log( + ` You should review these changes with ${chalk.cyan( + `git diff` + )} and commit them.` + ) + log() + } + } + + migrator.printExitLogs() +} + +async function upgradeSinglePackage (pluginId, options, context) { + const packageName = resolvePluginId(pluginId) + const pkg = getPackageJson(context) + + let depEntry, required + for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) { + if (pkg[depType] && pkg[depType][packageName]) { + depEntry = depType + required = pkg[depType][packageName] + break + } + } + if (!required) { + throw new Error(`Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('package.json')}`) + } + const installed = getInstalledVersion(packageName) + + let targetVersion = options.to || 'latest' + + // if the targetVersion is not an exact version + if (!/\d+\.\d+\.\d+/.test(targetVersion)) { + if (targetVersion === 'latest') { + logWithSpinner(`Getting latest version of ${packageName}`) + } else { + logWithSpinner(`Getting max satisfying version of ${packageName}@${options.to}`) + } + + targetVersion = await getVersion(packageName, targetVersion, context) + stopSpinner() + } + + if (targetVersion === installed) { + log(`Already installed ${packageName}@${targetVersion}`) + + const newRange = tryGetNewerRange(`^${targetVersion}`, required) + if (newRange !== required) { + pkg[depEntry][packageName] = newRange + fs.writeFileSync(path.resolve(context, 'package.json'), JSON.stringify(pkg, null, 2)) + log(`${chalk.green('✔')} Updated version range in ${chalk.yellow('package.json')}`) + } + return + } + + log(`Upgrading ${packageName} from ${installed} to ${targetVersion}`) + + if (isTestOrDebug) { + // link packages in current repo for test + await linkPackage(path.resolve(__dirname, `../../../${packageName}`), path.join(context, 'node_modules', packageName)) + } else { + await updatePackage(context, getCommand(context), `${packageName}@^${targetVersion}`) + } + + await runMigrator(packageName, { installed }, context) +} + +async function getUpgradable (context) { + // get current deps + // filter @vue/cli-service, @vue/cli-plugin-* & vue-cli-plugin-* + const pkg = getPackageJson(context) + const upgradable = [] + + for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) { + for (const [name, range] of Object.entries(pkg[depType] || {})) { + if (name !== '@vue/cli-service' && !isPlugin(name)) { + continue + } + + const installed = await getInstalledVersion(name) + const wanted = await getVersion(name, range, context) + const latest = await getVersion(name, 'latest', context) + + if (installed !== latest) { + upgradable.push({ name, installed, wanted, latest }) + } + } + } + + return upgradable +} + +async function checkForUpdates (context) { + logWithSpinner('Gathering pacakage information...') + const upgradable = await getUpgradable(context) + stopSpinner() + + if (!upgradable.length) { + done('Seems all plugins are up to date. Good work!') + return + } + + // format the output + // adapted from @angular/cli + const names = upgradable.map(dep => dep.name) + let namePad = Math.max(...names.map(x => x.length)) + 2 + if (!Number.isFinite(namePad)) { + namePad = 30 + } + const pads = [namePad, 12, 12, 12, 0] + console.log( + ' ' + + ['Name', 'Installed', 'Wanted', 'Latest', 'Command to upgrade'].map( + (x, i) => chalk.underline(x.padEnd(pads[i])) + ).join('') + ) + for (const p of upgradable) { + const fields = [p.name, p.installed, p.wanted, p.latest, `vue upgrade ${p.name}`] + console.log(' ' + fields.map((x, i) => x.padEnd(pads[i])).join('')) + } + + console.log(`Run ${chalk.yellow('vue upgrade --all')} to upgrade all the above plugins`) + + return upgradable +} + +async function upgradeAll (context) { + // TODO: should confirm for major version upgrades + // for patch & minor versions, upgrade directly + // for major versions, prompt before upgrading + const upgradable = await getUpgradable(context) + + if (!upgradable.length) { + done('Seems all plugins are up to date. Good work!') + return + } + + for (const p of upgradable) { + await upgradeSinglePackage(p.name, { to: p.latest }, context) + } + + done('All plugins are up to date!') +} + +async function upgrade (packageName, options, context = process.cwd()) { + if (!packageName) { + if (options.to) { + error(`Must specify a package name to upgrade to ${options.to}`) + process.exit(1) + } + + if (options.all) { + return upgradeAll(context) + } + + return checkForUpdates(context) + } + + return upgradeSinglePackage(packageName, options, context) +} + +module.exports = (...args) => { + return upgrade(...args).catch(err => { + error(err) + if (!process.env.VUE_CLI_TEST) { + process.exit(1) + } + }) +} diff --git a/packages/@vue/cli-upgrade/get-installed-version.js b/packages/@vue/cli/lib/util/getInstalledVersion.js similarity index 87% rename from packages/@vue/cli-upgrade/get-installed-version.js rename to packages/@vue/cli/lib/util/getInstalledVersion.js index ea6307f760..68741c00a4 100644 --- a/packages/@vue/cli-upgrade/get-installed-version.js +++ b/packages/@vue/cli/lib/util/getInstalledVersion.js @@ -1,5 +1,5 @@ const path = require('path') -const getPackageJson = require('./get-package-json') +const getPackageJson = require('./getPackageJson') module.exports = function getInstalledVersion (packageName) { // for first level deps, read package.json directly is way faster than `npm list` diff --git a/packages/@vue/cli-upgrade/get-package-json.js b/packages/@vue/cli/lib/util/getPackageJson.js similarity index 100% rename from packages/@vue/cli-upgrade/get-package-json.js rename to packages/@vue/cli/lib/util/getPackageJson.js diff --git a/packages/@vue/cli/lib/util/getPackageVersion.js b/packages/@vue/cli/lib/util/getPackageVersion.js index f3767b6546..1a7c58c44e 100644 --- a/packages/@vue/cli/lib/util/getPackageVersion.js +++ b/packages/@vue/cli/lib/util/getPackageVersion.js @@ -1,9 +1,10 @@ const { request } = require('@vue/cli-shared-utils') +const { getRegistry } = require('./packageManager') -module.exports = async function getPackageVersion (id, range = '') { - const registry = (await require('./shouldUseTaobao')()) - ? `https://registry.npm.taobao.org` - : `https://registry.npmjs.org` +module.exports = async function getPackageVersion (id, range = '', registry) { + if (!registry) { + registry = await getRegistry() + } let result try { diff --git a/packages/@vue/cli/lib/util/installDeps.js b/packages/@vue/cli/lib/util/installDeps.js index 9b5cbb2b4f..887dae4c30 100644 --- a/packages/@vue/cli/lib/util/installDeps.js +++ b/packages/@vue/cli/lib/util/installDeps.js @@ -3,7 +3,7 @@ const chalk = require('chalk') const execa = require('execa') const readline = require('readline') const registries = require('./registries') -const shouldUseTaobao = require('./shouldUseTaobao') +const { getRegistry } = require('./packageManager') const debug = require('debug')('vue-cli:install') @@ -91,14 +91,8 @@ function renderProgressBar (curr, total) { process.stderr.write(`[${complete}${incomplete}]${bar}`) } -async function addRegistryToArgs (command, args, cliRegistry) { - const altRegistry = ( - cliRegistry || ( - (await shouldUseTaobao(command)) - ? registries.taobao - : null - ) - ) +async function addRegistryToArgs (command, args) { + const altRegistry = await getRegistry({ packageManager: command }) if (altRegistry) { args.push(`--registry=${altRegistry}`) @@ -194,12 +188,12 @@ function executeCommand (command, args, targetDir) { }) } -exports.installDeps = async function installDeps (targetDir, command, cliRegistry) { +exports.installDeps = async function installDeps (targetDir, command) { checkPackageManagerIsSupported(command) const args = packageManagerConfig[command].installDeps - await addRegistryToArgs(command, args, cliRegistry) + await addRegistryToArgs(command, args) debug(`command: `, command) debug(`args: `, args) @@ -207,14 +201,14 @@ exports.installDeps = async function installDeps (targetDir, command, cliRegistr await executeCommand(command, args, targetDir) } -exports.installPackage = async function (targetDir, command, cliRegistry, packageName, dev = true) { +exports.installPackage = async function (targetDir, command, packageName, dev = true) { checkPackageManagerIsSupported(command) const args = packageManagerConfig[command].installPackage if (dev) args.push('-D') - await addRegistryToArgs(command, args, cliRegistry) + await addRegistryToArgs(command, args) args.push(packageName) @@ -224,12 +218,12 @@ exports.installPackage = async function (targetDir, command, cliRegistry, packag await executeCommand(command, args, targetDir) } -exports.uninstallPackage = async function (targetDir, command, cliRegistry, packageName) { +exports.uninstallPackage = async function (targetDir, command, packageName) { checkPackageManagerIsSupported(command) const args = packageManagerConfig[command].uninstallPackage - await addRegistryToArgs(command, args, cliRegistry) + await addRegistryToArgs(command, args) args.push(packageName) @@ -239,12 +233,12 @@ exports.uninstallPackage = async function (targetDir, command, cliRegistry, pack await executeCommand(command, args, targetDir) } -exports.updatePackage = async function (targetDir, command, cliRegistry, packageName) { +exports.updatePackage = async function (targetDir, command, packageName) { checkPackageManagerIsSupported(command) const args = packageManagerConfig[command].updatePackage - await addRegistryToArgs(command, args, cliRegistry) + await addRegistryToArgs(command, args) packageName.split(' ').forEach(name => args.push(name)) diff --git a/packages/@vue/cli/lib/util/linkBin.js b/packages/@vue/cli/lib/util/linkBin.js index 31c7c2bab6..d038ac551c 100644 --- a/packages/@vue/cli/lib/util/linkBin.js +++ b/packages/@vue/cli/lib/util/linkBin.js @@ -19,3 +19,12 @@ exports.linkBin = async (src, dest) => { await fs.chmod(dest, '755') } } + +exports.linkPackage = async (src, dest) => { + if (!process.env.VUE_CLI_TEST && !process.env.VUE_CLI_DEBUG) { + throw new Error(`linkPackage should only be used during tests or debugging.`) + } + + await fs.remove(dest) + await fs.symlink(src, dest, 'dir') +} diff --git a/packages/@vue/cli/lib/util/mergeDeps.js b/packages/@vue/cli/lib/util/mergeDeps.js index ad99f7f3b1..61bd8c6d43 100644 --- a/packages/@vue/cli/lib/util/mergeDeps.js +++ b/packages/@vue/cli/lib/util/mergeDeps.js @@ -1,7 +1,12 @@ const semver = require('semver') const { warn } = require('@vue/cli-shared-utils') -module.exports = function resolveDeps (generatorId, to, from, sources) { +const tryGetNewerRange = require('./tryGetNewerRange') + +const extractSemver = r => r.replace(/^.+#semver:/, '') +const injectSemver = (r, v) => semver.validRange(r) ? v : r.replace(/#semver:.+$/, `#semver:${v}`) + +module.exports = function resolveDeps (generatorId, to, from, sources, forceNewVersion) { const res = Object.assign({}, to) for (const name in from) { const r1 = to[name] @@ -37,7 +42,14 @@ module.exports = function resolveDeps (generatorId, to, from, sources) { sources[name] = generatorId } // warn incompatible version requirements - if (!semver.validRange(r1semver) || !semver.validRange(r2semver) || !semver.intersects(r1semver, r2semver)) { + if ( + !forceNewVersion && + ( + !semver.validRange(r1semver) || + !semver.validRange(r2semver) || + !semver.intersects(r1semver, r2semver) + ) + ) { warn( `conflicting versions for project dependency "${name}":\n\n` + `- ${r1} injected by generator "${sourceGeneratorId}"\n` + @@ -49,16 +61,3 @@ module.exports = function resolveDeps (generatorId, to, from, sources) { } return res } - -const leadRE = /^(~|\^|>=?)/ -const rangeToVersion = r => r.replace(leadRE, '').replace(/x/g, '0') -const extractSemver = r => r.replace(/^.+#semver:/, '') -const injectSemver = (r, v) => semver.validRange(r) ? v : r.replace(/#semver:.+$/, `#semver:${v}`) - -function tryGetNewerRange (r1, r2) { - const v1 = rangeToVersion(r1) - const v2 = rangeToVersion(r2) - if (semver.valid(v1) && semver.valid(v2)) { - return semver.gt(v1, v2) ? r1 : r2 - } -} diff --git a/packages/@vue/cli/lib/util/packageManager.js b/packages/@vue/cli/lib/util/packageManager.js new file mode 100644 index 0000000000..34eb1832a8 --- /dev/null +++ b/packages/@vue/cli/lib/util/packageManager.js @@ -0,0 +1,102 @@ +const execa = require('execa') +const minimist = require('minimist') +const semver = require('semver') +const LRU = require('lru-cache') + +const { + hasYarn, + hasProjectYarn, + hasPnpm3OrLater, + hasProjectPnpm +} = require('@vue/cli-shared-utils') + +const { loadOptions } = require('../options') +const registries = require('./registries') +const shouldUseTaobao = require('./shouldUseTaobao') + +function getCommand (cwd) { + if (!cwd) { + return loadOptions().packageManager || (hasYarn() ? 'yarn' : hasPnpm3OrLater() ? 'pnpm' : 'npm') + } + return hasProjectYarn(cwd) ? 'yarn' : hasProjectPnpm(cwd) ? 'pnpm' : 'npm' +} + +// Any command that implemented registry-related feature should support +// `-r` / `--registry` option +async function getRegistry ({ cwd, packageManager } = {}) { + const args = minimist(process.argv, { + alias: { + r: 'registry' + } + }) + + if (args.registry) { + return args.registry + } + + if (await shouldUseTaobao()) { + return registries.taobao + } + + if (!packageManager) { + packageManager = getCommand(cwd) + } + const { stdout } = await execa(packageManager, ['config', 'get', 'registry']) + return stdout +} + +const metadataCache = new LRU({ + max: 200, + maxAge: 1000 * 60 * 30 // 30 min. +}) + +async function getMetadata (packageName, { field = '', packageManager, cwd } = {}) { + if (!packageManager) { + packageManager = getCommand(cwd) + } + const registry = await getRegistry({ cwd, packageManager }) + + const metadataKey = `${packageManager}-${registry}-${packageName}` + let metadata = metadataCache.get(metadataKey) + + if (metadata) { + return metadata + } + + const { stdout } = await execa( + packageManager, + [ + 'info', + packageName, + field, + '--json', + '--registry', + registry + ] + ) + + metadata = JSON.parse(stdout) + if (packageManager === 'yarn') { + // `yarn info` outputs messages in the form of `{"type": "inspect", data: {}}` + metadata = metadata.data + } + + metadataCache.set(metadataKey, metadata) + return metadata +} + +async function getVersion (packageName, versionRange, cwd) { + const metadata = await getMetadata(packageName, { cwd }) + if (Object.keys(metadata['dist-tags']).includes(versionRange)) { + return metadata['dist-tags'][versionRange] + } + const versions = Array.isArray(metadata.versions) ? metadata.versions : Object.keys(metadata.versions) + return semver.maxSatisfying(versions, versionRange) +} + +module.exports = { + getCommand, + getRegistry, + getMetadata, + getVersion +} diff --git a/packages/@vue/cli/lib/util/readFiles.js b/packages/@vue/cli/lib/util/readFiles.js new file mode 100644 index 0000000000..c0b6f9abc6 --- /dev/null +++ b/packages/@vue/cli/lib/util/readFiles.js @@ -0,0 +1,24 @@ +const fs = require('fs-extra') +const path = require('path') + +const globby = require('globby') +const { isBinaryFileSync } = require('isbinaryfile') +const normalizeFilePaths = require('./normalizeFilePaths') + +module.exports = async function readFiles (context) { + const files = await globby(['**'], { + cwd: context, + onlyFiles: true, + gitignore: true, + ignore: ['**/node_modules/**', '**/.git/**'], + dot: true + }) + const res = {} + for (const file of files) { + const name = path.resolve(context, file) + res[file] = isBinaryFileSync(name) + ? fs.readFileSync(name) + : fs.readFileSync(name, 'utf-8') + } + return normalizeFilePaths(res) +} diff --git a/packages/@vue/cli/lib/util/tryGetNewerRange.js b/packages/@vue/cli/lib/util/tryGetNewerRange.js new file mode 100644 index 0000000000..529432bc1a --- /dev/null +++ b/packages/@vue/cli/lib/util/tryGetNewerRange.js @@ -0,0 +1,12 @@ +const semver = require('semver') + +const leadRE = /^(~|\^|>=?)/ +const rangeToVersion = r => r.replace(leadRE, '').replace(/x/g, '0') + +module.exports = function tryGetNewerRange (r1, r2) { + const v1 = rangeToVersion(r1) + const v2 = rangeToVersion(r2) + if (semver.valid(v1) && semver.valid(v2)) { + return semver.gt(v1, v2) ? r1 : r2 + } +} diff --git a/packages/@vue/cli/package.json b/packages/@vue/cli/package.json index 5da4c07375..ee683051fa 100644 --- a/packages/@vue/cli/package.json +++ b/packages/@vue/cli/package.json @@ -47,6 +47,7 @@ "js-yaml": "^3.13.1", "jscodeshift": "^0.6.4", "lodash.clonedeep": "^4.5.0", + "lru-cache": "^5.1.1", "minimist": "^1.2.0", "recast": "^0.18.1", "request": "^2.87.0",