@superhero/deep 4.8.0 → 4.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ---
2
+ #### v4.8.2
3
+ ---
4
+
5
+ Better support for different data structures, mainly Map and Set
6
+
7
+ ---
8
+ #### v4.8.1
9
+ ---\n\nVersion alignment...
10
+
1
11
  ---
2
12
  ## Version: 4.8.0
3
13
  ---\n\nVersion alignment...
package/assign.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export default function assign(a, ...b)
2
2
  {
3
3
  b.forEach((b) => assignB2A(a, b))
4
+ return a
4
5
  }
5
6
 
6
7
  function assignB2A(a, b)
@@ -15,18 +16,30 @@ function assignB2A(a, b)
15
16
  return a
16
17
  }
17
18
 
18
- if(Object.prototype.toString.call(a) === '[object Array]'
19
- && Object.prototype.toString.call(b) === '[object Array]')
19
+ const
20
+ aType = Object.prototype.toString.call(a),
21
+ bType = Object.prototype.toString.call(b)
22
+
23
+ if(aType === '[object Array]' && bType === '[object Array]')
20
24
  {
21
25
  return assignArray(a, b)
22
26
  }
23
27
 
24
- if(Object.prototype.toString.call(a) === '[object Object]'
25
- && Object.prototype.toString.call(b) === '[object Object]')
28
+ if(aType === '[object Object]' && bType === '[object Object]')
26
29
  {
27
30
  return assignObject(a, b)
28
31
  }
29
32
 
33
+ if(aType === '[object Set]' && bType === '[object Set]')
34
+ {
35
+ return assignSet(a, b)
36
+ }
37
+
38
+ if(aType === '[object Map]' && bType === '[object Map]')
39
+ {
40
+ return assignMap(a, b)
41
+ }
42
+
30
43
  return b
31
44
  }
32
45
 
@@ -38,36 +51,82 @@ function assignArray(a, b)
38
51
  return a
39
52
  }
40
53
 
54
+ function assignSet(a, b)
55
+ {
56
+ for(const v of b)
57
+ {
58
+ a.add(v)
59
+ }
60
+ return a
61
+ }
62
+
63
+ function assignMap(a, b)
64
+ {
65
+ for(const [k, v] of b)
66
+ {
67
+ if(a.has(k))
68
+ {
69
+ a.set(k, assignB2A(a.get(k), v))
70
+ }
71
+ else
72
+ {
73
+ a.set(k, v)
74
+ }
75
+ }
76
+ return a
77
+ }
78
+
41
79
  /**
42
- * @param {Object} a
43
- * @param {Object} b
44
- *
45
- * @throws {TypeError} If a property in "a" is also in "b",
46
- * but the property in "a" is not configurable.
47
- *
48
- * @returns {Object} a
80
+ * Mutates "a" by assigning properties from "b".
81
+ *
82
+ * Rules:
83
+ * - If both sides are plain objects, recurse.
84
+ * - If property exists in a:
85
+ * - if configurable: redefine with b's descriptor (value is assigned/merged)
86
+ * - else if writable and data property: write value
87
+ * - else: leave as-is
88
+ * - Accessors are copied as accessors; never forced into data descriptors.
49
89
  */
50
90
  function assignObject(a, b)
