@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
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { computed, effect, signal, startBatch, endBatch, pauseTracking, resumeTracking } from 'alien-signals';
1
+ import { signal, computed, effect, startBatch, endBatch, pauseTracking, resumeTracking } from 'alien-signals';
2
2
 
3
3
  // src/reactive/signal.ts
4
4
  function applySignalSet(s, v) {
@@ -17,7 +17,6 @@ function createSignal(initialValue) {
17
17
  const setter = (v) => applySignalSet(s, v);
18
18
  return [getter, setter];
19
19
  }
20
- var createValueSignal = createSignal;
21
20
  function internalEffect(fn) {
22
21
  const dispose = effect(fn);
23
22
  return dispose;
@@ -753,6 +752,7 @@ if (buildUnsafeEvalMode) {
753
752
  _unsafeEvalMode = buildUnsafeEvalMode;
754
753
  if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
755
754
  if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
755
+ if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
756
756
  }
757
757
  var runtimeConfig = readRuntimeConfig();
758
758
  var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
@@ -830,7 +830,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
830
830
  var RE_NUMBER = /^-?\d+(\.\d+)?$/;
831
831
  var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
832
832
  var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
833
- var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
834
833
  var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
835
834
  var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
836
835
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
@@ -841,7 +840,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
841
840
  var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
842
841
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
843
842
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
844
- var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
845
843
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
846
844
  var RE_STRIP_BRACES = /^\{|\}$/g;
847
845
  var RE_DIGIT_ONLY = /^\d+$/;
@@ -851,11 +849,11 @@ var RE_TOGGLE = /^(\w+)\s*=\s*!(\w+)$/;
851
849
  var RE_ASSIGN = /^(\w+)\s*=\s*(.+)$/;
852
850
  var RE_COMPOUND = /^(\w+)\s*(\+=|-=|\*=|\/=)\s*(.+)$/;
853
851
  var RE_IF_PREFIX = /^if\b/;
854
- var RE_UNQUOTED_KEYS = /(\w+)\s*:/g;
855
852
  var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
856
853
  var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
857
854
  var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
858
855
  var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
856
+ var RE_EVENT_REF = /\bevent\s*[.([]|\$event\b/;
859
857
  var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
860
858
  var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
861
859
  var EXPRESSION_CACHE_MAX = 2048;
@@ -870,7 +868,77 @@ function cacheExpression(key, factory) {
870
868
  var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
871
869
  var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
872
870
  var compiledTemplateCache = /* @__PURE__ */ new Map();
873
- var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "__proto__", "prototype"]);
871
+ var COMPILED_TEMPLATE_CACHE_MAX = 2048;
872
+ function cacheCompiledTemplate(key, template) {
873
+ if (compiledTemplateCache.size >= COMPILED_TEMPLATE_CACHE_MAX) {
874
+ const first = compiledTemplateCache.keys().next().value;
875
+ if (first !== void 0) compiledTemplateCache.delete(first);
876
+ }
877
+ compiledTemplateCache.set(key, template);
878
+ }
879
+ var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
880
+ "constructor",
881
+ "__proto__",
882
+ "prototype",
883
+ "__defineGetter__",
884
+ "__defineSetter__",
885
+ "__lookupGetter__",
886
+ "__lookupSetter__",
887
+ "eval",
888
+ "Function"
889
+ ]);
890
+ var BLOCKED_METHOD_REGEXES = (() => {
891
+ const result = [];
892
+ for (const name of UNSAFE_METHOD_NAMES) {
893
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
894
+ result.push({
895
+ name,
896
+ // Match as property access (.name) or bare identifier at start
897
+ dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
898
+ // Match bracket access with single quotes, double quotes, or backticks
899
+ bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
900
+ });
901
+ }
902
+ return result;
903
+ })();
904
+ function findBlockedMethod(expr) {
905
+ let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
906
+ cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
907
+ cleaned = cleaned.replace(/\s*\.\s*/g, ".");
908
+ for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
909
+ if (dotRe.test(cleaned)) return name;
910
+ if (bracketRe.test(cleaned)) return name;
911
+ }
912
+ if (cleaned.includes("[")) {
913
+ const bracketContents = extractBracketContents(cleaned);
914
+ for (const content of bracketContents) {
915
+ if (!content.includes("+")) continue;
916
+ const fragments = content.match(/['"`]([^'"`]*?)['"`]/g);
917
+ if (!fragments) continue;
918
+ const joined = fragments.map((f) => f.slice(1, -1)).join("");
919
+ if (UNSAFE_METHOD_NAMES.has(joined)) return joined;
920
+ }
921
+ }
922
+ return null;
923
+ }
924
+ function extractBracketContents(expr) {
925
+ const results = [];
926
+ let depth = 0;
927
+ let start = -1;
928
+ for (let i = 0; i < expr.length; i++) {
929
+ if (expr[i] === "[") {
930
+ if (depth === 0) start = i + 1;
931
+ depth++;
932
+ } else if (expr[i] === "]") {
933
+ depth--;
934
+ if (depth === 0 && start >= 0) {
935
+ results.push(expr.slice(start, i));
936
+ start = -1;
937
+ }
938
+ }
939
+ }
940
+ return results;
941
+ }
874
942
  var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
875
943
  function toTextValue(value2) {
876
944
  if (value2 == null) return "";
@@ -1145,6 +1213,7 @@ function consumeStatement(raw) {
1145
1213
  function parseIfHandler(expr, scope) {
1146
1214
  const input = expr.trim();
1147
1215
  if (!RE_IF_PREFIX.test(input)) return null;
1216
+ if (RE_EVENT_REF.test(input)) return null;
1148
1217
  let idx = 2;
1149
1218
  while (idx < input.length && /\s/.test(input[idx])) idx++;
1150
1219
  if (input[idx] !== "(") return null;
@@ -1205,7 +1274,7 @@ function compileTemplate(text) {
1205
1274
  dynamics,
1206
1275
  hasItemRef: dynamics.length > 0
1207
1276
  };
1208
- compiledTemplateCache.set(text, result);
1277
+ cacheCompiledTemplate(text, result);
1209
1278
  return result;
1210
1279
  }
1211
1280
  var templateTexts = /* @__PURE__ */ new WeakMap();
@@ -1546,6 +1615,68 @@ function cloneAttributeTemplates(el, item) {
1546
1615
  }
1547
1616
  }
1548
1617
  }
1618
+ function parseChainedAccess(expr, scope) {
1619
+ let pos = 0;
1620
+ const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
1621
+ if (!identMatch) return null;
1622
+ const rootName = identMatch[0];
1623
+ pos = rootName.length;
1624
+ if (pos >= expr.length) return null;
1625
+ if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
1626
+ const steps = [];
1627
+ while (pos < expr.length) {
1628
+ let optional = false;
1629
+ if (expr[pos] === "?" && expr[pos + 1] === ".") {
1630
+ optional = true;
1631
+ pos += 2;
1632
+ } else if (expr[pos] === ".") {
1633
+ pos += 1;
1634
+ } else {
1635
+ return null;
1636
+ }
1637
+ const nameMatch = expr.slice(pos).match(/^\w+/);
1638
+ if (!nameMatch) return null;
1639
+ const name = nameMatch[0];
1640
+ pos += name.length;
1641
+ if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
1642
+ if (pos < expr.length && expr[pos] === "(") {
1643
+ const balanced = readBalancedSegment(expr, pos, "(", ")");
1644
+ if (!balanced) return null;
1645
+ const argsRaw = balanced.inner.trim();
1646
+ const argFns = [];
1647
+ for (const arg of splitCallArgs(argsRaw)) {
1648
+ const parsed = parseExpression(arg, scope);
1649
+ if (!parsed) return null;
1650
+ argFns.push(parsed);
1651
+ }
1652
+ steps.push({ type: "call", name, optional, argFns });
1653
+ pos = balanced.end + 1;
1654
+ } else {
1655
+ steps.push({ type: "prop", name, optional });
1656
+ }
1657
+ }
1658
+ if (pos !== expr.length) return null;
1659
+ if (steps.length === 0) return null;
1660
+ const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
1661
+ return () => {
1662
+ let val = rootExpr();
1663
+ for (const step of steps) {
1664
+ if (val == null) {
1665
+ if (step.optional) return void 0;
1666
+ return void 0;
1667
+ }
1668
+ if (step.type === "prop") {
1669
+ val = val[step.name];
1670
+ } else {
1671
+ const method = val[step.name];
1672
+ if (typeof method !== "function") return void 0;
1673
+ const args = step.argFns.map((fn) => fn());
1674
+ val = method.apply(val, args);
1675
+ }
1676
+ }
1677
+ return val;
1678
+ };
1679
+ }
1549
1680
  function parseExpression(expr, scope) {
1550
1681
  const cachedFactory = expressionCache.get(expr);
1551
1682
  if (cachedFactory) return cachedFactory(scope);
@@ -1596,24 +1727,9 @@ function parseExpressionUncached(expr, scope) {
1596
1727
  if (RE_IDENTIFIER.test(expr)) {
1597
1728
  return () => scope.getters[expr]?.();
1598
1729
  }
1599
- const dotMatch = expr.match(RE_DOT_ACCESS);
1600
- if (dotMatch) {
1601
- const p1 = dotMatch[1], p2 = dotMatch[2];
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
- };
1730
+ {
1731
+ const chainResult = parseChainedAccess(expr, scope);
1732
+ if (chainResult) return chainResult;
1617
1733
  }
1618
1734
  const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
1619
1735
  if (groupedCallMatch) {
@@ -1637,28 +1753,6 @@ function parseExpressionUncached(expr, scope) {
1637
1753
  return method.apply(base, args);
1638
1754
  };
1639
1755
  }
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
1756
  if (expr.startsWith("!")) {
1663
1757
  const inner = parseExpression(expr.slice(1).trim(), scope);
1664
1758
  if (inner) return () => !inner();
@@ -1677,6 +1771,29 @@ function parseExpressionUncached(expr, scope) {
1677
1771
  return () => objExpr()?.[key];
1678
1772
  }
1679
1773
  }
1774
+ if (expr.startsWith("[")) {
1775
+ const balanced = readBalancedSegment(expr, 0, "[", "]");
1776
+ if (balanced && balanced.end === expr.length - 1) {
1777
+ const inner = balanced.inner.trim();
1778
+ if (inner === "") {
1779
+ return () => [];
1780
+ }
1781
+ const elements = splitCallArgs(inner);
1782
+ const elementFns = [];
1783
+ let allParsed = true;
1784
+ for (const el of elements) {
1785
+ const parsed = parseExpression(el.trim(), scope);
1786
+ if (!parsed) {
1787
+ allParsed = false;
1788
+ break;
1789
+ }
1790
+ elementFns.push(parsed);
1791
+ }
1792
+ if (allParsed) {
1793
+ return () => elementFns.map((fn) => fn());
1794
+ }
1795
+ }
1796
+ }
1680
1797
  const ternaryMatch = expr.match(RE_TERNARY);
1681
1798
  if (ternaryMatch) {
1682
1799
  const cond = parseExpression(ternaryMatch[1].trim(), scope);
@@ -1806,6 +1923,15 @@ function getScopeCache(cache, scope) {
1806
1923
  }
1807
1924
  return scoped;
1808
1925
  }
1926
+ function cspExpressionHint(expr) {
1927
+ if (expr.includes("...")) {
1928
+ return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
1929
+ }
1930
+ if (expr.includes("=>")) {
1931
+ return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
1932
+ }
1933
+ return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
1934
+ }
1809
1935
  function buildEvaluator(expr, scope) {
1810
1936
  const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
1811
1937
  const cache = getScopeCache(scopeExpressionCache, scope);
@@ -1818,11 +1944,17 @@ function buildEvaluator(expr, scope) {
1818
1944
  }
1819
1945
  if (!_allowUnsafeEval) {
1820
1946
  dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
1821
- reportDiagnostic("expression-unsupported", cleaned, "Unsupported expression in CSP-safe mode");
1947
+ reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
1822
1948
  const blocked = () => void 0;
1823
1949
  cache.set(cleaned, blocked);
1824
1950
  return blocked;
1825
1951
  }
1952
+ const blockedMethod = findBlockedMethod(cleaned);
1953
+ if (blockedMethod) {
1954
+ const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
1955
+ reportDiagnostic("expression-unsupported", cleaned, msg);
1956
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
1957
+ }
1826
1958
  try {
1827
1959
  const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
1828
1960
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1830,6 +1962,7 @@ function buildEvaluator(expr, scope) {
1830
1962
  return key in scope.getters;
1831
1963
  },
1832
1964
  get(_, key) {
1965
+ if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
1833
1966
  const g = scope.getters[key];
1834
1967
  return g ? g() : void 0;
1835
1968
  }
@@ -1838,7 +1971,7 @@ function buildEvaluator(expr, scope) {
1838
1971
  cache.set(cleaned, unsafe);
1839
1972
  return unsafe;
1840
1973
  } catch {
1841
- reportDiagnostic("expression-unsupported", cleaned, "Expression failed to compile");
1974
+ 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
1975
  const failed = () => void 0;
1843
1976
  cache.set(cleaned, failed);
1844
1977
  return failed;
@@ -1944,7 +2077,13 @@ function parseHandler(expr, scope) {
1944
2077
  return null;
1945
2078
  }
1946
2079
  function buildHandler(expr, scope) {
1947
- const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
2080
+ let cleaned = expr.trim();
2081
+ if (cleaned.startsWith("{")) {
2082
+ const seg = readBalancedSegment(cleaned, 0, "{", "}");
2083
+ if (seg && seg.end === cleaned.length - 1) {
2084
+ cleaned = seg.inner.trim();
2085
+ }
2086
+ }
1948
2087
  const cache = getScopeCache(scopeHandlerCache, scope);
1949
2088
  const cached = cache.get(cleaned);
1950
2089
  if (cached) return cached;
@@ -1956,7 +2095,7 @@ function buildHandler(expr, scope) {
1956
2095
  }
1957
2096
  if (!_allowUnsafeEval) {
1958
2097
  dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
1959
- reportDiagnostic("handler-unsupported", cleaned, "Unsupported handler in CSP-safe mode");
2098
+ reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
1960
2099
  const result = {
1961
2100
  handler: () => {
1962
2101
  },
@@ -1965,6 +2104,12 @@ function buildHandler(expr, scope) {
1965
2104
  cache.set(cleaned, result);
1966
2105
  return result;
1967
2106
  }
2107
+ const blockedMethod = findBlockedMethod(cleaned);
2108
+ if (blockedMethod) {
2109
+ const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
2110
+ reportDiagnostic("handler-unsupported", cleaned, msg);
2111
+ throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
2112
+ }
1968
2113
  try {
1969
2114
  const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
1970
2115
  const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
@@ -1973,6 +2118,7 @@ function buildHandler(expr, scope) {
1973
2118
  return key in scope.getters || key in scope.setters;
1974
2119
  },
1975
2120
  get(_, key) {
2121
+ if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
1976
2122
  const g = scope.getters[key];
1977
2123
  return g ? g() : void 0;
1978
2124
  },
@@ -1992,7 +2138,7 @@ function buildHandler(expr, scope) {
1992
2138
  cache.set(cleaned, result);
1993
2139
  return result;
1994
2140
  } catch {
1995
- reportDiagnostic("handler-unsupported", cleaned, "Handler failed to compile");
2141
+ 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
2142
  const result = {
1997
2143
  handler: () => {
1998
2144
  },
@@ -2002,16 +2148,21 @@ function buildHandler(expr, scope) {
2002
2148
  return result;
2003
2149
  }
2004
2150
  }
2151
+ var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2005
2152
  function parseState(raw) {
2153
+ let parsed;
2006
2154
  try {
2007
- return JSON.parse(raw);
2155
+ parsed = JSON.parse(raw);
2008
2156
  } catch {
2009
- try {
2010
- return JSON.parse(raw.replace(RE_UNQUOTED_KEYS, '"$1":'));
2011
- } catch {
2012
- return {};
2157
+ if (_debug) {
2158
+ dbg("parseState: Invalid JSON in data-forma-state \u2014 use valid JSON with quoted keys. Got:", raw.slice(0, 200));
2013
2159
  }
2160
+ return {};
2161
+ }
2162
+ for (const key of FORBIDDEN_STATE_KEYS) {
2163
+ if (key in parsed) delete parsed[key];
2014
2164
  }
2165
+ return parsed;
2015
2166
  }
2016
2167
  function initScope(stateEl) {
2017
2168
  const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
@@ -2026,7 +2177,7 @@ function initScope(stateEl) {
2026
2177
  const getters = {};
2027
2178
  const setters = {};
2028
2179
  for (const [key, initial] of Object.entries(state)) {
2029
- const [get, set] = createValueSignal(initial);
2180
+ const [get, set] = createSignal(initial);
2030
2181
  getters[key] = get;
2031
2182
  setters[key] = set;
2032
2183
  }
@@ -2034,6 +2185,18 @@ function initScope(stateEl) {
2034
2185
  return { getters, setters };
2035
2186
  }
2036
2187
  function bindElement(el, scope, disposers) {
2188
+ const elMagics = {
2189
+ $el: el,
2190
+ $dispatch: (name, detail) => {
2191
+ el.dispatchEvent(new CustomEvent(name, {
2192
+ bubbles: true,
2193
+ composed: true,
2194
+ // crosses Shadow DOM boundaries (important for <forma-stage>)
2195
+ detail
2196
+ }));
2197
+ }
2198
+ };
2199
+ scope = createChildScope(scope, elMagics);
2037
2200
  const known = getDirectives(el);
2038
2201
  const computedAttr = !known || known.has("data-computed") ? el.getAttribute("data-computed") : null;
2039
2202
  if (computedAttr) {
@@ -2413,16 +2576,16 @@ function bindElement(el, scope, disposers) {
2413
2576
  else if (k === "error") errorTarget = v;
2414
2577
  else if (k === "poll") interval = parseInt(v ?? "0", 10);
2415
2578
  }
2416
- const [getTarget, setTarget] = createValueSignal(null);
2579
+ const [getTarget, setTarget] = createSignal(null);
2417
2580
  scope.getters[target] = getTarget;
2418
2581
  scope.setters[target] = setTarget;
2419
2582
  if (loadingTarget) {
2420
- const [gl, sl] = createValueSignal(false);
2583
+ const [gl, sl] = createSignal(false);
2421
2584
  scope.getters[loadingTarget] = gl;
2422
2585
  scope.setters[loadingTarget] = sl;
2423
2586
  }
2424
2587
  if (errorTarget) {
2425
- const [ge, se] = createValueSignal(null);
2588
+ const [ge, se] = createSignal(null);
2426
2589
  scope.getters[errorTarget] = ge;
2427
2590
  scope.setters[errorTarget] = se;
2428
2591
  }
@@ -2681,6 +2844,7 @@ function setUnsafeEvalMode(mode) {
2681
2844
  _unsafeEvalMode = mode;
2682
2845
  if (mode === "locked-off") _allowUnsafeEval = false;
2683
2846
  if (mode === "locked-on") _allowUnsafeEval = true;
2847
+ if (mode === "mutable") _allowUnsafeEval = true;
2684
2848
  scopeExpressionCache = /* @__PURE__ */ new WeakMap();
2685
2849
  scopeHandlerCache = /* @__PURE__ */ new WeakMap();
2686
2850
  }