diff --git a/index.js b/index.js index 0ac71be..962a6d9 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,10 @@ function emptyTarget(val) { return Array.isArray(val) ? [] : {} } +function firstArrayEntry(arr) { + return arr && arr.length ? arr[0] : [] +} + function cloneUnlessOtherwiseSpecified(value, options) { return (options.clone !== false && options.isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, options) @@ -51,8 +55,21 @@ function propertyIsUnsafe(target, key) { && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable. } +// Retrieves either a new object or the appropriate target object to mutate. +function getDestinationObject(target, source, options) { + const targetDefined = typeof target !== 'undefined' + const isArray = Array.isArray(target) || Array.isArray(source) + const doMerge = options && (options.mergeWithTarget || options.clone === false) + + if (targetDefined && doMerge) { + return Array.isArray(target) ? firstArrayEntry(target) : target + } + + return isArray ? [] : {}; +} + function mergeObject(target, source, options) { - var destination = {} + var destination = getDestinationObject(target, source, options) if (options.isMergeableObject(target)) { getKeys(target).forEach(function(key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], options) @@ -100,7 +117,7 @@ deepmerge.all = function deepmergeAll(array, options) { return array.reduce(function(prev, next) { return deepmerge(prev, next, options) - }, {}) + }, getDestinationObject(array, undefined, options)) } module.exports = deepmerge diff --git a/test/merge.js b/test/merge.js index fa2ac5b..c34d2b5 100644 --- a/test/merge.js +++ b/test/merge.js @@ -1,6 +1,63 @@ var merge = require('../') var test = require('tape') +test('should handle an undefined value in the target object when merging', function(t) { + var src = { key1: 'value1', key2: { key4: 'value4'}, key3: ['value3'], key5: undefined } + var target = { key1: 'value', key2: undefined, key3: undefined, key5: ['value5'], key6: ['value6'], key7: { key8: 'value8'} } + + var notClonedRes = merge(target, src, {mergeWithTarget: true}) + + // Undefined target + t.assert(notClonedRes.key2 === target.key2, 'should merge object source into undefined value'); + t.assert(notClonedRes.key3 === target.key3, 'should merge array source into undefined target'); + t.assert(typeof notClonedRes.key2 === 'object', 'should retain object type when merging into undefined target'); + t.assert(Array.isArray(notClonedRes.key3), 'should retain array type when merging into undefined target'); + + // Explicit undefined source + t.assert(typeof key5 === 'undefined', 'should overwrite target value with explicitly undefined source value'); + + // Not defined source props + t.assert(Array.isArray(notClonedRes.key6), 'should preserve target property value when no source value exists'); + t.assert(typeof notClonedRes.key7 === 'object', 'should preserve target property value when no source value exists'); + t.end() +}) + +test('result should retain target type information when mergeWithTarget set to true', function(t) { + var src = { key1: 'value1', key2: 'value2' } + class CustomType {} + var target = new CustomType() + + var res = merge(target, src, {mergeWithTarget: true}) + t.not(src instanceof CustomType) + t.assert(target instanceof CustomType) + t.assert(res instanceof CustomType) + t.end() +}) + +test('modify target object if mergeWithTarget set to true', function(t) { + var src = { key1: 'value1', key2: 'value2' } + var target = { key3: 'value3'} + + var clonedRes = merge(target, src) + var notClonedRes = merge(target, src, {mergeWithTarget: true}) + + t.assert(clonedRes !== target, 'result should be cloned') + t.assert(notClonedRes === target, 'result should maintain target reference') + t.end() +}) + +test('merge.all mutates target object when mergeWithTarget set to true', function(t) { + var src = { key1: 'value1', key2: 'value2' } + var target = { key3: 'value3'} + + var clonedRes = merge.all([target, src]) + var notClonedRes = merge.all([target, src], {mergeWithTarget: true}) + + t.assert(clonedRes !== target, 'result should be cloned') + t.assert(notClonedRes === target, 'result should maintain first array entry reference') + t.end() +}) + test('add keys in target that do not exist at the root', function(t) { var src = { key1: 'value1', key2: 'value2' } var target = {}