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.
@@ -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 false;
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 (!t.isIdentifier(callee))
397
- return false;
398
- const binding = scope.bindings.get(callee.name);
399
- return binding?.kind === "getter";
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 false;
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 (t.isCallExpression(node) && isGetterCall(node, scope)) {
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 (t.isCallExpression(node) && isGetterCall(node, scope)) {
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: null };
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 createBindingDescriptor(expr, exprPath, options) {
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 (t.isIdentifier(expr, { name: "children" }) || compInfo.isComponent && compInfo.attributeName === null) {
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
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  runAnalyzeCli2,
5
5
  runCliIfMain2
6
- } from "./chunk-qj8m16x2.js";
6
+ } from "./chunk-3e981ff7.js";
7
7
  export {
8
8
  runAnalyzeCli2 as runAnalyzeCli,
9
9
  runCliIfMain2 as runCliIfMain
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-qj8m16x2.js";
20
+ } from "./chunk-3e981ff7.js";
21
21
 
22
22
  // src/transform.ts
23
23
  import { parse } from "@babel/parser";
24
- import _generate from "@babel/generator";
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 t.isStringLiteral(expr) || t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) || t.isNullLiteral(expr);
425
+ return t2.isStringLiteral(expr) || t2.isNumericLiteral(expr) || t2.isBooleanLiteral(expr) || t2.isNullLiteral(expr);
166
426
  }
167
427
  function isStaticStyleValue(expr) {
168
- if (t.isStringLiteral(expr))
428
+ if (t2.isStringLiteral(expr))
169
429
  return true;
170
- if (t.isTemplateLiteral(expr) && expr.expressions.length === 0)
430
+ if (t2.isTemplateLiteral(expr) && expr.expressions.length === 0)
171
431
  return true;
172
- if (!t.isObjectExpression(expr))
432
+ if (!t2.isObjectExpression(expr))
173
433
  return false;
174
434
  for (const prop of expr.properties) {
175
- if (!t.isObjectProperty(prop))
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 (t.isStringLiteral(expr))
448
+ if (t2.isStringLiteral(expr))
189
449
  return expr.value;
190
- if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
450
+ if (t2.isTemplateLiteral(expr) && expr.expressions.length === 0) {
191
451
  return expr.quasis[0]?.value?.raw ?? "";
192
452
  }
193
- if (!t.isObjectExpression(expr))
453
+ if (!t2.isObjectExpression(expr))
194
454
  return "";
195
455
  const parts = [];
196
456
  for (const prop of expr.properties) {
197
- if (!t.isObjectProperty(prop))
457
+ if (!t2.isObjectProperty(prop))
198
458
  continue;
199
459
  const key = prop.key;
200
460
  let name = null;
201
- if (t.isIdentifier(key))
461
+ if (t2.isIdentifier(key))
202
462
  name = key.name;
203
- else if (t.isStringLiteral(key))
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 (t.isNullLiteral(val))
468
+ if (t2.isNullLiteral(val))
209
469
  continue;
210
- if (t.isBooleanLiteral(val) && !val.value)
470
+ if (t2.isBooleanLiteral(val) && !val.value)
211
471
  continue;
212
472
  let valueStr = "";
213
- if (t.isStringLiteral(val))
473
+ if (t2.isStringLiteral(val))
214
474
  valueStr = val.value;
215
- else if (t.isNumericLiteral(val))
475
+ else if (t2.isNumericLiteral(val))
216
476
  valueStr = String(val.value);
217
- else if (t.isBooleanLiteral(val))
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" && t.isExpression(expr) && isStaticStyleValue(expr)) {
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
- traverse(ast, {
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 = t.callExpression(t.identifier("_$createTemplate"), [t.identifier(tmplId), t.arrayExpression(dynamicExprs)]);
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 ? t.jsxExpressionContainer(templateExpr) : templateExpr);
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 = t.importDeclaration([
403
- t.importSpecifier(t.identifier("_$bindText"), t.identifier("_$bindText")),
404
- t.importSpecifier(t.identifier("_$bindAttr"), t.identifier("_$bindAttr")),
405
- t.importSpecifier(t.identifier("_$bindStyle"), t.identifier("_$bindStyle")),
406
- t.importSpecifier(t.identifier("_$bindClass"), t.identifier("_$bindClass"))
407
- ], t.stringLiteral("sinwan/renderer"));
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 }) => t.variableDeclaration("const", [
417
- t.variableDeclarator(t.identifier(id), t.objectExpression([
418
- t.objectProperty(t.identifier("html"), t.stringLiteral(template.html)),
419
- t.objectProperty(t.identifier("slots"), t.arrayExpression(template.slots.map((slot) => t.objectExpression([
420
- t.objectProperty(t.identifier("path"), t.arrayExpression(slot.path.map((i) => t.numericLiteral(i)))),
421
- t.objectProperty(t.identifier("type"), t.stringLiteral(slot.type)),
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
- t.objectProperty(t.identifier("name"), t.stringLiteral(slot.name))
684
+ t2.objectProperty(t2.identifier("name"), t2.stringLiteral(slot.name))
424
685
  ] : []
425
686
  ]))))
426
687
  ]))
427
688
  ]));
428
689
  const importSpecifiers = [
429
- t.importSpecifier(t.identifier("_$createTemplate"), t.identifier("_$createTemplate"))
690
+ t2.importSpecifier(t2.identifier("_$createTemplate"), t2.identifier("_$createTemplate"))
430
691
  ];
431
692
  if (options.explicitBindings) {
432
- importSpecifiers.push(t.importSpecifier(t.identifier("_$bindText"), t.identifier("_$bindText")), t.importSpecifier(t.identifier("_$bindAttr"), t.identifier("_$bindAttr")), t.importSpecifier(t.identifier("_$bindStyle"), t.identifier("_$bindStyle")), t.importSpecifier(t.identifier("_$bindClass"), t.identifier("_$bindClass")));
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 = t.importDeclaration(importSpecifiers, t.stringLiteral("sinwan/renderer"));
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
+ };
@@ -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. **Reactive wrap** — JSX expressions that read signals, stores, `useState`, or `useFetch` become `() => …` (or explicit binding helpers when that mode is on).
11
- 3. **Template hoist** — static native-element trees become `_$createTemplate(...)` calls with slots for dynamic bits (`child`, `attr`, `event`, `ref`).
12
- 4. **Analyze (optional)** — a project-wide pass marks which *exported* component props are actually reactive, so static strings are not wrapped.
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)`
@@ -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` (Vite) | In-place component HMR in `vite serve` only |
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinwan-compiler",
3
- "version": "0.2.10",
3
+ "version": "0.3.0",
4
4
  "description": "Shared compiler core for Sinwan plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",