PageRenderTime 25ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/node_modules/webpack/lib/rules/RuleSetCompiler.js

https://gitlab.com/nguyenthehiep3232/marius
JavaScript | 379 lines | 277 code | 26 blank | 76 comment | 55 complexity | f9b52375d9f90a343a8c8832ebcfc030 MD5 | raw file
  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. /**
  8. * @typedef {Object} RuleCondition
  9. * @property {string | string[]} property
  10. * @property {boolean} matchWhenEmpty
  11. * @property {function(string): boolean} fn
  12. */
  13. /**
  14. * @typedef {Object} Condition
  15. * @property {boolean} matchWhenEmpty
  16. * @property {function(string): boolean} fn
  17. */
  18. /**
  19. * @typedef {Object} CompiledRule
  20. * @property {RuleCondition[]} conditions
  21. * @property {(Effect|function(object): Effect[])[]} effects
  22. * @property {CompiledRule[]=} rules
  23. * @property {CompiledRule[]=} oneOf
  24. */
  25. /**
  26. * @typedef {Object} Effect
  27. * @property {string} type
  28. * @property {any} value
  29. */
  30. /**
  31. * @typedef {Object} RuleSet
  32. * @property {Map<string, any>} references map of references in the rule set (may grow over time)
  33. * @property {function(object): Effect[]} exec execute the rule set
  34. */
  35. class RuleSetCompiler {
  36. constructor(plugins) {
  37. this.hooks = Object.freeze({
  38. /** @type {SyncHook<[string, object, Set<string>, CompiledRule, Map<string, any>]>} */
  39. rule: new SyncHook([
  40. "path",
  41. "rule",
  42. "unhandledProperties",
  43. "compiledRule",
  44. "references"
  45. ])
  46. });
  47. if (plugins) {
  48. for (const plugin of plugins) {
  49. plugin.apply(this);
  50. }
  51. }
  52. }
  53. /**
  54. * @param {object[]} ruleSet raw user provided rules
  55. * @returns {RuleSet} compiled RuleSet
  56. */
  57. compile(ruleSet) {
  58. const refs = new Map();
  59. const rules = this.compileRules("ruleSet", ruleSet, refs);
  60. /**
  61. * @param {object} data data passed in
  62. * @param {CompiledRule} rule the compiled rule
  63. * @param {Effect[]} effects an array where effects are pushed to
  64. * @returns {boolean} true, if the rule has matched
  65. */
  66. const execRule = (data, rule, effects) => {
  67. for (const condition of rule.conditions) {
  68. const p = condition.property;
  69. if (Array.isArray(p)) {
  70. let current = data;
  71. for (const subProperty of p) {
  72. if (
  73. current &&
  74. typeof current === "object" &&
  75. Object.prototype.hasOwnProperty.call(current, subProperty)
  76. ) {
  77. current = current[subProperty];
  78. } else {
  79. current = undefined;
  80. break;
  81. }
  82. }
  83. if (current !== undefined) {
  84. if (!condition.fn(current)) return false;
  85. continue;
  86. }
  87. } else if (p in data) {
  88. const value = data[p];
  89. if (value !== undefined) {
  90. if (!condition.fn(value)) return false;
  91. continue;
  92. }
  93. }
  94. if (!condition.matchWhenEmpty) {
  95. return false;
  96. }
  97. }
  98. for (const effect of rule.effects) {
  99. if (typeof effect === "function") {
  100. const returnedEffects = effect(data);
  101. for (const effect of returnedEffects) {
  102. effects.push(effect);
  103. }
  104. } else {
  105. effects.push(effect);
  106. }
  107. }
  108. if (rule.rules) {
  109. for (const childRule of rule.rules) {
  110. execRule(data, childRule, effects);
  111. }
  112. }
  113. if (rule.oneOf) {
  114. for (const childRule of rule.oneOf) {
  115. if (execRule(data, childRule, effects)) {
  116. break;
  117. }
  118. }
  119. }
  120. return true;
  121. };
  122. return {
  123. references: refs,
  124. exec: data => {
  125. /** @type {Effect[]} */
  126. const effects = [];
  127. for (const rule of rules) {
  128. execRule(data, rule, effects);
  129. }
  130. return effects;
  131. }
  132. };
  133. }
  134. /**
  135. * @param {string} path current path
  136. * @param {object[]} rules the raw rules provided by user
  137. * @param {Map<string, any>} refs references
  138. * @returns {CompiledRule[]} rules
  139. */
  140. compileRules(path, rules, refs) {
  141. return rules.map((rule, i) =>
  142. this.compileRule(`${path}[${i}]`, rule, refs)
  143. );
  144. }
  145. /**
  146. * @param {string} path current path
  147. * @param {object} rule the raw rule provided by user
  148. * @param {Map<string, any>} refs references
  149. * @returns {CompiledRule} normalized and compiled rule for processing
  150. */
  151. compileRule(path, rule, refs) {
  152. const unhandledProperties = new Set(
  153. Object.keys(rule).filter(key => rule[key] !== undefined)
  154. );
  155. /** @type {CompiledRule} */
  156. const compiledRule = {
  157. conditions: [],
  158. effects: [],
  159. rules: undefined,
  160. oneOf: undefined
  161. };
  162. this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
  163. if (unhandledProperties.has("rules")) {
  164. unhandledProperties.delete("rules");
  165. const rules = rule.rules;
  166. if (!Array.isArray(rules))
  167. throw this.error(path, rules, "Rule.rules must be an array of rules");
  168. compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
  169. }
  170. if (unhandledProperties.has("oneOf")) {
  171. unhandledProperties.delete("oneOf");
  172. const oneOf = rule.oneOf;
  173. if (!Array.isArray(oneOf))
  174. throw this.error(path, oneOf, "Rule.oneOf must be an array of rules");
  175. compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
  176. }
  177. if (unhandledProperties.size > 0) {
  178. throw this.error(
  179. path,
  180. rule,
  181. `Properties ${Array.from(unhandledProperties).join(", ")} are unknown`
  182. );
  183. }
  184. return compiledRule;
  185. }
  186. /**
  187. * @param {string} path current path
  188. * @param {any} condition user provided condition value
  189. * @returns {Condition} compiled condition
  190. */
  191. compileCondition(path, condition) {
  192. if (condition === "") {
  193. return {
  194. matchWhenEmpty: true,
  195. fn: str => str === ""
  196. };
  197. }
  198. if (!condition) {
  199. throw this.error(
  200. path,
  201. condition,
  202. "Expected condition but got falsy value"
  203. );
  204. }
  205. if (typeof condition === "string") {
  206. return {
  207. matchWhenEmpty: condition.length === 0,
  208. fn: str => typeof str === "string" && str.startsWith(condition)
  209. };
  210. }
  211. if (typeof condition === "function") {
  212. try {
  213. return {
  214. matchWhenEmpty: condition(""),
  215. fn: condition
  216. };
  217. } catch (err) {
  218. throw this.error(
  219. path,
  220. condition,
  221. "Evaluation of condition function threw error"
  222. );
  223. }
  224. }
  225. if (condition instanceof RegExp) {
  226. return {
  227. matchWhenEmpty: condition.test(""),
  228. fn: v => typeof v === "string" && condition.test(v)
  229. };
  230. }
  231. if (Array.isArray(condition)) {
  232. const items = condition.map((c, i) =>
  233. this.compileCondition(`${path}[${i}]`, c)
  234. );
  235. return this.combineConditionsOr(items);
  236. }
  237. if (typeof condition !== "object") {
  238. throw this.error(
  239. path,
  240. condition,
  241. `Unexpected ${typeof condition} when condition was expected`
  242. );
  243. }
  244. const conditions = [];
  245. for (const key of Object.keys(condition)) {
  246. const value = condition[key];
  247. switch (key) {
  248. case "or":
  249. if (value) {
  250. if (!Array.isArray(value)) {
  251. throw this.error(
  252. `${path}.or`,
  253. condition.and,
  254. "Expected array of conditions"
  255. );
  256. }
  257. conditions.push(this.compileCondition(`${path}.or`, value));
  258. }
  259. break;
  260. case "and":
  261. if (value) {
  262. if (!Array.isArray(value)) {
  263. throw this.error(
  264. `${path}.and`,
  265. condition.and,
  266. "Expected array of conditions"
  267. );
  268. }
  269. let i = 0;
  270. for (const item of value) {
  271. conditions.push(this.compileCondition(`${path}.and[${i}]`, item));
  272. i++;
  273. }
  274. }
  275. break;
  276. case "not":
  277. if (value) {
  278. const matcher = this.compileCondition(`${path}.not`, value);
  279. const fn = matcher.fn;
  280. conditions.push({
  281. matchWhenEmpty: !matcher.matchWhenEmpty,
  282. fn: v => !fn(v)
  283. });
  284. }
  285. break;
  286. default:
  287. throw this.error(
  288. `${path}.${key}`,
  289. condition[key],
  290. `Unexpected property ${key} in condition`
  291. );
  292. }
  293. }
  294. if (conditions.length === 0) {
  295. throw this.error(
  296. path,
  297. condition,
  298. "Expected condition, but got empty thing"
  299. );
  300. }
  301. return this.combineConditionsAnd(conditions);
  302. }
  303. /**
  304. * @param {Condition[]} conditions some conditions
  305. * @returns {Condition} merged condition
  306. */
  307. combineConditionsOr(conditions) {
  308. if (conditions.length === 0) {
  309. return {
  310. matchWhenEmpty: false,
  311. fn: () => false
  312. };
  313. } else if (conditions.length === 1) {
  314. return conditions[0];
  315. } else {
  316. return {
  317. matchWhenEmpty: conditions.some(c => c.matchWhenEmpty),
  318. fn: v => conditions.some(c => c.fn(v))
  319. };
  320. }
  321. }
  322. /**
  323. * @param {Condition[]} conditions some conditions
  324. * @returns {Condition} merged condition
  325. */
  326. combineConditionsAnd(conditions) {
  327. if (conditions.length === 0) {
  328. return {
  329. matchWhenEmpty: false,
  330. fn: () => false
  331. };
  332. } else if (conditions.length === 1) {
  333. return conditions[0];
  334. } else {
  335. return {
  336. matchWhenEmpty: conditions.every(c => c.matchWhenEmpty),
  337. fn: v => conditions.every(c => c.fn(v))
  338. };
  339. }
  340. }
  341. /**
  342. * @param {string} path current path
  343. * @param {any} value value at the error location
  344. * @param {string} message message explaining the problem
  345. * @returns {Error} an error object
  346. */
  347. error(path, value, message) {
  348. return new Error(
  349. `Compiling RuleSet failed: ${message} (at ${path}: ${value})`
  350. );
  351. }
  352. }
  353. module.exports = RuleSetCompiler;