data_deep-merge.js

import defaultIsMergeableObject from './is-mergeable-object';
import isFunction from './is-function';

/**
 * @module deepMerge
 * @description Merge two or more objects deeply.
 *
 */

function emptyTarget( val ) { // eslint-disable-line jsdoc/require-jsdoc
	return Array.isArray( val ) ? [] : {};
}

function cloneUnlessOtherwiseSpecified( value, options ) { // eslint-disable-line jsdoc/require-jsdoc
	return ( options.clone !== false && options.isMergeableObject( value ) )
		? deepMerge( emptyTarget( value ), value, options )
		: value;
}

function defaultArrayMerge( target, source, options ) { // eslint-disable-line jsdoc/require-jsdoc
	return target.concat( source ).map( function( element ) {
		return cloneUnlessOtherwiseSpecified( element, options );
	} );
}

function combineArrayMerge( target, source, options ) { // eslint-disable-line jsdoc/require-jsdoc
	const destination = target.slice();

	source.forEach( ( item, index ) => {
		if ( typeof destination[ index ] === 'undefined' ) {
			destination[ index ] = options.cloneUnlessOtherwiseSpecified( item, options );
		} else if ( options.isMergeableObject( item ) ) {
			destination[ index ] = deepMerge( target[ index ], item, options );
		} else if ( target.indexOf( item ) === -1 ) {
			destination.push( item );
		}
	} );
	return destination;
}

function getArrayMergeType( options ) { // eslint-disable-line jsdoc/require-jsdoc
	let arrayMergeType = defaultArrayMerge;
	if ( options.arrayMerge === 'combine' ) {
		arrayMergeType = combineArrayMerge;
	} else if ( isFunction( options.arrayMerge ) ) {
		arrayMergeType = options.arrayMerge;
	}
	return arrayMergeType;
}

function getMergeFunction( key, options ) { // eslint-disable-line jsdoc/require-jsdoc
	if ( ! options.customMerge ) {
		return deepMerge;
	}
	const customMerge = options.customMerge( key );
	return typeof customMerge === 'function' ? customMerge : deepMerge;
}

function getEnumerableOwnPropertySymbols( target ) { // eslint-disable-line jsdoc/require-jsdoc
	return Object.getOwnPropertySymbols
		? Object.getOwnPropertySymbols( target ).filter( function( symbol ) {
			return target.propertyIsEnumerable( symbol ); // eslint-disable-line
		} )
		: [];
}

function getKeys( target ) { // eslint-disable-line jsdoc/require-jsdoc
	return Object.keys( target ).concat( getEnumerableOwnPropertySymbols( target ) );
}

function propertyIsOnObject( object, property ) { // eslint-disable-line jsdoc/require-jsdoc
	try {
		return property in object;
	} catch ( _ ) {
		return false;
	}
}

// Protects from prototype poisoning and unexpected merging up the prototype chain.
function propertyIsUnsafe( target, key ) { // eslint-disable-line jsdoc/require-jsdoc
	return propertyIsOnObject( target, key ) && // Properties are safe to merge if they don't exist in the target yet,
		! ( Object.hasOwnProperty.call( target, key ) && // unsafe if they exist up the prototype chain,
			Object.propertyIsEnumerable.call( target, key ) ); // and also unsafe if they're nonenumerable.
}

function mergeObject( target, source, options ) { // eslint-disable-line jsdoc/require-jsdoc
	const destination = {};
	if ( options.isMergeableObject( target ) ) {
		getKeys( target ).forEach( function( key ) {
			destination[ key ] = cloneUnlessOtherwiseSpecified( target[ key ], options );
		} );
	}
	getKeys( source ).forEach( function( key ) {
		if ( propertyIsUnsafe( target, key ) ) {
			return;
		}

		if ( propertyIsOnObject( target, key ) && options.isMergeableObject( source[ key ] ) ) {
			destination[ key ] = getMergeFunction( key, options )( target[ key ], source[ key ], options );
		} else {
			destination[ key ] = cloneUnlessOtherwiseSpecified( source[ key ], options );
		}
	} );
	return destination;
}

/**
 * @description Merge two objects x and y deeply, returning a new merged object with the elements from both x and y.
 * If an element at the same key is present for both x and y, the value from y will appear in the result.
 * Merging creates a new object, so that neither x or y is modified.
 * Note: By default, arrays are merged by concatenating them.
 *
 * Taken and adapted from https://www.npmjs.com/package/deepmerge
 *
 * @since 1.0.0
 *
 * @param {Array|object} target  The original array or object to merge to.
 * @param {Array|object} source  The new array or object that will get priority when keys match.
 * @param {object}       options Options object.
 *
 * @requires isFunction
 * @requires defaultIsMergeableObject
 *
 * @return {Array|object} Returns a new array or object, after merging target and source.
 *
 * @example
 * import { deepMerge } from "@gravityforms/utils";
 *
 * function Example() {
 * 	const target = {
 * 		foo: { bar: 3 },
 * 		array: [ 1, 2, 3 ]
 * 	};
 *
 * 	const source = {
 * 		foo: { baz: 4, bar: 5 },
 * 		quux: 5,
 * 		array: [ 4, 5, 6 ],
 * 	};
 *
 * 	const output = deepMerge( target, source );
 * }
 *
 */
function deepMerge( target, source, options = {} ) {
	options.arrayMerge = getArrayMergeType( options );
	options.isMergeableObject = options.isMergeableObject || defaultIsMergeableObject;
	// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
	// implementations can use it. The caller may not replace it.
	options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;

	const sourceIsArray = Array.isArray( source );
	const targetIsArray = Array.isArray( target );
	const sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;

	if ( ! sourceAndTargetTypesMatch ) {
		return cloneUnlessOtherwiseSpecified( source, options );
	} else if ( sourceIsArray ) {
		return options.arrayMerge( target, source, options );
	}
	return mergeObject( target, source, options );
}

/**
 * @function all
 * @description Merges an array of arrays or objects using `deepMerge` into one array or object.
 *
 * @param {Array}  array   The array of arrays or objects to deep merge.
 * @param {object} options Options object.
 *
 * @return {Array|object} Returns a new array or object, after merging all items in the array.
 *
 * @example
 * import { deepMerge } from "@gravityforms/utils";
 *
 * function Example() {
 * 	const array = [
 * 		{
 * 			foo: { bar: 3 },
 * 			array: [ 1, 2, 3 ],
 * 		},
 * 		{
 * 			foo: { baz: 4, bar: 5 },
 * 			quux: 5,
 * 			array: [ 4, 5, 6 ],
 * 		},
 * 		{
 * 			foo: { baz: 1 },
 * 			array: [ 7, 8, 9 ],
 * 		},
 * 	];
 *
 * 	const output = deepMerge.all( array );
 * }
 *
 */
deepMerge.all = function deepMergeAll( array, options ) {
	if ( ! Array.isArray( array ) ) {
		throw new Error( 'first argument should be an array' );
	}

	return array.reduce( function( prev, next ) {
		return deepMerge( prev, next, options );
	}, {} );
};

export default deepMerge;