index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. "use strict";
  2. const {
  3. isAbsolute,
  4. join
  5. } = require('path');
  6. const {
  7. isMatch
  8. } = require('micromatch');
  9. const {
  10. getOptions
  11. } = require('./options');
  12. const linter = require('./linter');
  13. const {
  14. arrify,
  15. parseFiles,
  16. parseFoldersToGlobs
  17. } = require('./utils');
  18. /** @typedef {import('webpack').Compiler} Compiler */
  19. /** @typedef {import('./options').Options} Options */
  20. const ESLINT_PLUGIN = 'ESLintWebpackPlugin';
  21. let counter = 0;
  22. class ESLintWebpackPlugin {
  23. /**
  24. * @param {Options} options
  25. */
  26. constructor(options = {}) {
  27. this.key = ESLINT_PLUGIN;
  28. this.options = getOptions(options);
  29. this.run = this.run.bind(this);
  30. }
  31. /**
  32. * @param {Compiler} compiler
  33. * @returns {void}
  34. */
  35. apply(compiler) {
  36. // Generate key for each compilation,
  37. // this differentiates one from the other when being cached.
  38. this.key = compiler.name || `${this.key}_${counter += 1}`;
  39. const options = { ...this.options,
  40. exclude: parseFiles(this.options.exclude || [], this.getContext(compiler)),
  41. extensions: arrify(this.options.extensions),
  42. resourceQueryExclude: arrify(this.options.resourceQueryExclude || []).map(item => item instanceof RegExp ? item : new RegExp(item)),
  43. files: parseFiles(this.options.files || '', this.getContext(compiler))
  44. };
  45. const wanted = parseFoldersToGlobs(options.files, options.extensions);
  46. const exclude = parseFoldersToGlobs(this.options.exclude ? options.exclude : '**/node_modules/**', []); // If `lintDirtyModulesOnly` is disabled,
  47. // execute the linter on the build
  48. if (!this.options.lintDirtyModulesOnly) {
  49. compiler.hooks.run.tapPromise(this.key, c => this.run(c, options, wanted, exclude));
  50. }
  51. let isFirstRun = this.options.lintDirtyModulesOnly;
  52. compiler.hooks.watchRun.tapPromise(this.key, c => {
  53. if (isFirstRun) {
  54. isFirstRun = false;
  55. return Promise.resolve();
  56. }
  57. return this.run(c, options, wanted, exclude);
  58. });
  59. }
  60. /**
  61. * @param {Compiler} compiler
  62. * @param {Omit<Options, 'resourceQueryExclude'> & {resourceQueryExclude: RegExp[]}} options
  63. * @param {string[]} wanted
  64. * @param {string[]} exclude
  65. */
  66. async run(compiler, options, wanted, exclude) {
  67. // Do not re-hook
  68. if ( // @ts-ignore
  69. compiler.hooks.compilation.taps.find(({
  70. name
  71. }) => name === this.key)) {
  72. return;
  73. }
  74. compiler.hooks.compilation.tap(this.key, compilation => {
  75. /** @type {import('./linter').Linter} */
  76. let lint;
  77. /** @type {import('./linter').Reporter} */
  78. let report;
  79. /** @type number */
  80. let threads;
  81. try {
  82. ({
  83. lint,
  84. report,
  85. threads
  86. } = linter(this.key, options, compilation));
  87. } catch (e) {
  88. compilation.errors.push(e);
  89. return;
  90. }
  91. /** @type {string[]} */
  92. const files = []; // @ts-ignore
  93. // Add the file to be linted
  94. compilation.hooks.succeedModule.tap(this.key, ({
  95. resource
  96. }) => {
  97. if (resource) {
  98. const [file, query] = resource.split('?');
  99. if (file && !files.includes(file) && isMatch(file, wanted, {
  100. dot: true
  101. }) && !isMatch(file, exclude, {
  102. dot: true
  103. }) && options.resourceQueryExclude.every(reg => !reg.test(query))) {
  104. files.push(file);
  105. if (threads > 1) {
  106. lint(file);
  107. }
  108. }
  109. }
  110. }); // Lint all files added
  111. compilation.hooks.finishModules.tap(this.key, () => {
  112. if (files.length > 0 && threads <= 1) {
  113. lint(files);
  114. }
  115. }); // await and interpret results
  116. compilation.hooks.additionalAssets.tapPromise(this.key, processResults);
  117. async function processResults() {
  118. const {
  119. errors,
  120. warnings,
  121. generateReportAsset
  122. } = await report();
  123. if (warnings && !options.failOnWarning) {
  124. // @ts-ignore
  125. compilation.warnings.push(warnings);
  126. } else if (warnings && options.failOnWarning) {
  127. // @ts-ignore
  128. compilation.errors.push(warnings);
  129. }
  130. if (errors && options.failOnError) {
  131. // @ts-ignore
  132. compilation.errors.push(errors);
  133. } else if (errors && !options.failOnError) {
  134. // @ts-ignore
  135. compilation.warnings.push(errors);
  136. }
  137. if (generateReportAsset) {
  138. await generateReportAsset(compilation);
  139. }
  140. }
  141. });
  142. }
  143. /**
  144. *
  145. * @param {Compiler} compiler
  146. * @returns {string}
  147. */
  148. getContext(compiler) {
  149. if (!this.options.context) {
  150. return String(compiler.options.context);
  151. }
  152. if (!isAbsolute(this.options.context)) {
  153. return join(String(compiler.options.context), this.options.context);
  154. }
  155. return this.options.context;
  156. }
  157. }
  158. module.exports = ESLintWebpackPlugin;