assign-gingerly 0.0.32 → 0.0.34

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
@@ -110,7 +110,7 @@ console.log(obj);
110
110
 
111
111
  When the right hand side of an expression is an object, assignGingerly behavior depends on the context:
112
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)
113
+ - For **plain keys**: performs simple assignment (like `Object.assign`), unless the target property is readonly or an accessor (see Examples 3a and 3b below)
114
114
 
115
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.
116
116
 
@@ -144,38 +144,40 @@ console.log(obj.config); // { theme: 'dark' } - intermediate object created
144
144
 
145
145
  ## Example 3a - Automatic Readonly Property Detection
146
146
 
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:
147
+ assignGingerly automatically detects readonly properties and merges into them instead of attempting to replace them. This makes working with DOM properties like `dataset` ergonomic:
148
148
 
149
149
  ```TypeScript
150
- // Instead of this verbose syntax:
151
150
  const div = document.createElement('div');
152
151
  assignGingerly(div, {
153
- '?.style?.height': '15px',
154
- '?.style?.width': '20px'
155
- });
156
-
157
- // You can now use this cleaner syntax:
158
- assignGingerly(div, {
159
- style: {
160
- height: '15px',
161
- width: '20px'
152
+ dataset: {
153
+ userId: '123',
154
+ userName: 'Alice'
162
155
  }
163
156
  });
164
- console.log(div.style.height); // '15px'
165
- console.log(div.style.width); // '20px'
157
+ console.log(div.dataset.userId); // '123'
158
+ console.log(div.dataset.userName); // 'Alice'
166
159
  ```
167
160
 
168
161
  **How it works:**
169
162
 
170
163
  When assignGingerly encounters an object value being assigned to an existing property, it checks if that property is readonly:
171
164
  - **Data properties** with `writable: false`
172
- - **Accessor properties** with a getter but no setter
165
+ - **Accessor properties** with a getter but no setter (e.g., `dataset`, `shadowRoot`)
173
166
 
174
167
  If the property is readonly and its current value is an object, assignGingerly automatically merges into it recursively.
175
168
 
176
- **Examples of readonly properties:**
177
- - `HTMLElement.style` - The CSSStyleDeclaration object
178
- - `HTMLElement.dataset` - The DOMStringMap object
169
+ **Note on `element.style`:** The `style` property has both a getter and a setter, so it is *not* treated as readonly. Use nested path syntax instead:
170
+
171
+ ```TypeScript
172
+ // Use nested path syntax for style
173
+ assignGingerly(div, {
174
+ '?.style?.height': '15px',
175
+ '?.style?.width': '20px'
176
+ });
177
+ ```
178
+
179
+ **Examples of readonly properties that trigger merging:**
180
+ - `HTMLElement.dataset` - getter only, no setter
179
181
  - Custom objects with `Object.defineProperty(obj, 'prop', { value: {}, writable: false })`
180
182
  - Accessor properties with getter only: `Object.defineProperty(obj, 'prop', { get() { return {}; } })`
181
183
 
