@helpers4/object 2.0.4 → 3.0.0-alpha.1

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/lib/index.js CHANGED
@@ -1,49 +1,66 @@
1
- //#region helpers/object/compact.ts
2
- function compact(obj) {
3
- if (obj === void 0 || obj === null) return obj;
4
- const result = {};
5
- for (const key of Object.keys(obj)) if (obj[key]) result[key] = obj[key];
6
- return result;
7
- }
8
- //#endregion
9
- //#region helpers/object/_unsafeKeys.ts
10
- /**
11
- * This file is part of helpers4.
12
- * Copyright (C) 2025 baxyz
13
- * SPDX-License-Identifier: LGPL-3.0-or-later
14
- */
15
- /**
16
- * Keys that must never be set on a plain object (prototype-pollution guard).
17
- * @internal
18
- */
19
- var UNSAFE_KEYS = new Set([
1
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set([
20
2
  "__proto__",
21
3
  "constructor",
22
4
  "prototype"
23
5
  ]);
24
6
  //#endregion
25
- //#region helpers/object/deepClone.ts
7
+ //#region helpers/object/cloneDeep.ts
26
8
  /**
27
9
  * This file is part of helpers4.
28
10
  * Copyright (C) 2025 baxyz
29
11
  * SPDX-License-Identifier: LGPL-3.0-or-later
30
12
  */
31
13
  /**
32
- * Creates a deep copy of an object or array
33
- * @param obj - The object to clone
34
- * @returns Deep cloned object
14
+ * Creates a deep copy of an object or array.
15
+ *
16
+ * Recursively clones own enumerable string keys. `Date` instances are
17
+ * reconstructed with the same epoch value. Prototype-polluting keys
18
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
19
+ * Does **not** handle circular references.
20
+ *
21
+ * **Limitation:** symbol-keyed own properties are **not** copied — only string
22
+ * keys are processed. Use `structuredClone` if symbol propagation is required.
23
+ *
24
+ * @param obj - The value to clone
25
+ * @returns A deep copy of `obj`
26
+ * @example
27
+ * cloneDeep({ a: { b: [1, 2] } })
28
+ * // => { a: { b: [1, 2] } } — fully independent copy
29
+ *
30
+ * cloneDeep(new Date('2024-01-01'))
31
+ * // => Date with the same timestamp
32
+ *
33
+ * // Primitives pass through unchanged
34
+ * cloneDeep(42) // => 42
35
+ * cloneDeep(null) // => null
35
36
  * @since 1.9.0
36
37
  */
37
- function deepClone(obj) {
38
+ function cloneDeep(obj) {
38
39
  if (obj === null || typeof obj !== "object") return obj;
39
40
  if (obj instanceof Date) return new Date(obj.getTime());
40
- if (Array.isArray(obj)) return obj.map((item) => deepClone(item));
41
+ if (Array.isArray(obj)) return obj.map((item) => cloneDeep(item));
41
42
  const cloned = {};
42
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS.has(key)) cloned[key] = deepClone(obj[key]);
43
+ for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS.has(key)) cloned[key] = cloneDeep(obj[key]);
43
44
  return cloned;
44
45
  }
45
46
  //#endregion
46
- //#region helpers/type/isPlainObject.ts
47
+ //#region helpers/object/compact.ts
48
+ /**
49
+ * This file is part of helpers4.
50
+ * Copyright (C) 2025 baxyz
51
+ * SPDX-License-Identifier: LGPL-3.0-or-later
52
+ */
53
+ function compact(obj) {
54
+ if (obj === void 0 || obj === null) return obj;
55
+ const result = {};
56
+ for (const key of Object.keys(obj)) {
57
+ if (UNSAFE_KEYS.has(key)) continue;
58
+ if (obj[key]) result[key] = obj[key];
59
+ }
60
+ return result;
61
+ }
62
+ //#endregion
63
+ //#region helpers/guard/isPlainObject.ts
47
64
  /**
48
65
  * This file is part of helpers4.
49
66
  * Copyright (C) 2025 baxyz
@@ -71,36 +88,6 @@ function isPlainObject(value) {
71
88
  return proto === Object.prototype || proto === null;
72
89
  }
73
90
  //#endregion
74
- //#region helpers/object/deepMerge.ts
75
- /**
76
- * This file is part of helpers4.
77
- * Copyright (C) 2025 baxyz
78
- * SPDX-License-Identifier: LGPL-3.0-or-later
79
- */
80
- function mergeKey(target, source, key) {
81
- const targetValue = target[key];
82
- const sourceValue = source[key];
83
- if (isPlainObject(targetValue) && isPlainObject(sourceValue)) target[key] = deepMerge(targetValue, sourceValue);
84
- else if (sourceValue !== void 0) target[key] = sourceValue;
85
- }
86
- function deepMerge(target, ...sources) {
87
- if (target === void 0 || target === null) return target;
88
- if (!sources.length) return target;
89
- const source = sources.shift();
90
- if (!source) return deepMerge(target, ...sources);
91
- const t = target;
92
- const s = source;
93
- for (const key in source) {
94
- if (!Object.hasOwn(source, key) || UNSAFE_KEYS.has(key)) continue;
95
- mergeKey(t, s, key);
96
- }
97
- for (const sym of Object.getOwnPropertySymbols(source)) {
98
- if (!Object.prototype.propertyIsEnumerable.call(source, sym)) continue;
99
- mergeKey(t, s, sym);
100
- }
101
- return deepMerge(target, ...sources);
102
- }
103
- //#endregion
104
91
  //#region helpers/array/equalsDeep.ts
105
92
  /**
106
93
  * This file is part of helpers4.
@@ -265,7 +252,7 @@ function compare(dateA, dateB, options = {}) {
265
252
  }
266
253
  }
267
254
  //#endregion
268
- //#region helpers/type/isSpecialObject.ts
255
+ //#region helpers/guard/isSpecialObject.ts
269
256
  /**
270
257
  * This file is part of helpers4.
271
258
  * Copyright (C) 2025 baxyz
@@ -345,7 +332,7 @@ function diff(objA, objB, options = {}) {
345
332
  return diffPlainObjects(objA, objB, depth);
346
333
  }
347
334
  function diffPlainObjects(objA, objB, depth) {
348
- const keys = Array.from(new Set([...Object.keys(objA), ...Object.keys(objB)]));
335
+ const keys = Array.from(/* @__PURE__ */ new Set([...Object.keys(objA), ...Object.keys(objB)]));
349
336
  const differences = Object.create(null);
