assign-gingerly 0.0.78 → 0.0.79
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 +28 -22
- package/assignGingerly.ts +28 -22
- package/assignPermissions/PermissionProcessor.js +34 -7
- package/assignPermissions/PermissionProcessor.ts +45 -10
- package/evaluatePathWithAsyncMethods.js +6 -4
- package/evaluatePathWithAsyncMethods.ts +6 -4
- package/inferencer/types/NewCustomElement.md +180 -5
- package/inferencer/types/assign-gingerly/types.d.ts +5 -1
- package/inferencer/types/face-up/types.d.ts +5 -0
- package/package.json +1 -1
- package/types/assign-gingerly/types.d.ts +5 -1
package/assignGingerly.js
CHANGED
|
@@ -433,13 +433,14 @@ export function evaluatePathWithMethods(target, pathParts, value, withMethods, p
|
|
|
433
433
|
const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
|
|
434
434
|
const method = current[methodName];
|
|
435
435
|
if (typeof method === 'function') {
|
|
436
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
436
437
|
if (isZeroArgMethod || nextIsMethod) {
|
|
437
438
|
// Zero-arg call - next part is either a method or explicitly not an argument
|
|
438
|
-
current = method.call(current);
|
|
439
|
+
current = method.call(current, ...appendArgs);
|
|
439
440
|
}
|
|
440
441
|
else {
|
|
441
442
|
// Only current is method - call with next part as string arg
|
|
442
|
-
current = method.call(current, nextPart);
|
|
443
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
443
444
|
i++; // Skip next part since we consumed it as argument
|
|
444
445
|
}
|
|
445
446
|
}
|
|
@@ -541,8 +542,9 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
|
|
|
541
542
|
const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
|
|
542
543
|
const method = current[methodName];
|
|
543
544
|
if (typeof method === 'function') {
|
|
545
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
544
546
|
if (isZeroArgMethod) {
|
|
545
|
-
current = method.call(current);
|
|
547
|
+
current = method.call(current, ...appendArgs);
|
|
546
548
|
continue;
|
|
547
549
|
}
|
|
548
550
|
// For methods in the middle, we need to check the next part
|
|
@@ -551,15 +553,15 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
|
|
|
551
553
|
const nextIsMethod = nextPart && (withMethods.has(nextPart)
|
|
552
554
|
|| (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
|
|
553
555
|
if (nextIsMethod) {
|
|
554
|
-
current = method.call(current);
|
|
556
|
+
current = method.call(current, ...appendArgs);
|
|
555
557
|
}
|
|
556
558
|
else if (nextPart) {
|
|
557
|
-
current = method.call(current, nextPart);
|
|
559
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
558
560
|
// Skip next part
|
|
559
561
|
pathToForEach.splice(nextIndex, 1);
|
|
560
562
|
}
|
|
561
563
|
else {
|
|
562
|
-
current = method.call(current);
|
|
564
|
+
current = method.call(current, ...appendArgs);
|
|
563
565
|
}
|
|
564
566
|
}
|
|
565
567
|
else {
|
|
@@ -582,15 +584,16 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
|
|
|
582
584
|
// Last segment is a method - call it
|
|
583
585
|
const method = result.target[result.lastKey];
|
|
584
586
|
if (typeof method === 'function') {
|
|
587
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
585
588
|
if (result.isZeroArg) {
|
|
586
|
-
// Trailing | marker - call with
|
|
587
|
-
method.call(result.target);
|
|
589
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
590
|
+
method.call(result.target, ...appendArgs);
|
|
588
591
|
}
|
|
589
592
|
else if (Array.isArray(value)) {
|
|
590
|
-
method.apply(result.target, value);
|
|
593
|
+
method.apply(result.target, [...value, ...appendArgs]);
|
|
591
594
|
}
|
|
592
595
|
else {
|
|
593
|
-
method.call(result.target, value);
|
|
596
|
+
method.call(result.target, value, ...appendArgs);
|
|
594
597
|
}
|
|
595
598
|
}
|
|
596
599
|
}
|
|
@@ -991,12 +994,13 @@ export function assignGingerly(target, source, options, permissionProcessor) {
|
|
|
991
994
|
// Last segment is a method — call it
|
|
992
995
|
const method = result.target[result.lastKey];
|
|
993
996
|
if (typeof method === 'function') {
|
|
994
|
-
|
|
997
|
+
const appendArgs = capturedPermissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
998
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
995
999
|
const returnVal = result.isZeroArg
|
|
996
|
-
? method.call(result.target)
|
|
1000
|
+
? method.call(result.target, ...appendArgs)
|
|
997
1001
|
: Array.isArray(capturedValue)
|
|
998
|
-
? method.apply(result.target, capturedValue)
|
|
999
|
-
: method.call(result.target, capturedValue);
|
|
1002
|
+
? method.apply(result.target, [...capturedValue, ...appendArgs])
|
|
1003
|
+
: method.call(result.target, capturedValue, ...appendArgs);
|
|
1000
1004
|
// If it's an async method, await it (for side effects)
|
|
1001
1005
|
if (result.isAsyncMethod)
|
|
1002
1006
|
await returnVal;
|
|
@@ -1035,15 +1039,16 @@ export function assignGingerly(target, source, options, permissionProcessor) {
|
|
|
1035
1039
|
// Last segment is a method - call it
|
|
1036
1040
|
const method = result.target[result.lastKey];
|
|
1037
1041
|
if (typeof method === 'function') {
|
|
1042
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
1038
1043
|
if (result.isZeroArg) {
|
|
1039
|
-
// Trailing | marker - call with
|
|
1040
|
-
method.call(result.target);
|
|
1044
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1045
|
+
method.call(result.target, ...appendArgs);
|
|
1041
1046
|
}
|
|
1042
1047
|
else if (Array.isArray(value)) {
|
|
1043
|
-
method.apply(result.target, value);
|
|
1048
|
+
method.apply(result.target, [...value, ...appendArgs]);
|
|
1044
1049
|
}
|
|
1045
1050
|
else {
|
|
1046
|
-
method.call(result.target, value);
|
|
1051
|
+
method.call(result.target, value, ...appendArgs);
|
|
1047
1052
|
}
|
|
1048
1053
|
}
|
|
1049
1054
|
// Silently skip if not a function
|
|
@@ -1117,15 +1122,16 @@ export function assignGingerly(target, source, options, permissionProcessor) {
|
|
|
1117
1122
|
const methodName = isZeroArgKey ? key.slice(0, -1) : key;
|
|
1118
1123
|
const method = target[methodName];
|
|
1119
1124
|
if (typeof method === 'function') {
|
|
1125
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
1120
1126
|
if (isZeroArgKey) {
|
|
1121
|
-
// Trailing | marker - call with
|
|
1122
|
-
method.call(target);
|
|
1127
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1128
|
+
method.call(target, ...appendArgs);
|
|
1123
1129
|
}
|
|
1124
1130
|
else if (Array.isArray(value)) {
|
|
1125
|
-
method.apply(target, value);
|
|
1131
|
+
method.apply(target, [...value, ...appendArgs]);
|
|
1126
1132
|
}
|
|
1127
1133
|
else {
|
|
1128
|
-
method.call(target, value);
|
|
1134
|
+
method.call(target, value, ...appendArgs);
|
|
1129
1135
|
}
|
|
1130
1136
|
}
|
|
1131
1137
|
// Silently skip if not a function
|
package/assignGingerly.ts
CHANGED
|
@@ -525,12 +525,13 @@ export function evaluatePathWithMethods(
|
|
|
525
525
|
const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
|
|
526
526
|
const method = current[methodName];
|
|
527
527
|
if (typeof method === 'function') {
|
|
528
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
528
529
|
if (isZeroArgMethod || nextIsMethod) {
|
|
529
530
|
// Zero-arg call - next part is either a method or explicitly not an argument
|
|
530
|
-
current = method.call(current);
|
|
531
|
+
current = method.call(current, ...appendArgs);
|
|
531
532
|
} else {
|
|
532
533
|
// Only current is method - call with next part as string arg
|
|
533
|
-
current = method.call(current, nextPart);
|
|
534
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
534
535
|
i++; // Skip next part since we consumed it as argument
|
|
535
536
|
}
|
|
536
537
|
} else {
|
|
@@ -649,8 +650,9 @@ function applyToEach(
|
|
|
649
650
|
const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
|
|
650
651
|
const method = current[methodName];
|
|
651
652
|
if (typeof method === 'function') {
|
|
653
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
652
654
|
if (isZeroArgMethod) {
|
|
653
|
-
current = method.call(current);
|
|
655
|
+
current = method.call(current, ...appendArgs);
|
|
654
656
|
continue;
|
|
655
657
|
}
|
|
656
658
|
// For methods in the middle, we need to check the next part
|
|
@@ -659,13 +661,13 @@ function applyToEach(
|
|
|
659
661
|
const nextIsMethod = nextPart && (withMethods.has(nextPart)
|
|
660
662
|
|| (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
|
|
661
663
|
if (nextIsMethod) {
|
|
662
|
-
current = method.call(current);
|
|
664
|
+
current = method.call(current, ...appendArgs);
|
|
663
665
|
} else if (nextPart) {
|
|
664
|
-
current = method.call(current, nextPart);
|
|
666
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
665
667
|
// Skip next part
|
|
666
668
|
pathToForEach.splice(nextIndex, 1);
|
|
667
669
|
} else {
|
|
668
|
-
current = method.call(current);
|
|
670
|
+
current = method.call(current, ...appendArgs);
|
|
669
671
|
}
|
|
670
672
|
} else {
|
|
671
673
|
current = current[methodName];
|
|
@@ -687,13 +689,14 @@ function applyToEach(
|
|
|
687
689
|
// Last segment is a method - call it
|
|
688
690
|
const method = result.target[result.lastKey];
|
|
689
691
|
if (typeof method === 'function') {
|
|
692
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
690
693
|
if (result.isZeroArg) {
|
|
691
|
-
// Trailing | marker - call with
|
|
692
|
-
method.call(result.target);
|
|
694
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
695
|
+
method.call(result.target, ...appendArgs);
|
|
693
696
|
} else if (Array.isArray(value)) {
|
|
694
|
-
method.apply(result.target, value);
|
|
697
|
+
method.apply(result.target, [...value, ...appendArgs]);
|
|
695
698
|
} else {
|
|
696
|
-
method.call(result.target, value);
|
|
699
|
+
method.call(result.target, value, ...appendArgs);
|
|
697
700
|
}
|
|
698
701
|
}
|
|
699
702
|
} else {
|
|
@@ -1130,12 +1133,13 @@ export function assignGingerly(
|
|
|
1130
1133
|
// Last segment is a method — call it
|
|
1131
1134
|
const method = result.target[result.lastKey];
|
|
1132
1135
|
if (typeof method === 'function') {
|
|
1133
|
-
|
|
1136
|
+
const appendArgs = capturedPermissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
1137
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1134
1138
|
const returnVal = result.isZeroArg
|
|
1135
|
-
? method.call(result.target)
|
|
1139
|
+
? method.call(result.target, ...appendArgs)
|
|
1136
1140
|
: Array.isArray(capturedValue)
|
|
1137
|
-
? method.apply(result.target, capturedValue)
|
|
1138
|
-
: method.call(result.target, capturedValue);
|
|
1141
|
+
? method.apply(result.target, [...capturedValue, ...appendArgs])
|
|
1142
|
+
: method.call(result.target, capturedValue, ...appendArgs);
|
|
1139
1143
|
// If it's an async method, await it (for side effects)
|
|
1140
1144
|
if (result.isAsyncMethod) await returnVal;
|
|
1141
1145
|
}
|
|
@@ -1171,13 +1175,14 @@ export function assignGingerly(
|
|
|
1171
1175
|
// Last segment is a method - call it
|
|
1172
1176
|
const method = result.target[result.lastKey];
|
|
1173
1177
|
if (typeof method === 'function') {
|
|
1178
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
1174
1179
|
if (result.isZeroArg) {
|
|
1175
|
-
// Trailing | marker - call with
|
|
1176
|
-
method.call(result.target);
|
|
1180
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1181
|
+
method.call(result.target, ...appendArgs);
|
|
1177
1182
|
} else if (Array.isArray(value)) {
|
|
1178
|
-
method.apply(result.target, value);
|
|
1183
|
+
method.apply(result.target, [...value, ...appendArgs]);
|
|
1179
1184
|
} else {
|
|
1180
|
-
method.call(result.target, value);
|
|
1185
|
+
method.call(result.target, value, ...appendArgs);
|
|
1181
1186
|
}
|
|
1182
1187
|
}
|
|
1183
1188
|
// Silently skip if not a function
|
|
@@ -1245,13 +1250,14 @@ export function assignGingerly(
|
|
|
1245
1250
|
const methodName = isZeroArgKey ? key.slice(0, -1) : key;
|
|
1246
1251
|
const method = target[methodName];
|
|
1247
1252
|
if (typeof method === 'function') {
|
|
1253
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
1248
1254
|
if (isZeroArgKey) {
|
|
1249
|
-
// Trailing | marker - call with
|
|
1250
|
-
method.call(target);
|
|
1255
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1256
|
+
method.call(target, ...appendArgs);
|
|
1251
1257
|
} else if (Array.isArray(value)) {
|
|
1252
|
-
method.apply(target, value);
|
|
1258
|
+
method.apply(target, [...value, ...appendArgs]);
|
|
1253
1259
|
} else {
|
|
1254
|
-
method.call(target, value);
|
|
1260
|
+
method.call(target, value, ...appendArgs);
|
|
1255
1261
|
}
|
|
1256
1262
|
}
|
|
1257
1263
|
// Silently skip if not a function
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { isAllowedUrl } from './isAllowedUrl.js';
|
|
2
|
+
import { getValue } from '../resolve/getValues.js';
|
|
2
3
|
export class PermissionProcessor {
|
|
3
4
|
constructor(permissions) {
|
|
4
5
|
this.permissions = permissions;
|
|
5
6
|
const { props, attrs } = buildMaps(permissions);
|
|
6
7
|
this.props = props;
|
|
7
8
|
this.attrs = attrs;
|
|
8
|
-
|
|
9
|
+
const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
|
|
10
|
+
this.blockedMethods = blockedMethods;
|
|
11
|
+
this.configuredMethods = configuredMethods;
|
|
9
12
|
this.warned = new Set();
|
|
10
13
|
this.warnedMethods = new Set();
|
|
11
14
|
}
|
|
@@ -25,11 +28,29 @@ export class PermissionProcessor {
|
|
|
25
28
|
return true;
|
|
26
29
|
}
|
|
27
30
|
checkRestrictedMethod(methodName) {
|
|
28
|
-
if (!this.
|
|
31
|
+
if (!this.blockedMethods.has(methodName))
|
|
29
32
|
return false;
|
|
30
33
|
this.warnRestrictedMethod(methodName);
|
|
31
34
|
return true;
|
|
32
35
|
}
|
|
36
|
+
getMethodAppendArgs(methodName) {
|
|
37
|
+
const config = this.configuredMethods.get(methodName);
|
|
38
|
+
if (!config)
|
|
39
|
+
return undefined;
|
|
40
|
+
const rawArgs = config.appendArgs ?? config.addArgs;
|
|
41
|
+
if (!rawArgs || rawArgs.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
const resolved = [];
|
|
44
|
+
for (const arg of rawArgs) {
|
|
45
|
+
if (typeof arg === 'string' && arg.startsWith('?.')) {
|
|
46
|
+
resolved.push(getValue(arg, this.permissions));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
resolved.push(arg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
33
54
|
redirectRestrictedProp(target, key, value) {
|
|
34
55
|
if (!this.props.has(key))
|
|
35
56
|
return false;
|
|
@@ -151,16 +172,22 @@ function normalizeAttrNames(attr, propNames) {
|
|
|
151
172
|
return propNames;
|
|
152
173
|
return normalizeStrings(attr);
|
|
153
174
|
}
|
|
154
|
-
function
|
|
175
|
+
function buildMethodMaps(permissions) {
|
|
155
176
|
const methodSettings = permissions?.restrictedMethodSettings;
|
|
156
|
-
const
|
|
177
|
+
const blockedMethods = new Set();
|
|
178
|
+
const configuredMethods = new Map();
|
|
157
179
|
if (!methodSettings || methodSettings.length === 0) {
|
|
158
|
-
return
|
|
180
|
+
return { blockedMethods, configuredMethods };
|
|
159
181
|
}
|
|
160
182
|
for (const setting of methodSettings) {
|
|
161
183
|
if (typeof setting === 'string') {
|
|
162
|
-
|
|
184
|
+
blockedMethods.add(setting);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (configuredMethods.has(setting.method)) {
|
|
188
|
+
throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
|
|
163
189
|
}
|
|
190
|
+
configuredMethods.set(setting.method, setting);
|
|
164
191
|
}
|
|
165
|
-
return
|
|
192
|
+
return { blockedMethods, configuredMethods };
|
|
166
193
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AssignPermissions, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
|
|
1
|
+
import type { AssignPermissions, RestrictedMethodConfig, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
|
|
2
|
+
import { getValue } from '../resolve/getValues.js';
|
|
2
3
|
import { isAllowedUrl } from './isAllowedUrl.js';
|
|
3
4
|
|
|
4
5
|
export interface RestrictedPropSettingsMap {
|
|
@@ -10,7 +11,8 @@ export class PermissionProcessor {
|
|
|
10
11
|
private readonly permissions: AssignPermissions | undefined;
|
|
11
12
|
private readonly props: Map<string, RestrictedPropSetting | undefined>;
|
|
12
13
|
private readonly attrs: Map<string, RestrictedPropSetting | undefined>;
|
|
13
|
-
private readonly
|
|
14
|
+
private readonly blockedMethods: Set<string>;
|
|
15
|
+
private readonly configuredMethods: Map<string, RestrictedMethodConfig>;
|
|
14
16
|
private readonly warned = new Set<string>();
|
|
15
17
|
private readonly warnedMethods = new Set<string>();
|
|
16
18
|
|
|
@@ -19,7 +21,9 @@ export class PermissionProcessor {
|
|
|
19
21
|
const { props, attrs } = buildMaps(permissions);
|
|
20
22
|
this.props = props;
|
|
21
23
|
this.attrs = attrs;
|
|
22
|
-
|
|
24
|
+
const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
|
|
25
|
+
this.blockedMethods = blockedMethods;
|
|
26
|
+
this.configuredMethods = configuredMethods;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
get crossDomainImports(): boolean {
|
|
@@ -41,11 +45,36 @@ export class PermissionProcessor {
|
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
checkRestrictedMethod(methodName: string): boolean {
|
|
44
|
-
if (!this.
|
|
48
|
+
if (!this.blockedMethods.has(methodName)) return false;
|
|
45
49
|
this.warnRestrictedMethod(methodName);
|
|
46
50
|
return true;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Returns the resolved appendArgs for a configured method, if any.
|
|
55
|
+
* - String entries that start with `?.` are resolved against the permissions object.
|
|
56
|
+
* - Non-path strings are returned as-is.
|
|
57
|
+
* - Methods listed as plain strings in restrictedMethodSettings do not return args;
|
|
58
|
+
* they are fully blocked via checkRestrictedMethod.
|
|
59
|
+
*/
|
|
60
|
+
getMethodAppendArgs(methodName: string): any[] | undefined {
|
|
61
|
+
const config = this.configuredMethods.get(methodName);
|
|
62
|
+
if (!config) return undefined;
|
|
63
|
+
|
|
64
|
+
const rawArgs = config.appendArgs ?? config.addArgs;
|
|
65
|
+
if (!rawArgs || rawArgs.length === 0) return undefined;
|
|
66
|
+
|
|
67
|
+
const resolved: any[] = [];
|
|
68
|
+
for (const arg of rawArgs) {
|
|
69
|
+
if (typeof arg === 'string' && arg.startsWith('?.')) {
|
|
70
|
+
resolved.push(getValue(arg, this.permissions));
|
|
71
|
+
} else {
|
|
72
|
+
resolved.push(arg);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
redirectRestrictedProp(target: any, key: string, value: any): boolean {
|
|
50
79
|
if (!this.props.has(key)) return false;
|
|
51
80
|
const setting = this.props.get(key);
|
|
@@ -192,19 +221,25 @@ function normalizeAttrNames(
|
|
|
192
221
|
return normalizeStrings(attr);
|
|
193
222
|
}
|
|
194
223
|
|
|
195
|
-
function
|
|
224
|
+
function buildMethodMaps(permissions: AssignPermissions | undefined): { blockedMethods: Set<string>; configuredMethods: Map<string, RestrictedMethodConfig> } {
|
|
196
225
|
const methodSettings = permissions?.restrictedMethodSettings;
|
|
197
|
-
const
|
|
226
|
+
const blockedMethods = new Set<string>();
|
|
227
|
+
const configuredMethods = new Map<string, RestrictedMethodConfig>();
|
|
198
228
|
if (!methodSettings || methodSettings.length === 0) {
|
|
199
|
-
return
|
|
229
|
+
return { blockedMethods, configuredMethods };
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
for (const setting of methodSettings) {
|
|
203
233
|
if (typeof setting === 'string') {
|
|
204
|
-
|
|
234
|
+
blockedMethods.add(setting);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (configuredMethods.has(setting.method)) {
|
|
239
|
+
throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
|
|
205
240
|
}
|
|
206
|
-
|
|
241
|
+
configuredMethods.set(setting.method, setting);
|
|
207
242
|
}
|
|
208
243
|
|
|
209
|
-
return
|
|
244
|
+
return { blockedMethods, configuredMethods };
|
|
210
245
|
}
|
|
@@ -43,13 +43,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
|
|
|
43
43
|
// Async method — call and await
|
|
44
44
|
const method = current[baseName];
|
|
45
45
|
if (typeof method === 'function') {
|
|
46
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
46
47
|
if (isZeroArgAsync || nextIsMethod) {
|
|
47
48
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
48
|
-
current = await method.call(current);
|
|
49
|
+
current = await method.call(current, ...appendArgs);
|
|
49
50
|
}
|
|
50
51
|
else {
|
|
51
52
|
// Call with next part as string arg, then await
|
|
52
|
-
current = await method.call(current, nextPart);
|
|
53
|
+
current = await method.call(current, nextPart, ...appendArgs);
|
|
53
54
|
i++; // Skip next part since we consumed it as argument
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -65,13 +66,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
|
|
|
65
66
|
// Sync method — same logic as evaluatePathWithMethods
|
|
66
67
|
const method = current[baseName];
|
|
67
68
|
if (typeof method === 'function') {
|
|
69
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
68
70
|
if (isZeroArgSync || nextIsMethod) {
|
|
69
71
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
70
|
-
current = method.call(current);
|
|
72
|
+
current = method.call(current, ...appendArgs);
|
|
71
73
|
}
|
|
72
74
|
else {
|
|
73
75
|
// Call with next part as string arg
|
|
74
|
-
current = method.call(current, nextPart);
|
|
76
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
75
77
|
i++; // Skip next part since we consumed it as argument
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -69,12 +69,13 @@ export async function evaluatePathWithAsyncMethods(
|
|
|
69
69
|
// Async method — call and await
|
|
70
70
|
const method = current[baseName];
|
|
71
71
|
if (typeof method === 'function') {
|
|
72
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
72
73
|
if (isZeroArgAsync || nextIsMethod) {
|
|
73
74
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
74
|
-
current = await method.call(current);
|
|
75
|
+
current = await method.call(current, ...appendArgs);
|
|
75
76
|
} else {
|
|
76
77
|
// Call with next part as string arg, then await
|
|
77
|
-
current = await method.call(current, nextPart);
|
|
78
|
+
current = await method.call(current, nextPart, ...appendArgs);
|
|
78
79
|
i++; // Skip next part since we consumed it as argument
|
|
79
80
|
}
|
|
80
81
|
} else {
|
|
@@ -88,12 +89,13 @@ export async function evaluatePathWithAsyncMethods(
|
|
|
88
89
|
// Sync method — same logic as evaluatePathWithMethods
|
|
89
90
|
const method = current[baseName];
|
|
90
91
|
if (typeof method === 'function') {
|
|
92
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
91
93
|
if (isZeroArgSync || nextIsMethod) {
|
|
92
94
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
93
|
-
current = method.call(current);
|
|
95
|
+
current = method.call(current, ...appendArgs);
|
|
94
96
|
} else {
|
|
95
97
|
// Call with next part as string arg
|
|
96
|
-
current = method.call(current, nextPart);
|
|
98
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
97
99
|
i++; // Skip next part since we consumed it as argument
|
|
98
100
|
}
|
|
99
101
|
} else {
|
|
@@ -8,9 +8,10 @@ This document provides step-by-step instructions for creating a **brand new** cu
|
|
|
8
8
|
- Custom element features (composable behavior classes injected into elements) — see [NewCustomElementFeature.md](./NewCustomElementFeature.md)
|
|
9
9
|
- Enhancements (declarative behaviors attached to existing elements via attributes) — see [NewEnhancementInstructions.md](./NewEnhancementInstructions.md)
|
|
10
10
|
|
|
11
|
-
## Reference
|
|
11
|
+
## Reference Implementations
|
|
12
12
|
|
|
13
13
|
- **[time-ticker](https://github.com/bahrus/time-ticker)** — A non-visual custom element that fires events periodically. Demonstrates extending `ElementMaker`, a custom feature (`TimeTicker`), roundabout wiring via `defRef.json`, and the `def.js` / `wireFeatures.js` pattern.
|
|
14
|
+
- **[scratch-box](https://github.com/bahrus/scratch-box)** — A visual, form-associated custom element with a declarative shadow DOM template and zero custom element JavaScript. Demonstrates `cede` script definition from a static `root.html` and JSON feature configuration.
|
|
14
15
|
|
|
15
16
|
## Prerequisites
|
|
16
17
|
|
|
@@ -51,7 +52,9 @@ This document provides step-by-step instructions for creating a **brand new** cu
|
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"assign-gingerly": "0.0.48",
|
|
54
|
-
"el-maker": "0.0.0"
|
|
55
|
+
"el-maker": "0.0.0",
|
|
56
|
+
"imp-h": "0.0.5",
|
|
57
|
+
"mount-observer": "0.1.50"
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
```
|
|
@@ -92,7 +95,12 @@ export type T = AllProps;
|
|
|
92
95
|
- `AllProps` — includes internal/computed state managed by roundabout
|
|
93
96
|
- Export `T` as a convenience alias for use in `defRef.mjs` type annotations
|
|
94
97
|
|
|
95
|
-
## Step 4: Create the Element Class
|
|
98
|
+
## Step 4: Create the Element Class, if the complexity is too much for a "code-free" solution.
|
|
99
|
+
|
|
100
|
+
For visual web components that use declarative Shadow DOM, step 4 should be considered as a last resort, after exausting:
|
|
101
|
+
|
|
102
|
+
1. The power of assign-gingerly/assignFrom/RoundaboutLib configuration (JSON)
|
|
103
|
+
2. Defining a new reusable custom element feature to include with el-maker's package (Step 5).
|
|
96
104
|
|
|
97
105
|
Create `[element-name]-element.js` (e.g., `my-element-element.js`):
|
|
98
106
|
|
|
@@ -375,9 +383,176 @@ Plus infrastructure:
|
|
|
375
383
|
|
|
376
384
|
For elements like `time-ticker` that have no HTML template or shadow DOM, simply don't activate the `templateMaker` feature in `wireFeatures.js`. The feature remains declared in `supportedFeatures` (inherited from `ElementMaker`) but is never instantiated because no `assignFeatures` call references it.
|
|
377
385
|
|
|
378
|
-
## Elements With HTML
|
|
386
|
+
## Elements With HTML: Declarative Shadow DOM Without a JS Class
|
|
387
|
+
|
|
388
|
+
For visual elements you can skip the `def.js` / `wireFeatures.js` / custom element class entirely and register the element with a `cede` script that extends `el-maker`. The scratch-box checkbox is the reference implementation of this pattern: it is built from a single static HTML file (`root.html`) and a JSON feature configuration (`el-maker.json`).
|
|
389
|
+
|
|
390
|
+
### How scratch-box is structured
|
|
391
|
+
|
|
392
|
+
| File | Role |
|
|
393
|
+
|------|------|
|
|
394
|
+
| `root.html` | Declarative shadow DOM template, styles, inner form, and enhancement metadata. |
|
|
395
|
+
| `el-maker.mjs` | Type-checked configuration generator for the ElementMaker features. |
|
|
396
|
+
| `el-maker.json` | Generated JSON consumed by the `cede` script. |
|
|
397
|
+
|
|
398
|
+
### The template file (`root.html`)
|
|
399
|
+
|
|
400
|
+
The host element declares its shadow root declaratively, then contains everything needed inside the shadow DOM, including styles, a form element, and a `<be-hive>` block that wires up declarative enhancements:
|
|
401
|
+
|
|
402
|
+
```html
|
|
403
|
+
<scratch-box>
|
|
404
|
+
<template shadowrootmode=open>
|
|
405
|
+
<style adopt>
|
|
406
|
+
:host[hidden] { display:none; }
|
|
407
|
+
:host { display:block; background-color: HSL(250, 22%, 41%); padding: 1vw; }
|
|
408
|
+
/* ... remaining styles ... */
|
|
409
|
+
</style>
|
|
410
|
+
<form class="checkbox-wrapper">
|
|
411
|
+
<input 🪢 name=value type="checkbox" id="option"/>
|
|
412
|
+
<link itemprop=value>
|
|
413
|
+
<label for="option">
|
|
414
|
+
<slot name="labelTxt">scratch-box</slot>
|
|
415
|
+
<svg viewBox="0 0 60 40" aria-hidden="true" focusable="false">
|
|
416
|
+
<path d="M21,2 ..." stroke-width="4" fill="none" stroke-dasharray="270" stroke-dashoffset="270"></path>
|
|
417
|
+
</svg>
|
|
418
|
+
</label>
|
|
419
|
+
</form>
|
|
420
|
+
|
|
421
|
+
<be-hive>
|
|
422
|
+
<script type=emc-parser
|
|
423
|
+
src="be-hive/parsers/parse-grouped-capture-statements.js"
|
|
424
|
+
parser-name=parse-grouped-capture-statements></script>
|
|
425
|
+
<script type=emc
|
|
426
|
+
src="be-bound/🪢.json"
|
|
427
|
+
wait-for-parsers=parse-grouped-capture-statements></script>
|
|
428
|
+
</be-hive>
|
|
429
|
+
</template>
|
|
430
|
+
</scratch-box>
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Key details:
|
|
434
|
+
|
|
435
|
+
- `shadowrootmode=open` gives the element a declarative shadow DOM that the browser attaches before any script runs.
|
|
436
|
+
- The internal checkbox is named `value` and carries the `🪢` emoji attribute. That marks it for the `be-bound` enhancement so the host `value` property and the inner checkbox `checked` state stay in sync.
|
|
437
|
+
- `<link itemprop=value>` lets the `faceUp` feature expose the element as a form-associated value without any JS wiring.
|
|
438
|
+
- The `<slot name="labelTxt">` lets users provide the label from light DOM via `<span slot="labelTxt">...</span>`.
|
|
439
|
+
- `<style adopt>` with `adopt` ensures the styles are adopted into the shadow root instead of a separate `<style>` element.
|
|
440
|
+
|
|
441
|
+
### The feature configuration (`el-maker.mjs` → `el-maker.json`)
|
|
442
|
+
|
|
443
|
+
Instead of `wireFeatures.js`, the features are declared in JSON and consumed by the `cede` script. The source file is type-checked TypeScript via JSDoc comments and outputs `el-maker.json`:
|
|
444
|
+
|
|
445
|
+
```javascript
|
|
446
|
+
//@ts-check
|
|
447
|
+
|
|
448
|
+
import { writeFileSync } from 'fs';
|
|
449
|
+
import { fileURLToPath } from 'url';
|
|
450
|
+
import {akaMethods as m, aka, builtInEmoji} from 'assign-gingerly/DX/emojis.js';
|
|
451
|
+
|
|
452
|
+
/** @import {FontFaceFeatureConfig} from './types/font-face-feature/types'; */
|
|
453
|
+
/** @import {EndUserProps} from './types'; */
|
|
454
|
+
/** @import {RoundaboutOptions} from './types/roundabout/types' */
|
|
455
|
+
/** @import {ElMakerConfig} from './types/el-maker/types' */
|
|
456
|
+
|
|
457
|
+
const props = {
|
|
458
|
+
value: 'value',
|
|
459
|
+
name: 'name',
|
|
460
|
+
disabled: 'disabled',
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const fontFaceFeatureConfig = {
|
|
464
|
+
fontFamilies: [
|
|
465
|
+
{
|
|
466
|
+
name: 'Indie Flower',
|
|
467
|
+
url: 'https://fonts.gstatic.com/s/indieflower/v24/m8JVjfNVeKWVnh3QMuKkFcZVZ0uH5dI.woff2',
|
|
468
|
+
descriptors: {
|
|
469
|
+
style: 'normal',
|
|
470
|
+
weight: '400',
|
|
471
|
+
unicodeRange: '...',
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
// additional font-face descriptors...
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const raConfig = {
|
|
479
|
+
assignOptions: {
|
|
480
|
+
akaMethods: {
|
|
481
|
+
'🔍': m['🔍']
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
merges: [
|
|
485
|
+
{
|
|
486
|
+
ifKeyIn: ['disabled'],
|
|
487
|
+
assign: {
|
|
488
|
+
'?.shadowRoot?.🔍?.input?.disabled': '?.disabled',
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/** @type {ElMakerConfig<EndUserProps>} */
|
|
495
|
+
const features = {
|
|
496
|
+
assignFeatures: {
|
|
497
|
+
faceUp: { customData: { integrateWithRoundabout: true } },
|
|
498
|
+
truthSourcer: {},
|
|
499
|
+
roundabout: { customData: { raConfig } },
|
|
500
|
+
fontMgr: { customData: { fontFaceFeatureConfig } },
|
|
501
|
+
templateMaker: {},
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export function render() {
|
|
506
|
+
return JSON.stringify(features, null, 4);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
510
|
+
const outputFile = __filename.replace(/\.mjs$/, '.json');
|
|
511
|
+
writeFileSync(outputFile, render(), 'utf8');
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Run `node el-maker.mjs` (or `npm run build-el-maker` if your `package.json` includes a watch script) to regenerate `el-maker.json`.
|
|
515
|
+
|
|
516
|
+
### Registering the element in a page
|
|
517
|
+
|
|
518
|
+
The element is defined by a `cede` script. Include `imp-h` (or another template importer) to fetch `root.html`, and `el-maker/def.js` to provide the base class machinery:
|
|
519
|
+
|
|
520
|
+
```html
|
|
521
|
+
<!DOCTYPE html>
|
|
522
|
+
<html lang="en">
|
|
523
|
+
<head>
|
|
524
|
+
<meta charset="UTF-8">
|
|
525
|
+
<title>scratch-box demo</title>
|
|
526
|
+
<script type=module>
|
|
527
|
+
import 'be-hive/be-hive.js';
|
|
528
|
+
import 'imp-h/imp-h.js';
|
|
529
|
+
import 'el-maker/def.js';
|
|
530
|
+
</script>
|
|
531
|
+
</head>
|
|
532
|
+
<body>
|
|
533
|
+
<scratch-box imp-h="scratch-box/root.html">
|
|
534
|
+
<span slot=labelTxt>Create demo</span>
|
|
535
|
+
<script type=cede data-extends=el-maker src="scratch-box/el-maker.json"></script>
|
|
536
|
+
</scratch-box>
|
|
537
|
+
</body>
|
|
538
|
+
</html>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Notes:
|
|
542
|
+
|
|
543
|
+
- `imp-h` observes the `imp-h` attribute and imports the declarative shadow DOM template from `root.html`.
|
|
544
|
+
- The `<script type=cede data-extends=el-maker>` tells the mount observer to register the host element by extending `ElementMaker` and applying the feature JSON.
|
|
545
|
+
- No JS class file is required because all behavior is provided by the configured ElementMaker features and the declarative shadow DOM.
|
|
546
|
+
|
|
547
|
+
### When to use this pattern
|
|
548
|
+
|
|
549
|
+
Use the declarative shadow DOM + `cede` pattern when:
|
|
550
|
+
|
|
551
|
+
- The element is primarily visual and static.
|
|
552
|
+
- You want server-side rendering and progressive enhancement with no client-side custom element class.
|
|
553
|
+
- Feature configuration (form association, attribute reflection, reactive wiring, fonts) is sufficient for all behavior.
|
|
379
554
|
|
|
380
|
-
|
|
555
|
+
When you need custom runtime behavior beyond the shared features, fall back to the class-based pattern in the earlier sections and add your own feature.
|
|
381
556
|
|
|
382
557
|
## Tips
|
|
383
558
|
|
|
@@ -955,7 +955,8 @@ export interface RestrictedPropSetting {
|
|
|
955
955
|
*/
|
|
956
956
|
export interface RestrictedMethodConfig {
|
|
957
957
|
method: string;
|
|
958
|
-
|
|
958
|
+
appendArgs?: string[]; // Phase II: append args to each method call
|
|
959
|
+
addArgs?: string[]; // Deprecated alias for appendArgs
|
|
959
960
|
}
|
|
960
961
|
|
|
961
962
|
/**
|
|
@@ -982,6 +983,8 @@ export interface AssignPermissions {
|
|
|
982
983
|
/** Sanitizer options (Phase III+) */
|
|
983
984
|
sanitizerOptions?: Record<string, any>;
|
|
984
985
|
|
|
986
|
+
customSettings?: any;
|
|
987
|
+
|
|
985
988
|
/** Restricted method settings (Phase IV+) */
|
|
986
989
|
restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
|
|
987
990
|
}
|
|
@@ -999,6 +1002,7 @@ export declare class PermissionProcessor {
|
|
|
999
1002
|
get hasAttrs(): boolean;
|
|
1000
1003
|
checkRestrictedProp(key: string): boolean;
|
|
1001
1004
|
checkRestrictedMethod(methodName: string): boolean;
|
|
1005
|
+
getMethodAppendArgs(methodName: string): any[] | undefined;
|
|
1002
1006
|
redirectRestrictedProp(target: any, key: string, value: any): boolean;
|
|
1003
1007
|
checkRestrictedAttributeCall(methodName: string, args: any[]): { blocked: boolean; attrName?: string };
|
|
1004
1008
|
}
|
|
@@ -52,6 +52,11 @@ export interface FaceUpProps {
|
|
|
52
52
|
*/
|
|
53
53
|
value: string | File | FormData | null;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The control name used when submitting the form.
|
|
57
|
+
*/
|
|
58
|
+
name: string;
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
61
|
* Internal state for form restoration (optional).
|
|
57
62
|
* If provided, passed as the second argument to setFormValue().
|
package/package.json
CHANGED
|
@@ -955,7 +955,8 @@ export interface RestrictedPropSetting {
|
|
|
955
955
|
*/
|
|
956
956
|
export interface RestrictedMethodConfig {
|
|
957
957
|
method: string;
|
|
958
|
-
|
|
958
|
+
appendArgs?: string[]; // Phase II: append args to each method call
|
|
959
|
+
addArgs?: string[]; // Deprecated alias for appendArgs
|
|
959
960
|
}
|
|
960
961
|
|
|
961
962
|
/**
|
|
@@ -982,6 +983,8 @@ export interface AssignPermissions {
|
|
|
982
983
|
/** Sanitizer options (Phase III+) */
|
|
983
984
|
sanitizerOptions?: Record<string, any>;
|
|
984
985
|
|
|
986
|
+
customSettings?: any;
|
|
987
|
+
|
|
985
988
|
/** Restricted method settings (Phase IV+) */
|
|
986
989
|
restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
|
|
987
990
|
}
|
|
@@ -999,6 +1002,7 @@ export declare class PermissionProcessor {
|
|
|
999
1002
|
get hasAttrs(): boolean;
|
|
1000
1003
|
checkRestrictedProp(key: string): boolean;
|
|
1001
1004
|
checkRestrictedMethod(methodName: string): boolean;
|
|
1005
|
+
getMethodAppendArgs(methodName: string): any[] | undefined;
|
|
1002
1006
|
redirectRestrictedProp(target: any, key: string, value: any): boolean;
|
|
1003
1007
|
checkRestrictedAttributeCall(methodName: string, args: any[]): { blocked: boolean; attrName?: string };
|
|
1004
1008
|
}
|