assign-gingerly 0.0.71 → 0.0.73

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.
Files changed (38) hide show
  1. package/DX/emojis.js +16 -0
  2. package/DX/emojis.ts +16 -0
  3. package/README.md +30 -3
  4. package/assignFrom.js +6 -6
  5. package/assignFrom.ts +11 -10
  6. package/assignFromAsync.js +3 -3
  7. package/assignFromAsync.ts +4 -4
  8. package/assignGingerly.js +136 -69
  9. package/assignGingerly.ts +170 -103
  10. package/assignPermissions/isAllowedImportPath.js +41 -0
  11. package/assignPermissions/isAllowedImportPath.ts +38 -0
  12. package/assignPermissions/restrictedProps.js +43 -0
  13. package/assignPermissions/restrictedProps.ts +53 -0
  14. package/assignTentatively.js +45 -30
  15. package/assignTentatively.ts +85 -60
  16. package/defineWithFeatures.js +38 -32
  17. package/defineWithFeatures.ts +46 -39
  18. package/eachTime.js +12 -4
  19. package/eachTime.ts +17 -7
  20. package/enhanceAll.js +2 -2
  21. package/enhanceAll.ts +3 -3
  22. package/evaluatePathWithAsyncMethods.js +29 -16
  23. package/evaluatePathWithAsyncMethods.ts +31 -16
  24. package/handlers/addEventListener.js +11 -11
  25. package/handlers/addEventListener.ts +18 -15
  26. package/handlers/lazyLoad.ts +10 -7
  27. package/handlers/lazyLoadSwitch.ts +4 -3
  28. package/handlers/manageTemplateList.js +9 -9
  29. package/handlers/manageTemplateList.ts +11 -10
  30. package/handlers/rangeSelector.ts +4 -3
  31. package/inferencer/types/assign-gingerly/types.d.ts +63 -4
  32. package/inferencer/types/nested-regex-groups/types.d.ts +12 -0
  33. package/package.json +6 -5
  34. package/processHandlerCommands.js +7 -4
  35. package/processHandlerCommands.ts +13 -10
  36. package/types/assign-gingerly/types.d.ts +63 -4
  37. package/isAllowedImportPath.js +0 -42
  38. package/isAllowedImportPath.ts +0 -53
package/assignGingerly.ts CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { EnhancementConfig } from "./types/assign-gingerly/types";
4
4
  import type { AssignFromOptions, FeatureConfigsMap, IAssignGingerlyOptions } from "./types/assign-gingerly/types";
