@ix-xs/node-comfort 1.2.0 → 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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Obj.js CHANGED
@@ -1,421 +1,1119 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Object helpers: deep clone, merge, equality, diff, typed dot paths,
5
+ * pick and omit. Inputs are never mutated, and `__proto__`, `constructor`
6
+ * and `prototype` keys are always ignored, so user input can't pollute
7
+ * prototypes.
8
+ *
9
+ * @example
10
+ * obj.get(config, "db.pool.max", 10); // typed, paths autocompleted
11
+ * obj.set(state, "user.profile.name", "Ada"); // returns an updated copy
12
+ * obj.merge(defaults, userConfig);
13
+ */
14
+
15
+ const { types } = require("node:util");
16
+ const shared = require("./internal/shared.js");
17
+
18
+ /**
19
+ * Values that end a path: dates, regexes, maps, sets, functions and arrays.
20
+ * @typedef {Date | RegExp | Map<any, any> | Set<any> | ((...args: any[]) => any) | readonly any[]} PathLeaf
21
+ */
22
+
23
+ /**
24
+ * Every dotted path of an object type, up to 6 levels. This is what powers
25
+ * path completion in `obj.get()` and friends.
26
+ *
27
+ * @example
28
+ * // Paths<{ a: { b: number }, c: string }> is "a" | "a.b" | "c"
29
+ *
30
+ * @template T
31
+ * @template {unknown[]} [Depth=[]]
32
+ * @typedef {Depth["length"] extends 6 ? never : T extends PathLeaf ? never : T extends object ? { [K in keyof T & (string | number)]: NonNullable<T[K]> extends PathLeaf ? `${K}` : NonNullable<T[K]> extends object ? `${K}` | `${K}.${Paths<NonNullable<T[K]>, [...Depth, 0]>}` : `${K}` }[keyof T & (string | number)] : never} Paths
33
+ */
34
+
35
+ /**
36
+ * @template T
37
+ * @typedef {[Extract<T, null | undefined>] extends [never] ? never : undefined} NullishToUndefined
38
+ */
39
+
40
+ /**
41
+ * The type found at a dotted path of `T`, or `any` for an unknown path.
42
+ *
43
+ * @template T
44
+ * @template {string} P
45
+ * @typedef {P extends `${infer Head}.${infer Rest}` ? PathStep<T, Head> extends infer Next ? ([Next] extends [typeof _NOT_FOUND] ? any : PathValue<Next, Rest> | NullishToUndefined<T>) : any : PathStep<T, P> extends infer Next ? ([Next] extends [typeof _NOT_FOUND] ? any : Next | NullishToUndefined<T>) : any} PathValue
46
+ */
47
+
48
+ /**
49
+ * @template T
50
+ * @template {string} K
51
+ * @typedef {K extends keyof NonNullable<T> ? NonNullable<T>[K] : NonNullable<T> extends readonly (infer E)[] ? (K extends `${number}` ? E | undefined : typeof _NOT_FOUND) : typeof _NOT_FOUND} PathStep
52
+ */
53
+
1
54
  /**
2
- * Object utilities.
55
+ * A path: a known dotted path (completed by your editor), any other string
56
+ * like `"items[0].name"`, or an array of keys.
3
57
  *
4
- * Deep clone / merge / equality, safe nested get/set via dot-paths,
5
- * pick/omit, mapping over keys and values, and flatten/unflatten • the
6
- * everyday object plumbing, done without mutating your inputs.
58
+ * @template T
59
+ * @typedef {Paths<T> | (string & {}) | ReadonlyArray<string | number>} PathInput
60
+ */
61
+
62
+ /**
63
+ * `T`, read-only all the way down.
64
+ * @template T
65
+ * @typedef {T extends (...args: any[]) => any ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T} DeepReadonly
66
+ */
67
+
68
+ /**
69
+ * One difference reported by `diff()`.
70
+ * @typedef {object} Change
71
+ * @property {Array<string | number>} path Where it changed, like `["address", "city"]`.
72
+ * @property {"added" | "removed" | "changed"} type
73
+ * @property {unknown} [from] The old value.
74
+ * @property {unknown} [to] The new value.
75
+ */
76
+
77
+ /**
78
+ * Options for `mergeWith()`.
79
+ * @typedef {object} MergeOptions
80
+ * @property {"replace" | "concat" | "unique"} [arrays] When both sides have an array: keep the last, join them, or join without duplicates. Defaults to `"replace"`.
81
+ * @property {boolean} [skipUndefined] Don't let `undefined` overwrite an existing value.
82
+ */
83
+
84
+ /**
85
+ * Options for `sortKeys()`.
86
+ * @typedef {object} SortKeysOptions
87
+ * @property {boolean} [deep] Sort nested objects too, arrays included. Defaults to `true`. With `false`, only the first level is copied and sorted: what it holds is shared with the original.
88
+ * @property {(a: string, b: string) => number} [compare] How to order two keys. Defaults to code-unit order, which is stable across machines.
89
+ */
90
+
91
+ /**
92
+ * Options for `compact()`.
93
+ * @typedef {object} CompactOptions
94
+ * @property {boolean} [deep] Clean nested objects and arrays too.
95
+ * @property {boolean} [removeEmpty] Also remove `""`, `[]` and `{}`.
7
96
  */
8
97
 
9
- const checker = require("./Checker");
98
+ const _UNSAFE = shared.UNSAFE_KEYS;
99
+
100
+ /** Marks an unknown path segment in `PathStep`. */
101
+ const _NOT_FOUND = Symbol("not-found");
102
+
103
+ const _isPlain = shared.isPlainObject;
104
+
105
+ /** @param {ReadonlyArray<string | number>} keys */
106
+ const _isSafePath = (keys) => {
107
+ for (let i = 0; i < keys.length; i++) if (_UNSAFE.has(String(keys[i]))) return false;
108
+ return true;
109
+ };
110
+
111
+ /** @type {Map<string, { keys: Array<string | number>, safe: boolean }>} */
112
+ const _paths = new Map();
10
113
 
11
114
  /**
12
- * Parses a dot/bracket path into an array of keys.
13
- * @private
14
- * @param {string|Array<string|number>} path
15
- * @returns {Array<string|number>}
115
+ * Parses `"a.b[0].c"` or `["a", "b", 0, "c"]` into keys, and says whether
116
+ * the path stays clear of prototypes. Parsed strings are remembered: paths
117
+ * are almost always literals.
118
+ * @param {unknown} path
119
+ * @returns {{ keys: ReadonlyArray<string | number>, safe: boolean }}
16
120
  */
