# deepmerge-plus

> 深度合併兩個 JavaScript 物件的可列舉屬性 / Merge the enumerable attributes of two objects deeply.

`pnpm add deepmerge-plus`

## 概述

deepmerge-plus 是一個功能強大的 JavaScript 物件深度合併函式庫，支援：

- � depth-first 深度合併（遞迴處理巢狀物件）
- 📦 陣列合併（可自訂合併策略）
- ⚙️ 靈活的選項配置
- 🔧 自訂合併邏輯

---

## 安裝

```bash
# 使用 pnpm（推薦）
pnpm add deepmerge-plus

# 使用 npm
npm install deepmerge-plus

# 使用 yarn
yarn add deepmerge-plus
```

---

## 快速開始

```javascript
const merge = require('deepmerge-plus');

const target = {
  foo: { bar: 5 },
  array: [{ does: 'work', too: [1, 2, 3] }]
};

const source = {
  foo: { baz: 4 },
  quux: 5,
  array: [{ does: 'work', too: [4, 5, 6] }, { really: 'yes' }]
};

const result = merge(target, source);
console.log(result);
// => { foo: { bar: 5, baz: 4 }, array: [...], quux: 5 }
```

---

## 核心概念：左至右合併

在 `merge(target, source, options)` 中：

| 參數 | 術語 | 說明 |
|-----|------|------|
| `target` | 左側（left）| 被合併的物件，代表**現有值** |
| `source` | 右側（right）| 要合併進來的物件，代表**新值** |

### 預設行為

- **來源物件的值會覆蓋目標物件的值**
- 合併結果是將「右側」的值合併進「左側」
- 合併會建立一個新物件，因此 `target` 和 `source` 都不會被修改

```javascript
const target = { name: 'Alice', age: 25 };
const source = { name: 'Bob', city: 'Taipei' };

const result = merge(target, source);
// => { name: 'Bob', age: 25, city: 'Taipei' }
// name 被 source 覆蓋，age 保留 target，city 來自 source
```

---

## API

### merge(target, source, [options])

深度合併兩個物件 `target` 和 `source`，返回一個包含兩者元素的新合併物件。

```javascript
const result = merge(objectA, objectB, options);
```

### merge.all(arrayOfObjects, [options])

將任意數量的物件合併成單一結果物件。

```javascript
const result = merge.all([obj1, obj2, obj3], options);
```

---

## 選項說明

### 1. clone

是否啟用深度複製功能。

- **類型**: `boolean`
- **預設值**: `true`

```javascript
merge(target, source, { clone: true }); // 預設，深度複製物件
merge(target, source, { clone: false }); // 不複製，直接引用
```

### 2. arrayMerge

自訂陣列合併函式。

- **類型**: `function(target, source, options): array`
- **預設行為**: 串接兩個陣列

```javascript
// 覆寫模式：來源陣列覆蓋目標陣列
function overwriteMerge(destinationArray, sourceArray) {
  return sourceArray;
}

merge([1, 2, 3], [3, 2, 1], { arrayMerge: overwriteMerge });
// => [3, 2, 1]
```

```javascript
// 僅保留目標陣列
const keepTarget = (destination) => destination;
merge({ arr: [1,2,3] }, { arr: ['a','b','c'] }, { arrayMerge: keepTarget });
// => { arr: [1, 2, 3] }
```

### 3. isMergeableObject

自訂可合併物件判斷函式。

- **類型**: `function(value): boolean`

```javascript
const moment = require('moment');

merge(target, source, {
  isMergeableObject(value) {
    if (moment.isMoment(value)) return false;
    return isMergeableObject(value);
  }
});
```

### 4. keyValueOrMode（已棄用）

使用 `||` 運算子邏輯，已由 `keyValueUpsertMode` 取代。

- **類型**: `boolean`
- **狀態**: 已棄用，不建議在新程式碼中使用

### 5. keyValueUpsertMode（推薦）

Upsert 模式，控制何時應該保留目標中的現有值。

- **類型**: `boolean` 或 `function`
- **預設值**: `undefined`

#### 為 `true` 時

保留目標中已存在的值（`undefined` 除外）。

```javascript
const target = { name: 'Alice', age: undefined, count: 0, email: null };
const source = { name: 'Bob', age: 30, count: 5, email: 'bob@example.com' };

const result = merge(target, source, { keyValueUpsertMode: true });
// => { name: 'Alice', age: 30, count: 0, email: null }
// name: 保留 'Alice'（不是 undefined）
// age: 使用 30（目標值是 undefined）
// count: 保留 0（不是 undefined）
// email: 保留 null（不是 undefined）
```

**關鍵特性**：
- 使用 `??` 運算子，區分 `undefined` 與其他 falsy 值
- `null`、`0`、`false`、`''` 都會被保留
- 與 lodash 的 `_.defaultsDeep` 概念相似

#### 為 `function` 時

自訂函式，根據條件決定是否保留目標的值。

