From e80d585ea2a8c43eaec7ef6b2089164ff884a99d Mon Sep 17 00:00:00 2001 From: TehShrike Date: Tue, 19 Jan 2021 20:49:23 -0600 Subject: [PATCH] Un-deprecate clone, make it not a liar about the root object Fixes #208 Potentially fixes #186 Fixes #163 Potentially fixes #212 --- changelog.md | 1 + index.js | 38 ++++++++++++++++++++++++++------------ readme.md | 5 +---- test/merge.js | 22 ++++++++++++++++++++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index 96dff46..a5c256b 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - Breaking: by default, only [plain objects](https://github.com/sindresorhus/is-plain-obj/#is-plain-obj-) will have their properties merged, with all other values being copied to the target. [#152](https://github.com/TehShrike/deepmerge/issues/152) - Breaking: the `isMergeableObject` option is renamed to `isMergeable` [#168](https://github.com/TehShrike/deepmerge/pull/168) - Fixed: the options argument is no longer mutated (again) [#173](https://github.com/TehShrike/deepmerge/pull/173) +- Breaking+fixed: setting `clone` to `false` will cause values to be copied directly onto the destination object rather than cloning the destination and only mutating child properties. # [4.2.2](https://github.com/TehShrike/deepmerge/releases/tag/v4.2.2) diff --git a/index.js b/index.js index 2600ec5..033b8cf 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ const defaultIsMergeable = value => Array.isArray(value) || isPlainObj(value) const emptyTarget = value => Array.isArray(value) ? [] : {} const cloneUnlessOtherwiseSpecified = (value, options) => { - return (options.clone !== false && options.isMergeable(value)) + return (options.clone && options.isMergeable(value)) ? deepmerge(emptyTarget(value), value, options) : value } @@ -48,7 +48,7 @@ const propertyIsUnsafe = (target, key) => { } const mergeObject = (target, source, options) => { - const destination = {} + const destination = options.clone ? emptyTarget(target) : target if (options.isMergeable(target)) { getKeys(target) @@ -70,15 +70,18 @@ const mergeObject = (target, source, options) => { return destination } +const cloneOptionsWithDefault = inputOptions => ({ + arrayMerge: defaultArrayMerge, + isMergeable: defaultIsMergeable, + clone: true, + ...inputOptions, + // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() + // implementations can use it. The caller may not replace it. + cloneUnlessOtherwiseSpecified: cloneUnlessOtherwiseSpecified +}) + const deepmerge = (target, source, inputOptions) => { - const options = { - arrayMerge: defaultArrayMerge, - isMergeable: defaultIsMergeable, - ...inputOptions, - // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() - // implementations can use it. The caller may not replace it. - cloneUnlessOtherwiseSpecified: cloneUnlessOtherwiseSpecified - } + const options = cloneOptionsWithDefault(inputOptions) const sourceIsArray = Array.isArray(source) const targetIsArray = Array.isArray(target) @@ -92,12 +95,23 @@ const deepmerge = (target, source, inputOptions) => { return mergeObject(target, source, options) } -deepmerge.all = (array, options) => { +deepmerge.all = (array, inputOptions) => { if (!Array.isArray(array)) { throw new Error(`first argument should be an array`) } - return array.reduce((prev, next) => deepmerge(prev, next, options), {}) + const options = cloneOptionsWithDefault(inputOptions) + + if (array.length === 0) { + return {} + } else if (array.length === 1) { + const value = array[0] + return options.clone + ? deepmerge(emptyTarget(value), value, options) + : value + } + + return array.reduce((prev, next) => deepmerge(prev, next, options)) } module.exports = deepmerge diff --git a/readme.md b/readme.md index 196af72..c315fca 100644 --- a/readme.md +++ b/readme.md @@ -249,12 +249,9 @@ result.pets // => ['Cat', 'Parrot', 'Dog'] ### `clone` -*Deprecated.* - Defaults to `true`. -If `clone` is `false` then child objects will be copied directly instead of being cloned. This was the default behavior before version 2.x. - +If `clone` is `false` then child properties will be copied directly onto targets without cloning the target. # Testing diff --git a/test/merge.js b/test/merge.js index acf0fa6..789d2f8 100644 --- a/test/merge.js +++ b/test/merge.js @@ -685,3 +685,25 @@ test('should not mutate options', function(t) { t.deepEqual(options, {}) t.end() }) + +test('With clone: false, merge should not clone the target root', t => { + const destination = {} + const output = merge(destination, { + sup: true + }, { clone: false }) + + t.equal(destination, output) + t.end() +}) + +test('With clone: false, merge.all should not clone the target root', t => { + const destination = {} + const output = merge.all([ + destination, { + sup: true + } + ], { clone: false }) + + t.equal(destination, output) + t.end() +})