EntrypointOverlapPlugin.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const EntrypointOverlapWarning = require("../errors/EntrypointOverlapWarning");
  7. const { compareStrings } = require("../util/comparators");
  8. const getModuleSize = require("./getModuleSize");
  9. const getSourceModules = require("./getSourceModules");
  10. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  11. /** @import Chunk from "../Chunk" */
  12. /** @import Compiler from "../Compiler" */
  13. /** @import Module from "../Module" */
  14. /**
  15. * @import {
  16. * EntrypointOverlapDetails
  17. * } from "../errors/EntrypointOverlapWarning"
  18. */
  19. const PLUGIN_NAME = "EntrypointOverlapPlugin";
  20. // Enough to name the offenders without printing the module graph.
  21. const MAX_REPORTED_MODULES = 5;
  22. class EntrypointOverlapPlugin {
  23. /**
  24. * Creates an instance of EntrypointOverlapPlugin.
  25. * @param {PerformanceOptions} options the plugin options
  26. */
  27. constructor(options) {
  28. /** @type {PerformanceOptions["hints"]} */
  29. this.hints = options.hints;
  30. }
  31. /**
  32. * Applies the plugin by registering its hooks on the compiler.
  33. * @param {Compiler} compiler the compiler instance
  34. * @returns {void}
  35. */
  36. apply(compiler) {
  37. const hints = this.hints;
  38. if (!hints) return;
  39. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  40. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  41. const { chunkGraph, entrypoints, requestShortener } = compilation;
  42. if (entrypoints.size < 2) return;
  43. /** @type {Map<Module, { chunks: Set<Chunk>, entrypoints: Set<string> }>} */
  44. const reach = new Map();
  45. for (const [name, entrypoint] of entrypoints) {
  46. for (const chunk of entrypoint.chunks) {
  47. for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
  48. // Through scope hoisting: two entrypoints concatenating the same
  49. // file produce two modules, so the chunk graph alone shows none.
  50. for (const inner of getSourceModules(module)) {
  51. let entry = reach.get(inner);
  52. if (entry === undefined) {
  53. reach.set(
  54. inner,
  55. (entry = { chunks: new Set(), entrypoints: new Set() })
  56. );
  57. }
  58. entry.chunks.add(chunk);
  59. entry.entrypoints.add(name);
  60. }
  61. }
  62. }
  63. }
  64. /** @type {EntrypointOverlapDetails[]} */
  65. const overlapping = [];
  66. let wasted = 0;
  67. for (const [module, { chunks, entrypoints: names }] of reach) {
  68. // One chunk reached from several entrypoints is downloaded once, so
  69. // only separate copies cost anything.
  70. if (chunks.size < 2 || names.size < 2) continue;
  71. const extra = getModuleSize(module) * (chunks.size - 1);
  72. wasted += extra;
  73. overlapping.push({
  74. name: module.readableIdentifier(requestShortener),
  75. entrypoints: [...names].sort(compareStrings),
  76. wasted: extra
  77. });
  78. }
  79. if (overlapping.length === 0) return;
  80. // Ties break by name: which modules finish first is not stable.
  81. overlapping.sort(
  82. (a, b) => b.wasted - a.wasted || compareStrings(a.name, b.name)
  83. );
  84. const warning = new EntrypointOverlapWarning(
  85. overlapping.slice(0, MAX_REPORTED_MODULES),
  86. wasted
  87. );
  88. if (hints === "error") {
  89. compilation.errors.push(warning);
  90. } else if (hints === "stats") {
  91. compilation.hints.push(warning);
  92. } else {
  93. compilation.warnings.push(warning);
  94. }
  95. });
  96. });
  97. }
  98. }
  99. module.exports = EntrypointOverlapPlugin;