@superhero/deep 4.0.0
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/LICENCE +21 -0
- package/README.md +197 -0
- package/assign.js +89 -0
- package/assign.test.js +188 -0
- package/clone.js +9 -0
- package/clone.test.js +82 -0
- package/config.json +10 -0
- package/freeze.js +33 -0
- package/freeze.test.js +123 -0
- package/index.js +4 -0
- package/merge.js +152 -0
- package/merge.test.js +181 -0
- package/package.json +24 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Erik Landvall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
|
|
2
|
+
# Deep Utilities
|
|
3
|
+
|
|
4
|
+
This repository contains a set of deep utility classes for handling common operations like cloning, freezing, merging, and assigning objects and arrays. Below is the documentation for each utility class, its purpose, and examples.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. **DeepAssign**
|
|
9
|
+
|
|
10
|
+
### Purpose:
|
|
11
|
+
Deeply assigns properties from one or more source objects to a target object. Handles nested structures, arrays, and property descriptors.
|
|
12
|
+
|
|
13
|
+
### Features:
|
|
14
|
+
- Merges arrays with unique values.
|
|
15
|
+
- Deeply merges nested objects.
|
|
16
|
+
- Handles property descriptors (`writable`, `configurable`, `enumerable`).
|
|
17
|
+
|
|
18
|
+
### Example:
|
|
19
|
+
```javascript
|
|
20
|
+
import deepassign from '@superhero/deep/assign'
|
|
21
|
+
|
|
22
|
+
const target = { foo: { bar: 1 } }
|
|
23
|
+
const source = { foo: { baz: 2 } }
|
|
24
|
+
|
|
25
|
+
deepassign.assign(target, source)
|
|
26
|
+
|
|
27
|
+
console.log(target) // { foo: { bar: 1, baz: 2 } }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. **DeepMerge**
|
|
33
|
+
|
|
34
|
+
### Purpose:
|
|
35
|
+
Deeply merges two or more objects into a new object. Handles nested structures, circular references, arrays, and property descriptors.
|
|
36
|
+
|
|
37
|
+
### Features:
|
|
38
|
+
- Merges arrays with unique values while maintaining order.
|
|
39
|
+
- Merges nested objects with priority for restrictive property descriptors.
|
|
40
|
+
- Detects and handles circular references.
|
|
41
|
+
|
|
42
|
+
### Example:
|
|
43
|
+
```javascript
|
|
44
|
+
import deepmerge from '@superhero/deep/merge'
|
|
45
|
+
|
|
46
|
+
const obj1 = { foo: { bar: 1 }, arr: [1, 2] }
|
|
47
|
+
const obj2 = { foo: { baz: 2 }, arr: [2, 3] }
|
|
48
|
+
|
|
49
|
+
const result = deepmerge.merge(obj1, obj2)
|
|
50
|
+
|
|
51
|
+
console.log(result)
|
|
52
|
+
// { foo: { bar: 1, baz: 2 }, arr: [1, 2, 3] }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. **DeepFreeze**
|
|
58
|
+
|
|
59
|
+
### Purpose:
|
|
60
|
+
Recursively freezes an object, making it immutable. Handles nested structures and circular references.
|
|
61
|
+
|
|
62
|
+
### Features:
|
|
63
|
+
- Freezes nested objects and arrays.
|
|
64
|
+
- Handles circular references gracefully.
|
|
65
|
+
|
|
66
|
+
### Example:
|
|
67
|
+
```javascript
|
|
68
|
+
import deepfreeze from '@superhero/deep/freeze'
|
|
69
|
+
|
|
70
|
+
const obj = { foo: { bar: 'baz' } }
|
|
71
|
+
obj.foo.self = obj.foo // Circular reference
|
|
72
|
+
|
|
73
|
+
deepfreeze.freeze(obj)
|
|
74
|
+
|
|
75
|
+
obj.foo.bar = 'new value' // TypeError: Cannot assign to read-only property
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 4. **DeepClone**
|
|
81
|
+
|
|
82
|
+
### Purpose:
|
|
83
|
+
Creates a deep clone of an object. Supports nested structures, circular references, arrays, and custom serialization methods.
|
|
84
|
+
|
|
85
|
+
### Features:
|
|
86
|
+
- Uses `structuredClone` if available, falling back to JSON-based cloning.
|
|
87
|
+
- Handles nested objects, arrays, and custom `toJSON` methods.
|
|
88
|
+
- Supports circular references if `structuredClone` is available.
|
|
89
|
+
|
|
90
|
+
### Example:
|
|
91
|
+
```javascript
|
|
92
|
+
import deepclone from '@superhero/deep/clone'
|
|
93
|
+
|
|
94
|
+
const obj = { foo: { bar: 'baz' }, arr: [1, 2, 3] }
|
|
95
|
+
|
|
96
|
+
const clone = deepclone.clone(obj)
|
|
97
|
+
|
|
98
|
+
console.log(clone) // Deeply cloned object
|
|
99
|
+
console.log(clone === obj) // false
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Testing
|
|
105
|
+
Each utility class has a set of unit tests to ensure correctness across different cases.
|
|
106
|
+
|
|
107
|
+
### Run Tests
|
|
108
|
+
To execute the tests:
|
|
109
|
+
```bash
|
|
110
|
+
npm test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Test Coverage
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
▶ @superhero/deep/assign
|
|
117
|
+
✔ Merges arrays correctly (2.205053ms)
|
|
118
|
+
✔ Merges objects correctly (0.37599ms)
|
|
119
|
+
✔ Overwrites non-object properties correctly (0.211516ms)
|
|
120
|
+
✔ Handles undefined values correctly (0.174874ms)
|
|
121
|
+
▶ Descriptor properties
|
|
122
|
+
▶ Retains
|
|
123
|
+
✔ non-writable, non-configurable and non-enumarable (0.302976ms)
|
|
124
|
+
✔ writable but non-configurable and non-enumarable (0.250721ms)
|
|
125
|
+
✔ writable and configurable but non-enumarable (0.148421ms)
|
|
126
|
+
✔ Retains (0.912545ms)
|
|
127
|
+
▶ Assigns
|
|
128
|
+
✔ non-writable, non-configurable and non-enumarable (0.266101ms)
|
|
129
|
+
✔ Assigns (1.379408ms)
|
|
130
|
+
✔ Descriptor properties (2.578897ms)
|
|
131
|
+
✔ Merges nested arrays correctly (0.188364ms)
|
|
132
|
+
✔ Merges nested objects correctly (0.159693ms)
|
|
133
|
+
✔ Does not alter objects with no conflicts (0.127767ms)
|
|
134
|
+
✔ @superhero/deep/assign (7.763161ms)
|
|
135
|
+
|
|
136
|
+
▶ @superhero/deep/clone
|
|
137
|
+
✔ Clones simple objects (2.379372ms)
|
|
138
|
+
✔ Clones nested objects (0.316103ms)
|
|
139
|
+
✔ Clones arrays (0.31091ms)
|
|
140
|
+
✔ Handles circular references (0.204913ms)
|
|
141
|
+
✔ Clones objects with null prototype (0.385473ms)
|
|
142
|
+
✔ @superhero/deep/clone (5.133978ms)
|
|
143
|
+
|
|
144
|
+
▶ @superhero/deep/freeze
|
|
145
|
+
✔ Freezes a simple object (1.831304ms)
|
|
146
|
+
✔ Freezes nested objects recursively (0.242757ms)
|
|
147
|
+
✔ Handles circular references gracefully (0.155595ms)
|
|
148
|
+
✔ Freezes objects with symbols (0.190945ms)
|
|
149
|
+
✔ Handles already frozen objects without error (0.129469ms)
|
|
150
|
+
✔ Freezes objects with non-enumerable properties (0.193278ms)
|
|
151
|
+
✔ Freezes arrays (0.168815ms)
|
|
152
|
+
✔ Handles objects with null prototype (0.160426ms)
|
|
153
|
+
✔ Freezes objects with multiple property types (0.351257ms)
|
|
154
|
+
✔ @superhero/deep/freeze (5.204715ms)
|
|
155
|
+
|
|
156
|
+
▶ @superhero/deep/merge
|
|
157
|
+
✔ Merges arrays with unique values (2.511877ms)
|
|
158
|
+
✔ Merges arrays with order preserved (0.324244ms)
|
|
159
|
+
✔ Handles empty arrays correctly (0.319828ms)
|
|
160
|
+
✔ Handles arrays with duplicate values (0.258181ms)
|
|
161
|
+
✔ Merges objects and prioritizes restrictive descriptors (0.462677ms)
|
|
162
|
+
✔ Merges objects with non-enumerable properties (0.203703ms)
|
|
163
|
+
✔ Handles nested object merging (0.198121ms)
|
|
164
|
+
✔ Stops at circular references (0.137618ms)
|
|
165
|
+
✔ Stops when nested and with circular references (0.34896ms)
|
|
166
|
+
✔ Returns second value for non-object types (0.230399ms)
|
|
167
|
+
✔ Handles multiple merges sequentially (0.294846ms)
|
|
168
|
+
✔ @superhero/deep/merge (7.196893ms)
|
|
169
|
+
|
|
170
|
+
tests 36
|
|
171
|
+
pass 36
|
|
172
|
+
|
|
173
|
+
-----------------------------------------------------------------
|
|
174
|
+
file | line % | branch % | funcs % | uncovered lines
|
|
175
|
+
-----------------------------------------------------------------
|
|
176
|
+
assign.js | 100.00 | 100.00 | 100.00 |
|
|
177
|
+
assign.test.js | 100.00 | 100.00 | 100.00 |
|
|
178
|
+
clone.js | 100.00 | 100.00 | 100.00 |
|
|
179
|
+
clone.test.js | 96.34 | 87.50 | 100.00 | 56-58
|
|
180
|
+
freeze.js | 100.00 | 100.00 | 100.00 |
|
|
181
|
+
freeze.test.js | 100.00 | 100.00 | 100.00 |
|
|
182
|
+
merge.js | 100.00 | 100.00 | 100.00 |
|
|
183
|
+
merge.test.js | 100.00 | 100.00 | 100.00 |
|
|
184
|
+
-----------------------------------------------------------------
|
|
185
|
+
all files | 99.65 | 99.15 | 100.00 |
|
|
186
|
+
-----------------------------------------------------------------
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
This project is licensed under the MIT License.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Contributing
|
|
197
|
+
Feel free to submit issues or pull requests for improvements or additional features.
|
package/assign.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export default new class DeepAssign
|
|
2
|
+
{
|
|
3
|
+
assign(a, ...b)
|
|
4
|
+
{
|
|
5
|
+
b.forEach((b) => this.#assign(a, b))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#assign(a, b)
|
|
9
|
+
{
|
|
10
|
+
if(b === undefined)
|
|
11
|
+
{
|
|
12
|
+
return a
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if(Object.prototype.toString.call(a) === '[object Array]'
|
|
16
|
+
&& Object.prototype.toString.call(b) === '[object Array]')
|
|
17
|
+
{
|
|
18
|
+
return this.#assignArray(a, b)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if(Object.prototype.toString.call(a) === '[object Object]'
|
|
22
|
+
&& Object.prototype.toString.call(b) === '[object Object]')
|
|
23
|
+
{
|
|
24
|
+
return this.#assignObject(a, b)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return b
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#assignArray(a, b)
|
|
31
|
+
{
|
|
32
|
+
const values = new Set(a.concat(b))
|
|
33
|
+
a.length = 0
|
|
34
|
+
a.push(...values)
|
|
35
|
+
return a
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Object} a
|
|
40
|
+
* @param {Object} b
|
|
41
|
+
*
|
|
42
|
+
* @throws {TypeError} If a property in "a" is also in "b",
|
|
43
|
+
* but the property in "a" is not configurable.
|
|
44
|
+
*
|
|
45
|
+
* @returns {Object} a
|
|
46
|
+
*/
|
|
47
|
+
#assignObject(a, b)
|
|
48
|
+
{
|
|
49
|
+
for (const key of Object.getOwnPropertyNames(b))
|
|
50
|
+
{
|
|
51
|
+
if(Object.prototype.toString.call(a[key]) === '[object Object]'
|
|
52
|
+
&& Object.prototype.toString.call(b[key]) === '[object Object]')
|
|
53
|
+
{
|
|
54
|
+
this.#assignObject(a[key], b[key])
|
|
55
|
+
}
|
|
56
|
+
else if(Object.hasOwnProperty.call(a, key))
|
|
57
|
+
{
|
|
58
|
+
const descriptor_a = Object.getOwnPropertyDescriptor(a, key)
|
|
59
|
+
|
|
60
|
+
if(descriptor_a.configurable)
|
|
61
|
+
{
|
|
62
|
+
this.#assignPropertyDescriptor(a, b, key)
|
|
63
|
+
}
|
|
64
|
+
else if(descriptor_a.writable)
|
|
65
|
+
{
|
|
66
|
+
descriptor_a.value = b[key]
|
|
67
|
+
Object.defineProperty(a, key, descriptor_a)
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
{
|
|
76
|
+
this.#assignPropertyDescriptor(a, b, key)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return a
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#assignPropertyDescriptor(a, b, key)
|
|
84
|
+
{
|
|
85
|
+
const descriptor_b = Object.getOwnPropertyDescriptor(b, key)
|
|
86
|
+
descriptor_b.value = this.#assign(a[key], b[key])
|
|
87
|
+
Object.defineProperty(a, key, descriptor_b)
|
|
88
|
+
}
|
|
89
|
+
}
|
package/assign.test.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { suite, test } from 'node:test'
|
|
3
|
+
import deepassign from '@superhero/deep/assign'
|
|
4
|
+
|
|
5
|
+
suite('@superhero/deep/assign', () =>
|
|
6
|
+
{
|
|
7
|
+
test('Merges arrays correctly', () =>
|
|
8
|
+
{
|
|
9
|
+
const
|
|
10
|
+
a = [1, 2, 3],
|
|
11
|
+
b = [3, 4, 5],
|
|
12
|
+
expected = [1, 2, 3, 4, 5]
|
|
13
|
+
|
|
14
|
+
deepassign.assign(a, b)
|
|
15
|
+
assert.deepStrictEqual(a, expected, 'Arrays should merge with unique values')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('Merges objects correctly', () =>
|
|
19
|
+
{
|
|
20
|
+
const
|
|
21
|
+
a = { foo: 1, bar: { baz: 2 } },
|
|
22
|
+
b = { bar: { qux: 3 }, hello: 'world' },
|
|
23
|
+
expected = { foo: 1, bar: { baz: 2, qux: 3 }, hello: 'world' }
|
|
24
|
+
|
|
25
|
+
deepassign.assign(a, b)
|
|
26
|
+
assert.deepStrictEqual(a, expected, 'Objects should deep merge')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('Overwrites non-object properties correctly', () =>
|
|
30
|
+
{
|
|
31
|
+
const
|
|
32
|
+
a = { foo: 1 },
|
|
33
|
+
b = { foo: 2 },
|
|
34
|
+
expected = { foo: 2 }
|
|
35
|
+
|
|
36
|
+
deepassign.assign(a, b)
|
|
37
|
+
assert.deepStrictEqual(a, expected, 'Properties should be overwritten')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('Handles undefined values correctly', () =>
|
|
41
|
+
{
|
|
42
|
+
const
|
|
43
|
+
a = { foo: 1 },
|
|
44
|
+
b = { foo: undefined },
|
|
45
|
+
expected = { foo: 1 }
|
|
46
|
+
|
|
47
|
+
deepassign.assign(a, b)
|
|
48
|
+
assert.deepStrictEqual(a, expected, 'Undefined values should not overwrite existing properties')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
suite('Descriptor properties', () =>
|
|
52
|
+
{
|
|
53
|
+
suite('Retains', () =>
|
|
54
|
+
{
|
|
55
|
+
test('non-writable, non-configurable and non-enumarable', () =>
|
|
56
|
+
{
|
|
57
|
+
const a = {}
|
|
58
|
+
|
|
59
|
+
Object.defineProperty(a, 'foo',
|
|
60
|
+
{
|
|
61
|
+
value : 1,
|
|
62
|
+
writable : false,
|
|
63
|
+
configurable : false,
|
|
64
|
+
enumerable : false
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const b = { foo: 2 }
|
|
68
|
+
|
|
69
|
+
deepassign.assign(a, b)
|
|
70
|
+
|
|
71
|
+
const descriptor_a = Object.getOwnPropertyDescriptor(a, 'foo')
|
|
72
|
+
|
|
73
|
+
assert.strictEqual(descriptor_a.value, 1, 'Value of non-writeable property should not be overwritten')
|
|
74
|
+
assert.strictEqual(descriptor_a.writable, false, 'Writable state should remain unchanged')
|
|
75
|
+
assert.strictEqual(descriptor_a.configurable, false, 'Configurable state should remain unchanged')
|
|
76
|
+
assert.strictEqual(descriptor_a.enumerable, false, 'Enumerable state should remain unchanged')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('writable but non-configurable and non-enumarable', () =>
|
|
80
|
+
{
|
|
81
|
+
const a = {}
|
|
82
|
+
|
|
83
|
+
Object.defineProperty(a, 'foo',
|
|
84
|
+
{
|
|
85
|
+
value : 1,
|
|
86
|
+
writable : true,
|
|
87
|
+
configurable : false,
|
|
88
|
+
enumerable : false
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const b = { foo: 2 }
|
|
92
|
+
|
|
93
|
+
deepassign.assign(a, b)
|
|
94
|
+
|
|
95
|
+
const descriptor_a = Object.getOwnPropertyDescriptor(a, 'foo')
|
|
96
|
+
|
|
97
|
+
assert.strictEqual(descriptor_a.value, 2, 'Value of writeable property should be overwritten')
|
|
98
|
+
assert.strictEqual(descriptor_a.writable, true, 'Writable state should remain unchanged')
|
|
99
|
+
assert.strictEqual(descriptor_a.configurable, false, 'Configurable state should remain unchanged')
|
|
100
|
+
assert.strictEqual(descriptor_a.enumerable, false, 'Enumerable state should remain unchanged')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('writable and configurable but non-enumarable', () =>
|
|
104
|
+
{
|
|
105
|
+
const a = {}
|
|
106
|
+
|
|
107
|
+
Object.defineProperty(a, 'foo',
|
|
108
|
+
{
|
|
109
|
+
value : 1,
|
|
110
|
+
writable : true,
|
|
111
|
+
configurable : true,
|
|
112
|
+
enumerable : false
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const b = { foo: 2 }
|
|
116
|
+
|
|
117
|
+
deepassign.assign(a, b)
|
|
118
|
+
|
|
119
|
+
const descriptor_a = Object.getOwnPropertyDescriptor(a, 'foo')
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(descriptor_a.value, 2, 'Value of writeable property should be overwritten')
|
|
122
|
+
assert.strictEqual(descriptor_a.writable, true, 'Writable state should remain unchanged')
|
|
123
|
+
assert.strictEqual(descriptor_a.configurable, true, 'Configurable state should remain unchanged')
|
|
124
|
+
assert.strictEqual(descriptor_a.enumerable, true, 'Enumerable state should change to becouse the property is configurable')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
suite('Assigns', () =>
|
|
129
|
+
{
|
|
130
|
+
test('non-writable, non-configurable and non-enumarable', () =>
|
|
131
|
+
{
|
|
132
|
+
const
|
|
133
|
+
a = { foo: 1 },
|
|
134
|
+
b = {}
|
|
135
|
+
|
|
136
|
+
Object.defineProperty(a, 'foo',
|
|
137
|
+
{
|
|
138
|
+
value : 2,
|
|
139
|
+
writable : false,
|
|
140
|
+
configurable : false,
|
|
141
|
+
enumerable : false
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
deepassign.assign(a, b)
|
|
145
|
+
|
|
146
|
+
const descriptor_a = Object.getOwnPropertyDescriptor(a, 'foo')
|
|
147
|
+
|
|
148
|
+
assert.strictEqual(descriptor_a.value, 2, 'Value should be overwritten')
|
|
149
|
+
assert.strictEqual(descriptor_a.writable, false, 'Writable state should be assigned')
|
|
150
|
+
assert.strictEqual(descriptor_a.configurable, false, 'Configurable state should be assigned')
|
|
151
|
+
assert.strictEqual(descriptor_a.enumerable, false, 'Enumerable state should be assigned')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('Merges nested arrays correctly', () =>
|
|
157
|
+
{
|
|
158
|
+
const
|
|
159
|
+
a = { foo: [1, 2] },
|
|
160
|
+
b = { foo: [2, 3] },
|
|
161
|
+
expected = { foo: [1, 2, 3] }
|
|
162
|
+
|
|
163
|
+
deepassign.assign(a, b)
|
|
164
|
+
assert.deepStrictEqual(a, expected, 'Nested arrays should merge with unique values')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('Merges nested objects correctly', () =>
|
|
168
|
+
{
|
|
169
|
+
const
|
|
170
|
+
a = { foo: { bar: { baz: 1 }}},
|
|
171
|
+
b = { foo: { bar: { qux: 2 }}},
|
|
172
|
+
expected = { foo: { bar: { baz: 1, qux: 2 }}}
|
|
173
|
+
|
|
174
|
+
deepassign.assign(a, b)
|
|
175
|
+
assert.deepStrictEqual(a, expected, 'Nested objects should deep merge')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('Does not alter objects with no conflicts', () =>
|
|
179
|
+
{
|
|
180
|
+
const
|
|
181
|
+
a = { foo: 1 },
|
|
182
|
+
b = { bar: 2 },
|
|
183
|
+
expected = { foo: 1, bar: 2 }
|
|
184
|
+
|
|
185
|
+
deepassign.assign(a, b)
|
|
186
|
+
assert.deepStrictEqual(a, expected, 'Objects without conflicts should merge correctly')
|
|
187
|
+
})
|
|
188
|
+
})
|
package/clone.js
ADDED
package/clone.test.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { suite, test } from 'node:test'
|
|
3
|
+
import deepclone from '@superhero/deep/clone'
|
|
4
|
+
|
|
5
|
+
suite('@superhero/deep/clone', () =>
|
|
6
|
+
{
|
|
7
|
+
test('Clones simple objects', () =>
|
|
8
|
+
{
|
|
9
|
+
const
|
|
10
|
+
obj = { foo: 'bar', baz: 42 },
|
|
11
|
+
result = deepclone.clone(obj),
|
|
12
|
+
legacy = deepclone.clone(obj, true)
|
|
13
|
+
|
|
14
|
+
assert.deepStrictEqual(result, obj, 'Cloned object should be equal to the original')
|
|
15
|
+
assert.deepStrictEqual(legacy, obj, 'Cloned object should be equal to the original (legacy mode)')
|
|
16
|
+
|
|
17
|
+
assert.notStrictEqual(result, obj, 'Cloned object should not be the same reference as the original')
|
|
18
|
+
assert.notStrictEqual(legacy, obj, 'Cloned object should not be the same reference as the original (legacy mode)')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('Clones nested objects', () =>
|
|
22
|
+
{
|
|
23
|
+
const obj = { foo: { bar: { baz: 'qux' } } }
|
|
24
|
+
|
|
25
|
+
const
|
|
26
|
+
result = deepclone.clone(obj),
|
|
27
|
+
legacy = deepclone.clone(obj, true)
|
|
28
|
+
|
|
29
|
+
assert.deepStrictEqual(result, obj, 'Cloned nested object should be equal to the original')
|
|
30
|
+
assert.deepStrictEqual(legacy, obj, 'Cloned nested object should be equal to the original (legacy mode)')
|
|
31
|
+
|
|
32
|
+
assert.notStrictEqual(result.foo, obj.foo, 'Cloned nested object should not share reference with the original')
|
|
33
|
+
assert.notStrictEqual(legacy.foo, obj.foo, 'Cloned nested object should not share reference with the original (legacy mode)')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('Clones arrays', () =>
|
|
37
|
+
{
|
|
38
|
+
const arr = [1, 2, 3, [4, 5]]
|
|
39
|
+
|
|
40
|
+
const
|
|
41
|
+
result = deepclone.clone(arr),
|
|
42
|
+
legacy = deepclone.clone(arr, true)
|
|
43
|
+
|
|
44
|
+
assert.deepStrictEqual(result, arr, 'Cloned array should be equal to the original')
|
|
45
|
+
assert.deepStrictEqual(legacy, arr, 'Cloned array should be equal to the original (legacy mode)')
|
|
46
|
+
|
|
47
|
+
assert.notStrictEqual(result, arr, 'Cloned array should not share reference with the original')
|
|
48
|
+
assert.notStrictEqual(legacy, arr, 'Cloned array should not share reference with the original (legacy mode)')
|
|
49
|
+
|
|
50
|
+
assert.notStrictEqual(result[3], arr[3], 'Nested array in clone should not share reference with the original')
|
|
51
|
+
assert.notStrictEqual(legacy[3], arr[3], 'Nested array in clone should not share reference with the original (legacy mode)')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if(false === !!structuredClone)
|
|
55
|
+
{
|
|
56
|
+
test.skip('Handles circular references (structuredClone not available)')
|
|
57
|
+
test.skip('Clones objects with null prototype (structuredClone not available)')
|
|
58
|
+
}
|
|
59
|
+
else
|
|
60
|
+
{
|
|
61
|
+
test('Handles circular references', () =>
|
|
62
|
+
{
|
|
63
|
+
const obj = {}
|
|
64
|
+
obj.self = obj
|
|
65
|
+
|
|
66
|
+
const result = deepclone.clone(obj)
|
|
67
|
+
|
|
68
|
+
assert.strictEqual(result.self, result, 'Circular references should be preserved in the clone')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('Clones objects with null prototype', () =>
|
|
72
|
+
{
|
|
73
|
+
const obj = Object.create(null)
|
|
74
|
+
obj.foo = 'bar'
|
|
75
|
+
|
|
76
|
+
const result = deepclone.clone(obj)
|
|
77
|
+
|
|
78
|
+
assert.deepEqual(result, obj, 'Cloned object with null prototype should be equal to the original')
|
|
79
|
+
assert.notStrictEqual(result, obj, 'Cloned object with null prototype should not share reference with the original')
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
})
|
package/config.json
ADDED
package/freeze.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default new class DeepFreeze
|
|
2
|
+
{
|
|
3
|
+
freeze(obj)
|
|
4
|
+
{
|
|
5
|
+
const seen = new WeakSet
|
|
6
|
+
this.#freeze(obj, seen)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#freeze(obj, seen)
|
|
10
|
+
{
|
|
11
|
+
const objType = Object.prototype.toString.call(obj)
|
|
12
|
+
|
|
13
|
+
if('[object Array]' === objType
|
|
14
|
+
|| '[object Object]' === objType)
|
|
15
|
+
{
|
|
16
|
+
if(seen.has(obj))
|
|
17
|
+
{
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
else
|
|
21
|
+
{
|
|
22
|
+
seen.add(obj)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for(const key of [...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)])
|
|
26
|
+
{
|
|
27
|
+
this.#freeze(obj[key], seen)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Object.freeze(obj)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/freeze.test.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { suite, test } from 'node:test'
|
|
3
|
+
import deepfreeze from '@superhero/deep/freeze'
|
|
4
|
+
|
|
5
|
+
suite('@superhero/deep/freeze', () =>
|
|
6
|
+
{
|
|
7
|
+
test('Freezes a simple object', () =>
|
|
8
|
+
{
|
|
9
|
+
const obj = { foo: 'bar' }
|
|
10
|
+
|
|
11
|
+
deepfreeze.freeze(obj)
|
|
12
|
+
|
|
13
|
+
assert.throws(() => { obj.foo = 'baz' }, TypeError, 'Should not allow modifying a frozen object')
|
|
14
|
+
assert.strictEqual(Object.isFrozen(obj), true, 'Object should be frozen')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('Freezes nested objects recursively', () =>
|
|
18
|
+
{
|
|
19
|
+
const obj = { foo: { bar: { baz: 'qux' } } }
|
|
20
|
+
|
|
21
|
+
deepfreeze.freeze(obj)
|
|
22
|
+
|
|
23
|
+
assert.throws(() => { obj.foo.bar.baz = 'changed' }, TypeError, 'Should not allow modifying nested properties')
|
|
24
|
+
assert.strictEqual(Object.isFrozen(obj.foo.bar), true, 'Nested object should be frozen')
|
|
25
|
+
assert.strictEqual(Object.isFrozen(obj.foo), true, 'Parent object should be frozen')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('Handles circular references gracefully', () =>
|
|
29
|
+
{
|
|
30
|
+
const obj = {}
|
|
31
|
+
obj.self = obj
|
|
32
|
+
|
|
33
|
+
deepfreeze.freeze(obj)
|
|
34
|
+
|
|
35
|
+
assert.strictEqual(Object.isFrozen(obj), true, 'Object with circular reference should be frozen')
|
|
36
|
+
assert.strictEqual(Object.isFrozen(obj.self), true, 'Circular reference should also be frozen')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('Freezes objects with symbols', () =>
|
|
40
|
+
{
|
|
41
|
+
const sym = Symbol('test')
|
|
42
|
+
const obj = { [sym]: 'value' }
|
|
43
|
+
|
|
44
|
+
deepfreeze.freeze(obj)
|
|
45
|
+
|
|
46
|
+
assert.throws(() => { obj[sym] = 'new value' }, TypeError, 'Should not allow modifying properties with symbols')
|
|
47
|
+
assert.strictEqual(Object.isFrozen(obj), true, 'Object with symbols should be frozen')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('Handles already frozen objects without error', () =>
|
|
51
|
+
{
|
|
52
|
+
const obj = Object.freeze({ foo: 'bar' })
|
|
53
|
+
|
|
54
|
+
deepfreeze.freeze(obj) // Should not throw an error
|
|
55
|
+
assert.strictEqual(Object.isFrozen(obj), true, 'Already frozen object should remain frozen')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('Freezes objects with non-enumerable properties', () =>
|
|
59
|
+
{
|
|
60
|
+
const obj = {}
|
|
61
|
+
Object.defineProperty(obj, 'foo',
|
|
62
|
+
{
|
|
63
|
+
value : 'bar',
|
|
64
|
+
enumerable : false,
|
|
65
|
+
configurable : true,
|
|
66
|
+
writable : true
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
deepfreeze.freeze(obj)
|
|
70
|
+
|
|
71
|
+
assert.throws(() => { obj.foo = 'baz' }, TypeError, 'Should not allow modifying non-enumerable properties')
|
|
72
|
+
assert.strictEqual(Object.isFrozen(obj), true, 'Object with non-enumerable properties should be frozen')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('Freezes arrays', () =>
|
|
76
|
+
{
|
|
77
|
+
const arr = [1, 2, 3]
|
|
78
|
+
|
|
79
|
+
deepfreeze.freeze(arr)
|
|
80
|
+
|
|
81
|
+
assert.throws(() => { arr[0] = 4 }, TypeError, 'Should not allow modifying an array')
|
|
82
|
+
assert.strictEqual(Object.isFrozen(arr), true, 'Array should be frozen')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('Handles objects with null prototype', () =>
|
|
86
|
+
{
|
|
87
|
+
const obj = Object.create(null)
|
|
88
|
+
obj.foo = 'bar'
|
|
89
|
+
|
|
90
|
+
deepfreeze.freeze(obj)
|
|
91
|
+
|
|
92
|
+
assert.throws(() => { obj.foo = 'baz' }, TypeError, 'Should not allow modifying properties of objects with null prototype')
|
|
93
|
+
assert.strictEqual(Object.isFrozen(obj), true, 'Object with null prototype should be frozen')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('Freezes objects with multiple property types', () =>
|
|
97
|
+
{
|
|
98
|
+
const
|
|
99
|
+
sym = Symbol('test'),
|
|
100
|
+
obj =
|
|
101
|
+
{
|
|
102
|
+
[sym] : 'value',
|
|
103
|
+
foo : 'bar',
|
|
104
|
+
nested : { baz: 'qux' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Object.defineProperty(obj, 'nonEnum',
|
|
108
|
+
{
|
|
109
|
+
value : 'hidden',
|
|
110
|
+
enumerable : false,
|
|
111
|
+
configurable : true,
|
|
112
|
+
writable : true
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
deepfreeze.freeze(obj)
|
|
116
|
+
|
|
117
|
+
assert.throws(() => { obj.nested.baz = 'changed' }, TypeError, 'Should not allow modifying nested properties')
|
|
118
|
+
assert.throws(() => { obj[sym] = 'new value' }, TypeError, 'Should not allow modifying symbol properties')
|
|
119
|
+
assert.throws(() => { obj.nonEnum = 'visible' }, TypeError, 'Should not allow modifying non-enumerable properties')
|
|
120
|
+
assert.strictEqual(Object.isFrozen(obj.nested), true, 'Nested object should be frozen')
|
|
121
|
+
assert.strictEqual(Object.isFrozen(obj), true, 'Parent object should be frozen')
|
|
122
|
+
})
|
|
123
|
+
})
|
package/index.js
ADDED
package/merge.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When merging two objects [object Object], a new object is created with the
|
|
3
|
+
* properties of both objects defined. The descriptor of the property in the
|
|
4
|
+
* new object is determined by the descriptors of the properties in the two
|
|
5
|
+
* objects being merged. The priority of the property descriptor is set on the
|
|
6
|
+
* new object according to the more restrictive definition in the two sources.
|
|
7
|
+
*
|
|
8
|
+
* @example if the descriptor of the property in object "a" has "configurable"
|
|
9
|
+
* set to "false", and the descriptor of the property in object "b" has the
|
|
10
|
+
* "configurable" set to "true", the new object will have the descriptor for
|
|
11
|
+
* "configurable" set to "false".
|
|
12
|
+
*
|
|
13
|
+
* @example if the descriptor of the property in object "a" has "enumerable"
|
|
14
|
+
* set to "true", and the descriptor of the property in object "b" has the
|
|
15
|
+
* "enumerable" set to "true", the new object will have the descriptor for
|
|
16
|
+
* "enumerable" set to "true".
|
|
17
|
+
*
|
|
18
|
+
* @example if the descriptor of the property in object "a" has "writable" set
|
|
19
|
+
* to "false", and the descriptor of the property in object "b" has the
|
|
20
|
+
* "writable" set to "false", the new object will have the descriptor for
|
|
21
|
+
* "writable" set to "false".
|
|
22
|
+
*
|
|
23
|
+
* ----------------------------------------------------------------------------
|
|
24
|
+
*
|
|
25
|
+
* When merging two nested objects [object Object], and there is a circular
|
|
26
|
+
* reference, the merge will stop at the circular reference and return the
|
|
27
|
+
* object that contains the circular reference.
|
|
28
|
+
*
|
|
29
|
+
* ----------------------------------------------------------------------------
|
|
30
|
+
*
|
|
31
|
+
* When merging a and b of different types, the value of b is returned.
|
|
32
|
+
*
|
|
33
|
+
* @example if the type of the property in object "a" is object "c", and the
|
|
34
|
+
* type of the property in object "b" is a number, then the value of the
|
|
35
|
+
* property in the new object will be the number.
|
|
36
|
+
*
|
|
37
|
+
* @example if the type of the property in object "a" is a string, and the type
|
|
38
|
+
* of the property in object "b" is object "c", then the value of the property
|
|
39
|
+
* in the new object will be the object "c".
|
|
40
|
+
*
|
|
41
|
+
* ----------------------------------------------------------------------------
|
|
42
|
+
*
|
|
43
|
+
* When merging two arrays [object Array], a new array is created with the
|
|
44
|
+
* unique values of both arrays. The order of the values in the new array is
|
|
45
|
+
* determined by the order of the values in the two arrays being merged.
|
|
46
|
+
*
|
|
47
|
+
* @example if array "a" with values [1, 2, 3] is merged with array "b" with
|
|
48
|
+
* values [2, 3, 4], the new array will have values [1, 2, 3, 4].
|
|
49
|
+
*
|
|
50
|
+
* @example if array "a" with values [2, 3, 4] is merged with array "b" with
|
|
51
|
+
* values [1, 2, 3], the new array will have values [2, 3, 4, 1].
|
|
52
|
+
*
|
|
53
|
+
* @example if array "a" with values [1, 2, 3] is merged with an empty array
|
|
54
|
+
* "b", the new array will still have values [1, 2, 3].
|
|
55
|
+
*
|
|
56
|
+
* @example if array "a" with values [1, 1, 2, 2] is merged with array "b" with
|
|
57
|
+
* values [2, 2, 3, 3], the new array will have values [1, 2, 3].
|
|
58
|
+
*/
|
|
59
|
+
export default new class DeepMerge
|
|
60
|
+
{
|
|
61
|
+
merge(a, b, ...c)
|
|
62
|
+
{
|
|
63
|
+
const
|
|
64
|
+
seen = new WeakSet,
|
|
65
|
+
output = this.#merge(a, b, seen)
|
|
66
|
+
|
|
67
|
+
return c.length
|
|
68
|
+
? this.merge(output, ...c)
|
|
69
|
+
: output
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#merge(a, b, seen)
|
|
73
|
+
{
|
|
74
|
+
if(b === undefined)
|
|
75
|
+
{
|
|
76
|
+
return a
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const
|
|
80
|
+
aType = Object.prototype.toString.call(a),
|
|
81
|
+
bType = Object.prototype.toString.call(b)
|
|
82
|
+
|
|
83
|
+
if('[object Array]' === aType
|
|
84
|
+
&& '[object Array]' === bType)
|
|
85
|
+
{
|
|
86
|
+
return this.#mergeArray(a, b)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if('[object Object]' === aType
|
|
90
|
+
&& '[object Object]' === bType)
|
|
91
|
+
{
|
|
92
|
+
return this.#mergeObject(a, b, seen)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return b
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#mergeArray(a, b)
|
|
99
|
+
{
|
|
100
|
+
return [...new Set(a.concat(b))]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#mergeObject(a, b, seen)
|
|
104
|
+
{
|
|
105
|
+
if(seen.has(a))
|
|
106
|
+
{
|
|
107
|
+
return b
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
seen.add(a)
|
|
111
|
+
|
|
112
|
+
const output = {}
|
|
113
|
+
|
|
114
|
+
for(const key of Object.getOwnPropertyNames(a))
|
|
115
|
+
{
|
|
116
|
+
if(key in b)
|
|
117
|
+
{
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
{
|
|
122
|
+
const descriptor = Object.getOwnPropertyDescriptor(a, key)
|
|
123
|
+
Object.defineProperty(output, key, descriptor)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for(const key of Object.getOwnPropertyNames(b))
|
|
128
|
+
{
|
|
129
|
+
if(key in a)
|
|
130
|
+
{
|
|
131
|
+
const
|
|
132
|
+
descriptor_a = Object.getOwnPropertyDescriptor(a, key),
|
|
133
|
+
descriptor_b = Object.getOwnPropertyDescriptor(b, key)
|
|
134
|
+
|
|
135
|
+
Object.defineProperty(output, key,
|
|
136
|
+
{
|
|
137
|
+
configurable : descriptor_a.configurable && descriptor_b.configurable,
|
|
138
|
+
enumerable : descriptor_a.enumerable && descriptor_b.enumerable,
|
|
139
|
+
writable : descriptor_a.writable && descriptor_b.writable,
|
|
140
|
+
value : this.#merge(a[key], b[key], seen)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
{
|
|
145
|
+
const descriptor = Object.getOwnPropertyDescriptor(b, key)
|
|
146
|
+
Object.defineProperty(output, key, descriptor)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return output
|
|
151
|
+
}
|
|
152
|
+
}
|
package/merge.test.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { suite, test } from 'node:test'
|
|
3
|
+
import deepmerge from '@superhero/deep/merge'
|
|
4
|
+
|
|
5
|
+
suite('@superhero/deep/merge', () =>
|
|
6
|
+
{
|
|
7
|
+
test('Merges arrays with unique values', () =>
|
|
8
|
+
{
|
|
9
|
+
const
|
|
10
|
+
a = [1, 2, 3],
|
|
11
|
+
b = [2, 3, 4],
|
|
12
|
+
expected = [1, 2, 3, 4]
|
|
13
|
+
|
|
14
|
+
const result = deepmerge.merge(a, b)
|
|
15
|
+
assert.deepStrictEqual(result, expected, 'Arrays should merge with unique values')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('Merges arrays with order preserved', () =>
|
|
19
|
+
{
|
|
20
|
+
const
|
|
21
|
+
a = [2, 3, 4],
|
|
22
|
+
b = [1, 2, 3],
|
|
23
|
+
expected = [2, 3, 4, 1]
|
|
24
|
+
|
|
25
|
+
const result = deepmerge.merge(a, b)
|
|
26
|
+
assert.deepStrictEqual(result, expected, 'Order of values should be preserved')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('Handles empty arrays correctly', () =>
|
|
30
|
+
{
|
|
31
|
+
const
|
|
32
|
+
a = [1, 2, 3],
|
|
33
|
+
b = [],
|
|
34
|
+
expected = [1, 2, 3]
|
|
35
|
+
|
|
36
|
+
const result = deepmerge.merge(a, b)
|
|
37
|
+
assert.deepStrictEqual(result, expected, 'Merging with empty array should not alter values')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('Handles arrays with duplicate values', () =>
|
|
41
|
+
{
|
|
42
|
+
const
|
|
43
|
+
a = [1, 1, 2, 2],
|
|
44
|
+
b = [2, 2, 3, 3],
|
|
45
|
+
expected = [1, 2, 3]
|
|
46
|
+
|
|
47
|
+
const result = deepmerge.merge(a, b)
|
|
48
|
+
assert.deepStrictEqual(result, expected, 'Duplicate values should be removed')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('Merges objects and prioritizes restrictive descriptors', () =>
|
|
52
|
+
{
|
|
53
|
+
const
|
|
54
|
+
a = {},
|
|
55
|
+
b = {}
|
|
56
|
+
|
|
57
|
+
Object.defineProperty(a, 'foo',
|
|
58
|
+
{
|
|
59
|
+
value : 1,
|
|
60
|
+
writable : true,
|
|
61
|
+
configurable : false,
|
|
62
|
+
enumerable : true
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
Object.defineProperty(b, 'foo',
|
|
66
|
+
{
|
|
67
|
+
value : 2,
|
|
68
|
+
writable : false,
|
|
69
|
+
configurable : true,
|
|
70
|
+
enumerable : true
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const
|
|
74
|
+
result = deepmerge.merge(a, b),
|
|
75
|
+
descriptor = Object.getOwnPropertyDescriptor(result, 'foo')
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(descriptor.value, 2, 'Value should prioritize the second object')
|
|
78
|
+
assert.strictEqual(descriptor.writable, false, 'Writable state should reflect the more restrictive descriptor')
|
|
79
|
+
assert.strictEqual(descriptor.configurable, false, 'Configurable state should reflect the more restrictive descriptor')
|
|
80
|
+
assert.strictEqual(descriptor.enumerable, true, 'Enumerable state should remain unchanged')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('Merges objects with non-enumerable properties', () =>
|
|
84
|
+
{
|
|
85
|
+
const
|
|
86
|
+
a = {},
|
|
87
|
+
b = {}
|
|
88
|
+
|
|
89
|
+
Object.defineProperty(a, 'foo',
|
|
90
|
+
{
|
|
91
|
+
value : 1,
|
|
92
|
+
writable : true,
|
|
93
|
+
configurable : true,
|
|
94
|
+
enumerable : false
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
Object.defineProperty(b, 'foo',
|
|
98
|
+
{
|
|
99
|
+
value : 2,
|
|
100
|
+
writable : false,
|
|
101
|
+
configurable : false,
|
|
102
|
+
enumerable : false
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const
|
|
106
|
+
result = deepmerge.merge(a, b),
|
|
107
|
+
descriptor = Object.getOwnPropertyDescriptor(result, 'foo')
|
|
108
|
+
|
|
109
|
+
assert.strictEqual(descriptor.value, 2, 'Value should prioritize the second object')
|
|
110
|
+
assert.strictEqual(descriptor.writable, false, 'Writable state should reflect the more restrictive descriptor')
|
|
111
|
+
assert.strictEqual(descriptor.configurable, false, 'Configurable state should reflect the more restrictive descriptor')
|
|
112
|
+
assert.strictEqual(descriptor.enumerable, false, 'Enumerable state should remain unchanged')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('Handles nested object merging', () =>
|
|
116
|
+
{
|
|
117
|
+
const
|
|
118
|
+
a = { foo: { bar: 1 } },
|
|
119
|
+
b = { foo: { baz: 2 } },
|
|
120
|
+
expected = { foo: { bar: 1, baz: 2 } }
|
|
121
|
+
|
|
122
|
+
const result = deepmerge.merge(a, b)
|
|
123
|
+
assert.deepStrictEqual(result, expected, 'Nested objects should merge correctly')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('Stops at circular references', () =>
|
|
127
|
+
{
|
|
128
|
+
const
|
|
129
|
+
a = {},
|
|
130
|
+
b = {}
|
|
131
|
+
|
|
132
|
+
a.self = a
|
|
133
|
+
b.self = b
|
|
134
|
+
|
|
135
|
+
const result = deepmerge.merge(a, b)
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(result.self, b.self, 'Circular references should not merge further')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('Stops when nested and with circular references', () =>
|
|
141
|
+
{
|
|
142
|
+
const
|
|
143
|
+
a = { foo: { bar: { foo: { bar: 'baz' } } } },
|
|
144
|
+
b = { foo: {} }
|
|
145
|
+
|
|
146
|
+
b.foo.bar = b
|
|
147
|
+
|
|
148
|
+
const
|
|
149
|
+
resultA = deepmerge.merge(a, b),
|
|
150
|
+
resultB = deepmerge.merge(b, a)
|
|
151
|
+
|
|
152
|
+
assert.strictEqual(resultA.foo.bar.foo.bar, b, 'Circular references should not interfare with the merged result')
|
|
153
|
+
assert.strictEqual(resultB.foo.bar.foo.bar, 'baz', 'Circular references should not interfare with the merged result')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('Returns second value for non-object types', () =>
|
|
157
|
+
{
|
|
158
|
+
const
|
|
159
|
+
a = { foo: 'string' },
|
|
160
|
+
b = { foo: 42 },
|
|
161
|
+
expected = { foo: 42 }
|
|
162
|
+
|
|
163
|
+
const result = deepmerge.merge(a, b)
|
|
164
|
+
assert.deepStrictEqual(result, expected, 'Non-object types should replace with the second value')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('Handles multiple merges sequentially', () =>
|
|
168
|
+
{
|
|
169
|
+
const
|
|
170
|
+
a = { foo: 1 },
|
|
171
|
+
b = { bar: 2 },
|
|
172
|
+
c = { baz: 3 },
|
|
173
|
+
expected = { foo: 1, bar: 2, baz: 3 }
|
|
174
|
+
|
|
175
|
+
const resultA = deepmerge.merge(a, b, c)
|
|
176
|
+
assert.deepStrictEqual(resultA, expected, 'Multiple objects should merge sequentially')
|
|
177
|
+
|
|
178
|
+
const resultB = deepmerge.merge(a, b, undefined, c)
|
|
179
|
+
assert.deepStrictEqual(resultB, expected, 'Ignore undefined attributes')
|
|
180
|
+
})
|
|
181
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superhero/deep",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "A collection of deep structure operations",
|
|
5
|
+
"keywords": ["deep", "assign", "clone", "freeze", "merge"],
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./index.js",
|
|
11
|
+
"./*": "./*.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --trace-warnings --test --experimental-test-coverage"
|
|
15
|
+
},
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Erik Landvall",
|
|
18
|
+
"email": "erik@landvall.se"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/superhero/deep"
|
|
23
|
+
}
|
|
24
|
+
}
|