@superhero/deep 4.8.1 → 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 +6 -0
- package/assign.js +97 -28
- package/assign.test.js +161 -0
- package/clone.js +133 -55
- package/clone.test.js +15 -1
- package/merge.js +119 -77
- package/merge.test.js +229 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
|
92
|
+
for(const key of Reflect.ownKeys(b))
|
|
53
93
|
{
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
101
|
+
continue
|
|
58
102
|
}
|
|
59
|
-
|
|
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
|
-
|
|
114
|
+
assignObject(da.value, db.value)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
62
117
|
|
|
63
|
-
|
|
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,
|
|
123
|
+
assignPropertyDescriptor(a, da, db, key)
|
|
66
124
|
}
|
|
67
|
-
else if(
|
|
125
|
+
else if(da && ('value' in da) && da.writable)
|
|
68
126
|
{
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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,
|
|
146
|
+
function assignPropertyDescriptor(a, da, db, key)
|
|
87
147
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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 ??
|
|
7
|
+
options.fallback = options.fallback ?? ((value) => cloneObject(options, seen, value))
|
|
8
8
|
|
|
9
|
-
return deepClone(
|
|
9
|
+
return deepClone(options, seen, input)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function deepClone(
|
|
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]'
|
|
17
|
-
case '[object Object]'
|
|
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
|
|
47
|
+
return clone
|
|
21
48
|
}
|
|
22
49
|
|
|
23
|
-
function cloneArray(
|
|
50
|
+
function cloneArray(options, seen, array)
|
|
24
51
|
{
|
|
25
|
-
|
|
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
|
-
|
|
65
|
+
clone[i] = deepClone(options, seen, array[i])
|
|
28
66
|
}
|
|
29
67
|
|
|
30
|
-
|
|
31
|
-
return array.map((item) => deepClone(item, options, seen))
|
|
68
|
+
return clone
|
|
32
69
|
}
|
|
33
70
|
|
|
34
|
-
function cloneObject(
|
|
71
|
+
function cloneObject(options, seen, value)
|
|
35
72
|
{
|
|
36
|
-
if(
|
|
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
|
|
82
|
+
return already
|
|
39
83
|
}
|
|
40
84
|
|
|
41
|
-
|
|
85
|
+
const clone = Object.create(Object.getPrototypeOf(value))
|
|
42
86
|
|
|
43
|
-
|
|
87
|
+
seen.set(value, clone)
|
|
44
88
|
|
|
45
|
-
for(const key of
|
|
89
|
+
for (const key of Reflect.ownKeys(value))
|
|
46
90
|
{
|
|
47
|
-
|
|
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
|
|
94
|
+
return clone
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
function
|
|
97
|
+
function cloneSet(options, seen, set)
|
|
81
98
|
{
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
const already = seen.get(set)
|
|
100
|
+
|
|
101
|
+
if(already)
|
|
84
102
|
{
|
|
85
|
-
return
|
|
103
|
+
return already
|
|
86
104
|
}
|
|
87
105
|
|
|
88
|
-
const clone =
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
// copy keys unique to a
|
|
105
|
+
for(const key of Reflect.ownKeys(a))
|
|
120
106
|
{
|
|
121
|
-
if(key
|
|
107
|
+
if(Object.prototype.hasOwnProperty.call(b, key))
|
|
122
108
|
{
|
|
123
109
|
continue
|
|
124
110
|
}
|
|
125
|
-
|
|
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
|
-
|
|
119
|
+
// merge/copy keys from b
|
|
120
|
+
for(const key of Reflect.ownKeys(b))
|
|
133
121
|
{
|
|
134
|
-
if(key
|
|
122
|
+
if(Object.prototype.hasOwnProperty.call(a, key))
|
|
135
123
|
{
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
descriptor_b = Object.getOwnPropertyDescriptor(b, key)
|
|
124
|
+
const da = Object.getOwnPropertyDescriptor(a, key)
|
|
125
|
+
const db = Object.getOwnPropertyDescriptor(b, key)
|
|
139
126
|
|
|
140
|
-
|
|
127
|
+
if(!da || !db)
|
|
141
128
|
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
27
|
+
"@superhero/audit": "4.8.2",
|
|
28
28
|
"@superhero/syntax-check": "0.0.2"
|
|
29
29
|
},
|
|
30
30
|
"author": {
|