@fictjs/compiler 0.8.0 → 0.9.0
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 +19 -0
- package/dist/index.cjs +346 -52
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +346 -52
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,7 +39,17 @@ createFictPlugin({
|
|
|
39
39
|
|
|
40
40
|
- `dev` (default: `NODE_ENV !== 'production' && NODE_ENV !== 'test'`): enables compiler warnings/diagnostics. Set to `false` to silence warnings.
|
|
41
41
|
- `onWarn`: custom warning handler (only called when `dev` is enabled).
|
|
42
|
+
- `fineGrainedDom` (default: `true`): emits template-first fine-grained DOM operations for supported JSX.
|
|
43
|
+
- `lazyConditional` (default: `true`): enables control-flow lazy lowering for reactive branch returns where supported.
|
|
44
|
+
- `getterCache` (default: `true`): caches repeated getter reads within the same synchronous block.
|
|
45
|
+
- `optimize` (default: `true`): enables optimizer passes.
|
|
46
|
+
- `optimizeLevel` (default: `'safe'`): conservative algebraic optimization level.
|
|
42
47
|
- `inlineDerivedMemos` (default: `true`): allow the compiler to inline single-use derived values. Set to `false` for a “strict memo” mode where user-named derived values keep explicit memo accessors (unless `"use no memo"` disables memoization).
|
|
48
|
+
- `strictReactivity` (default: `false`): treat control-flow fallback diagnostics (`FICT-R003`, `FICT-R006`) as build errors. Useful for CI gates that require deterministic fine-grained reactivity without fallback paths.
|
|
49
|
+
- `strictGuarantee` (default: `true`): fail-closed mode for reactivity guarantees. Non-guaranteed reactivity diagnostics (including control-flow fallback and props fallback classes) are treated as hard errors and cannot be suppressed/downgraded.
|
|
50
|
+
- Opt-out: set `strictGuarantee: false` to compile in non-strict mode.
|
|
51
|
+
- CI override: set `FICT_STRICT_GUARANTEE=1` to force-enable `strictGuarantee` even when options request opt-out.
|
|
52
|
+
- Contract fixtures: see `packages/compiler/test/reactivity-guarantee-contract.test.ts` for the maintained guarantee/fallback/unsupported matrix checks.
|
|
43
53
|
- `emitModuleMetadata`:
|
|
44
54
|
- `true`: always write adjacent `.fict.meta.json` sidecar files next to source files.
|
|
45
55
|
- `false`: never write metadata files.
|
|
@@ -49,3 +59,12 @@ createFictPlugin({
|
|
|
49
59
|
- `reactiveScopes`: function names whose **first callback argument** is treated as a component-like reactive scope.
|
|
50
60
|
- Only **direct calls** are recognized (e.g., `renderHook(() => ...)` or `utils.renderHook(() => ...)`).
|
|
51
61
|
- **Aliases/indirect calls** are not recognized (e.g., `const rh = renderHook; rh(() => ...)`).
|
|
62
|
+
|
|
63
|
+
## Recommended Profiles
|
|
64
|
+
|
|
65
|
+
Use `docs/config-profiles.md` for copy-paste presets:
|
|
66
|
+
|
|
67
|
+
- strict default app profile (`strictGuarantee: true`)
|
|
68
|
+
- CI hard-gate profile
|
|
69
|
+
- migration/benchmark profile (`strictGuarantee: false`)
|
|
70
|
+
- one-shot build profile (`emitModuleMetadata: false`)
|
package/dist/index.cjs
CHANGED
|
@@ -14241,6 +14241,9 @@ var RUNTIME_ALIASES = {
|
|
|
14241
14241
|
hydrateComponent: "hydrateComponent",
|
|
14242
14242
|
registerResume: "__fictRegisterResume"
|
|
14243
14243
|
};
|
|
14244
|
+
var RUNTIME_HELPER_MODULES = {
|
|
14245
|
+
keyedList: "@fictjs/runtime/internal/list"
|
|
14246
|
+
};
|
|
14244
14247
|
var DelegatedEvents = /* @__PURE__ */ new Set([...DelegatedEventNames]);
|
|
14245
14248
|
var SAFE_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
14246
14249
|
// Console methods
|
|
@@ -24066,17 +24069,19 @@ function collectDeclaredNames(body, t4) {
|
|
|
24066
24069
|
function attachHelperImports(ctx, body, t4) {
|
|
24067
24070
|
if (ctx.helpersUsed.size === 0) return body;
|
|
24068
24071
|
const declared = collectDeclaredNames(body, t4);
|
|
24069
|
-
const
|
|
24072
|
+
const specifiersByModule = /* @__PURE__ */ new Map();
|
|
24070
24073
|
for (const name of ctx.helpersUsed) {
|
|
24071
24074
|
const alias = RUNTIME_ALIASES[name];
|
|
24072
24075
|
const helper = RUNTIME_HELPERS[name];
|
|
24073
24076
|
if (alias && helper) {
|
|
24074
24077
|
if (declared.has(alias)) continue;
|
|
24075
|
-
|
|
24078
|
+
const modulePath = RUNTIME_HELPER_MODULES[name] ?? RUNTIME_MODULE;
|
|
24079
|
+
const moduleSpecifiers = specifiersByModule.get(modulePath) ?? [];
|
|
24080
|
+
moduleSpecifiers.push(t4.importSpecifier(t4.identifier(alias), t4.identifier(helper)));
|
|
24081
|
+
specifiersByModule.set(modulePath, moduleSpecifiers);
|
|
24076
24082
|
}
|
|
24077
24083
|
}
|
|
24078
|
-
if (
|
|
24079
|
-
const importDecl = t4.importDeclaration(specifiers, t4.stringLiteral(RUNTIME_MODULE));
|
|
24084
|
+
if (specifiersByModule.size === 0) return body;
|
|
24080
24085
|
const helpers = [];
|
|
24081
24086
|
if (ctx.needsForOfHelper) {
|
|
24082
24087
|
const itemId = t4.identifier("item");
|
|
@@ -24114,7 +24119,15 @@ function attachHelperImports(ctx, body, t4) {
|
|
|
24114
24119
|
)
|
|
24115
24120
|
);
|
|
24116
24121
|
}
|
|
24117
|
-
|
|
24122
|
+
const modulePaths = Array.from(specifiersByModule.keys()).sort((a, b) => {
|
|
24123
|
+
if (a === RUNTIME_MODULE) return -1;
|
|
24124
|
+
if (b === RUNTIME_MODULE) return 1;
|
|
24125
|
+
return a.localeCompare(b);
|
|
24126
|
+
});
|
|
24127
|
+
const importDecls = modulePaths.map(
|
|
24128
|
+
(modulePath) => t4.importDeclaration(specifiersByModule.get(modulePath) ?? [], t4.stringLiteral(modulePath))
|
|
24129
|
+
);
|
|
24130
|
+
return [...importDecls, ...helpers, ...body];
|
|
24118
24131
|
}
|
|
24119
24132
|
|
|
24120
24133
|
// src/ir/codegen-jsx-keys.ts
|
|
@@ -25429,6 +25442,7 @@ function emitResumableEventBinding(targetId, eventName, expr, statements, ctx, c
|
|
|
25429
25442
|
// src/ir/codegen-runtime-imports.ts
|
|
25430
25443
|
var RUNTIME_IMPORT_MODULES = /* @__PURE__ */ new Set([
|
|
25431
25444
|
RUNTIME_MODULE,
|
|
25445
|
+
...Object.values(RUNTIME_HELPER_MODULES),
|
|
25432
25446
|
"@fictjs/runtime",
|
|
25433
25447
|
"@fictjs/runtime/advanced",
|
|
25434
25448
|
"fict",
|
|
@@ -28568,10 +28582,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28568
28582
|
}
|
|
28569
28583
|
return false;
|
|
28570
28584
|
};
|
|
28571
|
-
function hasNodeMatch(nodes, predicate) {
|
|
28585
|
+
function hasNodeMatch(nodes, predicate, options) {
|
|
28572
28586
|
let found = false;
|
|
28573
|
-
const visit = (node) => {
|
|
28587
|
+
const visit = (node, isRoot = false) => {
|
|
28574
28588
|
if (!node || found) return;
|
|
28589
|
+
if (!isRoot && options?.skipNestedFunctions && (t4.isFunctionExpression(node) || t4.isArrowFunctionExpression(node) || t4.isFunctionDeclaration(node) || t4.isObjectMethod(node) || t4.isClassMethod(node))) {
|
|
28590
|
+
return;
|
|
28591
|
+
}
|
|
28575
28592
|
if (predicate(node)) {
|
|
28576
28593
|
found = true;
|
|
28577
28594
|
return;
|
|
@@ -28594,17 +28611,134 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28594
28611
|
}
|
|
28595
28612
|
};
|
|
28596
28613
|
for (const node of nodes) {
|
|
28597
|
-
visit(node);
|
|
28614
|
+
visit(node, true);
|
|
28598
28615
|
if (found) return true;
|
|
28599
28616
|
}
|
|
28600
28617
|
return found;
|
|
28601
28618
|
}
|
|
28602
28619
|
const containsReturnStatement = (nodes) => hasNodeMatch(nodes, (node) => t4.isReturnStatement(node));
|
|
28603
|
-
const
|
|
28604
|
-
|
|
28605
|
-
|
|
28606
|
-
|
|
28607
|
-
|
|
28620
|
+
const getMemberRootIdentifier = (expr) => {
|
|
28621
|
+
let current = expr.object;
|
|
28622
|
+
while (t4.isMemberExpression(current) || t4.isOptionalMemberExpression(current)) {
|
|
28623
|
+
current = current.object;
|
|
28624
|
+
}
|
|
28625
|
+
return t4.isIdentifier(current) ? current : null;
|
|
28626
|
+
};
|
|
28627
|
+
const containsReactiveAccessorRead = (nodes, options) => hasNodeMatch(
|
|
28628
|
+
nodes,
|
|
28629
|
+
(node) => {
|
|
28630
|
+
if (t4.isCallExpression(node) || t4.isOptionalCallExpression(node)) {
|
|
28631
|
+
const callee = node.callee;
|
|
28632
|
+
return t4.isIdentifier(callee) && reactiveAccessorNames.has(callee.name);
|
|
28633
|
+
}
|
|
28634
|
+
if (t4.isMemberExpression(node) || t4.isOptionalMemberExpression(node)) {
|
|
28635
|
+
const root = getMemberRootIdentifier(node);
|
|
28636
|
+
return !!(root && reactiveAccessorNames.has(root.name));
|
|
28637
|
+
}
|
|
28638
|
+
return false;
|
|
28639
|
+
},
|
|
28640
|
+
options
|
|
28641
|
+
);
|
|
28642
|
+
const containsReactiveControlFlowRead = (nodes) => hasNodeMatch(
|
|
28643
|
+
nodes,
|
|
28644
|
+
(node) => {
|
|
28645
|
+
if (t4.isIfStatement(node)) {
|
|
28646
|
+
return containsReactiveAccessorRead([node.test], { skipNestedFunctions: true });
|
|
28647
|
+
}
|
|
28648
|
+
if (t4.isSwitchStatement(node)) {
|
|
28649
|
+
return containsReactiveAccessorRead([node.discriminant], { skipNestedFunctions: true });
|
|
28650
|
+
}
|
|
28651
|
+
if (t4.isConditionalExpression(node)) {
|
|
28652
|
+
return containsReactiveAccessorRead([node.test], { skipNestedFunctions: true });
|
|
28653
|
+
}
|
|
28654
|
+
if (t4.isWhileStatement(node) || t4.isDoWhileStatement(node)) {
|
|
28655
|
+
return containsReactiveAccessorRead([node.test], { skipNestedFunctions: true });
|
|
28656
|
+
}
|
|
28657
|
+
if (t4.isForStatement(node)) {
|
|
28658
|
+
const parts = [];
|
|
28659
|
+
if (node.init) parts.push(node.init);
|
|
28660
|
+
if (node.test) parts.push(node.test);
|
|
28661
|
+
if (node.update) parts.push(node.update);
|
|
28662
|
+
return parts.length > 0 && containsReactiveAccessorRead(parts, { skipNestedFunctions: true });
|
|
28663
|
+
}
|
|
28664
|
+
if (t4.isForOfStatement(node) || t4.isForInStatement(node)) {
|
|
28665
|
+
return containsReactiveAccessorRead([node.right], { skipNestedFunctions: true });
|
|
28666
|
+
}
|
|
28667
|
+
return false;
|
|
28668
|
+
},
|
|
28669
|
+
{ skipNestedFunctions: true }
|
|
28670
|
+
);
|
|
28671
|
+
const hasRiskyBranchControlFlow = (stmts) => {
|
|
28672
|
+
if (stmts.length === 0) return false;
|
|
28673
|
+
return containsReactiveControlFlowRead(stmts);
|
|
28674
|
+
};
|
|
28675
|
+
const isJSXLikeNode = (node) => !!node && (t4.isJSXElement(node) || t4.isJSXFragment(node));
|
|
28676
|
+
const isStoreSourceExpression = (expr) => {
|
|
28677
|
+
if (t4.isIdentifier(expr)) {
|
|
28678
|
+
return !!ctx.storeVars?.has(expr.name);
|
|
28679
|
+
}
|
|
28680
|
+
if (t4.isMemberExpression(expr) || t4.isOptionalMemberExpression(expr)) {
|
|
28681
|
+
const root = getMemberRootIdentifier(expr);
|
|
28682
|
+
return !!(root && ctx.storeVars?.has(root.name));
|
|
28683
|
+
}
|
|
28684
|
+
return false;
|
|
28685
|
+
};
|
|
28686
|
+
const hasRiskyStoreDestructureRead = (stmt) => {
|
|
28687
|
+
if (t4.isVariableDeclaration(stmt)) {
|
|
28688
|
+
for (const decl of stmt.declarations) {
|
|
28689
|
+
if (!decl.init) continue;
|
|
28690
|
+
const hasPattern = t4.isObjectPattern(decl.id) || t4.isArrayPattern(decl.id);
|
|
28691
|
+
if (!hasPattern) continue;
|
|
28692
|
+
if (isStoreSourceExpression(decl.init)) {
|
|
28693
|
+
return true;
|
|
28694
|
+
}
|
|
28695
|
+
}
|
|
28696
|
+
return false;
|
|
28697
|
+
}
|
|
28698
|
+
if (t4.isExpressionStatement(stmt) && t4.isAssignmentExpression(stmt.expression)) {
|
|
28699
|
+
const assignment = stmt.expression;
|
|
28700
|
+
const isPatternLhs = t4.isObjectPattern(assignment.left) || t4.isArrayPattern(assignment.left);
|
|
28701
|
+
if (!isPatternLhs) return false;
|
|
28702
|
+
return isStoreSourceExpression(assignment.right);
|
|
28703
|
+
}
|
|
28704
|
+
return false;
|
|
28705
|
+
};
|
|
28706
|
+
const hasRiskyBranchPreludeReads = (stmts) => {
|
|
28707
|
+
for (const stmt of stmts) {
|
|
28708
|
+
if (hasRiskyStoreDestructureRead(stmt)) {
|
|
28709
|
+
return true;
|
|
28710
|
+
}
|
|
28711
|
+
if (t4.isReturnStatement(stmt)) {
|
|
28712
|
+
const arg = stmt.argument;
|
|
28713
|
+
if (!arg || isJSXLikeNode(arg)) continue;
|
|
28714
|
+
if (containsReactiveAccessorRead([arg], { skipNestedFunctions: true })) {
|
|
28715
|
+
return true;
|
|
28716
|
+
}
|
|
28717
|
+
continue;
|
|
28718
|
+
}
|
|
28719
|
+
if (containsReactiveAccessorRead([stmt], { skipNestedFunctions: true })) {
|
|
28720
|
+
return true;
|
|
28721
|
+
}
|
|
28722
|
+
}
|
|
28723
|
+
return false;
|
|
28724
|
+
};
|
|
28725
|
+
const hasRiskyImmediateInvocationReads = (stmts) => hasNodeMatch(
|
|
28726
|
+
stmts,
|
|
28727
|
+
(node) => {
|
|
28728
|
+
if (!t4.isCallExpression(node) && !t4.isOptionalCallExpression(node)) return false;
|
|
28729
|
+
const callee = node.callee;
|
|
28730
|
+
if (!t4.isFunctionExpression(callee) && !t4.isArrowFunctionExpression(callee)) {
|
|
28731
|
+
return false;
|
|
28732
|
+
}
|
|
28733
|
+
const bodyNodes = t4.isBlockStatement(callee.body) ? callee.body.body : [callee.body];
|
|
28734
|
+
return containsReactiveAccessorRead(bodyNodes, { skipNestedFunctions: true });
|
|
28735
|
+
},
|
|
28736
|
+
{ skipNestedFunctions: true }
|
|
28737
|
+
);
|
|
28738
|
+
const needsTrackedBranchReads = (stmts) => {
|
|
28739
|
+
if (stmts.length === 0) return false;
|
|
28740
|
+
return hasRiskyBranchControlFlow(stmts) || hasRiskyBranchPreludeReads(stmts) || hasRiskyImmediateInvocationReads(stmts);
|
|
28741
|
+
};
|
|
28608
28742
|
const emitControlFlowFallbackWarning = (node, kind) => {
|
|
28609
28743
|
const onWarn = ctx.options?.onWarn;
|
|
28610
28744
|
if (!onWarn) return;
|
|
@@ -28673,7 +28807,7 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28673
28807
|
}
|
|
28674
28808
|
return found;
|
|
28675
28809
|
}
|
|
28676
|
-
function buildConditionalBindingExpr(testExpr, trueFn, falseFn) {
|
|
28810
|
+
function buildConditionalBindingExpr(testExpr, trueFn, falseFn, options) {
|
|
28677
28811
|
ctx.helpersUsed.add("conditional");
|
|
28678
28812
|
ctx.helpersUsed.add("createElement");
|
|
28679
28813
|
ctx.helpersUsed.add("onDestroy");
|
|
@@ -28684,6 +28818,16 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28684
28818
|
t4.identifier(RUNTIME_ALIASES.createElement),
|
|
28685
28819
|
falseFn
|
|
28686
28820
|
];
|
|
28821
|
+
if (options?.trackBranchReads) {
|
|
28822
|
+
const undefinedExpr = t4.unaryExpression("void", t4.numericLiteral(0));
|
|
28823
|
+
args.push(
|
|
28824
|
+
undefinedExpr,
|
|
28825
|
+
t4.cloneNode(undefinedExpr),
|
|
28826
|
+
t4.objectExpression([
|
|
28827
|
+
t4.objectProperty(t4.identifier("trackBranchReads"), t4.booleanLiteral(true))
|
|
28828
|
+
])
|
|
28829
|
+
);
|
|
28830
|
+
}
|
|
28687
28831
|
const bindingCall = t4.callExpression(t4.identifier(RUNTIME_ALIASES.conditional), args);
|
|
28688
28832
|
return t4.callExpression(
|
|
28689
28833
|
t4.arrowFunctionExpression(
|
|
@@ -28717,7 +28861,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28717
28861
|
const trueFn = buildBranchFunction(consequentStmts);
|
|
28718
28862
|
const falseFn = alternateStmts ? buildBranchFunction(alternateStmts) : null;
|
|
28719
28863
|
if (!trueFn || !falseFn) return null;
|
|
28720
|
-
|
|
28864
|
+
const shouldTrackBranchReads = needsTrackedBranchReads(consequentStmts) || (alternateStmts ? needsTrackedBranchReads(alternateStmts) : false);
|
|
28865
|
+
return buildConditionalBindingExpr(
|
|
28866
|
+
ifStmt.test,
|
|
28867
|
+
trueFn,
|
|
28868
|
+
falseFn,
|
|
28869
|
+
shouldTrackBranchReads ? { trackBranchReads: true } : void 0
|
|
28870
|
+
);
|
|
28721
28871
|
}
|
|
28722
28872
|
function isSupportedSwitchDiscriminant(_expr) {
|
|
28723
28873
|
return true;
|
|
@@ -28810,10 +28960,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28810
28960
|
),
|
|
28811
28961
|
[]
|
|
28812
28962
|
);
|
|
28963
|
+
let currentExprNeedsTrackedBranchReads = needsTrackedBranchReads(fallbackStatements);
|
|
28813
28964
|
for (let i = branches.length - 1; i >= 0; i--) {
|
|
28814
28965
|
const branch = branches[i];
|
|
28815
28966
|
const trueFn = buildBranchFunction(branch.statements, { disallowRenderHooks: true });
|
|
28816
28967
|
if (!trueFn) return null;
|
|
28968
|
+
const trueBranchNeedsTrackedBranchReads = needsTrackedBranchReads(branch.statements);
|
|
28969
|
+
const trackBranchReads = trueBranchNeedsTrackedBranchReads || currentExprNeedsTrackedBranchReads;
|
|
28817
28970
|
const falseFn = t4.arrowFunctionExpression(
|
|
28818
28971
|
[],
|
|
28819
28972
|
t4.blockStatement([t4.returnStatement(currentExpr)])
|
|
@@ -28830,7 +28983,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28830
28983
|
(acc, expr) => t4.logicalExpression("||", acc, expr),
|
|
28831
28984
|
comparisons[0]
|
|
28832
28985
|
);
|
|
28833
|
-
currentExpr = buildConditionalBindingExpr(
|
|
28986
|
+
currentExpr = buildConditionalBindingExpr(
|
|
28987
|
+
testExpr,
|
|
28988
|
+
trueFn,
|
|
28989
|
+
falseFn,
|
|
28990
|
+
trackBranchReads ? { trackBranchReads: true } : void 0
|
|
28991
|
+
);
|
|
28992
|
+
currentExprNeedsTrackedBranchReads = trackBranchReads;
|
|
28834
28993
|
}
|
|
28835
28994
|
return t4.callExpression(
|
|
28836
28995
|
t4.arrowFunctionExpression(
|
|
@@ -33179,8 +33338,52 @@ function shouldSuppressWarning(suppressions, code, line) {
|
|
|
33179
33338
|
});
|
|
33180
33339
|
}
|
|
33181
33340
|
var DEFAULT_ERROR_WARNING_CODES = /* @__PURE__ */ new Set(["FICT-R004"]);
|
|
33341
|
+
var STRICT_REACTIVITY_WARNING_CODES = /* @__PURE__ */ new Set(["FICT-R003", "FICT-R006"]);
|
|
33342
|
+
var STRICT_GUARANTEE_WARNING_CODES = /* @__PURE__ */ new Set([
|
|
33343
|
+
"FICT-P001",
|
|
33344
|
+
"FICT-P002",
|
|
33345
|
+
"FICT-P003",
|
|
33346
|
+
"FICT-P004",
|
|
33347
|
+
"FICT-P005",
|
|
33348
|
+
"FICT-J003",
|
|
33349
|
+
"FICT-S002",
|
|
33350
|
+
"FICT-R001",
|
|
33351
|
+
"FICT-R002",
|
|
33352
|
+
"FICT-R003",
|
|
33353
|
+
"FICT-R006"
|
|
33354
|
+
]);
|
|
33355
|
+
function readBooleanEnv(name) {
|
|
33356
|
+
const raw = process.env[name];
|
|
33357
|
+
if (!raw) return void 0;
|
|
33358
|
+
const normalized = raw.trim().toLowerCase();
|
|
33359
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
|
|
33360
|
+
return true;
|
|
33361
|
+
}
|
|
33362
|
+
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
|
|
33363
|
+
return false;
|
|
33364
|
+
}
|
|
33365
|
+
return void 0;
|
|
33366
|
+
}
|
|
33367
|
+
function validateStrictGuaranteeConfig(options, suppressions) {
|
|
33368
|
+
if (!options.strictGuarantee) return;
|
|
33369
|
+
if (suppressions.length > 0) {
|
|
33370
|
+
throw new Error(
|
|
33371
|
+
"strictGuarantee does not allow fict-ignore suppression comments. Remove suppressions to keep fail-closed guarantees."
|
|
33372
|
+
);
|
|
33373
|
+
}
|
|
33374
|
+
if (!options.warningLevels) return;
|
|
33375
|
+
for (const [code, level] of Object.entries(options.warningLevels)) {
|
|
33376
|
+
if (!STRICT_GUARANTEE_WARNING_CODES.has(code)) continue;
|
|
33377
|
+
if (level === "error") continue;
|
|
33378
|
+
throw new Error(
|
|
33379
|
+
`strictGuarantee does not allow downgrading ${code} to "${level}". Remove this warningLevels override.`
|
|
33380
|
+
);
|
|
33381
|
+
}
|
|
33382
|
+
}
|
|
33182
33383
|
function hasErrorEscalation(options) {
|
|
33183
33384
|
if (DEFAULT_ERROR_WARNING_CODES.size > 0) return true;
|
|
33385
|
+
if (options.strictGuarantee) return true;
|
|
33386
|
+
if (options.strictReactivity) return true;
|
|
33184
33387
|
if (options.warningsAsErrors === true) return true;
|
|
33185
33388
|
if (Array.isArray(options.warningsAsErrors) && options.warningsAsErrors.length > 0) return true;
|
|
33186
33389
|
if (options.warningLevels) {
|
|
@@ -33189,8 +33392,10 @@ function hasErrorEscalation(options) {
|
|
|
33189
33392
|
return false;
|
|
33190
33393
|
}
|
|
33191
33394
|
function resolveWarningLevel(code, options) {
|
|
33395
|
+
if (options.strictGuarantee && STRICT_GUARANTEE_WARNING_CODES.has(code)) return "error";
|
|
33192
33396
|
const override = options.warningLevels?.[code];
|
|
33193
33397
|
if (override) return override;
|
|
33398
|
+
if (options.strictReactivity && STRICT_REACTIVITY_WARNING_CODES.has(code)) return "error";
|
|
33194
33399
|
if (options.warningsAsErrors === true) return "error";
|
|
33195
33400
|
if (Array.isArray(options.warningsAsErrors) && options.warningsAsErrors.includes(code)) {
|
|
33196
33401
|
return "error";
|
|
@@ -33204,6 +33409,7 @@ function formatWarningAsError(warning) {
|
|
|
33204
33409
|
at ${location}`;
|
|
33205
33410
|
}
|
|
33206
33411
|
function createWarningDispatcher(onWarn, suppressions, options, dev) {
|
|
33412
|
+
validateStrictGuaranteeConfig(options, suppressions);
|
|
33207
33413
|
const hasEscalation = hasErrorEscalation(options);
|
|
33208
33414
|
if (!dev && !hasEscalation) return () => {
|
|
33209
33415
|
};
|
|
@@ -33309,7 +33515,7 @@ function isDynamicPropertyAccess(node, t4) {
|
|
|
33309
33515
|
if (!node.computed) return false;
|
|
33310
33516
|
return !(t4.isStringLiteral(node.property) || t4.isNumericLiteral(node.property));
|
|
33311
33517
|
}
|
|
33312
|
-
function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, reactiveBindingIds, effectMacroNames, warn, fileName, t4) {
|
|
33518
|
+
function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, reactiveBindingIds, stateMacroNames, memoMacroNames, effectMacroNames, warn, fileName, t4) {
|
|
33313
33519
|
const hasTrackedBinding = (path2, name, tracked) => {
|
|
33314
33520
|
const binding = path2.scope.getBinding(name);
|
|
33315
33521
|
return !!(binding && tracked.has(binding.identifier));
|
|
@@ -33324,6 +33530,107 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33324
33530
|
if (!root) return false;
|
|
33325
33531
|
return hasTrackedBinding(path2, root.name, reactiveBindingIds);
|
|
33326
33532
|
};
|
|
33533
|
+
const NON_ESCAPING_CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
33534
|
+
"map",
|
|
33535
|
+
"forEach",
|
|
33536
|
+
"filter",
|
|
33537
|
+
"some",
|
|
33538
|
+
"every",
|
|
33539
|
+
"find",
|
|
33540
|
+
"findIndex",
|
|
33541
|
+
"findLast",
|
|
33542
|
+
"findLastIndex",
|
|
33543
|
+
"flatMap",
|
|
33544
|
+
"reduce",
|
|
33545
|
+
"reduceRight",
|
|
33546
|
+
"sort",
|
|
33547
|
+
"toSorted",
|
|
33548
|
+
"then",
|
|
33549
|
+
"catch",
|
|
33550
|
+
"finally"
|
|
33551
|
+
]);
|
|
33552
|
+
const capturedClosureByBinding = /* @__PURE__ */ new Map();
|
|
33553
|
+
const shouldIgnoreIdentifierReference = (idPath) => {
|
|
33554
|
+
if (idPath.parentPath.isMemberExpression({ property: idPath.node }) && !idPath.parent.computed) {
|
|
33555
|
+
return true;
|
|
33556
|
+
}
|
|
33557
|
+
if (idPath.parentPath.isObjectProperty({ key: idPath.node }) && !idPath.parent.computed && !idPath.parent.shorthand) {
|
|
33558
|
+
return true;
|
|
33559
|
+
}
|
|
33560
|
+
return false;
|
|
33561
|
+
};
|
|
33562
|
+
const collectCapturedReactiveNames = (fnPath) => {
|
|
33563
|
+
const captured = /* @__PURE__ */ new Set();
|
|
33564
|
+
fnPath.traverse({
|
|
33565
|
+
Function(inner) {
|
|
33566
|
+
if (inner === fnPath) return;
|
|
33567
|
+
inner.skip();
|
|
33568
|
+
},
|
|
33569
|
+
Identifier(idPath) {
|
|
33570
|
+
if (shouldIgnoreIdentifierReference(idPath)) return;
|
|
33571
|
+
const name = idPath.node.name;
|
|
33572
|
+
const binding = idPath.scope.getBinding(name);
|
|
33573
|
+
if (!binding) return;
|
|
33574
|
+
if (!reactiveBindingIds.has(binding.identifier)) return;
|
|
33575
|
+
if (binding.scope === idPath.scope || binding.scope === fnPath.scope) return;
|
|
33576
|
+
captured.add(name);
|
|
33577
|
+
}
|
|
33578
|
+
});
|
|
33579
|
+
return captured;
|
|
33580
|
+
};
|
|
33581
|
+
const registerClosureCaptureBinding = (fnPath, captured) => {
|
|
33582
|
+
if (captured.size === 0) return;
|
|
33583
|
+
if (fnPath.isFunctionDeclaration() && fnPath.node.id) {
|
|
33584
|
+
const binding = fnPath.parentPath.scope.getBinding(fnPath.node.id.name);
|
|
33585
|
+
if (binding) {
|
|
33586
|
+
capturedClosureByBinding.set(binding.identifier, captured);
|
|
33587
|
+
}
|
|
33588
|
+
return;
|
|
33589
|
+
}
|
|
33590
|
+
if ((fnPath.isFunctionExpression() || fnPath.isArrowFunctionExpression()) && fnPath.parentPath.isVariableDeclarator()) {
|
|
33591
|
+
const id = fnPath.parentPath.node.id;
|
|
33592
|
+
if (!t4.isIdentifier(id)) return;
|
|
33593
|
+
const binding = fnPath.parentPath.scope.getBinding(id.name);
|
|
33594
|
+
if (binding) {
|
|
33595
|
+
capturedClosureByBinding.set(binding.identifier, captured);
|
|
33596
|
+
}
|
|
33597
|
+
return;
|
|
33598
|
+
}
|
|
33599
|
+
if (fnPath.parentPath.isAssignmentExpression({ right: fnPath.node })) {
|
|
33600
|
+
const left = fnPath.parentPath.node.left;
|
|
33601
|
+
if (!t4.isIdentifier(left)) return;
|
|
33602
|
+
const binding = fnPath.parentPath.scope.getBinding(left.name);
|
|
33603
|
+
if (binding) {
|
|
33604
|
+
capturedClosureByBinding.set(binding.identifier, captured);
|
|
33605
|
+
}
|
|
33606
|
+
}
|
|
33607
|
+
};
|
|
33608
|
+
const collectCapturedForArgument = (argPath) => {
|
|
33609
|
+
if (argPath.isArrowFunctionExpression() || argPath.isFunctionExpression()) {
|
|
33610
|
+
const captured2 = collectCapturedReactiveNames(argPath);
|
|
33611
|
+
return captured2.size > 0 ? captured2 : null;
|
|
33612
|
+
}
|
|
33613
|
+
if (!argPath.isIdentifier()) return null;
|
|
33614
|
+
const binding = argPath.scope.getBinding(argPath.node.name);
|
|
33615
|
+
if (!binding) return null;
|
|
33616
|
+
const captured = capturedClosureByBinding.get(binding.identifier);
|
|
33617
|
+
return captured && captured.size > 0 ? captured : null;
|
|
33618
|
+
};
|
|
33619
|
+
const isNonEscapingCallbackHost = (callee) => {
|
|
33620
|
+
const member = t4.isMemberExpression(callee) || t4.isOptionalMemberExpression(callee) ? callee : null;
|
|
33621
|
+
if (!member || member.computed || !t4.isIdentifier(member.property)) return false;
|
|
33622
|
+
return NON_ESCAPING_CALLBACK_METHODS.has(member.property.name);
|
|
33623
|
+
};
|
|
33624
|
+
const emitClosureCaptureWarning = (node, captured) => {
|
|
33625
|
+
const names = Array.from(captured).sort().join(", ");
|
|
33626
|
+
emitWarning(
|
|
33627
|
+
node,
|
|
33628
|
+
"FICT-R005",
|
|
33629
|
+
`Function captures reactive variable(s): ${names}. Pass them as parameters or memoize explicitly to avoid hidden dependencies.`,
|
|
33630
|
+
warn,
|
|
33631
|
+
fileName
|
|
33632
|
+
);
|
|
33633
|
+
};
|
|
33327
33634
|
const argumentHasReactive = (argPath) => {
|
|
33328
33635
|
if (argPath.isSpreadElement()) {
|
|
33329
33636
|
const inner = argPath.get("argument");
|
|
@@ -33346,12 +33653,7 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33346
33653
|
path2.skip();
|
|
33347
33654
|
},
|
|
33348
33655
|
Identifier(idPath) {
|
|
33349
|
-
if (
|
|
33350
|
-
return;
|
|
33351
|
-
}
|
|
33352
|
-
if (idPath.parentPath.isObjectProperty({ key: idPath.node }) && !idPath.parent.computed && !idPath.parent.shorthand) {
|
|
33353
|
-
return;
|
|
33354
|
-
}
|
|
33656
|
+
if (shouldIgnoreIdentifierReference(idPath)) return;
|
|
33355
33657
|
const binding = idPath.scope.getBinding(idPath.node.name);
|
|
33356
33658
|
if (binding && reactiveBindingIds.has(binding.identifier)) {
|
|
33357
33659
|
found = true;
|
|
@@ -33432,36 +33734,14 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33432
33734
|
}
|
|
33433
33735
|
},
|
|
33434
33736
|
Function(path2) {
|
|
33435
|
-
const captured =
|
|
33436
|
-
path2
|
|
33437
|
-
{
|
|
33438
|
-
Function(inner) {
|
|
33439
|
-
if (inner === path2) return;
|
|
33440
|
-
inner.skip();
|
|
33441
|
-
},
|
|
33442
|
-
Identifier(idPath) {
|
|
33443
|
-
const name = idPath.node.name;
|
|
33444
|
-
const binding = idPath.scope.getBinding(name);
|
|
33445
|
-
if (!binding) return;
|
|
33446
|
-
if (!reactiveBindingIds.has(binding.identifier)) return;
|
|
33447
|
-
if (binding.scope === idPath.scope || binding.scope === path2.scope) return;
|
|
33448
|
-
captured.add(name);
|
|
33449
|
-
}
|
|
33450
|
-
},
|
|
33451
|
-
{}
|
|
33452
|
-
);
|
|
33453
|
-
if (captured.size > 0) {
|
|
33454
|
-
emitWarning(
|
|
33455
|
-
path2.node,
|
|
33456
|
-
"FICT-R005",
|
|
33457
|
-
`Function captures reactive variable(s): ${Array.from(captured).join(", ")}. Pass them as parameters or memoize explicitly to avoid hidden dependencies.`,
|
|
33458
|
-
warn,
|
|
33459
|
-
fileName
|
|
33460
|
-
);
|
|
33461
|
-
}
|
|
33737
|
+
const captured = collectCapturedReactiveNames(path2);
|
|
33738
|
+
registerClosureCaptureBinding(path2, captured);
|
|
33462
33739
|
},
|
|
33463
33740
|
CallExpression(path2) {
|
|
33464
|
-
const
|
|
33741
|
+
const callNode = path2.node;
|
|
33742
|
+
if (isStateCall(callNode, t4, stateMacroNames)) return;
|
|
33743
|
+
if (isMemoCall(callNode, t4, memoMacroNames)) return;
|
|
33744
|
+
const isEffect = isEffectCall(callNode, t4, effectMacroNames);
|
|
33465
33745
|
if (isEffect) {
|
|
33466
33746
|
const argPath = path2.get("arguments.0");
|
|
33467
33747
|
if (argPath?.isFunctionExpression() || argPath?.isArrowFunctionExpression()) {
|
|
@@ -33484,7 +33764,7 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33484
33764
|
});
|
|
33485
33765
|
if (!hasReactiveDependency) {
|
|
33486
33766
|
emitWarning(
|
|
33487
|
-
|
|
33767
|
+
callNode,
|
|
33488
33768
|
"FICT-E001",
|
|
33489
33769
|
"Effect has no reactive reads; it will run once. Consider removing $effect or adding dependencies.",
|
|
33490
33770
|
warn,
|
|
@@ -33507,6 +33787,7 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33507
33787
|
const isSafe = calleeName && SAFE_FUNCTIONS.has(calleeName);
|
|
33508
33788
|
if (isSafe) return;
|
|
33509
33789
|
const argPaths = path2.get("arguments");
|
|
33790
|
+
const nonEscapingCallbackHost = isNonEscapingCallbackHost(callee);
|
|
33510
33791
|
for (const argPath of argPaths) {
|
|
33511
33792
|
if (argPath.isIdentifier() && hasTrackedBinding(argPath, argPath.node.name, stateBindingIds)) {
|
|
33512
33793
|
continue;
|
|
@@ -33522,6 +33803,13 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33522
33803
|
break;
|
|
33523
33804
|
}
|
|
33524
33805
|
}
|
|
33806
|
+
if (nonEscapingCallbackHost) return;
|
|
33807
|
+
for (const argPath of argPaths) {
|
|
33808
|
+
const captured = collectCapturedForArgument(argPath);
|
|
33809
|
+
if (!captured) continue;
|
|
33810
|
+
emitClosureCaptureWarning(argPath.node, captured);
|
|
33811
|
+
break;
|
|
33812
|
+
}
|
|
33525
33813
|
},
|
|
33526
33814
|
OptionalMemberExpression(path2) {
|
|
33527
33815
|
if (!path2.node.computed) return;
|
|
@@ -34386,6 +34674,8 @@ or extract the nested logic into a custom hook (useXxx).`
|
|
|
34386
34674
|
stateBindingIds,
|
|
34387
34675
|
stateRootBindingIds,
|
|
34388
34676
|
reactiveBindingIds,
|
|
34677
|
+
stateMacroNames,
|
|
34678
|
+
memoMacroNames,
|
|
34389
34679
|
effectMacroNames,
|
|
34390
34680
|
warn,
|
|
34391
34681
|
fileName,
|
|
@@ -34430,13 +34720,17 @@ var createFictPlugin = (0, import_helper_plugin_utils.declare)(
|
|
|
34430
34720
|
(api, options = {}) => {
|
|
34431
34721
|
api.assertVersion(7);
|
|
34432
34722
|
const t4 = api.types;
|
|
34723
|
+
const strictGuaranteeFromEnv = readBooleanEnv("FICT_STRICT_GUARANTEE") === true;
|
|
34433
34724
|
const normalizedOptions = {
|
|
34434
34725
|
...options,
|
|
34435
34726
|
fineGrainedDom: options.fineGrainedDom ?? true,
|
|
34727
|
+
lazyConditional: options.lazyConditional ?? true,
|
|
34728
|
+
getterCache: options.getterCache ?? true,
|
|
34436
34729
|
optimize: options.optimize ?? true,
|
|
34437
34730
|
optimizeLevel: options.optimizeLevel ?? "safe",
|
|
34438
34731
|
inlineDerivedMemos: options.inlineDerivedMemos ?? true,
|
|
34439
34732
|
emitModuleMetadata: options.emitModuleMetadata ?? "auto",
|
|
34733
|
+
strictGuarantee: strictGuaranteeFromEnv || options.strictGuarantee !== false,
|
|
34440
34734
|
dev: options.dev ?? (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test")
|
|
34441
34735
|
};
|
|
34442
34736
|
return {
|
package/dist/index.d.cts
CHANGED
|
@@ -23,9 +23,9 @@ interface FictCompilerOptions {
|
|
|
23
23
|
onWarn?: (warning: CompilerWarning) => void;
|
|
24
24
|
/** Internal: filename of the module being compiled. */
|
|
25
25
|
filename?: string;
|
|
26
|
-
/** Enable lazy evaluation of conditional derived values (Rule J optimization) */
|
|
26
|
+
/** Enable lazy evaluation of conditional derived values (Rule J optimization). Default: true. */
|
|
27
27
|
lazyConditional?: boolean;
|
|
28
|
-
/** Enable getter caching within the same sync block (Rule L optimization) */
|
|
28
|
+
/** Enable getter caching within the same sync block (Rule L optimization). Default: true. */
|
|
29
29
|
getterCache?: boolean;
|
|
30
30
|
/** Emit fine-grained DOM creation/binding code for supported JSX templates */
|
|
31
31
|
fineGrainedDom?: boolean;
|
|
@@ -63,6 +63,19 @@ interface FictCompilerOptions {
|
|
|
63
63
|
* Per-warning override. "off" suppresses, "error" throws, "warn" emits.
|
|
64
64
|
*/
|
|
65
65
|
warningLevels?: Record<string, 'off' | 'warn' | 'error'>;
|
|
66
|
+
/**
|
|
67
|
+
* Strict control-flow reactivity mode.
|
|
68
|
+
* When enabled, control-flow fallback diagnostics (`FICT-R003`, `FICT-R006`)
|
|
69
|
+
* are treated as errors unless explicitly overridden via `warningLevels`.
|
|
70
|
+
*/
|
|
71
|
+
strictReactivity?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Fail-closed reactivity guarantee mode.
|
|
74
|
+
* When enabled, diagnostics that indicate non-guaranteed reactive behavior are
|
|
75
|
+
* treated as hard errors and cannot be suppressed/downgraded.
|
|
76
|
+
* Default: true.
|
|
77
|
+
*/
|
|
78
|
+
strictGuarantee?: boolean;
|
|
66
79
|
/**
|
|
67
80
|
* Optional shared module metadata map for cross-module reactive imports.
|
|
68
81
|
* If omitted, the compiler uses a process-wide cache.
|
package/dist/index.d.ts
CHANGED
|
@@ -23,9 +23,9 @@ interface FictCompilerOptions {
|
|
|
23
23
|
onWarn?: (warning: CompilerWarning) => void;
|
|
24
24
|
/** Internal: filename of the module being compiled. */
|
|
25
25
|
filename?: string;
|
|
26
|
-
/** Enable lazy evaluation of conditional derived values (Rule J optimization) */
|
|
26
|
+
/** Enable lazy evaluation of conditional derived values (Rule J optimization). Default: true. */
|
|
27
27
|
lazyConditional?: boolean;
|
|
28
|
-
/** Enable getter caching within the same sync block (Rule L optimization) */
|
|
28
|
+
/** Enable getter caching within the same sync block (Rule L optimization). Default: true. */
|
|
29
29
|
getterCache?: boolean;
|
|
30
30
|
/** Emit fine-grained DOM creation/binding code for supported JSX templates */
|
|
31
31
|
fineGrainedDom?: boolean;
|
|
@@ -63,6 +63,19 @@ interface FictCompilerOptions {
|
|
|
63
63
|
* Per-warning override. "off" suppresses, "error" throws, "warn" emits.
|
|
64
64
|
*/
|
|
65
65
|
warningLevels?: Record<string, 'off' | 'warn' | 'error'>;
|
|
66
|
+
/**
|
|
67
|
+
* Strict control-flow reactivity mode.
|
|
68
|
+
* When enabled, control-flow fallback diagnostics (`FICT-R003`, `FICT-R006`)
|
|
69
|
+
* are treated as errors unless explicitly overridden via `warningLevels`.
|
|
70
|
+
*/
|
|
71
|
+
strictReactivity?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Fail-closed reactivity guarantee mode.
|
|
74
|
+
* When enabled, diagnostics that indicate non-guaranteed reactive behavior are
|
|
75
|
+
* treated as hard errors and cannot be suppressed/downgraded.
|
|
76
|
+
* Default: true.
|
|
77
|
+
*/
|
|
78
|
+
strictGuarantee?: boolean;
|
|
66
79
|
/**
|
|
67
80
|
* Optional shared module metadata map for cross-module reactive imports.
|
|
68
81
|
* If omitted, the compiler uses a process-wide cache.
|
package/dist/index.js
CHANGED
|
@@ -14226,6 +14226,9 @@ var RUNTIME_ALIASES = {
|
|
|
14226
14226
|
hydrateComponent: "hydrateComponent",
|
|
14227
14227
|
registerResume: "__fictRegisterResume"
|
|
14228
14228
|
};
|
|
14229
|
+
var RUNTIME_HELPER_MODULES = {
|
|
14230
|
+
keyedList: "@fictjs/runtime/internal/list"
|
|
14231
|
+
};
|
|
14229
14232
|
var DelegatedEvents = /* @__PURE__ */ new Set([...DelegatedEventNames]);
|
|
14230
14233
|
var SAFE_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
14231
14234
|
// Console methods
|
|
@@ -24051,17 +24054,19 @@ function collectDeclaredNames(body, t4) {
|
|
|
24051
24054
|
function attachHelperImports(ctx, body, t4) {
|
|
24052
24055
|
if (ctx.helpersUsed.size === 0) return body;
|
|
24053
24056
|
const declared = collectDeclaredNames(body, t4);
|
|
24054
|
-
const
|
|
24057
|
+
const specifiersByModule = /* @__PURE__ */ new Map();
|
|
24055
24058
|
for (const name of ctx.helpersUsed) {
|
|
24056
24059
|
const alias = RUNTIME_ALIASES[name];
|
|
24057
24060
|
const helper = RUNTIME_HELPERS[name];
|
|
24058
24061
|
if (alias && helper) {
|
|
24059
24062
|
if (declared.has(alias)) continue;
|
|
24060
|
-
|
|
24063
|
+
const modulePath = RUNTIME_HELPER_MODULES[name] ?? RUNTIME_MODULE;
|
|
24064
|
+
const moduleSpecifiers = specifiersByModule.get(modulePath) ?? [];
|
|
24065
|
+
moduleSpecifiers.push(t4.importSpecifier(t4.identifier(alias), t4.identifier(helper)));
|
|
24066
|
+
specifiersByModule.set(modulePath, moduleSpecifiers);
|
|
24061
24067
|
}
|
|
24062
24068
|
}
|
|
24063
|
-
if (
|
|
24064
|
-
const importDecl = t4.importDeclaration(specifiers, t4.stringLiteral(RUNTIME_MODULE));
|
|
24069
|
+
if (specifiersByModule.size === 0) return body;
|
|
24065
24070
|
const helpers = [];
|
|
24066
24071
|
if (ctx.needsForOfHelper) {
|
|
24067
24072
|
const itemId = t4.identifier("item");
|
|
@@ -24099,7 +24104,15 @@ function attachHelperImports(ctx, body, t4) {
|
|
|
24099
24104
|
)
|
|
24100
24105
|
);
|
|
24101
24106
|
}
|
|
24102
|
-
|
|
24107
|
+
const modulePaths = Array.from(specifiersByModule.keys()).sort((a, b) => {
|
|
24108
|
+
if (a === RUNTIME_MODULE) return -1;
|
|
24109
|
+
if (b === RUNTIME_MODULE) return 1;
|
|
24110
|
+
return a.localeCompare(b);
|
|
24111
|
+
});
|
|
24112
|
+
const importDecls = modulePaths.map(
|
|
24113
|
+
(modulePath) => t4.importDeclaration(specifiersByModule.get(modulePath) ?? [], t4.stringLiteral(modulePath))
|
|
24114
|
+
);
|
|
24115
|
+
return [...importDecls, ...helpers, ...body];
|
|
24103
24116
|
}
|
|
24104
24117
|
|
|
24105
24118
|
// src/ir/codegen-jsx-keys.ts
|
|
@@ -25414,6 +25427,7 @@ function emitResumableEventBinding(targetId, eventName, expr, statements, ctx, c
|
|
|
25414
25427
|
// src/ir/codegen-runtime-imports.ts
|
|
25415
25428
|
var RUNTIME_IMPORT_MODULES = /* @__PURE__ */ new Set([
|
|
25416
25429
|
RUNTIME_MODULE,
|
|
25430
|
+
...Object.values(RUNTIME_HELPER_MODULES),
|
|
25417
25431
|
"@fictjs/runtime",
|
|
25418
25432
|
"@fictjs/runtime/advanced",
|
|
25419
25433
|
"fict",
|
|
@@ -28553,10 +28567,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28553
28567
|
}
|
|
28554
28568
|
return false;
|
|
28555
28569
|
};
|
|
28556
|
-
function hasNodeMatch(nodes, predicate) {
|
|
28570
|
+
function hasNodeMatch(nodes, predicate, options) {
|
|
28557
28571
|
let found = false;
|
|
28558
|
-
const visit = (node) => {
|
|
28572
|
+
const visit = (node, isRoot = false) => {
|
|
28559
28573
|
if (!node || found) return;
|
|
28574
|
+
if (!isRoot && options?.skipNestedFunctions && (t4.isFunctionExpression(node) || t4.isArrowFunctionExpression(node) || t4.isFunctionDeclaration(node) || t4.isObjectMethod(node) || t4.isClassMethod(node))) {
|
|
28575
|
+
return;
|
|
28576
|
+
}
|
|
28560
28577
|
if (predicate(node)) {
|
|
28561
28578
|
found = true;
|
|
28562
28579
|
return;
|
|
@@ -28579,17 +28596,134 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28579
28596
|
}
|
|
28580
28597
|
};
|
|
28581
28598
|
for (const node of nodes) {
|
|
28582
|
-
visit(node);
|
|
28599
|
+
visit(node, true);
|
|
28583
28600
|
if (found) return true;
|
|
28584
28601
|
}
|
|
28585
28602
|
return found;
|
|
28586
28603
|
}
|
|
28587
28604
|
const containsReturnStatement = (nodes) => hasNodeMatch(nodes, (node) => t4.isReturnStatement(node));
|
|
28588
|
-
const
|
|
28589
|
-
|
|
28590
|
-
|
|
28591
|
-
|
|
28592
|
-
|
|
28605
|
+
const getMemberRootIdentifier = (expr) => {
|
|
28606
|
+
let current = expr.object;
|
|
28607
|
+
while (t4.isMemberExpression(current) || t4.isOptionalMemberExpression(current)) {
|
|
28608
|
+
current = current.object;
|
|
28609
|
+
}
|
|
28610
|
+
return t4.isIdentifier(current) ? current : null;
|
|
28611
|
+
};
|
|
28612
|
+
const containsReactiveAccessorRead = (nodes, options) => hasNodeMatch(
|
|
28613
|
+
nodes,
|
|
28614
|
+
(node) => {
|
|
28615
|
+
if (t4.isCallExpression(node) || t4.isOptionalCallExpression(node)) {
|
|
28616
|
+
const callee = node.callee;
|
|
28617
|
+
return t4.isIdentifier(callee) && reactiveAccessorNames.has(callee.name);
|
|
28618
|
+
}
|
|
28619
|
+
if (t4.isMemberExpression(node) || t4.isOptionalMemberExpression(node)) {
|
|
28620
|
+
const root = getMemberRootIdentifier(node);
|
|
28621
|
+
return !!(root && reactiveAccessorNames.has(root.name));
|
|
28622
|
+
}
|
|
28623
|
+
return false;
|
|
28624
|
+
},
|
|
28625
|
+
options
|
|
28626
|
+
);
|
|
28627
|
+
const containsReactiveControlFlowRead = (nodes) => hasNodeMatch(
|
|
28628
|
+
nodes,
|
|
28629
|
+
(node) => {
|
|
28630
|
+
if (t4.isIfStatement(node)) {
|
|
28631
|
+
return containsReactiveAccessorRead([node.test], { skipNestedFunctions: true });
|
|
28632
|
+
}
|
|
28633
|
+
if (t4.isSwitchStatement(node)) {
|
|
28634
|
+
return containsReactiveAccessorRead([node.discriminant], { skipNestedFunctions: true });
|
|
28635
|
+
}
|
|
28636
|
+
if (t4.isConditionalExpression(node)) {
|
|
28637
|
+
return containsReactiveAccessorRead([node.test], { skipNestedFunctions: true });
|
|
28638
|
+
}
|
|
28639
|
+
if (t4.isWhileStatement(node) || t4.isDoWhileStatement(node)) {
|
|
28640
|
+
return containsReactiveAccessorRead([node.test], { skipNestedFunctions: true });
|
|
28641
|
+
}
|
|
28642
|
+
if (t4.isForStatement(node)) {
|
|
28643
|
+
const parts = [];
|
|
28644
|
+
if (node.init) parts.push(node.init);
|
|
28645
|
+
if (node.test) parts.push(node.test);
|
|
28646
|
+
if (node.update) parts.push(node.update);
|
|
28647
|
+
return parts.length > 0 && containsReactiveAccessorRead(parts, { skipNestedFunctions: true });
|
|
28648
|
+
}
|
|
28649
|
+
if (t4.isForOfStatement(node) || t4.isForInStatement(node)) {
|
|
28650
|
+
return containsReactiveAccessorRead([node.right], { skipNestedFunctions: true });
|
|
28651
|
+
}
|
|
28652
|
+
return false;
|
|
28653
|
+
},
|
|
28654
|
+
{ skipNestedFunctions: true }
|
|
28655
|
+
);
|
|
28656
|
+
const hasRiskyBranchControlFlow = (stmts) => {
|
|
28657
|
+
if (stmts.length === 0) return false;
|
|
28658
|
+
return containsReactiveControlFlowRead(stmts);
|
|
28659
|
+
};
|
|
28660
|
+
const isJSXLikeNode = (node) => !!node && (t4.isJSXElement(node) || t4.isJSXFragment(node));
|
|
28661
|
+
const isStoreSourceExpression = (expr) => {
|
|
28662
|
+
if (t4.isIdentifier(expr)) {
|
|
28663
|
+
return !!ctx.storeVars?.has(expr.name);
|
|
28664
|
+
}
|
|
28665
|
+
if (t4.isMemberExpression(expr) || t4.isOptionalMemberExpression(expr)) {
|
|
28666
|
+
const root = getMemberRootIdentifier(expr);
|
|
28667
|
+
return !!(root && ctx.storeVars?.has(root.name));
|
|
28668
|
+
}
|
|
28669
|
+
return false;
|
|
28670
|
+
};
|
|
28671
|
+
const hasRiskyStoreDestructureRead = (stmt) => {
|
|
28672
|
+
if (t4.isVariableDeclaration(stmt)) {
|
|
28673
|
+
for (const decl of stmt.declarations) {
|
|
28674
|
+
if (!decl.init) continue;
|
|
28675
|
+
const hasPattern = t4.isObjectPattern(decl.id) || t4.isArrayPattern(decl.id);
|
|
28676
|
+
if (!hasPattern) continue;
|
|
28677
|
+
if (isStoreSourceExpression(decl.init)) {
|
|
28678
|
+
return true;
|
|
28679
|
+
}
|
|
28680
|
+
}
|
|
28681
|
+
return false;
|
|
28682
|
+
}
|
|
28683
|
+
if (t4.isExpressionStatement(stmt) && t4.isAssignmentExpression(stmt.expression)) {
|
|
28684
|
+
const assignment = stmt.expression;
|
|
28685
|
+
const isPatternLhs = t4.isObjectPattern(assignment.left) || t4.isArrayPattern(assignment.left);
|
|
28686
|
+
if (!isPatternLhs) return false;
|
|
28687
|
+
return isStoreSourceExpression(assignment.right);
|
|
28688
|
+
}
|
|
28689
|
+
return false;
|
|
28690
|
+
};
|
|
28691
|
+
const hasRiskyBranchPreludeReads = (stmts) => {
|
|
28692
|
+
for (const stmt of stmts) {
|
|
28693
|
+
if (hasRiskyStoreDestructureRead(stmt)) {
|
|
28694
|
+
return true;
|
|
28695
|
+
}
|
|
28696
|
+
if (t4.isReturnStatement(stmt)) {
|
|
28697
|
+
const arg = stmt.argument;
|
|
28698
|
+
if (!arg || isJSXLikeNode(arg)) continue;
|
|
28699
|
+
if (containsReactiveAccessorRead([arg], { skipNestedFunctions: true })) {
|
|
28700
|
+
return true;
|
|
28701
|
+
}
|
|
28702
|
+
continue;
|
|
28703
|
+
}
|
|
28704
|
+
if (containsReactiveAccessorRead([stmt], { skipNestedFunctions: true })) {
|
|
28705
|
+
return true;
|
|
28706
|
+
}
|
|
28707
|
+
}
|
|
28708
|
+
return false;
|
|
28709
|
+
};
|
|
28710
|
+
const hasRiskyImmediateInvocationReads = (stmts) => hasNodeMatch(
|
|
28711
|
+
stmts,
|
|
28712
|
+
(node) => {
|
|
28713
|
+
if (!t4.isCallExpression(node) && !t4.isOptionalCallExpression(node)) return false;
|
|
28714
|
+
const callee = node.callee;
|
|
28715
|
+
if (!t4.isFunctionExpression(callee) && !t4.isArrowFunctionExpression(callee)) {
|
|
28716
|
+
return false;
|
|
28717
|
+
}
|
|
28718
|
+
const bodyNodes = t4.isBlockStatement(callee.body) ? callee.body.body : [callee.body];
|
|
28719
|
+
return containsReactiveAccessorRead(bodyNodes, { skipNestedFunctions: true });
|
|
28720
|
+
},
|
|
28721
|
+
{ skipNestedFunctions: true }
|
|
28722
|
+
);
|
|
28723
|
+
const needsTrackedBranchReads = (stmts) => {
|
|
28724
|
+
if (stmts.length === 0) return false;
|
|
28725
|
+
return hasRiskyBranchControlFlow(stmts) || hasRiskyBranchPreludeReads(stmts) || hasRiskyImmediateInvocationReads(stmts);
|
|
28726
|
+
};
|
|
28593
28727
|
const emitControlFlowFallbackWarning = (node, kind) => {
|
|
28594
28728
|
const onWarn = ctx.options?.onWarn;
|
|
28595
28729
|
if (!onWarn) return;
|
|
@@ -28658,7 +28792,7 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28658
28792
|
}
|
|
28659
28793
|
return found;
|
|
28660
28794
|
}
|
|
28661
|
-
function buildConditionalBindingExpr(testExpr, trueFn, falseFn) {
|
|
28795
|
+
function buildConditionalBindingExpr(testExpr, trueFn, falseFn, options) {
|
|
28662
28796
|
ctx.helpersUsed.add("conditional");
|
|
28663
28797
|
ctx.helpersUsed.add("createElement");
|
|
28664
28798
|
ctx.helpersUsed.add("onDestroy");
|
|
@@ -28669,6 +28803,16 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28669
28803
|
t4.identifier(RUNTIME_ALIASES.createElement),
|
|
28670
28804
|
falseFn
|
|
28671
28805
|
];
|
|
28806
|
+
if (options?.trackBranchReads) {
|
|
28807
|
+
const undefinedExpr = t4.unaryExpression("void", t4.numericLiteral(0));
|
|
28808
|
+
args.push(
|
|
28809
|
+
undefinedExpr,
|
|
28810
|
+
t4.cloneNode(undefinedExpr),
|
|
28811
|
+
t4.objectExpression([
|
|
28812
|
+
t4.objectProperty(t4.identifier("trackBranchReads"), t4.booleanLiteral(true))
|
|
28813
|
+
])
|
|
28814
|
+
);
|
|
28815
|
+
}
|
|
28672
28816
|
const bindingCall = t4.callExpression(t4.identifier(RUNTIME_ALIASES.conditional), args);
|
|
28673
28817
|
return t4.callExpression(
|
|
28674
28818
|
t4.arrowFunctionExpression(
|
|
@@ -28702,7 +28846,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28702
28846
|
const trueFn = buildBranchFunction(consequentStmts);
|
|
28703
28847
|
const falseFn = alternateStmts ? buildBranchFunction(alternateStmts) : null;
|
|
28704
28848
|
if (!trueFn || !falseFn) return null;
|
|
28705
|
-
|
|
28849
|
+
const shouldTrackBranchReads = needsTrackedBranchReads(consequentStmts) || (alternateStmts ? needsTrackedBranchReads(alternateStmts) : false);
|
|
28850
|
+
return buildConditionalBindingExpr(
|
|
28851
|
+
ifStmt.test,
|
|
28852
|
+
trueFn,
|
|
28853
|
+
falseFn,
|
|
28854
|
+
shouldTrackBranchReads ? { trackBranchReads: true } : void 0
|
|
28855
|
+
);
|
|
28706
28856
|
}
|
|
28707
28857
|
function isSupportedSwitchDiscriminant(_expr) {
|
|
28708
28858
|
return true;
|
|
@@ -28795,10 +28945,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28795
28945
|
),
|
|
28796
28946
|
[]
|
|
28797
28947
|
);
|
|
28948
|
+
let currentExprNeedsTrackedBranchReads = needsTrackedBranchReads(fallbackStatements);
|
|
28798
28949
|
for (let i = branches.length - 1; i >= 0; i--) {
|
|
28799
28950
|
const branch = branches[i];
|
|
28800
28951
|
const trueFn = buildBranchFunction(branch.statements, { disallowRenderHooks: true });
|
|
28801
28952
|
if (!trueFn) return null;
|
|
28953
|
+
const trueBranchNeedsTrackedBranchReads = needsTrackedBranchReads(branch.statements);
|
|
28954
|
+
const trackBranchReads = trueBranchNeedsTrackedBranchReads || currentExprNeedsTrackedBranchReads;
|
|
28802
28955
|
const falseFn = t4.arrowFunctionExpression(
|
|
28803
28956
|
[],
|
|
28804
28957
|
t4.blockStatement([t4.returnStatement(currentExpr)])
|
|
@@ -28815,7 +28968,13 @@ function transformControlFlowReturns(statements, ctx) {
|
|
|
28815
28968
|
(acc, expr) => t4.logicalExpression("||", acc, expr),
|
|
28816
28969
|
comparisons[0]
|
|
28817
28970
|
);
|
|
28818
|
-
currentExpr = buildConditionalBindingExpr(
|
|
28971
|
+
currentExpr = buildConditionalBindingExpr(
|
|
28972
|
+
testExpr,
|
|
28973
|
+
trueFn,
|
|
28974
|
+
falseFn,
|
|
28975
|
+
trackBranchReads ? { trackBranchReads: true } : void 0
|
|
28976
|
+
);
|
|
28977
|
+
currentExprNeedsTrackedBranchReads = trackBranchReads;
|
|
28819
28978
|
}
|
|
28820
28979
|
return t4.callExpression(
|
|
28821
28980
|
t4.arrowFunctionExpression(
|
|
@@ -33164,8 +33323,52 @@ function shouldSuppressWarning(suppressions, code, line) {
|
|
|
33164
33323
|
});
|
|
33165
33324
|
}
|
|
33166
33325
|
var DEFAULT_ERROR_WARNING_CODES = /* @__PURE__ */ new Set(["FICT-R004"]);
|
|
33326
|
+
var STRICT_REACTIVITY_WARNING_CODES = /* @__PURE__ */ new Set(["FICT-R003", "FICT-R006"]);
|
|
33327
|
+
var STRICT_GUARANTEE_WARNING_CODES = /* @__PURE__ */ new Set([
|
|
33328
|
+
"FICT-P001",
|
|
33329
|
+
"FICT-P002",
|
|
33330
|
+
"FICT-P003",
|
|
33331
|
+
"FICT-P004",
|
|
33332
|
+
"FICT-P005",
|
|
33333
|
+
"FICT-J003",
|
|
33334
|
+
"FICT-S002",
|
|
33335
|
+
"FICT-R001",
|
|
33336
|
+
"FICT-R002",
|
|
33337
|
+
"FICT-R003",
|
|
33338
|
+
"FICT-R006"
|
|
33339
|
+
]);
|
|
33340
|
+
function readBooleanEnv(name) {
|
|
33341
|
+
const raw = process.env[name];
|
|
33342
|
+
if (!raw) return void 0;
|
|
33343
|
+
const normalized = raw.trim().toLowerCase();
|
|
33344
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
|
|
33345
|
+
return true;
|
|
33346
|
+
}
|
|
33347
|
+
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
|
|
33348
|
+
return false;
|
|
33349
|
+
}
|
|
33350
|
+
return void 0;
|
|
33351
|
+
}
|
|
33352
|
+
function validateStrictGuaranteeConfig(options, suppressions) {
|
|
33353
|
+
if (!options.strictGuarantee) return;
|
|
33354
|
+
if (suppressions.length > 0) {
|
|
33355
|
+
throw new Error(
|
|
33356
|
+
"strictGuarantee does not allow fict-ignore suppression comments. Remove suppressions to keep fail-closed guarantees."
|
|
33357
|
+
);
|
|
33358
|
+
}
|
|
33359
|
+
if (!options.warningLevels) return;
|
|
33360
|
+
for (const [code, level] of Object.entries(options.warningLevels)) {
|
|
33361
|
+
if (!STRICT_GUARANTEE_WARNING_CODES.has(code)) continue;
|
|
33362
|
+
if (level === "error") continue;
|
|
33363
|
+
throw new Error(
|
|
33364
|
+
`strictGuarantee does not allow downgrading ${code} to "${level}". Remove this warningLevels override.`
|
|
33365
|
+
);
|
|
33366
|
+
}
|
|
33367
|
+
}
|
|
33167
33368
|
function hasErrorEscalation(options) {
|
|
33168
33369
|
if (DEFAULT_ERROR_WARNING_CODES.size > 0) return true;
|
|
33370
|
+
if (options.strictGuarantee) return true;
|
|
33371
|
+
if (options.strictReactivity) return true;
|
|
33169
33372
|
if (options.warningsAsErrors === true) return true;
|
|
33170
33373
|
if (Array.isArray(options.warningsAsErrors) && options.warningsAsErrors.length > 0) return true;
|
|
33171
33374
|
if (options.warningLevels) {
|
|
@@ -33174,8 +33377,10 @@ function hasErrorEscalation(options) {
|
|
|
33174
33377
|
return false;
|
|
33175
33378
|
}
|
|
33176
33379
|
function resolveWarningLevel(code, options) {
|
|
33380
|
+
if (options.strictGuarantee && STRICT_GUARANTEE_WARNING_CODES.has(code)) return "error";
|
|
33177
33381
|
const override = options.warningLevels?.[code];
|
|
33178
33382
|
if (override) return override;
|
|
33383
|
+
if (options.strictReactivity && STRICT_REACTIVITY_WARNING_CODES.has(code)) return "error";
|
|
33179
33384
|
if (options.warningsAsErrors === true) return "error";
|
|
33180
33385
|
if (Array.isArray(options.warningsAsErrors) && options.warningsAsErrors.includes(code)) {
|
|
33181
33386
|
return "error";
|
|
@@ -33189,6 +33394,7 @@ function formatWarningAsError(warning) {
|
|
|
33189
33394
|
at ${location}`;
|
|
33190
33395
|
}
|
|
33191
33396
|
function createWarningDispatcher(onWarn, suppressions, options, dev) {
|
|
33397
|
+
validateStrictGuaranteeConfig(options, suppressions);
|
|
33192
33398
|
const hasEscalation = hasErrorEscalation(options);
|
|
33193
33399
|
if (!dev && !hasEscalation) return () => {
|
|
33194
33400
|
};
|
|
@@ -33294,7 +33500,7 @@ function isDynamicPropertyAccess(node, t4) {
|
|
|
33294
33500
|
if (!node.computed) return false;
|
|
33295
33501
|
return !(t4.isStringLiteral(node.property) || t4.isNumericLiteral(node.property));
|
|
33296
33502
|
}
|
|
33297
|
-
function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, reactiveBindingIds, effectMacroNames, warn, fileName, t4) {
|
|
33503
|
+
function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, reactiveBindingIds, stateMacroNames, memoMacroNames, effectMacroNames, warn, fileName, t4) {
|
|
33298
33504
|
const hasTrackedBinding = (path2, name, tracked) => {
|
|
33299
33505
|
const binding = path2.scope.getBinding(name);
|
|
33300
33506
|
return !!(binding && tracked.has(binding.identifier));
|
|
@@ -33309,6 +33515,107 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33309
33515
|
if (!root) return false;
|
|
33310
33516
|
return hasTrackedBinding(path2, root.name, reactiveBindingIds);
|
|
33311
33517
|
};
|
|
33518
|
+
const NON_ESCAPING_CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
33519
|
+
"map",
|
|
33520
|
+
"forEach",
|
|
33521
|
+
"filter",
|
|
33522
|
+
"some",
|
|
33523
|
+
"every",
|
|
33524
|
+
"find",
|
|
33525
|
+
"findIndex",
|
|
33526
|
+
"findLast",
|
|
33527
|
+
"findLastIndex",
|
|
33528
|
+
"flatMap",
|
|
33529
|
+
"reduce",
|
|
33530
|
+
"reduceRight",
|
|
33531
|
+
"sort",
|
|
33532
|
+
"toSorted",
|
|
33533
|
+
"then",
|
|
33534
|
+
"catch",
|
|
33535
|
+
"finally"
|
|
33536
|
+
]);
|
|
33537
|
+
const capturedClosureByBinding = /* @__PURE__ */ new Map();
|
|
33538
|
+
const shouldIgnoreIdentifierReference = (idPath) => {
|
|
33539
|
+
if (idPath.parentPath.isMemberExpression({ property: idPath.node }) && !idPath.parent.computed) {
|
|
33540
|
+
return true;
|
|
33541
|
+
}
|
|
33542
|
+
if (idPath.parentPath.isObjectProperty({ key: idPath.node }) && !idPath.parent.computed && !idPath.parent.shorthand) {
|
|
33543
|
+
return true;
|
|
33544
|
+
}
|
|
33545
|
+
return false;
|
|
33546
|
+
};
|
|
33547
|
+
const collectCapturedReactiveNames = (fnPath) => {
|
|
33548
|
+
const captured = /* @__PURE__ */ new Set();
|
|
33549
|
+
fnPath.traverse({
|
|
33550
|
+
Function(inner) {
|
|
33551
|
+
if (inner === fnPath) return;
|
|
33552
|
+
inner.skip();
|
|
33553
|
+
},
|
|
33554
|
+
Identifier(idPath) {
|
|
33555
|
+
if (shouldIgnoreIdentifierReference(idPath)) return;
|
|
33556
|
+
const name = idPath.node.name;
|
|
33557
|
+
const binding = idPath.scope.getBinding(name);
|
|
33558
|
+
if (!binding) return;
|
|
33559
|
+
if (!reactiveBindingIds.has(binding.identifier)) return;
|
|
33560
|
+
if (binding.scope === idPath.scope || binding.scope === fnPath.scope) return;
|
|
33561
|
+
captured.add(name);
|
|
33562
|
+
}
|
|
33563
|
+
});
|
|
33564
|
+
return captured;
|
|
33565
|
+
};
|
|
33566
|
+
const registerClosureCaptureBinding = (fnPath, captured) => {
|
|
33567
|
+
if (captured.size === 0) return;
|
|
33568
|
+
if (fnPath.isFunctionDeclaration() && fnPath.node.id) {
|
|
33569
|
+
const binding = fnPath.parentPath.scope.getBinding(fnPath.node.id.name);
|
|
33570
|
+
if (binding) {
|
|
33571
|
+
capturedClosureByBinding.set(binding.identifier, captured);
|
|
33572
|
+
}
|
|
33573
|
+
return;
|
|
33574
|
+
}
|
|
33575
|
+
if ((fnPath.isFunctionExpression() || fnPath.isArrowFunctionExpression()) && fnPath.parentPath.isVariableDeclarator()) {
|
|
33576
|
+
const id = fnPath.parentPath.node.id;
|
|
33577
|
+
if (!t4.isIdentifier(id)) return;
|
|
33578
|
+
const binding = fnPath.parentPath.scope.getBinding(id.name);
|
|
33579
|
+
if (binding) {
|
|
33580
|
+
capturedClosureByBinding.set(binding.identifier, captured);
|
|
33581
|
+
}
|
|
33582
|
+
return;
|
|
33583
|
+
}
|
|
33584
|
+
if (fnPath.parentPath.isAssignmentExpression({ right: fnPath.node })) {
|
|
33585
|
+
const left = fnPath.parentPath.node.left;
|
|
33586
|
+
if (!t4.isIdentifier(left)) return;
|
|
33587
|
+
const binding = fnPath.parentPath.scope.getBinding(left.name);
|
|
33588
|
+
if (binding) {
|
|
33589
|
+
capturedClosureByBinding.set(binding.identifier, captured);
|
|
33590
|
+
}
|
|
33591
|
+
}
|
|
33592
|
+
};
|
|
33593
|
+
const collectCapturedForArgument = (argPath) => {
|
|
33594
|
+
if (argPath.isArrowFunctionExpression() || argPath.isFunctionExpression()) {
|
|
33595
|
+
const captured2 = collectCapturedReactiveNames(argPath);
|
|
33596
|
+
return captured2.size > 0 ? captured2 : null;
|
|
33597
|
+
}
|
|
33598
|
+
if (!argPath.isIdentifier()) return null;
|
|
33599
|
+
const binding = argPath.scope.getBinding(argPath.node.name);
|
|
33600
|
+
if (!binding) return null;
|
|
33601
|
+
const captured = capturedClosureByBinding.get(binding.identifier);
|
|
33602
|
+
return captured && captured.size > 0 ? captured : null;
|
|
33603
|
+
};
|
|
33604
|
+
const isNonEscapingCallbackHost = (callee) => {
|
|
33605
|
+
const member = t4.isMemberExpression(callee) || t4.isOptionalMemberExpression(callee) ? callee : null;
|
|
33606
|
+
if (!member || member.computed || !t4.isIdentifier(member.property)) return false;
|
|
33607
|
+
return NON_ESCAPING_CALLBACK_METHODS.has(member.property.name);
|
|
33608
|
+
};
|
|
33609
|
+
const emitClosureCaptureWarning = (node, captured) => {
|
|
33610
|
+
const names = Array.from(captured).sort().join(", ");
|
|
33611
|
+
emitWarning(
|
|
33612
|
+
node,
|
|
33613
|
+
"FICT-R005",
|
|
33614
|
+
`Function captures reactive variable(s): ${names}. Pass them as parameters or memoize explicitly to avoid hidden dependencies.`,
|
|
33615
|
+
warn,
|
|
33616
|
+
fileName
|
|
33617
|
+
);
|
|
33618
|
+
};
|
|
33312
33619
|
const argumentHasReactive = (argPath) => {
|
|
33313
33620
|
if (argPath.isSpreadElement()) {
|
|
33314
33621
|
const inner = argPath.get("argument");
|
|
@@ -33331,12 +33638,7 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33331
33638
|
path2.skip();
|
|
33332
33639
|
},
|
|
33333
33640
|
Identifier(idPath) {
|
|
33334
|
-
if (
|
|
33335
|
-
return;
|
|
33336
|
-
}
|
|
33337
|
-
if (idPath.parentPath.isObjectProperty({ key: idPath.node }) && !idPath.parent.computed && !idPath.parent.shorthand) {
|
|
33338
|
-
return;
|
|
33339
|
-
}
|
|
33641
|
+
if (shouldIgnoreIdentifierReference(idPath)) return;
|
|
33340
33642
|
const binding = idPath.scope.getBinding(idPath.node.name);
|
|
33341
33643
|
if (binding && reactiveBindingIds.has(binding.identifier)) {
|
|
33342
33644
|
found = true;
|
|
@@ -33417,36 +33719,14 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33417
33719
|
}
|
|
33418
33720
|
},
|
|
33419
33721
|
Function(path2) {
|
|
33420
|
-
const captured =
|
|
33421
|
-
path2
|
|
33422
|
-
{
|
|
33423
|
-
Function(inner) {
|
|
33424
|
-
if (inner === path2) return;
|
|
33425
|
-
inner.skip();
|
|
33426
|
-
},
|
|
33427
|
-
Identifier(idPath) {
|
|
33428
|
-
const name = idPath.node.name;
|
|
33429
|
-
const binding = idPath.scope.getBinding(name);
|
|
33430
|
-
if (!binding) return;
|
|
33431
|
-
if (!reactiveBindingIds.has(binding.identifier)) return;
|
|
33432
|
-
if (binding.scope === idPath.scope || binding.scope === path2.scope) return;
|
|
33433
|
-
captured.add(name);
|
|
33434
|
-
}
|
|
33435
|
-
},
|
|
33436
|
-
{}
|
|
33437
|
-
);
|
|
33438
|
-
if (captured.size > 0) {
|
|
33439
|
-
emitWarning(
|
|
33440
|
-
path2.node,
|
|
33441
|
-
"FICT-R005",
|
|
33442
|
-
`Function captures reactive variable(s): ${Array.from(captured).join(", ")}. Pass them as parameters or memoize explicitly to avoid hidden dependencies.`,
|
|
33443
|
-
warn,
|
|
33444
|
-
fileName
|
|
33445
|
-
);
|
|
33446
|
-
}
|
|
33722
|
+
const captured = collectCapturedReactiveNames(path2);
|
|
33723
|
+
registerClosureCaptureBinding(path2, captured);
|
|
33447
33724
|
},
|
|
33448
33725
|
CallExpression(path2) {
|
|
33449
|
-
const
|
|
33726
|
+
const callNode = path2.node;
|
|
33727
|
+
if (isStateCall(callNode, t4, stateMacroNames)) return;
|
|
33728
|
+
if (isMemoCall(callNode, t4, memoMacroNames)) return;
|
|
33729
|
+
const isEffect = isEffectCall(callNode, t4, effectMacroNames);
|
|
33450
33730
|
if (isEffect) {
|
|
33451
33731
|
const argPath = path2.get("arguments.0");
|
|
33452
33732
|
if (argPath?.isFunctionExpression() || argPath?.isArrowFunctionExpression()) {
|
|
@@ -33469,7 +33749,7 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33469
33749
|
});
|
|
33470
33750
|
if (!hasReactiveDependency) {
|
|
33471
33751
|
emitWarning(
|
|
33472
|
-
|
|
33752
|
+
callNode,
|
|
33473
33753
|
"FICT-E001",
|
|
33474
33754
|
"Effect has no reactive reads; it will run once. Consider removing $effect or adding dependencies.",
|
|
33475
33755
|
warn,
|
|
@@ -33492,6 +33772,7 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33492
33772
|
const isSafe = calleeName && SAFE_FUNCTIONS.has(calleeName);
|
|
33493
33773
|
if (isSafe) return;
|
|
33494
33774
|
const argPaths = path2.get("arguments");
|
|
33775
|
+
const nonEscapingCallbackHost = isNonEscapingCallbackHost(callee);
|
|
33495
33776
|
for (const argPath of argPaths) {
|
|
33496
33777
|
if (argPath.isIdentifier() && hasTrackedBinding(argPath, argPath.node.name, stateBindingIds)) {
|
|
33497
33778
|
continue;
|
|
@@ -33507,6 +33788,13 @@ function runWarningPass(programPath, stateBindingIds, stateRootBindingIds, react
|
|
|
33507
33788
|
break;
|
|
33508
33789
|
}
|
|
33509
33790
|
}
|
|
33791
|
+
if (nonEscapingCallbackHost) return;
|
|
33792
|
+
for (const argPath of argPaths) {
|
|
33793
|
+
const captured = collectCapturedForArgument(argPath);
|
|
33794
|
+
if (!captured) continue;
|
|
33795
|
+
emitClosureCaptureWarning(argPath.node, captured);
|
|
33796
|
+
break;
|
|
33797
|
+
}
|
|
33510
33798
|
},
|
|
33511
33799
|
OptionalMemberExpression(path2) {
|
|
33512
33800
|
if (!path2.node.computed) return;
|
|
@@ -34371,6 +34659,8 @@ or extract the nested logic into a custom hook (useXxx).`
|
|
|
34371
34659
|
stateBindingIds,
|
|
34372
34660
|
stateRootBindingIds,
|
|
34373
34661
|
reactiveBindingIds,
|
|
34662
|
+
stateMacroNames,
|
|
34663
|
+
memoMacroNames,
|
|
34374
34664
|
effectMacroNames,
|
|
34375
34665
|
warn,
|
|
34376
34666
|
fileName,
|
|
@@ -34415,13 +34705,17 @@ var createFictPlugin = declare(
|
|
|
34415
34705
|
(api, options = {}) => {
|
|
34416
34706
|
api.assertVersion(7);
|
|
34417
34707
|
const t4 = api.types;
|
|
34708
|
+
const strictGuaranteeFromEnv = readBooleanEnv("FICT_STRICT_GUARANTEE") === true;
|
|
34418
34709
|
const normalizedOptions = {
|
|
34419
34710
|
...options,
|
|
34420
34711
|
fineGrainedDom: options.fineGrainedDom ?? true,
|
|
34712
|
+
lazyConditional: options.lazyConditional ?? true,
|
|
34713
|
+
getterCache: options.getterCache ?? true,
|
|
34421
34714
|
optimize: options.optimize ?? true,
|
|
34422
34715
|
optimizeLevel: options.optimizeLevel ?? "safe",
|
|
34423
34716
|
inlineDerivedMemos: options.inlineDerivedMemos ?? true,
|
|
34424
34717
|
emitModuleMetadata: options.emitModuleMetadata ?? "auto",
|
|
34718
|
+
strictGuarantee: strictGuaranteeFromEnv || options.strictGuarantee !== false,
|
|
34425
34719
|
dev: options.dev ?? (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test")
|
|
34426
34720
|
};
|
|
34427
34721
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fictjs/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Babel plugin for Fict Compiler",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/babel__helper-plugin-utils": "^7.10.3",
|
|
49
49
|
"@types/babel__traverse": "^7.28.0",
|
|
50
50
|
"tsup": "^8.5.1",
|
|
51
|
-
"@fictjs/runtime": "0.
|
|
51
|
+
"@fictjs/runtime": "0.9.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|