Skip to content

Commit

Permalink
Merge pull request #1 from Pinta365/simplemerge
Browse files Browse the repository at this point in the history
Simplemerge
  • Loading branch information
Pinta365 authored Mar 13, 2024
2 parents 571bf9f + f88e05e commit ce64a77
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 251 deletions.
92 changes: 59 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@

## Overview

Cross-runtime deep object merging in JavaScript environments, including Deno, Bun, Node.js and Browser. It is designed to handle complex data structures, including arrays, Maps, Sets, and primitive values. It provides flexible customization options for handling array, Set, and Map merging strategies.
Cross-runtime deep object merging in JavaScript environments, including Deno, Bun, Node.js and Browser. It is designed
to handle complex data structures, including arrays, Maps, Sets, and primitive values. It provides flexible
customization options for handling array, Set, and Map merging strategies.

Follow the library on [JSR.io](https://jsr.io/@cross/deepmerge)

## Features

* **Cross-Runtime Compatibility:**
* **Deep Merging:** Recursively combines objects at all levels of nesting.
* **Array Merging Customization:** Choose among strategies:
* `combine` (default): Concatenates arrays, preserves duplicates.
* `unique`: Produces an array of unique elements.
* `replace`: Overwrites the target array with the source array.
* **Set Merging Customization:** Select between strategies:
* `combine` (default): Adds new set elements.
* `replace`: Overwrites the target set with the source set.
* **Map Merging Customization:** Select between strategies:
* `combine` (default): Adds new entries, replaces entries with the same key.
* `replace`: Overwrites the target map with the source map.
- **Cross-Runtime Compatibility:**
- **Deep Merging:** Recursively combines objects at all levels of nesting.
- **Array Merging Customization:** Choose among strategies:
- `combine` (default): Concatenates arrays, preserves duplicates.
- `unique`: Produces an array of unique elements.
- `replace`: Overwrites the target array with the source array.
- **Set Merging Customization:** Select between strategies:
- `combine` (default): Adds new set elements.
- `replace`: Overwrites the target set with the source set.
- **Map Merging Customization:** Select between strategies:
- `combine` (default): Adds new entries, replaces entries with the same key.
- `replace`: Overwrites the target map with the source map.

## Installation

Expand All @@ -39,27 +41,41 @@ npx jsr add @cross/deepmerge

```javascript
import { deepMerge } from "@cross/deepmerge";
//or for simple object merging.
import { simpleMerge } from "@cross/deepmerge";
```

For browser you can use esm.sh as CDN to retrieve the ESM module for now.

```javascript
import { deepMerge } from "https://esm.sh/jsr/@cross/deepmerge";
//or for simple object merging.
import { simpleMerge } from "https://esm.sh/jsr/@cross/deepmerge";
```

Here is an simple example [jsfiddle](https://jsfiddle.net/pinta365/54gnohdb/) to try it out live in your browser.

## Usage

Most basic usage of `deepMerge` is just to merge one or more objects:
Most basic usage of `simpleMerge` and `deepMerge` is just to merge one or more objects:

```javascript
const object1 = { a: 1, b: { c: 2 } };
const object2 = { b: { d: 3 }, e: 4 };

const merged = deepMerge(object1, object2);
// Example interface of the object we are merging.
interface MergeableObj {
a?: number;
b?: {
c?: number;
d?: number;
};
e?: number;
}

const object1: MergeableObj = { a: 1, b: { c: 2 } };
const object2: MergeableObj = { b: { c:1, d: 3 }, e: 4 };

const merged = simpleMerge(object1, object2); //We don't need deepMerge for this simple object.
console.log(merged);
// Output: { a: 1, b: { c: 2, d: 3 }, e: 4 }
// Output: { a: 1, b: { c: 1, d: 3 }, e: 4 }
```

Below is an more advanced example that showcases the usage of the `deepMerge` library with more complex objects,
Expand Down Expand Up @@ -98,8 +114,8 @@ console.log(merged);
// }
```

Below demonstrates a possible use case of default or standard configurations that can be customized or overridden by user-specified
configurations.
Below demonstrates a possible use case of default or standard configurations that can be customized or overridden by
user-specified configurations.

```javascript
import { deepMerge } from "@cross/deepmerge";
Expand Down Expand Up @@ -151,23 +167,33 @@ console.log(mergedConfig);

For detailed docs see the [JSR docs](https://jsr.io/@cross/deepmerge/doc)

### `simpleMerge(...sources)`

Simple object deep merger that can be used for most objects situations where you need to merge objects without arrays,
sets and maps.

### `deepMerge(...sources)`

More complex object deep merger that handles arrays, sets and maps also.

### `deepMerge.withOptions(options, ...sources)`

Same as above but with options as seen below.

#### `DeepMergeOptions`
* **`arrayMergeStrategy`**
* **"combine"**: (default behavior) Appends arrays, preserving duplicates.
* **"unique"**: Creates an array with only unique elements.
* **"replace"**: Substitutes the target array entirely with the source array.

* **`setMergeStrategy`**
* **"combine"**: (default behavior) Adds new members to the target Set.
* **"replace"**: Overwrites the target Set with the source Set.

* **`mapMergeStrategy`**
* **"combine"**: (default behavior) Merges with the source Map, replacing values for matching keys.
* **"replace"**: Overwrites the target Map with the source Map.

- **`arrayMergeStrategy`**
- **"combine"**: (default behavior) Appends arrays, preserving duplicates.
- **"unique"**: Creates an array with only unique elements.
- **"replace"**: Substitutes the target array entirely with the source array.

- **`setMergeStrategy`**
- **"combine"**: (default behavior) Adds new members to the target Set.
- **"replace"**: Overwrites the target Set with the source Set.

- **`mapMergeStrategy`**
- **"combine"**: (default behavior) Merges with the source Map, replacing values for matching keys.
- **"replace"**: Overwrites the target Map with the source Map.

## Issues

Expand Down
6 changes: 5 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"name": "@cross/deepmerge",
"version": "0.1.1",
"exports": "./mod.ts",

"tasks": {
"publish": "deno publish --config jsr.jsonc"
"publish-dry": "deno publish --dry-run"
},
"lock": false,
"fmt": {
Expand Down
5 changes: 0 additions & 5 deletions jsr.jsonc

This file was deleted.

215 changes: 3 additions & 212 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,213 +1,4 @@
/**
* @fileoverview This module provides a versatile deep object merging function (deepMerge)
* that works consistently across Deno, Bun, and Node.js. It offers
* customizable array merging strategies.
*/

/**
* Optional options interface to control the deep merge process.
*
** **`arrayMergeStrategy`**
* * **"combine"**: (default behavior) Appends arrays, preserving duplicates.
* * **"unique"**: Creates an array with only unique elements.
* * **"replace"**: Substitutes the target array entirely with the source array.
*
** **`setMergeStrategy`**
* * **"combine"**: (default behavior) Adds new members to the target Set.
* * **"replace"**: Overwrites the target Set with the source Set.
*
** **`mapMergeStrategy`**
* * **"combine"**: (default behavior) Merges with the source Map, replacing values for matching keys.
* * **"replace"**: Overwrites the target Map with the source Map.
*/
interface DeepMergeOptions {
arrayMergeStrategy?: "combine" | "unique" | "replace";
setMergeStrategy?: "combine" | "replace";
mapMergeStrategy?: "combine" | "replace";
}

/**
* Represents basic primitive JavaScript data types.
*/
type Primitive = string | number | boolean | bigint | symbol | undefined | null;

/**
* Represents a Map-like structure where keys can be strings, numbers, or symbols,
* and values must be of a type that can be merged.
*/
type MergeableMap = Map<string | number | symbol, MergeableValue>;

/**
* Represents a Set-like structure containing only mergeable values.
*/
type MergeableSet = Set<MergeableValue>;

/**
* Represents an array-like structure containing only mergeable values.
*/
type MergeableArray = Array<MergeableValue>;

/**
* Represents the types of values that can be included within objects,
* Maps, Sets and arrays for the purpose of deep merging.
*/
type MergeableValue =
| MergeableObject
| MergeableMap
| MergeableSet
| MergeableArray
| Primitive;

/**
* Represents an object with string, number or symbol keys, where the
* values can be any mergeable type (including nested objects, Maps,
* Sets, arrays, or primitives).
*/
interface MergeableObject {
[key: string | number | symbol]: MergeableValue;
}

/**
* @param {*} item - An item to evaluate.
* @returns {boolean} True if the item is a plain object, false otherwise.
*/
function isObject(item: unknown): item is MergeableObject {
if (typeof item !== "object" || item === null || Array.isArray(item)) {
return false;
}

if (item instanceof Map || item instanceof Set) {
return false;
}

return true;
}

/**
* Performs a deep merge of objects, handling arrays, Maps, Sets, and primitives.
* Supports multiple source objects.
*
* **Customizing Behavior with the attached `deepMerge.withOptions()` method**
* `.withOptions()` allows you to create a modified version of `deepMerge` with
* pre-configured merge behavior. For instance:
*
* ```javascript
* const combineUniqueArrays = deepMerge.withOptions({ arrayMergeStrategy: 'unique' });
* const combined = combineUniqueArrays({ arr: [1, 2] }, { arr: [2, 3] }); // combined.arr = [1, 2, 3]
* ```
*
* @template T
* @param {T} target - The base object to merge into.
* @param {...T} sources - One or more source objects to merge.
* @returns {T} The merged result.
*/
function deepMerge<T>(...sources: T[]): T {
const result = {} as T;
const visited = new WeakMap<object, MergeableValue>();
return deepMergeCore(result, {}, visited, ...sources);
}

/**
* Creates a variation of the deepMerge function that applies provided options.
*
* @template T
* @param {DeepMergeOptions} options - Options to control the merge behavior.
* @param {...T} sources - Source objects.
* @returns {T} The merged result.
*/
deepMerge.withOptions = function <T>(options: DeepMergeOptions, ...sources: T[]): T {
const result = {} as T;
const visited = new WeakMap<object, MergeableValue>();
return deepMergeCore(result, options, visited, ...sources);
};

/**
* Core recursive function for deep merging objects. This function handles the logic
* of merging nested objects, arrays, Maps, Sets, and primitives, while taking
* into account the provided merge options.
*
* @template T
* @param {DeepMergeOptions} options - Controls merge behavior, particularly for arrays.
* @param {...T} sources - One or more source objects to merge (processed recursively).
* @returns {T} The deeply merged result.
*/
function deepMergeCore<T>(
current: T,
options: DeepMergeOptions,
visited: WeakMap<object, MergeableValue>,
...sources: T[]
): T {
if (!sources.length) return current;
const source = sources.shift() as MergeableObject;

if (isObject(source)) {
if (visited.has(source)) {
return visited.get(source) as T;
}

visited.set(source, current as MergeableObject);

for (const key in source) {
const sourceValue = source[key];

if (sourceValue instanceof Map) {
switch (options.mapMergeStrategy) {
case "replace":
(current as MergeableObject)[key] = new Map(sourceValue as MergeableMap);
break;
case "combine":
default:
(current as MergeableObject)[key] = new Map([
...((current as MergeableObject)[key] as MergeableMap) || [],
...(sourceValue as MergeableMap),
]);
}
} else if (sourceValue instanceof Set) {
switch (options.setMergeStrategy) {
case "replace":
(current as MergeableObject)[key] = new Set(sourceValue as MergeableSet);
break;
case "combine":
default:
(current as MergeableObject)[key] = new Set([
...((current as MergeableObject)[key] as MergeableSet) || [],
...(sourceValue as MergeableSet),
]);
}
} else if (Array.isArray(sourceValue)) {
switch (options.arrayMergeStrategy) {
case "unique":
(current as MergeableObject)[key] = Array.from(
new Set([
...((current as MergeableObject)[key] as MergeableArray) || [],
...(sourceValue as MergeableArray),
])
);
break;
case "replace":
(current as MergeableObject)[key] = sourceValue;
break;
case "combine":
default:
(current as MergeableObject)[key] = [
...((current as MergeableObject)[key] as MergeableArray) || [],
...(sourceValue as MergeableArray),
];
}
} else if (isObject(sourceValue)) {
(current as MergeableObject)[key] = (current as MergeableObject)[key] || {};
(current as MergeableObject)[key] = deepMergeCore((current as MergeableObject)[key] as MergeableObject, options, visited, sourceValue);
} else {
(current as MergeableObject)[key] = sourceValue;
}
}
} else {
return source as T;
}

return deepMergeCore(current, options, visited, ...sources);
}

export { deepMerge };
export type { DeepMergeOptions };
export { deepMerge } from "./src/deepmerge.ts";
export type { DeepMergeOptions } from "./src/deepmerge.ts";

export { simpleMerge } from "./src/simplemerge.ts";
Loading

0 comments on commit ce64a77

Please sign in to comment.