@astralsight/astroforge-rsbuild-plugin 0.0.6 → 0.0.8

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.d.mts CHANGED
@@ -134,6 +134,7 @@ type StyleSlotValue = {
134
134
  };
135
135
  interface Binding {
136
136
  path: string;
137
+ expr?: string;
137
138
  is_callable: boolean;
138
139
  }
139
140
  interface Conditional {
package/dist/index.mjs CHANGED
@@ -322,8 +322,9 @@ function extractPageModuleFromTsx(source, options) {
322
322
  });
323
323
  const bindings = collectAstroForgeImports(ast.program.body);
324
324
  const pageFunction = findDefaultPageFunction(ast.program.body, options.filename);
325
- const script = extractScript(source, ast.program.body, pageFunction.node, options.filename);
326
- const template = templateFromRenderExpression(pageFunction.renderExpression, bindings, options.filename);
325
+ const scriptExtraction = extractScript(source, ast.program.body, pageFunction.node, options.filename);
326
+ const script = scriptExtraction.script;
327
+ const template = templateFromRenderExpression(pageFunction.renderExpression, bindings, createTemplateContext(source, options.filename, scriptExtraction.context));
327
328
  const components = extractLocalComponents(source, ast.program.body, pageFunction.node, bindings, options.filename);
328
329
  const componentImports = collectComponentImports(ast.program.body, bindings, Object.keys(components));
329
330
  const imports = importsFromTemplate(template, components);
@@ -346,16 +347,17 @@ function extractComponentFromTsx(source, options) {
346
347
  });
347
348
  const bindings = collectAstroForgeImports(ast.program.body);
348
349
  const located = locateComponentFunction(ast.program.body, options.exportName, options.filename);
349
- const template = templateFromRenderExpression(located.renderExpression, bindings, options.filename);
350
+ const script = {
351
+ ...createEmptyScript(),
352
+ props: extractComponentProps(located.node, ast.program.body, options.filename)
353
+ };
354
+ const template = templateFromRenderExpression(located.renderExpression, bindings, createTemplateContext(source, options.filename));
350
355
  const componentImports = collectComponentImports(ast.program.body, bindings, []);
