package/LICENSE000644 0000002063 3560116604 010266 0ustar00000000 000000 MIT License Copyright (c) 2021-2022 Peter Placzek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package/dist/index.cjs000644 0000022151 3560116604 012034 0ustar00000000 000000 'use strict'; var PriorityName = /*#__PURE__*/ function(PriorityName) { PriorityName["LEFT"] = "left"; PriorityName["RIGHT"] = "right"; return PriorityName; }({}); function isObject(item) { return !!item && typeof item === 'object' && !Array.isArray(item); } function isSafeKey(key) { return key !== '__proto__' && key !== 'prototype' && key !== 'constructor'; } function isEqual(x, y) { if (Object.is(x, y)) return true; if (x instanceof Date && y instanceof Date) { return x.getTime() === y.getTime(); } if (x instanceof RegExp && y instanceof RegExp) { return x.toString() === y.toString(); } if (isObject(x) && isObject(y)) { const keysX = Reflect.ownKeys(x); const keysY = Reflect.ownKeys(y); if (keysX.length !== keysY.length) { return false; } for(let i = 0; i < keysX.length; i++){ const key = keysX[i]; if (!Reflect.has(y, key) || !isEqual(x[key], y[key])) { return false; } } return true; } if (Array.isArray(x) && Array.isArray(y)) { if (x.length !== y.length) { return false; } for(let i = 0; i < x.length; i++){ if (!isEqual(x[i], y[i])) { return false; } } return true; } return false; } function distinctArray(arr) { for(let i = 0; i < arr.length; i++){ for(let j = arr.length - 1; j > i; j--){ if (isEqual(arr[i], arr[j])) { arr.splice(j, 1); } } } return arr; } /* 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'); })(); function polyfillClone(input) { const map = new WeakMap(); const fn = (value)=>{ if (Array.isArray(value)) { if (map.has(value)) { return map.get(value); } const cloned = []; map.set(value, cloned); value.map((el)=>cloned.push(fn(el))); return cloned; } if (isObject(value)) { if (map.has(value)) { return map.get(value); } const output = {}; const keys = Object.keys(value); map.set(value, output); for(let i = 0; i < keys.length; i++){ output[keys[i]] = fn(value[keys[i]]); } return output; } return value; }; return fn(input); } /* istanbul ignore next */ function clone(value) { if (gT.structuredClone) { return gT.structuredClone(value); } /* istanbul ignore next */ return polyfillClone(value); } // eslint-disable-next-line @typescript-eslint/ban-types function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } function buildOptions(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; } function togglePriority(priority) { return priority === PriorityName.LEFT ? `${PriorityName.RIGHT}` : `${PriorityName.LEFT}`; } function baseMerger(context, ...sources) { let target; let source; 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(); source = sources.pop(); } else { target = sources.shift(); source = sources.shift(); } if (!source) { if (Array.isArray(target) && context.options.arrayDistinct) { return distinctArray(target); } return target; } if (Array.isArray(target) && Array.isArray(source)) { target.push(...source); if (context.options.arrayPriority === PriorityName.RIGHT) { return baseMerger(context, ...sources, target); } return baseMerger(context, target, ...sources); } context.map.set(source, true); if (isObject(target) && isObject(source)) { const keys = Object.keys(source); for(let i = 0; i < keys.length; i++){ const key = keys[i]; if (hasOwnProperty(target, key)) { if (!isSafeKey(key)) { continue; } if (context.options.strategy) { const applied = context.options.strategy(target, key, source[key]); if (typeof applied !== 'undefined') { continue; } } if (isObject(target[key]) && isObject(source[key])) { if (context.map.has(source[key])) { const sourceKeys = Object.keys(source[key]); for(let j = 0; j < sourceKeys.length; j++){ if (isSafeKey(sourceKeys[j]) && !hasOwnProperty(target[key], sourceKeys[j])) { target[key][sourceKeys[j]] = source[key][sourceKeys[j]]; } } continue; } if (context.options.priority === PriorityName.RIGHT) { target[key] = baseMerger(context, source[key], target[key]); } else { target[key] = baseMerger(context, target[key], source[key]); } continue; } if (context.options.array && Array.isArray(target[key]) && Array.isArray(source[key])) { 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], source[key]) }); break; case PriorityName.RIGHT: Object.assign(target, { [key]: baseMerger(context, source[key], target[key]) }); break; } } } else { Object.assign(target, { [key]: source[key] }); } } } context.map = new WeakMap(); if (context.options.priority === PriorityName.RIGHT) { return baseMerger(context, ...sources, target); } return baseMerger(context, target, ...sources); } function createMerger(input) { const options = buildOptions(input); return (...sources)=>{ if (!sources.length) { throw new SyntaxError('At least one input element is required.'); } const ctx = { options, map: new WeakMap() }; if (options.clone) { return baseMerger(ctx, ...clone(sources)); } if (!options.inPlace) { 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({}); } } return baseMerger(ctx, ...sources); }; } const merge = createMerger(); /** * Assign source attributes to a target object. * * @param target * @param sources */ function assign(target, ...sources) { return createMerger({ inPlace: true, priority: 'left', array: false })(target, ...sources); } exports.PriorityName = PriorityName; exports.assign = assign; exports.buildOptions = buildOptions; exports.clone = clone; exports.createMerger = createMerger; exports.distinctArray = distinctArray; exports.hasOwnProperty = hasOwnProperty; exports.isEqual = isEqual; exports.isObject = isObject; exports.isSafeKey = isSafeKey; exports.merge = merge; exports.polyfillClone = polyfillClone; exports.togglePriority = togglePriority; //# sourceMappingURL=index.cjs.map package/package.json000644 0000004222 3560116604 011546 0ustar00000000 000000 { "name": "smob", "version": "1.6.1", "description": "Zero dependency library to safe merge objects.", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", "exports": { "./package.json": "./package.json", ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }, "files": [ "dist" ], "engines": { "node": ">=20.0.0" }, "scripts": { "build:types": "tsc --emitDeclarationOnly", "build:js": "rollup -c", "build": "rimraf dist && npm run build:types && npm run build:js", "commit": "npx git-cz", "lint": "eslint --ext .js,.ts ./src", "lint:fix": "npm run lint -- --fix", "test": "cross-env NODE_ENV=test vitest run --config test/vitest.config.ts", "test:coverage": "cross-env NODE_ENV=test vitest run --config test/vitest.config.ts --coverage", "prepare": "npx husky install", "prepublishOnly": "npm run build" }, "author": { "name": "Peter Placzek", "email": "contact@tada5hi.net", "url": "https://github.com/tada5hi" }, "license": "MIT", "keywords": [ "object", "object-merge", "merge", "safe", "deep-merge", "merge-deep" ], "repository": { "type": "git", "url": "git+https://github.com/Tada5hi/smob.git" }, "bugs": { "url": "https://github.com/Tada5hi/smob/issues" }, "homepage": "https://github.com/Tada5hi/smob#readme", "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.3", "@swc/core": "^1.15.11", "@tada5hi/commitlint-config": "^1.2.7", "@tada5hi/eslint-config-typescript": "^1.2.18", "@tada5hi/tsconfig": "^0.7.0", "@types/node": "^25.2.3", "@vitest/coverage-v8": "^4.0.18", "cross-env": "^10.1.0", "eslint": "^8.57.1", "husky": "^9.1.7", "rollup": "^4.57.1", "typescript": "^5.9.3", "vitest": "^4.0.18", "workspaces-publish": "^1.6.0" } } package/README.MD000644 0000007303 3560116604 010442 0ustar00000000 000000 # SMOB ๐Ÿงช [![npm version](https://badge.fury.io/js/smob.svg)](https://badge.fury.io/js/smob) [![main](https://github.com/tada5hi/smob/actions/workflows/main.yml/badge.svg)](https://github.com/tada5hi/smob/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/tada5hi/smob/branch/master/graph/badge.svg?token=0VL41WO0CG)](https://codecov.io/gh/tada5hi/smob) [![Known Vulnerabilities](https://snyk.io/test/github/Tada5hi/smob/badge.svg?targetFile=package.json)](https://snyk.io/test/github/Tada5hi/smob?targetFile=package.json) [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) A zero dependency library to **s**afe **m**erge **ob**jects and arrays with customizable behavior. **Table of Contents** - [Installation](#installation) - [Usage](#usage) - [Merger](#merger) - [Utils](#utils) - [License](#license) ## Installation ```bash npm install smob --save ``` ## Usage ```typescript import { merge } from "smob"; 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 object has by **default** the highest priority. The merge behaviour can be changed by creating a custom [merger](#merger). **Arguments** - sources `(any[] | Record)[]`: The source arrays/objects. ```typescript import { merge } from 'smob'; merge({ a: 1 }, { b: 2 }, { c: 3 }); // { a: 1, b: 2, c: 3 } merge(['foo'], ['bar']); // ['foo', 'bar'] ``` ### Merger A custom merger can simply be created by using the `createMerger` method. **Array** ```typescript import { createMerger } from 'smob'; const merge = createMerger({ array: false }); merge({ a: [1,2,3] }, { a: [4,5,6] }); // { a: [1,2,3] } ``` **ArrayDistinct** ```typescript import { createMerger } from 'smob'; const merge = createMerger({ arrayDistinct: true }); merge({ a: [1,2,3] }, { a: [3,4,5] }); // { a: [1,2,3,4,5] } ``` **Priority** ```typescript import { createMerger } from 'smob'; const merge = createMerger({ priority: 'right' }); merge({ a: 1 }, { a: 2 }, { a: 3 }) // { a: 3 } ``` **Strategy** ```typescript import { createMerger } from 'smob'; const merge = createMerger({ strategy: (target, key, value) => { if ( typeof target[key] === 'number' && typeof value === 'number' ) { target[key] += value; return target; } } }); merge({ a: 1 }, { a: 2 }, { a: 3 }); // { a: 6 } ``` A returned value indicates that the strategy has been applied. ## Utils ### distinctArray ```typescript import { distinctArray } from 'smob'; distnctArray(['foo', 'bar', 'foo']); // ['foo', 'bar'] ``` The function also removes non-primitive elements that are identical by value or reference. **Objects** ```typescript import { distinctArray } from 'smob'; distinctArray([{ foo: 'bar' }, { foo: 'bar' }]); // [{ foo: 'bar' }] ``` **Arrays** ```typescript import { distinctArray } from 'smob'; distinctArray([['foo', 'bar'], ['foo', 'bar']]); // [['foo', 'bar']] ``` ### isEqual Checks if two (non-primitive) elements are identical by value or reference. ````typescript import { isEqual } from 'smob'; isEqual({foo: 'bar'}, {foo: 'bar'}); // true isEqual(['foo', 'bar'], ['foo', 'bar']); // true ```` ## License Made with ๐Ÿ’š Published under [MIT License](./LICENSE). package/dist/index.mjs000644 0000021524 3560116604 012051 0ustar00000000 000000 var PriorityName = /*#__PURE__*/ function(PriorityName) { PriorityName["LEFT"] = "left"; PriorityName["RIGHT"] = "right"; return PriorityName; }({}); function isObject(item) { return !!item && typeof item === 'object' && !Array.isArray(item); } function isSafeKey(key) { return key !== '__proto__' && key !== 'prototype' && key !== 'constructor'; } function isEqual(x, y) { if (Object.is(x, y)) return true; if (x instanceof Date && y instanceof Date) { return x.getTime() === y.getTime(); } if (x instanceof RegExp && y instanceof RegExp) { return x.toString() === y.toString(); } if (isObject(x) && isObject(y)) { const keysX = Reflect.ownKeys(x); const keysY = Reflect.ownKeys(y); if (keysX.length !== keysY.length) { return false; } for(let i = 0; i < keysX.length; i++){ const key = keysX[i]; if (!Reflect.has(y, key) || !isEqual(x[key], y[key])) { return false; } } return true; } if (Array.isArray(x) && Array.isArray(y)) { if (x.length !== y.length) { return false; } for(let i = 0; i < x.length; i++){ if (!isEqual(x[i], y[i])) { return false; } } return true; } return false; } function distinctArray(arr) { for(let i = 0; i < arr.length; i++){ for(let j = arr.length - 1; j > i; j--){ if (isEqual(arr[i], arr[j])) { arr.splice(j, 1); } } } return arr; } /* 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'); })(); function polyfillClone(input) { const map = new WeakMap(); const fn = (value)=>{ if (Array.isArray(value)) { if (map.has(value)) { return map.get(value); } const cloned = []; map.set(value, cloned); value.map((el)=>cloned.push(fn(el))); return cloned; } if (isObject(value)) { if (map.has(value)) { return map.get(value); } const output = {}; const keys = Object.keys(value); map.set(value, output); for(let i = 0; i < keys.length; i++){ output[keys[i]] = fn(value[keys[i]]); } return output; } return value; }; return fn(input); } /* istanbul ignore next */ function clone(value) { if (gT.structuredClone) { return gT.structuredClone(value); } /* istanbul ignore next */ return polyfillClone(value); } // eslint-disable-next-line @typescript-eslint/ban-types function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } function buildOptions(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; } function togglePriority(priority) { return priority === PriorityName.LEFT ? `${PriorityName.RIGHT}` : `${PriorityName.LEFT}`; } function baseMerger(context, ...sources) { let target; let source; 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(); source = sources.pop(); } else { target = sources.shift(); source = sources.shift(); } if (!source) { if (Array.isArray(target) && context.options.arrayDistinct) { return distinctArray(target); } return target; } if (Array.isArray(target) && Array.isArray(source)) { target.push(...source); if (context.options.arrayPriority === PriorityName.RIGHT) { return baseMerger(context, ...sources, target); } return baseMerger(context, target, ...sources); } context.map.set(source, true); if (isObject(target) && isObject(source)) { const keys = Object.keys(source); for(let i = 0; i < keys.length; i++){ const key = keys[i]; if (hasOwnProperty(target, key)) { if (!isSafeKey(key)) { continue; } if (context.options.strategy) { const applied = context.options.strategy(target, key, source[key]); if (typeof applied !== 'undefined') { continue; } } if (isObject(target[key]) && isObject(source[key])) { if (context.map.has(source[key])) { const sourceKeys = Object.keys(source[key]); for(let j = 0; j < sourceKeys.length; j++){ if (isSafeKey(sourceKeys[j]) && !hasOwnProperty(target[key], sourceKeys[j])) { target[key][sourceKeys[j]] = source[key][sourceKeys[j]]; } } continue; } if (context.options.priority === PriorityName.RIGHT) { target[key] = baseMerger(context, source[key], target[key]); } else { target[key] = baseMerger(context, target[key], source[key]); } continue; } if (context.options.array && Array.isArray(target[key]) && Array.isArray(source[key])) { 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], source[key]) }); break; case PriorityName.RIGHT: Object.assign(target, { [key]: baseMerger(context, source[key], target[key]) }); break; } } } else { Object.assign(target, { [key]: source[key] }); } } } context.map = new WeakMap(); if (context.options.priority === PriorityName.RIGHT) { return baseMerger(context, ...sources, target); } return baseMerger(context, target, ...sources); } function createMerger(input) { const options = buildOptions(input); return (...sources)=>{ if (!sources.length) { throw new SyntaxError('At least one input element is required.'); } const ctx = { options, map: new WeakMap() }; if (options.clone) { return baseMerger(ctx, ...clone(sources)); } if (!options.inPlace) { 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({}); } } return baseMerger(ctx, ...sources); }; } const merge = createMerger(); /** * Assign source attributes to a target object. * * @param target * @param sources */ function assign(target, ...sources) { return createMerger({ inPlace: true, priority: 'left', array: false })(target, ...sources); } export { PriorityName, assign, buildOptions, clone, createMerger, distinctArray, hasOwnProperty, isEqual, isObject, isSafeKey, merge, polyfillClone, togglePriority }; //# sourceMappingURL=index.mjs.map package/dist/utils/array.d.ts000644 0000000077 3560116604 013277 0ustar00000000 000000 export declare function distinctArray(arr: T[]): T[]; package/dist/utils/check.d.ts000644 0000000301 3560116604 013224 0ustar00000000 000000 export declare function isObject(item: unknown): item is Record; export declare function isSafeKey(key: string): boolean; export declare function isEqual(x: any, y: any): boolean; package/dist/utils/clone.d.ts000644 0000000146 3560116604 013256 0ustar00000000 000000 export declare function polyfillClone(input: T): T; export declare function clone(value: T): T; package/dist/constants.d.ts000644 0000000114 3560116604 013025 0ustar00000000 000000 export declare enum PriorityName { LEFT = "left", RIGHT = "right" } package/dist/index.d.ts000644 0000000203 3560116604 012117 0ustar00000000 000000 export * from './constants'; export * from './module'; export * from './utils'; export * from './presets'; export * from './type'; package/dist/utils/index.d.ts000644 0000000200 3560116604 013254 0ustar00000000 000000 export * from './array'; export * from './check'; export * from './clone'; export * from './object'; export * from './options'; package/dist/module.d.ts000644 0000000234 3560116604 012301 0ustar00000000 000000 import type { Merger, OptionsInput } from './type'; export declare function createMerger(input?: OptionsInput): Merger; export declare const merge: Merger; package/dist/utils/object.d.ts000644 0000000175 3560116604 013426 0ustar00000000 000000 export declare function hasOwnProperty(obj: X, prop: Y): obj is X & Record; package/dist/utils/options.d.ts000644 0000000401 3560116604 013643 0ustar00000000 000000 import { PriorityName } from '../constants'; import type { Options, OptionsInput } from '../type'; export declare function buildOptions(options?: OptionsInput): Options; export declare function togglePriority(priority: `${PriorityName}`): "left" | "right"; package/dist/presets.d.ts000644 0000000431 3560116604 012500 0ustar00000000 000000 import type { MergerResult } from './type'; /** * Assign source attributes to a target object. * * @param target * @param sources */ export declare function assign, B extends Record[]>(target: A, ...sources: B): A & MergerResult; package/dist/type.d.ts000644 0000003324 3560116604 012000 0ustar00000000 000000 import type { PriorityName } from './constants'; type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; export type Options = { /** * Merge object array properties. * * default: true */ array: boolean; /** * Remove duplicates, when merging array elements. * * 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. * * @param target * @param key * @param value */ strategy?: (target: Record, key: string, value: unknown) => Record | undefined; /** * Merge sources in place. * * default: false */ inPlace?: boolean; /** * Deep clone input sources. * * default: false */ clone?: boolean; /** * Merge sources from left-to-right or right-to-left. * From v2 upwards default to right. * * default: left */ priority: `${PriorityName}`; }; export type OptionsInput = Partial; export type MergerSource = any[] | Record; export type MergerSourceUnwrap = T extends Array ? Return : T; export type MergerResult = UnionToIntersection>; export type MergerContext = { options: Options; map: WeakMap; }; export type Merger = (...sources: B) => MergerResult; export {};