sinwan-compiler 0.2.10 → 0.3.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/dist/{chunk-qj8m16x2.js → chunk-3e981ff7.js} +64 -16
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +301 -39
- package/dist/live-cc-destructure.d.ts +19 -0
- package/dist/reactive-wrap.d.ts +10 -1
- package/docs/README.md +7 -3
- package/docs/api.md +4 -0
- package/docs/architecture.md +2 -0
- package/docs/plugins.md +1 -1
- package/docs/transform.md +20 -0
- package/package.json +1 -1
|
@@ -143,6 +143,10 @@ function trackLocalScopeBindings(path, names, scope) {
|
|
|
143
143
|
const init = p.node.init;
|
|
144
144
|
if (!init)
|
|
145
145
|
return;
|
|
146
|
+
if (t.isIdentifier(id) && t.isCallExpression(init) && t.isIdentifier(init.callee) && (init.callee.name === "_$createLiveRest" || init.callee.name === "createLiveRest")) {
|
|
147
|
+
scope.bindings.set(id.name, { kind: "prop" });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
146
150
|
const source = isReactiveSourceCall(init, names);
|
|
147
151
|
if (source) {
|
|
148
152
|
if (source.isArray) {
|
|
@@ -231,6 +235,8 @@ function trackLocalScopeBindings(path, names, scope) {
|
|
|
231
235
|
const body = fn.body;
|
|
232
236
|
if (body && containsReactiveRead(body, scope)) {
|
|
233
237
|
scope.bindings.set(name, { kind: "getter" });
|
|
238
|
+
} else {
|
|
239
|
+
scope.bindings.set(name, { kind: "function" });
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
});
|
|
@@ -329,6 +335,9 @@ function visitCallCalleeAndArgs(node, visitExpr) {
|
|
|
329
335
|
visitExpr(arg, true);
|
|
330
336
|
}
|
|
331
337
|
}
|
|
338
|
+
function isSignalValuePath(path) {
|
|
339
|
+
return path[0] === "value" || path.length >= 2 && path[1] === "value";
|
|
340
|
+
}
|
|
332
341
|
function getMemberExpressionRootAndPath(node) {
|
|
333
342
|
const path = [];
|
|
334
343
|
let current = node;
|
|
@@ -375,8 +384,9 @@ function isReactiveRead(node, scope) {
|
|
|
375
384
|
if (!rootPath)
|
|
376
385
|
return false;
|
|
377
386
|
const binding = scope.bindings.get(rootPath.root);
|
|
378
|
-
if (!binding)
|
|
379
|
-
return
|
|
387
|
+
if (!binding) {
|
|
388
|
+
return isSignalValuePath(rootPath.path);
|
|
389
|
+
}
|
|
380
390
|
if (binding.kind === "prop") {
|
|
381
391
|
return true;
|
|
382
392
|
}
|
|
@@ -393,10 +403,27 @@ function isReactiveRead(node, scope) {
|
|
|
393
403
|
}
|
|
394
404
|
function isGetterCall(node, scope) {
|
|
395
405
|
const callee = node.callee;
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
406
|
+
if (t.isIdentifier(callee)) {
|
|
407
|
+
const binding = scope.bindings.get(callee.name);
|
|
408
|
+
if (binding?.kind === "getter")
|
|
409
|
+
return true;
|
|
410
|
+
if (binding)
|
|
411
|
+
return false;
|
|
412
|
+
return node.arguments.length === 0;
|
|
413
|
+
}
|
|
414
|
+
if (isMemberLike(callee) && !callee.computed && t.isIdentifier(callee.property)) {
|
|
415
|
+
if (node.arguments.length !== 0)
|
|
416
|
+
return false;
|
|
417
|
+
const rootPath = getMemberExpressionRootAndPath(callee);
|
|
418
|
+
if (!rootPath)
|
|
419
|
+
return false;
|
|
420
|
+
const binding = scope.bindings.get(rootPath.root);
|
|
421
|
+
if (binding?.kind === "mutable" || binding?.kind === "function" || binding?.kind === "signal" || binding?.kind === "computed" || binding?.kind === "prop" || binding?.kind === "getter" || binding?.kind === "signalObject") {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
400
427
|
}
|
|
401
428
|
function isReactiveValue2(node, scope) {
|
|
402
429
|
if (t.isTSNonNullExpression(node)) {
|
|
@@ -414,8 +441,9 @@ function isReactiveValue2(node, scope) {
|
|
|
414
441
|
if (!rootPath)
|
|
415
442
|
return false;
|
|
416
443
|
const binding = scope.bindings.get(rootPath.root);
|
|
417
|
-
if (!binding)
|
|
418
|
-
return
|
|
444
|
+
if (!binding) {
|
|
445
|
+
return isSignalValuePath(rootPath.path);
|
|
446
|
+
}
|
|
419
447
|
if (binding.kind === "prop" || binding.kind === "mutable") {
|
|
420
448
|
return true;
|
|
421
449
|
}
|
|
@@ -440,7 +468,7 @@ function containsReactiveValue2(expr, scope) {
|
|
|
440
468
|
return;
|
|
441
469
|
}
|
|
442
470
|
}
|
|
443
|
-
if (
|
|
471
|
+
if (isCallLike(node) && isGetterCall(node, scope)) {
|
|
444
472
|
found = true;
|
|
445
473
|
return;
|
|
446
474
|
}
|
|
@@ -489,7 +517,7 @@ function containsReactiveRead(expr, scope) {
|
|
|
489
517
|
return;
|
|
490
518
|
}
|
|
491
519
|
}
|
|
492
|
-
if (
|
|
520
|
+
if (isCallLike(node) && isGetterCall(node, scope)) {
|
|
493
521
|
found = true;
|
|
494
522
|
return;
|
|
495
523
|
}
|
|
@@ -551,7 +579,7 @@ function getComponentExpressionInfo(exprPath) {
|
|
|
551
579
|
}
|
|
552
580
|
}
|
|
553
581
|
if (!firstChar || firstChar !== firstChar.toUpperCase() || !/[A-Z]/.test(firstChar)) {
|
|
554
|
-
return { isComponent: false, componentName: null, attributeName
|
|
582
|
+
return { isComponent: false, componentName: null, attributeName };
|
|
555
583
|
}
|
|
556
584
|
return { isComponent: true, componentName, attributeName };
|
|
557
585
|
}
|
|
@@ -654,7 +682,25 @@ function transformPropMemberAccess(expr, scope) {
|
|
|
654
682
|
function wrapExpression(expr) {
|
|
655
683
|
return t.arrowFunctionExpression([], expr);
|
|
656
684
|
}
|
|
657
|
-
function
|
|
685
|
+
function isPropRoot(expr, scope) {
|
|
686
|
+
if (t.isIdentifier(expr)) {
|
|
687
|
+
return scope.bindings.get(expr.name)?.kind === "prop";
|
|
688
|
+
}
|
|
689
|
+
if (t.isCallExpression(expr) && t.isIdentifier(expr.callee, { name: "_$unwrap" }) && expr.arguments.length === 1 && t.isIdentifier(expr.arguments[0])) {
|
|
690
|
+
return scope.bindings.get(expr.arguments[0].name)?.kind === "prop";
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
function isChildrenPropRead(expr, scope) {
|
|
695
|
+
if (t.isIdentifier(expr, { name: "children" }))
|
|
696
|
+
return true;
|
|
697
|
+
if (!isMemberLike(expr) || expr.computed)
|
|
698
|
+
return false;
|
|
699
|
+
if (!t.isIdentifier(expr.property, { name: "children" }))
|
|
700
|
+
return false;
|
|
701
|
+
return isPropRoot(expr.object, scope);
|
|
702
|
+
}
|
|
703
|
+
function createBindingDescriptor(expr, exprPath, options, scope, childrenGetter) {
|
|
658
704
|
if (!options.explicitBindings) {
|
|
659
705
|
return wrapExpression(expr);
|
|
660
706
|
}
|
|
@@ -681,7 +727,7 @@ function createBindingDescriptor(expr, exprPath, options) {
|
|
|
681
727
|
]);
|
|
682
728
|
}
|
|
683
729
|
const compInfo = getComponentExpressionInfo(exprPath);
|
|
684
|
-
if (
|
|
730
|
+
if (isChildrenPropRead(expr, scope) || childrenGetter || compInfo.isComponent && compInfo.attributeName === null) {
|
|
685
731
|
return wrapExpression(expr);
|
|
686
732
|
}
|
|
687
733
|
return t.callExpression(t.identifier("_$bindText"), [wrapExpression(expr)]);
|
|
@@ -1114,12 +1160,13 @@ function wrapReactiveExpressions2(ast, options = {}) {
|
|
|
1114
1160
|
trackPropBindings(scope, path.node.params[0], allProps);
|
|
1115
1161
|
}
|
|
1116
1162
|
}
|
|
1117
|
-
if (scope.bindings.size === 0)
|
|
1118
|
-
return;
|
|
1119
1163
|
path.traverse({
|
|
1120
1164
|
JSXExpressionContainer(exprPath) {
|
|
1121
1165
|
const expr = exprPath.node.expression;
|
|
1122
1166
|
const compInfo = getComponentExpressionInfo(exprPath);
|
|
1167
|
+
if (compInfo.attributeName?.startsWith("on")) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1123
1170
|
if (compInfo.isComponent && compInfo.attributeName !== null) {
|
|
1124
1171
|
const knownProp = !!compInfo.componentName && isReactiveComponentProp(compInfo.componentName, compInfo.attributeName, componentNames, reactiveProps, importedReactiveProps);
|
|
1125
1172
|
if (!knownProp && (!shouldWrap(expr, scope) || isReactiveContainerPassThrough(expr, scope))) {
|
|
@@ -1127,10 +1174,11 @@ function wrapReactiveExpressions2(ast, options = {}) {
|
|
|
1127
1174
|
}
|
|
1128
1175
|
}
|
|
1129
1176
|
if (shouldWrap(expr, scope)) {
|
|
1177
|
+
const childrenGetter = isChildrenPropRead(expr, scope);
|
|
1130
1178
|
if (transformPropMemberAccess(expr, scope)) {
|
|
1131
1179
|
needsResolve = true;
|
|
1132
1180
|
}
|
|
1133
|
-
exprPath.replaceWith(t.jsxExpressionContainer(createBindingDescriptor(expr, exprPath, options)));
|
|
1181
|
+
exprPath.replaceWith(t.jsxExpressionContainer(createBindingDescriptor(expr, exprPath, options, scope, childrenGetter)));
|
|
1134
1182
|
}
|
|
1135
1183
|
}
|
|
1136
1184
|
});
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export { transformJSX, type TransformOptions } from "./transform";
|
|
8
8
|
export { COMPILER_TEMPLATE_SLOT_PROTOCOL } from "./transform";
|
|
9
9
|
export { autoWrapComponents } from "./auto-cc";
|
|
10
|
+
export { rewriteLiveCcDestructure } from "./live-cc-destructure";
|
|
10
11
|
export { analyze, analyzeProject, analyzeModule, loadMetadata, AnalyzerCache, type AnalyzeOptions, type ModuleAnalysis, type ProjectAnalysis, type ImportedCallSite, type ImportInfo, type WorkspacesConfig, } from "./analyze";
|
|
11
12
|
export { collectExportedComponents, type ExportedComponent } from "./exports";
|
|
12
13
|
export { runAnalyzeCli } from "./cli";
|
package/dist/index.js
CHANGED
|
@@ -17,15 +17,275 @@ import {
|
|
|
17
17
|
propagateReactiveProps2,
|
|
18
18
|
wrapReactiveExpressions2,
|
|
19
19
|
runAnalyzeCli2
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-3e981ff7.js";
|
|
21
21
|
|
|
22
22
|
// src/transform.ts
|
|
23
23
|
import { parse } from "@babel/parser";
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
// src/live-cc-destructure.ts
|
|
25
26
|
import * as t from "@babel/types";
|
|
26
27
|
import _traverse from "@babel/traverse";
|
|
27
|
-
var generate = typeof _generate === "function" ? _generate : _generate.default ?? _generate;
|
|
28
28
|
var traverse = typeof _traverse === "function" ? _traverse : _traverse.default ?? _traverse;
|
|
29
|
+
var CC_SOURCE_MODULES = new Set(["sinwan", "sinwan/component"]);
|
|
30
|
+
var LIVE_REST_LOCAL = "_$createLiveRest";
|
|
31
|
+
var SPREAD_LOCAL = "_$getSpreadProps";
|
|
32
|
+
var LIVE_REST_IMPORTED = "createLiveRest";
|
|
33
|
+
var SPREAD_IMPORTED = "getSpreadProps";
|
|
34
|
+
function collectCcAliases(ast) {
|
|
35
|
+
const aliases = new Set;
|
|
36
|
+
traverse(ast, {
|
|
37
|
+
ImportDeclaration(path) {
|
|
38
|
+
const source = path.node.source.value;
|
|
39
|
+
if (!CC_SOURCE_MODULES.has(source))
|
|
40
|
+
return;
|
|
41
|
+
for (const spec of path.node.specifiers) {
|
|
42
|
+
if (!t.isImportSpecifier(spec))
|
|
43
|
+
continue;
|
|
44
|
+
const imported = t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
45
|
+
if (imported === "cc") {
|
|
46
|
+
aliases.add(spec.local.name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return aliases;
|
|
52
|
+
}
|
|
53
|
+
function parseFlatObjectPattern(pattern) {
|
|
54
|
+
const props = [];
|
|
55
|
+
let rest = null;
|
|
56
|
+
for (const prop of pattern.properties) {
|
|
57
|
+
if (t.isRestElement(prop)) {
|
|
58
|
+
if (!t.isIdentifier(prop.argument))
|
|
59
|
+
return null;
|
|
60
|
+
rest = prop.argument.name;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!t.isObjectProperty(prop) || prop.computed)
|
|
64
|
+
return null;
|
|
65
|
+
if (!t.isIdentifier(prop.key) && !t.isStringLiteral(prop.key))
|
|
66
|
+
return null;
|
|
67
|
+
let value = prop.value;
|
|
68
|
+
let defaultValue = null;
|
|
69
|
+
if (t.isAssignmentPattern(value)) {
|
|
70
|
+
defaultValue = value.right;
|
|
71
|
+
value = value.left;
|
|
72
|
+
}
|
|
73
|
+
if (!t.isIdentifier(value))
|
|
74
|
+
return null;
|
|
75
|
+
const keyName = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value;
|
|
76
|
+
props.push({
|
|
77
|
+
key: prop.key,
|
|
78
|
+
keyName,
|
|
79
|
+
local: value.name,
|
|
80
|
+
defaultValue
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return { props, rest };
|
|
84
|
+
}
|
|
85
|
+
function pickPropsName(used) {
|
|
86
|
+
if (!used.has("props"))
|
|
87
|
+
return "props";
|
|
88
|
+
let index = 0;
|
|
89
|
+
let name = "_props";
|
|
90
|
+
while (used.has(name)) {
|
|
91
|
+
index += 1;
|
|
92
|
+
name = `_props${index}`;
|
|
93
|
+
}
|
|
94
|
+
return name;
|
|
95
|
+
}
|
|
96
|
+
function collectUsedNames(fnPath) {
|
|
97
|
+
const used = new Set;
|
|
98
|
+
fnPath.traverse({
|
|
99
|
+
Identifier(p) {
|
|
100
|
+
used.add(p.node.name);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return used;
|
|
104
|
+
}
|
|
105
|
+
function memberAccess(propsName, key) {
|
|
106
|
+
if (t.isIdentifier(key)) {
|
|
107
|
+
return t.memberExpression(t.identifier(propsName), t.identifier(key.name));
|
|
108
|
+
}
|
|
109
|
+
return t.memberExpression(t.identifier(propsName), t.stringLiteral(key.value), true);
|
|
110
|
+
}
|
|
111
|
+
function liveRead(propsName, binding) {
|
|
112
|
+
const read = memberAccess(propsName, binding.key);
|
|
113
|
+
if (!binding.defaultValue)
|
|
114
|
+
return read;
|
|
115
|
+
return t.conditionalExpression(t.binaryExpression("===", memberAccess(propsName, binding.key), t.unaryExpression("void", t.numericLiteral(0))), binding.defaultValue, read);
|
|
116
|
+
}
|
|
117
|
+
function ensureBlockBody(fn) {
|
|
118
|
+
if (t.isBlockStatement(fn.body))
|
|
119
|
+
return fn.body;
|
|
120
|
+
const body = t.blockStatement([t.returnStatement(fn.body)]);
|
|
121
|
+
fn.body = body;
|
|
122
|
+
if (t.isArrowFunctionExpression(fn)) {
|
|
123
|
+
fn.expression = false;
|
|
124
|
+
}
|
|
125
|
+
return body;
|
|
126
|
+
}
|
|
127
|
+
function replaceWithExpression(path, expr) {
|
|
128
|
+
const parent = path.parent;
|
|
129
|
+
if (t.isObjectProperty(parent) && parent.shorthand && parent.value === path.node) {
|
|
130
|
+
parent.shorthand = false;
|
|
131
|
+
parent.value = expr;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
path.replaceWith(expr);
|
|
135
|
+
}
|
|
136
|
+
function isCcParamBinding(path, locals, fnNode) {
|
|
137
|
+
if (!path.isReferencedIdentifier())
|
|
138
|
+
return false;
|
|
139
|
+
if (!locals.has(path.node.name))
|
|
140
|
+
return false;
|
|
141
|
+
const binding = path.scope.getBinding(path.node.name);
|
|
142
|
+
if (!binding)
|
|
143
|
+
return false;
|
|
144
|
+
const fnParent = binding.path.getFunctionParent();
|
|
145
|
+
return fnParent?.node === fnNode;
|
|
146
|
+
}
|
|
147
|
+
function rewriteSpreads(fnPath, liveNames, fnNode) {
|
|
148
|
+
let changed = false;
|
|
149
|
+
fnPath.traverse({
|
|
150
|
+
JSXSpreadAttribute(p) {
|
|
151
|
+
const arg = p.node.argument;
|
|
152
|
+
if (!t.isIdentifier(arg) || !liveNames.has(arg.name))
|
|
153
|
+
return;
|
|
154
|
+
const binding = p.scope.getBinding(arg.name);
|
|
155
|
+
const fnParent = binding?.path.getFunctionParent();
|
|
156
|
+
if (fnParent && fnParent.node !== fnNode)
|
|
157
|
+
return;
|
|
158
|
+
p.node.argument = t.callExpression(t.identifier(SPREAD_LOCAL), [
|
|
159
|
+
t.identifier(arg.name)
|
|
160
|
+
]);
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return changed;
|
|
165
|
+
}
|
|
166
|
+
function hasLocalSpecifier(stmt, local) {
|
|
167
|
+
return stmt.specifiers.some((spec) => t.isImportSpecifier(spec) && spec.local.name === local);
|
|
168
|
+
}
|
|
169
|
+
function ensureComponentImport(ast, imported, local) {
|
|
170
|
+
let target = null;
|
|
171
|
+
for (const stmt of ast.program.body) {
|
|
172
|
+
if (!t.isImportDeclaration(stmt))
|
|
173
|
+
continue;
|
|
174
|
+
if (hasLocalSpecifier(stmt, local))
|
|
175
|
+
return;
|
|
176
|
+
if (stmt.source.value === "sinwan/component") {
|
|
177
|
+
target = stmt;
|
|
178
|
+
} else if (stmt.source.value === "sinwan" && target === null) {
|
|
179
|
+
target = stmt;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const spec = t.importSpecifier(t.identifier(local), t.identifier(imported));
|
|
183
|
+
if (target) {
|
|
184
|
+
target.specifiers.push(spec);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function copyPatternType(pattern, id) {
|
|
188
|
+
if (pattern.typeAnnotation) {
|
|
189
|
+
id.typeAnnotation = pattern.typeAnnotation;
|
|
190
|
+
}
|
|
191
|
+
if (pattern.optional) {
|
|
192
|
+
id.optional = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function rewriteFunctionDestructure(fnPath) {
|
|
196
|
+
const fn = fnPath.node;
|
|
197
|
+
const param = fn.params[0];
|
|
198
|
+
if (!param) {
|
|
199
|
+
return { rest: false, spread: false };
|
|
200
|
+
}
|
|
201
|
+
if (t.isIdentifier(param)) {
|
|
202
|
+
const spread = rewriteSpreads(fnPath, new Set([param.name]), fn);
|
|
203
|
+
return { rest: false, spread };
|
|
204
|
+
}
|
|
205
|
+
if (!t.isObjectPattern(param)) {
|
|
206
|
+
return { rest: false, spread: false };
|
|
207
|
+
}
|
|
208
|
+
const flat = parseFlatObjectPattern(param);
|
|
209
|
+
if (!flat) {
|
|
210
|
+
return { rest: false, spread: false };
|
|
211
|
+
}
|
|
212
|
+
const used = collectUsedNames(fnPath);
|
|
213
|
+
for (const binding of flat.props)
|
|
214
|
+
used.add(binding.local);
|
|
215
|
+
if (flat.rest)
|
|
216
|
+
used.add(flat.rest);
|
|
217
|
+
const propsName = pickPropsName(used);
|
|
218
|
+
const locals = new Set(flat.props.map((binding) => binding.local));
|
|
219
|
+
const reads = new Map;
|
|
220
|
+
for (const binding of flat.props) {
|
|
221
|
+
reads.set(binding.local, binding);
|
|
222
|
+
}
|
|
223
|
+
fnPath.traverse({
|
|
224
|
+
Identifier(p) {
|
|
225
|
+
if (p.findParent((parent) => parent.node === param)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!isCcParamBinding(p, locals, fn))
|
|
229
|
+
return;
|
|
230
|
+
const binding = reads.get(p.node.name);
|
|
231
|
+
if (!binding)
|
|
232
|
+
return;
|
|
233
|
+
replaceWithExpression(p, liveRead(propsName, binding));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const propsId = t.identifier(propsName);
|
|
237
|
+
copyPatternType(param, propsId);
|
|
238
|
+
fn.params[0] = propsId;
|
|
239
|
+
const liveNames = new Set([propsName]);
|
|
240
|
+
if (flat.rest) {
|
|
241
|
+
const body = ensureBlockBody(fn);
|
|
242
|
+
body.body.unshift(t.variableDeclaration("const", [
|
|
243
|
+
t.variableDeclarator(t.identifier(flat.rest), t.callExpression(t.identifier(LIVE_REST_LOCAL), [
|
|
244
|
+
t.identifier(propsName),
|
|
245
|
+
t.arrayExpression(flat.props.map((binding) => t.stringLiteral(binding.keyName)))
|
|
246
|
+
]))
|
|
247
|
+
]));
|
|
248
|
+
liveNames.add(flat.rest);
|
|
249
|
+
}
|
|
250
|
+
const spread = rewriteSpreads(fnPath, liveNames, fn);
|
|
251
|
+
return { rest: flat.rest != null, spread };
|
|
252
|
+
}
|
|
253
|
+
function rewriteLiveCcDestructure(ast) {
|
|
254
|
+
const aliases = collectCcAliases(ast);
|
|
255
|
+
if (aliases.size === 0)
|
|
256
|
+
return { rest: false, spread: false };
|
|
257
|
+
let needsRest = false;
|
|
258
|
+
let needsSpread = false;
|
|
259
|
+
traverse(ast, {
|
|
260
|
+
CallExpression(path) {
|
|
261
|
+
const callee = path.node.callee;
|
|
262
|
+
if (!t.isIdentifier(callee) || !aliases.has(callee.name))
|
|
263
|
+
return;
|
|
264
|
+
const firstArg = path.node.arguments[0];
|
|
265
|
+
if (!firstArg || !t.isFunction(firstArg))
|
|
266
|
+
return;
|
|
267
|
+
const argPaths = path.get("arguments");
|
|
268
|
+
const fnPath = argPaths[0];
|
|
269
|
+
const result = rewriteFunctionDestructure(fnPath);
|
|
270
|
+
needsRest = needsRest || result.rest;
|
|
271
|
+
needsSpread = needsSpread || result.spread;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
if (needsRest) {
|
|
275
|
+
ensureComponentImport(ast, LIVE_REST_IMPORTED, LIVE_REST_LOCAL);
|
|
276
|
+
}
|
|
277
|
+
if (needsSpread) {
|
|
278
|
+
ensureComponentImport(ast, SPREAD_IMPORTED, SPREAD_LOCAL);
|
|
279
|
+
}
|
|
280
|
+
return { rest: needsRest, spread: needsSpread };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/transform.ts
|
|
284
|
+
import _generate from "@babel/generator";
|
|
285
|
+
import * as t2 from "@babel/types";
|
|
286
|
+
import _traverse2 from "@babel/traverse";
|
|
287
|
+
var generate = typeof _generate === "function" ? _generate : _generate.default ?? _generate;
|
|
288
|
+
var traverse2 = typeof _traverse2 === "function" ? _traverse2 : _traverse2.default ?? _traverse2;
|
|
29
289
|
var COMPILER_TEMPLATE_SLOT_PROTOCOL = {
|
|
30
290
|
slotPrefix: "s",
|
|
31
291
|
encodeSlot(index) {
|
|
@@ -162,17 +422,17 @@ function warnSuspectedStyleInterpolation(attrValue, filename, line) {
|
|
|
162
422
|
console.warn('[Sinwan] JSX string literal style attribute contains "${...}" which will not be interpolated. ' + "Use style={`...${...}`} instead. (" + filename + location + ")");
|
|
163
423
|
}
|
|
164
424
|
function isLiteralStyleValue(expr) {
|
|
165
|
-
return
|
|
425
|
+
return t2.isStringLiteral(expr) || t2.isNumericLiteral(expr) || t2.isBooleanLiteral(expr) || t2.isNullLiteral(expr);
|
|
166
426
|
}
|
|
167
427
|
function isStaticStyleValue(expr) {
|
|
168
|
-
if (
|
|
428
|
+
if (t2.isStringLiteral(expr))
|
|
169
429
|
return true;
|
|
170
|
-
if (
|
|
430
|
+
if (t2.isTemplateLiteral(expr) && expr.expressions.length === 0)
|
|
171
431
|
return true;
|
|
172
|
-
if (!
|
|
432
|
+
if (!t2.isObjectExpression(expr))
|
|
173
433
|
return false;
|
|
174
434
|
for (const prop of expr.properties) {
|
|
175
|
-
if (!
|
|
435
|
+
if (!t2.isObjectProperty(prop))
|
|
176
436
|
return false;
|
|
177
437
|
if (prop.computed)
|
|
178
438
|
return false;
|
|
@@ -185,36 +445,36 @@ function toKebabCase(value) {
|
|
|
185
445
|
return value.includes("-") ? value : value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
186
446
|
}
|
|
187
447
|
function serializeStyleValue(expr) {
|
|
188
|
-
if (
|
|
448
|
+
if (t2.isStringLiteral(expr))
|
|
189
449
|
return expr.value;
|
|
190
|
-
if (
|
|
450
|
+
if (t2.isTemplateLiteral(expr) && expr.expressions.length === 0) {
|
|
191
451
|
return expr.quasis[0]?.value?.raw ?? "";
|
|
192
452
|
}
|
|
193
|
-
if (!
|
|
453
|
+
if (!t2.isObjectExpression(expr))
|
|
194
454
|
return "";
|
|
195
455
|
const parts = [];
|
|
196
456
|
for (const prop of expr.properties) {
|
|
197
|
-
if (!
|
|
457
|
+
if (!t2.isObjectProperty(prop))
|
|
198
458
|
continue;
|
|
199
459
|
const key = prop.key;
|
|
200
460
|
let name = null;
|
|
201
|
-
if (
|
|
461
|
+
if (t2.isIdentifier(key))
|
|
202
462
|
name = key.name;
|
|
203
|
-
else if (
|
|
463
|
+
else if (t2.isStringLiteral(key))
|
|
204
464
|
name = key.value;
|
|
205
465
|
if (!name)
|
|
206
466
|
continue;
|
|
207
467
|
const val = prop.value;
|
|
208
|
-
if (
|
|
468
|
+
if (t2.isNullLiteral(val))
|
|
209
469
|
continue;
|
|
210
|
-
if (
|
|
470
|
+
if (t2.isBooleanLiteral(val) && !val.value)
|
|
211
471
|
continue;
|
|
212
472
|
let valueStr = "";
|
|
213
|
-
if (
|
|
473
|
+
if (t2.isStringLiteral(val))
|
|
214
474
|
valueStr = val.value;
|
|
215
|
-
else if (
|
|
475
|
+
else if (t2.isNumericLiteral(val))
|
|
216
476
|
valueStr = String(val.value);
|
|
217
|
-
else if (
|
|
477
|
+
else if (t2.isBooleanLiteral(val))
|
|
218
478
|
valueStr = String(val.value);
|
|
219
479
|
if (valueStr === "")
|
|
220
480
|
continue;
|
|
@@ -259,7 +519,7 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
259
519
|
const expr = attr.value.expression;
|
|
260
520
|
if (expr.type === "JSXEmptyExpression")
|
|
261
521
|
continue;
|
|
262
|
-
if (attrName === "style" &&
|
|
522
|
+
if (attrName === "style" && t2.isExpression(expr) && isStaticStyleValue(expr)) {
|
|
263
523
|
const serialized = serializeStyleValue(expr);
|
|
264
524
|
html += ` ${htmlName}="${escapeHtml(serialized)}"`;
|
|
265
525
|
continue;
|
|
@@ -357,6 +617,7 @@ function transformJSX(code, filename, options = {}) {
|
|
|
357
617
|
sourceFilename: filename
|
|
358
618
|
});
|
|
359
619
|
autoWrapComponents2(ast, filename);
|
|
620
|
+
rewriteLiveCcDestructure(ast);
|
|
360
621
|
wrapReactiveExpressions2(ast, {
|
|
361
622
|
explicitBindings: options.explicitBindings,
|
|
362
623
|
analyze: options.analyze,
|
|
@@ -367,7 +628,7 @@ function transformJSX(code, filename, options = {}) {
|
|
|
367
628
|
const hoist = options.hoist !== false;
|
|
368
629
|
const templates = [];
|
|
369
630
|
let templateCounter = 0;
|
|
370
|
-
|
|
631
|
+
traverse2(ast, {
|
|
371
632
|
JSXElement(path) {
|
|
372
633
|
if (!hoist)
|
|
373
634
|
return;
|
|
@@ -387,9 +648,9 @@ function transformJSX(code, filename, options = {}) {
|
|
|
387
648
|
const tmplId = `_$tmpl_${templateCounter++}`;
|
|
388
649
|
templates.push({ id: tmplId, template: extracted });
|
|
389
650
|
const dynamicExprs = extracted.slots.filter((s) => s.expr).map((s) => s.expr);
|
|
390
|
-
const templateExpr =
|
|
651
|
+
const templateExpr = t2.callExpression(t2.identifier("_$createTemplate"), [t2.identifier(tmplId), t2.arrayExpression(dynamicExprs)]);
|
|
391
652
|
const parentIsJSX = path.parentPath?.isJSXElement() || path.parentPath?.isJSXFragment();
|
|
392
|
-
path.replaceWith(parentIsJSX ?
|
|
653
|
+
path.replaceWith(parentIsJSX ? t2.jsxExpressionContainer(templateExpr) : templateExpr);
|
|
393
654
|
} catch (err) {
|
|
394
655
|
if (options.dev) {
|
|
395
656
|
console.warn(`[Sinwan] template hoisting skipped in ${filename}: ${err.message}`);
|
|
@@ -399,12 +660,12 @@ function transformJSX(code, filename, options = {}) {
|
|
|
399
660
|
});
|
|
400
661
|
if (templates.length === 0) {
|
|
401
662
|
if (options.explicitBindings) {
|
|
402
|
-
const bindingImport =
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
],
|
|
663
|
+
const bindingImport = t2.importDeclaration([
|
|
664
|
+
t2.importSpecifier(t2.identifier("_$bindText"), t2.identifier("_$bindText")),
|
|
665
|
+
t2.importSpecifier(t2.identifier("_$bindAttr"), t2.identifier("_$bindAttr")),
|
|
666
|
+
t2.importSpecifier(t2.identifier("_$bindStyle"), t2.identifier("_$bindStyle")),
|
|
667
|
+
t2.importSpecifier(t2.identifier("_$bindClass"), t2.identifier("_$bindClass"))
|
|
668
|
+
], t2.stringLiteral("sinwan/renderer"));
|
|
408
669
|
ast.program.body.unshift(bindingImport);
|
|
409
670
|
}
|
|
410
671
|
const result = generate(ast, {
|
|
@@ -413,25 +674,25 @@ function transformJSX(code, filename, options = {}) {
|
|
|
413
674
|
});
|
|
414
675
|
return { code: result.code, map: result.map };
|
|
415
676
|
}
|
|
416
|
-
const templateDecls = templates.map(({ id, template }) =>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
677
|
+
const templateDecls = templates.map(({ id, template }) => t2.variableDeclaration("const", [
|
|
678
|
+
t2.variableDeclarator(t2.identifier(id), t2.objectExpression([
|
|
679
|
+
t2.objectProperty(t2.identifier("html"), t2.stringLiteral(template.html)),
|
|
680
|
+
t2.objectProperty(t2.identifier("slots"), t2.arrayExpression(template.slots.map((slot) => t2.objectExpression([
|
|
681
|
+
t2.objectProperty(t2.identifier("path"), t2.arrayExpression(slot.path.map((i) => t2.numericLiteral(i)))),
|
|
682
|
+
t2.objectProperty(t2.identifier("type"), t2.stringLiteral(slot.type)),
|
|
422
683
|
...slot.name ? [
|
|
423
|
-
|
|
684
|
+
t2.objectProperty(t2.identifier("name"), t2.stringLiteral(slot.name))
|
|
424
685
|
] : []
|
|
425
686
|
]))))
|
|
426
687
|
]))
|
|
427
688
|
]));
|
|
428
689
|
const importSpecifiers = [
|
|
429
|
-
|
|
690
|
+
t2.importSpecifier(t2.identifier("_$createTemplate"), t2.identifier("_$createTemplate"))
|
|
430
691
|
];
|
|
431
692
|
if (options.explicitBindings) {
|
|
432
|
-
importSpecifiers.push(
|
|
693
|
+
importSpecifiers.push(t2.importSpecifier(t2.identifier("_$bindText"), t2.identifier("_$bindText")), t2.importSpecifier(t2.identifier("_$bindAttr"), t2.identifier("_$bindAttr")), t2.importSpecifier(t2.identifier("_$bindStyle"), t2.identifier("_$bindStyle")), t2.importSpecifier(t2.identifier("_$bindClass"), t2.identifier("_$bindClass")));
|
|
433
694
|
}
|
|
434
|
-
const importDecl =
|
|
695
|
+
const importDecl = t2.importDeclaration(importSpecifiers, t2.stringLiteral("sinwan/renderer"));
|
|
435
696
|
ast.program.body.unshift(importDecl, ...templateDecls);
|
|
436
697
|
const result = generate(ast, { sourceMaps: true, sourceFileName: filename });
|
|
437
698
|
return { code: result.code, map: result.map };
|
|
@@ -509,6 +770,7 @@ export {
|
|
|
509
770
|
isReactiveValue2 as isReactiveValue,
|
|
510
771
|
loadMetadata2 as loadMetadata,
|
|
511
772
|
propagateReactiveProps2 as propagateReactiveProps,
|
|
773
|
+
rewriteLiveCcDestructure,
|
|
512
774
|
runAnalyzeCli2 as runAnalyzeCli,
|
|
513
775
|
trackReactiveImports2 as trackReactiveImports,
|
|
514
776
|
transformJSX,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite flat `cc` / auto-`cc` object destructure so prop reads stay live.
|
|
3
|
+
*
|
|
4
|
+
* `cc(({ value }) => <span>{value}</span>)` becomes
|
|
5
|
+
* `cc((props) => <span>{props.value}</span>)` before reactive wrap, so JSX
|
|
6
|
+
* emits `() => props.value` instead of snapshotting a destructured local.
|
|
7
|
+
*
|
|
8
|
+
* Nested patterns are left as snapshots. JSX spreads of the props (or rest)
|
|
9
|
+
* identifier use `getSpreadProps` so native attributes keep getter functions.
|
|
10
|
+
*/
|
|
11
|
+
import * as t from "@babel/types";
|
|
12
|
+
/**
|
|
13
|
+
* Rewrite flat `cc` destructure and raw JSX spreads. Returns whether any
|
|
14
|
+
* helper imports are required.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rewriteLiveCcDestructure(ast: t.Node): {
|
|
17
|
+
rest: boolean;
|
|
18
|
+
spread: boolean;
|
|
19
|
+
};
|
package/dist/reactive-wrap.d.ts
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* - `createMutable` / `createStore` from `sinwan/store`
|
|
10
10
|
* - `signal` / `computed` from `sinwan/reactivity`
|
|
11
11
|
* - `useState` from `sinwan/react`
|
|
12
|
+
* - `.value` reads of signals returned from other modules (`useTheme`,
|
|
13
|
+
* `inject`, custom hooks) even when those identifiers were not created
|
|
14
|
+
* by a local `signal()` call
|
|
15
|
+
* - zero-arity getter calls returned from hooks (`counter()`, `api.count()`)
|
|
16
|
+
* the same way local `useState` getters (`count()`) are wrapped
|
|
12
17
|
*
|
|
13
18
|
* Example:
|
|
14
19
|
* <p>{state.name}</p> → <p>{() => state.name}</p>
|
|
@@ -40,6 +45,10 @@ export interface ComputedBinding {
|
|
|
40
45
|
export interface GetterBinding {
|
|
41
46
|
kind: "getter";
|
|
42
47
|
}
|
|
48
|
+
/** Local helper that is not a useState-style getter. `{greet()}` stays eager. */
|
|
49
|
+
export interface FunctionBinding {
|
|
50
|
+
kind: "function";
|
|
51
|
+
}
|
|
43
52
|
/**
|
|
44
53
|
* Binding for a variable holding the object returned by `useFetch()` /
|
|
45
54
|
* `createFetch()(...)`. Every property on that object is a `Signal` or
|
|
@@ -54,7 +63,7 @@ export interface SignalObjectBinding {
|
|
|
54
63
|
export interface PropBinding {
|
|
55
64
|
kind: "prop";
|
|
56
65
|
}
|
|
57
|
-
export type Binding = MutableBinding | SignalBinding | ComputedBinding | GetterBinding | SignalObjectBinding | PropBinding;
|
|
66
|
+
export type Binding = MutableBinding | SignalBinding | ComputedBinding | GetterBinding | FunctionBinding | SignalObjectBinding | PropBinding;
|
|
58
67
|
export interface ReactiveScope {
|
|
59
68
|
bindings: Map<string, Binding>;
|
|
60
69
|
}
|
package/docs/README.md
CHANGED
|
@@ -7,9 +7,10 @@ If you are building a Sinwan app, start with [Plugins](plugins.md). If you are c
|
|
|
7
7
|
## Pipeline
|
|
8
8
|
|
|
9
9
|
1. **Auto-`cc`** — exported functions that look like components (`App`, `Card`, …) are wrapped with `cc(...)` so the rest of the pipeline sees the same components the runtime will.
|
|
10
|
-
2. **
|
|
11
|
-
3. **
|
|
12
|
-
4. **
|
|
10
|
+
2. **Live destructure** — flat `cc(({ value }) => …)` becomes `cc((props) => …)` so `{value}` stays a live `props.value` read. `{...props}` spreads the raw bag.
|
|
11
|
+
3. **Reactive wrap** — JSX expressions that read signals, stores, `useState`, `useFetch`, `.value` on signals returned from other modules (`useTheme`, `inject`), or zero-arity getters returned from hooks (`counter()`) become `() => …` (or explicit binding helpers when that mode is on).
|
|
12
|
+
4. **Template hoist** — static native-element trees become `_$createTemplate(...)` calls with slots for dynamic bits (`child`, `attr`, `event`, `ref`).
|
|
13
|
+
5. **Analyze (optional)** — a project-wide pass marks which *exported* component props are actually reactive, so static strings are not wrapped.
|
|
13
14
|
|
|
14
15
|
```text
|
|
15
16
|
source.tsx
|
|
@@ -18,6 +19,9 @@ source.tsx
|
|
|
18
19
|
autoWrapComponents() plain `export function App()` → cc(App)
|
|
19
20
|
│
|
|
20
21
|
▼
|
|
22
|
+
rewriteLiveCcDestructure() cc(({ value }) => …) → props.value
|
|
23
|
+
│
|
|
24
|
+
▼
|
|
21
25
|
wrapReactiveExpressions() {count.value} → {() => count.value}
|
|
22
26
|
{arr.map(x => n.value)} → {() => arr.map(...)}
|
|
23
27
|
also wraps those reads as children/attrs of <Label>
|
package/docs/api.md
CHANGED
|
@@ -31,6 +31,10 @@ See [Transform](transform.md) for wrapping rules and options.
|
|
|
31
31
|
|
|
32
32
|
Mutates a Babel AST: wrap exported component-like functions with `cc(...)`. Returns `true` if anything changed. `transformJSX` already runs this.
|
|
33
33
|
|
|
34
|
+
### `rewriteLiveCcDestructure(ast)`
|
|
35
|
+
|
|
36
|
+
Mutates a Babel AST: rewrite flat `cc(({ value }) => …)` params to a `props` identifier and `{...props}` spreads to `getSpreadProps(props)`. Nested destructure is left as a snapshot. `transformJSX` already runs this before reactive wrap.
|
|
37
|
+
|
|
34
38
|
## Analysis
|
|
35
39
|
|
|
36
40
|
### `analyze(options)`
|
package/docs/architecture.md
CHANGED
|
@@ -8,6 +8,7 @@ This page is for people changing the compiler. App authors can stay on [Transfor
|
|
|
8
8
|
src/
|
|
9
9
|
index.ts Public exports
|
|
10
10
|
auto-cc.ts Exported function → cc(...)
|
|
11
|
+
live-cc-destructure.ts Flat cc(({ value }) => …) → props.value
|
|
11
12
|
reactive-wrap.ts Imports, scopes, call graph, wrap
|
|
12
13
|
transform.ts Hoist + transformJSX
|
|
13
14
|
analyze.ts Project scan, resolution, AnalyzerCache
|
|
@@ -20,6 +21,7 @@ src/
|
|
|
20
21
|
```text
|
|
21
22
|
transformJSX
|
|
22
23
|
autoWrapComponents
|
|
24
|
+
rewriteLiveCcDestructure ← flat destructure stays live; raw JSX spreads
|
|
23
25
|
wrapReactiveExpressions ← analyze / analyzeMetadata / resolveImport
|
|
24
26
|
hoist JSXElement roots ← _$createTemplate + slots
|
|
25
27
|
|
package/docs/plugins.md
CHANGED
|
@@ -55,7 +55,7 @@ The Vite plugin sets `resolve.dedupe: ["sinwan"]` for the same duplicate-runtime
|
|
|
55
55
|
| `explicitBindings` | `false` | `_$bind*` helpers instead of bare `() =>` |
|
|
56
56
|
| `analyze` | unset | Path to CLI JSON (production) |
|
|
57
57
|
| `cache` | `true` | Incremental analyzer in dev. `false` turns it off. An object sets `root`, `tsConfigPath`, `bunfigPath`, `workspaces`, `cachePath` |
|
|
58
|
-
| `fastRefresh` | `true`
|
|
58
|
+
| `fastRefresh` | Bun: same as `dev`; Vite: `true` in `serve` | Per-component `import.meta.hot.accept` so routed/looked-up pages hot-update |
|
|
59
59
|
|
|
60
60
|
## Dev vs production
|
|
61
61
|
|
package/docs/transform.md
CHANGED
|
@@ -54,6 +54,24 @@ Not wrapped: lowercase helpers, generators, multi-argument functions, or functio
|
|
|
54
54
|
|
|
55
55
|
`cc` may be imported from `sinwan` or `sinwan/component`.
|
|
56
56
|
|
|
57
|
+
## Live `cc` props
|
|
58
|
+
|
|
59
|
+
`cc` setup runs once. The runtime wraps the raw props bag in a Proxy that invokes **zero-arity getters** on each read (`props.value`), while `'value' in props` inspects the raw bag.
|
|
60
|
+
|
|
61
|
+
The compiler rewrites **flat** destructure so those reads stay live:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
export const Child = cc(({ value }) => <span>{value}</span>);
|
|
65
|
+
|
|
66
|
+
// becomes (then wrap)
|
|
67
|
+
export const Child = cc((props) => <span>{() => props.value}</span>);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- Defaults `({ value = "" })` stay JS “undefined → default” **on each read**. Controlled vs omitted still uses `'value' in props`, not a destructure default.
|
|
71
|
+
- Rest `({ value, ...rest })` becomes `const rest = createLiveRest(props, ["value"])`.
|
|
72
|
+
- Nested `({ user: { name } })` is **not** rewritten (setup snapshot).
|
|
73
|
+
- Native or forwarded `{...props}` / `{...rest}` spreads `getSpreadProps(...)` so attributes keep getter functions instead of freezing unwrapped values at setup.
|
|
74
|
+
|
|
57
75
|
## What counts as reactive
|
|
58
76
|
|
|
59
77
|
Tracked imports:
|
|
@@ -70,8 +88,10 @@ Reads the compiler wraps:
|
|
|
70
88
|
- Store / mutable fields: `state.name`, destructured `const { name } = state`
|
|
71
89
|
- Signals and computeds: `count.value`, including `count.value?.n` and `count.value!.n`
|
|
72
90
|
- `useState` getters: `count()`
|
|
91
|
+
- Zero-arity getters **returned from another hook**: `{count()}`, `{counter()}`, `{api.count()}`. Local helpers (`greet()`), calls with arguments (`format("x")`), and event attributes (`onclick={make()}`) stay eager.
|
|
73
92
|
- `useFetch` signal values: `f.data.value` (not the shell `f` or `f.data`)
|
|
74
93
|
- Calls to a **local** function whose body reads one of the above
|
|
94
|
+
- `.value` reads of signals that came from **another module** (`useTheme()`, `inject()`, custom hooks) even when the identifier was not created by a local `signal()` call: `{theme.value}`, `{api.theme.value}`. Nested JSX inside `.map` still wraps the inner slots (`variant={theme.value === value ? …}`) rather than remounting the list.
|
|
75
95
|
- Reads **inside call callbacks** that run while the JSX expression is evaluated: `.map` / `.filter` / `Array.from` callbacks, IIFEs, and default parameters of those callbacks. `<For each={keys.map((k) => groups[query.value])}>` becomes `each={() => keys.map(...)}`. Nested JSX inside those callbacks is a separate wrap site (`items.map((i) => <span>{count.value}</span>)` wraps the inner text, not the whole map).
|
|
76
96
|
|
|
77
97
|
Not wrapped:
|