source: frontend/node_modules/webpack/lib/rules/RuleSetCompiler.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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