51
91
  {
52
- for (const key of Object.getOwnPropertyNames(b))
92
+ for(const key of Reflect.ownKeys(b))
53
93
  {
54
- if(Object.prototype.toString.call(a[key]) === '[object Object]'
55
- && Object.prototype.toString.call(b[key]) === '[object Object]')
94
+ const
95
+ hasA = Object.prototype.hasOwnProperty.call(a, key),
96
+ da = hasA ? Object.getOwnPropertyDescriptor(a, key) : undefined,
97
+ db = Object.getOwnPropertyDescriptor(b, key)
98
+
99
+ if(!db)
56
100
  {
57
- assignObject(a[key], b[key])
101
+ continue
58
102
  }
59
- else if(Object.hasOwnProperty.call(a, key))
103
+
104
+ const aIsData = !!da && ('value' in da)
105
+ const bIsData = 'value' in db
106
+
107
+ // recurse only when both are data props holding plain objects
108
+ if(hasA
109
+ && aIsData
110
+ && bIsData
111
+ && Object.prototype.toString.call(da.value) === '[object Object]'
112
+ && Object.prototype.toString.call(db.value) === '[object Object]')
60
113
  {
61
- const descriptor_a = Object.getOwnPropertyDescriptor(a, key)
114
+ assignObject(da.value, db.value)
115
+ continue
116
+ }
62
117
 
63
- if(descriptor_a.configurable)
118
+ if(hasA)
119
+ {
120
+ // if not configurable, we can only write if it's a writable data prop
121
+ if(da && da.configurable)
64
122
  {
65
- assignPropertyDescriptor(a, b, key)
123
+ assignPropertyDescriptor(a, da, db, key)
66
124
  }
67
- else if(descriptor_a.writable)
125
+ else if(da && ('value' in da) && da.writable)
68
126
  {
69
- descriptor_a.value = b[key]
70
- Object.defineProperty(a, key, descriptor_a)
127
+ // only safe for data properties
128
+ const next = assignB2A(da.value, bIsData ? db.value : undefined)
129
+ Object.defineProperty(a, key, { ...da, value: next })
71
130
  }
72
131
  else
73
132
  {
@@ -76,16 +135,26 @@ function assignObject(a, b)
76
135
  }
77
136
  else
78
137
  {
79
- assignPropertyDescriptor(a, b, key)
138
+ // define new property with b's descriptor, but merge the value if needed
139
+ assignPropertyDescriptor(a, undefined, db, key)
80
140
  }
81
141
  }
82
142
 
83
143
  return a
84
144
  }
85
145
 
86
- function assignPropertyDescriptor(a, b, key)
146
+ function assignPropertyDescriptor(a, da, db, key)
87
147
  {
88
- const descriptor_b = Object.getOwnPropertyDescriptor(b, key)
89
- descriptor_b.value = assignB2A(a[key], b[key])
90
- Object.defineProperty(a, key, descriptor_b)
91
- }
148
+ // Accessor: copy as-is (no merging of get/set)
149
+ if(!('value' in db))
150
+ {
151
+ Object.defineProperty(a, key, db)
152
+ return
153
+ }
154
+
155
+ const aValue = da && ('value' in da) ? da.value : undefined
156
+ const next = assignB2A(aValue, db.value)
157
+
158
+ const descriptor = { ...db, value: next }
159
+ Object.defineProperty(a, key, descriptor)
160
+ }
package/assign.test.js CHANGED
@@ -185,4 +185,165 @@ suite('@superhero/deep/assign', () =>
185
185
  deepassign(a, b)
186
186
  assert.deepStrictEqual(a, expected, 'Objects without conflicts should merge correctly')
187
187
  })
