BroadContextsPlugin.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const ContextModule = require("../ContextModule");
  7. const BroadContextsWarning = require("../errors/BroadContextsWarning");
  8. const { compareStrings } = require("../util/comparators");
  9. const getModuleSize = require("./getModuleSize");
  10. /** @typedef {import("../../declarations/WebpackOptions").PerformanceOptions} PerformanceOptions */
  11. /** @typedef {import("../Compiler")} Compiler */
  12. /** @typedef {import("../Dependency")} Dependency */
  13. /** @typedef {import("../Module")} Module */
  14. /** @typedef {import("../errors/BroadContextsWarning").BroadContextDetails} BroadContextDetails */
  15. const PLUGIN_NAME = "BroadContextsPlugin";
  16. // Enough to name the offenders without printing every context in the build.
  17. const MAX_REPORTED_CONTEXTS = 5;
  18. // A directory holding a handful of files costs little however it is matched,
  19. // and warning about it would bury the contexts that do carry weight.
  20. const MIN_REPORTED_MODULES = 20;
  21. // A bare leading wildcard takes every name in the directory, whether it is
  22. // `require.context`'s default or one webpack derived from an expression.
  23. const WILDCARD_PREFIX = "^\\.\\/.*";
  24. /**
  25. * Whether a context takes whatever the directory holds. A pattern that narrows
  26. * by anything other than a leading wildcard is left alone, as is one webpack
  27. * cannot read — an unrecognized filter is never guessed at.
  28. * @param {ContextModule} module a context module
  29. * @returns {boolean} true when nothing narrows it
  30. */
  31. const isBroad = (module) => {
  32. const { regExp, include, exclude } = module.options;
  33. if (include || exclude) return false;
  34. return (
  35. Boolean(regExp) &&
  36. /** @type {RegExp} */ (regExp).source.startsWith(WILDCARD_PREFIX)
  37. );
  38. };
  39. class BroadContextsPlugin {
  40. /**
  41. * Creates an instance of BroadContextsPlugin.
  42. * @param {PerformanceOptions} options the plugin options
  43. */
  44. constructor(options) {
  45. /** @type {PerformanceOptions["hints"]} */
  46. this.hints = options.hints;
  47. }
  48. /**
  49. * Applies the plugin by registering its hooks on the compiler.
  50. * @param {Compiler} compiler the compiler instance
  51. * @returns {void}
  52. */
  53. apply(compiler) {
  54. const hints = this.hints;
  55. if (!hints) return;
  56. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  57. // `afterSeal` is past the hash, which folds every message into it — a
  58. // hint reported earlier would change the build's identity.
  59. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  60. const { moduleGraph, requestShortener } = compilation;
  61. /** @type {BroadContextDetails[]} */
  62. const broad = [];
  63. for (const module of compilation.modules) {
  64. if (!(module instanceof ContextModule)) continue;
  65. if (!isBroad(module)) continue;
  66. // One file is reached under several requests — with and without its
  67. // extension — so the dependencies outnumber what is really matched.
  68. /** @type {Set<Module>} */
  69. const matched = new Set();
  70. /**
  71. * @param {Dependency[]} dependencies the dependencies to walk
  72. * @returns {void}
  73. */
  74. const collect = (dependencies) => {
  75. for (const dependency of dependencies) {
  76. const matchedModule = moduleGraph.getModule(dependency);
  77. if (matchedModule) matched.add(matchedModule);
  78. }
  79. };
  80. collect(module.dependencies);
  81. // A lazy context keeps each match in a block of its own, so reading
  82. // only `dependencies` would report it as matching nothing.
  83. for (const block of module.blocks) collect(block.dependencies);
  84. if (matched.size < MIN_REPORTED_MODULES) continue;
  85. let size = 0;
  86. for (const matchedModule of matched) {
  87. size += getModuleSize(matchedModule);
  88. }
  89. broad.push({
  90. name: module.readableIdentifier(requestShortener),
  91. modules: matched.size,
  92. size
  93. });
  94. }
  95. if (broad.length === 0) return;
  96. // Ties break by name: module order is not stable across runs.
  97. broad.sort((a, b) => b.size - a.size || compareStrings(a.name, b.name));
  98. const warning = new BroadContextsWarning(
  99. broad.slice(0, MAX_REPORTED_CONTEXTS),
  100. broad.length
  101. );
  102. if (hints === "error") {
  103. compilation.errors.push(warning);
  104. } else if (hints === "stats") {
  105. compilation.hints.push(warning);
  106. } else {
  107. compilation.warnings.push(warning);
  108. }
  109. });
  110. });
  111. }
  112. }
  113. module.exports = BroadContextsPlugin;