350
337
  for (const key of keys) {
351
338
  const hasA = Object.prototype.hasOwnProperty.call(objA, key);
@@ -476,7 +463,7 @@ function equalsShallow(objA, objB) {
476
463
  * - Non-bracket text trailing a `]` within the same dot-segment throws a RangeError;
477
464
  * use a dot separator instead (`'a[0].b'` not `'a[0]b'`).
478
465
  *
479
- * @internal
466
+ * @ignore
480
467
  * @example
481
468
  * parsePath('a.b.c') // => ['a', 'b', 'c'] (all strings)
482
469
  * parsePath('layers.1.x') // => ['layers', '1', 'x'] (all strings)
@@ -521,6 +508,7 @@ function parsePathUncached(path) {
521
508
  * SPDX-License-Identifier: LGPL-3.0-or-later
522
509
  */
523
510
  function get(obj, path, defaultValue) {
511
+ if (obj == null) return defaultValue;
524
512
  const keys = typeof path === "string" ? parsePath(path) : path;
525
513
  let result = obj;
526
514
  for (const key of keys) {
@@ -541,6 +529,9 @@ function get(obj, path, defaultValue) {
541
529
  *
542
530
  * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
543
531
  * older targets and provides stricter return-type inference.
532
+ * `null` and `undefined` are treated as empty arrays and return `{}`.
533
+ * Items whose computed key is a prototype-polluting string (`__proto__`,
534
+ * `constructor`, `prototype`) are silently skipped.
544
535
  *
545
536
  * @param items - The array to group
546
537
  * @param keyFn - Function that returns the group key for each item
@@ -548,12 +539,16 @@ function get(obj, path, defaultValue) {
548
539
  * @example
549
540
  * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')
550
541
  * // => { odd: [1, 3], even: [2, 4] }
542
+ * groupBy(null, n => n % 2 === 0 ? 'even' : 'odd')
543
+ * // => {}
551
544
  * @since 2.0.0
552
545
  */
553
546
  function groupBy(items, keyFn) {
554
547
  const result = {};
548
+ if (items == null) return result;
555
549
  for (const item of items) {
556
550
  const key = keyFn(item);
551
+ if (typeof key === "string" && UNSAFE_KEYS.has(key)) continue;
557
552
  if (result[key] === void 0) result[key] = [];
558
553
  result[key].push(item);
559
554
  }
@@ -569,6 +564,9 @@ function groupBy(items, keyFn) {
569
564
  /**
570
565
  * Returns a new object with keys and values swapped.
571
566
  * If multiple keys share the same value, the last one wins.
567
+ * `null` and `undefined` are treated as empty objects and return `{}`.
568
+ * Entries whose source key **or** value is a prototype-polluting string
569
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
572
570
  *
573
571
  * @param obj - The object whose keys and values are to be swapped
574
572
  * @returns A new object with values as keys and original keys as values
@@ -578,11 +576,19 @@ function groupBy(items, keyFn) {
578
576
  *
579
577
  * invert({ one: 1, two: 2 })
580
578
  * // => { 1: 'one', 2: 'two' }
579
+ *
580
+ * invert(null)
581
+ * // => {}
581
582
  * @since 2.0.0
582
583
  */
583
584
  function invert(obj) {
584
585
  const result = {};
585
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) result[obj[key]] = key;
586
+ if (obj == null) return result;
587
+ for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS.has(key)) {
588
+ const val = obj[key];
589
+ if (typeof val === "string" && UNSAFE_KEYS.has(val)) continue;
590
+ result[val] = key;
591
+ }
586
592
  return result;
587
593
  }
588
594
  //#endregion
@@ -594,20 +600,23 @@ function invert(obj) {
594
600
  */
595
601
  /**
596
602
  * Checks if a plain object has no own enumerable string-keyed properties.
603
+ * `null` and `undefined` are treated as empty objects and return `true`.
597
604
  *
598
605
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
599
606
  * separately if symbol keys matter for your use case.
600
607
  *
601
608
  * @param value - The object to check
602
- * @returns `true` if the object has no own enumerable string-keyed properties
609
+ * @returns `true` if the object has no own enumerable string-keyed properties, or if `value` is `null`/`undefined`
603
610
  * @example
604
611
  * isEmpty({}) // => true
612
+ * isEmpty(null) // => true
613
+ * isEmpty(undefined) // => true
605
614
  * isEmpty({ a: 1 }) // => false
606
615
  * isEmpty({ a: undefined }) // => false (key exists)
607
616
  * @since 2.0.3
608
617
  */
609
618
  function isEmpty(value) {
610
- return Object.keys(value).length === 0;
619
+ return value == null || Object.keys(value).length === 0;
611
620
  }
612
621
  //#endregion
613
622
  //#region helpers/object/isNonEmpty.ts
@@ -618,19 +627,23 @@ function isEmpty(value) {
618
627
  */
619
628
  /**
620
629
  * Checks if a plain object has at least one own enumerable string-keyed property.
630
+ * `null` and `undefined` are treated as empty objects and return `false`.
621
631
  *
622
632
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
623
633
  * separately if symbol keys matter for your use case.
624
634
  *
625
635
  * @param value - The object to check
626
- * @returns `true` if the object has at least one own enumerable string-keyed property
636
+ * @returns `true` if the object has at least one own enumerable string-keyed property; `false` for empty, `null`, or `undefined`.
637
+ * Acts as a type guard: the `if` branch narrows `Record<PropertyKey, unknown> | null | undefined` to `Record<PropertyKey, unknown>`.
627
638
  * @example
628
639
  * isNonEmpty({ a: 1 }) // => true
629
640
  * isNonEmpty({}) // => false
641
+ * isNonEmpty(null) // => false
642
+ * isNonEmpty(undefined) // => false
630
643
  * @since 2.0.3
631
644
  */
632
645
  function isNonEmpty(value) {
633
- return Object.keys(value).length > 0;
646
+ return value != null && Object.keys(value).length > 0;
634
647
  }
635
648
  //#endregion
636
649
  //#region helpers/object/map.ts
@@ -647,7 +660,8 @@ function isNonEmpty(value) {
647
660
  * when `mapKey` is omitted the original keys are preserved.
648
661
  *
649
662
  * Note: if two different keys map to the same output key the last one wins
650
- * (insertion order).
663
+ * (insertion order). Entries whose mapped key is a prototype-polluting string
664
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
651
665
  *
652
666
  * @param obj - The source object
653
667
  * @param mapValue - Callback called with `(value, key)` for each entry.
@@ -668,14 +682,44 @@ function isNonEmpty(value) {
668
682
  */
669
683
  function map(obj, mapValue, mapKey) {
670
684
  const result = {};
685
+ if (obj == null) return result;
671
686
  for (const key of Object.keys(obj)) {
672
687
  const value = obj[key];
673
688
  const newKey = mapKey ? mapKey(key, value) : key;
689
+ if (typeof newKey === "string" && UNSAFE_KEYS.has(newKey)) continue;
674
690
  result[newKey] = mapValue ? mapValue(value, key) : value;
675
691
  }
676
692
  return result;
677
693
  }
678
694
  //#endregion
695
+ //#region helpers/object/mergeDeep.ts
696
+ /**
697
+ * This file is part of helpers4.
698
+ * Copyright (C) 2025 baxyz
699
+ * SPDX-License-Identifier: LGPL-3.0-or-later
700
+ */
701
+ function applyKey(result, source, key) {
702
+ const resultValue = result[key];
703
+ const sourceValue = source[key];
704
+ if (isPlainObject(resultValue) && isPlainObject(sourceValue)) result[key] = mergeDeep(resultValue, sourceValue);
705
+ else if (sourceValue !== void 0) result[key] = isPlainObject(sourceValue) || Array.isArray(sourceValue) ? cloneDeep(sourceValue) : sourceValue;
706
+ }
707
+ function applySource(result, source) {
708
+ for (const key in source) {
709
+ if (!Object.hasOwn(source, key) || UNSAFE_KEYS.has(key)) continue;
710
+ applyKey(result, source, key);
711
+ }
712
+ for (const sym of Object.getOwnPropertySymbols(source)) {
713
+ if (!Object.prototype.propertyIsEnumerable.call(source, sym)) continue;
714
+ applyKey(result, source, sym);
715
+ }
716
+ }
717
+ function mergeDeep(...sources) {
718
+ const result = {};
719
+ for (const source of sources) if (source !== null && source !== void 0) applySource(result, source);
720
+ return result;
721
+ }
722
+ //#endregion
679
723
  //#region helpers/object/omit.ts
680
724
  function omit(obj, keys) {
681
725
  if (obj === void 0 || obj === null) return obj;
@@ -685,14 +729,22 @@ function omit(obj, keys) {
685
729
  }
686
730
  //#endregion
687
731
  //#region helpers/object/pick.ts
732
+ /**
733
+ * This file is part of helpers4.
734
+ * Copyright (C) 2025 baxyz
735
+ * SPDX-License-Identifier: LGPL-3.0-or-later
736
+ */
688
737
  function pick(obj, keys) {
689
738
  if (obj === void 0 || obj === null) return obj;
690
739
  const result = {};
691
- for (const key of keys) if (Object.prototype.hasOwnProperty.call(obj, key)) result[key] = obj[key];
740
+ for (const key of keys) {
741
+ if (typeof key === "string" && UNSAFE_KEYS.has(key)) continue;
742
+ if (Object.prototype.hasOwnProperty.call(obj, key)) result[key] = obj[key];
743
+ }
692
744
  return result;
693
745
  }
694
746
  //#endregion
695
- //#region helpers/type/isNullish.ts
747
+ //#region helpers/guard/isNullish.ts
696
748
  /**
697
749
  * This file is part of helpers4.
698
750
  * Copyright (C) 2025 baxyz
@@ -765,6 +817,6 @@ function set(obj, path, value) {
765
817
  return obj;
766
818
  }
767
819
  //#endregion
768
- export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
820
+ export { cloneDeep, compact, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, mergeDeep, omit, pick, removeUndefinedNull, safeJsonParse, set };
769
821
 
770
822
  //# sourceMappingURL=index.js.map