sinwan-compiler 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,53 @@
1
1
  // @bun
2
2
  import {
3
+ identifier,
4
+ stringLiteral,
5
+ numericLiteral,
6
+ callExpression,
7
+ memberExpression,
8
+ variableDeclarator,
9
+ variableDeclaration,
10
+ arrayExpression,
11
+ objectExpression,
12
+ objectProperty,
13
+ importSpecifier,
14
+ importDeclaration,
15
+ jsxExpressionContainer,
16
+ blockStatement,
17
+ returnStatement,
18
+ binaryExpression,
19
+ conditionalExpression,
20
+ unaryExpression,
21
+ isNode,
22
+ isIdentifier,
23
+ isStringLiteral,
24
+ isNumericLiteral,
25
+ isBooleanLiteral,
26
+ isNullLiteral,
27
+ isTemplateLiteral,
28
+ templateElementRaw,
29
+ isParenthesizedExpression,
30
+ unwrapParens,
31
+ isObjectExpression,
32
+ isObjectProperty,
33
+ isObjectPattern,
34
+ isRestElement,
35
+ isAssignmentPattern,
36
+ isBlockStatement,
37
+ isArrowFunctionExpression,
38
+ isFunction,
39
+ isImportDeclaration,
40
+ isImportSpecifier,
41
+ isJSXIdentifier,
42
+ isJSXOpeningElement,
43
+ isJSXMemberExpression,
44
+ isExpression,
45
+ importedBindingName,
46
+ moduleSourceValue,
47
+ singlePath,
48
+ traverse,
3
49
  autoWrapComponents2,
50
+ parse,
4
51
  analyzeModule2,
5
52
  analyzeProject2,
6
53
  AnalyzerCache2,
@@ -17,15 +64,9 @@ import {
17
64
  propagateReactiveProps2,
18
65
  wrapReactiveExpressions2,
19
66
  runAnalyzeCli2
20
- } from "./chunk-fadpxq9v.js";
21
-
22
- // src/transform.ts
23
- import { parse } from "@babel/parser";
67
+ } from "./chunk-rshqq0t3.js";
24
68
 
25
69
  // src/live-cc-destructure.ts
26
- import * as t from "@babel/types";
27
- import _traverse from "@babel/traverse";
28
- var traverse = typeof _traverse === "function" ? _traverse : _traverse.default ?? _traverse;
29
70
  var CC_SOURCE_MODULES = new Set(["sinwan", "sinwan/component"]);
30
71
  var LIVE_REST_LOCAL = "_$createLiveRest";
31
72
  var SPREAD_LOCAL = "_$getSpreadProps";
