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/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));
@@ -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
- 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
- }
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
- 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];
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 == 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;
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
- return deepEqual(x[key], y[key], store);
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) => deepEqual(a, b, /* @__PURE__ */ new WeakMap());
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
- for (const { first: first2, last: last2, path } of frames.splice(depth).reverse()) {
317
- for (const key of Object.keys(first2)) {
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
- for (const key of Reflect.ownKeys(obj)) {
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.6.0",
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",
@@ -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. |
34
- | Build a store while walking | `map(data, (key, value, store, path) => { ... }, store?)`; returns the store, ignores callback return values. |
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. 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. 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). In a consumer project, read the installed
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.