17
- const _toPath = (path) => {
18
- if (Array.isArray(path)) return path;
19
- return String(path)
20
- .replace(/\[(\d+)\]/g, ".$1")
21
- .split(".")
22
- .filter((p) => p !== "");
121
+ const _parsePath = (path) => {
122
+ if (Array.isArray(path)) return { keys: path, safe: _isSafePath(path) };
123
+ const text = String(path);
124
+ let parsed = _paths.get(text);
125
+ if (parsed === undefined) {
126
+ const dotted = text.includes("[") ? text.replace(/\[(\d+)\]/g, ".$1").replace(/\[["']?([^\]"']+)["']?\]/g, ".$1") : text;
127
+ const keys = dotted.includes(".") ? dotted.split(".").filter((part) => part !== "") : dotted === "" ? [] : [dotted];
128
+ parsed = { keys, safe: _isSafePath(keys) };
129
+ if (_paths.size >= 1000) _paths.clear();
130
+ _paths.set(text, parsed);
131
+ }
132
+ return parsed;
23
133
  };
24
134
 
25
135
  /**
26
- * Manual deep-clone used when `structuredClone` is unavailable or throws
27
- * (e.g. objects containing functions). Handles arrays, Maps, Sets, Dates and
28
- * RegExps, and guards against circular references.
29
- * @private
30
- * @param {*} value - Value to clone.
31
- * @param {WeakMap} seen - Cache of already-cloned references.
32
- * @returns {*} The cloned value.
136
+ * Deep copy that keeps what `structuredClone` loses: functions and class
137
+ * prototypes. Shared references stay shared and cycles are preserved.
138
+ * @param {any} value
139
+ * @param {Map<object, any>} seen
140
+ * @returns {any}
33
141
  */
34
142
  const _deepClone = (value, seen) => {
35
143
  if (value === null || typeof value !== "object") return value;
36
- if (seen.has(value)) return seen.get(value);
37
- if (checker.isDate(value)) return new Date(value.getTime());
38
- if (checker.isRegExp(value)) return new RegExp(value.source, value.flags);
39
-
144
+ const known = seen.get(value);
145
+ if (known !== undefined) return known;
40
146
  if (Array.isArray(value)) {
41
- const arr = [];
42
- seen.set(value, arr);
43
- for (const item of value) arr.push(_deepClone(item, seen));
44
- return arr;
147
+ /** @type {any[]} */
148
+ const out = new Array(value.length);
149
+ seen.set(value, out);
150
+ for (let i = 0; i < value.length; i++) if (i in value) out[i] = _deepClone(value[i], seen);
151
+ return out;
45
152
  }
46
- if (checker.isMap(value)) {
47
- const map = new Map();
48
- seen.set(value, map);
49
- for (const [k, v] of value) map.set(k, _deepClone(v, seen));
50
- return map;
153
+ const proto = Object.getPrototypeOf(value);
154
+ if (proto === Object.prototype || proto === null) {
155
+ /** @type {any} */
156
+ const out = proto === null ? Object.create(null) : {};
157
+ seen.set(value, out);
158
+ const keys = Object.keys(value);
159
+ for (let i = 0; i < keys.length; i++) {
160
+ const key = keys[i];
161
+ if (!_UNSAFE.has(key)) out[key] = _deepClone(value[key], seen);
162
+ }
163
+ const symbols = Object.getOwnPropertySymbols(value);
164
+ for (let i = 0; i < symbols.length; i++) {
165
+ if (Object.prototype.propertyIsEnumerable.call(value, symbols[i])) out[symbols[i]] = _deepClone(value[symbols[i]], seen);
166
+ }
167
+ return out;
51
168
  }
52
- if (checker.isSet(value)) {
53
- const set = new Set();
54
- seen.set(value, set);
55
- for (const v of value) set.add(_deepClone(v, seen));
56
- return set;
169
+ if (types.isDate(value)) return new Date(value.getTime());
170
+ if (types.isRegExp(value)) {
171
+ const out = new RegExp(value.source, value.flags);
172
+ out.lastIndex = value.lastIndex;
173
+ return out;
57
174
  }
58
-
59
- const out = {};
175
+ if (types.isMap(value)) {
176
+ const out = new Map();
177
+ seen.set(value, out);
178
+ for (const [k, v] of value) out.set(_deepClone(k, seen), _deepClone(v, seen));
179
+ return out;
180
+ }
181
+ if (types.isSet(value)) {
182
+ const out = new Set();
183
+ seen.set(value, out);
184
+ for (const v of value) out.add(_deepClone(v, seen));
185
+ return out;
186
+ }
187
+ if (ArrayBuffer.isView(value)) {
188
+ // Buffer.slice() is a view on the same memory, not a copy.
189
+ if (Buffer.isBuffer(value)) return Buffer.from(value);
190
+ if (types.isDataView(value)) return new DataView(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
191
+ return /** @type {any} */ (value).slice();
192
+ }
193
+ if (types.isAnyArrayBuffer(value)) return value.slice(0);
194
+ if (value instanceof URL) return new URL(value.href);
195
+ if (value instanceof URLSearchParams) return new URLSearchParams(value);
196
+ // Things that hold state outside their properties are shared, not copied.
197
+ if (types.isPromise(value) || types.isWeakMap(value) || types.isWeakSet(value) || types.isProxy(value)) return value;
198
+ const out = Object.create(proto);
60
199
  seen.set(value, out);
61
- for (const key of Object.keys(value)) {
62
- out[key] = _deepClone(value[key], seen);
200
+ for (const key of Reflect.ownKeys(value)) {
201
+ if (typeof key === "string" && _UNSAFE.has(key)) continue;
202
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, key));
203
+ if ("value" in descriptor) descriptor.value = _deepClone(descriptor.value, seen);
204
+ Object.defineProperty(out, key, descriptor);
63
205
  }
64
206
  return out;
65
207
  };
66
208
 
67
- module.exports = {
68
- /**
69
- * Deeply clones a value.
70
- *
71
- * Uses the native `structuredClone` when the value supports it (plain data,
72
- * Dates, Maps, Sets, typed arrays, ...), and falls back to a manual deep copy
73
- * otherwise. Functions are copied by reference.
74
- *
75
- * @example
76
- * const copy = nodeComfort.obj.clone({ a: { b: 1 } });
77
- *
78
- * @template T
79
- * @param {T} value - Value to clone.
80
- * @returns {T} A deep copy.
81
- */
82
- clone(value) {
83
- if (value === null || typeof value !== "object") return value;
84
- try {
85
- return structuredClone(value);
86
- } catch {
87
- return _deepClone(value, new WeakMap());
209
+ /**
210
+ * Is it plain data, which merge copies, rather than a live object (a class
211
+ * instance, a connection) that it must keep as is?
212
+ * @param {unknown} value
213
+ * @returns {boolean}
214
+ */
215
+ const _isData = (value) =>
216
+ Array.isArray(value) || _isPlain(value) || types.isDate(value) || types.isMap(value) || types.isSet(value) || types.isRegExp(value) || ArrayBuffer.isView(value) || types.isAnyArrayBuffer(value);
217
+
218
+ /**
219
+ * Merges `source` into `target`, which the caller owns. What comes from the
220
+ * source is copied, so the result shares nothing with the inputs.
221
+ * @param {Record<string, any>} target
222
+ * @param {Record<string, any>} source
223
+ * @param {MergeOptions} options
224
+ * @returns {Record<string, any>}
225
+ */
226
+ const _mergeInto = (target, source, options) => {
227
+ const keys = Object.keys(source);
228
+ for (let i = 0; i < keys.length; i++) {
229
+ const key = keys[i];
230
+ if (_UNSAFE.has(key)) continue;
231
+ const value = source[key];
232
+ if (value === undefined && options.skipUndefined) continue;
233
+ const current = Object.prototype.hasOwnProperty.call(target, key) ? target[key] : undefined;
234
+ if (_isPlain(value)) {
235
+ target[key] = _mergeInto(_isPlain(current) ? current : {}, value, options);
236
+ } else if (Array.isArray(value)) {
237
+ const copy = _deepClone(value, new Map());
238
+ if (!Array.isArray(current) || options.arrays === undefined || options.arrays === "replace") target[key] = copy;
239
+ else target[key] = options.arrays === "unique" ? [...new Set([...current, ...copy])] : [...current, ...copy];
240
+ } else {
241
+ target[key] = _isData(value) ? _deepClone(value, new Map()) : value;
88
242
  }
89
- },
90
-
91
- /**
92
- * Deeply merges two or more plain objects into a new object.
93
- *
94
- * Later sources win. Plain-object values are merged recursively; arrays and
95
- * other values are replaced (not concatenated). Inputs are never mutated.
96
- *
97
- * @example
98
- * nodeComfort.obj.merge({ a: { x: 1 } }, { a: { y: 2 } });
99
- * // { a: { x: 1, y: 2 } }
100
- *
101
- * @param {...Record<string, any>} sources - Objects to merge.
102
- * @returns {Record<string, any>} The merged object.
103
- */
104
- merge(...sources) {
105
- const result = {};
106
- for (const source of sources) {
107
- if (!checker.isPlainObject(source)) continue;
108
- for (const key of Object.keys(source)) {
109
- const value = source[key];
110
- if (checker.isPlainObject(value) && checker.isPlainObject(result[key])) {
111
- result[key] = this.merge(result[key], value);
112
- } else if (checker.isPlainObject(value)) {
113
- result[key] = this.merge(value);
114
- } else {
115
- result[key] = value;
116
- }
243
+ }
244
+ return target;
245
+ };
246
+
247
+
248
+ /**
249
+ * Deep copy. Unlike `structuredClone`, it keeps functions and class
250
+ * instances (with their prototype), and it's faster on plain data. Shared
251
+ * references and cycles are preserved.
252
+ *
253
+ * @example
254
+ * const copy = obj.clone({ a: { b: [1, 2] }, when: new Date() });
255
+ * copy.a.b.push(3); // the original is untouched
256
+ *
257
+ * @template T
258
+ * @param {T} value
259
+ * @returns {T}
260
+ */
261
+ function clone(value) {
262
+ if (value === null || typeof value !== "object") return value;
263
+ return _deepClone(value, new Map());
264
+ }
265
+
266
+ /**
267
+ * Deep-merges objects into a new one. Later objects win, nested objects are
268
+ * merged, arrays are replaced. See `mergeWith()` to join arrays instead.
269
+ *
270
+ * @example
271
+ * obj.merge({ db: { host: "localhost", port: 5432 } }, { db: { port: 6543 } });
272
+ * // { db: { host: "localhost", port: 6543 } }
273
+ *
274
+ * @template {object} A
275
+ * @overload
276
+ * @param {A} a
277
+ * @returns {A}
278
+ */
279
+ /**
280
+ * Deep-merges `b` into a copy of `a`.
281
+ *
282
+ * @template {object} A
283
+ * @template {object} B
284
+ * @overload
285
+ * @param {A} a
286
+ * @param {B} b
287
+ * @returns {A & B}
288
+ */
289
+ /**
290
+ * Deep-merges three objects; the last one wins.
291
+ *
292
+ * @template {object} A
293
+ * @template {object} B
294
+ * @template {object} C
295
+ * @overload
296
+ * @param {A} a
297
+ * @param {B} b
298
+ * @param {C} c
299
+ * @returns {A & B & C}
300
+ */
301
+ /**
302
+ * Deep-merges any number of objects; the last one wins.
303
+ *
304
+ * @overload
305
+ * @param {...object} sources
306
+ * @returns {Record<string, any>}
307
+ */
308
+ /**
309
+ * @param {...object} sources
310
+ * @returns {Record<string, any>}
311
+ */
312
+ function merge(...sources) {
313
+ /** @type {Record<string, any>} */
314
+ const out = {};
315
+ for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, _NO_OPTIONS);
316
+ return out;
317
+ }
318
+
319
+ /**
320
+ * Like `merge()`, with a choice of what happens to arrays and `undefined`.
321
+ *
322
+ * @example
323
+ * obj.mergeWith({ arrays: "unique" }, { tags: ["a", "b"] }, { tags: ["b", "c"] });
324
+ * // { tags: ["a", "b", "c"] }
325
+ * obj.mergeWith({ skipUndefined: true }, { port: 80 }, { port: undefined });
326
+ * // { port: 80 }
327
+ *
328
+ * @param {MergeOptions} options
329
+ * @param {...object} sources
330
+ * @returns {Record<string, any>}
331
+ */
332
+ function mergeWith(options, ...sources) {
333
+ /** @type {Record<string, any>} */
334
+ const out = {};
335
+ for (const source of sources) if (_isPlain(source)) _mergeInto(out, source, options ?? _NO_OPTIONS);
336
+ return out;
337
+ }
338
+
339
+ /**
340
+ * Fills in missing properties from defaults, deeply. Values that are set,
341
+ * even `null` or `0`, are kept.
342
+ *
343
+ * @example
344
+ * obj.defaults({ port: 8080, db: { user: "me" } }, { port: 80, db: { user: "root", pass: "" } });
345
+ * // { port: 8080, db: { user: "me", pass: "" } }
346
+ *
347
+ * @template {object} T
348
+ * @template {object} D
349
+ * @param {T} target
350
+ * @param {...D} sources The first one has the highest priority.
351
+ * @returns {T & D}
352
+ */
353
+ function defaults(target, ...sources) {
354
+ /** @type {(t: any, s: any) => any} */
355
+ const fill = (t, s) => {
356
+ const out = _isPlain(t) ? { ...t } : {};
357
+ for (const key of Object.keys(s)) {
358
+ if (_UNSAFE.has(key)) continue;
359
+ if (out[key] === undefined) out[key] = clone(s[key]);
360
+ else if (_isPlain(out[key]) && _isPlain(s[key])) out[key] = fill(out[key], s[key]);
361
+ }
362
+ return out;
363
+ };
364
+ return sources.reduce((acc, source) => (_isPlain(source) ? fill(acc, source) : acc), /** @type {any} */ (clone(target)));
365
+ }
366
+
367
+ /**
368
+ * Deep equality. Knows about arrays, dates, regexes, maps, sets, typed
369
+ * arrays, `NaN` and circular references.
370
+ *
371
+ * @example
372
+ * obj.equal({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }); // true
373
+ * obj.equal(new Set([1, 2]), new Set([2, 1])); // true
374
+ * obj.equal({ a: 1 }, { a: 1, b: undefined }); // false
375
+ *
376
+ * @param {unknown} a
377
+ * @param {unknown} b
378
+ * @returns {boolean}
379
+ */
380
+ function equal(a, b) {
381
+ if (Object.is(a, b)) return true;
382
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
383
+ return _equal(a, b, [], []);
384
+ }
385
+
386
+ /**
387
+ * Same kind of built-in value, even when they come from different realms
388
+ * (a worker, a `vm` context) where prototypes are not shared.
389
+ * @param {object} a
390
+ * @param {object} b
391
+ * @returns {boolean}
392
+ */
393
+ const _sameBuiltIn = (a, b) => {
394
+ const tag = Object.prototype.toString.call(a);
395
+ if (tag !== Object.prototype.toString.call(b)) return false;
396
+ return tag !== "[object Object]" || (Object.getPrototypeOf(Object.getPrototypeOf(a) ?? {}) === null && Object.getPrototypeOf(Object.getPrototypeOf(b) ?? {}) === null);
397
+ };
398
+
399
+ /**
400
+ * Own enumerable keys and values are the same.
401
+ * @param {any} a
402
+ * @param {any} b
403
+ * @param {object[]} stackA
404
+ * @param {object[]} stackB
405
+ * @returns {boolean}
406
+ */
407
+ const _equalKeys = (a, b, stackA, stackB) => {
408
+ const keys = Object.keys(a);
409
+ if (keys.length !== Object.keys(b).length) return false;
410
+ for (let i = 0; i < keys.length; i++) {
411
+ const key = keys[i];
412
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !_equal(a[key], b[key], stackA, stackB)) return false;
413
+ }
414
+ return true;
415
+ };
416
+
417
+ /**
418
+ * The stacks hold the objects being compared, so a cycle is recognized
419
+ * without a `WeakMap` lookup for every object.
420
+ * @param {any} a
421
+ * @param {any} b
422
+ * @param {object[]} stackA
423
+ * @param {object[]} stackB
424
+ * @returns {boolean}
425
+ */
426
+ function _equal(a, b, stackA, stackB) {
427
+ if (Object.is(a, b)) return true;
428
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
429
+ const proto = Object.getPrototypeOf(a);
430
+ if (proto !== Object.getPrototypeOf(b) && !_sameBuiltIn(a, b)) return false;
431
+ for (let i = stackA.length - 1; i >= 0; i--) if (stackA[i] === a) return stackB[i] === b;
432
+ stackA.push(a);
433
+ stackB.push(b);
434
+ const result = _equalObjects(a, b, proto, stackA, stackB);
435
+ stackA.pop();
436
+ stackB.pop();
437
+ return result;
438
+ }
439
+
440
+ /**
441
+ * @param {any} a
442
+ * @param {any} b
443
+ * @param {object | null} proto
444
+ * @param {object[]} stackA
445
+ * @param {object[]} stackB
446
+ * @returns {boolean}
447
+ */
448
+ const _equalObjects = (a, b, proto, stackA, stackB) => {
449
+ if (Array.isArray(a)) {
450
+ if (a.length !== b.length) return false;
451
+ for (let i = 0; i < a.length; i++) if (!_equal(a[i], b[i], stackA, stackB)) return false;
452
+ return true;
453
+ }
454
+ if (proto === Object.prototype || proto === null) return _equalKeys(a, b, stackA, stackB);
455
+ if (types.isDate(a)) return a.getTime() === b.getTime();
456
+ if (types.isRegExp(a)) return a.source === b.source && a.flags === b.flags;
457
+ if (ArrayBuffer.isView(a) || types.isAnyArrayBuffer(a)) {
458
+ const x = ArrayBuffer.isView(a) ? new Uint8Array(a.buffer, a.byteOffset, a.byteLength) : new Uint8Array(a);
459
+ const y = ArrayBuffer.isView(b) ? new Uint8Array(b.buffer, b.byteOffset, b.byteLength) : new Uint8Array(b);
460
+ if (x.length !== y.length) return false;
461
+ for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
462
+ return true;
463
+ }
464
+ if (types.isMap(a)) {
465
+ if (a.size !== b.size) return false;
466
+ outer: for (const [key, value] of a) {
467
+ if (b.has(key)) {
468
+ if (!_equal(value, b.get(key), stackA, stackB)) return false;
469
+ continue;
117
470
  }
471
+ for (const [otherKey, otherValue] of b) if (_equal(key, otherKey, stackA, stackB) && _equal(value, otherValue, stackA, stackB)) continue outer;
472
+ return false;
118
473
  }
119
- return result;
120
- },
121
-
122
- /**
123
- * Deeply compares two values for structural equality.
124
- *
125
- * Handles plain objects, arrays, Dates, RegExps, Maps and Sets.
126
- *
127
- * @example
128
- * nodeComfort.obj.equal({ a: [1, 2] }, { a: [1, 2] }); // true
129
- *
130
- * @param {*} a - First value.
131
- * @param {*} b - Second value.
132
- * @returns {boolean} True if the values are deeply equal.
133
- */
134
- equal(a, b) {
135
- if (a === b) return true;
136
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
137
- return a !== a && b !== b; // both NaN
474
+ return true;
475
+ }
476
+ if (types.isSet(a)) {
477
+ if (a.size !== b.size) return false;
478
+ outer: for (const value of a) {
479
+ if (b.has(value)) continue;
480
+ for (const other of b) if (_equal(value, other, stackA, stackB)) continue outer;
481
+ return false;
138
482
  }
139
- if (checker.isDate(a) && checker.isDate(b)) return a.getTime() === b.getTime();
140
- if (checker.isRegExp(a) && checker.isRegExp(b)) return a.toString() === b.toString();
483
+ return true;
484
+ }
485
+ // These keep their content outside enumerable properties.
486
+ if (types.isBoxedPrimitive(a)) return Object.is(a.valueOf(), b.valueOf());
487
+ if (types.isNativeError(a)) return a.name === b.name && a.message === b.message && _equalKeys(a, b, stackA, stackB);
488
+ if (a instanceof URL) return a.href === b.href;
489
+ if (a instanceof URLSearchParams) return a.toString() === b.toString();
490
+ return _equalKeys(a, b, stackA, stackB);
491
+ };
141
492
 
142
- if (Array.isArray(a) || Array.isArray(b)) {
143
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
144
- return a.every((item, i) => this.equal(item, b[i]));
493
+ /**
494
+ * Lists what changed between two values, deeply. Handy for audit logs and
495
+ * partial updates.
496
+ *
497
+ * @example
498
+ * obj.diff({ name: "Ada", tags: ["a"] }, { name: "Ada L.", tags: ["a", "b"] });
499
+ * // [
500
+ * // { path: ["name"], type: "changed", from: "Ada", to: "Ada L." },
501
+ * // { path: ["tags", 1], type: "added", to: "b" },
502
+ * // ]
503
+ *
504
+ * @param {unknown} before
505
+ * @param {unknown} after
506
+ * @returns {Change[]} Empty when nothing changed.
507
+ */
508
+ function diff(before, after) {
509
+ /** @type {Change[]} */
510
+ const changes = [];
511
+ /** @type {Array<string | number>} */
512
+ const path = [];
513
+ /** @type {(a: any, b: any) => void} */
514
+ const walk = (a, b) => {
515
+ if (Object.is(a, b)) return;
516
+ if (Array.isArray(a) && Array.isArray(b)) {
517
+ const size = Math.max(a.length, b.length);
518
+ for (let i = 0; i < size; i++) {
519
+ path.push(i);
520
+ if (i >= b.length) changes.push({ path: [...path], type: "removed", from: a[i] });
521
+ else if (i >= a.length) changes.push({ path: [...path], type: "added", to: b[i] });
522
+ else walk(a[i], b[i]);
523
+ path.pop();
524
+ }
525
+ return;
145
526
  }
146
- if (checker.isMap(a) && checker.isMap(b)) {
147
- if (a.size !== b.size) return false;
148
- for (const [k, v] of a) {
149
- if (!b.has(k) || !this.equal(v, b.get(k))) return false;
527
+ if (_isPlain(a) && _isPlain(b)) {
528
+ for (const key of Object.keys(a)) {
529
+ path.push(key);
530
+ if (Object.prototype.hasOwnProperty.call(b, key)) walk(a[key], b[key]);
531
+ else changes.push({ path: [...path], type: "removed", from: a[key] });
532
+ path.pop();
533
+ }
534
+ for (const key of Object.keys(b)) {
535
+ if (!Object.prototype.hasOwnProperty.call(a, key)) changes.push({ path: [...path, key], type: "added", to: b[key] });
150
536
  }
151
- return true;
537
+ return;
152
538
  }
153
- if (checker.isSet(a) && checker.isSet(b)) {
154
- if (a.size !== b.size) return false;
155
- for (const v of a) if (!b.has(v)) return false;
156
- return true;
539
+ if (!equal(a, b)) changes.push({ path: [...path], type: "changed", from: a, to: b });
540
+ };
541
+ walk(before, after);
542
+ return changes;
543
+ }
544
+
545
+ /**
546
+ * Reads a nested value from a dotted path. Your editor completes the paths
547
+ * and knows the type of the result.
548
+ *
549
+ * @example
550
+ * obj.get(config, "db.pool.max"); // number | undefined
551
+ * obj.get(config, "db.pool.max", 10); // number
552
+ * obj.get(data, "users[0].name");
553
+ * obj.get(data, ["users", 0, "name"]);
554
+ *
555
+ * @template T
556
+ * @template {string} P
557
+ * @overload
558
+ * @param {T} object
559
+ * @param {P | Paths<T> | ReadonlyArray<string | number>} path Like `"a.b.c"`, `"a[0].b"` or `["a", 0, "b"]`.
560
+ * @returns {PathValue<T, P>}
561
+ */
562
+ /**
563
+ * Reads a nested value from a dotted path, or returns `fallback` if there's
564
+ * nothing there.
565
+ *
566
+ * @template T
567
+ * @template {string} P
568
+ * @template F
569
+ * @overload
570
+ * @param {T} object
571
+ * @param {P | Paths<T> | ReadonlyArray<string | number>} path Like `"a.b.c"`, `"a[0].b"` or `["a", 0, "b"]`.
572
+ * @param {F} fallback
573
+ * @returns {Exclude<PathValue<T, P>, undefined> | F}
574
+ */
575
+ /**
576
+ * @param {unknown} object
577
+ * @param {string | ReadonlyArray<string | number>} path
578
+ * @param {unknown} [fallback]
579
+ * @returns {any}
580
+ */
581
+ function get(object, path, fallback) {
582
+ const { keys, safe } = _parsePath(path);
583
+ if (!safe) return fallback;
584
+ /** @type {any} */
585
+ let current = object;
586
+ for (let i = 0; i < keys.length; i++) {
587
+ if (current === null || current === undefined) return fallback;
588
+ current = current[keys[i]];
589
+ }
590
+ return current === undefined ? fallback : current;
591
+ }
592
+
593
+ /**
594
+ * Returns a copy with a nested value set. Missing levels are created
595
+ * (arrays when the next key is a number).
596
+ *
597
+ * @example
598
+ * obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } }
599
+ * obj.set(state, "user.name", "Ada"); // state is untouched
600
+ *
601
+ * @template T
602
+ * @param {T} object
603
+ * @param {PathInput<T>} path
604
+ * @param {unknown} value
605
+ * @returns {T}
606
+ */
607
+ function set(object, path, value) {
608
+ const { keys, safe } = _parsePath(path);
609
+ /** @type {any} */
610
+ const root = object === null || typeof object !== "object" ? {} : clone(object);
611
+ if (!keys.length || !safe) return root;
612
+ let current = root;
613
+ for (let i = 0; i < keys.length - 1; i++) {
614
+ const key = keys[i];
615
+ const next = keys[i + 1];
616
+ if (current[key] === null || typeof current[key] !== "object") {
617
+ current[key] = typeof next === "number" || /^\d+$/.test(String(next)) ? [] : {};
157
618
  }
619
+ current = current[key];
620
+ }
621
+ current[keys[keys.length - 1]] = value;
622
+ return root;
623
+ }
158
624
 
159
- const keysA = Object.keys(a);
160
- const keysB = Object.keys(b);
161
- if (keysA.length !== keysB.length) return false;
162
- return keysA.every((key) => Object.hasOwn(b, key) && this.equal(a[key], b[key]));
163
- },
164
-
165
- /**
166
- * Gets a nested value from an object using a dot/bracket path.
167
- *
168
- * @example
169
- * nodeComfort.obj.get({ a: { b: [{ c: 42 }] } }, "a.b[0].c"); // 42
170
- * nodeComfort.obj.get({}, "a.b.c", "default"); // "default"
171
- *
172
- * @param {Record<string, any>} object - Source object.
173
- * @param {string|Array<string|number>} path - Dot path or key array.
174
- * @param {*} [fallback] - Returned when the path does not resolve.
175
- * @returns {*} The value at the path, or the fallback.
176
- */
177
- get(object, path, fallback) {
178
- const keys = _toPath(path);
179
- let current = object;
180
- for (const key of keys) {
181
- if (current == null || !(key in Object(current))) return fallback;
182
- current = current[key];
625
+ /**
626
+ * Returns a copy where the value at `path` is replaced by what `updater`
627
+ * returns for it. Handy for counters and lists.
628
+ *
629
+ * @example
630
+ * obj.update(state, "stats.visits", (n = 0) => n + 1);
631
+ * obj.update(cart, "items", (items = []) => [...items, product]);
632
+ *
633
+ * @template T
634
+ * @param {T} object
635
+ * @param {PathInput<T>} path
636
+ * @param {(value: any) => unknown} updater Gets the current value, `undefined` if there's none.
637
+ * @returns {T}
638
+ */
639
+ function update(object, path, updater) {
640
+ return set(object, path, updater(get(object, /** @type {any} */ (path))));
641
+ }
642
+
643
+ /**
644
+ * Returns a copy without the property at `path`.
645
+ *
646
+ * @example
647
+ * obj.unset({ a: { b: 1, c: 2 } }, "a.b"); // { a: { c: 2 } }
648
+ *
649
+ * @template T
650
+ * @param {T} object
651
+ * @param {PathInput<T>} path
652
+ * @returns {T}
653
+ */
654
+ function unset(object, path) {
655
+ const { keys, safe } = _parsePath(path);
656
+ /** @type {any} */
657
+ const root = clone(object);
658
+ if (!keys.length || !safe || root === null || typeof root !== "object") return root;
659
+ let current = root;
660
+ for (let i = 0; i < keys.length - 1; i++) {
661
+ current = current?.[keys[i]];
662
+ if (current === null || typeof current !== "object") return root;
663
+ }
664
+ const lastKey = keys[keys.length - 1];
665
+ if (Array.isArray(current) && (typeof lastKey === "number" || /^\d+$/.test(lastKey))) current.splice(Number(lastKey), 1);
666
+ else delete current[lastKey];
667
+ return root;
668
+ }
669
+
670
+ /**
671
+ * Does the path exist? A property set to `undefined` still counts.
672
+ *
673
+ * @example
674
+ * obj.has({ a: { b: undefined } }, "a.b"); // true
675
+ * obj.has({ a: {} }, "a.b"); // false
676
+ *
677
+ * @template T
678
+ * @param {T} object
679
+ * @param {PathInput<T>} path
680
+ * @returns {boolean}
681
+ */
682
+ function has(object, path) {
683
+ const { keys, safe } = _parsePath(path);
684
+ if (!keys.length || !safe) return false;
685
+ /** @type {any} */
686
+ let current = object;
687
+ for (let i = 0; i < keys.length; i++) {
688
+ if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), keys[i])) return false;
689
+ current = current[keys[i]];
690
+ }
691
+ return true;
692
+ }
693
+
694
+ /**
695
+ * A copy with only the listed keys.
696
+ *
697
+ * @example
698
+ * obj.pick(user, ["id", "email"]);
699
+ *
700
+ * @template {object} T
701
+ * @template {keyof T} K
702
+ * @param {T} object
703
+ * @param {readonly K[]} keys
704
+ * @returns {Pick<T, K>}
705
+ */
706
+ function pick(object, keys) {
707
+ /** @type {any} */
708
+ const out = {};
709
+ if (object === null || object === undefined) return out;
710
+ for (const key of keys) {
711
+ if (Object.prototype.hasOwnProperty.call(object, key) && !_UNSAFE.has(/** @type {any} */ (key))) out[key] = object[key];
712
+ }
713
+ return out;
714
+ }
715
+
716
+ /**
717
+ * A copy without the listed keys.
718
+ *
719
+ * @example
720
+ * obj.omit(user, ["password", "token"]);
721
+ *
722
+ * @template {object} T
723
+ * @template {keyof T} K
724
+ * @param {T} object
725
+ * @param {readonly K[]} keys
726
+ * @returns {Omit<T, K>}
727
+ */
728
+ function omit(object, keys) {
729
+ const exclude = new Set(/** @type {readonly PropertyKey[]} */ (keys));
730
+ /** @type {any} */
731
+ const out = {};
732
+ for (const key of Object.keys(object ?? {})) {
733
+ if (!exclude.has(key) && !_UNSAFE.has(key)) out[key] = /** @type {any} */ (object)[key];
734
+ }
735
+ return out;
736
+ }
737
+
738
+ /**
739
+ * Keeps the entries that pass a test.
740
+ *
741
+ * @example
742
+ * obj.filter({ a: 1, b: 2, c: 3 }, (value) => value > 1); // { b: 2, c: 3 }
743
+ *
744
+ * @template {object} T
745
+ * @param {T} object
746
+ * @param {(value: T[keyof T], key: keyof T & string) => unknown} predicate
747
+ * @returns {Partial<T>}
748
+ */
749
+ function filter(object, predicate) {
750
+ /** @type {any} */
751
+ const out = {};
752
+ for (const key of Object.keys(object ?? {})) {
753
+ if (_UNSAFE.has(key)) continue;
754
+ const value = /** @type {any} */ (object)[key];
755
+ if (predicate(value, /** @type {keyof T & string} */ (key))) out[key] = value;
756
+ }
757
+ return out;
758
+ }
759
+
760
+ /**
761
+ * Transforms every value, keeping the keys.
762
+ *
763
+ * @example
764
+ * obj.mapValues({ a: 1, b: 2 }, (v) => v * 10); // { a: 10, b: 20 }
765
+ *
766
+ * @template {object} T
767
+ * @template R
768
+ * @param {T} object
769
+ * @param {(value: T[keyof T], key: keyof T & string) => R} fn
770
+ * @returns {{ [K in keyof T]: R }}
771
+ */
772
+ function mapValues(object, fn) {
773
+ /** @type {any} */
774
+ const out = {};
775
+ for (const key of Object.keys(object ?? {})) {
776
+ if (!_UNSAFE.has(key)) out[key] = fn(/** @type {any} */ (object)[key], /** @type {keyof T & string} */ (key));
777
+ }
778
+ return out;
779
+ }
780
+
781
+ /**
782
+ * Transforms every key, keeping the values.
783
+ *
784
+ * @example
785
+ * obj.mapKeys({ first_name: "Ada" }, (key) => nc.str.camelCase(key)); // { firstName: "Ada" }
786
+ *
787
+ * @template {object} T
788
+ * @param {T} object
789
+ * @param {(key: keyof T & string, value: T[keyof T]) => PropertyKey} fn Returns the new key.
790
+ * @returns {Record<string, T[keyof T]>}
791
+ */
792
+ function mapKeys(object, fn) {
793
+ /** @type {any} */
794
+ const out = {};
795
+ for (const [key, value] of Object.entries(object ?? {})) {
796
+ const next = fn(/** @type {keyof T & string} */ (key), value);
797
+ if (typeof next === "string" && _UNSAFE.has(next)) continue;
798
+ out[next] = value;
799
+ }
800
+ return out;
801
+ }
802
+
803
+ /**
804
+ * Renames some keys and leaves the others alone.
805
+ *
806
+ * @example
807
+ * obj.renameKeys({ _id: 1, name: "Ada" }, { _id: "id" }); // { id: 1, name: "Ada" }
808
+ *
809
+ * @template {object} T
810
+ * @param {T} object
811
+ * @param {Partial<Record<keyof T, string>>} mapping Old name to new name.
812
+ * @returns {Record<string, T[keyof T]>}
813
+ */
814
+ function renameKeys(object, mapping) {
815
+ const map = /** @type {Record<string, string | undefined>} */ (mapping);
816
+ return mapKeys(object, (key) => map[key] ?? key);
817
+ }
818
+
819
+ /**
820
+ * Swaps keys and values.
821
+ *
822
+ * @example
823
+ * obj.invert({ a: "x", b: "y" }); // { x: "a", y: "b" }
824
+ *
825
+ * @param {Record<string, PropertyKey>} object
826
+ * @returns {Record<string, string>}
827
+ */
828
+ function invert(object) {
829
+ /** @type {Record<string, string>} */
830
+ const out = {};
831
+ for (const [key, value] of Object.entries(object ?? {})) {
832
+ const next = String(value);
833
+ if (!_UNSAFE.has(next)) out[next] = key;
834
+ }
835
+ return out;
836
+ }
837
+
838
+ /**
839
+ * Removes `null` and `undefined` values, and optionally empty ones.
840
+ *
841
+ * @example
842
+ * obj.compact({ a: 1, b: null, c: undefined }); // { a: 1 }
843
+ * obj.compact({ a: { b: null, c: "" } }, { deep: true, removeEmpty: true }); // {}
844
+ *
845
+ * @template {object} T
846
+ * @param {T} object
847
+ * @param {CompactOptions} [options]
848
+ * @returns {Partial<T>}
849
+ */
850
+ function compact(object, options = {}) {
851
+ /** @type {(value: any) => boolean} */
852
+ const isEmptyValue = (value) =>
853
+ value === null ||
854
+ value === undefined ||
855
+ (options.removeEmpty === true &&
856
+ (value === "" || (Array.isArray(value) && value.length === 0) || (_isPlain(value) && Object.keys(value).length === 0)));
857
+ /** @type {(value: any) => any} */
858
+ const clean = (value) => {
859
+ if (!options.deep) return value;
860
+ if (Array.isArray(value)) return value.map(clean).filter((v) => !isEmptyValue(v));
861
+ if (_isPlain(value)) return compact(value, options);
862
+ return value;
863
+ };
864
+ /** @type {any} */
865
+ const out = {};
866
+ for (const [key, value] of Object.entries(object ?? {})) {
867
+ if (_UNSAFE.has(key)) continue;
868
+ const cleaned = clean(value);
869
+ if (!isEmptyValue(cleaned)) out[key] = cleaned;
870
+ }
871
+ return out;
872
+ }
873
+
874
+ /**
875
+ * Flattens nested objects into dotted keys. Arrays stay as values.
876
+ *
877
+ * @example
878
+ * obj.flatten({ db: { host: "x", port: 5432 } }); // { "db.host": "x", "db.port": 5432 }
879
+ *
880
+ * @param {object} object
881
+ * @param {string} [prefix=""] Added in front of every key.
882
+ * @returns {Record<string, unknown>}
883
+ */
884
+ function flatten(object, prefix = "") {
885
+ /** @type {Record<string, unknown>} */
886
+ const out = {};
887
+ /** @type {(value: Record<string, unknown>, path: string) => void} */
888
+ const walk = (value, path) => {
889
+ for (const key of Object.keys(value)) {
890
+ if (_UNSAFE.has(key)) continue;
891
+ const child = value[key];
892
+ const next = path ? `${path}.${key}` : key;
893
+ if (_isPlain(child) && Object.keys(child).length) walk(child, next);
894
+ else out[next] = child;
183
895
  }
184
- return current === undefined ? fallback : current;
185
- },
186
-
187
- /**
188
- * Sets a nested value in an object using a dot/bracket path (returns a clone).
189
- *
190
- * Intermediate objects/arrays are created as needed. The original object is
191
- * not mutated.
192
- *
193
- * @example
194
- * nodeComfort.obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } }
195
- *
196
- * @param {Record<string, any>} object - Source object.
197
- * @param {string|Array<string|number>} path - Dot path or key array.
198
- * @param {*} value - Value to set.
199
- * @returns {Record<string, any>} A new object with the value set.
200
- */
201
- set(object, path, value) {
202
- const keys = _toPath(path);
203
- if (!keys.length) return object;
204
- const root = this.clone(object) ?? {};
205
- let current = root;
896
+ };
897
+ walk(/** @type {any} */ (object ?? {}), prefix);
898
+ return out;
899
+ }
900
+
901
+ /**
902
+ * Every leaf path of an object, in dotted form. Arrays count as leaves.
903
+ *
904
+ * @example
905
+ * obj.paths({ db: { host: "x", port: 5432 }, tags: ["a"] }); // ["db.host", "db.port", "tags"]
906
+ *
907
+ * @param {object} object
908
+ * @returns {string[]}
909
+ */
910
+ function paths(object) {
911
+ return Object.keys(flatten(object));
912
+ }
913
+
914
+ /**
915
+ * Rebuilds nested objects from dotted keys. The opposite of `flatten()`.
916
+ *
917
+ * @example
918
+ * obj.unflatten({ "db.host": "x", "db.port": 5432 }); // { db: { host: "x", port: 5432 } }
919
+ *
920
+ * @param {Record<string, unknown>} object
921
+ * @returns {Record<string, any>}
922
+ */
923
+ function unflatten(object) {
924
+ /** @type {Record<string, any>} */
925
+ const out = {};
926
+ for (const [path, value] of Object.entries(object ?? {})) {
927
+ const { keys, safe } = _parsePath(path);
928
+ if (!keys.length || !safe) continue;
929
+ let current = out;
206
930
  for (let i = 0; i < keys.length - 1; i++) {
207
931
  const key = keys[i];
208
- if (current[key] == null || typeof current[key] !== "object") {
209
- current[key] = typeof keys[i + 1] === "number" || /^\d+$/.test(String(keys[i + 1])) ? [] : {};
932
+ if (current[key] === null || typeof current[key] !== "object") {
933
+ current[key] = /^\d+$/.test(String(keys[i + 1])) ? [] : {};
210
934
  }
211
935
  current = current[key];
212
936
  }
213
937
  current[keys[keys.length - 1]] = value;
214
- return root;
215
- },
216
-
217
- /**
218
- * Checks whether a nested path exists in an object.
219
- *
220
- * @param {Record<string, any>} object - Source object.
221
- * @param {string|Array<string|number>} path - Dot path or key array.
222
- * @returns {boolean} True if the path resolves.
223
- */
224
- has(object, path) {
225
- const keys = _toPath(path);
226
- let current = object;
227
- for (const key of keys) {
228
- if (current == null || !(key in Object(current))) return false;
229
- current = current[key];
230
- }
231
- return true;
232
- },
233
-
234
- /**
235
- * Returns a new object containing only the given keys.
236
- *
237
- * @example
238
- * nodeComfort.obj.pick({ a: 1, b: 2, c: 3 }, ["a", "c"]); // { a: 1, c: 3 }
239
- *
240
- * @param {Record<string, any>} object - Source object.
241
- * @param {string[]} keys - Keys to keep.
242
- * @returns {Record<string, any>} The picked object.
243
- */
244
- pick(object, keys) {
245
- const result = {};
246
- for (const key of keys) {
247
- if (object != null && Object.hasOwn(object, key)) result[key] = object[key];
248
- }
249
- return result;
250
- },
251
-
252
- /**
253
- * Returns a new object without the given keys.
254
- *
255
- * @example
256
- * nodeComfort.obj.omit({ a: 1, b: 2, c: 3 }, ["b"]); // { a: 1, c: 3 }
257
- *
258
- * @param {Record<string, any>} object - Source object.
259
- * @param {string[]} keys - Keys to remove.
260
- * @returns {Record<string, any>} The remaining object.
261
- */
262
- omit(object, keys) {
263
- const exclude = new Set(keys);
264
- const result = {};
265
- for (const key of Object.keys(object ?? {})) {
266
- if (!exclude.has(key)) result[key] = object[key];
267
- }
268
- return result;
269
- },
270
-
271
- /**
272
- * Returns a new object keeping only the entries that pass a predicate.
273
- *
274
- * @example
275
- * nodeComfort.obj.filter({ a: 1, b: 2, c: 3 }, (v) => v > 1); // { b: 2, c: 3 }
276
- *
277
- * @param {Record<string, any>} object - Source object.
278
- * @param {(value: any, key: string) => boolean} predicate - Test function.
279
- * @returns {Record<string, any>} The filtered object.
280
- */
281
- filter(object, predicate) {
282
- const result = {};
283
- for (const [key, value] of Object.entries(object ?? {})) {
284
- if (predicate(value, key)) result[key] = value;
285
- }
286
- return result;
287
- },
288
-
289
- /**
290
- * Maps over an object's values, returning a new object with the same keys.
291
- *
292
- * @example
293
- * nodeComfort.obj.mapValues({ a: 1, b: 2 }, (v) => v * 10); // { a: 10, b: 20 }
294
- *
295
- * @param {Record<string, any>} object - Source object.
296
- * @param {(value: any, key: string) => any} fn - Mapping function.
297
- * @returns {Record<string, any>} The mapped object.
298
- */
299
- mapValues(object, fn) {
300
- const result = {};
301
- for (const [key, value] of Object.entries(object ?? {})) {
302
- result[key] = fn(value, key);
303
- }
304
- return result;
305
- },
306
-
307
- /**
308
- * Maps over an object's keys, returning a new object with remapped keys.
309
- *
310
- * @example
311
- * nodeComfort.obj.mapKeys({ a: 1 }, (k) => k.toUpperCase()); // { A: 1 }
312
- *
313
- * @param {Record<string, any>} object - Source object.
314
- * @param {(key: string, value: any) => string} fn - Key-mapping function.
315
- * @returns {Record<string, any>} The remapped object.
316
- */
317
- mapKeys(object, fn) {
318
- const result = {};
319
- for (const [key, value] of Object.entries(object ?? {})) {
320
- result[fn(key, value)] = value;
321
- }
322
- return result;
323
- },
324
-
325
- /**
326
- * Swaps keys and values of an object.
327
- *
328
- * @example
329
- * nodeComfort.obj.invert({ a: "x", b: "y" }); // { x: "a", y: "b" }
330
- *
331
- * @param {Record<string, any>} object - Source object.
332
- * @returns {Record<string, string>} The inverted object.
333
- */
334
- invert(object) {
335
- const result = {};
336
- for (const [key, value] of Object.entries(object ?? {})) {
337
- result[value] = key;
338
- }
339
- return result;
340
- },
341
-
342
- /**
343
- * Removes entries whose value is `null` or `undefined` (shallow).
344
- *
345
- * @example
346
- * nodeComfort.obj.compact({ a: 1, b: null, c: undefined }); // { a: 1 }
347
- *
348
- * @param {Record<string, any>} object - Source object.
349
- * @returns {Record<string, any>} The compacted object.
350
- */
351
- compact(object) {
352
- return this.filter(object, (v) => v !== null && v !== undefined);
353
- },
354
-
355
- /**
356
- * Flattens a nested object into a single-level object with dot-path keys.
357
- *
358
- * @example
359
- * nodeComfort.obj.flatten({ a: { b: { c: 1 } } }); // { "a.b.c": 1 }
360
- *
361
- * @param {Record<string, any>} object - Source object.
362
- * @param {string} [prefix=""] - Prefix for keys (used internally).
363
- * @returns {Record<string, any>} The flattened object.
364
- */
365
- flatten(object, prefix = "") {
366
- const result = {};
367
- for (const [key, value] of Object.entries(object ?? {})) {
368
- const path = prefix ? `${prefix}.${key}` : key;
369
- if (checker.isPlainObject(value) && Object.keys(value).length) {
370
- Object.assign(result, this.flatten(value, path));
371
- } else {
372
- result[path] = value;
938
+ }
939
+ return out;
940
+ }
941
+
942
+ /**
943
+ * `Object.keys`, but typed with the object's actual keys.
944
+ *
945
+ * @example
946
+ * for (const key of obj.keys(config)) config[key]; // no type error
947
+ *
948
+ * @template {object} T
949
+ * @param {T} object
950
+ * @returns {Array<keyof T & string>}
951
+ */
952
+ function keys(object) {
953
+ return /** @type {Array<keyof T & string>} */ (Object.keys(object ?? {}));
954
+ }
955
+
956
+ /**
957
+ * `Object.entries`, with typed keys and values.
958
+ *
959
+ * @template {object} T
960
+ * @param {T} object
961
+ * @returns {Array<[keyof T & string, T[keyof T]]>}
962
+ */
963
+ function entries(object) {
964
+ return /** @type {Array<[keyof T & string, T[keyof T]]>} */ (Object.entries(object ?? {}));
965
+ }
966
+
967
+ /**
968
+ * `Object.fromEntries` that accepts any iterable (a `Map` too) and skips
969
+ * unsafe keys.
970
+ *
971
+ * @example
972
+ * obj.fromEntries(new Map([["x", true]])); // { x: true }
973
+ *
974
+ * @template {PropertyKey} K
975
+ * @template V
976
+ * @param {Iterable<readonly [K, V]>} pairs
977
+ * @returns {Record<K, V>}
978
+ */
979
+ function fromEntries(pairs) {
980
+ /** @type {any} */
981
+ const out = {};
982
+ for (const [key, value] of pairs) {
983
+ if (typeof key === "string" && _UNSAFE.has(key)) continue;
984
+ out[key] = value;
985
+ }
986
+ return out;
987
+ }
988
+
989
+ /**
990
+ * How many entries: own keys of an object, size of a map or set, length of
991
+ * an array or string.
992
+ *
993
+ * @example
994
+ * obj.size({ a: 1, b: 2 }); // 2
995
+ *
996
+ * @param {unknown} value
997
+ * @returns {number}
998
+ */
999
+ function size(value) {
1000
+ if (value === null || value === undefined) return 0;
1001
+ if (typeof value === "string" || Array.isArray(value)) return value.length;
1002
+ if (types.isMap(value) || types.isSet(value)) return /** @type {Set<unknown>} */ (value).size;
1003
+ return typeof value === "object" ? Object.keys(value).length : 0;
1004
+ }
1005
+
1006
+ /**
1007
+ * Is there nothing in it? No own keys for an object, no entries for a Map
1008
+ * or a Set. `null` and `undefined` count as empty; a date, a regular
1009
+ * expression and an error never do.
1010
+ *
1011
+ * @example
1012
+ * obj.isEmpty({}); // true
1013
+ * obj.isEmpty(new Map([["a", 1]])); // false
1014
+ *
1015
+ * @param {unknown} object
1016
+ * @returns {boolean}
1017
+ */
1018
+ function isEmpty(object) {
1019
+ if (object === null || object === undefined) return true;
1020
+ if (types.isMap(object) || types.isSet(object)) return /** @type {Set<unknown>} */ (object).size === 0;
1021
+ if (types.isDate(object) || types.isRegExp(object) || types.isNativeError(object)) return false;
1022
+ return Object.keys(object).length === 0;
1023
+ }
1024
+
1025
+ /**
1026
+ * Freezes an object and everything inside it. Good for configuration and
1027
+ * constants.
1028
+ *
1029
+ * @example
1030
+ * const CONFIG = obj.deepFreeze({ db: { host: "localhost" } });
1031
+ * CONFIG.db.host = "x"; // TypeScript error, and ignored at runtime
1032
+ *
1033
+ * @template T
1034
+ * @param {T} object
1035
+ * @returns {DeepReadonly<T>} The same object.
1036
+ */
1037
+ function deepFreeze(object) {
1038
+ if (object === null || (typeof object !== "object" && typeof object !== "function") || Object.isFrozen(object)) return /** @type {DeepReadonly<T>} */ (object);
1039
+ // Typed arrays and buffers can't be frozen: their memory stays writable.
1040
+ if (ArrayBuffer.isView(object)) return /** @type {DeepReadonly<T>} */ (object);
1041
+ Object.freeze(object);
1042
+ for (const key of Reflect.ownKeys(/** @type {object} */ (object))) deepFreeze(/** @type {any} */ (object)[key]);
1043
+ return /** @type {DeepReadonly<T>} */ (object);
1044
+ }
1045
+
1046
+ /**
1047
+ * A copy with its keys in order, nested objects included. Two objects with
1048
+ * the same content then give the same JSON, which makes stable hashes,
1049
+ * cache keys and readable diffs.
1050
+ *
1051
+ * @example
1052
+ * obj.sortKeys({ b: 1, a: { d: 1, c: 2 } }); // { a: { c: 2, d: 1 }, b: 1 }
1053
+ * nc.crypto.hash(JSON.stringify(obj.sortKeys(payload)));
1054
+ *
1055
+ * @template T
1056
+ * @param {T} object
1057
+ * @param {SortKeysOptions} [options]
1058
+ * @returns {T}
1059
+ */
1060
+ function sortKeys(object, options = {}) {
1061
+ const deep = options.deep ?? true;
1062
+ /** @type {object[]} */
1063
+ const ancestors = [];
1064
+ /** @type {(value: any) => any} */
1065
+ const walk = (value) => {
1066
+ if (value === null || typeof value !== "object" || ancestors.includes(value)) return value;
1067
+ const isArray = Array.isArray(value);
1068
+ if (!isArray && !_isPlain(value)) return value;
1069
+ ancestors.push(value);
1070
+ /** @type {any} */
1071
+ let out;
1072
+ if (isArray) out = deep ? value.map(walk) : value;
1073
+ else {
1074
+ out = {};
1075
+ for (const key of Object.keys(value).sort(options.compare)) {
1076
+ if (!_UNSAFE.has(key)) out[key] = deep ? walk(value[key]) : value[key];
373
1077
  }
374
1078
  }
375
- return result;
376
- },
377
-
378
- /**
379
- * Rebuilds a nested object from a flat, dot-path keyed object.
380
- *
381
- * @example
382
- * nodeComfort.obj.unflatten({ "a.b.c": 1 }); // { a: { b: { c: 1 } } }
383
- *
384
- * @param {Record<string, any>} object - Flat object.
385
- * @returns {Record<string, any>} The nested object.
386
- */
387
- unflatten(object) {
388
- let result = {};
389
- for (const [path, value] of Object.entries(object ?? {})) {
390
- result = this.set(result, path, value);
391
- }
392
- return result;
393
- },
394
-
395
- /**
396
- * Checks whether an object has no own enumerable keys.
397
- *
398
- * @param {Record<string, any>} object - Source object.
399
- * @returns {boolean} True if the object is empty.
400
- */
401
- isEmpty(object) {
402
- return object == null || Object.keys(object).length === 0;
403
- },
404
-
405
- /**
406
- * Freezes an object deeply, making it (and all nested objects) immutable.
407
- *
408
- * @param {Record<string, any>} object - Object to freeze.
409
- * @returns {Record<string, any>} The same, now deeply-frozen, object.
410
- */
411
- deepFreeze(object) {
412
- if (object == null || typeof object !== "object" || Object.isFrozen(object)) {
413
- return object;
414
- }
415
- Object.freeze(object);
416
- for (const key of Object.keys(object)) {
417
- this.deepFreeze(object[key]);
418
- }
419
- return object;
420
- },
1079
+ ancestors.pop();
1080
+ return out;
1081
+ };
1082
+ return walk(object);
1083
+ }
1084
+
1085
+ /** @type {MergeOptions} */
1086
+ const _NO_OPTIONS = {};
1087
+
1088
+ module.exports = {
1089
+ clone,
1090
+ merge,
1091
+ mergeWith,
1092
+ defaults,
1093
+ equal,
1094
+ diff,
1095
+ // not shorthand: Node's ES module export detection stops at a bare `get`
1096
+ get: get,
1097
+ set,
1098
+ update,
1099
+ unset,
1100
+ has,
1101
+ pick,
1102
+ omit,
1103
+ filter,
1104
+ mapValues,
1105
+ mapKeys,
1106
+ renameKeys,
1107
+ invert,
1108
+ compact,
1109
+ flatten,
1110
+ unflatten,
1111
+ paths,
1112
+ sortKeys,
1113
+ keys,
1114
+ entries,
1115
+ fromEntries,
1116
+ size,
1117
+ isEmpty,
1118
+ deepFreeze,
421
1119
  };