From bfed5b0d836de8f27dd12f8892cd98a66a075779 Mon Sep 17 00:00:00 2001 From: Uzlopak Date: Thu, 30 Jun 2022 13:01:37 +0200 Subject: [PATCH] Initial implementation (#1) * initial implementation * remove examples * add all Option * 100% test coverage * write Readme.md * add Licensing Info to old deepmerge * fix descriptin * add CI-badge * add primitive benchmark * remove clone option in tests * Update LICENSE Co-authored-by: Matteo Collina * improve benchmarks * add unit test * add test for prototype pollution * fix prototype pollution, improve performance of all * add use strict everywhere * improve perf by using less branches and less lookup methods * less branches * rename map to clone * add test for non-enumerable symbol keys * add with lint to ci.yml * Apply suggestions from code review Co-authored-by: Manuel Spigolon * Update test/index.test.js Co-authored-by: Manuel Spigolon * make bechmarks nicer * fix npm scrpits Co-authored-by: Matteo Collina Co-authored-by: Manuel Spigolon --- .github/workflows/ci.yml | 2 + .npmrc | 1 + .taprc | 3 + LICENSE | 24 ++ README.md | 102 +++-- benchmark/.npmrc | 1 + benchmark/bench.all.js | 53 +++ benchmark/bench.compare.detailed.js | 148 +++++++ benchmark/bench.compare.js | 94 +++++ benchmark/bench.js | 53 +++ benchmark/package.json | 23 ++ docs/.gitkeep | 0 examples/.gitkeep | 0 index.js | 133 ++++++ package.json | 41 ++ test/.gitkeep | 0 test/all.test.js | 73 ++++ test/index.test.js | 605 ++++++++++++++++++++++++++++ test/symbol.test.js | 48 +++ types/.gitkeep | 0 types/index.d.ts | 66 +++ types/index.test-d.ts | 51 +++ 22 files changed, 1484 insertions(+), 37 deletions(-) create mode 100644 .npmrc create mode 100644 .taprc create mode 100644 LICENSE create mode 100644 benchmark/.npmrc create mode 100644 benchmark/bench.all.js create mode 100644 benchmark/bench.compare.detailed.js create mode 100644 benchmark/bench.compare.js create mode 100644 benchmark/bench.js create mode 100644 benchmark/package.json delete mode 100644 docs/.gitkeep delete mode 100644 examples/.gitkeep create mode 100644 index.js create mode 100644 package.json delete mode 100644 test/.gitkeep create mode 100644 test/all.test.js create mode 100644 test/index.test.js create mode 100644 test/symbol.test.js delete mode 100644 types/.gitkeep create mode 100644 types/index.d.ts create mode 100644 types/index.test-d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32b3d16..913752e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,3 +13,5 @@ on: jobs: test: uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + with: + lint: true diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/.taprc b/.taprc new file mode 100644 index 0000000..688fda9 --- /dev/null +++ b/.taprc @@ -0,0 +1,3 @@ +ts: false +jsx: false +coverage: true \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a3069cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) The Fastify Team + +The Fastify team members are listed at https://github.com/fastify/fastify#team +and in the README file. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 2a2ee72..0344569 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,65 @@ -# skeleton - -Template repository to create standardized Fastify plugins. - -# Getting started - -- Click on `Use this template` above to create a new repository based on this repository. - -# What's included? - -1. Github CI Actions for installing, testing your package. -2. Github CI Actions to validate different package managers. -3. Dependabot V2 config to automate dependency updates. -4. Template for the GitHub App [Stale](https://github.com/apps/stale) to mark issues as stale. -5. Template for the GitHub App [tests-checker](https://github.com/apps/tests-checker) to check if a PR contains tests. - -# Repository structure - -``` -├── .github -│ ├── workflows -│ │ ├── ci.yml -│ │ └── package-manager-ci.yml -│ ├── .stale.yml -│ ├── dependabot.yml -│ └── tests_checker.yml -│ -├── docs (Documentation) -│ -├── examples (Code examples) -│ -├── test (Application tests) -│ -├── types (Typescript types) -│ -└── README.md -``` \ No newline at end of file +# @fastify/deepmerge + +![CI](https://github.com/fastify/deepmerge/workflows/CI/badge.svg) +[![NPM version](https://img.shields.io/npm/v/@fastify/deepmerge.svg?style=flat)](https://www.npmjs.com/package/@fastify/deepmerge) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) + +Merges the enumerable properties of two or more objects deeply. Fastest implementation of deepmerge, see section 'Benchmarks'. + +### Install +``` +npm i @fastify/deepmerge +``` + +### Usage + +The module exports a function, which provides a function to deepmerge Objects. + +``` +deepmerge(options) +``` + +`options` is optional and can contain following values + +- `symbols` (`boolean`, optional) - should also merge object-keys which are symbols, default is false +- `all` (`boolean`, optional) - merges all parameters, default is false + +```js +const deepmerge = require('@fastify/deepmegre')() +const result = deepmerge({a: 'value'}, { b: 404 }) +console.log(result) // {a: 'value', b: 404 } +``` + +```js +const deepmerge = require('@fastify/deepmegre')({ all: true }) +const result = deepmerge({a: 'value'}, { b: 404 }, { a: 404 }) +console.log(result) // {a: 404, b: 404 } +``` + +## Benchmarks + +The benchmarks are available in the benchmark-folder. + +`npm run bench` - benchmark various use cases of deepmerge: +``` +@fastify/deepmerge: merge regex with date x 1,266,447,885 ops/sec ±0.14% (97 runs sampled) +@fastify/deepmerge: merge object with a primitive x 1,266,435,016 ops/sec ±0.33% (97 runs sampled) +@fastify/deepmerge: merge two arrays containing strings x 25,591,739 ops/sec ±0.24% (98 runs sampled) +@fastify/deepmerge: two merge arrays containing objects x 976,182 ops/sec ±0.46% (98 runs sampled) +@fastify/deepmerge: merge two flat objects x 10,027,879 ops/sec ±0.36% (94 runs sampled) +@fastify/deepmerge: merge nested objects x 5,341,227 ops/sec ±0.67% (94 runs sampled) +``` + +`npm run bench:compare` - comparison of @fastify/deepmerge with other popular deepmerge implementation: +``` +@fastify/deepmerge x 403,777 ops/sec ±0.22% (98 runs sampled) +deepmerge x 21,143 ops/sec ±0.83% (93 runs sampled) +merge-deep x 89,447 ops/sec ±0.59% (95 runs sampled) +ts-deepmerge x 185,601 ops/sec ±0.59% (96 runs sampled) +deepmerge-ts x 185,310 ops/sec ±0.50% (92 runs sampled) +lodash.merge x 89,053 ops/sec ±0.37% (99 runs sampled) +``` + +## License + +Licensed under [MIT](./LICENSE). \ No newline at end of file diff --git a/benchmark/.npmrc b/benchmark/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/benchmark/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/benchmark/bench.all.js b/benchmark/bench.all.js new file mode 100644 index 0000000..2c9b86d --- /dev/null +++ b/benchmark/bench.all.js @@ -0,0 +1,53 @@ +'use strict' + +const Benchmark = require('benchmark') +const deepmerge = require('..')({ symbol: false, all: true }) + +const sourceSimple = { key1: 'changed', key2: 'value2' } +const targetSimple = { key1: 'value1', key3: 'value3' } + +const sourceNested = { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + } +} +const targetNested = { + key1: 'value1', + key2: 'value2' +} + +const primitive = 'primitive' + +const date = new Date() +const regex = /a/g + +const simpleArrayTarget = ['a1', 'a2', 'c1', 'f1', 'p1'] +const simpleArraySource = ['t1', 's1', 'c2', 'r1', 'p2', 'p3'] + +const complexArraySource = [{ ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }] +const complexArrayTarget = [{ ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }] + +new Benchmark.Suite() + .add('@fastify/deepmerge: merge regex with date', function () { + deepmerge(regex, date) + }) + .add('@fastify/deepmerge: merge object with a primitive', function () { + deepmerge(targetSimple, primitive) + }) + .add('@fastify/deepmerge: merge two arrays containing strings', function () { + deepmerge(simpleArrayTarget, simpleArraySource) + }) + .add('@fastify/deepmerge: two merge arrays containing objects', function () { + deepmerge(complexArrayTarget, complexArraySource) + }) + .add('@fastify/deepmerge: merge two flat objects', function () { + deepmerge(targetSimple, sourceSimple) + }) + .add('@fastify/deepmerge: merge nested objects', function () { + deepmerge(targetNested, sourceNested) + }) + .on('cycle', function (event) { + console.log(String(event.target)) + }) + .run() diff --git a/benchmark/bench.compare.detailed.js b/benchmark/bench.compare.detailed.js new file mode 100644 index 0000000..b991637 --- /dev/null +++ b/benchmark/bench.compare.detailed.js @@ -0,0 +1,148 @@ +'use strict' + +const Benchmark = require('benchmark') +const fastifyDeepmerge = require('..')({ symbol: false }) +const deepmerge = require('deepmerge') +const mergedeep = require('merge-deep') +const tsDeepmerge = require('ts-deepmerge').default +const deepmergeTs = require('deepmerge-ts').deepmerge +const lodashMerge = require('lodash.merge') + +const sourceSimple = { key1: 'changed', key2: 'value2' } +const targetSimple = { key1: 'value1', key3: 'value3' } + +const sourceNested = { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + } +} +const targetNested = { + key1: 'value1', + key2: 'value2' +} + +const primitive = 'primitive' + +const date = new Date() +const regex = /a/g + +const simpleArrayTarget = ['a1', 'a2', 'c1', 'f1', 'p1'] +const simpleArraySource = ['t1', 's1', 'c2', 'r1', 'p2', 'p3'] + +const complexArraySource = [{ ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }] +const complexArrayTarget = [{ ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }] + +new Benchmark.Suite() + .add('@fastify/deepmerge: merge regex with date', function () { + fastifyDeepmerge(regex, date) + }) + .add('@fastify/deepmerge: merge object with a primitive', function () { + fastifyDeepmerge(targetSimple, primitive) + }) + .add('@fastify/deepmerge: merge two arrays containing strings', function () { + fastifyDeepmerge(simpleArrayTarget, simpleArraySource) + }) + .add('@fastify/deepmerge: two merge arrays containing objects', function () { + fastifyDeepmerge(complexArrayTarget, complexArraySource) + }) + .add('@fastify/deepmerge: merge two flat objects', function () { + fastifyDeepmerge(targetSimple, sourceSimple) + }) + .add('@fastify/deepmerge: merge nested objects', function () { + fastifyDeepmerge(targetNested, sourceNested) + }) + .add('deepmerge: merge regex with date', function () { + deepmerge(regex, date) + }) + .add('deepmerge: merge object with a primitive', function () { + deepmerge(targetSimple, primitive) + }) + .add('deepmerge: merge two arrays containing strings', function () { + deepmerge(simpleArrayTarget, simpleArraySource) + }) + .add('deepmerge: two merge arrays containing objects', function () { + deepmerge(complexArrayTarget, complexArraySource) + }) + .add('deepmerge: merge two flat objects', function () { + deepmerge(targetSimple, sourceSimple) + }) + .add('deepmerge: merge nested objects', function () { + deepmerge(targetNested, sourceNested) + }) + .add('merge-deep: merge regex with date', function () { + mergedeep(regex, date) + }) + .add('merge-deep: merge object with a primitive', function () { + mergedeep(targetSimple, primitive) + }) + .add('merge-deep: merge two arrays containing strings', function () { + mergedeep(simpleArrayTarget, simpleArraySource) + }) + .add('merge-deep: two merge arrays containing objects', function () { + mergedeep(complexArrayTarget, complexArraySource) + }) + .add('merge-deep: merge two flat objects', function () { + mergedeep(targetSimple, sourceSimple) + }) + .add('merge-deep: merge nested objects', function () { + mergedeep(targetNested, sourceNested) + }) + .add('ts-deepmerge: merge regex with date', function () { + tsDeepmerge(regex, date) + }) + .add('ts-deepmerge: merge object with a primitive', function () { + tsDeepmerge(targetSimple, primitive) + }) + .add('ts-deepmerge: merge two arrays containing strings', function () { + tsDeepmerge(simpleArrayTarget, simpleArraySource) + }) + .add('ts-deepmerge: two merge arrays containing objects', function () { + tsDeepmerge(complexArrayTarget, complexArraySource) + }) + .add('ts-deepmerge: merge two flat objects', function () { + tsDeepmerge(targetSimple, sourceSimple) + }) + .add('ts-deepmerge: merge nested objects', function () { + tsDeepmerge(targetNested, sourceNested) + }) + .add('deepmerge-ts: merge regex with date', function () { + deepmergeTs(regex, date) + }) + .add('deepmerge-ts: merge object with a primitive', function () { + deepmergeTs(targetSimple, primitive) + }) + .add('deepmerge-ts: merge two arrays containing strings', function () { + deepmergeTs(simpleArrayTarget, simpleArraySource) + }) + .add('deepmerge-ts: two merge arrays containing objects', function () { + deepmergeTs(complexArrayTarget, complexArraySource) + }) + .add('deepmerge-ts: merge two flat objects', function () { + deepmergeTs(targetSimple, sourceSimple) + }) + .add('deepmerge-ts: merge nested objects', function () { + deepmergeTs(targetNested, sourceNested) + }) + .add('lodash.merge: merge regex with date', function () { + lodashMerge(regex, date) + }) + .add('lodash.merge: merge object with a primitive', function () { + lodashMerge(targetSimple, primitive) + }) + .add('lodash.merge: merge two arrays containing strings', function () { + lodashMerge(simpleArrayTarget, simpleArraySource) + }) + .add('lodash.merge: two merge arrays containing objects', function () { + lodashMerge(complexArrayTarget, complexArraySource) + }) + .add('lodash.merge: merge two flat objects', function () { + lodashMerge(targetSimple, sourceSimple) + }) + .add('lodash.merge: merge nested objects', function () { + lodashMerge(targetNested, sourceNested) + }) + .on('cycle', function (event) { + console.log(String(event.target)) + }) + .run() diff --git a/benchmark/bench.compare.js b/benchmark/bench.compare.js new file mode 100644 index 0000000..33487ba --- /dev/null +++ b/benchmark/bench.compare.js @@ -0,0 +1,94 @@ +'use strict' + +const Benchmark = require('benchmark') +const deepmergeThis = require('..')({ symbol: false }) +const deepmerge = require('deepmerge') +const mergedeep = require('merge-deep') +const tsDeepmerge = require('ts-deepmerge').default +const deepmergeTs = require('deepmerge-ts').deepmerge +const lodashMerge = require('lodash.merge') + +const sourceSimple = { key1: 'changed', key2: 'value2' } +const targetSimple = { key1: 'value1', key3: 'value3' } + +const sourceNested = { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + } +} +const targetNested = { + key1: 'value1', + key2: 'value2' +} + +const primitive = 'primitive' + +const date = new Date() +const regex = /a/g + +const simpleArrayTarget = ['a1', 'a2', 'c1', 'f1', 'p1'] +const simpleArraySource = ['t1', 's1', 'c2', 'r1', 'p2', 'p3'] + +const complexArraySource = [{ ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }] +const complexArrayTarget = [{ ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }] + +new Benchmark.Suite() + .add('@fastify/deepmerge', function () { + deepmergeThis(regex, date) + deepmergeThis(targetSimple, primitive) + deepmergeThis(simpleArrayTarget, simpleArraySource) + deepmergeThis(complexArrayTarget, complexArraySource) + deepmergeThis(complexArrayTarget, complexArraySource) + deepmergeThis(targetSimple, sourceSimple) + deepmergeThis(targetNested, sourceNested) + }) + .add('deepmerge', function () { + deepmerge(regex, date) + deepmerge(targetSimple, primitive) + deepmerge(simpleArrayTarget, simpleArraySource) + deepmerge(complexArrayTarget, complexArraySource) + deepmerge(complexArrayTarget, complexArraySource) + deepmerge(targetSimple, sourceSimple) + deepmerge(targetNested, sourceNested) + }) + .add('merge-deep', function () { + mergedeep(regex, date) + mergedeep(targetSimple, primitive) + mergedeep(simpleArrayTarget, simpleArraySource) + mergedeep(complexArrayTarget, complexArraySource) + mergedeep(complexArrayTarget, complexArraySource) + mergedeep(targetSimple, sourceSimple) + mergedeep(targetNested, sourceNested) + }) + .add('ts-deepmerge', function () { + tsDeepmerge(regex, date) + tsDeepmerge(targetSimple, primitive) + tsDeepmerge(simpleArrayTarget, simpleArraySource) + tsDeepmerge(complexArrayTarget, complexArraySource) + tsDeepmerge(complexArrayTarget, complexArraySource) + tsDeepmerge(targetSimple, sourceSimple) + tsDeepmerge(targetNested, sourceNested) + }) + .add('deepmerge-ts', function () { + deepmergeTs(regex, date) + deepmergeTs(targetSimple, primitive) + deepmergeTs(simpleArrayTarget, simpleArraySource) + deepmergeTs(complexArrayTarget, complexArraySource) + deepmergeTs(complexArrayTarget, complexArraySource) + deepmergeTs(targetSimple, sourceSimple) + deepmergeTs(targetNested, sourceNested) + }) + .add('lodash.merge', function () { + lodashMerge(regex, date) + lodashMerge(targetSimple, primitive) + lodashMerge(simpleArrayTarget, simpleArraySource) + lodashMerge(complexArrayTarget, complexArraySource) + lodashMerge(complexArrayTarget, complexArraySource) + lodashMerge(targetSimple, sourceSimple) + lodashMerge(targetNested, sourceNested) + }) + .on('cycle', function (event) { + console.log(String(event.target)) + }) + .run() diff --git a/benchmark/bench.js b/benchmark/bench.js new file mode 100644 index 0000000..baaffe0 --- /dev/null +++ b/benchmark/bench.js @@ -0,0 +1,53 @@ +'use strict' + +const Benchmark = require('benchmark') +const deepmerge = require('..')({ symbol: false }) + +const sourceSimple = { key1: 'changed', key2: 'value2' } +const targetSimple = { key1: 'value1', key3: 'value3' } + +const sourceNested = { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + } +} +const targetNested = { + key1: 'value1', + key2: 'value2' +} + +const primitive = 'primitive' + +const date = new Date() +const regex = /a/g + +const simpleArrayTarget = ['a1', 'a2', 'c1', 'f1', 'p1'] +const simpleArraySource = ['t1', 's1', 'c2', 'r1', 'p2', 'p3'] + +const complexArraySource = [{ ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }, { ...sourceSimple }] +const complexArrayTarget = [{ ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }, { ...targetSimple }] + +new Benchmark.Suite() + .add('@fastify/deepmerge: merge regex with date', function () { + deepmerge(regex, date) + }) + .add('@fastify/deepmerge: merge object with a primitive', function () { + deepmerge(targetSimple, primitive) + }) + .add('@fastify/deepmerge: merge two arrays containing strings', function () { + deepmerge(simpleArrayTarget, simpleArraySource) + }) + .add('@fastify/deepmerge: two merge arrays containing objects', function () { + deepmerge(complexArrayTarget, complexArraySource) + }) + .add('@fastify/deepmerge: merge two flat objects', function () { + deepmerge(targetSimple, sourceSimple) + }) + .add('@fastify/deepmerge: merge nested objects', function () { + deepmerge(targetNested, sourceNested) + }) + .on('cycle', function (event) { + console.log(String(event.target)) + }) + .run() diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 0000000..cc9327b --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,23 @@ +{ + "name": "benchmarks", + "version": "1.0.0", + "description": "", + "main": "bench.all.js", + "private": true, + "scripts": { + "bench": "node ./bench.js", + "bench:all": "node ./bench.all.js", + "bench:compare": "node ./bench.compare.js", + "bench:compare:detailed": "node ./bench.compare.detailed.js" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "benchmark": "^2.1.4", + "deepmerge": "^4.2.2", + "deepmerge-ts": "^4.2.1", + "lodash.merge": "^4.6.2", + "merge-deep": "^3.0.3", + "ts-deepmerge": "^2.0.1" + } +} diff --git a/docs/.gitkeep b/docs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/examples/.gitkeep b/examples/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/index.js b/index.js new file mode 100644 index 0000000..00684ec --- /dev/null +++ b/index.js @@ -0,0 +1,133 @@ +'use strict' + +// based on https://github.com/TehShrike/deepmerge +// MIT License +// Copyright (c) 2012 - 2022 James Halliday, Josh Duff, and other contributors of deepmerge + +function deepmergeConstructor (options) { + const prototypeKeys = ['constructor', '__proto__', 'prototype'] + + function isNotPrototypeKey (value) { + return prototypeKeys.indexOf(value) === -1 + } + + function cloneArray (value) { + let i = 0 + const il = value.length + const result = new Array(il) + for (i = 0; i < il; ++i) { + result[i] = clone(value[i]) + } + return result + } + + function concatArrays (target, source) { + const tl = target.length + const sl = source.length + let i = 0 + const result = new Array(tl + sl) + for (i = 0; i < tl; ++i) { + result[i] = clone(target[i]) + } + for (i = 0; i < sl; ++i) { + result[i + tl] = clone(source[i]) + } + return result + } + + const propertyIsEnumerable = Object.prototype.propertyIsEnumerable + function getSymbolsAndKeys (value) { + const result = Object.keys(value) + const keys = Object.getOwnPropertySymbols(value) + for (let i = 0, il = keys.length; i < il; ++i) { + propertyIsEnumerable.call(value, keys[i]) && result.push(keys[i]) + } + return result + } + + const getKeys = options && options.symbols + ? getSymbolsAndKeys + : Object.keys + + function isMergeableObject (value) { + return typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Date) + } + + function isPrimitive (value) { + return typeof value !== 'object' || value === null + } + + function isPrimitiveOrBuiltIn (value) { + return typeof value !== 'object' || value === null || value instanceof RegExp || value instanceof Date + } + + function clone (entry) { + return isMergeableObject(entry) + ? Array.isArray(entry) + ? cloneArray(entry) + : mergeObject({}, entry) + : entry + } + + function mergeObject (target, source) { + const result = {} + const targetKeys = getKeys(target) + const sourceKeys = getKeys(source) + let i, il, key + for (i = 0, il = targetKeys.length; i < il; ++i) { + isNotPrototypeKey(key = targetKeys[i]) && + (sourceKeys.indexOf(key) === -1) && + (result[key] = clone(target[key])) + } + + for (i = 0, il = sourceKeys.length; i < il; ++i) { + isNotPrototypeKey(key = sourceKeys[i]) && + ( + key in target && (targetKeys.indexOf(key) !== -1 && (result[key] = _deepmerge(target[key], source[key])), true) || // eslint-disable-line no-mixed-operators + (result[key] = clone(source[key])) + ) + } + return result + } + + function _deepmerge (target, source) { + const sourceIsArray = Array.isArray(source) + const targetIsArray = Array.isArray(target) + + if (isPrimitive(source)) { + return source + } else if (isPrimitiveOrBuiltIn(target)) { + return clone(source) + } else if (sourceIsArray && targetIsArray) { + return concatArrays(target, source) + } else if (sourceIsArray !== targetIsArray) { + return clone(source) + } else { + return mergeObject(target, source) + } + } + + function _deepmergeAll () { + switch (arguments.length) { + case 0: + return {} + case 1: + return clone(arguments[0]) + case 2: + return _deepmerge(arguments[0], arguments[1]) + } + let result + for (let i = 0, il = arguments.length; i < il; ++i) { + result = _deepmerge(result, arguments[i]) + } + return result + } + + return options && options.all + ? _deepmergeAll + : _deepmerge +} + +module.exports = deepmergeConstructor +module.exports.default = deepmergeConstructor +module.exports.deepmerge = deepmergeConstructor diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba8cbe5 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "@fastify/deepmerge", + "version": "1.0.0", + "description": "Merges the enumerable properties of two or more objects deeply.", + "main": "index.js", + "types": "types/index.d.ts", + "scripts": { + "lint": "standard", + "lint:fix": "standard --fix", + "test": "npm run test:unit && npm run test:typescript", + "test:unit": "tap -J test/*.test.js test/**/*.test.js", + "test:typescript": "tsd" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fastify/deepmerge.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/fastify/deepmerge/issues" + }, + "homepage": "https://github.com/fastify/deepmerge#readme", + "devDependencies": { + "standard": "^17.0.0", + "tap": "^16.3.0", + "tsd": "^0.21.0" + }, + "files": [ + "LICENSE", + "README.md", + "index.js", + "types/index.d.ts" + ], + "keywords": [ + "merge", + "deep", + "recursive", + "object" + ] +} diff --git a/test/.gitkeep b/test/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/all.test.js b/test/all.test.js new file mode 100644 index 0000000..37e4938 --- /dev/null +++ b/test/all.test.js @@ -0,0 +1,73 @@ +'use strict' + +// based on https://github.com/TehShrike/deepmerge/tree/3c39fb376158fa3cfc75250cfc4414064a90f582/test +// MIT License +// Copyright (c) 2012 - 2022 James Halliday, Josh Duff, and other contributors of deepmerge + +const deepmerge = require('../index')({ all: true }) +const test = require('tap').test + +test('return an empty object if first argument is an array with no elements', function (t) { + t.same(deepmerge(), {}) + t.end() +}) + +test('Work just fine if first argument is an array with least than two elements', function (t) { + const actual = deepmerge({ example: true }) + const expected = { example: true } + t.same(actual, expected) + t.end() +}) + +test('execute correctly if options object were not passed', function (t) { + t.doesNotThrow(deepmerge.bind(null, { example: true }, { another: '123' })) + t.end() +}) + +test('execute correctly if options object were passed', function (t) { + t.doesNotThrow(deepmerge.bind(null, { example: true }, { another: '123' })) + t.end() +}) + +test('invoke merge on every item in array should result with all props', function (t) { + const firstObject = { first: true } + const secondObject = { second: false } + const thirdObject = { third: 123 } + const fourthObject = { fourth: 'some string' } + + const mergedObject = deepmerge(firstObject, secondObject, thirdObject, fourthObject) + + t.ok(mergedObject.first === true) + t.ok(mergedObject.second === false) + t.ok(mergedObject.third === 123) + t.ok(mergedObject.fourth === 'some string') + t.end() +}) + +test('invoke merge on every item in array with clone should clone all elements', function (t) { + const firstObject = { a: { d: 123 } } + const secondObject = { b: { e: true } } + const thirdObject = { c: { f: 'string' } } + + const mergedWithClone = deepmerge(firstObject, secondObject, thirdObject) + + t.not(mergedWithClone.a, firstObject.a) + t.not(mergedWithClone.b, secondObject.b) + t.not(mergedWithClone.c, thirdObject.c) + + t.end() +}) + +test('invoke merge on every item in array without clone should clone all elements', function (t) { + const firstObject = { a: { d: 123 } } + const secondObject = { b: { e: true } } + const thirdObject = { c: { f: 'string' } } + + const mergedWithoutClone = deepmerge(firstObject, secondObject, thirdObject) + + t.not(mergedWithoutClone.a, firstObject.a) + t.not(mergedWithoutClone.b, secondObject.b) + t.not(mergedWithoutClone.c, thirdObject.c) + + t.end() +}) diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..3d43867 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,605 @@ +'use strict' + +// based on https://github.com/TehShrike/deepmerge/tree/3c39fb376158fa3cfc75250cfc4414064a90f582/test +// MIT License +// Copyright (c) 2012 - 2022 James Halliday, Josh Duff, and other contributors of deepmerge + +const deepmerge = require('../index')({ symbols: true }) +const test = require('tap').test + +test('add keys in target that do not exist at the root', function (t) { + const src = { key1: 'value1', key2: 'value2' } + const target = {} + + const res = deepmerge(target, src) + + t.same(target, {}, 'merge should be immutable') + t.same(res, src) + t.end() +}) + +test('merge existing simple keys in target at the roots', function (t) { + const src = { key1: 'changed', key2: 'value2' } + const target = { key1: 'value1', key3: 'value3' } + + const expected = { + key1: 'changed', + key2: 'value2', + key3: 'value3' + } + + t.same(target, { key1: 'value1', key3: 'value3' }) + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('merge nested objects into target', function (t) { + const src = { + key1: { + subkey1: 'changed', + subkey3: 'added' + } + } + const target = { + key1: { + subkey1: 'value1', + subkey2: 'value2' + } + } + + const expected = { + key1: { + subkey1: 'changed', + subkey2: 'value2', + subkey3: 'added' + } + } + + t.same(target, { + key1: { + subkey1: 'value1', + subkey2: 'value2' + } + }) + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('replace simple key with nested object in target', function (t) { + const src = { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + } + } + const target = { + key1: 'value1', + key2: 'value2' + } + + const expected = { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + }, + key2: 'value2' + } + + t.same(target, { key1: 'value1', key2: 'value2' }) + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should add nested object in target', function (t) { + const src = { + b: { + c: {} + } + } + + const target = { + a: {} + } + + const expected = { + a: {}, + b: { + c: {} + } + } + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should clone source and target', function (t) { + const src = { + b: { + c: 'foo' + } + } + + const target = { + a: { + d: 'bar' + } + } + + const expected = { + a: { + d: 'bar' + }, + b: { + c: 'foo' + } + } + + const merged = deepmerge(target, src) + + t.same(merged, expected) + + t.not(merged.a, target.a) + t.not(merged.b, src.b) + + t.end() +}) + +test('should clone source and target', function (t) { + const src = { + b: { + c: 'foo' + } + } + + const target = { + a: { + d: 'bar' + } + } + + const merged = deepmerge(target, src) + t.not(merged.a, target.a) + t.not(merged.b, src.b) + + t.end() +}) + +test('should replace object with simple key in target', function (t) { + const src = { key1: 'value1' } + const target = { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + }, + key2: 'value2' + } + + const expected = { key1: 'value1', key2: 'value2' } + + t.same(target, { + key1: { + subkey1: 'subvalue1', + subkey2: 'subvalue2' + }, + key2: 'value2' + }) + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should replace objects with arrays', function (t) { + const target = { key1: { subkey: 'one' } } + + const src = { key1: ['subkey'] } + + const expected = { key1: ['subkey'] } + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should replace arrays with objects', function (t) { + const target = { key1: ['subkey'] } + + const src = { key1: { subkey: 'one' } } + + const expected = { key1: { subkey: 'one' } } + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should replace object with primitive', function (t) { + const target = { key1: new Date() } + + const src = 'test' + + const expected = 'test' + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should replace Date with RegExp', function (t) { + const target = new Date() + + const src = /a/g + + const expected = /a/g + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should replace dates with arrays', function (t) { + const target = { key1: new Date() } + + const src = { key1: ['subkey'] } + + const expected = { key1: ['subkey'] } + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should replace null with arrays', function (t) { + const target = { + key1: null + } + + const src = { + key1: ['subkey'] + } + + const expected = { + key1: ['subkey'] + } + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should work on simple array', function (t) { + const src = ['one', 'three'] + const target = ['one', 'two'] + + const expected = ['one', 'two', 'one', 'three'] + + t.same(deepmerge(target, src), expected) + t.ok(Array.isArray(deepmerge(target, src))) + t.end() +}) + +test('should work on another simple array', function (t) { + const target = ['a1', 'a2', 'c1', 'f1', 'p1'] + const src = ['t1', 's1', 'c2', 'r1', 'p2', 'p3'] + + const expected = ['a1', 'a2', 'c1', 'f1', 'p1', 't1', 's1', 'c2', 'r1', 'p2', 'p3'] + t.same(target, ['a1', 'a2', 'c1', 'f1', 'p1']) + t.same(deepmerge(target, src), expected) + t.ok(Array.isArray(deepmerge(target, src))) + t.end() +}) + +test('should work on array properties', function (t) { + const src = { + key1: ['one', 'three'], + key2: ['four'] + } + const target = { + key1: ['one', 'two'] + } + + const expected = { + key1: ['one', 'two', 'one', 'three'], + key2: ['four'] + } + + t.same(deepmerge(target, src), expected) + t.ok(Array.isArray(deepmerge(target, src).key1)) + t.ok(Array.isArray(deepmerge(target, src).key2)) + t.end() +}) + +test('should work on array properties with clone option', function (t) { + const src = { + key1: ['one', 'three'], + key2: ['four'] + } + const target = { + key1: ['one', 'two'] + } + + t.same(target, { + key1: ['one', 'two'] + }) + const merged = deepmerge(target, src) + t.not(merged.key1, src.key1) + t.not(merged.key1, target.key1) + t.not(merged.key2, src.key2) + t.end() +}) + +test('should work on array of objects', function (t) { + const src = [ + { key1: ['one', 'three'], key2: ['one'] }, + { key3: ['five'] } + ] + const target = [ + { key1: ['one', 'two'] }, + { key3: ['four'] } + ] + + const expected = [ + { key1: ['one', 'two'] }, + { key3: ['four'] }, + { key1: ['one', 'three'], key2: ['one'] }, + { key3: ['five'] } + ] + + t.same(deepmerge(target, src), expected) + t.ok(Array.isArray(deepmerge(target, src)), 'result should be an array') + t.ok(Array.isArray(deepmerge(target, src)[0].key1), 'subkey should be an array too') + + t.end() +}) + +test('should work on array of objects with clone option', function (t) { + const src = [ + { key1: ['one', 'three'], key2: ['one'] }, + { key3: ['five'] } + ] + const target = [ + { key1: ['one', 'two'] }, + { key3: ['four'] } + ] + + const expected = [ + { key1: ['one', 'two'] }, + { key3: ['four'] }, + { key1: ['one', 'three'], key2: ['one'] }, + { key3: ['five'] } + ] + + const merged = deepmerge(target, src) + t.same(merged, expected) + t.ok(Array.isArray(deepmerge(target, src)), 'result should be an array') + t.ok(Array.isArray(deepmerge(target, src)[0].key1), 'subkey should be an array too') + t.not(merged[0].key1, src[0].key1) + t.not(merged[0].key1, target[0].key1) + t.not(merged[0].key2, src[0].key2) + t.not(merged[1].key3, src[1].key3) + t.not(merged[1].key3, target[1].key3) + t.end() +}) + +test('should treat regular expressions like primitive values', function (t) { + const target = { key1: /abc/ } + const src = { key1: /efg/ } + const expected = { key1: /efg/ } + + t.same(deepmerge(target, src), expected) + t.same(deepmerge(target, src).key1.test('efg'), true) + t.end() +}) + +test('should treat regular expressions like primitive values and should not' + + ' clone even with clone option', function (t) { + const target = { key1: /abc/ } + const src = { key1: /efg/ } + + const output = deepmerge(target, src) + + t.equal(output.key1, src.key1) + t.end() +} +) + +test('should treat dates like primitives', function (t) { + const monday = new Date('2016-09-27T01:08:12.761Z') + const tuesday = new Date('2016-09-28T01:18:12.761Z') + + const target = { + key: monday + } + const source = { + key: tuesday + } + + const expected = { + key: tuesday + } + const actual = deepmerge(target, source) + + t.same(actual, expected) + t.equal(actual.key.valueOf(), tuesday.valueOf()) + t.end() +}) + +test('should treat dates like primitives and should not clone even with clone' + + ' option', function (t) { + const monday = new Date('2016-09-27T01:08:12.761Z') + const tuesday = new Date('2016-09-28T01:18:12.761Z') + + const target = { + key: monday + } + const source = { + key: tuesday + } + + const actual = deepmerge(target, source) + + t.equal(actual.key, tuesday) + t.end() +}) + +test('should work on array with null in it', function (t) { + const target = [] + + const src = [null] + + const expected = [null] + + t.same(deepmerge(target, src), expected) + t.end() +}) + +test('should clone array\'s element if it is object', function (t) { + const a = { key: 'yup' } + const target = [] + const source = [a] + + const output = deepmerge(target, source) + + t.not(output[0], a) + t.equal(output[0].key, 'yup') + t.end() +}) + +test('should clone an array property when there is no target array', function (t) { + const someObject = {} + const target = {} + const source = { ary: [someObject] } + const output = deepmerge(target, source) + + t.same(output, { ary: [{}] }) + t.not(output.ary[0], someObject) + t.end() +}) + +test('should overwrite values when property is initialised but undefined', function (t) { + const target1 = { value: [] } + const target2 = { value: null } + const target3 = { value: 2 } + + const src = { value: undefined } + + function hasUndefinedProperty (o) { + t.ok(Object.hasOwnProperty.call(o, 'value')) + t.equal(typeof o.value, 'undefined') + } + + hasUndefinedProperty(deepmerge(target1, src)) + hasUndefinedProperty(deepmerge(target2, src)) + hasUndefinedProperty(deepmerge(target3, src)) + + t.end() +}) + +test('should overwrite null with the source', function (t) { + const expected = { a: 'string' } + const actual = deepmerge(null, { a: 'string' }) + + t.same(actual, expected) + t.end() +}) + +test('dates should copy correctly in an array', function (t) { + const monday = new Date('2016-09-27T01:08:12.761Z') + const tuesday = new Date('2016-09-28T01:18:12.761Z') + + const target = [monday, 'dude'] + const source = [tuesday, 'lol'] + + const expected = [monday, 'dude', tuesday, 'lol'] + const actual = deepmerge(target, source) + + t.same(actual, expected) + t.end() +}) + +test('merging objects with own __proto__ in target', function (t) { + const user = {} + const malicious = JSON.parse('{ "__proto__": { "admin": true } }') + const mergedObject = deepmerge(malicious, user) + t.notOk(mergedObject.__proto__.admin, 'non-plain properties should not be merged') // eslint-disable-line no-proto + t.notOk(mergedObject.admin, 'the destination should have an unmodified prototype') + t.end() +}) + +test('merging objects with own prototype in target', function (t) { + const user = {} + const malicious = JSON.parse('{ "prototype": { "admin": true } }') + const mergedObject = deepmerge(malicious, user) + t.notOk(mergedObject.admin, 'the destination should have an unmodified prototype') + t.end() +}) + +test('merging objects with own __proto__ in source', function (t) { + const user = {} + const malicious = JSON.parse('{ "__proto__": { "admin": true } }') + const mergedObject = deepmerge(user, malicious) + t.notOk(mergedObject.__proto__.admin, 'non-plain properties should not be merged') // eslint-disable-line no-proto + t.notOk(mergedObject.admin, 'the destination should have an unmodified prototype') + t.end() +}) + +test('merging objects with own prototype in source', function (t) { + const user = {} + const malicious = JSON.parse('{ "prototype": { "admin": true } }') + const mergedObject = deepmerge(user, malicious) + t.notOk(mergedObject.admin, 'the destination should have an unmodified prototype') + t.end() +}) + +test('merging objects with plain and non-plain properties in target', function (t) { + const parent = { + parentKey: 'should be undefined' + } + + const target = Object.create(parent) + target.plainKey = 'should be replaced' + + const source = { + parentKey: 'foo', + plainKey: 'bar', + newKey: 'baz' + } + + const mergedObject = deepmerge(target, source) + t.equal(undefined, mergedObject.parentKey, 'inherited properties of target should be removed, not merged or ignored') + t.equal('bar', mergedObject.plainKey, 'enumerable own properties of target should be merged') + t.equal('baz', mergedObject.newKey, 'properties not yet on target should be merged') + t.end() +}) + +test('merging objects with plain and non-plain properties in source', function (t) { + const parent = { + parentKey: 'should be foo' + } + + const source = Object.create(parent) + source.plainKey = 'bar' + + const target = { + parentKey: 'foo', + plainKey: 'should be bar', + newKey: 'baz' + } + + const mergedObject = deepmerge(target, source) + t.equal('foo', mergedObject.parentKey, 'inherited properties of source should not be merged') + t.equal('bar', mergedObject.plainKey, 'enumerable own properties of source should be merged') + t.equal('baz', mergedObject.newKey, 'properties set on target should not be modified') + t.end() +}) + +test('merging objects with null prototype', function (t) { + const target = Object.create(null) + const source = Object.create(null) + target.wheels = 4 + target.trunk = { toolbox: ['hammer'] } + source.trunk = { toolbox: ['wrench'] } + source.engine = 'v8' + const expected = { + wheels: 4, + engine: 'v8', + trunk: { + toolbox: ['hammer', 'wrench'] + } + } + + t.same(expected, deepmerge(target, source)) + t.end() +}) diff --git a/test/symbol.test.js b/test/symbol.test.js new file mode 100644 index 0000000..c169bd4 --- /dev/null +++ b/test/symbol.test.js @@ -0,0 +1,48 @@ +'use strict' + +// based on https://github.com/TehShrike/deepmerge/tree/3c39fb376158fa3cfc75250cfc4414064a90f582/test +// MIT License +// Copyright (c) 2012 - 2022 James Halliday, Josh Duff, and other contributors of deepmerge + +const deepmerge = require('../index')({ symbols: true }) +const test = require('tap').test + +test('copy symbol keys in target that do not exist on the target', function (t) { + const mySymbol = Symbol('test') + const src = { [mySymbol]: 'value1' } + const target = {} + + const res = deepmerge(target, src) + + t.equal(res[mySymbol], 'value1') + t.same(Object.getOwnPropertySymbols(res), Object.getOwnPropertySymbols(src)) + t.end() +}) + +test('copy symbol keys in target that do exist on the target', function (t) { + const mySymbol = Symbol('test') + const src = { [mySymbol]: 'value1' } + const target = { [mySymbol]: 'wat' } + + const res = deepmerge(target, src) + + t.equal(res[mySymbol], 'value1') + t.end() +}) + +test('does not copy enumerable symbol keys in source', function (t) { + const mySymbol = Symbol('test') + const src = { } + const target = { [mySymbol]: 'wat' } + + Object.defineProperty(src, mySymbol, { + value: 'value1', + writable: false, + enumerable: false + }) + + const res = deepmerge(target, src) + + t.equal(res[mySymbol], 'wat') + t.end() +}) diff --git a/types/.gitkeep b/types/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..1d175e2 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,66 @@ +declare function deepmerge(options: Options & { all: true }): DeepMergeAllFn; +declare function deepmerge(options?: Options): DeepMergeFn; + +type DeepMergeFn = (target: T1, source: T2) => DeepMerge; +type DeepMergeAllFn = >(...targets: T) => DeepMergeAll<{}, T>; + +type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint; + +type BuiltIns = Primitive | Date | RegExp; + +type MergeTypes = T extends Array + ? U extends Array + ? Array + : T + : U + +type DifferenceKeys< + T, + U, + T0 = Omit & Omit, + T1 = { [K in keyof T0]: T0[K] } + > = T1 + +type IntersectionKeys = Omit> + +type DeepMergeHelper< + T, + U, + T0 = DifferenceKeys + & { [K in keyof IntersectionKeys]: DeepMerge }, + T1 = { [K in keyof T0]: T0[K] } + > = T1 + +type DeepMerge = + U extends BuiltIns + ? U + : [T, U] extends [any[], any[]] + ? MergeTypes + : [T, U] extends [{ [key: string]: unknown }, { [key: string]: unknown }] + ? DeepMergeHelper + : U + +type First = T extends [infer _I, ...infer _Rest] ? _I : never; +type Rest = T extends [infer _I, ...infer _Rest] ? _Rest : never + +type DeepMergeAll = First extends never + ? R + : DeepMergeAll>, Rest>; + +interface Options { + symbols?: boolean; + all?: boolean; +} + +export default deepmerge +export { + deepmerge, + Options +} \ No newline at end of file diff --git a/types/index.test-d.ts b/types/index.test-d.ts new file mode 100644 index 0000000..7a68e0d --- /dev/null +++ b/types/index.test-d.ts @@ -0,0 +1,51 @@ +import { expectAssignable, expectError, expectType } from "tsd"; +import { deepmerge } from "."; + +expectType(deepmerge()({ a: 'a' }, { b: 'b' }).a) +expectType(deepmerge()({ a: 'a' }, { b: 'b' }).b) +expectType(deepmerge()({ a: 2 }, { b: 'b' }).a) +expectType(deepmerge()({ a: 2 }, { b: 'b' }).b) +expectType(deepmerge()({ a: 2 }, { a: 'b' }).a) + +expectError(deepmerge(1)) +expectError(deepmerge({ symbols: 2 })) +expectError(deepmerge({ symbol: 2 })) + +expectAssignable(deepmerge({ symbols: true })) + +expectType(deepmerge()('string', { a: 'string' }).a) +expectType(deepmerge()(1, { a: 'string' }).a) + +expectType(deepmerge()({ a: 'string' }, 'string')) +expectType(deepmerge()({ a: 'string' }, 1)) +expectType(deepmerge()({ a: 'string' }, new Date())) +expectType(deepmerge()({ a: 'string' }, /a/g)) +expectType<{}>(deepmerge()(/a/, {})) + +expectType(deepmerge()({ a: 'string' }, { a: 1 }).a) +expectType(deepmerge()({ a: 'string' }, { b: 1 }).a) +expectType(deepmerge()({ a: 'string' }, { }).a) +expectType(deepmerge()({ a: 'string' }, { b: 1 }).b) +expectType<{ a: string }>(deepmerge()({ a: { a: 'string' } }, { b: 1 }).a) +expectType<{ a: number }>(deepmerge()({ a: { a: 'string' } }, { a: { a: 1 } }).a) +expectType<{ a: number, b: string }>(deepmerge()({ a: { a: 'string' } }, { a: { a: 1, b: 'string' } }).a) +expectType<{ a: number, b: string }>(deepmerge()({ a: { a: { a: 'string' } } }, { a: { a: 1, b: 'string' } }).a) +expectType<{ a: { a: string, b: string } }>(deepmerge()({ a: { a: { a: 'string' } } }, { a: { a: { b: 'string' } } }).a) +expectType(deepmerge()({ a: [1,2,3,4] }, { a: 'string' }).a) +expectType(deepmerge()({ a: [1,2,3,4] }, { a: [1,2,3,4] }).a) +expectType<(number|string)[]>(deepmerge()({ a: [1,2,3,4] }, { a: ['a'] }).a) +expectType>(deepmerge()({ a: [1] }, { a: [2] }).a) +expectType<{b: number[]}>(deepmerge()({ a: {b: {}} }, { a: {b: [2]} }).a) +expectType<{b: Date}>(deepmerge()({ a: {b: {}} }, { a: {b: new Date()} }).a) +expectType<{b: RegExp}>(deepmerge()({ a: {b: {}} }, { a: {b: /abc/g } }).a) +expectType(deepmerge()({ a: {b: {}} }, new Date())) +expectType>(deepmerge()({ a: {b: {}} }, new Map())) + +expectAssignable(deepmerge({ all: true })) +expectAssignable(deepmerge({ all: true, symbols: true })) +expectType(deepmerge({all: true, symbols: true})({a: 'a'}).a) +expectType(deepmerge({all: true, symbols: true})({a: 'a'}, {b: 'a'}).a) +expectType(deepmerge({all: true, symbols: true})({a: 'a'}, {b: 'a'}).b) +expectType(deepmerge({all: true, symbols: true})({a: 'a'}, {a: 2}).a) +expectType(deepmerge({all: true, symbols: true})({a: 'a'}, 2)) +expectType(deepmerge({all: true, symbols: true})({a: 'a'}, 'string')) \ No newline at end of file