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.
- package/DX/emojis.js +16 -0
- package/DX/emojis.ts +16 -0
- package/README.md +30 -3
- package/assignFrom.js +6 -6
- package/assignFrom.ts +11 -10
- package/assignFromAsync.js +3 -3
- package/assignFromAsync.ts +4 -4
- package/assignGingerly.js +136 -69
- package/assignGingerly.ts +170 -103
- package/assignPermissions/isAllowedImportPath.js +41 -0
- package/assignPermissions/isAllowedImportPath.ts +38 -0
- package/assignPermissions/restrictedProps.js +43 -0
- package/assignPermissions/restrictedProps.ts +53 -0
- package/assignTentatively.js +45 -30
- package/assignTentatively.ts +85 -60
- package/defineWithFeatures.js +38 -32
- package/defineWithFeatures.ts +46 -39
- package/eachTime.js +12 -4
- package/eachTime.ts +17 -7
- package/enhanceAll.js +2 -2
- package/enhanceAll.ts +3 -3
- package/evaluatePathWithAsyncMethods.js +29 -16
- package/evaluatePathWithAsyncMethods.ts +31 -16
- package/handlers/addEventListener.js +11 -11
- package/handlers/addEventListener.ts +18 -15
- package/handlers/lazyLoad.ts +10 -7
- package/handlers/lazyLoadSwitch.ts +4 -3
- package/handlers/manageTemplateList.js +9 -9
- package/handlers/manageTemplateList.ts +11 -10
- package/handlers/rangeSelector.ts +4 -3
- package/inferencer/types/assign-gingerly/types.d.ts +63 -4
- package/inferencer/types/nested-regex-groups/types.d.ts +12 -0
- package/package.json +6 -5
- package/processHandlerCommands.js +7 -4
- package/processHandlerCommands.ts +13 -10
- package/types/assign-gingerly/types.d.ts +63 -4
- package/isAllowedImportPath.js +0 -42
- 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
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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 (!(
|
|
462
|
-
current[
|
|
487
|
+
if (!(methodName in current) || typeof current[methodName] !== 'object' || current[methodName] === null) {
|
|
488
|
+
current[methodName] = {};
|
|
463
489
|
}
|
|
464
|
-
current = current[
|
|
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
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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[
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
779
|
-
lhsParent[lhsKey] =
|
|
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
|
-
|
|
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
|
|
805
|
-
target[path] =
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
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 (
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
+
}
|