GithubHelp home page GithubHelp logo

mesqueeb / merge-anything Goto Github PK

View Code? Open in Web Editor NEW
233.0 1.0 11.0 1.03 MB

Merge objects & other types recursively. A simple & small integration.

Home Page: https://npmjs.com/merge-anything

License: MIT License

JavaScript 2.65% TypeScript 97.35%
javascript merge deepmerge recursively object-assign deep-assign nested-assign typescript deep-merge merge-object

merge-anything's Introduction

Merge anything πŸ₯‘

Total Downloads Latest Stable Version

npm i merge-anything

Merge objects & other types recursively. Fully TypeScript supported! A simple & small integration.

Motivation

I created this package because I tried a lot of similar packages that do merging/deepmerging/recursive object assign etc. But all had its quirks, and all of them break things they are not supposed to break... 😞

I was looking for:

  • a simple merge function like Object.assign() but deep
  • supports merging of nested properties
  • supports TypeScript: the type of the result is what JS actually returns
  • supports symbols
  • supports enumerable & nonenumerable props
  • does not break special class instances ‼️

This last one is crucial! In JavaScript almost everything is an object, sure, but I don't want a merge function trying to merge eg. two new Date() instances! So many libraries use custom classes that create objects with special prototypes, and such objects all break when trying to merge them. So we gotta be careful!

merge-anything will merge objects and nested properties, but only as long as they're "plain objects". As soon as a sub-prop is not a "plain object" and has a special prototype, it will copy that instance over "as is". ♻️

Meet the family (more tiny utils with TS support)

