@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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- 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
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
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
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @
|
|
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
|
|
18
|
-
if (Array.isArray(path)) return path;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @
|
|
30
|
-
* @
|
|
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
|
-
|
|
37
|
-
if (
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
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
|
|
537
|
+
return;
|
|
152
538
|
}
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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]
|
|
209
|
-
current[key] =
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
};
|