sinwan-compiler 0.2.11 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ad9mh6gz.js → chunk-fadpxq9v.js} +27 -4
- 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/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 +18 -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) {
|
|
@@ -678,7 +682,25 @@ function transformPropMemberAccess(expr, scope) {
|
|
|
678
682
|
function wrapExpression(expr) {
|
|
679
683
|
return t.arrowFunctionExpression([], expr);
|
|
680
684
|
}
|
|
681
|
-
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) {
|
|
682
704
|
if (!options.explicitBindings) {
|
|
683
705
|
return wrapExpression(expr);
|
|
684
706
|
}
|
|
@@ -705,7 +727,7 @@ function createBindingDescriptor(expr, exprPath, options) {
|
|
|
705
727
|
]);
|
|
706
728
|
}
|
|
707
729
|
const compInfo = getComponentExpressionInfo(exprPath);
|
|
708
|
-
if (
|
|
730
|
+
if (isChildrenPropRead(expr, scope) || childrenGetter || compInfo.isComponent && compInfo.attributeName === null) {
|
|
709
731
|
return wrapExpression(expr);
|
|
710
732
|
}
|
|
711
733
|
return t.callExpression(t.identifier("_$bindText"), [wrapExpression(expr)]);
|
|
@@ -952,7 +974,7 @@ function wrapReactiveExpressions2(ast, options = {}) {
|
|
|
952
974
|
}
|
|
953
975
|
if (source === "sinwan/reactivity") {
|
|
954
976
|
for (const spec of p.node.specifiers) {
|
|
955
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported) && spec.imported.name === "unwrap") {
|
|
977
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported) && spec.imported.name === "unwrap" && spec.local.name === RESOLVE_HELPER) {
|
|
956
978
|
hasUnwrapImport = true;
|
|
957
979
|
}
|
|
958
980
|
}
|
|
@@ -1152,10 +1174,11 @@ function wrapReactiveExpressions2(ast, options = {}) {
|
|
|
1152
1174
|
}
|
|
1153
1175
|
}
|
|
1154
1176
|
if (shouldWrap(expr, scope)) {
|
|
1177
|
+
const childrenGetter = isChildrenPropRead(expr, scope);
|
|
1155
1178
|
if (transformPropMemberAccess(expr, scope)) {
|
|
1156
1179
|
needsResolve = true;
|
|
1157
1180
|
}
|
|
1158
|
-
exprPath.replaceWith(t.jsxExpressionContainer(createBindingDescriptor(expr, exprPath, options)));
|
|
1181
|
+
exprPath.replaceWith(t.jsxExpressionContainer(createBindingDescriptor(expr, exprPath, options, scope, childrenGetter)));
|
|
1159
1182
|
}
|
|
1160
1183
|
}
|
|
1161
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-fadpxq9v.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/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:
|