@helpers4/object 2.0.3 → 2.1.0

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,37 +1,57 @@
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
- }
1
+ var UNSAFE_KEYS = new Set([
2
+ "__proto__",
3
+ "constructor",
4
+ "prototype"
5
+ ]);
8
6
  //#endregion
9
- //#region helpers/object/deepClone.ts
7
+ //#region helpers/object/cloneDeep.ts
10
8
  /**
11
9
  * This file is part of helpers4.
12
10
  * Copyright (C) 2025 baxyz
13
11
  * SPDX-License-Identifier: LGPL-3.0-or-later
14
12
  */
15
- var UNSAFE_KEYS$2 = new Set([
16
- "__proto__",
17
- "constructor",
18
- "prototype"
19
- ]);
20
13
  /**
21
- * Creates a deep copy of an object or array
22
- * @param obj - The object to clone
23
- * @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
24
36
  * @since 1.9.0
25
37
  */
26
- function deepClone(obj) {
38
+ function cloneDeep(obj) {
27
39
  if (obj === null || typeof obj !== "object") return obj;
28
40
  if (obj instanceof Date) return new Date(obj.getTime());
29
- if (Array.isArray(obj)) return obj.map((item) => deepClone(item));
41
+ if (Array.isArray(obj)) return obj.map((item) => cloneDeep(item));
30
42
  const cloned = {};
31
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS$2.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]);
32
44
  return cloned;
33
45
  }
34
46
  //#endregion
47
+ //#region helpers/object/compact.ts
48
+ function compact(obj) {
49
+ if (obj === void 0 || obj === null) return obj;
50
+ const result = {};
51
+ for (const key of Object.keys(obj)) if (obj[key]) result[key] = obj[key];
52
+ return result;
53
+ }
54
+ //#endregion
35
55
  //#region helpers/type/isPlainObject.ts
