Skip to content

Commit

Permalink
feat: deep clone input sources
Browse files Browse the repository at this point in the history
  • Loading branch information
tada5hi committed May 28, 2023
1 parent 57db735 commit 384acff
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 40 deletions.
80 changes: 49 additions & 31 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
*/

import { PriorityName } from './constants';

import type {
Merger,
MergerResult,
MergerSource,
MergerSourceUnwrap,
Options,
OptionsInput,
} from './type';

import {
buildOptions,
clone,
distinctArray,
hasOwnProperty,
isObject,
Expand All @@ -31,24 +33,14 @@ function baseMerger<B extends MergerSource[]>(
throw new SyntaxError('At least one input element is required.');
}

let target : B;
let source : B | undefined;
let target : MergerSourceUnwrap<B>;
let source : MergerSourceUnwrap<B> | undefined;
if (options.priority === PriorityName.RIGHT) {
target = options.modifyTarget ?
sources.pop() as B :
structuredClone(sources.pop() as B);

source = options.cloneSource ?
sources.pop() as B :
structuredClone(sources.pop() as B);
target = sources.pop() as MergerSourceUnwrap<B>;
source = sources.pop() as MergerSourceUnwrap<B>;
} else {
target = options.modifyTarget ?
sources.shift() as B :
structuredClone(sources.shift() as B);

source = options.cloneSource ?
sources.shift() as B :
structuredClone(sources.shift() as B);
target = sources.shift() as MergerSourceUnwrap<B>;
source = sources.shift() as MergerSourceUnwrap<B>;
}

if (!source) {
Expand All @@ -66,21 +58,19 @@ function baseMerger<B extends MergerSource[]>(
Array.isArray(target) &&
Array.isArray(source)
) {
if (options.modifyTarget) {
target.push(...source as MergerSource[]);
}
target.push(...source as MergerSource[]);

if (options.priority === PriorityName.RIGHT) {
return baseMerger(
options,
...sources,
options.modifyTarget ? target as B : target.concat(source) as B,
target,
) as MergerResult<B>;
}

return baseMerger(
options,
options.modifyTarget ? target as B : target.concat(source) as B,
target,
...sources,
) as MergerResult<B>;
}
Expand All @@ -91,13 +81,13 @@ function baseMerger<B extends MergerSource[]>(
) {
const keys = Object.keys(source);
for (let i = 0; i < keys.length; i++) {
const key = keys[i] as (keyof B);

if (!isSafeKey(key as string)) {
continue;
}
const key = keys[i] as (keyof MergerSourceUnwrap<B>);

if (hasOwnProperty(target, key)) {
if (!isSafeKey(key as string)) {
continue;
}

if (options.strategy) {
const applied = options.strategy(target, key as string, source[key]);
if (typeof applied !== 'undefined') {
Expand All @@ -114,9 +104,17 @@ function baseMerger<B extends MergerSource[]>(
}

if (options.priority === PriorityName.RIGHT) {
target[key] = baseMerger(options, source[key] as MergerSource, target[key] as MergerSource) as B[keyof B];
target[key] = baseMerger(
options,
source[key] as MergerSource,
target[key] as MergerSource,
) as MergerSourceUnwrap<B>[keyof MergerSourceUnwrap<B>];
} else {
target[key] = baseMerger(options, target[key] as MergerSource, source[key] as MergerSource) as B[keyof B];
target[key] = baseMerger(
options,
target[key] as MergerSource,
source[key] as MergerSource,
) as MergerSourceUnwrap<B>[keyof MergerSourceUnwrap<B>];
}

continue;
Expand Down Expand Up @@ -160,7 +158,27 @@ export function createMerger(input?: OptionsInput) : Merger {

return <B extends MergerSource[]>(
...sources: B
) : MergerResult<B> => baseMerger(options, ...sources);
) : MergerResult<B> => {
if (options.clone) {
return baseMerger(options, ...clone(sources));
}

if (!options.inPlace) {
if (options.priority === PriorityName.LEFT) {
if (Array.isArray(sources[0])) {
sources.unshift([]);
} else {
sources.unshift({});
}
} else if (Array.isArray(sources[0])) {
sources.push([]);
} else {
sources.push({});
}
}

return baseMerger(options, ...sources);
};
}

export const merge = createMerger();
Expand All @@ -170,7 +188,7 @@ export function assign<A extends Record<string, any>, B extends Record<string, a
...sources: B
) : A & MergerResult<B> {
return createMerger({
modifyTarget: true,
inPlace: true,
priority: 'left',
array: false,
})(target, ...sources) as A & MergerResult<B>;
Expand Down
26 changes: 24 additions & 2 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,33 @@ export type MergerResult<B extends MergerSource> = UnionToIntersection<MergerSou
export type Merger = <B extends MergerSource[]>(...sources: B) => MergerResult<B>;

export type Options = {
/**
* Merge array object attributes?
*/
array: boolean,
/**
* Remove duplicates in array.
*/
arrayDistinct: boolean,
/**
* Strategy to merge different object keys.
*
* @param target
* @param key
* @param value
*/
strategy?: (target: Record<string, any>, key: string, value: unknown) => Record<string, any> | undefined,
modifyTarget?: boolean,
cloneSource?: boolean,
/**
* Merge sources in place.
*/
inPlace?: boolean
/**
* Deep clone input arrays/objects.
*/
clone?: boolean,
/**
* Merge sources from left-right or left-right.
*/
priority: `${PriorityName}`
};

Expand Down
47 changes: 47 additions & 0 deletions src/utils/clone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2023.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/

import { isObject } from './check';

/* istanbul ignore next */
const gT = (() => {
if (typeof globalThis !== 'undefined') {
return globalThis;
}

// eslint-disable-next-line no-restricted-globals
if (typeof self !== 'undefined') {
// eslint-disable-next-line no-restricted-globals
return self;
}

if (typeof window !== 'undefined') {
return window;
}

if (typeof global !== 'undefined') {
return global;
}

throw new Error('unable to locate global object');
})();

export function clone<T>(value: T) : T {
if (gT.structuredClone) {
return gT.structuredClone(value);
}

if (isObject(value)) {
return { ...value };
}

if (Array.isArray(value)) {
return [...value] as T;
}

return value;
}
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

export * from './array';
export * from './check';
export * from './clone';
export * from './cut';
export * from './has-own-property';
export * from './options';
2 changes: 1 addition & 1 deletion src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function buildOptions(options?: OptionsInput) : Options {

options.array = options.array ?? true;
options.arrayDistinct = options.arrayDistinct ?? false;
options.modifyTarget = options.modifyTarget ?? false;
options.inPlace = options.inPlace ?? false;
options.priority = options.priority || PriorityName.LEFT;

return options as Options;
Expand Down
10 changes: 5 additions & 5 deletions test/unit/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ describe('src/module/*.ts', () => {
bar: 'baz'
}

const merger = createMerger({ modifyTarget: false });
const merger = createMerger({ inPlace: false, clone: true });
expect(merger({foo: x}, { foo: y })).toEqual({foo: {foo: 'bar', bar: 'baz'}});

expect(x).toEqual({foo: 'bar'});
Expand All @@ -250,7 +250,7 @@ describe('src/module/*.ts', () => {
bar: 'baz'
}

const merger = createMerger({ modifyTarget: false, priority: 'right' });
const merger = createMerger({ inPlace: false, clone: true, priority: 'right' });
expect(merger({foo: x}, { foo: y })).toEqual({foo: {foo: 'bar', bar: 'baz'}});

expect(x).toEqual({foo: 'bar'});
Expand All @@ -266,7 +266,7 @@ describe('src/module/*.ts', () => {
bar: 'baz'
}

const merger = createMerger({ modifyTarget: true });
const merger = createMerger({ inPlace: true });
expect(merger({foo: x}, { foo: y })).toEqual({foo: {foo: 'bar', bar: 'baz'}});

expect(x).toEqual({foo: 'bar', bar: 'baz'});
Expand All @@ -282,7 +282,7 @@ describe('src/module/*.ts', () => {
bar: 'baz'
}

const merger = createMerger({ modifyTarget: true, priority: 'right' });
const merger = createMerger({ inPlace: true, priority: 'right' });
expect(merger({foo: x}, { foo: y })).toEqual({foo: {foo: 'bar', bar: 'baz'}});

expect(x).toEqual({foo: 'bar' });
Expand All @@ -300,7 +300,7 @@ describe('src/module/*.ts', () => {
foo: xB
}

const merger = createMerger({ modifyTarget: true, priority: 'right' });
const merger = createMerger({ inPlace: true, priority: 'right' });
expect(merger({foo: x}, { foo: y })).toEqual({foo: {foo: ['baz', 'bar'] }});

expect(x).toEqual({foo: ['bar'] });
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "@tada5hi/tsconfig",
"compilerOptions": {
"lib": [
"ESNext"
"ESNext",
"DOM"
],
"module": "ESNext",
"outDir": "dist",
Expand Down

0 comments on commit 384acff

Please sign in to comment.