188
+
189
+ suite('Map/Set + accessors', () =>
190
+ {
191
+ test('Assigns sets as a union', () =>
192
+ {
193
+ const
194
+ a = new Set([1, 2, 3]),
195
+ b = new Set([3, 4, 5])
196
+
197
+ deepassign(a, b)
198
+
199
+ assert.deepStrictEqual(a, new Set([1, 2, 3, 4, 5]))
200
+ })
201
+
202
+ test('Assigns maps by key and merges values on conflict', () =>
203
+ {
204
+ const
205
+ a = new Map([['x', { foo: 1 }], ['y', 1]]),
206
+ b = new Map([['x', { bar: 2 }], ['z', 3]])
207
+
208
+ deepassign(a, b)
209
+
210
+ assert.ok(a instanceof Map)
211
+ assert.deepStrictEqual(a.get('x'), { foo: 1, bar: 2 })
212
+ assert.strictEqual(a.get('y'), 1)
213
+ assert.strictEqual(a.get('z'), 3)
214
+ })
215
+
216
+ test('Assigns symbol keys (including non-enumerables)', () =>
217
+ {
218
+ const k = Symbol('k')
219
+
220
+ const a = {}
221
+ const b = {}
222
+
223
+ Object.defineProperty(b, k,
224
+ {
225
+ value : 42,
226
+ writable : true,
227
+ configurable : true,
228
+ enumerable : false
229
+ })
230
+
231
+ deepassign(a, b)
232
+
233
+ const d = Object.getOwnPropertyDescriptor(a, k)
234
+ assert.strictEqual(d.value, 42)
235
+ assert.strictEqual(d.enumerable, false)
236
+ })
237
+
238
+ test('Does not invoke getters while assigning', () =>
239
+ {
240
+ const a = {}
241
+ const b = {}
242
+
243
+ let calls = 0
244
+
245
+ Object.defineProperty(a, 'foo',
246
+ {
247
+ enumerable : true,
248
+ configurable : true,
249
+ get() { calls++; return { foo: 1 } }
250
+ })
251
+
252
+ Object.defineProperty(b, 'foo',
253
+ {
254
+ enumerable : true,
255
+ configurable : true,
256
+ value : { bar: 2 },
257
+ writable : true
258
+ })
259
+
260
+ deepassign(a, b)
261
+
262
+ assert.strictEqual(calls, 0, 'Getter must not be invoked during assign')
263
+
264
+ const d = Object.getOwnPropertyDescriptor(a, 'foo')
265
+ assert.ok('value' in d)
266
+ assert.deepStrictEqual(d.value, { bar: 2 })
267
+ })
268
+
269
+ test('Copies accessor descriptors from b when a does not have the property', () =>
270
+ {
271
+ const a = {}
272
+ const b = {}
273
+
274
+ const getB = () => 123
275
+
276
+ Object.defineProperty(b, 'foo',
277
+ {
278
+ enumerable : true,
279
+ configurable : true,
280
+ get : getB
281
+ })
282
+
283
+ deepassign(a, b)
284
+
285
+ const d = Object.getOwnPropertyDescriptor(a, 'foo')
286
+ assert.strictEqual(d.get, getB)
287
+ assert.strictEqual(d.set, undefined)
288
+ assert.strictEqual(d.value, undefined)
289
+ })
290
+
291
+ test('Does not override a non-configurable accessor on a', () =>
292
+ {
293
+ const a = {}
294
+ const b = {}
295
+
296
+ const getA = () => 1
297
+ const getB = () => 2
298
+
299
+ Object.defineProperty(a, 'foo',
300
+ {
301
+ enumerable : true,
302
+ configurable : false,
303
+ get : getA
304
+ })
305
+
306
+ Object.defineProperty(b, 'foo',
307
+ {
308
+ enumerable : true,
309
+ configurable : true,
310
+ get : getB
311
+ })
312
+
313
+ deepassign(a, b)
314
+
315
+ const d = Object.getOwnPropertyDescriptor(a, 'foo')
316
+ assert.strictEqual(d.get, getA, 'Non-configurable accessor must remain')
317
+ })
318
+
319
+ test('Assigns into writable non-configurable data properties', () =>
320
+ {
321
+ const a = {}
322
+ const b = {}
323
+
324
+ Object.defineProperty(a, 'foo',
325
+ {
326
+ enumerable : false,
327
+ configurable : false,
328
+ writable : true,
329
+ value : 1
330
+ })
331
+
332
+ Object.defineProperty(b, 'foo',
333
+ {
334
+ enumerable : true,
335
+ configurable : true,
336
+ writable : true,
337
+ value : 2
338
+ })
339
+
340
+ deepassign(a, b)
341
+
342
+ const d = Object.getOwnPropertyDescriptor(a, 'foo')
343
+ assert.strictEqual(d.value, 2)
344
+ assert.strictEqual(d.configurable, false)
345
+ assert.strictEqual(d.enumerable, false)
346
+ assert.strictEqual(d.writable, true)
347
+ })
348
+ })
188
349
  })
package/clone.js CHANGED
@@ -1,95 +1,173 @@
1
1
  export default function clone(input, options = {})
2
2
  {
3
- const seen = new WeakSet()
3
+ const seen = new WeakMap()
4
4
 
5
5
  options.preservesImutable = options.preservesImutable ?? false
6
6
  options.preservesEnumerable = options.preservesEnumerable ?? true
7
- options.fallback = options.fallback ?? cloneFallback.bind(null, options, seen)
7
+ options.fallback = options.fallback ?? ((value) => cloneObject(options, seen, value))
8
8
 
9
- return deepClone(input, options, seen)
9
+ return deepClone(options, seen, input)
10
10
  }
11
11
 
12
- function deepClone(value, options, seen)
12
+ function deepClone(options, seen, value)
13
13
  {
14
+ let clone
15
+
14
16
  switch(Object.prototype.toString.call(value))
15
17
  {
16
- case '[object Array]' : return cloneArray(value, options, seen)
17
- case '[object Object]' : return cloneObject(value, options, seen)
18
+ case '[object Array]' : clone = cloneArray(options, seen, value) ; break
19
+ case '[object Object]' : clone = cloneObject(options, seen, value) ; break
20
+ case '[object Set]' : clone = cloneSet(options, seen, value) ; break
21
+ case '[object Map]' : clone = cloneMap(options, seen, value) ; break
22
+ case '[object Date]' : clone = new Date(value.getTime()) ; break
23
+ case '[object RegExp]' : clone = new RegExp(value.source, value.flags) ; break
24
+ default : clone = options.fallback(value) ; break
25
+ }
26
+
27
+ if(options.preservesImutable
28
+ && value && typeof value === 'object'
29
+ && clone && typeof clone === 'object')
30
+ {
31
+ if(false === Object.isExtensible(value))
32
+ {
33
+ Object.preventExtensions(clone)
34
+ }
35
+
36
+ if(Object.isSealed(value))
37
+ {
38
+ Object.seal(clone)
39
+ }
40
+
41
+ if(Object.isFrozen(value))
42
+ {
43
+ Object.freeze(clone)
44
+ }
18
45
  }
19
46
 
20
- return options.fallback(value)
47
+ return clone
21
48
  }
