@ix-xs/node-comfort 1.1.31 → 2.0.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 (77) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -748
  4. package/index.js +84 -13
  5. package/package.json +201 -21
  6. package/src/Arr.js +901 -0
  7. package/src/Async.js +585 -0
  8. package/src/Cache.js +329 -0
  9. package/src/Checker.js +979 -188
  10. package/src/Cli.js +1034 -0
  11. package/src/Color.js +415 -0
  12. package/src/Crypto.js +707 -0
  13. package/src/Emitter.js +289 -0
  14. package/src/Env.js +555 -0
  15. package/src/FS.js +1483 -768
  16. package/src/Func.js +692 -0
  17. package/src/Http.js +538 -0
  18. package/src/Id.js +418 -0
  19. package/src/Logger.js +739 -307
  20. package/src/Num.js +849 -0
  21. package/src/Obj.js +904 -0
  22. package/src/SQLite.js +1308 -994
  23. package/src/Schema.js +1586 -0
  24. package/src/Str.js +999 -0
  25. package/src/Sys.js +511 -0
  26. package/src/Time.js +1323 -0
  27. package/src/Utils.js +321 -325
  28. package/src/errors.js +292 -0
  29. package/types/index.d.ts +693 -120
  30. package/types/src/Arr.d.ts +562 -0
  31. package/types/src/Arr.d.ts.map +1 -0
  32. package/types/src/Async.d.ts +321 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +195 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +609 -81
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +582 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +294 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +534 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +170 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +382 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +709 -201
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +455 -0
  51. package/types/src/Func.d.ts.map +1 -0
  52. package/types/src/Http.d.ts +374 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +200 -0
  55. package/types/src/Id.d.ts.map +1 -0
  56. package/types/src/Logger.d.ts +374 -24
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +578 -0
  59. package/types/src/Num.d.ts.map +1 -0
  60. package/types/src/Obj.d.ts +475 -0
  61. package/types/src/Obj.d.ts.map +1 -0
  62. package/types/src/SQLite.d.ts +647 -305
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +940 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +696 -0
  67. package/types/src/Str.d.ts.map +1 -0
  68. package/types/src/Sys.d.ts +269 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +664 -0
  71. package/types/src/Time.d.ts.map +1 -0
  72. package/types/src/Utils.d.ts +154 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +292 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/tsconfig.json +0 -18
  77. package/types/index.d.ts.map +0 -1
