diff --git a/README.MD b/README.MD index cb8eefd..cd67550 100644 --- a/README.MD +++ b/README.MD @@ -33,9 +33,10 @@ const output = merge(...sources); The following merge options are set by default: - **array**: `true` Merge object array properties. - **arrayDistinct**: `false` Remove duplicates, when merging array elements. +- **arrayPriority**: `left` (options.priority) The source aka leftmost array has by **default** the highest priority. - **clone**: `false` Deep clone input sources. - **inPlace**: `false` Merge sources in place. -- **priority**: `left` The source aka leftmost array/object has by **default** the highest priority. +- **priority**: `left` The source aka leftmost object has by **default** the highest priority. The merge behaviour can be changed by creating a custom [merger](#merger). diff --git a/src/module.ts b/src/module.ts index 92a1210..b85fddb 100644 --- a/src/module.ts +++ b/src/module.ts @@ -13,7 +13,7 @@ import { distinctArray, hasOwnProperty, isObject, - isSafeKey, + isSafeKey, togglePriority, } from './utils'; function baseMerger( @@ -22,7 +22,18 @@ function baseMerger( ) : MergerResult { let target : MergerSourceUnwrap; let source : MergerSourceUnwrap | undefined; - if (context.options.priority === PriorityName.RIGHT) { + + let { priority } = context.options; + if (sources.length >= 2) { + if ( + Array.isArray(sources.at(0)) && + Array.isArray(sources.at(-1)) + ) { + priority = context.options.arrayPriority; + } + } + + if (priority === PriorityName.RIGHT) { target = sources.pop() as MergerSourceUnwrap; source = sources.pop() as MergerSourceUnwrap; } else { @@ -47,7 +58,7 @@ function baseMerger( ) { target.push(...source as MergerSource[]); - if (context.options.priority === PriorityName.RIGHT) { + if (context.options.arrayPriority === PriorityName.RIGHT) { return baseMerger( context, ...sources, @@ -124,7 +135,11 @@ function baseMerger( Array.isArray(target[key]) && Array.isArray(source[key]) ) { - switch (context.options.priority) { + const arrayPriority = context.options.priority !== context.options.arrayPriority ? + togglePriority(context.options.arrayPriority) : + context.options.arrayPriority; + + switch (arrayPriority) { case PriorityName.LEFT: Object.assign(target, { [key]: baseMerger(context, target[key] as MergerSource, source[key] as MergerSource), @@ -174,14 +189,24 @@ export function createMerger(input?: OptionsInput) : Merger { } if (!options.inPlace) { - if (options.priority === PriorityName.LEFT) { - if (Array.isArray(sources[0])) { - sources.unshift([]); - } else { - sources.unshift({}); - } - } else if (Array.isArray(sources[0])) { + if ( + Array.isArray(sources.at(0)) && + options.arrayPriority === PriorityName.LEFT + ) { + sources.unshift([]); + return baseMerger(ctx, ...sources); + } + + if ( + Array.isArray(sources.at(-1)) && + options.arrayPriority === PriorityName.RIGHT + ) { sources.push([]); + return baseMerger(ctx, ...sources); + } + + if (options.priority === PriorityName.LEFT) { + sources.unshift({}); } else { sources.push({}); } diff --git a/src/type.ts b/src/type.ts index 4d2bed8..5ca6f10 100644 --- a/src/type.ts +++ b/src/type.ts @@ -16,6 +16,13 @@ export type Options = { * default: false */ arrayDistinct: boolean, + /** + * Merge sources from left-to-right or right-to-left. + * From v2 upwards default to left independent of the option priority. + * + * default: left (aka. options.priority) + */ + arrayPriority: `${PriorityName}`, /** * Strategy to merge different object keys. * @@ -38,6 +45,7 @@ export type Options = { clone?: boolean, /** * Merge sources from left-to-right or right-to-left. + * From v2 upwards default to right. * * default: left */ diff --git a/src/utils/options.ts b/src/utils/options.ts index 4ced836..3f66a2e 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -1,14 +1,19 @@ import { PriorityName } from '../constants'; import type { Options, OptionsInput } from '../type'; -export function buildOptions(options?: OptionsInput) : Options { - options = options || {}; - +export function buildOptions(options: OptionsInput = {}) : Options { options.array = options.array ?? true; options.arrayDistinct = options.arrayDistinct ?? false; options.clone = options.clone ?? false; options.inPlace = options.inPlace ?? false; options.priority = options.priority || PriorityName.LEFT; + options.arrayPriority = options.arrayPriority || options.priority; return options as Options; } + +export function togglePriority(priority: `${PriorityName}`) { + return priority === PriorityName.LEFT ? + `${PriorityName.RIGHT}` : + `${PriorityName.LEFT}`; +} diff --git a/test/unit/module.spec.ts b/test/unit/module.spec.ts index 52d26c4..4b5b41f 100644 --- a/test/unit/module.spec.ts +++ b/test/unit/module.spec.ts @@ -239,12 +239,73 @@ describe('src/module/*.ts', () => { }); it('should merge arrays with right priority', () => { - const merger = createMerger({ priority: 'right' }); - expect(merger([4, 5, 6], [1, 2, 3, 4])).toEqual([1, 2, 3, 4, 4, 5, 6]); + const merger = createMerger({ arrayPriority: 'right' }); + expect(merger([4, 5, 6], [1, 2, 3])).toEqual([1, 2, 3, 4, 5, 6]); expect(merger({ foo: [4, 5, 6] }, { foo: [1, 2, 3, 4] })).toEqual({ foo: [1, 2, 3, 4, 4, 5, 6] }); }); + it('should merge with different priorities for arrays and objects', () => { + const merger = createMerger({ + arrayPriority: 'left', + priority: 'right', + }); + + expect(merger({ foo: [1, 2, 3], bar: 'baz' }, { foo: [4, 5, 6], bar: 'boz' })).toEqual({ + foo: [1, 2, 3, 4, 5, 6], + bar: 'boz', + }); + + expect(merger( + { + foo: [1, 2, 3], + bar: 'baz', + biz: 'bar', + boz: 'baz', + }, + { + foo: [4, 5, 6], + bar: 'biz', + biz: 'boz', + }, + { + foo: [7, 8, 9], + bar: 'boz', + }, + )).toEqual({ + foo: [1, 2, 3, 4, 5, 6, 7, 8, 9], + bar: 'boz', + biz: 'boz', + boz: 'baz', + }); + + expect(merger( + { + foo: { + bar: [1, 2, 3], + }, + bar: 'baz', + }, + { + foo: { + bar: [4, 5, 6], + }, + bar: 'boz', + }, + { + foo: { + bar: [7, 8, 9], + }, + bar: 'biz', + }, + )).toEqual({ + foo: { + bar: [1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + bar: 'biz', + }); + }); + it('should merge with destruction', () => { const x = { foo: 'bar',