@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.
- package/CHANGELOG.md +190 -0
- package/LICENSE +21 -0
- package/README.md +210 -748
- package/index.js +84 -13
- package/package.json +201 -21
- package/src/Arr.js +901 -0
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +979 -188
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -768
- package/src/Func.js +692 -0
- package/src/Http.js +538 -0
- package/src/Id.js +418 -0
- package/src/Logger.js +739 -307
- package/src/Num.js +849 -0
- package/src/Obj.js +904 -0
- package/src/SQLite.js +1308 -994
- package/src/Schema.js +1586 -0
- package/src/Str.js +999 -0
- package/src/Sys.js +511 -0
- package/src/Time.js +1323 -0
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -120
- package/types/src/Arr.d.ts +562 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +609 -81
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +709 -201
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +455 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +200 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +374 -24
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +578 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +475 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +647 -305
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +696 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +664 -0
- package/types/src/Time.d.ts.map +1 -0
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/tsconfig.json +0 -18
- 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
|
+
};
|