```javascript
const target = { name: 'Alice', count: 0 };
const source = { name: 'Bob', count: 5 };

const result = merge(target, source, {
  keyValueUpsertMode: (value, options, tmpRuntimeTarget) => {
    const targetValue = tmpRuntimeTarget.target?.[tmpRuntimeTarget.key];
    return targetValue === 0; // 只有當目標值為 0 時保留
  }
});
// => { name: 'Bob', count: 0 }
// name: targetValue='Alice' !== 0，回傳 false，使用 source 的 'Bob'
// count: targetValue=0 === 0，回傳 true，保留 target 的 0
```

**函式參數說明**：

| 參數 | 類型 | 說明 |
|-----|------|------|
| `value` | `unknown` | 來源物件中該鍵的值 |
| `optionsRuntime` | `IOptions` | 目前的合併選項 |
| `tmpRuntimeTarget` | `ICache` | 包含 `target`、`source`、`destination`、`key` 等資訊 |
| `tmpRuntimeData` | `ITmpRuntimeData` | 包含 `level`、`paths`、`root`、`parent` 等資訊 |

#### 進一步閱讀

如需了解 `keyValueUpsertMode` 與陣列合併策略的詳細組合應用，請參閱：
- [keyValueUpsertMode 與陣列合併策略](./docs/examples/key-value-upsert-mode-array-merge-strategies.md)

---

## 實際應用場景

### 1. 配置合併（保留使用者設定）

```javascript
// 預設配置
const defaultConfig = {
  theme: 'light',
  language: 'en',
  notifications: { email: true, sms: false },
  timeout: 3000
};

// 使用者自訂配置
const userConfig = {
  theme: 'dark',
  notifications: { email: false }
};

// 合併：保留使用者設定，填充預設值
const finalConfig = merge(defaultConfig, userConfig, {
  keyValueUpsertMode: true
});
// => { theme: 'dark', language: 'en', notifications: { email: false }, timeout: 3000 }
```

**對比 lodash**：
- `_.defaults(userConfig, defaultConfig)` - 填充缺失的鍵
- `merge(defaultConfig, userConfig, { keyValueUpsertMode: true })` - 保留使用者設定

### 2. 表單資料合併

```javascript
// 預設表單值
const defaultValues = {
  username: '',
  password: '',
  rememberMe: false,
  preferences: { newsletter: true, language: 'en' }
};

// 使用者已輸入的資料
const userInput = {
  username: 'alice',
  preferences: { language: 'zh-TW' }
};

// 合併：保留使用者已輸入的資料
const formData = merge(defaultValues, userInput, {
  keyValueUpsertMode: true
});
// => { username: 'alice', password: '', rememberMe: false, preferences: { newsletter: true, language: 'zh-TW' } }
```

### 3. 環境變數覆蓋

```javascript
// 基礎環境配置
const baseConfig = {
  API_URL: 'https://api.example.com',
  DEBUG: false,
  CACHE: { enabled: true, ttl: 3600 }
};

// 開發環境配置
const devConfig = {
  DEBUG: true,
  API_URL: 'https://dev-api.example.com'
};

// 生產環境配置
const prodConfig = {
  DEBUG: false
};

// 合併環境配置（保留最基礎的值）
const envConfig = merge(baseConfig, devConfig, {
  keyValueUpsertMode: true
});
// => { API_URL: 'https://dev-api.example.com', DEBUG: true, CACHE: { enabled: true, ttl: 3600 } }
```

### 4. 深度合併（類似 _.merge）

```javascript
const target = { user: { name: 'Alice', age: 25 }, role: 'admin' };
const source = { user: { name: 'Bob', city: 'Taipei' }, role: 'user' };

// 預設深度合併（source 覆蓋 target）
const result1 = merge(target, source);
// => { user: { name: 'Bob', age: 25, city: 'Taipei' }, role: 'user' }

// 使用 keyValueUpsertMode 保留 target 的值
const result2 = merge(target, source, { keyValueUpsertMode: true });
// => { user: { name: 'Alice', age: 25, city: 'Taipei' }, role: 'admin' }
```

---

## 與 lodash 函式對比

| lodash 函式 | deepmerge-plus 實作 | 說明 |
|------------|-------------------|------|
| `_.defaults` | `merge(target, source, { keyValueUpsertMode: true })` | 淺層預設值（需正確參數順序） |
| `_.defaultsDeep` | `merge(target, source, { keyValueUpsertMode: true })` | 深度預設值 |
| `_.merge` | `merge(target, source)` | 預設深度合併 |

**重要語義差異**：
- `_.defaults` / `_.defaultsDeep`：填充缺失的鍵（fill missing）
- `keyValueUpsertMode: true`：有值則保留（preserve if exists）

---

## 類型定義

```typescript
interface IOptions {
  /** 是否啟用深度複製，預設 true */
  clone?: boolean;

  /** 自訂陣列合併函式 */
  arrayMerge?: <T extends any[]>(target: T, source: any[], options?: IOptions) => T[];

  /** 自訂可合併物件判斷函式 */
  isMergeableObject?: (value: any) => boolean;

  /** 已棄用：使用 keyValueUpsertMode 取代 */
  keyValueOrMode?: boolean;

  /** Upsert 模式：true 或自訂函式 */
  keyValueUpsertMode?:
    | boolean
    | ((value: unknown, optionsRuntime?: IOptions, tmpRuntimeTarget?: ICache) => boolean);
}
```

---

## 測試

```bash
pnpm test
```

---

## 授權

MIT
