@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.
Files changed (46) hide show
  1. package/README.md +201 -13
  2. package/dist/{chunk-KX5WRZH7.js → chunk-DPSAVBCP.js} +3 -4
  3. package/dist/chunk-DPSAVBCP.js.map +1 -0
  4. package/dist/{chunk-FPSLC62A.cjs → chunk-KH3F5NRU.cjs} +2 -4
  5. package/dist/chunk-KH3F5NRU.cjs.map +1 -0
  6. package/dist/{chunk-VKKPX4YU.js → chunk-TKGUHASG.js} +79 -19
  7. package/dist/chunk-TKGUHASG.js.map +1 -0
  8. package/dist/{chunk-VP5EOGM5.cjs → chunk-YSKF3VRA.cjs} +87 -26
  9. package/dist/chunk-YSKF3VRA.cjs.map +1 -0
  10. package/dist/forma-runtime-csp.js +2 -0
  11. package/dist/forma-runtime.js +2 -0
  12. package/dist/formajs-runtime-hardened.global.js +1 -1
  13. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  14. package/dist/formajs-runtime.global.js +1 -1
  15. package/dist/formajs-runtime.global.js.map +1 -1
  16. package/dist/formajs.global.js +1 -1
  17. package/dist/formajs.global.js.map +1 -1
  18. package/dist/index.cjs +154 -96
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +83 -35
  21. package/dist/index.d.ts +83 -35
  22. package/dist/index.js +99 -40
  23. package/dist/index.js.map +1 -1
  24. package/dist/runtime-hardened.cjs +224 -60
  25. package/dist/runtime-hardened.cjs.map +1 -1
  26. package/dist/runtime-hardened.d.cts +86 -0
  27. package/dist/runtime-hardened.d.ts +86 -0
  28. package/dist/runtime-hardened.js +225 -61
  29. package/dist/runtime-hardened.js.map +1 -1
  30. package/dist/runtime.cjs +248 -83
  31. package/dist/runtime.cjs.map +1 -1
  32. package/dist/runtime.js +226 -61
  33. package/dist/runtime.js.map +1 -1
  34. package/dist/ssr/index.cjs +69 -73
  35. package/dist/ssr/index.cjs.map +1 -1
  36. package/dist/ssr/index.d.cts +4 -0
  37. package/dist/ssr/index.d.ts +4 -0
  38. package/dist/ssr/index.js +69 -73
  39. package/dist/ssr/index.js.map +1 -1
  40. package/dist/tc39-compat.cjs +3 -3
  41. package/dist/tc39-compat.js +1 -1
  42. package/package.json +24 -3
  43. package/dist/chunk-FPSLC62A.cjs.map +0 -1
  44. package/dist/chunk-KX5WRZH7.js.map +0 -1
  45. package/dist/chunk-VKKPX4YU.js.map +0 -1
  46. package/dist/chunk-VP5EOGM5.cjs.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,5 +1,5 @@
1
- import { internalEffect, reconcileList, batch } from './chunk-VKKPX4YU.js';
2
- import { createValueSignal, createComputed } from './chunk-KX5WRZH7.js';
1
+ import { internalEffect, reconcileList, batch } from './chunk-TKGUHASG.js';
2
+ import { createSignal, createComputed } from './chunk-DPSAVBCP.js';
3
3
 
4
4
  // src/dom/reconcile.ts
