objwalk 1.6.0 → 1.7.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/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
- if (segments.length > 0 || root) {
18
- callback(key, value, path, [...segments]);
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.forEach((item, index) => {
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
- for (const item of sourcePath) {
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
- break;
181
+ return false;
184
182
  }
185
- }
183
+ });
186
184
  return;
187
185
  }
188
186
  set(result, destinationPath, pick(source, sourcePath));
@@ -211,115 +209,108 @@ var isPlainObject = (value) => {
211
209
  var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
212
210
  var merge = (target, ...sources) => {
213
211
  const targetRecord = target;
214
- for (const source of sources) {
215
- if (!isPlainObject(source)) {
216
- continue;
217
- }
218
- for (const [key, sourceValue] of Object.entries(source)) {
219
- if (unsafeKeys.has(key)) {
220
- continue;
221
- }
222
- const targetValue = targetRecord[key];
223
- if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
224
- merge(targetValue, sourceValue);
225
- } else {
226
- targetRecord[key] = sourceValue;
227
- }
212
+ each(sources, (_index, source) => {
213
+ if (isPlainObject(source)) {
214
+ each(Object.entries(source), (_index2, [key, sourceValue]) => {
215
+ if (unsafeKeys.has(key) === false) {
216
+ const targetValue = targetRecord[key];
217
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
218
+ merge(targetValue, sourceValue);
219
+ } else {
220
+ targetRecord[key] = sourceValue;
221
+ }
222
+ }
223
+ }, { descend: ({ segments }) => segments.length === 0 });
228
224
  }
229
- }
225
+ }, { descend: ({ segments }) => segments.length === 0 });
230
226
  return target;
231
227
  };
232
228
 
233
229
  // src/flush.ts
