@phun-ky/defaults-deep 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.9](https://github.com/phun-ky/defaults-deep/compare/2.0.8...2.0.9) (2026-10-25)
4
+
5
+ ### Tasks
6
+
7
+ * 🤖 lint everything, typecheck strictly, drop unused tooling ([cc50f38](https://github.com/phun-ky/defaults-deep/commit/cc50f38a388a4a9442d0754b8feb22fb2a753644)), closes [#97](https://github.com/phun-ky/defaults-deep/issues/97) [#102](https://github.com/phun-ky/defaults-deep/issues/102), references [#110](https://github.com/phun-ky/defaults-deep/issues/110) [#98](https://github.com/phun-ky/defaults-deep/issues/98) [#99](https://github.com/phun-ky/defaults-deep/issues/99) [#101](https://github.com/phun-ky/defaults-deep/issues/101)
8
+ * 🤖 raise coverage thresholds to the new floor ([c740289](https://github.com/phun-ky/defaults-deep/commit/c740289452c5915f415b59822ae35a0afa87948a))
9
+
10
+ ### Documentation
11
+
12
+ * ✏️ describe mergeWith as internal in the API comment ([49db942](https://github.com/phun-ky/defaults-deep/commit/49db942ca1a2af576adec778b4cafe6c5e89cd5a))
13
+
14
+ ### Bug
15
+
16
+ * 🐛 give keys that share a source object their own merged objects ([b2de955](https://github.com/phun-ky/defaults-deep/commit/b2de955f8a6d1fedab0171b08564744736101278)), closes [#110](https://github.com/phun-ky/defaults-deep/issues/110)
17
+
3
18
  ## [2.0.8](https://github.com/phun-ky/defaults-deep/compare/2.0.7...2.0.8) (2026-10-25)
4
19
 
5
20
  ### Tasks
@@ -2,7 +2,7 @@
2
2
  * @phun-ky/defaults-deep
3
3
  * Like lodash's _.defaultsDeep, but with array preservation
4
4
  * @author Alexander Vassbotn Røyne-Helgesen <alexander@phun-ky.net>
5
- * @version 2.0.8
5
+ * @version 2.0.9
6
6
  * @license
7
7
  * Copyright (c) 2026 Alexander Vassbotn Røyne-Helgesen
8
8
  *
@@ -40,5 +40,5 @@
40
40
  * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
41
41
  * PERFORMANCE OF THIS SOFTWARE.
42
42
  */
43
- "use strict";var t=require("@phun-ky/typeof");function e(e,...o){const i=o.at(-1),s=t.isFunction(i)?i:void 0,c=s?o.slice(0,-1):o,r=new WeakMap;for(const t of c)n(e,t,s,r);return e}const o=new Set(["__proto__","constructor","prototype"]),n=(e,s,c,r)=>{if(t.isObjectLoose(s))for(const f of i(s)){if("string"==typeof f&&o.has(f))continue;const i=s[f],u=e[f],a=c?.(u,i,f,e,s,r);if(t.isNotUndefined(a))e[f]=a;else{if(t.isObjectPlain(u)&&t.isObjectPlain(i)){const t=r.get(i);if(t){e[f]=t;continue}r.set(i,u),n(u,i,c,r);continue}if(t.isObjectPlain(i)){const o=r.get(i);if(o){e[f]=o;continue}const s=t.isObjectPlain(u)?u:{};e[f]=s,r.set(i,s),n(s,i,c,r);continue}e[f]=i}}},i=t=>[...Object.keys(t),...Object.getOwnPropertySymbols(t).filter(e=>Object.prototype.propertyIsEnumerable.call(t,e))];module.exports=(...o)=>{const n={};for(const i of o.slice().reverse())t.isObjectLoose(i)&&e(n,i,(t,e)=>Array.isArray(e)?e:void 0);return n};
43
+ "use strict";var e=require("@phun-ky/typeof");function t(t,...o){const i=o.at(-1),s=e.isFunction(i)?i:void 0,c=s?o.slice(0,-1):o,r=new WeakMap;for(const e of c)n(t,e,s,r);return t}const o=new Set(["__proto__","constructor","prototype"]),n=(t,s,c,r)=>{if(e.isObjectLoose(s))for(const f of i(s)){if("string"==typeof f&&o.has(f))continue;const i=s[f],l=t[f],u=c?.(l,i,f,t,s,r);if(e.isNotUndefined(u))t[f]=u;else{if(e.isObjectPlain(l)&&e.isObjectPlain(i)){const e=r.get(i);if(e){t[f]=e;continue}r.set(i,l),n(l,i,c,r),r.delete(i);continue}if(e.isObjectPlain(i)){const o=r.get(i);if(o){t[f]=o;continue}const s=e.isObjectPlain(l)?l:{};t[f]=s,r.set(i,s),n(s,i,c,r),r.delete(i);continue}t[f]=i}}},i=e=>[...Object.keys(e),...Object.getOwnPropertySymbols(e).filter(t=>Object.prototype.propertyIsEnumerable.call(e,t))];module.exports=(...o)=>{const n={};for(const i of o.slice().reverse())e.isObjectLoose(i)&&t(n,i,(e,t)=>Array.isArray(t)?t:void 0);return n};
44
44
  //# sourceMappingURL=defaults-deep.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"defaults-deep.cjs","sources":["../src/utils/merge-with.ts","../src/main.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n isFunction,\n isNotUndefined,\n isObjectLoose,\n isObjectPlain\n} from '@phun-ky/typeof';\n\n/* node:coverage disable */\n/**\n * Customizer function used by {@link mergeWith} to override merge behaviour.\n *\n * If the customizer returns `undefined`, {@link mergeWith} falls back to its default\n * deep-merge rules. Any other return value will be assigned to the destination key.\n *\n * @param objValue - Current value on the destination (`object`) for `key`.\n * @param srcValue - Incoming value from the current `source` for `key`.\n * @param key - Property key being merged (string or symbol).\n * @param object - Destination object being mutated/merged into.\n * @param source - Current source object being merged from.\n * @param stack - A `WeakMap` used internally to prevent infinite recursion on circular references.\n * @returns The value to assign for this key, or `undefined` to use default merge behaviour.\n *\n * @example\n * ```ts\n * import { mergeWith, type MergeWithCustomizer } from \"./mergeWith\";\n *\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * return undefined;\n * };\n *\n * const a = { list: [1] };\n * const b = { list: [2] };\n *\n * mergeWith(a, b, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n/* node:coverage enable */\nexport type MergeWithCustomizer = (\n objValue: unknown,\n srcValue: unknown,\n key: string | symbol,\n object: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n stack: WeakMap<object, object>\n) => unknown;\n\ntype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n k: infer I\n) => void\n ? I\n : never;\n\ntype MergeResult<T, S extends readonly any[]> = T &\n UnionToIntersection<S[number]>;\n\n/**\n * Deep-merges one or more source objects into a destination object, with optional customization.\n *\n * This is a \"vanilla TS\" alternative to lodash's `mergeWith` with a deliberately conservative\n * default strategy:\n *\n * - **Mutates** and returns the destination `object`.\n * - Enumerates **own enumerable string keys** and **own enumerable symbol keys** from each source.\n * - When both destination and source values are **plain objects**, merges recursively.\n * - When the source value is a **plain object** but the destination is not, creates `{}` and merges into it.\n * - All other value types (arrays, dates, maps, sets, functions, primitives, class instances, etc.)\n * are assigned by **replacement** unless the customizer handles them.\n * - Circular references in **sources** are prevented from causing infinite recursion via a `WeakMap` stack.\n *\n * @typeParam T - Destination object type.\n * @typeParam S - Tuple/array of source object types.\n *\n * @param object - Destination object to merge into (will be mutated).\n * @param args - One or more source objects, optionally followed by a {@link MergeWithCustomizer}.\n * @returns The same `object` reference, now merged with all sources.\n *\n * @remarks\n * - The default merge behaviour only recurses into \"plain objects\", as determined by `isObjectPlain`.\n * - \"Loose objects\" are guarded using `isObjectLoose` to avoid attempting to merge non-object sources.\n * - If you want lodash-like array merging semantics (concat or index-wise), implement it via `customizer`.\n *\n * @example\n * ```ts\n * const target = { a: { x: 1 }, b: 1 };\n * const source = { a: { y: 2 }, b: 2 };\n *\n * mergeWith(target, source);\n * // => { a: { x: 1, y: 2 }, b: 2 }\n * // target is mutated\n * ```\n *\n * @example\n * ```ts\n * const target = { list: [1] };\n * const source = { list: [2] };\n *\n * mergeWith(target, source);\n * // => { list: [2] } (arrays replace by default)\n * ```\n *\n * @example\n * ```ts\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * };\n *\n * mergeWith({ list: [1] }, { list: [2] }, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(object: T, ...sources: S): MergeResult<T, S>;\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(\n object: T,\n ...args: [...sources: S, customizer: MergeWithCustomizer]\n): MergeResult<T, S>;\n\nexport function mergeWith(\n object: Record<PropertyKey, unknown>,\n ...args: (Record<PropertyKey, unknown> | MergeWithCustomizer)[]\n): Record<PropertyKey, unknown> {\n const maybeCustomizer = args.at(-1);\n const customizer = isFunction(maybeCustomizer)\n ? (maybeCustomizer as MergeWithCustomizer)\n : undefined;\n const sources = (customizer ? args.slice(0, -1) : args) as Record<\n PropertyKey,\n unknown\n >[];\n const stack = new WeakMap<object, object>();\n\n for (const source of sources) {\n baseMerge(\n object as Record<PropertyKey, unknown>,\n source,\n customizer,\n stack\n );\n }\n\n return object as any;\n}\n\nconst UNSAFE_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype']);\n\n/**\n * Internal recursive merge implementation used by {@link mergeWith}.\n *\n * @param target - Destination object that is being mutated.\n * @param source - Current source object being merged in.\n * @param customizer - Optional custom merge override.\n * @param stack - Tracks visited source objects to avoid infinite recursion.\n */\nconst baseMerge = (\n target: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n customizer: MergeWithCustomizer | undefined,\n stack: WeakMap<object, object>\n): void => {\n if (!isObjectLoose(source)) return;\n\n for (const key of ownKeys(source)) {\n if (typeof key === 'string' && UNSAFE_KEYS.has(key)) {\n continue;\n }\n const srcValue = (source as any)[key];\n const objValue = (target as any)[key];\n const customized = customizer?.(\n objValue,\n srcValue,\n key,\n target,\n source,\n stack\n );\n\n if (isNotUndefined(customized)) {\n (target as any)[key] = customized;\n continue;\n }\n\n // Both are plain objects: merge into existing destination object.\n if (isObjectPlain(objValue) && isObjectPlain(srcValue)) {\n // If we've seen this src object before, reuse the prior destination.\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n // Cache before recursing to handle cycles.\n stack.set(srcValue as object, objValue as object);\n\n baseMerge(\n objValue as Record<PropertyKey, unknown>,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n continue;\n }\n\n // Source is a plain object but destination isn't: create destination object and merge into it.\n if (isObjectPlain(srcValue)) {\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n const next: Record<PropertyKey, unknown> = isObjectPlain(objValue)\n ? (objValue as Record<PropertyKey, unknown>)\n : {};\n\n (target as any)[key] = next;\n\n // Cache before recursing to handle cycles and repeated refs.\n stack.set(srcValue as object, next as object);\n\n baseMerge(\n next,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n continue;\n }\n\n // Arrays, primitives, dates, maps, sets, functions, etc. are assigned by replacement\n (target as any)[key] = srcValue;\n }\n};\n/**\n * Returns enumerable own property keys (string + symbol) for an object.\n *\n * This mirrors lodash's behaviour of considering both string keys and symbol keys,\n * but only those that are enumerable.\n *\n * @param obj - Object to inspect.\n * @returns Array of enumerable own keys (strings and symbols).\n *\n * @example\n * ```ts\n * const sym = Symbol(\"x\");\n * const o = { a: 1, [sym]: 2 };\n * Object.defineProperty(o, \"hidden\", { value: 3, enumerable: false });\n *\n * ownKeys(o);\n * // => [\"a\", Symbol(\"x\")]\n * ```\n */\nconst ownKeys = (obj: object): (string | symbol)[] => [\n ...Object.keys(obj),\n ...Object.getOwnPropertySymbols(obj).filter((s) =>\n Object.prototype.propertyIsEnumerable.call(obj, s)\n )\n];\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { isObjectLoose } from '@phun-ky/typeof';\n\nimport { mergeWith } from './utils/merge-with';\n\n/**\n * Deeply applies \"defaults\" from right-to-left sources into a new object, while preserving arrays.\n *\n * This function mimics the common lodash pattern:\n * `_.toArray(arguments).reverse().forEach(x => _.mergeWith(output, x, customizer))`,\n * but implemented in vanilla TypeScript with a small, predictable merge core.\n *\n * **Key behaviours**\n * - **Returns a new object** (the returned object is created inside the function).\n * - Sources are processed **right-to-left** (`reverse()`), meaning **earlier arguments win**:\n * values from the first argument are treated as \"defaults\" that should *not* be overwritten\n * by later arguments (because later arguments are merged first).\n * - Only **object-like** sources participate in merging. Non-objects are skipped via `isObjectLoose`.\n * - Uses {@link mergeWith} for the deep merge logic.\n * - **Arrays are preserved by replacement**: if a source value is an array, it replaces the\n * destination value at that key (rather than being merged element-by-element).\n *\n * @param args - A list of potential default/source values. Non-object values are ignored.\n * @returns A new object containing the merged result.\n *\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { a: 1, nested: { x: 1 }, list: [1] },\n * { a: 2, nested: { y: 2 }, list: [2] }\n * );\n * // => { a: 1, nested: { x: 1, y: 2 }, list: [1] }\n * // (earlier args win; arrays preserved by replacement)\n * ```\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { opts: { retry: 3 } },\n * null,\n * { opts: { timeout: 1000 } }\n * );\n * // => { opts: { retry: 3, timeout: 1000 } }\n * // (non-object sources are ignored)\n * ```\n *\n * @remarks\n *\n * - The customizer only checks the *source* value for arrays. If `s` is an array, it is returned\n * and assigned as-is. Otherwise returning `undefined` delegates merging to {@link mergeWith}'s\n * default behaviour (which deep-merges plain objects and replaces other value types).\n * - The output type is `Record<PropertyKey, unknown>` to keep the API broadly usable without\n * over-promising specific shapes.\n *\n */\nconst defaultsDeep = (...args: unknown[]) => {\n const output: Record<PropertyKey, unknown> = {};\n\n for (const item of args.slice().reverse()) {\n if (!isObjectLoose(item)) continue;\n\n mergeWith(output, item, (_o: any, s: any[] | undefined) =>\n Array.isArray(s) ? s : undefined\n );\n }\n\n return output;\n};\n\nexport default defaultsDeep;\n"],"names":["mergeWith","object","args","maybeCustomizer","at","customizer","isFunction","undefined","sources","slice","stack","WeakMap","source","baseMerge","UNSAFE_KEYS","Set","target","isObjectLoose","key","ownKeys","has","srcValue","objValue","customized","isNotUndefined","isObjectPlain","cached","get","set","next","obj","Object","keys","getOwnPropertySymbols","filter","s","prototype","propertyIsEnumerable","call","output","item","reverse","_o","Array","isArray"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uDAmIgBA,EACdC,KACGC,GAEH,MAAMC,EAAkBD,EAAKE,OACvBC,EAAaC,EAAAA,WAAWH,GACzBA,OACDI,EACEC,EAAWH,EAAaH,EAAKO,MAAM,GAAG,GAAMP,EAI5CQ,EAAQ,IAAIC,QAElB,IAAK,MAAMC,KAAUJ,EACnBK,EACEZ,EACAW,EACAP,EACAK,GAIJ,OAAOT,CACT,CAEA,MAAMa,EAAc,IAAIC,IAAY,CAAC,YAAa,cAAe,cAU3DF,EAAY,CAChBG,EACAJ,EACAP,EACAK,KAEA,GAAKO,EAAAA,cAAcL,GAEnB,IAAK,MAAMM,KAAOC,EAAQP,GAAS,CACjC,GAAmB,iBAARM,GAAoBJ,EAAYM,IAAIF,GAC7C,SAEF,MAAMG,EAAYT,EAAeM,GAC3BI,EAAYN,EAAeE,GAC3BK,EAAalB,IACjBiB,EACAD,EACAH,EACAF,EACAJ,EACAF,GAGF,GAAIc,EAAAA,eAAeD,GAChBP,EAAeE,GAAOK,MADzB,CAMA,GAAIE,EAAAA,cAAcH,IAAaG,EAAAA,cAAcJ,GAAW,CAEtD,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAGAhB,EAAMkB,IAAIP,EAAoBC,GAE9BT,EACES,EACAD,EACAhB,EACAK,GAEF,QACF,CAGA,GAAIe,EAAAA,cAAcJ,GAAW,CAC3B,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAEA,MAAMG,EAAqCJ,EAAAA,cAAcH,GACpDA,EACD,CAAA,EAEHN,EAAeE,GAAOW,EAGvBnB,EAAMkB,IAAIP,EAAoBQ,GAE9BhB,EACEgB,EACAR,EACAhB,EACAK,GAEF,QACF,CAGCM,EAAeE,GAAOG,CApDvB,CAqDF,GAqBIF,EAAWW,GAAqC,IACjDC,OAAOC,KAAKF,MACZC,OAAOE,sBAAsBH,GAAKI,OAAQC,GAC3CJ,OAAOK,UAAUC,qBAAqBC,KAAKR,EAAKK,oBCtN/B,IAAIjC,KACvB,MAAMqC,EAAuC,CAAA,EAE7C,IAAK,MAAMC,KAAQtC,EAAKO,QAAQgC,UACzBxB,EAAAA,cAAcuB,IAEnBxC,EAAUuC,EAAQC,EAAM,CAACE,EAASP,IAChCQ,MAAMC,QAAQT,GAAKA,OAAI5B,GAI3B,OAAOgC"}
1
+ {"version":3,"file":"defaults-deep.cjs","sources":["../src/utils/merge-with.ts","../src/main.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n isFunction,\n isNotUndefined,\n isObjectLoose,\n isObjectPlain\n} from '@phun-ky/typeof';\n\n/* node:coverage disable */\n/**\n * Customizer function used by {@link mergeWith} to override merge behaviour.\n *\n * If the customizer returns `undefined`, {@link mergeWith} falls back to its default\n * deep-merge rules. Any other return value will be assigned to the destination key.\n *\n * @param objValue - Current value on the destination (`object`) for `key`.\n * @param srcValue - Incoming value from the current `source` for `key`.\n * @param key - Property key being merged (string or symbol).\n * @param object - Destination object being mutated/merged into.\n * @param source - Current source object being merged from.\n * @param stack - A `WeakMap` used internally to prevent infinite recursion on circular references.\n * @returns The value to assign for this key, or `undefined` to use default merge behaviour.\n *\n * @example\n * ```ts\n * import { mergeWith, type MergeWithCustomizer } from \"./mergeWith\";\n *\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * return undefined;\n * };\n *\n * const a = { list: [1] };\n * const b = { list: [2] };\n *\n * mergeWith(a, b, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n/* node:coverage enable */\nexport type MergeWithCustomizer = (\n objValue: unknown,\n srcValue: unknown,\n key: string | symbol,\n object: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n stack: WeakMap<object, object>\n) => unknown;\n\ntype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n k: infer I\n) => void\n ? I\n : never;\n\ntype MergeResult<T, S extends readonly any[]> = T &\n UnionToIntersection<S[number]>;\n\n/**\n * Deep-merges one or more source objects into a destination object, with optional customization.\n *\n * This is a \"vanilla TS\" alternative to lodash's `mergeWith` with a deliberately conservative\n * default strategy:\n *\n * - **Mutates** and returns the destination `object`.\n * - Enumerates **own enumerable string keys** and **own enumerable symbol keys** from each source.\n * - When both destination and source values are **plain objects**, merges recursively.\n * - When the source value is a **plain object** but the destination is not, creates `{}` and merges into it.\n * - All other value types (arrays, dates, maps, sets, functions, primitives, class instances, etc.)\n * are assigned by **replacement** unless the customizer handles them.\n * - Circular references in **sources** are prevented from causing infinite recursion via a `WeakMap` stack.\n *\n * @typeParam T - Destination object type.\n * @typeParam S - Tuple/array of source object types.\n *\n * @param object - Destination object to merge into (will be mutated).\n * @param args - One or more source objects, optionally followed by a {@link MergeWithCustomizer}.\n * @returns The same `object` reference, now merged with all sources.\n *\n * @remarks\n * - The default merge behaviour only recurses into \"plain objects\", as determined by `isObjectPlain`.\n * - \"Loose objects\" are guarded using `isObjectLoose` to avoid attempting to merge non-object sources.\n * - If you want lodash-like array merging semantics (concat or index-wise), implement it via `customizer`.\n *\n * @example\n * ```ts\n * const target = { a: { x: 1 }, b: 1 };\n * const source = { a: { y: 2 }, b: 2 };\n *\n * mergeWith(target, source);\n * // => { a: { x: 1, y: 2 }, b: 2 }\n * // target is mutated\n * ```\n *\n * @example\n * ```ts\n * const target = { list: [1] };\n * const source = { list: [2] };\n *\n * mergeWith(target, source);\n * // => { list: [2] } (arrays replace by default)\n * ```\n *\n * @example\n * ```ts\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * };\n *\n * mergeWith({ list: [1] }, { list: [2] }, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(object: T, ...sources: S): MergeResult<T, S>;\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(\n object: T,\n ...args: [...sources: S, customizer: MergeWithCustomizer]\n): MergeResult<T, S>;\n\nexport function mergeWith(\n object: Record<PropertyKey, unknown>,\n ...args: (Record<PropertyKey, unknown> | MergeWithCustomizer)[]\n): Record<PropertyKey, unknown> {\n const maybeCustomizer = args.at(-1);\n const customizer = isFunction(maybeCustomizer)\n ? (maybeCustomizer as MergeWithCustomizer)\n : undefined;\n const sources = (customizer ? args.slice(0, -1) : args) as Record<\n PropertyKey,\n unknown\n >[];\n const stack = new WeakMap<object, object>();\n\n for (const source of sources) {\n baseMerge(\n object as Record<PropertyKey, unknown>,\n source,\n customizer,\n stack\n );\n }\n\n return object as any;\n}\n\nconst UNSAFE_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype']);\n/**\n * Internal recursive merge implementation used by {@link mergeWith}.\n *\n * @param target - Destination object that is being mutated.\n * @param source - Current source object being merged in.\n * @param customizer - Optional custom merge override.\n * @param stack - The source objects on the current path, each mapped to its\n * destination, to stop at cycles. Only the current path: a source object\n * reached again through a sibling key gets its own destination.\n */\nconst baseMerge = (\n target: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n customizer: MergeWithCustomizer | undefined,\n stack: WeakMap<object, object>\n): void => {\n if (!isObjectLoose(source)) return;\n\n for (const key of ownKeys(source)) {\n if (typeof key === 'string' && UNSAFE_KEYS.has(key)) {\n continue;\n }\n\n const srcValue = (source as any)[key];\n const objValue = (target as any)[key];\n const customized = customizer?.(\n objValue,\n srcValue,\n key,\n target,\n source,\n stack\n );\n\n if (isNotUndefined(customized)) {\n (target as any)[key] = customized;\n continue;\n }\n\n // Both are plain objects: merge into existing destination object.\n if (isObjectPlain(objValue) && isObjectPlain(srcValue)) {\n // A cycle back to a source object on the current path: point at its destination.\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n stack.set(srcValue as object, objValue as object);\n\n baseMerge(\n objValue as Record<PropertyKey, unknown>,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n stack.delete(srcValue as object);\n continue;\n }\n\n // Source is a plain object but destination isn't: create destination object and merge into it.\n if (isObjectPlain(srcValue)) {\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n const next: Record<PropertyKey, unknown> = isObjectPlain(objValue)\n ? (objValue as Record<PropertyKey, unknown>)\n : {};\n\n (target as any)[key] = next;\n\n stack.set(srcValue as object, next as object);\n\n baseMerge(\n next,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n stack.delete(srcValue as object);\n continue;\n }\n\n // Arrays, primitives, dates, maps, sets, functions, etc. are assigned by replacement\n (target as any)[key] = srcValue;\n }\n};\n/**\n * Returns enumerable own property keys (string + symbol) for an object.\n *\n * This mirrors lodash's behaviour of considering both string keys and symbol keys,\n * but only those that are enumerable.\n *\n * @param obj - Object to inspect.\n * @returns Array of enumerable own keys (strings and symbols).\n *\n * @example\n * ```ts\n * const sym = Symbol(\"x\");\n * const o = { a: 1, [sym]: 2 };\n * Object.defineProperty(o, \"hidden\", { value: 3, enumerable: false });\n *\n * ownKeys(o);\n * // => [\"a\", Symbol(\"x\")]\n * ```\n */\nconst ownKeys = (obj: object): (string | symbol)[] => [\n ...Object.keys(obj),\n ...Object.getOwnPropertySymbols(obj).filter((s) =>\n Object.prototype.propertyIsEnumerable.call(obj, s)\n )\n];\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { isObjectLoose } from '@phun-ky/typeof';\n\nimport { mergeWith } from './utils/merge-with';\n\n/**\n * Deeply applies \"defaults\" from right-to-left sources into a new object, while preserving arrays.\n *\n * This function mimics the common lodash pattern:\n * `_.toArray(arguments).reverse().forEach(x => _.mergeWith(output, x, customizer))`,\n * but implemented in vanilla TypeScript with a small, predictable merge core.\n *\n * **Key behaviours**\n * - **Returns a new object** (the returned object is created inside the function).\n * - Sources are processed **right-to-left** (`reverse()`), meaning **earlier arguments win**:\n * values from the first argument are treated as \"defaults\" that should *not* be overwritten\n * by later arguments (because later arguments are merged first).\n * - Only **object-like** sources participate in merging. Non-objects are skipped via `isObjectLoose`.\n * - Uses an internal `mergeWith` for the deep merge logic.\n * - **Arrays are preserved by replacement**: if a source value is an array, it replaces the\n * destination value at that key (rather than being merged element-by-element).\n *\n * @param args - A list of potential default/source values. Non-object values are ignored.\n * @returns A new object containing the merged result.\n *\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { a: 1, nested: { x: 1 }, list: [1] },\n * { a: 2, nested: { y: 2 }, list: [2] }\n * );\n * // => { a: 1, nested: { x: 1, y: 2 }, list: [1] }\n * // (earlier args win; arrays preserved by replacement)\n * ```\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { opts: { retry: 3 } },\n * null,\n * { opts: { timeout: 1000 } }\n * );\n * // => { opts: { retry: 3, timeout: 1000 } }\n * // (non-object sources are ignored)\n * ```\n *\n * @remarks\n *\n * - The customizer only checks the *source* value for arrays. If `s` is an array, it is returned\n * and assigned as-is. Otherwise returning `undefined` delegates merging to `mergeWith`'s\n * default behaviour (which deep-merges plain objects and replaces other value types).\n * - The output type is `Record<PropertyKey, unknown>` to keep the API broadly usable without\n * over-promising specific shapes.\n *\n */\nconst defaultsDeep = (...args: unknown[]) => {\n const output: Record<PropertyKey, unknown> = {};\n\n for (const item of args.slice().reverse()) {\n if (!isObjectLoose(item)) continue;\n\n mergeWith(output, item, (_o: any, s: any[] | undefined) =>\n Array.isArray(s) ? s : undefined\n );\n }\n\n return output;\n};\n\nexport default defaultsDeep;\n"],"names":["mergeWith","object","args","maybeCustomizer","at","customizer","isFunction","undefined","sources","slice","stack","WeakMap","source","baseMerge","UNSAFE_KEYS","Set","target","isObjectLoose","key","ownKeys","has","srcValue","objValue","customized","isNotUndefined","isObjectPlain","cached","get","set","delete","next","obj","Object","keys","getOwnPropertySymbols","filter","s","prototype","propertyIsEnumerable","call","output","item","reverse","_o","Array","isArray"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uDAmIgBA,EACdC,KACGC,GAEH,MAAMC,EAAkBD,EAAKE,OACvBC,EAAaC,EAAAA,WAAWH,GACzBA,OACDI,EACEC,EAAWH,EAAaH,EAAKO,MAAM,GAAG,GAAMP,EAI5CQ,EAAQ,IAAIC,QAElB,IAAK,MAAMC,KAAUJ,EACnBK,EACEZ,EACAW,EACAP,EACAK,GAIJ,OAAOT,CACT,CAEA,MAAMa,EAAc,IAAIC,IAAY,CAAC,YAAa,cAAe,cAW3DF,EAAY,CAChBG,EACAJ,EACAP,EACAK,KAEA,GAAKO,EAAAA,cAAcL,GAEnB,IAAK,MAAMM,KAAOC,EAAQP,GAAS,CACjC,GAAmB,iBAARM,GAAoBJ,EAAYM,IAAIF,GAC7C,SAGF,MAAMG,EAAYT,EAAeM,GAC3BI,EAAYN,EAAeE,GAC3BK,EAAalB,IACjBiB,EACAD,EACAH,EACAF,EACAJ,EACAF,GAGF,GAAIc,EAAAA,eAAeD,GAChBP,EAAeE,GAAOK,MADzB,CAMA,GAAIE,EAAAA,cAAcH,IAAaG,EAAAA,cAAcJ,GAAW,CAEtD,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAEAhB,EAAMkB,IAAIP,EAAoBC,GAE9BT,EACES,EACAD,EACAhB,EACAK,GAEFA,EAAMmB,OAAOR,GACb,QACF,CAGA,GAAII,EAAAA,cAAcJ,GAAW,CAC3B,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAEA,MAAMI,EAAqCL,EAAAA,cAAcH,GACpDA,EACD,CAAA,EAEHN,EAAeE,GAAOY,EAEvBpB,EAAMkB,IAAIP,EAAoBS,GAE9BjB,EACEiB,EACAT,EACAhB,EACAK,GAEFA,EAAMmB,OAAOR,GACb,QACF,CAGCL,EAAeE,GAAOG,CApDvB,CAqDF,GAqBIF,EAAWY,GAAqC,IACjDC,OAAOC,KAAKF,MACZC,OAAOE,sBAAsBH,GAAKI,OAAQC,GAC3CJ,OAAOK,UAAUC,qBAAqBC,KAAKR,EAAKK,oBCxN/B,IAAIlC,KACvB,MAAMsC,EAAuC,CAAA,EAE7C,IAAK,MAAMC,KAAQvC,EAAKO,QAAQiC,UACzBzB,EAAAA,cAAcwB,IAEnBzC,EAAUwC,EAAQC,EAAM,CAACE,EAASP,IAChCQ,MAAMC,QAAQT,GAAKA,OAAI7B,GAI3B,OAAOiC"}
@@ -11,7 +11,7 @@
11
11
  * values from the first argument are treated as "defaults" that should *not* be overwritten
12
12
  * by later arguments (because later arguments are merged first).
13
13
  * - Only **object-like** sources participate in merging. Non-objects are skipped via `isObjectLoose`.
14
- * - Uses {@link mergeWith} for the deep merge logic.
14
+ * - Uses an internal `mergeWith` for the deep merge logic.
15
15
  * - **Arrays are preserved by replacement**: if a source value is an array, it replaces the
16
16
  * destination value at that key (rather than being merged element-by-element).
17
17
  *
@@ -43,7 +43,7 @@
43
43
  * @remarks
44
44
  *
45
45
  * - The customizer only checks the *source* value for arrays. If `s` is an array, it is returned
46
- * and assigned as-is. Otherwise returning `undefined` delegates merging to {@link mergeWith}'s
46
+ * and assigned as-is. Otherwise returning `undefined` delegates merging to `mergeWith`'s
47
47
  * default behaviour (which deep-merges plain objects and replaces other value types).
48
48
  * - The output type is `Record<PropertyKey, unknown>` to keep the API broadly usable without
49
49
  * over-promising specific shapes.
@@ -11,7 +11,7 @@
11
11
  * values from the first argument are treated as "defaults" that should *not* be overwritten
12
12
  * by later arguments (because later arguments are merged first).
13
13
  * - Only **object-like** sources participate in merging. Non-objects are skipped via `isObjectLoose`.
14
- * - Uses {@link mergeWith} for the deep merge logic.
14
+ * - Uses an internal `mergeWith` for the deep merge logic.
15
15
  * - **Arrays are preserved by replacement**: if a source value is an array, it replaces the
16
16
  * destination value at that key (rather than being merged element-by-element).
17
17
  *
@@ -43,7 +43,7 @@
43
43
  * @remarks
44
44
  *
45
45
  * - The customizer only checks the *source* value for arrays. If `s` is an array, it is returned
46
- * and assigned as-is. Otherwise returning `undefined` delegates merging to {@link mergeWith}'s
46
+ * and assigned as-is. Otherwise returning `undefined` delegates merging to `mergeWith`'s
47
47
  * default behaviour (which deep-merges plain objects and replaces other value types).
48
48
  * - The output type is `Record<PropertyKey, unknown>` to keep the API broadly usable without
49
49
  * over-promising specific shapes.
@@ -2,7 +2,7 @@
2
2
  * @phun-ky/defaults-deep
3
3
  * Like lodash's _.defaultsDeep, but with array preservation
4
4
  * @author Alexander Vassbotn Røyne-Helgesen <alexander@phun-ky.net>
5
- * @version 2.0.8
5
+ * @version 2.0.9
6
6
  * @license
7
7
  * Copyright (c) 2026 Alexander Vassbotn Røyne-Helgesen
8
8
  *
@@ -40,5 +40,5 @@
40
40
  * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
41
41
  * PERFORMANCE OF THIS SOFTWARE.
42
42
  */
43
- import{isFunction as t,isObjectLoose as o,isNotUndefined as e,isObjectPlain as n}from"@phun-ky/typeof";function r(o,...e){const n=e.at(-1),r=t(n)?n:void 0,c=r?e.slice(0,-1):e,i=new WeakMap;for(const t of c)s(o,t,r,i);return o}const c=new Set(["__proto__","constructor","prototype"]),s=(t,r,f,p)=>{if(o(r))for(const o of i(r)){if("string"==typeof o&&c.has(o))continue;const i=r[o],u=t[o],y=f?.(u,i,o,t,r,p);if(e(y))t[o]=y;else{if(n(u)&&n(i)){const e=p.get(i);if(e){t[o]=e;continue}p.set(i,u),s(u,i,f,p);continue}if(n(i)){const e=p.get(i);if(e){t[o]=e;continue}const r=n(u)?u:{};t[o]=r,p.set(i,r),s(r,i,f,p);continue}t[o]=i}}},i=t=>[...Object.keys(t),...Object.getOwnPropertySymbols(t).filter(o=>Object.prototype.propertyIsEnumerable.call(t,o))],f=(...t)=>{const e={};for(const n of t.slice().reverse())o(n)&&r(e,n,(t,o)=>Array.isArray(o)?o:void 0);return e};export{f as default};
43
+ import{isFunction as t,isObjectLoose as e,isNotUndefined as o,isObjectPlain as n}from"@phun-ky/typeof";function r(e,...o){const n=o.at(-1),r=t(n)?n:void 0,c=r?o.slice(0,-1):o,i=new WeakMap;for(const t of c)s(e,t,r,i);return e}const c=new Set(["__proto__","constructor","prototype"]),s=(t,r,f,p)=>{if(e(r))for(const e of i(r)){if("string"==typeof e&&c.has(e))continue;const i=r[e],u=t[e],l=f?.(u,i,e,t,r,p);if(o(l))t[e]=l;else{if(n(u)&&n(i)){const o=p.get(i);if(o){t[e]=o;continue}p.set(i,u),s(u,i,f,p),p.delete(i);continue}if(n(i)){const o=p.get(i);if(o){t[e]=o;continue}const r=n(u)?u:{};t[e]=r,p.set(i,r),s(r,i,f,p),p.delete(i);continue}t[e]=i}}},i=t=>[...Object.keys(t),...Object.getOwnPropertySymbols(t).filter(e=>Object.prototype.propertyIsEnumerable.call(t,e))],f=(...t)=>{const o={};for(const n of t.slice().reverse())e(n)&&r(o,n,(t,e)=>Array.isArray(e)?e:void 0);return o};export{f as default};
44
44
  //# sourceMappingURL=defaults-deep.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"defaults-deep.js","sources":["../src/utils/merge-with.ts","../src/main.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n isFunction,\n isNotUndefined,\n isObjectLoose,\n isObjectPlain\n} from '@phun-ky/typeof';\n\n/* node:coverage disable */\n/**\n * Customizer function used by {@link mergeWith} to override merge behaviour.\n *\n * If the customizer returns `undefined`, {@link mergeWith} falls back to its default\n * deep-merge rules. Any other return value will be assigned to the destination key.\n *\n * @param objValue - Current value on the destination (`object`) for `key`.\n * @param srcValue - Incoming value from the current `source` for `key`.\n * @param key - Property key being merged (string or symbol).\n * @param object - Destination object being mutated/merged into.\n * @param source - Current source object being merged from.\n * @param stack - A `WeakMap` used internally to prevent infinite recursion on circular references.\n * @returns The value to assign for this key, or `undefined` to use default merge behaviour.\n *\n * @example\n * ```ts\n * import { mergeWith, type MergeWithCustomizer } from \"./mergeWith\";\n *\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * return undefined;\n * };\n *\n * const a = { list: [1] };\n * const b = { list: [2] };\n *\n * mergeWith(a, b, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n/* node:coverage enable */\nexport type MergeWithCustomizer = (\n objValue: unknown,\n srcValue: unknown,\n key: string | symbol,\n object: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n stack: WeakMap<object, object>\n) => unknown;\n\ntype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n k: infer I\n) => void\n ? I\n : never;\n\ntype MergeResult<T, S extends readonly any[]> = T &\n UnionToIntersection<S[number]>;\n\n/**\n * Deep-merges one or more source objects into a destination object, with optional customization.\n *\n * This is a \"vanilla TS\" alternative to lodash's `mergeWith` with a deliberately conservative\n * default strategy:\n *\n * - **Mutates** and returns the destination `object`.\n * - Enumerates **own enumerable string keys** and **own enumerable symbol keys** from each source.\n * - When both destination and source values are **plain objects**, merges recursively.\n * - When the source value is a **plain object** but the destination is not, creates `{}` and merges into it.\n * - All other value types (arrays, dates, maps, sets, functions, primitives, class instances, etc.)\n * are assigned by **replacement** unless the customizer handles them.\n * - Circular references in **sources** are prevented from causing infinite recursion via a `WeakMap` stack.\n *\n * @typeParam T - Destination object type.\n * @typeParam S - Tuple/array of source object types.\n *\n * @param object - Destination object to merge into (will be mutated).\n * @param args - One or more source objects, optionally followed by a {@link MergeWithCustomizer}.\n * @returns The same `object` reference, now merged with all sources.\n *\n * @remarks\n * - The default merge behaviour only recurses into \"plain objects\", as determined by `isObjectPlain`.\n * - \"Loose objects\" are guarded using `isObjectLoose` to avoid attempting to merge non-object sources.\n * - If you want lodash-like array merging semantics (concat or index-wise), implement it via `customizer`.\n *\n * @example\n * ```ts\n * const target = { a: { x: 1 }, b: 1 };\n * const source = { a: { y: 2 }, b: 2 };\n *\n * mergeWith(target, source);\n * // => { a: { x: 1, y: 2 }, b: 2 }\n * // target is mutated\n * ```\n *\n * @example\n * ```ts\n * const target = { list: [1] };\n * const source = { list: [2] };\n *\n * mergeWith(target, source);\n * // => { list: [2] } (arrays replace by default)\n * ```\n *\n * @example\n * ```ts\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * };\n *\n * mergeWith({ list: [1] }, { list: [2] }, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(object: T, ...sources: S): MergeResult<T, S>;\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(\n object: T,\n ...args: [...sources: S, customizer: MergeWithCustomizer]\n): MergeResult<T, S>;\n\nexport function mergeWith(\n object: Record<PropertyKey, unknown>,\n ...args: (Record<PropertyKey, unknown> | MergeWithCustomizer)[]\n): Record<PropertyKey, unknown> {\n const maybeCustomizer = args.at(-1);\n const customizer = isFunction(maybeCustomizer)\n ? (maybeCustomizer as MergeWithCustomizer)\n : undefined;\n const sources = (customizer ? args.slice(0, -1) : args) as Record<\n PropertyKey,\n unknown\n >[];\n const stack = new WeakMap<object, object>();\n\n for (const source of sources) {\n baseMerge(\n object as Record<PropertyKey, unknown>,\n source,\n customizer,\n stack\n );\n }\n\n return object as any;\n}\n\nconst UNSAFE_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype']);\n\n/**\n * Internal recursive merge implementation used by {@link mergeWith}.\n *\n * @param target - Destination object that is being mutated.\n * @param source - Current source object being merged in.\n * @param customizer - Optional custom merge override.\n * @param stack - Tracks visited source objects to avoid infinite recursion.\n */\nconst baseMerge = (\n target: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n customizer: MergeWithCustomizer | undefined,\n stack: WeakMap<object, object>\n): void => {\n if (!isObjectLoose(source)) return;\n\n for (const key of ownKeys(source)) {\n if (typeof key === 'string' && UNSAFE_KEYS.has(key)) {\n continue;\n }\n const srcValue = (source as any)[key];\n const objValue = (target as any)[key];\n const customized = customizer?.(\n objValue,\n srcValue,\n key,\n target,\n source,\n stack\n );\n\n if (isNotUndefined(customized)) {\n (target as any)[key] = customized;\n continue;\n }\n\n // Both are plain objects: merge into existing destination object.\n if (isObjectPlain(objValue) && isObjectPlain(srcValue)) {\n // If we've seen this src object before, reuse the prior destination.\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n // Cache before recursing to handle cycles.\n stack.set(srcValue as object, objValue as object);\n\n baseMerge(\n objValue as Record<PropertyKey, unknown>,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n continue;\n }\n\n // Source is a plain object but destination isn't: create destination object and merge into it.\n if (isObjectPlain(srcValue)) {\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n const next: Record<PropertyKey, unknown> = isObjectPlain(objValue)\n ? (objValue as Record<PropertyKey, unknown>)\n : {};\n\n (target as any)[key] = next;\n\n // Cache before recursing to handle cycles and repeated refs.\n stack.set(srcValue as object, next as object);\n\n baseMerge(\n next,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n continue;\n }\n\n // Arrays, primitives, dates, maps, sets, functions, etc. are assigned by replacement\n (target as any)[key] = srcValue;\n }\n};\n/**\n * Returns enumerable own property keys (string + symbol) for an object.\n *\n * This mirrors lodash's behaviour of considering both string keys and symbol keys,\n * but only those that are enumerable.\n *\n * @param obj - Object to inspect.\n * @returns Array of enumerable own keys (strings and symbols).\n *\n * @example\n * ```ts\n * const sym = Symbol(\"x\");\n * const o = { a: 1, [sym]: 2 };\n * Object.defineProperty(o, \"hidden\", { value: 3, enumerable: false });\n *\n * ownKeys(o);\n * // => [\"a\", Symbol(\"x\")]\n * ```\n */\nconst ownKeys = (obj: object): (string | symbol)[] => [\n ...Object.keys(obj),\n ...Object.getOwnPropertySymbols(obj).filter((s) =>\n Object.prototype.propertyIsEnumerable.call(obj, s)\n )\n];\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { isObjectLoose } from '@phun-ky/typeof';\n\nimport { mergeWith } from './utils/merge-with';\n\n/**\n * Deeply applies \"defaults\" from right-to-left sources into a new object, while preserving arrays.\n *\n * This function mimics the common lodash pattern:\n * `_.toArray(arguments).reverse().forEach(x => _.mergeWith(output, x, customizer))`,\n * but implemented in vanilla TypeScript with a small, predictable merge core.\n *\n * **Key behaviours**\n * - **Returns a new object** (the returned object is created inside the function).\n * - Sources are processed **right-to-left** (`reverse()`), meaning **earlier arguments win**:\n * values from the first argument are treated as \"defaults\" that should *not* be overwritten\n * by later arguments (because later arguments are merged first).\n * - Only **object-like** sources participate in merging. Non-objects are skipped via `isObjectLoose`.\n * - Uses {@link mergeWith} for the deep merge logic.\n * - **Arrays are preserved by replacement**: if a source value is an array, it replaces the\n * destination value at that key (rather than being merged element-by-element).\n *\n * @param args - A list of potential default/source values. Non-object values are ignored.\n * @returns A new object containing the merged result.\n *\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { a: 1, nested: { x: 1 }, list: [1] },\n * { a: 2, nested: { y: 2 }, list: [2] }\n * );\n * // => { a: 1, nested: { x: 1, y: 2 }, list: [1] }\n * // (earlier args win; arrays preserved by replacement)\n * ```\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { opts: { retry: 3 } },\n * null,\n * { opts: { timeout: 1000 } }\n * );\n * // => { opts: { retry: 3, timeout: 1000 } }\n * // (non-object sources are ignored)\n * ```\n *\n * @remarks\n *\n * - The customizer only checks the *source* value for arrays. If `s` is an array, it is returned\n * and assigned as-is. Otherwise returning `undefined` delegates merging to {@link mergeWith}'s\n * default behaviour (which deep-merges plain objects and replaces other value types).\n * - The output type is `Record<PropertyKey, unknown>` to keep the API broadly usable without\n * over-promising specific shapes.\n *\n */\nconst defaultsDeep = (...args: unknown[]) => {\n const output: Record<PropertyKey, unknown> = {};\n\n for (const item of args.slice().reverse()) {\n if (!isObjectLoose(item)) continue;\n\n mergeWith(output, item, (_o: any, s: any[] | undefined) =>\n Array.isArray(s) ? s : undefined\n );\n }\n\n return output;\n};\n\nexport default defaultsDeep;\n"],"names":["mergeWith","object","args","maybeCustomizer","at","customizer","isFunction","undefined","sources","slice","stack","WeakMap","source","baseMerge","UNSAFE_KEYS","Set","target","isObjectLoose","key","ownKeys","has","srcValue","objValue","customized","isNotUndefined","isObjectPlain","cached","get","set","next","obj","Object","keys","getOwnPropertySymbols","filter","s","prototype","propertyIsEnumerable","call","defaultsDeep","output","item","reverse","_o","Array","isArray"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gHAmIgBA,EACdC,KACGC,GAEH,MAAMC,EAAkBD,EAAKE,OACvBC,EAAaC,EAAWH,GACzBA,OACDI,EACEC,EAAWH,EAAaH,EAAKO,MAAM,GAAG,GAAMP,EAI5CQ,EAAQ,IAAIC,QAElB,IAAK,MAAMC,KAAUJ,EACnBK,EACEZ,EACAW,EACAP,EACAK,GAIJ,OAAOT,CACT,CAEA,MAAMa,EAAc,IAAIC,IAAY,CAAC,YAAa,cAAe,cAU3DF,EAAY,CAChBG,EACAJ,EACAP,EACAK,KAEA,GAAKO,EAAcL,GAEnB,IAAK,MAAMM,KAAOC,EAAQP,GAAS,CACjC,GAAmB,iBAARM,GAAoBJ,EAAYM,IAAIF,GAC7C,SAEF,MAAMG,EAAYT,EAAeM,GAC3BI,EAAYN,EAAeE,GAC3BK,EAAalB,IACjBiB,EACAD,EACAH,EACAF,EACAJ,EACAF,GAGF,GAAIc,EAAeD,GAChBP,EAAeE,GAAOK,MADzB,CAMA,GAAIE,EAAcH,IAAaG,EAAcJ,GAAW,CAEtD,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAGAhB,EAAMkB,IAAIP,EAAoBC,GAE9BT,EACES,EACAD,EACAhB,EACAK,GAEF,QACF,CAGA,GAAIe,EAAcJ,GAAW,CAC3B,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAEA,MAAMG,EAAqCJ,EAAcH,GACpDA,EACD,CAAA,EAEHN,EAAeE,GAAOW,EAGvBnB,EAAMkB,IAAIP,EAAoBQ,GAE9BhB,EACEgB,EACAR,EACAhB,EACAK,GAEF,QACF,CAGCM,EAAeE,GAAOG,CApDvB,CAqDF,GAqBIF,EAAWW,GAAqC,IACjDC,OAAOC,KAAKF,MACZC,OAAOE,sBAAsBH,GAAKI,OAAQC,GAC3CJ,OAAOK,UAAUC,qBAAqBC,KAAKR,EAAKK,KCtN9CI,EAAe,IAAIrC,KACvB,MAAMsC,EAAuC,CAAA,EAE7C,IAAK,MAAMC,KAAQvC,EAAKO,QAAQiC,UACzBzB,EAAcwB,IAEnBzC,EAAUwC,EAAQC,EAAM,CAACE,EAASR,IAChCS,MAAMC,QAAQV,GAAKA,OAAI5B,GAI3B,OAAOiC"}
1
+ {"version":3,"file":"defaults-deep.js","sources":["../src/utils/merge-with.ts","../src/main.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n isFunction,\n isNotUndefined,\n isObjectLoose,\n isObjectPlain\n} from '@phun-ky/typeof';\n\n/* node:coverage disable */\n/**\n * Customizer function used by {@link mergeWith} to override merge behaviour.\n *\n * If the customizer returns `undefined`, {@link mergeWith} falls back to its default\n * deep-merge rules. Any other return value will be assigned to the destination key.\n *\n * @param objValue - Current value on the destination (`object`) for `key`.\n * @param srcValue - Incoming value from the current `source` for `key`.\n * @param key - Property key being merged (string or symbol).\n * @param object - Destination object being mutated/merged into.\n * @param source - Current source object being merged from.\n * @param stack - A `WeakMap` used internally to prevent infinite recursion on circular references.\n * @returns The value to assign for this key, or `undefined` to use default merge behaviour.\n *\n * @example\n * ```ts\n * import { mergeWith, type MergeWithCustomizer } from \"./mergeWith\";\n *\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * return undefined;\n * };\n *\n * const a = { list: [1] };\n * const b = { list: [2] };\n *\n * mergeWith(a, b, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n/* node:coverage enable */\nexport type MergeWithCustomizer = (\n objValue: unknown,\n srcValue: unknown,\n key: string | symbol,\n object: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n stack: WeakMap<object, object>\n) => unknown;\n\ntype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n k: infer I\n) => void\n ? I\n : never;\n\ntype MergeResult<T, S extends readonly any[]> = T &\n UnionToIntersection<S[number]>;\n\n/**\n * Deep-merges one or more source objects into a destination object, with optional customization.\n *\n * This is a \"vanilla TS\" alternative to lodash's `mergeWith` with a deliberately conservative\n * default strategy:\n *\n * - **Mutates** and returns the destination `object`.\n * - Enumerates **own enumerable string keys** and **own enumerable symbol keys** from each source.\n * - When both destination and source values are **plain objects**, merges recursively.\n * - When the source value is a **plain object** but the destination is not, creates `{}` and merges into it.\n * - All other value types (arrays, dates, maps, sets, functions, primitives, class instances, etc.)\n * are assigned by **replacement** unless the customizer handles them.\n * - Circular references in **sources** are prevented from causing infinite recursion via a `WeakMap` stack.\n *\n * @typeParam T - Destination object type.\n * @typeParam S - Tuple/array of source object types.\n *\n * @param object - Destination object to merge into (will be mutated).\n * @param args - One or more source objects, optionally followed by a {@link MergeWithCustomizer}.\n * @returns The same `object` reference, now merged with all sources.\n *\n * @remarks\n * - The default merge behaviour only recurses into \"plain objects\", as determined by `isObjectPlain`.\n * - \"Loose objects\" are guarded using `isObjectLoose` to avoid attempting to merge non-object sources.\n * - If you want lodash-like array merging semantics (concat or index-wise), implement it via `customizer`.\n *\n * @example\n * ```ts\n * const target = { a: { x: 1 }, b: 1 };\n * const source = { a: { y: 2 }, b: 2 };\n *\n * mergeWith(target, source);\n * // => { a: { x: 1, y: 2 }, b: 2 }\n * // target is mutated\n * ```\n *\n * @example\n * ```ts\n * const target = { list: [1] };\n * const source = { list: [2] };\n *\n * mergeWith(target, source);\n * // => { list: [2] } (arrays replace by default)\n * ```\n *\n * @example\n * ```ts\n * const concatArrays: MergeWithCustomizer = (objValue, srcValue) => {\n * if (Array.isArray(objValue) && Array.isArray(srcValue)) {\n * return objValue.concat(srcValue);\n * }\n * };\n *\n * mergeWith({ list: [1] }, { list: [2] }, concatArrays);\n * // => { list: [1, 2] }\n * ```\n */\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(object: T, ...sources: S): MergeResult<T, S>;\n\nexport function mergeWith<\n T extends Record<PropertyKey, any>,\n S extends readonly Record<PropertyKey, any>[]\n>(\n object: T,\n ...args: [...sources: S, customizer: MergeWithCustomizer]\n): MergeResult<T, S>;\n\nexport function mergeWith(\n object: Record<PropertyKey, unknown>,\n ...args: (Record<PropertyKey, unknown> | MergeWithCustomizer)[]\n): Record<PropertyKey, unknown> {\n const maybeCustomizer = args.at(-1);\n const customizer = isFunction(maybeCustomizer)\n ? (maybeCustomizer as MergeWithCustomizer)\n : undefined;\n const sources = (customizer ? args.slice(0, -1) : args) as Record<\n PropertyKey,\n unknown\n >[];\n const stack = new WeakMap<object, object>();\n\n for (const source of sources) {\n baseMerge(\n object as Record<PropertyKey, unknown>,\n source,\n customizer,\n stack\n );\n }\n\n return object as any;\n}\n\nconst UNSAFE_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype']);\n/**\n * Internal recursive merge implementation used by {@link mergeWith}.\n *\n * @param target - Destination object that is being mutated.\n * @param source - Current source object being merged in.\n * @param customizer - Optional custom merge override.\n * @param stack - The source objects on the current path, each mapped to its\n * destination, to stop at cycles. Only the current path: a source object\n * reached again through a sibling key gets its own destination.\n */\nconst baseMerge = (\n target: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n customizer: MergeWithCustomizer | undefined,\n stack: WeakMap<object, object>\n): void => {\n if (!isObjectLoose(source)) return;\n\n for (const key of ownKeys(source)) {\n if (typeof key === 'string' && UNSAFE_KEYS.has(key)) {\n continue;\n }\n\n const srcValue = (source as any)[key];\n const objValue = (target as any)[key];\n const customized = customizer?.(\n objValue,\n srcValue,\n key,\n target,\n source,\n stack\n );\n\n if (isNotUndefined(customized)) {\n (target as any)[key] = customized;\n continue;\n }\n\n // Both are plain objects: merge into existing destination object.\n if (isObjectPlain(objValue) && isObjectPlain(srcValue)) {\n // A cycle back to a source object on the current path: point at its destination.\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n stack.set(srcValue as object, objValue as object);\n\n baseMerge(\n objValue as Record<PropertyKey, unknown>,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n stack.delete(srcValue as object);\n continue;\n }\n\n // Source is a plain object but destination isn't: create destination object and merge into it.\n if (isObjectPlain(srcValue)) {\n const cached = stack.get(srcValue as object);\n\n if (cached) {\n (target as any)[key] = cached;\n continue;\n }\n\n const next: Record<PropertyKey, unknown> = isObjectPlain(objValue)\n ? (objValue as Record<PropertyKey, unknown>)\n : {};\n\n (target as any)[key] = next;\n\n stack.set(srcValue as object, next as object);\n\n baseMerge(\n next,\n srcValue as Record<PropertyKey, unknown>,\n customizer,\n stack\n );\n stack.delete(srcValue as object);\n continue;\n }\n\n // Arrays, primitives, dates, maps, sets, functions, etc. are assigned by replacement\n (target as any)[key] = srcValue;\n }\n};\n/**\n * Returns enumerable own property keys (string + symbol) for an object.\n *\n * This mirrors lodash's behaviour of considering both string keys and symbol keys,\n * but only those that are enumerable.\n *\n * @param obj - Object to inspect.\n * @returns Array of enumerable own keys (strings and symbols).\n *\n * @example\n * ```ts\n * const sym = Symbol(\"x\");\n * const o = { a: 1, [sym]: 2 };\n * Object.defineProperty(o, \"hidden\", { value: 3, enumerable: false });\n *\n * ownKeys(o);\n * // => [\"a\", Symbol(\"x\")]\n * ```\n */\nconst ownKeys = (obj: object): (string | symbol)[] => [\n ...Object.keys(obj),\n ...Object.getOwnPropertySymbols(obj).filter((s) =>\n Object.prototype.propertyIsEnumerable.call(obj, s)\n )\n];\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { isObjectLoose } from '@phun-ky/typeof';\n\nimport { mergeWith } from './utils/merge-with';\n\n/**\n * Deeply applies \"defaults\" from right-to-left sources into a new object, while preserving arrays.\n *\n * This function mimics the common lodash pattern:\n * `_.toArray(arguments).reverse().forEach(x => _.mergeWith(output, x, customizer))`,\n * but implemented in vanilla TypeScript with a small, predictable merge core.\n *\n * **Key behaviours**\n * - **Returns a new object** (the returned object is created inside the function).\n * - Sources are processed **right-to-left** (`reverse()`), meaning **earlier arguments win**:\n * values from the first argument are treated as \"defaults\" that should *not* be overwritten\n * by later arguments (because later arguments are merged first).\n * - Only **object-like** sources participate in merging. Non-objects are skipped via `isObjectLoose`.\n * - Uses an internal `mergeWith` for the deep merge logic.\n * - **Arrays are preserved by replacement**: if a source value is an array, it replaces the\n * destination value at that key (rather than being merged element-by-element).\n *\n * @param args - A list of potential default/source values. Non-object values are ignored.\n * @returns A new object containing the merged result.\n *\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { a: 1, nested: { x: 1 }, list: [1] },\n * { a: 2, nested: { y: 2 }, list: [2] }\n * );\n * // => { a: 1, nested: { x: 1, y: 2 }, list: [1] }\n * // (earlier args win; arrays preserved by replacement)\n * ```\n *\n * @example\n * ```ts\n * defaultsDeep(\n * { opts: { retry: 3 } },\n * null,\n * { opts: { timeout: 1000 } }\n * );\n * // => { opts: { retry: 3, timeout: 1000 } }\n * // (non-object sources are ignored)\n * ```\n *\n * @remarks\n *\n * - The customizer only checks the *source* value for arrays. If `s` is an array, it is returned\n * and assigned as-is. Otherwise returning `undefined` delegates merging to `mergeWith`'s\n * default behaviour (which deep-merges plain objects and replaces other value types).\n * - The output type is `Record<PropertyKey, unknown>` to keep the API broadly usable without\n * over-promising specific shapes.\n *\n */\nconst defaultsDeep = (...args: unknown[]) => {\n const output: Record<PropertyKey, unknown> = {};\n\n for (const item of args.slice().reverse()) {\n if (!isObjectLoose(item)) continue;\n\n mergeWith(output, item, (_o: any, s: any[] | undefined) =>\n Array.isArray(s) ? s : undefined\n );\n }\n\n return output;\n};\n\nexport default defaultsDeep;\n"],"names":["mergeWith","object","args","maybeCustomizer","at","customizer","isFunction","undefined","sources","slice","stack","WeakMap","source","baseMerge","UNSAFE_KEYS","Set","target","isObjectLoose","key","ownKeys","has","srcValue","objValue","customized","isNotUndefined","isObjectPlain","cached","get","set","delete","next","obj","Object","keys","getOwnPropertySymbols","filter","s","prototype","propertyIsEnumerable","call","defaultsDeep","output","item","reverse","_o","Array","isArray"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gHAmIgBA,EACdC,KACGC,GAEH,MAAMC,EAAkBD,EAAKE,OACvBC,EAAaC,EAAWH,GACzBA,OACDI,EACEC,EAAWH,EAAaH,EAAKO,MAAM,GAAG,GAAMP,EAI5CQ,EAAQ,IAAIC,QAElB,IAAK,MAAMC,KAAUJ,EACnBK,EACEZ,EACAW,EACAP,EACAK,GAIJ,OAAOT,CACT,CAEA,MAAMa,EAAc,IAAIC,IAAY,CAAC,YAAa,cAAe,cAW3DF,EAAY,CAChBG,EACAJ,EACAP,EACAK,KAEA,GAAKO,EAAcL,GAEnB,IAAK,MAAMM,KAAOC,EAAQP,GAAS,CACjC,GAAmB,iBAARM,GAAoBJ,EAAYM,IAAIF,GAC7C,SAGF,MAAMG,EAAYT,EAAeM,GAC3BI,EAAYN,EAAeE,GAC3BK,EAAalB,IACjBiB,EACAD,EACAH,EACAF,EACAJ,EACAF,GAGF,GAAIc,EAAeD,GAChBP,EAAeE,GAAOK,MADzB,CAMA,GAAIE,EAAcH,IAAaG,EAAcJ,GAAW,CAEtD,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAEAhB,EAAMkB,IAAIP,EAAoBC,GAE9BT,EACES,EACAD,EACAhB,EACAK,GAEFA,EAAMmB,OAAOR,GACb,QACF,CAGA,GAAII,EAAcJ,GAAW,CAC3B,MAAMK,EAAShB,EAAMiB,IAAIN,GAEzB,GAAIK,EAAQ,CACTV,EAAeE,GAAOQ,EACvB,QACF,CAEA,MAAMI,EAAqCL,EAAcH,GACpDA,EACD,CAAA,EAEHN,EAAeE,GAAOY,EAEvBpB,EAAMkB,IAAIP,EAAoBS,GAE9BjB,EACEiB,EACAT,EACAhB,EACAK,GAEFA,EAAMmB,OAAOR,GACb,QACF,CAGCL,EAAeE,GAAOG,CApDvB,CAqDF,GAqBIF,EAAWY,GAAqC,IACjDC,OAAOC,KAAKF,MACZC,OAAOE,sBAAsBH,GAAKI,OAAQC,GAC3CJ,OAAOK,UAAUC,qBAAqBC,KAAKR,EAAKK,KCxN9CI,EAAe,IAAItC,KACvB,MAAMuC,EAAuC,CAAA,EAE7C,IAAK,MAAMC,KAAQxC,EAAKO,QAAQkC,UACzB1B,EAAcyB,IAEnB1C,EAAUyC,EAAQC,EAAM,CAACE,EAASR,IAChCS,MAAMC,QAAQV,GAAKA,OAAI7B,GAI3B,OAAOkC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phun-ky/defaults-deep",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Like lodash's _.defaultsDeep, but with array preservation",
5
5
  "type": "module",
6
6
  "main": "./dist/defaults-deep.cjs",
@@ -34,7 +34,9 @@
34
34
  "check:pack": "node scripts/check-pack.mjs",
35
35
  "clean": "rm -rf dist dts",
36
36
  "commit": "npx git-cz",
37
+ "docs:check": "typedoc --emit none --treatWarningsAsErrors",
37
38
  "docs:gen": "node ./node_modules/.bin/typedoc",
39
+ "prepare": "git config core.hooksPath .githooks || true",
38
40
  "prerollup:dev": "npm run clean",
39
41
  "pretest:ci": "rm -rf coverage && mkdir -p coverage",
40
42
  "release": "release-it",
@@ -42,11 +44,12 @@
42
44
  "rollup": "rollup -c",
43
45
  "rollup:dev": "rollup -c -w",
44
46
  "sbom": "node scripts/sbom.mjs",
45
- "style:code": "npx putout src",
46
- "style:format": "eslint -c ./eslint.config.mjs src/**/*.ts --fix --no-warn-ignored && ./node_modules/.bin/prettier --write ./eslint.config.mjs src",
47
- "style:lint": "eslint -c ./eslint.config.mjs src/**/*.ts --no-warn-ignored && ./node_modules/.bin/prettier --check src",
48
- "test": "glob-bin -c \"node --import tsx --import global-jsdom/register --test --no-warnings\" \"./src/**/__tests__/**/*.[jt]s\"",
49
- "test:ci": "glob-bin -c \"node --import tsx --import global-jsdom/register --test --no-warnings --experimental-test-coverage --test-reporter=cobertura --test-reporter-destination=coverage/cobertura-coverage.xml --test-reporter=spec --test-reporter-destination=stdout\" \"./src/**/__tests__/**/*.[jt]s\""
47
+ "style:format": "eslint . --fix && prettier --write \"**/*.{json,yml,yaml}\"",
48
+ "style:format-check": "prettier --check \"**/*.{json,yml,yaml}\"",
49
+ "style:lint": "eslint .",
50
+ "test": "node --import tsx --test --enable-source-maps \"src/**/__tests__/**/*.ts\"",
51
+ "test:ci": "node --import tsx --test --enable-source-maps --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-exclude=\"src/**/__tests__/**\" --test-coverage-lines=99 --test-coverage-functions=100 --test-coverage-branches=97 --test-reporter=cobertura --test-reporter-destination=coverage/cobertura-coverage.xml --test-reporter=spec --test-reporter-destination=stdout \"src/**/__tests__/**/*.ts\"",
52
+ "typecheck": "tsc -p tsconfig.json"
50
53
  },
51
54
  "funding": "https://github.com/phun-ky/defaults-deep?sponsor=1",
52
55
  "repository": {
@@ -131,39 +134,32 @@
131
134
  "@release-it/conventional-changelog": "^12.0.2",
132
135
  "@rollup/plugin-node-resolve": "^16.0.1",
133
136
  "@rollup/plugin-terser": "^1.0.0",
134
- "@types/node": "^25.9.2",
137
+ "@types/node": "^24.19.2",
135
138
  "cobertura": "^1.0.1",
136
139
  "eslint": "^10.0.0",
137
- "eslint-config-phun-ky": "^2.0.0",
140
+ "eslint-config-phun-ky": "^2.0.10",
141
+ "fast-check": "^4.10.2",
138
142
  "git-cz": "^4.9.0",
139
- "glob-bin": "^1.0.0",
140
- "global-jsdom": "^29.0.0",
141
- "jsdom": "^29.0.2",
143
+ "globals": "^17.13.0",
142
144
  "prettier": "^3.8.0",
143
145
  "publint": "^0.3.25",
144
- "putout": "^42.0.13",
145
- "quibble": "^0.9.1",
146
146
  "release-it": "^21.1.1",
147
147
  "remark-github": "^12.0.0",
148
148
  "remark-toc": "^9.0.0",
149
149
  "rollup": "^4.12.0",
150
150
  "rollup-plugin-dts": "^6.1.0",
151
151
  "rollup-plugin-typescript2": "^0.37.0",
152
- "tslib": "^2.3.1",
153
152
  "tsx": "^4.7.1",
154
153
  "typedoc": "^0.28.2",
155
- "typedoc-plugin-frontmatter": "^1.0.0",
156
154
  "typedoc-plugin-markdown": "^4.2.3",
157
155
  "typedoc-plugin-mdn-links": "^5.0.1",
158
- "typedoc-plugin-no-inherit": "^1.4.0",
159
156
  "typedoc-plugin-remark": "^2.0.0",
160
157
  "typedoc-plugin-rename-defaults": "^0.7.1",
161
158
  "typescript": "^6.0.2",
162
159
  "unified-prettier": "^2.0.1"
163
160
  },
164
161
  "engines": {
165
- "node": ">=22.9.0",
166
- "npm": ">=11.0.0"
162
+ "node": ">=22.9.0"
167
163
  },
168
164
  "publishConfig": {
169
165
  "access": "public"
@@ -174,6 +170,9 @@
174
170
  },
175
171
  "release-it": {
176
172
  "undici": "^7.30.1"
173
+ },
174
+ "micromark-extension-math": {
175
+ "katex": "^0.18.2"
177
176
  }
178
177
  },
179
178
  "dependencies": {
package/src/main.ts CHANGED
@@ -16,7 +16,7 @@ import { mergeWith } from './utils/merge-with';
16
16
  * values from the first argument are treated as "defaults" that should *not* be overwritten
17
17
  * by later arguments (because later arguments are merged first).
18
18
  * - Only **object-like** sources participate in merging. Non-objects are skipped via `isObjectLoose`.
19
- * - Uses {@link mergeWith} for the deep merge logic.
19
+ * - Uses an internal `mergeWith` for the deep merge logic.
20
20
  * - **Arrays are preserved by replacement**: if a source value is an array, it replaces the
21
21
  * destination value at that key (rather than being merged element-by-element).
22
22
  *
@@ -48,7 +48,7 @@ import { mergeWith } from './utils/merge-with';
48
48
  * @remarks
49
49
  *
50
50
  * - The customizer only checks the *source* value for arrays. If `s` is an array, it is returned
51
- * and assigned as-is. Otherwise returning `undefined` delegates merging to {@link mergeWith}'s
51
+ * and assigned as-is. Otherwise returning `undefined` delegates merging to `mergeWith`'s
52
52
  * default behaviour (which deep-merges plain objects and replaces other value types).
53
53
  * - The output type is `Record<PropertyKey, unknown>` to keep the API broadly usable without
54
54
  * over-promising specific shapes.
@@ -156,14 +156,15 @@ export function mergeWith(
156
156
  }
157
157
 
158
158
  const UNSAFE_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype']);
159
-
160
159
  /**
161
160
  * Internal recursive merge implementation used by {@link mergeWith}.
162
161
  *
163
162
  * @param target - Destination object that is being mutated.
164
163
  * @param source - Current source object being merged in.
165
164
  * @param customizer - Optional custom merge override.
166
- * @param stack - Tracks visited source objects to avoid infinite recursion.
165
+ * @param stack - The source objects on the current path, each mapped to its
166
+ * destination, to stop at cycles. Only the current path: a source object
167
+ * reached again through a sibling key gets its own destination.
167
168
  */
168
169
  const baseMerge = (
169
170
  target: Record<PropertyKey, unknown>,
@@ -177,6 +178,7 @@ const baseMerge = (
177
178
  if (typeof key === 'string' && UNSAFE_KEYS.has(key)) {
178
179
  continue;
179
180
  }
181
+
180
182
  const srcValue = (source as any)[key];
181
183
  const objValue = (target as any)[key];
182
184
  const customized = customizer?.(
@@ -195,7 +197,7 @@ const baseMerge = (
195
197
 
196
198
  // Both are plain objects: merge into existing destination object.
197
199
  if (isObjectPlain(objValue) && isObjectPlain(srcValue)) {
198
- // If we've seen this src object before, reuse the prior destination.
200
+ // A cycle back to a source object on the current path: point at its destination.
199
201
  const cached = stack.get(srcValue as object);
200
202
 
201
203
  if (cached) {
@@ -203,7 +205,6 @@ const baseMerge = (
203
205
  continue;
204
206
  }
205
207
 
206
- // Cache before recursing to handle cycles.
207
208
  stack.set(srcValue as object, objValue as object);
208
209
 
209
210
  baseMerge(
@@ -212,6 +213,7 @@ const baseMerge = (
212
213
  customizer,
213
214
  stack
214
215
  );
216
+ stack.delete(srcValue as object);
215
217
  continue;
216
218
  }
217
219
 
@@ -230,7 +232,6 @@ const baseMerge = (
230
232
 
231
233
  (target as any)[key] = next;
232
234
 
233
- // Cache before recursing to handle cycles and repeated refs.
234
235
  stack.set(srcValue as object, next as object);
235
236
 
236
237
  baseMerge(
@@ -239,6 +240,7 @@ const baseMerge = (
239
240
  customizer,
240
241
  stack
241
242
  );
243
+ stack.delete(srcValue as object);
242
244
  continue;
243
245
  }
244
246