assign-gingerly 0.0.71 → 0.0.72

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/assignGingerly.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { buildRestrictedPropSet, checkRestrictedProp, redirectRestrictedProp } from './isAllowedImportPath.js';
1
2
  import { normalizeAliasOptions } from './getValues.js';
2
3
  /**
3
4
  * GUID for global instance map storage to ensure uniqueness across package versions
@@ -197,6 +198,23 @@ function parseIncCommand(key) {
197
198
  }
198
199
  return key.substring(0, key.length - 3); // Remove ' +=' suffix
199
200
  }
201
+ /**
202
+ * Apply the scalar and array semantics of the += command.
203
+ */
204
+ function addValue(lhs, rhs) {
205
+ if (Array.isArray(lhs)) {
206
+ return Array.isArray(rhs) ? [...lhs, ...rhs] : [...lhs, rhs];
207
+ }
208
+ if (typeof lhs === 'number' && typeof rhs === 'string') {
209
+ const parsed = Number(rhs);
210
+ return Number.isNaN(parsed) ? lhs + rhs : lhs + parsed;
211
+ }
212
+ if (typeof lhs === 'string' && typeof rhs === 'number') {
213
+ const parsed = Number(lhs);
214
+ return Number.isNaN(parsed) ? lhs + rhs : (parsed + rhs).toString();
215
+ }
216
+ return lhs + rhs;
217
+ }
200
218
  /**
201
219
  * Helper function to check if a key represents a =! command
202
220
  */
