@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 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
@@ -0,0 +1,9 @@
1
+ export default new class DeepClone
2
+ {
3
+ clone(a, legacy = false)
4
+ {
5
+ return structuredClone && false === legacy
6
+ ? structuredClone(a)
7
+ : JSON.parse(JSON.stringify(a))
8
+ }
9
+ }
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
@@ -0,0 +1,10 @@
1
+ {
2
+ "locator":
3
+ {
4
+ "@superhero/deep" : true,
5
+ "@superhero/deep/assign" : true,
6
+ "@superhero/deep/clone" : true,
7
+ "@superhero/deep/freeze" : true,
8
+ "@superhero/deep/merge" : true
9
+ }
10
+ }
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
@@ -0,0 +1,4 @@
1
+ export * from '@superhero/deep/assign'
2
+ export * from '@superhero/deep/clone'
3
+ export * from '@superhero/deep/freeze'
4
+ export * from '@superhero/deep/merge'
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
+ }