objwalk 1.5.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
@@ -1,3 +1,35 @@
1
+ // src/helpers/createPath.ts
2
+ var createPath = (parentPath, key, parentIsArray) => {
3
+ if (parentIsArray) {
4
+ return parentPath + "[" + key + "]";
5
+ }
6
+ return parentPath ? parentPath + "." + key : String(key);
7
+ };
8
+
9
+ // src/helpers/isWalkable.ts
10
+ var isWalkable = (value) => {
11
+ return value !== null && typeof value === "object";
12
+ };
13
+
14
+ // src/each.ts
15
+ var each = (data, callback, { root = false, descend } = {}) => {
16
+ const walk = (value, path, segments, key) => {
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] }))) {
19
+ if (Array.isArray(value)) {
20
+ return value.every(
21
+ (item, index) => walk(item, createPath(path, index, true), [...segments, index], index)
22
+ );
23
+ }
24
+ return Object.keys(value).every(
25
+ (childKey) => walk(value[childKey], createPath(path, childKey, false), [...segments, childKey], childKey)
26
+ );
27
+ }
28
+ return proceed;
29
+ };
30
+ walk(data, "", [], void 0);
31
+ };
32
+
1
33
  // src/helpers/getValueByKey.ts
2
34
  var getValueByKey = (object, key, { inherited = true, ignore = [] } = {}) => {
3
35
  if (object === null || object === void 0 || ignore.includes(key) || !inherited && !Object.hasOwn(object, key)) {
@@ -49,51 +81,6 @@ var parsePathSelection = (path = "") => {
49
81
  };
50
82
  };
51
83
 
52
- // src/helpers/createPath.ts
53
- var createPath = (parentPath, key, parentIsArray) => {
54
- if (parentIsArray) {
55
- return parentPath + "[" + key + "]";
56
- }
57
- return parentPath ? parentPath + "." + key : String(key);
58
- };
59
-
60
- // src/helpers/isWalkable.ts
61
- var isWalkable = (value) => {
62
- return value !== null && typeof value === "object";
63
- };
64
-
65
- // src/each.ts
66
- var each = (data, callback) => {
67
- const walk = (value, path, key, parentIsArray) => {
68
- const currentPath = path || createPath("", key, parentIsArray);
69
- callback(key, value, currentPath);
70
- if (!isWalkable(value)) {
71
- return;
72
- }
73
- if (Array.isArray(value)) {
74
- value.forEach((item, index) => {
75
- walk(item, createPath(currentPath, index, true), index, true);
76
- });
77
- return;
78
- }
79
- Object.entries(value).forEach(([childKey, item]) => {
80
- walk(item, createPath(currentPath, childKey, false), childKey, false);
81
- });
82
- };
83
- if (!isWalkable(data)) {
84
- return;
85
- }
86
- if (Array.isArray(data)) {
87
- data.forEach((item, index) => {
88
- walk(item, createPath("", index, true), index, true);
89
- });
90
- return;
91
- }
92
- Object.entries(data).forEach(([key, value]) => {
93
- walk(value, createPath("", key, false), key, false);
94
- });
95
- };
96
-
97
84
  // src/pick.ts
98
85
  var pick = (data = {}, path = "", options = {}) => {
99
86
  const { cleanPath, keys } = parsePathSelection(path);
@@ -187,13 +174,13 @@ var extract = (source, mapping) => {
187
174
  return;
188
175
  }
189
176
  if (Array.isArray(sourcePath)) {
190
- for (const item of sourcePath) {
177
+ each(sourcePath, (_key, item) => {
191
178
  const value = pick(source, item);
192
179
  if (value !== void 0) {
193
180
  set(result, destinationPath, value);
194
- break;
181
+ return false;
195
182
  }
196
- }
183
+ });
197
184
  return;
198
185
  }
199
186
  set(result, destinationPath, pick(source, sourcePath));
@@ -209,114 +196,144 @@ var map = (data, callback, store = {}) => {
209
196
  return store;
210
197
  };
211
198
 
212
- // src/merge.ts
213
- var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
199
+ // src/helpers/isPlainObject.ts
214
200
  var isPlainObject = (value) => {
215
- if (value === null || typeof value !== "object") {
216
- return false;
201
+ if (value !== null && typeof value === "object") {
202
+ const prototype = Object.getPrototypeOf(value);
203
+ return prototype === Object.prototype || prototype === null;
217
204
  }
218
- const prototype = Object.getPrototypeOf(value);
219
- return prototype === Object.prototype || prototype === null;
205
+ return false;
220
206
  };
207
+
208
+ // src/merge.ts
209
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
221
210
  var merge = (target, ...sources) => {
222
211
  const targetRecord = target;
223
- for (const source of sources) {
224
- if (!isPlainObject(source)) {
225
- continue;
226
- }
227
- for (const [key, sourceValue] of Object.entries(source)) {
228
- if (unsafeKeys.has(key)) {
229
- continue;
230
- }
231
- const targetValue = targetRecord[key];
232
- if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
233
- merge(targetValue, sourceValue);
234
- } else {
235
- targetRecord[key] = sourceValue;
236
- }
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 });
237
224
  }
238
- }
225
+ }, { descend: ({ segments }) => segments.length === 0 });
239
226
  return target;