234
230
  var flush = (data = {}, path) => {
235
- if (path.length > 0) {
236
- const keys = path.split(".");
237
- const lastKey = keys.pop() ?? "";
238
- const match = lastKey.match(/^(\w+)\[(\d+)\]$/);
239
- let parent = getValueByPath(data, keys.join("."), { inherited: false });
240
- let key = lastKey;
241
- if (match) {
242
- const [, arrayKey, index] = match;
243
- const array = getValueByKey(parent, arrayKey, { inherited: false });
244
- parent = Array.isArray(array) ? array : void 0;
245
- key = index;
246
- }
247
- if (parent !== null && typeof parent === "object" && Object.hasOwn(parent, key)) {
248
- delete parent[key];
231
+ const paths = Array.isArray(path) ? path : [path];
232
+ each(paths, (_key, path2) => {
233
+ if (path2.length > 0) {
234
+ const keys = path2.split(".");
235
+ const lastKey = keys.pop() ?? "";
236
+ const match = lastKey.match(/^(\w+)\[(\d+)\]$/);
237
+ let parent = getValueByPath(data, keys.join("."), { inherited: false });
238
+ let key = lastKey;
239
+ if (match) {
240
+ const [, arrayKey, index] = match;
241
+ const array = getValueByKey(parent, arrayKey, { inherited: false });
242
+ parent = Array.isArray(array) ? array : void 0;
243
+ key = index;
244
+ }
245
+ if (parent !== null && typeof parent === "object" && Object.hasOwn(parent, key)) {
246
+ delete parent[key];
247
+ }
249
248
  }
250
- }
249
+ });
251
250
  return data;
252
251
  };
253
252
 
254
253
  // src/deepEqual.ts
255
254
  var deepEqual = (x, y, store) => {
256
- if (Number.isNaN(x) && Number.isNaN(y)) {
257
- return true;
258
- }
259
- if (x === y) {
255
+ if (x === y || Number.isNaN(x) && Number.isNaN(y)) {
260
256
  return true;
261
257
  }
262
- if (x == null || y == null) {
263
- return false;
264
- }
265
- if (typeof x !== "object" || typeof y !== "object") {
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;
258
+ if (x !== null && y !== null && typeof x === "object" && typeof y === "object") {
259
+ let seen = store.get(x);
260
+ if (seen !== void 0 && seen.has(y)) {
261
+ return true;
303
262
  }
304
- return deepEqual(x[key], y[key], store);
305
- });
263
+ if (seen === void 0) {
264
+ seen = /* @__PURE__ */ new WeakSet();
265
+ store.set(x, seen);
266
+ }
267
+ seen.add(y);
268
+ if (x instanceof Date || y instanceof Date) {
269
+ return x instanceof Date && y instanceof Date && x.getTime() === y.getTime();
270
+ }
271
+ if (x instanceof RegExp || y instanceof RegExp) {
272
+ return x instanceof RegExp && y instanceof RegExp && x.toString() === y.toString();
273
+ }
274
+ if (Array.isArray(x) && Array.isArray(y)) {
275
+ const { length } = x;
276
+ let equal = length === y.length;
277
+ let index = 0;
278
+ while (equal && index < length) {
279
+ equal = deepEqual(x[index], y[index], store);
280
+ index += 1;
281
+ }
282
+ return equal;
283
+ } else if (Array.isArray(x) === false && Array.isArray(y) === false) {
284
+ const keysX = Reflect.ownKeys(x);
285
+ const keysY = Reflect.ownKeys(y);
286
+ return keysX.length === keysY.length && keysX.every(
287
+ (key) => Object.hasOwn(y, key) && deepEqual(x[key], y[key], store)
288
+ );
289
+ }
290
+ }
291
+ return false;
306
292
  };
307
293
 
308
294
  // src/equally.ts
309
- var equally = (a, b) => deepEqual(a, b, /* @__PURE__ */ new WeakMap());
295
+ var equally = (a, b, paths) => {
296
+ if (paths !== void 0) {
297
+ return paths.every((path) => equally(getValueByPath(a, path), getValueByPath(b, path)));
298
+ }
299
+ return deepEqual(a, b, /* @__PURE__ */ new WeakMap());
300
+ };
310
301
 
311
302
  // src/diff.ts
312
303
  var diff = (first, last) => {
313
304
  const operations = [];
314
305
  const frames = [];
315
306
  const finish = (depth) => {
316
- for (const { first: first2, last: last2, path } of frames.splice(depth).reverse()) {
317
- for (const key of Object.keys(first2)) {
307
+ each(frames.splice(depth).reverse(), (_index, { first: first2, last: last2, path }) => {
308
+ each(Object.keys(first2), (_index2, key) => {
318
309
  if (Object.prototype.propertyIsEnumerable.call(last2, key) === false) {
319
310
  operations.push({ type: "delete", path: [...path, key] });
320
311
  }
321
- }
322
- }
312
+ });
313
+ }, { descend: ({ segments }) => segments.length === 0 });
323
314
  return frames.at(-1);
324
315
  };
325
316
  let descend = false;
@@ -370,9 +361,9 @@ var clone = (value) => {
370
361
  }
371
362
  const result = {};
372
363
  store.set(obj, result);
373
- for (const key of Reflect.ownKeys(obj)) {
364
+ each(Reflect.ownKeys(obj), (_index, key) => {
374
365
  result[key] = cloneValue(obj[key]);
375
- }
366
+ });
376
367
  return result;
377
368
  };
378
369
  return cloneValue(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objwalk",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",
@@ -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 a nested property | `flush(data, path)`; deletes an own property, mutates and returns `data`; missing paths are left unchanged. |
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. |
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
34
  | Build a store while walking | `map(data, (key, value, store, path) => { ... }, store?)`; returns the store, ignores callback return values. |
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,15 @@ 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. Callback return values remain ignored, including
94
- false. Mutating received segments does not affect traversal. Callback/getter/descend
95
- errors propagate. Cycles are not detected.
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. `map` continues to
112
+ ignore callback return values for both objects and arrays.
113
+ Mutating received segments does not affect traversal. Callback/getter/descend errors
114
+ propagate. Cycles are not detected.
96
115
 
97
116
  ## Differences
98
117
 
@@ -147,5 +166,6 @@ Fallback applies only to exceptions. Preserve valid falsy results and native
147
166
  `undefined`. Neither fallback nor a throwing `onError` changes stringify's
148
167
  return type of `string | undefined`.
149
168
 
150
- Full usage: [README](../../README.md). In a consumer project, read the installed
169
+ Full usage: [README](../../README.md). Edge cases: [specification](../../SPEC.md).
170
+ In a consumer project, read the installed
151
171
  package's README and declarations when checking the available version's API.