36
56
  /**
37
57
  * This file is part of helpers4.
@@ -60,32 +80,6 @@ function isPlainObject(value) {
60
80
  return proto === Object.prototype || proto === null;
61
81
  }
62
82
  //#endregion
63
- //#region helpers/object/deepMerge.ts
64
- /**
65
- * This file is part of helpers4.
66
- * Copyright (C) 2025 baxyz
67
- * SPDX-License-Identifier: LGPL-3.0-or-later
68
- */
69
- var UNSAFE_KEYS$1 = new Set([
70
- "__proto__",
71
- "constructor",
72
- "prototype"
73
- ]);
74
- function deepMerge(target, ...sources) {
75
- if (target === void 0 || target === null) return target;
76
- if (!sources.length) return target;
77
- const source = sources.shift();
78
- if (!source) return deepMerge(target, ...sources);
79
- for (const key in source) {
80
- if (!Object.hasOwn(source, key) || UNSAFE_KEYS$1.has(key)) continue;
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
- return deepMerge(target, ...sources);
87
- }
88
- //#endregion
89
83
  //#region helpers/array/equalsDeep.ts
90
84
  /**
91
85
  * This file is part of helpers4.
@@ -443,22 +437,71 @@ function equalsShallow(objA, objB) {
443
437
  return true;
444
438
  }
445
439
  //#endregion
446
- //#region helpers/object/get.ts
440
+ //#region helpers/object/_parsePath.ts
447
441
  /**
448
442
  * This file is part of helpers4.
449
443
  * Copyright (C) 2025 baxyz
450
444
  * SPDX-License-Identifier: LGPL-3.0-or-later
451
445
  */
452
446
  /**
453
- * Gets a value from an object using a dot-notated path
454
- * @param obj - The object to get value from
455
- * @param path - The dot-notated path (e.g., 'a.b.c')
456
- * @param defaultValue - Default value if path doesn't exist
457
- * @returns The value at the path or default value
458
- * @since 1.9.0
447
+ * Parses a string path into an array of string/number segments.
448
+ *
449
+ * - Dot separators (`.`) split segments; each segment becomes a string key.
450
+ * - Bracket indices (`[n]`) become number keys.
451
+ * - A leading `.` is treated as "current level" and stripped before parsing,
452
+ * so `.[0]` ≡ `[0]` and `.a.b` ≡ `a.b`.
453
+ * - Empty string (or bare `.`) returns `['']` (addresses the `''` key on the root object).
454
+ * - Consecutive dots (`a..b`) produce an empty-string segment: `['a', '', 'b']`.
455
+ * - Non-bracket text trailing a `]` within the same dot-segment throws a RangeError;
456
+ * use a dot separator instead (`'a[0].b'` not `'a[0]b'`).
457
+ *
458
+ * @ignore
459
+ * @example
460
+ * parsePath('a.b.c') // => ['a', 'b', 'c'] (all strings)
461
+ * parsePath('layers.1.x') // => ['layers', '1', 'x'] (all strings)
462
+ * parsePath('layers[1].x') // => ['layers', 1, 'x'] (bracket → number)
463
+ * parsePath('.[0]') // => [0] (leading dot stripped)
464
+ * parsePath('a..b') // => ['a', '', 'b'] (empty segment preserved)
465
+ * parsePath('.') // => [''] (leading dot → empty-string key)
466
+ */
467
+ var BRACKET = /\[(\d+)\]/g;
468
+ var parsePathCache = /* @__PURE__ */ new Map();
469
+ function parsePath(path) {
470
+ const cached = parsePathCache.get(path);
471
+ if (cached !== void 0) return cached;
472
+ const result = Object.freeze(parsePathUncached(path));
473
+ parsePathCache.set(path, result);
474
+ return result;
475
+ }
476
+ function parsePathUncached(path) {
477
+ if (path === "") return [""];
478
+ if (path[0] === ".") path = path.slice(1);
479
+ if (path === "") return [""];
480
+ const result = [];
481
+ for (const dotSegment of path.split(".")) {
482
+ BRACKET.lastIndex = 0;
483
+ let last = 0;
484
+ let match;
485
+ while ((match = BRACKET.exec(dotSegment)) !== null) {
486
+ if (match.index > last) result.push(dotSegment.slice(last, match.index));
487
+ result.push(parseInt(match[1], 10));
488
+ last = BRACKET.lastIndex;
489
+ }
490
+ if (last === 0) result.push(dotSegment);
491
+ else if (last < dotSegment.length) throw new RangeError(`parsePath: non-bracket text '${dotSegment.slice(last)}' after ']' in segment '${dotSegment}' — use 'a[0].b' instead of 'a[0]b'`);
492
+ }
493
+ return result;
494
+ }
495
+ //#endregion
496
+ //#region helpers/object/get.ts
497
+ /**
498
+ * This file is part of helpers4.
499
+ * Copyright (C) 2025 baxyz
500
+ * SPDX-License-Identifier: LGPL-3.0-or-later
459
501
  */
460
502
  function get(obj, path, defaultValue) {
461
- const keys = path.split(".");
503
+ if (obj == null) return defaultValue;
504
+ const keys = typeof path === "string" ? parsePath(path) : path;
462
505
  let result = obj;
463
506
  for (const key of keys) {
464
507
  if (result == null || typeof result !== "object") return defaultValue;
@@ -478,6 +521,9 @@ function get(obj, path, defaultValue) {
478
521
  *
479
522
  * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
480
523
  * older targets and provides stricter return-type inference.
524
+ * `null` and `undefined` are treated as empty arrays and return `{}`.
525
+ * Items whose computed key is a prototype-polluting string (`__proto__`,
526
+ * `constructor`, `prototype`) are silently skipped.
481
527
  *
482
528
  * @param items - The array to group
483
529
  * @param keyFn - Function that returns the group key for each item
@@ -485,12 +531,16 @@ function get(obj, path, defaultValue) {
485
531
  * @example
486
532
  * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')
487
533
  * // => { odd: [1, 3], even: [2, 4] }
534
+ * groupBy(null, n => n % 2 === 0 ? 'even' : 'odd')
535
+ * // => {}
488
536
  * @since 2.0.0
489
537
  */
490
538
  function groupBy(items, keyFn) {
491
539
  const result = {};
540
+ if (items == null) return result;
492
541
  for (const item of items) {
493
542
  const key = keyFn(item);
543
+ if (typeof key === "string" && UNSAFE_KEYS.has(key)) continue;
494
544
  if (result[key] === void 0) result[key] = [];
495
545
  result[key].push(item);
496
546
  }
@@ -506,6 +556,9 @@ function groupBy(items, keyFn) {
506
556
  /**
507
557
  * Returns a new object with keys and values swapped.
508
558
  * If multiple keys share the same value, the last one wins.
559
+ * `null` and `undefined` are treated as empty objects and return `{}`.
560
+ * Entries whose source key **or** value is a prototype-polluting string
561
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
509
562
  *
510
563
  * @param obj - The object whose keys and values are to be swapped
511
564
  * @returns A new object with values as keys and original keys as values
@@ -515,11 +568,19 @@ function groupBy(items, keyFn) {
515
568
  *
516
569
  * invert({ one: 1, two: 2 })
517
570
  * // => { 1: 'one', 2: 'two' }
571
+ *
572
+ * invert(null)
573
+ * // => {}
518
574
  * @since 2.0.0
519
575
  */
520
576
  function invert(obj) {
521
577
  const result = {};
522
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) result[obj[key]] = key;
578
+ if (obj == null) return result;
579
+ for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS.has(key)) {
580
+ const val = obj[key];
581
+ if (typeof val === "string" && UNSAFE_KEYS.has(val)) continue;
582
+ result[val] = key;
583
+ }
523
584
  return result;
524
585
  }
525
586
  //#endregion
@@ -531,20 +592,23 @@ function invert(obj) {
531
592
  */
532
593
  /**
533
594
  * Checks if a plain object has no own enumerable string-keyed properties.
595
+ * `null` and `undefined` are treated as empty objects and return `true`.
534
596
  *
535
597
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
536
598
  * separately if symbol keys matter for your use case.
537
599
  *
538
600
  * @param value - The object to check
539
- * @returns `true` if the object has no own enumerable string-keyed properties
601
+ * @returns `true` if the object has no own enumerable string-keyed properties, or if `value` is `null`/`undefined`
540
602
  * @example
541
603
  * isEmpty({}) // => true
604
+ * isEmpty(null) // => true
605
+ * isEmpty(undefined) // => true
542
606
  * isEmpty({ a: 1 }) // => false
543
607
  * isEmpty({ a: undefined }) // => false (key exists)
544
608
  * @since 2.0.3
545
609
  */
546
610
  function isEmpty(value) {
547
- return Object.keys(value).length === 0;
611
+ return value == null || Object.keys(value).length === 0;
548
612
  }
549
613
  //#endregion
550
614
  //#region helpers/object/isNonEmpty.ts
@@ -555,19 +619,23 @@ function isEmpty(value) {
555
619
  */
556
620
  /**
557
621
  * Checks if a plain object has at least one own enumerable string-keyed property.
622
+ * `null` and `undefined` are treated as empty objects and return `false`.
558
623
  *
559
624
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
560
625
  * separately if symbol keys matter for your use case.
561
626
  *
562
627
  * @param value - The object to check
563
- * @returns `true` if the object has at least one own enumerable string-keyed property
628
+ * @returns `true` if the object has at least one own enumerable string-keyed property; `false` for empty, `null`, or `undefined`.
629
+ * Acts as a type guard: the `if` branch narrows `Record<PropertyKey, unknown> | null | undefined` to `Record<PropertyKey, unknown>`.
564
630
  * @example
565
631
  * isNonEmpty({ a: 1 }) // => true
566
632
  * isNonEmpty({}) // => false
633
+ * isNonEmpty(null) // => false
634
+ * isNonEmpty(undefined) // => false
567
635
  * @since 2.0.3
568
636
  */
569
637
  function isNonEmpty(value) {
570
- return Object.keys(value).length > 0;
638
+ return value != null && Object.keys(value).length > 0;
571
639
  }
572
640
  //#endregion
573
641
  //#region helpers/object/map.ts
@@ -584,7 +652,8 @@ function isNonEmpty(value) {
584
652
  * when `mapKey` is omitted the original keys are preserved.
585
653
  *
586
654
  * Note: if two different keys map to the same output key the last one wins
587
- * (insertion order).
655
+ * (insertion order). Entries whose mapped key is a prototype-polluting string
656
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
588
657
  *
589
658
  * @param obj - The source object
590
659
  * @param mapValue - Callback called with `(value, key)` for each entry.
@@ -605,14 +674,44 @@ function isNonEmpty(value) {
605
674
  */
606
675
  function map(obj, mapValue, mapKey) {
607
676
  const result = {};
677
+ if (obj == null) return result;
608
678
  for (const key of Object.keys(obj)) {
609
679
  const value = obj[key];
610
680
  const newKey = mapKey ? mapKey(key, value) : key;
681
+ if (typeof newKey === "string" && UNSAFE_KEYS.has(newKey)) continue;
611
682
  result[newKey] = mapValue ? mapValue(value, key) : value;
612
683
  }
613
684
  return result;
614
685
  }
615
686
  //#endregion
687
+ //#region helpers/object/mergeDeep.ts
688
+ /**
689
+ * This file is part of helpers4.
690
+ * Copyright (C) 2025 baxyz
691
+ * SPDX-License-Identifier: LGPL-3.0-or-later
692
+ */
693
+ function applyKey(result, source, key) {
694
+ const resultValue = result[key];
695
+ const sourceValue = source[key];
696
+ if (isPlainObject(resultValue) && isPlainObject(sourceValue)) result[key] = mergeDeep(resultValue, sourceValue);
697
+ else if (sourceValue !== void 0) result[key] = isPlainObject(sourceValue) || Array.isArray(sourceValue) ? cloneDeep(sourceValue) : sourceValue;
698
+ }
699
+ function applySource(result, source) {
700
+ for (const key in source) {
701
+ if (!Object.hasOwn(source, key) || UNSAFE_KEYS.has(key)) continue;
702
+ applyKey(result, source, key);
703
+ }
704
+ for (const sym of Object.getOwnPropertySymbols(source)) {
705
+ if (!Object.prototype.propertyIsEnumerable.call(source, sym)) continue;
706
+ applyKey(result, source, sym);
707
+ }
708
+ }
709
+ function mergeDeep(...sources) {
710
+ const result = {};
711
+ for (const source of sources) if (source !== null && source !== void 0) applySource(result, source);
712
+ return result;
713
+ }
714
+ //#endregion
616
715
  //#region helpers/object/omit.ts
617
716
  function omit(obj, keys) {
618
717
  if (obj === void 0 || obj === null) return obj;
@@ -688,32 +787,20 @@ function safeJsonParse(input, fallback) {
688
787
  * Copyright (C) 2025 baxyz
689
788
  * SPDX-License-Identifier: LGPL-3.0-or-later
690
789
  */
691
- var UNSAFE_KEYS = new Set([
692
- "__proto__",
693
- "constructor",
694
- "prototype"
695
- ]);
696
- /**
697
- * Sets a value in an object using a dot-notated path
698
- * @param obj - The object to set value in
699
- * @param path - The dot-notated path (e.g., 'a.b.c')
700
- * @param value - The value to set
701
- * @returns The modified object
702
- * @since 1.9.0
703
- */
704
790
  function set(obj, path, value) {
705
- const keys = path.split(".");
706
- if (keys.some((k) => UNSAFE_KEYS.has(k))) return obj;
791
+ const keys = typeof path === "string" ? parsePath(path) : path;
792
+ if (keys.length === 0) return obj;
793
+ if (keys.some((k) => typeof k === "string" && UNSAFE_KEYS.has(k))) return obj;
707
794
  let current = obj;
708
795
  for (let i = 0; i < keys.length - 1; i++) {
709
796
  const key = keys[i];
710
- if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
797
+ if (!Object.hasOwn(current, key) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
711
798
  current = current[key];
712
799
  }
713
800
  current[keys[keys.length - 1]] = value;
714
801
  return obj;
715
802
  }
716
803
  //#endregion
717
- export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
804
+ export { cloneDeep, cloneDeep as deepClone, compact, mergeDeep as deepMerge, mergeDeep, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
718
805
 
719
806
  //# sourceMappingURL=index.js.map