diff --git a/packages/@vue/cli/__tests__/Upgrader.spec.js b/packages/@vue/cli-plugin-babel/__tests__/babelMigrator.spec.js similarity index 54% rename from packages/@vue/cli/__tests__/Upgrader.spec.js rename to packages/@vue/cli-plugin-babel/__tests__/babelMigrator.spec.js index 3b831296de..42414a9c9a 100644 --- a/packages/@vue/cli/__tests__/Upgrader.spec.js +++ b/packages/@vue/cli-plugin-babel/__tests__/babelMigrator.spec.js @@ -1,20 +1,7 @@ -const fs = require('fs') -const path = require('path') -const create = require('@vue/cli-test-utils/createTestProject') +const create = require('@vue/cli-test-utils/createUpgradableProject') const { logs } = require('@vue/cli-shared-utils') -const Upgrader = require('../lib/Upgrader') - jest.setTimeout(300000) - -const outsideTestFolder = path.resolve(__dirname, '../../../../../vue-upgrade-tests') - -beforeAll(() => { - if (!fs.existsSync(outsideTestFolder)) { - fs.mkdirSync(outsideTestFolder) - } -}) - beforeEach(() => { process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN = true }) @@ -26,12 +13,12 @@ test('upgrade: plugin-babel v3.5', async () => { version: '3.5.3' } } - }, outsideTestFolder) + }) const pkg = JSON.parse(await project.read('package.json')) expect(pkg.dependencies).not.toHaveProperty('core-js') - await (new Upgrader(project.dir)).upgrade('babel', {}) + await project.upgrade('babel') const updatedPkg = JSON.parse(await project.read('package.json')) expect(updatedPkg.dependencies).toHaveProperty('core-js') @@ -49,31 +36,13 @@ test('upgrade: plugin-babel with core-js 2', async () => { version: '3.8.0' } } - }, outsideTestFolder) + }) const pkg = JSON.parse(await project.read('package.json')) expect(pkg.dependencies['core-js']).toMatch('^2') - await (new Upgrader(project.dir)).upgrade('babel', {}) + await project.upgrade('babel') const updatedPkg = JSON.parse(await project.read('package.json')) expect(updatedPkg.dependencies['core-js']).toMatch('^3') }) - -test('upgrade: should add eslint to devDependencies', async () => { - const project = await create('plugin-eslint-v3.0', { - plugins: { - '@vue/cli-plugin-eslint': { - version: '3.0.0' - } - } - }, outsideTestFolder) - - const pkg = JSON.parse(await project.read('package.json')) - expect(pkg.devDependencies).not.toHaveProperty('eslint') - - await (new Upgrader(project.dir)).upgrade('eslint', {}) - - const updatedPkg = JSON.parse(await project.read('package.json')) - expect(updatedPkg.devDependencies.eslint).toMatch('^4') -}) diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintMigrator.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintMigrator.spec.js new file mode 100644 index 0000000000..1a570e38cf --- /dev/null +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintMigrator.spec.js @@ -0,0 +1,68 @@ +jest.setTimeout(300000) +jest.mock('inquirer') + +beforeEach(() => { + process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN = true +}) + +const create = require('@vue/cli-test-utils/createUpgradableProject') +const { expectPrompts } = require('inquirer') + +test('upgrade: should add eslint to devDependencies', async () => { + const project = await create('plugin-eslint-v3.0', { + plugins: { + '@vue/cli-plugin-eslint': { + version: '3.0.0' + } + } + }) + + const pkg = JSON.parse(await project.read('package.json')) + expect(pkg.devDependencies).not.toHaveProperty('eslint') + + expectPrompts([ + { + message: `Your current ESLint version is v4`, + confirm: false + } + ]) + + await project.upgrade('eslint') + + const updatedPkg = JSON.parse(await project.read('package.json')) + expect(updatedPkg.devDependencies.eslint).toMatch('^4') +}) + +test.only('upgrade: should upgrade eslint from v5 to v6', async () => { + const project = await create('plugin-eslint-with-eslint-5', { + plugins: { + '@vue/cli-plugin-eslint': { + version: '3.12.1', + config: 'airbnb' + } + } + }) + + const pkg = JSON.parse(await project.read('package.json')) + expect(pkg.devDependencies.eslint).toMatch('^5') + + expectPrompts([ + { + message: `Your current ESLint version is v5`, + confirm: true + } + ]) + + try { + await project.upgrade('eslint') + } catch (e) { + // TODO: + // Currently the `afterInvoke` hook will fail, + // because deps are not correctly installed in test env. + // Need to fix later. + } + + const updatedPkg = JSON.parse(await project.read('package.json')) + expect(updatedPkg.devDependencies.eslint).toMatch('^6') + expect(updatedPkg.devDependencies).toHaveProperty('eslint-plugin-import') +}) diff --git a/packages/@vue/cli-plugin-eslint/eslintDeps.js b/packages/@vue/cli-plugin-eslint/eslintDeps.js new file mode 100644 index 0000000000..80baf86200 --- /dev/null +++ b/packages/@vue/cli-plugin-eslint/eslintDeps.js @@ -0,0 +1,45 @@ +const DEPS_MAP = { + base: { + eslint: '^6.7.2', + 'eslint-plugin-vue': '^6.1.2' + }, + airbnb: { + '@vue/eslint-config-airbnb': '^5.0.1', + 'eslint-plugin-import': '^2.18.2' + }, + prettier: { + '@vue/eslint-config-prettier': '^6.0.0', + 'eslint-plugin-prettier': '^3.1.1', + prettier: '^1.19.1' + }, + standard: { + '@vue/eslint-config-standard': '^5.1.0', + 'eslint-plugin-import': '^2.18.2', + 'eslint-plugin-node': '^9.1.0', + 'eslint-plugin-promise': '^4.2.1', + 'eslint-plugin-standard': '^4.0.0' + }, + typescript: { + '@vue/eslint-config-typescript': '^5.0.1', + '@typescript-eslint/eslint-plugin': '^2.10.0', + '@typescript-eslint/parser': '^2.10.0' + } +} + +exports.DEPS_MAP = DEPS_MAP + +exports.getDeps = function (api, preset) { + const deps = Object.assign({}, DEPS_MAP.base, DEPS_MAP[preset]) + + if (api.hasPlugin('typescript')) { + Object.assign(deps, DEPS_MAP.typescript) + } + + if (api.hasPlugin('babel') && !api.hasPlugin('typescript')) { + Object.assign(deps, { + 'babel-eslint': '^10.0.3' + }) + } + + return deps +} diff --git a/packages/@vue/cli-plugin-eslint/eslintOptions.js b/packages/@vue/cli-plugin-eslint/eslintOptions.js index c278c036a4..4f4c528ba1 100644 --- a/packages/@vue/cli-plugin-eslint/eslintOptions.js +++ b/packages/@vue/cli-plugin-eslint/eslintOptions.js @@ -1,4 +1,4 @@ -exports.config = api => { +exports.config = (api, preset) => { const config = { root: true, env: { node: true }, @@ -11,11 +11,35 @@ exports.config = api => { 'no-debugger': makeJSOnlyValue(`process.env.NODE_ENV === 'production' ? 'error' : 'off'`) } } + if (api.hasPlugin('babel') && !api.hasPlugin('typescript')) { config.parserOptions = { parser: 'babel-eslint' } } + + if (preset === 'airbnb') { + config.extends.push('@vue/airbnb') + } else if (preset === 'standard') { + config.extends.push('@vue/standard') + } else if (preset === 'prettier') { + config.extends.push(...['eslint:recommended', '@vue/prettier']) + } else { + // default + config.extends.push('eslint:recommended') + } + + if (api.hasPlugin('typescript')) { + // typically, typescript ruleset should be appended to the end of the `extends` array + // but that is not the case for prettier, as there are conflicting rules + if (preset === 'prettier') { + config.extends.pop() + config.extends.push(...['@vue/typescript/recommended', '@vue/prettier', '@vue/prettier/@typescript-eslint']) + } else { + config.extends.push('@vue/typescript/recommended') + } + } + return config } diff --git a/packages/@vue/cli-plugin-eslint/generator/index.js b/packages/@vue/cli-plugin-eslint/generator/index.js index b6a4b76e84..042c4fdf56 100644 --- a/packages/@vue/cli-plugin-eslint/generator/index.js +++ b/packages/@vue/cli-plugin-eslint/generator/index.js @@ -2,74 +2,15 @@ const fs = require('fs') const path = require('path') module.exports = (api, { config, lintOn = [] }, _, invoking) => { - const eslintConfig = require('../eslintOptions').config(api) - const extentions = require('../eslintOptions').extensions(api) - .map(ext => ext.replace(/^\./, '')) // remove the leading `.` + const eslintConfig = require('../eslintOptions').config(api, config) + const devDependencies = require('../eslintDeps').getDeps(api, config) const pkg = { scripts: { lint: 'vue-cli-service lint' }, eslintConfig, - devDependencies: { - eslint: '^6.7.2', - 'eslint-plugin-vue': '^6.1.2' - } - } - - if (api.hasPlugin('babel') && !api.hasPlugin('typescript')) { - pkg.devDependencies['babel-eslint'] = '^10.0.3' - } - - switch (config) { - case 'airbnb': - eslintConfig.extends.push('@vue/airbnb') - Object.assign(pkg.devDependencies, { - '@vue/eslint-config-airbnb': '^5.0.1', - 'eslint-plugin-import': '^2.18.2' - }) - break - case 'standard': - eslintConfig.extends.push('@vue/standard') - Object.assign(pkg.devDependencies, { - '@vue/eslint-config-standard': '^5.1.0', - 'eslint-plugin-import': '^2.18.2', - 'eslint-plugin-node': '^9.1.0', - 'eslint-plugin-promise': '^4.2.1', - 'eslint-plugin-standard': '^4.0.0' - }) - break - case 'prettier': - eslintConfig.extends.push( - ...(api.hasPlugin('typescript') - ? ['eslint:recommended', '@vue/typescript/recommended', '@vue/prettier', '@vue/prettier/@typescript-eslint'] - : ['eslint:recommended', '@vue/prettier'] - ) - ) - Object.assign(pkg.devDependencies, { - '@vue/eslint-config-prettier': '^6.0.0', - 'eslint-plugin-prettier': '^3.1.1', - prettier: '^1.19.1' - }) - break - default: - // default - eslintConfig.extends.push('eslint:recommended') - break - } - - // typescript support - if (api.hasPlugin('typescript')) { - Object.assign(pkg.devDependencies, { - '@vue/eslint-config-typescript': '^5.0.1', - '@typescript-eslint/eslint-plugin': '^2.10.0', - '@typescript-eslint/parser': '^2.10.0' - }) - if (config !== 'prettier') { - // for any config other than `prettier`, - // typescript ruleset should be appended to the end of the `extends` array - eslintConfig.extends.push('@vue/typescript/recommended') - } + devDependencies } const editorConfigTemplatePath = path.resolve(__dirname, `./template/${config}/_editorconfig`) @@ -102,6 +43,8 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => { pkg.gitHooks = { 'pre-commit': 'lint-staged' } + const extentions = require('../eslintOptions').extensions(api) + .map(ext => ext.replace(/^\./, '')) // remove the leading `.` pkg['lint-staged'] = { [`*.{${extentions.join(',')}}`]: ['vue-cli-service lint', 'git add'] } @@ -157,10 +100,6 @@ module.exports.applyTS = api => { parser: '@typescript-eslint/parser' } }, - devDependencies: { - '@vue/eslint-config-typescript': '^5.0.1', - '@typescript-eslint/eslint-plugin': '^2.7.0', - '@typescript-eslint/parser': '^2.7.0' - } + devDependencies: require('../eslintDeps').DEPS_MAP.typescript }) } diff --git a/packages/@vue/cli-plugin-eslint/migrator/index.js b/packages/@vue/cli-plugin-eslint/migrator/index.js index a1915af9d3..fe5caa41e8 100644 --- a/packages/@vue/cli-plugin-eslint/migrator/index.js +++ b/packages/@vue/cli-plugin-eslint/migrator/index.js @@ -1,26 +1,74 @@ -module.exports = (api) => { +const inquirer = require('inquirer') +const { semver } = require('@vue/cli-shared-utils') + +module.exports = async (api) => { + const pkg = require(api.resolve('package.json')) + + let localESLintRange = pkg.devDependencies.eslint + // if project is scaffolded by Vue CLI 3.0.x or earlier, // the ESLint dependency (ESLint v4) is inside @vue/cli-plugin-eslint; // in Vue CLI v4 it should be extracted to the project dependency list. - if (api.fromVersion('^3')) { - const pkg = require(api.resolve('package.json')) - const hasESLint = [ - 'dependencies', - 'devDependencies', - 'peerDependencies', - 'optionalDependencies' - ].some(depType => - Object.keys(pkg[depType] || {}).includes('eslint') + if (api.fromVersion('^3') && !localESLintRange) { + localESLintRange = '^4.19.1' + api.extendPackage({ + devDependencies: { + eslint: localESLintRange, + 'babel-eslint': '^8.2.5', + 'eslint-plugin-vue': '^4.5.0' + } + }) + } + + const localESLintMajor = semver.major( + semver.maxSatisfying( + ['4.99.0', '5.99.0', '6.99.0'], + localESLintRange ) + ) + + if (localESLintMajor === 6) { + return + } + + const { confirmUpgrade } = await inquirer.prompt([{ + name: 'confirmUpgrade', + type: 'confirm', + message: + `Your current ESLint version is v${localESLintMajor}.` + + `The lastest major version is v6.\n` + + `Do you want to upgrade? (May contain breaking changes)\n` + }]) + + if (confirmUpgrade) { + const { getDeps } = require('../eslintDeps') + + const newDeps = getDeps(api) + if (pkg.devDependencies['@vue/eslint-config-airbnb']) { + Object.assign(newDeps, getDeps(api, 'airbnb')) + } + if (pkg.devDependencies['@vue/eslint-config-standard']) { + Object.assign(newDeps, getDeps(api, 'standard')) + } + if (pkg.devDependencies['@vue/eslint-config-prettier']) { + Object.assign(newDeps, getDeps(api, 'prettier')) + } + + api.extendPackage({ devDependencies: newDeps }) - if (!hasESLint) { + // in case anyone's upgrading from the legacy `typescript-eslint-parser` + if (api.hasPlugin('typescript')) { api.extendPackage({ - devDependencies: { - eslint: '^4.19.1' + eslintConfig: { + parserOptions: { + parser: '@typescript-eslint/parser' + } } }) } - // TODO: add a prompt for users to optionally upgrade their eslint configs to a new major version + // TODO: + // transform `@vue/prettier` to `eslint:recommended` + `@vue/prettier` + // transform `@vue/typescript` to `@vue/typescript/recommended` and also fix prettier compatibility for it } } diff --git a/packages/@vue/cli-plugin-eslint/package.json b/packages/@vue/cli-plugin-eslint/package.json index 080f777e87..d49e8c4359 100644 --- a/packages/@vue/cli-plugin-eslint/package.json +++ b/packages/@vue/cli-plugin-eslint/package.json @@ -26,6 +26,7 @@ "@vue/cli-shared-utils": "^4.1.2", "eslint-loader": "^2.1.2", "globby": "^9.2.0", + "inquirer": "^6.3.1", "webpack": "^4.0.0", "yorkie": "^2.0.0" }, diff --git a/packages/@vue/cli-test-utils/createUpgradableProject.js b/packages/@vue/cli-test-utils/createUpgradableProject.js new file mode 100644 index 0000000000..37b62c9755 --- /dev/null +++ b/packages/@vue/cli-test-utils/createUpgradableProject.js @@ -0,0 +1,24 @@ +const fs = require('fs') +const path = require('path') + +const createTestProject = require('./createTestProject') +const Upgrader = require('@vue/cli/lib/Upgrader') + +const outsideTestFolder = path.resolve(__dirname, '../../../../vue-upgrade-tests') + +module.exports = async function createUpgradableProject (...args) { + if (!fs.existsSync(outsideTestFolder)) { + fs.mkdirSync(outsideTestFolder) + } + process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN = true + + const project = await createTestProject(...args, outsideTestFolder) + const upgrade = async function upgrade (pluginName, options) { + return (new Upgrader(project.dir)).upgrade(pluginName, options || {}) + } + + return { + ...project, + upgrade + } +}