240
227
  };
241
228
 
242
229
  // src/flush.ts
243
230
  var flush = (data = {}, path) => {
244
- if (path.length > 0) {
245
- const keys = path.split(".");
246
- const lastKey = keys.pop() ?? "";
247
- const match = lastKey.match(/^(\w+)\[(\d+)\]$/);
248
- let parent = getValueByPath(data, keys.join("."), { inherited: false });
249
- let key = lastKey;
250
- if (match) {
251
- const [, arrayKey, index] = match;
252
- const array = getValueByKey(parent, arrayKey, { inherited: false });
253
- parent = Array.isArray(array) ? array : void 0;
254
- key = index;
255
- }
256
- if (parent !== null && typeof parent === "object" && Object.hasOwn(parent, key)) {
257
- 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
+ }
258
248
  }
259
- }
249
+ });
260
250
  return data;
261
251
  };
262
252
 
263
253
  // src/deepEqual.ts
264
254
  var deepEqual = (x, y, store) => {
265
- if (Number.isNaN(x) && Number.isNaN(y)) {
255
+ if (x === y || Number.isNaN(x) && Number.isNaN(y)) {
266
256
  return true;
267
257
  }
268
- if (x === y) {
269
- return true;
270
- }
271
- if (x == null || y == null) {
272
- return false;
273
- }
274
- if (typeof x !== "object" || typeof y !== "object") {
275
- return false;
276
- }
277
- let seen = store.get(x);
278
- if (seen !== void 0 && seen.has(y)) {
279
- return true;
280
- }
281
- if (seen === void 0) {
282
- seen = /* @__PURE__ */ new WeakSet();
283
- store.set(x, seen);
284
- }
285
- seen.add(y);
286
- if (x instanceof Date && y instanceof Date) {
287
- return x.getTime() === y.getTime();
288
- }
289
- if (x instanceof RegExp && y instanceof RegExp) {
290
- return x.toString() === y.toString();
291
- }
292
- const isArrayX = Array.isArray(x);
293
- const isArrayY = Array.isArray(y);
294
- if (isArrayX !== isArrayY) {
295
- return false;
296
- }
297
- if (isArrayX) {
298
- return equalArrays(x, y, store);
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;
262
+ }
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
+ }
299
290
  }
300
- return equalObjects(x, y, store);
291
+ return false;
301
292
  };