package/src/Obj.js ADDED
@@ -0,0 +1,904 @@
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
+
17
+ /**
18
+ * Values that end a path: dates, regexes, maps, sets, functions and arrays.
19
+ * @typedef {Date | RegExp | Map<any, any> | Set<any> | ((...args: any[]) => any) | readonly any[]} PathLeaf
20
+ */
21
+
22
+ /**
23
+ * Every dotted path of an object type, up to 6 levels. This is what powers
24
+ * path completion in `obj.get()` and friends.
25
+ *
26
+ * @example
27
+ * // Paths<{ a: { b: number }, c: string }> is "a" | "a.b" | "c"
28
+ *
29
+ * @template T
30
+ * @template {unknown[]} [Depth=[]]
31
+ * @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
32
+ */
33
+
34
+ /**
35
+ * @template T
36
+ * @typedef {[Extract<T, null | undefined>] extends [never] ? never : undefined} NullishToUndefined
37
+ */
38
+
39
+ /**
40
+ * The type found at a dotted path of `T`, or `any` for an unknown path.
41
+ *
42
+ * @template T
43
+ * @template {string} P
44
+ * @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
45
+ */
46
+
47
+ /**
48
+ * @template T
49
+ * @template {string} K
50
+ * @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
51
+ */
52
+
53
+ /**
54
+ * A path: a known dotted path (completed by your editor), any other string
55
+ * like `"items[0].name"`, or an array of keys.
56
+ *
57
+ * @template T
58
+ * @typedef {Paths<T> | (string & {}) | ReadonlyArray<string | number>} PathInput
59
+ */
60
+
61
+ /**
62
+ * `T`, read-only all the way down.
63
+ * @template T
64
+ * @typedef {T extends (...args: any[]) => any ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T} DeepReadonly
65
+ */
66
+
67
+ /**
68
+ * One difference reported by `diff()`.
69
+ * @typedef {object} Change
70
+ * @property {Array<string | number>} path Where it changed, like `["address", "city"]`.
71
+ * @property {"added" | "removed" | "changed"} type
72
+ * @property {unknown} [from] The old value.
73
+ * @property {unknown} [to] The new value.
74
+ */
75
+
76
+ /**
77
+ * Options for `mergeWith()`.
78
+ * @typedef {object} MergeOptions
79
+ * @property {"replace" | "concat" | "unique"} [arrays] When both sides have an array: keep the last, join them, or join without duplicates. Defaults to `"replace"`.
80
+ * @property {boolean} [skipUndefined] Don't let `undefined` overwrite an existing value.
81
+ */
82
+
83
+ /**
84
+ * Options for `compact()`.
85
+ * @typedef {object} CompactOptions
86
+ * @property {boolean} [deep] Clean nested objects and arrays too.
87
+ * @property {boolean} [removeEmpty] Also remove `""`, `[]` and `{}`.
88
+ */
89
+
90
+ const _UNSAFE = new Set(["__proto__", "constructor", "prototype"]);
91
+
92
+ /** Marks an unknown path segment in `PathStep`. */
93
+ const _NOT_FOUND = Symbol("not-found");
94
+
95
+ /** @param {unknown} value */
96
+ const _isPlain = (value) => {
97
+ if (typeof value !== "object" || value === null) return false;
98
+ const proto = Object.getPrototypeOf(value);
99
+ // The last check accepts objects from another realm (a worker, a `vm`).
100
+ return proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null;
101
+ };
102
+
103
+ /**
104
+ * Parses `"a.b[0].c"` or `["a", "b", 0, "c"]` into keys.
105
+ * @param {unknown} path
106
+ * @returns {Array<string | number>}
107
+ */
108
+ const _toPath = (path) => {
109
+ if (Array.isArray(path)) return path;
110
+ return String(path)
111
+ .replace(/\[(\d+)\]/g, ".$1")
112
+ .replace(/\[["']?([^\]"']+)["']?\]/g, ".$1")
113
+ .split(".")
114
+ .filter((part) => part !== "");
115
+ };
116
+
117
+ /** @param {Array<string | number>} keys */
118
+ const _isSafePath = (keys) => keys.every((key) => !_UNSAFE.has(String(key)));
119
+
120
+ /**
121
+ * Deep copy for what `structuredClone` can't handle (functions, class instances).
122
+ * @param {any} value
123
+ * @param {WeakMap<object, any>} seen
124
+ * @returns {any}
125
+ */
126
+ const _deepClone = (value, seen) => {
127
+ if (value === null || typeof value !== "object") return value;
128
+ if (seen.has(value)) return seen.get(value);
129
+ if (types.isDate(value)) return new Date(value.getTime());
130
+ if (types.isRegExp(value)) return new RegExp(value.source, value.flags);
131
+ if (Array.isArray(value)) {
132
+ const out = /** @type {any[]} */ ([]);
133
+ seen.set(value, out);
134
+ for (const item of value) out.push(_deepClone(item, seen));
135
+ return out;
136
+ }
137
+ if (types.isMap(value)) {
138
+ const out = new Map();
139
+ seen.set(value, out);
140
+ for (const [k, v] of value) out.set(_deepClone(k, seen), _deepClone(v, seen));
141
+ return out;
142
+ }
143
+ if (types.isSet(value)) {
144
+ const out = new Set();
145
+ seen.set(value, out);
146
+ for (const v of value) out.add(_deepClone(v, seen));
147
+ return out;
148
+ }
149
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
150
+ return /** @type {any} */ (value).slice();
151
+ }
152
+ const out = Object.create(Object.getPrototypeOf(value));
153
+ seen.set(value, out);
154
+ for (const key of Reflect.ownKeys(value)) {
155
+ if (typeof key === "string" && _UNSAFE.has(key)) continue;
156
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, key));
157
+ if ("value" in descriptor) descriptor.value = _deepClone(descriptor.value, seen);
158
+ Object.defineProperty(out, key, descriptor);
159
+ }
160
+ return out;
161
+ };
162
+
163
+ /**
164
+ * @param {any} target
165
+ * @param {any} source
166
+ * @param {MergeOptions} options
167
+ * @returns {any}
168
+ */
169
+ const _mergeTwo = (target, source, options) => {
170
+ if (Array.isArray(target) && Array.isArray(source)) {
171
+ if (options.arrays === "concat") return [...target, ...source];
172
+ if (options.arrays === "unique") return [...new Set([...target, ...source])];
173
+ return [...source];
174
+ }
175
+ if (!_isPlain(source)) return source;
176
+ const out = /** @type {Record<string, any>} */ (_isPlain(target) ? { ...target } : {});
177
+ for (const key of Object.keys(source)) {
178
+ if (_UNSAFE.has(key)) continue;
179
+ const value = source[key];
180
+ if (value === undefined && options.skipUndefined) continue;
181
+ const current = out[key];
182
+ out[key] = (_isPlain(value) && _isPlain(current)) || (Array.isArray(value) && Array.isArray(current))
183
+ ? _mergeTwo(current, value, options)
184
+ : _isPlain(value)
185
+ ? _mergeTwo({}, value, options)
186
+ : value;
187
+ }
188
+ return out;
189
+ };
190
+
191
+ /**
192
+ * Deep copy. Uses `structuredClone` when it can, and falls back to a copy
193
+ * that keeps functions and class prototypes.
194
+ *
195
+ * @example
196
+ * const copy = obj.clone({ a: { b: [1, 2] }, when: new Date() });
197
+ * copy.a.b.push(3); // the original is untouched
198
+ *
199
+ * @template T
200
+ * @param {T} value
201
+ * @returns {T}
202
+ */
203
+ function clone(value) {
204
+ if (value === null || typeof value !== "object") return value;
205
+ if (_isPlain(value) || Array.isArray(value)) {
206
+ try {
207
+ return structuredClone(value);
208
+ } catch {
209
+ // Functions or exotic values inside: fall through to the manual copy.
210
+ }
211
+ }
212
+ return _deepClone(value, new WeakMap());
213
+ }
214
+
215
+ /**
216
+ * Deep-merges objects into a new one. Later objects win, nested objects are
217
+ * merged, arrays are replaced. See `mergeWith()` to join arrays instead.
218
+ *
219
+ * @example
220
+ * obj.merge({ db: { host: "localhost", port: 5432 } }, { db: { port: 6543 } });
221
+ * // { db: { host: "localhost", port: 6543 } }
222
+ *
223
+ * @template {object} A
224
+ * @overload
225
+ * @param {A} a
226
+ * @returns {A}
227
+ */
228
+ /**
229
+ * Deep-merges `b` into a copy of `a`.
230
+ *
231
+ * @template {object} A
232
+ * @template {object} B
233
+ * @overload
234
+ * @param {A} a
235
+ * @param {B} b
236
+ * @returns {A & B}
237
+ */
238
+ /**
239
+ * Deep-merges three objects; the last one wins.
240
+ *
241
+ * @template {object} A
242
+ * @template {object} B
243
+ * @template {object} C
244
+ * @overload
245
+ * @param {A} a
246
+ * @param {B} b
247
+ * @param {C} c
248
+ * @returns {A & B & C}
249
+ */
250
+ /**
251
+ * Deep-merges any number of objects; the last one wins.
252
+ *
253
+ * @overload
254
+ * @param {...object} sources
255
+ * @returns {Record<string, any>}
256
+ */
257
+ /**
258
+ * @param {...object} sources
259
+ * @returns {Record<string, any>}
260
+ */
261
+ function merge(...sources) {
262
+ return sources.reduce((acc, source) => (_isPlain(source) ? _mergeTwo(acc, source, {}) : acc), /** @type {Record<string, any>} */ ({}));
263
+ }
264
+
265
+ /**
266
+ * Like `merge()`, with a choice of what happens to arrays and `undefined`.
267
+ *
268
+ * @example
269
+ * obj.mergeWith({ arrays: "unique" }, { tags: ["a", "b"] }, { tags: ["b", "c"] });
270
+ * // { tags: ["a", "b", "c"] }
271
+ * obj.mergeWith({ skipUndefined: true }, { port: 80 }, { port: undefined });
272
+ * // { port: 80 }
273
+ *
274
+ * @param {MergeOptions} options
275
+ * @param {...object} sources
276
+ * @returns {Record<string, any>}
277
+ */
278
+ function mergeWith(options, ...sources) {
279
+ return sources.reduce((acc, source) => (_isPlain(source) ? _mergeTwo(acc, source, options) : acc), /** @type {Record<string, any>} */ ({}));
280
+ }
281
+
282
+ /**
283
+ * Fills in missing properties from defaults, deeply. Values that are set,
284
+ * even `null` or `0`, are kept.
285
+ *
286
+ * @example
287
+ * obj.defaults({ port: 8080, db: { user: "me" } }, { port: 80, db: { user: "root", pass: "" } });
288
+ * // { port: 8080, db: { user: "me", pass: "" } }
289
+ *
290
+ * @template {object} T
291
+ * @template {object} D
292
+ * @param {T} target
293
+ * @param {...D} sources The first one has the highest priority.
294
+ * @returns {T & D}
295
+ */
296
+ function defaults(target, ...sources) {
297
+ /** @type {(t: any, s: any) => any} */
298
+ const fill = (t, s) => {
299
+ const out = _isPlain(t) ? { ...t } : {};
300
+ for (const key of Object.keys(s)) {
301
+ if (_UNSAFE.has(key)) continue;
302
+ if (out[key] === undefined) out[key] = clone(s[key]);
303
+ else if (_isPlain(out[key]) && _isPlain(s[key])) out[key] = fill(out[key], s[key]);
304
+ }
305
+ return out;
306
+ };
307
+ return sources.reduce((acc, source) => (_isPlain(source) ? fill(acc, source) : acc), /** @type {any} */ (clone(target)));
308
+ }
309
+
310
+ /**
311
+ * Deep equality. Knows about arrays, dates, regexes, maps, sets, typed
312
+ * arrays, `NaN` and circular references.
313
+ *
314
+ * @example
315
+ * obj.equal({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }); // true
316
+ * obj.equal(new Set([1, 2]), new Set([2, 1])); // true
317
+ * obj.equal({ a: 1 }, { a: 1, b: undefined }); // false
318
+ *
319
+ * @param {unknown} a
320
+ * @param {unknown} b
321
+ * @returns {boolean}
322
+ */
323
+ function equal(a, b) {
324
+ return _equal(a, b, new WeakMap());
325
+ }
326
+
327
+ /**
328
+ * Same kind of built-in value, even when they come from different realms
329
+ * (a worker, a `vm` context) where prototypes are not shared.
330
+ * @param {object} a
331
+ * @param {object} b
332
+ * @returns {boolean}
333
+ */
334
+ const _sameBuiltIn = (a, b) => {
335
+ const tag = Object.prototype.toString.call(a);
336
+ if (tag !== Object.prototype.toString.call(b)) return false;
337
+ return tag !== "[object Object]" || (Object.getPrototypeOf(Object.getPrototypeOf(a) ?? {}) === null && Object.getPrototypeOf(Object.getPrototypeOf(b) ?? {}) === null);
338
+ };
339
+
340
+ /**
341
+ * @param {any} a
342
+ * @param {any} b
343
+ * @param {WeakMap<object, object>} seen
344
+ * @returns {boolean}
345
+ */
346
+ function _equal(a, b, seen) {
347
+ if (Object.is(a, b)) return true;
348
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
349
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b) && !_sameBuiltIn(a, b)) return false;
350
+ if (seen.get(a) === b) return true;
351
+ seen.set(a, b);
352
+
353
+ if (types.isDate(a)) return a.getTime() === b.getTime();
354
+ if (types.isRegExp(a)) return a.source === b.source && a.flags === b.flags;
355
+ if (Array.isArray(a)) return a.length === b.length && a.every((item, i) => _equal(item, b[i], seen));
356
+ if (ArrayBuffer.isView(a) && !(a instanceof DataView)) {
357
+ const x = /** @type {any} */ (a);
358
+ return x.length === b.length && x.every((/** @type {unknown} */ v, /** @type {number} */ i) => Object.is(v, b[i]));
359
+ }
360
+ if (types.isMap(a)) {
361
+ if (a.size !== b.size) return false;
362
+ outer: for (const [key, value] of a) {
363
+ if (b.has(key)) {
364
+ if (!_equal(value, b.get(key), seen)) return false;
365
+ continue;
366
+ }
367
+ for (const [otherKey, otherValue] of b) if (_equal(key, otherKey, seen) && _equal(value, otherValue, seen)) continue outer;
368
+ return false;
369
+ }
370
+ return true;
371
+ }
372
+ if (types.isSet(a)) {
373
+ if (a.size !== b.size) return false;
374
+ outer: for (const value of a) {
375
+ if (b.has(value)) continue;
376
+ for (const other of b) if (_equal(value, other, seen)) continue outer;
377
+ return false;
378
+ }
379
+ return true;
380
+ }
381
+ const keysA = Object.keys(a);
382
+ const keysB = Object.keys(b);
383
+ if (keysA.length !== keysB.length) return false;
384
+ return keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) && _equal(a[key], b[key], seen));
385
+ }
386
+
387
+ /**
388
+ * Lists what changed between two values, deeply. Handy for audit logs and
389
+ * partial updates.
390
+ *
391
+ * @example
392
+ * obj.diff({ name: "Ada", tags: ["a"] }, { name: "Ada L.", tags: ["a", "b"] });
393
+ * // [
394
+ * // { path: ["name"], type: "changed", from: "Ada", to: "Ada L." },
395
+ * // { path: ["tags", 1], type: "added", to: "b" },
396
+ * // ]
397
+ *
398
+ * @param {unknown} before
399
+ * @param {unknown} after
400
+ * @returns {Change[]} Empty when nothing changed.
401
+ */
402
+ function diff(before, after) {
403
+ /** @type {Change[]} */
404
+ const changes = [];
405
+ /** @type {(a: any, b: any, path: Array<string | number>) => void} */
406
+ const walk = (a, b, path) => {
407
+ if (equal(a, b)) return;
408
+ const bothArrays = Array.isArray(a) && Array.isArray(b);
409
+ if (bothArrays || (_isPlain(a) && _isPlain(b))) {
410
+ const keys = bothArrays
411
+ ? Array.from({ length: Math.max(a.length, b.length) }, (_, i) => i)
412
+ : [...new Set([...Object.keys(a), ...Object.keys(b)])];
413
+ for (const key of keys) {
414
+ const inA = bothArrays ? /** @type {number} */ (key) < a.length : Object.prototype.hasOwnProperty.call(a, key);
415
+ const inB = bothArrays ? /** @type {number} */ (key) < b.length : Object.prototype.hasOwnProperty.call(b, key);
416
+ if (inA && !inB) changes.push({ path: [...path, key], type: "removed", from: a[key] });
417
+ else if (!inA && inB) changes.push({ path: [...path, key], type: "added", to: b[key] });
418
+ else walk(a[key], b[key], [...path, key]);
419
+ }
420
+ return;
421
+ }
422
+ changes.push({ path, type: "changed", from: a, to: b });
423
+ };
424
+ walk(before, after, []);
425
+ return changes;
426
+ }
427
+
428
+ /**
429
+ * Reads a nested value from a dotted path. Your editor completes the paths
430
+ * and knows the type of the result.
431
+ *
432
+ * @example
433
+ * obj.get(config, "db.pool.max"); // number | undefined
434
+ * obj.get(config, "db.pool.max", 10); // number
435
+ * obj.get(data, "users[0].name");
436
+ * obj.get(data, ["users", 0, "name"]);
437
+ *
438
+ * @template T
439
+ * @template {string} P
440
+ * @overload
441
+ * @param {T} object
442
+ * @param {P | Paths<T> | ReadonlyArray<string | number>} path Like `"a.b.c"`, `"a[0].b"` or `["a", 0, "b"]`.
443
+ * @returns {PathValue<T, P>}
444
+ */
445
+ /**
446
+ * Reads a nested value from a dotted path, or returns `fallback` if there's
447
+ * nothing there.
448
+ *
449
+ * @template T
450
+ * @template {string} P
451
+ * @template F
452
+ * @overload
453
+ * @param {T} object
454
+ * @param {P | Paths<T> | ReadonlyArray<string | number>} path Like `"a.b.c"`, `"a[0].b"` or `["a", 0, "b"]`.
455
+ * @param {F} fallback
456
+ * @returns {Exclude<PathValue<T, P>, undefined> | F}
457
+ */
458
+ /**
459
+ * @param {unknown} object
460
+ * @param {string | ReadonlyArray<string | number>} path
461
+ * @param {unknown} [fallback]
462
+ * @returns {any}
463
+ */
464
+ function get(object, path, fallback) {
465
+ const keys = _toPath(path);
466
+ if (!_isSafePath(keys)) return fallback;
467
+ /** @type {any} */
468
+ let current = object;
469
+ for (const key of keys) {
470
+ if (current === null || current === undefined) return fallback;
471
+ current = current[key];
472
+ }
473
+ return current === undefined ? fallback : current;
474
+ }
475
+
476
+ /**
477
+ * Returns a copy with a nested value set. Missing levels are created
478
+ * (arrays when the next key is a number).
479
+ *
480
+ * @example
481
+ * obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } }
482
+ * obj.set(state, "user.name", "Ada"); // state is untouched
483
+ *
484
+ * @template T
485
+ * @param {T} object
486
+ * @param {PathInput<T>} path
487
+ * @param {unknown} value
488
+ * @returns {T}
489
+ */
490
+ function set(object, path, value) {
491
+ const keys = _toPath(path);
492
+ /** @type {any} */
493
+ const root = object === null || typeof object !== "object" ? {} : clone(object);
494
+ if (!keys.length || !_isSafePath(keys)) return root;
495
+ let current = root;
496
+ for (let i = 0; i < keys.length - 1; i++) {
497
+ const key = keys[i];
498
+ const next = keys[i + 1];
499
+ if (current[key] === null || typeof current[key] !== "object") {
500
+ current[key] = typeof next === "number" || /^\d+$/.test(String(next)) ? [] : {};
501
+ }
502
+ current = current[key];
503
+ }
504
+ current[keys[keys.length - 1]] = value;
505
+ return root;
506
+ }
507
+
508
+ /**
509
+ * Returns a copy without the property at `path`.
510
+ *
511
+ * @example
512
+ * obj.unset({ a: { b: 1, c: 2 } }, "a.b"); // { a: { c: 2 } }
513
+ *
514
+ * @template T
515
+ * @param {T} object
516
+ * @param {PathInput<T>} path
517
+ * @returns {T}
518
+ */
519
+ function unset(object, path) {
520
+ const keys = _toPath(path);
521
+ /** @type {any} */
522
+ const root = clone(object);
523
+ if (!keys.length || !_isSafePath(keys) || root === null || typeof root !== "object") return root;
524
+ let current = root;
525
+ for (let i = 0; i < keys.length - 1; i++) {
526
+ current = current?.[keys[i]];
527
+ if (current === null || typeof current !== "object") return root;
528
+ }
529
+ const lastKey = keys[keys.length - 1];
530
+ if (Array.isArray(current) && typeof lastKey === "number") current.splice(lastKey, 1);
531
+ else delete current[lastKey];
532
+ return root;
533
+ }
534
+
535
+ /**
536
+ * Does the path exist? A property set to `undefined` still counts.
537
+ *
538
+ * @example
539
+ * obj.has({ a: { b: undefined } }, "a.b"); // true
540
+ * obj.has({ a: {} }, "a.b"); // false
541
+ *
542
+ * @template T
543
+ * @param {T} object
544
+ * @param {PathInput<T>} path
545
+ * @returns {boolean}
546
+ */
547
+ function has(object, path) {
548
+ const keys = _toPath(path);
549
+ if (!keys.length || !_isSafePath(keys)) return false;
550
+ /** @type {any} */
551
+ let current = object;
552
+ for (const key of keys) {
553
+ if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) return false;
554
+ current = current[key];
555
+ }
556
+ return true;
557
+ }
558
+
559
+ /**
560
+ * A copy with only the listed keys.
561
+ *
562
+ * @example
563
+ * obj.pick(user, ["id", "email"]);
564
+ *
565
+ * @template {object} T
566
+ * @template {keyof T} K
567
+ * @param {T} object
568
+ * @param {readonly K[]} keys
569
+ * @returns {Pick<T, K>}
570
+ */
571
+ function pick(object, keys) {
572
+ /** @type {any} */
573
+ const out = {};
574
+ for (const key of keys) {
575
+ if (object != null && Object.prototype.hasOwnProperty.call(object, key)) out[key] = object[key];
576
+ }
577
+ return out;
578
+ }
579
+
580
+ /**
581
+ * A copy without the listed keys.
582
+ *
583
+ * @example
584
+ * obj.omit(user, ["password", "token"]);
585
+ *
586
+ * @template {object} T
587
+ * @template {keyof T} K
588
+ * @param {T} object
589
+ * @param {readonly K[]} keys
590
+ * @returns {Omit<T, K>}
591
+ */
592
+ function omit(object, keys) {
593
+ const exclude = new Set(/** @type {readonly PropertyKey[]} */ (keys));
594
+ /** @type {any} */
595
+ const out = {};
596
+ for (const key of Object.keys(object ?? {})) {
597
+ if (!exclude.has(key)) out[key] = /** @type {any} */ (object)[key];
598
+ }
599
+ return out;
600
+ }
601
+
602
+ /**
603
+ * Keeps the entries that pass a test.
604
+ *
605
+ * @example
606
+ * obj.filter({ a: 1, b: 2, c: 3 }, (value) => value > 1); // { b: 2, c: 3 }
607
+ *
608
+ * @template {object} T
609
+ * @param {T} object
610
+ * @param {(value: T[keyof T], key: keyof T & string) => unknown} predicate
611
+ * @returns {Partial<T>}
612
+ */
613
+ function filter(object, predicate) {
614
+ /** @type {any} */
615
+ const out = {};
616
+ for (const [key, value] of Object.entries(object ?? {})) {
617
+ if (predicate(value, /** @type {keyof T & string} */ (key))) out[key] = value;
618
+ }
619
+ return out;
620
+ }
621
+
622
+ /**
623
+ * Transforms every value, keeping the keys.
624
+ *
625
+ * @example
626
+ * obj.mapValues({ a: 1, b: 2 }, (v) => v * 10); // { a: 10, b: 20 }
627
+ *
628
+ * @template {object} T
629
+ * @template R
630
+ * @param {T} object
631
+ * @param {(value: T[keyof T], key: keyof T & string) => R} fn
632
+ * @returns {{ [K in keyof T]: R }}
633
+ */
634
+ function mapValues(object, fn) {
635
+ /** @type {any} */
636
+ const out = {};
637
+ for (const [key, value] of Object.entries(object ?? {})) out[key] = fn(value, /** @type {keyof T & string} */ (key));
638
+ return out;
639
+ }
640
+
641
+ /**
642
+ * Transforms every key, keeping the values.
643
+ *
644
+ * @example
645
+ * obj.mapKeys({ first_name: "Ada" }, (key) => nc.str.camelCase(key)); // { firstName: "Ada" }
646
+ *
647
+ * @template {object} T
648
+ * @param {T} object
649
+ * @param {(key: keyof T & string, value: T[keyof T]) => PropertyKey} fn Returns the new key.
650
+ * @returns {Record<string, T[keyof T]>}
651
+ */
652
+ function mapKeys(object, fn) {
653
+ /** @type {any} */
654
+ const out = {};
655
+ for (const [key, value] of Object.entries(object ?? {})) {
656
+ const next = fn(/** @type {keyof T & string} */ (key), value);
657
+ if (typeof next === "string" && _UNSAFE.has(next)) continue;
658
+ out[next] = value;
659
+ }
660
+ return out;
661
+ }
662
+
663
+ /**
664
+ * Renames some keys and leaves the others alone.
665
+ *
666
+ * @example
667
+ * obj.renameKeys({ _id: 1, name: "Ada" }, { _id: "id" }); // { id: 1, name: "Ada" }
668
+ *
669
+ * @template {object} T
670
+ * @param {T} object
671
+ * @param {Partial<Record<keyof T, string>>} mapping Old name to new name.
672
+ * @returns {Record<string, T[keyof T]>}
673
+ */
674
+ function renameKeys(object, mapping) {
675
+ const map = /** @type {Record<string, string | undefined>} */ (mapping);
676
+ return mapKeys(object, (key) => map[key] ?? key);
677
+ }
678
+
679
+ /**
680
+ * Swaps keys and values.
681
+ *
682
+ * @example
683
+ * obj.invert({ a: "x", b: "y" }); // { x: "a", y: "b" }
684
+ *
685
+ * @param {Record<string, PropertyKey>} object
686
+ * @returns {Record<string, string>}
687
+ */
688
+ function invert(object) {
689
+ /** @type {Record<string, string>} */
690
+ const out = {};
691
+ for (const [key, value] of Object.entries(object ?? {})) {
692
+ const next = String(value);
693
+ if (!_UNSAFE.has(next)) out[next] = key;
694
+ }
695
+ return out;
696
+ }
697
+
698
+ /**
699
+ * Removes `null` and `undefined` values, and optionally empty ones.
700
+ *
701
+ * @example
702
+ * obj.compact({ a: 1, b: null, c: undefined }); // { a: 1 }
703
+ * obj.compact({ a: { b: null, c: "" } }, { deep: true, removeEmpty: true }); // {}
704
+ *
705
+ * @template {object} T
706
+ * @param {T} object
707
+ * @param {CompactOptions} [options]
708
+ * @returns {Partial<T>}
709
+ */
710
+ function compact(object, options = {}) {
711
+ /** @type {(value: any) => boolean} */
712
+ const isEmptyValue = (value) =>
713
+ value === null ||
714
+ value === undefined ||
715
+ (options.removeEmpty === true &&
716
+ (value === "" || (Array.isArray(value) && value.length === 0) || (_isPlain(value) && Object.keys(value).length === 0)));
717
+ /** @type {(value: any) => any} */
718
+ const clean = (value) => {
719
+ if (!options.deep) return value;
720
+ if (Array.isArray(value)) return value.map(clean).filter((v) => !isEmptyValue(v));
721
+ if (_isPlain(value)) return compact(value, options);
722
+ return value;
723
+ };
724
+ /** @type {any} */
725
+ const out = {};
726
+ for (const [key, value] of Object.entries(object ?? {})) {
727
+ const cleaned = clean(value);
728
+ if (!isEmptyValue(cleaned)) out[key] = cleaned;
729
+ }
730
+ return out;
731
+ }
732
+
733
+ /**
734
+ * Flattens nested objects into dotted keys. Arrays stay as values.
735
+ *
736
+ * @example
737
+ * obj.flatten({ db: { host: "x", port: 5432 } }); // { "db.host": "x", "db.port": 5432 }
738
+ *
739
+ * @param {object} object
740
+ * @param {string} [prefix=""] Added in front of every key.
741
+ * @returns {Record<string, unknown>}
742
+ */
743
+ function flatten(object, prefix = "") {
744
+ /** @type {Record<string, unknown>} */
745
+ const out = {};
746
+ for (const [key, value] of Object.entries(object ?? {})) {
747
+ const path = prefix ? `${prefix}.${key}` : key;
748
+ if (_isPlain(value) && Object.keys(/** @type {object} */ (value)).length) Object.assign(out, flatten(/** @type {object} */ (value), path));
749
+ else out[path] = value;
750
+ }
751
+ return out;
752
+ }
753
+
754
+ /**
755
+ * Rebuilds nested objects from dotted keys. The opposite of `flatten()`.
756
+ *
757
+ * @example
758
+ * obj.unflatten({ "db.host": "x", "db.port": 5432 }); // { db: { host: "x", port: 5432 } }
759
+ *
760
+ * @param {Record<string, unknown>} object
761
+ * @returns {Record<string, any>}
762
+ */
763
+ function unflatten(object) {
764
+ /** @type {Record<string, any>} */
765
+ const out = {};
766
+ for (const [path, value] of Object.entries(object ?? {})) {
767
+ const keys = _toPath(path);
768
+ if (!keys.length || !_isSafePath(keys)) continue;
769
+ let current = out;
770
+ for (let i = 0; i < keys.length - 1; i++) {
771
+ const key = keys[i];
772
+ if (current[key] === null || typeof current[key] !== "object") {
773
+ current[key] = /^\d+$/.test(String(keys[i + 1])) ? [] : {};
774
+ }
775
+ current = current[key];
776
+ }
777
+ current[keys[keys.length - 1]] = value;
778
+ }
779
+ return out;
780
+ }
781
+
782
+ /**
783
+ * `Object.keys`, but typed with the object's actual keys.
784
+ *
785
+ * @example
786
+ * for (const key of obj.keys(config)) config[key]; // no type error
787
+ *
788
+ * @template {object} T
789
+ * @param {T} object
790
+ * @returns {Array<keyof T & string>}
791
+ */
792
+ function keys(object) {
793
+ return /** @type {Array<keyof T & string>} */ (Object.keys(object ?? {}));
794
+ }
795
+
796
+ /**
797
+ * `Object.entries`, with typed keys and values.
798
+ *
799
+ * @template {object} T
800
+ * @param {T} object
801
+ * @returns {Array<[keyof T & string, T[keyof T]]>}
802
+ */
803
+ function entries(object) {
804
+ return /** @type {Array<[keyof T & string, T[keyof T]]>} */ (Object.entries(object ?? {}));
805
+ }
806
+
807
+ /**
808
+ * `Object.fromEntries` that accepts any iterable (a `Map` too) and skips
809
+ * unsafe keys.
810
+ *
811
+ * @example
812
+ * obj.fromEntries(new Map([["x", true]])); // { x: true }
813
+ *
814
+ * @template {PropertyKey} K
815
+ * @template V
816
+ * @param {Iterable<readonly [K, V]>} pairs
817
+ * @returns {Record<K, V>}
818
+ */
819
+ function fromEntries(pairs) {
820
+ /** @type {any} */
821
+ const out = {};
822
+ for (const [key, value] of pairs) {
823
+ if (typeof key === "string" && _UNSAFE.has(key)) continue;
824
+ out[key] = value;
825
+ }
826
+ return out;
827
+ }
828
+
829
+ /**
830
+ * How many entries: own keys of an object, size of a map or set, length of
831
+ * an array or string.
832
+ *
833
+ * @example
834
+ * obj.size({ a: 1, b: 2 }); // 2
835
+ *
836
+ * @param {unknown} value
837
+ * @returns {number}
838
+ */
839
+ function size(value) {
840
+ if (value === null || value === undefined) return 0;
841
+ if (typeof value === "string" || Array.isArray(value)) return value.length;
842
+ if (types.isMap(value) || types.isSet(value)) return /** @type {Set<unknown>} */ (value).size;
843
+ return typeof value === "object" ? Object.keys(value).length : 0;
844
+ }
845
+
846
+ /**
847
+ * Does the object have no own keys? `null` and `undefined` count as empty.
848
+ *
849
+ * @param {unknown} object
850
+ * @returns {boolean}
851
+ */
852
+ function isEmpty(object) {
853
+ return object === null || object === undefined || Object.keys(object).length === 0;
854
+ }
855
+
856
+ /**
857
+ * Freezes an object and everything inside it. Good for configuration and
858
+ * constants.
859
+ *
860
+ * @example
861
+ * const CONFIG = obj.deepFreeze({ db: { host: "localhost" } });
862
+ * CONFIG.db.host = "x"; // TypeScript error, and ignored at runtime
863
+ *
864
+ * @template T
865
+ * @param {T} object
866
+ * @returns {DeepReadonly<T>} The same object.
867
+ */
868
+ function deepFreeze(object) {
869
+ if (object !== null && (typeof object === "object" || typeof object === "function") && !Object.isFrozen(object)) {
870
+ Object.freeze(object);
871
+ for (const key of Reflect.ownKeys(/** @type {object} */ (object))) deepFreeze(/** @type {any} */ (object)[key]);
872
+ }
873
+ return /** @type {DeepReadonly<T>} */ (object);
874
+ }
875
+
876
+ module.exports = {
877
+ clone,
878
+ merge,
879
+ mergeWith,
880
+ defaults,
881
+ equal,
882
+ diff,
883
+ // not shorthand: Node's ES module export detection stops at a bare `get`
884
+ get: get,
885
+ set,
886
+ unset,
887
+ has,
888
+ pick,
889
+ omit,
890
+ filter,
891
+ mapValues,
892
+ mapKeys,
893
+ renameKeys,
894
+ invert,
895
+ compact,
896
+ flatten,
897
+ unflatten,
898
+ keys,
899
+ entries,
900
+ fromEntries,
901
+ size,
902
+ isEmpty,
903
+ deepFreeze,
904
+ };