npm.io
1.0.1 • Published 21h ago

@stackline/deepmerge

Licence
MIT
Version
1.0.1
Deps
0
Size
122 kB
Vulns
0
Weekly
0

@stackline/deepmerge

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

npm version npm downloads CI license zero dependencies

Docs and playground | npm | Security | Changelog | Issues

Why this package?

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:

  • rejection of __proto__, prototype, and constructor at every depth;
  • immutable merges with cycle and shared-reference preservation;
  • configurable depth and key limits for hostile or malformed inputs;
  • the familiar deepmerge v4 default API and extension hooks;
  • ESM, callable CommonJS, TypeScript, and browser builds;
  • TypeScript compatibility tested from 3.9 through 7.0;
  • zero runtime dependencies.

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

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.

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.

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:

  • dangerous keys are always rejected or skipped;
  • cycles are preserved;
  • traversal limits are enabled by default;
  • invalid option values fail early with a controlled error.

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.

Trust and maintenance

  • No runtime dependencies.
  • Every release is built from the public repository.
  • CI validates behavior, types, package exports, clean installs, and supported runtimes.
  • Security reports have a dedicated private process in SECURITY.md.
  • Release history is recorded in CHANGELOG.md.

Contributing

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

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.

Keywords