objwalk 1.3.1 → 1.4.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
@@ -4,6 +4,8 @@ Small utilities for reading, writing, walking, and reshaping nested JavaScript o
4
4
 
5
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.
6
6
 
7
+ > Coding agent? Load the [objwalk skill](skills/objwalk/SKILL.md).
8
+
7
9
  ## Install
8
10
 
9
11
  ```bash
@@ -13,7 +15,7 @@ npm install objwalk
13
15
  ## Usage
14
16
 
15
17
  ```ts
16
- import { each, equally, extract, map, merge, pick, set } from 'objwalk'
18
+ import { each, equally, extract, map, merge, parse, pick, set, stringify } from 'objwalk'
17
19
  ```
18
20
 
19
21
  ## pick
@@ -214,6 +216,90 @@ The callback receives:
214
216
 
215
217
  `map` returns `store`.
216
218
 
219
+ ## parse and stringify
220
+
221
+ Parse JSON or accept a ready value, and serialize values with optional error handling.
222
+ Exceptions return `undefined`, unless a fallback is provided or `onError` throws.
223
+
224
+ ```ts
225
+ import { parse, stringify, type ParseOptions, type StringifyOptions } from 'objwalk'
226
+
227
+ // Signatures:
228
+ // parse(raw: unknown, options?: ParseOptions): unknown
229
+ // stringify(value: unknown, options?: StringifyOptions): string | undefined
230
+
231
+ const value = parse('{"a":1}') // { a: 1 }, typed as unknown
232
+ const valueOrNull = parse('{', { fallback: null }) // null
233
+ const text = stringify(value, { fallback: 'null', space: 2 })
234
+ const object = { a: 1 }
235
+ const array = [1, 2]
236
+
237
+ parse(object) === object // true
238
+ parse(array) === array // true
239
+
240
+ parse('[]') // []
241
+ stringify({}) // '{}'
242
+ stringify([]) // '[]'
243
+ stringify(null) // 'null'
244
+ stringify('{}') // '"{}"'
245
+ ```
246
+
247
+ Root values can be objects, arrays, strings, numbers, booleans, or `null`;
248
+ neither function requires an expected shape or restricts values to objects.
249
+ `parse` returns `unknown`; successful parsing does not validate the expected data structure.
250
+ Validate the result before using it as a specific type. There is no `parse<T>()` overload.
251
+
252
+ `parse` accepts `unknown`. Plain objects (with `Object.prototype` or a `null` prototype) and
253
+ arrays are returned by reference, without cloning, serialization, mutation, or calling `reviver`.
254
+ Other inputs use `JSON.parse(typeof raw === 'string' ? raw : String(raw), reviver)`.
255
+ ArrayBuffer, DataView, and typed-array views use this text path; there is no binary decoder.
256
+ For example, a Node.js Buffer containing JSON text can be parsed through its native string conversion.
257
+ Errors during input recognition or string conversion follow the same `onError`/`fallback` contract.
258
+
259
+ | Option | `ParseOptions` | `StringifyOptions` |
260
+ | --- | --- | --- |
261
+ | `fallback` | `unknown`, default `undefined` | `string`, default `undefined` |
262
+ | `onError` | `(error: unknown) => void` | `(error: unknown) => void` |
263
+ | `reviver` | Native `JSON.parse` callback, only for the text path | — |
264
+ | `replacer` | — | Native callback or array of strings/numbers (also accepts `null`) |
265
+ | `space` | — | `string` or `number` for native JSON formatting |
266
+
267
+ Callbacks retain native `this` binding. For example, a reviver can transform or delete properties,
268
+ and a replacer can select or transform values:
269
+
270
+ ```ts
271
+ parse('{"count":2,"secret":true}', {
272
+ reviver: (key, value) => key === 'secret' ? undefined : value,
273
+ }) // { count: 2 }
274
+
275
+ stringify({ name: 'Ada', secret: true }, { replacer: ['name'], space: 2 })
276
+ // '{\n "name": "Ada"\n}'
277
+ ```
278
+
279
+ On an exception, `onError` receives the original error once, then the function returns `fallback`.
280
+ The callback's return value is ignored. Exceptions from `reviver`, `replacer`, `toJSON`, or getters
281
+ follow the same rule. If `onError` throws, its exception propagates to the caller:
282
+
283
+ ```ts
284
+ const requiredText = stringify(value, {
285
+ onError: (error) => {
286
+ throw error
287
+ },
288
+ }) // still string | undefined
289
+
290
+ stringify(1n, { fallback: 'null' }) // 'null' (BigInt throws natively)
291
+ stringify(undefined, { fallback: 'null' }) // undefined (no exception)
292
+ parse('false', { fallback: true }) // false
293
+ parse('null', { fallback: {} }) // null
294
+ ```
295
+
296
+ Fallbacks apply only to exceptions: valid `0`, `false`, `''`, and `null` are preserved.
297
+ Likewise, a reviver returning `undefined` for the root does not trigger a fallback.
298
+ Top-level `undefined`, functions, and symbols serialize to native `undefined` without calling
299
+ `onError`. Native handling of these values inside objects/arrays, `NaN`, `Infinity`, and `toJSON`
300
+ is preserved. Circular references throw natively and use the same error handling as BigInt.
301
+ The wrappers do not mutate inputs, undo callback side effects, log errors, or repair invalid JSON.
302
+
217
303
  ## Path syntax
218
304
 
219
305
  | Syntax | Description |
@@ -224,6 +310,15 @@ The callback receives:
224
310
  | `user.{*}` | List keys from one object. |
225
311
  | `items[n].title` | Map every item in an array during `extract`. |
226
312
 
313
+ ## Agent skill
314
+
315
+ The package includes an [objwalk skill](skills/objwalk/SKILL.md) in `skills/objwalk/`.
316
+ After installation, point your agent at `node_modules/objwalk/skills/objwalk/SKILL.md`.
317
+ If your agent uses a skill directory, link the entire `skills/objwalk` folder, not just `SKILL.md`.
318
+
319
+ The skill explains the existing object utilities and JSON helpers, and directs agents to use
320
+ them without redundant wrappers that only repeat object checks.
321
+
227
322
  ## API
228
323
 
229
324
  ```ts
