@barefootjs/go-template 0.7.0 → 0.9.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.
@@ -32,7 +32,10 @@ import {
32
32
  identifierPath,
33
33
  emitParsedExpr,
34
34
  emitIRNode,
35
- emitAttrValue
35
+ emitAttrValue,
36
+ augmentInheritedPropAccesses,
37
+ parseRecordIndexAccess,
38
+ collectContextConsumers
36
39
  } from "@barefootjs/jsx";
37
40
  import { findInterpolationEnd } from "@barefootjs/jsx/scanner";
38
41
  function wrapIfMultiToken(rendered) {
@@ -117,7 +120,13 @@ class GoTemplateAdapter extends BaseAdapter {
117
120
  localStructFields = new Map;
118
121
  synthStructTypes = new Map;
119
122
  usesHtmlTemplate = false;
123
+ usesFmt = false;
124
+ childComponentShapes = new Map;
120
125
  moduleStringConsts = new Map;
126
+ localConstants = [];
127
+ contextConsumers = [];
128
+ childContextConsumers = new Map;
129
+ nillablePropNames = new Set;
121
130
  constructor(options = {}) {
122
131
  super();
123
132
  this.options = {
@@ -133,6 +142,10 @@ class GoTemplateAdapter extends BaseAdapter {
133
142
  this.propsObjectName = ir.metadata.propsObjectName;
134
143
  this.restPropsName = ir.metadata.restPropsName ?? null;
135
144
  this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
145
+ this.localConstants = ir.metadata.localConstants ?? [];
146
+ this.contextConsumers = collectContextConsumers(ir.metadata);
147
+ augmentInheritedPropAccesses(ir);
148
+ this.nillablePropNames = this.collectNillablePropNames(ir);
136
149
  if (!options?.siblingTemplatesRegistered) {
137
150
  this.checkImportedLoopChildComponents(ir);
138
151
  }
@@ -345,8 +358,46 @@ ${scriptRegistrations}${templateBody}
345
358
  return `{{if .Scripts}}${registrations.join("")}{{end}}
346
359
  `;
347
360
  }
361
+ registerChildComponentShape(ir) {
362
+ const name = ir.metadata.componentName;
363
+ if (!name)
364
+ return;
365
+ const paramNames = new Set((ir.metadata.propsParams ?? []).map((p) => p.name));
366
+ const restPropsName = ir.metadata.restPropsName ?? null;
367
+ const restBagField = restPropsName ? this.capitalizeFieldName(restPropsName) : null;
368
+ this.childComponentShapes.set(name, { paramNames, restBagField });
369
+ this.childContextConsumers.set(name, collectContextConsumers(ir.metadata));
370
+ }
371
+ contextFieldName(c) {
372
+ return this.capitalizeFieldName(c.localName);
373
+ }
374
+ contextConsumerGoType(c) {
375
+ if (typeof c.defaultValue === "number")
376
+ return "int";
377
+ if (typeof c.defaultValue === "boolean")
378
+ return "bool";
379
+ return "string";
380
+ }
381
+ contextConsumerGoDefault(c) {
382
+ if (typeof c.defaultValue === "number")
383
+ return String(c.defaultValue);
384
+ if (typeof c.defaultValue === "boolean")
385
+ return String(c.defaultValue);
386
+ if (typeof c.defaultValue === "string")
387
+ return `"${this.escapeGoString(c.defaultValue)}"`;
388
+ return '""';
389
+ }
390
+ nonCollidingContextConsumers(taken) {
391
+ return this.contextConsumers.filter((c) => !taken.has(this.contextFieldName(c)));
392
+ }
348
393
  generateTypes(ir) {
349
394
  this.usesHtmlTemplate = false;
395
+ this.usesFmt = false;
396
+ this.propsObjectName = ir.metadata.propsObjectName;
397
+ augmentInheritedPropAccesses(ir);
398
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
399
+ this.localConstants = ir.metadata.localConstants ?? [];
400
+ this.contextConsumers = collectContextConsumers(ir.metadata);
350
401
  const lines = [];
351
402
  const componentName = ir.metadata.componentName;
352
403
  this.localTypeNames = new Set;
@@ -408,6 +459,8 @@ ${goFields.join(`
408
459
  header.push(`package ${this.options.packageName}`);
409
460
  header.push("");
410
461
  header.push("import (");
462
+ if (this.usesFmt)
463
+ header.push('\t"fmt"');
411
464
  if (this.usesHtmlTemplate)
412
465
  header.push('\t"html/template"');
413
466
  header.push('\t"math/rand"');
@@ -568,6 +621,19 @@ ${goFields.join(`
568
621
  }
569
622
  return overrides;
570
623
  }
624
+ resolvePropGoType(param, propTypeOverrides) {
625
+ return propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
626
+ }
627
+ collectNillablePropNames(ir) {
628
+ const propTypeOverrides = this.buildPropTypeOverrides(ir);
629
+ const nillable = new Set;
630
+ for (const param of ir.metadata.propsParams) {
631
+ if (this.resolvePropGoType(param, propTypeOverrides) === "interface{}") {
632
+ nillable.add(param.name);
633
+ }
634
+ }
635
+ return nillable;
636
+ }
571
637
  generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
572
638
  const inputTypeName = `${componentName}Input`;
573
639
  lines.push(`// ${inputTypeName} is the user-facing input type.`);
@@ -581,12 +647,16 @@ ${goFields.join(`
581
647
  const fieldName = this.capitalizeFieldName(param.name);
582
648
  if (nestedArrayFields.has(fieldName))
583
649
  continue;
584
- const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
650
+ const goType = this.resolvePropGoType(param, propTypeOverrides);
585
651
  lines.push(` ${fieldName} ${goType}`);
586
652
  }
587
653
  for (const nested of inputNested) {
588
654
  lines.push(` ${nested.name}s []${nested.name}Input`);
589
655
  }
656
+ const takenInput = new Set(ir.metadata.propsParams.map((p) => this.capitalizeFieldName(p.name)));
657
+ for (const c of this.nonCollidingContextConsumers(takenInput)) {
658
+ lines.push(` ${this.contextFieldName(c)} ${this.contextConsumerGoType(c)}`);
659
+ }
590
660
  const restPropsName = ir.metadata.restPropsName;
591
661
  if (restPropsName) {
592
662
  const seen = new Set;
@@ -620,7 +690,7 @@ ${goFields.join(`
620
690
  const fieldName = this.capitalizeFieldName(param.name);
621
691
  if (nestedArrayFields.has(fieldName))
622
692
  continue;
623
- const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
693
+ const goType = this.resolvePropGoType(param, propTypeOverrides);
624
694
  const jsonTag = this.toJsonTag(param.name);
625
695
  lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
626
696
  propFieldNames.add(fieldName);
@@ -664,6 +734,15 @@ ${goFields.join(`
664
734
  const goType = this.inferMemoType(memo, ir.metadata.signals, propsParamMap);
665
735
  lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
666
736
  }
737
+ const takenProps = new Set([
738
+ ...ir.metadata.propsParams.map((p) => this.capitalizeFieldName(p.name)),
739
+ ...ir.metadata.signals.map((s) => this.capitalizeFieldName(s.getter)),
740
+ ...ir.metadata.memos.map((m) => this.capitalizeFieldName(m.name))
741
+ ]);
742
+ for (const c of this.nonCollidingContextConsumers(takenProps)) {
743
+ const jsonTag = this.toJsonTag(c.localName);
744
+ lines.push(` ${this.contextFieldName(c)} ${this.contextConsumerGoType(c)} \`json:"${jsonTag}"\``);
745
+ }
667
746
  for (const nested of nestedComponents) {
668
747
  if (nested.isDynamic && !nested.isPropDerived) {
669
748
  lines.push(` ${nested.name}s []${nested.name}Props \`json:"-"\``);
@@ -773,25 +852,55 @@ ${goFields.join(`
773
852
  const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`;
774
853
  lines.push(` ${nested.name}s: ${varName},`);
775
854
  }
855
+ const memoPropsParamMap = new Map(ir.metadata.propsParams.map((p) => [p.name, p]));
776
856
  for (const memo of ir.metadata.memos) {
777
857
  const fieldName = this.capitalizeFieldName(memo.name);
778
- const memoValue = this.computeMemoInitialValue(memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars);
858
+ const goType = this.inferMemoType(memo, ir.metadata.signals, memoPropsParamMap);
859
+ const memoValue = this.computeMemoInitialValue(memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars, goType);
779
860
  lines.push(` ${fieldName}: ${memoValue},`);
780
861
  }
862
+ const takenInit = new Set([
863
+ ...ir.metadata.propsParams.map((p) => this.capitalizeFieldName(p.name)),
864
+ ...ir.metadata.signals.map((s) => this.capitalizeFieldName(s.getter)),
865
+ ...ir.metadata.memos.map((m) => this.capitalizeFieldName(m.name))
866
+ ]);
867
+ for (const c of this.nonCollidingContextConsumers(takenInit)) {
868
+ const field = this.contextFieldName(c);
869
+ const def = this.contextConsumerGoDefault(c);
870
+ const defaulted = c.defaultValue === null || def === '""' || def === "0" || def === "false" ? `in.${field}` : this.applyGoFallback(`in.${field}`, def);
871
+ lines.push(` ${field}: ${defaulted},`);
872
+ }
781
873
  const staticChildren = this.collectStaticChildInstances(ir.root);
782
874
  for (const child of staticChildren) {
783
875
  lines.push(` ${child.fieldName}: New${child.name}Props(${child.name}Input{`);
784
876
  lines.push(` ScopeID: scopeID + "_${child.slotId}",`);
785
877
  lines.push(` BfParent: scopeID,`);
786
878
  lines.push(` BfMount: "${child.slotId}",`);
879
+ if (child.contextBindings) {
880
+ for (const consumer of this.childContextConsumers.get(child.name) ?? []) {
881
+ const goVal = child.contextBindings.get(consumer.contextName);
882
+ if (goVal !== undefined) {
883
+ lines.push(` ${this.contextFieldName(consumer)}: ${goVal},`);
884
+ }
885
+ }
886
+ }
887
+ const childShape = this.childComponentShapes.get(child.name);
888
+ const restBagEntries = [];
889
+ const emitChildField = (jsxName, goValue) => {
890
+ if (childShape && childShape.restBagField && !childShape.paramNames.has(jsxName)) {
891
+ restBagEntries.push(`${JSON.stringify(jsxName)}: ${goValue}`);
892
+ return;
893
+ }
894
+ lines.push(` ${this.capitalizeFieldName(jsxName)}: ${goValue},`);
895
+ };
787
896
  for (const prop of child.props) {
788
897
  switch (prop.value.kind) {
789
898
  case "literal":
790
- lines.push(` ${this.capitalizeFieldName(prop.name)}: ${this.goLiteral(prop.value.value)},`);
899
+ emitChildField(prop.name, this.goLiteral(prop.value.value));
791
900
  break;
792
901
  case "boolean-shorthand":
793
902
  case "boolean-attr":
794
- lines.push(` ${this.capitalizeFieldName(prop.name)}: true,`);
903
+ emitChildField(prop.name, "true");
795
904
  break;
796
905
  case "expression":
797
906
  case "spread":
@@ -800,7 +909,7 @@ ${goFields.join(`
800
909
  if (parts) {
801
910
  const goExpr = this.templatePartsToGoCode(parts, ir.metadata.propsParams);
802
911
  if (goExpr !== null) {
803
- lines.push(` ${this.capitalizeFieldName(prop.name)}: ${goExpr},`);
912
+ emitChildField(prop.name, goExpr);
804
913
  break;
805
914
  }
806
915
  }
@@ -809,7 +918,7 @@ ${goFields.join(`
809
918
  break;
810
919
  const resolvedValue = this.resolveDynamicPropValue(exprText, ir.metadata.signals, ir.metadata.memos, ir.metadata.propsParams);
811
920
  if (resolvedValue !== null) {
812
- lines.push(` ${this.capitalizeFieldName(prop.name)}: ${resolvedValue},`);
921
+ emitChildField(prop.name, resolvedValue);
813
922
  }
814
923
  break;
815
924
  }
@@ -817,6 +926,9 @@ ${goFields.join(`
817
926
  break;
818
927
  }
819
928
  }
929
+ if (childShape?.restBagField && restBagEntries.length > 0) {
930
+ lines.push(` ${childShape.restBagField}: map[string]any{${restBagEntries.join(", ")}},`);
931
+ }
820
932
  if (child.childrenText !== null) {
821
933
  lines.push(` Children: ${JSON.stringify(child.childrenText)},`);
822
934
  } else if (child.childrenHtml !== null) {
@@ -887,7 +999,7 @@ ${goFields.join(`
887
999
  }
888
1000
  collectStaticChildInstances(node) {
889
1001
  const result = [];
890
- this.collectStaticChildInstancesRecursive(node, result, false);
1002
+ this.collectStaticChildInstancesRecursive(node, result, false, new Map);
891
1003
  return result;
892
1004
  }
893
1005
  extractTextChildren(children) {
@@ -911,12 +1023,12 @@ ${goFields.join(`
911
1023
  return null;
912
1024
  return html;
913
1025
  }
914
- collectStaticChildInstancesRecursive(node, result, inLoop) {
1026
+ collectStaticChildInstancesRecursive(node, result, inLoop, providerCtx) {
915
1027
  if (node.type === "component") {
916
1028
  const comp = node;
917
1029
  if (comp.dynamicTag) {
918
1030
  for (const child of comp.children) {
919
- this.collectStaticChildInstancesRecursive(child, result, inLoop);
1031
+ this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx);
920
1032
  }
921
1033
  return;
922
1034
  }
@@ -928,48 +1040,65 @@ ${goFields.join(`
928
1040
  props: comp.props,
929
1041
  fieldName: `${comp.name}${suffix}`,
930
1042
  childrenText: this.extractTextChildren(comp.children),
931
- childrenHtml: this.extractHtmlChildren(comp.children)
1043
+ childrenHtml: this.extractHtmlChildren(comp.children),
1044
+ contextBindings: providerCtx.size > 0 ? providerCtx : undefined
932
1045
  });
933
1046
  }
934
1047
  if (comp.name === "Portal" && comp.children) {
935
1048
  for (const child of comp.children) {
936
- this.collectStaticChildInstancesRecursive(child, result, inLoop);
1049
+ this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx);
937
1050
  }
938
1051
  }
939
1052
  } else if (node.type === "loop") {
940
1053
  const loop = node;
941
1054
  for (const child of loop.children) {
942
- this.collectStaticChildInstancesRecursive(child, result, true);
1055
+ this.collectStaticChildInstancesRecursive(child, result, true, providerCtx);
943
1056
  }
944
1057
  } else if (node.type === "element") {
945
1058
  const element = node;
946
1059
  for (const child of element.children) {
947
- this.collectStaticChildInstancesRecursive(child, result, inLoop);
1060
+ this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx);
948
1061
  }
949
1062
  } else if (node.type === "fragment") {
950
1063
  const fragment = node;
951
1064
  for (const child of fragment.children) {
952
- this.collectStaticChildInstancesRecursive(child, result, inLoop);
1065
+ this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx);
953
1066
  }
954
1067
  } else if (node.type === "conditional") {
955
1068
  const cond = node;
956
- this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop);
1069
+ this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop, providerCtx);
957
1070
  if (cond.whenFalse) {
958
- this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop);
1071
+ this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx);
959
1072
  }
960
1073
  } else if (node.type === "provider") {
961
1074
  const p = node;
1075
+ const childCtx = this.extendProviderContext(providerCtx, p);
962
1076
  for (const child of p.children) {
963
- this.collectStaticChildInstancesRecursive(child, result, inLoop);
1077
+ this.collectStaticChildInstancesRecursive(child, result, inLoop, childCtx);
964
1078
  }
965
1079
  } else if (node.type === "async") {
966
1080
  const a = node;
967
- this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop);
1081
+ this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop, providerCtx);
968
1082
  for (const child of a.children) {
969
- this.collectStaticChildInstancesRecursive(child, result, inLoop);
1083
+ this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx);
970
1084
  }
971
1085
  }
972
1086
  }
1087
+ extendProviderContext(current, p) {
1088
+ const v = p.valueProp?.value;
1089
+ if (!v || v.kind !== "literal")
1090
+ return current;
1091
+ let goLit = null;
1092
+ if (typeof v.value === "string")
1093
+ goLit = `"${this.escapeGoString(v.value)}"`;
1094
+ else if (typeof v.value === "number" || typeof v.value === "boolean")
1095
+ goLit = String(v.value);
1096
+ if (goLit === null)
1097
+ return current;
1098
+ const next = new Map(current);
1099
+ next.set(p.contextName, goLit);
1100
+ return next;
1101
+ }
973
1102
  collectSpreadSlots(node) {
974
1103
  const result = [];
975
1104
  this.collectSpreadSlotsRecursive(node, result);
@@ -1094,6 +1223,9 @@ ${goFields.join(`
1094
1223
  }
1095
1224
  buildSpreadInitializer(spreadExpr, ir) {
1096
1225
  const trimmed = spreadExpr.trim();
1226
+ const conditional = this.buildConditionalSpreadInitializer(trimmed, ir);
1227
+ if (conditional !== undefined)
1228
+ return conditional;
1097
1229
  const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed);
1098
1230
  if (callMatch) {
1099
1231
  const getterName = callMatch[1];
@@ -1117,9 +1249,127 @@ ${goFields.join(`
1117
1249
  if (ir.metadata.restPropsName === trimmed) {
1118
1250
  return `in.${this.capitalizeFieldName(trimmed)}`;
1119
1251
  }
1252
+ const localConst = (ir.metadata.localConstants ?? []).find((c) => c.name === trimmed && !c.isModule);
1253
+ if (localConst?.value !== undefined) {
1254
+ const initTrimmed = localConst.value.trim();
1255
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
1256
+ const resolved = this.buildConditionalSpreadInitializer(initTrimmed, ir);
1257
+ if (resolved)
1258
+ return resolved;
1259
+ if (resolved === null)
1260
+ return null;
1261
+ }
1262
+ }
1120
1263
  }
1121
1264
  return null;
1122
1265
  }
1266
+ buildConditionalSpreadInitializer(spreadExpr, ir) {
1267
+ const expr = this.parseLiteralExpression(spreadExpr);
1268
+ if (!expr || !ts.isConditionalExpression(expr))
1269
+ return;
1270
+ const whenTrue = this.unwrapParens(expr.whenTrue);
1271
+ const whenFalse = this.unwrapParens(expr.whenFalse);
1272
+ if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
1273
+ return;
1274
+ }
1275
+ const goCond = this.conditionToGoBool(expr.condition, ir);
1276
+ if (goCond === null)
1277
+ return null;
1278
+ const trueMap = this.objectLiteralToGoSpreadMap(whenTrue, ir);
1279
+ const falseMap = this.objectLiteralToGoSpreadMap(whenFalse, ir);
1280
+ if (trueMap === null || falseMap === null)
1281
+ return null;
1282
+ return `func() map[string]any {
1283
+ ` + ` if ${goCond} {
1284
+ ` + ` return ${trueMap}
1285
+ ` + ` }
1286
+ ` + ` return ${falseMap}
1287
+ ` + ` }()`;
1288
+ }
1289
+ unwrapParens(node) {
1290
+ let e = node;
1291
+ while (ts.isParenthesizedExpression(e))
1292
+ e = e.expression;
1293
+ return e;
1294
+ }
1295
+ conditionToGoBool(condition, ir) {
1296
+ let node = this.unwrapParens(condition);
1297
+ let negate = false;
1298
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
1299
+ negate = true;
1300
+ node = this.unwrapParens(node.operand);
1301
+ }
1302
+ if (!ts.isIdentifier(node))
1303
+ return null;
1304
+ const param = ir.metadata.propsParams.find((p) => p.name === node.text);
1305
+ if (!param)
1306
+ return null;
1307
+ const field = `in.${this.capitalizeFieldName(param.name)}`;
1308
+ const prim = param.type.kind === "primitive" ? param.type.primitive : undefined;
1309
+ let truthy;
1310
+ if (prim === "boolean") {
1311
+ truthy = field;
1312
+ } else if (prim === "number") {
1313
+ truthy = `${field} != 0`;
1314
+ } else if (prim === "string") {
1315
+ truthy = `${field} != ""`;
1316
+ } else {
1317
+ truthy = `bf.Truthy(${field})`;
1318
+ }
1319
+ if (!negate)
1320
+ return truthy;
1321
+ if (prim === "boolean")
1322
+ return `!${field}`;
1323
+ if (prim === "number")
1324
+ return `${field} == 0`;
1325
+ if (prim === "string")
1326
+ return `${field} == ""`;
1327
+ return `!bf.Truthy(${field})`;
1328
+ }
1329
+ objectLiteralToGoSpreadMap(obj, ir) {
1330
+ const entries = [];
1331
+ for (const prop of obj.properties) {
1332
+ if (!ts.isPropertyAssignment(prop))
1333
+ return null;
1334
+ let key;
1335
+ if (ts.isIdentifier(prop.name)) {
1336
+ key = prop.name.text;
1337
+ } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
1338
+ key = prop.name.text;
1339
+ } else {
1340
+ return null;
1341
+ }
1342
+ const val = this.unwrapParens(prop.initializer);
1343
+ let goVal;
1344
+ if (ts.isStringLiteral(val) || ts.isNoSubstitutionTemplateLiteral(val)) {
1345
+ goVal = JSON.stringify(val.text);
1346
+ } else if (ts.isIdentifier(val)) {
1347
+ const param = ir.metadata.propsParams.find((p) => p.name === val.text);
1348
+ if (!param)
1349
+ return null;
1350
+ goVal = `in.${this.capitalizeFieldName(param.name)}`;
1351
+ } else {
1352
+ const indexed = this.recordIndexAccessToGoMap(val, ir);
1353
+ if (indexed === null)
1354
+ return null;
1355
+ goVal = indexed;
1356
+ }
1357
+ entries.push(`${JSON.stringify(key)}: ${goVal}`);
1358
+ }
1359
+ return `map[string]any{${entries.join(", ")}}`;
1360
+ }
1361
+ recordIndexAccessToGoMap(val, ir) {
1362
+ const parsed = parseRecordIndexAccess(val, ir.metadata.localConstants ?? [], ir.metadata.propsParams);
1363
+ if (!parsed)
1364
+ return null;
1365
+ const entries = parsed.entries.map((e) => {
1366
+ const mapVal = e.value.kind === "number" ? e.value.text : JSON.stringify(e.value.text);
1367
+ return `${JSON.stringify(e.key)}: ${mapVal}`;
1368
+ });
1369
+ this.usesFmt = true;
1370
+ const field = `in.${this.capitalizeFieldName(parsed.indexPropName)}`;
1371
+ return `map[string]any{${entries.join(", ")}}[fmt.Sprint(${field})]`;
1372
+ }
1123
1373
  convertInitialValue(value, typeInfo, propsParams) {
1124
1374
  if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
1125
1375
  if (propsParams?.some((p) => p.name === value)) {
@@ -1360,7 +1610,142 @@ ${goFields.join(`
1360
1610
  }
1361
1611
  return null;
1362
1612
  }
1363
- computeMemoInitialValue(memo, signals, propsParams, propFallbackVars = GoTemplateAdapter.EMPTY_PROP_FALLBACK_VARS) {
1613
+ computeTemplateLiteralMemoInitialValue(computation, propsParams) {
1614
+ const sf = ts.createSourceFile("__memo.ts", `const __x = (${computation});`, ts.ScriptTarget.Latest, false);
1615
+ const stmt = sf.statements[0];
1616
+ if (!stmt || !ts.isVariableStatement(stmt))
1617
+ return null;
1618
+ let init = stmt.declarationList.declarations[0]?.initializer;
1619
+ while (init && ts.isParenthesizedExpression(init))
1620
+ init = init.expression;
1621
+ if (!init || !ts.isArrowFunction(init))
1622
+ return null;
1623
+ let body = init.body;
1624
+ while (ts.isParenthesizedExpression(body)) {
1625
+ body = body.expression;
1626
+ }
1627
+ const localKeyBindings = new Map;
1628
+ if (ts.isBlock(body)) {
1629
+ let returned = null;
1630
+ for (const s of body.statements) {
1631
+ if (ts.isVariableStatement(s)) {
1632
+ for (const d of s.declarationList.declarations) {
1633
+ if (!ts.isIdentifier(d.name) || !d.initializer)
1634
+ continue;
1635
+ const binding = this.parseLocalKeyBinding(d.initializer);
1636
+ if (binding)
1637
+ localKeyBindings.set(d.name.text, binding);
1638
+ }
1639
+ } else if (ts.isReturnStatement(s) && s.expression) {
1640
+ returned = s.expression;
1641
+ } else if (ts.isExpressionStatement(s) || ts.isEmptyStatement(s)) {} else {
1642
+ return null;
1643
+ }
1644
+ }
1645
+ if (!returned)
1646
+ return null;
1647
+ body = returned;
1648
+ while (ts.isParenthesizedExpression(body)) {
1649
+ body = body.expression;
1650
+ }
1651
+ }
1652
+ if (!ts.isTemplateExpression(body) && !ts.isNoSubstitutionTemplateLiteral(body)) {
1653
+ return null;
1654
+ }
1655
+ const propNames = new Set(propsParams.map((p) => p.name));
1656
+ const escGo = (s) => `"${this.escapeGoString(s)}"`;
1657
+ const segments = [];
1658
+ if (ts.isNoSubstitutionTemplateLiteral(body)) {
1659
+ return escGo(body.text);
1660
+ }
1661
+ if (body.head.text)
1662
+ segments.push(escGo(body.head.text));
1663
+ for (const span of body.templateSpans) {
1664
+ const resolved = this.resolveTemplateInterpolation(span.expression, propNames, localKeyBindings);
1665
+ if (resolved === null)
1666
+ return null;
1667
+ segments.push(resolved);
1668
+ if (span.literal.text)
1669
+ segments.push(escGo(span.literal.text));
1670
+ }
1671
+ if (segments.length === 0)
1672
+ return '""';
1673
+ return segments.join(" + ");
1674
+ }
1675
+ resolveTemplateInterpolation(expr, propNames, localKeyBindings = new Map) {
1676
+ let node = expr;
1677
+ while (ts.isParenthesizedExpression(node))
1678
+ node = node.expression;
1679
+ if (ts.isIdentifier(node)) {
1680
+ const inlined = this.resolveModuleStringConst(node.text);
1681
+ if (inlined !== null)
1682
+ return inlined;
1683
+ return null;
1684
+ }
1685
+ if (ts.isElementAccessExpression(node)) {
1686
+ const indexed = this.recordIndexInterpolationToGo(node, propNames, localKeyBindings);
1687
+ if (indexed !== null)
1688
+ return indexed;
1689
+ return null;
1690
+ }
1691
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
1692
+ const right = node.right;
1693
+ const isEmptyStr = (ts.isStringLiteral(right) || ts.isNoSubstitutionTemplateLiteral(right)) && right.text === "";
1694
+ const propName2 = this.propsAccessName(node.left);
1695
+ if (propName2 && propNames.has(propName2) && isEmptyStr) {
1696
+ return `in.${this.capitalizeFieldName(propName2)}`;
1697
+ }
1698
+ return null;
1699
+ }
1700
+ const propName = this.propsAccessName(node);
1701
+ if (propName && propNames.has(propName)) {
1702
+ return `in.${this.capitalizeFieldName(propName)}`;
1703
+ }
1704
+ return null;
1705
+ }
1706
+ parseLocalKeyBinding(init) {
1707
+ let node = init;
1708
+ while (ts.isParenthesizedExpression(node))
1709
+ node = node.expression;
1710
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
1711
+ const propName2 = this.propsAccessName(node.left);
1712
+ const right = node.right;
1713
+ if (propName2 && (ts.isStringLiteral(right) || ts.isNoSubstitutionTemplateLiteral(right))) {
1714
+ return { propName: propName2, defaultLiteral: right.text };
1715
+ }
1716
+ return null;
1717
+ }
1718
+ const propName = this.propsAccessName(node);
1719
+ if (propName)
1720
+ return { propName };
1721
+ return null;
1722
+ }
1723
+ recordIndexInterpolationToGo(node, propNames, localKeyBindings) {
1724
+ const parsed = parseRecordIndexAccess(node, this.localConstants ?? [], [...propNames].map((name) => ({ name })), (name) => localKeyBindings.get(name) ?? null);
1725
+ if (!parsed)
1726
+ return null;
1727
+ const goVal = (v) => v.kind === "number" ? v.text : JSON.stringify(v.text);
1728
+ const entries = parsed.entries.map((e) => `${JSON.stringify(e.key)}: ${goVal(e.value)}`);
1729
+ if (parsed.defaultKey !== undefined) {
1730
+ const def = parsed.entries.find((e) => e.key === parsed.defaultKey);
1731
+ if (def)
1732
+ entries.unshift(`"": ${goVal(def.value)}`);
1733
+ }
1734
+ const allString = parsed.entries.every((e) => e.value.kind === "string");
1735
+ const mapType = allString ? "map[string]string" : "map[string]any";
1736
+ this.usesFmt = true;
1737
+ return `${mapType}{${entries.join(", ")}}[fmt.Sprint(in.${this.capitalizeFieldName(parsed.indexPropName)})]`;
1738
+ }
1739
+ propsAccessName(node) {
1740
+ if (!ts.isPropertyAccessExpression(node))
1741
+ return null;
1742
+ if (!ts.isIdentifier(node.expression))
1743
+ return null;
1744
+ if (!this.propsObjectName || node.expression.text !== this.propsObjectName)
1745
+ return null;
1746
+ return node.name.text;
1747
+ }
1748
+ computeMemoInitialValue(memo, signals, propsParams, propFallbackVars = GoTemplateAdapter.EMPTY_PROP_FALLBACK_VARS, goType) {
1364
1749
  const computation = memo.computation;
1365
1750
  const propRef = (propName) => {
1366
1751
  const hoisted = propFallbackVars.get(propName);
@@ -1368,6 +1753,9 @@ ${goFields.join(`
1368
1753
  return hoisted.varName;
1369
1754
  return `in.${this.capitalizeFieldName(propName)}`;
1370
1755
  };
1756
+ const tmplMemo = this.computeTemplateLiteralMemoInitialValue(computation, propsParams);
1757
+ if (tmplMemo !== null)
1758
+ return tmplMemo;
1371
1759
  const arithmeticMatch = computation.match(/\(\)\s*=>\s*(\w+)\(\)\s*([*+\-/])\s*(\d+)/);
1372
1760
  if (arithmeticMatch) {
1373
1761
  const [, depName, operator, operand] = arithmeticMatch;
@@ -1387,8 +1775,8 @@ ${goFields.join(`
1387
1775
  return `${hoisted.varName} ${operator} ${operand}`;
1388
1776
  const fieldName = this.capitalizeFieldName(propName);
1389
1777
  if (param.type) {
1390
- const goType = this.typeInfoToGo(param.type, param.defaultValue);
1391
- if (goType === "interface{}")
1778
+ const goType2 = this.typeInfoToGo(param.type, param.defaultValue);
1779
+ if (goType2 === "interface{}")
1392
1780
  return `in.${fieldName}.(int) ${operator} ${operand}`;
1393
1781
  }
1394
1782
  return `in.${fieldName} ${operator} ${operand}`;
@@ -1417,8 +1805,8 @@ ${goFields.join(`
1417
1805
  if (param) {
1418
1806
  const fieldName = this.capitalizeFieldName(varName);
1419
1807
  if (param.type) {
1420
- const goType = this.typeInfoToGo(param.type, param.defaultValue);
1421
- if (goType === "interface{}")
1808
+ const goType2 = this.typeInfoToGo(param.type, param.defaultValue);
1809
+ if (goType2 === "interface{}")
1422
1810
  return `in.${fieldName}.(int) ${operator} ${operand}`;
1423
1811
  }
1424
1812
  return `in.${fieldName} ${operator} ${operand}`;
@@ -1432,9 +1820,40 @@ ${goFields.join(`
1432
1820
  return `in.${this.capitalizeFieldName(varName)}`;
1433
1821
  }
1434
1822
  }
1823
+ if (goType === "bool")
1824
+ return "false";
1825
+ if (goType === "string")
1826
+ return '""';
1435
1827
  return "0";
1436
1828
  }
1829
+ isTemplateLiteralMemo(computation) {
1830
+ const sf = ts.createSourceFile("__memo.ts", `const __x = (${computation});`, ts.ScriptTarget.Latest, false);
1831
+ const stmt = sf.statements[0];
1832
+ if (!stmt || !ts.isVariableStatement(stmt))
1833
+ return false;
1834
+ let init = stmt.declarationList.declarations[0]?.initializer;
1835
+ while (init && ts.isParenthesizedExpression(init))
1836
+ init = init.expression;
1837
+ if (!init || !ts.isArrowFunction(init))
1838
+ return false;
1839
+ let body = init.body;
1840
+ while (ts.isParenthesizedExpression(body)) {
1841
+ body = body.expression;
1842
+ }
1843
+ if (ts.isBlock(body)) {
1844
+ const ret = body.statements.find(ts.isReturnStatement);
1845
+ if (!ret || !ret.expression)
1846
+ return false;
1847
+ body = ret.expression;
1848
+ while (ts.isParenthesizedExpression(body)) {
1849
+ body = body.expression;
1850
+ }
1851
+ }
1852
+ return ts.isTemplateExpression(body) || ts.isNoSubstitutionTemplateLiteral(body);
1853
+ }
1437
1854
  inferMemoType(memo, signals, propsParamMap) {
1855
+ if (this.isTemplateLiteralMemo(memo.computation))
1856
+ return "string";
1438
1857
  if (memo.computation.includes("*") || memo.computation.includes("/") || memo.computation.includes("+") || memo.computation.includes("-")) {
1439
1858
  for (const dep of memo.deps) {
1440
1859
  const signal = signals.find((s) => s.getter === dep);
@@ -1458,8 +1877,39 @@ ${goFields.join(`
1458
1877
  }
1459
1878
  }
1460
1879
  }
1880
+ if (this.typeInfoToGo(memo.type) === "interface{}" && this.isBooleanMemo(memo, signals, propsParamMap)) {
1881
+ return "bool";
1882
+ }
1461
1883
  return this.typeInfoToGo(memo.type);
1462
1884
  }
1885
+ isBooleanMemo(memo, signals, propsParamMap) {
1886
+ const c = memo.computation;
1887
+ if (/(!==|===|!=(?!=)|==(?!=))/.test(c))
1888
+ return true;
1889
+ if (/=>\s*!/.test(c))
1890
+ return true;
1891
+ const isBoolGetter = (name) => {
1892
+ const sig = signals.find((s) => s.getter === name);
1893
+ if (sig) {
1894
+ if (this.typeInfoToGo(sig.type) === "bool")
1895
+ return true;
1896
+ if (/\?\?\s*(true|false)\b/.test(sig.initialValue))
1897
+ return true;
1898
+ const propName = this.extractPropNameFromInitialValue(sig.initialValue) ?? sig.initialValue;
1899
+ const prop2 = propsParamMap.get(propName);
1900
+ if (prop2 && this.typeInfoToGo(prop2.type, prop2.defaultValue) === "bool")
1901
+ return true;
1902
+ return false;
1903
+ }
1904
+ const prop = propsParamMap.get(name);
1905
+ return !!prop && this.typeInfoToGo(prop.type, prop.defaultValue) === "bool";
1906
+ };
1907
+ const ternary = c.match(/=>\s*\w+\(\)\s*\?\s*(\w+)\(\)\s*:\s*(\w+)\(\)/);
1908
+ if (ternary) {
1909
+ return isBoolGetter(ternary[1]) && isBoolGetter(ternary[2]);
1910
+ }
1911
+ return false;
1912
+ }
1463
1913
  inferTypeFromValue(value) {
1464
1914
  if (value === "true" || value === "false")
1465
1915
  return "bool";
@@ -1807,8 +2257,42 @@ ${goFields.join(`
1807
2257
  if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
1808
2258
  return init.text;
1809
2259
  }
2260
+ const joined = this.evalStringArrayJoin(init);
2261
+ if (joined !== null)
2262
+ return joined;
1810
2263
  return null;
1811
2264
  }
2265
+ evalStringArrayJoin(node) {
2266
+ if (!ts.isCallExpression(node))
2267
+ return null;
2268
+ const callee = node.expression;
2269
+ if (!ts.isPropertyAccessExpression(callee))
2270
+ return null;
2271
+ if (callee.name.text !== "join")
2272
+ return null;
2273
+ let recv = callee.expression;
2274
+ while (ts.isParenthesizedExpression(recv))
2275
+ recv = recv.expression;
2276
+ if (!ts.isArrayLiteralExpression(recv))
2277
+ return null;
2278
+ const parts = [];
2279
+ for (const el of recv.elements) {
2280
+ if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) {
2281
+ parts.push(el.text);
2282
+ } else {
2283
+ return null;
2284
+ }
2285
+ }
2286
+ let sep = ",";
2287
+ if (node.arguments.length >= 1) {
2288
+ const arg = node.arguments[0];
2289
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg))
2290
+ sep = arg.text;
2291
+ else
2292
+ return null;
2293
+ }
2294
+ return parts.join(sep);
2295
+ }
1812
2296
  resolveModuleStringConst(name) {
1813
2297
  if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
1814
2298
  return null;
@@ -2968,6 +3452,12 @@ ${children}`;
2968
3452
  if (parsed.kind === "conditional" || parsed.kind === "template-literal") {
2969
3453
  return `${name}="${this.renderParsedExpr(parsed)}"`;
2970
3454
  }
3455
+ const bareId = value.expr.trim();
3456
+ const propName = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
3457
+ if (/^[A-Za-z_$][\w$]*$/.test(propName) && this.nillablePropNames.has(propName)) {
3458
+ const field = `.${this.capitalizeFieldName(propName)}`;
3459
+ return `{{if ne ${field} nil}}${name}="{{${this.convertExpressionToGo(value.expr)}}}"{{end}}`;
3460
+ }
2971
3461
  return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`;
2972
3462
  },
2973
3463
  emitBooleanAttr: (_value, name) => name,