Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli-plugin-eslint): use ESLint class instead of CLIEngine #6714

Merged
merged 5 commits into from Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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