UNPKG

12.1 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5/*
6<rules>: <rule>
7<rules>: [<rule>]
8<rule>: {
9 resource: {
10 test: <condition>,
11 include: <condition>,
12 exclude: <condition>,
13 },
14 resource: <condition>, -> resource.test
15 test: <condition>, -> resource.test
16 include: <condition>, -> resource.include
17 exclude: <condition>, -> resource.exclude
18 resourceQuery: <condition>,
19 compiler: <condition>,
20 issuer: <condition>,
21 use: "loader", -> use[0].loader
22 loader: <>, -> use[0].loader
23 loaders: <>, -> use
24 options: {}, -> use[0].options,
25 query: {}, -> options
26 parser: {},
27 use: [
28 "loader" -> use[x].loader
29 ],
30 use: [
31 {
32 loader: "loader",
33 options: {}
34 }
35 ],
36 rules: [
37 <rule>
38 ],
39 oneOf: [
40 <rule>
41 ]
42}
43
44<condition>: /regExp/
45<condition>: function(arg) {}
46<condition>: "starting"
47<condition>: [<condition>] // or
48<condition>: { and: [<condition>] }
49<condition>: { or: [<condition>] }
50<condition>: { not: [<condition>] }
51<condition>: { test: <condition>, include: <condition>, exclude: <condition> }
52
53
54normalized:
55
56{
57 resource: function(),
58 resourceQuery: function(),
59 compiler: function(),
60 issuer: function(),
61 use: [
62 {
63 loader: string,
64 options: string,
65 <any>: <any>
66 }
67 ],
68 rules: [<rule>],
69 oneOf: [<rule>],
70 <any>: <any>,
71}
72
73*/
74
75"use strict";
76
77const notMatcher = matcher => {
78 return str => {
79 return !matcher(str);
80 };
81};
82
83const orMatcher = items => {
84 return str => {
85 for (let i = 0; i < items.length; i++) {
86 if (items[i](str)) return true;
87 }
88 return false;
89 };
90};
91
92const andMatcher = items => {
93 return str => {
94 for (let i = 0; i < items.length; i++) {
95 if (!items[i](str)) return false;
96 }
97 return true;
98 };
99};
100
101module.exports = class RuleSet {
102 constructor(rules) {
103 this.references = Object.create(null);
104 this.rules = RuleSet.normalizeRules(rules, this.references, "ref-");
105 }
106
107 static normalizeRules(rules, refs, ident) {
108 if (Array.isArray(rules)) {
109 return rules.map((rule, idx) => {
110 return RuleSet.normalizeRule(rule, refs, `${ident}-${idx}`);
111 });
112 } else if (rules) {
113 return [RuleSet.normalizeRule(rules, refs, ident)];
114 } else {
115 return [];
116 }
117 }
118
119 static normalizeRule(rule, refs, ident) {
120 if (typeof rule === "string") {
121 return {
122 use: [
123 {
124 loader: rule
125 }
126 ]
127 };
128 }
129 if (!rule) {
130 throw new Error("Unexcepted null when object was expected as rule");
131 }
132 if (typeof rule !== "object") {
133 throw new Error(
134 "Unexcepted " +
135 typeof rule +
136 " when object was expected as rule (" +
137 rule +
138 ")"
139 );
140 }
141
142 const newRule = {};
143 let useSource;
144 let resourceSource;
145 let condition;
146
147 const checkUseSource = newSource => {
148 if (useSource && useSource !== newSource) {
149 throw new Error(
150 RuleSet.buildErrorMessage(
151 rule,
152 new Error(
153 "Rule can only have one result source (provided " +
154 newSource +
155 " and " +
156 useSource +
157 ")"
158 )
159 )
160 );
161 }
162 useSource = newSource;
163 };
164
165 const checkResourceSource = newSource => {
166 if (resourceSource && resourceSource !== newSource) {
167 throw new Error(
168 RuleSet.buildErrorMessage(
169 rule,
170 new Error(
171 "Rule can only have one resource source (provided " +
172 newSource +
173 " and " +
174 resourceSource +
175 ")"
176 )
177 )
178 );
179 }
180 resourceSource = newSource;
181 };
182
183 if (rule.test || rule.include || rule.exclude) {
184 checkResourceSource("test + include + exclude");
185 condition = {
186 test: rule.test,
187 include: rule.include,
188 exclude: rule.exclude
189 };
190 try {
191 newRule.resource = RuleSet.normalizeCondition(condition);
192 } catch (error) {
193 throw new Error(RuleSet.buildErrorMessage(condition, error));
194 }
195 }
196
197 if (rule.resource) {
198 checkResourceSource("resource");
199 try {
200 newRule.resource = RuleSet.normalizeCondition(rule.resource);
201 } catch (error) {
202 throw new Error(RuleSet.buildErrorMessage(rule.resource, error));
203 }
204 }
205
206 if (rule.realResource) {
207 try {
208 newRule.realResource = RuleSet.normalizeCondition(rule.realResource);
209 } catch (error) {
210 throw new Error(RuleSet.buildErrorMessage(rule.realResource, error));
211 }
212 }
213
214 if (rule.resourceQuery) {
215 try {
216 newRule.resourceQuery = RuleSet.normalizeCondition(rule.resourceQuery);
217 } catch (error) {
218 throw new Error(RuleSet.buildErrorMessage(rule.resourceQuery, error));
219 }
220 }
221
222 if (rule.compiler) {
223 try {
224 newRule.compiler = RuleSet.normalizeCondition(rule.compiler);
225 } catch (error) {
226 throw new Error(RuleSet.buildErrorMessage(rule.compiler, error));
227 }
228 }
229
230 if (rule.issuer) {
231 try {
232 newRule.issuer = RuleSet.normalizeCondition(rule.issuer);
233 } catch (error) {
234 throw new Error(RuleSet.buildErrorMessage(rule.issuer, error));
235 }
236 }
237
238 if (rule.loader && rule.loaders) {
239 throw new Error(
240 RuleSet.buildErrorMessage(
241 rule,
242 new Error(
243 "Provided loader and loaders for rule (use only one of them)"
244 )
245 )
246 );
247 }
248
249 const loader = rule.loaders || rule.loader;
250 if (typeof loader === "string" && !rule.options && !rule.query) {
251 checkUseSource("loader");
252 newRule.use = RuleSet.normalizeUse(loader.split("!"), ident);
253 } else if (typeof loader === "string" && (rule.options || rule.query)) {
254 checkUseSource("loader + options/query");
255 newRule.use = RuleSet.normalizeUse(
256 {
257 loader: loader,
258 options: rule.options,
259 query: rule.query
260 },
261 ident
262 );
263 } else if (loader && (rule.options || rule.query)) {
264 throw new Error(
265 RuleSet.buildErrorMessage(
266 rule,
267 new Error(
268 "options/query cannot be used with loaders (use options for each array item)"
269 )
270 )
271 );
272 } else if (loader) {
273 checkUseSource("loaders");
274 newRule.use = RuleSet.normalizeUse(loader, ident);
275 } else if (rule.options || rule.query) {
276 throw new Error(
277 RuleSet.buildErrorMessage(
278 rule,
279 new Error(
280 "options/query provided without loader (use loader + options)"
281 )
282 )
283 );
284 }
285
286 if (rule.use) {
287 checkUseSource("use");
288 newRule.use = RuleSet.normalizeUse(rule.use, ident);
289 }
290
291 if (rule.rules) {
292 newRule.rules = RuleSet.normalizeRules(
293 rule.rules,
294 refs,
295 `${ident}-rules`
296 );
297 }
298
299 if (rule.oneOf) {
300 newRule.oneOf = RuleSet.normalizeRules(
301 rule.oneOf,
302 refs,
303 `${ident}-oneOf`
304 );
305 }
306
307 const keys = Object.keys(rule).filter(key => {
308 return ![
309 "resource",
310 "resourceQuery",
311 "compiler",
312 "test",
313 "include",
314 "exclude",
315 "issuer",
316 "loader",
317 "options",
318 "query",
319 "loaders",
320 "use",
321 "rules",
322 "oneOf"
323 ].includes(key);
324 });
325 for (const key of keys) {
326 newRule[key] = rule[key];
327 }
328
329 if (Array.isArray(newRule.use)) {
330 for (const item of newRule.use) {
331 if (item.ident) {
332 refs[item.ident] = item.options;
333 }
334 }
335 }
336
337 return newRule;
338 }
339
340 static buildErrorMessage(condition, error) {
341 const conditionAsText = JSON.stringify(
342 condition,
343 (key, value) => {
344 return value === undefined ? "undefined" : value;
345 },
346 2
347 );
348 return error.message + " in " + conditionAsText;
349 }
350
351 static normalizeUse(use, ident) {
352 if (typeof use === "function") {
353 return data => RuleSet.normalizeUse(use(data), ident);
354 }
355 if (Array.isArray(use)) {
356 return use
357 .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`))
358 .reduce((arr, items) => arr.concat(items), []);
359 }
360 return [RuleSet.normalizeUseItem(use, ident)];
361 }
362
363 static normalizeUseItemString(useItemString) {
364 const idx = useItemString.indexOf("?");
365 if (idx >= 0) {
366 return {
367 loader: useItemString.substr(0, idx),
368 options: useItemString.substr(idx + 1)
369 };
370 }
371 return {
372 loader: useItemString,
373 options: undefined
374 };
375 }
376
377 static normalizeUseItem(item, ident) {
378 if (typeof item === "string") {
379 return RuleSet.normalizeUseItemString(item);
380 }
381
382 const newItem = {};
383
384 if (item.options && item.query) {
385 throw new Error("Provided options and query in use");
386 }
387
388 if (!item.loader) {
389 throw new Error("No loader specified");
390 }
391
392 newItem.options = item.options || item.query;
393
394 if (typeof newItem.options === "object" && newItem.options) {
395 if (newItem.options.ident) {
396 newItem.ident = newItem.options.ident;
397 } else {
398 newItem.ident = ident;
399 }
400 }
401
402 const keys = Object.keys(item).filter(function(key) {
403 return !["options", "query"].includes(key);
404 });
405
406 for (const key of keys) {
407 newItem[key] = item[key];
408 }
409
410 return newItem;
411 }
412
413 static normalizeCondition(condition) {
414 if (!condition) throw new Error("Expected condition but got falsy value");
415 if (typeof condition === "string") {
416 return str => str.indexOf(condition) === 0;
417 }
418 if (typeof condition === "function") {
419 return condition;
420 }
421 if (condition instanceof RegExp) {
422 return condition.test.bind(condition);
423 }
424 if (Array.isArray(condition)) {
425 const items = condition.map(c => RuleSet.normalizeCondition(c));
426 return orMatcher(items);
427 }
428 if (typeof condition !== "object") {
429 throw Error(
430 "Unexcepted " +
431 typeof condition +
432 " when condition was expected (" +
433 condition +
434 ")"
435 );
436 }
437
438 const matchers = [];
439 Object.keys(condition).forEach(key => {
440 const value = condition[key];
441 switch (key) {
442 case "or":
443 case "include":
444 case "test":
445 if (value) matchers.push(RuleSet.normalizeCondition(value));
446 break;
447 case "and":
448 if (value) {
449 const items = value.map(c => RuleSet.normalizeCondition(c));
450 matchers.push(andMatcher(items));
451 }
452 break;
453 case "not":
454 case "exclude":
455 if (value) {
456 const matcher = RuleSet.normalizeCondition(value);
457 matchers.push(notMatcher(matcher));
458 }
459 break;
460 default:
461 throw new Error("Unexcepted property " + key + " in condition");
462 }
463 });
464 if (matchers.length === 0) {
465 throw new Error("Excepted condition but got " + condition);
466 }
467 if (matchers.length === 1) {
468 return matchers[0];
469 }
470 return andMatcher(matchers);
471 }
472
473 exec(data) {
474 const result = [];
475 this._run(
476 data,
477 {
478 rules: this.rules
479 },
480 result
481 );
482 return result;
483 }
484
485 _run(data, rule, result) {
486 // test conditions
487 if (rule.resource && !data.resource) return false;
488 if (rule.realResource && !data.realResource) return false;
489 if (rule.resourceQuery && !data.resourceQuery) return false;
490 if (rule.compiler && !data.compiler) return false;
491 if (rule.issuer && !data.issuer) return false;
492 if (rule.resource && !rule.resource(data.resource)) return false;
493 if (rule.realResource && !rule.realResource(data.realResource))
494 return false;
495 if (data.issuer && rule.issuer && !rule.issuer(data.issuer)) return false;
496 if (
497 data.resourceQuery &&
498 rule.resourceQuery &&
499 !rule.resourceQuery(data.resourceQuery)
500 ) {
501 return false;
502 }
503 if (data.compiler && rule.compiler && !rule.compiler(data.compiler)) {
504 return false;
505 }
506
507 // apply
508 const keys = Object.keys(rule).filter(key => {
509 return ![
510 "resource",
511 "realResource",
512 "resourceQuery",
513 "compiler",
514 "issuer",
515 "rules",
516 "oneOf",
517 "use",
518 "enforce"
519 ].includes(key);
520 });
521 for (const key of keys) {
522 result.push({
523 type: key,
524 value: rule[key]
525 });
526 }
527
528 if (rule.use) {
529 const process = use => {
530 if (typeof use === "function") {
531 process(use(data));
532 } else if (Array.isArray(use)) {
533 use.forEach(process);
534 } else {
535 result.push({
536 type: "use",
537 value: use,
538 enforce: rule.enforce
539 });
540 }
541 };
542 process(rule.use);
543 }
544
545 if (rule.rules) {
546 for (let i = 0; i < rule.rules.length; i++) {
547 this._run(data, rule.rules[i], result);
548 }
549 }
550
551 if (rule.oneOf) {
552 for (let i = 0; i < rule.oneOf.length; i++) {
553 if (this._run(data, rule.oneOf[i], result)) break;
554 }
555 }
556
557 return true;
558 }
559
560 findOptionsByIdent(ident) {
561 const options = this.references[ident];
562 if (!options) {
563 throw new Error("Can't find options with ident '" + ident + "'");
564 }
565 return options;
566 }
567};