UnusedAliasesPlugin.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const UnusedAliasesWarning = require("../errors/UnusedAliasesWarning");
  7. const { join } = require("../util/fs");
  8. const {
  9. ABSOLUTE_PATH_REGEXP,
  10. WINDOWS_PATH_SEPARATOR_REGEXP
  11. } = require("../util/identifier");
  12. const matchAlias = require("../util/matchAlias");
  13. const getSourceModules = require("./getSourceModules");
  14. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  15. /** @import Compiler from "../Compiler" */
  16. /** @import Dependency from "../Dependency" */
  17. /** @import DependenciesBlock from "../DependenciesBlock" */
  18. /** @import { ContextDependencyOptions } from "../dependencies/ContextDependency" */
  19. /** @import Module from "../Module" */
  20. /**
  21. * One `resolve.alias` entry, prepared for repeated matching.
  22. * @typedef {object} PendingAlias
  23. * @property {string} name the alias name as written, for the report
  24. * @property {string} matchName the name to compare with, separators normalized when absolute
  25. * @property {boolean} onlyModule whether the alias only matches the whole request
  26. * @property {boolean} absolute whether the name is an absolute path
  27. * @property {string | undefined} wildcardPrefix the part of `matchName` before its single "*", when it has one
  28. * @property {string} wildcardSuffix the part of `matchName` after that "*"
  29. * @property {boolean} used whether some request reached this alias
  30. */
  31. const PLUGIN_NAME = "UnusedAliasesPlugin";
  32. const SLASH = "/";
  33. /**
  34. * Whether an alias applies to a request. `AliasPlugin` accepts a single "*" in
  35. * a name, matching by prefix and suffix instead of by segment.
  36. * @param {string} request the request, separators normalized when the alias is absolute
  37. * @param {PendingAlias} alias the alias to test
  38. * @returns {boolean} true when the alias applies
  39. */
  40. const aliasApplies = (request, alias) =>
  41. alias.wildcardPrefix !== undefined
  42. ? request.startsWith(alias.wildcardPrefix) &&
  43. request.endsWith(alias.wildcardSuffix)
  44. : matchAlias(request, alias.matchName, alias.onlyModule);
  45. class UnusedAliasesPlugin {
  46. /**
  47. * Creates an instance of UnusedAliasesPlugin.
  48. * @param {PerformanceOptions} options the plugin options
  49. */
  50. constructor(options) {
  51. /** @type {PerformanceOptions["hints"]} */
  52. this.hints = options.hints;
  53. }
  54. /**
  55. * Applies the plugin by registering its hooks on the compiler.
  56. * @param {Compiler} compiler the compiler instance
  57. * @returns {void}
  58. */
  59. apply(compiler) {
  60. const hints = this.hints;
  61. compiler.hooks.compilation.tap(
  62. PLUGIN_NAME,
  63. (compilation, { normalModuleFactory }) => {
  64. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  65. // Nothing was factorized, so no alias could match — an empty build
  66. // is not evidence that an alias is unused.
  67. if (compilation.modules.size === 0) return;
  68. const configured =
  69. normalModuleFactory.getResolver("normal").options.alias;
  70. if (configured.length === 0) return;
  71. /** @type {PendingAlias[]} */
  72. const entries = [];
  73. let hasAbsoluteName = false;
  74. let unused = configured.length;
  75. for (const { name, onlyModule } of configured) {
  76. const absolute = ABSOLUTE_PATH_REGEXP.test(name);
  77. // An absolute name is compared with paths, whose separator differs
  78. // per platform
  79. const matchName = absolute
  80. ? name.replace(WINDOWS_PATH_SEPARATOR_REGEXP, SLASH)
  81. : name;
  82. const starIndex = matchName.indexOf("*");
  83. // `onlyModule` turns the wildcard off, so only the whole name matches
  84. const wildcardPrefix =
  85. !onlyModule &&
  86. starIndex !== -1 &&
  87. !matchName.includes("*", starIndex + 1)
  88. ? matchName.slice(0, starIndex)
  89. : undefined;
  90. if (absolute) hasAbsoluteName = true;
  91. entries.push({
  92. name,
  93. matchName,
  94. onlyModule: Boolean(onlyModule),
  95. absolute,
  96. wildcardPrefix,
  97. wildcardSuffix:
  98. wildcardPrefix === undefined
  99. ? ""
  100. : matchName.slice(starIndex + 1),
  101. used: false
  102. });
  103. }
  104. /**
  105. * Marks the one alias a request reaches: `AliasPlugin` stops at the
  106. * first match, and a resolved path is only seen by an absolute name.
  107. * @param {string} request the request to test
  108. * @param {boolean=} absoluteOnly whether to test absolute names only
  109. * @returns {void}
  110. */
  111. const consume = (request, absoluteOnly) => {
  112. /** @type {string | undefined} */
  113. let normalized;
  114. for (let i = 0; i < entries.length; i++) {
  115. const alias = entries[i];
  116. let candidate = request;
  117. if (alias.absolute) {
  118. if (normalized === undefined) {
  119. normalized = request.replace(
  120. WINDOWS_PATH_SEPARATOR_REGEXP,
  121. SLASH
  122. );
  123. }
  124. candidate = normalized;
  125. } else if (absoluteOnly) {
  126. continue;
  127. }
  128. if (aliasApplies(candidate, alias)) {
  129. if (!alias.used) {
  130. alias.used = true;
  131. unused--;
  132. }
  133. return;
  134. }
  135. }
  136. };
  137. for (const module of compilation.modules) {
  138. if (unused === 0) break;
  139. for (const sourceModule of getSourceModules(module)) {
  140. const rawRequest =
  141. /** @type {Module & { rawRequest?: string }} */
  142. (sourceModule).rawRequest;
  143. if (typeof rawRequest === "string") consume(rawRequest);
  144. }
  145. }
  146. // Only what is left unaccounted for is worth the walk: a request the
  147. // graph keeps nowhere else is on the dependency that asked for it.
  148. if (unused > 0) {
  149. const fs = compiler.inputFileSystem || undefined;
  150. // One import states its request on several dependencies, and a
  151. // request every module makes states it once per module.
  152. /** @type {Set<string>} */
  153. const seen = new Set();
  154. for (const module of compilation.modules) {
  155. if (unused === 0) break;
  156. const context = module.context;
  157. /** @type {DependenciesBlock[]} */
  158. const blocks = [module];
  159. while (blocks.length > 0) {
  160. const block =
  161. /** @type {DependenciesBlock} */
  162. (blocks.pop());
  163. for (const dependency of block.dependencies) {
  164. const typed =
  165. /** @type {Dependency & { request?: string, options?: ContextDependencyOptions }} */
  166. (dependency);
  167. // A `ContextModule` keeps the request in its options, and one
  168. // an alias sent to `false` reaches no `rawRequest` at all.
  169. const request =
  170. typeof typed.request === "string"
  171. ? typed.request
  172. : typed.options &&
  173. typeof typed.options.request === "string"
  174. ? typed.options.request
  175. : undefined;
  176. if (request === undefined) continue;
  177. if (!seen.has(request)) {
  178. seen.add(request);
  179. consume(request);
  180. }
  181. // An absolute name matches the path the resolver saw, which for
  182. // a relative request is the issuer's context joined with it, so
  183. // the same request answers differently per module.
  184. if (
  185. hasAbsoluteName &&
  186. context !== null &&
  187. request.startsWith(".")
  188. ) {
  189. consume(join(fs, context, request), true);
  190. }
  191. }
  192. for (const child of block.blocks) blocks.push(child);
  193. }
  194. }
  195. }
  196. if (unused === 0) return;
  197. const warning = new UnusedAliasesWarning(
  198. entries
  199. .filter((alias) => !alias.used)
  200. .map((alias) => `'${alias.name}'`)
  201. .sort()
  202. );
  203. if (hints === "error") {
  204. compilation.errors.push(warning);
  205. } else if (hints === "stats") {
  206. compilation.hints.push(warning);
  207. } else {
  208. compilation.warnings.push(warning);
  209. }
  210. });
  211. }
  212. );
  213. }
  214. }
  215. module.exports = UnusedAliasesPlugin;