objwalk 1.4.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Small utilities for reading, writing, walking, and reshaping nested JavaScript objects.
4
4
 
5
- `objwalk` is built around a compact path syntax, so the same mental model works for picking values, setting values, extracting a new object shape, and walking through nested data.
5
+ `objwalk` is built around a compact path syntax, so the same mental model works for picking, setting, and deleting values, extracting a new object shape, and walking through nested data.
6
6
 
7
7
  > Coding agent? Load the [objwalk skill](skills/objwalk/SKILL.md).
8
8
 
@@ -15,7 +15,7 @@ npm install objwalk
15
15
  ## Usage
16
16
 
17
17
  ```ts
18
- import { each, equally, extract, 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
@@ -96,6 +96,38 @@ set(data, 'items[0].title', 'First')
96
96
 
97
97
  `set` mutates the object passed to it and returns the same object.
98
98
 
99
+ ## flush
100
+
101
+ Delete a property by path using native `delete` semantics.
102
+
103
+ ```ts
104
+ const data = { user: { name: 'Ada', role: 'admin' } }
105
+
106
+ flush(data, 'user.name')
107
+ // { user: { role: 'admin' } }
108
+
109
+ flush({}, 'path.to.key') // {}
110
+ flush({ user: null }, 'user.name') // { user: null }
111
+ ```
112
+
113
+ `flush(data, path)` mutates and returns the same object. Missing paths, `null`,
114
+ `undefined`, and primitive parents are left unchanged. It does not create intermediate
115
+ containers or remove empty parents. An empty path leaves the object unchanged.
116
+ Only own properties are traversed and deleted; inherited properties are left unchanged.
117
+
118
+ Array indexes use the same explicit syntax as `pick` and `set`:
119
+
120
+ ```ts
121
+ const data = { items: [{ title: 'First' }, { title: 'Second' }] }
122
+
123
+ flush(data, 'items[0].title') // { items: [{}, { title: 'Second' }] }
124
+ flush(data, 'items[1]') // { items: [{}, <empty item>] }
125
+ ```
126
+
127
+ Deleting an array item leaves a hole, preserves its length, and does not shift later items.
128
+ Selection syntax (`{name,email}`, `{*}`) and `[n]` mappings are not supported by `flush`.
129
+ Errors from getters, proxies, or deleting non-configurable properties propagate to the caller.
130
+
99
131
  ## equally
100
132
 
101
133
  Deeply compare two values for structural equality.
@@ -111,6 +143,55 @@ equally({ a: 1 }, { a: 1, b: 2 })
111
143
  `equally` treats `NaN` as equal to `NaN`, compares `Date` by timestamp and `RegExp` by
112
144
  source and flags, and follows circular references without looping.
113
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
+
114
195
  ## merge
115
196
 
116
197
  Deeply merge plain objects into a target object.
@@ -184,16 +265,52 @@ extract(source, {
184
265
  Walk through an object and call a function for each value.
185
266
 
186
267
  ```ts
