UseEffectRulePlugin.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const util = require("util");
  7. /** @typedef {import("../../declarations/WebpackOptions").Falsy} Falsy */
  8. /** @typedef {import("../../declarations/WebpackOptions").RuleSetLoader} RuleSetLoader */
  9. /** @typedef {import("../../declarations/WebpackOptions").RuleSetLoaderOptions} RuleSetLoaderOptions */
  10. /** @typedef {import("../../declarations/WebpackOptions").RuleSetRule} RuleSetRule */
  11. /** @typedef {import("../../declarations/WebpackOptions").RuleSetUse} RuleSetUse */
  12. /** @typedef {import("../../declarations/WebpackOptions").RuleSetUseItem} RuleSetUseItem */
  13. /** @typedef {import("./RuleSetCompiler")} RuleSetCompiler */
  14. /** @typedef {import("./RuleSetCompiler").Effect} Effect */
  15. /** @typedef {import("./RuleSetCompiler").EffectData} EffectData */
  16. /** @typedef {import("./RuleSetCompiler").EffectUseType} EffectUseType */
  17. const PLUGIN_NAME = "UseEffectRulePlugin";
  18. class UseEffectRulePlugin {
  19. /**
  20. * @param {RuleSetCompiler} ruleSetCompiler the rule set compiler
  21. * @returns {void}
  22. */
  23. apply(ruleSetCompiler) {
  24. ruleSetCompiler.hooks.rule.tap(
  25. PLUGIN_NAME,
  26. (path, rule, unhandledProperties, result, references) => {
  27. /**
  28. * @param {keyof RuleSetRule} property property
  29. * @param {string} correctProperty correct property
  30. */
  31. const conflictWith = (property, correctProperty) => {
  32. if (unhandledProperties.has(property)) {
  33. throw ruleSetCompiler.error(
  34. `${path}.${property}`,
  35. rule[property],
  36. `A Rule must not have a '${property}' property when it has a '${correctProperty}' property`
  37. );
  38. }
  39. };
  40. if (unhandledProperties.has("use")) {
  41. unhandledProperties.delete("use");
  42. unhandledProperties.delete("enforce");
  43. conflictWith("loader", "use");
  44. conflictWith("options", "use");
  45. const use = /** @type {RuleSetUse} */ (rule.use);
  46. const enforce = rule.enforce;
  47. const type =
  48. /** @type {EffectUseType} */
  49. (enforce ? `use-${enforce}` : "use");
  50. /**
  51. * @param {string} path options path
  52. * @param {string} defaultIdent default ident when none is provided
  53. * @param {RuleSetUseItem} item user provided use value
  54. * @returns {(Effect | ((effectData: EffectData) => Effect[]))} effect
  55. */
  56. const useToEffect = (path, defaultIdent, item) => {
  57. if (typeof item === "function") {
  58. return data =>
  59. useToEffectsWithoutIdent(
  60. path,
  61. /** @type {RuleSetUseItem | RuleSetUseItem[]} */
  62. (item(data))
  63. );
  64. }
  65. return useToEffectRaw(path, defaultIdent, item);
  66. };
  67. /**
  68. * @param {string} path options path
  69. * @param {string} defaultIdent default ident when none is provided
  70. * @param {Exclude<NonNullable<RuleSetUseItem>, EXPECTED_FUNCTION>} item user provided use value
  71. * @returns {Effect} effect
  72. */
  73. const useToEffectRaw = (path, defaultIdent, item) => {
  74. if (typeof item === "string") {
  75. return {
  76. type,
  77. value: {
  78. loader: item,
  79. options: undefined,
  80. ident: undefined
  81. }
  82. };
  83. }
  84. const loader = /** @type {string} */ (item.loader);
  85. const options = item.options;
  86. let ident = item.ident;
  87. if (options && typeof options === "object") {
  88. if (!ident) ident = defaultIdent;
  89. references.set(ident, options);
  90. }
  91. if (typeof options === "string") {
  92. util.deprecate(
  93. () => {},
  94. `Using a string as loader options is deprecated (${path}.options)`,
  95. "DEP_WEBPACK_RULE_LOADER_OPTIONS_STRING"
  96. )();
  97. }
  98. return {
  99. type: enforce ? `use-${enforce}` : "use",
  100. value: {
  101. loader,
  102. options,
  103. ident
  104. }
  105. };
  106. };
  107. /**
  108. * @param {string} path options path
  109. * @param {RuleSetUseItem | (Falsy | RuleSetUseItem)[]} items user provided use value
  110. * @returns {Effect[]} effects
  111. */
  112. const useToEffectsWithoutIdent = (path, items) => {
  113. if (Array.isArray(items)) {
  114. return items.filter(Boolean).map((item, idx) =>
  115. useToEffectRaw(
  116. `${path}[${idx}]`,
  117. "[[missing ident]]",
  118. /** @type {Exclude<RuleSetUseItem, EXPECTED_FUNCTION>} */
  119. (item)
  120. )
  121. );
  122. }
  123. return [
  124. useToEffectRaw(
  125. path,
  126. "[[missing ident]]",
  127. /** @type {Exclude<RuleSetUseItem, EXPECTED_FUNCTION>} */
  128. (items)
  129. )
  130. ];
  131. };
  132. /**
  133. * @param {string} path current path
  134. * @param {RuleSetUse} items user provided use value
  135. * @returns {(Effect | ((effectData: EffectData) => Effect[]))[]} effects
  136. */
  137. const useToEffects = (path, items) => {
  138. if (Array.isArray(items)) {
  139. return items.filter(Boolean).map((item, idx) => {
  140. const subPath = `${path}[${idx}]`;
  141. return useToEffect(
  142. subPath,
  143. subPath,
  144. /** @type {RuleSetUseItem} */
  145. (item)
  146. );
  147. });
  148. }
  149. return [
  150. useToEffect(path, path, /** @type {RuleSetUseItem} */ (items))
  151. ];
  152. };
  153. if (typeof use === "function") {
  154. result.effects.push(data =>
  155. useToEffectsWithoutIdent(`${path}.use`, use(data))
  156. );
  157. } else {
  158. for (const effect of useToEffects(`${path}.use`, use)) {
  159. result.effects.push(effect);
  160. }
  161. }
  162. }
  163. if (unhandledProperties.has("loader")) {
  164. unhandledProperties.delete("loader");
  165. unhandledProperties.delete("options");
  166. unhandledProperties.delete("enforce");
  167. const loader = /** @type {RuleSetLoader} */ (rule.loader);
  168. const options = rule.options;
  169. const enforce = rule.enforce;
  170. if (loader.includes("!")) {
  171. throw ruleSetCompiler.error(
  172. `${path}.loader`,
  173. loader,
  174. "Exclamation mark separated loader lists has been removed in favor of the 'use' property with arrays"
  175. );
  176. }
  177. if (loader.includes("?")) {
  178. throw ruleSetCompiler.error(
  179. `${path}.loader`,
  180. loader,
  181. "Query arguments on 'loader' has been removed in favor of the 'options' property"
  182. );
  183. }
  184. if (typeof options === "string") {
  185. util.deprecate(
  186. () => {},
  187. `Using a string as loader options is deprecated (${path}.options)`,
  188. "DEP_WEBPACK_RULE_LOADER_OPTIONS_STRING"
  189. )();
  190. }
  191. const ident =
  192. options && typeof options === "object" ? path : undefined;
  193. if (ident) {
  194. references.set(
  195. ident,
  196. /** @type {RuleSetLoaderOptions} */
  197. (options)
  198. );
  199. }
  200. result.effects.push({
  201. type: enforce ? `use-${enforce}` : "use",
  202. value: {
  203. loader,
  204. options,
  205. ident
  206. }
  207. });
  208. }
  209. }
  210. );
  211. }
  212. }
  213. module.exports = UseEffectRulePlugin;