source: imaps-frontend/node_modules/webpack/lib/rules/RuleSetCompiler.js@ 79a0317

main
Last change on this file since 79a0317 was 79a0317, checked in by stefan toskovski <stefantoska84@…>, 3 days ago

F4 Finalna Verzija

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