Skip to content

Commit

Permalink
feat(cli-plugin-eslint): use ESLint class instead of CLIEngine (#6714)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Sep 29, 2021
1 parent 5f86080 commit 099ba93
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 56 deletions.
50 changes: 50 additions & 0 deletions packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js
Expand Up @@ -270,3 +270,53 @@ 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(';')
})

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
foo=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}`)
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)
})
8 changes: 4 additions & 4 deletions packages/@vue/cli-plugin-eslint/generator/index.js
Expand Up @@ -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)
})
}
}
Expand All @@ -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) {}
})
}
Expand Down
4 changes: 2 additions & 2 deletions packages/@vue/cli-plugin-eslint/index.js
Expand Up @@ -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)
}
)
}
141 changes: 102 additions & 39 deletions packages/@vue/cli-plugin-eslint/lint.js
Expand Up @@ -2,28 +2,31 @@ 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'],
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: '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)
Expand All @@ -37,34 +40,71 @@ 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.
// Though, if users provided their own `.eslintignore` file, we don't want to
// 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._
Expand All @@ -79,51 +119,53 @@ 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)}`)
}
}

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}).`)
}
Expand All @@ -138,14 +180,35 @@ function normalizeConfig (args) {
const config = {}
for (const key in args) {
if (renamedArrayArgs[key]) {
config[renamedArrayArgs[key]] = 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]) {
config[renamedArgs[key]] = 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 === 'true' : defaultBool
}
return object
}
}

function camelize (str) {
Expand Down
4 changes: 2 additions & 2 deletions packages/@vue/cli-service/types/cli-service-test.ts
Expand Up @@ -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 => {})
Expand Down
17 changes: 9 additions & 8 deletions scripts/buildEditorConfig.js
Expand Up @@ -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]
Expand Down Expand Up @@ -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/'))
Expand All @@ -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) {
Expand All @@ -119,6 +120,6 @@ exports.buildEditorConfig = function buildEditorConfig () {
fs.mkdirSync(templateDir)
}
fs.writeFileSync(`${templateDir}/_editorconfig`, content)
})
}))
console.log('EditorConfig files up-to-date.')
}
2 changes: 1 addition & 1 deletion scripts/release.js
Expand Up @@ -88,7 +88,7 @@ const release = async () => {
})
delete process.env.PREFIX

// buildEditorConfig()
// await buildEditorConfig()

try {
await execa('git', ['add', '-A'], { stdio: 'inherit' })
Expand Down

0 comments on commit 099ba93

Please sign in to comment.