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.
@@ -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 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) {
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 (t.isIdentifier(expr, { name: "children" }) || compInfo.isComponent && compInfo.attributeName === null) {
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
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  runAnalyzeCli2,
5
5
  runCliIfMain2
6
- } from "./chunk-ad9mh6gz.js";
6
+ } from "./chunk-fadpxq9v.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-ad9mh6gz.js";
20
+ } from "./chunk-fadpxq9v.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
+ };
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`, `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).
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinwan-compiler",
3
- "version": "0.2.11",
3
+ "version": "0.3.1",
4
4
  "description": "Shared compiler core for Sinwan plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",