22
49
 
23
- function cloneArray(array, options, seen)
50
+ function cloneArray(options, seen, array)
24
51
  {
25
- if(seen.has(array))
52
+ const already = seen.get(array)
53
+
54
+ if(already)
55
+ {
56
+ return already
57
+ }
58
+
59
+ const clone = new Array(array.length)
60
+
61
+ seen.set(array, clone)
62
+
63
+ for(let i = 0; i < array.length; i++)
26
64
  {
27
- return array
65
+ clone[i] = deepClone(options, seen, array[i])
28
66
  }
29
67
 
30
- seen.add(array)
31
- return array.map((item) => deepClone(item, options, seen))
68
+ return clone
32
69
  }
33
70
 
34
- function cloneObject(obj, options, seen)
71
+ function cloneObject(options, seen, value)
35
72
  {
36
- if(seen.has(obj))
73
+ if(typeof value !== 'object' || value === null)
74
+ {
75
+ return value
76
+ }
77
+
78
+ const already = seen.get(value)
79
+
80
+ if(already)
37
81
  {
38
- return obj
82
+ return already
39
83
  }
40
84
 
41
- seen.add(obj)
85
+ const clone = Object.create(Object.getPrototypeOf(value))
42
86
 
43
- const output = {}
87
+ seen.set(value, clone)
44
88
 
45
- for(const key of Object.getOwnPropertyNames(obj))
89
+ for (const key of Reflect.ownKeys(value))
46
90
  {
47
- if(options.preservesImutable)
48
- {
49
- const descriptor = Object.getOwnPropertyDescriptor(obj, key)
50
- Object.defineProperty(output, key,
51
- {
52
- enumerable : options.preservesEnumerable ? descriptor.enumerable : true,
53
- writable : descriptor.writable,
54
- configurable : descriptor.configurable,
55
- value : deepClone(descriptor.value, options, seen)
56
- })
57
- continue
58
- }
59
- else if(options.preservesEnumerable)
60
- {
61
- const descriptor = Object.getOwnPropertyDescriptor(obj, key)
62
- Object.defineProperty(output, key,
63
- {
64
- enumerable : descriptor.enumerable,
65
- writable : true,
66
- configurable : true,
67
- value : deepClone(descriptor.value, options, seen)
68
- })
69
- continue
70
- }
71
- else
72
- {
73
- output[key] = deepClone(obj[key], options, seen)
74
- }
91
+ cloneProperty(options, seen, value, clone, key)
75
92
  }
76
93
 
77
- return output
94
+ return clone
78
95
  }
79
96
 
80
- function cloneFallback(options, seen, value)
97
+ function cloneSet(options, seen, set)
81
98
  {
82
- if(typeof value !== 'object'
83
- || value === null)
99
+ const already = seen.get(set)
100
+
101
+ if(already)
84
102
  {
85
- return value
103
+ return already
86
104
  }
87
105
 
88
- const clone = Object.create(Object.getPrototypeOf(value))
89
- for(const key in value)
106
+ const clone = new Set()
107
+
108
+ seen.set(set, clone)
109
+
110
+ for(const value of set)
111
+ {
112
+ clone.add(deepClone(options, seen, value))
113
+ }
114
+
115
+ return clone
116
+ }
117
+
118
+ function cloneMap(options, seen, map)
119
+ {
120
+ const already = seen.get(map)
121
+
122
+ if(already)
123
+ {
124
+ return already
125
+ }
126
+
127
+ const clone = new Map()
128
+
129
+ seen.set(map, clone)
130
+
131
+ for(const [key, value] of map)
90
132
  {
91
- clone[key] = deepClone(value[key], options, seen)
133
+ clone.set(
134
+ deepClone(options, seen, key),
135
+ deepClone(options, seen, value)
136
+ )
92
137
  }
93
138
 
94
139
  return clone
95
- }
140
+ }
141
+
142
+ function cloneProperty(options, seen, src, target, key)
143
+ {
144
+ const descriptor = Object.getOwnPropertyDescriptor(src, key)
145
+
146
+ if(!descriptor)
147
+ {
148
+ return
149
+ }
150
+
151
+ if('value' in descriptor)
152
+ {
153
+ const clonedValue = deepClone(options, seen, descriptor.value)
154
+
155
+ Object.defineProperty(target, key,
156
+ {
157
+ enumerable : options.preservesEnumerable ? descriptor.enumerable : true,
158
+ writable : options.preservesImutable ? descriptor.writable : true,
159
+ configurable : options.preservesImutable ? descriptor.configurable : true,
160
+ value : clonedValue
161
+ })
162
+ }
163
+ else
164
+ {
165
+ Object.defineProperty(target, key,
166
+ {
167
+ enumerable : options.preservesEnumerable ? descriptor.enumerable : true,
168
+ configurable : options.preservesImutable ? descriptor.configurable : true,
169
+ get : descriptor.get,
170
+ set : descriptor.set
171
+ })
172
+ }
173
+ }
package/clone.test.js CHANGED
@@ -109,7 +109,21 @@ suite('@superhero/deep/clone', () =>
109
109
  obj.self = obj
110
110
 
111
111
  const cloned = deepclone(obj)
112
- assert.strictEqual(cloned.self, obj, 'Circular references should be preserved in the clone')
112
+
113
+ assert.notStrictEqual(cloned, obj)
114
+ assert.strictEqual(cloned.self, cloned)
115
+ })
116
+
117
+ test('Preserves shared references', () =>
118
+ {
119
+ const shared = { x: 1 }
120
+ const obj = { a: shared, b: shared }
121
+
122
+ const cloned = deepclone(obj)
123
+
124
+ assert.notStrictEqual(cloned, obj)
125
+ assert.strictEqual(cloned.a, cloned.b)
126
+ assert.notStrictEqual(cloned.a, shared)
113
127
  })
