RuleSetCompiler.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { SyncHook } = require("tapable");
  7. /** @typedef {import("enhanced-resolve").ResolveRequest} ResolveRequest */
  8. /** @typedef {import("../../declarations/WebpackOptions").Falsy} Falsy */
  9. /** @typedef {import("../../declarations/WebpackOptions").RuleSetUseItem} RuleSetUseItem */
  10. /** @typedef {import("../../declarations/WebpackOptions").RuleSetLoaderOptions} RuleSetLoaderOptions */
  11. /** @typedef {import("../../declarations/WebpackOptions").RuleSetRule} RuleSetRule */
  12. /** @typedef {(Falsy | RuleSetRule)[]} RuleSetRules */
  13. /**
  14. * @typedef {(value: EffectData[keyof EffectData]) => boolean} RuleConditionFunction
  15. */
  16. /**
  17. * @typedef {object} RuleCondition
  18. * @property {string | string[]} property
  19. * @property {boolean} matchWhenEmpty
  20. * @property {RuleConditionFunction} fn
  21. */
  22. /**
  23. * @typedef {object} Condition
  24. * @property {boolean} matchWhenEmpty
  25. * @property {RuleConditionFunction} fn
  26. */
  27. /**
  28. * @typedef {object} EffectData
  29. * @property {string=} resource
  30. * @property {string=} realResource
  31. * @property {string=} resourceQuery
  32. * @property {string=} resourceFragment
  33. * @property {string=} scheme
  34. * @property {ImportAttributes=} attributes
  35. * @property {string=} mimetype
  36. * @property {string} dependency
  37. * @property {ResolveRequest["descriptionFileData"]=} descriptionData
  38. * @property {string=} compiler
  39. * @property {string} issuer
  40. * @property {string} issuerLayer
  41. */
  42. /**
  43. * @typedef {object} CompiledRule
  44. * @property {RuleCondition[]} conditions
  45. * @property {(Effect | ((effectData: EffectData) => Effect[]))[]} effects
  46. * @property {CompiledRule[]=} rules
  47. * @property {CompiledRule[]=} oneOf
  48. */
  49. /** @typedef {"use" | "use-pre" | "use-post"} EffectUseType */
  50. /**
  51. * @typedef {object} EffectUse
  52. * @property {EffectUseType} type
  53. * @property {{ loader: string, options?: string | null | Record<string, EXPECTED_ANY>, ident?: string }} value
  54. */
  55. /**
  56. * @typedef {object} EffectBasic
  57. * @property {string} type
  58. * @property {EXPECTED_ANY} value
  59. */
  60. /** @typedef {EffectUse | EffectBasic} Effect */
  61. /** @typedef {Map<string, RuleSetLoaderOptions>} References */
  62. /**
  63. * @typedef {object} RuleSet
  64. * @property {References} references map of references in the rule set (may grow over time)
  65. * @property {(effectData: EffectData) => Effect[]} exec execute the rule set
  66. */
  67. /**
  68. * @template T
  69. * @template {T[keyof T]} V
  70. * @typedef {({ [key in keyof Required<T>]: Required<T>[key] extends V ? key : never })[keyof T]} KeysOfTypes
  71. */
  72. /** @typedef {Set<string>} UnhandledProperties */
  73. /** @typedef {(data: EffectData) => (RuleSetUseItem | (Falsy | RuleSetUseItem)[])} RuleSetUseFn */
  74. /** @typedef {(value: string) => boolean} RuleSetConditionFn */
  75. /** @typedef {{ apply: (ruleSetCompiler: RuleSetCompiler) => void }} RuleSetPlugin */
  76. class RuleSetCompiler {
  77. /**
  78. * @param {RuleSetPlugin[]} plugins plugins
  79. */
  80. constructor(plugins) {
  81. this.hooks = Object.freeze({
  82. /** @type {SyncHook<[string, RuleSetRule, UnhandledProperties, CompiledRule, References]>} */
  83. rule: new SyncHook([
  84. "path",
  85. "rule",
  86. "unhandledProperties",
  87. "compiledRule",
  88. "references"
  89. ])
  90. });
  91. if (plugins) {
  92. for (const plugin of plugins) {
  93. plugin.apply(this);
  94. }
  95. }
  96. }
  97. /**
  98. * @param {RuleSetRules} ruleSet raw user provided rules
  99. * @returns {RuleSet} compiled RuleSet
  100. */
  101. compile(ruleSet) {
  102. /** @type {References} */
  103. const refs = new Map();
  104. const rules = this.compileRules("ruleSet", ruleSet, refs);
  105. /**
  106. * @param {EffectData} data data passed in
  107. * @param {CompiledRule} rule the compiled rule
  108. * @param {Effect[]} effects an array where effects are pushed to
  109. * @returns {boolean} true, if the rule has matched
  110. */
  111. const execRule = (data, rule, effects) => {
  112. for (const condition of rule.conditions) {
  113. const p = condition.property;
  114. if (Array.isArray(p)) {
  115. /** @type {EXPECTED_ANY} */
  116. let current = data;
  117. for (const subProperty of p) {
  118. if (
  119. current &&
  120. typeof current === "object" &&
  121. Object.prototype.hasOwnProperty.call(current, subProperty)
  122. ) {
  123. current = current[/** @type {keyof EffectData} */ (subProperty)];
  124. } else {
  125. current = undefined;
  126. break;
  127. }
  128. }
  129. if (current !== undefined) {
  130. if (!condition.fn(current)) return false;
  131. continue;
  132. }
  133. } else if (p in data) {
  134. const value = data[/** @type {keyof EffectData} */ (p)];
  135. if (value !== undefined) {
  136. if (!condition.fn(value)) return false;
  137. continue;
  138. }
  139. }
  140. if (!condition.matchWhenEmpty) {
  141. return false;
  142. }
  143. }
  144. for (const effect of rule.effects) {
  145. if (typeof effect === "function") {
  146. const returnedEffects = effect(data);
  147. for (const effect of returnedEffects) {
  148. effects.push(effect);
  149. }
  150. } else {
  151. effects.push(effect);
  152. }
  153. }
  154. if (rule.rules) {
  155. for (const childRule of rule.rules) {
  156. execRule(data, childRule, effects);
  157. }
  158. }
  159. if (rule.oneOf) {
  160. for (const childRule of rule.oneOf) {
  161. if (execRule(data, childRule, effects)) {
  162. break;
  163. }
  164. }
  165. }
  166. return true;
  167. };
  168. return {
  169. references: refs,
  170. exec: (data) => {
  171. /** @type {Effect[]} */
  172. const effects = [];
  173. for (const rule of rules) {
  174. execRule(data, rule, effects);
  175. }
  176. return effects;
  177. }
  178. };
  179. }
  180. /**
  181. * @param {string} path current path
  182. * @param {RuleSetRules} rules the raw rules provided by user
  183. * @param {References} refs references
  184. * @returns {CompiledRule[]} rules
  185. */
  186. compileRules(path, rules, refs) {
  187. return rules
  188. .filter(Boolean)
  189. .map((rule, i) =>
  190. this.compileRule(
  191. `${path}[${i}]`,
  192. /** @type {RuleSetRule} */ (rule),
  193. refs
  194. )
  195. );
  196. }
  197. /**
  198. * @param {string} path current path
  199. * @param {RuleSetRule} rule the raw rule provided by user
  200. * @param {References} refs references
  201. * @returns {CompiledRule} normalized and compiled rule for processing
  202. */
  203. compileRule(path, rule, refs) {
  204. /** @type {UnhandledProperties} */
  205. const unhandledProperties = new Set(
  206. Object.keys(rule).filter(
  207. (key) => rule[/** @type {keyof RuleSetRule} */ (key)] !== undefined
  208. )
  209. );
  210. /** @type {CompiledRule} */
  211. const compiledRule = {
  212. conditions: [],
  213. effects: [],
  214. rules: undefined,
  215. oneOf: undefined
  216. };
  217. this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
  218. if (unhandledProperties.has("rules")) {
  219. unhandledProperties.delete("rules");
  220. const rules = rule.rules;
  221. if (!Array.isArray(rules)) {
  222. throw this.error(path, rules, "Rule.rules must be an array of rules");
  223. }
  224. compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
  225. }
  226. if (unhandledProperties.has("oneOf")) {
  227. unhandledProperties.delete("oneOf");
  228. const oneOf = rule.oneOf;
  229. if (!Array.isArray(oneOf)) {
  230. throw this.error(path, oneOf, "Rule.oneOf must be an array of rules");
  231. }
  232. compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
  233. }
  234. if (unhandledProperties.size > 0) {
  235. throw this.error(
  236. path,
  237. rule,
  238. `Properties ${[...unhandledProperties].join(", ")} are unknown`
  239. );
  240. }
  241. return compiledRule;
  242. }
  243. /**
  244. * @param {string} path current path
  245. * @param {RuleSetLoaderOptions} condition user provided condition value
  246. * @returns {Condition} compiled condition
  247. */
  248. compileCondition(path, condition) {
  249. if (condition === "") {
  250. return {
  251. matchWhenEmpty: true,
  252. fn: (str) => str === ""
  253. };
  254. }
  255. if (!condition) {
  256. throw this.error(
  257. path,
  258. condition,
  259. "Expected condition but got falsy value"
  260. );
  261. }
  262. if (typeof condition === "string") {
  263. return {
  264. matchWhenEmpty: condition.length === 0,
  265. fn: (str) => typeof str === "string" && str.startsWith(condition)
  266. };
  267. }
  268. if (typeof condition === "function") {
  269. try {
  270. return {
  271. matchWhenEmpty: condition(""),
  272. fn: /** @type {RuleConditionFunction} */ (condition)
  273. };
  274. } catch (_err) {
  275. throw this.error(
  276. path,
  277. condition,
  278. "Evaluation of condition function threw error"
  279. );
  280. }
  281. }
  282. if (condition instanceof RegExp) {
  283. return {
  284. matchWhenEmpty: condition.test(""),
  285. fn: (v) => typeof v === "string" && condition.test(v)
  286. };
  287. }
  288. if (Array.isArray(condition)) {
  289. const items = condition.map((c, i) =>
  290. this.compileCondition(`${path}[${i}]`, c)
  291. );
  292. return this.combineConditionsOr(items);
  293. }
  294. if (typeof condition !== "object") {
  295. throw this.error(
  296. path,
  297. condition,
  298. `Unexpected ${typeof condition} when condition was expected`
  299. );
  300. }
  301. /** @type {Condition[]} */
  302. const conditions = [];
  303. for (const key of Object.keys(condition)) {
  304. const value = condition[key];
  305. switch (key) {
  306. case "or":
  307. if (value) {
  308. if (!Array.isArray(value)) {
  309. throw this.error(
  310. `${path}.or`,
  311. condition.or,
  312. "Expected array of conditions"
  313. );
  314. }
  315. conditions.push(this.compileCondition(`${path}.or`, value));
  316. }
  317. break;
  318. case "and":
  319. if (value) {
  320. if (!Array.isArray(value)) {
  321. throw this.error(
  322. `${path}.and`,
  323. condition.and,
  324. "Expected array of conditions"
  325. );
  326. }
  327. let i = 0;
  328. for (const item of value) {
  329. conditions.push(this.compileCondition(`${path}.and[${i}]`, item));
  330. i++;
  331. }
  332. }
  333. break;
  334. case "not":
  335. if (value) {
  336. const matcher = this.compileCondition(`${path}.not`, value);
  337. const fn = matcher.fn;
  338. conditions.push({
  339. matchWhenEmpty: !matcher.matchWhenEmpty,
  340. fn: /** @type {RuleConditionFunction} */ ((v) => !fn(v))
  341. });
  342. }
  343. break;
  344. default:
  345. throw this.error(
  346. `${path}.${key}`,
  347. condition[key],
  348. `Unexpected property ${key} in condition`
  349. );
  350. }
  351. }
  352. if (conditions.length === 0) {
  353. throw this.error(
  354. path,
  355. condition,
  356. "Expected condition, but got empty thing"
  357. );
  358. }
  359. return this.combineConditionsAnd(conditions);
  360. }
  361. /**
  362. * @param {Condition[]} conditions some conditions
  363. * @returns {Condition} merged condition
  364. */
  365. combineConditionsOr(conditions) {
  366. if (conditions.length === 0) {
  367. return {
  368. matchWhenEmpty: false,
  369. fn: () => false
  370. };
  371. } else if (conditions.length === 1) {
  372. return conditions[0];
  373. }
  374. return {
  375. matchWhenEmpty: conditions.some((c) => c.matchWhenEmpty),
  376. fn: (v) => conditions.some((c) => c.fn(v))
  377. };
  378. }
  379. /**
  380. * @param {Condition[]} conditions some conditions
  381. * @returns {Condition} merged condition
  382. */
  383. combineConditionsAnd(conditions) {
  384. if (conditions.length === 0) {
  385. return {
  386. matchWhenEmpty: false,
  387. fn: () => false
  388. };
  389. } else if (conditions.length === 1) {
  390. return conditions[0];
  391. }
  392. return {
  393. matchWhenEmpty: conditions.every((c) => c.matchWhenEmpty),
  394. fn: (v) => conditions.every((c) => c.fn(v))
  395. };
  396. }
  397. /**
  398. * @param {string} path current path
  399. * @param {EXPECTED_ANY} value value at the error location
  400. * @param {string} message message explaining the problem
  401. * @returns {Error} an error object
  402. */
  403. error(path, value, message) {
  404. return new Error(
  405. `Compiling RuleSet failed: ${message} (at ${path}: ${value})`
  406. );
  407. }
  408. }
  409. module.exports = RuleSetCompiler;