assign-gingerly 0.0.24 → 0.0.26
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 +140 -1
- package/assignGingerly.js +128 -22
- package/assignGingerly.ts +163 -39
- package/package.json +3 -3
- package/playwright.config.ts +8 -8
- package/types/assign-gingerly/types.d.ts +1 -0
package/README.md
CHANGED
|
@@ -108,10 +108,40 @@ console.log(obj);
|
|
|
108
108
|
// }
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
When the right hand side of an expression is an object, assignGingerly
|
|
111
|
+
When the right hand side of an expression is an object, assignGingerly behavior depends on the context:
|
|
112
|
+
- For **nested paths** (starting with `?.`): recursively merges into nested objects, creating them if needed
|
|
113
|
+
- For **plain keys**: performs simple assignment (like `Object.assign`), unless the target property is readonly or a class instance (see Examples 3a and 3b below)
|
|
112
114
|
|
|
113
115
|
Of course, just as Object.assign led to object spread notation, assignGingerly could lead to some sort of deep structural JavaScript syntax, but that is outside the scope of this polyfill package.
|
|
114
116
|
|
|
117
|
+
## Example 3-plain - Plain Key Object Assignment
|
|
118
|
+
|
|
119
|
+
For plain keys (without `?.` prefix), assignGingerly performs simple assignment, just like `Object.assign`:
|
|
120
|
+
|
|
121
|
+
```TypeScript
|
|
122
|
+
const obj = {};
|
|
123
|
+
const template = document.createElement('template');
|
|
124
|
+
template.innerHTML = '<div>Hello</div>';
|
|
125
|
+
|
|
126
|
+
assignGingerly(obj, {
|
|
127
|
+
template: template,
|
|
128
|
+
config: { theme: 'dark', lang: 'en' }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log(obj.template === template); // true - direct assignment
|
|
132
|
+
console.log(obj.config); // { theme: 'dark', lang: 'en' } - direct assignment
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This is different from nested paths, which create intermediate objects:
|
|
136
|
+
|
|
137
|
+
```TypeScript
|
|
138
|
+
const obj = {};
|
|
139
|
+
assignGingerly(obj, {
|
|
140
|
+
'?.config?.theme': 'dark'
|
|
141
|
+
});
|
|
142
|
+
console.log(obj.config); // { theme: 'dark' } - intermediate object created
|
|
143
|
+
```
|
|
144
|
+
|
|
115
145
|
## Example 3a - Automatic Readonly Property Detection
|
|
116
146
|
|
|
117
147
|
assignGingerly automatically detects readonly properties and merges into them instead of attempting to replace them. This makes working with DOM properties like `style` and `dataset` much more ergonomic:
|
|
@@ -325,6 +355,115 @@ assignGingerly(div, {
|
|
|
325
355
|
// All instances and readonly objects preserved
|
|
326
356
|
```
|
|
327
357
|
|
|
358
|
+
## Example 3c - Method Calls with withMethods
|
|
359
|
+
|
|
360
|
+
The `withMethods` option allows you to call methods as part of property assignment, which is particularly useful for DOM APIs like `classList` and `part`:
|
|
361
|
+
|
|
362
|
+
```TypeScript
|
|
363
|
+
import assignGingerly from 'assign-gingerly';
|
|
364
|
+
|
|
365
|
+
const element = document.createElement('div');
|
|
366
|
+
|
|
367
|
+
// Simple method calls
|
|
368
|
+
assignGingerly(element, {
|
|
369
|
+
'?.classList?.add': 'myClass',
|
|
370
|
+
'?.part?.add': 'myPart'
|
|
371
|
+
}, { withMethods: ['add'] });
|
|
372
|
+
|
|
373
|
+
console.log(element.classList.contains('myClass')); // true
|
|
374
|
+
console.log(element.part.contains('myPart')); // true
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**How it works:**
|
|
378
|
+
|
|
379
|
+
When a path segment matches a name in the `withMethods` array/set:
|
|
380
|
+
- If it's the **last segment**: the method is called with the RHS value as an argument
|
|
381
|
+
- If it's a **middle segment** and the next segment is also a method: called with no arguments
|
|
382
|
+
- If it's a **middle segment** and the next segment is NOT a method: called with the next segment as a string argument
|
|
383
|
+
- If the property is not a function: silently skipped
|
|
384
|
+
|
|
385
|
+
**Array arguments:**
|
|
386
|
+
|
|
387
|
+
Arrays are spread as multiple arguments:
|
|
388
|
+
|
|
389
|
+
```TypeScript
|
|
390
|
+
assignGingerly(element, {
|
|
391
|
+
'?.setAttribute': ['data-id', '123']
|
|
392
|
+
}, { withMethods: ['setAttribute'] });
|
|
393
|
+
|
|
394
|
+
// Equivalent to: element.setAttribute('data-id', '123')
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Chained method calls:**
|
|
398
|
+
|
|
399
|
+
Methods can be chained to navigate through object hierarchies:
|
|
400
|
+
|
|
401
|
+
```TypeScript
|
|
402
|
+
const elementRef = {
|
|
403
|
+
deref() { return this.element; },
|
|
404
|
+
element: document.createElement('div')
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
assignGingerly(elementRef, {
|
|
408
|
+
'?.deref?.classList?.add': 'active'
|
|
409
|
+
}, { withMethods: ['deref', 'add'] });
|
|
410
|
+
|
|
411
|
+
// Equivalent to: elementRef.deref().classList.add('active')
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Complex chaining:**
|
|
415
|
+
|
|
416
|
+
```TypeScript
|
|
417
|
+
const shadowRoot = {
|
|
418
|
+
querySelector(selector) {
|
|
419
|
+
return this.elements[selector];
|
|
420
|
+
},
|
|
421
|
+
elements: {
|
|
422
|
+
'my-element': document.createElement('div')
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
assignGingerly(shadowRoot, {
|
|
427
|
+
'?.querySelector?.my-element?.classList?.add': 'highlighted'
|
|
428
|
+
}, { withMethods: ['querySelector', 'add'] });
|
|
429
|
+
|
|
430
|
+
// Equivalent to: shadowRoot.querySelector('my-element').classList.add('highlighted')
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Using Set for withMethods:**
|
|
434
|
+
|
|
435
|
+
For better performance with many methods, use a Set:
|
|
436
|
+
|
|
437
|
+
```TypeScript
|
|
438
|
+
const methods = new Set(['add', 'remove', 'toggle', 'setAttribute']);
|
|
439
|
+
|
|
440
|
+
assignGingerly(element, {
|
|
441
|
+
'?.classList?.add': 'class1',
|
|
442
|
+
'?.classList?.remove': 'class2',
|
|
443
|
+
'?.setAttribute': ['data-value', '42']
|
|
444
|
+
}, { withMethods: methods });
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Mixing methods and normal assignments:**
|
|
448
|
+
|
|
449
|
+
```TypeScript
|
|
450
|
+
assignGingerly(element, {
|
|
451
|
+
'?.classList?.add': 'active',
|
|
452
|
+
'?.dataset?.userId': '123',
|
|
453
|
+
'?.style?.height': '100px'
|
|
454
|
+
}, { withMethods: ['add'] });
|
|
455
|
+
|
|
456
|
+
// classList.add() is called
|
|
457
|
+
// dataset.userId and style.height are assigned normally
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Benefits:**
|
|
461
|
+
|
|
462
|
+
- Cleaner syntax for DOM manipulation
|
|
463
|
+
- Works with any object methods, not just DOM APIs
|
|
464
|
+
- Silent failure for non-existent methods (garbage in, garbage out)
|
|
465
|
+
- Supports method chaining and complex navigation patterns
|
|
466
|
+
|
|
328
467
|
While we are in the business of passing values of object A into object B, we might as well add some extremely common behavior that allows updating properties of object B based on the current values of object B -- things like incrementing, toggling, and deleting. Deleting is critical for assignTentatively, but is included with both functions.
|
|
329
468
|
|
|
330
469
|
## Example 4 - Incrementing values with += command
|
package/assignGingerly.js
CHANGED
|
@@ -270,6 +270,55 @@ function isClassInstance(value) {
|
|
|
270
270
|
// Plain objects have Object.prototype or null as prototype
|
|
271
271
|
return proto !== Object.prototype && proto !== null;
|
|
272
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Helper function to evaluate a nested path with method calls
|
|
275
|
+
* Handles chained method calls where path segments can be methods
|
|
276
|
+
*/
|
|
277
|
+
function evaluatePathWithMethods(target, pathParts, value, withMethods) {
|
|
278
|
+
let current = target;
|
|
279
|
+
let i = 0;
|
|
280
|
+
// Process all segments except the last one
|
|
281
|
+
while (i < pathParts.length - 1) {
|
|
282
|
+
const part = pathParts[i];
|
|
283
|
+
const nextPart = pathParts[i + 1];
|
|
284
|
+
if (withMethods.has(part)) {
|
|
285
|
+
const method = current[part];
|
|
286
|
+
if (typeof method === 'function') {
|
|
287
|
+
// Check if next part is also a method
|
|
288
|
+
if (withMethods.has(nextPart)) {
|
|
289
|
+
// Both are methods - call first with no args
|
|
290
|
+
current = method.call(current);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Only current is method - call with next part as string arg
|
|
294
|
+
current = method.call(current, nextPart);
|
|
295
|
+
i++; // Skip next part since we consumed it as argument
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// Not a function - just access property (create if needed)
|
|
300
|
+
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
301
|
+
current[part] = {};
|
|
302
|
+
}
|
|
303
|
+
current = current[part];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// Not a method - normal property access (create if needed)
|
|
308
|
+
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
309
|
+
current[part] = {};
|
|
310
|
+
}
|
|
311
|
+
current = current[part];
|
|
312
|
+
}
|
|
313
|
+
i++;
|
|
314
|
+
}
|
|
315
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
316
|
+
return {
|
|
317
|
+
target: current,
|
|
318
|
+
lastKey,
|
|
319
|
+
isMethod: withMethods.has(lastKey)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
273
322
|
/**
|
|
274
323
|
* Main assignGingerly function
|
|
275
324
|
*/
|
|
@@ -277,6 +326,12 @@ export function assignGingerly(target, source, options) {
|
|
|
277
326
|
if (!target || typeof target !== 'object') {
|
|
278
327
|
return target;
|
|
279
328
|
}
|
|
329
|
+
// Convert withMethods array to Set for O(1) lookup
|
|
330
|
+
const withMethodsSet = options?.withMethods
|
|
331
|
+
? options.withMethods instanceof Set
|
|
332
|
+
? options.withMethods
|
|
333
|
+
: new Set(options.withMethods)
|
|
334
|
+
: undefined;
|
|
280
335
|
const registry = options?.registry instanceof EnhancementRegistry
|
|
281
336
|
? options.registry
|
|
282
337
|
: options?.registry
|
|
@@ -437,32 +492,86 @@ export function assignGingerly(target, source, options) {
|
|
|
437
492
|
}
|
|
438
493
|
if (isNestedPath(key)) {
|
|
439
494
|
const pathParts = parsePath(key);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
495
|
+
// Check if we need to handle methods
|
|
496
|
+
if (withMethodsSet) {
|
|
497
|
+
const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet);
|
|
498
|
+
if (result.isMethod) {
|
|
499
|
+
// Last segment is a method - call it
|
|
500
|
+
const method = result.target[result.lastKey];
|
|
501
|
+
if (typeof method === 'function') {
|
|
502
|
+
if (Array.isArray(value)) {
|
|
503
|
+
method.apply(result.target, value);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
method.call(result.target, value);
|
|
507
|
+
}
|
|
449
508
|
}
|
|
450
|
-
//
|
|
451
|
-
|
|
509
|
+
// Silently skip if not a function
|
|
510
|
+
continue;
|
|
452
511
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
512
|
+
// Not a method - proceed with normal assignment using evaluated target
|
|
513
|
+
const lastKey = result.lastKey;
|
|
514
|
+
const parent = result.target;
|
|
515
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
516
|
+
// Check if property exists and is readonly OR is a class instance
|
|
517
|
+
if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
|
|
518
|
+
const currentValue = parent[lastKey];
|
|
519
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
520
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
|
|
521
|
+
}
|
|
522
|
+
assignGingerly(currentValue, value, options);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
// Property is writable and not a class instance - replace it
|
|
526
|
+
parent[lastKey] = value;
|
|
457
527
|
}
|
|
458
|
-
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
parent[lastKey] = value;
|
|
459
531
|
}
|
|
460
532
|
}
|
|
461
533
|
else {
|
|
462
|
-
|
|
534
|
+
// No withMethods - use original logic
|
|
535
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
536
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
537
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
538
|
+
// Check if property exists and is readonly OR is a class instance
|
|
539
|
+
if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
|
|
540
|
+
// Property is readonly or a class instance - check if current value is an object
|
|
541
|
+
const currentValue = parent[lastKey];
|
|
542
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
543
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
|
|
544
|
+
}
|
|
545
|
+
// Recursively apply assignGingerly to the readonly object or class instance
|
|
546
|
+
assignGingerly(currentValue, value, options);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
// Property is writable and not a class instance - replace it
|
|
550
|
+
parent[lastKey] = value;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
parent[lastKey] = value;
|
|
555
|
+
}
|
|
463
556
|
}
|
|
464
557
|
}
|
|
465
558
|
else {
|
|
559
|
+
// Non-nested path
|
|
560
|
+
// Check if this is a method call
|
|
561
|
+
if (withMethodsSet && withMethodsSet.has(key)) {
|
|
562
|
+
const method = target[key];
|
|
563
|
+
if (typeof method === 'function') {
|
|
564
|
+
if (Array.isArray(value)) {
|
|
565
|
+
method.apply(target, value);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
method.call(target, value);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// Silently skip if not a function
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
// Normal assignment
|
|
466
575
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
467
576
|
// Check if property exists and is readonly OR is a class instance
|
|
468
577
|
if (key in target && (isReadonlyProperty(target, key) || isClassInstance(target[key]))) {
|
|
@@ -475,11 +584,8 @@ export function assignGingerly(target, source, options) {
|
|
|
475
584
|
assignGingerly(currentValue, value, options);
|
|
476
585
|
}
|
|
477
586
|
else {
|
|
478
|
-
// Property is writable and not a class instance -
|
|
479
|
-
|
|
480
|
-
target[key] = {};
|
|
481
|
-
}
|
|
482
|
-
assignGingerly(target[key], value, options);
|
|
587
|
+
// Property is writable and not a class instance - replace it
|
|
588
|
+
target[key] = value;
|
|
483
589
|
}
|
|
484
590
|
}
|
|
485
591
|
else {
|
package/assignGingerly.ts
CHANGED
|
@@ -29,28 +29,37 @@ export interface ItemscopeManagerConfig<T = any> {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// Polyfill for WeakMap.prototype.getOrInsert
|
|
33
|
-
|
|
34
|
-
// if (typeof WeakMap.prototype.getOrInsertComputed !== 'function') {
|
|
35
|
-
// WeakMap.prototype.getOrInsertComputed = function(key, insert) {
|
|
36
|
-
// if (this.has(key)) return this.get(key);
|
|
37
|
-
// const value = insert();
|
|
38
|
-
// this.set(key, value);
|
|
39
|
-
// return value;
|
|
40
|
-
// };
|
|
41
|
-
// }
|
|
42
|
-
|
|
43
|
-
// /**
|
|
44
|
-
// * @deprecated Use EnhancementConfig instead
|
|
45
|
-
// */
|
|
46
|
-
// export type IBaseRegistryItem<T = any> = EnhancementConfig<T>;
|
|
47
|
-
|
|
48
32
|
/**
|
|
49
33
|
* Interface for the options passed to assignGingerly
|
|
50
34
|
*/
|
|
51
35
|
export interface IAssignGingerlyOptions {
|
|
52
36
|
registry?: typeof EnhancementRegistry | EnhancementRegistry;
|
|
53
37
|
bypassChecks?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* List of property names that should be treated as methods to call
|
|
41
|
+
* rather than properties to assign.
|
|
42
|
+
*
|
|
43
|
+
* When a path segment matches a name in this array/set:
|
|
44
|
+
* - If the property is a function, call it with appropriate arguments
|
|
45
|
+
* - For the last segment: use RHS value as argument (spread if array)
|
|
46
|
+
* - For middle segments: use next segment as string argument (if next is not also a method)
|
|
47
|
+
* - If consecutive segments are both methods, first is called with no arguments
|
|
48
|
+
* - If the property is not a function, silently skip
|
|
49
|
+
*
|
|
50
|
+
* Example:
|
|
51
|
+
* assignGingerly(element, {
|
|
52
|
+
* '?.classList?.add': 'myClass'
|
|
53
|
+
* }, { withMethods: ['add'] });
|
|
54
|
+
* // Calls: element.classList.add('myClass')
|
|
55
|
+
*
|
|
56
|
+
* Chained methods:
|
|
57
|
+
* assignGingerly(elementRef, {
|
|
58
|
+
* '?.deref?.querySelector?.myElement?.classList?.add': 'active'
|
|
59
|
+
* }, { withMethods: ['deref', 'querySelector', 'add'] });
|
|
60
|
+
* // Calls: elementRef.deref().querySelector('myElement').classList.add('active')
|
|
61
|
+
*/
|
|
62
|
+
withMethods?: string[] | Set<string>;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/**
|
|
@@ -356,6 +365,62 @@ function isClassInstance(value: any): boolean {
|
|
|
356
365
|
return proto !== Object.prototype && proto !== null;
|
|
357
366
|
}
|
|
358
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Helper function to evaluate a nested path with method calls
|
|
370
|
+
* Handles chained method calls where path segments can be methods
|
|
371
|
+
*/
|
|
372
|
+
function evaluatePathWithMethods(
|
|
373
|
+
target: any,
|
|
374
|
+
pathParts: string[],
|
|
375
|
+
value: any,
|
|
376
|
+
withMethods: Set<string>
|
|
377
|
+
): { target: any; lastKey: string; isMethod: boolean } {
|
|
378
|
+
let current = target;
|
|
379
|
+
let i = 0;
|
|
380
|
+
|
|
381
|
+
// Process all segments except the last one
|
|
382
|
+
while (i < pathParts.length - 1) {
|
|
383
|
+
const part = pathParts[i];
|
|
384
|
+
const nextPart = pathParts[i + 1];
|
|
385
|
+
|
|
386
|
+
if (withMethods.has(part)) {
|
|
387
|
+
const method = current[part];
|
|
388
|
+
if (typeof method === 'function') {
|
|
389
|
+
// Check if next part is also a method
|
|
390
|
+
if (withMethods.has(nextPart)) {
|
|
391
|
+
// Both are methods - call first with no args
|
|
392
|
+
current = method.call(current);
|
|
393
|
+
} else {
|
|
394
|
+
// Only current is method - call with next part as string arg
|
|
395
|
+
current = method.call(current, nextPart);
|
|
396
|
+
i++; // Skip next part since we consumed it as argument
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
// Not a function - just access property (create if needed)
|
|
400
|
+
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
401
|
+
current[part] = {};
|
|
402
|
+
}
|
|
403
|
+
current = current[part];
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
// Not a method - normal property access (create if needed)
|
|
407
|
+
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
408
|
+
current[part] = {};
|
|
409
|
+
}
|
|
410
|
+
current = current[part];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
i++;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
417
|
+
return {
|
|
418
|
+
target: current,
|
|
419
|
+
lastKey,
|
|
420
|
+
isMethod: withMethods.has(lastKey)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
359
424
|
/**
|
|
360
425
|
* Main assignGingerly function
|
|
361
426
|
*/
|
|
@@ -368,6 +433,13 @@ export function assignGingerly(
|
|
|
368
433
|
return target;
|
|
369
434
|
}
|
|
370
435
|
|
|
436
|
+
// Convert withMethods array to Set for O(1) lookup
|
|
437
|
+
const withMethodsSet = options?.withMethods
|
|
438
|
+
? options.withMethods instanceof Set
|
|
439
|
+
? options.withMethods
|
|
440
|
+
: new Set(options.withMethods)
|
|
441
|
+
: undefined;
|
|
442
|
+
|
|
371
443
|
const registry = options?.registry instanceof EnhancementRegistry
|
|
372
444
|
? options.registry
|
|
373
445
|
: options?.registry
|
|
@@ -540,30 +612,85 @@ export function assignGingerly(
|
|
|
540
612
|
|
|
541
613
|
if (isNestedPath(key)) {
|
|
542
614
|
const pathParts = parsePath(key);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (
|
|
549
|
-
//
|
|
550
|
-
const
|
|
551
|
-
if (typeof
|
|
552
|
-
|
|
615
|
+
|
|
616
|
+
// Check if we need to handle methods
|
|
617
|
+
if (withMethodsSet) {
|
|
618
|
+
const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet);
|
|
619
|
+
|
|
620
|
+
if (result.isMethod) {
|
|
621
|
+
// Last segment is a method - call it
|
|
622
|
+
const method = result.target[result.lastKey];
|
|
623
|
+
if (typeof method === 'function') {
|
|
624
|
+
if (Array.isArray(value)) {
|
|
625
|
+
method.apply(result.target, value);
|
|
626
|
+
} else {
|
|
627
|
+
method.call(result.target, value);
|
|
628
|
+
}
|
|
553
629
|
}
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
630
|
+
// Silently skip if not a function
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Not a method - proceed with normal assignment using evaluated target
|
|
635
|
+
const lastKey = result.lastKey;
|
|
636
|
+
const parent = result.target;
|
|
637
|
+
|
|
638
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
639
|
+
// Check if property exists and is readonly OR is a class instance
|
|
640
|
+
if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
|
|
641
|
+
const currentValue = parent[lastKey];
|
|
642
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
643
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
|
|
644
|
+
}
|
|
645
|
+
assignGingerly(currentValue, value, options);
|
|
646
|
+
} else {
|
|
647
|
+
// Property is writable and not a class instance - replace it
|
|
648
|
+
parent[lastKey] = value;
|
|
560
649
|
}
|
|
561
|
-
|
|
650
|
+
} else {
|
|
651
|
+
parent[lastKey] = value;
|
|
562
652
|
}
|
|
563
653
|
} else {
|
|
564
|
-
|
|
654
|
+
// No withMethods - use original logic
|
|
655
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
656
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
657
|
+
|
|
658
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
659
|
+
// Check if property exists and is readonly OR is a class instance
|
|
660
|
+
if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
|
|
661
|
+
// Property is readonly or a class instance - check if current value is an object
|
|
662
|
+
const currentValue = parent[lastKey];
|
|
663
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
664
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
|
|
665
|
+
}
|
|
666
|
+
// Recursively apply assignGingerly to the readonly object or class instance
|
|
667
|
+
assignGingerly(currentValue, value, options);
|
|
668
|
+
} else {
|
|
669
|
+
// Property is writable and not a class instance - replace it
|
|
670
|
+
parent[lastKey] = value;
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
parent[lastKey] = value;
|
|
674
|
+
}
|
|
565
675
|
}
|
|
566
676
|
} else {
|
|
677
|
+
// Non-nested path
|
|
678
|
+
|
|
679
|
+
// Check if this is a method call
|
|
680
|
+
if (withMethodsSet && withMethodsSet.has(key)) {
|
|
681
|
+
const method = target[key];
|
|
682
|
+
if (typeof method === 'function') {
|
|
683
|
+
if (Array.isArray(value)) {
|
|
684
|
+
method.apply(target, value);
|
|
685
|
+
} else {
|
|
686
|
+
method.call(target, value);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Silently skip if not a function
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Normal assignment
|
|
567
694
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
568
695
|
// Check if property exists and is readonly OR is a class instance
|
|
569
696
|
if (key in target && (isReadonlyProperty(target, key) || isClassInstance(target[key]))) {
|
|
@@ -575,11 +702,8 @@ export function assignGingerly(
|
|
|
575
702
|
// Recursively apply assignGingerly to the readonly object or class instance
|
|
576
703
|
assignGingerly(currentValue, value, options);
|
|
577
704
|
} else {
|
|
578
|
-
// Property is writable and not a class instance -
|
|
579
|
-
|
|
580
|
-
target[key] = {};
|
|
581
|
-
}
|
|
582
|
-
assignGingerly(target[key], value, options);
|
|
705
|
+
// Property is writable and not a class instance - replace it
|
|
706
|
+
target[key] = value;
|
|
583
707
|
}
|
|
584
708
|
} else {
|
|
585
709
|
target[key] = value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
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": {
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"chrome": "npx playwright cr http://localhost:8000"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@playwright/test": "1.59.
|
|
66
|
+
"@playwright/test": "1.59.1",
|
|
67
67
|
"spa-ssi": "0.0.27",
|
|
68
|
-
"@types/node": "25.5.
|
|
68
|
+
"@types/node": "25.5.2",
|
|
69
69
|
"typescript": "6.0.2"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/playwright.config.ts
CHANGED
|
@@ -34,15 +34,15 @@ export default defineConfig({
|
|
|
34
34
|
use: { ...devices['Desktop Chrome'] },
|
|
35
35
|
},
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
{
|
|
38
|
+
name: 'firefox',
|
|
39
|
+
use: { ...devices['Desktop Firefox'] },
|
|
40
|
+
},
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
{
|
|
43
|
+
name: 'webkit',
|
|
44
|
+
use: { ...devices['Desktop Safari'] },
|
|
45
|
+
},
|
|
46
46
|
],
|
|
47
47
|
|
|
48
48
|
/* Run your local dev server before starting the tests */
|
|
@@ -169,6 +169,7 @@ export type IEnhancementRegistryItem<T = any> = EnhancementConfig<T>;
|
|
|
169
169
|
export interface IAssignGingerlyOptions {
|
|
170
170
|
registry?: typeof EnhancementRegistry | EnhancementRegistry;
|
|
171
171
|
bypassChecks?: boolean;
|
|
172
|
+
withMethods?: string[] | Set<string>;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
/**
|