source: imaps-frontend/node_modules/webpack/lib/util/cleverMerge.js

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

F4 Finalna Verzija

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