@@ -234,4 +329,6 @@ each(data, callback)
234
329
  map(data, callback)
235
330
  merge(target, ...sources)
236
331
  equally(a, b)
332
+ parse(raw, options?) // unknown
333
+ stringify(value, options?) // string | undefined
237
334
  ```
package/dist/index.cjs CHANGED
@@ -26,8 +26,10 @@ __export(index_exports, {
26
26
  extract: () => extract,
27
27
  map: () => map,
28
28
  merge: () => merge,
29
+ parse: () => parse,
29
30
  pick: () => pick,
30
- set: () => set
31
+ set: () => set,
32
+ stringify: () => stringify
31
33
  });
32
34
  module.exports = __toCommonJS(index_exports);
33
35
 
@@ -363,6 +365,39 @@ var clone = (value) => {
363
365
  };
364
366
  return cloneValue(value);
365
367
  };
368
+
369
+ // src/parse.ts
370
+ var parse = (raw, { fallback, onError, reviver } = {}) => {
371
+ try {
372
+ if (Array.isArray(raw)) {
373
+ return raw;
374
+ }
375
+ if (typeof raw === "object" && raw !== null) {
376
+ const isBinary = raw instanceof ArrayBuffer || ArrayBuffer.isView(raw);
377
+ const prototype = Object.getPrototypeOf(raw);
378
+ if (isBinary === false && (prototype === Object.prototype || prototype === null)) {
379
+ return raw;
380
+ }
381
+ }
382
+ return JSON.parse(typeof raw === "string" ? raw : String(raw), reviver);
383
+ } catch (error) {
384
+ onError?.(error);
385
+ return fallback;
386
+ }
387
+ };
388
+
389
+ // src/stringify.ts
390
+ var stringify = (value, { fallback, onError, replacer, space } = {}) => {
391
+ try {
392
+ if (typeof replacer === "function") {
393
+ return JSON.stringify(value, replacer, space);
394
+ }
395
+ return JSON.stringify(value, replacer, space);
396
+ } catch (error) {
397
+ onError?.(error);
398
+ return fallback;
399
+ }
400
+ };
366
401
  // Annotate the CommonJS export names for ESM import in node:
367
402
  0 && (module.exports = {
368
403
  clone,
@@ -371,6 +406,8 @@ var clone = (value) => {
371
406
  extract,
372
407
  map,
373
408
  merge,
409
+ parse,
374
410
  pick,
375
- set
411
+ set,
412
+ stringify
376
413
  });
package/dist/index.d.cts CHANGED
@@ -3,6 +3,17 @@ type PickOptions = {
3
3
  inherited?: boolean;
4
4
  ignore?: readonly string[];
5
5
  };
6
+ type ParseOptions = {
7
+ fallback?: unknown;
8
+ onError?: (error: unknown) => void;
9
+ reviver?: Parameters<typeof JSON.parse>[1];
10
+ };
11
+ type StringifyOptions = {
12
+ fallback?: string;
13
+ onError?: (error: unknown) => void;
14
+ replacer?: ParseOptions['reviver'] | Parameters<typeof JSON.stringify>[1];
15
+ space?: string | number;
16
+ };
6
17
 
7
18
  type EachCallback = (key: WalkKey, value: any, path: string) => void;
8
19
  declare const each: (data: any, callback: EachCallback) => void;
@@ -25,4 +36,8 @@ declare const equally: (a: unknown, b: unknown) => boolean;
25
36
 
26
37
  declare const clone: <T>(value: T) => T;
27
38
 
28
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type PickOptions, type WalkKey, clone, each, equally, extract, map, merge, pick, set };
39
+ declare const parse: (raw: unknown, { fallback, onError, reviver }?: ParseOptions) => unknown;
40
+
41
+ declare const stringify: (value: unknown, { fallback, onError, replacer, space }?: StringifyOptions) => string | undefined;
42
+
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 };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,17 @@ type PickOptions = {
3
3
  inherited?: boolean;
4
4
  ignore?: readonly string[];
5
5
  };
6
+ type ParseOptions = {
7
+ fallback?: unknown;
8
+ onError?: (error: unknown) => void;
9
+ reviver?: Parameters<typeof JSON.parse>[1];
10
+ };
11
+ type StringifyOptions = {
12
+ fallback?: string;
13
+ onError?: (error: unknown) => void;
14
+ replacer?: ParseOptions['reviver'] | Parameters<typeof JSON.stringify>[1];
15
+ space?: string | number;
16
+ };
6
17
 
7
18
  type EachCallback = (key: WalkKey, value: any, path: string) => void;
8
19
  declare const each: (data: any, callback: EachCallback) => void;
@@ -25,4 +36,8 @@ declare const equally: (a: unknown, b: unknown) => boolean;
25
36
 
26
37
  declare const clone: <T>(value: T) => T;
27
38
 
28
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type PickOptions, type WalkKey, clone, each, equally, extract, map, merge, pick, set };
39
+ declare const parse: (raw: unknown, { fallback, onError, reviver }?: ParseOptions) => unknown;
40
+
41
+ declare const stringify: (value: unknown, { fallback, onError, replacer, space }?: StringifyOptions) => string | undefined;
42
+
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 };
package/dist/index.js CHANGED
@@ -330,6 +330,39 @@ var clone = (value) => {
330
330
  };
331
331
  return cloneValue(value);
332
332
  };
333
+
334
+ // src/parse.ts
335
+ var parse = (raw, { fallback, onError, reviver } = {}) => {
336
+ try {
337
+ if (Array.isArray(raw)) {
338
+ return raw;
339
+ }
340
+ if (typeof raw === "object" && raw !== null) {
341
+ const isBinary = raw instanceof ArrayBuffer || ArrayBuffer.isView(raw);
342
+ const prototype = Object.getPrototypeOf(raw);
343
+ if (isBinary === false && (prototype === Object.prototype || prototype === null)) {
344
+ return raw;
345
+ }
346
+ }
347
+ return JSON.parse(typeof raw === "string" ? raw : String(raw), reviver);
348
+ } catch (error) {
349
+ onError?.(error);
350
+ return fallback;
351
+ }
352
+ };
353
+
354
+ // src/stringify.ts
355
+ var stringify = (value, { fallback, onError, replacer, space } = {}) => {
356
+ try {
357
+ if (typeof replacer === "function") {
358
+ return JSON.stringify(value, replacer, space);
359
+ }
360
+ return JSON.stringify(value, replacer, space);
361
+ } catch (error) {
362
+ onError?.(error);
363
+ return fallback;
364
+ }
365
+ };
333
366
  export {
334
367
  clone,
335
368
  each,
@@ -337,6 +370,8 @@ export {
337
370
  extract,
338
371
  map,
339
372
  merge,
373
+ parse,
340
374
  pick,
341
- set
375
+ set,
376
+ stringify
342
377
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objwalk",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Small utilities for reading, writing, walking, and reshaping nested JavaScript objects.",
5
5
  "keywords": [
6
6
  "object",
@@ -39,7 +39,8 @@
39
39
  },
40
40
  "files": [
41
41
  "dist",
42
- "README.md"
42
+ "README.md",
43
+ "skills"
43
44
  ],
44
45
  "scripts": {
45
46
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
@@ -0,0 +1,96 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # objwalk
7
+
8
+ **objwalk is a library for working with JavaScript objects. Extra wrappers whose
9
+ only purpose is to check whether a value is an object before calling objwalk are
10
+ redundant.** Use the public functions directly within their input contracts.
11
+ Do not add `safePick`, `safeMerge`, `parseObject`, `isObjectPayload`, or a custom
12
+ JSON decoder to repeat behaviour already owned by the library.
13
+
14
+ Application-specific validation of required fields and allowed values still
15
+ belongs at untyped boundaries. `parse` returns `unknown`; it does not prove a
16
+ domain type. Other functions keep their declared input contracts: do not bypass
17
+ them with type assertions or assume every method accepts arbitrary `unknown`.
18
+
19
+ Named imports only:
20
+
21
+ ```ts
22
+ import { clone, each, equally, extract, map, merge, parse, pick, set, stringify } from 'objwalk'
23
+ ```
24
+
25
+ ## Choose the existing operation
26
+
27
+ | Need | Function and behaviour |
28
+ | --- | --- |
29
+ | Read a nested value | `pick(data, path, options?)`; unresolved paths return `undefined`. |
30
+ | Write a nested value | `set(data, path, value)`; creates intermediate containers, mutates and returns `data`. |
31
+ | 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
+ | Build a store while walking | `map(data, (key, value, store, path) => { ... }, store?)`; returns the store, ignores callback return values. |
34
+ | Deep merge | `merge(target, ...sources)`; mutates and returns the target, merges plain objects, replaces arrays and non-plain values. |
35
+ | Deep copy | `clone(value)`; preserves `Date`, `RegExp`, symbol keys, and circular references. |
36
+ | Deep comparison | `equally(a, b)`; supports `NaN`, `Date`, `RegExp`, and circular references. |
37
+
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
+ do not infer that capability from `clone` or `equally`.
41
+
42
+ ## Paths and reshaping
43
+
44
+ | Syntax | Use |
45
+ | --- | --- |
46
+ | `user.name` | Nested property. |
47
+ | `items[0].title` | Explicit array index. |
48
+ | `user.{name,email}` | Select several keys with `pick`; a single selected key returns its value. |
49
+ | `user.{*}` | List object keys with `pick`. |
50
+ | `items[n].title` | Map array items in `extract`, with `[n]` in both source and destination paths. |
51
+
52
+ `[n]` is an `extract` mapping feature, not a general wildcard for `pick` or `set`.
53
+ An array of source paths in `extract` selects the first result that is not
54
+ `undefined`, preserving `null`, `false`, `0`, and empty strings.
55
+
56
+ `pick` reads inherited properties by default. Use its existing options when own
57
+ properties or ignored keys are required:
58
+
59
+ ```ts
60
+ pick(data, 'user.name', {
61
+ inherited: false,
62
+ ignore: ['__proto__', 'prototype', 'constructor'],
63
+ })
64
+ ```
65
+
66
+ ## JSON and ready values
67
+
68
+ Call `parse(raw, options?)` directly with `raw: unknown`. The library chooses
69
+ between ready values and text; do not surround it with `typeof`, `Array.isArray`,
70
+ `ArrayBuffer.isView`, or prototype checks to make that choice.
71
+
72
+ - Plain objects with `Object.prototype` or a null prototype, and arrays, return
73
+ by the same reference. They are not copied or traversed; `reviver` is not called.
74
+ - Other inputs use `JSON.parse(typeof raw === 'string' ? raw : String(raw), reviver)`.
75
+ ArrayBuffer, DataView, and typed-array views take this text path. A Buffer with
76
+ JSON text can use its native string conversion; there is no binary decoder.
77
+ - Parsed roots may be objects, arrays, strings, numbers, booleans, or `null`.
78
+ There is no object-only mode and no `parse<T>()` overload.
79
+ - `stringify(value, options?)` always uses native `JSON.stringify`, including
80
+ `replacer` callback/array and `space`. `stringify('{}')` returns `'"{}"'`.
81
+
82
+ Both functions return `undefined` on an exception by default. Options include
83
+ `fallback` and `onError(error: unknown)`. `parse` also accepts native `reviver`.
84
+ `ParseOptions` and `StringifyOptions` are public named type exports.
85
+
86
+ `onError` receives the original exception once. Its return value is ignored;
87
+ if it throws, that exception propagates. Errors from input recognition, string
88
+ conversion, reviver, replacer, toJSON, and getters follow the same contract.
89
+
90
+ Fallback applies only to exceptions. Preserve valid falsy results and native
91
+ `undefined`: `stringify(undefined, { fallback: 'null' })` still returns
92
+ `undefined`. Neither fallback nor a throwing `onError` changes stringify's
93
+ return type of `string | undefined`.
94
+
95
+ Full usage: [README](../../README.md). In a consumer project, read the installed
96
+ package's README and declarations when checking the available version's API.