114
128
 
115
129
  test('Clones objects with null prototype', () =>
package/merge.js CHANGED
@@ -1,66 +1,8 @@
1
1
  import deepclone from '@superhero/deep/clone'
2
2
 
3
- /**
4
- * When merging two objects [object Object], a new object is created with the
5
- * properties of both objects defined. The descriptor of the property in the
6
- * new object is determined by the descriptors of the properties in the two
7
- * objects being merged. The priority of the property descriptor is set on the
8
- * new object according to the more restrictive definition in the two sources.
9
- *
10
- * @example if the descriptor of the property in object "a" has "configurable"
11
- * set to "false", and the descriptor of the property in object "b" has the
12
- * "configurable" set to "true", the new object will have the descriptor for
13
- * "configurable" set to "false".
14
- *
15
- * @example if the descriptor of the property in object "a" has "enumerable"
16
- * set to "true", and the descriptor of the property in object "b" has the
17
- * "enumerable" set to "true", the new object will have the descriptor for
18
- * "enumerable" set to "true".
19
- *
20
- * @example if the descriptor of the property in object "a" has "writable" set
21
- * to "false", and the descriptor of the property in object "b" has the
22
- * "writable" set to "false", the new object will have the descriptor for
23
- * "writable" set to "false".
24
- *
25
- * ----------------------------------------------------------------------------
26
- *
27
- * When merging two nested objects [object Object], and there is a circular
28
- * reference, the merge will stop at the circular reference and return the
29
- * object that contains the circular reference.
30
- *
31
- * ----------------------------------------------------------------------------
32
- *
33
- * When merging a and b of different types, the value of b is returned.
34
- *
35
- * @example if the type of the property in object "a" is object "c", and the
36
- * type of the property in object "b" is a number, then the value of the
37
- * property in the new object will be the number.
38
- *
39
- * @example if the type of the property in object "a" is a string, and the type
40
- * of the property in object "b" is object "c", then the value of the property
41
- * in the new object will be the object "c".
42
- *
43
- * ----------------------------------------------------------------------------
44
- *
45
- * When merging two arrays [object Array], a new array is created with the
46
- * unique values of both arrays. The order of the values in the new array is
47
- * determined by the order of the values in the two arrays being merged.
48
- *
49
- * @example if array "a" with values [1, 2, 3] is merged with array "b" with
50
- * values [2, 3, 4], the new array will have values [1, 2, 3, 4].
51
- *
52
- * @example if array "a" with values [2, 3, 4] is merged with array "b" with
53
- * values [1, 2, 3], the new array will have values [2, 3, 4, 1].
54
- *
55
- * @example if array "a" with values [1, 2, 3] is merged with an empty array
56
- * "b", the new array will still have values [1, 2, 3].
57
- *
58
- * @example if array "a" with values [1, 1, 2, 2] is merged with array "b" with
59
- * values [2, 2, 3, 3], the new array will have values [1, 2, 3].
60
- */
61
3
  export default function merge(a, b, ...c)
62
4
  {
63
- const
5
+ const
64
6
  seen = new WeakSet,
65
7
  output = mergeAandB(a, b, seen)
66
8
 
@@ -97,6 +39,18 @@ function mergeAandB(a, b, seen)
97
39
  return mergeObject(a, b, seen)
98
40
  }
99
41
 
42
+ if('[object Set]' === aType
43
+ && '[object Set]' === bType)
44
+ {
45
+ return mergeSet(a, b)
46
+ }
47
+
48
+ if('[object Map]' === aType
49
+ && '[object Map]' === bType)
50
+ {
51
+ return mergeMap(a, b, seen)
52
+ }
53
+
100
54
  return b
101
55
  }
