objwalk 1.5.0 → 1.6.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 CHANGED
@@ -15,7 +15,7 @@ npm install objwalk
15
15
  ## Usage
16
16
 
17
17
  ```ts
18
- import { each, equally, extract, flush, map, merge, parse, pick, set, stringify } from 'objwalk'
18
+ import { diff, each, equally, extract, flush, map, merge, parse, pick, set, stringify } from 'objwalk'
19
19
  ```
20
20
 
21
21
  ## pick
@@ -143,6 +143,55 @@ equally({ a: 1 }, { a: 1, b: 2 })
143
143
  `equally` treats `NaN` as equal to `NaN`, compares `Date` by timestamp and `RegExp` by
144
144
  source and flags, and follows circular references without looping.
145
145
 
146
+ ## diff
147
+
148
+ Describe changes from `first` to `last` as a list of `set` and `delete` operations.
149
+
150
+ ```ts
151
+ import { diff, type DiffOperation } from 'objwalk'
152
+
153
+ const operations: DiffOperation[] = diff(
154
+ { hp: 10, target: 'enemy', position: { x: 1, y: 2 } },
155
+ { hp: null, position: { x: 3, y: 2 } }
156
+ )
157
+ // [
158
+ // { type: 'set', path: ['hp'], value: null },
159
+ // { type: 'set', path: ['position', 'x'], value: 3 },
160
+ // { type: 'delete', path: ['target'] },
161
+ // ]
162
+
163
+ diff({}, { a: undefined })
164
+ // [{ type: 'set', path: ['a'], value: undefined }]
165
+
166
+ diff({ a: 1 }, {})
167
+ // [{ type: 'delete', path: ['a'] }]
168
+
169
+ diff(1, null)
170
+ // [{ type: 'set', path: [], value: null }]
171
+
172
+ diff({ a: 1 }, { a: 1 }) // []
173
+ ```
174
+
175
+ - When both values are plain objects (with `Object.prototype` or a null prototype),
176
+ `diff` recursively compares own enumerable string keys. Symbol and non-enumerable
177
+ keys are ignored during that traversal, and prototype or descriptor changes are not reported.
178
+ - Arrays and other values use `equally`; changed values are replaced as a whole.
179
+ Newly added properties also use a single `set`, including when their value is an empty object.
180
+ - A `set` carries the new value, including `null` or `undefined`; a `delete` means
181
+ the key is absent from the final object's enumerable keys. Empty parents remain.
182
+ - Paths are arrays of literal string keys, so dots, brackets, and empty keys are unambiguous.
183
+ An empty path `[]` means replacement of the root. These are not the string paths
184
+ accepted by `pick`, `set`, or `flush`; joining segments with `.` can lose information.
185
+ - Operations follow the final object's key order, recursively, then report removed
186
+ keys in the first object's key order at each level. No changes return `[]`.
187
+
188
+ `diff(first, last): DiffOperation[]` does not mutate or clone either input. Values in `set`
189
+ operations retain references from `last`; this is not an independent snapshot.
190
+ Getter and proxy errors propagate. Inputs must be acyclic; cycle detection is not performed.
191
+ Shared references without cycles are supported. There is no options argument or patch applier.
192
+ Native JSON serialization drops an `undefined` operation value, so these operations are not
193
+ a lossless JSON transport format for arbitrary JavaScript values.
194
+
146
195
  ## merge
147
196
 
148
197
  Deeply merge plain objects into a target object.
