MissingSideEffectsPlugin.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const NormalModule = require("../NormalModule");
  7. const MissingSideEffectsWarning = require("../errors/MissingSideEffectsWarning");
  8. const { compareStrings } = require("../util/comparators");
  9. const getModuleSize = require("./getModuleSize");
  10. const hasOnlyUnusedExports = require("./hasOnlyUnusedExports");
  11. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  12. /** @import Compiler from "../Compiler" */
  13. /** @import Module from "../Module" */
  14. /**
  15. * @import {
  16. * MissingSideEffectsDetails
  17. * } from "../errors/MissingSideEffectsWarning"
  18. */
  19. const PLUGIN_NAME = "MissingSideEffectsPlugin";
  20. // Enough to name the costliest without listing the whole dependency tree.
  21. const MAX_REPORTED_PACKAGES = 5;
  22. /**
  23. * The package a module belongs to, when its package.json leaves 'sideEffects'
  24. * unsaid. Nearly every package does, so only the costly ones are worth naming.
  25. * @param {Module} module the module to look up
  26. * @returns {string | undefined} the package name, or undefined
  27. */
  28. const getUndeclaredPackage = (module) => {
  29. if (!(module instanceof NormalModule)) return undefined;
  30. const resolveData = module.resourceResolveData;
  31. const description = resolveData && resolveData.descriptionFileData;
  32. if (!description || typeof description.name !== "string") return undefined;
  33. if ("sideEffects" in description) return undefined;
  34. return description.name;
  35. };
  36. class MissingSideEffectsPlugin {
  37. /**
  38. * Creates an instance of MissingSideEffectsPlugin.
  39. * @param {PerformanceOptions} options the plugin options
  40. */
  41. constructor(options) {
  42. /** @type {PerformanceOptions["hints"]} */
  43. this.hints = options.hints;
  44. }
  45. /**
  46. * Applies the plugin by registering its hooks on the compiler.
  47. * @param {Compiler} compiler the compiler instance
  48. * @returns {void}
  49. */
  50. apply(compiler) {
  51. const hints = this.hints;
  52. if (!hints) return;
  53. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  54. /** @type {MissingSideEffectsWarning | undefined} */
  55. let warning;
  56. // Usage is final here, and concatenation has not merged the modules
  57. // away yet, so each one can still be measured and attributed.
  58. compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => {
  59. const { chunkGraph, moduleGraph } = compilation;
  60. /** @type {Map<string, { modules: number, size: number }>} */
  61. const byPackage = new Map();
  62. let wasted = 0;
  63. for (const module of modules) {
  64. // One webpack already left out costs nothing.
  65. if (chunkGraph.getNumberOfModuleChunks(module) === 0) continue;
  66. if (!hasOnlyUnusedExports(module, moduleGraph, chunkGraph)) continue;
  67. const name = getUndeclaredPackage(module);
  68. if (name === undefined) continue;
  69. const size = getModuleSize(module);
  70. const entry = byPackage.get(name);
  71. wasted += size;
  72. if (entry === undefined) {
  73. byPackage.set(name, { modules: 1, size });
  74. } else {
  75. entry.modules++;
  76. entry.size += size;
  77. }
  78. }
  79. if (byPackage.size === 0) return;
  80. /** @type {MissingSideEffectsDetails[]} */
  81. const packages = [];
  82. for (const [name, { modules: count, size }] of byPackage) {
  83. packages.push({ name, modules: count, size });
  84. }
  85. // Ties break by name: module order is not stable across runtimes.
  86. packages.sort(
  87. (a, b) => b.size - a.size || compareStrings(a.name, b.name)
  88. );
  89. warning = new MissingSideEffectsWarning(
  90. packages.slice(0, MAX_REPORTED_PACKAGES),
  91. packages.length,
  92. wasted
  93. );
  94. });
  95. // Reported past the hash: `createHash` folds every message into it, so
  96. // a hint pushed earlier would change the build's identity.
  97. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  98. if (warning === undefined) return;
  99. if (hints === "error") {
  100. compilation.errors.push(warning);
  101. } else if (hints === "stats") {
  102. compilation.hints.push(warning);
  103. } else {
  104. compilation.warnings.push(warning);
  105. }
  106. });
  107. });
  108. }
  109. }
  110. module.exports = MissingSideEffectsPlugin;