objwalk 0.1.0 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Sergey Khalilov
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
15
+ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -13,7 +13,7 @@ npm install objwalk
13
13
  ## Usage
14
14
 
15
15
  ```ts
16
- import { each, extract, map, pick, set } from 'objwalk'
16
+ import { each, extract, map, merge, pick, set } from 'objwalk'
17
17
  ```
18
18
 
19
19
  ## pick
@@ -81,6 +81,21 @@ set(data, 'items[0].title', 'First')
81
81
 
82
82
  `set` mutates the object passed to it and returns the same object.
83
83
 
84
+ ## merge
85
+
86
+ Deeply merge plain objects into a target object.
87
+
88
+ ```ts
89
+ const target = { user: { name: 'Ada', flags: { admin: false } } }
90
+
91
+ merge(target, { user: { email: 'ada@example.com', flags: { admin: true } } })
92
+ // {
93
+ // user: { name: 'Ada', email: 'ada@example.com', flags: { admin: true } },
94
+ // }
95
+ ```
96
+
97
+ `merge` mutates and returns `target`. Sources are applied from left to right. Arrays and non-plain objects, such as `Date`, replace the existing value rather than being merged.
98
+
84
99
  ## extract
85
100
 
86
101
  Create a new object by mapping destination paths to source paths.
@@ -189,4 +204,5 @@ set(data, path, value)
189
204
  extract(data, mapping)
190
205
  each(data, callback)
191
206
  map(data, callback)