302
- var equalArrays = (x, y, store) => x.length === y.length && x.every((value, index) => deepEqual(value, y[index], store));
303
- var equalObjects = (x, y, store) => {
304
- const keysX = Reflect.ownKeys(x);
305
- const keysY = Reflect.ownKeys(y);
306
- if (keysX.length !== keysY.length) {
307
- return false;
293
+
294
+ // src/equally.ts
295
+ var equally = (a, b, paths) => {
296
+ if (paths !== void 0) {
297
+ return paths.every((path) => equally(getValueByPath(a, path), getValueByPath(b, path)));
308
298
  }
309
- return keysX.every((key) => {
310
- if (!Object.hasOwn(y, key)) {
311
- return false;
299
+ return deepEqual(a, b, /* @__PURE__ */ new WeakMap());
300
+ };
301
+
302
+ // src/diff.ts
303
+ var diff = (first, last) => {
304
+ const operations = [];
305
+ const frames = [];
306
+ const finish = (depth) => {
307
+ each(frames.splice(depth).reverse(), (_index, { first: first2, last: last2, path }) => {
308
+ each(Object.keys(first2), (_index2, key) => {
309
+ if (Object.prototype.propertyIsEnumerable.call(last2, key) === false) {
310
+ operations.push({ type: "delete", path: [...path, key] });
311
+ }
312
+ });
313
+ }, { descend: ({ segments }) => segments.length === 0 });
314
+ return frames.at(-1);
315
+ };
316
+ let descend = false;
317
+ each(last, (key, value, _path, segments) => {
318
+ const path = segments.map(String);
319
+ const frame = finish(segments.length);
320
+ const present = key === void 0 || frame !== void 0 && Object.prototype.propertyIsEnumerable.call(frame.first, key);
321
+ const previous = key === void 0 ? first : present ? frame?.first[key] : void 0;
322
+ descend = false;
323
+ if (present && isPlainObject(previous) && isPlainObject(value)) {
324
+ frames.push({ first: previous, last: value, path });
325
+ descend = true;
326
+ } else if (present === false || equally(previous, value) === false) {
327
+ operations.push({ type: "set", path, value });
312
328
  }
313
- return deepEqual(x[key], y[key], store);
329
+ }, {
330
+ root: true,
331
+ descend: () => descend
314
332
  });
333
+ finish(0);
334
+ return operations;
315
335
  };
316
336
 
317
- // src/equally.ts
318
- var equally = (a, b) => deepEqual(a, b, /* @__PURE__ */ new WeakMap());
319
-
320
337
  // src/clone.ts
321
338
  var clone = (value) => {
322
339
  const store = /* @__PURE__ */ new WeakMap();
@@ -344,9 +361,9 @@ var clone = (value) => {
344
361
  }
345
362
  const result = {};
346
363
  store.set(obj, result);
347
- for (const key of Reflect.ownKeys(obj)) {
364
+ each(Reflect.ownKeys(obj), (_index, key) => {
348
365
  result[key] = cloneValue(obj[key]);
349
- }
366
+ });
350
367
  return result;
351
368
  };
352
369
  return cloneValue(value);
@@ -386,6 +403,7 @@ var stringify = (value, { fallback, onError, replacer, space } = {}) => {
386
403
  };
387
404
  export {
388
405
  clone,
406
+ diff,
389
407
  each,
390
408
  equally,
391
409
  extract,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objwalk",
3
- "version": "1.5.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",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: objwalk
3
- description: Use when writing or editing JavaScript or TypeScript that imports `objwalk`, reads, writes, or deletes nested object paths, reshapes or walks objects, or uses objwalk JSON helpers.
3
+ description: Use when writing or editing JavaScript or TypeScript that imports `objwalk`, reads, writes, or deletes nested object paths, computes differences, reshapes or walks objects, or uses objwalk JSON helpers.
4
4
  ---
5
5
 
6
6
  # objwalk
@@ -19,7 +19,7 @@ them with type assertions or assume every method accepts arbitrary `unknown`.
19
19
  Named imports only:
20
20
 
21
21
  ```ts
22
- import { clone, each, equally, extract, flush, map, merge, parse, pick, set, stringify } from 'objwalk'
22
+ import { clone, diff, each, equally, extract, flush, map, merge, parse, pick, set, stringify } from 'objwalk'
23
23
  ```
24
24
 
25
25
  ## Choose the existing operation
@@ -28,16 +28,17 @@ import { clone, each, equally, extract, flush, map, merge, parse, pick, set, str
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) => { ... })`; visits containers and leaves, excluding the root. |
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
+ | Describe changes | `diff(first, last): DiffOperation[]`; returns `set`/`delete` operations, or `[]` when unchanged. |
38
39
 
39
40
  `set`, `flush`, and `merge` mutate their targets. Use `clone` first only when the task
40
- requires an independent copy. `each` and `map` do not support circular traversal;
41
+ requires an independent copy. `each`, `map`, and `diff` do not support circular traversal;
41
42
  do not infer that capability from `clone` or `equally`.
42
43
 
43
44
  ## Paths and reshaping
@@ -52,7 +53,8 @@ do not infer that capability from `clone` or `equally`.
52
53
 
53
54
  `[n]` is an `extract` mapping feature, not a general wildcard for `pick`, `set`, or `flush`.
54
55
  An array of source paths in `extract` selects the first result that is not
55
- `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.
56
58
 
57
59
  Use `flush` to remove a property rather than `set(data, path, undefined)`, which
58
60
  leaves the property present. `flush` traverses and deletes only own properties.
@@ -62,6 +64,18 @@ but deleting an item leaves a hole without shifting indexes or changing length.
62
64
  Selection syntax (`{name,email}`, `{*}`) is not supported. Errors from getters,
63
65
  proxies, or deleting non-configurable properties propagate.
64
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
+
65
79
  `pick` reads inherited properties by default. Use its existing options when own
66
80
  properties or ignored keys are required:
67
81
 
@@ -72,6 +86,57 @@ pick(data, 'user.name', {
72
86
  })
73
87
  ```
74
88
 
89
+ ## Walking
90
+
91
+ `each` passes literal `segments: WalkKey[]` as its fourth callback argument.
92
+ Object keys are strings; array indexes are numbers. Prefer segments when keys may
93
+ contain dots, brackets, or empty strings; the string `path` can be ambiguous.
94
+
95
+ `EachOptions` accepts `root` (default `false`) and
96
+ `descend: ({ key, value, path, segments }) => boolean`.
97
+ With `root: true`, the root is visited first, including primitive/nullish values,
98
+ with key `undefined`, path `''`, and segments `[]`. Callback keys are then typed
99
+ `WalkKey | undefined`; legacy calls keep `WalkKey`. Use the public
100
+ `EachCallback<WalkKey | undefined>` for an explicitly typed root-aware callback.
101
+ `EachEntry` and `EachOptions` are also public named exports.
102
+
103
+ `descend` is called only for containers, after their callback, and returning false
104
+ skips their children. It also runs for a root container excluded from callbacks.
105
+ For example, `descend: ({ value }) => Array.isArray(value) === false` visits arrays
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.
115
+
116
+ ## Differences
117
+
118
+ `diff` recursively compares two plain objects by their own enumerable string keys;
119
+ symbol/non-enumerable keys, prototypes, and descriptors are not reported during that traversal.
120
+ Arrays and other values use `equally` and are replaced as a whole when changed.
121
+ New properties are also set as whole values. The public `DiffOperation` type is:
122
+
123
+ ```ts
124
+ type DiffOperation =
125
+ | { type: 'set'; path: string[]; value: unknown }
126
+ | { type: 'delete'; path: string[] }
127
+ ```
128
+
129
+ Paths contain literal keys; `[]` replaces the root. Do not join them with `.` to
130
+ call `set` or `flush`: keys may contain dots or brackets. `merge` does not apply
131
+ these operations. There is no patch applier or options argument.
132
+
133
+ Deletion is distinct from assigning `null` or `undefined`, including adding an own
134
+ `undefined` property. Empty parent objects remain. Inputs are not mutated or cloned;
135
+ `set.value` retains references from `last`. Native JSON serialization loses an
136
+ `undefined` value, so do not assume lossless transport for arbitrary JS values.
137
+ Getter/proxy errors propagate. Cycles are outside the contract, without detection;
138
+ shared references without cycles work.
139
+
75
140
  ## JSON and ready values
76
141
 
77
142
  Call `parse(raw, options?)` directly with `raw: unknown`. The library chooses
@@ -101,5 +166,6 @@ Fallback applies only to exceptions. Preserve valid falsy results and native
101
166
  `undefined`. Neither fallback nor a throwing `onError` changes stringify's
102
167
  return type of `string | undefined`.
103
168
 
104
- 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
105
171
  package's README and declarations when checking the available version's API.