Usage

  • Unlimited β€” Merge will merge an unlimited amount of plain objects you pass as the arguments
  • Nested β€” Nested objects are merged deeply (see example below)
  • No modification β€” Merge always returns a new object without modifying the original, but does keep object/array references for nested props (see #A note on JavaScript object references)
import { merge } from 'merge-anything'

const starter = { name: 'Squirtle', types: { water: true } }
const newValues = { name: 'Wartortle', types: { fighting: true }, level: 16 }

const evolution = merge(starter, newValues, { is: 'cool' })
// returns {
//   name: 'Wartortle',
//   types: { water: true, fighting: true },
//   level: 16,
//   is: 'cool'
// }

TypeScript Support

In the example above, if you are using TypeScript, and you hover over evolution, you can actually see the type of your new object right then and there. This is very powerful, because you can merge things, and without needing any, TypeScript will know exactly how your newly merged objects look!

typescript support

The return type of the merge() function is usable as a TypeScript utility as well:

import type { Merge } from 'merge-anything'

type A1 = { name: string }
type A2 = { types: { water: boolean } }
type A3 = { types: { fighting: boolean } }

type Result = Merge<A1, [A2, A3]>

Rules

This package will recursively go through plain objects and merge the values onto a new object.

Please note that this package recognises special JavaScript objects like class instances. In such cases it will not recursively merge them like objects, but assign the class onto the new object "as is"!

// all passed objects do not get modified
const a = { a: 'a' }
const b = { b: 'b' }
const c = { c: 'c' }
const result = merge(a, b, c)
// a === {a: 'a'}
// b === {b: 'b'}
// c === {c: 'c'}
// result === {a: 'a', b: 'b', c: 'c'}
// However, be careful with JavaScript object references with nested props. See below: A note on JavaScript object references

// arrays get overwritten
// (for "concat" logic, see Extensions below)
merge({ array: ['a'] }, { array: ['b'] }) // returns {array: ['b']}

// empty objects merge into objects
merge({ obj: { prop: 'a' } }, { obj: {} }) // returns {obj: {prop: 'a'}}

// but non-objects overwrite objects
merge({ obj: { prop: 'a' } }, { obj: null }) // returns {obj: null}

// and empty objects overwrite non-objects
merge({ prop: 'a' }, { prop: {} }) // returns {prop: {}}

merge-anything properly keeps special objects intact like dates, regex, functions, class instances etc.

However, it's very important you understand how to work around JavaScript object references. Please be sure to read #a note on JavaScript object references down below.

Concat arrays

The default behaviour is that arrays are overwritten. You can import mergeAndConcat if you need to concatenate arrays. But don't worry if you don't need this, this library is tree-shakable and won't import code you don't use!

import { mergeAndConcat } from 'merge-anything'

mergeAndConcat(
  { nested: { prop: { array: ['a'] } } },
  { nested: { prop: { array: ['b'] } } }
)
// returns { nested: { prop: { array: ['a', 'b'] } } },

Compare Function when a value is merged

There might be times you need to tweak the logic when two things are merged. You can provide your own custom function that's triggered every time a value is overwritten.

For this case we use mergeAndCompare. Here is an example with a compare function that concatenates strings:

import { mergeAndCompare } from 'merge-anything'

function concatStrings(originVal, newVal, key) {
  if (typeof originVal === 'string' && typeof newVal === 'string') {
    // concat logic
    return `${originVal}${newVal}`
  }
  // always return newVal as fallback!!
  return newVal
}

mergeAndCompare(concatStrings, { name: 'John' }, { name: 'Simth' })
// returns { name: 'JohnSmith' }

Note for TypeScript users. The type returned by this function might not be correct. In that case you have to cast the result to your own provided interface

A note on JavaScript object references

Be careful for JavaScript object reference. Any property that's nested will be reactive and linked between the original and the merged objects! Down below we'll show how to prevent this.

const original = { airport: { status: 'dep. πŸ›«' } }
const extraInfo = { airport: { location: 'Brussels' } }
const merged = merge(original, extraInfo)

// we change the status from departuring πŸ›« to landing πŸ›¬
merged.airport.status = 'lan. πŸ›¬'

// the `merged` value will be modified
// merged.airport.status === 'lan. πŸ›¬'

// However `original` value will also be modified!!
// original.airport.status === 'lan. πŸ›¬'

The key rule to remember is:

Any property that's nested more than 1 level without an overlapping parent property will be reactive and linked in both the merge result and the source

However, there is a really easy solution. We can just copy the merge result to get rid of any reactivity. For this we can use the copy-anything library. This library also makes sure that special class instances do not break, so you can use it without fear of breaking stuff!

See below how we integrate 'copy-anything':

import { copy } from 'copy-anything'

const original = { airport: { status: 'dep. πŸ›«' } }
const extraInfo = { airport: { location: 'Brussels' } }
const merged = copy(merge(original, extraInfo))

// we change the status from departuring πŸ›« to landing πŸ›¬
merged.airport.status = 'lan. πŸ›¬'(merged.airport.status === 'lan. πŸ›¬')(
  // true
  // `original` won't be modified!
  original.airport.status === 'dep. πŸ›«'
) // true

You can then play around where you want to place the copy() function.

Copy Anything is also fully TypeScript supported!

Source code

It is literally just going through an object recursively and assigning the values to a new object like below. However, it's wrapped to allow extra params etc. The code below is the basic integration, that will make you understand the basics how it works.

import { isPlainObject } from 'is-what'

function mergeRecursively(origin, newComer) {
  if (!isPlainObject(newComer)) return newComer
  // define newObject to merge all values upon
  const newObject = isPlainObject(origin)
    ? Object.keys(origin).reduce((carry, key) => {
        const targetVal = origin[key]
        if (!Object.keys(newComer).includes(key)) carry[key] = targetVal
        return carry
      }, {})
    : {}
  return Object.keys(newComer).reduce((carry, key) => {
    const newVal = newComer[key]
    const targetVal = origin[key]
    // early return when targetVal === undefined
    if (targetVal === undefined) {
      carry[key] = newVal
      return carry
    }
    // When newVal is an object do the merge recursively
    if (isPlainObject(newVal)) {
      carry[key] = mergeRecursively(targetVal, newVal)
      return carry
    }
    // all the rest
    carry[key] = newVal
    return carry
  }, newObject)
}

* Of course, there are small differences with the actual source code to cope with rare cases & extra features. The actual source code is here.

merge-anything's People

Contributors

dependabot[bot] avatar exuanbo avatar mesqueeb avatar productdevbook avatar vassudanagunta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

merge-anything's Issues

fix: MergeDeep type to more gracefully merge optional nested object types

type D1 = { [key in string]?: { cool: boolean } | null }
type D2 = { [key in string]?: { notCool: boolean } | null }
type TestD = PrettyPrint<MergeDeep<D1, D2>>

current

{
    [x: string]: {
        cool: boolean;
    } | {
        notCool: boolean;
    } | {
        cool: boolean;
        notCool: boolean;
    } | null;
}

expected

{
    [x: string]: {
        cool?: boolean;
        notCool?: boolean;
    } | null;
}

Arrays don't merge unless within an object.

example:

import { mergeAndConcat } from "merge-anything";

console.log(mergeAndConcat([{ one: "two" }], [{ three: "four" }])) 
// output: [ { three: 'four' } ]

a simple workaround is this

import { mergeAndConcat } from "merge-anything";

console.log(mergeAndConcat({ temp: [{ one: "two" }] }, { temp: [{ three: "four" }] }).temp) 
// output: [ { one: 'two' }, { three: 'four' } ]

I went to check if the deepmerge package supports this and it does it would be nice if merge-anything was to support this as well as I enjoy the suite of libs more

lol I realize this is the concat variant which is supposed to do this though it would be interesting to do like reverse depth

merged missing keys

So I've verified the toMerge, has the expected object, with the expected key, seems though I'm only getting the keys from process.env in my resulting merged object, even though the keys in toMerge do not exist in process.env. the key is just LOG_LEVEL: "silly", error is AssertionError [ERR_ASSERTION]: LOG_LEVEL is not set

import { DI, IContainer, IRegistration, IResolver, Injectable, Resolved } from '@aurelia/kernel';

import assert from 'assert';
import { constantCase } from 'change-case';
import { env } from 'process';
import { merge } from 'merge-anything';
import dotenv, { DotenvParseOutput } from 'dotenv';
import path from 'path';
import findup from 'find-up';
import fs from 'fs';

function pushEnv(filepath: string, toMerge: DotenvParseOutput[]): void {
  const path = filepath.trim() === '' ? undefined : filepath.trim();
  if (path && fs.existsSync(path)) {
    const buffer = fs.readFileSync(path);
    const envMap = dotenv.parse(buffer);
    toMerge.push(envMap);
  }
}

export class EnvironmentResolver implements IResolver<unknown>, IRegistration {
  static required(key: string): IResolver {
    return new EnvironmentResolver(key, true);
  }
  static optional(key: string): IResolver {
    return new EnvironmentResolver(key, false);
  }

  private constructor(private readonly key: string, required: boolean) {
    const toMerge: DotenvParseOutput[] = [];

    const envFile = findup.sync('.env');
    const envRoot = envFile ? path.dirname(envFile) : undefined;

    if (envRoot) {
      pushEnv(`${envRoot}/.env.defaults`, toMerge);
      pushEnv(`${envRoot}/.env`, toMerge);
      pushEnv(`${envRoot}/.env.${env['NODE_ENV']}`, toMerge);
    }
    pushEnv(`${env['DOTENV_PATH']}`, toMerge);

    const merged = merge({} as DotenvParseOutput, ...toMerge, env);

    const envVar = constantCase(key);
    if (required) {
      assert(merged[envVar], `${envVar} is not set`);
    }
  }
  readonly $isResolver: true = true;

  register(container: IContainer): IResolver<void> {
    return container.registerResolver(this.key, this);
  }
  resolve(): Resolved<unknown> {
    const envVar = constantCase(this.key);
    return env[envVar];
  }
}
yarn info -A merge-anything
└─ merge-anything@npm:3.0.6
   β”œβ”€ Version: 3.0.6
   β”‚
   └─ Dependencies
      β”œβ”€ is-what@npm:^3.11.2 β†’ npm:3.11.2
      └─ ts-toolbelt@npm:8.0 β†’ npm:8.0.7

async mergeAndCompare

Hi,
beautiful work done there man!

could we have the compare function to be async?

thanks in advanced.

merge fails on imported objects

In relation to this, here's a relatively simple demonstration of the problem:

// getters.js
export const meaning = () => 42


// index.js
import * as getters from './getters'
import { merge } from 'merge-anything'

const otherThing = {
  something () { return 'anything' }
}

console.log('getters:', getters)
console.log('otherThing:', otherThing)
console.log('merge(getters, otherThing):', merge(getters, otherThing))
console.log('merge(otherThing, getters):', merge(otherThing, getters))

The result of the merge is just the second argument, not a merge:

image

Combine arrays of object

Thanks for creating this library, there are several libraries that do deep merge, but this is the only one I've seen written in TypeScript.

I would like to see a default function for deep merging arrays of objects, such as the one introduced in the deepmerge library.
This means that I want to use the following strategy for merging:

const x = { a: [{ foo: 42 }] }
const y = { a: [{ bar: 42 }] }

const expect = { a: [{ foo: 42, bar: 42 }] }

Or please provide a sample that uses mergeAndCompare to achieve the same process.

Thanks!

Browser support information

Could you share your intended browser support of this package in the README or in some kind of documentation?

Invalid "exports" main target "dist/index.cjs.js"

I am trying to build the documentation of my project and I have the following error:

> @bootstrap-styled/[email protected] styleguide /home/dka/workspace/github.com/bootstrap-styled/css-utils
> styleguidist server

Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "dist/index.cjs.js" defined in the package config /home/dka/workspace/github.com/bootstrap-styled/css-utils/node_modules/merge-anything/package.json
Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "dist/index.cjs.js" defined in the package config /home/dka/workspace/github.com/bootstrap-styled/css-utils/node_modules/merge-anything/package.json
    at resolveExportsTarget (internal/modules/cjs/loader.js:542:13)
    at resolveExportsTarget (internal/modules/cjs/loader.js:581:20)
    at applyExports (internal/modules/cjs/loader.js:455:14)
    at resolveExports (internal/modules/cjs/loader.js:508:23)
    at Function.Module._findPath (internal/modules/cjs/loader.js:632:31)
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:1001:27)
    at Function.Module._load (internal/modules/cjs/loader.js:884:27)
    at Module.require (internal/modules/cjs/loader.js:1074:19)
    at require (internal/modules/cjs/helpers.js:72:18)
    at Object.<anonymous> (/home/dka/workspace/github.com/bootstrap-styled/css-utils/node_modules/styled-components/dist/styled-components.cjs.js:17:29)
