@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
package/dist/runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { internalEffect, reconcileList, batch } from './chunk-
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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, "
|
|
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
|
-
|
|
1882
|
+
parsed = JSON.parse(raw);
|
|
1734
1883
|
} catch {
|
|
1735
|
-
|
|
1736
|
-
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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
|
}
|