@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
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, effect,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
};
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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, "
|
|
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
|
-
|
|
2155
|
+
parsed = JSON.parse(raw);
|
|
2008
2156
|
} catch {
|
|
2009
|
-
|
|
2010
|
-
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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
|
}
|