npm ERR! code ELIFECYCLE

Is this related to this project? Seems like export are wrong.

Like Object.assign, but doesn't actually assign

According to the README.md

a simple merge function like Object.assign() but deep

I would expect the first object to be mutated if this is supposed to be a deep version of Object.assign.

Are there any options or plans to assign to the first object?

Jest tests are not running after updating from 5.0.* to 5.1.7

We recently updated the β€žmerge-anythingβ€œ dependency in our project from 5.0.2 to version 5.1.7 which changes how the exported files are treated. This is a breaking change...
The default export is now modules instead of commonjs (https://github.com/mesqueeb/merge-anything/blob/v5.1.7/package.json#L9). Unfortunately, Jest in combination with stencil does not support modules in test so far (ionic-team/stencil#3230). These two things together simply break the tests with a more or less helpful messages.

Merge anything compile error fails in typescript

The line in merge.d.ts fails for me when importing

declare type PlainObject = {
    [key: string | symbol]: any;
};

I notice that in merge.ts the same problem line is:

// @ts-ignore
type PlainObject = { [key: string | symbol]: any }

Is it possible to fix this?

Transpiled index.cjs.js misses export = merge;

When using your lib from native node.js ESM code (node.js >= 12 with --experimental-modules enabled),

import merge from 'merge-anything';
merge();

yields TypeError: merge is not a function

This is because the transpiled dist/index.cjs.js misses an export = merge statement (in constrast to e.g. your copy-anything/dist/index.cjs.js's correctly including a module.exports = copy; line.)

Maybe you can adjust your transpiler config to fix that? I'm not that familiar with rollup...

Thanks!

Symbol properties are ignored

Symbols can be used as property keys, but they are ignored.

const merge = require('merge-anything');

const mySymbol = Symbol('mySymbol');

const x = { value: 42, [mySymbol]: 'hello' };

console.log(x);
// { value: 42, [Symbol(mySymbol)]: 'hello' }

const y = { other: 33 };
const z = merge(x, y);

console.log(z);
// { value: 42, other: 33 }
// expected: { value: 42, other: 33, [Symbol(mySymbol)]: 'hello' }

🚨 Potential Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)

πŸ‘‹ Hello, @mesqueeb - a potential medium severity Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321) vulnerability in your repository has been disclosed to us.

