source: frontend/node_modules/webpack/lib/util/cleverMerge.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: 20.4 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8/** @type {WeakMap<EXPECTED_OBJECT, WeakMap<EXPECTED_OBJECT, EXPECTED_OBJECT>>} */
9const mergeCache = new WeakMap();
10/** @typedef {Map<string, Map<string | number | boolean, EXPECTED_OBJECT>>} InnerPropertyCache */
11/** @type {WeakMap<EXPECTED_OBJECT, InnerPropertyCache>} */
12const setPropertyCache = new WeakMap();
13const DELETE = Symbol("DELETE");
14const DYNAMIC_INFO = Symbol("cleverMerge dynamic info");
15
16/**
17 * Merges two given objects and caches the result to avoid computation if same objects passed as arguments again.
18 * @template T
19 * @template O
20 * @example
21 * // performs cleverMerge(first, second), stores the result in WeakMap and returns result
22 * cachedCleverMerge({a: 1}, {a: 2})
23 * {a: 2}
24 * // when same arguments passed, gets the result from WeakMap and returns it.
25 * cachedCleverMerge({a: 1}, {a: 2})
26 * {a: 2}
27 * @param {T | null | undefined} first first object
28 * @param {O | null | undefined} second second object
29 * @returns {T & O | T | O} merged object of first and second object
30 */
31const cachedCleverMerge = (first, second) => {
32 if (second === undefined) return /** @type {T} */ (first);
33 if (first === undefined) return /** @type {O} */ (second);
34 if (typeof second !== "object" || second === null) {
35 return /** @type {O} */ (second);
36 }
37 if (typeof first !== "object" || first === null) {
38 return /** @type {T} */ (first);
39 }
40
41 let innerCache = mergeCache.get(first);
42 if (innerCache === undefined) {
43 innerCache = new WeakMap();
44 mergeCache.set(first, innerCache);
45 }
46 const prevMerge = /** @type {T & O} */ (innerCache.get(second));
47 if (prevMerge !== undefined) return prevMerge;
48 const newMerge = _cleverMerge(first, second, true);
49 innerCache.set(second, newMerge);
50 return newMerge;
51};
52
53/**
54 * Caches d set property.
55 * @template T
56 * @param {Partial<T>} obj object
57 * @param {string} property property
58 * @param {string | number | boolean} value assignment value
59 * @returns {T} new object
60 */
61const cachedSetProperty = (obj, property, value) => {
62 let mapByProperty = setPropertyCache.get(obj);
63
64 if (mapByProperty === undefined) {
65 mapByProperty = new Map();
66 setPropertyCache.set(obj, mapByProperty);
67 }
68
69 let mapByValue = mapByProperty.get(property);
70
71 if (mapByValue === undefined) {
72 mapByValue = new Map();
73 mapByProperty.set(property, mapByValue);
74 }
75
76 let result = mapByValue.get(value);
77
78 if (result) return /** @type {T} */ (result);
79
80 result = {
81 ...obj,
82 [property]: value
83 };
84 mapByValue.set(value, result);
85
86 return /** @type {T} */ (result);
87};
88
89/**
90 * Defines the by values type used by this module.
91 * @typedef {Map<string, EXPECTED_ANY>} ByValues
92 */
93
94/**
95 * Defines the object parsed property entry type used by this module.
96 * @template T
97 * @typedef {object} ObjectParsedPropertyEntry
98 * @property {T[keyof T] | undefined} base base value
99 * @property {`by${string}` | undefined} byProperty the name of the selector property
100 * @property {ByValues | undefined} byValues value depending on selector property, merged with base
101 */
102
103/** @typedef {(function(...EXPECTED_ANY): object) & { [DYNAMIC_INFO]: [DynamicFunction, object] }} DynamicFunction */
104
105/**
106 * Defines the parsed object static type used by this module.
107 * @template {object} T
108 * @typedef {Map<keyof T, ObjectParsedPropertyEntry<T>>} ParsedObjectStatic
109 */
110
111/**
112 * Defines the parsed object dynamic type used by this module.
113 * @template {object} T
114 * @typedef {{ byProperty: `by${string}`, fn: DynamicFunction }} ParsedObjectDynamic
115 */
116
117/**
118 * Defines the parsed object type used by this module.
119 * @template {object} T
120 * @typedef {object} ParsedObject
121 * @property {ParsedObjectStatic<T>} static static properties (key is property name)
122 * @property {ParsedObjectDynamic<T> | undefined} dynamic dynamic part
123 */
124
125/** @type {WeakMap<EXPECTED_OBJECT, ParsedObject<EXPECTED_ANY>>} */
126const parseCache = new WeakMap();
127
128/**
129 * Caches d parse object.
130 * @template {object} T
131 * @param {T} obj the object
132 * @returns {ParsedObject<T>} parsed object
133 */
134const cachedParseObject = (obj) => {
135 const entry = parseCache.get(obj);
136 if (entry !== undefined) return entry;
137 const result = parseObject(obj);
138 parseCache.set(obj, result);
139 return result;
140};
141
142/** @typedef {{ [p: string]: { [p: string]: EXPECTED_ANY } } | DynamicFunction} ByObject */
143
144/**
145 * Returns parsed object.
146 * @template {object} T
147 * @param {T} obj the object
148 * @returns {ParsedObject<T>} parsed object
149 */
150const parseObject = (obj) => {
151 /** @type {ParsedObjectStatic<T>} */
152 const info = new Map();
153 /** @type {ParsedObjectDynamic<T> | undefined} */
154 let dynamicInfo;
155 /**
156 * Returns object parsed property entry.
157 * @param {keyof T} p path
158 * @returns {Partial<ObjectParsedPropertyEntry<T>>} object parsed property entry
159 */
160 const getInfo = (p) => {
161 const entry = info.get(p);
162 if (entry !== undefined) return entry;
163 const newEntry = {
164 base: undefined,
165 byProperty: undefined,
166 byValues: undefined
167 };
168 info.set(p, newEntry);
169 return newEntry;
170 };
171 for (const key_ of Object.keys(obj)) {
172 const key = /** @type {keyof T} */ (key_);
173 if (typeof key === "string" && key.startsWith("by")) {
174 const byProperty = key;
175 const byObj = /** @type {ByObject} */ (obj[byProperty]);
176 if (typeof byObj === "object") {
177 for (const byValue of Object.keys(byObj)) {
178 const obj = byObj[/** @type {keyof (keyof T)} */ (byValue)];
179 for (const key of Object.keys(obj)) {
180 const entry = getInfo(/** @type {keyof T} */ (key));
181 if (entry.byProperty === undefined) {
182 entry.byProperty = /** @type {`by${string}`} */ (byProperty);
183 entry.byValues = new Map();
184 } else if (entry.byProperty !== byProperty) {
185 throw new Error(
186 `${/** @type {string} */ (byProperty)} and ${entry.byProperty} for a single property is not supported`
187 );
188 }
189 /** @type {ByValues} */
190 (entry.byValues).set(byValue, obj[key]);
191 if (byValue === "default") {
192 for (const otherByValue of Object.keys(byObj)) {
193 if (
194 !(
195 /** @type {ByValues} */
196 (entry.byValues).has(otherByValue)
197 )
198 ) {
199 /** @type {ByValues} */
200 (entry.byValues).set(otherByValue, undefined);
201 }
202 }
203 }
204 }
205 }
206 } else if (typeof byObj === "function") {
207 if (dynamicInfo === undefined) {
208 dynamicInfo = {
209 byProperty: /** @type {`by${string}`} */ (key),
210 fn: byObj
211 };
212 } else {
213 throw new Error(
214 `${key} and ${dynamicInfo.byProperty} when both are functions is not supported`
215 );
216 }
217 } else {
218 const entry = getInfo(key);
219 entry.base = obj[key];
220 }
221 } else {
222 const entry = getInfo(key);
223 entry.base = obj[key];
224 }
225 }
226 return {
227 static: info,
228 dynamic: dynamicInfo
229 };
230};
231
232/**
233 * Returns the object.
234 * @template {object} T
235 * @param {ParsedObjectStatic<T>} info static properties (key is property name)
236 * @param {{ byProperty: `by${string}`, fn: DynamicFunction } | undefined} dynamicInfo dynamic part
237 * @returns {T} the object
238 */
239const serializeObject = (info, dynamicInfo) => {
240 const obj = /** @type {EXPECTED_ANY} */ ({});
241 // Setup byProperty structure
242 for (const entry of info.values()) {
243 if (entry.byProperty !== undefined) {
244 const byProperty = entry.byProperty;
245 const byObj = (obj[byProperty] = obj[byProperty] || {});
246 for (const byValue of /** @type {ByValues} */ (entry.byValues).keys()) {
247 byObj[byValue] = byObj[byValue] || {};
248 }
249 }
250 }
251 for (const [key, entry] of info) {
252 if (entry.base !== undefined) {
253 obj[key] = entry.base;
254 }
255 // Fill byProperty structure
256 if (entry.byProperty !== undefined) {
257 const byProperty = entry.byProperty;
258 const byObj = (obj[byProperty] = obj[byProperty] || {});
259 for (const byValue of Object.keys(byObj)) {
260 const value = getFromByValues(
261 /** @type {ByValues} */
262 (entry.byValues),
263 byValue
264 );
265 if (value !== undefined) byObj[byValue][key] = value;
266 }
267 }
268 }
269 if (dynamicInfo !== undefined) {
270 obj[dynamicInfo.byProperty] = dynamicInfo.fn;
271 }
272 return obj;
273};
274
275const VALUE_TYPE_UNDEFINED = 0;
276const VALUE_TYPE_ATOM = 1;
277const VALUE_TYPE_ARRAY_EXTEND = 2;
278const VALUE_TYPE_OBJECT = 3;
279const VALUE_TYPE_DELETE = 4;
280
281/**
282 * Returns value type.
283 * @template T
284 * @param {T} value a single value
285 * @returns {VALUE_TYPE_UNDEFINED | VALUE_TYPE_ATOM | VALUE_TYPE_ARRAY_EXTEND | VALUE_TYPE_OBJECT | VALUE_TYPE_DELETE} value type
286 */
287const getValueType = (value) => {
288 if (value === undefined) {
289 return VALUE_TYPE_UNDEFINED;
290 } else if (value === DELETE) {
291 return VALUE_TYPE_DELETE;
292 } else if (Array.isArray(value)) {
293 if (value.includes("...")) return VALUE_TYPE_ARRAY_EXTEND;
294 return VALUE_TYPE_ATOM;
295 } else if (
296 typeof value === "object" &&
297 value !== null &&
298 (!value.constructor || value.constructor === Object)
299 ) {
300 return VALUE_TYPE_OBJECT;
301 }
302 return VALUE_TYPE_ATOM;
303};
304
305/**
306 * Merges two objects. Objects are deeply clever merged.
307 * Arrays might reference the old value with "...".
308 * Non-object values take preference over object values.
309 * @template T
310 * @template O
311 * @param {T} first first object
312 * @param {O} second second object
313 * @returns {T & O | T | O} merged object of first and second object
314 */
315const cleverMerge = (first, second) => {
316 if (second === undefined) return first;
317 if (first === undefined) return second;
318 if (typeof second !== "object" || second === null) return second;
319 if (typeof first !== "object" || first === null) return first;
320
321 return /** @type {T & O} */ (_cleverMerge(first, second, false));
322};
323
324/**
325 * Returns merged object of first and second object.
326 * @template {object} T
327 * @template {object} O
328 * Merges two objects. Objects are deeply clever merged.
329 * @param {T} first first
330 * @param {O} second second
331 * @param {boolean} internalCaching should parsing of objects and nested merges be cached
332 * @returns {T & O} merged object of first and second object
333 */
334const _cleverMerge = (first, second, internalCaching = false) => {
335 const firstObject = internalCaching
336 ? cachedParseObject(first)
337 : parseObject(first);
338 const { static: firstInfo, dynamic: firstDynamicInfo } = firstObject;
339
340 // If the first argument has a dynamic part we modify the dynamic part to merge the second argument
341 if (firstDynamicInfo !== undefined) {
342 let { byProperty, fn } = firstDynamicInfo;
343 const fnInfo = fn[DYNAMIC_INFO];
344 if (fnInfo) {
345 second =
346 /** @type {O} */
347 (
348 internalCaching
349 ? cachedCleverMerge(fnInfo[1], second)
350 : cleverMerge(fnInfo[1], second)
351 );
352 fn = fnInfo[0];
353 }
354 /** @type {DynamicFunction} */
355 const newFn = (...args) => {
356 const fnResult = fn(...args);
357 return internalCaching
358 ? cachedCleverMerge(fnResult, second)
359 : cleverMerge(fnResult, second);
360 };
361 newFn[DYNAMIC_INFO] = [fn, second];
362 return /** @type {T & O} */ (
363 serializeObject(firstObject.static, { byProperty, fn: newFn })
364 );
365 }
366
367 // If the first part is static only, we merge the static parts and keep the dynamic part of the second argument
368 const secondObject = internalCaching
369 ? cachedParseObject(second)
370 : parseObject(second);
371 const { static: secondInfo, dynamic: secondDynamicInfo } = secondObject;
372 const resultInfo = new Map();
373 for (const [key, firstEntry] of firstInfo) {
374 const secondEntry = secondInfo.get(
375 /** @type {keyof (T | O)} */
376 (key)
377 );
378 const entry =
379 secondEntry !== undefined
380 ? mergeEntries(firstEntry, secondEntry, internalCaching)
381 : firstEntry;
382 resultInfo.set(key, entry);
383 }
384 for (const [key, secondEntry] of secondInfo) {
385 if (!firstInfo.has(/** @type {keyof (T | O)} */ (key))) {
386 resultInfo.set(key, secondEntry);
387 }
388 }
389 return /** @type {T & O} */ (serializeObject(resultInfo, secondDynamicInfo));
390};
391
392/**
393 * Merges the provided values into a single result.
394 * @template T, O
395 * @param {ObjectParsedPropertyEntry<T>} firstEntry a
396 * @param {ObjectParsedPropertyEntry<O>} secondEntry b
397 * @param {boolean} internalCaching should parsing of objects and nested merges be cached
398 * @returns {ObjectParsedPropertyEntry<T> | ObjectParsedPropertyEntry<O> | ObjectParsedPropertyEntry<T & O>} new entry
399 */
400const mergeEntries = (firstEntry, secondEntry, internalCaching) => {
401 switch (getValueType(secondEntry.base)) {
402 case VALUE_TYPE_ATOM:
403 case VALUE_TYPE_DELETE:
404 // No need to consider firstEntry at all
405 // second value override everything
406 // = second.base + second.byProperty
407 return secondEntry;
408 case VALUE_TYPE_UNDEFINED:
409 if (!firstEntry.byProperty) {
410 // = first.base + second.byProperty
411 return {
412 base: firstEntry.base,
413 byProperty: secondEntry.byProperty,
414 byValues: secondEntry.byValues
415 };
416 } else if (firstEntry.byProperty !== secondEntry.byProperty) {
417 throw new Error(
418 `${firstEntry.byProperty} and ${secondEntry.byProperty} for a single property is not supported`
419 );
420 } else {
421 // = first.base + (first.byProperty + second.byProperty)
422 // need to merge first and second byValues
423 /** @type {Map<string, T & O>} */
424 const newByValues = new Map(firstEntry.byValues);
425 for (const [key, value] of /** @type {ByValues} */ (
426 secondEntry.byValues
427 )) {
428 const firstValue = getFromByValues(
429 /** @type {ByValues} */
430 (firstEntry.byValues),
431 key
432 );
433 newByValues.set(
434 key,
435 mergeSingleValue(firstValue, value, internalCaching)
436 );
437 }
438 return {
439 base: firstEntry.base,
440 byProperty: firstEntry.byProperty,
441 byValues: newByValues
442 };
443 }
444 default: {
445 if (!firstEntry.byProperty) {
446 // The simple case
447 // = (first.base + second.base) + second.byProperty
448 return {
449 base:
450 /** @type {T[keyof T] & O[keyof O]} */
451 (
452 mergeSingleValue(
453 firstEntry.base,
454 secondEntry.base,
455 internalCaching
456 )
457 ),
458 byProperty: secondEntry.byProperty,
459 byValues: secondEntry.byValues
460 };
461 }
462 /** @type {O[keyof O] | T[keyof T] | (T[keyof T] & O[keyof O]) | (T[keyof T] | undefined)[] | (O[keyof O] | undefined)[] | (O[keyof O] | T[keyof T] | undefined)[] | undefined} */
463 let newBase;
464 /** @type {Map<string, (T & O) | O[keyof O] | (O[keyof O] | undefined)[] | ((T & O) | undefined)[] | (T & O & O[keyof O]) | ((T & O) | O[keyof O] | undefined)[] | undefined>} */
465 const intermediateByValues = new Map(firstEntry.byValues);
466 for (const [key, value] of intermediateByValues) {
467 intermediateByValues.set(
468 key,
469 mergeSingleValue(value, secondEntry.base, internalCaching)
470 );
471 }
472 if (
473 [.../** @type {ByValues} */ (firstEntry.byValues).values()].every(
474 (value) => {
475 const type = getValueType(value);
476 return type === VALUE_TYPE_ATOM || type === VALUE_TYPE_DELETE;
477 }
478 )
479 ) {
480 // = (first.base + second.base) + ((first.byProperty + second.base) + second.byProperty)
481 newBase = mergeSingleValue(
482 firstEntry.base,
483 secondEntry.base,
484 internalCaching
485 );
486 } else {
487 // = first.base + ((first.byProperty (+default) + second.base) + second.byProperty)
488 newBase = firstEntry.base;
489 if (!intermediateByValues.has("default")) {
490 intermediateByValues.set("default", secondEntry.base);
491 }
492 }
493 if (!secondEntry.byProperty) {
494 // = first.base + (first.byProperty + second.base)
495 return {
496 base: /** @type {T[keyof T] & O[keyof O]} */ (newBase),
497 byProperty: firstEntry.byProperty,
498 byValues: intermediateByValues
499 };
500 } else if (firstEntry.byProperty !== secondEntry.byProperty) {
501 throw new Error(
502 `${firstEntry.byProperty} and ${secondEntry.byProperty} for a single property is not supported`
503 );
504 }
505 /** @type {Map<string, (T & O) | O[keyof O] | (O[keyof O] | undefined)[] | (T & O & O[keyof O]) | ((T & O) | undefined)[] | ((T & O) | O[keyof O] | undefined)[] | undefined>} */
506 const newByValues = new Map(intermediateByValues);
507 for (const [key, value] of /** @type {ByValues} */ (
508 secondEntry.byValues
509 )) {
510 const firstValue = getFromByValues(intermediateByValues, key);
511 newByValues.set(
512 key,
513 mergeSingleValue(firstValue, value, internalCaching)
514 );
515 }
516 return {
517 base: /** @type {T[keyof T] & O[keyof O]} */ (newBase),
518 byProperty: firstEntry.byProperty,
519 byValues: newByValues
520 };
521 }
522 }
523};
524
525/**
526 * Gets from by values.
527 * @template V
528 * @param {ByValues} byValues all values
529 * @param {string} key value of the selector
530 * @returns {V | undefined} value
531 */
532const getFromByValues = (byValues, key) => {
533 if (key !== "default" && byValues.has(key)) {
534 return byValues.get(key);
535 }
536 return byValues.get("default");
537};
538
539/**
540 * Merges single value.
541 * @template A
542 * @template B
543 * @param {A | A[]} a value
544 * @param {B | B[]} b value
545 * @param {boolean} internalCaching should parsing of objects and nested merges be cached
546 * @returns {A & B | (A | B)[] | A | A[] | B | B[]} value
547 */
548const mergeSingleValue = (a, b, internalCaching) => {
549 const bType = getValueType(b);
550 const aType = getValueType(a);
551 switch (bType) {
552 case VALUE_TYPE_DELETE:
553 case VALUE_TYPE_ATOM:
554 return b;
555 case VALUE_TYPE_OBJECT: {
556 return aType !== VALUE_TYPE_OBJECT
557 ? b
558 : internalCaching
559 ? cachedCleverMerge(a, b)
560 : cleverMerge(a, b);
561 }
562 case VALUE_TYPE_UNDEFINED:
563 return a;
564 case VALUE_TYPE_ARRAY_EXTEND:
565 switch (
566 aType !== VALUE_TYPE_ATOM
567 ? aType
568 : Array.isArray(a)
569 ? VALUE_TYPE_ARRAY_EXTEND
570 : VALUE_TYPE_OBJECT
571 ) {
572 case VALUE_TYPE_UNDEFINED:
573 return b;
574 case VALUE_TYPE_DELETE:
575 return /** @type {B[]} */ (b).filter((item) => item !== "...");
576 case VALUE_TYPE_ARRAY_EXTEND: {
577 /** @type {(A | B)[]} */
578 const newArray = [];
579 for (const item of /** @type {B[]} */ (b)) {
580 if (item === "...") {
581 for (const item of /** @type {A[]} */ (a)) {
582 newArray.push(item);
583 }
584 } else {
585 newArray.push(item);
586 }
587 }
588 return newArray;
589 }
590 case VALUE_TYPE_OBJECT:
591 return /** @type {(A | B)[]} */ (b).map((item) =>
592 item === "..." ? /** @type {A} */ (a) : item
593 );
594 default:
595 throw new Error("Not implemented");
596 }
597 default:
598 throw new Error("Not implemented");
599 }
600};
601
602/**
603 * Removes operations.
604 * @template {object} T
605 * @param {T} obj the object
606 * @param {(keyof T)[]=} keysToKeepOriginalValue keys to keep original value
607 * @returns {T} the object without operations like "..." or DELETE
608 */
609const removeOperations = (obj, keysToKeepOriginalValue = []) => {
610 const newObj = /** @type {T} */ ({});
611 for (const _key of Object.keys(obj)) {
612 const key = /** @type {keyof T} */ (_key);
613 const value = obj[key];
614 const type = getValueType(value);
615 if (type === VALUE_TYPE_OBJECT && keysToKeepOriginalValue.includes(key)) {
616 newObj[key] = value;
617 continue;
618 }
619 switch (type) {
620 case VALUE_TYPE_UNDEFINED:
621 case VALUE_TYPE_DELETE:
622 break;
623 case VALUE_TYPE_OBJECT:
624 newObj[key] =
625 /** @type {T[keyof T]} */
626 (
627 removeOperations(
628 /** @type {T} */
629 (value),
630 keysToKeepOriginalValue
631 )
632 );
633 break;
634 case VALUE_TYPE_ARRAY_EXTEND:
635 newObj[key] =
636 /** @type {T[keyof T]} */
637 (
638 /** @type {EXPECTED_ANY[]} */
639 (value).filter((i) => i !== "...")
640 );
641 break;
642 default:
643 newObj[key] = value;
644 break;
645 }
646 }
647 return newObj;
648};
649
650/**
651 * Resolves by property.
652 * @template T
653 * @template {keyof T} P
654 * @template V
655 * @param {T} obj the object
656 * @param {P} byProperty the by description
657 * @param {...V} values values
658 * @returns {Omit<T, P>} object with merged byProperty
659 */
660const resolveByProperty = (obj, byProperty, ...values) => {
661 if (typeof obj !== "object" || obj === null || !(byProperty in obj)) {
662 return obj;
663 }
664 const { [byProperty]: _byValue, ..._remaining } = obj;
665 const remaining = /** @type {T} */ (_remaining);
666 const byValue =
667 /** @type {Record<string, T> | ((...args: V[]) => T)} */
668 (_byValue);
669 if (typeof byValue === "object") {
670 const key = /** @type {string} */ (values[0]);
671 if (key in byValue) {
672 return cachedCleverMerge(remaining, byValue[key]);
673 } else if ("default" in byValue) {
674 return cachedCleverMerge(remaining, byValue.default);
675 }
676 return remaining;
677 } else if (typeof byValue === "function") {
678 // eslint-disable-next-line prefer-spread
679 const result = byValue.apply(null, values);
680 return cachedCleverMerge(
681 remaining,
682 resolveByProperty(result, byProperty, ...values)
683 );
684 }
685 return obj;
686};
687
688module.exports.DELETE = DELETE;
689module.exports.cachedCleverMerge = cachedCleverMerge;
690module.exports.cachedSetProperty = cachedSetProperty;
691module.exports.cleverMerge = cleverMerge;
692module.exports.removeOperations = removeOperations;
693module.exports.resolveByProperty = resolveByProperty;
Note: See TracBrowser for help on using the repository browser.