RuleSetCompiler.js 12 KB

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