UnusedReexportsPlugin.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
  7. const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
  8. const UnusedReexportsWarning = require("../errors/UnusedReexportsWarning");
  9. const { compareStrings } = require("../util/comparators");
  10. const getModuleSize = require("./getModuleSize");
  11. const hasOnlyUnusedExports = require("./hasOnlyUnusedExports");
  12. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  13. /** @import Compiler from "../Compiler" */
  14. /** @import Module from "../Module" */
  15. /** @import ModuleGraph from "../ModuleGraph" */
  16. /** @import { UnusedReexportDetails } from "../errors/UnusedReexportsWarning" */
  17. const PLUGIN_NAME = "UnusedReexportsPlugin";
  18. // Enough to name the offenders without listing the barrel.
  19. const MAX_REPORTED_MODULES = 5;
  20. /**
  21. * Tells whether every module pulling this one in does so by re-exporting it.
  22. * One an importer wants for its side effects alone is deliberate, not a
  23. * barrel's leftover.
  24. * @param {Module} module the module to look up
  25. * @param {ModuleGraph} moduleGraph the module graph
  26. * @returns {boolean} true when only re-exports pull it in
  27. */
  28. const isOnlyReexported = (module, moduleGraph) => {
  29. // A re-export emits a side-effect edge of its own, so edges are grouped by
  30. // the statement they come from — `sourceOrder` — rather than by their type.
  31. /** @type {Map<Module, Set<number | undefined>>} */
  32. const reexportedStatements = new Map();
  33. /** @type {[Module, number | undefined][]} */
  34. const edges = [];
  35. for (const connection of moduleGraph.getIncomingConnections(module)) {
  36. const origin = connection.originModule;
  37. // An entry has no origin: it is in the build because it was asked for.
  38. if (!origin) return false;
  39. const dependency = connection.dependency;
  40. // Only an ESM import statement can carry a re-export.
  41. if (!(dependency instanceof HarmonyImportDependency)) return false;
  42. if (dependency instanceof HarmonyExportImportedSpecifierDependency) {
  43. const statements = reexportedStatements.get(origin);
  44. if (statements === undefined) {
  45. reexportedStatements.set(origin, new Set([dependency.sourceOrder]));
  46. } else {
  47. statements.add(dependency.sourceOrder);
  48. }
  49. }
  50. edges.push([origin, dependency.sourceOrder]);
  51. }
  52. if (edges.length === 0) return false;
  53. for (const [origin, sourceOrder] of edges) {
  54. const statements = reexportedStatements.get(origin);
  55. if (statements === undefined || !statements.has(sourceOrder)) return false;
  56. }
  57. return true;
  58. };
  59. class UnusedReexportsPlugin {
  60. /**
  61. * Creates an instance of UnusedReexportsPlugin.
  62. * @param {PerformanceOptions} options the plugin options
  63. */
  64. constructor(options) {
  65. /** @type {PerformanceOptions["hints"]} */
  66. this.hints = options.hints;
  67. }
  68. /**
  69. * Applies the plugin by registering its hooks on the compiler.
  70. * @param {Compiler} compiler the compiler instance
  71. * @returns {void}
  72. */
  73. apply(compiler) {
  74. const hints = this.hints;
  75. if (!hints) return;
  76. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  77. /** @type {UnusedReexportsWarning | undefined} */
  78. let warning;
  79. // Usage is final here, and concatenation has not merged the modules
  80. // away yet, so each one can still be named and measured.
  81. compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => {
  82. const { chunkGraph, moduleGraph, requestShortener } = compilation;
  83. /** @type {UnusedReexportDetails[]} */
  84. const unused = [];
  85. let wasted = 0;
  86. for (const module of modules) {
  87. // One webpack already left out costs nothing.
  88. if (chunkGraph.getNumberOfModuleChunks(module) === 0) continue;
  89. if (!hasOnlyUnusedExports(module, moduleGraph, chunkGraph)) {
  90. continue;
  91. }
  92. if (!isOnlyReexported(module, moduleGraph)) continue;
  93. const size = getModuleSize(module);
  94. wasted += size;
  95. unused.push({
  96. name: module.readableIdentifier(requestShortener),
  97. size
  98. });
  99. }
  100. if (unused.length === 0) return;
  101. // Ties break by name: module order is not stable across runtimes.
  102. unused.sort(
  103. (a, b) => b.size - a.size || compareStrings(a.name, b.name)
  104. );
  105. warning = new UnusedReexportsWarning(
  106. unused.slice(0, MAX_REPORTED_MODULES),
  107. unused.length,
  108. wasted
  109. );
  110. });
  111. // Reported past the hash: `createHash` folds every message into it, so
  112. // a hint pushed earlier would change the build's identity.
  113. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  114. if (warning === undefined) return;
  115. if (hints === "error") {
  116. compilation.errors.push(warning);
  117. } else if (hints === "stats") {
  118. compilation.hints.push(warning);
  119. } else {
  120. compilation.warnings.push(warning);
  121. }
  122. });
  123. });
  124. }
  125. }
  126. module.exports = UnusedReexportsPlugin;