@barefootjs/go-template 0.6.1 → 0.8.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/build.js CHANGED
@@ -457,6 +457,8 @@ class GoTemplateAdapter extends BaseAdapter {
457
457
  localStructFields = new Map;
458
458
  synthStructTypes = new Map;
459
459
  usesHtmlTemplate = false;
460
+ moduleStringConsts = new Map;
461
+ nillablePropNames = new Set;
460
462
  constructor(options = {}) {
461
463
  super();
462
464
  this.options = {
@@ -471,6 +473,8 @@ class GoTemplateAdapter extends BaseAdapter {
471
473
  this.templateVarCounter = 0;
472
474
  this.propsObjectName = ir.metadata.propsObjectName;
473
475
  this.restPropsName = ir.metadata.restPropsName ?? null;
476
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
477
+ this.nillablePropNames = this.collectNillablePropNames(ir);
474
478
  if (!options?.siblingTemplatesRegistered) {
475
479
  this.checkImportedLoopChildComponents(ir);
476
480
  }
@@ -906,6 +910,19 @@ ${goFields.join(`
906
910
  }
907
911
  return overrides;
908
912
  }
913
+ resolvePropGoType(param, propTypeOverrides) {
914
+ return propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
915
+ }
916
+ collectNillablePropNames(ir) {
917
+ const propTypeOverrides = this.buildPropTypeOverrides(ir);
918
+ const nillable = new Set;
919
+ for (const param of ir.metadata.propsParams) {
920
+ if (this.resolvePropGoType(param, propTypeOverrides) === "interface{}") {
921
+ nillable.add(param.name);
922
+ }
923
+ }
924
+ return nillable;
925
+ }
909
926
  generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
910
927
  const inputTypeName = `${componentName}Input`;
911
928
  lines.push(`// ${inputTypeName} is the user-facing input type.`);
@@ -919,7 +936,7 @@ ${goFields.join(`
919
936
  const fieldName = this.capitalizeFieldName(param.name);
920
937
  if (nestedArrayFields.has(fieldName))
921
938
  continue;
922
- const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
939
+ const goType = this.resolvePropGoType(param, propTypeOverrides);
923
940
  lines.push(` ${fieldName} ${goType}`);
924
941
  }
925
942
  for (const nested of inputNested) {
@@ -958,7 +975,7 @@ ${goFields.join(`
958
975
  const fieldName = this.capitalizeFieldName(param.name);
959
976
  if (nestedArrayFields.has(fieldName))
960
977
  continue;
961
- const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
978
+ const goType = this.resolvePropGoType(param, propTypeOverrides);
962
979
  const jsonTag = this.toJsonTag(param.name);
963
980
  lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
964
981
  propFieldNames.add(fieldName);
@@ -1252,6 +1269,12 @@ ${goFields.join(`
1252
1269
  collectStaticChildInstancesRecursive(node, result, inLoop) {
1253
1270
  if (node.type === "component") {
1254
1271
  const comp = node;
1272
+ if (comp.dynamicTag) {
1273
+ for (const child of comp.children) {
1274
+ this.collectStaticChildInstancesRecursive(child, result, inLoop);
1275
+ }
1276
+ return;
1277
+ }
1255
1278
  if (comp.name !== "Portal" && !inLoop && comp.slotId) {
1256
1279
  const suffix = slotIdToFieldSuffix(comp.slotId);
1257
1280
  result.push({
@@ -1426,6 +1449,9 @@ ${goFields.join(`
1426
1449
  }
1427
1450
  buildSpreadInitializer(spreadExpr, ir) {
1428
1451
  const trimmed = spreadExpr.trim();
1452
+ const conditional = this.buildConditionalSpreadInitializer(trimmed, ir);
1453
+ if (conditional !== undefined)
1454
+ return conditional;
1429
1455
  const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed);
1430
1456
  if (callMatch) {
1431
1457
  const getterName = callMatch[1];
@@ -1452,6 +1478,98 @@ ${goFields.join(`
1452
1478
  }
1453
1479
  return null;
1454
1480
  }
1481
+ buildConditionalSpreadInitializer(spreadExpr, ir) {
1482
+ const expr = this.parseLiteralExpression(spreadExpr);
1483
+ if (!expr || !ts.isConditionalExpression(expr))
1484
+ return;
1485
+ const whenTrue = this.unwrapParens(expr.whenTrue);
1486
+ const whenFalse = this.unwrapParens(expr.whenFalse);
1487
+ if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
1488
+ return;
1489
+ }
1490
+ const goCond = this.conditionToGoBool(expr.condition, ir);
1491
+ if (goCond === null)
1492
+ return null;
1493
+ const trueMap = this.objectLiteralToGoSpreadMap(whenTrue, ir);
1494
+ const falseMap = this.objectLiteralToGoSpreadMap(whenFalse, ir);
1495
+ if (trueMap === null || falseMap === null)
1496
+ return null;
1497
+ return `func() map[string]any {
1498
+ ` + ` if ${goCond} {
1499
+ ` + ` return ${trueMap}
1500
+ ` + ` }
1501
+ ` + ` return ${falseMap}
1502
+ ` + ` }()`;
1503
+ }
1504
+ unwrapParens(node) {
1505
+ let e = node;
1506
+ while (ts.isParenthesizedExpression(e))
1507
+ e = e.expression;
1508
+ return e;
1509
+ }
1510
+ conditionToGoBool(condition, ir) {
1511
+ let node = this.unwrapParens(condition);
1512
+ let negate = false;
1513
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
1514
+ negate = true;
1515
+ node = this.unwrapParens(node.operand);
1516
+ }
1517
+ if (!ts.isIdentifier(node))
1518
+ return null;
1519
+ const param = ir.metadata.propsParams.find((p) => p.name === node.text);
1520
+ if (!param)
1521
+ return null;
1522
+ const field = `in.${this.capitalizeFieldName(param.name)}`;
1523
+ const prim = param.type.kind === "primitive" ? param.type.primitive : undefined;
1524
+ let truthy;
1525
+ if (prim === "boolean") {
1526
+ truthy = field;
1527
+ } else if (prim === "number") {
1528
+ truthy = `${field} != 0`;
1529
+ } else if (prim === "string") {
1530
+ truthy = `${field} != ""`;
1531
+ } else {
1532
+ truthy = `bf.Truthy(${field})`;
1533
+ }
1534
+ if (!negate)
1535
+ return truthy;
1536
+ if (prim === "boolean")
1537
+ return `!${field}`;
1538
+ if (prim === "number")
1539
+ return `${field} == 0`;
1540
+ if (prim === "string")
1541
+ return `${field} == ""`;
1542
+ return `!bf.Truthy(${field})`;
1543
+ }
1544
+ objectLiteralToGoSpreadMap(obj, ir) {
1545
+ const entries = [];
1546
+ for (const prop of obj.properties) {
1547
+ if (!ts.isPropertyAssignment(prop))
1548
+ return null;
1549
+ let key;
1550
+ if (ts.isIdentifier(prop.name)) {
1551
+ key = prop.name.text;
1552
+ } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
1553
+ key = prop.name.text;
1554
+ } else {
1555
+ return null;
1556
+ }
1557
+ const val = this.unwrapParens(prop.initializer);
1558
+ let goVal;
1559
+ if (ts.isStringLiteral(val) || ts.isNoSubstitutionTemplateLiteral(val)) {
1560
+ goVal = JSON.stringify(val.text);
1561
+ } else if (ts.isIdentifier(val)) {
1562
+ const param = ir.metadata.propsParams.find((p) => p.name === val.text);
1563
+ if (!param)
1564
+ return null;
1565
+ goVal = `in.${this.capitalizeFieldName(param.name)}`;
1566
+ } else {
1567
+ return null;
1568
+ }
1569
+ entries.push(`${JSON.stringify(key)}: ${goVal}`);
1570
+ }
1571
+ return `map[string]any{${entries.join(", ")}}`;
1572
+ }
1455
1573
  convertInitialValue(value, typeInfo, propsParams) {
1456
1574
  if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
1457
1575
  if (propsParams?.some((p) => p.name === value)) {
@@ -2088,6 +2206,9 @@ ${goFields.join(`
2088
2206
  return emitParsedExpr(expr, this);
2089
2207
  }
2090
2208
  identifier(name) {
2209
+ const inlined = this.resolveModuleStringConst(name);
2210
+ if (inlined !== null)
2211
+ return inlined;
2091
2212
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2092
2213
  if (currentLoopParam && name === currentLoopParam)
2093
2214
  return ".";
@@ -2109,6 +2230,48 @@ ${goFields.join(`
2109
2230
  const prefix = this.loopParamStack.length > 0 ? "$." : ".";
2110
2231
  return `${prefix}${this.capitalizeFieldName(name)}`;
2111
2232
  }
2233
+ collectModuleStringConsts(constants) {
2234
+ const map = new Map;
2235
+ for (const c of constants ?? []) {
2236
+ if (!c.isModule)
2237
+ continue;
2238
+ if (c.value === undefined)
2239
+ continue;
2240
+ const literal = this.parsePureStringLiteral(c.value);
2241
+ if (literal !== null)
2242
+ map.set(c.name, literal);
2243
+ }
2244
+ return map;
2245
+ }
2246
+ parsePureStringLiteral(source) {
2247
+ const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
2248
+ const stmt = sf.statements[0];
2249
+ if (!stmt || !ts.isVariableStatement(stmt))
2250
+ return null;
2251
+ const decl = stmt.declarationList.declarations[0];
2252
+ let init = decl?.initializer;
2253
+ while (init && ts.isParenthesizedExpression(init))
2254
+ init = init.expression;
2255
+ if (!init)
2256
+ return null;
2257
+ if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
2258
+ return init.text;
2259
+ }
2260
+ return null;
2261
+ }
2262
+ resolveModuleStringConst(name) {
2263
+ if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
2264
+ return null;
2265
+ }
2266
+ if (this.loopVarRefCount.has(name))
2267
+ return null;
2268
+ if (this.isOuterLoopParam(name))
2269
+ return null;
2270
+ const value = this.moduleStringConsts.get(name);
2271
+ if (value === undefined)
2272
+ return null;
2273
+ return `"${this.escapeGoString(value)}"`;
2274
+ }
2112
2275
  literal(value, literalType) {
2113
2276
  if (literalType === "string")
2114
2277
  return `"${value}"`;
@@ -2949,6 +3112,9 @@ ${goFields.join(`
2949
3112
  switch (expr.kind) {
2950
3113
  case "identifier":
2951
3114
  {
3115
+ const inlined = this.resolveModuleStringConst(expr.name);
3116
+ if (inlined !== null)
3117
+ return plain(inlined);
2952
3118
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2953
3119
  if (currentLoopParam && expr.name === currentLoopParam) {
2954
3120
  return plain(".");
@@ -2971,6 +3137,20 @@ ${goFields.join(`
2971
3137
  if (expr.callee.kind === "identifier" && expr.args.length === 0) {
2972
3138
  return plain(this.rootFieldRef(expr.callee.name));
2973
3139
  }
3140
+ if (expr.callee.kind === "identifier" && (identifierPath(expr.callee) ?? expr.callee.name) === "isValidElement" && expr.args.length === 1) {
3141
+ return this.renderConditionExpr(expr.args[0]);
3142
+ }
3143
+ if (expr.callee.kind === "identifier" && !this.templatePrimitives[identifierPath(expr.callee) ?? ""]) {
3144
+ const path = identifierPath(expr.callee) ?? expr.callee.name;
3145
+ this.errors.push({
3146
+ code: "BF102",
3147
+ severity: "error",
3148
+ message: `Predicate '${path}(...)' cannot be evaluated in a Go template. ` + `A server-side template cannot call user-defined JavaScript predicates.`,
3149
+ loc: this.makeLoc(),
3150
+ suggestion: { message: GO_REMEDIATION_OPTIONS }
3151
+ });
3152
+ return plain("false");
3153
+ }
2974
3154
  return plain(this.renderParsedExpr(expr));
2975
3155
  }
2976
3156
  case "member": {
@@ -3182,6 +3362,9 @@ ${goFields.join(`
3182
3362
  if (comp.name === "Portal") {
3183
3363
  return this.renderPortalComponent(comp);
3184
3364
  }
3365
+ if (comp.dynamicTag) {
3366
+ return this.renderChildren(comp.children);
3367
+ }
3185
3368
  let templateCall;
3186
3369
  if (this.inLoop) {
3187
3370
  templateCall = `{{template "${comp.name}" .}}`;
@@ -3235,6 +3418,11 @@ ${children}`;
3235
3418
  if (parsed.kind === "conditional" || parsed.kind === "template-literal") {
3236
3419
  return `${name}="${this.renderParsedExpr(parsed)}"`;
3237
3420
  }
3421
+ const bareId = value.expr.trim();
3422
+ if (this.nillablePropNames.has(bareId)) {
3423
+ const field = `.${this.capitalizeFieldName(bareId)}`;
3424
+ return `{{if ne ${field} nil}}${name}="{{${this.convertExpressionToGo(value.expr)}}}"{{end}}`;
3425
+ }
3238
3426
  return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`;
3239
3427
  },
3240
3428
  emitBooleanAttr: (_value, name) => name,
@@ -3325,7 +3513,7 @@ ${children}`;
3325
3513
  continue;
3326
3514
  const branches = caseEntries.map(([k, v], i) => {
3327
3515
  const head = i === 0 ? "{{if" : "{{else if";
3328
- return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${v}`;
3516
+ return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${this.escapeAttrText(v)}`;
3329
3517
  });
3330
3518
  output += branches.join("") + "{{end}}";
3331
3519
  }
package/dist/index.js CHANGED
@@ -117,6 +117,8 @@ class GoTemplateAdapter extends BaseAdapter {
117
117
  localStructFields = new Map;
118
118
  synthStructTypes = new Map;
119
119
  usesHtmlTemplate = false;
120
+ moduleStringConsts = new Map;
121
+ nillablePropNames = new Set;
120
122
  constructor(options = {}) {
121
123
  super();
122
124
  this.options = {
@@ -131,6 +133,8 @@ class GoTemplateAdapter extends BaseAdapter {
131
133
  this.templateVarCounter = 0;
132
134
  this.propsObjectName = ir.metadata.propsObjectName;
133
135
  this.restPropsName = ir.metadata.restPropsName ?? null;
136
+ this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
137
+ this.nillablePropNames = this.collectNillablePropNames(ir);
134
138
  if (!options?.siblingTemplatesRegistered) {
135
139
  this.checkImportedLoopChildComponents(ir);
136
140
  }
@@ -566,6 +570,19 @@ ${goFields.join(`
566
570
  }
567
571
  return overrides;
568
572
  }
573
+ resolvePropGoType(param, propTypeOverrides) {
574
+ return propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
575
+ }
576
+ collectNillablePropNames(ir) {
577
+ const propTypeOverrides = this.buildPropTypeOverrides(ir);
578
+ const nillable = new Set;
579
+ for (const param of ir.metadata.propsParams) {
580
+ if (this.resolvePropGoType(param, propTypeOverrides) === "interface{}") {
581
+ nillable.add(param.name);
582
+ }
583
+ }
584
+ return nillable;
585
+ }
569
586
  generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
570
587
  const inputTypeName = `${componentName}Input`;
571
588
  lines.push(`// ${inputTypeName} is the user-facing input type.`);
@@ -579,7 +596,7 @@ ${goFields.join(`
579
596
  const fieldName = this.capitalizeFieldName(param.name);
580
597
  if (nestedArrayFields.has(fieldName))
581
598
  continue;
582
- const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
599
+ const goType = this.resolvePropGoType(param, propTypeOverrides);
583
600
  lines.push(` ${fieldName} ${goType}`);
584
601
  }
585
602
  for (const nested of inputNested) {
@@ -618,7 +635,7 @@ ${goFields.join(`
618
635
  const fieldName = this.capitalizeFieldName(param.name);
619
636
  if (nestedArrayFields.has(fieldName))
620
637
  continue;
621
- const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
638
+ const goType = this.resolvePropGoType(param, propTypeOverrides);
622
639
  const jsonTag = this.toJsonTag(param.name);
623
640
  lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
624
641
  propFieldNames.add(fieldName);
@@ -912,6 +929,12 @@ ${goFields.join(`
912
929
  collectStaticChildInstancesRecursive(node, result, inLoop) {
913
930
  if (node.type === "component") {
914
931
  const comp = node;
932
+ if (comp.dynamicTag) {
933
+ for (const child of comp.children) {
934
+ this.collectStaticChildInstancesRecursive(child, result, inLoop);
935
+ }
936
+ return;
937
+ }
915
938
  if (comp.name !== "Portal" && !inLoop && comp.slotId) {
916
939
  const suffix = slotIdToFieldSuffix(comp.slotId);
917
940
  result.push({
@@ -1086,6 +1109,9 @@ ${goFields.join(`
1086
1109
  }
1087
1110
  buildSpreadInitializer(spreadExpr, ir) {
1088
1111
  const trimmed = spreadExpr.trim();
1112
+ const conditional = this.buildConditionalSpreadInitializer(trimmed, ir);
1113
+ if (conditional !== undefined)
1114
+ return conditional;
1089
1115
  const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed);
1090
1116
  if (callMatch) {
1091
1117
  const getterName = callMatch[1];
@@ -1112,6 +1138,98 @@ ${goFields.join(`
1112
1138
  }
1113
1139
  return null;
1114
1140
  }
1141
+ buildConditionalSpreadInitializer(spreadExpr, ir) {
1142
+ const expr = this.parseLiteralExpression(spreadExpr);
1143
+ if (!expr || !ts.isConditionalExpression(expr))
1144
+ return;
1145
+ const whenTrue = this.unwrapParens(expr.whenTrue);
1146
+ const whenFalse = this.unwrapParens(expr.whenFalse);
1147
+ if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
1148
+ return;
1149
+ }
1150
+ const goCond = this.conditionToGoBool(expr.condition, ir);
1151
+ if (goCond === null)
1152
+ return null;
1153
+ const trueMap = this.objectLiteralToGoSpreadMap(whenTrue, ir);
1154
+ const falseMap = this.objectLiteralToGoSpreadMap(whenFalse, ir);
1155
+ if (trueMap === null || falseMap === null)
1156
+ return null;
1157
+ return `func() map[string]any {
1158
+ ` + ` if ${goCond} {
1159
+ ` + ` return ${trueMap}
1160
+ ` + ` }
1161
+ ` + ` return ${falseMap}
1162
+ ` + ` }()`;
1163
+ }
1164
+ unwrapParens(node) {
1165
+ let e = node;
1166
+ while (ts.isParenthesizedExpression(e))
1167
+ e = e.expression;
1168
+ return e;
1169
+ }
1170
+ conditionToGoBool(condition, ir) {
1171
+ let node = this.unwrapParens(condition);
1172
+ let negate = false;
1173
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
1174
+ negate = true;
1175
+ node = this.unwrapParens(node.operand);
1176
+ }
1177
+ if (!ts.isIdentifier(node))
1178
+ return null;
1179
+ const param = ir.metadata.propsParams.find((p) => p.name === node.text);
1180
+ if (!param)
1181
+ return null;
1182
+ const field = `in.${this.capitalizeFieldName(param.name)}`;
1183
+ const prim = param.type.kind === "primitive" ? param.type.primitive : undefined;
1184
+ let truthy;
1185
+ if (prim === "boolean") {
1186
+ truthy = field;
1187
+ } else if (prim === "number") {
1188
+ truthy = `${field} != 0`;
1189
+ } else if (prim === "string") {
1190
+ truthy = `${field} != ""`;
1191
+ } else {
1192
+ truthy = `bf.Truthy(${field})`;
1193
+ }
1194
+ if (!negate)
1195
+ return truthy;
1196
+ if (prim === "boolean")
1197
+ return `!${field}`;
1198
+ if (prim === "number")
1199
+ return `${field} == 0`;
1200
+ if (prim === "string")
1201
+ return `${field} == ""`;
1202
+ return `!bf.Truthy(${field})`;
1203
+ }
1204
+ objectLiteralToGoSpreadMap(obj, ir) {
1205
+ const entries = [];
1206
+ for (const prop of obj.properties) {
1207
+ if (!ts.isPropertyAssignment(prop))
1208
+ return null;
1209
+ let key;
1210
+ if (ts.isIdentifier(prop.name)) {
1211
+ key = prop.name.text;
1212
+ } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
1213
+ key = prop.name.text;
1214
+ } else {
1215
+ return null;
1216
+ }
1217
+ const val = this.unwrapParens(prop.initializer);
1218
+ let goVal;
1219
+ if (ts.isStringLiteral(val) || ts.isNoSubstitutionTemplateLiteral(val)) {
1220
+ goVal = JSON.stringify(val.text);
1221
+ } else if (ts.isIdentifier(val)) {
1222
+ const param = ir.metadata.propsParams.find((p) => p.name === val.text);
1223
+ if (!param)
1224
+ return null;
1225
+ goVal = `in.${this.capitalizeFieldName(param.name)}`;
1226
+ } else {
1227
+ return null;
1228
+ }
1229
+ entries.push(`${JSON.stringify(key)}: ${goVal}`);
1230
+ }
1231
+ return `map[string]any{${entries.join(", ")}}`;
1232
+ }
1115
1233
  convertInitialValue(value, typeInfo, propsParams) {
1116
1234
  if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
1117
1235
  if (propsParams?.some((p) => p.name === value)) {
@@ -1748,6 +1866,9 @@ ${goFields.join(`
1748
1866
  return emitParsedExpr(expr, this);
1749
1867
  }
1750
1868
  identifier(name) {
1869
+ const inlined = this.resolveModuleStringConst(name);
1870
+ if (inlined !== null)
1871
+ return inlined;
1751
1872
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
1752
1873
  if (currentLoopParam && name === currentLoopParam)
1753
1874
  return ".";
@@ -1769,6 +1890,48 @@ ${goFields.join(`
1769
1890
  const prefix = this.loopParamStack.length > 0 ? "$." : ".";
1770
1891
  return `${prefix}${this.capitalizeFieldName(name)}`;
1771
1892
  }
1893
+ collectModuleStringConsts(constants) {
1894
+ const map = new Map;
1895
+ for (const c of constants ?? []) {
1896
+ if (!c.isModule)
1897
+ continue;
1898
+ if (c.value === undefined)
1899
+ continue;
1900
+ const literal = this.parsePureStringLiteral(c.value);
1901
+ if (literal !== null)
1902
+ map.set(c.name, literal);
1903
+ }
1904
+ return map;
1905
+ }
1906
+ parsePureStringLiteral(source) {
1907
+ const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
1908
+ const stmt = sf.statements[0];
1909
+ if (!stmt || !ts.isVariableStatement(stmt))
1910
+ return null;
1911
+ const decl = stmt.declarationList.declarations[0];
1912
+ let init = decl?.initializer;
1913
+ while (init && ts.isParenthesizedExpression(init))
1914
+ init = init.expression;
1915
+ if (!init)
1916
+ return null;
1917
+ if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
1918
+ return init.text;
1919
+ }
1920
+ return null;
1921
+ }
1922
+ resolveModuleStringConst(name) {
1923
+ if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
1924
+ return null;
1925
+ }
1926
+ if (this.loopVarRefCount.has(name))
1927
+ return null;
1928
+ if (this.isOuterLoopParam(name))
1929
+ return null;
1930
+ const value = this.moduleStringConsts.get(name);
1931
+ if (value === undefined)
1932
+ return null;
1933
+ return `"${this.escapeGoString(value)}"`;
1934
+ }
1772
1935
  literal(value, literalType) {
1773
1936
  if (literalType === "string")
1774
1937
  return `"${value}"`;
@@ -2609,6 +2772,9 @@ ${goFields.join(`
2609
2772
  switch (expr.kind) {
2610
2773
  case "identifier":
2611
2774
  {
2775
+ const inlined = this.resolveModuleStringConst(expr.name);
2776
+ if (inlined !== null)
2777
+ return plain(inlined);
2612
2778
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2613
2779
  if (currentLoopParam && expr.name === currentLoopParam) {
2614
2780
  return plain(".");
@@ -2631,6 +2797,20 @@ ${goFields.join(`
2631
2797
  if (expr.callee.kind === "identifier" && expr.args.length === 0) {
2632
2798
  return plain(this.rootFieldRef(expr.callee.name));
2633
2799
  }
2800
+ if (expr.callee.kind === "identifier" && (identifierPath(expr.callee) ?? expr.callee.name) === "isValidElement" && expr.args.length === 1) {
2801
+ return this.renderConditionExpr(expr.args[0]);
2802
+ }
2803
+ if (expr.callee.kind === "identifier" && !this.templatePrimitives[identifierPath(expr.callee) ?? ""]) {
2804
+ const path = identifierPath(expr.callee) ?? expr.callee.name;
2805
+ this.errors.push({
2806
+ code: "BF102",
2807
+ severity: "error",
2808
+ message: `Predicate '${path}(...)' cannot be evaluated in a Go template. ` + `A server-side template cannot call user-defined JavaScript predicates.`,
2809
+ loc: this.makeLoc(),
2810
+ suggestion: { message: GO_REMEDIATION_OPTIONS }
2811
+ });
2812
+ return plain("false");
2813
+ }
2634
2814
  return plain(this.renderParsedExpr(expr));
2635
2815
  }
2636
2816
  case "member": {
@@ -2842,6 +3022,9 @@ ${goFields.join(`
2842
3022
  if (comp.name === "Portal") {
2843
3023
  return this.renderPortalComponent(comp);
2844
3024
  }
3025
+ if (comp.dynamicTag) {
3026
+ return this.renderChildren(comp.children);
3027
+ }
2845
3028
  let templateCall;
2846
3029
  if (this.inLoop) {
2847
3030
  templateCall = `{{template "${comp.name}" .}}`;
@@ -2895,6 +3078,11 @@ ${children}`;
2895
3078
  if (parsed.kind === "conditional" || parsed.kind === "template-literal") {
2896
3079
  return `${name}="${this.renderParsedExpr(parsed)}"`;
2897
3080
  }
3081
+ const bareId = value.expr.trim();
3082
+ if (this.nillablePropNames.has(bareId)) {
3083
+ const field = `.${this.capitalizeFieldName(bareId)}`;
3084
+ return `{{if ne ${field} nil}}${name}="{{${this.convertExpressionToGo(value.expr)}}}"{{end}}`;
3085
+ }
2898
3086
  return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`;
2899
3087
  },
2900
3088
  emitBooleanAttr: (_value, name) => name,
@@ -2985,7 +3173,7 @@ ${children}`;
2985
3173
  continue;
2986
3174
  const branches = caseEntries.map(([k, v], i) => {
2987
3175
  const head = i === 0 ? "{{if" : "{{else if";
2988
- return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${v}`;
3176
+ return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${this.escapeAttrText(v)}`;
2989
3177
  });
2990
3178
  output += branches.join("") + "{{end}}";
2991
3179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/go-template",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,6 +54,6 @@
54
54
  },
55
55
  "devDependencies": {
56
56
  "@barefootjs/adapter-tests": "0.1.0",
57
- "@barefootjs/jsx": "0.6.1"
57
+ "@barefootjs/jsx": "0.8.0"
58
58
  }
59
59
  }