@@ -364,12 +382,17 @@ export function evaluatePathWithMethods(target, pathParts, value, withMethods) {
364
382
  while (i < pathParts.length - 1) {
365
383
  const part = pathParts[i];
366
384
  const nextPart = pathParts[i + 1];
367
- if (withMethods.has(part)) {
368
- const method = current[part];
385
+ // A trailing | marks a zero-argument method call: 'deref|' calls deref()
386
+ // without consuming the next segment. Only applies to names in withMethods.
387
+ const isZeroArgMethod = part.endsWith('|') && withMethods.has(part.slice(0, -1));
388
+ const nextIsMethod = withMethods.has(nextPart)
389
+ || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1)));
390
+ if (withMethods.has(part) || isZeroArgMethod) {
391
+ const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
392
+ const method = current[methodName];
369
393
  if (typeof method === 'function') {
370
- // Check if next part is also a method
371
- if (withMethods.has(nextPart)) {
372
- // Both are methods - call first with no args
394
+ if (isZeroArgMethod || nextIsMethod) {
395
+ // Zero-arg call - next part is either a method or explicitly not an argument
373
396
  current = method.call(current);
374
397
  }
375
398
  else {
@@ -380,10 +403,10 @@ export function evaluatePathWithMethods(target, pathParts, value, withMethods) {
380
403
  }
381
404
  else {
382
405
  // Not a function - just access property (create if needed)
383
- if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
384
- current[part] = {};
406
+ if (!(methodName in current) || typeof current[methodName] !== 'object' || current[methodName] === null) {
407
+ current[methodName] = {};
385
408
  }
386
- current = current[part];
409
+ current = current[methodName];
387
410
  }
388
411
  }
389
412
  else {
@@ -395,11 +418,16 @@ export function evaluatePathWithMethods(target, pathParts, value, withMethods) {
395
418
  }
396
419
  i++;
397
420
  }
398
- const lastKey = pathParts[pathParts.length - 1];
421
+ // Strip a trailing | from the last segment only when it names a listed method;
422
+ // otherwise it is a literal property name (e.g. an exotic key ending in |).
423
+ const rawLastKey = pathParts[pathParts.length - 1];
424
+ const isZeroArg = rawLastKey.endsWith('|') && withMethods.has(rawLastKey.slice(0, -1));
425
+ const lastKey = isZeroArg ? rawLastKey.slice(0, -1) : rawLastKey;
399
426
  return {
400
427
  target: current,
401
428
  lastKey,
402
- isMethod: withMethods.has(lastKey)
429
+ isMethod: withMethods.has(lastKey),
430
+ isZeroArg
403
431
  };
404
432
  }
405
433
  /**
@@ -446,7 +474,7 @@ function isReactiveForEachSymbol(segment, aliasMap) {
446
474
  /**
447
475
  * Apply a path to each item in an iterable
448
476
  */
449
- function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, options) {
477
+ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, options, permissions, restrictedPropSet) {
450
478
  // Convert to array for iteration
451
479
  const items = Array.isArray(iterable) ? iterable : Array.from(iterable);
452
480
  // Apply the remaining path to each item
@@ -465,13 +493,22 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
465
493
  // Navigate to the nested iterable
466
494
  let current = item;
467
495
  for (const part of pathToForEach) {
468
- if (withMethods.has(part)) {
469
- const method = current[part];
496
+ // A trailing | marks a zero-argument method call (only for names in withMethods)
497
+ const isZeroArgMethod = part.endsWith('|') && withMethods.has(part.slice(0, -1));
498
+ if (withMethods.has(part) || isZeroArgMethod) {
499
+ const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
500
+ const method = current[methodName];
470
501
  if (typeof method === 'function') {
502
+ if (isZeroArgMethod) {
503
+ current = method.call(current);
504
+ continue;
505
+ }
471
506
  // For methods in the middle, we need to check the next part
472
507
  const nextIndex = pathToForEach.indexOf(part) + 1;
473
508
  const nextPart = pathToForEach[nextIndex];
474
- if (nextPart && withMethods.has(nextPart)) {
509
+ const nextIsMethod = nextPart && (withMethods.has(nextPart)
510
+ || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
511
+ if (nextIsMethod) {
475
512
  current = method.call(current);
476
513
  }
477
514
  else if (nextPart) {
@@ -484,7 +521,7 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
484
521
  }
485
522
  }
486
523
  else {
487
- current = current[part];
524
+ current = current[methodName];
488
525
  }
489
526
  }
490
527
  else {
@@ -493,7 +530,7 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
493
530
  }
494
531
  // Recursively apply to the nested iterable
495
532
  if (isIterable(current)) {
496
- applyToEach(current, pathAfterForEach, value, withMethods, aliasMap, options);
533
+ applyToEach(current, pathAfterForEach, value, withMethods, aliasMap, options, permissions, restrictedPropSet);
497
534
  }
498
535
  }
499
536
  else {
@@ -503,7 +540,11 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
503
540
  // Last segment is a method - call it
504
541
  const method = result.target[result.lastKey];
505
542
  if (typeof method === 'function') {
506
- if (Array.isArray(value)) {
543
+ if (result.isZeroArg) {
544
+ // Trailing | marker - call with no arguments, ignoring the value
545
+ method.call(result.target);
546
+ }
547
+ else if (Array.isArray(value)) {
507
548
  method.apply(result.target, value);
508
549
  }
509
550
  else {
@@ -515,13 +556,16 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
515
556
  // Normal assignment
516
557
  const lastKey = result.lastKey;
517
558
  const parent = result.target;
518
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
559
+ if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
560
+ // skip
561
+ }
562
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
519
563
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
520
564
  const currentValue = parent[lastKey];
521
565
  if (typeof currentValue !== 'object' || currentValue === null) {
522
566
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
523
567
  }
524
- assignGingerly(currentValue, value, options);
568
+ assignGingerly(currentValue, value, options, permissions);
525
569
  }
526
570
  else {
527
571
  parent[lastKey] = value;
@@ -563,6 +607,8 @@ export function assignGingerly(target, source, options, permissions) {
563
607
  return target;
564
608
  }
565
609
  const { aliasMap, withMethods: withMethodsSet } = normalizeAliasOptions(options);
610
+ // Normalize restrictedPropSettings once per top-level call
611
+ const restrictedPropSet = buildRestrictedPropSet(permissions);
566
612
  // Convert withAsyncMethods array to Set for O(1) lookup
567
613
  const withAsyncMethodsSet = options?.withAsyncMethods
568
614
  ? options.withAsyncMethods instanceof Set
@@ -654,6 +700,9 @@ export function assignGingerly(target, source, options, permissions) {
654
700
  lhsValue = lhsParent[lhsKey];
655
701
  }
656
702
  //TODO: this logic seems to occur twice at least. Maybe make it a method?
703
+ if (checkRestrictedProp(restrictedPropSet, lhsKey)) {
704
+ continue;
705
+ }
657
706
  // Event handler: Element LHS + object RHS with 'on' property
658
707
  if (lhsValue instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
659
708
  const capturedLhs = lhsValue;
@@ -661,28 +710,22 @@ export function assignGingerly(target, source, options, permissions) {
661
710
  const capturedTarget = target;
662
711
  const capturedOptions = options;
663
712
  import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
664
- attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
713
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissions);
665
714
  });
666
715
  continue;
667
716
  }
668
717
  if (!(lhsKey in lhsParent)) {
669
718
  lhsParent[lhsKey] = value;
670
719
  }
671
- else if (Array.isArray(lhsValue)) {
672
- lhsParent[lhsKey] = Array.isArray(value)
673
- ? [...lhsValue, ...value]
674
- : [...lhsValue, value];
675
- }
676
- else if (typeof lhsValue === 'number' && typeof value === 'string') {
677
- const parsed = Number(value);
678
- lhsParent[lhsKey] = isNaN(parsed) ? lhsValue + value : lhsValue + parsed;
679
- }
680
720
  else {
681
- lhsParent[lhsKey] += value;
721
+ lhsParent[lhsKey] = addValue(lhsValue, value);
682
722
  }
683
723
  }
684
724
  else {
685
725
  // Plain key - direct operation on target
726
+ if (checkRestrictedProp(restrictedPropSet, path)) {
727
+ continue;
728
+ }
686
729
  // Event handler: Element LHS + object RHS with 'on' property
687
730
  if (target[path] instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
688
731
  const capturedLhs = target[path];
@@ -690,24 +733,15 @@ export function assignGingerly(target, source, options, permissions) {
690
733
  const capturedTarget = target;
691
734
  const capturedOptions = options;
692
735
  import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
693
- attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
736
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissions);
694
737
  });
695
738
  continue;
696
739
  }
697
740
  if (!(path in target)) {
698
741
  target[path] = value;
699
742
  }
700
- else if (Array.isArray(target[path])) {
701
- target[path] = Array.isArray(value)
702
- ? [...target[path], ...value]
703
- : [...target[path], value];
704
- }
705
- else if (typeof target[path] === 'number' && typeof value === 'string') {
706
- const parsed = Number(value);
707
- target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
708
- }
709
743
  else {
710
- target[path] += value;
744
+ target[path] = addValue(target[path], value);
711
745
  }
712
746
  }
713
747
  }
@@ -763,8 +797,10 @@ export function assignGingerly(target, source, options, permissions) {
763
797
  valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
764
798
  }
765
799
  }
766
- // Apply negation to LHS
767
- lhsParent[lhsLastKey] = !valueToNegate;
800
+ // Apply negation to LHS — check restriction first
801
+ if (!checkRestrictedProp(restrictedPropSet, lhsLastKey)) {
802
+ lhsParent[lhsLastKey] = !valueToNegate;
803
+ }
768
804
  }
