Alexandro.Net

Download README · GitHub release

@stackline/deepmerge

Secure, immutable, zero-dependency deep merge with a deepmerge-compatible API for ESM, CommonJS, TypeScript, and browsers.

npm version license GitHub repository Docs Reddit community

Documentation | npm | Issues | Repository

Current package version: 1.0.4


Why this package?

Secure, immutable, zero-dependency deep merge for modern JavaScript, with a deepmerge-compatible API.

Deep merge sits on a trust boundary in configuration loaders, build tools, servers, CLIs, and browser applications. A useful replacement must be safe for untrusted object keys without forcing existing projects to rewrite every merge.

@stackline/deepmerge combines:

Trust and maintenance

Compatibility

Item Value
Package @stackline/deepmerge@1.0.4
Node.js runtime >=14.17.0
CommonJS / primary entry ./dist/index.cjs
ES module entry ./dist/index.js
Type declarations ./dist/index.d.ts

Installation

Install under the package's public name:

npm install @stackline/deepmerge

Or replace deepmerge without changing application imports:

npm install deepmerge@npm:@stackline/deepmerge

Usage

Existing code can continue to use:

import merge from 'deepmerge';

const config = merge(defaults, environment);

Quick start

import merge from '@stackline/deepmerge';

const defaults = {
  server: { port: 3000, headers: { accept: 'application/json' } },
  plugins: ['core']
};

const production = {
  server: { port: 8080, headers: { authorization: 'Bearer token' } },
  plugins: ['metrics']
};

const config = merge(defaults, production);

// {
//   server: {
//     port: 8080,
//     headers: {
//       accept: 'application/json',
//       authorization: 'Bearer token'
//     }
//   },
//   plugins: ['core', 'metrics']
// }

Neither input is mutated.

Features and Integrations

API compatibility

merge(target, source, options?)

Returns a new merged value. Objects merge recursively. Arrays concatenate by default. When an array and object occupy the same position, the source wins.

merge.all(objects, options?)

const config = merge.all([
  { logging: { level: 'info' } },
  { logging: { format: 'json' } },
  { region: 'ca-central-1' }
]);

Options

Option Default Purpose
arrayMerge concatenate Replace or customize array behavior
clone true Set false to preserve nested input references
customMerge none Select a merge function for a property
isMergeableObject built in Decide which values can be traversed
onUnsafeKey "skip" Skip or throw on dangerous keys
maxDepth 1000 Bound recursive traversal
maxKeys 100000 Bound enumerable object keys per merge

The callback options include cloneUnlessOtherwiseSpecified, matching the extension-hook shape used by deepmerge v4.

Named exports

import merge, {
  DeepMergeLimitError,
  UnsafeKeyError,
  all,
  deepmerge,
  isMergeableObject
} from '@stackline/deepmerge';

CommonJS remains callable:

const merge = require('@stackline/deepmerge');

merge({ left: true }, { right: true });
merge.all([{ one: 1 }, { two: 2 }]);

Array strategies

Overwrite arrays:

const overwrite = (_target, source) => source;
const result = merge([1, 2], [3], { arrayMerge: overwrite });
// [3]

Merge arrays by index:

const byIndex = (target, source, options) => {
  const output = target.slice();

  source.forEach((value, index) => {
    output[index] = index in output
      ? merge(output[index], value, options)
      : options.cloneUnlessOtherwiseSpecified(value, options);
  });

  return output;
};

Cycles and shared references

Circular and repeated references are preserved instead of overflowing the stack or being duplicated unexpectedly:

const shared = { enabled: true };
const source = { first: shared, second: shared };
source.self = source;

const result = merge({}, source);

result.first === result.second; // true
result.self === result;         // true

TypeScript

The package ships declaration files for modern ESM, CommonJS, and older TypeScript resolvers. Return types recursively combine the target and source.

import merge from '@stackline/deepmerge';

const result = merge(
  { service: { port: 3000 } },
  { service: { secure: true } }
);

result.service.port;   // number
result.service.secure; // boolean

The release matrix tests TypeScript 3.9, 4.7, 4.9, 5.9, 6.0, and 7.0. The JavaScript runtime supports Node.js 14.17 and newer.

Browser

Use the ESM build with a bundler, or load the small browser global directly:

<script src="https://unpkg.com/@stackline/deepmerge@1/dist/index.min.js"></script>
<script>
  const merged = StacklineDeepmerge(
    { theme: { contrast: 'normal' } },
    { theme: { motion: 'reduced' } }
  );
</script>

Migration from deepmerge

The lowest-change migration uses an npm alias:

npm uninstall deepmerge
npm install deepmerge@npm:@stackline/deepmerge

The compatibility suite covers documented options and 5,000 deterministic, JSON-compatible differential cases against deepmerge@4.3.1.

Intentional hardening differences:

See Compatibility for the full contract.

Performance

Security checks, cycle tracking, and resource limits add measurable work. The included benchmark compares this package with deepmerge@4.3.1 on the same process:

npm run benchmark

Use benchmark results as regression signals, not universal claims. Runtime, CPU, input shape, and custom callbacks materially affect throughput.

Adoption resources

The examples are included in the npm tarball and run against the package's public exports. They cover hostile configuration input, custom array strategy, cycles, and shared references.

Security

Secure by default

Dangerous keys are skipped from both inputs before their values are read:

import merge from '@stackline/deepmerge';

const payload = JSON.parse(`{
  "profile": {
    "name": "Ada",
    "constructor": {
      "prototype": { "isAdmin": true }
    }
  }
}`);

const result = merge({}, payload);

console.log(result);                    // { profile: { name: 'Ada' } }
console.log(Object.prototype.isAdmin); // undefined

Use strict rejection when silent filtering is not appropriate:

merge({}, payload, { onUnsafeKey: 'throw' });
// UnsafeKeyError: Refusing to merge unsafe key constructor at
// <root>.profile.constructor

Security limits are enabled by default:

merge(target, source, {
  maxDepth: 1000,
  maxKeys: 100000
});

Set a smaller limit at an exposed API boundary. Infinity is accepted when the input is already trusted.

Local Development

git clone https://github.com/alexandroit/stackline-deepmerge.git
cd stackline-deepmerge
npm ci
npm run test

Release tooling uses Node.js 24.20.0 and npm 11.19.0. The consumer runtime contract remains the one documented above.

Contributing

Read CONTRIBUTING.md before opening a pull request. Changes to compatibility or security behavior require focused regression tests.

Consumer Smoke Test

Run the repository's existing consumer/package check after installing development dependencies:

npm run test:install

Release Checklist

Run npm run test and inspect the package contents before release. Publish a new version through the GitHub Actions publishing workflow, using the SHA-512 digest of the reviewed tarball. Verify the exact published version, tarball integrity, and npm provenance after the run.

License

MIT. See LICENSE and NOTICE.

@stackline/deepmerge is an independent project and is not affiliated with or endorsed by the maintainers of the deepmerge package.

Credits and original authors

Original copyright, license notices and contributor acknowledgements remain part of this distribution. Stackline maintenance does not replace authorship of the original work.

Community and Links

Use this repository's issue tracker for reproducible bugs and feature requests. Join r/Stackline for examples, usage questions and release discussions.