assign-gingerly 0.0.2 → 0.0.4

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
@@ -1,13 +1,15 @@
1
- # assign-gingerly
1
+ # assign-gingerly and assign-tentatively
2
2
 
3
3
  [![Playwright Tests](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml/badge.svg?branch=baseline)](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml)
4
4
  [![NPM version](https://badge.fury.io/js/assign-gingerly.png)](http://badge.fury.io/js/assign-gingerly)
5
5
  [![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/assign-gingerly?style=for-the-badge)](https://bundlephobia.com/result?p=assign-gingerly)
6
6
  <img src="http://img.badgesize.io/https://cdn.jsdelivr.net/npm/assign-gingerly?compression=gzip">
7
7
 
8
- This package provides a utility function for carefully merging one object into another.
8
+ This package provides two utility functions for carefully merging one object into another.
9
9
 
10
- It builds on Object.assign. It adds support for:
10
+ ## assignGingerly
11
+
12
+ assignGingerly builds on Object.assign. assign-gingerly adds support for:
11
13
 
12
14
  1. Carefully merging in nested properties.
13
15
  2. Dependency injection based on a mapping protocol.
@@ -61,6 +63,176 @@ console.log(obj);
61
63
 
62
64
  When the right hand side of an expression is an object, assignGingerly is recursively applied (passing the third argument in if applicable, which will be discussed below)
63
65
 
66
+ ## Example 4 - Incrementing values with !inc command
67
+
68
+ The `!inc` command allows you to increment numeric values:
69
+
70
+ ```TypeScript
71
+ const obj = {
72
+ a: {
73
+ b: {
74
+ c: 2
75
+ }
76
+ }
77
+ };
78
+ assignGingerly(obj, {
79
+ '!inc ?.a?.b?.c': 3,
80
+ '!inc ?.a?.d?.e': -2
81
+ });
82
+ console.log(obj);
83
+ // {
84
+ // a: {
85
+ // b: { c: 5 }, // 2 + 3 = 5
86
+ // d: { e: -2 } // non-existent path created with value -2
87
+ // }
88
+ // }
89
+ ```
90
+
91
+ The `!inc` command syntax is `!inc <path>` where the path can use the `?.` nested notation. The right-hand side value is added to the existing value using `+=`. If the path doesn't exist, it's created and set directly to the value. Non-numeric increments will allow JavaScript to throw its natural error.
92
+
93
+ ## Example 5 - Toggling boolean values with !toggle command
94
+
95
+ The `!toggle` command allows you to toggle boolean values either immediately or after a delay:
96
+
97
+ ```TypeScript
98
+ const obj = {
99
+ a: {
100
+ b: {
101
+ c: true
102
+ }
103
+ }
104
+ };
105
+ assignGingerly(obj, {
106
+ '!toggle ?.a?.b?.c': 0, // Toggle immediately
107
+ '!toggle ?.a?.d?.e': 20 // Toggle after 20ms
108
+ });
109
+ console.log(obj);
110
+ // {
111
+ // a: {
112
+ // b: { c: false } // Toggled immediately
113
+ // // d doesn't exist yet
114
+ // }
115
+ // }
116
+
117
+ setTimeout(() => {
118
+ console.log(obj);
119
+ // {
120
+ // a: {
121
+ // b: { c: false },
122
+ // d: { e: true } // Created and toggled after 20ms
123
+ // }
124
+ // }
125
+ }, 40);
126
+ ```
127
+
128
+ The `!toggle` command syntax is `!toggle <path>` where the path can use the `?.` nested notation. The right-hand side value determines the behavior:
129
+ - **RHS = 0**: Toggle the existing value immediately (non-existent paths are not created)
130
+ - **RHS > 0**: Schedule the toggle to happen after N milliseconds (non-existent paths are created and initialized to `true`)
131
+
132
+ For existing values, the toggle is performed using JavaScript's logical NOT operator (`!value`). Non-numeric delay values will be passed to `setTimeout` and may throw an error.
133
+
134
+ ## Example 6 - Deleting properties with !delete command
135
+
136
+ The `!delete` command allows you to delete properties either immediately or after a delay:
137
+
138
+ ```TypeScript
139
+ const obj = {
140
+ a: {
141
+ b: {
142
+ c: true,
143
+ d: 'hello'
144
+ }
145
+ }
146
+ };
147
+ assignGingerly(obj, {
148
+ '!delete ?.a?.b?.c': 0, // Delete immediately
149
+ '!delete ?.a?.b': 20 // Delete after 20ms
150
+ });
151
+ console.log(obj);
152
+ // {
153
+ // a: {
154
+ // b: { d: 'hello' } // c deleted immediately
155
+ // }
156
+ // }
157
+
158
+ setTimeout(() => {
159
+ console.log(obj);
160
+ // {
161
+ // a: {} // b deleted after 20ms
162
+ // }
163
+ }, 40);
164
+ ```
165
+
166
+ The `!delete` command syntax is `!delete <path>` where the path can use the `?.` nested notation. The right-hand side value determines the behavior:
167
+ - **RHS = 0**: Delete the final property immediately (non-existent paths are silently skipped)
168
+ - **RHS > 0**: Schedule the deletion to happen after N milliseconds (non-existent paths are silently skipped)
169
+
170
+ **Important**: The `!delete` command only deletes the **final property** in the path. The entire nested chain is not deleted. For example, `'!delete ?.a?.b?.c': 0` only deletes property `c`, leaving the structure `a.b` intact. If any intermediate path doesn't exist, the command is silently skipped without error.
171
+
172
+ ## Example 7 - Reversible assignments with assignTentatively
173
+
174
+ The `assignTentatively` function works like `assignGingerly` but with a powerful addition: **reversibility**. It tracks changes and generates a reversal object that can undo all modifications:
175
+
176
+ ```TypeScript
177
+ import assignTentatively from 'assign-gingerly/assignTentatively';
178
+
179
+ const obj = { f: { g: 'hello' } };
180
+ const reversal = {};
181
+
182
+ assignTentatively(obj, {
183
+ '?.style?.height': '15px',
184
+ '?.a?.b?.c': {
185
+ d: 'hello',
186
+ e: 'world'
187
+ },
188
+ '?.f?.g': 'bye'
189
+ }, { reversal });
190
+
191
+ console.log(obj);
192
+ // {
193
+ // f: { g: 'bye' },
194
+ // style: { height: '15px' },
195
+ // a: { b: { c: { d: 'hello', e: 'world' } } }
196
+ // }
197
+
198
+ console.log(reversal);
199
+ // {
200
+ // '!delete ?.a': 0,
201
+ // '!delete ?.style': 0,
202
+ // '?.f?.g': 'hello'
203
+ // }
204
+
205
+ // Later, restore to original state:
206
+ assignTentatively(obj, reversal);
207
+ console.log(obj);
208
+ // {
209
+ // f: { g: 'hello' }
210
+ // }
211
+ ```
212
+
213
+ **Key differences from assignGingerly:**
214
+ - **No setTimeout support**: All `!toggle`, `!inc`, and `!delete` commands execute immediately, regardless of the RHS value
215
+ - **No registry/DI support**: Dependency injection features are not available (pass it in and it will be ignored)
216
+ - **Reversal tracking**: Maintains a reversal object that records:
217
+ - **Original values** of modified existing properties
218
+ - **!delete commands** for newly created top-level paths (e.g., `!delete ?.a` for paths created under `a`)
219
+ - **Original values** for deleted properties
220
+
221
+ **Reversal guarantee:**
222
+ ```JavaScript
223
+ const reversal = {};
224
+ const obj = {...originalObj};
225
+ const string1 = JSON.stringify(obj);
226
+
227
+ assignTentatively(obj, sourceChanges, { reversal });
228
+ assignTentatively(obj, reversal);
229
+
230
+ const string2 = JSON.stringify(obj);
231
+ console.log(string1 === string2); // true
232
+ ```
233
+
234
+ This guarantees that applying the reversal object restores the object to its exact original state.
235
+
64
236
  ## Dependency injection based on a registry object and a Symbolic reference
65
237
 
66
238
  ```Typescript
@@ -96,20 +268,18 @@ baseRegistry.push([
96
268
  map: {
97
269
  [isHappy]: 'isHappy'
98
270
  },
99
- spawn: MyEnhancement
271
+ spawn: MyEnhancement,
100
272
  },{
101
273
 
102
274
  map: {
103
275
  [isMellow]: 'isMellow'
104
276
  },
105
- spawn: async () => {
106
- return YourEnhancement;
107
- }
277
+ spawn: YourEnhancement,
108
278
  }
109
279
  ]);
110
280
  //end of dependency injection
111
281
 
112
- const asyncResult = await assignGingerly({}, {
282
+ const result = assignGingerly({}, {
113
283
  [isHappy]: true,
114
284
  [isMellow]: true,
115
285
  '?.style.height': '40px',
@@ -117,7 +287,7 @@ const asyncResult = await assignGingerly({}, {
117
287
  }, {
118
288
  registry: BaseRegistry
119
289
  });
120
- asyncResult.set[isMellow] = false;
290
+ result.set[isMellow] = false;
121
291
  ```
122
292
 
123
293
  The assignGingerly searches the registry for any items that has a mapping with a matching symbol of isHappy and isMellow, and if found, sees if it already has an instance of the spawn class associated with the first passed in parameter. If no such instance is found, it instantiates one, associates the instance with the first parameter, then sets the property value.
@@ -126,12 +296,10 @@ It also adds a lazy property to the first passed in parameter, "set", which retu
126
296
 
127
297
  The suggestion to use Symbol.for with a guid, as opposed to just Symbol(), is based on some negative experiences I've had with multiple versions of the same library being referenced, but is not required. Regular symbols could also be used when that risk can be avoided.
128
298
 
129
- Note that the example above is the first time we mention async. This is only necessary if you wish to work directly with the merged object. This allows for lazy loading of the spawning class, which can be useful for large applications that don't need to download all the classes at once. If you are just "depositing" values into the object, no need to await for anything. Also, the assignGingerly should first do all the class instantiations that are already loaded (where the class constructor is specified in spawn), and then does all the lazy loaded ones.
130
-
131
299
  ## Support for JSON assignment with Symbol.for symbols
132
300
 
133
301
  ```JavaScript
134
- const asyncResult = await assignGingerly({}, {
302
+ const result = assignGingerly({}, {
135
303
  "[Symbol.for('TFWsx0YH5E6eSfhE7zfLxA')]": true,
136
304
  "[Symbol.for('BqnnTPWRHkWdVGWcGQoAiw')]": true,
137
305
  '?.style.height': '40px',
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Map to store spawned instances associated with objects
3
+ */
4
+ const instanceMap = new WeakMap();
5
+ /**
6
+ * Base registry class for managing dependency injection
7
+ */
8
+ export class BaseRegistry {
9
+ items = [];
10
+ push(items) {
11
+ if (Array.isArray(items)) {
12
+ this.items.push(...items);
13
+ }
14
+ else {
15
+ this.items.push(items);
16
+ }
17
+ }
18
+ getItems() {
19
+ return this.items;
20
+ }
21
+ findBySymbol(symbol) {
22
+ return this.items.find(item => {
23
+ const map = item.map;
24
+ return Object.keys(map).some(key => {
25
+ if (typeof key === 'symbol' || (typeof map[key] === 'symbol')) {
26
+ return key === symbol || map[key] === symbol;
27
+ }
28
+ return false;
29
+ }) || Object.getOwnPropertySymbols(map).some(sym => sym === symbol);
30
+ });
31
+ }
32
+ }
33
+ /**
34
+ * Helper function to check if a string key represents a Symbol.for expression
35
+ */
36
+ function isSymbolForKey(key) {
37
+ return key.startsWith('[Symbol.for(') && key.endsWith(')]');
38
+ }
39
+ /**
40
+ * Helper function to extract the symbol key from a Symbol.for string
41
+ */
42
+ function parseSymbolForKey(key) {
43
+ const match = key.match(/^\[Symbol\.for\(['"](.+)['"]\)\]$/);
44
+ if (match && match[1]) {
45
+ return Symbol.for(match[1]);
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Helper function to check if a key represents an !inc command
51
+ */
52
+ function isIncCommand(key) {
53
+ return key.startsWith('!inc ');
54
+ }
55
+ /**
56
+ * Helper function to parse an !inc command and extract the path
57
+ */
58
+ function parseIncCommand(key) {
59
+ if (!isIncCommand(key)) {
60
+ return null;
61
+ }
62
+ return key.substring(5); // Remove '!inc ' prefix
63
+ }
64
+ /**
65
+ * Helper function to check if a key represents a !toggle command
66
+ */
67
+ function isToggleCommand(key) {
68
+ return key.startsWith('!toggle ');
69
+ }
70
+ /**
71
+ * Helper function to parse a !toggle command and extract the path
72
+ */
73
+ function parseToggleCommand(key) {
74
+ if (!isToggleCommand(key)) {
75
+ return null;
76
+ }
77
+ return key.substring(8); // Remove '!toggle ' prefix
78
+ }
79
+ /**
80
+ * Helper function to check if a key represents a !delete command
81
+ */
82
+ function isDeleteCommand(key) {
83
+ return key.startsWith('!delete ');
84
+ }
85
+ /**
86
+ * Helper function to parse a !delete command and extract the path
87
+ */
88
+ function parseDeleteCommand(key) {
89
+ if (!isDeleteCommand(key)) {
90
+ return null;
91
+ }
92
+ return key.substring(8); // Remove '!delete ' prefix
93
+ }
94
+ /**
95
+ * Helper function to parse a path string with ?. notation
96
+ */
97
+ function parsePath(path) {
98
+ return path
99
+ .split('.')
100
+ .map(part => part.replace(/\?/g, ''))
101
+ .filter(part => part.length > 0);
102
+ }
103
+ /**
104
+ * Helper function to check if a path starts with ?. notation
105
+ */
106
+ function isNestedPath(path) {
107
+ return path.startsWith('?.');
108
+ }
109
+ /**
110
+ * Helper function to get or create a nested object
111
+ */
112
+ function ensureNestedPath(obj, pathParts) {
113
+ let current = obj;
114
+ for (const part of pathParts.slice(0, -1)) {
115
+ if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
116
+ current[part] = {};
117
+ }
118
+ current = current[part];
119
+ }
120
+ return current;
121
+ }
122
+ /**
123
+ * Main assignGingerly function
124
+ */
125
+ export function assignGingerly(target, source, options) {
126
+ if (!target || typeof target !== 'object') {
127
+ return target;
128
+ }
129
+ const registry = options?.registry instanceof BaseRegistry
130
+ ? options.registry
131
+ : options?.registry
132
+ ? new options.registry()
133
+ : undefined;
134
+ // Convert Symbol.for string keys to actual symbols
135
+ const processedSource = {};
136
+ for (const key of Object.keys(source)) {
137
+ if (isSymbolForKey(key)) {
138
+ const symbol = parseSymbolForKey(key);
139
+ if (symbol) {
140
+ processedSource[symbol] = source[key];
141
+ }
142
+ else {
143
+ // Invalid Symbol.for format - treat as regular string key
144
+ processedSource[key] = source[key];
145
+ }
146
+ }
147
+ else {
148
+ processedSource[key] = source[key];
149
+ }
150
+ }
151
+ // Copy over actual symbol keys
152
+ for (const sym of Object.getOwnPropertySymbols(source)) {
153
+ processedSource[sym] = source[sym];
154
+ }
155
+ // First pass: handle all non-symbol keys and sync operations
156
+ for (const key of Object.keys(processedSource)) {
157
+ const value = processedSource[key];
158
+ // Handle !inc commands
159
+ if (isIncCommand(key)) {
160
+ const path = parseIncCommand(key);
161
+ if (path) {
162
+ const pathParts = parsePath(path);
163
+ const lastKey = pathParts[pathParts.length - 1];
164
+ const parent = ensureNestedPath(target, pathParts);
165
+ // If the path doesn't exist, set it directly to the value
166
+ if (!(lastKey in parent)) {
167
+ parent[lastKey] = value;
168
+ }
169
+ else {
170
+ // Path exists, apply increment: oldValue += newValue
171
+ parent[lastKey] += value;
172
+ }
173
+ }
174
+ continue;
175
+ }
176
+ // Handle !toggle commands
177
+ if (isToggleCommand(key)) {
178
+ const path = parseToggleCommand(key);
179
+ if (path) {
180
+ const delay = value;
181
+ if (delay === 0) {
182
+ // Immediate toggle
183
+ const pathParts = parsePath(path);
184
+ const lastKey = pathParts[pathParts.length - 1];
185
+ const parent = ensureNestedPath(target, pathParts);
186
+ if (lastKey in parent) {
187
+ // Path exists, toggle it
188
+ parent[lastKey] = !parent[lastKey];
189
+ }
190
+ // If path doesn't exist, don't create it for immediate toggle
191
+ }
192
+ else {
193
+ // Delayed toggle using setTimeout
194
+ setTimeout(() => {
195
+ const pathParts = parsePath(path);
196
+ const lastKey = pathParts[pathParts.length - 1];
197
+ const parent = ensureNestedPath(target, pathParts);
198
+ if (lastKey in parent) {
199
+ // Path exists, toggle it
200
+ parent[lastKey] = !parent[lastKey];
201
+ }
202
+ else {
203
+ // Path doesn't exist, initialize to true
204
+ parent[lastKey] = true;
205
+ }
206
+ }, delay);
207
+ }
208
+ }
209
+ continue;
210
+ }
211
+ // Handle !delete commands
212
+ if (isDeleteCommand(key)) {
213
+ const path = parseDeleteCommand(key);
214
+ if (path) {
215
+ const delay = value;
216
+ if (delay === 0) {
217
+ // Immediate delete
218
+ const pathParts = parsePath(path);
219
+ if (pathParts.length > 0) {
220
+ const lastKey = pathParts[pathParts.length - 1];
221
+ const parentPathParts = pathParts.slice(0, -1);
222
+ // Navigate to parent without creating intermediate paths
223
+ let parent = target;
224
+ let canDelete = true;
225
+ for (const part of parentPathParts) {
226
+ if (!(part in parent) || typeof parent[part] !== 'object' || parent[part] === null) {
227
+ canDelete = false;
228
+ break;
229
+ }
230
+ parent = parent[part];
231
+ }
232
+ if (canDelete && lastKey in parent) {
233
+ delete parent[lastKey];
234
+ }
235
+ }
236
+ }
237
+ else {
238
+ // Delayed delete using setTimeout
239
+ setTimeout(() => {
240
+ const pathParts = parsePath(path);
241
+ if (pathParts.length > 0) {
242
+ const lastKey = pathParts[pathParts.length - 1];
243
+ const parentPathParts = pathParts.slice(0, -1);
244
+ // Navigate to parent without creating intermediate paths
245
+ let parent = target;
246
+ let canDelete = true;
247
+ for (const part of parentPathParts) {
248
+ if (!(part in parent) || typeof parent[part] !== 'object' || parent[part] === null) {
249
+ canDelete = false;
250
+ break;
251
+ }
252
+ parent = parent[part];
253
+ }
254
+ if (canDelete && lastKey in parent) {
255
+ delete parent[lastKey];
256
+ }
257
+ }
258
+ }, delay);
259
+ }
260
+ }
261
+ continue;
262
+ }
263
+ if (isNestedPath(key)) {
264
+ const pathParts = parsePath(key);
265
+ const lastKey = pathParts[pathParts.length - 1];
266
+ const parent = ensureNestedPath(target, pathParts);
267
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
268
+ // Recursively apply assignGingerly for nested objects
269
+ if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
270
+ parent[lastKey] = {};
271
+ }
272
+ assignGingerly(parent[lastKey], value, options);
273
+ }
274
+ else {
275
+ parent[lastKey] = value;
276
+ }
277
+ }
278
+ else {
279
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
280
+ // Recursively apply assignGingerly for nested objects
281
+ if (!(key in target) || typeof target[key] !== 'object') {
282
+ target[key] = {};
283
+ }
284
+ assignGingerly(target[key], value, options);
285
+ }
286
+ else {
287
+ target[key] = value;
288
+ }
289
+ }
290
+ }
291
+ // Second pass: handle symbol keys for dependency injection
292
+ const symbols = Object.getOwnPropertySymbols(processedSource);
293
+ for (const sym of symbols) {
294
+ const value = processedSource[sym];
295
+ if (registry) {
296
+ const registryItem = registry.findBySymbol(sym);
297
+ if (registryItem) {
298
+ // Get or initialize the instances map for this target
299
+ if (!instanceMap.has(target)) {
300
+ instanceMap.set(target, new Map());
301
+ }
302
+ const instances = instanceMap.get(target);
303
+ // Check if instance already exists
304
+ let instance = instances.get(sym);
305
+ if (!instance) {
306
+ const SpawnClass = registryItem.spawn;
307
+ instance = new SpawnClass();
308
+ instances.set(sym, instance);
309
+ }
310
+ // Find the mapped property name
311
+ const mappedKey = registryItem.map[sym];
312
+ if (mappedKey && instance && typeof instance === 'object') {
313
+ instance[mappedKey] = value;
314
+ }
315
+ }
316
+ }
317
+ }
318
+ // Add lazy 'set' property that returns a proxy
319
+ if (registry && !('set' in target)) {
320
+ Object.defineProperty(target, 'set', {
321
+ get() {
322
+ return new Proxy({}, {
323
+ set: (_, prop, value) => {
324
+ if (typeof prop === 'symbol') {
325
+ const registryItem = registry.findBySymbol(prop);
326
+ if (registryItem) {
327
+ if (!instanceMap.has(target)) {
328
+ instanceMap.set(target, new Map());
329
+ }
330
+ const instances = instanceMap.get(target);
331
+ let instance = instances.get(prop);
332
+ if (!instance) {
333
+ const SpawnClass = registryItem.spawn;
334
+ instance = new SpawnClass();
335
+ instances.set(prop, instance);
336
+ }
337
+ const mappedKey = registryItem.map[prop];
338
+ if (mappedKey && instance && typeof instance === 'object') {
339
+ instance[mappedKey] = value;
340
+ }
341
+ }
342
+ }
343
+ return true;
344
+ },
345
+ });
346
+ },
347
+ configurable: true,
348
+ });
349
+ }
350
+ return target;
351
+ }
352
+ export default assignGingerly;
package/index.js CHANGED
@@ -1,224 +1,3 @@
1
- /**
2
- * Map to store spawned instances associated with objects
3
- */
4
- const instanceMap = new WeakMap();
5
- /**
6
- * Base registry class for managing dependency injection
7
- */
8
- export class BaseRegistry {
9
- items = [];
10
- push(items) {
11
- if (Array.isArray(items)) {
12
- this.items.push(...items);
13
- }
14
- else {
15
- this.items.push(items);
16
- }
17
- }
18
- getItems() {
19
- return this.items;
20
- }
21
- findBySymbol(symbol) {
22
- return this.items.find(item => {
23
- const map = item.map;
24
- return Object.keys(map).some(key => {
25
- if (typeof key === 'symbol' || (typeof map[key] === 'symbol')) {
26
- return key === symbol || map[key] === symbol;
27
- }
28
- return false;
29
- }) || Object.getOwnPropertySymbols(map).some(sym => sym === symbol);
30
- });
31
- }
32
- }
33
- /**
34
- * Helper function to check if a string key represents a Symbol.for expression
35
- */
36
- function isSymbolForKey(key) {
37
- return key.startsWith('[Symbol.for(') && key.endsWith(')]');
38
- }
39
- /**
40
- * Helper function to extract the symbol key from a Symbol.for string
41
- */
42
- function parseSymbolForKey(key) {
43
- const match = key.match(/^\[Symbol\.for\(['"](.+)['"]\)\]$/);
44
- if (match && match[1]) {
45
- return Symbol.for(match[1]);
46
- }
47
- return null;
48
- }
49
- /**
50
- * Helper function to parse a path string with ?. notation
51
- */
52
- function parsePath(path) {
53
- return path
54
- .split('.')
55
- .map(part => part.replace(/\?/g, ''))
56
- .filter(part => part.length > 0);
57
- }
58
- /**
59
- * Helper function to check if a path starts with ?. notation
60
- */
61
- function isNestedPath(path) {
62
- return path.startsWith('?.');
63
- }
64
- /**
65
- * Helper function to get or create a nested object
66
- */
67
- function ensureNestedPath(obj, pathParts) {
68
- let current = obj;
69
- for (const part of pathParts.slice(0, -1)) {
70
- if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
71
- current[part] = {};
72
- }
73
- current = current[part];
74
- }
75
- return current;
76
- }
77
- /**
78
- * Main assignGingerly function
79
- */
80
- export async function assignGingerly(target, source, options) {
81
- if (!target || typeof target !== 'object') {
82
- return target;
83
- }
84
- const registry = options?.registry instanceof BaseRegistry
85
- ? options.registry
86
- : options?.registry
87
- ? new options.registry()
88
- : undefined;
89
- // Track promises for async spawning
90
- const asyncSpawns = [];
91
- // Convert Symbol.for string keys to actual symbols
92
- const processedSource = {};
93
- for (const key of Object.keys(source)) {
94
- if (isSymbolForKey(key)) {
95
- const symbol = parseSymbolForKey(key);
96
- if (symbol) {
97
- processedSource[symbol] = source[key];
98
- }
99
- else {
100
- // Invalid Symbol.for format - treat as regular string key
101
- processedSource[key] = source[key];
102
- }
103
- }
104
- else {
105
- processedSource[key] = source[key];
106
- }
107
- }
108
- // Copy over actual symbol keys
109
- for (const sym of Object.getOwnPropertySymbols(source)) {
110
- processedSource[sym] = source[sym];
111
- }
112
- // First pass: handle all non-symbol keys and sync operations
113
- for (const key of Object.keys(processedSource)) {
114
- const value = processedSource[key];
115
- if (isNestedPath(key)) {
116
- const pathParts = parsePath(key);
117
- const lastKey = pathParts[pathParts.length - 1];
118
- const parent = ensureNestedPath(target, pathParts);
119
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
120
- // Recursively apply assignGingerly for nested objects
121
- if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
122
- parent[lastKey] = {};
123
- }
124
- await assignGingerly(parent[lastKey], value, options);
125
- }
126
- else {
127
- parent[lastKey] = value;
128
- }
129
- }
130
- else {
131
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
132
- // Recursively apply assignGingerly for nested objects
133
- if (!(key in target) || typeof target[key] !== 'object') {
134
- target[key] = {};
135
- }
136
- await assignGingerly(target[key], value, options);
137
- }
138
- else {
139
- target[key] = value;
140
- }
141
- }
142
- }
143
- // Second pass: handle symbol keys for dependency injection
144
- const symbols = Object.getOwnPropertySymbols(processedSource);
145
- for (const sym of symbols) {
146
- const value = processedSource[sym];
147
- if (registry) {
148
- const registryItem = registry.findBySymbol(sym);
149
- if (registryItem) {
150
- // Get or initialize the instances map for this target
151
- if (!instanceMap.has(target)) {
152
- instanceMap.set(target, new Map());
153
- }
154
- const instances = instanceMap.get(target);
155
- // Check if instance already exists
156
- let instance = instances.get(sym);
157
- if (!instance) {
158
- // Check if spawn is a constructor or a promise
159
- const SpawnClass = await Promise.resolve(registryItem.spawn);
160
- instance = new SpawnClass();
161
- instances.set(sym, instance);
162
- }
163
- // Find the mapped property name
164
- const mappedKey = registryItem.map[sym];
165
- if (mappedKey && instance && typeof instance === 'object') {
166
- instance[mappedKey] = value;
167
- }
168
- }
169
- }
170
- }
171
- // Add lazy 'set' property that returns a proxy
172
- if (registry && !('set' in target)) {
173
- Object.defineProperty(target, 'set', {
174
- get() {
175
- return new Proxy({}, {
176
- set: (_, prop, value) => {
177
- if (typeof prop === 'symbol') {
178
- const registryItem = registry.findBySymbol(prop);
179
- if (registryItem) {
180
- if (!instanceMap.has(target)) {
181
- instanceMap.set(target, new Map());
182
- }
183
- const instances = instanceMap.get(target);
184
- let instance = instances.get(prop);
185
- if (!instance) {
186
- const SpawnClass = registryItem.spawn;
187
- if (SpawnClass instanceof Promise) {
188
- // Handle async case - would need to be awaited externally
189
- SpawnClass.then((SC) => {
190
- instance = new SC();
191
- instances.set(prop, instance);
192
- const mappedKey = registryItem.map[prop];
193
- if (mappedKey && instance && typeof instance === 'object') {
194
- instance[mappedKey] = value;
195
- }
196
- });
197
- }
198
- else {
199
- instance = new SpawnClass();
200
- instances.set(prop, instance);
201
- const mappedKey = registryItem.map[prop];
202
- if (mappedKey && instance && typeof instance === 'object') {
203
- instance[mappedKey] = value;
204
- }
205
- }
206
- }
207
- else {
208
- const mappedKey = registryItem.map[prop];
209
- if (mappedKey && instance && typeof instance === 'object') {
210
- instance[mappedKey] = value;
211
- }
212
- }
213
- }
214
- }
215
- return true;
216
- },
217
- });
218
- },
219
- configurable: true,
220
- });
221
- }
222
- return target;
223
- }
224
- export default assignGingerly;
1
+ export { assignGingerly } from './assignGingerly.js';
2
+ export { assignTentatively } from './assignTentatively.js';
3
+ export { BaseRegistry } from './assignGingerly.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -13,20 +13,29 @@
13
13
  "license": "MIT",
14
14
  "author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
15
15
  "type": "module",
16
- "types": "index.d.ts",
16
+ "types": "types.d.ts",
17
17
  "files": [
18
- "index.js",
19
- "index.d.ts",
18
+ "assignGingerly.js",
19
+ "types.d.ts",
20
20
  "README.md",
21
21
  "LICENSE"
22
22
  ],
23
23
  "exports": {
24
24
  ".": {
25
- "import": "./index.js",
26
- "types": "./index.d.ts"
25
+ "default": "./index.js",
26
+ "types": "./types.d.ts"
27
+ },
28
+ "./assignGingerly.js": {
29
+ "default": "./assignGingerly.js",
30
+ "types": "./types.d.ts"
31
+ },
32
+ "./assignTentatively.js": {
33
+ "default": "./assignTentatively.js",
34
+ "types": "./types.d.ts"
27
35
  }
28
36
  },
29
37
  "main": "index.js",
38
+ "module": "index.js",
30
39
  "scripts": {
31
40
  "serve": "node ./node_modules/spa-ssi/serve.js",
32
41
  "test": "playwright test",
@@ -30,6 +30,6 @@ export declare function assignGingerly(
30
30
  target: any,
31
31
  source: Record<string | symbol, any>,
32
32
  options?: IAssignGingerlyOptions
33
- ): Promise<any>;
33
+ ): any;
34
34
 
35
35
  export default assignGingerly;