351
356
  return {
352
357
  component: {
353
358
  name: kebabCase(located.localName),
354
359
  template,
355
- script: {
356
- ...createEmptyScript(),
357
- props: extractComponentProps(located.node, ast.program.body, options.filename)
358
- },
360
+ script,
359
361
  style: extractStyleTable(source, ast.program.body, options.filename, options.loadStyle)
360
362
  },
361
363
  componentImports
@@ -467,7 +469,7 @@ function extractLocalComponents(source, body, pageFunction, bindings, filename)
467
469
  const name = kebabCase(candidate.name);
468
470
  components[name] = {
469
471
  name,
470
- template: templateFromRenderExpression(renderExpressionFromFunction(fn, filename), bindings, filename),
472
+ template: templateFromRenderExpression(renderExpressionFromFunction(fn, filename), bindings, createTemplateContext(source, filename)),
471
473
  script: {
472
474
  ...createEmptyScript(),
473
475
  props: extractComponentProps(fn, body, filename)
@@ -655,13 +657,19 @@ function findDefaultPageFunction(body, filename) {
655
657
  function extractScript(source, moduleBody, pageFunction, filename) {
656
658
  const script = createEmptyScript();
657
659
  const body = unwrapExpression(pageFunction.body);
658
- if (body.type !== "BlockStatement") return script;
659
660
  const context = createScriptContext(source, filename);
661
+ if (body.type !== "BlockStatement") return {
662
+ script,
663
+ context
664
+ };
660
665
  for (const statement of body.body) collectUseStateDeclaration(script, context, statement);
661
666
  for (const statement of body.body) collectMethod(script, context, statement);
662
667
  collectPageLifecycle(script, context, moduleBody);
663
668
  collectUseEffectCalls(script, context, body.body);
664
- return script;
669
+ return {
670
+ script,
671
+ context
672
+ };
665
673
  }
666
674
  function createScriptContext(source, filename) {
667
675
  return {
@@ -671,6 +679,22 @@ function createScriptContext(source, filename) {
671
679
  stateSetters: /* @__PURE__ */ new Map()
672
680
  };
673
681
  }
682
+ function createTemplateContext(source, filename, scriptContext = createScriptContext(source, filename), aliases = /* @__PURE__ */ new Map()) {
683
+ return {
684
+ source,
685
+ filename,
686
+ scriptContext,
687
+ aliases
688
+ };
689
+ }
690
+ function withTemplateAliases(context, aliases) {
691
+ const next = new Map(context.aliases);
692
+ for (const [source, target] of aliases) next.set(source, target);
693
+ return {
694
+ ...context,
695
+ aliases: next
696
+ };
697
+ }
674
698
  function collectUseStateDeclaration(script, context, statement) {
675
699
  if (statement.type !== "VariableDeclaration") return;
676
700
  for (const declarator of statement.declarations) {
@@ -881,6 +905,180 @@ function lowerStateSetterCall(context, stateName, node) {
881
905
  }
882
906
  return `this.${stateName} = ${lowerExpression(context, expression)}`;
883
907
  }
908
+ function lowerTemplateEventHandler(context, node) {
909
+ const fn = unwrapExpression(node);
910
+ const firstParam = fn.params?.[0];
911
+ const aliases = [];
912
+ if (firstParam?.type === "Identifier") aliases.push([firstParam.name, "evt"]);
913
+ return `function(evt) ${lowerTemplateFunctionBody(withTemplateAliases(context, aliases), fn.body)}`;
914
+ }
915
+ function lowerTemplateFunctionBody(context, body) {
916
+ const node = unwrapExpression(body);
917
+ if (node.type !== "BlockStatement") return `{ return ${lowerTemplateExpression(context, node)}; }`;
918
+ const statements = node.body.map((statement) => lowerTemplateStatement(context, statement)).filter(Boolean);
919
+ if (statements.length === 0) return "{}";
920
+ return `{\n${statements.map((statement) => ` ${statement}`).join("\n")}\n}`;
921
+ }
922
+ function lowerTemplateStatement(context, statement) {
923
+ switch (statement.type) {
924
+ case "ExpressionStatement": return `${lowerTemplateExpression(context, statement.expression)};`;
925
+ case "ReturnStatement": return statement.argument ? `return ${lowerTemplateExpression(context, statement.argument)};` : "return;";
926
+ case "VariableDeclaration": return lowerTemplateVariableDeclaration(context, statement);
927
+ case "IfStatement": return lowerTemplateIfStatement(context, statement);
928
+ case "BlockStatement": {
929
+ const body = statement.body.map((item) => lowerTemplateStatement(context, item)).filter(Boolean).map((item) => ` ${item}`).join("\n");
930
+ return body ? `{\n${body}\n}` : "{}";
931
+ }
932
+ default: throw new Error(`${context.filename ?? "TSX"}: 内联事件暂不支持 ${statement.type}`);
933
+ }
934
+ }
935
+ function lowerTemplateVariableDeclaration(context, statement) {
936
+ const declarations = statement.declarations.map((declarator) => {
937
+ const id = sourceForNode(context.source, declarator.id);
938
+ if (!declarator.init) return id;
939
+ return `${id} = ${lowerTemplateExpression(context, declarator.init)}`;
940
+ });
941
+ return `${statement.kind} ${declarations.join(", ")};`;
942
+ }
943
+ function lowerTemplateIfStatement(context, statement) {
944
+ const test = lowerTemplateExpression(context, statement.test);
945
+ const consequent = lowerTemplateStatementAsBlock(context, statement.consequent);
946
+ if (!statement.alternate) return `if (${test}) ${consequent}`;
947
+ return `if (${test}) ${consequent} else ${statement.alternate.type === "IfStatement" ? lowerTemplateIfStatement(context, statement.alternate) : lowerTemplateStatementAsBlock(context, statement.alternate)}`;
948
+ }
949
+ function lowerTemplateStatementAsBlock(context, statement) {
950
+ if (statement.type === "BlockStatement") return lowerTemplateStatement(context, statement);
951
+ return `{\n ${lowerTemplateStatement(context, statement)}\n}`;
952
+ }
953
+ function lowerTemplateExpression(context, expression) {
954
+ const node = unwrapExpression(expression);
955
+ switch (node.type) {
956
+ case "Identifier": return lowerTemplateIdentifier(context, node.name);
957
+ case "StringLiteral": return JSON.stringify(node.value);
958
+ case "NumericLiteral": return String(node.value);
959
+ case "BooleanLiteral": return String(node.value);
960
+ case "NullLiteral": return "null";
961
+ case "TemplateLiteral": return lowerTemplateLiteral(context, node);
962
+ case "ConditionalExpression": return `${lowerTemplateExpression(context, node.test)} ? ${lowerTemplateExpression(context, node.consequent)} : ${lowerTemplateExpression(context, node.alternate)}`;
963
+ case "BinaryExpression":
964
+ case "LogicalExpression": return `${lowerTemplateExpression(context, node.left)} ${node.operator} ${lowerTemplateExpression(context, node.right)}`;
965
+ case "UnaryExpression": return `${node.operator}${lowerTemplateExpression(context, node.argument)}`;
966
+ case "AssignmentExpression": return `${lowerTemplateExpression(context, node.left)} ${node.operator} ${lowerTemplateExpression(context, node.right)}`;
967
+ case "UpdateExpression": {
968
+ const argument = lowerTemplateExpression(context, node.argument);
969
+ return node.prefix ? `${node.operator}${argument}` : `${argument}${node.operator}`;
970
+ }
971
+ case "MemberExpression":
972
+ case "OptionalMemberExpression": return lowerTemplateMemberExpression(context, node);
973
+ case "CallExpression":
974
+ case "OptionalCallExpression": return lowerTemplateCallExpression(context, node);
975
+ case "ArrayExpression": return `[${node.elements.map((item) => item ? item.type === "SpreadElement" ? `...${lowerTemplateExpression(context, item.argument)}` : lowerTemplateExpression(context, item) : "").join(", ")}]`;
976
+ case "ObjectExpression": return lowerTemplateObjectExpression(context, node);
977
+ case "ArrowFunctionExpression":
978
+ case "FunctionExpression": return lowerTemplateEventHandler(context, node);
979
+ default: throw new Error(`${context.filename ?? "TSX"}: 文本绑定暂不支持 ${node.type}`);
980
+ }
981
+ }
982
+ function lowerTemplateIdentifier(context, name) {
983
+ const alias = context.aliases.get(name);
984
+ if (alias) return alias;
985
+ if (TEMPLATE_GLOBALS.has(name)) return name;
986
+ return `_vm_.${name}`;
987
+ }
988
+ function lowerTemplateMemberExpression(context, node) {
989
+ const object = unwrapExpression(node.object);
990
+ let base;
991
+ if (object.type === "Identifier" && object.name === "props") base = "_vm_";
992
+ else base = lowerTemplateExpression(context, object);
993
+ const operator = node.optional ? "?." : ".";
994
+ if (node.computed) return `${base}${node.optional ? "?." : ""}[${lowerTemplateExpression(context, node.property)}]`;
995
+ const property = sourceForNode(context.source, node.property);
996
+ return `${base}${operator}${property}`;
997
+ }
998
+ function lowerTemplateCallExpression(context, node) {
999
+ const setterTarget = node.callee.type === "Identifier" ? context.scriptContext.stateSetters.get(node.callee.name) : void 0;
1000
+ if (setterTarget) return lowerTemplateStateSetterCall(context, setterTarget, node);
1001
+ const optional = node.optional ? "?." : "";
1002
+ return `${lowerTemplateExpression(context, node.callee)}${optional}(${node.arguments.map((arg) => {
1003
+ if (arg.type === "SpreadElement") return `...${lowerTemplateExpression(context, arg.argument)}`;
1004
+ return lowerTemplateExpression(context, arg);
1005
+ }).join(", ")})`;
1006
+ }
1007
+ function lowerTemplateStateSetterCall(context, stateName, node) {
1008
+ const next = node.arguments[0];
1009
+ if (!next) return `_vm_.${stateName} = null`;
1010
+ const expression = unwrapExpression(next);
1011
+ if (expression.type === "ArrowFunctionExpression") {
1012
+ const firstParam = expression.params[0];
1013
+ const aliases = firstParam?.type === "Identifier" ? [[firstParam.name, `_vm_.${stateName}`]] : [];
1014
+ if (expression.body.type === "BlockStatement") throw new Error(`${context.filename ?? "TSX"}: setState updater 暂不支持 block body`);
1015
+ return `_vm_.${stateName} = ${lowerTemplateExpression(withTemplateAliases(context, aliases), expression.body)}`;
1016
+ }
1017
+ return `_vm_.${stateName} = ${lowerTemplateExpression(context, expression)}`;
1018
+ }
1019
+ function lowerTemplateLiteral(context, node) {
1020
+ const parts = [];
1021
+ node.quasis.forEach((quasi, index) => {
1022
+ const text = quasi.value.cooked ?? quasi.value.raw ?? "";
1023
+ if (text) parts.push(JSON.stringify(text));
1024
+ const expression = node.expressions[index];
1025
+ if (expression) parts.push(`(${lowerTemplateExpression(context, expression)})`);
1026
+ });
1027
+ return parts.length > 0 ? parts.join(" + ") : "\"\"";
1028
+ }
1029
+ function lowerTemplateObjectExpression(context, node) {
1030
+ return `{ ${node.properties.map((property) => {
1031
+ if (property.type === "SpreadElement") return `...${lowerTemplateExpression(context, property.argument)}`;
1032
+ if (property.type !== "ObjectProperty") throw new Error(`${context.filename ?? "TSX"}: 文本绑定暂不支持对象方法`);
1033
+ return `${property.computed ? `[${lowerTemplateExpression(context, property.key)}]` : jsObjectKey(objectPropertyKey(property.key, context.filename))}: ${lowerTemplateExpression(context, property.value)}`;
1034
+ }).join(", ")} }`;
1035
+ }
1036
+ function jsObjectKey(key) {
1037
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
1038
+ }
1039
+ const TEMPLATE_GLOBALS = new Set([
1040
+ "Array",
1041
+ "Boolean",
1042
+ "Date",
1043
+ "Infinity",
1044
+ "JSON",
1045
+ "Math",
1046
+ "NaN",
1047
+ "Number",
1048
+ "Object",
1049
+ "Promise",
1050
+ "String",
1051
+ "console",
1052
+ "false",
1053
+ "isFinite",
1054
+ "isNaN",
1055
+ "null",
1056
+ "parseFloat",
1057
+ "parseInt",
1058
+ "network",
1059
+ "router",
1060
+ "storage",
1061
+ "true",
1062
+ "undefined",
1063
+ "velaBattery",
1064
+ "velaBluetoothBLE",
1065
+ "velaBrightness",
1066
+ "velaConfiguration",
1067
+ "velaCrypto",
1068
+ "velaDebug",
1069
+ "velaEvent",
1070
+ "velaExchange",
1071
+ "velaFolme",
1072
+ "velaInterconnect",
1073
+ "velaJumpApp",
1074
+ "velaLocale",
1075
+ "velaMediaSession",
1076
+ "velaMqttMessage",
1077
+ "velaProtobuf",
1078
+ "velaServiceClient",
1079
+ "velaVolume",
1080
+ "velaZlib"
1081
+ ]);
884
1082
  function findTopLevelBinding(body, name) {
885
1083
  for (const statement of body) {
886
1084
  if (statement.type === "FunctionDeclaration" && statement.id?.name === name) return statement;
@@ -899,50 +1097,50 @@ function renderExpressionFromFunction(node, filename) {
899
1097
  }
900
1098
  throw new Error(`${filename ?? "TSX"}: 页面函数缺少 return 语句`);
901
1099
  }
902
- function templateFromRenderExpression(expression, bindings, filename) {
903
- const node = nodeFromJsx(expression, bindings, filename);
1100
+ function templateFromRenderExpression(expression, bindings, context) {
1101
+ const node = nodeFromJsx(expression, bindings, context);
904
1102
  if (node.kind === "fragment") return node.value;
905
1103
  return [node];
906
1104
  }
907
- function nodeFromJsx(node, bindings, filename) {
1105
+ function nodeFromJsx(node, bindings, context) {
908
1106
  switch (node.type) {
909
1107
  case "JSXElement": return {
910
1108
  kind: "element",
911
- value: elementFromJsx(node, bindings, filename)
1109
+ value: elementFromJsx(node, bindings, context)
912
1110
  };
913
1111
  case "JSXFragment": return {
914
1112
  kind: "fragment",
915
- value: childrenFromJsx(node.children, bindings, filename)
1113
+ value: childrenFromJsx(node.children, bindings, context)
916
1114
  };
917
- default: throw new Error(`${filename ?? "TSX"}: 不支持的 JSX 根节点 ${node.type}`);
1115
+ default: throw new Error(`${context.filename ?? "TSX"}: 不支持的 JSX 根节点 ${node.type}`);
918
1116
  }
919
1117
  }
920
- function elementFromJsx(node, bindings, filename) {
921
- const name = jsxElementName(node.openingElement.name, filename);
1118
+ function elementFromJsx(node, bindings, context) {
1119
+ const name = jsxElementName(node.openingElement.name, context.filename);
922
1120
  const tag = bindings.get(name) ?? name;
923
1121
  const isComponent = !bindings.has(name) && /^[A-Z]/.test(name);
924
1122
  const attrs = {};
925
1123
  const events = {};
926
1124
  for (const attr of node.openingElement.attributes) {
927
- if (attr.type === "JSXSpreadAttribute") throw new Error(`${filename ?? "TSX"}: 暂不支持 JSX spread 属性`);
928
- const attrName = jsxAttributeName(attr.name, filename);
1125
+ if (attr.type === "JSXSpreadAttribute") throw new Error(`${context.filename ?? "TSX"}: 暂不支持 JSX spread 属性`);
1126
+ const attrName = jsxAttributeName(attr.name, context.filename);
929
1127
  if (attrName === "key") continue;
930
1128
  if (isEventAttribute(attrName)) {
931
- events[eventNameFromAttribute(attrName)] = bindingFromAttribute(attr.value, true, filename);
1129
+ events[eventNameFromAttribute(attrName)] = bindingFromAttribute(attr.value, true, context);
932
1130
  continue;
933
1131
  }
934
1132
  const normalizedName = normalizeAttributeName(attrName, isComponent);
935
- attrs[normalizedName] = attrFromValue(normalizedName, attr.value, filename);
1133
+ attrs[normalizedName] = attrFromValue(normalizedName, attr.value, context);
936
1134
  }
937
1135
  return {
938
1136
  tag: isComponent ? kebabCase(tag) : tag,
939
1137
  is_component: isComponent,
940
1138
  attrs,
941
1139
  events,
942
- children: childrenFromJsx(node.children, bindings, filename)
1140
+ children: childrenFromJsx(node.children, bindings, context)
943
1141
  };
944
1142
  }
945
- function childrenFromJsx(children, bindings, filename) {
1143
+ function childrenFromJsx(children, bindings, context) {
946
1144
  const out = [];
947
1145
  for (const child of children) {
948
1146
  if (child.type === "JSXText") {
@@ -954,7 +1152,7 @@ function childrenFromJsx(children, bindings, filename) {
954
1152
  continue;
955
1153
  }
956
1154
  if (child.type === "JSXElement" || child.type === "JSXFragment") {
957
- const node = nodeFromJsx(child, bindings, filename);
1155
+ const node = nodeFromJsx(child, bindings, context);
958
1156
  if (node.kind === "fragment") out.push(...node.value);
959
1157
  else out.push(node);
960
1158
  continue;
@@ -962,18 +1160,18 @@ function childrenFromJsx(children, bindings, filename) {
962
1160
  if (child.type === "JSXExpressionContainer") {
963
1161
  const expression = unwrapExpression(child.expression);
964
1162
  if (expression.type === "JSXEmptyExpression") continue;
965
- out.push(nodeFromExpression(expression, bindings, filename));
1163
+ out.push(nodeFromExpression(expression, bindings, context));
966
1164
  continue;
967
1165
  }
968
- throw new Error(`${filename ?? "TSX"}: 不支持的 JSX 子节点 ${child.type}`);
1166
+ throw new Error(`${context.filename ?? "TSX"}: 不支持的 JSX 子节点 ${child.type}`);
969
1167
  }
970
1168
  return out;
971
1169
  }
972
- function nodeFromExpression(expression, bindings, filename) {
973
- const conditional = conditionalFromExpression(expression, bindings, filename);
974
- if (conditional) return conditional;
975
- const list = listFromExpression(expression, bindings, filename);
1170
+ function nodeFromExpression(expression, bindings, context) {
1171
+ const list = listFromExpression(expression, bindings, context);
976
1172
  if (list) return list;
1173
+ const conditional = conditionalFromExpression(expression, bindings, context);
1174
+ if (conditional) return conditional;
977
1175
  const value = literalValue(expression);
978
1176
  if (value !== void 0) return {
979
1177
  kind: "text",
@@ -981,65 +1179,80 @@ function nodeFromExpression(expression, bindings, filename) {
981
1179
  };
982
1180
  return {
983
1181
  kind: "expression",
984
- value: bindingFromExpression(expression, false, filename)
1182
+ value: bindingFromExpression(expression, false, context)
985
1183
  };
986
1184
  }
987
- function conditionalFromExpression(expression, bindings, filename) {
1185
+ function conditionalFromExpression(expression, bindings, context) {
988
1186
  const node = unwrapExpression(expression);
989
- if (node.type === "ConditionalExpression") return {
990
- kind: "conditional",
991
- value: { branches: [{
992
- guard: bindingFromExpression(node.test, false, filename),
993
- body: nodesFromBranchExpression(node.consequent, bindings, filename)
994
- }, ...alternateBranches(node.alternate, bindings, filename)] }
995
- };
996
- if (node.type === "LogicalExpression" && node.operator === "&&") return {
997
- kind: "conditional",
998
- value: { branches: [{
999
- guard: bindingFromExpression(node.left, false, filename),
1000
- body: nodesFromBranchExpression(node.right, bindings, filename)
1001
- }] }
1002
- };
1187
+ if (node.type === "ConditionalExpression") {
1188
+ if (!isControlFlowBranch(node.consequent) && !isControlFlowBranch(node.alternate)) return;
1189
+ return {
1190
+ kind: "conditional",
1191
+ value: { branches: [{
1192
+ guard: bindingFromExpression(node.test, false, context),
1193
+ body: nodesFromBranchExpression(node.consequent, bindings, context)
1194
+ }, ...alternateBranches(node.alternate, bindings, context)] }
1195
+ };
1196
+ }
1197
+ if (node.type === "LogicalExpression" && node.operator === "&&") {
1198
+ if (!isControlFlowBranch(node.right)) return;
1199
+ return {
1200
+ kind: "conditional",
1201
+ value: { branches: [{
1202
+ guard: bindingFromExpression(node.left, false, context),
1203
+ body: nodesFromBranchExpression(node.right, bindings, context)
1204
+ }] }
1205
+ };
1206
+ }
1003
1207
  }
1004
- function alternateBranches(expression, bindings, filename) {
1208
+ function alternateBranches(expression, bindings, context) {
1005
1209
  const node = unwrapExpression(expression);
1006
1210
  if (node.type === "ConditionalExpression") return [{
1007
- guard: bindingFromExpression(node.test, false, filename),
1008
- body: nodesFromBranchExpression(node.consequent, bindings, filename)
1009
- }, ...alternateBranches(node.alternate, bindings, filename)];
1211
+ guard: bindingFromExpression(node.test, false, context),
1212
+ body: nodesFromBranchExpression(node.consequent, bindings, context)
1213
+ }, ...alternateBranches(node.alternate, bindings, context)];
1010
1214
  return [{
1011
1215
  guard: null,
1012
- body: nodesFromBranchExpression(node, bindings, filename)
1216
+ body: nodesFromBranchExpression(node, bindings, context)
1013
1217
  }];
1014
1218
  }
1015
- function nodesFromBranchExpression(expression, bindings, filename) {
1219
+ function nodesFromBranchExpression(expression, bindings, context) {
1016
1220
  const node = unwrapExpression(expression);
1017
1221
  if (node.type === "NullLiteral") return [];
1018
1222
  if (node.type === "BooleanLiteral" && node.value === false) return [];
1019
1223
  if (node.type === "JSXElement" || node.type === "JSXFragment") {
1020
- const result = nodeFromJsx(node, bindings, filename);
1224
+ const result = nodeFromJsx(node, bindings, context);
1021
1225
  return result.kind === "fragment" ? result.value : [result];
1022
1226
  }
1023
- return [nodeFromExpression(node, bindings, filename)];
1227
+ return [nodeFromExpression(node, bindings, context)];
1024
1228
  }
1025
- function listFromExpression(expression, bindings, filename) {
1229
+ function isControlFlowBranch(expression) {
1230
+ const node = unwrapExpression(expression);
1231
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
1232
+ if (node.type === "NullLiteral") return true;
1233
+ if (node.type === "BooleanLiteral" && node.value === false) return true;
1234
+ if (node.type === "ConditionalExpression") return isControlFlowBranch(node.consequent) || isControlFlowBranch(node.alternate);
1235
+ return false;
1236
+ }
1237
+ function listFromExpression(expression, bindings, context) {
1026
1238
  const node = unwrapExpression(expression);
1027
1239
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "map") return;
1028
1240
  const callback = unwrapExpression(node.arguments[0]);
1029
- if (!callback || !isFunctionLike(callback)) throw new Error(`${filename ?? "TSX"}: list render 的 map 参数必须是函数`);
1241
+ if (!callback || !isFunctionLike(callback)) throw new Error(`${context.filename ?? "TSX"}: list render 的 map 参数必须是函数`);
1030
1242
  const itemParam = callback.params[0];
1031
- if (itemParam?.type !== "Identifier") throw new Error(`${filename ?? "TSX"}: list render 必须声明 item 参数`);
1243
+ if (itemParam?.type !== "Identifier") throw new Error(`${context.filename ?? "TSX"}: list render 必须声明 item 参数`);
1032
1244
  const indexParam = callback.params[1];
1033
- if (indexParam && indexParam.type !== "Identifier") throw new Error(`${filename ?? "TSX"}: list render 的 index 参数必须是标识符`);
1034
- const bodyExpression = renderExpressionFromMapCallback(callback, filename);
1245
+ if (indexParam && indexParam.type !== "Identifier") throw new Error(`${context.filename ?? "TSX"}: list render 的 index 参数必须是标识符`);
1246
+ const bodyExpression = renderExpressionFromMapCallback(callback, context.filename);
1247
+ const listContext = withTemplateAliases(context, [[itemParam.name, itemParam.name], ...indexParam ? [[indexParam.name, indexParam.name]] : []]);
1035
1248
  return {
1036
1249
  kind: "list",
1037
1250
  value: {
1038
- source: bindingFromExpression(node.callee.object, false, filename),
1251
+ source: bindingFromExpression(node.callee.object, false, context),
1039
1252
  item_var: itemParam.name,
1040
1253
  index_var: indexParam?.name,
1041
- key: keyBindingFromJsx(bodyExpression, filename),
1042
- body: nodesFromBranchExpression(bodyExpression, bindings, filename)
1254
+ key: keyBindingFromJsx(bodyExpression, listContext),
1255
+ body: nodesFromBranchExpression(bodyExpression, bindings, listContext)
1043
1256
  }
1044
1257
  };
1045
1258
  }
@@ -1053,14 +1266,14 @@ function renderExpressionFromMapCallback(callback, filename) {
1053
1266
  }
1054
1267
  throw new Error(`${filename ?? "TSX"}: list render 函数缺少 return 语句`);
1055
1268
  }
1056
- function keyBindingFromJsx(expression, filename) {
1269
+ function keyBindingFromJsx(expression, context) {
1057
1270
  const node = unwrapExpression(expression);
1058
1271
  if (node.type !== "JSXElement") return;
1059
1272
  const keyAttr = node.openingElement.attributes.find((attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === "key");
1060
1273
  if (!keyAttr) return;
1061
- return bindingFromAttribute(keyAttr.value, false, filename);
1274
+ return bindingFromAttribute(keyAttr.value, false, context);
1062
1275
  }
1063
- function attrFromValue(name, value, filename) {
1276
+ function attrFromValue(name, value, context) {
1064
1277
  if (!value) return {
1065
1278
  kind: "static",
1066
1279
  value: true
@@ -1069,7 +1282,7 @@ function attrFromValue(name, value, filename) {
1069
1282
  kind: "static",
1070
1283
  value: value.value
1071
1284
  };
1072
- if (value.type !== "JSXExpressionContainer") throw new Error(`${filename ?? "TSX"}: 不支持的属性值 ${value.type}`);
1285
+ if (value.type !== "JSXExpressionContainer") throw new Error(`${context.filename ?? "TSX"}: 不支持的属性值 ${value.type}`);
1073
1286
  const expression = unwrapExpression(value.expression);
1074
1287
  const literal = literalValue(expression);
1075
1288
  if (literal !== void 0) return {
@@ -1082,7 +1295,7 @@ function attrFromValue(name, value, filename) {
1082
1295
  value: name === "style" ? normalizeStyleLiteral(staticObject) : staticObject
1083
1296
  };
1084
1297
  if (name === "style") {
1085
- const styleObject = styleObjectAttr(expression, filename);
1298
+ const styleObject = styleObjectAttr(expression, context);
1086
1299
  if (styleObject) return {
1087
1300
  kind: "style_object",
1088
1301
  value: styleObject
@@ -1090,16 +1303,16 @@ function attrFromValue(name, value, filename) {
1090
1303
  }
1091
1304
  return {
1092
1305
  kind: "dynamic",
1093
- value: bindingFromExpression(expression, false, filename)
1306
+ value: bindingFromExpression(expression, false, context)
1094
1307
  };
1095
1308
  }
1096
- function styleObjectAttr(expression, filename) {
1309
+ function styleObjectAttr(expression, context) {
1097
1310
  const node = unwrapExpression(expression);
1098
1311
  if (node.type !== "ObjectExpression") return void 0;
1099
1312
  const slots = [];
1100
1313
  for (const property of node.properties) {
1101
- if (property.type !== "ObjectProperty" || property.computed) throw new Error(`${filename ?? "TSX"}: style 对象暂不支持展开、方法或计算键`);
1102
- const key = kebabToCamel(objectPropertyKey(property.key, filename));
1314
+ if (property.type !== "ObjectProperty" || property.computed) throw new Error(`${context.filename ?? "TSX"}: style 对象暂不支持展开、方法或计算键`);
1315
+ const key = kebabToCamel(objectPropertyKey(property.key, context.filename));
1103
1316
  const value = staticAttrLiteral(property.value);
1104
1317
  if (value !== void 0) {
1105
1318
  slots.push({
@@ -1115,7 +1328,7 @@ function styleObjectAttr(expression, filename) {
1115
1328
  name: key,
1116
1329
  value: {
1117
1330
  kind: "dynamic",
1118
- value: bindingFromExpression(property.value, false, filename)
1331
+ value: bindingFromExpression(property.value, false, context)
1119
1332
  }
1120
1333
  });
1121
1334
  }
@@ -1160,17 +1373,27 @@ function staticAttrLiteral(expression) {
1160
1373
  return out;
1161
1374
  }
1162
1375
  }
1163
- function bindingFromAttribute(value, callable, filename) {
1164
- if (!value || value.type !== "JSXExpressionContainer") throw new Error(`${filename ?? "TSX"}: 事件属性必须使用表达式绑定`);
1165
- return bindingFromExpression(unwrapExpression(value.expression), callable, filename);
1376
+ function bindingFromAttribute(value, callable, context) {
1377
+ if (!value || value.type !== "JSXExpressionContainer") throw new Error(`${context.filename ?? "TSX"}: 事件属性必须使用表达式绑定`);
1378
+ return bindingFromExpression(unwrapExpression(value.expression), callable, context);
1166
1379
  }
1167
- function bindingFromExpression(expression, callable, filename) {
1380
+ function bindingFromExpression(expression, callable, context) {
1381
+ const node = unwrapExpression(expression);
1382
+ if (callable && isFunctionLike(node)) return {
1383
+ path: sourceForNode(context.source, node),
1384
+ expr: lowerTemplateEventHandler(context, node),
1385
+ is_callable: true
1386
+ };
1168
1387
  const path = bindingPath(expression);
1169
- if (!path) throw new Error(`${filename ?? "TSX"}: 当前阶段仅支持标识符或成员访问绑定`);
1170
- return {
1388
+ if (path) return {
1171
1389
  path,
1172
1390
  is_callable: callable
1173
1391
  };
1392
+ return {
1393
+ path: sourceForNode(context.source, node),
1394
+ expr: lowerTemplateExpression(context, node),
1395
+ is_callable: callable
1396
+ };
1174
1397
  }
1175
1398
  function bindingPath(expression) {
1176
1399
  const node = unwrapExpression(expression);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astralsight/astroforge-rsbuild-plugin",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "AstroForge Plugin for RsBuild",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@babel/parser": "^7.29.3",
41
- "@astralsight/astroforge-core": "^0.0.6"
41
+ "@astralsight/astroforge-core": "^0.0.8"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsdown"