@stackline/deepmerge 1.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/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes are documented here. This project follows Semantic
4
+ Versioning.
5
+
6
+ ## [1.0.0] - 2026-08-16
7
+
8
+ ### Added
9
+
10
+ - Immutable deep object and array merge with zero runtime dependencies.
11
+ - Default API compatibility with `deepmerge` v4, including `all`,
12
+ `arrayMerge`, `customMerge`, `clone`, and `isMergeableObject`.
13
+ - Prototype-pollution protection for `__proto__`, `prototype`, and
14
+ `constructor` in target and source objects at every depth.
15
+ - Optional strict unsafe-key rejection through `onUnsafeKey: 'throw'`.
16
+ - Circular and shared-reference preservation.
17
+ - Configurable `maxDepth` and `maxKeys` traversal limits.
18
+ - ESM, callable CommonJS, browser-global, and source-map distributions.
19
+ - TypeScript declarations tested from 3.9 through 7.0.
20
+ - Differential compatibility tests against `deepmerge@4.3.1`.
21
+ - Public documentation, security policy, CI matrix, and live playground.
22
+
23
+ [1.0.0]: https://github.com/alexandroit/stackline-deepmerge/releases/tag/v1.0.0
@@ -0,0 +1,51 @@
1
+ # Contributing
2
+
3
+ Thank you for improving `@stackline/deepmerge`.
4
+
5
+ ## Development
6
+
7
+ Use Node.js 20 or newer for the development toolchain. The published runtime
8
+ is separately tested on older supported Node.js versions.
9
+
10
+ ```bash
11
+ npm ci
12
+ npm test
13
+ npm run test:attw
14
+ npm run benchmark
15
+ ```
16
+
17
+ ## Pull requests
18
+
19
+ Keep each change focused. A pull request should include:
20
+
21
+ - a clear behavior statement;
22
+ - tests for new or changed behavior;
23
+ - compatibility impact;
24
+ - security impact when object traversal changes;
25
+ - documentation for public API changes.
26
+
27
+ Do not weaken dangerous-key filtering, cycle handling, or resource limits to
28
+ gain benchmark throughput.
29
+
30
+ ## Compatibility changes
31
+
32
+ The default API intentionally tracks documented `deepmerge` v4 behavior.
33
+ Before changing merge semantics:
34
+
35
+ 1. add a focused local test;
36
+ 2. add or update a differential compatibility case;
37
+ 3. document intentional differences;
38
+ 4. validate direct and npm-alias installation shapes.
39
+
40
+ ## Security changes
41
+
42
+ Security fixes should not be discussed in a public issue before a patched
43
+ release exists. Follow [SECURITY.md](SECURITY.md).
44
+
45
+ ## Commits
46
+
47
+ Use a short imperative subject. Keep generated output and unrelated formatting
48
+ out of the same commit.
49
+
50
+ By contributing, you agree that your contribution is licensed under the MIT
51
+ License.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandro Paixao Marques and contributors
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/NOTICE ADDED
@@ -0,0 +1,14 @@
1
+ @stackline/deepmerge
2
+ Copyright (c) 2026 Alexandro Paixao Marques and contributors
3
+
4
+ This package is an independent implementation. Its public behavior is designed
5
+ to support migration from deepmerge version 4, a separate MIT-licensed project
6
+ by James Halliday, Josh Duff, and other contributors:
7
+
8
+ https://github.com/TehShrike/deepmerge
9
+
10
+ The upstream package is used as a development-only behavioral reference in the
11
+ differential compatibility tests. It is not included as a runtime dependency.
12
+
13
+ The name "deepmerge" is used descriptively for API compatibility. This project
14
+ is not affiliated with or endorsed by the upstream maintainers.
package/README.md ADDED
@@ -0,0 +1,311 @@
1
+ # @stackline/deepmerge
2
+
3
+ > Secure, immutable, zero-dependency deep merge for modern JavaScript, with a
4
+ > `deepmerge`-compatible API.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@stackline/deepmerge.svg?style=flat-square)](https://www.npmjs.com/package/@stackline/deepmerge)
7
+ [![npm downloads](https://img.shields.io/npm/dm/@stackline/deepmerge.svg?style=flat-square)](https://www.npmjs.com/package/@stackline/deepmerge)
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/alexandroit/stackline-deepmerge/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/alexandroit/stackline-deepmerge/actions/workflows/ci.yml)
9
+ [![license](https://img.shields.io/npm/l/@stackline/deepmerge.svg?style=flat-square)](LICENSE)
10
+ [![zero dependencies](https://img.shields.io/badge/runtime%20dependencies-0-159957?style=flat-square)](package.json)
11
+
12
+ **[Docs and playground](https://alexandro.net/docs/vanilla/deepmerge/)** |
13
+ **[npm](https://www.npmjs.com/package/@stackline/deepmerge)** |
14
+ **[Security](SECURITY.md)** |
15
+ **[Changelog](CHANGELOG.md)** |
16
+ **[Issues](https://github.com/alexandroit/stackline-deepmerge/issues)**
17
+
18
+ ## Why this package?
19
+
20
+ Deep merge sits on a trust boundary in configuration loaders, build tools,
21
+ servers, CLIs, and browser applications. A useful replacement must be safe for
22
+ untrusted object keys without forcing existing projects to rewrite every merge.
23
+
24
+ `@stackline/deepmerge` combines:
25
+
26
+ - rejection of `__proto__`, `prototype`, and `constructor` at every depth;
27
+ - immutable merges with cycle and shared-reference preservation;
28
+ - configurable depth and key limits for hostile or malformed inputs;
29
+ - the familiar `deepmerge` v4 default API and extension hooks;
30
+ - ESM, callable CommonJS, TypeScript, and browser builds;
31
+ - TypeScript compatibility tested from 3.9 through 7.0;
32
+ - zero runtime dependencies.
33
+
34
+ ## Installation
35
+
36
+ Install under the package's public name:
37
+
38
+ ```bash
39
+ npm install @stackline/deepmerge
40
+ ```
41
+
42
+ Or replace `deepmerge` without changing application imports:
43
+
44
+ ```bash
45
+ npm install deepmerge@npm:@stackline/deepmerge
46
+ ```
47
+
48
+ Existing code can continue to use:
49
+
50
+ ```js
51
+ import merge from 'deepmerge';
52
+
53
+ const config = merge(defaults, environment);
54
+ ```
55
+
56
+ ## Quick start
57
+
58
+ ```js
59
+ import merge from '@stackline/deepmerge';
60
+
61
+ const defaults = {
62
+ server: { port: 3000, headers: { accept: 'application/json' } },
63
+ plugins: ['core']
64
+ };
65
+
66
+ const production = {
67
+ server: { port: 8080, headers: { authorization: 'Bearer token' } },
68
+ plugins: ['metrics']
69
+ };
70
+
71
+ const config = merge(defaults, production);
72
+
73
+ // {
74
+ // server: {
75
+ // port: 8080,
76
+ // headers: {
77
+ // accept: 'application/json',
78
+ // authorization: 'Bearer token'
79
+ // }
80
+ // },
81
+ // plugins: ['core', 'metrics']
82
+ // }
83
+ ```
84
+
85
+ Neither input is mutated.
86
+
87
+ ## Secure by default
88
+
89
+ Dangerous keys are skipped from both inputs before their values are read:
90
+
91
+ ```js
92
+ import merge from '@stackline/deepmerge';
93
+
94
+ const payload = JSON.parse(`{
95
+ "profile": {
96
+ "name": "Ada",
97
+ "constructor": {
98
+ "prototype": { "isAdmin": true }
99
+ }
100
+ }
101
+ }`);
102
+
103
+ const result = merge({}, payload);
104
+
105
+ console.log(result); // { profile: { name: 'Ada' } }
106
+ console.log(Object.prototype.isAdmin); // undefined
107
+ ```
108
+
109
+ Use strict rejection when silent filtering is not appropriate:
110
+
111
+ ```js
112
+ merge({}, payload, { onUnsafeKey: 'throw' });
113
+ // UnsafeKeyError: Refusing to merge unsafe key constructor at
114
+ // <root>.profile.constructor
115
+ ```
116
+
117
+ Security limits are enabled by default:
118
+
119
+ ```js
120
+ merge(target, source, {
121
+ maxDepth: 1000,
122
+ maxKeys: 100000
123
+ });
124
+ ```
125
+
126
+ Set a smaller limit at an exposed API boundary. `Infinity` is accepted when
127
+ the input is already trusted.
128
+
129
+ ## API compatibility
130
+
131
+ ### `merge(target, source, options?)`
132
+
133
+ Returns a new merged value. Objects merge recursively. Arrays concatenate by
134
+ default. When an array and object occupy the same position, the source wins.
135
+
136
+ ### `merge.all(objects, options?)`
137
+
138
+ ```js
139
+ const config = merge.all([
140
+ { logging: { level: 'info' } },
141
+ { logging: { format: 'json' } },
142
+ { region: 'ca-central-1' }
143
+ ]);
144
+ ```
145
+
146
+ ### Options
147
+
148
+ | Option | Default | Purpose |
149
+ | :--- | :--- | :--- |
150
+ | `arrayMerge` | concatenate | Replace or customize array behavior |
151
+ | `clone` | `true` | Set `false` to preserve nested input references |
152
+ | `customMerge` | none | Select a merge function for a property |
153
+ | `isMergeableObject` | built in | Decide which values can be traversed |
154
+ | `onUnsafeKey` | `"skip"` | Skip or throw on dangerous keys |
155
+ | `maxDepth` | `1000` | Bound recursive traversal |
156
+ | `maxKeys` | `100000` | Bound enumerable object keys per merge |
157
+
158
+ The callback options include `cloneUnlessOtherwiseSpecified`, matching the
159
+ extension-hook shape used by `deepmerge` v4.
160
+
161
+ ### Named exports
162
+
163
+ ```js
164
+ import merge, {
165
+ DeepMergeLimitError,
166
+ UnsafeKeyError,
167
+ all,
168
+ deepmerge,
169
+ isMergeableObject
170
+ } from '@stackline/deepmerge';
171
+ ```
172
+
173
+ CommonJS remains callable:
174
+
175
+ ```js
176
+ const merge = require('@stackline/deepmerge');
177
+
178
+ merge({ left: true }, { right: true });
179
+ merge.all([{ one: 1 }, { two: 2 }]);
180
+ ```
181
+
182
+ ## Array strategies
183
+
184
+ Overwrite arrays:
185
+
186
+ ```js
187
+ const overwrite = (_target, source) => source;
188
+ const result = merge([1, 2], [3], { arrayMerge: overwrite });
189
+ // [3]
190
+ ```
191
+
192
+ Merge arrays by index:
193
+
194
+ ```js
195
+ const byIndex = (target, source, options) => {
196
+ const output = target.slice();
197
+
198
+ source.forEach((value, index) => {
199
+ output[index] = index in output
200
+ ? merge(output[index], value, options)
201
+ : options.cloneUnlessOtherwiseSpecified(value, options);
202
+ });
203
+
204
+ return output;
205
+ };
206
+ ```
207
+
208
+ ## Cycles and shared references
209
+
210
+ Circular and repeated references are preserved instead of overflowing the
211
+ stack or being duplicated unexpectedly:
212
+
213
+ ```js
214
+ const shared = { enabled: true };
215
+ const source = { first: shared, second: shared };
216
+ source.self = source;
217
+
218
+ const result = merge({}, source);
219
+
220
+ result.first === result.second; // true
221
+ result.self === result; // true
222
+ ```
223
+
224
+ ## TypeScript
225
+
226
+ The package ships declaration files for modern ESM, CommonJS, and older
227
+ TypeScript resolvers. Return types recursively combine the target and source.
228
+
229
+ ```ts
230
+ import merge from '@stackline/deepmerge';
231
+
232
+ const result = merge(
233
+ { service: { port: 3000 } },
234
+ { service: { secure: true } }
235
+ );
236
+
237
+ result.service.port; // number
238
+ result.service.secure; // boolean
239
+ ```
240
+
241
+ The release matrix tests TypeScript `3.9`, `4.7`, `4.9`, `5.9`, `6.0`, and
242
+ `7.0`. The JavaScript runtime supports Node.js `14.17` and newer.
243
+
244
+ ## Browser
245
+
246
+ Use the ESM build with a bundler, or load the small browser global directly:
247
+
248
+ ```html
249
+ <script src="https://unpkg.com/@stackline/deepmerge@1.0.0/dist/index.min.js"></script>
250
+ <script>
251
+ const merged = StacklineDeepmerge(
252
+ { theme: { contrast: 'normal' } },
253
+ { theme: { motion: 'reduced' } }
254
+ );
255
+ </script>
256
+ ```
257
+
258
+ ## Migration from `deepmerge`
259
+
260
+ The lowest-change migration uses an npm alias:
261
+
262
+ ```bash
263
+ npm uninstall deepmerge
264
+ npm install deepmerge@npm:@stackline/deepmerge
265
+ ```
266
+
267
+ The compatibility suite covers documented options and 5,000 deterministic,
268
+ JSON-compatible differential cases against `deepmerge@4.3.1`.
269
+
270
+ Intentional hardening differences:
271
+
272
+ - dangerous keys are always rejected or skipped;
273
+ - cycles are preserved;
274
+ - traversal limits are enabled by default;
275
+ - invalid option values fail early with a controlled error.
276
+
277
+ See [Compatibility](docs/COMPATIBILITY.md) for the full contract.
278
+
279
+ ## Performance
280
+
281
+ Security checks, cycle tracking, and resource limits add measurable work. The
282
+ included benchmark compares this package with `deepmerge@4.3.1` on the same
283
+ process:
284
+
285
+ ```bash
286
+ npm run benchmark
287
+ ```
288
+
289
+ Use benchmark results as regression signals, not universal claims. Runtime,
290
+ CPU, input shape, and custom callbacks materially affect throughput.
291
+
292
+ ## Trust and maintenance
293
+
294
+ - No runtime dependencies.
295
+ - Every release is built from the public repository.
296
+ - CI validates behavior, types, package exports, clean installs, and supported
297
+ runtimes.
298
+ - Security reports have a dedicated private process in [SECURITY.md](SECURITY.md).
299
+ - Release history is recorded in [CHANGELOG.md](CHANGELOG.md).
300
+
301
+ ## Contributing
302
+
303
+ Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Changes
304
+ to compatibility or security behavior require focused regression tests.
305
+
306
+ ## License
307
+
308
+ MIT. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
309
+
310
+ `@stackline/deepmerge` is an independent project and is not affiliated with or
311
+ endorsed by the maintainers of the `deepmerge` package.
package/SECURITY.md ADDED
@@ -0,0 +1,50 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ | Version | Security updates |
6
+ | :--- | :---: |
7
+ | `1.x` | Yes |
8
+ | `< 1.0.0` | No public releases |
9
+
10
+ ## Reporting a vulnerability
11
+
12
+ Do not open a public issue for a suspected vulnerability.
13
+
14
+ Use the repository's
15
+ [private security advisory form](https://github.com/alexandroit/stackline-deepmerge/security/advisories/new).
16
+
17
+ Include:
18
+
19
+ - the affected version;
20
+ - a minimal reproducer or malicious input;
21
+ - the observed impact;
22
+ - the runtime and module system;
23
+ - any proposed mitigation.
24
+
25
+ We aim to acknowledge complete reports within three business days. Confirmed
26
+ issues are fixed privately, covered by regression tests, and disclosed after a
27
+ patched release is available.
28
+
29
+ ## Security contract
30
+
31
+ The default merge path:
32
+
33
+ - rejects `__proto__`, `prototype`, and `constructor` at every object depth;
34
+ - checks both target and source keys;
35
+ - does not read values behind rejected unsafe keys;
36
+ - copies only own enumerable string and symbol keys;
37
+ - writes accepted keys as data properties;
38
+ - bounds depth and enumerable key traversal;
39
+ - tracks circular and shared references.
40
+
41
+ Use `{ onUnsafeKey: 'throw' }` when an unsafe key must reject the entire
42
+ operation instead of being filtered.
43
+
44
+ ## Trust boundary
45
+
46
+ Options and callbacks are executable application code. Do not accept
47
+ `arrayMerge`, `customMerge`, or `isMergeableObject` functions from untrusted
48
+ input. Safe property getters may execute when their values are merged, just as
49
+ normal JavaScript property access does. Convert hostile wire formats with a
50
+ trusted parser and use strict input limits at exposed boundaries.