5
5
  function getBindTargets(el) {
@@ -479,6 +479,7 @@ if (buildUnsafeEvalMode) {
479
479
  _unsafeEvalMode = buildUnsafeEvalMode;
480
480
  if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
481
481
  if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
482
+ if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
482
483
  }
483
484
  var runtimeConfig = readRuntimeConfig();
484
485
  var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
@@ -556,7 +557,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
556
557
  var RE_NUMBER = /^-?\d+(\.\d+)?$/;
557
558
  var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
558
559
  var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
559
- var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
560
560
  var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
561
561
  var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
562
562
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
@@ -567,7 +567,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
567
567
  var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
568
568
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
569
569
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
570
- var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
571
570
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
572
571
  var RE_STRIP_BRACES = /^\{|\}$/g;
573
572
  var RE_DIGIT_ONLY = /^\d+$/;
@@ -577,11 +576,11 @@ var RE_TOGGLE = /^(\w+)\s*=\s*!(\w+)$/;
577
576
  var RE_ASSIGN = /^(\w+)\s*=\s*(.+)$/;
578
577
  var RE_COMPOUND = /^(\w+)\s*(\+=|-=|\*=|\/=)\s*(.+)$/;
579
578
  var RE_IF_PREFIX = /^if\b/;
580
- var RE_UNQUOTED_KEYS = /(\w+)\s*:/g;
581
579
  var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
582
580
  var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
583
581
  var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
584
582
  var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
583
+ var RE_EVENT_REF = /\bevent\s*[.([]|\$event\b/;
585
584
  var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
586
585
  var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
587
586
  var EXPRESSION_CACHE_MAX = 2048;
@@ -596,7 +595,77 @@ function cacheExpression(key, factory) {
596
595
  var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
597
596
  var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
598
597
  var compiledTemplateCache = /* @__PURE__ */ new Map();
599
- var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "__proto__", "prototype"]);
598
+ var COMPILED_TEMPLATE_CACHE_MAX = 2048;
599
+ function cacheCompiledTemplate(key, template) {
600
+ if (compiledTemplateCache.size >= COMPILED_TEMPLATE_CACHE_MAX) {
601
+ const first = compiledTemplateCache.keys().next().value;
602
+ if (first !== void 0) compiledTemplateCache.delete(first);
603
+ }
604
+ compiledTemplateCache.set(key, template);
605
+ }
606
+ var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
607
+ "constructor",
608
+ "__proto__",
609
+ "prototype",
610
+ "__defineGetter__",
611
+ "__defineSetter__",
612
+ "__lookupGetter__",
613
+ "__lookupSetter__",
614
+ "eval",
615
+ "Function"
616
+ ]);
617
+ var BLOCKED_METHOD_REGEXES = (() => {
618
+ const result = [];
619
+ for (const name of UNSAFE_METHOD_NAMES) {
620
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
621
+ result.push({
622
+ name,
623
+ // Match as property access (.name) or bare identifier at start
624
+ dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
625
+ // Match bracket access with single quotes, double quotes, or backticks
626
+ bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
627
+ });
628
+ }
629
+ return result;
630
+ })();
631
+ function findBlockedMethod(expr) {
632
+ let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
633
+ cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
634
+ cleaned = cleaned.replace(/\s*\.\s*/g, ".");
635
+ for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
636
+ if (dotRe.test(cleaned)) return name;
637
+ if (bracketRe.test(cleaned)) return name;
638
+ }
639
+ if (cleaned.includes("[")) {
640
+ const bracketContents = extractBracketContents(cleaned);
641
+ for (const content of bracketContents) {
642
+ if (!content.includes("+")) continue;
643
+ const fragments = content.match(/['"`]([^'"`]*?)['"`]/g);
644
+ if (!fragments) continue;
645
+ const joined = fragments.map((f) => f.slice(1, -1)).join("");
646
+ if (UNSAFE_METHOD_NAMES.has(joined)) return joined;
647
+ }
648
+ }
649
+ return null;
650
+ }
651
+ function extractBracketContents(expr) {
652
+ const results = [];
653
+ let depth = 0;
654
+ let start = -1;
655
+ for (let i = 0; i < expr.length; i++) {
656
+ if (expr[i] === "[") {
657
+ if (depth === 0) start = i + 1;
658
+ depth++;
659
+ } else if (expr[i] === "]") {
660
+ depth--;
661
+ if (depth === 0 && start >= 0) {
662
+ results.push(expr.slice(start, i));
663
+ start = -1;
664
+ }
665
+ }
666
+ }
667
+ return results;
668
+ }
600
669
  var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
601
670
  function toTextValue(value) {
602
671
  if (value == null) return "";
@@ -871,6 +940,7 @@ function consumeStatement(raw) {
871
940
  function parseIfHandler(expr, scope) {
872
941
  const input = expr.trim();
873
942
  if (!RE_IF_PREFIX.test(input)) return null;
943
+ if (RE_EVENT_REF.test(input)) return null;
874
944
  let idx = 2;
875
945
  while (idx < input.length && /\s/.test(input[idx])) idx++;
876
946
  if (input[idx] !== "(") return null;
@@ -931,7 +1001,7 @@ function compileTemplate(text) {
931
1001
  dynamics,
932
1002
  hasItemRef: dynamics.length > 0
933
1003
  };
934
- compiledTemplateCache.set(text, result);
1004
+ cacheCompiledTemplate(text, result);
935
1005
  return result;
936
1006
  }
937
1007
  var templateTexts = /* @__PURE__ */ new WeakMap();
@@ -1272,6 +1342,68 @@ function cloneAttributeTemplates(el, item) {
1272
1342
  }
1273
1343
  }
1274
1344
  }
1345
+ function parseChainedAccess(expr, scope) {
1346
+ let pos = 0;
1347
+ const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
1348
+ if (!identMatch) return null;
1349
+ const rootName = identMatch[0];
1350
+ pos = rootName.length;
1351
+ if (pos >= expr.length) return null;
1352
+ if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
1353
+ const steps = [];
1354
+ while (pos < expr.length) {
1355
+ let optional = false;
1356
+ if (expr[pos] === "?" && expr[pos + 1] === ".") {
1357
+ optional = true;
1358
+ pos += 2;
1359
+ } else if (expr[pos] === ".") {
1360
+ pos += 1;
1361
+ } else {
1362
+ return null;
1363
+ }
1364
+ const nameMatch = expr.slice(pos).match(/^\w+/);
1365
+ if (!nameMatch) return null;
1366
+ const name = nameMatch[0];
1367
+ pos += name.length;
1368
+ if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
1369
+ if (pos < expr.length && expr[pos] === "(") {
1370
+ const balanced = readBalancedSegment(expr, pos, "(", ")");
1371
+ if (!balanced) return null;
1372
+ const argsRaw = balanced.inner.trim();
1373
+ const argFns = [];
1374
+ for (const arg of splitCallArgs(argsRaw)) {
1375
+ const parsed = parseExpression(arg, scope);
1376
+ if (!parsed) return null;
1377
+ argFns.push(parsed);
1378
+ }
1379
+ steps.push({ type: "call", name, optional, argFns });
1380
+ pos = balanced.end + 1;
1381
+ } else {
1382
+ steps.push({ type: "prop", name, optional });
1383
+ }
1384
+ }
1385
+ if (pos !== expr.length) return null;
1386
+ if (steps.length === 0) return null;
1387
+ const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
1388
+ return () => {
1389
+ let val = rootExpr();
1390
+ for (const step of steps) {
1391
+ if (val == null) {
1392
+ if (step.optional) return void 0;
1393
+ return void 0;
1394
+ }
1395
+ if (step.type === "prop") {
1396
+ val = val[step.name];
1397
+ } else {
1398
+ const method = val[step.name];
1399
+ if (typeof method !== "function") return void 0;
1400
+ const args = step.argFns.map((fn) => fn());
1401
+ val = method.apply(val, args);
1402
+ }
1403
+ }
1404
+ return val;
1405
+ };
1406
+ }
1275
1407
  function parseExpression(expr, scope) {
1276
1408
  const cachedFactory = expressionCache.get(expr);
1277
1409
  if (cachedFactory) return cachedFactory(scope);
@@ -1322,24 +1454,9 @@ function parseExpressionUncached(expr, scope) {
1322
1454
  if (RE_IDENTIFIER.test(expr)) {
1323
1455
  return () => scope.getters[expr]?.();
1324
1456
  }
1325
- const dotMatch = expr.match(RE_DOT_ACCESS);
1326
- if (dotMatch) {
1327
- const p1 = dotMatch[1], p2 = dotMatch[2];
1328
- return () => {
1329
- const obj = scope.getters[p1]?.();
1330
- return obj?.[p2];
1331
- };
1332
- }
1333
- const deepDotMatch = expr.match(RE_DEEP_DOT);
1334
- if (deepDotMatch) {
1335
- const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
1336
- return () => {
1337
- let val = scope.getters[p1]?.();
1338
- val = val?.[p2];
1339
- val = val?.[p3];
1340
- if (p4) val = val?.[p4];
1341
- return val;
1342
- };
1457
+ {
1458
+ const chainResult = parseChainedAccess(expr, scope);
1459
+ if (chainResult) return chainResult;
1343
1460
  }
1344
1461
  const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
1345
1462
  if (groupedCallMatch) {
@@ -1363,28 +1480,6 @@ function parseExpressionUncached(expr, scope) {
1363
1480
  return method.apply(base, args);
1364
1481
  };
1365
1482
  }
1366
- const callMatch = expr.match(RE_METHOD_CALL);
1367
- if (callMatch) {
1368
- const baseName = callMatch[1];
1369
- const methodName = callMatch[2];
1370
- const argsRaw = callMatch[3].trim();
1371
- if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
1372
- const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
1373
- if (!baseExpr) return null;
1374
- const argFns = [];
1375
- for (const arg of splitCallArgs(argsRaw)) {
1376
- const parsed = parseExpression(arg, scope);
1377
- if (!parsed) return null;
1378
- argFns.push(parsed);
1379
- }
1380
- return () => {
1381
- const base = baseExpr();
1382
- const method = base?.[methodName];
1383
- if (typeof method !== "function") return void 0;
1384
- const args = argFns.map((fn) => fn());
1385
- return method.apply(base, args);
1386
- };
1387
- }
1388
1483
  if (expr.startsWith("!")) {
1389
1484
  const inner = parseExpression(expr.slice(1).trim(), scope);
1390
1485
  if (inner) return () => !inner();
@@ -1403,6 +1498,29 @@ function parseExpressionUncached(expr, scope) {
1403
1498
  return () => objExpr()?.[key];
1404
1499
  }
1405
1500
  }
1501
+ if (expr.startsWith("[")) {
1502
+ const balanced = readBalancedSegment(expr, 0, "[", "]");
1503
+ if (balanced && balanced.end === expr.length - 1) {
1504
+ const inner = balanced.inner.trim();
1505
+ if (inner === "") {
1506
+ return () => [];
1507
+ }
1508
+ const elements = splitCallArgs(inner);
1509
+ const elementFns = [];
1510
+ let allParsed = true;
1511
+ for (const el of elements) {
1512
+ const parsed = parseExpression(el.trim(), scope);
1513
+ if (!parsed) {
1514
+ allParsed = false;
1515
+ break;
1516
+ }
1517
+ elementFns.push(parsed);
1518
+ }
1519
+ if (allParsed) {
1520
+ return () => elementFns.map((fn) => fn());
1521
+ }
1522
+ }
1523
+ }
1406
1524
  const ternaryMatch = expr.match(RE_TERNARY);
1407
1525
  if (ternaryMatch) {
1408
1526
  const cond = parseExpression(ternaryMatch[1].trim(), scope);
@@ -1532,6 +1650,15 @@ function getScopeCache(cache, scope) {
1532
1650
  }
1533
1651
  return scoped;
1534
1652
  }
1653
+ function cspExpressionHint(expr) {
1654
+ if (expr.includes("...")) {
1655
+ return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
1656
+ }
1657
+ if (expr.includes("=>")) {
1658
+ return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
1659
+ }
1660
+ return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
1661
+ }
1535
1662
  function buildEvaluator(expr, scope) {
1536
1663
  const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1537
1664
  const cache = getScopeCache(scopeExpressionCache, scope);
@@ -1544,11 +1671,17 @@ function buildEvaluator(expr, scope) {
1544
1671
  }
1545
1672
  if (!_allowUnsafeEval) {
1546
1673
  dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
1547
- reportDiagnostic("expression-unsupported", cleaned, "Unsupported expression in CSP-safe mode");
1674
+ reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
1548
1675
  const blocked = () => void 0;
1549
1676
  cache.set(cleaned, blocked);
1550
1677
  return blocked;
1551
1678
  }
1679
+ const blockedMethod = findBlockedMethod(cleaned);
1680
+ if (blockedMethod) {
1681
+ const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
1682
+ reportDiagnostic("expression-unsupported", cleaned, msg);
1683
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1684
+ }
1552
1685
  try {
1553
1686
  const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
1554
1687
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1556,6 +1689,7 @@ function buildEvaluator(expr, scope) {
1556
1689
  return key in scope.getters;
1557
1690
  },
1558
1691
  get(_, key) {
1692
+ if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
1559
1693
  const g = scope.getters[key];
1560
1694
  return g ? g() : void 0;
1561
1695
  }
@@ -1564,7 +1698,7 @@ function buildEvaluator(expr, scope) {
1564
1698
  cache.set(cleaned, unsafe);
1565
1699
  return unsafe;
1566
1700
  } catch {
1567
- reportDiagnostic("expression-unsupported", cleaned, "Expression failed to compile");
1701
+ 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.");
1568
1702
  const failed = () => void 0;
1569
1703
  cache.set(cleaned, failed);
1570
1704
  return failed;
@@ -1670,7 +1804,13 @@ function parseHandler(expr, scope) {
1670
1804
  return null;
1671
1805
  }
1672
1806
  function buildHandler(expr, scope) {
1673
- const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1807
+ let cleaned = expr.trim();
1808
+ if (cleaned.startsWith("{")) {
1809
+ const seg = readBalancedSegment(cleaned, 0, "{", "}");
1810
+ if (seg && seg.end === cleaned.length - 1) {
1811
+ cleaned = seg.inner.trim();
1812
+ }
1813
+ }
1674
1814
  const cache = getScopeCache(scopeHandlerCache, scope);
1675
1815
  const cached = cache.get(cleaned);
1676
1816
  if (cached) return cached;
@@ -1682,7 +1822,7 @@ function buildHandler(expr, scope) {
1682
1822
  }
1683
1823
  if (!_allowUnsafeEval) {
1684
1824
  dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
1685
- reportDiagnostic("handler-unsupported", cleaned, "Unsupported handler in CSP-safe mode");
1825
+ reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
1686
1826
  const result = {
1687
1827
  handler: () => {
1688
1828
  },
@@ -1691,6 +1831,12 @@ function buildHandler(expr, scope) {
1691
1831
  cache.set(cleaned, result);
1692
1832
  return result;
1693
1833
  }
1834
+ const blockedMethod = findBlockedMethod(cleaned);
1835
+ if (blockedMethod) {
1836
+ const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
1837
+ reportDiagnostic("handler-unsupported", cleaned, msg);
1838
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1839
+ }
1694
1840
  try {
1695
1841
  const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
1696
1842
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1699,6 +1845,7 @@ function buildHandler(expr, scope) {
1699
1845
  return key in scope.getters || key in scope.setters;
1700
1846
  },
1701
1847
  get(_, key) {
1848
+ if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
1702
1849
  const g = scope.getters[key];
1703
1850
  return g ? g() : void 0;
1704
1851
  },
@@ -1718,7 +1865,7 @@ function buildHandler(expr, scope) {
1718
1865
  cache.set(cleaned, result);
1719
1866
  return result;
1720
1867
  } catch {
1721
- reportDiagnostic("handler-unsupported", cleaned, "Handler failed to compile");
1868
+ 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.");
1722
1869
  const result = {
1723
1870
  handler: () => {
1724
1871
  },
@@ -1728,16 +1875,21 @@ function buildHandler(expr, scope) {
1728
1875
  return result;
1729
1876
  }
1730
1877
  }
1878
+ var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1731
1879
  function parseState(raw) {
1880
+ let parsed;
1732
1881
  try {
1733
- return JSON.parse(raw);
1882
+ parsed = JSON.parse(raw);
1734
1883
  } catch {
1735
- try {
1736
- return JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
1737
- } catch {
1738
- return {};
1884
+ if (_debug) {
1885
+ dbg("parseState: Invalid JSON in data-forma-state \u2014 use valid JSON with quoted keys. Got:", raw.slice(0, 200));
1739
1886
  }
1887
+ return {};
1888
+ }
1889
+ for (const key of FORBIDDEN_STATE_KEYS) {
1890
+ if (key in parsed) delete parsed[key];
1740
1891
  }
1892
+ return parsed;
1741
1893
  }
1742
1894
  function initScope(stateEl) {
1743
1895
  const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
@@ -1752,7 +1904,7 @@ function initScope(stateEl) {
1752
1904
  const getters = {};
1753
1905
  const setters = {};
1754
1906
  for (const [key, initial] of Object.entries(state)) {
1755
- const [get, set] = createValueSignal(initial);
1907
+ const [get, set] = createSignal(initial);
1756
1908
  getters[key] = get;
1757
1909
  setters[key] = set;
1758
1910
  }
@@ -1760,6 +1912,18 @@ function initScope(stateEl) {
1760
1912
  return { getters, setters };
1761
1913
  }
1762
1914
  function bindElement(el, scope, disposers) {
1915
+ const elMagics = {
1916
+ $el: el,
1917
+ $dispatch: (name, detail) => {
1918
+ el.dispatchEvent(new CustomEvent(name, {
1919
+ bubbles: true,
1920
+ composed: true,
1921
+ // crosses Shadow DOM boundaries (important for <forma-stage>)
1922
+ detail
1923
+ }));
1924
+ }
1925
+ };
1926
+ scope = createChildScope(scope, elMagics);
1763
1927
  const known = getDirectives(el);
1764
1928
  const computedAttr = !known || known.has("data-computed") ? el.getAttribute("data-computed") : null;
1765
1929
  if (computedAttr) {
@@ -2139,16 +2303,16 @@ function bindElement(el, scope, disposers) {
2139
2303
  else if (k === "error") errorTarget = v;
2140
2304
  else if (k === "poll") interval = parseInt(v ?? "0", 10);
2141
2305
  }
2142
- const [getTarget, setTarget] = createValueSignal(null);
2306
+ const [getTarget, setTarget] = createSignal(null);
2143
2307
  scope.getters[target] = getTarget;
2144
2308
  scope.setters[target] = setTarget;
2145
2309
  if (loadingTarget) {
2146
- const [gl, sl] = createValueSignal(false);
2310
+ const [gl, sl] = createSignal(false);
2147
2311
  scope.getters[loadingTarget] = gl;
2148
2312
  scope.setters[loadingTarget] = sl;
2149
2313
  }
2150
2314
  if (errorTarget) {
2151
- const [ge, se] = createValueSignal(null);
2315
+ const [ge, se] = createSignal(null);
2152
2316
  scope.getters[errorTarget] = ge;
2153
2317
  scope.setters[errorTarget] = se;
2154
2318
  }
@@ -2407,6 +2571,7 @@ function setUnsafeEvalMode(mode) {
2407
2571
  _unsafeEvalMode = mode;
2408
2572
  if (mode === "locked-off") _allowUnsafeEval = false;
2409
2573
  if (mode === "locked-on") _allowUnsafeEval = true;
2574
+ if (mode === "mutable") _allowUnsafeEval = true;
2410
2575
  scopeExpressionCache = /* @__PURE__ */ new WeakMap();
2411
2576
  scopeHandlerCache = /* @__PURE__ */ new WeakMap();
2412
2577
  }