OsDependentRuleWarningPlugin.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const OsDependentRuleWarning = require("../errors/OsDependentRuleWarning");
  7. /**
  8. * @import {
  9. * Falsy,
  10. * PerformanceOptions,
  11. * RuleSetCondition,
  12. * RuleSetRule
  13. * } from "../../declarations/WebpackOptions"
  14. */
  15. /** @import Compiler from "../Compiler" */
  16. const PLUGIN_NAME = "OsDependentRuleWarningPlugin";
  17. // Only these carry a path. `resourceQuery`, `resourceFragment`, `mimetype` and
  18. // `scheme` legitimately contain a `/`.
  19. const PATH_PROPERTIES = [
  20. "test",
  21. "include",
  22. "exclude",
  23. "resource",
  24. "realResource",
  25. "issuer"
  26. ];
  27. /** @typedef {"posix" | "windows" | undefined} Separator */
  28. /**
  29. * Which separator a pattern can match, walking its source so an escape is never
  30. * mistaken for the separator after it (`\.` is not `\\`); both means portable.
  31. * @param {RegExp} regExp the condition
  32. * @returns {Separator} the only separator it matches, or `undefined` when it is portable
  33. */
  34. const onlyMatchedSeparator = (regExp) => {
  35. const { source } = regExp;
  36. let posix = false;
  37. let windows = false;
  38. for (let i = 0; i < source.length; i++) {
  39. const character = source[i];
  40. if (character === "\\") {
  41. const escaped = source[i + 1];
  42. if (escaped === "\\") {
  43. windows = true;
  44. } else if (escaped === "/") {
  45. posix = true;
  46. }
  47. i++;
  48. } else if (character === "/") {
  49. posix = true;
  50. }
  51. }
  52. if (posix === windows) return undefined;
  53. return posix ? "posix" : "windows";
  54. };
  55. /**
  56. * @param {RuleSetCondition} condition the condition to walk
  57. * @param {(regExp: RegExp) => void} callback called for every regexp within
  58. * @returns {void}
  59. */
  60. const forEachRegExp = (condition, callback) => {
  61. if (condition instanceof RegExp) {
  62. callback(condition);
  63. } else if (Array.isArray(condition)) {
  64. for (const item of condition) forEachRegExp(item, callback);
  65. } else if (condition && typeof condition === "object") {
  66. // `not` is skipped: negated, a separator says which paths are *excluded*,
  67. // so the condition still matches the other OS.
  68. for (const key of ["and", "or"]) {
  69. const nested = condition[/** @type {keyof typeof condition} */ (key)];
  70. if (nested !== undefined) {
  71. forEachRegExp(/** @type {RuleSetCondition} */ (nested), callback);
  72. }
  73. }
  74. }
  75. };
  76. class OsDependentRuleWarningPlugin {
  77. /**
  78. * Creates an instance of OsDependentRuleWarningPlugin.
  79. * @param {PerformanceOptions} options the plugin options
  80. */
  81. constructor(options) {
  82. /** @type {PerformanceOptions["hints"]} */
  83. this.hints = options.hints;
  84. }
  85. /**
  86. * Applies the plugin by registering its hooks on the compiler.
  87. * @param {Compiler} compiler the compiler instance
  88. * @returns {void}
  89. */
  90. apply(compiler) {
  91. const hints = this.hints;
  92. const rules =
  93. compiler.options.module && compiler.options.module.rules
  94. ? compiler.options.module.rules
  95. : [];
  96. /** @type {string[]} */
  97. const descriptions = [];
  98. /**
  99. * @param {(RuleSetRule | Falsy)[]} ruleSet rules to walk
  100. * @param {string} path where `ruleSet` sits in the configuration
  101. * @returns {void}
  102. */
  103. const walk = (ruleSet, path) => {
  104. for (let i = 0; i < ruleSet.length; i++) {
  105. const rule = ruleSet[i];
  106. if (!rule || typeof rule !== "object") continue;
  107. const rulePath = `${path}[${i}]`;
  108. for (const property of PATH_PROPERTIES) {
  109. const condition = rule[/** @type {keyof RuleSetRule} */ (property)];
  110. if (condition === undefined) continue;
  111. forEachRegExp(
  112. /** @type {RuleSetCondition} */ (condition),
  113. (regExp) => {
  114. const separator = onlyMatchedSeparator(regExp);
  115. if (separator === undefined) return;
  116. descriptions.push(
  117. `${rulePath}.${property} (${regExp}) only matches ${
  118. separator === "posix" ? "'/'" : "'\\'"
  119. } paths`
  120. );
  121. }
  122. );
  123. }
  124. if (Array.isArray(rule.oneOf)) walk(rule.oneOf, `${rulePath}.oneOf`);
  125. if (Array.isArray(rule.rules)) walk(rule.rules, `${rulePath}.rules`);
  126. }
  127. };
  128. walk(/** @type {RuleSetRule[]} */ (rules), "module.rules");
  129. if (descriptions.length === 0) return;
  130. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  131. // Reported past the hash: `createHash` folds every message into it, so
  132. // a hint pushed earlier would change the build's identity.
  133. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  134. const warning = new OsDependentRuleWarning(descriptions);
  135. if (hints === "error") {
  136. compilation.errors.push(warning);
  137. } else if (hints === "stats") {
  138. compilation.hints.push(warning);
  139. } else {
  140. compilation.warnings.push(warning);
  141. }
  142. });
  143. });
  144. }
  145. }
  146. module.exports = OsDependentRuleWarningPlugin;