@stackline/deepmerge 1.0.0 → 1.0.1
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 +19 -0
- package/README.md +12 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.min.js +1 -1
- package/docs/ADOPTION.md +48 -0
- package/docs/ARCHITECTURE.md +60 -0
- package/docs/BENCHMARKS.md +25 -0
- package/docs/COMPATIBILITY.md +72 -0
- package/docs/MARKET_RESEARCH.md +110 -0
- package/docs/RELEASING.md +53 -0
- package/examples/array-strategy.mjs +17 -0
- package/examples/cyclic-graph.mjs +14 -0
- package/examples/secure-config.mjs +22 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes are documented here. This project follows Semantic
|
|
4
4
|
Versioning.
|
|
5
5
|
|
|
6
|
+
## [1.0.1] - 2026-08-21
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Executable adoption examples for hostile configuration input, custom array
|
|
11
|
+
strategies, cycles, and shared references.
|
|
12
|
+
- Direct-install and npm-alias migration guide.
|
|
13
|
+
- Reproducible benchmark methodology and public Stackline catalog link.
|
|
14
|
+
- First-party documentation analytics that never records playground input.
|
|
15
|
+
- Trusted-publishing workflow for provenance-enabled future releases.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Package tarballs now include the public guides and executable examples.
|
|
20
|
+
- CI resolves release tarballs dynamically instead of hardcoding a version.
|
|
21
|
+
|
|
22
|
+
No runtime API or declaration behavior changed in this release.
|
|
23
|
+
|
|
6
24
|
## [1.0.0] - 2026-08-16
|
|
7
25
|
|
|
8
26
|
### Added
|
|
@@ -21,3 +39,4 @@ Versioning.
|
|
|
21
39
|
- Public documentation, security policy, CI matrix, and live playground.
|
|
22
40
|
|
|
23
41
|
[1.0.0]: https://github.com/alexandroit/stackline-deepmerge/releases/tag/v1.0.0
|
|
42
|
+
[1.0.1]: https://github.com/alexandroit/stackline-deepmerge/compare/v1.0.0...v1.0.1
|
package/README.md
CHANGED
|
@@ -246,7 +246,7 @@ The release matrix tests TypeScript `3.9`, `4.7`, `4.9`, `5.9`, `6.0`, and
|
|
|
246
246
|
Use the ESM build with a bundler, or load the small browser global directly:
|
|
247
247
|
|
|
248
248
|
```html
|
|
249
|
-
<script src="https://unpkg.com/@stackline/deepmerge@1
|
|
249
|
+
<script src="https://unpkg.com/@stackline/deepmerge@1/dist/index.min.js"></script>
|
|
250
250
|
<script>
|
|
251
251
|
const merged = StacklineDeepmerge(
|
|
252
252
|
{ theme: { contrast: 'normal' } },
|
|
@@ -289,6 +289,17 @@ npm run benchmark
|
|
|
289
289
|
Use benchmark results as regression signals, not universal claims. Runtime,
|
|
290
290
|
CPU, input shape, and custom callbacks materially affect throughput.
|
|
291
291
|
|
|
292
|
+
## Adoption resources
|
|
293
|
+
|
|
294
|
+
- [Direct install and drop-in alias guide](docs/ADOPTION.md)
|
|
295
|
+
- [Reproducible benchmark methodology](docs/BENCHMARKS.md)
|
|
296
|
+
- [Executable examples](examples)
|
|
297
|
+
- [Stackline open-source catalog](https://alexandro.net/docs/open-source/)
|
|
298
|
+
|
|
299
|
+
The examples are included in the npm tarball and run against the package's
|
|
300
|
+
public exports. They cover hostile configuration input, custom array strategy,
|
|
301
|
+
cycles, and shared references.
|
|
302
|
+
|
|
292
303
|
## Trust and maintenance
|
|
293
304
|
|
|
294
305
|
- No runtime dependencies.
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/index.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @stackline/deepmerge v1.0.
|
|
1
|
+
/*! @stackline/deepmerge v1.0.1 | MIT */
|
|
2
2
|
var StacklineDeepmergeModule=(()=>{var h=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var N=(e,n)=>{for(var r in n)h(e,r,{get:n[r],enumerable:!0})},V=(e,n,r,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of W(n))!L.call(e,t)&&t!==r&&h(e,t,{get:()=>n[t],enumerable:!(o=C(n,t))||o.enumerable});return e};var z=e=>V(h({},"__esModule",{value:!0}),e);var ne={};N(ne,{DeepMergeLimitError:()=>p,UnsafeKeyError:()=>a,all:()=>P,deepmerge:()=>y,default:()=>ee,isMergeableObject:()=>M});var G=Object.prototype.toString,F=Object.prototype.hasOwnProperty,k=Object.prototype.propertyIsEnumerable,J=new Set(["__proto__","prototype","constructor"]),Y=typeof Symbol=="function"&&typeof Symbol.for=="function"?Symbol.for("react.element"):60103,a=class extends TypeError{constructor(n,r){let o=$(r.concat(n));super(`Refusing to merge unsafe key ${String(n)} at ${o}`),this.name="UnsafeKeyError",this.code="ERR_DEEPMERGE_UNSAFE_KEY",this.key=n,this.path=o}},p=class extends RangeError{constructor(n,r,o){let t=$(o);super(`Deep merge ${n} limit of ${r} exceeded at ${t}`),this.name="DeepMergeLimitError",this.code="ERR_DEEPMERGE_LIMIT",this.kind=n,this.limit=r,this.path=t}};function M(e){if(!e||typeof e!="object")return!1;let n=G.call(e);return n==="[object Date]"||n==="[object RegExp]"?!1:e.$$typeof!==Y}function $(e){if(e.length===0)return"<root>";let n="<root>";for(let r of e)typeof r=="number"?n+=`[${r}]`:typeof r=="symbol"?n+=`[${String(r)}]`:/^[A-Za-z_$][\w$]*$/.test(r)?n+=`.${r}`:n+=`[${JSON.stringify(r)}]`;return n}function A(e,n,r){return e.concat(n).map(o=>r.cloneUnlessOtherwiseSpecified(o,r))}function O(e,n,r){let o=e===void 0?n:e;if(o===1/0)return o;if(!Number.isSafeInteger(o)||o<0)throw new TypeError(`${r} must be a non-negative safe integer or Infinity`);return o}function I(e){if(e!==void 0&&(e===null||typeof e!="object"))throw new TypeError("options must be an object when provided");let n=e||{},r=n.onUnsafeKey===void 0?"skip":n.onUnsafeKey;if(r!=="skip"&&r!=="throw")throw new TypeError("onUnsafeKey must be either 'skip' or 'throw'");let o={cloneMemo:new WeakMap,pairMemo:new WeakMap,keyCount:0,callbackDepth:0,options:null},t={...n,arrayMerge:n.arrayMerge===void 0?A:n.arrayMerge,isMergeableObject:n.isMergeableObject===void 0?M:n.isMergeableObject,maxDepth:O(n.maxDepth,1e3,"maxDepth"),maxKeys:O(n.maxKeys,1e5,"maxKeys"),onUnsafeKey:r};if(typeof t.arrayMerge!="function")throw new TypeError("arrayMerge must be a function");if(typeof t.isMergeableObject!="function")throw new TypeError("isMergeableObject must be a function");if(t.customMerge!==void 0&&typeof t.customMerge!="function")throw new TypeError("customMerge must be a function");return t.cloneUnlessOtherwiseSpecified=(f,i)=>{if(i&&i!==t){let u=I(i);return l(f,u,0,[])}return l(f,o,o.callbackDepth+1,[])},o.options=t,o}function m(e){return e!==null&&(typeof e=="object"||typeof e=="function")}function U(e,n,r){if(!m(n)||!m(r))return;let o=e.pairMemo.get(n);return o&&o.get(r)}function T(e,n,r,o){if(!m(n)||!m(r))return;let t=e.pairMemo.get(n);t||(t=new WeakMap,e.pairMemo.set(n,t)),t.set(r,o)}function g(e,n,r){m(n)&&e.cloneMemo.set(n,r)}function Z(e,n,r){if(n>e.options.maxDepth)throw new p("depth",e.options.maxDepth,r)}function q(e,n,r){if(e.keyCount+=n,e.keyCount>e.options.maxKeys)throw new p("key",e.options.maxKeys,r)}function x(e){let n=Object.keys(e);return typeof Object.getOwnPropertySymbols!="function"?n:n.concat(Object.getOwnPropertySymbols(Object(e)).filter(r=>k.call(e,r)))}function B(e){return typeof e=="string"&&J.has(e)}function D(e,n,r){if(!B(e))return!1;if(n.options.onUnsafeKey==="throw")throw new a(e,r);return!0}function _(e,n){try{return n in e}catch(r){return!1}}function H(e,n){return _(e,n)&&!(F.call(e,n)&&k.call(e,n))}function K(e,n,r){Object.defineProperty(e,n,{configurable:!0,enumerable:!0,value:r,writable:!0})}function l(e,n,r,o){if(n.options.clone===!1||!n.options.isMergeableObject(e))return e;let t=n.cloneMemo.get(e);return t!==void 0?t:w(Array.isArray(e)?[]:{},e,n,r,o)}function Q(e,n,r,o,t){if(r.options.arrayMerge!==A){let s=r.callbackDepth;r.callbackDepth=o;try{return r.options.arrayMerge(e,n,r.options)}finally{r.callbackDepth=s}}let f=U(r,e,n);if(f!==void 0)return f;let i=[];T(r,e,n,i),g(r,e,i),g(r,n,i);let u=e.concat(n);i.length=u.length;for(let s=0;s<u.length;s+=1)s in u&&(i[s]=l(u[s],r,o+1,t.concat(s)));return i}function X(e,n){if(!n.options.customMerge)return;let r=n.options.customMerge(e,n.options);return typeof r=="function"?r:void 0}function v(e,n,r,o,t){let f=U(r,e,n);if(f!==void 0)return f;let i={};T(r,e,n,i),g(r,e,i),g(r,n,i);let u=r.options.isMergeableObject(e)?x(e):[],s=x(n);q(r,u.length+s.length,t);let E=[],j=new Set;for(let c of s)D(c,r,t)||H(e,c)||(E.push(c),j.add(c));for(let c of u)D(c,r,t)||j.has(c)||K(i,c,l(e[c],r,o+1,t.concat(c)));for(let c of E){let b=n[c],d;if(_(e,c)&&r.options.isMergeableObject(b)){let S=X(c,r);if(S){let R=r.callbackDepth;r.callbackDepth=o;try{d=S(e[c],b,r.options)}finally{r.callbackDepth=R}}else d=w(e[c],b,r,o+1,t.concat(c))}else d=l(b,r,o+1,t.concat(c));K(i,c,d)}return i}function w(e,n,r,o,t){Z(r,o,t);let f=Array.isArray(n),i=Array.isArray(e);return f!==i?l(n,r,o+1,t):f?Q(e,n,r,o,t):v(e,n,r,o,t)}function y(e,n,r){return w(e,n,I(r),0,[])}function P(e,n){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce((r,o)=>y(r,o,n),{})}y.all=P;y.isMergeableObject=M;y.UnsafeKeyError=a;y.DeepMergeLimitError=p;y.deepmerge=y;var ee=y;return z(ne);})();
|
|
3
3
|
(typeof globalThis !== 'undefined' ? globalThis : window).StacklineDeepmerge = StacklineDeepmergeModule.default;
|
|
4
4
|
//# sourceMappingURL=index.min.js.map
|
package/docs/ADOPTION.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Adoption Guide
|
|
2
|
+
|
|
3
|
+
## Choose the direct install when
|
|
4
|
+
|
|
5
|
+
- the application is new;
|
|
6
|
+
- imports can use the Stackline package name;
|
|
7
|
+
- explicit ownership of the hardened behavior is desirable.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @stackline/deepmerge
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import merge from '@stackline/deepmerge';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Choose the npm alias when
|
|
18
|
+
|
|
19
|
+
- an application already imports `deepmerge`;
|
|
20
|
+
- a framework or internal package expects the existing package name;
|
|
21
|
+
- the migration should avoid source changes.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm uninstall deepmerge
|
|
25
|
+
npm install deepmerge@npm:@stackline/deepmerge
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Existing imports remain unchanged:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import merge from 'deepmerge';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Commit the changed lockfile and run the application's complete test suite. The
|
|
35
|
+
alias changes package resolution, so source imports do not need to change.
|
|
36
|
+
|
|
37
|
+
## Security boundary
|
|
38
|
+
|
|
39
|
+
Use `onUnsafeKey: 'throw'` when an unsafe key should reject an entire request.
|
|
40
|
+
The default `skip` mode ignores `__proto__`, `prototype`, and `constructor` at
|
|
41
|
+
every traversed level. Depth and key budgets remain enabled in both modes.
|
|
42
|
+
|
|
43
|
+
## Compatibility review
|
|
44
|
+
|
|
45
|
+
Before migration, review custom `arrayMerge`, `customMerge`, and
|
|
46
|
+
`isMergeableObject` callbacks. The package preserves the documented v4 call
|
|
47
|
+
shape, while cycles, unsafe keys, invalid options, and traversal limits are
|
|
48
|
+
intentionally hardened.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
The runtime has one responsibility: merge two JavaScript values without
|
|
6
|
+
mutating either input while enforcing a small set of security invariants.
|
|
7
|
+
|
|
8
|
+
## Merge state
|
|
9
|
+
|
|
10
|
+
Each top-level merge creates isolated state containing:
|
|
11
|
+
|
|
12
|
+
- normalized options;
|
|
13
|
+
- a target/source pair memo for recursive merges;
|
|
14
|
+
- a clone memo for repeated source references;
|
|
15
|
+
- the cumulative enumerable-key count;
|
|
16
|
+
- callback depth for compatibility hooks.
|
|
17
|
+
|
|
18
|
+
No state is shared between calls.
|
|
19
|
+
|
|
20
|
+
## Traversal
|
|
21
|
+
|
|
22
|
+
`mergeInternal` dispatches by array shape:
|
|
23
|
+
|
|
24
|
+
- array plus array uses the configured array strategy;
|
|
25
|
+
- mismatched array/object shape clones the source;
|
|
26
|
+
- object plus object copies target-only keys and then merges source keys.
|
|
27
|
+
|
|
28
|
+
Accepted values are written with `Object.defineProperty` as own enumerable data
|
|
29
|
+
properties. This avoids invoking legacy object prototype setters during writes.
|
|
30
|
+
|
|
31
|
+
## Security invariants
|
|
32
|
+
|
|
33
|
+
Before reading an enumerable property value, traversal checks:
|
|
34
|
+
|
|
35
|
+
1. the key is not `__proto__`, `prototype`, or `constructor`;
|
|
36
|
+
2. the target does not expose that property only through an inherited or
|
|
37
|
+
non-enumerable slot;
|
|
38
|
+
3. configured depth and key budgets have not been exceeded.
|
|
39
|
+
|
|
40
|
+
Both source and target keys pass the dangerous-key filter. Rejected getters are
|
|
41
|
+
never invoked.
|
|
42
|
+
|
|
43
|
+
## Cycle handling
|
|
44
|
+
|
|
45
|
+
Weak maps preserve cycles without retaining merged graphs after the operation.
|
|
46
|
+
The pair memo identifies a repeated target/source recursion. The clone memo
|
|
47
|
+
preserves repeated references when a source value is cloned into the result.
|
|
48
|
+
|
|
49
|
+
## Distribution
|
|
50
|
+
|
|
51
|
+
One source module produces:
|
|
52
|
+
|
|
53
|
+
- `dist/index.js`: ESM;
|
|
54
|
+
- `dist/index.cjs`: callable CommonJS;
|
|
55
|
+
- `dist/index.min.js`: browser global;
|
|
56
|
+
- resolver-specific `.d.ts`, `.d.cts`, and `.d.mts` declarations;
|
|
57
|
+
- source maps for each JavaScript build.
|
|
58
|
+
|
|
59
|
+
The CommonJS footer assigns the function itself to `module.exports`, preserving
|
|
60
|
+
legacy `require('deepmerge')({...})` usage.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Benchmark Methodology
|
|
2
|
+
|
|
3
|
+
The checked-in benchmark compares `@stackline/deepmerge` with
|
|
4
|
+
`deepmerge@4.3.1` in the same Node.js process and with the same nested
|
|
5
|
+
configuration objects.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm ci
|
|
9
|
+
npm run benchmark
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Set `BENCHMARK_ITERATIONS` to control the sample size:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
BENCHMARK_ITERATIONS=500000 npm run benchmark
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The benchmark warms both implementations before measurement and reports
|
|
19
|
+
operations per second. It is a regression tool, not a universal performance
|
|
20
|
+
claim. CPU, Node.js version, object shape, callbacks, cycle tracking, and
|
|
21
|
+
security limits materially affect results.
|
|
22
|
+
|
|
23
|
+
Compatibility and security take priority over winning a synthetic throughput
|
|
24
|
+
number. Report the complete command, Node.js version, CPU, and raw output when
|
|
25
|
+
sharing results.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
## Behavioral target
|
|
4
|
+
|
|
5
|
+
The default contract follows documented `deepmerge@4.3.1` behavior:
|
|
6
|
+
|
|
7
|
+
- inputs are not mutated;
|
|
8
|
+
- plain and custom mergeable objects recurse;
|
|
9
|
+
- arrays concatenate and clone by default;
|
|
10
|
+
- source wins when array/object shapes differ;
|
|
11
|
+
- `clone: false` preserves nested references;
|
|
12
|
+
- custom array and property merge hooks receive compatible options;
|
|
13
|
+
- Date, RegExp, and React element values are atomic by default;
|
|
14
|
+
- own enumerable symbols are copied;
|
|
15
|
+
- `all` merges an array of objects.
|
|
16
|
+
|
|
17
|
+
The test suite includes documented scenarios and a deterministic corpus of
|
|
18
|
+
5,000 JSON-compatible input pairs compared against `deepmerge@4.3.1`.
|
|
19
|
+
|
|
20
|
+
## Intentional differences
|
|
21
|
+
|
|
22
|
+
| Area | `@stackline/deepmerge` behavior |
|
|
23
|
+
| :--- | :--- |
|
|
24
|
+
| Dangerous keys | Always skipped or rejected at every depth |
|
|
25
|
+
| Cycles | Preserved through weak-reference memoization |
|
|
26
|
+
| Resource limits | Depth and key limits enabled by default |
|
|
27
|
+
| Invalid options | Rejected early with controlled errors |
|
|
28
|
+
| Package format | Native ESM, callable CJS, browser build, conditional types |
|
|
29
|
+
| Runtime dependencies | None |
|
|
30
|
+
|
|
31
|
+
## Module systems
|
|
32
|
+
|
|
33
|
+
| Consumer | Supported form |
|
|
34
|
+
| :--- | :--- |
|
|
35
|
+
| ESM | Default and named exports |
|
|
36
|
+
| CommonJS | Callable `module.exports` plus static helpers |
|
|
37
|
+
| Bundlers | ESM `module` field and `browser` export condition |
|
|
38
|
+
| Browser script | `globalThis.StacklineDeepmerge` |
|
|
39
|
+
| npm alias | `deepmerge@npm:@stackline/deepmerge` |
|
|
40
|
+
|
|
41
|
+
## TypeScript
|
|
42
|
+
|
|
43
|
+
The release test matrix covers:
|
|
44
|
+
|
|
45
|
+
- 3.9.10;
|
|
46
|
+
- 4.7.4;
|
|
47
|
+
- 4.9.5;
|
|
48
|
+
- 5.9.3;
|
|
49
|
+
- 6.0.3;
|
|
50
|
+
- 7.0.2.
|
|
51
|
+
|
|
52
|
+
Older resolvers use `dist/index.d.ts`. Modern ESM and CommonJS resolvers use
|
|
53
|
+
`dist/index.d.mts` and `dist/index.d.cts` respectively.
|
|
54
|
+
|
|
55
|
+
## Runtime
|
|
56
|
+
|
|
57
|
+
The published JavaScript targets ES2018 and declares Node.js 14.17 or newer.
|
|
58
|
+
The development toolchain requires a newer Node.js release; that does not alter
|
|
59
|
+
the package's runtime floor.
|
|
60
|
+
|
|
61
|
+
## Migration test
|
|
62
|
+
|
|
63
|
+
Test the alias in the consuming project without source changes:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install deepmerge@npm:@stackline/deepmerge
|
|
67
|
+
npm test
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If a project relies on undocumented prototype keys, accessor descriptors, or a
|
|
71
|
+
specific cycle failure, treat migration as a behavior change and report the use
|
|
72
|
+
case before production rollout.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Market Research
|
|
2
|
+
|
|
3
|
+
Research date: 2026-08-15
|
|
4
|
+
|
|
5
|
+
## Decision
|
|
6
|
+
|
|
7
|
+
Build `@stackline/deepmerge`: a secure, zero-dependency, modern-package
|
|
8
|
+
replacement for the established `deepmerge` API.
|
|
9
|
+
|
|
10
|
+
The goal is not to predict one million monthly downloads. No package can
|
|
11
|
+
guarantee adoption. The goal is to enter a proven, extremely high-volume
|
|
12
|
+
category with a low-friction migration and clear unmet maintenance needs.
|
|
13
|
+
|
|
14
|
+
## Demand snapshot
|
|
15
|
+
|
|
16
|
+
The official npm downloads API reported the following for 2026-07-11 through
|
|
17
|
+
2026-08-09:
|
|
18
|
+
|
|
19
|
+
| Package | Downloads |
|
|
20
|
+
| :--- | ---: |
|
|
21
|
+
| `lodash.merge` | 493,506,292 |
|
|
22
|
+
| `deepmerge` | 353,418,274 |
|
|
23
|
+
| `deep-extend` | 180,360,708 |
|
|
24
|
+
| `defu` | 143,244,968 |
|
|
25
|
+
| `deepmerge-ts` | 64,560,064 |
|
|
26
|
+
| `@fastify/deepmerge` | 13,948,869 |
|
|
27
|
+
| `ts-deepmerge` | 13,902,566 |
|
|
28
|
+
| `merge-deep` | 7,970,115 |
|
|
29
|
+
|
|
30
|
+
Source: `https://api.npmjs.org/downloads/point/last-month/<package>`.
|
|
31
|
+
|
|
32
|
+
These values are a dated market snapshot, not implementation constants or
|
|
33
|
+
future promises.
|
|
34
|
+
|
|
35
|
+
## Maintenance gap
|
|
36
|
+
|
|
37
|
+
At the research snapshot:
|
|
38
|
+
|
|
39
|
+
- npm reported `deepmerge@4.3.1`, modified 2023-03-16;
|
|
40
|
+
- the upstream GitHub repository was last pushed 2023-04-21;
|
|
41
|
+
- the repository had 2,816 stars and 59 open issues;
|
|
42
|
+
- open requests covered ESM packaging, default-import interop, TypeScript
|
|
43
|
+
inference, and cyclic objects.
|
|
44
|
+
|
|
45
|
+
Sources:
|
|
46
|
+
|
|
47
|
+
- [deepmerge on npm](https://www.npmjs.com/package/deepmerge)
|
|
48
|
+
- [deepmerge repository](https://github.com/TehShrike/deepmerge)
|
|
49
|
+
- [ESM request 194](https://github.com/TehShrike/deepmerge/issues/194)
|
|
50
|
+
- [Cycle request 207](https://github.com/TehShrike/deepmerge/issues/207)
|
|
51
|
+
- [ESM request 250](https://github.com/TehShrike/deepmerge/issues/250)
|
|
52
|
+
- [Default import issue 255](https://github.com/TehShrike/deepmerge/issues/255)
|
|
53
|
+
- [Type inference issue 271](https://github.com/TehShrike/deepmerge/issues/271)
|
|
54
|
+
|
|
55
|
+
## Security need
|
|
56
|
+
|
|
57
|
+
Merge utilities repeatedly appear in prototype-pollution advisories because
|
|
58
|
+
they recursively turn attacker-controlled property names into object writes.
|
|
59
|
+
Modern exploit research also shows that read-side gadgets can make inherited
|
|
60
|
+
pollution dangerous even when a specific merge implementation does not write
|
|
61
|
+
directly to `Object.prototype`.
|
|
62
|
+
|
|
63
|
+
References:
|
|
64
|
+
|
|
65
|
+
- [GitHub Security Lab merge-deep advisory](https://securitylab.github.com/advisories/GHSL-2020-160-merge-deep/)
|
|
66
|
+
- [GHSA-9wv6-86v2-598j](https://github.com/advisories/GHSA-9wv6-86v2-598j)
|
|
67
|
+
- [GHSA-wf5p-g6vw-rhxx](https://github.com/advisories/GHSA-wf5p-g6vw-rhxx)
|
|
68
|
+
|
|
69
|
+
## Product thesis
|
|
70
|
+
|
|
71
|
+
Adoption depends on reducing migration cost:
|
|
72
|
+
|
|
73
|
+
1. Preserve the callable default export and documented extension hooks.
|
|
74
|
+
2. Support an npm alias so existing `import merge from 'deepmerge'` calls do
|
|
75
|
+
not change.
|
|
76
|
+
3. Ship both modern and legacy module shapes with accurate types.
|
|
77
|
+
4. Remove runtime supply-chain exposure by using zero dependencies.
|
|
78
|
+
5. Make unsafe-key behavior and resource limits visible and testable.
|
|
79
|
+
6. Provide a browser playground that demonstrates actual merge behavior.
|
|
80
|
+
|
|
81
|
+
## Alternatives considered
|
|
82
|
+
|
|
83
|
+
### General utility collection
|
|
84
|
+
|
|
85
|
+
Rejected because it increases API surface and supply-chain responsibility
|
|
86
|
+
without a sharper migration story.
|
|
87
|
+
|
|
88
|
+
### Fork the existing implementation
|
|
89
|
+
|
|
90
|
+
Rejected for this package. An independent implementation can adopt modern
|
|
91
|
+
architecture and security invariants while behavioral differential tests
|
|
92
|
+
protect compatibility.
|
|
93
|
+
|
|
94
|
+
### Type-only deep merge
|
|
95
|
+
|
|
96
|
+
Rejected because the largest need is runtime behavior at trust boundaries.
|
|
97
|
+
|
|
98
|
+
### New incompatible API
|
|
99
|
+
|
|
100
|
+
Rejected for 1.0. A new API would discard the strongest distribution path:
|
|
101
|
+
drop-in migration from an established package.
|
|
102
|
+
|
|
103
|
+
## Success measures
|
|
104
|
+
|
|
105
|
+
- clean direct and alias installs;
|
|
106
|
+
- no runtime dependencies;
|
|
107
|
+
- no known package export or type-resolution defects;
|
|
108
|
+
- compatibility reports from real consumers;
|
|
109
|
+
- security reports handled through private disclosure;
|
|
110
|
+
- sustained download growth without weakening the contract.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Only maintainers with npm scope access can publish a release.
|
|
4
|
+
|
|
5
|
+
## Required checks
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm ci
|
|
9
|
+
npm test
|
|
10
|
+
npm run test:attw
|
|
11
|
+
npm run benchmark
|
|
12
|
+
npm run audit:dependencies
|
|
13
|
+
npm pack --dry-run
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run the full TypeScript matrix and clean-install smoke test. Verify GitHub CI is
|
|
17
|
+
green on `main` before public npm publication.
|
|
18
|
+
|
|
19
|
+
## Artifact rule
|
|
20
|
+
|
|
21
|
+
Build one tarball and identify its SHA-512 digest. Publish that exact file to
|
|
22
|
+
Verdaccio. The trusted GitHub workflow rebuilds from the reviewed commit,
|
|
23
|
+
requires the expected digest as input, and refuses to publish to npm unless the
|
|
24
|
+
bytes are identical.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
version="$(node -p "require('./package.json').version")"
|
|
28
|
+
mkdir -p "release/$version"
|
|
29
|
+
npm pack --ignore-scripts --pack-destination "release/$version"
|
|
30
|
+
(cd "release/$version" && sha512sum *.tgz > SHA512SUMS)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Do not rebuild between registries. After each publish, install from that
|
|
34
|
+
registry in an empty project and test direct ESM, direct CommonJS, npm alias,
|
|
35
|
+
and TypeScript resolution.
|
|
36
|
+
|
|
37
|
+
## Release order
|
|
38
|
+
|
|
39
|
+
1. Update version, changelog, docs, and project memory.
|
|
40
|
+
2. Run all required checks.
|
|
41
|
+
3. Commit and push `main`.
|
|
42
|
+
4. Wait for GitHub checks.
|
|
43
|
+
5. Create the final tarball and digest.
|
|
44
|
+
6. Publish and smoke-test Verdaccio.
|
|
45
|
+
7. Run `publish.yml` with the tarball's SHA-512 hex digest and watch it publish
|
|
46
|
+
the byte-identical artifact through npm trusted publishing.
|
|
47
|
+
8. Download from public npm, compare SHA-512, and smoke-test.
|
|
48
|
+
9. Push the signed or annotated version tag.
|
|
49
|
+
10. Create the GitHub release.
|
|
50
|
+
11. Deploy and verify public documentation.
|
|
51
|
+
12. Record package metadata and validation evidence in project memory.
|
|
52
|
+
|
|
53
|
+
Published npm versions are immutable. Never reuse a version number.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import merge from '@stackline/deepmerge';
|
|
4
|
+
|
|
5
|
+
const overwriteArrays = (_target, source) => source;
|
|
6
|
+
const config = merge(
|
|
7
|
+
{ plugins: ['core'], server: { port: 3000 } },
|
|
8
|
+
{ plugins: ['metrics'], server: { secure: true } },
|
|
9
|
+
{ arrayMerge: overwriteArrays }
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
assert.deepEqual(config, {
|
|
13
|
+
plugins: ['metrics'],
|
|
14
|
+
server: { port: 3000, secure: true }
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
console.log(config);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import merge from '@stackline/deepmerge';
|
|
4
|
+
|
|
5
|
+
const shared = { enabled: true };
|
|
6
|
+
const source = { first: shared, second: shared };
|
|
7
|
+
source.self = source;
|
|
8
|
+
|
|
9
|
+
const result = merge({}, source);
|
|
10
|
+
|
|
11
|
+
assert.equal(result.first, result.second);
|
|
12
|
+
assert.equal(result.self, result);
|
|
13
|
+
|
|
14
|
+
console.log({ cyclePreserved: true, sharedReferencePreserved: true });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import merge from '@stackline/deepmerge';
|
|
4
|
+
|
|
5
|
+
const defaults = {
|
|
6
|
+
cache: { enabled: true, ttl: 60 },
|
|
7
|
+
server: { host: '127.0.0.1', port: 3000 }
|
|
8
|
+
};
|
|
9
|
+
const environment = JSON.parse(`{
|
|
10
|
+
"server": { "port": 8080 },
|
|
11
|
+
"constructor": { "prototype": { "isAdmin": true } }
|
|
12
|
+
}`);
|
|
13
|
+
|
|
14
|
+
const config = merge(defaults, environment);
|
|
15
|
+
|
|
16
|
+
assert.deepEqual(config, {
|
|
17
|
+
cache: { enabled: true, ttl: 60 },
|
|
18
|
+
server: { host: '127.0.0.1', port: 8080 }
|
|
19
|
+
});
|
|
20
|
+
assert.equal(Object.prototype.isAdmin, undefined);
|
|
21
|
+
|
|
22
|
+
console.log(config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackline/deepmerge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Secure, immutable, zero-dependency deep merge with a deepmerge-compatible API for ESM, CommonJS, TypeScript, and browsers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepmerge",
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
},
|
|
60
60
|
"files": [
|
|
61
61
|
"dist",
|
|
62
|
+
"docs",
|
|
63
|
+
"examples",
|
|
62
64
|
"CHANGELOG.md",
|
|
63
65
|
"CONTRIBUTING.md",
|
|
64
66
|
"LICENSE",
|
|
@@ -73,10 +75,11 @@
|
|
|
73
75
|
"clean": "node scripts/clean.mjs",
|
|
74
76
|
"build": "node scripts/build.mjs",
|
|
75
77
|
"lint": "eslint . && node scripts/check-markdown.mjs",
|
|
76
|
-
"test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:package && npm run test:install && npm run test:docs",
|
|
78
|
+
"test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:examples && npm run test:package && npm run test:install && npm run test:docs",
|
|
77
79
|
"test:unit": "node --test --test-reporter=spec test/*.test.mjs",
|
|
78
80
|
"test:coverage": "c8 --all --src src --check-coverage --lines 100 --functions 100 --statements 100 --branches 95 node --test test/core.test.mjs test/cycles-limits.test.mjs test/security.test.mjs test/compatibility.test.mjs",
|
|
79
81
|
"test:types": "node scripts/test-types.mjs",
|
|
82
|
+
"test:examples": "node examples/secure-config.mjs && node examples/array-strategy.mjs && node examples/cyclic-graph.mjs",
|
|
80
83
|
"test:package": "node --test test/package.test.mjs && node scripts/check-dist.mjs && publint",
|
|
81
84
|
"test:install": "node scripts/smoke-install.mjs",
|
|
82
85
|
"test:docs": "npm run docs:build && node scripts/check-docs.mjs",
|