@@ -35,13 +76,15 @@ function collectCcAliases(ast) {
35
76
  const aliases = new Set;
36
77
  traverse(ast, {
37
78
  ImportDeclaration(path) {
38
- const source = path.node.source.value;
79
+ const source = path.node.source?.value;
80
+ if (typeof source !== "string")
81
+ return;
39
82
  if (!CC_SOURCE_MODULES.has(source))
40
83
  return;
41
- for (const spec of path.node.specifiers) {
42
- if (!t.isImportSpecifier(spec))
84
+ for (const spec of path.node.specifiers ?? []) {
85
+ if (!isImportSpecifier(spec))
43
86
  continue;
44
- const imported = t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
87
+ const imported = importedBindingName(spec.imported);
45
88
  if (imported === "cc") {
46
89
  aliases.add(spec.local.name);
47
90
  }
@@ -53,26 +96,28 @@ function collectCcAliases(ast) {
53
96
  function parseFlatObjectPattern(pattern) {
54
97
  const props = [];
55
98
  let rest = null;
56
- for (const prop of pattern.properties) {
57
- if (t.isRestElement(prop)) {
58
- if (!t.isIdentifier(prop.argument))
99
+ for (const prop of pattern.properties ?? []) {
100
+ if (isRestElement(prop)) {
101
+ if (!isIdentifier(prop.argument))
59
102
  return null;
60
103
  rest = prop.argument.name;
61
104
  continue;
62
105
  }
63
- if (!t.isObjectProperty(prop) || prop.computed)
106
+ if (!isObjectProperty(prop) || prop.computed)
64
107
  return null;
65
- if (!t.isIdentifier(prop.key) && !t.isStringLiteral(prop.key))
108
+ if (!isIdentifier(prop.key) && !isStringLiteral(prop.key))
109
+ return null;
110
+ if (!prop.value)
66
111
  return null;
67
112
  let value = prop.value;
68
113
  let defaultValue = null;
69
- if (t.isAssignmentPattern(value)) {
114
+ if (isAssignmentPattern(value)) {
70
115
  defaultValue = value.right;
71
116
  value = value.left;
72
117
  }
73
- if (!t.isIdentifier(value))
118
+ if (!isIdentifier(value))
74
119
  return null;
75
- const keyName = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value;
120
+ const keyName = isIdentifier(prop.key) ? prop.key.name : prop.key.value;
76
121
  props.push({
77
122
  key: prop.key,
78
123
  keyName,
@@ -97,36 +142,38 @@ function collectUsedNames(fnPath) {
97
142
  const used = new Set;
98
143
  fnPath.traverse({
99
144
  Identifier(p) {
100
- used.add(p.node.name);
145
+ if (typeof p.node.name === "string")
146
+ used.add(p.node.name);
101
147
  }
102
148
  });
103
149
  return used;
104
150
  }
105
151
  function memberAccess(propsName, key) {
106
- if (t.isIdentifier(key)) {
107
- return t.memberExpression(t.identifier(propsName), t.identifier(key.name));
152
+ if (isIdentifier(key)) {
153
+ return memberExpression(identifier(propsName), identifier(key.name));
108
154
  }
109
- return t.memberExpression(t.identifier(propsName), t.stringLiteral(key.value), true);
155
+ const computedKey = isStringLiteral(key) ? key.value : String(key.value ?? "");
156
+ return memberExpression(identifier(propsName), stringLiteral(computedKey), true);
110
157
  }
111
158
  function liveRead(propsName, binding) {
112
159
  const read = memberAccess(propsName, binding.key);
113
160
  if (!binding.defaultValue)
114
161
  return read;
115
- return t.conditionalExpression(t.binaryExpression("===", memberAccess(propsName, binding.key), t.unaryExpression("void", t.numericLiteral(0))), binding.defaultValue, read);
162
+ return conditionalExpression(binaryExpression("===", memberAccess(propsName, binding.key), unaryExpression("void", numericLiteral(0))), binding.defaultValue, read);
116
163
  }
117
164
  function ensureBlockBody(fn) {
118
- if (t.isBlockStatement(fn.body))
165
+ if (isBlockStatement(fn.body))
119
166
  return fn.body;
120
- const body = t.blockStatement([t.returnStatement(fn.body)]);
167
+ const body = blockStatement([returnStatement(fn.body)]);
121
168
  fn.body = body;
122
- if (t.isArrowFunctionExpression(fn)) {
169
+ if (isArrowFunctionExpression(fn)) {
123
170
  fn.expression = false;
124
171
  }
125
172
  return body;
126
173
  }
127
174
  function replaceWithExpression(path, expr) {
128
175
  const parent = path.parent;
129
- if (t.isObjectProperty(parent) && parent.shorthand && parent.value === path.node) {
176
+ if (isObjectProperty(parent) && parent.shorthand && parent.value === path.node) {
130
177
  parent.shorthand = false;
131
178
  parent.value = expr;
132
179
  return;
@@ -149,14 +196,14 @@ function rewriteSpreads(fnPath, liveNames, fnNode) {
149
196
  fnPath.traverse({
150
197
  JSXSpreadAttribute(p) {
151
198
  const arg = p.node.argument;
152
- if (!t.isIdentifier(arg) || !liveNames.has(arg.name))
199
+ if (!isIdentifier(arg) || !liveNames.has(arg.name))
153
200
  return;
154
201
  const binding = p.scope.getBinding(arg.name);
155
202
  const fnParent = binding?.path.getFunctionParent();
156
203
  if (fnParent && fnParent.node !== fnNode)
157
204
  return;
158
- p.node.argument = t.callExpression(t.identifier(SPREAD_LOCAL), [
159
- t.identifier(arg.name)
205
+ p.node.argument = callExpression(identifier(SPREAD_LOCAL), [
206
+ identifier(arg.name)
160
207
  ]);
161
208
  changed = true;
162
209
  }
@@ -164,22 +211,22 @@ function rewriteSpreads(fnPath, liveNames, fnNode) {
164
211
  return changed;
165
212
  }
166
213
  function hasLocalSpecifier(stmt, local) {
167
- return stmt.specifiers.some((spec) => t.isImportSpecifier(spec) && spec.local.name === local);
214
+ return stmt.specifiers.some((spec) => isImportSpecifier(spec) && spec.local.name === local);
168
215
  }
169
216
  function ensureComponentImport(ast, imported, local) {
170
217
  let target = null;
171
218
  for (const stmt of ast.program.body) {
172
- if (!t.isImportDeclaration(stmt))
219
+ if (!isImportDeclaration(stmt))
173
220
  continue;
174
221
  if (hasLocalSpecifier(stmt, local))
175
222
  return;
176
- if (stmt.source.value === "sinwan/component") {
223
+ if (moduleSourceValue(stmt.source) === "sinwan/component") {
177
224
  target = stmt;
178
- } else if (stmt.source.value === "sinwan" && target === null) {
225
+ } else if (moduleSourceValue(stmt.source) === "sinwan" && target === null) {
179
226
  target = stmt;
180
227
  }
181
228
  }
182
- const spec = t.importSpecifier(t.identifier(local), t.identifier(imported));
229
+ const spec = importSpecifier(identifier(local), identifier(imported));
183
230
  if (target) {
184
231
  target.specifiers.push(spec);
185
232
  }
@@ -193,16 +240,19 @@ function copyPatternType(pattern, id) {
193
240
  }
194
241
  }
195
242
  function rewriteFunctionDestructure(fnPath) {
243
+ if (!isFunction(fnPath.node)) {
244
+ return { rest: false, spread: false };
245
+ }
196
246
  const fn = fnPath.node;
197
247
  const param = fn.params[0];
198
248
  if (!param) {
199
249
  return { rest: false, spread: false };
200
250
  }
201
- if (t.isIdentifier(param)) {
251
+ if (isIdentifier(param)) {
202
252
  const spread = rewriteSpreads(fnPath, new Set([param.name]), fn);
203
253
  return { rest: false, spread };
204
254
  }
205
- if (!t.isObjectPattern(param)) {
255
+ if (!isObjectPattern(param)) {
206
256
  return { rest: false, spread: false };
207
257
  }
208
258
  const flat = parseFlatObjectPattern(param);
@@ -233,16 +283,16 @@ function rewriteFunctionDestructure(fnPath) {
233
283
  replaceWithExpression(p, liveRead(propsName, binding));
234
284
  }
235
285
  });
236
- const propsId = t.identifier(propsName);
286
+ const propsId = identifier(propsName);
237
287
  copyPatternType(param, propsId);
238
288
  fn.params[0] = propsId;
239
289
  const liveNames = new Set([propsName]);
240
290
  if (flat.rest) {
241
291
  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)))
292
+ body.body.unshift(variableDeclaration("const", [
293
+ variableDeclarator(identifier(flat.rest), callExpression(identifier(LIVE_REST_LOCAL), [
294
+ identifier(propsName),
295
+ arrayExpression(flat.props.map((binding) => stringLiteral(binding.keyName)))
246
296
  ]))
247
297
  ]));
248
298
  liveNames.add(flat.rest);
@@ -259,13 +309,15 @@ function rewriteLiveCcDestructure(ast) {
259
309
  traverse(ast, {
260
310
  CallExpression(path) {
261
311
  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))
312
+ if (!isIdentifier(callee) || !aliases.has(callee.name))
266
313
  return;
267
314
  const argPaths = path.get("arguments");
268
- const fnPath = argPaths[0];
315
+ let fnPath = Array.isArray(argPaths) ? argPaths[0] : argPaths;
316
+ if (!fnPath)
317
+ return;
318
+ while (isParenthesizedExpression(fnPath.node)) {
319
+ fnPath = singlePath(fnPath.get("expression"));
320
+ }
269
321
  const result = rewriteFunctionDestructure(fnPath);
270
322
  needsRest = needsRest || result.rest;
271
323
  needsSpread = needsSpread || result.spread;
@@ -279,13 +331,42 @@ function rewriteLiveCcDestructure(ast) {
279
331
  }
280
332
  return { rest: needsRest, spread: needsSpread };
281
333
  }
282
-
334
+ // src/ast/generate.ts
335
+ import { print } from "esrap";
336
+ import tsx from "esrap/languages/tsx";
337
+ function programOf(ast) {
338
+ if (ast.type === "File" && ast.program) {
339
+ return ast.program;
340
+ }
341
+ return ast;
342
+ }
343
+ function esrapComments(list) {
344
+ if (!list || list.length === 0)
345
+ return;
346
+ return list.map((comment) => ({
347
+ type: comment.type === "CommentLine" || comment.type === "Line" ? "Line" : "Block",
348
+ value: comment.value
349
+ }));
350
+ }
351
+ function generate(ast, options = {}, _code) {
352
+ const root = programOf(ast);
353
+ const language = tsx({
354
+ quotes: "double",
355
+ getLeadingComments: (node) => esrapComments(node.leadingComments),
356
+ getTrailingComments: (node) => esrapComments(node.trailingComments)
357
+ });
358
+ const result = print(root, language, {
359
+ sourceMapSource: options.sourceFileName,
360
+ sourceMapContent: _code
361
+ });
362
+ const map = options.sourceMaps && result.map ? {
363
+ ...result.map,
364
+ file: options.sourceFileName,
365
+ sources: options.sourceFileName ? [options.sourceFileName] : result.map.sources
366
+ } : options.sourceMaps ? result.map : null;
367
+ return { code: result.code, map };
368
+ }
283
369
  // 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;
289
370
  var COMPILER_TEMPLATE_SLOT_PROTOCOL = {
290
371
  slotPrefix: "s",
291
372
  encodeSlot(index) {
@@ -360,29 +441,41 @@ function htmlAttributeName(attrName) {
360
441
  return attrName;
361
442
  }
362
443
  function jsxAttrName(attr) {
363
- if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") {
444
+ if (attr.type !== "JSXAttribute")
364
445
  return;
446
+ const n = attr.name;
447
+ if (!n || typeof n === "string")
448
+ return;
449
+ if (n.type === "JSXIdentifier") {
450
+ return typeof n.name === "string" ? n.name : undefined;
451
+ }
452
+ if (n.type === "JSXNamespacedName") {
453
+ const namespace = n.namespace;
454
+ const name = n.name;
455
+ return `${namespace.name}:${name.name}`;
365
456
  }
366
- return attr.name.name;
367
457
  }
368
458
  function hasAttr(attributes, name) {
369
- for (const attr of attributes) {
459
+ for (const attr of attributes ?? []) {
370
460
  if (jsxAttrName(attr) === name)
371
461
  return true;
372
462
  }
373
463
  return false;
374
464
  }
375
465
  function attrIsExpression(attributes, name) {
376
- for (const attr of attributes) {
466
+ for (const attr of attributes ?? []) {
377
467
  if (attr.type !== "JSXAttribute" || jsxAttrName(attr) !== name)
378
468
  continue;
379
- return attr.value?.type === "JSXExpressionContainer";
469
+ return isNode(attr.value) && attr.value.type === "JSXExpressionContainer";
380
470
  }
381
471
  return false;
382
472
  }
383
473
  function elementNeedsJsxEnhancer(node) {
384
- const tagName = jsxNameToString(node.openingElement.name);
385
- const attrs = node.openingElement.attributes;
474
+ const opening = node.openingElement;
475
+ if (!isJSXOpeningElement(opening))
476
+ return false;
477
+ const tagName = jsxNameToString(opening.name);
478
+ const attrs = opening.attributes;
386
479
  switch (tagName) {
387
480
  case "select":
388
481
  return hasAttr(attrs, "defaultValue") || hasAttr(attrs, "value");
@@ -422,17 +515,17 @@ function warnSuspectedStyleInterpolation(attrValue, filename, line) {
422
515
  console.warn('[Sinwan] JSX string literal style attribute contains "${...}" which will not be interpolated. ' + "Use style={`...${...}`} instead. (" + filename + location + ")");
423
516
  }
424
517
  function isLiteralStyleValue(expr) {
425
- return t2.isStringLiteral(expr) || t2.isNumericLiteral(expr) || t2.isBooleanLiteral(expr) || t2.isNullLiteral(expr);
518
+ return isStringLiteral(expr) || isNumericLiteral(expr) || isBooleanLiteral(expr) || isNullLiteral(expr);
426
519
  }
427
520
  function isStaticStyleValue(expr) {
428
- if (t2.isStringLiteral(expr))
521
+ if (isStringLiteral(expr))
429
522
  return true;
430
- if (t2.isTemplateLiteral(expr) && expr.expressions.length === 0)
523
+ if (isTemplateLiteral(expr) && expr.expressions.length === 0)
431
524
  return true;
432
- if (!t2.isObjectExpression(expr))
525
+ if (!isObjectExpression(expr))
433
526
  return false;
434
527
  for (const prop of expr.properties) {
435
- if (!t2.isObjectProperty(prop))
528
+ if (!isObjectProperty(prop))
436
529
  return false;
437
530
  if (prop.computed)
438
531
  return false;
@@ -445,36 +538,36 @@ function toKebabCase(value) {
445
538
  return value.includes("-") ? value : value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
446
539
  }
447
540
  function serializeStyleValue(expr) {
448
- if (t2.isStringLiteral(expr))
541
+ if (isStringLiteral(expr))
449
542
  return expr.value;
450
- if (t2.isTemplateLiteral(expr) && expr.expressions.length === 0) {
451
- return expr.quasis[0]?.value?.raw ?? "";
543
+ if (isTemplateLiteral(expr) && (expr.expressions ?? []).length === 0) {
544
+ return templateElementRaw((expr.quasis ?? [])[0]);
452
545
  }
453
- if (!t2.isObjectExpression(expr))
546
+ if (!isObjectExpression(expr))
454
547
  return "";
455
548
  const parts = [];
456
549
  for (const prop of expr.properties) {
457
- if (!t2.isObjectProperty(prop))
550
+ if (!isObjectProperty(prop))
458
551
  continue;
459
552
  const key = prop.key;
460
553
  let name = null;
461
- if (t2.isIdentifier(key))
554
+ if (isIdentifier(key))
462
555
  name = key.name;
463
- else if (t2.isStringLiteral(key))
556
+ else if (isStringLiteral(key))
464
557
  name = key.value;
465
558
  if (!name)
466
559
  continue;
467
560
  const val = prop.value;
468
- if (t2.isNullLiteral(val))
561
+ if (isNullLiteral(val))
469
562
  continue;
470
- if (t2.isBooleanLiteral(val) && !val.value)
563
+ if (isBooleanLiteral(val) && !val.value)
471
564
  continue;
472
565
  let valueStr = "";
473
- if (t2.isStringLiteral(val))
566
+ if (isStringLiteral(val))
474
567
  valueStr = val.value;
475
- else if (t2.isNumericLiteral(val))
568
+ else if (isNumericLiteral(val))
476
569
  valueStr = String(val.value);
477
- else if (t2.isBooleanLiteral(val))
570
+ else if (isBooleanLiteral(val))
478
571
  valueStr = String(val.value);
479
572
  if (valueStr === "")
480
573
  continue;
@@ -483,7 +576,8 @@ function serializeStyleValue(expr) {
483
576
  return parts.join(";");
484
577
  }
485
578
  function elementToHtml(node, slots, path, filename, counter) {
486
- const tagName = jsxNameToString(node.openingElement.name);
579
+ const opening = node.openingElement;
580
+ const tagName = jsxNameToString(opening?.name);
487
581
  if (tagName === "")
488
582
  return "";
489
583
  const firstChar = tagName.charAt(0);
@@ -493,11 +587,13 @@ function elementToHtml(node, slots, path, filename, counter) {
493
587
  const isVoid = VOID_ELEMENTS.has(tagName);
494
588
  let html = `<${tagName}`;
495
589
  let textareaDefault = "";
496
- for (const attr of node.openingElement.attributes) {
590
+ for (const attr of opening?.attributes ?? []) {
497
591
  if (attr.type === "JSXSpreadAttribute") {
498
592
  throw new Error("Cannot hoist element with spread attributes");
499
593
  }
500
- const attrName = attr.name.name;
594
+ const attrName = jsxAttrName(attr);
595
+ if (!attrName)
596
+ continue;
501
597
  if (attrName === "children")
502
598
  continue;
503
599
  if (attrName === "ref") {
@@ -519,7 +615,7 @@ function elementToHtml(node, slots, path, filename, counter) {
519
615
  const expr = attr.value.expression;
520
616
  if (expr.type === "JSXEmptyExpression")
521
617
  continue;
522
- if (attrName === "style" && t2.isExpression(expr) && isStaticStyleValue(expr)) {
618
+ if (attrName === "style" && isExpression(expr) && isStaticStyleValue(expr)) {
523
619
  const serialized = serializeStyleValue(expr);
524
620
  html += ` ${htmlName}="${escapeHtml(serialized)}"`;
525
621
  continue;
@@ -530,10 +626,12 @@ function elementToHtml(node, slots, path, filename, counter) {
530
626
  name: attrName,
531
627
  expr
532
628
  });
533
- html += ` ${htmlName}=""`;
629
+ if (attrName !== "class" && !attrName.startsWith("on")) {
630
+ html += ` ${htmlName}=""`;
631
+ }
534
632
  continue;
535
633
  }
536
- if (attr.value?.type === "JSXText" || attr.value?.type === "StringLiteral") {
634
+ if (attr.value?.type === "JSXText" || isStringLiteral(attr.value)) {
537
635
  const rawValue = attr.value.value;
538
636
  if (attrName === "style") {
539
637
  warnSuspectedStyleInterpolation(rawValue, filename, attr.value.loc?.start?.line);
@@ -556,7 +654,61 @@ function elementToHtml(node, slots, path, filename, counter) {
556
654
  html += `</${tagName}>`;
557
655
  return html;
558
656
  }
559
- function childrenToHtml(children, slots, path, filename, startIndex, counter) {
657
+ function getSoleDynamicChild(children) {
658
+ const meaningful = [];
659
+ for (const child of children) {
660
+ if (child.type === "JSXText") {
661
+ if (cleanJSXText(child.value ?? ""))
662
+ meaningful.push(child);
663
+ continue;
664
+ }
665
+ if (child.type === "JSXExpressionContainer") {
666
+ if (child.expression.type === "JSXEmptyExpression")
667
+ continue;
668
+ meaningful.push(child);
669
+ continue;
670
+ }
671
+ if (child.type === "JSXElement" || child.type === "JSXFragment") {
672
+ meaningful.push(child);
673
+ }
674
+ }
675
+ if (meaningful.length !== 1)
676
+ return null;
677
+ const only = meaningful[0];
678
+ if (only.type === "JSXFragment")
679
+ return getSoleDynamicChild(only.children);
680
+ if (only.type === "JSXExpressionContainer")
681
+ return only;
682
+ if (only.type === "JSXElement") {
683
+ const childTag = jsxIdentifierTagName(only.openingElement?.name);
684
+ const isComponent = !!childTag && childTag.charAt(0) === childTag.charAt(0).toUpperCase();
685
+ if (isComponent || elementNeedsJsxEnhancer(only))
686
+ return only;
687
+ return null;
688
+ }
689
+ return null;
690
+ }
691
+ function childrenToHtml(children, slots, path, filename, startIndex, counter, allowParentFill = true) {
692
+ if (allowParentFill && startIndex === 0) {
693
+ const sole = getSoleDynamicChild(children);
694
+ if (sole != null) {
695
+ counter.value++;
696
+ if (sole.type === "JSXExpressionContainer") {
697
+ slots.push({
698
+ path: [...path],
699
+ type: "child",
700
+ expr: sole.expression
701
+ });
702
+ } else {
703
+ slots.push({
704
+ path: [...path],
705
+ type: "child",
706
+ expr: sole
707
+ });
708
+ }
709
+ return { html: "", childIndex: 0 };
710
+ }
711
+ }
560
712
  let html = "";
561
713
  let childIndex = startIndex;
562
714
  for (const child of children) {
@@ -567,8 +719,18 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
567
719
  childIndex++;
568
720
  }
569
721
  } else if (child.type === "JSXExpressionContainer") {
570
- if (child.expression.type === "JSXEmptyExpression")
722
+ if (child.expression.type === "JSXEmptyExpression") {
723
+ const comments = [
724
+ ...child.expression.leadingComments ?? [],
725
+ ...child.leadingComments ?? []
726
+ ];
727
+ for (const comment of comments) {
728
+ if (comment?.type === "CommentBlock" && typeof comment.value === "string") {
729
+ html += `<!--${comment.value.replace(/--/g, "")}-->`;
730
+ }
731
+ }
571
732
  continue;
733
+ }
572
734
  const slot = nextSlotId(counter);
573
735
  slots.push({
574
736
  path: [...path, childIndex],
@@ -578,8 +740,7 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
578
740
  html += slot;
579
741
  childIndex++;
580
742
  } else if (child.type === "JSXElement") {
581
- const childName = child.openingElement.name;
582
- const childTag = childName.type === "JSXIdentifier" ? childName.name : "";
743
+ const childTag = jsxIdentifierTagName(child.openingElement?.name);
583
744
  const isFirstCharUpper = childTag && childTag.charAt(0) === childTag.charAt(0).toUpperCase();
584
745
  if (isFirstCharUpper || elementNeedsJsxEnhancer(child)) {
585
746
  const slot = nextSlotId(counter);
@@ -595,7 +756,7 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
595
756
  childIndex++;
596
757
  }
597
758
  } else if (child.type === "JSXFragment") {
598
- const fragResult = childrenToHtml(child.children, slots, path, filename, childIndex, counter);
759
+ const fragResult = childrenToHtml(child.children, slots, path, filename, childIndex, counter, false);
599
760
  html += fragResult.html;
600
761
  childIndex = fragResult.childIndex;
601
762
  }
@@ -603,13 +764,43 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
603
764
  return { html, childIndex };
604
765
  }
605
766
  function jsxNameToString(name) {
606
- if (name.type === "JSXIdentifier")
767
+ if (isJSXIdentifier(name))
607
768
  return name.name;
608
- if (name.type === "JSXMemberExpression") {
769
+ if (isJSXMemberExpression(name)) {
609
770
  return jsxNameToString(name.object) + "." + jsxNameToString(name.property);
610
771
  }
611
772
  return "";
612
773
  }
774
+ function jsxIdentifierTagName(name) {
775
+ return isJSXIdentifier(name) ? name.name : "";
776
+ }
777
+ function rendererHelperImported(body, local) {
778
+ for (const statement of body) {
779
+ if (!isImportDeclaration(statement))
780
+ continue;
781
+ const source = moduleSourceValue(statement.source);
782
+ if (source !== "sinwan/renderer" && source !== "sinwan/renderer/web") {
783
+ continue;
784
+ }
785
+ for (const specifier of statement.specifiers ?? []) {
786
+ if (isImportSpecifier(specifier) && isIdentifier(specifier.local) && specifier.local.name === local) {
787
+ return true;
788
+ }
789
+ }
790
+ }
791
+ return false;
792
+ }
793
+ function isStructuralJsxChild(path) {
794
+ let cur = path.parentPath;
795
+ while (cur) {
796
+ if (cur.isJSXExpressionContainer())
797
+ return false;
798
+ if (cur.isJSXElement())
799
+ return true;
800
+ cur = cur.parentPath;
801
+ }
802
+ return false;
803
+ }
613
804
  function transformJSX(code, filename, options = {}) {
614
805
  const ast = parse(code, {
615
806
  sourceType: "module",
@@ -619,6 +810,8 @@ function transformJSX(code, filename, options = {}) {
619
810
  autoWrapComponents2(ast, filename);
620
811
  rewriteLiveCcDestructure(ast);
621
812
  wrapReactiveExpressions2(ast, {
813
+ derivedLocals: options.derivedLocals,
814
+ dev: options.dev,
622
815
  explicitBindings: options.explicitBindings,
623
816
  analyze: options.analyze,
624
817
  analyzeMetadata: options.analyzeMetadata,
@@ -628,18 +821,20 @@ function transformJSX(code, filename, options = {}) {
628
821
  const hoist = options.hoist !== false;
629
822
  const templates = [];
630
823
  let templateCounter = 0;
631
- traverse2(ast, {
824
+ traverse(ast, {
632
825
  JSXElement(path) {
633
826
  if (!hoist)
634
827
  return;
635
- if (path.findParent((p) => p.isJSXElement()))
828
+ if (isStructuralJsxChild(path))
636
829
  return;
637
830
  const opening = path.node.openingElement;
638
- const tagName = opening.name;
639
- if (tagName.type !== "JSXIdentifier")
831
+ if (!isJSXOpeningElement(opening))
640
832
  return;
641
- const name = tagName.name;
642
- if (!name || name[0] !== name[0].toLowerCase())
833
+ if (!isJSXIdentifier(opening.name))
834
+ return;
835
+ const name = opening.name.name;
836
+ const first = name[0];
837
+ if (!first || first !== first.toLowerCase())
643
838
  return;
644
839
  if (elementNeedsJsxEnhancer(path.node))
645
840
  return;
@@ -648,9 +843,9 @@ function transformJSX(code, filename, options = {}) {
648
843
  const tmplId = `_$tmpl_${templateCounter++}`;
649
844
  templates.push({ id: tmplId, template: extracted });
650
845
  const dynamicExprs = extracted.slots.filter((s) => s.expr).map((s) => s.expr);
651
- const templateExpr = t2.callExpression(t2.identifier("_$createTemplate"), [t2.identifier(tmplId), t2.arrayExpression(dynamicExprs)]);
846
+ const templateExpr = callExpression(identifier("_$createTemplate"), [identifier(tmplId), arrayExpression(dynamicExprs)]);
652
847
  const parentIsJSX = path.parentPath?.isJSXElement() || path.parentPath?.isJSXFragment();
653
- path.replaceWith(parentIsJSX ? t2.jsxExpressionContainer(templateExpr) : templateExpr);
848
+ path.replaceWith(parentIsJSX ? jsxExpressionContainer(templateExpr) : templateExpr);
654
849
  } catch (err) {
655
850
  if (options.dev) {
656
851
  console.warn(`[Sinwan] template hoisting skipped in ${filename}: ${err.message}`);
@@ -660,13 +855,15 @@ function transformJSX(code, filename, options = {}) {
660
855
  });
661
856
  if (templates.length === 0) {
662
857
  if (options.explicitBindings) {
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"));
669
- ast.program.body.unshift(bindingImport);
858
+ const bindingImport = importDeclaration([
859
+ importSpecifier(identifier("_$bindText"), identifier("_$bindText")),
860
+ importSpecifier(identifier("_$bindAttr"), identifier("_$bindAttr")),
861
+ importSpecifier(identifier("_$bindStyle"), identifier("_$bindStyle")),
862
+ importSpecifier(identifier("_$bindClass"), identifier("_$bindClass"))
863
+ ], stringLiteral("sinwan/renderer/web"));
864
+ bindingImport.specifiers = (bindingImport.specifiers ?? []).filter((candidate) => isImportSpecifier(candidate) && isIdentifier(candidate.local) && !rendererHelperImported(ast.program.body, candidate.local.name));
865
+ if (bindingImport.specifiers.length > 0)
866
+ ast.program.body.unshift(bindingImport);
670
867
  }
671
868
  const result = generate(ast, {
672
869
  sourceMaps: true,
@@ -674,38 +871,49 @@ function transformJSX(code, filename, options = {}) {
674
871
  });
675
872
  return { code: result.code, map: result.map };
676
873
  }
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)),
874
+ const templateDecls = templates.map(({ id, template }) => variableDeclaration("const", [
875
+ variableDeclarator(identifier(id), objectExpression([
876
+ objectProperty(identifier("html"), stringLiteral(template.html)),
877
+ objectProperty(identifier("slots"), arrayExpression(template.slots.map((slot) => objectExpression([
878
+ objectProperty(identifier("path"), arrayExpression(slot.path.map((i) => numericLiteral(i)))),
879
+ objectProperty(identifier("type"), stringLiteral(slot.type)),
683
880
  ...slot.name ? [
684
- t2.objectProperty(t2.identifier("name"), t2.stringLiteral(slot.name))
881
+ objectProperty(identifier("name"), stringLiteral(slot.name))
685
882
  ] : []
686
883
  ]))))
687
884
  ]))
688
885
  ]));
689
886
  const importSpecifiers = [
690
- t2.importSpecifier(t2.identifier("_$createTemplate"), t2.identifier("_$createTemplate"))
887
+ importSpecifier(identifier("_$createTemplate"), identifier("_$createTemplate"))
691
888
  ];
692
889
  if (options.explicitBindings) {
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")));
890
+ importSpecifiers.push(importSpecifier(identifier("_$bindText"), identifier("_$bindText")), importSpecifier(identifier("_$bindAttr"), identifier("_$bindAttr")), importSpecifier(identifier("_$bindStyle"), identifier("_$bindStyle")), importSpecifier(identifier("_$bindClass"), identifier("_$bindClass")));
891
+ }
892
+ const freshSpecifiers = importSpecifiers.filter((specifier) => {
893
+ const local = specifier.local;
894
+ return isIdentifier(local) && !rendererHelperImported(ast.program.body, local.name);
895
+ });
896
+ ast.program.body.unshift(...templateDecls);
897
+ if (freshSpecifiers.length > 0) {
898
+ ast.program.body.unshift(importDeclaration(freshSpecifiers, stringLiteral("sinwan/renderer/web")));
694
899
  }
695
- const importDecl = t2.importDeclaration(importSpecifiers, t2.stringLiteral("sinwan/renderer"));
696
- ast.program.body.unshift(importDecl, ...templateDecls);
697
900
  const result = generate(ast, { sourceMaps: true, sourceFileName: filename });
698
901
  return { code: result.code, map: result.map };
699
902
  }
700
903
  // src/exports.ts
701
- import { parse as parse2 } from "@babel/parser";
702
904
  function isComponentName(name) {
703
905
  return !!name && name[0] === name[0].toUpperCase() && /^[A-Z]/.test(name);
704
906
  }
907
+ function isTypeOnlyExport(node) {
908
+ return node.exportKind === "type";
909
+ }
910
+ function isIdent(n) {
911
+ return !!n && typeof n === "object" && n.type === "Identifier" && typeof n.name === "string";
912
+ }
705
913
  function collectExportedComponents(code, filename) {
706
914
  let ast;
707
915
  try {
708
- ast = parse2(code, {
916
+ ast = parse(code, {
709
917
  sourceType: "module",
710
918
  plugins: ["jsx", "typescript"],
711
919
  sourceFilename: filename
@@ -724,30 +932,35 @@ function collectExportedComponents(code, filename) {
724
932
  };
725
933
  for (const node of ast.program.body) {
726
934
  if (node.type === "ExportNamedDeclaration") {
727
- if (node.declaration) {
728
- const decl = node.declaration;
729
- if (decl.type === "FunctionDeclaration" && decl.id && isComponentName(decl.id.name)) {
730
- add(decl.id.name, decl.id.name);
731
- } else if (decl.type === "VariableDeclaration") {
732
- for (const d of decl.declarations) {
733
- if (d.id?.type === "Identifier" && isComponentName(d.id.name)) {
935
+ if (isTypeOnlyExport(node))
936
+ continue;
937
+ const declaration = node.declaration;
938
+ if (declaration) {
939
+ if (declaration.type === "FunctionDeclaration" && isIdent(declaration.id) && isComponentName(declaration.id.name)) {
940
+ add(declaration.id.name, declaration.id.name);
941
+ } else if (declaration.type === "VariableDeclaration") {
942
+ for (const d of declaration.declarations ?? []) {
943
+ if (isIdent(d.id) && isComponentName(d.id.name)) {
734
944
  add(d.id.name, d.id.name);
735
945
  }
736
946
  }
737
947
  }
738
948
  } else if (node.specifiers && !node.source) {
739
949
  for (const spec of node.specifiers) {
740
- if (spec.type === "ExportSpecifier" && spec.local?.type === "Identifier" && spec.exported?.type === "Identifier" && isComponentName(spec.exported.name)) {
950
+ if (isTypeOnlyExport(spec))
951
+ continue;
952
+ if (spec.type === "ExportSpecifier" && isIdent(spec.local) && isIdent(spec.exported) && isComponentName(spec.exported.name)) {
741
953
  add(spec.local.name, spec.exported.name);
742
954
  }
743
955
  }
744
956
  }
745
957
  } else if (node.type === "ExportDefaultDeclaration") {
746
- const decl = node.declaration;
747
- if (decl.type === "Identifier") {
748
- add(decl.name, "default");
749
- } else if (decl.type === "FunctionDeclaration" && decl.id) {
750
- add(decl.id.name, "default");
958
+ const raw = node.declaration;
959
+ const declaration = raw && typeof raw === "object" && "type" in raw ? unwrapParens(raw) : raw;
960
+ if (isIdent(declaration)) {
961
+ add(declaration.name, "default");
962
+ } else if (declaration.type === "FunctionDeclaration" && isIdent(declaration.id)) {
963
+ add(declaration.id.name, "default");
751
964
  }
752
965
  }
753
966
  }