102
56
 
@@ -105,6 +59,37 @@ function mergeArray(a, b)
105
59
  return [...new Set(a.concat(b))]
106
60
  }
107
61
 
62
+ function mergeSet(a, b)
63
+ {
64
+ return new Set([...a, ...b])
65
+ }
66
+
67
+ function mergeMap(a, b, seen)
68
+ {
69
+ if(seen.has(a))
70
+ {
71
+ return b
72
+ }
73
+
74
+ seen.add(a)
75
+
76
+ const output = new Map(a)
77
+
78
+ for(const [key, bValue] of b)
79
+ {
80
+ if(output.has(key))
81
+ {
82
+ const aValue = output.get(key)
83
+ output.set(key, mergeAandB(aValue, bValue, seen))
84
+ }
85
+ else
86
+ {
87
+ output.set(key, bValue)
88
+ }
89
+ }
90
+
91
+ return output
92
+ }
108
93
  function mergeObject(a, b, seen)
109
94
  {
110
95
  if(seen.has(a))
@@ -114,43 +99,100 @@ function mergeObject(a, b, seen)
114
99
 
115
100
  seen.add(a)
116
101
 
117
- const output = {}
102
+ const output = Object.create(Object.getPrototypeOf(a))
118
103
 
119
- for(const key of Object.getOwnPropertyNames(a))
104
+ // copy keys unique to a
105
+ for(const key of Reflect.ownKeys(a))
120
106
  {
121
- if(key in b)
107
+ if(Object.prototype.hasOwnProperty.call(b, key))
122
108
  {
123
109
  continue
124
110
  }
125
- else
111
+
112
+ const descriptor = Object.getOwnPropertyDescriptor(a, key)
113
+ if(descriptor)
126
114
  {
127
- const descriptor = Object.getOwnPropertyDescriptor(a, key)
128
115
  Object.defineProperty(output, key, descriptor)
129
116
  }
130
117
  }
131
118
 
132
- for(const key of Object.getOwnPropertyNames(b))
119
+ // merge/copy keys from b
120
+ for(const key of Reflect.ownKeys(b))
133
121
  {
134
- if(key in a)
122
+ if(Object.prototype.hasOwnProperty.call(a, key))
135
123
  {
136
- const
137
- descriptor_a = Object.getOwnPropertyDescriptor(a, key),
138
- descriptor_b = Object.getOwnPropertyDescriptor(b, key)
124
+ const da = Object.getOwnPropertyDescriptor(a, key)
125
+ const db = Object.getOwnPropertyDescriptor(b, key)
139
126
 
140
- Object.defineProperty(output, key,
127
+ if(!da || !db)
141
128
  {
142
- configurable : descriptor_a.configurable && descriptor_b.configurable,
143
- enumerable : descriptor_a.enumerable && descriptor_b.enumerable,
144
- writable : descriptor_a.writable && descriptor_b.writable,
145
- value : mergeAandB(a[key], b[key], seen)
146
- })
129
+ continue
130
+ }
131
+
132
+ Object.defineProperty(output, key, mergeDescriptor(da, db, seen))
147
133
  }
148
134
  else
149
135
  {
150
136
  const descriptor = Object.getOwnPropertyDescriptor(b, key)
151
- Object.defineProperty(output, key, descriptor)
137
+ if(descriptor)
138
+ {
139
+ Object.defineProperty(output, key, descriptor)
140
+ }
152
141
  }
153
142
  }
154
143
 
155
144
  return output
