@getforma/core 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +265 -13
- package/dist/{chunk-VKKPX4YU.js → chunk-5H52PKGF.js} +62 -12
- package/dist/chunk-5H52PKGF.js.map +1 -0
- package/dist/{chunk-VP5EOGM5.cjs → chunk-TSQ7AKFT.cjs} +62 -11
- package/dist/chunk-TSQ7AKFT.cjs.map +1 -0
- package/dist/forma-runtime-csp.js +2 -0
- package/dist/forma-runtime.js +2 -0
- package/dist/formajs-runtime-hardened.global.js +1 -1
- package/dist/formajs-runtime-hardened.global.js.map +1 -1
- package/dist/formajs-runtime.global.js +1 -1
- package/dist/formajs-runtime.global.js.map +1 -1
- package/dist/formajs.global.js +1 -1
- package/dist/formajs.global.js.map +1 -1
- package/dist/index.cjs +100 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -27
- package/dist/index.d.ts +43 -27
- package/dist/index.js +57 -27
- package/dist/index.js.map +1 -1
- package/dist/runtime-hardened.cjs +166 -50
- package/dist/runtime-hardened.cjs.map +1 -1
- package/dist/runtime-hardened.d.cts +86 -0
- package/dist/runtime-hardened.d.ts +86 -0
- package/dist/runtime-hardened.js +166 -50
- package/dist/runtime-hardened.js.map +1 -1
- package/dist/runtime.cjs +188 -72
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +167 -51
- package/dist/runtime.js.map +1 -1
- package/dist/ssr/index.cjs +59 -71
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.d.cts +4 -0
- package/dist/ssr/index.d.ts +4 -0
- package/dist/ssr/index.js +59 -71
- package/dist/ssr/index.js.map +1 -1
- package/package.json +26 -3
- package/dist/chunk-VKKPX4YU.js.map +0 -1
- package/dist/chunk-VP5EOGM5.cjs.map +0 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
type UnsafeEvalMode = 'mutable' | 'locked-off' | 'locked-on';
|
|
2
|
+
interface RuntimeDiagnostic {
|
|
3
|
+
kind: 'handler-unsupported' | 'expression-unsupported';
|
|
4
|
+
expr: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
count: number;
|
|
7
|
+
firstSeenAt: number;
|
|
8
|
+
lastSeenAt: number;
|
|
9
|
+
}
|
|
10
|
+
/** Yield control to keep the main thread responsive during large batches. */
|
|
11
|
+
declare function yieldToMain(): Promise<void>;
|
|
12
|
+
interface ContainmentHintsOptions {
|
|
13
|
+
selector?: string;
|
|
14
|
+
contain?: string;
|
|
15
|
+
contentVisibility?: string;
|
|
16
|
+
containIntrinsicSize?: string;
|
|
17
|
+
skipIfAlreadySet?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Apply CSS containment hints to opt-in containers for lower layout/paint cost. */
|
|
20
|
+
declare function applyContainmentHints(root?: ParentNode, options?: ContainmentHintsOptions): number;
|
|
21
|
+
/**
|
|
22
|
+
* Load a pre-compiled directive map from the server.
|
|
23
|
+
* Keys are data-forma-id values, values are arrays of directive attribute names.
|
|
24
|
+
*/
|
|
25
|
+
declare function setDirectiveMap(map: Record<string, string[]> | null): void;
|
|
26
|
+
declare function initRuntime(): void;
|
|
27
|
+
/** Dispose all FormaJS scopes — clears effects, intervals, event listeners, and stops the observer. */
|
|
28
|
+
declare function destroyRuntime(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Mount a specific element or subtree — scans for `data-forma-state`
|
|
31
|
+
* elements and initializes their reactive bindings.
|
|
32
|
+
*
|
|
33
|
+
* Use this for manual control when injecting HTML dynamically.
|
|
34
|
+
* With the MutationObserver active, this is usually not needed.
|
|
35
|
+
*
|
|
36
|
+
* @param el - The root element to scan (checks itself and all descendants).
|
|
37
|
+
*/
|
|
38
|
+
declare function mount(el: Element): void;
|
|
39
|
+
/**
|
|
40
|
+
* Unmount a specific element or subtree — disposes all reactive bindings
|
|
41
|
+
* for `data-forma-state` elements within.
|
|
42
|
+
*
|
|
43
|
+
* @param el - The root element to clean up (checks itself and all descendants).
|
|
44
|
+
*/
|
|
45
|
+
declare function unmount(el: Element): void;
|
|
46
|
+
/** Enable/disable debug logging. Also toggleable via window.__FORMA_DEBUG = true */
|
|
47
|
+
declare function setDebug(on: boolean): void;
|
|
48
|
+
/** Set unsafe-eval mode. `locked-off` is hardened and non-toggleable via setUnsafeEval. */
|
|
49
|
+
declare function setUnsafeEvalMode(mode: UnsafeEvalMode): void;
|
|
50
|
+
/** Enable/disable unsafe `new Function` fallback for complex expressions. */
|
|
51
|
+
declare function setUnsafeEval(on: boolean): void;
|
|
52
|
+
/** Get current unsafe-eval policy mode. */
|
|
53
|
+
declare function getUnsafeEvalMode(): UnsafeEvalMode;
|
|
54
|
+
/** Enable/disable runtime diagnostics for unsupported expressions/handlers. */
|
|
55
|
+
declare function setDiagnostics(on: boolean): void;
|
|
56
|
+
/** Runtime diagnostics captured while parsing/binding templates. */
|
|
57
|
+
declare function getDiagnostics(): RuntimeDiagnostic[];
|
|
58
|
+
/** Clear runtime diagnostics collected so far. */
|
|
59
|
+
declare function clearDiagnostics(): void;
|
|
60
|
+
interface ScopeDescriptor {
|
|
61
|
+
element: Element;
|
|
62
|
+
id: string;
|
|
63
|
+
values: Record<string, {
|
|
64
|
+
value: unknown;
|
|
65
|
+
type: string;
|
|
66
|
+
}>;
|
|
67
|
+
initialJSON: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* DevTools: enumerate all active scopes and their current signal values.
|
|
71
|
+
* Only called when the State Inspector panel is open — zero overhead otherwise.
|
|
72
|
+
*/
|
|
73
|
+
declare function getScopes(): ScopeDescriptor[];
|
|
74
|
+
/**
|
|
75
|
+
* DevTools: set a state value on a specific scope element.
|
|
76
|
+
* Triggers normal reactive effects (data-show, data-text, etc.).
|
|
77
|
+
*/
|
|
78
|
+
declare function setScopeValue(element: Element, key: string, value: unknown): void;
|
|
79
|
+
/**
|
|
80
|
+
* DevTools: reset all values on a scope to their initial JSON state.
|
|
81
|
+
*/
|
|
82
|
+
declare function resetScope(element: Element): void;
|
|
83
|
+
/** Reconcile a container's DOM against a new HTML string. */
|
|
84
|
+
declare function reconcile(container: Element, html: string): void;
|
|
85
|
+
|
|
86
|
+
export { applyContainmentHints, clearDiagnostics, destroyRuntime, getDiagnostics, getScopes, getUnsafeEvalMode, initRuntime, mount, reconcile, resetScope, setDebug, setDiagnostics, setDirectiveMap, setScopeValue, setUnsafeEval, setUnsafeEvalMode, unmount, yieldToMain };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
type UnsafeEvalMode = 'mutable' | 'locked-off' | 'locked-on';
|
|
2
|
+
interface RuntimeDiagnostic {
|
|
3
|
+
kind: 'handler-unsupported' | 'expression-unsupported';
|
|
4
|
+
expr: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
count: number;
|
|
7
|
+
firstSeenAt: number;
|
|
8
|
+
lastSeenAt: number;
|
|
9
|
+
}
|
|
10
|
+
/** Yield control to keep the main thread responsive during large batches. */
|
|
11
|
+
declare function yieldToMain(): Promise<void>;
|
|
12
|
+
interface ContainmentHintsOptions {
|
|
13
|
+
selector?: string;
|
|
14
|
+
contain?: string;
|
|
15
|
+
contentVisibility?: string;
|
|
16
|
+
containIntrinsicSize?: string;
|
|
17
|
+
skipIfAlreadySet?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Apply CSS containment hints to opt-in containers for lower layout/paint cost. */
|
|
20
|
+
declare function applyContainmentHints(root?: ParentNode, options?: ContainmentHintsOptions): number;
|
|
21
|
+
/**
|
|
22
|
+
* Load a pre-compiled directive map from the server.
|
|
23
|
+
* Keys are data-forma-id values, values are arrays of directive attribute names.
|
|
24
|
+
*/
|
|
25
|
+
declare function setDirectiveMap(map: Record<string, string[]> | null): void;
|
|
26
|
+
declare function initRuntime(): void;
|
|
27
|
+
/** Dispose all FormaJS scopes — clears effects, intervals, event listeners, and stops the observer. */
|
|
28
|
+
declare function destroyRuntime(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Mount a specific element or subtree — scans for `data-forma-state`
|
|
31
|
+
* elements and initializes their reactive bindings.
|
|
32
|
+
*
|
|
33
|
+
* Use this for manual control when injecting HTML dynamically.
|
|
34
|
+
* With the MutationObserver active, this is usually not needed.
|
|
35
|
+
*
|
|
36
|
+
* @param el - The root element to scan (checks itself and all descendants).
|
|
37
|
+
*/
|
|
38
|
+
declare function mount(el: Element): void;
|
|
39
|
+
/**
|
|
40
|
+
* Unmount a specific element or subtree — disposes all reactive bindings
|
|
41
|
+
* for `data-forma-state` elements within.
|
|
42
|
+
*
|
|
43
|
+
* @param el - The root element to clean up (checks itself and all descendants).
|
|
44
|
+
*/
|
|
45
|
+
declare function unmount(el: Element): void;
|
|
46
|
+
/** Enable/disable debug logging. Also toggleable via window.__FORMA_DEBUG = true */
|
|
47
|
+
declare function setDebug(on: boolean): void;
|
|
48
|
+
/** Set unsafe-eval mode. `locked-off` is hardened and non-toggleable via setUnsafeEval. */
|
|
49
|
+
declare function setUnsafeEvalMode(mode: UnsafeEvalMode): void;
|
|
50
|
+
/** Enable/disable unsafe `new Function` fallback for complex expressions. */
|
|
51
|
+
declare function setUnsafeEval(on: boolean): void;
|
|
52
|
+
/** Get current unsafe-eval policy mode. */
|
|
53
|
+
declare function getUnsafeEvalMode(): UnsafeEvalMode;
|
|
54
|
+
/** Enable/disable runtime diagnostics for unsupported expressions/handlers. */
|
|
55
|
+
declare function setDiagnostics(on: boolean): void;
|
|
56
|
+
/** Runtime diagnostics captured while parsing/binding templates. */
|
|
57
|
+
declare function getDiagnostics(): RuntimeDiagnostic[];
|
|
58
|
+
/** Clear runtime diagnostics collected so far. */
|
|
59
|
+
declare function clearDiagnostics(): void;
|
|
60
|
+
interface ScopeDescriptor {
|
|
61
|
+
element: Element;
|
|
62
|
+
id: string;
|
|
63
|
+
values: Record<string, {
|
|
64
|
+
value: unknown;
|
|
65
|
+
type: string;
|
|
66
|
+
}>;
|
|
67
|
+
initialJSON: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* DevTools: enumerate all active scopes and their current signal values.
|
|
71
|
+
* Only called when the State Inspector panel is open — zero overhead otherwise.
|
|
72
|
+
*/
|
|
73
|
+
declare function getScopes(): ScopeDescriptor[];
|
|
74
|
+
/**
|
|
75
|
+
* DevTools: set a state value on a specific scope element.
|
|
76
|
+
* Triggers normal reactive effects (data-show, data-text, etc.).
|
|
77
|
+
*/
|
|
78
|
+
declare function setScopeValue(element: Element, key: string, value: unknown): void;
|
|
79
|
+
/**
|
|
80
|
+
* DevTools: reset all values on a scope to their initial JSON state.
|
|
81
|
+
*/
|
|
82
|
+
declare function resetScope(element: Element): void;
|
|
83
|
+
/** Reconcile a container's DOM against a new HTML string. */
|
|
84
|
+
declare function reconcile(container: Element, html: string): void;
|
|
85
|
+
|
|
86
|
+
export { applyContainmentHints, clearDiagnostics, destroyRuntime, getDiagnostics, getScopes, getUnsafeEvalMode, initRuntime, mount, reconcile, resetScope, setDebug, setDiagnostics, setDirectiveMap, setScopeValue, setUnsafeEval, setUnsafeEvalMode, unmount, yieldToMain };
|
package/dist/runtime-hardened.js
CHANGED
|
@@ -753,6 +753,7 @@ if (buildUnsafeEvalMode) {
|
|
|
753
753
|
_unsafeEvalMode = buildUnsafeEvalMode;
|
|
754
754
|
if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
|
|
755
755
|
if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
|
|
756
|
+
if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
|
|
756
757
|
}
|
|
757
758
|
var runtimeConfig = readRuntimeConfig();
|
|
758
759
|
var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
|
|
@@ -830,7 +831,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
|
|
|
830
831
|
var RE_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
831
832
|
var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
|
|
832
833
|
var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
|
|
833
|
-
var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
|
|
834
834
|
var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
|
|
835
835
|
var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
|
|
836
836
|
var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
|
|
@@ -841,7 +841,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
|
|
|
841
841
|
var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
|
|
842
842
|
var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
|
|
843
843
|
var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
|
|
844
|
-
var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
|
|
845
844
|
var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
|
|
846
845
|
var RE_STRIP_BRACES = /^\{|\}$/g;
|
|
847
846
|
var RE_DIGIT_ONLY = /^\d+$/;
|
|
@@ -856,6 +855,7 @@ var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
|
|
|
856
855
|
var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
|
|
857
856
|
var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
|
|
858
857
|
var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
|
|
858
|
+
var RE_EVENT_REF = /\bevent\b|\$event\b/;
|
|
859
859
|
var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
|
|
860
860
|
var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
|
|
861
861
|
var EXPRESSION_CACHE_MAX = 2048;
|
|
@@ -870,7 +870,40 @@ function cacheExpression(key, factory) {
|
|
|
870
870
|
var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
|
|
871
871
|
var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
|
|
872
872
|
var compiledTemplateCache = /* @__PURE__ */ new Map();
|
|
873
|
-
var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
873
|
+
var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
874
|
+
"constructor",
|
|
875
|
+
"__proto__",
|
|
876
|
+
"prototype",
|
|
877
|
+
"__defineGetter__",
|
|
878
|
+
"__defineSetter__",
|
|
879
|
+
"__lookupGetter__",
|
|
880
|
+
"__lookupSetter__",
|
|
881
|
+
"eval"
|
|
882
|
+
]);
|
|
883
|
+
var BLOCKED_METHOD_REGEXES = (() => {
|
|
884
|
+
const result = [];
|
|
885
|
+
for (const name of UNSAFE_METHOD_NAMES) {
|
|
886
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
887
|
+
result.push({
|
|
888
|
+
name,
|
|
889
|
+
// Match as property access (.name) or bare identifier at start
|
|
890
|
+
dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
|
|
891
|
+
// Match bracket access with single quotes, double quotes, or backticks
|
|
892
|
+
bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
return result;
|
|
896
|
+
})();
|
|
897
|
+
function findBlockedMethod(expr) {
|
|
898
|
+
let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
899
|
+
cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
|
|
900
|
+
cleaned = cleaned.replace(/\s*\.\s*/g, ".");
|
|
901
|
+
for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
|
|
902
|
+
if (dotRe.test(cleaned)) return name;
|
|
903
|
+
if (bracketRe.test(cleaned)) return name;
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
874
907
|
var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
|
|
875
908
|
function toTextValue(value2) {
|
|
876
909
|
if (value2 == null) return "";
|
|
@@ -1145,6 +1178,7 @@ function consumeStatement(raw) {
|
|
|
1145
1178
|
function parseIfHandler(expr, scope) {
|
|
1146
1179
|
const input = expr.trim();
|
|
1147
1180
|
if (!RE_IF_PREFIX.test(input)) return null;
|
|
1181
|
+
if (RE_EVENT_REF.test(input)) return null;
|
|
1148
1182
|
let idx = 2;
|
|
1149
1183
|
while (idx < input.length && /\s/.test(input[idx])) idx++;
|
|
1150
1184
|
if (input[idx] !== "(") return null;
|
|
@@ -1546,6 +1580,68 @@ function cloneAttributeTemplates(el, item) {
|
|
|
1546
1580
|
}
|
|
1547
1581
|
}
|
|
1548
1582
|
}
|
|
1583
|
+
function parseChainedAccess(expr, scope) {
|
|
1584
|
+
let pos = 0;
|
|
1585
|
+
const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
|
|
1586
|
+
if (!identMatch) return null;
|
|
1587
|
+
const rootName = identMatch[0];
|
|
1588
|
+
pos = rootName.length;
|
|
1589
|
+
if (pos >= expr.length) return null;
|
|
1590
|
+
if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
|
|
1591
|
+
const steps = [];
|
|
1592
|
+
while (pos < expr.length) {
|
|
1593
|
+
let optional = false;
|
|
1594
|
+
if (expr[pos] === "?" && expr[pos + 1] === ".") {
|
|
1595
|
+
optional = true;
|
|
1596
|
+
pos += 2;
|
|
1597
|
+
} else if (expr[pos] === ".") {
|
|
1598
|
+
pos += 1;
|
|
1599
|
+
} else {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
const nameMatch = expr.slice(pos).match(/^\w+/);
|
|
1603
|
+
if (!nameMatch) return null;
|
|
1604
|
+
const name = nameMatch[0];
|
|
1605
|
+
pos += name.length;
|
|
1606
|
+
if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
|
|
1607
|
+
if (pos < expr.length && expr[pos] === "(") {
|
|
1608
|
+
const balanced = readBalancedSegment(expr, pos, "(", ")");
|
|
1609
|
+
if (!balanced) return null;
|
|
1610
|
+
const argsRaw = balanced.inner.trim();
|
|
1611
|
+
const argFns = [];
|
|
1612
|
+
for (const arg of splitCallArgs(argsRaw)) {
|
|
1613
|
+
const parsed = parseExpression(arg, scope);
|
|
1614
|
+
if (!parsed) return null;
|
|
1615
|
+
argFns.push(parsed);
|
|
1616
|
+
}
|
|
1617
|
+
steps.push({ type: "call", name, optional, argFns });
|
|
1618
|
+
pos = balanced.end + 1;
|
|
1619
|
+
} else {
|
|
1620
|
+
steps.push({ type: "prop", name, optional });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (pos !== expr.length) return null;
|
|
1624
|
+
if (steps.length === 0) return null;
|
|
1625
|
+
const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
|
|
1626
|
+
return () => {
|
|
1627
|
+
let val = rootExpr();
|
|
1628
|
+
for (const step of steps) {
|
|
1629
|
+
if (val == null) {
|
|
1630
|
+
if (step.optional) return void 0;
|
|
1631
|
+
return void 0;
|
|
1632
|
+
}
|
|
1633
|
+
if (step.type === "prop") {
|
|
1634
|
+
val = val[step.name];
|
|
1635
|
+
} else {
|
|
1636
|
+
const method = val[step.name];
|
|
1637
|
+
if (typeof method !== "function") return void 0;
|
|
1638
|
+
const args = step.argFns.map((fn) => fn());
|
|
1639
|
+
val = method.apply(val, args);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return val;
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1549
1645
|
function parseExpression(expr, scope) {
|
|
1550
1646
|
const cachedFactory = expressionCache.get(expr);
|
|
1551
1647
|
if (cachedFactory) return cachedFactory(scope);
|
|
@@ -1596,24 +1692,9 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1596
1692
|
if (RE_IDENTIFIER.test(expr)) {
|
|
1597
1693
|
return () => scope.getters[expr]?.();
|
|
1598
1694
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
return () => {
|
|
1603
|
-
const obj = scope.getters[p1]?.();
|
|
1604
|
-
return obj?.[p2];
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
const deepDotMatch = expr.match(RE_DEEP_DOT);
|
|
1608
|
-
if (deepDotMatch) {
|
|
1609
|
-
const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
|
|
1610
|
-
return () => {
|
|
1611
|
-
let val = scope.getters[p1]?.();
|
|
1612
|
-
val = val?.[p2];
|
|
1613
|
-
val = val?.[p3];
|
|
1614
|
-
if (p4) val = val?.[p4];
|
|
1615
|
-
return val;
|
|
1616
|
-
};
|
|
1695
|
+
{
|
|
1696
|
+
const chainResult = parseChainedAccess(expr, scope);
|
|
1697
|
+
if (chainResult) return chainResult;
|
|
1617
1698
|
}
|
|
1618
1699
|
const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
|
|
1619
1700
|
if (groupedCallMatch) {
|
|
@@ -1637,28 +1718,6 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1637
1718
|
return method.apply(base, args);
|
|
1638
1719
|
};
|
|
1639
1720
|
}
|
|
1640
|
-
const callMatch = expr.match(RE_METHOD_CALL);
|
|
1641
|
-
if (callMatch) {
|
|
1642
|
-
const baseName = callMatch[1];
|
|
1643
|
-
const methodName = callMatch[2];
|
|
1644
|
-
const argsRaw = callMatch[3].trim();
|
|
1645
|
-
if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
|
|
1646
|
-
const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
|
|
1647
|
-
if (!baseExpr) return null;
|
|
1648
|
-
const argFns = [];
|
|
1649
|
-
for (const arg of splitCallArgs(argsRaw)) {
|
|
1650
|
-
const parsed = parseExpression(arg, scope);
|
|
1651
|
-
if (!parsed) return null;
|
|
1652
|
-
argFns.push(parsed);
|
|
1653
|
-
}
|
|
1654
|
-
return () => {
|
|
1655
|
-
const base = baseExpr();
|
|
1656
|
-
const method = base?.[methodName];
|
|
1657
|
-
if (typeof method !== "function") return void 0;
|
|
1658
|
-
const args = argFns.map((fn) => fn());
|
|
1659
|
-
return method.apply(base, args);
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
1721
|
if (expr.startsWith("!")) {
|
|
1663
1722
|
const inner = parseExpression(expr.slice(1).trim(), scope);
|
|
1664
1723
|
if (inner) return () => !inner();
|
|
@@ -1677,6 +1736,29 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1677
1736
|
return () => objExpr()?.[key];
|
|
1678
1737
|
}
|
|
1679
1738
|
}
|
|
1739
|
+
if (expr.startsWith("[")) {
|
|
1740
|
+
const balanced = readBalancedSegment(expr, 0, "[", "]");
|
|
1741
|
+
if (balanced && balanced.end === expr.length - 1) {
|
|
1742
|
+
const inner = balanced.inner.trim();
|
|
1743
|
+
if (inner === "") {
|
|
1744
|
+
return () => [];
|
|
1745
|
+
}
|
|
1746
|
+
const elements = splitCallArgs(inner);
|
|
1747
|
+
const elementFns = [];
|
|
1748
|
+
let allParsed = true;
|
|
1749
|
+
for (const el of elements) {
|
|
1750
|
+
const parsed = parseExpression(el.trim(), scope);
|
|
1751
|
+
if (!parsed) {
|
|
1752
|
+
allParsed = false;
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1755
|
+
elementFns.push(parsed);
|
|
1756
|
+
}
|
|
1757
|
+
if (allParsed) {
|
|
1758
|
+
return () => elementFns.map((fn) => fn());
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1680
1762
|
const ternaryMatch = expr.match(RE_TERNARY);
|
|
1681
1763
|
if (ternaryMatch) {
|
|
1682
1764
|
const cond = parseExpression(ternaryMatch[1].trim(), scope);
|
|
@@ -1806,6 +1888,15 @@ function getScopeCache(cache, scope) {
|
|
|
1806
1888
|
}
|
|
1807
1889
|
return scoped;
|
|
1808
1890
|
}
|
|
1891
|
+
function cspExpressionHint(expr) {
|
|
1892
|
+
if (expr.includes("...")) {
|
|
1893
|
+
return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1894
|
+
}
|
|
1895
|
+
if (expr.includes("=>")) {
|
|
1896
|
+
return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1897
|
+
}
|
|
1898
|
+
return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1899
|
+
}
|
|
1809
1900
|
function buildEvaluator(expr, scope) {
|
|
1810
1901
|
const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
|
|
1811
1902
|
const cache = getScopeCache(scopeExpressionCache, scope);
|
|
@@ -1818,11 +1909,17 @@ function buildEvaluator(expr, scope) {
|
|
|
1818
1909
|
}
|
|
1819
1910
|
if (!_allowUnsafeEval) {
|
|
1820
1911
|
dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
|
|
1821
|
-
reportDiagnostic("expression-unsupported", cleaned,
|
|
1912
|
+
reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
|
|
1822
1913
|
const blocked = () => void 0;
|
|
1823
1914
|
cache.set(cleaned, blocked);
|
|
1824
1915
|
return blocked;
|
|
1825
1916
|
}
|
|
1917
|
+
const blockedMethod = findBlockedMethod(cleaned);
|
|
1918
|
+
if (blockedMethod) {
|
|
1919
|
+
const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
|
|
1920
|
+
reportDiagnostic("expression-unsupported", cleaned, msg);
|
|
1921
|
+
throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
|
|
1922
|
+
}
|
|
1826
1923
|
try {
|
|
1827
1924
|
const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
|
|
1828
1925
|
const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
@@ -1838,7 +1935,7 @@ function buildEvaluator(expr, scope) {
|
|
|
1838
1935
|
cache.set(cleaned, unsafe);
|
|
1839
1936
|
return unsafe;
|
|
1840
1937
|
} catch {
|
|
1841
|
-
reportDiagnostic("expression-unsupported", cleaned, "Expression
|
|
1938
|
+
reportDiagnostic("expression-unsupported", cleaned, "Expression too complex for CSP-safe mode. Enable unsafe-eval via FormaRuntime.unsafeEval = true, or use the standard (non-hardened) build.");
|
|
1842
1939
|
const failed = () => void 0;
|
|
1843
1940
|
cache.set(cleaned, failed);
|
|
1844
1941
|
return failed;
|
|
@@ -1944,7 +2041,13 @@ function parseHandler(expr, scope) {
|
|
|
1944
2041
|
return null;
|
|
1945
2042
|
}
|
|
1946
2043
|
function buildHandler(expr, scope) {
|
|
1947
|
-
|
|
2044
|
+
let cleaned = expr.trim();
|
|
2045
|
+
if (cleaned.startsWith("{")) {
|
|
2046
|
+
const seg = readBalancedSegment(cleaned, 0, "{", "}");
|
|
2047
|
+
if (seg && seg.end === cleaned.length - 1) {
|
|
2048
|
+
cleaned = seg.inner.trim();
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
1948
2051
|
const cache = getScopeCache(scopeHandlerCache, scope);
|
|
1949
2052
|
const cached = cache.get(cleaned);
|
|
1950
2053
|
if (cached) return cached;
|
|
@@ -1956,7 +2059,7 @@ function buildHandler(expr, scope) {
|
|
|
1956
2059
|
}
|
|
1957
2060
|
if (!_allowUnsafeEval) {
|
|
1958
2061
|
dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
|
|
1959
|
-
reportDiagnostic("handler-unsupported", cleaned,
|
|
2062
|
+
reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
|
|
1960
2063
|
const result = {
|
|
1961
2064
|
handler: () => {
|
|
1962
2065
|
},
|
|
@@ -1965,6 +2068,12 @@ function buildHandler(expr, scope) {
|
|
|
1965
2068
|
cache.set(cleaned, result);
|
|
1966
2069
|
return result;
|
|
1967
2070
|
}
|
|
2071
|
+
const blockedMethod = findBlockedMethod(cleaned);
|
|
2072
|
+
if (blockedMethod) {
|
|
2073
|
+
const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
|
|
2074
|
+
reportDiagnostic("handler-unsupported", cleaned, msg);
|
|
2075
|
+
throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
|
|
2076
|
+
}
|
|
1968
2077
|
try {
|
|
1969
2078
|
const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
|
|
1970
2079
|
const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
@@ -1992,7 +2101,7 @@ function buildHandler(expr, scope) {
|
|
|
1992
2101
|
cache.set(cleaned, result);
|
|
1993
2102
|
return result;
|
|
1994
2103
|
} catch {
|
|
1995
|
-
reportDiagnostic("handler-unsupported", cleaned, "
|
|
2104
|
+
reportDiagnostic("handler-unsupported", cleaned, "Expression too complex for CSP-safe mode. Enable unsafe-eval via FormaRuntime.unsafeEval = true, or use the standard (non-hardened) build.");
|
|
1996
2105
|
const result = {
|
|
1997
2106
|
handler: () => {
|
|
1998
2107
|
},
|
|
@@ -2002,16 +2111,22 @@ function buildHandler(expr, scope) {
|
|
|
2002
2111
|
return result;
|
|
2003
2112
|
}
|
|
2004
2113
|
}
|
|
2114
|
+
var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2005
2115
|
function parseState(raw) {
|
|
2116
|
+
let parsed;
|
|
2006
2117
|
try {
|
|
2007
|
-
|
|
2118
|
+
parsed = JSON.parse(raw);
|
|
2008
2119
|
} catch {
|
|
2009
2120
|
try {
|
|
2010
|
-
|
|
2121
|
+
parsed = JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
|
|
2011
2122
|
} catch {
|
|
2012
2123
|
return {};
|
|
2013
2124
|
}
|
|
2014
2125
|
}
|
|
2126
|
+
for (const key of FORBIDDEN_STATE_KEYS) {
|
|
2127
|
+
if (key in parsed) delete parsed[key];
|
|
2128
|
+
}
|
|
2129
|
+
return parsed;
|
|
2015
2130
|
}
|
|
2016
2131
|
function initScope(stateEl) {
|
|
2017
2132
|
const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
|
|
@@ -2681,6 +2796,7 @@ function setUnsafeEvalMode(mode) {
|
|
|
2681
2796
|
_unsafeEvalMode = mode;
|
|
2682
2797
|
if (mode === "locked-off") _allowUnsafeEval = false;
|
|
2683
2798
|
if (mode === "locked-on") _allowUnsafeEval = true;
|
|
2799
|
+
if (mode === "mutable") _allowUnsafeEval = true;
|
|
2684
2800
|
scopeExpressionCache = /* @__PURE__ */ new WeakMap();
|
|
2685
2801
|
scopeHandlerCache = /* @__PURE__ */ new WeakMap();
|
|
2686
2802
|
}
|