769
805
  continue;
770
806
  }
@@ -817,6 +853,11 @@ export function assignGingerly(target, source, options, permissions) {
817
853
  if (isMergeCommand(key)) {
818
854
  const path = parseMergeCommand(key);
819
855
  if (path) {
856
+ const pathParts = isNestedPath(path) ? parsePath(path) : [path];
857
+ const lastKey = pathParts[pathParts.length - 1];
858
+ if (checkRestrictedProp(restrictedPropSet, lastKey)) {
859
+ continue;
860
+ }
820
861
  // Navigate to the target sub-object
821
862
  let mergeTarget;
822
863
  if (isNestedPath(path)) {
@@ -860,7 +901,7 @@ export function assignGingerly(target, source, options, permissions) {
860
901
  (async () => {
861
902
  try {
862
903
  const { handleEachTime } = await import('./eachTime.js');
863
- await handleEachTime(target, pathParts, forEachIndex, value, withMethodsSet, aliasMap, options);
904
+ await handleEachTime(target, pathParts, forEachIndex, value, withMethodsSet, aliasMap, options, permissions, restrictedPropSet);
864
905
  }
865
906
  catch (error) {
866
907
  console.error('Error in @eachTime:', error);
@@ -888,20 +929,22 @@ export function assignGingerly(target, source, options, permissions) {
888
929
  }
889
930
  // Apply to each item in the iterable
890
931
  if (isIterable(current)) {
891
- applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options);
932
+ applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options, permissions, restrictedPropSet);
892
933
  }
893
934
  // If not iterable, let JavaScript throw error naturally when trying to iterate
894
935
  continue;
895
936
  }
896
937
  // No @each in path - handle normally
897
938
  // Check if we need to handle async methods (fire-and-forget)
898
- if (withAsyncMethodsSet && pathParts.some(p => withAsyncMethodsSet.has(p))) {
939
+ if (withAsyncMethodsSet && pathParts.some(p => withAsyncMethodsSet.has(p) || (p.endsWith('|') && withAsyncMethodsSet.has(p.slice(0, -1))))) {
899
940
  // Fire-and-forget: dynamically import the async evaluator and run the chain
900
941
  const capturedTarget = target;
901
942
  const capturedPathParts = pathParts;
902
943
  const capturedValue = value;
903
944
  const capturedWithMethodsSet = withMethodsSet || new Set();
904
945
  const capturedOptions = options;
946
+ const capturedPermissions = permissions;
947
+ const capturedRestrictedPropSet = restrictedPropSet;
905
948
  (async () => {
906
949
  const { evaluatePathWithAsyncMethods } = await import('./evaluatePathWithAsyncMethods.js');
907
950
  const result = await evaluatePathWithAsyncMethods(capturedTarget, capturedPathParts, capturedValue, capturedWithMethodsSet, withAsyncMethodsSet);
@@ -909,9 +952,12 @@ export function assignGingerly(target, source, options, permissions) {
909
952
  // Last segment is a method — call it
910
953
  const method = result.target[result.lastKey];
911
954
  if (typeof method === 'function') {
912
- const returnVal = Array.isArray(capturedValue)
913
- ? method.apply(result.target, capturedValue)
914
- : method.call(result.target, capturedValue);
955
+ // Trailing | marker - call with no arguments, ignoring the value
956
+ const returnVal = result.isZeroArg
957
+ ? method.call(result.target)
958
+ : Array.isArray(capturedValue)
959
+ ? method.apply(result.target, capturedValue)
960
+ : method.call(result.target, capturedValue);
915
961
  // If it's an async method, await it (for side effects)
916
962
  if (result.isAsyncMethod)
917
963
  await returnVal;
@@ -921,13 +967,16 @@ export function assignGingerly(target, source, options, permissions) {
921
967
  // Not a method — assign the value
922
968
  const lastKey = result.lastKey;
923
969
  const parent = result.target;
924
- if (typeof capturedValue === 'object' && capturedValue !== null && !Array.isArray(capturedValue)) {
970
+ if (redirectRestrictedProp(capturedRestrictedPropSet, parent, lastKey, capturedValue)) {
971
+ // skip
972
+ }
973
+ else if (typeof capturedValue === 'object' && capturedValue !== null && !Array.isArray(capturedValue)) {
925
974
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
926
975
  const currentValue = parent[lastKey];
927
976
  if (typeof currentValue !== 'object' || currentValue === null) {
928
977
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
929
978
  }
930
- assignGingerly(currentValue, capturedValue, capturedOptions);
979
+ assignGingerly(currentValue, capturedValue, capturedOptions, capturedPermissions);
931
980
  }
932
981
  else {
933
982
  parent[lastKey] = capturedValue;
@@ -947,7 +996,11 @@ export function assignGingerly(target, source, options, permissions) {
947
996
  // Last segment is a method - call it
948
997
  const method = result.target[result.lastKey];
949
998
  if (typeof method === 'function') {
950
- if (Array.isArray(value)) {
999
+ if (result.isZeroArg) {
1000
+ // Trailing | marker - call with no arguments, ignoring the value
1001
+ method.call(result.target);
1002
+ }
1003
+ else if (Array.isArray(value)) {
951
1004
  method.apply(result.target, value);
952
1005
  }
953
1006
  else {
@@ -960,18 +1013,21 @@ export function assignGingerly(target, source, options, permissions) {
960
1013
  // Not a method - proceed with normal assignment using evaluated target
961
1014
  const lastKey = result.lastKey;
962
1015
  const parent = result.target;
963
- // Check for static assignTo protocol
964
- if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1016
+ if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
1017
+ // skip
1018
+ // Check for static assignTo protocol
1019
+ }
1020
+ else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
965
1021
  continue;
966
1022
  }
967
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1023
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
968
1024
  // Check if property exists and is readonly
969
1025
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
970
1026
  const currentValue = parent[lastKey];
971
1027
  if (typeof currentValue !== 'object' || currentValue === null) {
972
1028
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
973
1029
  }
974
- assignGingerly(currentValue, value, options);
1030
+ assignGingerly(currentValue, value, options, permissions);
975
1031
  }
976
1032
  else {
977
1033
  // Property is writable - replace it
@@ -986,11 +1042,14 @@ export function assignGingerly(target, source, options, permissions) {
986
1042
  // No withMethods - use original logic
987
1043
  const lastKey = pathParts[pathParts.length - 1];
988
1044
  const parent = ensureNestedPath(target, pathParts);
989
- // Check for static assignTo protocol
990
- if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1045
+ if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
1046
+ // skip
1047
+ // Check for static assignTo protocol
1048
+ }
1049
+ else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
991
1050
  continue;
992
1051
  }
993
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1052
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
994
1053
  // Check if property exists and is readonly
995
1054
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
996
1055
  // Property is readonly - check if current value is an object
@@ -999,7 +1058,7 @@ export function assignGingerly(target, source, options, permissions) {
999
1058
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
1000
1059
  }
1001
1060
  // Recursively apply assignGingerly to the readonly object
1002
- assignGingerly(currentValue, value, options);
1061
+ assignGingerly(currentValue, value, options, permissions);
1003
1062
  }
1004
1063
  else {
1005
1064
  // Property is writable - replace it
@@ -1013,11 +1072,17 @@ export function assignGingerly(target, source, options, permissions) {
1013
1072
  }
1014
1073
  else {
1015
1074
  // Non-nested path
1016
- // Check if this is a method call
1017
- if (withMethodsSet && withMethodsSet.has(key)) {
1018
- const method = target[key];
1075
+ // Check if this is a method call (a trailing | marks a zero-argument call)
1076
+ const isZeroArgKey = key.endsWith('|') && withMethodsSet !== undefined && withMethodsSet.has(key.slice(0, -1));
1077
+ if (withMethodsSet && (withMethodsSet.has(key) || isZeroArgKey)) {
1078
+ const methodName = isZeroArgKey ? key.slice(0, -1) : key;
1079
+ const method = target[methodName];
1019
1080
  if (typeof method === 'function') {
1020
- if (Array.isArray(value)) {
1081
+ if (isZeroArgKey) {
1082
+ // Trailing | marker - call with no arguments, ignoring the value
1083
+ method.call(target);
1084
+ }
1085
+ else if (Array.isArray(value)) {
1021
1086
  method.apply(target, value);
1022
1087
  }
1023
1088
  else {
@@ -1027,12 +1092,14 @@ export function assignGingerly(target, source, options, permissions) {
1027
1092
  // Silently skip if not a function
1028
1093
  continue;
1029
1094
  }
1030
- // Normal assignment
1031
- // Check for static assignTo protocol
1032
- if (key in target && tryAssignTo(target[key], value, target, key)) {
1095
+ if (redirectRestrictedProp(restrictedPropSet, target, key, value)) {
1096
+ // skip
1097
+ // Check for static assignTo protocol
1098
+ }
1099
+ else if (key in target && tryAssignTo(target[key], value, target, key)) {
1033
1100
  continue;
1034
1101
  }
1035
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1102
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1036
1103
  // Check if property exists and is readonly
1037
1104
  if (key in target && isReadonlyProperty(target, key)) {
1038
1105
  // Property is readonly - check if current value is an object
@@ -1041,7 +1108,7 @@ export function assignGingerly(target, source, options, permissions) {
1041
1108
  throw new Error(`Cannot merge object into readonly primitive property '${String(key)}'`);
1042
1109
  }
1043
1110
  // Recursively apply assignGingerly to the readonly object
1044
- assignGingerly(currentValue, value, options);
1111
+ assignGingerly(currentValue, value, options, permissions);
1045
1112
  }
1046
1113
  else {
1047
1114
  // Property is writable - replace it