156
- }
145
+ }
146
+
147
+ function mergeDescriptor(da, db, seen)
148
+ {
149
+ const enumerable = (da.enumerable ?? true) && (db.enumerable ?? true)
150
+ const configurable = (da.configurable ?? true) && (db.configurable ?? true)
151
+
152
+ const aIsData = 'value' in da
153
+ const bIsData = 'value' in db
154
+
155
+ // data + data: keep “most restrictive” flags and merge the values
156
+ if(aIsData && bIsData)
157
+ {
158
+ const writable = (da.writable ?? true) && (db.writable ?? true)
159
+
160
+ return {
161
+ enumerable,
162
+ configurable,
163
+ writable,
164
+ value: mergeAandB(da.value, db.value, seen)
165
+ }
166
+ }
167
+
168
+ // accessor + accessor: keep “most restrictive” flags and combine get/set
169
+ // (b wins when both define the same accessor)
170
+ if(!aIsData && !bIsData)
171
+ {
172
+ return {
173
+ enumerable,
174
+ configurable,
175
+ get: db.get ?? da.get,
176
+ set: db.set ?? da.set
177
+ }
178
+ }
179
+
180
+ // mixed (data vs accessor): do NOT evaluate accessors; prefer b’s shape,
181
+ // but apply “most restrictive” flags
182
+ if(bIsData)
183
+ {
184
+ return {
185
+ enumerable,
186
+ configurable,
187
+ writable: (da.writable ?? true) && (db.writable ?? true),
188
+ value: db.value
189
+ }
190
+ }
191
+
192
+ return {
193
+ enumerable,
194
+ configurable,
195
+ get: db.get,
196
+ set: db.set
197
+ }
198
+ }
package/merge.test.js CHANGED
@@ -178,4 +178,233 @@ suite('@superhero/deep/merge', () =>
178
178
  const resultB = deepmerge(a, b, undefined, c)
179
179
  assert.deepStrictEqual(resultB, expected, 'Ignore undefined attributes')
180
180
  })