207
+ merge(target, ...sources)
192
208
  ```
package/dist/index.cjs CHANGED
@@ -23,21 +23,69 @@ __export(index_exports, {
23
23
  each: () => each,
24
24
  extract: () => extract,
25
25
  map: () => map,
26
+ merge: () => merge,
26
27
  pick: () => pick,
27
28
  set: () => set
28
29
  });
29
30
  module.exports = __toCommonJS(index_exports);
30
31
 
31
- // src/each.ts
32
- var isWalkable = (value) => {
33
- return value !== null && typeof value === "object";
32
+ // src/helpers/getValueByPath.ts
33
+ var arraySegmentRegex = /^(\w+)\[(\d+)\]$/;
34
+ var getValueByPath = (obj, path) => {
35
+ if (!path) return obj;
36
+ return path.split(".").reduce((result, current) => {
37
+ if (result === null || result === void 0) {
38
+ return void 0;
39
+ }
40
+ const match = current.match(arraySegmentRegex);
41
+ if (match) {
42
+ const [, key, index] = match;
43
+ const array = result[key];
44
+ return Array.isArray(array) ? array[parseInt(index, 10)] : void 0;
45
+ }
46
+ return result[current];
47
+ }, obj);
48
+ };
49
+
50
+ // src/helpers/parsePathSelection.ts
51
+ var selectionRegex = /(.*)\{([^}]+)\}$/;
52
+ var normalizeSelectionPath = (path) => {
53
+ return path.endsWith(".") ? path.slice(0, -1) : path;
54
+ };
55
+ var parseSelectedKeys = (selection) => {
56
+ if (selection.trim() === "*") {
57
+ return "all";
58
+ }
59
+ return selection.split(",").map((key) => key.trim()).filter(Boolean);
34
60
  };
61
+ var parsePathSelection = (path = "") => {
62
+ const matches = path.match(selectionRegex);
63
+ if (!matches) {
64
+ return {
65
+ cleanPath: path,
66
+ keys: []
67
+ };
68
+ }
69
+ return {
70
+ cleanPath: normalizeSelectionPath(matches[1]),
71
+ keys: parseSelectedKeys(matches[2])
72
+ };
73
+ };
74
+
75
+ // src/helpers/createPath.ts
35
76
  var createPath = (parentPath, key, parentIsArray) => {
36
77
  if (parentIsArray) {
37
78
  return parentPath + "[" + key + "]";
38
79
  }
39
80
  return parentPath ? parentPath + "." + key : String(key);
40
81
  };
82
+
83
+ // src/helpers/isWalkable.ts
84
+ var isWalkable = (value) => {
85
+ return value !== null && typeof value === "object";
86
+ };
87
+
88
+ // src/each.ts
41
89
  var each = (data, callback) => {
42
90
  const walk = (value, path, key, parentIsArray) => {
43
91
  const currentPath = path || createPath("", key, parentIsArray);
@@ -69,48 +117,6 @@ var each = (data, callback) => {
69
117
  });
70
118
  };
71
119
 
72
- // src/helpers/getValueByPath.ts
73
- var getValueByPath = (obj, path) => {
74
- if (!path) return obj;
75
- return path.split(".").reduce((result, current) => {
76
- if (result !== null && result !== void 0) {
77
- const match = current.match(/(w+)[(d+)]/);
78
- if (match) {
79
- const [, key, index] = match;
80
- const array = result[key];
81
- return array && Array.isArray(array) ? array[parseInt(index, 10)] : void 0;
82
- }
83
- return result[current];
84
- }
85
- return void 0;
86
- }, obj);
87
- };
88
-
89
- // src/helpers/parsePathSelection.ts
90
- var selectionRegex = /(.*){([^}]+)}$/;
91
- var normalizeSelectionPath = (path) => {
92
- return path.endsWith(".") ? path.slice(0, -1) : path;
93
- };
94
- var parseSelectedKeys = (selection) => {
95
- if (selection.trim() === "*") {
96
- return "all";
97
- }
98
- return selection.split(",").map((key) => key.trim()).filter(Boolean);
99
- };
100
- var parsePathSelection = (path = "") => {
101
- const matches = path.match(selectionRegex);
102
- if (!matches) {
103
- return {
104
- cleanPath: path,
105
- keys: []
106
- };
107
- }
108
- return {
109
- cleanPath: normalizeSelectionPath(matches[1]),
110
- keys: parseSelectedKeys(matches[2])
111
- };
112
- };
113
-
114
120
  // src/pick.ts
115
121
  var pick = (data = {}, path = "") => {
116
122
  const { cleanPath, keys } = parsePathSelection(path);
@@ -225,11 +231,42 @@ var map = (data, callback, store = {}) => {
225
231
  });
226
232
  return store;
227
233
  };
234
+
235
+ // src/merge.ts
236
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
237
+ var isPlainObject = (value) => {
238
+ if (value === null || typeof value !== "object") {
239
+ return false;
240
+ }
241
+ const prototype = Object.getPrototypeOf(value);
242
+ return prototype === Object.prototype || prototype === null;
243
+ };
244
+ var merge = (target, ...sources) => {
245
+ const targetRecord = target;
246
+ for (const source of sources) {
247
+ if (!isPlainObject(source)) {
248
+ continue;
249
+ }
250
+ for (const [key, sourceValue] of Object.entries(source)) {
251
+ if (unsafeKeys.has(key)) {
252
+ continue;
253
+ }
254
+ const targetValue = targetRecord[key];
255
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
256
+ merge(targetValue, sourceValue);
257
+ } else {
258
+ targetRecord[key] = sourceValue;
259
+ }
260
+ }
261
+ }
262
+ return target;
263
+ };
228
264
  // Annotate the CommonJS export names for ESM import in node:
229
265
  0 && (module.exports = {
230
266
  each,
231
267
  extract,
232
268
  map,
269
+ merge,
233
270
  pick,
234
271
  set
235
272
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  type WalkKey = string | number;
2
+
2
3
  type EachCallback = (key: WalkKey, value: any, path: string) => void;
3
4
  declare const each: (data: any, callback: EachCallback) => void;
4
5
 
@@ -11,7 +12,9 @@ declare const extract: (source: ObjwalkObject$1, mapping: ExtractMapping) => Obj
11
12
  type MapCallback<TStore extends Record<string, any> = Record<string, any>> = (key: WalkKey, value: any, store: TStore, path: string) => void;
12
13
  declare const map: <TStore extends Record<string, any> = Record<string, any>>(data: any, callback: MapCallback<TStore>, store?: TStore) => TStore;
13
14
 
15
+ declare const merge: <T extends ObjwalkObject$1>(target: T, ...sources: unknown[]) => T;
16
+
14
17
  type ObjwalkObject = Record<string, any>;
15
18
  declare const set: (data: ObjwalkObject | undefined, path: string, value: any) => ObjwalkObject;
16
19
 
17
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type WalkKey, each, extract, map, pick, set };
20
+ export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type WalkKey, each, extract, map, merge, pick, set };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  type WalkKey = string | number;
2
+
2
3
  type EachCallback = (key: WalkKey, value: any, path: string) => void;
3
4
  declare const each: (data: any, callback: EachCallback) => void;
4
5
 
@@ -11,7 +12,9 @@ declare const extract: (source: ObjwalkObject$1, mapping: ExtractMapping) => Obj
11
12
  type MapCallback<TStore extends Record<string, any> = Record<string, any>> = (key: WalkKey, value: any, store: TStore, path: string) => void;
12
13
  declare const map: <TStore extends Record<string, any> = Record<string, any>>(data: any, callback: MapCallback<TStore>, store?: TStore) => TStore;
13
14
 
15
+ declare const merge: <T extends ObjwalkObject$1>(target: T, ...sources: unknown[]) => T;
16
+
14
17
  type ObjwalkObject = Record<string, any>;
15
18
  declare const set: (data: ObjwalkObject | undefined, path: string, value: any) => ObjwalkObject;
16
19
 
17
- export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type WalkKey, each, extract, map, pick, set };
20
+ export { type EachCallback, type ExtractMapping, type MapCallback, type ObjwalkObject$1 as ObjwalkObject, type WalkKey, each, extract, map, merge, pick, set };
package/dist/index.js CHANGED
@@ -1,13 +1,60 @@
1
- // src/each.ts
2
- var isWalkable = (value) => {
3
- return value !== null && typeof value === "object";
1
+ // src/helpers/getValueByPath.ts
2
+ var arraySegmentRegex = /^(\w+)\[(\d+)\]$/;
3
+ var getValueByPath = (obj, path) => {
4
+ if (!path) return obj;
5
+ return path.split(".").reduce((result, current) => {
6
+ if (result === null || result === void 0) {
7
+ return void 0;
8
+ }
9
+ const match = current.match(arraySegmentRegex);
10
+ if (match) {
11
+ const [, key, index] = match;
12
+ const array = result[key];
13
+ return Array.isArray(array) ? array[parseInt(index, 10)] : void 0;
14
+ }
15
+ return result[current];
16
+ }, obj);
17
+ };
18
+
19
+ // src/helpers/parsePathSelection.ts
20
+ var selectionRegex = /(.*)\{([^}]+)\}$/;
21
+ var normalizeSelectionPath = (path) => {
22
+ return path.endsWith(".") ? path.slice(0, -1) : path;
23
+ };
24
+ var parseSelectedKeys = (selection) => {
25
+ if (selection.trim() === "*") {
26
+ return "all";
27
+ }
28
+ return selection.split(",").map((key) => key.trim()).filter(Boolean);
4
29
  };
30
+ var parsePathSelection = (path = "") => {
31
+ const matches = path.match(selectionRegex);
32
+ if (!matches) {
33
+ return {
34
+ cleanPath: path,
35
+ keys: []
36
+ };
37
+ }
38
+ return {
39
+ cleanPath: normalizeSelectionPath(matches[1]),
40
+ keys: parseSelectedKeys(matches[2])
41
+ };
42
+ };
43
+
44
+ // src/helpers/createPath.ts
5
45
  var createPath = (parentPath, key, parentIsArray) => {
6
46
  if (parentIsArray) {
7
47
  return parentPath + "[" + key + "]";
8
48
  }
9
49
  return parentPath ? parentPath + "." + key : String(key);
10
50
  };
51
+
52
+ // src/helpers/isWalkable.ts
53
+ var isWalkable = (value) => {
54
+ return value !== null && typeof value === "object";
55
+ };
56
+
57
+ // src/each.ts
11
58
  var each = (data, callback) => {
12
59
  const walk = (value, path, key, parentIsArray) => {
13
60
  const currentPath = path || createPath("", key, parentIsArray);
@@ -39,48 +86,6 @@ var each = (data, callback) => {
39
86
  });
40
87
  };
41
88
 
42
- // src/helpers/getValueByPath.ts
43
- var getValueByPath = (obj, path) => {
44
- if (!path) return obj;
45
- return path.split(".").reduce((result, current) => {
46
- if (result !== null && result !== void 0) {
47
- const match = current.match(/(w+)[(d+)]/);
48
- if (match) {
49
- const [, key, index] = match;
50
- const array = result[key];
51
- return array && Array.isArray(array) ? array[parseInt(index, 10)] : void 0;
52
- }
53
- return result[current];
54
- }
55
- return void 0;
56
- }, obj);
57
- };
58
-
59
- // src/helpers/parsePathSelection.ts
60
- var selectionRegex = /(.*){([^}]+)}$/;
61
- var normalizeSelectionPath = (path) => {
62
- return path.endsWith(".") ? path.slice(0, -1) : path;
63
- };
64
- var parseSelectedKeys = (selection) => {
65
- if (selection.trim() === "*") {
66
- return "all";
67
- }
68
- return selection.split(",").map((key) => key.trim()).filter(Boolean);
69
- };
70
- var parsePathSelection = (path = "") => {
71
- const matches = path.match(selectionRegex);
72
- if (!matches) {
73
- return {
74
- cleanPath: path,
75
- keys: []
76
- };
77
- }
78
- return {
79
- cleanPath: normalizeSelectionPath(matches[1]),
80
- keys: parseSelectedKeys(matches[2])
81
- };
82
- };
83
-
84
89
  // src/pick.ts
85
90
  var pick = (data = {}, path = "") => {
86
91
  const { cleanPath, keys } = parsePathSelection(path);
@@ -195,10 +200,41 @@ var map = (data, callback, store = {}) => {
195
200
  });
196
201
  return store;
197
202
  };
203
+
204
+ // src/merge.ts
205
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
206
+ var isPlainObject = (value) => {
207
+ if (value === null || typeof value !== "object") {
208
+ return false;
209
+ }
210
+ const prototype = Object.getPrototypeOf(value);
211
+ return prototype === Object.prototype || prototype === null;
212
+ };
213
+ var merge = (target, ...sources) => {
214
+ const targetRecord = target;
215
+ for (const source of sources) {
216
+ if (!isPlainObject(source)) {
217
+ continue;
218
+ }
219
+ for (const [key, sourceValue] of Object.entries(source)) {
220
+ if (unsafeKeys.has(key)) {
221
+ continue;
222
+ }
223
+ const targetValue = targetRecord[key];
224
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
225
+ merge(targetValue, sourceValue);
226
+ } else {
227
+ targetRecord[key] = sourceValue;
228
+ }
229
+ }
230
+ }
231
+ return target;
232
+ };
198
233
  export {
199
234
  each,
200
235
  extract,
201
236
  map,
237
+ merge,
202
238
  pick,
203
239
  set
204
240
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objwalk",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Small utilities for reading, writing, walking, and reshaping nested JavaScript objects.",
5
5
  "keywords": [
6
6
  "object",
@@ -18,6 +18,14 @@
18
18
  ],
19
19
  "license": "ISC",
20
20
  "author": "Sergey Khalilov <sergey.khalilov@gmail.com>",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/khalilov/objwalk.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/khalilov/objwalk/issues"
27
+ },
28
+ "homepage": "https://github.com/khalilov/objwalk#readme",
21
29
  "type": "module",
22
30
  "main": "./dist/index.cjs",
23
31
  "module": "./dist/index.js",
@@ -35,11 +43,12 @@
35
43
  ],
36
44
  "scripts": {
37
45
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
38
- "test": "echo \"Error: no test specified\" && exit 1",
46
+ "test": "vitest run",
39
47
  "pack:check": "npm pack --dry-run"
40
48
  },
41
49
  "devDependencies": {
42
50
  "tsup": "^8.5.1",
43
- "typescript": "^5.9.3"
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.1.10"
44
53
  }
45
54
  }