@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.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunkYSKF3VRA_cjs = require('./chunk-YSKF3VRA.cjs');
|
|
4
|
+
var chunkKH3F5NRU_cjs = require('./chunk-KH3F5NRU.cjs');
|
|
5
5
|
|
|
6
6
|
// src/dom/reconcile.ts
|
|
7
7
|
function getBindTargets(el) {
|
|
@@ -481,6 +481,7 @@ if (buildUnsafeEvalMode) {
|
|
|
481
481
|
_unsafeEvalMode = buildUnsafeEvalMode;
|
|
482
482
|
if (_unsafeEvalMode === "locked-off") _allowUnsafeEval = false;
|
|
483
483
|
if (_unsafeEvalMode === "locked-on") _allowUnsafeEval = true;
|
|
484
|
+
if (_unsafeEvalMode === "mutable") _allowUnsafeEval = true;
|
|
484
485
|
}
|
|
485
486
|
var runtimeConfig = readRuntimeConfig();
|
|
486
487
|
var configUnsafeMode = runtimeConfig.lockUnsafeEval ? "locked-off" : runtimeConfig.unsafeEvalMode;
|
|
@@ -558,7 +559,6 @@ var RE_STRING_DOUBLE = /^"[^"]*"$/;
|
|
|
558
559
|
var RE_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
559
560
|
var RE_IDENTIFIER = /^[a-zA-Z_$]\w*$/;
|
|
560
561
|
var RE_DOT_ACCESS = /^(\w+)\.(\w+)$/;
|
|
561
|
-
var RE_DEEP_DOT = /^(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?$/;
|
|
562
562
|
var RE_BRACKET = /^(\w+)\[(\d+|'[^']*'|"[^"]*")\]$/;
|
|
563
563
|
var RE_TERNARY = /^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/;
|
|
564
564
|
var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
|
|
@@ -569,7 +569,6 @@ var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
|
|
|
569
569
|
var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
|
|
570
570
|
var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
|
|
571
571
|
var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
|
|
572
|
-
var RE_METHOD_CALL = /^(\w+)\.(\w+)\((.*)\)$/;
|
|
573
572
|
var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
|
|
574
573
|
var RE_STRIP_BRACES = /^\{|\}$/g;
|
|
575
574
|
var RE_DIGIT_ONLY = /^\d+$/;
|
|
@@ -579,11 +578,11 @@ var RE_TOGGLE = /^(\w+)\s*=\s*!(\w+)$/;
|
|
|
579
578
|
var RE_ASSIGN = /^(\w+)\s*=\s*(.+)$/;
|
|
580
579
|
var RE_COMPOUND = /^(\w+)\s*(\+=|-=|\*=|\/=)\s*(.+)$/;
|
|
581
580
|
var RE_IF_PREFIX = /^if\b/;
|
|
582
|
-
var RE_UNQUOTED_KEYS = /(\w+)\s*:/g;
|
|
583
581
|
var RE_COMPUTED = /^(\w+)\s*=\s*(.+)$/;
|
|
584
582
|
var RE_FETCH = /^(.+?)(?:→|->)\s*(\S+)(.*)$/;
|
|
585
583
|
var RE_FETCH_METHOD = /^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i;
|
|
586
584
|
var RE_STRIP_ITEM_BRACES = /^\{item\.?|\}$/g;
|
|
585
|
+
var RE_EVENT_REF = /\bevent\s*[.([]|\$event\b/;
|
|
587
586
|
var RE_REFETCH_CALL = /^\$refetch\(\s*['"]([^'"]+)['"]\s*\)$/;
|
|
588
587
|
var TRANSITION_STATE_SYM = /* @__PURE__ */ Symbol.for("forma-transition-state");
|
|
589
588
|
var EXPRESSION_CACHE_MAX = 2048;
|
|
@@ -598,7 +597,77 @@ function cacheExpression(key, factory) {
|
|
|
598
597
|
var scopeExpressionCache = /* @__PURE__ */ new WeakMap();
|
|
599
598
|
var scopeHandlerCache = /* @__PURE__ */ new WeakMap();
|
|
600
599
|
var compiledTemplateCache = /* @__PURE__ */ new Map();
|
|
601
|
-
var
|
|
600
|
+
var COMPILED_TEMPLATE_CACHE_MAX = 2048;
|
|
601
|
+
function cacheCompiledTemplate(key, template) {
|
|
602
|
+
if (compiledTemplateCache.size >= COMPILED_TEMPLATE_CACHE_MAX) {
|
|
603
|
+
const first = compiledTemplateCache.keys().next().value;
|
|
604
|
+
if (first !== void 0) compiledTemplateCache.delete(first);
|
|
605
|
+
}
|
|
606
|
+
compiledTemplateCache.set(key, template);
|
|
607
|
+
}
|
|
608
|
+
var UNSAFE_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
609
|
+
"constructor",
|
|
610
|
+
"__proto__",
|
|
611
|
+
"prototype",
|
|
612
|
+
"__defineGetter__",
|
|
613
|
+
"__defineSetter__",
|
|
614
|
+
"__lookupGetter__",
|
|
615
|
+
"__lookupSetter__",
|
|
616
|
+
"eval",
|
|
617
|
+
"Function"
|
|
618
|
+
]);
|
|
619
|
+
var BLOCKED_METHOD_REGEXES = (() => {
|
|
620
|
+
const result = [];
|
|
621
|
+
for (const name of UNSAFE_METHOD_NAMES) {
|
|
622
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
623
|
+
result.push({
|
|
624
|
+
name,
|
|
625
|
+
// Match as property access (.name) or bare identifier at start
|
|
626
|
+
dotRe: new RegExp(`(?:^|\\.)${escaped}(?:\\s*\\(|\\s*$|[^\\w$])`, "m"),
|
|
627
|
+
// Match bracket access with single quotes, double quotes, or backticks
|
|
628
|
+
bracketRe: new RegExp(`\\[\\s*(?:'${escaped}'|"${escaped}"|\`` + escaped + `\`)\\s*\\]`)
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
632
|
+
})();
|
|
633
|
+
function findBlockedMethod(expr) {
|
|
634
|
+
let cleaned = expr.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
635
|
+
cleaned = cleaned.replace(/\/\/[^\n]*/g, "");
|
|
636
|
+
cleaned = cleaned.replace(/\s*\.\s*/g, ".");
|
|
637
|
+
for (const { name, dotRe, bracketRe } of BLOCKED_METHOD_REGEXES) {
|
|
638
|
+
if (dotRe.test(cleaned)) return name;
|
|
639
|
+
if (bracketRe.test(cleaned)) return name;
|
|
640
|
+
}
|
|
641
|
+
if (cleaned.includes("[")) {
|
|
642
|
+
const bracketContents = extractBracketContents(cleaned);
|
|
643
|
+
for (const content of bracketContents) {
|
|
644
|
+
if (!content.includes("+")) continue;
|
|
645
|
+
const fragments = content.match(/['"`]([^'"`]*?)['"`]/g);
|
|
646
|
+
if (!fragments) continue;
|
|
647
|
+
const joined = fragments.map((f) => f.slice(1, -1)).join("");
|
|
648
|
+
if (UNSAFE_METHOD_NAMES.has(joined)) return joined;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
function extractBracketContents(expr) {
|
|
654
|
+
const results = [];
|
|
655
|
+
let depth = 0;
|
|
656
|
+
let start = -1;
|
|
657
|
+
for (let i = 0; i < expr.length; i++) {
|
|
658
|
+
if (expr[i] === "[") {
|
|
659
|
+
if (depth === 0) start = i + 1;
|
|
660
|
+
depth++;
|
|
661
|
+
} else if (expr[i] === "]") {
|
|
662
|
+
depth--;
|
|
663
|
+
if (depth === 0 && start >= 0) {
|
|
664
|
+
results.push(expr.slice(start, i));
|
|
665
|
+
start = -1;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return results;
|
|
670
|
+
}
|
|
602
671
|
var TEXT_BINDING_SYM = /* @__PURE__ */ Symbol.for("forma-text-binding-cache");
|
|
603
672
|
function toTextValue(value) {
|
|
604
673
|
if (value == null) return "";
|
|
@@ -873,6 +942,7 @@ function consumeStatement(raw) {
|
|
|
873
942
|
function parseIfHandler(expr, scope) {
|
|
874
943
|
const input = expr.trim();
|
|
875
944
|
if (!RE_IF_PREFIX.test(input)) return null;
|
|
945
|
+
if (RE_EVENT_REF.test(input)) return null;
|
|
876
946
|
let idx = 2;
|
|
877
947
|
while (idx < input.length && /\s/.test(input[idx])) idx++;
|
|
878
948
|
if (input[idx] !== "(") return null;
|
|
@@ -897,7 +967,7 @@ function parseIfHandler(expr, scope) {
|
|
|
897
967
|
}
|
|
898
968
|
if (rest.length > 0) return null;
|
|
899
969
|
return (e) => {
|
|
900
|
-
|
|
970
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
901
971
|
if (condExpr()) thenHandler(e);
|
|
902
972
|
else elseHandler?.(e);
|
|
903
973
|
});
|
|
@@ -933,7 +1003,7 @@ function compileTemplate(text) {
|
|
|
933
1003
|
dynamics,
|
|
934
1004
|
hasItemRef: dynamics.length > 0
|
|
935
1005
|
};
|
|
936
|
-
|
|
1006
|
+
cacheCompiledTemplate(text, result);
|
|
937
1007
|
return result;
|
|
938
1008
|
}
|
|
939
1009
|
var templateTexts = /* @__PURE__ */ new WeakMap();
|
|
@@ -1274,6 +1344,68 @@ function cloneAttributeTemplates(el, item) {
|
|
|
1274
1344
|
}
|
|
1275
1345
|
}
|
|
1276
1346
|
}
|
|
1347
|
+
function parseChainedAccess(expr, scope) {
|
|
1348
|
+
let pos = 0;
|
|
1349
|
+
const identMatch = expr.match(/^[a-zA-Z_$]\w*/);
|
|
1350
|
+
if (!identMatch) return null;
|
|
1351
|
+
const rootName = identMatch[0];
|
|
1352
|
+
pos = rootName.length;
|
|
1353
|
+
if (pos >= expr.length) return null;
|
|
1354
|
+
if (expr[pos] !== "." && !(expr[pos] === "?" && expr[pos + 1] === ".")) return null;
|
|
1355
|
+
const steps = [];
|
|
1356
|
+
while (pos < expr.length) {
|
|
1357
|
+
let optional = false;
|
|
1358
|
+
if (expr[pos] === "?" && expr[pos + 1] === ".") {
|
|
1359
|
+
optional = true;
|
|
1360
|
+
pos += 2;
|
|
1361
|
+
} else if (expr[pos] === ".") {
|
|
1362
|
+
pos += 1;
|
|
1363
|
+
} else {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
const nameMatch = expr.slice(pos).match(/^\w+/);
|
|
1367
|
+
if (!nameMatch) return null;
|
|
1368
|
+
const name = nameMatch[0];
|
|
1369
|
+
pos += name.length;
|
|
1370
|
+
if (UNSAFE_METHOD_NAMES.has(name)) return () => void 0;
|
|
1371
|
+
if (pos < expr.length && expr[pos] === "(") {
|
|
1372
|
+
const balanced = readBalancedSegment(expr, pos, "(", ")");
|
|
1373
|
+
if (!balanced) return null;
|
|
1374
|
+
const argsRaw = balanced.inner.trim();
|
|
1375
|
+
const argFns = [];
|
|
1376
|
+
for (const arg of splitCallArgs(argsRaw)) {
|
|
1377
|
+
const parsed = parseExpression(arg, scope);
|
|
1378
|
+
if (!parsed) return null;
|
|
1379
|
+
argFns.push(parsed);
|
|
1380
|
+
}
|
|
1381
|
+
steps.push({ type: "call", name, optional, argFns });
|
|
1382
|
+
pos = balanced.end + 1;
|
|
1383
|
+
} else {
|
|
1384
|
+
steps.push({ type: "prop", name, optional });
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (pos !== expr.length) return null;
|
|
1388
|
+
if (steps.length === 0) return null;
|
|
1389
|
+
const rootExpr = rootName === "Math" ? (() => Math) : (() => scope.getters[rootName]?.());
|
|
1390
|
+
return () => {
|
|
1391
|
+
let val = rootExpr();
|
|
1392
|
+
for (const step of steps) {
|
|
1393
|
+
if (val == null) {
|
|
1394
|
+
if (step.optional) return void 0;
|
|
1395
|
+
return void 0;
|
|
1396
|
+
}
|
|
1397
|
+
if (step.type === "prop") {
|
|
1398
|
+
val = val[step.name];
|
|
1399
|
+
} else {
|
|
1400
|
+
const method = val[step.name];
|
|
1401
|
+
if (typeof method !== "function") return void 0;
|
|
1402
|
+
const args = step.argFns.map((fn) => fn());
|
|
1403
|
+
val = method.apply(val, args);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return val;
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1277
1409
|
function parseExpression(expr, scope) {
|
|
1278
1410
|
const cachedFactory = expressionCache.get(expr);
|
|
1279
1411
|
if (cachedFactory) return cachedFactory(scope);
|
|
@@ -1324,24 +1456,9 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1324
1456
|
if (RE_IDENTIFIER.test(expr)) {
|
|
1325
1457
|
return () => scope.getters[expr]?.();
|
|
1326
1458
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
return () => {
|
|
1331
|
-
const obj = scope.getters[p1]?.();
|
|
1332
|
-
return obj?.[p2];
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
|
-
const deepDotMatch = expr.match(RE_DEEP_DOT);
|
|
1336
|
-
if (deepDotMatch) {
|
|
1337
|
-
const p1 = deepDotMatch[1], p2 = deepDotMatch[2], p3 = deepDotMatch[3], p4 = deepDotMatch[4];
|
|
1338
|
-
return () => {
|
|
1339
|
-
let val = scope.getters[p1]?.();
|
|
1340
|
-
val = val?.[p2];
|
|
1341
|
-
val = val?.[p3];
|
|
1342
|
-
if (p4) val = val?.[p4];
|
|
1343
|
-
return val;
|
|
1344
|
-
};
|
|
1459
|
+
{
|
|
1460
|
+
const chainResult = parseChainedAccess(expr, scope);
|
|
1461
|
+
if (chainResult) return chainResult;
|
|
1345
1462
|
}
|
|
1346
1463
|
const groupedCallMatch = expr.match(RE_GROUP_METHOD_CALL);
|
|
1347
1464
|
if (groupedCallMatch) {
|
|
@@ -1365,28 +1482,6 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1365
1482
|
return method.apply(base, args);
|
|
1366
1483
|
};
|
|
1367
1484
|
}
|
|
1368
|
-
const callMatch = expr.match(RE_METHOD_CALL);
|
|
1369
|
-
if (callMatch) {
|
|
1370
|
-
const baseName = callMatch[1];
|
|
1371
|
-
const methodName = callMatch[2];
|
|
1372
|
-
const argsRaw = callMatch[3].trim();
|
|
1373
|
-
if (UNSAFE_METHOD_NAMES.has(methodName)) return () => void 0;
|
|
1374
|
-
const baseExpr = baseName === "Math" ? (() => Math) : parseExpression(baseName, scope);
|
|
1375
|
-
if (!baseExpr) return null;
|
|
1376
|
-
const argFns = [];
|
|
1377
|
-
for (const arg of splitCallArgs(argsRaw)) {
|
|
1378
|
-
const parsed = parseExpression(arg, scope);
|
|
1379
|
-
if (!parsed) return null;
|
|
1380
|
-
argFns.push(parsed);
|
|
1381
|
-
}
|
|
1382
|
-
return () => {
|
|
1383
|
-
const base = baseExpr();
|
|
1384
|
-
const method = base?.[methodName];
|
|
1385
|
-
if (typeof method !== "function") return void 0;
|
|
1386
|
-
const args = argFns.map((fn) => fn());
|
|
1387
|
-
return method.apply(base, args);
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
1485
|
if (expr.startsWith("!")) {
|
|
1391
1486
|
const inner = parseExpression(expr.slice(1).trim(), scope);
|
|
1392
1487
|
if (inner) return () => !inner();
|
|
@@ -1405,6 +1500,29 @@ function parseExpressionUncached(expr, scope) {
|
|
|
1405
1500
|
return () => objExpr()?.[key];
|
|
1406
1501
|
}
|
|
1407
1502
|
}
|
|
1503
|
+
if (expr.startsWith("[")) {
|
|
1504
|
+
const balanced = readBalancedSegment(expr, 0, "[", "]");
|
|
1505
|
+
if (balanced && balanced.end === expr.length - 1) {
|
|
1506
|
+
const inner = balanced.inner.trim();
|
|
1507
|
+
if (inner === "") {
|
|
1508
|
+
return () => [];
|
|
1509
|
+
}
|
|
1510
|
+
const elements = splitCallArgs(inner);
|
|
1511
|
+
const elementFns = [];
|
|
1512
|
+
let allParsed = true;
|
|
1513
|
+
for (const el of elements) {
|
|
1514
|
+
const parsed = parseExpression(el.trim(), scope);
|
|
1515
|
+
if (!parsed) {
|
|
1516
|
+
allParsed = false;
|
|
1517
|
+
break;
|
|
1518
|
+
}
|
|
1519
|
+
elementFns.push(parsed);
|
|
1520
|
+
}
|
|
1521
|
+
if (allParsed) {
|
|
1522
|
+
return () => elementFns.map((fn) => fn());
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1408
1526
|
const ternaryMatch = expr.match(RE_TERNARY);
|
|
1409
1527
|
if (ternaryMatch) {
|
|
1410
1528
|
const cond = parseExpression(ternaryMatch[1].trim(), scope);
|
|
@@ -1534,6 +1652,15 @@ function getScopeCache(cache, scope) {
|
|
|
1534
1652
|
}
|
|
1535
1653
|
return scoped;
|
|
1536
1654
|
}
|
|
1655
|
+
function cspExpressionHint(expr) {
|
|
1656
|
+
if (expr.includes("...")) {
|
|
1657
|
+
return `Unsupported expression in CSP-safe mode: spread syntax detected. Use .concat() instead, or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1658
|
+
}
|
|
1659
|
+
if (expr.includes("=>")) {
|
|
1660
|
+
return `Unsupported expression in CSP-safe mode: arrow function detected. Extract logic to a data-computed attribute, or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1661
|
+
}
|
|
1662
|
+
return `Unsupported expression in CSP-safe mode. Simplify the expression or enable unsafe-eval via setUnsafeEval(true).`;
|
|
1663
|
+
}
|
|
1537
1664
|
function buildEvaluator(expr, scope) {
|
|
1538
1665
|
const cleaned = expr.replace(RE_STRIP_BRACES, "").trim();
|
|
1539
1666
|
const cache = getScopeCache(scopeExpressionCache, scope);
|
|
@@ -1546,11 +1673,17 @@ function buildEvaluator(expr, scope) {
|
|
|
1546
1673
|
}
|
|
1547
1674
|
if (!_allowUnsafeEval) {
|
|
1548
1675
|
dbg("buildEvaluator: blocked unsafe eval fallback for expression:", cleaned);
|
|
1549
|
-
reportDiagnostic("expression-unsupported", cleaned,
|
|
1676
|
+
reportDiagnostic("expression-unsupported", cleaned, cspExpressionHint(cleaned));
|
|
1550
1677
|
const blocked = () => void 0;
|
|
1551
1678
|
cache.set(cleaned, blocked);
|
|
1552
1679
|
return blocked;
|
|
1553
1680
|
}
|
|
1681
|
+
const blockedMethod = findBlockedMethod(cleaned);
|
|
1682
|
+
if (blockedMethod) {
|
|
1683
|
+
const msg = `Blocked unsafe method "${blockedMethod}" in expression`;
|
|
1684
|
+
reportDiagnostic("expression-unsupported", cleaned, msg);
|
|
1685
|
+
throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
|
|
1686
|
+
}
|
|
1554
1687
|
try {
|
|
1555
1688
|
const fn = new Function("__scope", `with(__scope) { return (${cleaned}); }`);
|
|
1556
1689
|
const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
@@ -1558,6 +1691,7 @@ function buildEvaluator(expr, scope) {
|
|
|
1558
1691
|
return key in scope.getters;
|
|
1559
1692
|
},
|
|
1560
1693
|
get(_, key) {
|
|
1694
|
+
if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
|
|
1561
1695
|
const g = scope.getters[key];
|
|
1562
1696
|
return g ? g() : void 0;
|
|
1563
1697
|
}
|
|
@@ -1566,7 +1700,7 @@ function buildEvaluator(expr, scope) {
|
|
|
1566
1700
|
cache.set(cleaned, unsafe);
|
|
1567
1701
|
return unsafe;
|
|
1568
1702
|
} catch {
|
|
1569
|
-
reportDiagnostic("expression-unsupported", cleaned, "Expression
|
|
1703
|
+
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.");
|
|
1570
1704
|
const failed = () => void 0;
|
|
1571
1705
|
cache.set(cleaned, failed);
|
|
1572
1706
|
return failed;
|
|
@@ -1582,7 +1716,7 @@ function parseHandler(expr, scope) {
|
|
|
1582
1716
|
const handlers = stmts.map((s) => parseHandler(s, scope));
|
|
1583
1717
|
if (handlers.every((h) => h !== null)) {
|
|
1584
1718
|
return (e) => {
|
|
1585
|
-
|
|
1719
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
1586
1720
|
for (const h of handlers) h(e);
|
|
1587
1721
|
});
|
|
1588
1722
|
};
|
|
@@ -1595,7 +1729,7 @@ function parseHandler(expr, scope) {
|
|
|
1595
1729
|
const name = incrMatch[1];
|
|
1596
1730
|
const op = incrMatch[2];
|
|
1597
1731
|
return () => {
|
|
1598
|
-
|
|
1732
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
1599
1733
|
const val = scope.getters[name]?.() ?? 0;
|
|
1600
1734
|
scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
|
|
1601
1735
|
});
|
|
@@ -1606,7 +1740,7 @@ function parseHandler(expr, scope) {
|
|
|
1606
1740
|
const op = preIncrMatch[1];
|
|
1607
1741
|
const name = preIncrMatch[2];
|
|
1608
1742
|
return () => {
|
|
1609
|
-
|
|
1743
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
1610
1744
|
const val = scope.getters[name]?.() ?? 0;
|
|
1611
1745
|
scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
|
|
1612
1746
|
});
|
|
@@ -1616,7 +1750,7 @@ function parseHandler(expr, scope) {
|
|
|
1616
1750
|
if (toggleMatch && toggleMatch[1] === toggleMatch[2]) {
|
|
1617
1751
|
const name = toggleMatch[1];
|
|
1618
1752
|
return () => {
|
|
1619
|
-
|
|
1753
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
1620
1754
|
scope.setters[name]?.(!scope.getters[name]?.());
|
|
1621
1755
|
});
|
|
1622
1756
|
};
|
|
@@ -1628,7 +1762,7 @@ function parseHandler(expr, scope) {
|
|
|
1628
1762
|
if (valExpr) {
|
|
1629
1763
|
if (_debug) dbg(`parseHandler: assignment "${name} = ..." \u2014 setter exists:`, !!scope.setters[name], ", getter exists:", !!scope.getters[name]);
|
|
1630
1764
|
return () => {
|
|
1631
|
-
|
|
1765
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
1632
1766
|
const val = valExpr();
|
|
1633
1767
|
if (_debug) dbg(`SETTER: ${name} = ${val} (was: ${scope.getters[name]?.()})`);
|
|
1634
1768
|
scope.setters[name]?.(val);
|
|
@@ -1643,7 +1777,7 @@ function parseHandler(expr, scope) {
|
|
|
1643
1777
|
const valExpr = parseExpression(compoundMatch[3].trim(), scope);
|
|
1644
1778
|
if (valExpr) {
|
|
1645
1779
|
return () => {
|
|
1646
|
-
|
|
1780
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
1647
1781
|
const current = scope.getters[name]?.() ?? 0;
|
|
1648
1782
|
const val = valExpr();
|
|
1649
1783
|
switch (op) {
|
|
@@ -1672,7 +1806,13 @@ function parseHandler(expr, scope) {
|
|
|
1672
1806
|
return null;
|
|
1673
1807
|
}
|
|
1674
1808
|
function buildHandler(expr, scope) {
|
|
1675
|
-
|
|
1809
|
+
let cleaned = expr.trim();
|
|
1810
|
+
if (cleaned.startsWith("{")) {
|
|
1811
|
+
const seg = readBalancedSegment(cleaned, 0, "{", "}");
|
|
1812
|
+
if (seg && seg.end === cleaned.length - 1) {
|
|
1813
|
+
cleaned = seg.inner.trim();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1676
1816
|
const cache = getScopeCache(scopeHandlerCache, scope);
|
|
1677
1817
|
const cached = cache.get(cleaned);
|
|
1678
1818
|
if (cached) return cached;
|
|
@@ -1684,7 +1824,7 @@ function buildHandler(expr, scope) {
|
|
|
1684
1824
|
}
|
|
1685
1825
|
if (!_allowUnsafeEval) {
|
|
1686
1826
|
dbg("buildHandler: blocked unsafe eval fallback for expression:", cleaned);
|
|
1687
|
-
reportDiagnostic("handler-unsupported", cleaned,
|
|
1827
|
+
reportDiagnostic("handler-unsupported", cleaned, cspExpressionHint(cleaned));
|
|
1688
1828
|
const result = {
|
|
1689
1829
|
handler: () => {
|
|
1690
1830
|
},
|
|
@@ -1693,6 +1833,12 @@ function buildHandler(expr, scope) {
|
|
|
1693
1833
|
cache.set(cleaned, result);
|
|
1694
1834
|
return result;
|
|
1695
1835
|
}
|
|
1836
|
+
const blockedMethod = findBlockedMethod(cleaned);
|
|
1837
|
+
if (blockedMethod) {
|
|
1838
|
+
const msg = `Blocked unsafe method "${blockedMethod}" in handler`;
|
|
1839
|
+
reportDiagnostic("handler-unsupported", cleaned, msg);
|
|
1840
|
+
throw new Error(`[FormaJS] ${msg}: ${cleaned}`);
|
|
1841
|
+
}
|
|
1696
1842
|
try {
|
|
1697
1843
|
const fn = new Function("__scope", "$event", "event", `with(__scope) { ${cleaned} }`);
|
|
1698
1844
|
const proxy = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
@@ -1701,6 +1847,7 @@ function buildHandler(expr, scope) {
|
|
|
1701
1847
|
return key in scope.getters || key in scope.setters;
|
|
1702
1848
|
},
|
|
1703
1849
|
get(_, key) {
|
|
1850
|
+
if (UNSAFE_METHOD_NAMES.has(key)) return void 0;
|
|
1704
1851
|
const g = scope.getters[key];
|
|
1705
1852
|
return g ? g() : void 0;
|
|
1706
1853
|
},
|
|
@@ -1711,7 +1858,7 @@ function buildHandler(expr, scope) {
|
|
|
1711
1858
|
}
|
|
1712
1859
|
});
|
|
1713
1860
|
const unsafeHandler = (e) => {
|
|
1714
|
-
|
|
1861
|
+
chunkYSKF3VRA_cjs.batch(() => fn(proxy, e, e));
|
|
1715
1862
|
};
|
|
1716
1863
|
const result = {
|
|
1717
1864
|
handler: unsafeHandler,
|
|
@@ -1720,7 +1867,7 @@ function buildHandler(expr, scope) {
|
|
|
1720
1867
|
cache.set(cleaned, result);
|
|
1721
1868
|
return result;
|
|
1722
1869
|
} catch {
|
|
1723
|
-
reportDiagnostic("handler-unsupported", cleaned, "
|
|
1870
|
+
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.");
|
|
1724
1871
|
const result = {
|
|
1725
1872
|
handler: () => {
|
|
1726
1873
|
},
|
|
@@ -1730,16 +1877,21 @@ function buildHandler(expr, scope) {
|
|
|
1730
1877
|
return result;
|
|
1731
1878
|
}
|
|
1732
1879
|
}
|
|
1880
|
+
var FORBIDDEN_STATE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1733
1881
|
function parseState(raw) {
|
|
1882
|
+
let parsed;
|
|
1734
1883
|
try {
|
|
1735
|
-
|
|
1884
|
+
parsed = JSON.parse(raw);
|
|
1736
1885
|
} catch {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
} catch {
|
|
1740
|
-
return {};
|
|
1886
|
+
if (_debug) {
|
|
1887
|
+
dbg("parseState: Invalid JSON in data-forma-state \u2014 use valid JSON with quoted keys. Got:", raw.slice(0, 200));
|
|
1741
1888
|
}
|
|
1889
|
+
return {};
|
|
1890
|
+
}
|
|
1891
|
+
for (const key of FORBIDDEN_STATE_KEYS) {
|
|
1892
|
+
if (key in parsed) delete parsed[key];
|
|
1742
1893
|
}
|
|
1894
|
+
return parsed;
|
|
1743
1895
|
}
|
|
1744
1896
|
function initScope(stateEl) {
|
|
1745
1897
|
const raw = stateEl.getAttribute("data-forma-state") ?? "{}";
|
|
@@ -1754,7 +1906,7 @@ function initScope(stateEl) {
|
|
|
1754
1906
|
const getters = {};
|
|
1755
1907
|
const setters = {};
|
|
1756
1908
|
for (const [key, initial] of Object.entries(state)) {
|
|
1757
|
-
const [get, set] =
|
|
1909
|
+
const [get, set] = chunkKH3F5NRU_cjs.createSignal(initial);
|
|
1758
1910
|
getters[key] = get;
|
|
1759
1911
|
setters[key] = set;
|
|
1760
1912
|
}
|
|
@@ -1762,6 +1914,18 @@ function initScope(stateEl) {
|
|
|
1762
1914
|
return { getters, setters };
|
|
1763
1915
|
}
|
|
1764
1916
|
function bindElement(el, scope, disposers) {
|
|
1917
|
+
const elMagics = {
|
|
1918
|
+
$el: el,
|
|
1919
|
+
$dispatch: (name, detail) => {
|
|
1920
|
+
el.dispatchEvent(new CustomEvent(name, {
|
|
1921
|
+
bubbles: true,
|
|
1922
|
+
composed: true,
|
|
1923
|
+
// crosses Shadow DOM boundaries (important for <forma-stage>)
|
|
1924
|
+
detail
|
|
1925
|
+
}));
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
scope = createChildScope(scope, elMagics);
|
|
1765
1929
|
const known = getDirectives(el);
|
|
1766
1930
|
const computedAttr = !known || known.has("data-computed") ? el.getAttribute("data-computed") : null;
|
|
1767
1931
|
if (computedAttr) {
|
|
@@ -1776,7 +1940,7 @@ function bindElement(el, scope, disposers) {
|
|
|
1776
1940
|
const prevGetter = scope.getters[name];
|
|
1777
1941
|
delete scope.getters[name];
|
|
1778
1942
|
const evaluate = buildEvaluator(`{${expr}}`, scope);
|
|
1779
|
-
const getter =
|
|
1943
|
+
const getter = chunkKH3F5NRU_cjs.createComputed(evaluate);
|
|
1780
1944
|
scope.getters[name] = getter;
|
|
1781
1945
|
if (!prevGetter) {
|
|
1782
1946
|
delete scope.setters[name];
|
|
@@ -1787,7 +1951,7 @@ function bindElement(el, scope, disposers) {
|
|
|
1787
1951
|
const textExpr = !known || known.has("data-text") ? el.getAttribute("data-text") : null;
|
|
1788
1952
|
if (textExpr) {
|
|
1789
1953
|
const evaluate = buildEvaluator(textExpr, scope);
|
|
1790
|
-
const dispose =
|
|
1954
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
1791
1955
|
setElementTextFast(el, toTextValue(evaluate()));
|
|
1792
1956
|
});
|
|
1793
1957
|
disposers.push(dispose);
|
|
@@ -1802,7 +1966,7 @@ function bindElement(el, scope, disposers) {
|
|
|
1802
1966
|
dbg(`bindElement: data-show="${showExpr}" on <${tag}${cls}>`);
|
|
1803
1967
|
}
|
|
1804
1968
|
let initialized = false;
|
|
1805
|
-
const dispose =
|
|
1969
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
1806
1970
|
const visible = !!evaluate();
|
|
1807
1971
|
if (_debug) dbg(`data-show effect: "${showExpr}" \u2192 ${visible}`);
|
|
1808
1972
|
applyShowVisibility(el, visible, transition, !initialized);
|
|
@@ -1821,7 +1985,7 @@ function bindElement(el, scope, disposers) {
|
|
|
1821
1985
|
const parent = el.parentNode;
|
|
1822
1986
|
let inserted = true;
|
|
1823
1987
|
let initialized = false;
|
|
1824
|
-
const dispose =
|
|
1988
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
1825
1989
|
const show = !!evaluate();
|
|
1826
1990
|
if (show && !inserted) {
|
|
1827
1991
|
clearTransitionState(el);
|
|
@@ -1860,7 +2024,7 @@ function bindElement(el, scope, disposers) {
|
|
|
1860
2024
|
const setter = scope.setters[prop];
|
|
1861
2025
|
if (getter && setter) {
|
|
1862
2026
|
const input = el;
|
|
1863
|
-
const dispose =
|
|
2027
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
1864
2028
|
const val = getter();
|
|
1865
2029
|
if (input.type === "checkbox") {
|
|
1866
2030
|
input.checked = !!val;
|
|
@@ -1924,14 +2088,14 @@ function bindElement(el, scope, disposers) {
|
|
|
1924
2088
|
} else if (name.startsWith("data-class:")) {
|
|
1925
2089
|
const cls = name.slice(11);
|
|
1926
2090
|
const evaluate = buildEvaluator(attr.value, scope);
|
|
1927
|
-
const dispose =
|
|
2091
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
1928
2092
|
el.classList.toggle(cls, !!evaluate());
|
|
1929
2093
|
});
|
|
1930
2094
|
disposers.push(dispose);
|
|
1931
2095
|
} else if (name.startsWith("data-bind:")) {
|
|
1932
2096
|
const attrName = name.slice(10);
|
|
1933
2097
|
const evaluate = buildEvaluator(attr.value, scope);
|
|
1934
|
-
const dispose =
|
|
2098
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
1935
2099
|
const val = evaluate();
|
|
1936
2100
|
if (val == null || val === false) {
|
|
1937
2101
|
el.removeAttribute(attrName);
|
|
@@ -1954,7 +2118,7 @@ function bindElement(el, scope, disposers) {
|
|
|
1954
2118
|
if (saved !== null) setter(JSON.parse(saved));
|
|
1955
2119
|
} catch {
|
|
1956
2120
|
}
|
|
1957
|
-
const dispose =
|
|
2121
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
1958
2122
|
try {
|
|
1959
2123
|
localStorage.setItem(key, JSON.stringify(getter()));
|
|
1960
2124
|
} catch {
|
|
@@ -2041,7 +2205,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2041
2205
|
});
|
|
2042
2206
|
}
|
|
2043
2207
|
} : void 0;
|
|
2044
|
-
const dispose =
|
|
2208
|
+
const dispose = chunkYSKF3VRA_cjs.internalEffect(() => {
|
|
2045
2209
|
const rawItems = evaluate();
|
|
2046
2210
|
if (!Array.isArray(rawItems)) {
|
|
2047
2211
|
for (const n of oldNodes) {
|
|
@@ -2062,7 +2226,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2062
2226
|
}
|
|
2063
2227
|
const prevNodes = new Set(oldNodes);
|
|
2064
2228
|
if (keyProp) {
|
|
2065
|
-
const result =
|
|
2229
|
+
const result = chunkYSKF3VRA_cjs.reconcileList(
|
|
2066
2230
|
el,
|
|
2067
2231
|
oldItems,
|
|
2068
2232
|
rawItems,
|
|
@@ -2092,7 +2256,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2092
2256
|
} else {
|
|
2093
2257
|
const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
|
|
2094
2258
|
const oldWrapped = oldItems;
|
|
2095
|
-
const result =
|
|
2259
|
+
const result = chunkYSKF3VRA_cjs.reconcileList(
|
|
2096
2260
|
el,
|
|
2097
2261
|
oldWrapped,
|
|
2098
2262
|
wrapped,
|
|
@@ -2141,16 +2305,16 @@ function bindElement(el, scope, disposers) {
|
|
|
2141
2305
|
else if (k === "error") errorTarget = v;
|
|
2142
2306
|
else if (k === "poll") interval = parseInt(v ?? "0", 10);
|
|
2143
2307
|
}
|
|
2144
|
-
const [getTarget, setTarget] =
|
|
2308
|
+
const [getTarget, setTarget] = chunkKH3F5NRU_cjs.createSignal(null);
|
|
2145
2309
|
scope.getters[target] = getTarget;
|
|
2146
2310
|
scope.setters[target] = setTarget;
|
|
2147
2311
|
if (loadingTarget) {
|
|
2148
|
-
const [gl, sl] =
|
|
2312
|
+
const [gl, sl] = chunkKH3F5NRU_cjs.createSignal(false);
|
|
2149
2313
|
scope.getters[loadingTarget] = gl;
|
|
2150
2314
|
scope.setters[loadingTarget] = sl;
|
|
2151
2315
|
}
|
|
2152
2316
|
if (errorTarget) {
|
|
2153
|
-
const [ge, se] =
|
|
2317
|
+
const [ge, se] = chunkKH3F5NRU_cjs.createSignal(null);
|
|
2154
2318
|
scope.getters[errorTarget] = ge;
|
|
2155
2319
|
scope.setters[errorTarget] = se;
|
|
2156
2320
|
}
|
|
@@ -2409,6 +2573,7 @@ function setUnsafeEvalMode(mode) {
|
|
|
2409
2573
|
_unsafeEvalMode = mode;
|
|
2410
2574
|
if (mode === "locked-off") _allowUnsafeEval = false;
|
|
2411
2575
|
if (mode === "locked-on") _allowUnsafeEval = true;
|
|
2576
|
+
if (mode === "mutable") _allowUnsafeEval = true;
|
|
2412
2577
|
scopeExpressionCache = /* @__PURE__ */ new WeakMap();
|
|
2413
2578
|
scopeHandlerCache = /* @__PURE__ */ new WeakMap();
|
|
2414
2579
|
}
|
|
@@ -2461,7 +2626,7 @@ function getScopes() {
|
|
|
2461
2626
|
function setScopeValue(element, key, value) {
|
|
2462
2627
|
const scope = element.__formaScope;
|
|
2463
2628
|
if (!scope?.setters[key]) return;
|
|
2464
|
-
|
|
2629
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
2465
2630
|
scope.setters[key](value);
|
|
2466
2631
|
});
|
|
2467
2632
|
}
|
|
@@ -2470,7 +2635,7 @@ function resetScope(element) {
|
|
|
2470
2635
|
const initialJSON = element.__formaInitialState;
|
|
2471
2636
|
if (!scope || !initialJSON) return;
|
|
2472
2637
|
const initial = parseState(initialJSON);
|
|
2473
|
-
|
|
2638
|
+
chunkYSKF3VRA_cjs.batch(() => {
|
|
2474
2639
|
for (const [key, val] of Object.entries(initial)) {
|
|
2475
2640
|
scope.setters[key]?.(val);
|
|
2476
2641
|
}
|
|
@@ -2500,7 +2665,7 @@ function getReconciler() {
|
|
|
2500
2665
|
}
|
|
2501
2666
|
}
|
|
2502
2667
|
},
|
|
2503
|
-
batch:
|
|
2668
|
+
batch: chunkYSKF3VRA_cjs.batch
|
|
2504
2669
|
});
|
|
2505
2670
|
}
|
|
2506
2671
|
return _reconciler;
|