181
+
182
+ test('Merges sets with unique values', () =>
183
+ {
184
+ const
185
+ a = new Set([1, 2, 3]),
186
+ b = new Set([2, 3, 4]),
187
+ expected = new Set([1, 2, 3, 4])
188
+
189
+ const result = deepmerge(a, b)
190
+ assert.deepStrictEqual(result, expected, 'Sets should merge as a union')
191
+ })
192
+
193
+ test('Merges maps by key and keeps b for new keys', () =>
194
+ {
195
+ const
196
+ a = new Map([['a', 1]]),
197
+ b = new Map([['b', 2]])
198
+
199
+ const result = deepmerge(a, b)
200
+
201
+ assert.ok(result instanceof Map)
202
+ assert.strictEqual(result.get('a'), 1)
203
+ assert.strictEqual(result.get('b'), 2)
204
+ })
205
+
206
+ test('Merges maps by key and deep-merges values on conflicts', () =>
207
+ {
208
+ const
209
+ a = new Map([['x', { foo: 1 }]]),
210
+ b = new Map([['x', { bar: 2 }]])
211
+
212
+ const result = deepmerge(a, b)
213
+
214
+ assert.ok(result instanceof Map)
215
+ assert.deepStrictEqual(result.get('x'), { foo: 1, bar: 2 })
216
+ })
217
+
218
+ test('Merges accessor descriptors without evaluating getters', () =>
219
+ {
220
+ const a = {}
221
+ const b = {}
222
+
223
+ let getterCalls = 0
224
+
225
+ Object.defineProperty(a, 'foo',
226
+ {
227
+ enumerable : true,
228
+ configurable : true,
229
+ get() { getterCalls++; return 1 }
230
+ })
231
+
232
+ Object.defineProperty(b, 'foo',
233
+ {
234
+ enumerable : true,
235
+ configurable : true,
236
+ get() { getterCalls++; return 2 }
237
+ })
238
+
239
+ const result = deepmerge(a, b)
240
+ const descriptor = Object.getOwnPropertyDescriptor(result, 'foo')
241
+
242
+ assert.strictEqual(getterCalls, 0, 'Getters must not be invoked during merge')
243
+ assert.ok(descriptor.get, 'Merged property should keep a getter')
244
+ assert.strictEqual(typeof descriptor.get, 'function')
245
+ })
246
+
247
+ test('Merges accessor+accessor and prefers b get/set when present', () =>
248
+ {
249
+ const a = {}
250
+ const b = {}
251
+
252
+ const getA = () => 1
253
+ const setA = () => {}
254
+ const getB = () => 2
255
+
256
+ Object.defineProperty(a, 'foo',
257
+ {
258
+ enumerable : true,
259
+ configurable : true,
260
+ get : getA,
261
+ set : setA
262
+ })
263
+
264
+ Object.defineProperty(b, 'foo',
265
+ {
266
+ enumerable : true,
267
+ configurable : true,
268
+ get : getB
269
+ // no set here
270
+ })
271
+
272
+ const result = deepmerge(a, b)
273
+ const d = Object.getOwnPropertyDescriptor(result, 'foo')
274
+
275
+ assert.strictEqual(d.get, getB, 'Getter should prefer b')
276
+ assert.strictEqual(d.set, setA, 'Setter should fall back to a when b lacks it')
277
+ })
278
+
279
+ test('Merges data+accessor and prefers b shape without invoking accessors', () =>
280
+ {
281
+ const a = {}
282
+ const b = {}
283
+
284
+ Object.defineProperty(a, 'foo',
285
+ {
286
+ enumerable : true,
287
+ configurable : true,
288
+ value : 1,
289
+ writable : true
290
+ })
291
+
292
+ let calls = 0
293
+ Object.defineProperty(b, 'foo',
294
+ {
295
+ enumerable : true,
296
+ configurable : true,
297
+ get() { calls++; return 2 }
298
+ })
299
+
300
+ const result = deepmerge(a, b)
301
+ const d = Object.getOwnPropertyDescriptor(result, 'foo')
302
+
303
+ assert.strictEqual(calls, 0, 'Getter must not be invoked during merge')
304
+ assert.ok(d.get, 'Result should be an accessor (b shape)')
305
+ assert.strictEqual(d.value, undefined)
306
+ })
307
+
308
+ // ... to cover some edge cases ...
309
+
310
+ test('Returns first value when second is undefined', () =>
311
+ {
312
+ const a = { foo: 1 }
313
+ const result = deepmerge(a, undefined)
314
+
315
+ assert.deepStrictEqual(result, a)
316
+ assert.notStrictEqual(result, a)
317
+ })
318
+
319
+ test('Short-circuits on identical references', () =>
320
+ {
321
+ const a = { foo: 1 }
322
+ const result = deepmerge(a, a)
323
+
324
+ assert.deepStrictEqual(result, a)
325
+ assert.notStrictEqual(result, a)
326
+ })
327
+
328
+ test('Merges symbol keys and prioritizes restrictive descriptors', () =>
329
+ {
330
+ const
331
+ a = {},
332
+ b = {},
333
+ key = Symbol('foo')
334
+
335
+ Object.defineProperty(a, key,
336
+ {
337
+ value : 1,
338
+ writable : false,
339
+ configurable : false,
340
+ enumerable : false
341
+ })
342
+
343
+ Object.defineProperty(b, key,
344
+ {
345
+ value : 2,
346
+ writable : true,
347
+ configurable : true,
348
+ enumerable : true
349
+ })
350
+
351
+ const
352
+ result = deepmerge(a, b),
353
+ descriptor = Object.getOwnPropertyDescriptor(result, key)
354
+
355
+ assert.strictEqual(descriptor.value, 2)
356
+ assert.strictEqual(descriptor.writable, false)
357
+ assert.strictEqual(descriptor.configurable, false)
358
+ assert.strictEqual(descriptor.enumerable, false)
359
+ })
360
+
361
+ test('Merges accessor+data and prefers b data without invoking getter', () =>
362
+ {
363
+ const a = {}
364
+ const b = {}
365
+
366
+ let calls = 0
367
+
368
+ Object.defineProperty(a, 'foo',
369
+ {
370
+ enumerable : true,
371
+ configurable : true,
372
+ get() { calls++; return 1 }
373
+ })
374
+
375
+ Object.defineProperty(b, 'foo',
376
+ {
377
+ enumerable : true,
378
+ configurable : true,
379
+ writable : true,
380
+ value : 42
381
+ })
382
+
383
+ const
384
+ result = deepmerge(a, b),
385
+ d = Object.getOwnPropertyDescriptor(result, 'foo')
386
+
387
+ assert.strictEqual(calls, 0)
388
+ assert.strictEqual(d.value, 42)
389
+ assert.strictEqual(d.get, undefined)
390
+ assert.strictEqual(d.set, undefined)
391
+ })
392
+
393
+ test('Stops at circular references inside maps', () =>
394
+ {
395
+ const a = new Map()
396
+ const b = new Map()
397
+
398
+ a.set('self', a)
399
+ b.set('self', b)
400
+
401
+ const result = deepmerge(a, b)
402
+
403
+ assert.ok(result instanceof Map)
404
+
405
+ const inner = result.get('self')
406
+ assert.ok(inner instanceof Map)
407
+ assert.strictEqual(inner.get('self'), inner)
408
+ })
409
+
181
410
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superhero/deep",
3
- "version": "4.8.0",
3
+ "version": "4.8.2",
4
4
  "description": "A collection of deep structure operations",
5
5
  "keywords": [
6
6
  "deep",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {},
26
26
  "devDependencies": {
27
- "@superhero/audit": "4.8.0",
27
+ "@superhero/audit": "4.8.2",
28
28
  "@superhero/syntax-check": "0.0.2"
29
29
  },
30
30
  "author": {