objwalk 1.6.0 → 1.8.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/README.md +137 -369
- package/SPEC.md +442 -0
- package/dist/index.cjs +113 -106
- package/dist/index.d.cts +21 -6
- package/dist/index.d.ts +21 -6
- package/dist/index.js +113 -106
- package/package.json +3 -2
- package/skills/objwalk/SKILL.md +50 -9
package/dist/index.js
CHANGED
|
@@ -14,20 +14,18 @@ var isWalkable = (value) => {
|
|
|
14
14
|
// src/each.ts
|
|
15
15
|
var each = (data, callback, { root = false, descend } = {}) => {
|
|
16
16
|
const walk = (value, path, segments, key) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
if (isWalkable(value) && (descend === void 0 || descend({ key, value, path, segments: [...segments] }))) {
|
|
17
|
+
const proceed = segments.length > 0 || root ? callback(key, value, path, [...segments]) !== false : true;
|
|
18
|
+
if (proceed && isWalkable(value) && (descend === void 0 || descend({ key, value, path, segments: [...segments] }))) {
|
|
21
19
|
if (Array.isArray(value)) {
|
|
22
|
-
value.
|
|
23
|
-
walk(item, createPath(path, index, true), [...segments, index], index)
|
|
24
|
-
|
|
25
|
-
} else {
|
|
26
|
-
Object.entries(value).forEach(([childKey, item]) => {
|
|
27
|
-
walk(item, createPath(path, childKey, false), [...segments, childKey], childKey);
|
|
28
|
-
});
|
|
20
|
+
return value.every(
|
|
21
|
+
(item, index) => walk(item, createPath(path, index, true), [...segments, index], index)
|
|
22
|
+
);
|
|
29
23
|
}
|
|
24
|
+
return Object.keys(value).every(
|
|
25
|
+
(childKey) => walk(value[childKey], createPath(path, childKey, false), [...segments, childKey], childKey)
|
|
26
|
+
);
|
|
30
27
|
}
|
|
28
|
+
return proceed;
|
|
31
29
|
};
|
|
32
30
|
walk(data, "", [], void 0);
|
|
33
31
|
};
|
|
@@ -176,13 +174,13 @@ var extract = (source, mapping) => {
|
|
|
176
174
|
return;
|
|
177
175
|
}
|
|
178
176
|
if (Array.isArray(sourcePath)) {
|
|
179
|
-
|
|
177
|
+
each(sourcePath, (_key, item) => {
|
|
180
178
|
const value = pick(source, item);
|
|
181
179
|
if (value !== void 0) {
|
|
182
180
|
set(result, destinationPath, value);
|
|
183
|
-
|
|
181
|
+
return false;
|
|
184
182
|
}
|
|
185
|
-
}
|
|
183
|
+
});
|
|
186
184
|
return;
|
|
187
185
|
}
|
|
188
186
|
set(result, destinationPath, pick(source, sourcePath));
|
|
@@ -190,14 +188,6 @@ var extract = (source, mapping) => {
|
|
|
190
188
|
return result;
|
|
191
189
|
};
|
|
192
190
|
|
|
193
|
-
// src/map.ts
|
|
194
|
-
var map = (data, callback, store = {}) => {
|
|
195
|
-
each(data, (key, value, path) => {
|
|
196
|
-
callback(key, value, store, path);
|
|
197
|
-
});
|
|
198
|
-
return store;
|
|
199
|
-
};
|
|
200
|
-
|
|
201
191
|
// src/helpers/isPlainObject.ts
|
|
202
192
|
var isPlainObject = (value) => {
|
|
203
193
|
if (value !== null && typeof value === "object") {
|
|
@@ -207,119 +197,136 @@ var isPlainObject = (value) => {
|
|
|
207
197
|
return false;
|
|
208
198
|
};
|
|
209
199
|
|
|
200
|
+
// src/map.ts
|
|
201
|
+
var map = (data, callback, { descend, shallow = false } = {}) => {
|
|
202
|
+
const walk = (value, key, path, segments) => {
|
|
203
|
+
const root = key === void 0;
|
|
204
|
+
let mapped = root ? Array.isArray(value) ? [] : {} : value;
|
|
205
|
+
if ((root || Array.isArray(value) || isPlainObject(value)) && (root || shallow === false) && (descend === void 0 || descend({ key, value, path, segments: [...segments] }))) {
|
|
206
|
+
mapped = Array.isArray(value) ? Array(value.length) : {};
|
|
207
|
+
each(value, (key2, value2) => {
|
|
208
|
+
const childPath = createPath(path, key2, Array.isArray(mapped));
|
|
209
|
+
const childSegments = [...segments, key2];
|
|
210
|
+
const result = walk(value2, key2, childPath, childSegments);
|
|
211
|
+
Object.defineProperty(mapped, key2, {
|
|
212
|
+
value: result,
|
|
213
|
+
writable: true,
|
|
214
|
+
enumerable: true,
|
|
215
|
+
configurable: true
|
|
216
|
+
});
|
|
217
|
+
}, { descend: ({ segments: segments2 }) => segments2.length === 0 });
|
|
218
|
+
}
|
|
219
|
+
return key !== void 0 ? callback(key, mapped, path) : mapped;
|
|
220
|
+
};
|
|
221
|
+
return walk(data, void 0, "", []);
|
|
222
|
+
};
|
|
223
|
+
|
|
210
224
|
// src/merge.ts
|
|
211
225
|
var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
212
226
|
var merge = (target, ...sources) => {
|
|
213
227
|
const targetRecord = target;
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
targetRecord[key] = sourceValue;
|
|
227
|
-
}
|
|
228
|
+
each(sources, (_index, source) => {
|
|
229
|
+
if (isPlainObject(source)) {
|
|
230
|
+
each(Object.entries(source), (_index2, [key, sourceValue]) => {
|
|
231
|
+
if (unsafeKeys.has(key) === false) {
|
|
232
|
+
const targetValue = targetRecord[key];
|
|
233
|
+
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
|
|
234
|
+
merge(targetValue, sourceValue);
|
|
235
|
+
} else {
|
|
236
|
+
targetRecord[key] = sourceValue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}, { descend: ({ segments }) => segments.length === 0 });
|
|
228
240
|
}
|
|
229
|
-
}
|
|
241
|
+
}, { descend: ({ segments }) => segments.length === 0 });
|
|
230
242
|
return target;
|
|
231
243
|
};
|
|
232
244
|
|
|
233
245
|
// src/flush.ts
|
|
234
246
|
var flush = (data = {}, path) => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
const paths = Array.isArray(path) ? path : [path];
|
|
248
|
+
each(paths, (_key, path2) => {
|
|
249
|
+
if (path2.length > 0) {
|
|
250
|
+
const keys = path2.split(".");
|
|
251
|
+
const lastKey = keys.pop() ?? "";
|
|
252
|
+
const match = lastKey.match(/^(\w+)\[(\d+)\]$/);
|
|
253
|
+
let parent = getValueByPath(data, keys.join("."), { inherited: false });
|
|
254
|
+
let key = lastKey;
|
|
255
|
+
if (match) {
|
|
256
|
+
const [, arrayKey, index] = match;
|
|
257
|
+
const array = getValueByKey(parent, arrayKey, { inherited: false });
|
|
258
|
+
parent = Array.isArray(array) ? array : void 0;
|
|
259
|
+
key = index;
|
|
260
|
+
}
|
|
261
|
+
if (parent !== null && typeof parent === "object" && Object.hasOwn(parent, key)) {
|
|
262
|
+
delete parent[key];
|
|
263
|
+
}
|
|
249
264
|
}
|
|
250
|
-
}
|
|
265
|
+
});
|
|
251
266
|
return data;
|
|
252
267
|
};
|
|
253
268
|
|
|
254
269
|
// src/deepEqual.ts
|
|
255
270
|
var deepEqual = (x, y, store) => {
|
|
256
|
-
if (Number.isNaN(x) && Number.isNaN(y)) {
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
if (x === y) {
|
|
271
|
+
if (x === y || Number.isNaN(x) && Number.isNaN(y)) {
|
|
260
272
|
return true;
|
|
261
273
|
}
|
|
262
|
-
if (x
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
let seen = store.get(x);
|
|
269
|
-
if (seen !== void 0 && seen.has(y)) {
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
if (seen === void 0) {
|
|
273
|
-
seen = /* @__PURE__ */ new WeakSet();
|
|
274
|
-
store.set(x, seen);
|
|
275
|
-
}
|
|
276
|
-
seen.add(y);
|
|
277
|
-
if (x instanceof Date && y instanceof Date) {
|
|
278
|
-
return x.getTime() === y.getTime();
|
|
279
|
-
}
|
|
280
|
-
if (x instanceof RegExp && y instanceof RegExp) {
|
|
281
|
-
return x.toString() === y.toString();
|
|
282
|
-
}
|
|
283
|
-
const isArrayX = Array.isArray(x);
|
|
284
|
-
const isArrayY = Array.isArray(y);
|
|
285
|
-
if (isArrayX !== isArrayY) {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
if (isArrayX) {
|
|
289
|
-
return equalArrays(x, y, store);
|
|
290
|
-
}
|
|
291
|
-
return equalObjects(x, y, store);
|
|
292
|
-
};
|
|
293
|
-
var equalArrays = (x, y, store) => x.length === y.length && x.every((value, index) => deepEqual(value, y[index], store));
|
|
294
|
-
var equalObjects = (x, y, store) => {
|
|
295
|
-
const keysX = Reflect.ownKeys(x);
|
|
296
|
-
const keysY = Reflect.ownKeys(y);
|
|
297
|
-
if (keysX.length !== keysY.length) {
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
return keysX.every((key) => {
|
|
301
|
-
if (!Object.hasOwn(y, key)) {
|
|
302
|
-
return false;
|
|
274
|
+
if (x !== null && y !== null && typeof x === "object" && typeof y === "object") {
|
|
275
|
+
let seen = store.get(x);
|
|
276
|
+
if (seen !== void 0 && seen.has(y)) {
|
|
277
|
+
return true;
|
|
303
278
|
}
|
|
304
|
-
|
|
305
|
-
|
|
279
|
+
if (seen === void 0) {
|
|
280
|
+
seen = /* @__PURE__ */ new WeakSet();
|
|
281
|
+
store.set(x, seen);
|
|
282
|
+
}
|
|
283
|
+
seen.add(y);
|
|
284
|
+
if (x instanceof Date || y instanceof Date) {
|
|
285
|
+
return x instanceof Date && y instanceof Date && x.getTime() === y.getTime();
|
|
286
|
+
}
|
|
287
|
+
if (x instanceof RegExp || y instanceof RegExp) {
|
|
288
|
+
return x instanceof RegExp && y instanceof RegExp && x.toString() === y.toString();
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(x) && Array.isArray(y)) {
|
|
291
|
+
const { length } = x;
|
|
292
|
+
let equal = length === y.length;
|
|
293
|
+
let index = 0;
|
|
294
|
+
while (equal && index < length) {
|
|
295
|
+
equal = deepEqual(x[index], y[index], store);
|
|
296
|
+
index += 1;
|
|
297
|
+
}
|
|
298
|
+
return equal;
|
|
299
|
+
} else if (Array.isArray(x) === false && Array.isArray(y) === false) {
|
|
300
|
+
const keysX = Reflect.ownKeys(x);
|
|
301
|
+
const keysY = Reflect.ownKeys(y);
|
|
302
|
+
return keysX.length === keysY.length && keysX.every(
|
|
303
|
+
(key) => Object.hasOwn(y, key) && deepEqual(x[key], y[key], store)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
306
308
|
};
|
|
307
309
|
|
|
308
310
|
// src/equally.ts
|
|
309
|
-
var equally = (a, b) =>
|
|
311
|
+
var equally = (a, b, paths) => {
|
|
312
|
+
if (paths !== void 0) {
|
|
313
|
+
return paths.every((path) => equally(getValueByPath(a, path), getValueByPath(b, path)));
|
|
314
|
+
}
|
|
315
|
+
return deepEqual(a, b, /* @__PURE__ */ new WeakMap());
|
|
316
|
+
};
|
|
310
317
|
|
|
311
318
|
// src/diff.ts
|
|
312
319
|
var diff = (first, last) => {
|
|
313
320
|
const operations = [];
|
|
314
321
|
const frames = [];
|
|
315
322
|
const finish = (depth) => {
|
|
316
|
-
|
|
317
|
-
|
|
323
|
+
each(frames.splice(depth).reverse(), (_index, { first: first2, last: last2, path }) => {
|
|
324
|
+
each(Object.keys(first2), (_index2, key) => {
|
|
318
325
|
if (Object.prototype.propertyIsEnumerable.call(last2, key) === false) {
|
|
319
326
|
operations.push({ type: "delete", path: [...path, key] });
|
|
320
327
|
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
328
|
+
});
|
|
329
|
+
}, { descend: ({ segments }) => segments.length === 0 });
|
|
323
330
|
return frames.at(-1);
|
|
324
331
|
};
|
|
325
332
|
let descend = false;
|
|
@@ -370,9 +377,9 @@ var clone = (value) => {
|
|
|
370
377
|
}
|
|
371
378
|
const result = {};
|
|
372
379
|
store.set(obj, result);
|
|
373
|
-
|
|
380
|
+
each(Reflect.ownKeys(obj), (_index, key) => {
|
|
374
381
|
result[key] = cloneValue(obj[key]);
|
|
375
|
-
}
|
|
382
|
+
});
|
|
376
383
|
return result;
|
|
377
384
|
};
|
|
378
385
|
return cloneValue(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "objwalk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Small utilities for reading, writing, walking, and reshaping nested JavaScript objects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"object",
|
|
@@ -40,11 +40,12 @@
|
|
|
40
40
|
"files": [
|
|
41
41
|
"dist",
|
|
42
42
|
"README.md",
|
|
43
|
+
"SPEC.md",
|
|
43
44
|
"skills"
|
|
44
45
|
],
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
47
|
-
"test": "vitest run",
|
|
48
|
+
"test": "tsc --project tsconfig.test.json && vitest run",
|
|
48
49
|
"pack:check": "npm pack --dry-run"
|
|
49
50
|
},
|
|
50
51
|
"prepublishOnly": "npm run build",
|
package/skills/objwalk/SKILL.md
CHANGED
|
@@ -28,13 +28,13 @@ import { clone, diff, each, equally, extract, flush, map, merge, parse, pick, se
|
|
|
28
28
|
| --- | --- |
|
|
29
29
|
| Read a nested value | `pick(data, path, options?)`; unresolved paths return `undefined`. |
|
|
30
30
|
| Write a nested value | `set(data, path, value)`; creates intermediate containers, mutates and returns `data`. |
|
|
31
|
-
| Delete
|
|
31
|
+
| Delete nested properties | `flush(data, path)`; accepts one path or `string[]`, deletes own properties, mutates and returns `data`; missing paths are left unchanged. |
|
|
32
32
|
| Reshape an object | `extract(source, mapping)`; mapping keys are destination paths, values are source paths. |
|
|
33
|
-
| Visit nested values | `each(data, (key, value, path, segments) => { ... }, options?)`; root is excluded by default. |
|
|
34
|
-
|
|
|
33
|
+
| Visit nested values | `each(data, (key, value, path, segments) => { ... }, options?)`; supports objects and arrays, root is excluded by default; returning `false` stops the entire traversal. |
|
|
34
|
+
| Transform nested values | `map(data, (key, value, path) => replacement, options?)`; returns a new array or dictionary with the same keys. |
|
|
35
35
|
| Deep merge | `merge(target, ...sources)`; mutates and returns the target, merges plain objects, replaces arrays and non-plain values. |
|
|
36
36
|
| Deep copy | `clone(value)`; preserves `Date`, `RegExp`, symbol keys, and circular references. |
|
|
37
|
-
| Deep comparison | `equally(a, b)`; supports `NaN`, `Date`, `RegExp`, and circular references. |
|
|
37
|
+
| Deep comparison | `equally(a, b, paths?)`; supports `NaN`, `Date`, `RegExp`, and circular references; optional `string[]` compares only selected paths. |
|
|
38
38
|
| Describe changes | `diff(first, last): DiffOperation[]`; returns `set`/`delete` operations, or `[]` when unchanged. |
|
|
39
39
|
|
|
40
40
|
`set`, `flush`, and `merge` mutate their targets. Use `clone` first only when the task
|
|
@@ -53,7 +53,8 @@ do not infer that capability from `clone` or `equally`.
|
|
|
53
53
|
|
|
54
54
|
`[n]` is an `extract` mapping feature, not a general wildcard for `pick`, `set`, or `flush`.
|
|
55
55
|
An array of source paths in `extract` selects the first result that is not
|
|
56
|
-
`undefined`, preserving `null`, `false`, `0`, and empty strings
|
|
56
|
+
`undefined`, preserving `null`, `false`, `0`, and empty strings; later paths are
|
|
57
|
+
not read. Sparse path arrays in `flush` and fallback arrays in `extract` skip holes.
|
|
57
58
|
|
|
58
59
|
Use `flush` to remove a property rather than `set(data, path, undefined)`, which
|
|
59
60
|
leaves the property present. `flush` traverses and deletes only own properties.
|
|
@@ -63,6 +64,18 @@ but deleting an item leaves a hole without shifting indexes or changing length.
|
|
|
63
64
|
Selection syntax (`{name,email}`, `{*}`) is not supported. Errors from getters,
|
|
64
65
|
proxies, or deleting non-configurable properties propagate.
|
|
65
66
|
|
|
67
|
+
`flush` processes a path array in order; `[]` leaves the target unchanged.
|
|
68
|
+
`equally` without `paths` (or with `undefined`) compares whole values. With `paths`,
|
|
69
|
+
every selected value must match; `[]` returns true and `''` selects the whole input.
|
|
70
|
+
It uses dot notation and explicit array indexes, reading inherited properties like
|
|
71
|
+
`pick`; unresolved paths compare as `undefined`, including against an explicit
|
|
72
|
+
`undefined` value. Selection syntax and `[n]` mappings are not supported.
|
|
73
|
+
Getter/proxy errors propagate.
|
|
74
|
+
|
|
75
|
+
`equally` compares `Date` and `RegExp` only with their respective types. Arrays
|
|
76
|
+
compare every index, including holes, which read as `undefined`; holes match
|
|
77
|
+
explicit `undefined` slots but not other values.
|
|
78
|
+
|
|
66
79
|
`pick` reads inherited properties by default. Use its existing options when own
|
|
67
80
|
properties or ignored keys are required:
|
|
68
81
|
|
|
@@ -90,9 +103,36 @@ with key `undefined`, path `''`, and segments `[]`. Callback keys are then typed
|
|
|
90
103
|
`descend` is called only for containers, after their callback, and returning false
|
|
91
104
|
skips their children. It also runs for a root container excluded from callbacks.
|
|
92
105
|
For example, `descend: ({ value }) => Array.isArray(value) === false` visits arrays
|
|
93
|
-
without traversing their elements.
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
without traversing their elements. Returning exactly `false` from the callback stops
|
|
107
|
+
the entire traversal, including siblings and ancestor branches; other return values
|
|
108
|
+
are ignored. The stopped container's `descend` is not called, and its children and
|
|
109
|
+
subsequent values are not read. Object keys are collected on entering a container;
|
|
110
|
+
values are read as visited. `each` still returns `undefined`. Existing callbacks
|
|
111
|
+
that returned `false` must omit that return to keep walking. Mutating received
|
|
112
|
+
segments does not affect traversal. Callback/getter/descend errors
|
|
113
|
+
propagate. Cycles are not detected.
|
|
114
|
+
|
|
115
|
+
`map(data, callback, options?)` returns a new array for an array root and a new
|
|
116
|
+
ordinary dictionary for a dictionary root. Its callback is `(key, value, path)`;
|
|
117
|
+
return the replacement value. There is no store argument. `false`, `null`, and
|
|
118
|
+
`undefined` are ordinary output values, not traversal control. To accumulate a
|
|
119
|
+
different structure, use `each` with an explicit accumulator.
|
|
120
|
+
|
|
121
|
+
Deep map callbacks run children first, then their containing value; the root is
|
|
122
|
+
excluded. Plain objects and arrays are rebuilt, while non-plain objects such as
|
|
123
|
+
Date and RegExp are passed to the callback as leaves. Callback results are not
|
|
124
|
+
traversed again. Source data is not assigned to, but returned or pruned values
|
|
125
|
+
can retain references. Array holes and literal object keys are preserved.
|
|
126
|
+
|
|
127
|
+
`MapOptions` is the third argument. `descend` receives original containers before
|
|
128
|
+
children are mapped; false prunes their children and passes the original value to
|
|
129
|
+
the callback. It also runs for the excluded root, where false produces an empty
|
|
130
|
+
result of the same kind. Literal `shallow: true` visits only immediate children,
|
|
131
|
+
typing dictionary keys as `string` and array keys as `number`. When both options
|
|
132
|
+
are present, `descend` is evaluated only for the root. Deep/dynamic calls keep
|
|
133
|
+
`WalkKey`; a custom descend predicate alone cannot narrow keys.
|
|
134
|
+
`MapCallback<TResult, TKey>` describes the callback's returned value and key type.
|
|
135
|
+
This is a breaking replacement of the old store-based map, not an overload.
|
|
96
136
|
|
|
97
137
|
## Differences
|
|
98
138
|
|
|
@@ -147,5 +187,6 @@ Fallback applies only to exceptions. Preserve valid falsy results and native
|
|
|
147
187
|
`undefined`. Neither fallback nor a throwing `onError` changes stringify's
|
|
148
188
|
return type of `string | undefined`.
|
|
149
189
|
|
|
150
|
-
Full usage: [README](../../README.md).
|
|
190
|
+
Full usage: [README](../../README.md). Edge cases: [specification](../../SPEC.md).
|
|
191
|
+
In a consumer project, read the installed
|
|
151
192
|
package's README and declarations when checking the available version's API.
|