RedundantDynamicImportsPlugin.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
  7. const Entrypoint = require("../Entrypoint");
  8. const RedundantDynamicImportWarning = require("../errors/RedundantDynamicImportWarning");
  9. const { compareStrings } = require("../util/comparators");
  10. const formatLocation = require("../util/formatLocation");
  11. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  12. /** @import Chunk from "../Chunk" */
  13. /** @import ChunkGroup from "../ChunkGroup" */
  14. /** @import Compiler from "../Compiler" */
  15. /** @import DependenciesBlock from "../DependenciesBlock" */
  16. /** @import Module from "../Module" */
  17. const PLUGIN_NAME = "RedundantDynamicImportsPlugin";
  18. // Enough to name the offenders without printing every call site.
  19. const MAX_REPORTED_IMPORTS = 5;
  20. class RedundantDynamicImportsPlugin {
  21. /**
  22. * Creates an instance of RedundantDynamicImportsPlugin.
  23. * @param {PerformanceOptions} options the plugin options
  24. */
  25. constructor(options) {
  26. /** @type {PerformanceOptions["hints"]} */
  27. this.hints = options.hints;
  28. }
  29. /**
  30. * Applies the plugin by registering its hooks on the compiler.
  31. * @param {Compiler} compiler the compiler instance
  32. * @returns {void}
  33. */
  34. apply(compiler) {
  35. const hints = this.hints;
  36. if (!hints) return;
  37. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  38. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  39. const { chunkGraph, moduleGraph, requestShortener } = compilation;
  40. /** @type {string[]} */
  41. const descriptions = [];
  42. /**
  43. * The chunks certain to be loaded when `chunk` runs: itself, plus what
  44. * every entrypoint able to reach it brings. Entrypoints are alternative
  45. * load paths, so they are intersected — one that carries the target says
  46. * nothing about a page that loads another. A shared runtime name is not
  47. * enough either: with `runtimeChunk: "single"` two entrypoints share one
  48. * runtime while neither loads the other's initial chunks.
  49. * @param {Chunk} chunk the chunk the importer runs in
  50. * @returns {Set<Chunk>} the chunks loaded before it runs
  51. */
  52. const computeLoadedChunks = (chunk) => {
  53. const chunks = new Set([chunk]);
  54. /** @type {Set<ChunkGroup>} */
  55. const entrypoints = new Set();
  56. /** @type {Set<ChunkGroup>} */
  57. const queue = new Set(chunk.groupsIterable);
  58. for (const group of queue) {
  59. // An entrypoint is a load path in itself and carries nothing of
  60. // what reaches it: an async one — a worker — starts from its own
  61. // chunks, however much the module spawning it had loaded.
  62. if (group.isInitial() || group instanceof Entrypoint) {
  63. entrypoints.add(group);
  64. continue;
  65. }
  66. // Only initial chunks are certain: an async ancestor is loaded on
  67. // the path taken, and a group can be reached by several.
  68. for (const parent of group.getParents()) queue.add(parent);
  69. }
  70. if (entrypoints.size === 0) return chunks;
  71. /** @type {Set<Chunk> | undefined} */
  72. let common;
  73. for (const entrypoint of entrypoints) {
  74. /** @type {Set<Chunk>} */
  75. const loaded = new Set();
  76. /** @type {Set<ChunkGroup>} */
  77. const chain = new Set([entrypoint]);
  78. // An entrypoint reached through `dependOn` is loaded first too, so
  79. // within one path they add up rather than cancel each other out.
  80. for (const group of chain) {
  81. for (const member of group.chunks) loaded.add(member);
  82. for (const parent of group.getParents()) chain.add(parent);
  83. }
  84. if (common === undefined) {
  85. common = loaded;
  86. continue;
  87. }
  88. for (const member of common) {
  89. if (!loaded.has(member)) common.delete(member);
  90. }
  91. }
  92. for (const member of /** @type {Set<Chunk>} */ (common)) {
  93. chunks.add(member);
  94. }
  95. return chunks;
  96. };
  97. // The answer depends on the chunk alone, and every module sharing one
  98. // asks the same question. Dropped with the hook it is built in.
  99. /** @type {Map<Chunk, Set<Chunk>>} */
  100. const loadedChunks = new Map();
  101. /**
  102. * @param {Chunk} chunk the chunk the importer runs in
  103. * @returns {Set<Chunk>} the chunks loaded before it runs
  104. */
  105. const getLoadedChunks = (chunk) => {
  106. let loaded = loadedChunks.get(chunk);
  107. if (loaded === undefined) {
  108. loaded = computeLoadedChunks(chunk);
  109. loadedChunks.set(chunk, loaded);
  110. }
  111. return loaded;
  112. };
  113. /**
  114. * @param {DependenciesBlock} block the block to walk
  115. * @param {Module} importer the module the block belongs to
  116. * @param {Set<Chunk>[]} contexts what is loaded in each place it runs
  117. * @returns {void}
  118. */
  119. const walk = (block, importer, contexts) => {
  120. for (const nested of block.blocks) {
  121. if (nested instanceof AsyncDependenciesBlock) {
  122. for (const dependency of nested.dependencies) {
  123. const target = moduleGraph.getModule(dependency);
  124. if (!target) continue;
  125. // Redundant only where it is redundant everywhere: a shared
  126. // importer still defers for an entry that lacks the target.
  127. let redundant = true;
  128. for (const loaded of contexts) {
  129. let present = false;
  130. for (const chunk of chunkGraph.getModuleChunksIterable(
  131. target
  132. )) {
  133. if (loaded.has(chunk)) {
  134. present = true;
  135. break;
  136. }
  137. }
  138. if (!present) {
  139. redundant = false;
  140. break;
  141. }
  142. }
  143. if (!redundant) continue;
  144. descriptions.push(
  145. `${importer.readableIdentifier(requestShortener)}${
  146. nested.loc ? ` ${formatLocation(nested.loc)}` : ""
  147. } imports ${target.readableIdentifier(requestShortener)}`
  148. );
  149. }
  150. }
  151. walk(nested, importer, contexts);
  152. }
  153. };
  154. for (const module of compilation.modules) {
  155. if (module.blocks.length === 0) continue;
  156. /** @type {Set<Chunk>[]} */
  157. const contexts = [];
  158. for (const chunk of chunkGraph.getModuleChunksIterable(module)) {
  159. contexts.push(getLoadedChunks(chunk));
  160. }
  161. if (contexts.length === 0) continue;
  162. walk(module, module, contexts);
  163. }
  164. if (descriptions.length === 0) return;
  165. // Module order is not stable across runtimes, so the reported subset
  166. // would otherwise differ between them.
  167. descriptions.sort(compareStrings);
  168. const warning = new RedundantDynamicImportWarning(
  169. descriptions.slice(0, MAX_REPORTED_IMPORTS)
  170. );
  171. if (hints === "error") {
  172. compilation.errors.push(warning);
  173. } else if (hints === "stats") {
  174. compilation.hints.push(warning);
  175. } else {
  176. compilation.warnings.push(warning);
  177. }
  178. });
  179. });
  180. }
  181. }
  182. module.exports = RedundantDynamicImportsPlugin;