Next Steps

1️⃣ Visit https://huntr.dev/bounties/1-other-mesqueeb/merge-anything for more advisory information.

2️⃣ Sign-up to validate or speak to the researcher for more assistance.

3️⃣ Propose a patch or outsource it to our community - whoever fixes it gets paid.


Confused or need more help?

  • Join us on our Discord and a member of our team will be happy to help! πŸ€—

  • Speak to a member of our team: @JamieSlome


This issue was automatically generated by huntr.dev - a bug bounty board for securing open source code.

Prototype Pollution

This module has prototype pollution vulnerability

import { merge } from 'merge-anything'

const ev = merge({}, JSON.parse('{"__proto__":{"polluted":"test"}}'));
console.log(ev.polluted);

No named or default exports available

Using v ^5.1.3 and:

import { merge } from 'merge-anything'

I get "Module '"merge-anything"' has no exported member 'merge'". I have tried default export as well. I am using type="module" in my package.json as well as this for my TSConfig:

 "target": "es2020",
    "lib": ["es2020"],
    "module": "node16",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true, // Eases ESM support
    "types": ["node"],
    "allowSyntheticDefaultImports": true,

I think there is something missing in package. If I click (using VSCode) on "merge-anything" it takes me to the declaration file:

export * from './merge';
export * from './extensions';

