From eed5633655ea1a67ca3dc411e33fc40ea9ecafa9 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 25 Sep 2021 23:40:30 +0900 Subject: [PATCH 1/5] Use ESLint class instead of CLIEngine --- .../__tests__/eslintPlugin.spec.js | 23 ++++ .../@vue/cli-plugin-eslint/generator/index.js | 8 +- packages/@vue/cli-plugin-eslint/index.js | 4 +- packages/@vue/cli-plugin-eslint/lint.js | 127 ++++++++++++------ .../cli-service/types/cli-service-test.ts | 4 +- scripts/buildEditorConfig.js | 17 +-- scripts/release.js | 2 +- 7 files changed, 129 insertions(+), 56 deletions(-) diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js index 6245a034c0..ead8df83da 100644 --- a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js @@ -270,3 +270,26 @@ test(`should use formatter 'codeframe'`, async () => { await donePromise }) + +test(`should work with eslint v8`, async () => { + const project = await create('eslint-v8', { + plugins: { + '@vue/cli-plugin-babel': {}, + '@vue/cli-plugin-eslint': { + config: 'airbnb', + lintOn: 'save' + } + } + }) + const { read, write, run } = project + await run('npm i -D eslint@^8.0.0-0 eslint-formatter-codeframe') + // should've applied airbnb autofix + const main = await read('src/main.js') + expect(main).toMatch(';') + // remove semicolons + const updatedMain = main.replace(/;/g, '') + await write('src/main.js', updatedMain) + // lint + await run('vue-cli-service lint') + expect(await read('src/main.js')).toMatch(';') +}) diff --git a/packages/@vue/cli-plugin-eslint/generator/index.js b/packages/@vue/cli-plugin-eslint/generator/index.js index 01f39dde44..7bdfb31213 100644 --- a/packages/@vue/cli-plugin-eslint/generator/index.js +++ b/packages/@vue/cli-plugin-eslint/generator/index.js @@ -69,8 +69,8 @@ module.exports = (api, { config, lintOn = [] }, rootOptions, invoking) => { api.assertCliVersion('^4.0.0-beta.0') } catch (e) { if (config && config !== 'base') { - api.onCreateComplete(() => { - require('../lint')({ silent: true }, api) + api.onCreateComplete(async () => { + await require('../lint')({ silent: true }, api) }) } } @@ -84,9 +84,9 @@ module.exports = (api, { config, lintOn = [] }, rootOptions, invoking) => { // FIXME: at the moment we have to catch the bug and silently fail. Need to fix later. module.exports.hooks = (api) => { // lint & fix after create to ensure files adhere to chosen config - api.afterAnyInvoke(() => { + api.afterAnyInvoke(async () => { try { - require('../lint')({ silent: true }, api) + await require('../lint')({ silent: true }, api) } catch (e) {} }) } diff --git a/packages/@vue/cli-plugin-eslint/index.js b/packages/@vue/cli-plugin-eslint/index.js index 863c404551..f67d7c4fbf 100644 --- a/packages/@vue/cli-plugin-eslint/index.js +++ b/packages/@vue/cli-plugin-eslint/index.js @@ -77,8 +77,8 @@ module.exports = (api, options) => { details: 'For more options, see https://eslint.org/docs/user-guide/command-line-interface#options' }, - args => { - require('./lint')(args, api) + async args => { + await require('./lint')(args, api) } ) } diff --git a/packages/@vue/cli-plugin-eslint/lint.js b/packages/@vue/cli-plugin-eslint/lint.js index 9d5eec9ea1..08b9a369ec 100644 --- a/packages/@vue/cli-plugin-eslint/lint.js +++ b/packages/@vue/cli-plugin-eslint/lint.js @@ -2,28 +2,28 @@ const fs = require('fs') const globby = require('globby') const renamedArrayArgs = { - ext: 'extensions', - env: 'envs', - global: 'globals', - rulesdir: 'rulePaths', - plugin: 'plugins', - 'ignore-pattern': 'ignorePattern' + ext: ['extensions'], + env: ['overrideConfig', 'env'], + global: ['overrideConfig', 'globals'], + rulesdir: ['rulePaths'], + plugin: ['overrideConfig', 'plugins'], + 'ignore-pattern': ['overrideConfig', 'ignorePatterns'] } const renamedArgs = { - 'inline-config': 'allowInlineConfig', - rule: 'rules', - eslintrc: 'useEslintrc', - c: 'configFile', - config: 'configFile', - 'output-file': 'outputFile' + 'inline-config': ['allowInlineConfig'], + rule: ['overrideConfig', 'rules'], + eslintrc: ['useEslintrc'], + c: ['overrideConfigFile'], + config: ['overrideConfigFile'], + 'output-file': ['outputFile'] } -module.exports = function lint (args = {}, api) { +module.exports = async function lint (args = {}, api) { const path = require('path') const cwd = api.resolve('.') const { log, done, exit, chalk, loadModule } = require('@vue/cli-shared-utils') - const { CLIEngine } = loadModule('eslint', cwd, true) || require('eslint') + const { ESLint } = loadModule('eslint', cwd, true) || require('eslint') const extensions = require('./eslintOptions').extensions(api) const argsConfig = normalizeConfig(args) @@ -37,7 +37,11 @@ module.exports = function lint (args = {}, api) { const noFixWarningsPredicate = (lintResult) => lintResult.severity === 2 config.fix = config.fix && (noFixWarnings ? noFixWarningsPredicate : true) - if (!fs.existsSync(api.resolve('.eslintignore')) && !config.ignorePattern) { + if (!config.overrideConfig) { + config.overrideConfig = {} + } + + if (!fs.existsSync(api.resolve('.eslintignore')) && !config.overrideConfig.ignorePatterns) { // .eslintrc.js files (ignored by default) // However, we need to lint & fix them so as to make the default generated project's // code style consistent with user's selected eslint config. @@ -45,26 +49,59 @@ module.exports = function lint (args = {}, api) { // add our own customized ignore pattern here (in eslint, ignorePattern is // an addition to eslintignore, i.e. it can't be overridden by user), // following the principle of least astonishment. - config.ignorePattern = [ + config.overrideConfig.ignorePatterns = [ '!.*.js', '!{src,tests}/**/.*.js' ] } - - const engine = new CLIEngine(config) - - const defaultFilesToLint = [ + /** @type {import('eslint').ESLint} */ + const eslint = new ESLint(Object.fromEntries([ + + // File enumeration + 'cwd', + 'errorOnUnmatchedPattern', + 'extensions', + 'globInputPaths', + 'ignore', + 'ignorePath', + + // Linting + 'allowInlineConfig', + 'baseConfig', + 'overrideConfig', + 'overrideConfigFile', + 'plugins', + 'reportUnusedDisableDirectives', + 'resolvePluginsRelativeTo', + 'rulePaths', + 'useEslintrc', + + // Autofix + 'fix', + 'fixTypes', + + // Cache-related + 'cache', + 'cacheLocation', + 'cacheStrategy' + ].map(k => [k, config[k]]))) + + const defaultFilesToLint = [] + + for (const pattern of [ 'src', 'tests', // root config files '*.js', '.*.js' - ] - .filter(pattern => - globby - .sync(pattern, { cwd, absolute: true }) - .some(p => !engine.isPathIgnored(p)) - ) + ]) { + if ((await Promise.all(globby + .sync(pattern, { cwd, absolute: true }) + .map(p => eslint.isPathIgnored(p)))) + .some(r => !r)) { + defaultFilesToLint.push(pattern) + } + } const files = args._ && args._.length ? args._ @@ -79,15 +116,17 @@ module.exports = function lint (args = {}, api) { if (!api.invoking) { process.cwd = () => cwd } - const report = engine.executeOnFiles(files) + const resultResults = await eslint.lintFiles(files) + const reportErrorCount = resultResults.reduce((p, c) => p + c.errorCount, 0) + const reportWarningCount = resultResults.reduce((p, c) => p + c.warningCount, 0) process.cwd = processCwd - const formatter = engine.getFormatter(args.format || 'codeframe') + const formatter = await eslint.loadFormatter(args.format || 'codeframe') if (config.outputFile) { const outputFilePath = path.resolve(config.outputFile) try { - fs.writeFileSync(outputFilePath, formatter(report.results)) + fs.writeFileSync(outputFilePath, formatter.format(resultResults)) log(`Lint results saved to ${chalk.blue(outputFilePath)}`) } catch (err) { log(`Error saving lint results to ${chalk.blue(outputFilePath)}: ${chalk.red(err)}`) @@ -95,35 +134,35 @@ module.exports = function lint (args = {}, api) { } if (config.fix) { - CLIEngine.outputFixes(report) + await ESLint.outputFixes(resultResults) } const maxErrors = argsConfig.maxErrors || 0 const maxWarnings = typeof argsConfig.maxWarnings === 'number' ? argsConfig.maxWarnings : Infinity - const isErrorsExceeded = report.errorCount > maxErrors - const isWarningsExceeded = report.warningCount > maxWarnings + const isErrorsExceeded = reportErrorCount > maxErrors + const isWarningsExceeded = reportWarningCount > maxWarnings if (!isErrorsExceeded && !isWarningsExceeded) { if (!args.silent) { - const hasFixed = report.results.some(f => f.output) + const hasFixed = resultResults.some(f => f.output) if (hasFixed) { log(`The following files have been auto-fixed:`) log() - report.results.forEach(f => { + resultResults.forEach(f => { if (f.output) { log(` ${chalk.blue(path.relative(cwd, f.filePath))}`) } }) log() } - if (report.warningCount || report.errorCount) { - console.log(formatter(report.results)) + if (reportWarningCount || reportErrorCount) { + console.log(formatter.format(resultResults)) } else { done(hasFixed ? `All lint errors auto-fixed.` : `No lint errors found!`) } } } else { - console.log(formatter(report.results)) + console.log(formatter.format(resultResults)) if (isErrorsExceeded && typeof argsConfig.maxErrors === 'number') { log(`Eslint found too many errors (maximum: ${argsConfig.maxErrors}).`) } @@ -138,9 +177,19 @@ function normalizeConfig (args) { const config = {} for (const key in args) { if (renamedArrayArgs[key]) { - config[renamedArrayArgs[key]] = args[key].split(',') + const keyPaths = [...renamedArrayArgs[key]] + const lastKey = keyPaths.pop() + for (const k of keyPaths) { + config[k] = {} + } + config[lastKey] = args[key].split(',') } else if (renamedArgs[key]) { - config[renamedArgs[key]] = args[key] + const keyPaths = [...renamedArgs[key]] + const lastKey = keyPaths.pop() + for (const k of keyPaths) { + config[k] = {} + } + config[lastKey] = args[key] } else if (key !== '_') { config[camelize(key)] = args[key] } diff --git a/packages/@vue/cli-service/types/cli-service-test.ts b/packages/@vue/cli-service/types/cli-service-test.ts index 43ec272afe..ead164e792 100644 --- a/packages/@vue/cli-service/types/cli-service-test.ts +++ b/packages/@vue/cli-service/types/cli-service-test.ts @@ -17,8 +17,8 @@ const servicePlugin: ServicePlugin = (api, options) => { }, details: 'For more options, see https://eslint.org/docs/user-guide/command-line-interface#options' }, - args => { - require('./lint')(args, api) + async args => { + await require('./lint')(args, api) } ) api.registerCommand('lint', args => {}) diff --git a/scripts/buildEditorConfig.js b/scripts/buildEditorConfig.js index 4845cc09fd..b3d087e03e 100644 --- a/scripts/buildEditorConfig.js +++ b/scripts/buildEditorConfig.js @@ -10,18 +10,19 @@ const fs = require('fs') const path = require('path') -const CLIEngine = require('eslint').CLIEngine +const ESLint = require('eslint').ESLint // Convert eslint rules to editorconfig rules. -function convertRules (config) { +async function convertRules (config) { const result = {} - const eslintRules = new CLIEngine({ + const eslint = new ESLint({ useEslintrc: false, baseConfig: { extends: [require.resolve(`@vue/eslint-config-${config}`)] } - }).getConfigForFile().rules + }) + const eslintRules = (await eslint.calculateConfigForFile()).rules const getRuleOptions = (ruleName, defaultOptions = []) => { const ruleConfig = eslintRules[ruleName] @@ -90,7 +91,7 @@ function convertRules (config) { return result } -exports.buildEditorConfig = function buildEditorConfig () { +exports.buildEditorConfig = async function buildEditorConfig () { console.log('Building EditorConfig files...') // Get built-in eslint configs const configList = fs.readdirSync(path.resolve(__dirname, '../packages/@vue/')) @@ -100,10 +101,10 @@ exports.buildEditorConfig = function buildEditorConfig () { }) .filter(x => x) - configList.forEach(config => { + await Promise.all(configList.map(async config => { let content = '[*.{js,jsx,ts,tsx,vue}]\n' - const editorconfig = convertRules(config) + const editorconfig = await convertRules(config) // `eslint-config-prettier` & `eslint-config-typescript` do not have any style rules if (!Object.keys(editorconfig).length) { @@ -119,6 +120,6 @@ exports.buildEditorConfig = function buildEditorConfig () { fs.mkdirSync(templateDir) } fs.writeFileSync(`${templateDir}/_editorconfig`, content) - }) + })) console.log('EditorConfig files up-to-date.') } diff --git a/scripts/release.js b/scripts/release.js index 2ab14c82c1..2c71aeaa15 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -88,7 +88,7 @@ const release = async () => { }) delete process.env.PREFIX - // buildEditorConfig() + // await buildEditorConfig() try { await execa('git', ['add', '-A'], { stdio: 'inherit' }) From ade9d131561a09f87ccb245a2206d8f8378b05da Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 28 Sep 2021 08:15:57 +0000 Subject: [PATCH 2/5] fix(cli-plugin-eslint): args bug --- .../__tests__/eslintPlugin.spec.js | 26 ++++++++++++ packages/@vue/cli-plugin-eslint/lint.js | 42 ++++++++++++------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js index ead8df83da..fb7ebb7397 100644 --- a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js @@ -293,3 +293,29 @@ test(`should work with eslint v8`, async () => { await run('vue-cli-service lint') expect(await read('src/main.js')).toMatch(';') }) + +test(`should work with eslint args`, async () => { + const project = await create('eslint-with-args', { + plugins: { + '@vue/cli-plugin-babel': {}, + '@vue/cli-plugin-eslint': { + config: 'airbnb', + lintOn: 'save' + } + } + }) + const { read, write, run } = project + await write('src/main.js', ` +foo() // Check for apply --global +$('hi!') // Check for apply --env +`) + // result file name + const resultsFile = 'lint_results.json' + // lint + await run(`vue-cli-service lint --ext .js --plugin vue --env jquery --global foo --format json --output-file ${resultsFile}`) + expect(await read('src/main.js')).toMatch(';') + + const resultsContents = JSON.parse(await read(resultsFile)) + const resultForMain = resultsContents.find(({ filePath }) => filePath.endsWith('src/main.js')) + expect(resultForMain.messages.length).toBe(0) +}) diff --git a/packages/@vue/cli-plugin-eslint/lint.js b/packages/@vue/cli-plugin-eslint/lint.js index 08b9a369ec..08959c65db 100644 --- a/packages/@vue/cli-plugin-eslint/lint.js +++ b/packages/@vue/cli-plugin-eslint/lint.js @@ -3,13 +3,16 @@ const globby = require('globby') const renamedArrayArgs = { ext: ['extensions'], - env: ['overrideConfig', 'env'], - global: ['overrideConfig', 'globals'], rulesdir: ['rulePaths'], plugin: ['overrideConfig', 'plugins'], 'ignore-pattern': ['overrideConfig', 'ignorePatterns'] } +const renamedObjectArgs = { + env: { key: ['overrideConfig', 'env'], def: true }, + global: { key: ['overrideConfig', 'globals'], def: false } +} + const renamedArgs = { 'inline-config': ['allowInlineConfig'], rule: ['overrideConfig', 'rules'], @@ -177,24 +180,35 @@ function normalizeConfig (args) { const config = {} for (const key in args) { if (renamedArrayArgs[key]) { - const keyPaths = [...renamedArrayArgs[key]] - const lastKey = keyPaths.pop() - for (const k of keyPaths) { - config[k] = {} - } - config[lastKey] = args[key].split(',') + applyConfig(renamedArrayArgs[key], args[key].split(',')) + } else if (renamedObjectArgs[key]) { + const obj = arrayToBoolObject(args[key].split(','), renamedObjectArgs[key].def) + applyConfig(renamedObjectArgs[key].key, obj) } else if (renamedArgs[key]) { - const keyPaths = [...renamedArgs[key]] - const lastKey = keyPaths.pop() - for (const k of keyPaths) { - config[k] = {} - } - config[lastKey] = args[key] + applyConfig(renamedArgs[key], args[key]) } else if (key !== '_') { config[camelize(key)] = args[key] } } return config + + function applyConfig ([...keyPaths], value) { + let targetConfig = config + const lastKey = keyPaths.pop() + for (const k of keyPaths) { + targetConfig = targetConfig[k] || (targetConfig[k] = {}) + } + targetConfig[lastKey] = value + } + + function arrayToBoolObject (array, defaultBool) { + const object = {} + for (const element of array) { + const [key, value] = element.split(':') + object[key] = value != null ? value : defaultBool + } + return object + } } function camelize (str) { From 34efae4165cb860fb99964998e6336f5d995f7e0 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 28 Sep 2021 08:25:36 +0000 Subject: [PATCH 3/5] fix(cli-plugin-eslint): args bug --- .../@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js | 6 ++++-- packages/@vue/cli-plugin-eslint/lint.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js index fb7ebb7397..9017651e19 100644 --- a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js @@ -308,14 +308,16 @@ test(`should work with eslint args`, async () => { await write('src/main.js', ` foo() // Check for apply --global $('hi!') // Check for apply --env +$=42 `) // result file name const resultsFile = 'lint_results.json' // lint - await run(`vue-cli-service lint --ext .js --plugin vue --env jquery --global foo --format json --output-file ${resultsFile}`) + await run(`vue-cli-service lint --ext .js --plugin vue --env jquery --global foo:true --format json --output-file ${resultsFile}`) expect(await read('src/main.js')).toMatch(';') const resultsContents = JSON.parse(await read(resultsFile)) const resultForMain = resultsContents.find(({ filePath }) => filePath.endsWith('src/main.js')) - expect(resultForMain.messages.length).toBe(0) + expect(resultForMain.messages.length).toBe(1) + expect(resultForMain.messages[0].ruleId).toBe('no-global-assign') }) diff --git a/packages/@vue/cli-plugin-eslint/lint.js b/packages/@vue/cli-plugin-eslint/lint.js index 08959c65db..9eedd7f2b2 100644 --- a/packages/@vue/cli-plugin-eslint/lint.js +++ b/packages/@vue/cli-plugin-eslint/lint.js @@ -205,7 +205,7 @@ function normalizeConfig (args) { const object = {} for (const element of array) { const [key, value] = element.split(':') - object[key] = value != null ? value : defaultBool + object[key] = value != null ? value === 'true' : defaultBool } return object } From fdb41dc3d21ae6a55ed9efe06c9e70154d107915 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 28 Sep 2021 08:28:51 +0000 Subject: [PATCH 4/5] fix: testcase --- .../cli-plugin-eslint/__tests__/eslintPlugin.spec.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js index 9017651e19..3b3006fbba 100644 --- a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js @@ -312,8 +312,14 @@ $=42 `) // result file name const resultsFile = 'lint_results.json' - // lint - await run(`vue-cli-service lint --ext .js --plugin vue --env jquery --global foo:true --format json --output-file ${resultsFile}`) + try { + // lint + await run(`vue-cli-service lint --ext .js --plugin vue --env jquery --global foo:true --format json --output-file ${resultsFile}`) + } catch (e) { + // lint should fail + expect(e.code).toBe(1) + expect(e.failed).toBeTruthy() + } expect(await read('src/main.js')).toMatch(';') const resultsContents = JSON.parse(await read(resultsFile)) From a00287507436c743f769d21f20e81aaebf9256ee Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 28 Sep 2021 08:37:39 +0000 Subject: [PATCH 5/5] fix: testcase --- .../__tests__/eslintPlugin.spec.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js index 3b3006fbba..1538c20841 100644 --- a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js @@ -308,22 +308,15 @@ test(`should work with eslint args`, async () => { await write('src/main.js', ` foo() // Check for apply --global $('hi!') // Check for apply --env -$=42 +foo=42 `) // result file name const resultsFile = 'lint_results.json' - try { - // lint - await run(`vue-cli-service lint --ext .js --plugin vue --env jquery --global foo:true --format json --output-file ${resultsFile}`) - } catch (e) { - // lint should fail - expect(e.code).toBe(1) - expect(e.failed).toBeTruthy() - } + // lint + await run(`vue-cli-service lint --ext .js --plugin vue --env jquery --global foo:true --format json --output-file ${resultsFile}`) expect(await read('src/main.js')).toMatch(';') const resultsContents = JSON.parse(await read(resultsFile)) const resultForMain = resultsContents.find(({ filePath }) => filePath.endsWith('src/main.js')) - expect(resultForMain.messages.length).toBe(1) - expect(resultForMain.messages[0].ruleId).toBe('no-global-assign') + expect(resultForMain.messages.length).toBe(0) })