5
- import type { AssignPermissions } from "./isAllowedImportPath.js";
5
+ import type { AssignPermissions } from './types/assign-gingerly/types.js';
6
+ import { buildRestrictedPropSet, checkRestrictedProp, redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
7
+ import type { RestrictedPropSettingsMap } from './assignPermissions/restrictedProps.js';
6
8
  import { normalizeAliasOptions } from './getValues.js';
7
9
 
8
10
  /**
@@ -248,12 +250,30 @@ function isIncCommand(key: string): boolean {
248
250
  /**
249
251
  * Helper function to parse an += command and extract the path
250
252
  */
251
- function parseIncCommand(key: string): string | null {
252
- if (!isIncCommand(key)) {
253
- return null;
254
- }
255
- return key.substring(0, key.length - 3); // Remove ' +=' suffix
256
- }
253
+ function parseIncCommand(key: string): string | null {
254
+ if (!isIncCommand(key)) {
255
+ return null;
256
+ }
257
+ return key.substring(0, key.length - 3); // Remove ' +=' suffix
258
+ }
259
+
260
+ /**
261
+ * Apply the scalar and array semantics of the += command.
262
+ */
263
+ function addValue(lhs: any, rhs: any): any {
264
+ if (Array.isArray(lhs)) {
265
+ return Array.isArray(rhs) ? [...lhs, ...rhs] : [...lhs, rhs];
266
+ }
267
+ if (typeof lhs === 'number' && typeof rhs === 'string') {
268
+ const parsed = Number(rhs);
269
+ return Number.isNaN(parsed) ? lhs + rhs : lhs + parsed;
270
+ }
271
+ if (typeof lhs === 'string' && typeof rhs === 'number') {
272
+ const parsed = Number(lhs);
273
+ return Number.isNaN(parsed) ? lhs + rhs : (parsed + rhs).toString();
274
+ }
275
+ return lhs + rhs;
276
+ }
257
277
 
258
278
  /**
259
279
  * Helper function to check if a key represents a =! command
@@ -435,7 +455,7 @@ export function evaluatePathWithMethods(
435
455
  pathParts: string[],
436
456
  value: any,
437
457
  withMethods: Set<string>
438
- ): { target: any; lastKey: string; isMethod: boolean } {
458
+ ): { target: any; lastKey: string; isMethod: boolean; isZeroArg: boolean } {
439
459
  let current = target;
440
460
  let i = 0;
441
461
 
@@ -444,12 +464,18 @@ export function evaluatePathWithMethods(
444
464
  const part = pathParts[i];
445
465
  const nextPart = pathParts[i + 1];
446
466
 
447
- if (withMethods.has(part)) {
448
- const method = current[part];
467
+ // A trailing | marks a zero-argument method call: 'deref|' calls deref()
468
+ // without consuming the next segment. Only applies to names in withMethods.
469
+ const isZeroArgMethod = part.endsWith('|') && withMethods.has(part.slice(0, -1));
470
+ const nextIsMethod = withMethods.has(nextPart)
471
+ || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1)));
472
+
473
+ if (withMethods.has(part) || isZeroArgMethod) {
474
+ const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
475
+ const method = current[methodName];
449
476
  if (typeof method === 'function') {
450
- // Check if next part is also a method
451
- if (withMethods.has(nextPart)) {
452
- // Both are methods - call first with no args
477
+ if (isZeroArgMethod || nextIsMethod) {
478
+ // Zero-arg call - next part is either a method or explicitly not an argument
453
479
  current = method.call(current);
454
480
  } else {
455
481
  // Only current is method - call with next part as string arg
@@ -458,10 +484,10 @@ export function evaluatePathWithMethods(
458
484
  }
459
485
  } else {
460
486
  // Not a function - just access property (create if needed)
461
- if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
462
- current[part] = {};
487
+ if (!(methodName in current) || typeof current[methodName] !== 'object' || current[methodName] === null) {
488
+ current[methodName] = {};
463
489
  }
464
- current = current[part];
490
+ current = current[methodName];
465
491
  }
466
492
  } else {
467
493
  // Not a method - normal property access (create if needed)
@@ -474,11 +500,16 @@ export function evaluatePathWithMethods(
474
500
  i++;
475
501
  }
476
502
 
477
- const lastKey = pathParts[pathParts.length - 1];
503
+ // Strip a trailing | from the last segment only when it names a listed method;
504
+ // otherwise it is a literal property name (e.g. an exotic key ending in |).
505
+ const rawLastKey = pathParts[pathParts.length - 1];
506
+ const isZeroArg = rawLastKey.endsWith('|') && withMethods.has(rawLastKey.slice(0, -1));
507
+ const lastKey = isZeroArg ? rawLastKey.slice(0, -1) : rawLastKey;
478
508
  return {
479
509
  target: current,
480
510
  lastKey,
481
- isMethod: withMethods.has(lastKey)
511
+ isMethod: withMethods.has(lastKey),
512
+ isZeroArg
482
513
  };
483
514
  }
484
515
 
@@ -536,7 +567,9 @@ function applyToEach(
536
567
  value: any,
537
568
  withMethods: Set<string>,
538
569
  aliasMap: Map<string, string>,
539
- options?: IAssignGingerlyOptions
570
+ options?: IAssignGingerlyOptions,
571
+ permissions?: AssignPermissions,
572
+ restrictedPropSet?: RestrictedPropSettingsMap
540
573
  ): void {
541
574
  // Convert to array for iteration
542
575
  const items = Array.isArray(iterable) ? iterable : Array.from(iterable);
@@ -560,13 +593,22 @@ function applyToEach(
560
593
  // Navigate to the nested iterable
561
594
  let current = item;
562
595
  for (const part of pathToForEach) {
563
- if (withMethods.has(part)) {
564
- const method = current[part];
596
+ // A trailing | marks a zero-argument method call (only for names in withMethods)
597
+ const isZeroArgMethod = part.endsWith('|') && withMethods.has(part.slice(0, -1));
598
+ if (withMethods.has(part) || isZeroArgMethod) {
599
+ const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
600
+ const method = current[methodName];
565
601
  if (typeof method === 'function') {
602
+ if (isZeroArgMethod) {
603
+ current = method.call(current);
604
+ continue;
605
+ }
566
606
  // For methods in the middle, we need to check the next part
567
607
  const nextIndex = pathToForEach.indexOf(part) + 1;
568
608
  const nextPart = pathToForEach[nextIndex];
569
- if (nextPart && withMethods.has(nextPart)) {
609
+ const nextIsMethod = nextPart && (withMethods.has(nextPart)
610
+ || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
611
+ if (nextIsMethod) {
570
612
  current = method.call(current);
571
613
  } else if (nextPart) {
572
614
  current = method.call(current, nextPart);
@@ -576,7 +618,7 @@ function applyToEach(
576
618
  current = method.call(current);
577
619
  }
578
620
  } else {
579
- current = current[part];
621
+ current = current[methodName];
580
622
  }
581
623
  } else {
582
624
  current = current[part];
@@ -585,7 +627,7 @@ function applyToEach(
585
627
 
586
628
  // Recursively apply to the nested iterable
587
629
  if (isIterable(current)) {
588
- applyToEach(current, pathAfterForEach, value, withMethods, aliasMap, options);
630
+ applyToEach(current, pathAfterForEach, value, withMethods, aliasMap, options, permissions, restrictedPropSet);
589
631
  }
590
632
  } else {
591
633
  // No nested @each, evaluate the remaining path normally
@@ -595,7 +637,10 @@ function applyToEach(
595
637
  // Last segment is a method - call it
596
638
  const method = result.target[result.lastKey];
597
639
  if (typeof method === 'function') {
598
- if (Array.isArray(value)) {
640
+ if (result.isZeroArg) {
641
+ // Trailing | marker - call with no arguments, ignoring the value
642
+ method.call(result.target);
643
+ } else if (Array.isArray(value)) {
599
644
  method.apply(result.target, value);
600
645
  } else {
601
646
  method.call(result.target, value);
@@ -606,13 +651,15 @@ function applyToEach(
606
651
  const lastKey = result.lastKey;
607
652
  const parent = result.target;
608
653
 
609
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
654
+ if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
655
+ // skip
656
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
610
657
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
611
658
  const currentValue = parent[lastKey];
612
659
  if (typeof currentValue !== 'object' || currentValue === null) {
613
660
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
614
661
  }
615
- assignGingerly(currentValue, value, options);
662
+ assignGingerly(currentValue, value, options, permissions);
616
663
  } else {
617
664
  parent[lastKey] = value;
618
665
  }
@@ -663,6 +710,9 @@ export function assignGingerly(
663
710
 
664
711
  const { aliasMap, withMethods: withMethodsSet } = normalizeAliasOptions(options);
665
712
 
713
+ // Normalize restrictedPropSettings once per top-level call
714
+ const restrictedPropSet = buildRestrictedPropSet(permissions);
715
+
666
716
  // Convert withAsyncMethods array to Set for O(1) lookup
667
717
  const withAsyncMethodsSet = options?.withAsyncMethods
668
718
  ? options.withAsyncMethods instanceof Set
@@ -761,55 +811,49 @@ export function assignGingerly(
761
811
  }
762
812
 
763
813
  //TODO: this logic seems to occur twice at least. Maybe make it a method?
764
- // Event handler: Element LHS + object RHS with 'on' property
814
+ if (checkRestrictedProp(restrictedPropSet, lhsKey)) {
815
+ continue;
816
+ }
817
+
818
+ // Event handler: Element LHS + object RHS with 'on' property
765
819
  if (lhsValue instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
766
820
  const capturedLhs = lhsValue;
767
821
  const capturedValue = value;
768
822
  const capturedTarget = target;
769
- const capturedOptions = options as AssignFromOptions | undefined;
770
- import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
771
- attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
823
+ const capturedOptions = options as AssignFromOptions | undefined;
824
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
825
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissions);
772
826
  });
773
827
  continue;
774
828
  }
775
829
 
776
- if (!(lhsKey in lhsParent)) {
777
- lhsParent[lhsKey] = value;
778
- } else if (Array.isArray(lhsValue)) {
779
- lhsParent[lhsKey] = Array.isArray(value)
780
- ? [...lhsValue, ...value]
781
- : [...lhsValue, value];
782
- } else if (typeof lhsValue === 'number' && typeof value === 'string') {
783
- const parsed = Number(value);
784
- lhsParent[lhsKey] = isNaN(parsed) ? lhsValue + value : lhsValue + parsed;
785
- } else {
786
- lhsParent[lhsKey] += value;
830
+ if (!(lhsKey in lhsParent)) {
831
+ lhsParent[lhsKey] = value;
832
+ } else {
833
+ lhsParent[lhsKey] = addValue(lhsValue, value);
787
834
  }
788
835
  } else {
789
- // Plain key - direct operation on target
790
- // Event handler: Element LHS + object RHS with 'on' property
836
+ // Plain key - direct operation on target
837
+ if (checkRestrictedProp(restrictedPropSet, path)) {
838
+ continue;
839
+ }
840
+
841
+ // Event handler: Element LHS + object RHS with 'on' property
791
842
  if (target[path] instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
792
843
  const capturedLhs = target[path];
793
844
  const capturedValue = value;
794
845
  const capturedTarget = target;
795
- const capturedOptions = options as AssignFromOptions | undefined;
796
- import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
797
- attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
846
+ const capturedOptions = options as AssignFromOptions | undefined;
847
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
848
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissions);
798
849
  });
799
850
  continue;
800
851
  }
801
852
 
802
- if (!(path in target)) {
803
- target[path] = value;
804
- } else if (Array.isArray(target[path])) {
805
- target[path] = Array.isArray(value)
806
- ? [...target[path], ...value]
807
- : [...target[path], value];
808
- } else if (typeof target[path] === 'number' && typeof value === 'string') {
809
- const parsed = Number(value);
810
- target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
811
- } else {
812
- target[path] += value;
853
+ if (!(path in target)) {
854
+ target[path] = value;
855
+ } else {
856
+ target[path] = addValue(target[path], value);
813
857
  }
814
858
  }
815
859
  }
@@ -864,8 +908,10 @@ export function assignGingerly(
864
908
  }
865
909
  }
866
910
 
867
- // Apply negation to LHS
868
- lhsParent[lhsLastKey] = !valueToNegate;
911
+ // Apply negation to LHS — check restriction first
912
+ if (!checkRestrictedProp(restrictedPropSet, lhsLastKey)) {
913
+ lhsParent[lhsLastKey] = !valueToNegate;
914
+ }
869
915
  }
870
916
  continue;
871
917
  }
@@ -915,11 +961,16 @@ export function assignGingerly(
915
961
  }
916
962
 
917
963
  // Handle Y= merge commands (recursive assignGingerly into sub-object)
918
- if (isMergeCommand(key)) {
919
- const path = parseMergeCommand(key);
920
- if (path) {
921
- // Navigate to the target sub-object
922
- let mergeTarget: any;
964
+ if (isMergeCommand(key)) {
965
+ const path = parseMergeCommand(key);
966
+ if (path) {
967
+ const pathParts = isNestedPath(path) ? parsePath(path) : [path];
968
+ const lastKey = pathParts[pathParts.length - 1];
969
+ if (checkRestrictedProp(restrictedPropSet, lastKey)) {
970
+ continue;
971
+ }
972
+ // Navigate to the target sub-object
973
+ let mergeTarget: any;
923
974
  if (isNestedPath(path)) {
924
975
  if (withMethodsSet) {
925
976
  const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
@@ -970,9 +1021,11 @@ export function assignGingerly(
970
1021
  pathParts,
971
1022
  forEachIndex,
972
1023
  value,
973
- withMethodsSet,
974
- aliasMap,
975
- options
1024
+ withMethodsSet,
1025
+ aliasMap,
1026
+ options,
1027
+ permissions,
1028
+ restrictedPropSet
976
1029
  );
977
1030
  } catch (error) {
978
1031
  console.error('Error in @eachTime:', error);
@@ -1002,7 +1055,7 @@ export function assignGingerly(
1002
1055
 
1003
1056
  // Apply to each item in the iterable
1004
1057
  if (isIterable(current)) {
1005
- applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options);
1058
+ applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options, permissions, restrictedPropSet);
1006
1059
  }
1007
1060
  // If not iterable, let JavaScript throw error naturally when trying to iterate
1008
1061
 
@@ -1011,13 +1064,15 @@ export function assignGingerly(
1011
1064
 
1012
1065
  // No @each in path - handle normally
1013
1066
  // Check if we need to handle async methods (fire-and-forget)
1014
- if (withAsyncMethodsSet && pathParts.some(p => withAsyncMethodsSet.has(p))) {
1067
+ if (withAsyncMethodsSet && pathParts.some(p => withAsyncMethodsSet.has(p) || (p.endsWith('|') && withAsyncMethodsSet.has(p.slice(0, -1))))) {
1015
1068
  // Fire-and-forget: dynamically import the async evaluator and run the chain
1016
1069
  const capturedTarget = target;
1017
1070
  const capturedPathParts = pathParts;
1018
1071
  const capturedValue = value;
1019
- const capturedWithMethodsSet = withMethodsSet || new Set<string>();
1020
- const capturedOptions = options;
1072
+ const capturedWithMethodsSet = withMethodsSet || new Set<string>();
1073
+ const capturedOptions = options;
1074
+ const capturedPermissions = permissions;
1075
+ const capturedRestrictedPropSet = restrictedPropSet;
1021
1076
  (async () => {
1022
1077
  const { evaluatePathWithAsyncMethods } = await import('./evaluatePathWithAsyncMethods.js');
1023
1078
  const result = await evaluatePathWithAsyncMethods(
@@ -1029,7 +1084,10 @@ export function assignGingerly(
1029
1084
  // Last segment is a method — call it
1030
1085
  const method = result.target[result.lastKey];
1031
1086
  if (typeof method === 'function') {
1032
- const returnVal = Array.isArray(capturedValue)
1087
+ // Trailing | marker - call with no arguments, ignoring the value
1088
+ const returnVal = result.isZeroArg
1089
+ ? method.call(result.target)
1090
+ : Array.isArray(capturedValue)
1033
1091
  ? method.apply(result.target, capturedValue)
1034
1092
  : method.call(result.target, capturedValue);
1035
1093
  // If it's an async method, await it (for side effects)
@@ -1038,14 +1096,16 @@ export function assignGingerly(
1038
1096
  } else {
1039
1097
  // Not a method — assign the value
1040
1098
  const lastKey = result.lastKey;
1041
- const parent = result.target;
1042
- if (typeof capturedValue === 'object' && capturedValue !== null && !Array.isArray(capturedValue)) {
1099
+ const parent = result.target;
1100
+ if (redirectRestrictedProp(capturedRestrictedPropSet, parent, lastKey, capturedValue)) {
1101
+ // skip
1102
+ } else if (typeof capturedValue === 'object' && capturedValue !== null && !Array.isArray(capturedValue)) {
1043
1103
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
1044
1104
  const currentValue = parent[lastKey];
1045
1105
  if (typeof currentValue !== 'object' || currentValue === null) {
1046
1106
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
1047
1107
  }
1048
- assignGingerly(currentValue, capturedValue, capturedOptions);
1108
+ assignGingerly(currentValue, capturedValue, capturedOptions, capturedPermissions);
1049
1109
  } else {
1050
1110
  parent[lastKey] = capturedValue;
1051
1111
  }
@@ -1065,7 +1125,10 @@ export function assignGingerly(
1065
1125
  // Last segment is a method - call it
1066
1126
  const method = result.target[result.lastKey];
1067
1127
  if (typeof method === 'function') {
1068
- if (Array.isArray(value)) {
1128
+ if (result.isZeroArg) {
1129
+ // Trailing | marker - call with no arguments, ignoring the value
1130
+ method.call(result.target);
1131
+ } else if (Array.isArray(value)) {
1069
1132
  method.apply(result.target, value);
1070
1133
  } else {
1071
1134
  method.call(result.target, value);
@@ -1079,19 +1142,19 @@ export function assignGingerly(
1079
1142
  const lastKey = result.lastKey;
1080
1143
  const parent = result.target;
1081
1144
 
1082
- // Check for static assignTo protocol
1083
- if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1084
- continue;
1085
- }
1086
-
1087
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1145
+ if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
1146
+ // skip
1147
+ // Check for static assignTo protocol
1148
+ } else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1149
+ continue;
1150
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1088
1151
  // Check if property exists and is readonly
1089
1152
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
1090
1153
  const currentValue = parent[lastKey];
1091
1154
  if (typeof currentValue !== 'object' || currentValue === null) {
1092
1155
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
1093
1156
  }
1094
- assignGingerly(currentValue, value, options);
1157
+ assignGingerly(currentValue, value, options, permissions);
1095
1158
  } else {
1096
1159
  // Property is writable - replace it
1097
1160
  parent[lastKey] = value;
@@ -1104,12 +1167,12 @@ export function assignGingerly(
1104
1167
  const lastKey = pathParts[pathParts.length - 1];
1105
1168
  const parent = ensureNestedPath(target, pathParts);
1106
1169
 
1107
- // Check for static assignTo protocol
1108
- if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1109
- continue;
1110
- }
1111
-
1112
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1170
+ if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
1171
+ // skip
1172
+ // Check for static assignTo protocol
1173
+ } else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1174
+ continue;
1175
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1113
1176
  // Check if property exists and is readonly
1114
1177
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
1115
1178
  // Property is readonly - check if current value is an object
@@ -1118,7 +1181,7 @@ export function assignGingerly(
1118
1181
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
1119
1182
  }
1120
1183
  // Recursively apply assignGingerly to the readonly object
1121
- assignGingerly(currentValue, value, options);
1184
+ assignGingerly(currentValue, value, options, permissions);
1122
1185
  } else {
1123
1186
  // Property is writable - replace it
1124
1187
  parent[lastKey] = value;
@@ -1130,11 +1193,16 @@ export function assignGingerly(
1130
1193
  } else {
1131
1194
  // Non-nested path
1132
1195
 
1133
- // Check if this is a method call
1134
- if (withMethodsSet && withMethodsSet.has(key)) {
1135
- const method = target[key];
1196
+ // Check if this is a method call (a trailing | marks a zero-argument call)
1197
+ const isZeroArgKey = key.endsWith('|') && withMethodsSet !== undefined && withMethodsSet.has(key.slice(0, -1));
1198
+ if (withMethodsSet && (withMethodsSet.has(key) || isZeroArgKey)) {
1199
+ const methodName = isZeroArgKey ? key.slice(0, -1) : key;
1200
+ const method = target[methodName];
1136
1201
  if (typeof method === 'function') {
1137
- if (Array.isArray(value)) {
1202
+ if (isZeroArgKey) {
1203
+ // Trailing | marker - call with no arguments, ignoring the value
1204
+ method.call(target);
1205
+ } else if (Array.isArray(value)) {
1138
1206
  method.apply(target, value);
1139
1207
  } else {
1140
1208
  method.call(target, value);
@@ -1144,13 +1212,12 @@ export function assignGingerly(
1144
1212
  continue;
1145
1213
  }
1146
1214
 
1147
- // Normal assignment
1148
- // Check for static assignTo protocol
1149
- if (key in target && tryAssignTo(target[key], value, target, key)) {
1150
- continue;
1151
- }
1152
-
1153
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1215
+ if (redirectRestrictedProp(restrictedPropSet, target, key, value)) {
1216
+ // skip
1217
+ // Check for static assignTo protocol
1218
+ } else if (key in target && tryAssignTo(target[key], value, target, key)) {
1219
+ continue;
1220
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1154
1221
  // Check if property exists and is readonly
1155
1222
  if (key in target && isReadonlyProperty(target, key)) {
1156
1223
  // Property is readonly - check if current value is an object
@@ -1159,7 +1226,7 @@ export function assignGingerly(
1159
1226
  throw new Error(`Cannot merge object into readonly primitive property '${String(key)}'`);
1160
1227
  }
1161
1228
  // Recursively apply assignGingerly to the readonly object
1162
- assignGingerly(currentValue, value, options);
1229
+ assignGingerly(currentValue, value, options, permissions);
1163
1230
  } else {
1164
1231
  // Property is writable - replace it
1165
1232
  target[key] = value;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Check whether an import specifier resolves to the current origin or is
3
+ * covered by an import-map entry.
4
+ */
5
+ export function isAllowedImportPath(value) {
6
+ if (typeof document === 'undefined' || typeof location === 'undefined')
7
+ return false;
8
+ if (!value || !(value.startsWith('./') || value.startsWith('../') || value.startsWith('/'))) {
9
+ return isImportMapSpecifier(value);
10
+ }
11
+ try {
12
+ return new URL(value, document.baseURI).origin === location.origin;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function isImportMapSpecifier(value) {
19
+ const importMaps = document.querySelectorAll('script[type="importmap"]');
20
+ for (const importMap of importMaps) {
21
+ try {
22
+ const parsed = JSON.parse(importMap.textContent ?? '');
23
+ if (!isImportMap(parsed))
24
+ continue;
25
+ for (const key of Object.keys(parsed.imports)) {
26
+ if (key.endsWith('/') ? value.startsWith(key) : value === key)
27
+ return true;
28
+ }
29
+ }
30
+ catch {
31
+ // Ignore malformed import maps and continue searching.
32
+ }
33
+ }
34
+ return false;
35
+ }
36
+ function isImportMap(value) {
37
+ if (!value || typeof value !== 'object' || !('imports' in value))
38
+ return false;
39
+ const { imports } = value;
40
+ return !!imports && typeof imports === 'object' && !Array.isArray(imports);
41
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Check whether an import specifier resolves to the current origin or is
3
+ * covered by an import-map entry.
4
+ */
5
+ export function isAllowedImportPath(value: string): boolean {
6
+ if (typeof document === 'undefined' || typeof location === 'undefined') return false;
7
+ if (!value || !(value.startsWith('./') || value.startsWith('../') || value.startsWith('/'))) {
8
+ return isImportMapSpecifier(value);
9
+ }
10
+
11
+ try {
12
+ return new URL(value, document.baseURI).origin === location.origin;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ function isImportMapSpecifier(value: string): boolean {
19
+ const importMaps = document.querySelectorAll('script[type="importmap"]');
20
+ for (const importMap of importMaps) {
21
+ try {
22
+ const parsed = JSON.parse(importMap.textContent ?? '');
23
+ if (!isImportMap(parsed)) continue;
24
+ for (const key of Object.keys(parsed.imports)) {
25
+ if (key.endsWith('/') ? value.startsWith(key) : value === key) return true;
26
+ }
27
+ } catch {
28
+ // Ignore malformed import maps and continue searching.
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+
34
+ function isImportMap(value: unknown): value is { imports: Record<string, unknown> } {
35
+ if (!value || typeof value !== 'object' || !('imports' in value)) return false;
36
+ const { imports } = value;
37
+ return !!imports && typeof imports === 'object' && !Array.isArray(imports);
38
+ }
@@ -0,0 +1,43 @@
1
+ const warnedOnce = new Set();
2
+ function warnRestricted(key) {
3
+ if (!warnedOnce.has(key)) {
4
+ warnedOnce.add(key);
5
+ console.warn(`assignGingerly: property '${key}' is in restrictedPropSettings — assignment skipped.`);
6
+ }
7
+ }
8
+ export function buildRestrictedPropSet(permissions) {
9
+ const settings = permissions?.restrictedPropSettings;
10
+ if (!settings || settings.length === 0)
11
+ return undefined;
12
+ const restrictedPropSet = new Map();
13
+ for (const setting of settings) {
14
+ const prop = typeof setting === 'string' ? setting : setting.prop;
15
+ if (restrictedPropSet.has(prop)) {
16
+ throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${prop}'.`);
17
+ }
18
+ restrictedPropSet.set(prop, typeof setting === 'string' ? undefined : setting);
19
+ }
20
+ return restrictedPropSet;
21
+ }
22
+ export function checkRestrictedProp(restrictedPropSet, key) {
23
+ if (!restrictedPropSet || !restrictedPropSet.has(key))
24
+ return false;
25
+ warnRestricted(key);
26
+ return true;
27
+ }
28
+ export function redirectRestrictedProp(restrictedPropSet, target, key, value) {
29
+ if (!restrictedPropSet || !restrictedPropSet.has(key))
30
+ return false;
31
+ const setting = restrictedPropSet.get(key);
32
+ if (!setting?.useMethod) {
33
+ warnRestricted(key);
34
+ return true;
35
+ }
36
+ const method = target?.[setting.useMethod];
37
+ if (typeof method !== 'function') {
38
+ warnRestricted(key);
39
+ return true;
40
+ }
41
+ method.call(target, value);
42
+ return true;
43
+ }