lint.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. const fs = require('fs')
  2. const globby = require('globby')
  3. const renamedArrayArgs = {
  4. ext: ['extensions'],
  5. rulesdir: ['rulePaths'],
  6. plugin: ['overrideConfig', 'plugins'],
  7. 'ignore-pattern': ['overrideConfig', 'ignorePatterns']
  8. }
  9. const renamedObjectArgs = {
  10. env: { key: ['overrideConfig', 'env'], def: true },
  11. global: { key: ['overrideConfig', 'globals'], def: false }
  12. }
  13. const renamedArgs = {
  14. 'inline-config': ['allowInlineConfig'],
  15. rule: ['overrideConfig', 'rules'],
  16. eslintrc: ['useEslintrc'],
  17. c: ['overrideConfigFile'],
  18. config: ['overrideConfigFile'],
  19. 'output-file': ['outputFile']
  20. }
  21. module.exports = async function lint (args = {}, api) {
  22. const path = require('path')
  23. const cwd = api.resolve('.')
  24. const { log, done, exit, chalk, loadModule } = require('@vue/cli-shared-utils')
  25. const { ESLint } = loadModule('eslint', cwd, true) || require('eslint')
  26. const extensions = require('./eslintOptions').extensions(api)
  27. const argsConfig = normalizeConfig(args)
  28. const config = Object.assign({
  29. extensions,
  30. fix: true,
  31. cwd
  32. }, argsConfig)
  33. const noFixWarnings = (argsConfig.fixWarnings === false)
  34. const noFixWarningsPredicate = (lintResult) => lintResult.severity === 2
  35. config.fix = config.fix && (noFixWarnings ? noFixWarningsPredicate : true)
  36. if (!config.overrideConfig) {
  37. config.overrideConfig = {}
  38. }
  39. if (!fs.existsSync(api.resolve('.eslintignore')) && !config.overrideConfig.ignorePatterns) {
  40. // .eslintrc.js files (ignored by default)
  41. // However, we need to lint & fix them so as to make the default generated project's
  42. // code style consistent with user's selected eslint config.
  43. // Though, if users provided their own `.eslintignore` file, we don't want to
  44. // add our own customized ignore pattern here (in eslint, ignorePattern is
  45. // an addition to eslintignore, i.e. it can't be overridden by user),
  46. // following the principle of least astonishment.
  47. config.overrideConfig.ignorePatterns = [
  48. '!.*.js',
  49. '!{src,tests}/**/.*.js'
  50. ]
  51. }
  52. /** @type {import('eslint').ESLint} */
  53. const eslint = new ESLint(Object.fromEntries([
  54. // File enumeration
  55. 'cwd',
  56. 'errorOnUnmatchedPattern',
  57. 'extensions',
  58. 'globInputPaths',
  59. 'ignore',
  60. 'ignorePath',
  61. // Linting
  62. 'allowInlineConfig',
  63. 'baseConfig',
  64. 'overrideConfig',
  65. 'overrideConfigFile',
  66. 'plugins',
  67. 'reportUnusedDisableDirectives',
  68. 'resolvePluginsRelativeTo',
  69. 'rulePaths',
  70. 'useEslintrc',
  71. // Autofix
  72. 'fix',
  73. 'fixTypes',
  74. // Cache-related
  75. 'cache',
  76. 'cacheLocation',
  77. 'cacheStrategy'
  78. ].map(k => [k, config[k]])))
  79. const defaultFilesToLint = []
  80. for (const pattern of [
  81. 'src',
  82. 'tests',
  83. // root config files
  84. '*.js',
  85. '.*.js'
  86. ]) {
  87. if ((await Promise.all(globby
  88. .sync(pattern, { cwd, absolute: true })
  89. .map(p => eslint.isPathIgnored(p))))
  90. .some(r => !r)) {
  91. defaultFilesToLint.push(pattern)
  92. }
  93. }
  94. const files = args._ && args._.length
  95. ? args._
  96. : defaultFilesToLint
  97. // mock process.cwd before executing
  98. // See:
  99. // https://github.com/vuejs/vue-cli/issues/2554
  100. // https://github.com/benmosher/eslint-plugin-import/issues/602
  101. // https://github.com/eslint/eslint/issues/11218
  102. const processCwd = process.cwd
  103. if (!api.invoking) {
  104. process.cwd = () => cwd
  105. }
  106. const resultResults = await eslint.lintFiles(files)
  107. const reportErrorCount = resultResults.reduce((p, c) => p + c.errorCount, 0)
  108. const reportWarningCount = resultResults.reduce((p, c) => p + c.warningCount, 0)
  109. process.cwd = processCwd
  110. const formatter = await eslint.loadFormatter(args.format || 'stylish')
  111. if (config.outputFile) {
  112. const outputFilePath = path.resolve(config.outputFile)
  113. try {
  114. fs.writeFileSync(outputFilePath, formatter.format(resultResults))
  115. log(`Lint results saved to ${chalk.blue(outputFilePath)}`)
  116. } catch (err) {
  117. log(`Error saving lint results to ${chalk.blue(outputFilePath)}: ${chalk.red(err)}`)
  118. }
  119. }
  120. if (config.fix) {
  121. await ESLint.outputFixes(resultResults)
  122. }
  123. const maxErrors = argsConfig.maxErrors || 0
  124. const maxWarnings = typeof argsConfig.maxWarnings === 'number' ? argsConfig.maxWarnings : Infinity
  125. const isErrorsExceeded = reportErrorCount > maxErrors
  126. const isWarningsExceeded = reportWarningCount > maxWarnings
  127. if (!isErrorsExceeded && !isWarningsExceeded) {
  128. if (!args.silent) {
  129. const hasFixed = resultResults.some(f => f.output)
  130. if (hasFixed) {
  131. log(`The following files have been auto-fixed:`)
  132. log()
  133. resultResults.forEach(f => {
  134. if (f.output) {
  135. log(` ${chalk.blue(path.relative(cwd, f.filePath))}`)
  136. }
  137. })
  138. log()
  139. }
  140. if (reportWarningCount || reportErrorCount) {
  141. console.log(formatter.format(resultResults))
  142. } else {
  143. done(hasFixed ? `All lint errors auto-fixed.` : `No lint errors found!`)
  144. }
  145. }
  146. } else {
  147. console.log(formatter.format(resultResults))
  148. if (isErrorsExceeded && typeof argsConfig.maxErrors === 'number') {
  149. log(`Eslint found too many errors (maximum: ${argsConfig.maxErrors}).`)
  150. }
  151. if (isWarningsExceeded) {
  152. log(`Eslint found too many warnings (maximum: ${argsConfig.maxWarnings}).`)
  153. }
  154. exit(1)
  155. }
  156. }
  157. function normalizeConfig (args) {
  158. const config = {}
  159. for (const key in args) {
  160. if (renamedArrayArgs[key]) {
  161. applyConfig(renamedArrayArgs[key], args[key].split(','))
  162. } else if (renamedObjectArgs[key]) {
  163. const obj = arrayToBoolObject(args[key].split(','), renamedObjectArgs[key].def)
  164. applyConfig(renamedObjectArgs[key].key, obj)
  165. } else if (renamedArgs[key]) {
  166. applyConfig(renamedArgs[key], args[key])
  167. } else if (key !== '_') {
  168. config[camelize(key)] = args[key]
  169. }
  170. }
  171. return config
  172. function applyConfig ([...keyPaths], value) {
  173. let targetConfig = config
  174. const lastKey = keyPaths.pop()
  175. for (const k of keyPaths) {
  176. targetConfig = targetConfig[k] || (targetConfig[k] = {})
  177. }
  178. targetConfig[lastKey] = value
  179. }
  180. function arrayToBoolObject (array, defaultBool) {
  181. const object = {}
  182. for (const element of array) {
  183. const [key, value] = element.split(':')
  184. object[key] = value != null ? value === 'true' : defaultBool
  185. }
  186. return object
  187. }
  188. }
  189. function camelize (str) {
  190. return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '')
  191. }