187
- each(data, (key, value, path) => {
188
- console.log(key, value, path)
268
+ each(data, (key, value, path, segments) => {
269
+ console.log(key, value, path, segments)
189
270
  })
190
271
  ```
191
272
 
192
273
  The callback receives:
193
274
 
194
- - `key`: the current object key or array index
275
+ - `key`: the current object key or numeric array index; `undefined` for the root when enabled
195
276
  - `value`: the current value
196
- - `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.
197
314
 
198
315
  ## map
199
316
 
@@ -304,8 +421,8 @@ The wrappers do not mutate inputs, undo callback side effects, log errors, or re
304
421
 
305
422
  | Syntax | Description |
306
423
  | --- | --- |
307
- | `user.name` | Read or write a nested property. |
308
- | `items[0].title` | Read or write an array item by index. |
424
+ | `user.name` | Read, write, or delete a nested property. |
425
+ | `items[0].title` | Read, write, or delete an array item's property by index. |
309
426
  | `user.{name,email}` | Pick several keys from one object. |
310
427
  | `user.{*}` | List keys from one object. |
311
428
  | `items[n].title` | Map every item in an array during `extract`. |
@@ -324,11 +441,13 @@ them without redundant wrappers that only repeat object checks.
324
441
  ```ts
325
442
  pick(data, path)
326
443
  set(data, path, value)
444
+ flush(data, path)
327
445
  extract(data, mapping)
328
- each(data, callback)
446
+ each(data, callback, options?)
329
447
  map(data, callback)
330
448
  merge(target, ...sources)
331
449
  equally(a, b)
450
+ diff(first, last) // DiffOperation[]
332
451
  parse(raw, options?) // unknown
333
452
  stringify(value, options?) // string | undefined
334
453
  ```
package/dist/index.cjs CHANGED
@@ -21,9 +21,11 @@ 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,
28
+ flush: () => flush,
27
29
  map: () => map,
28
30
  merge: () => merge,
29
31
  parse: () => parse,
@@ -33,6 +35,40 @@ __export(index_exports, {
33
35
  });
34
36
  module.exports = __toCommonJS(index_exports);
35
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
+
36
72
  // src/helpers/getValueByKey.ts
37
73
  var getValueByKey = (object, key, { inherited = true, ignore = [] } = {}) => {
38
74
  if (object === null || object === void 0 || ignore.includes(key) || !inherited && !Object.hasOwn(object, key)) {
@@ -84,51 +120,6 @@ var parsePathSelection = (path = "") => {
84
120
  };
85
121
  };
86
122
 
87
- // src/helpers/createPath.ts
88
- var createPath = (parentPath, key, parentIsArray) => {
89
- if (parentIsArray) {
90
- return parentPath + "[" + key + "]";
91
- }
92
- return parentPath ? parentPath + "." + key : String(key);
93
- };
94
-
95
- // src/helpers/isWalkable.ts
96
- var isWalkable = (value) => {
97
- return value !== null && typeof value === "object";
98
- };
99
-
100
- // src/each.ts
101
- var each = (data, callback) => {
102
- const walk = (value, path, key, parentIsArray) => {
103
- const currentPath = path || createPath("", key, parentIsArray);
104
- callback(key, value, currentPath);
105
- if (!isWalkable(value)) {
106
- return;
107
- }
108
- if (Array.isArray(value)) {
109
- value.forEach((item, index) => {
110
- walk(item, createPath(currentPath, index, true), index, true);
111
- });
112
- return;
113
- }
114
- Object.entries(value).forEach(([childKey, item]) => {
115
- walk(item, createPath(currentPath, childKey, false), childKey, false);
116
- });
117
- };
118
- if (!isWalkable(data)) {
119
- return;
120
- }
121
- if (Array.isArray(data)) {
122
- data.forEach((item, index) => {
123
- walk(item, createPath("", index, true), index, true);
124
- });
125
- return;
126
- }
127
- Object.entries(data).forEach(([key, value]) => {
128
- walk(value, createPath("", key, false), key, false);
129
- });
130
- };
131
-
132
123
  // src/pick.ts
133
124
  var pick = (data = {}, path = "", options = {}) => {
134
125
  const { cleanPath, keys } = parsePathSelection(path);
@@ -244,15 +235,17 @@ var map = (data, callback, store = {}) => {
244
235
  return store;
245
236
  };
246
237
 
247
- // src/merge.ts
248
- var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
238
+ // src/helpers/isPlainObject.ts
249
239
  var isPlainObject = (value) => {
250
- if (value === null || typeof value !== "object") {
251
- return false;
240
+ if (value !== null && typeof value === "object") {
241
+ const prototype = Object.getPrototypeOf(value);
242
+ return prototype === Object.prototype || prototype === null;
252
243
  }
253
- const prototype = Object.getPrototypeOf(value);
254
- return prototype === Object.prototype || prototype === null;
244
+ return false;
255
245
  };
246
+
247
+ // src/merge.ts
248
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
256
249
  var merge = (target, ...sources) => {
257
250
  const targetRecord = target;
258
251
  for (const source of sources) {
@@ -274,6 +267,27 @@ var merge = (target, ...sources) => {
274
267
  return target;
275
268
  };
276
269
 
270
+ // src/flush.ts
271
+ var flush = (data = {}, path) => {
272
+ if (path.length > 0) {
273
+ const keys = path.split(".");
274
+ const lastKey = keys.pop() ?? "";
275
+ const match = lastKey.match(/^(\w+)\[(\d+)\]$/);
276
+ let parent = getValueByPath(data, keys.join("."), { inherited: false });
277
+ let key = lastKey;
278
+ if (match) {
279
+ const [, arrayKey, index] = match;
280
+ const array = getValueByKey(parent, arrayKey, { inherited: false });
281
+ parent = Array.isArray(array) ? array : void 0;
282
+ key = index;
283
+ }
284
+ if (parent !== null && typeof parent === "object" && Object.hasOwn(parent, key)) {
285
+ delete parent[key];
286
+ }
287
+ }
288
+ return data;
289
+ };
290
+
277
291
  // src/deepEqual.ts
278
292
  var deepEqual = (x, y, store) => {
279
293
  if (Number.isNaN(x) && Number.isNaN(y)) {
@@ -331,6 +345,41 @@ var equalObjects = (x, y, store) => {
331
345
  // src/equally.ts
332
346
  var equally = (a, b) => deepEqual(a, b, /* @__PURE__ */ new WeakMap());
333
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
+
334
383
  // src/clone.ts
335
384
  var clone = (value) => {
336
385
  const store = /* @__PURE__ */ new WeakMap();
@@ -401,9 +450,11 @@ var stringify = (value, { fallback, onError, replacer, space } = {}) => {
401
450
  // Annotate the CommonJS export names for ESM import in node:
402
451
  0 && (module.exports = {
403
452
  clone,
453
+ diff,
404
454
  each,
405
455
  equally,
406
456
  extract,
457
+ flush,
407
458
  map,
408
459
  merge,
409
460
  parse,
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;
@@ -32,12 +56,16 @@ declare const merge: <T extends ObjwalkObject$1>(target: T, ...sources: unknown[
32
56
  type ObjwalkObject = Record<string, any>;
33
57
  declare const set: (data: ObjwalkObject | undefined, path: string, value: any) => ObjwalkObject;
34
58
 
59
+ declare const flush: (data: ObjwalkObject$1 | undefined, path: string) => ObjwalkObject$1;
60
+
35
61
  declare const equally: (a: unknown, b: unknown) => boolean;
36
62
 
63
+ declare const diff: (first: unknown, last: unknown) => DiffOperation[];
64
+
37
65
  declare const clone: <T>(value: T) => T;
38
66
 
39
67
  declare const parse: (raw: unknown, { fallback, onError, reviver }?: ParseOptions) => unknown;
40
68
 
41
69
  declare const stringify: (value: unknown, { fallback, onError, replacer, space }?: StringifyOptions) => string | undefined;
42
70
 
43
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type ParseOptions, type PickOptions, type StringifyOptions, type WalkKey, clone, each, equally, extract, 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;
@@ -32,12 +56,16 @@ declare const merge: <T extends ObjwalkObject$1>(target: T, ...sources: unknown[
32
56
  type ObjwalkObject = Record<string, any>;
33
57
  declare const set: (data: ObjwalkObject | undefined, path: string, value: any) => ObjwalkObject;
34
58
 
59
+ declare const flush: (data: ObjwalkObject$1 | undefined, path: string) => ObjwalkObject$1;
60
+
35
61
  declare const equally: (a: unknown, b: unknown) => boolean;
36
62
 
63
+ declare const diff: (first: unknown, last: unknown) => DiffOperation[];
64
+
37
65
  declare const clone: <T>(value: T) => T;
38
66
 
39
67
  declare const parse: (raw: unknown, { fallback, onError, reviver }?: ParseOptions) => unknown;
40
68
 
41
69
  declare const stringify: (value: unknown, { fallback, onError, replacer, space }?: StringifyOptions) => string | undefined;
42
70
 
43
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type ParseOptions, type PickOptions, type StringifyOptions, type WalkKey, clone, each, equally, extract, 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) {
@@ -239,6 +230,27 @@ var merge = (target, ...sources) => {
239
230
  return target;
240
231
  };
241
232
 
233
+ // src/flush.ts
234
+ 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];
249
+ }
250
+ }
251
+ return data;
252
+ };
253
+
242
254
  // src/deepEqual.ts
243
255
  var deepEqual = (x, y, store) => {
244
256
  if (Number.isNaN(x) && Number.isNaN(y)) {
@@ -296,6 +308,41 @@ var equalObjects = (x, y, store) => {
296
308
  // src/equally.ts
297
309
  var equally = (a, b) => deepEqual(a, b, /* @__PURE__ */ new WeakMap());
298
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
+
299
346
  // src/clone.ts
300
347
  var clone = (value) => {
301
348
  const store = /* @__PURE__ */ new WeakMap();
@@ -365,9 +412,11 @@ var stringify = (value, { fallback, onError, replacer, space } = {}) => {
365
412
  };
366
413
  export {
367
414
  clone,
415
+ diff,
368
416
  each,
369
417
  equally,
370
418
  extract,
419
+ flush,
371
420
  map,
372
421
  merge,
373
422
  parse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objwalk",
3
- "version": "1.4.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 or writes 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, 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,15 +28,17 @@ import { clone, each, equally, extract, map, merge, parse, pick, set, stringify
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
32
  | Reshape an object | `extract(source, mapping)`; mapping keys are destination paths, values are source paths. |
32
- | 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. |
33
34
  | Build a store while walking | `map(data, (key, value, store, path) => { ... }, store?)`; returns the store, ignores callback return values. |
34
35
  | Deep merge | `merge(target, ...sources)`; mutates and returns the target, merges plain objects, replaces arrays and non-plain values. |
35
36
  | Deep copy | `clone(value)`; preserves `Date`, `RegExp`, symbol keys, and circular references. |
36
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. |
37
39
 
38
- `set` and `merge` mutate their targets. Use `clone` first only when the task
39
- requires an independent copy. `each` and `map` do not support circular traversal;
40
+ `set`, `flush`, and `merge` mutate their targets. Use `clone` first only when the task
41
+ requires an independent copy. `each`, `map`, and `diff` do not support circular traversal;
40
42
  do not infer that capability from `clone` or `equally`.
41
43
 
42
44
  ## Paths and reshaping
@@ -49,10 +51,18 @@ do not infer that capability from `clone` or `equally`.
49
51
  | `user.{*}` | List object keys with `pick`. |
50
52
  | `items[n].title` | Map array items in `extract`, with `[n]` in both source and destination paths. |
51
53
 
52
- `[n]` is an `extract` mapping feature, not a general wildcard for `pick` or `set`.
54
+ `[n]` is an `extract` mapping feature, not a general wildcard for `pick`, `set`, or `flush`.
53
55
  An array of source paths in `extract` selects the first result that is not
54
56
  `undefined`, preserving `null`, `false`, `0`, and empty strings.
55
57
 
58
+ Use `flush` to remove a property rather than `set(data, path, undefined)`, which
59
+ leaves the property present. `flush` traverses and deletes only own properties.
60
+ Missing, nullish, or primitive parents and empty paths are left unchanged; no
61
+ containers are created and empty parents remain. Explicit array indexes work,
62
+ but deleting an item leaves a hole without shifting indexes or changing length.
63
+ Selection syntax (`{name,email}`, `{*}`) is not supported. Errors from getters,
64
+ proxies, or deleting non-configurable properties propagate.
65
+
56
66
  `pick` reads inherited properties by default. Use its existing options when own
57
67
  properties or ignored keys are required:
58
68
 
@@ -63,6 +73,51 @@ pick(data, 'user.name', {
63
73
  })
64
74
  ```
65
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
+
66
121
  ## JSON and ready values
67
122
 
68
123
  Call `parse(raw, options?)` directly with `raw: unknown`. The library chooses