@@ -216,16 +265,52 @@ extract(source, {
216
265
  Walk through an object and call a function for each value.
217
266
 
218
267
  ```ts
219
- each(data, (key, value, path) => {
220
- console.log(key, value, path)
268
+ each(data, (key, value, path, segments) => {
269
+ console.log(key, value, path, segments)
221
270
  })
222
271
  ```
223
272
 
224
273
  The callback receives:
225
274
 
226
- - `key`: the current object key or array index
275
+ - `key`: the current object key or numeric array index; `undefined` for the root when enabled
227
276
  - `value`: the current value
228
- - `path`: the full path to the current value
277
+ - `path`: the full string path to the current value
278
+ - `segments`: literal path segments (`WalkKey[]`), with string object keys and numeric array indexes
279
+
280
+ For example, a literal key `'user.name'` has segments `['user.name']`, while a nested
281
+ `user.name` has `['user', 'name']`. `items[0].id` has `['items', 0, 'id']`.
282
+ The first three callback arguments keep their existing meaning; old callbacks still work.
283
+ Callback return values are ignored, including `false`.
284
+
285
+ Pass options as the third argument:
286
+
287
+ ```ts
288
+ each(data, (key, value, path, segments) => {
289
+ // The root is visited first with key undefined, path '', and segments [].
290
+ }, {
291
+ root: true,
292
+ // Visit arrays themselves, but skip their contents.
293
+ descend: ({ value }) => Array.isArray(value) === false,
294
+ })
295
+ ```
296
+
297
+ | Option | Behavior | Default |
298
+ | --- | --- | --- |
299
+ | `root` | Visit the root, including primitive, nullish, or empty root values | `false` |
300
+ | `descend` | Receive `{ key, value, path, segments }` and decide whether to visit a container's children | Visit all children |
301
+
302
+ `descend` runs only for objects and arrays, after their callback. It also runs for the
303
+ root container when `root` is false, allowing the entire traversal to be skipped.
304
+ Returning `false` skips children, without evaluating their getters; the container itself
305
+ is still visited when applicable. Errors from callbacks, getters, and `descend` propagate.
306
+ Mutating the received `segments` arrays does not change traversal paths.
307
+
308
+ `EachCallback`, `EachEntry`, and `EachOptions` are public named type exports.
309
+ Without `root`, or with `root: false`, callback keys remain `WalkKey`. With `root: true`
310
+ or a dynamic `root` option, keys are `WalkKey | undefined`. An explicitly typed callback
311
+ can use `EachCallback<WalkKey | undefined>` when root visits are possible.
312
+ Traversal does not detect cycles. `diff` uses `each` with `root: true` and a `descend`
313
+ predicate that stops at arrays, non-plain objects, and branches replaced as a whole.
229
314
 
230
315
  ## map
231
316
 
@@ -358,10 +443,11 @@ pick(data, path)
358
443
  set(data, path, value)
359
444
  flush(data, path)
360
445
  extract(data, mapping)
361
- each(data, callback)
446
+ each(data, callback, options?)
362
447
  map(data, callback)
363
448
  merge(target, ...sources)
364
449
  equally(a, b)
450
+ diff(first, last) // DiffOperation[]
365
451
  parse(raw, options?) // unknown
366
452
  stringify(value, options?) // string | undefined
367
453
  ```
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  clone: () => clone,
24
+ diff: () => diff,
24
25
  each: () => each,
25
26
  equally: () => equally,
26
27
  extract: () => extract,
@@ -34,6 +35,40 @@ __export(index_exports, {
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
36
37
 
38
+ // src/helpers/createPath.ts
39
+ var createPath = (parentPath, key, parentIsArray) => {
40
+ if (parentIsArray) {
41
+ return parentPath + "[" + key + "]";
42
+ }
43
+ return parentPath ? parentPath + "." + key : String(key);
44
+ };
45
+
46
+ // src/helpers/isWalkable.ts
47
+ var isWalkable = (value) => {
48
+ return value !== null && typeof value === "object";
49
+ };
50
+
51
+ // src/each.ts
52
+ var each = (data, callback, { root = false, descend } = {}) => {
53
+ const walk = (value, path, segments, key) => {
54
+ if (segments.length > 0 || root) {
55
+ callback(key, value, path, [...segments]);
56
+ }
57
+ if (isWalkable(value) && (descend === void 0 || descend({ key, value, path, segments: [...segments] }))) {
58
+ if (Array.isArray(value)) {
59
+ value.forEach((item, index) => {
60
+ walk(item, createPath(path, index, true), [...segments, index], index);
61
+ });
62
+ } else {
63
+ Object.entries(value).forEach(([childKey, item]) => {
64
+ walk(item, createPath(path, childKey, false), [...segments, childKey], childKey);
65
+ });
66
+ }
67
+ }
68
+ };
69
+ walk(data, "", [], void 0);
70
+ };
71
+
37
72
  // src/helpers/getValueByKey.ts
38
73
  var getValueByKey = (object, key, { inherited = true, ignore = [] } = {}) => {
39
74
  if (object === null || object === void 0 || ignore.includes(key) || !inherited && !Object.hasOwn(object, key)) {
@@ -85,51 +120,6 @@ var parsePathSelection = (path = "") => {
85
120
  };
86
121
  };
87
122
 
88
- // src/helpers/createPath.ts
89
- var createPath = (parentPath, key, parentIsArray) => {
90
- if (parentIsArray) {
91
- return parentPath + "[" + key + "]";
92
- }
93
- return parentPath ? parentPath + "." + key : String(key);
94
- };
95
-
96
- // src/helpers/isWalkable.ts
97
- var isWalkable = (value) => {
98
- return value !== null && typeof value === "object";
99
- };
100
-
101
- // src/each.ts
102
- var each = (data, callback) => {
103
- const walk = (value, path, key, parentIsArray) => {
104
- const currentPath = path || createPath("", key, parentIsArray);
105
- callback(key, value, currentPath);
106
- if (!isWalkable(value)) {
107
- return;
108
- }
109
- if (Array.isArray(value)) {
110
- value.forEach((item, index) => {
111
- walk(item, createPath(currentPath, index, true), index, true);
112
- });
113
- return;
114
- }
115
- Object.entries(value).forEach(([childKey, item]) => {
116
- walk(item, createPath(currentPath, childKey, false), childKey, false);
117
- });
118
- };
119
- if (!isWalkable(data)) {
120
- return;
121
- }
122
- if (Array.isArray(data)) {
123
- data.forEach((item, index) => {
124
- walk(item, createPath("", index, true), index, true);
125
- });
126
- return;
127
- }
128
- Object.entries(data).forEach(([key, value]) => {
129
- walk(value, createPath("", key, false), key, false);
130
- });
131
- };
132
-
133
123
  // src/pick.ts
134
124
  var pick = (data = {}, path = "", options = {}) => {
135
125
  const { cleanPath, keys } = parsePathSelection(path);
@@ -245,15 +235,17 @@ var map = (data, callback, store = {}) => {
245
235
  return store;
246
236
  };
247
237
 
248
- // src/merge.ts
249
- var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
238
+ // src/helpers/isPlainObject.ts
250
239
  var isPlainObject = (value) => {
251
- if (value === null || typeof value !== "object") {
252
- return false;
240
+ if (value !== null && typeof value === "object") {
241
+ const prototype = Object.getPrototypeOf(value);
242
+ return prototype === Object.prototype || prototype === null;
253
243
  }
254
- const prototype = Object.getPrototypeOf(value);
255
- return prototype === Object.prototype || prototype === null;
244
+ return false;
256
245
  };
246
+
247
+ // src/merge.ts
248
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
257
249
  var merge = (target, ...sources) => {
258
250
  const targetRecord = target;
259
251
  for (const source of sources) {
@@ -353,6 +345,41 @@ var equalObjects = (x, y, store) => {
353
345
  // src/equally.ts
354
346
  var equally = (a, b) => deepEqual(a, b, /* @__PURE__ */ new WeakMap());
355
347
 
348
+ // src/diff.ts
349
+ var diff = (first, last) => {
350
+ const operations = [];
351
+ const frames = [];
352
+ const finish = (depth) => {
353
+ for (const { first: first2, last: last2, path } of frames.splice(depth).reverse()) {
354
+ for (const key of Object.keys(first2)) {
355
+ if (Object.prototype.propertyIsEnumerable.call(last2, key) === false) {
356
+ operations.push({ type: "delete", path: [...path, key] });
357
+ }
358
+ }
359
+ }
360
+ return frames.at(-1);
361
+ };
362
+ let descend = false;
363
+ each(last, (key, value, _path, segments) => {
364
+ const path = segments.map(String);
365
+ const frame = finish(segments.length);
366
+ const present = key === void 0 || frame !== void 0 && Object.prototype.propertyIsEnumerable.call(frame.first, key);
367
+ const previous = key === void 0 ? first : present ? frame?.first[key] : void 0;
368
+ descend = false;
369
+ if (present && isPlainObject(previous) && isPlainObject(value)) {
370
+ frames.push({ first: previous, last: value, path });
371
+ descend = true;
372
+ } else if (present === false || equally(previous, value) === false) {
373
+ operations.push({ type: "set", path, value });
374
+ }
375
+ }, {
376
+ root: true,
377
+ descend: () => descend
378
+ });
379
+ finish(0);
380
+ return operations;
381
+ };
382
+
356
383
  // src/clone.ts
357
384
  var clone = (value) => {
358
385
  const store = /* @__PURE__ */ new WeakMap();
@@ -423,6 +450,7 @@ var stringify = (value, { fallback, onError, replacer, space } = {}) => {
423
450
  // Annotate the CommonJS export names for ESM import in node:
424
451
  0 && (module.exports = {
425
452
  clone,
453
+ diff,
426
454
  each,
427
455
  equally,
428
456
  extract,
package/dist/index.d.cts CHANGED
@@ -1,4 +1,12 @@
1
1
  type WalkKey = string | number;
2
+ type DiffOperation = {
3
+ type: 'set';
4
+ path: string[];
5
+ value: unknown;
6
+ } | {
7
+ type: 'delete';
8
+ path: string[];
9
+ };
2
10
  type PickOptions = {
3
11
  inherited?: boolean;
4
12
  ignore?: readonly string[];
@@ -15,8 +23,24 @@ type StringifyOptions = {
15
23
  space?: string | number;
16
24
  };
17
25
 
18
- type EachCallback = (key: WalkKey, value: any, path: string) => void;
19
- declare const each: (data: any, callback: EachCallback) => void;
26
+ type EachCallback<TKey extends WalkKey | undefined = WalkKey> = (key: TKey, value: any, path: string, segments: WalkKey[]) => void;
27
+ type EachEntry = {
28
+ key: WalkKey | undefined;
29
+ value: any;
30
+ path: string;
31
+ segments: WalkKey[];
32
+ };
33
+ type EachOptions = {
34
+ root?: boolean;
35
+ descend?: (entry: EachEntry) => boolean;
36
+ };
37
+ type Each = {
38
+ (data: any, callback: EachCallback, options?: EachOptions & {
39
+ root?: false;
40
+ }): void;
41
+ (data: any, callback: EachCallback<WalkKey | undefined>, options: EachOptions): void;
42
+ };
43
+ declare const each: Each;
20
44
 
21
45
  type ObjwalkObject$1 = Record<string, any>;
22
46
  declare const pick: (data?: ObjwalkObject$1, path?: string, options?: PickOptions) => any;
@@ -36,10 +60,12 @@ declare const flush: (data: ObjwalkObject$1 | undefined, path: string) => Objwal
36
60
 
37
61
  declare const equally: (a: unknown, b: unknown) => boolean;
38
62
 
63
+ declare const diff: (first: unknown, last: unknown) => DiffOperation[];
64
+
39
65
  declare const clone: <T>(value: T) => T;
40
66
 
41
67
  declare const parse: (raw: unknown, { fallback, onError, reviver }?: ParseOptions) => unknown;
42
68
 
43
69
  declare const stringify: (value: unknown, { fallback, onError, replacer, space }?: StringifyOptions) => string | undefined;
44
70
 
45
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type ParseOptions, type PickOptions, type StringifyOptions, type WalkKey, clone, each, equally, extract, flush, map, merge, parse, pick, set, stringify };
71
+ export { type DiffOperation, type EachCallback, type EachEntry, type EachOptions, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type ParseOptions, type PickOptions, type StringifyOptions, type WalkKey, clone, diff, each, equally, extract, flush, map, merge, parse, pick, set, stringify };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  type WalkKey = string | number;
2
+ type DiffOperation = {
3
+ type: 'set';
4
+ path: string[];
5
+ value: unknown;
6
+ } | {
7
+ type: 'delete';
8
+ path: string[];
9
+ };
2
10
  type PickOptions = {
3
11
  inherited?: boolean;
4
12
  ignore?: readonly string[];
@@ -15,8 +23,24 @@ type StringifyOptions = {
15
23
  space?: string | number;
16
24
  };
17
25
 
18
- type EachCallback = (key: WalkKey, value: any, path: string) => void;
19
- declare const each: (data: any, callback: EachCallback) => void;
26
+ type EachCallback<TKey extends WalkKey | undefined = WalkKey> = (key: TKey, value: any, path: string, segments: WalkKey[]) => void;
27
+ type EachEntry = {
28
+ key: WalkKey | undefined;
29
+ value: any;
30
+ path: string;
31
+ segments: WalkKey[];
32
+ };
33
+ type EachOptions = {
34
+ root?: boolean;
35
+ descend?: (entry: EachEntry) => boolean;
36
+ };
37
+ type Each = {
38
+ (data: any, callback: EachCallback, options?: EachOptions & {
39
+ root?: false;
40
+ }): void;
41
+ (data: any, callback: EachCallback<WalkKey | undefined>, options: EachOptions): void;
42
+ };
43
+ declare const each: Each;
20
44
 
21
45
  type ObjwalkObject$1 = Record<string, any>;
22
46
  declare const pick: (data?: ObjwalkObject$1, path?: string, options?: PickOptions) => any;
@@ -36,10 +60,12 @@ declare const flush: (data: ObjwalkObject$1 | undefined, path: string) => Objwal
36
60
 
37
61
  declare const equally: (a: unknown, b: unknown) => boolean;
38
62
 
63
+ declare const diff: (first: unknown, last: unknown) => DiffOperation[];
64
+
39
65
  declare const clone: <T>(value: T) => T;
40
66
 
41
67
  declare const parse: (raw: unknown, { fallback, onError, reviver }?: ParseOptions) => unknown;
42
68
 
43
69
  declare const stringify: (value: unknown, { fallback, onError, replacer, space }?: StringifyOptions) => string | undefined;
44
70
 
45
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type ParseOptions, type PickOptions, type StringifyOptions, type WalkKey, clone, each, equally, extract, flush, map, merge, parse, pick, set, stringify };
71
+ export { type DiffOperation, type EachCallback, type EachEntry, type EachOptions, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type ParseOptions, type PickOptions, type StringifyOptions, type WalkKey, clone, diff, each, equally, extract, flush, map, merge, parse, pick, set, stringify };
package/dist/index.js CHANGED
@@ -1,3 +1,37 @@
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
+ 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] }))) {
21
+ 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
+ });
29
+ }
30
+ }
31
+ };
32
+ walk(data, "", [], void 0);
33
+ };
34
+
1
35
  // src/helpers/getValueByKey.ts
2
36
  var getValueByKey = (object, key, { inherited = true, ignore = [] } = {}) => {
3
37
  if (object === null || object === void 0 || ignore.includes(key) || !inherited && !Object.hasOwn(object, key)) {
@@ -49,51 +83,6 @@ var parsePathSelection = (path = "") => {
49
83
  };
50
84
  };
51
85
 
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
86
  // src/pick.ts
98
87
  var pick = (data = {}, path = "", options = {}) => {
99
88
  const { cleanPath, keys } = parsePathSelection(path);
@@ -209,15 +198,17 @@ var map = (data, callback, store = {}) => {
209
198
  return store;
210
199
  };
211
200
 
212
- // src/merge.ts
213
- var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
201
+ // src/helpers/isPlainObject.ts
214
202
  var isPlainObject = (value) => {
215
- if (value === null || typeof value !== "object") {
216
- return false;
203
+ if (value !== null && typeof value === "object") {
204
+ const prototype = Object.getPrototypeOf(value);
205
+ return prototype === Object.prototype || prototype === null;
217
206
  }
218
- const prototype = Object.getPrototypeOf(value);
219
- return prototype === Object.prototype || prototype === null;
207
+ return false;
220
208
  };
209
+
210
+ // src/merge.ts
211
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
221
212
  var merge = (target, ...sources) => {
222
213
  const targetRecord = target;
223
214
  for (const source of sources) {
@@ -317,6 +308,41 @@ var equalObjects = (x, y, store) => {
317
308
  // src/equally.ts
318
309
  var equally = (a, b) => deepEqual(a, b, /* @__PURE__ */ new WeakMap());
319
310
 
311
+ // src/diff.ts
312
+ var diff = (first, last) => {
313
+ const operations = [];
314
+ const frames = [];
315
+ const finish = (depth) => {
316
+ for (const { first: first2, last: last2, path } of frames.splice(depth).reverse()) {
317
+ for (const key of Object.keys(first2)) {
318
+ if (Object.prototype.propertyIsEnumerable.call(last2, key) === false) {
319
+ operations.push({ type: "delete", path: [...path, key] });
320
+ }
321
+ }
322
+ }
323
+ return frames.at(-1);
324
+ };
325
+ let descend = false;
326
+ each(last, (key, value, _path, segments) => {
327
+ const path = segments.map(String);
328
+ const frame = finish(segments.length);
329
+ const present = key === void 0 || frame !== void 0 && Object.prototype.propertyIsEnumerable.call(frame.first, key);
330
+ const previous = key === void 0 ? first : present ? frame?.first[key] : void 0;
331
+ descend = false;
332
+ if (present && isPlainObject(previous) && isPlainObject(value)) {
333
+ frames.push({ first: previous, last: value, path });
334
+ descend = true;
335
+ } else if (present === false || equally(previous, value) === false) {
336
+ operations.push({ type: "set", path, value });
337
+ }
338
+ }, {
339
+ root: true,
340
+ descend: () => descend
341
+ });
342
+ finish(0);
343
+ return operations;
344
+ };
345
+
320
346
  // src/clone.ts
321
347
  var clone = (value) => {
322
348
  const store = /* @__PURE__ */ new WeakMap();
@@ -386,6 +412,7 @@ var stringify = (value, { fallback, onError, replacer, space } = {}) => {
386
412
  };
387
413
  export {
388
414
  clone,
415
+ diff,
389
416
  each,
390
417
  equally,
391
418
  extract,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objwalk",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Small utilities for reading, writing, walking, and reshaping nested JavaScript objects.",
5
5
  "keywords": [
6
6
  "object",
@@ -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
@@ -30,14 +30,15 @@ import { clone, each, equally, extract, flush, map, merge, parse, pick, set, str
30
30
  | Write a nested value | `set(data, path, value)`; creates intermediate containers, mutates and returns `data`. |
31
31
  | Delete a nested property | `flush(data, path)`; deletes an own property, 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?)`; root is excluded by default. |
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
37
  | Deep comparison | `equally(a, b)`; supports `NaN`, `Date`, `RegExp`, and circular references. |
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
@@ -72,6 +73,51 @@ pick(data, 'user.name', {
72
73
  })
73
74
  ```
74
75
 
76
+ ## Walking
77
+
78
+ `each` passes literal `segments: WalkKey[]` as its fourth callback argument.
79
+ Object keys are strings; array indexes are numbers. Prefer segments when keys may
80
+ contain dots, brackets, or empty strings; the string `path` can be ambiguous.
81
+
82
+ `EachOptions` accepts `root` (default `false`) and
83
+ `descend: ({ key, value, path, segments }) => boolean`.
84
+ With `root: true`, the root is visited first, including primitive/nullish values,
85
+ with key `undefined`, path `''`, and segments `[]`. Callback keys are then typed
86
+ `WalkKey | undefined`; legacy calls keep `WalkKey`. Use the public
87
+ `EachCallback<WalkKey | undefined>` for an explicitly typed root-aware callback.
88
+ `EachEntry` and `EachOptions` are also public named exports.
89
+
90
+ `descend` is called only for containers, after their callback, and returning false
91
+ skips their children. It also runs for a root container excluded from callbacks.
92
+ 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.
96
+
97
+ ## Differences
98
+
99
+ `diff` recursively compares two plain objects by their own enumerable string keys;
100
+ symbol/non-enumerable keys, prototypes, and descriptors are not reported during that traversal.
101
+ Arrays and other values use `equally` and are replaced as a whole when changed.
102
+ New properties are also set as whole values. The public `DiffOperation` type is:
103
+
104
+ ```ts
105
+ type DiffOperation =
106
+ | { type: 'set'; path: string[]; value: unknown }
107
+ | { type: 'delete'; path: string[] }
108
+ ```
109
+
110
+ Paths contain literal keys; `[]` replaces the root. Do not join them with `.` to
111
+ call `set` or `flush`: keys may contain dots or brackets. `merge` does not apply
112
+ these operations. There is no patch applier or options argument.
113
+
114
+ Deletion is distinct from assigning `null` or `undefined`, including adding an own
115
+ `undefined` property. Empty parent objects remain. Inputs are not mutated or cloned;
116
+ `set.value` retains references from `last`. Native JSON serialization loses an
117
+ `undefined` value, so do not assume lossless transport for arbitrary JS values.
118
+ Getter/proxy errors propagate. Cycles are outside the contract, without detection;
119
+ shared references without cycles work.
120
+
75
121
  ## JSON and ready values
76
122
 
77
123
  Call `parse(raw, options?)` directly with `raw: unknown`. The library chooses