@@ -225,134 +227,54 @@ assignGingerly(config, {
225
227
  console.log(config.settings.theme); // 'dark'
226
228
  ```
227
229
 
228
- ## Example 3b - Automatic Class Instance Preservation
230
+ ## Example 3b - Class Instances Are Replaced
229
231
 
230
- In addition to readonly property detection, assignGingerly automatically preserves class instances when merging. This is particularly useful when working with enhancement instances:
232
+ Unlike readonly/accessor properties, class instances on writable properties are **replaced** by simple assignment, just like plain objects. This allows you to swap one object for another without unexpected merging:
231
233
 
232
234
  ```TypeScript
233
- import 'assign-gingerly/object-extension.js';
234
-
235
- // Define an enhancement class
236
- class MyEnhancement {
237
- constructor(element, ctx, initVals) {
238
- this.element = element;
239
- this.instanceId = Math.random(); // Track instance identity
240
- if (initVals) {
241
- Object.assign(this, initVals);
242
- }
235
+ class FakeDocumentFragment {
236
+ constructor() {
237
+ this.nodeType = 11;
238
+ this.childNodes = [];
243
239
  }
244
- prop1 = null;
245
- prop2 = null;
246
240
  }
247
241
 
248
- const element = document.createElement('div');
249
- element.enh = {
250
- myEnh: new MyEnhancement(element, {}, {})
242
+ const obj = {
243
+ clone: new FakeDocumentFragment()
251
244
  };
252
245
 
253
- const originalId = element.enh.myEnh.instanceId;
254
-
255
- // Clean syntax - no need for ?.myEnh?.prop1 notation
256
- assignGingerly(element, {
257
- enh: {
258
- myEnh: {
259
- prop1: 'value1',
260
- prop2: 'value2'
261
- }
262
- }
263
- });
264
-
265
- console.log(element.enh.myEnh.instanceId === originalId); // true - instance preserved!
266
- console.log(element.enh.myEnh.prop1); // 'value1'
267
- console.log(element.enh.myEnh.prop2); // 'value2'
268
- ```
269
-
270
- **How it works:**
271
-
272
- When assignGingerly encounters an object value being assigned to an existing property, it checks if the current value is a class instance (not a plain object):
273
-
274
- - **Class instances** are detected by checking if their prototype is something other than `Object.prototype` or `null`
275
- - **Plain objects** `{}` have `Object.prototype` as their prototype
276
- - **Class instances** have their class's prototype
277
-
278
- If the existing value is a class instance, assignGingerly merges into it instead of replacing it.
279
-
280
- **What counts as a class instance:**
281
- - Custom class instances: `new MyClass()`
282
- - Built-in class instances: `new Date()`, `new Map()`, `new Set()`, etc.
283
- - Enhancement instances on the `enh` property
284
- - Any object whose prototype is not `Object.prototype` or `null`
285
-
286
- **What doesn't count:**
287
- - Plain objects: `{}`, `{ a: 1 }`
288
- - Arrays: `[]`, `[1, 2, 3]` (arrays are replaced, not merged)
289
- - Primitives: strings, numbers, booleans
290
-
291
- **Benefits:**
292
-
293
- This feature enables clean, framework-friendly syntax for updating enhancements:
246
+ const element = document.createElement('div');
294
247
 
295
- ```TypeScript
296
- // Before: Verbose nested path syntax
297
- assignGingerly(element, {
298
- '?.enh?.mellowYellow?.madAboutFourteen': true
248
+ // Replace the DocumentFragment with the actual element
249
+ assignGingerly(obj, {
250
+ clone: element
299
251
  });
300
252
 
301
- // After: Clean object syntax
302
- assignGingerly(element, {
303
- enh: {
304
- mellowYellow: {
305
- madAboutFourteen: true
306
- }
307
- }
308
- });
253
+ console.log(obj.clone === element); // true - replaced, not merged
309
254
  ```
310
255
 
311
- **Additional examples:**
312
-
313
- ```TypeScript
314
- // Multiple enhancements at once
315
- assignGingerly(element, {
316
- enh: {
317
- enhancement1: { prop: 'value1' },
318
- enhancement2: { prop: 'value2' }
319
- }
320
- });
321
-
322
- // Works with built-in classes too
323
- const obj = {
324
- timestamp: new Date('2024-01-01')
325
- };
256
+ **Why replacement instead of merging?**
326
257
 
327
- assignGingerly(obj, {
328
- timestamp: {
329
- customProp: 'metadata'
330
- }
331
- });
258
+ In real-world use cases, you often need to replace one object with another of a completely different type. For example, replacing a cloned DocumentFragment with the actual web component element. Automatic merging would corrupt the target by mixing properties from incompatible types.
332
259
 
333
- console.log(obj.timestamp instanceof Date); // true - Date instance preserved
334
- console.log(obj.timestamp.customProp); // 'metadata'
335
- ```
336
-
337
- **Combined with readonly detection:**
260
+ **Readonly/accessor properties are still merged:**
338
261
 
339
- Both readonly properties and class instances are preserved:
262
+ The distinction is clear:
263
+ - **Writable data properties**: always replaced (whether holding a plain object or class instance)
264
+ - **Readonly data properties** (`writable: false`): merged into
265
+ - **Getter-only accessor properties** (no setter): merged into
266
+ - **Getter+setter accessor properties** (e.g., `style`): setter runs with the value as-is
340
267
 
341
268
  ```TypeScript
342
269
  const div = document.createElement('div');
343
- div.enh = {
344
- myEnh: new MyEnhancement(div, {}, {})
345
- };
346
270
 
347
271
  assignGingerly(div, {
348
- style: { height: '100px' }, // Readonly - merged
349
- enh: {
350
- myEnh: { prop: 'value' } // Class instance - merged
351
- },
352
- dataset: { userId: '123' } // Readonly - merged
272
+ dataset: { userId: '123' }, // Getter-only - merged
273
+ '?.style?.height': '100px' // Use nested path for style
353
274
  });
354
275
 
355
- // All instances and readonly objects preserved
276
+ console.log(div.dataset.userId); // '123'
277
+ console.log(div.style.height); // '100px'
356
278
  ```
357
279
 
358
280
  ## Example 3c - Method Calls with withMethods
@@ -812,7 +734,7 @@ console.log(obj);
812
734
  // }
813
735
  ```
814
736
 
815
- The `+=` command syntax is `<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. If the expression is a string, string concatenation is used. If the expression can't be "added to", it allows JavaScript to throw its natural error.
737
+ The `+=` command syntax is `<path> +=` where the path uses the `?.` nested notation for nested properties, or a plain key for direct properties. 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. If the expression is a string, string concatenation is used. If the expression can't be "added to", it allows JavaScript to throw its natural error.
816
738
 
817
739
  ## Example 5 - Toggling boolean values and negating
818
740
 
@@ -842,7 +764,7 @@ console.log(obj);
842
764
  // }
843
765
  ```
844
766
 
845
- The `=!` command syntax is `<path> =!` where the path can use the `?.` nested notation.
767
+ The `=!` command syntax is `<path> =!` where the path uses the `?.` nested notation for nested properties, or a plain key for direct properties.
846
768
 
847
769
  For existing values, the toggle is performed using JavaScript's logical NOT operator (`!value`), regardless of what type it is.
848
770
 
package/assignGingerly.js CHANGED
@@ -201,11 +201,13 @@ function parseDeleteCommand(key) {
201
201
  }
202
202
  /**
203
203
  * Helper function to parse a path string with ?. notation
204
+ * Always splits on '?.' delimiter, preserving dots that are part of values
205
+ * (e.g., CSS class selectors like '.username')
206
+ * Paths must use ?. notation — plain dot notation is not supported.
204
207
  */
205
208
  function parsePath(path) {
206
209
  return path
207
- .split('.')
208
- .map(part => part.replace(/\?/g, ''))
210
+ .split('?.')
209
211
  .filter(part => part.length > 0);
210
212
  }
211
213
  /**
@@ -228,11 +230,14 @@ function ensureNestedPath(obj, pathParts) {
228
230
  return current;
229
231
  }
230
232
  /**
231
- * Helper function to check if a property is readonly
232
- * A property is readonly if:
233
+ * Helper function to check if a property should be merged into rather than replaced.
234
+ * A property is non-replaceable if:
233
235
  * - It's a data property with writable: false, OR
234
236
  * - It's an accessor property with a getter but no setter
235
237
  *
238
+ * Properties with both a getter and setter (e.g., element.style) are treated as
239
+ * replaceable — the setter runs with whatever value is provided (garbage in, garbage out).
240
+ *
236
241
  * Exported for use by eachTime.ts
237
242
  */
238
243
  export function isReadonlyProperty(obj, propName) {
@@ -253,8 +258,8 @@ export function isReadonlyProperty(obj, propName) {
253
258
  if ('value' in descriptor) {
254
259
  return descriptor.writable === false;
255
260
  }
256
- // If it's an accessor property, check if it has only a getter (no setter)
257
- if ('get' in descriptor) {
261
+ // If it's an accessor property with a getter but no setter, it's readonly
262
+ if ('get' in descriptor && descriptor.get !== undefined) {
258
263
  return descriptor.set === undefined;
259
264
  }
260
265
  return false;
@@ -439,10 +444,10 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
439
444
  const lastKey = result.lastKey;
440
445
  const parent = result.target;
441
446
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
442
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
447
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
443
448
  const currentValue = parent[lastKey];
444
449
  if (typeof currentValue !== 'object' || currentValue === null) {
445
- throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
450
+ throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
446
451
  }
447
452
  assignGingerly(currentValue, value, options);
448
453
  }
@@ -569,16 +574,25 @@ export function assignGingerly(target, source, options) {
569
574
  if (isIncCommand(key)) {
570
575
  const path = parseIncCommand(key);
571
576
  if (path) {
572
- const pathParts = parsePath(path);
573
- const lastKey = pathParts[pathParts.length - 1];
574
- const parent = ensureNestedPath(target, pathParts);
575
- // If the path doesn't exist, set it directly to the value
576
- if (!(lastKey in parent)) {
577
- parent[lastKey] = value;
577
+ if (isNestedPath(path)) {
578
+ const pathParts = parsePath(path);
579
+ const lastKey = pathParts[pathParts.length - 1];
580
+ const parent = ensureNestedPath(target, pathParts);
581
+ if (!(lastKey in parent)) {
582
+ parent[lastKey] = value;
583
+ }
584
+ else {
585
+ parent[lastKey] += value;
586
+ }
578
587
  }
579
588
  else {
580
- // Path exists, apply increment: oldValue += newValue
581
- parent[lastKey] += value;
589
+ // Plain key - direct operation on target
590
+ if (!(path in target)) {
591
+ target[path] = value;
592
+ }
593
+ else {
594
+ target[path] += value;
595
+ }
582
596
  }
583
597
  }
584
598
  continue;
@@ -588,10 +602,18 @@ export function assignGingerly(target, source, options) {
588
602
  const lhsPath = parseToggleCommand(key);
589
603
  if (lhsPath) {
590
604
  const rhsPath = value;
591
- // Parse LHS path
592
- const lhsPathParts = parsePath(lhsPath);
593
- const lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
594
- const lhsParent = ensureNestedPath(target, lhsPathParts);
605
+ // Resolve LHS
606
+ let lhsParent;
607
+ let lhsLastKey;
608
+ if (isNestedPath(lhsPath)) {
609
+ const lhsPathParts = parsePath(lhsPath);
610
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
611
+ lhsParent = ensureNestedPath(target, lhsPathParts);
612
+ }
613
+ else {
614
+ lhsLastKey = lhsPath;
615
+ lhsParent = target;
616
+ }
595
617
  // Determine what to negate
596
618
  let valueToNegate;
597
619
  if (rhsPath === '.') {
@@ -600,26 +622,30 @@ export function assignGingerly(target, source, options) {
600
622
  valueToNegate = lhsParent[lhsLastKey];
601
623
  }
602
624
  else {
603
- // LHS doesn't exist, treat as undefined -> !undefined = true
604
625
  valueToNegate = undefined;
605
626
  }
606
627
  }
607
628
  else {
608
629
  // RHS path: navigate to get the value (don't create paths)
609
- const rhsPathParts = parsePath(rhsPath);
610
- let current = target;
611
- let exists = true;
612
- for (const part of rhsPathParts) {
613
- if (current && typeof current === 'object' && part in current) {
614
- current = current[part];
615
- }
616
- else {
617
- exists = false;
618
- break;
630
+ if (isNestedPath(rhsPath)) {
631
+ const rhsPathParts = parsePath(rhsPath);
632
+ let current = target;
633
+ let exists = true;
634
+ for (const part of rhsPathParts) {
635
+ if (current && typeof current === 'object' && part in current) {
636
+ current = current[part];
637
+ }
638
+ else {
639
+ exists = false;
640
+ break;
641
+ }
619
642
  }
643
+ valueToNegate = exists ? current : true;
644
+ }
645
+ else {
646
+ // Plain key RHS
647
+ valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
620
648
  }
621
- // If RHS doesn't exist, treat as truthy (will become false)
622
- valueToNegate = exists ? current : true;
623
649
  }
624
650
  // Apply negation to LHS
625
651
  lhsParent[lhsLastKey] = !valueToNegate;
@@ -630,28 +656,37 @@ export function assignGingerly(target, source, options) {
630
656
  if (isDeleteCommand(key)) {
631
657
  const path = parseDeleteCommand(key);
632
658
  if (path !== null) {
633
- const pathParts = parsePath(path);
634
659
  // Determine the parent object
635
660
  let parent = target;
636
661
  let canDelete = true;
637
- // If path is empty or just '?', delete from root
638
- if (pathParts.length === 0) {
639
- parent = target;
640
- }
641
- else {
642
- // Navigate to parent object
643
- for (const part of pathParts) {
644
- if (parent && typeof parent === 'object' && part in parent) {
645
- parent = parent[part];
646
- }
647
- else {
648
- canDelete = false;
649
- break;
662
+ if (isNestedPath(path)) {
663
+ const pathParts = parsePath(path);
664
+ if (pathParts.length === 0) {
665
+ parent = target;
666
+ }
667
+ else {
668
+ for (const part of pathParts) {
669
+ if (parent && typeof parent === 'object' && part in parent) {
670
+ parent = parent[part];
671
+ }
672
+ else {
673
+ canDelete = false;
674
+ break;
675
+ }
650
676
  }
651
677
  }
652
678
  }
679
+ else if (path.length > 0) {
680
+ // Plain key - navigate one level
681
+ if (parent && typeof parent === 'object' && path in parent) {
682
+ parent = parent[path];
683
+ }
684
+ else {
685
+ canDelete = false;
686
+ }
687
+ }
688
+ // else: empty path = delete from root
653
689
  if (canDelete && typeof parent === 'object' && parent !== null) {
654
- // RHS can be a string (single property) or array (multiple properties)
655
690
  const propertiesToDelete = Array.isArray(value) ? value : [value];
656
691
  for (const prop of propertiesToDelete) {
657
692
  if (prop in parent) {
@@ -729,16 +764,16 @@ export function assignGingerly(target, source, options) {
729
764
  const lastKey = result.lastKey;
730
765
  const parent = result.target;
731
766
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
732
- // Check if property exists and is readonly OR is a class instance
733
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
767
+ // Check if property exists and is readonly
768
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
734
769
  const currentValue = parent[lastKey];
735
770
  if (typeof currentValue !== 'object' || currentValue === null) {
736
- throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
771
+ throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
737
772
  }
738
773
  assignGingerly(currentValue, value, options);
739
774
  }
740
775
  else {
741
- // Property is writable and not a class instance - replace it
776
+ // Property is writable - replace it
742
777
  parent[lastKey] = value;
743
778
  }
744
779
  }
@@ -751,18 +786,18 @@ export function assignGingerly(target, source, options) {
751
786
  const lastKey = pathParts[pathParts.length - 1];
752
787
  const parent = ensureNestedPath(target, pathParts);
753
788
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
754
- // Check if property exists and is readonly OR is a class instance
755
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
756
- // Property is readonly or a class instance - check if current value is an object
789
+ // Check if property exists and is readonly
790
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
791
+ // Property is readonly - check if current value is an object
757
792
  const currentValue = parent[lastKey];
758
793
  if (typeof currentValue !== 'object' || currentValue === null) {
759
- throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
794
+ throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
760
795
  }
761
- // Recursively apply assignGingerly to the readonly object or class instance
796
+ // Recursively apply assignGingerly to the readonly object
762
797
  assignGingerly(currentValue, value, options);
763
798
  }
764
799
  else {
765
- // Property is writable and not a class instance - replace it
800
+ // Property is writable - replace it
766
801
  parent[lastKey] = value;
767
802
  }
768
803
  }
@@ -789,18 +824,18 @@ export function assignGingerly(target, source, options) {
789
824
  }
790
825
  // Normal assignment
791
826
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
792
- // Check if property exists and is readonly OR is a class instance
793
- if (key in target && (isReadonlyProperty(target, key) || isClassInstance(target[key]))) {
794
- // Property is readonly or a class instance - check if current value is an object
827
+ // Check if property exists and is readonly
828
+ if (key in target && isReadonlyProperty(target, key)) {
829
+ // Property is readonly - check if current value is an object
795
830
  const currentValue = target[key];
796
831
  if (typeof currentValue !== 'object' || currentValue === null) {
797
- throw new Error(`Cannot merge object into ${isReadonlyProperty(target, key) ? 'readonly ' : ''}primitive property '${String(key)}'`);
832
+ throw new Error(`Cannot merge object into readonly primitive property '${String(key)}'`);
798
833
  }
799
- // Recursively apply assignGingerly to the readonly object or class instance
834
+ // Recursively apply assignGingerly to the readonly object
800
835
  assignGingerly(currentValue, value, options);
801
836
  }
802
837
  else {
803
- // Property is writable and not a class instance - replace it
838
+ // Property is writable - replace it
804
839
  target[key] = value;
805
840
  }
806
841
  }
package/assignGingerly.ts CHANGED
@@ -326,11 +326,13 @@ function parseDeleteCommand(key: string): string | null {
326
326
 
327
327
  /**
328
328
  * Helper function to parse a path string with ?. notation
329
+ * Always splits on '?.' delimiter, preserving dots that are part of values
330
+ * (e.g., CSS class selectors like '.username')
331
+ * Paths must use ?. notation — plain dot notation is not supported.
329
332
  */
330
333
  function parsePath(path: string): string[] {
331
334
  return path
332
- .split('.')
333
- .map(part => part.replace(/\?/g, ''))
335
+ .split('?.')
334
336
  .filter(part => part.length > 0);
335
337
  }
336
338
 
@@ -356,11 +358,14 @@ function ensureNestedPath(obj: any, pathParts: string[]): any {
356
358
  }
357
359
 
358
360
  /**
359
- * Helper function to check if a property is readonly
360
- * A property is readonly if:
361
+ * Helper function to check if a property should be merged into rather than replaced.
362
+ * A property is non-replaceable if:
361
363
  * - It's a data property with writable: false, OR
362
364
  * - It's an accessor property with a getter but no setter
363
365
  *
366
+ * Properties with both a getter and setter (e.g., element.style) are treated as
367
+ * replaceable — the setter runs with whatever value is provided (garbage in, garbage out).
368
+ *
364
369
  * Exported for use by eachTime.ts
365
370
  */
366
371
  export function isReadonlyProperty(obj: any, propName: string | symbol): boolean {
@@ -383,8 +388,8 @@ export function isReadonlyProperty(obj: any, propName: string | symbol): boolean
383
388
  return descriptor.writable === false;
384
389
  }
385
390
 
386
- // If it's an accessor property, check if it has only a getter (no setter)
387
- if ('get' in descriptor) {
391
+ // If it's an accessor property with a getter but no setter, it's readonly
392
+ if ('get' in descriptor && descriptor.get !== undefined) {
388
393
  return descriptor.set === undefined;
389
394
  }
390
395
 
@@ -589,10 +594,10 @@ function applyToEach(
589
594
  const parent = result.target;
590
595
 
591
596
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
592
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
597
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
593
598
  const currentValue = parent[lastKey];
594
599
  if (typeof currentValue !== 'object' || currentValue === null) {
595
- throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
600
+ throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
596
601
  }
597
602
  assignGingerly(currentValue, value, options);
598
603
  } else {
@@ -733,16 +738,22 @@ export function assignGingerly(
733
738
  if (isIncCommand(key)) {
734
739
  const path = parseIncCommand(key);
735
740
  if (path) {
736
- const pathParts = parsePath(path);
737
- const lastKey = pathParts[pathParts.length - 1];
738
- const parent = ensureNestedPath(target, pathParts);
739
-
740
- // If the path doesn't exist, set it directly to the value
741
- if (!(lastKey in parent)) {
742
- parent[lastKey] = value;
741
+ if (isNestedPath(path)) {
742
+ const pathParts = parsePath(path);
743
+ const lastKey = pathParts[pathParts.length - 1];
744
+ const parent = ensureNestedPath(target, pathParts);
745
+ if (!(lastKey in parent)) {
746
+ parent[lastKey] = value;
747
+ } else {
748
+ parent[lastKey] += value;
749
+ }
743
750
  } else {
744
- // Path exists, apply increment: oldValue += newValue
745
- parent[lastKey] += value;
751
+ // Plain key - direct operation on target
752
+ if (!(path in target)) {
753
+ target[path] = value;
754
+ } else {
755
+ target[path] += value;
756
+ }
746
757
  }
747
758
  }
748
759
  continue;
@@ -754,10 +765,17 @@ export function assignGingerly(
754
765
  if (lhsPath) {
755
766
  const rhsPath = value;
756
767
 
757
- // Parse LHS path
758
- const lhsPathParts = parsePath(lhsPath);
759
- const lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
760
- const lhsParent = ensureNestedPath(target, lhsPathParts);
768
+ // Resolve LHS
769
+ let lhsParent: any;
770
+ let lhsLastKey: string;
771
+ if (isNestedPath(lhsPath)) {
772
+ const lhsPathParts = parsePath(lhsPath);
773
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
774
+ lhsParent = ensureNestedPath(target, lhsPathParts);
775
+ } else {
776
+ lhsLastKey = lhsPath;
777
+ lhsParent = target;
778
+ }
761
779
 
762
780
  // Determine what to negate
763
781
  let valueToNegate;
@@ -766,26 +784,27 @@ export function assignGingerly(
766
784
  if (lhsLastKey in lhsParent) {
767
785
  valueToNegate = lhsParent[lhsLastKey];
768
786
  } else {
769
- // LHS doesn't exist, treat as undefined -> !undefined = true
770
787
  valueToNegate = undefined;
771
788
  }
772
789
  } else {
773
790
  // RHS path: navigate to get the value (don't create paths)
774
- const rhsPathParts = parsePath(rhsPath);
775
- let current = target;
776
- let exists = true;
777
-
778
- for (const part of rhsPathParts) {
779
- if (current && typeof current === 'object' && part in current) {
780
- current = current[part];
781
- } else {
782
- exists = false;
783
- break;
791
+ if (isNestedPath(rhsPath)) {
792
+ const rhsPathParts = parsePath(rhsPath);
793
+ let current = target;
794
+ let exists = true;
795
+ for (const part of rhsPathParts) {
796
+ if (current && typeof current === 'object' && part in current) {
797
+ current = current[part];
798
+ } else {
799
+ exists = false;
800
+ break;
801
+ }
784
802
  }
803
+ valueToNegate = exists ? current : true;
804
+ } else {
805
+ // Plain key RHS
806
+ valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
785
807
  }
786
-
787
- // If RHS doesn't exist, treat as truthy (will become false)
788
- valueToNegate = exists ? current : true;
789
808
  }
790
809
 
791
810
  // Apply negation to LHS
@@ -798,31 +817,36 @@ export function assignGingerly(
798
817
  if (isDeleteCommand(key)) {
799
818
  const path = parseDeleteCommand(key);
800
819
  if (path !== null) {
801
- const pathParts = parsePath(path);
802
-
803
820
  // Determine the parent object
804
821
  let parent = target;
805
822
  let canDelete = true;
806
823
 
807
- // If path is empty or just '?', delete from root
808
- if (pathParts.length === 0) {
809
- parent = target;
810
- } else {
811
- // Navigate to parent object
812
- for (const part of pathParts) {
813
- if (parent && typeof parent === 'object' && part in parent) {
814
- parent = parent[part];
815
- } else {
816
- canDelete = false;
817
- break;
824
+ if (isNestedPath(path)) {
825
+ const pathParts = parsePath(path);
826
+ if (pathParts.length === 0) {
827
+ parent = target;
828
+ } else {
829
+ for (const part of pathParts) {
830
+ if (parent && typeof parent === 'object' && part in parent) {
831
+ parent = parent[part];
832
+ } else {
833
+ canDelete = false;
834
+ break;
835
+ }
818
836
  }
819
837
  }
838
+ } else if (path.length > 0) {
839
+ // Plain key - navigate one level
840
+ if (parent && typeof parent === 'object' && path in parent) {
841
+ parent = parent[path];
842
+ } else {
843
+ canDelete = false;
844
+ }
820
845
  }
846
+ // else: empty path = delete from root
821
847
 
822
848
  if (canDelete && typeof parent === 'object' && parent !== null) {
823
- // RHS can be a string (single property) or array (multiple properties)
824
849
  const propertiesToDelete = Array.isArray(value) ? value : [value];
825
-
826
850
  for (const prop of propertiesToDelete) {
827
851
  if (prop in parent) {
828
852
  delete parent[prop];
@@ -918,15 +942,15 @@ export function assignGingerly(
918
942
  const parent = result.target;
919
943
 
920
944
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
921
- // Check if property exists and is readonly OR is a class instance
922
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
945
+ // Check if property exists and is readonly
946
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
923
947
  const currentValue = parent[lastKey];
924
948
  if (typeof currentValue !== 'object' || currentValue === null) {
925
- throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
949
+ throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
926
950
  }
927
951
  assignGingerly(currentValue, value, options);
928
952
  } else {
929
- // Property is writable and not a class instance - replace it
953
+ // Property is writable - replace it
930
954
  parent[lastKey] = value;
931
955
  }
932
956
  } else {
@@ -938,17 +962,17 @@ export function assignGingerly(
938
962
  const parent = ensureNestedPath(target, pathParts);
939
963
 
940
964
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
941
- // Check if property exists and is readonly OR is a class instance
942
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
943
- // Property is readonly or a class instance - check if current value is an object
965
+ // Check if property exists and is readonly
966
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
967
+ // Property is readonly - check if current value is an object
944
968
  const currentValue = parent[lastKey];
945
969
  if (typeof currentValue !== 'object' || currentValue === null) {
946
- throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
970
+ throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
947
971
  }
948
- // Recursively apply assignGingerly to the readonly object or class instance
972
+ // Recursively apply assignGingerly to the readonly object
949
973
  assignGingerly(currentValue, value, options);
950
974
  } else {
951
- // Property is writable and not a class instance - replace it
975
+ // Property is writable - replace it
952
976
  parent[lastKey] = value;
953
977
  }
954
978
  } else {
@@ -974,17 +998,17 @@ export function assignGingerly(
974
998
 
975
999
  // Normal assignment
976
1000
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
977
- // Check if property exists and is readonly OR is a class instance
978
- if (key in target && (isReadonlyProperty(target, key) || isClassInstance(target[key]))) {
979
- // Property is readonly or a class instance - check if current value is an object
1001
+ // Check if property exists and is readonly
1002
+ if (key in target && isReadonlyProperty(target, key)) {
1003
+ // Property is readonly - check if current value is an object
980
1004
  const currentValue = target[key];
981
1005
  if (typeof currentValue !== 'object' || currentValue === null) {
982
- throw new Error(`Cannot merge object into ${isReadonlyProperty(target, key) ? 'readonly ' : ''}primitive property '${String(key)}'`);
1006
+ throw new Error(`Cannot merge object into readonly primitive property '${String(key)}'`);
983
1007
  }
984
- // Recursively apply assignGingerly to the readonly object or class instance
1008
+ // Recursively apply assignGingerly to the readonly object
985
1009
  assignGingerly(currentValue, value, options);
986
1010
  } else {
987
- // Property is writable and not a class instance - replace it
1011
+ // Property is writable - replace it
988
1012
  target[key] = value;
989
1013
  }
990
1014
  } else {
package/eachTime.js CHANGED
@@ -58,7 +58,7 @@ export async function handleEachTime(target, pathParts, forEachIndex, value, wit
58
58
  (async () => {
59
59
  try {
60
60
  // Import needed functions from assignGingerly
61
- const { evaluatePathWithMethods, assignGingerly, isReadonlyProperty, isClassInstance } = await import('./assignGingerly.js');
61
+ const { evaluatePathWithMethods, assignGingerly, isReadonlyProperty } = await import('./assignGingerly.js');
62
62
  if (pathAfterForEach.length > 0) {
63
63
  const result = evaluatePathWithMethods(mountedElement, pathAfterForEach, value, withMethods || new Set());
64
64
  if (result.isMethod) {
@@ -78,17 +78,17 @@ export async function handleEachTime(target, pathParts, forEachIndex, value, wit
78
78
  const lastKey = result.lastKey;
79
79
  const parent = result.target;
80
80
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
81
- // Check if property exists and is readonly OR is a class instance
82
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
81
+ // Check if property exists and is readonly
82
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
83
83
  const currentValue = parent[lastKey];
84
84
  if (typeof currentValue !== 'object' || currentValue === null) {
85
- throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
85
+ throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
86
86
  }
87
87
  // Recursively apply assignGingerly
88
88
  assignGingerly(currentValue, value, options);
89
89
  }
90
90
  else {
91
- // Property is writable and not a class instance - replace it
91
+ // Property is writable - replace it
92
92
  parent[lastKey] = value;
93
93
  }
94
94
  }
package/eachTime.ts CHANGED
@@ -77,8 +77,7 @@ export async function handleEachTime(
77
77
  const {
78
78
  evaluatePathWithMethods,
79
79
  assignGingerly,
80
- isReadonlyProperty,
81
- isClassInstance
80
+ isReadonlyProperty
82
81
  } = await import('./assignGingerly.js');
83
82
 
84
83
  if (pathAfterForEach.length > 0) {
@@ -105,18 +104,18 @@ export async function handleEachTime(
105
104
  const parent = result.target;
106
105
 
107
106
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
108
- // Check if property exists and is readonly OR is a class instance
109
- if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
107
+ // Check if property exists and is readonly
108
+ if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
110
109
  const currentValue = parent[lastKey];
111
110
  if (typeof currentValue !== 'object' || currentValue === null) {
112
111
  throw new Error(
113
- `Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`
112
+ `Cannot merge object into readonly primitive property '${String(lastKey)}'`
114
113
  );
115
114
  }
116
115
  // Recursively apply assignGingerly
117
116
  assignGingerly(currentValue, value, options);
118
117
  } else {
119
- // Property is writable and not a class instance - replace it
118
+ // Property is writable - replace it
120
119
  parent[lastKey] = value;
121
120
  }
122
121
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
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": {