@getforma/core 0.3.0 → 0.6.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 +201 -13
- package/dist/{chunk-KX5WRZH7.js → chunk-DPSAVBCP.js} +3 -4
- package/dist/chunk-DPSAVBCP.js.map +1 -0
- package/dist/{chunk-FPSLC62A.cjs → chunk-KH3F5NRU.cjs} +2 -4
- package/dist/chunk-KH3F5NRU.cjs.map +1 -0
- package/dist/{chunk-VKKPX4YU.js → chunk-TKGUHASG.js} +79 -19
- package/dist/chunk-TKGUHASG.js.map +1 -0
- package/dist/{chunk-VP5EOGM5.cjs → chunk-YSKF3VRA.cjs} +87 -26
- package/dist/chunk-YSKF3VRA.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 +154 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -35
- package/dist/index.d.ts +83 -35
- package/dist/index.js +99 -40
- package/dist/index.js.map +1 -1
- package/dist/runtime-hardened.cjs +224 -60
- 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 +225 -61
- package/dist/runtime-hardened.js.map +1 -1
- package/dist/runtime.cjs +248 -83
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +226 -61
- package/dist/runtime.js.map +1 -1
- package/dist/ssr/index.cjs +69 -73
- 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 +69 -73
- package/dist/ssr/index.js.map +1 -1
- package/dist/tc39-compat.cjs +3 -3
- package/dist/tc39-compat.js +1 -1
- package/package.json +24 -3
- package/dist/chunk-FPSLC62A.cjs.map +0 -1
- package/dist/chunk-KX5WRZH7.js.map +0 -1
- package/dist/chunk-VKKPX4YU.js.map +0 -1
- package/dist/chunk-VP5EOGM5.cjs.map +0 -1
|
@@ -19,7 +19,6 @@ function createSignal(initialValue) {
|
|
|
19
19
|
const setter = (v) => applySignalSet(s, v);
|
|
20
20
|
return [getter, setter];
|
|
21
21
|
}
|
|
22
|
-
var createValueSignal = createSignal;
|
|
23
22
|
function internalEffect(fn) {
|
|
24
23
|
const dispose = alienSignals.effect(fn);
|
|
25
24
|
return dispose;
|
|
@@ -755,6 +754,7 @@ if (buildUnsafeEvalMode) {
|
|
|
755
754
|
_unsafeEvalMode = buildUnsafeEvalMode;
|
|
756
755
|
if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
|
|
757
756
|
if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
|
|
757
|
+
if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
|
|
758
758
|
}
|
|
759
759
|
var runtimeConfig = readRuntimeConfig();
|
|
760
760
|
var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
|
|
@@ -832,7 +832,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
|
|
|
832
832
|
var RE_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
833
833
|
var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
|
|
834
834
|
var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
|
|
835
|
-
var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
|
|
836
835
|
var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
|
|
837
836
|
var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
|
|
838
837
|
var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
|
|
@@ -843,7 +842,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
|
|
|
843
842
|
var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
|
|
844
843
|
var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
|
|
845
844
|
var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
|
|
846
|
-
var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
|
|
847
845
|
var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
|
|
848
846
|
var RE_STRIP_BRACES = /^\{|\}$/g;
|
|
849
847
|
var RE_DIGIT_ONLY = /^\d+$/;
|
|
@@ -853,11 +851,11 @@ var RE_TOGGLE = /^(\w+)\s*=\s*!(\w+)$/;
|
|
|
853
851
|
var RE_ASSIGN = /^(\w+)\s*=\s*(.+)$/;
|
|
854
852
|
var RE_COMPOUND = /^(\w+)\s*(\+=|-=|\*=|\/=)\s*(.+)$/;
|
|
855
853
|
var RE_IF_PREFIX = /^if\b/;
|
|
856
|
-
var RE_UNQUOTED_KEYS = /(\w+)\s*:/g;
|
|
857
854
|
var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
|
|
858
855
|
var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
|
|
859
856
|
var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
|
|
860
857
|
var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
|
|
858
|
+
var RE_EVENT_REF = /\bevent\s*[.([]|\$event\b/;
|
|
861
859
|
var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
|
|
862
860
|
var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
|
|
863
861
|
var EXPRESSION_CACHE_MAX = 2048;
|
|
@@ -872,7 +870,77 @@ function cacheExpression(key, factory) {
|
|
|
872
870
|
var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
|
|
873
871
|
var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
|
|
874
872
|
var compiledTemplateCache = /* @__PURE__ */ new Map();
|
|
875
|
-
var
|
|
873
|
+
var COMPILED_TEMPLATE_CACHE_MAX = 2048;
|
|
874
|
+
function cacheCompiledTemplate(key, template) {
|
|
875
|
+
if (compiledTemplateCache.size >= COMPILED_TEMPLATE_CACHE_MAX) {
|
|
876
|
+
const first = compiledTemplateCache.keys().next().value;
|
|
877
|
+
if (first !== void 0) compiledTemplateCache.delete(first);
|
|
878
|
+
}
|
|
879
|
+
compiledTemplateCache.set(key, template);
|
|
880
|
+
}
|
|
881
|
+
var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
882
|
+
"constructor",
|
|
883
|
+
"__proto__",
|
|
884
|
+
"prototype",
|
|
885
|
+
"__defineGetter__",
|
|
886
|
+
"__defineSetter__",
|
|
887
|
+
"__lookupGetter__",
|
|
888
|
+
"__lookupSetter__",
|
|
889
|
+
"eval",
|
|
890
|
+
"Function"
|
|
891
|
+
]);
|
|
892
|
+
var BLOCKED_METHOD_REGEXES = (() => {
|
|
893
|
+
const result = [];
|
|
894
|
+
for (const name of UNSAFE_METHOD_NAMES) {
|
|
895
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
896
|
+
result.push({
|
|
897
|
+
name,
|
|
898
|
+
// Match as property access (.name) or bare identifier at start
|
|
899
|
+
dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
|
|
900
|
+
// Match bracket access with single quotes, double quotes, or backticks
|
|
901
|
+
bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
return result;
|
|
905
|
+
})();
|
|
906
|
+
function findBlockedMethod(expr) {
|
|
907
|
+
let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
908
|
+
cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
|
|
909
|
+
cleaned = cleaned.replace(/\s*\.\s*/g, ".");
|
|
910
|
+
for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
|
|
911
|
+
if (dotRe.test(cleaned)) return name;
|
|
912
|
+
if (bracketRe.test(cleaned)) return name;
|
|
913
|
+
}
|
|
914
|
+
if (cleaned.includes("[")) {
|
|
915
|
+
const bracketContents = extractBracketContents(cleaned);
|
|
916
|
+
for (const content of bracketContents) {
|
|
917
|
+
if (!content.includes("+")) continue;
|
|
918
|
+
const fragments = content.match(/['"`]([^'"`]*?)['"`]/g);
|
|
919
|
+
if (!fragments) continue;
|
|
920
|
+
const joined = fragments.map((f) => f.slice(1, -1)).join("");
|
|
921
|
+
if (UNSAFE_METHOD_NAMES.has(joined)) return joined;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
function extractBracketContents(expr) {
|
|
927
|
+
const results = [];
|
|
928
|
+
let depth = 0;
|
|
929
|
+
let start = -1;
|
|
930
|
+
for (let i = 0; i < expr.length; i++) {
|
|
931
|
+
if (expr[i] === "[") {
|
|
932
|
+
if (depth === 0) start = i + 1;
|
|
933
|
+
depth++;
|
|
934
|
+
} else if (expr[i] === "]") {
|
|
935
|
+
depth--;
|
|
936
|
+
if (depth === 0 && start >= 0) {
|
|
937
|
+
results.push(expr.slice(start, i));
|
|
938
|
+
start = -1;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return results;
|
|
943
|
+
}
|
|
876
944
|
var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
|
|
877
945
|
function toTextValue(value2) {
|
|
878
946
|
if (value2 == null) return "";
|
|
@@ -1147,6 +1215,7 @@ function consumeStatement(raw) {
|
|
|
1147
1215
|
function parseIfHandler(expr, scope) {
|
|
1148
1216
|
const input = expr.trim();
|
|
1149
1217
|
if (!RE_IF_PREFIX.test(input)) return null;
|
|
1218
|
+
if (RE_EVENT_REF.test(input)) return null;
|
|
1150
1219
|
let idx = 2;
|
|
1151
1220
|
while (idx < input.length && /\s/.test(input[idx])) idx++;
|
|
1152
1221
|
if (input[idx] !== "(") return null;
|
|
@@ -1207,7 +1276,7 @@ function compileTemplate(text) {
|
|
|
1207
1276
|
dynamics,
|
|
1208
1277
|
hasItemRef: dynamics.length > 0
|
|
1209
1278
|
};
|
|
1210
|
-
|
|
1279
|
+
cacheCompiledTemplate(text, result);
|
|
1211
1280
|
return result;
|
|
1212
1281
|
}
|
|
1213
1282
|
var templateTexts = /* @__PURE__ */ new WeakMap();
|
|
@@ -1548,6 +1617,68 @@ function cloneAttributeTemplates(el, item) {
|
|
|
1548
1617
|
}
|
|
1549
1618
|
}
|
|
1550
1619
|
}
|
|
1620
|
+
function parseChainedAccess(expr, scope) {
|
|
1621
|
+
let pos = 0;
|
|
1622
|
+
const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
|
|
1623
|
+
if (!identMatch) return null;
|
|
1624
|
+
const rootName = identMatch[0];
|
|
1625
|
+
pos = rootName.length;
|
|
1626
|
+
if (pos >= expr.length) return null;
|
|
1627
|
+
if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
|
|
1628
|
+
const steps = [];
|
|
1629
|
+
while (pos < expr.length) {
|
|
1630
|
+
let optional = false;
|
|
1631
|
+
if (expr[pos] === "?" && expr[pos + 1] === ".") {
|
|
1632
|
+
optional = true;
|
|
1633
|
+
pos += 2;
|
|
1634
|
+
} else if (expr[pos] === ".") {
|
|
1635
|
+
pos += 1;
|
|
1636
|
+
} else {
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
const nameMatch = expr.slice(pos).match(/^\w+/);
|
|
1640
|
+
if (!nameMatch) return null;
|
|
1641
|
+
const name = nameMatch[0];
|
|
1642
|
+
pos += name.length;
|
|
1643
|
+
if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
|
|
1644
|
+
if (pos < expr.length && expr[pos] === "(") {
|
|
1645
|
+
const balanced = readBalancedSegment(expr, pos, "(", ")");
|
|
1646
|
+
if (!balanced) return null;
|
|
1647
|
+
const argsRaw = balanced.inner.trim();
|
|
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
|
+
steps.push({ type: "call", name, optional, argFns });
|
|
1655
|
+
pos = balanced.end + 1;
|
|
1656
|
+
} else {
|
|
1657
|
+
steps.push({ type: "prop", name, optional });
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
if (pos !== expr.length) return null;
|
|
1661
|
+
if (steps.length === 0) return null;
|
|
1662
|
+
const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
|
|
1663
|
+
return () => {
|
|
1664
|
+
let val = rootExpr();
|
|
1665
|
+
for (const step of steps) {
|
|
1666
|
+
if (val == null) {
|
|
1667
|
+
if (step.optional) return void 0;
|
|
1668
|
+
return void 0;
|
|
1669
|
+
}
|
|
1670
|
+
if (step.type === "prop") {
|
|
1671
|
+
val = val[step.name];
|
|
1672
|
+
} else {
|
|
1673
|
+
const method = val[step.name];
|
|
1674
|
+
if (typeof method !== "function") return void 0;
|
|
1675
|
+
const args = step.argFns.map((fn) => fn());
|
|
1676
|
+
val = method.apply(val, args);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return val;
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1551
1682
|
function parseExpression(expr, scope) {
|
|
1552
1683
|
const cachedFactory = expressionCache.get(expr);
|
|
1553
1684
|
if (cachedFactory) return cachedFactory(scope);
|
|
@@ -1598,24 +1729,9 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1598
1729
|
if (RE_IDENTIFIER.test(expr)) {
|
|
1599
1730
|
return () => scope.getters[expr]?.();
|
|
1600
1731
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
return () => {
|
|
1605
|
-
const obj = scope.getters[p1]?.();
|
|
1606
|
-
return obj?.[p2];
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
const deepDotMatch = expr.match(RE_DEEP_DOT);
|
|
1610
|
-
if (deepDotMatch) {
|
|
1611
|
-
const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
|
|
1612
|
-
return () => {
|
|
1613
|
-
let val = scope.getters[p1]?.();
|
|
1614
|
-
val = val?.[p2];
|
|
1615
|
-
val = val?.[p3];
|
|
1616
|
-
if (p4) val = val?.[p4];
|
|
1617
|
-
return val;
|
|
1618
|
-
};
|
|
1732
|
+
{
|
|
1733
|
+
const chainResult = parseChainedAccess(expr, scope);
|
|
1734
|
+
if (chainResult) return chainResult;
|
|
1619
1735
|
}
|
|
1620
1736
|
const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
|
|
1621
1737
|
if (groupedCallMatch) {
|
|
@@ -1639,28 +1755,6 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1639
1755
|
return method.apply(base, args);
|
|
1640
1756
|
};
|
|
1641
1757
|
}
|
|
1642
|
-
const callMatch = expr.match(RE_METHOD_CALL);
|
|
1643
|
-
if (callMatch) {
|
|
1644
|
-
const baseName = callMatch[1];
|
|
1645
|
-
const methodName = callMatch[2];
|
|
1646
|
-
const argsRaw = callMatch[3].trim();
|
|
1647
|
-
if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
|
|
1648
|
-
const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
|
|
1649
|
-
if (!baseExpr) return null;
|
|
1650
|
-
const argFns = [];
|
|
1651
|
-
for (const arg of splitCallArgs(argsRaw)) {
|
|
1652
|
-
const parsed = parseExpression(arg, scope);
|
|
1653
|
-
if (!parsed) return null;
|
|
1654
|
-
argFns.push(parsed);
|
|
1655
|
-
}
|
|
1656
|
-
return () => {
|
|
1657
|
-
const base = baseExpr();
|
|
1658
|
-
const method = base?.[methodName];
|
|
1659
|
-
if (typeof method !== "function") return void 0;
|
|
1660
|
-
const args = argFns.map((fn) => fn());
|
|
1661
|
-
return method.apply(base, args);
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
1758
|
if (expr.startsWith("!")) {
|
|
1665
1759
|
const inner = parseExpression(expr.slice(1).trim(), scope);
|
|
1666
1760
|
if (inner) return () => !inner();
|
|
@@ -1679,6 +1773,29 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1679
1773
|
return () => objExpr()?.[key];
|
|
1680
1774
|
}
|
|
1681
1775
|
}
|
|
1776
|
+
if (expr.startsWith("[")) {
|
|
1777
|
+
const balanced = readBalancedSegment(expr, 0, "[", "]");
|
|
1778
|
+
if (balanced && balanced.end === expr.length - 1) {
|
|
1779
|
+
const inner = balanced.inner.trim();
|
|
1780
|
+
if (inner === "") {
|
|
1781
|
+
return () => [];
|
|
1782
|
+
}
|
|
1783
|
+
const elements = splitCallArgs(inner);
|
|
1784
|
+
const elementFns = [];
|
|
1785
|
+
let allParsed = true;
|
|
1786
|
+
for (const el of elements) {
|
|
1787
|
+
const parsed = parseExpression(el.trim(), scope);
|
|
1788
|
+
if (!parsed) {
|
|
1789
|
+
allParsed = false;
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1792
|
+
elementFns.push(parsed);
|
|
1793
|
+
}
|
|
1794
|
+
if (allParsed) {
|
|
1795
|
+
return () => elementFns.map((fn) => fn());
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1682
1799
|
const ternaryMatch = expr.match(RE_TERNARY);
|
|
1683
1800
|
if (ternaryMatch) {
|
|
1684
1801
|
const cond = parseExpression(ternaryMatch[1].trim(), scope);
|
|
@@ -1808,6 +1925,15 @@ function getScopeCache(cache, scope) {
|
|
|
1808
1925
|
}
|
|
1809
1926
|
return scoped;
|
|
1810
1927
|
}
|
|
1928
|
+
function cspExpressionHint(expr) {
|
|
1929
|
+
if (expr.includes("...")) {
|
|
1930
|
+
return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1931
|
+
}
|
|
1932
|
+
if (expr.includes("=>")) {
|
|
1933
|
+
return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1934
|
+
}
|
|
1935
|
+
return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1936
|
+
}
|
|
1811
1937
|
function buildEvaluator(expr, scope) {
|
|
1812
1938
|
const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
|
|
1813
1939
|
const cache = getScopeCache(scopeExpressionCache, scope);
|
|
@@ -1820,11 +1946,17 @@ function buildEvaluator(expr, scope) {
|
|
|
1820
1946
|
}
|
|
1821
1947
|
if (!_allowUnsafeEval) {
|
|
1822
1948
|
dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
|
|
1823
|
-
reportDiagnostic("expression-unsupported", cleaned,
|
|
1949
|
+
reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
|
|
1824
1950
|
const blocked = () => void 0;
|
|
1825
1951
|
cache.set(cleaned, blocked);
|
|
1826
1952
|
return blocked;
|
|
1827
1953
|
}
|
|
1954
|
+
const blockedMethod = findBlockedMethod(cleaned);
|
|
1955
|
+
if (blockedMethod) {
|
|
1956
|
+
const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
|
|
1957
|
+
reportDiagnostic("expression-unsupported", cleaned, msg);
|
|
1958
|
+
throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
|
|
1959
|
+
}
|
|
1828
1960
|
try {
|
|
1829
1961
|
const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
|
|
1830
1962
|
const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
@@ -1832,6 +1964,7 @@ function buildEvaluator(expr, scope) {
|
|
|
1832
1964
|
return key in scope.getters;
|
|
1833
1965
|
},
|
|
1834
1966
|
get(_, key) {
|
|
1967
|
+
if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
|
|
1835
1968
|
const g = scope.getters[key];
|
|
1836
1969
|
return g ? g() : void 0;
|
|
1837
1970
|
}
|
|
@@ -1840,7 +1973,7 @@ function buildEvaluator(expr, scope) {
|
|
|
1840
1973
|
cache.set(cleaned, unsafe);
|
|
1841
1974
|
return unsafe;
|
|
1842
1975
|
} catch {
|
|
1843
|
-
reportDiagnostic("expression-unsupported", cleaned, "Expression
|
|
1976
|
+
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.");
|
|
1844
1977
|
const failed = () => void 0;
|
|
1845
1978
|
cache.set(cleaned, failed);
|
|
1846
1979
|
return failed;
|
|
@@ -1946,7 +2079,13 @@ function parseHandler(expr, scope) {
|
|
|
1946
2079
|
return null;
|
|
1947
2080
|
}
|
|
1948
2081
|
function buildHandler(expr, scope) {
|
|
1949
|
-
|
|
2082
|
+
let cleaned = expr.trim();
|
|
2083
|
+
if (cleaned.startsWith("{")) {
|
|
2084
|
+
const seg = readBalancedSegment(cleaned, 0, "{", "}");
|
|
2085
|
+
if (seg && seg.end === cleaned.length - 1) {
|
|
2086
|
+
cleaned = seg.inner.trim();
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
1950
2089
|
const cache = getScopeCache(scopeHandlerCache, scope);
|
|
1951
2090
|
const cached = cache.get(cleaned);
|
|
1952
2091
|
if (cached) return cached;
|
|
@@ -1958,7 +2097,7 @@ function buildHandler(expr, scope) {
|
|
|
1958
2097
|
}
|
|
1959
2098
|
if (!_allowUnsafeEval) {
|
|
1960
2099
|
dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
|
|
1961
|
-
reportDiagnostic("handler-unsupported", cleaned,
|
|
2100
|
+
reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
|
|
1962
2101
|
const result = {
|
|
1963
2102
|
handler: () => {
|
|
1964
2103
|
},
|
|
@@ -1967,6 +2106,12 @@ function buildHandler(expr, scope) {
|
|
|
1967
2106
|
cache.set(cleaned, result);
|
|
1968
2107
|
return result;
|
|
1969
2108
|
}
|
|
2109
|
+
const blockedMethod = findBlockedMethod(cleaned);
|
|
2110
|
+
if (blockedMethod) {
|
|
2111
|
+
const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
|
|
2112
|
+
reportDiagnostic("handler-unsupported", cleaned, msg);
|
|
2113
|
+
throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
|
|
2114
|
+
}
|
|
1970
2115
|
try {
|
|
1971
2116
|
const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
|
|
1972
2117
|
const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
@@ -1975,6 +2120,7 @@ function buildHandler(expr, scope) {
|
|
|
1975
2120
|
return key in scope.getters || key in scope.setters;
|
|
1976
2121
|
},
|
|
1977
2122
|
get(_, key) {
|
|
2123
|
+
if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
|
|
1978
2124
|
const g = scope.getters[key];
|
|
1979
2125
|
return g ? g() : void 0;
|
|
1980
2126
|
},
|
|
@@ -1994,7 +2140,7 @@ function buildHandler(expr, scope) {
|
|
|
1994
2140
|
cache.set(cleaned, result);
|
|
1995
2141
|
return result;
|
|
1996
2142
|
} catch {
|
|
1997
|
-
reportDiagnostic("handler-unsupported", cleaned, "
|
|
2143
|
+
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.");
|
|
1998
2144
|
const result = {
|
|
1999
2145
|
handler: () => {
|
|
2000
2146
|
},
|
|
@@ -2004,16 +2150,21 @@ function buildHandler(expr, scope) {
|
|
|
2004
2150
|
return result;
|
|
2005
2151
|
}
|
|
2006
2152
|
}
|
|
2153
|
+
var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2007
2154
|
function parseState(raw) {
|
|
2155
|
+
let parsed;
|
|
2008
2156
|
try {
|
|
2009
|
-
|
|
2157
|
+
parsed = JSON.parse(raw);
|
|
2010
2158
|
} catch {
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
} catch {
|
|
2014
|
-
return {};
|
|
2159
|
+
if (_debug) {
|
|
2160
|
+
dbg("parseState: Invalid JSON in data-forma-state \u2014 use valid JSON with quoted keys. Got:", raw.slice(0, 200));
|
|
2015
2161
|
}
|
|
2162
|
+
return {};
|
|
2163
|
+
}
|
|
2164
|
+
for (const key of FORBIDDEN_STATE_KEYS) {
|
|
2165
|
+
if (key in parsed) delete parsed[key];
|
|
2016
2166
|
}
|
|
2167
|
+
return parsed;
|
|
2017
2168
|
}
|
|
2018
2169
|
function initScope(stateEl) {
|
|
2019
2170
|
const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
|
|
@@ -2028,7 +2179,7 @@ function initScope(stateEl) {
|
|
|
2028
2179
|
const getters = {};
|
|
2029
2180
|
const setters = {};
|
|
2030
2181
|
for (const [key, initial] of Object.entries(state)) {
|
|
2031
|
-
const [get, set] =
|
|
2182
|
+
const [get, set] = createSignal(initial);
|
|
2032
2183
|
getters[key] = get;
|
|
2033
2184
|
setters[key] = set;
|
|
2034
2185
|
}
|
|
@@ -2036,6 +2187,18 @@ function initScope(stateEl) {
|
|
|
2036
2187
|
return { getters, setters };
|
|
2037
2188
|
}
|
|
2038
2189
|
function bindElement(el, scope, disposers) {
|
|
2190
|
+
const elMagics = {
|
|
2191
|
+
$el: el,
|
|
2192
|
+
$dispatch: (name, detail) => {
|
|
2193
|
+
el.dispatchEvent(new CustomEvent(name, {
|
|
2194
|
+
bubbles: true,
|
|
2195
|
+
composed: true,
|
|
2196
|
+
// crosses Shadow DOM boundaries (important for <forma-stage>)
|
|
2197
|
+
detail
|
|
2198
|
+
}));
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
scope = createChildScope(scope, elMagics);
|
|
2039
2202
|
const known = getDirectives(el);
|
|
2040
2203
|
const computedAttr = !known || known.has("data-computed") ? el.getAttribute("data-computed") : null;
|
|
2041
2204
|
if (computedAttr) {
|
|
@@ -2415,16 +2578,16 @@ function bindElement(el, scope, disposers) {
|
|
|
2415
2578
|
else if (k === "error") errorTarget = v;
|
|
2416
2579
|
else if (k === "poll") interval = parseInt(v ?? "0", 10);
|
|
2417
2580
|
}
|
|
2418
|
-
const [getTarget, setTarget] =
|
|
2581
|
+
const [getTarget, setTarget] = createSignal(null);
|
|
2419
2582
|
scope.getters[target] = getTarget;
|
|
2420
2583
|
scope.setters[target] = setTarget;
|
|
2421
2584
|
if (loadingTarget) {
|
|
2422
|
-
const [gl, sl] =
|
|
2585
|
+
const [gl, sl] = createSignal(false);
|
|
2423
2586
|
scope.getters[loadingTarget] = gl;
|
|
2424
2587
|
scope.setters[loadingTarget] = sl;
|
|
2425
2588
|
}
|
|
2426
2589
|
if (errorTarget) {
|
|
2427
|
-
const [ge, se] =
|
|
2590
|
+
const [ge, se] = createSignal(null);
|
|
2428
2591
|
scope.getters[errorTarget] = ge;
|
|
2429
2592
|
scope.setters[errorTarget] = se;
|
|
2430
2593
|
}
|
|
@@ -2683,6 +2846,7 @@ function setUnsafeEvalMode(mode) {
|
|
|
2683
2846
|
_unsafeEvalMode = mode;
|
|
2684
2847
|
if (mode === "locked-off") _allowUnsafeEval = false;
|
|
2685
2848
|
if (mode === "locked-on") _allowUnsafeEval = true;
|
|
2849
|
+
if (mode === "mutable") _allowUnsafeEval = true;
|
|
2686
2850
|
scopeExpressionCache = /* @__PURE__ */ new WeakMap();
|
|
2687
2851
|
scopeHandlerCache = /* @__PURE__ */ new WeakMap();
|
|
2688
2852
|
}
|