source: trip-planner-front/node_modules/webpack/lib/util/cleverMerge.js@ 8d391a1

Last change on this file since 8d391a1 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

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