For ESM exports these need extensions. So:

export * from './merge.js'
export * from './extensions.js'

And then it works.

ts-toolbelt dependency too loose

I'm getting this error using yarn2 with PnP

➀ YN0000: [@bb/graph-main]: ../../../../../.yarn/cache/ts-toolbelt-npm-8.4.0-f6995cd030-dbc3ad2626.zip/node_modules/ts-toolbelt/out/index.d.ts(983,46): error TS1110: Type expected.
> yarn why -R ts-toolbelt
└─ @bb/graph-main@workspace:ts/lib/graph/packages/main
   β”œβ”€ filter-anything@npm:2.1.6 (via npm:^2.1.6)
   β”‚  └─ ts-toolbelt@npm:8.0.7 (via npm:8.0.7)
   └─ merge-anything@npm:3.0.6 (via npm:^3.0.6)
      └─ ts-toolbelt@npm:8.4.0 (via npm:^8.0.6)

the version I pulled in was 8.4.0, looking at the versions on ts-toolbelt that's next, not latest. I've worked around it by adding this to my .yarnrc.yml

packageExtensions:
  merge-anything@^3:
    dependencies:
      ts-toolbelt: '8.0'

P.S. it may also be worthwhile to loosen the dependency on filter-anything to be 8.0 instead of specifically 8.0.7.

`interface` does not satisfy `Record<string | number | symbol, unknown>`

In TypeScript, interfaces do not satisfy the type Record<string | number | symbol, unknown> which is not what the functions are expecting. Here is a minimal example:

import { merge } from "merge-anything";

interface Arguments {
    key: string;
}

merge<Arguments, Arguments[]>({ key: "value1" }, { key: "value2" });

image

Handling "undefined"

Maybe this is by design, but I was surprised to learn that this:

import { merge } from "merge-anything"

const defaultTheme = {}
const theme = undefined

merge(defaultTheme, theme)

Returns undefined. Is this expected, maybe it is, but I was kind of expecting it to return {}. Am I right?

I would hate to have to handle this scenario like this everytime:

merge(defaultTheme, theme || {})

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.