@barefootjs/go-template 0.5.0 → 0.5.2

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
@@ -90,6 +90,8 @@ class GoTemplateAdapter extends BaseAdapter {
90
90
  templateVarCounter = 0;
91
91
  localTypeNames = new Set;
92
92
  localTypeAliases = new Map;
93
+ localStructFields = new Map;
94
+ synthStructTypes = new Map;
93
95
  usesHtmlTemplate = false;
94
96
  constructor(options = {}) {
95
97
  super();
@@ -323,6 +325,7 @@ ${scriptRegistrations}${templateBody}
323
325
  const componentName = ir.metadata.componentName;
324
326
  this.localTypeNames = new Set;
325
327
  this.localTypeAliases = new Map;
328
+ this.localStructFields = new Map;
326
329
  for (const td of ir.metadata.typeDefinitions) {
327
330
  if (td.name === "Props" || td.name === `${componentName}Props`)
328
331
  continue;
@@ -331,6 +334,11 @@ ${scriptRegistrations}${templateBody}
331
334
  this.localTypeNames.add(td.name);
332
335
  if (td.definition.match(/^type \w+ = ('[^']*'(\s*\|\s*'[^']*')*)/)) {
333
336
  this.localTypeAliases.set(td.name, "string");
337
+ } else {
338
+ const fields = this.structFieldsFor(td);
339
+ if (fields.length > 0) {
340
+ this.localStructFields.set(td.name, new Map(fields.map((f) => [f.tsName, f.goName])));
341
+ }
334
342
  }
335
343
  }
336
344
  for (const td of ir.metadata.typeDefinitions) {
@@ -344,6 +352,26 @@ ${scriptRegistrations}${templateBody}
344
352
  lines.push("");
345
353
  }
346
354
  }
355
+ this.synthStructTypes = new Map;
356
+ for (const signal of ir.metadata.signals) {
357
+ const synth = this.synthesizeStructFromSignal(signal, componentName);
358
+ if (!synth)
359
+ continue;
360
+ this.localTypeNames.add(synth.name);
361
+ this.localStructFields.set(synth.name, new Map(synth.fields.map((f) => [f.tsName, f.goName])));
362
+ this.synthStructTypes.set(signal.getter, {
363
+ kind: "array",
364
+ raw: `${synth.name}[]`,
365
+ elementType: { kind: "interface", raw: synth.name }
366
+ });
367
+ const goFields = synth.fields.map((f) => ` ${f.goName} ${f.goType} \`json:"${this.toJsonTag(f.tsName)}"\``);
368
+ lines.push(`// ${synth.name} is a synthesised element type for the ${signal.getter} signal.`);
369
+ lines.push(`type ${synth.name} struct {
370
+ ${goFields.join(`
371
+ `)}
372
+ }`);
373
+ lines.push("");
374
+ }
347
375
  const nestedComponents = this.findNestedComponents(ir.root);
348
376
  const propTypeOverrides = this.buildPropTypeOverrides(ir);
349
377
  const spreadSlots = this.collectSpreadSlots(ir.root);
@@ -365,35 +393,114 @@ ${scriptRegistrations}${templateBody}
365
393
  `);
366
394
  }
367
395
  typeDefinitionToGo(td) {
368
- const def = td.definition;
369
- if (def.match(/^type \w+ = ('[^']*'(\s*\|\s*'[^']*')*)/)) {
396
+ if (td.definition.match(/^type \w+ = ('[^']*'(\s*\|\s*'[^']*')*)/)) {
370
397
  return `// ${td.name} is a string type.
371
398
  type ${td.name} = string`;
372
399
  }
373
- const bodyMatch = def.match(/(?:type \w+ = |interface \w+ )\{([\s\S]*)\}/);
374
- if (!bodyMatch)
375
- return null;
376
- const body = bodyMatch[1];
377
- const goFields = [];
378
- const fieldEntries = body.split(/[;\n]/).map((s) => s.trim()).filter(Boolean);
379
- for (const entry of fieldEntries) {
380
- const fieldMatch = entry.match(/^(\w+)\??\s*:\s*(.+)$/);
381
- if (!fieldMatch)
382
- continue;
383
- const [, fieldName, tsType] = fieldMatch;
384
- const goFieldName = this.capitalizeFieldName(fieldName);
385
- const goType = this.tsTypeStringToGo(tsType.trim());
386
- const jsonTag = this.toJsonTag(fieldName);
387
- goFields.push(` ${goFieldName} ${goType} \`json:"${jsonTag}"\``);
388
- }
389
- if (goFields.length === 0)
400
+ const fields = this.structFieldsFor(td);
401
+ if (fields.length === 0)
390
402
  return null;
403
+ const goFields = fields.map((f) => ` ${f.goName} ${f.goType} \`json:"${this.toJsonTag(f.tsName)}"\``);
391
404
  return `// ${td.name} represents a ${td.name.toLowerCase()}.
392
405
  type ${td.name} struct {
393
406
  ${goFields.join(`
394
407
  `)}
395
408
  }`;
396
409
  }
410
+ structFieldsFor(td) {
411
+ const fields = [];
412
+ for (const prop of td.properties ?? []) {
413
+ if (!GoTemplateAdapter.GO_IDENTIFIER.test(prop.name))
414
+ continue;
415
+ fields.push({
416
+ tsName: prop.name,
417
+ goName: this.capitalizeFieldName(prop.name),
418
+ goType: this.typeInfoToGo(prop.type)
419
+ });
420
+ }
421
+ return fields;
422
+ }
423
+ synthesizeStructFromSignal(signal, componentName) {
424
+ if (signal.type.kind !== "array")
425
+ return null;
426
+ const elem = signal.type.elementType;
427
+ if (elem && elem.kind !== "unknown")
428
+ return null;
429
+ const node = this.parseLiteralExpression(signal.initialValue);
430
+ if (!node || !ts.isArrayLiteralExpression(node) || node.elements.length === 0)
431
+ return null;
432
+ const order = [];
433
+ const goTypes = new Map;
434
+ for (let i = 0;i < node.elements.length; i++) {
435
+ const el = node.elements[i];
436
+ if (!ts.isObjectLiteralExpression(el))
437
+ return null;
438
+ const seen = new Set;
439
+ for (const prop of el.properties) {
440
+ if (!ts.isPropertyAssignment(prop))
441
+ return null;
442
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name) && !ts.isNumericLiteral(prop.name)) {
443
+ return null;
444
+ }
445
+ const key = prop.name.text;
446
+ if (!GoTemplateAdapter.GO_IDENTIFIER.test(key))
447
+ return null;
448
+ const goType = this.scalarLiteralGoType(prop.initializer);
449
+ if (!goType)
450
+ return null;
451
+ seen.add(key);
452
+ const prev = goTypes.get(key);
453
+ if (prev === undefined) {
454
+ if (i !== 0)
455
+ return null;
456
+ order.push(key);
457
+ goTypes.set(key, goType);
458
+ } else {
459
+ const merged = this.mergeScalarGoType(prev, goType);
460
+ if (!merged)
461
+ return null;
462
+ goTypes.set(key, merged);
463
+ }
464
+ }
465
+ if (seen.size !== order.length)
466
+ return null;
467
+ }
468
+ const name = `${componentName}${this.capitalizeFieldName(signal.getter)}Item`;
469
+ if (this.localTypeNames.has(name))
470
+ return null;
471
+ return {
472
+ name,
473
+ fields: order.map((key) => ({
474
+ tsName: key,
475
+ goName: this.capitalizeFieldName(key),
476
+ goType: goTypes.get(key)
477
+ }))
478
+ };
479
+ }
480
+ scalarLiteralGoType(node) {
481
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
482
+ return this.numericLiteralGoType(node.operand.text);
483
+ }
484
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
485
+ return "string";
486
+ if (ts.isNumericLiteral(node))
487
+ return this.numericLiteralGoType(node.text);
488
+ if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) {
489
+ return "bool";
490
+ }
491
+ return null;
492
+ }
493
+ numericLiteralGoType(text) {
494
+ return /[.eE]/.test(text) && !text.startsWith("0x") ? "float64" : "int";
495
+ }
496
+ mergeScalarGoType(a, b) {
497
+ if (a === b)
498
+ return a;
499
+ const numeric = new Set(["int", "float64"]);
500
+ if (numeric.has(a) && numeric.has(b))
501
+ return "float64";
502
+ return null;
503
+ }
397
504
  tsTypeStringToGo(tsType) {
398
505
  const t = tsType.trim();
399
506
  if (t === "number")
@@ -498,6 +605,11 @@ ${goFields.join(`
498
605
  if (propFieldNames.has(fieldName))
499
606
  continue;
500
607
  const jsonTag = this.toJsonTag(signal.getter);
608
+ const synthType = this.synthStructTypes.get(signal.getter);
609
+ if (synthType) {
610
+ lines.push(` ${fieldName} ${this.typeInfoToGo(synthType)} \`json:"${jsonTag}"\``);
611
+ continue;
612
+ }
501
613
  let goType;
502
614
  let referencedProp = propsParamMap.get(signal.initialValue);
503
615
  if (!referencedProp) {
@@ -626,7 +738,8 @@ ${goFields.join(`
626
738
  if (hoisted) {
627
739
  lines.push(` ${fieldName}: ${hoisted.varName},`);
628
740
  } else {
629
- const initialValue = this.convertInitialValue(signal.initialValue, signal.type, ir.metadata.propsParams);
741
+ const bakeType = this.synthStructTypes.get(signal.getter) ?? signal.type;
742
+ const initialValue = this.convertInitialValue(signal.initialValue, bakeType, ir.metadata.propsParams);
630
743
  lines.push(` ${fieldName}: ${initialValue},`);
631
744
  }
632
745
  }
@@ -1007,10 +1120,7 @@ ${goFields.join(`
1007
1120
  }
1008
1121
  }
1009
1122
  if (typeInfo.kind === "array") {
1010
- if (value === "[]" || value === "null" || value === "undefined") {
1011
- return "nil";
1012
- }
1013
- return "nil";
1123
+ return this.jsLiteralToGo(value, typeInfo) ?? "nil";
1014
1124
  }
1015
1125
  if (typeInfo.kind === "interface" && typeInfo.raw) {
1016
1126
  const aliasBase = this.localTypeAliases.get(typeInfo.raw);
@@ -1023,6 +1133,81 @@ ${goFields.join(`
1023
1133
  }
1024
1134
  return "nil";
1025
1135
  }
1136
+ jsLiteralToGo(value, typeInfo) {
1137
+ const expr = this.parseLiteralExpression(value);
1138
+ if (!expr)
1139
+ return null;
1140
+ return this.tsLiteralToGo(expr, typeInfo);
1141
+ }
1142
+ parseLiteralExpression(value) {
1143
+ const sf = ts.createSourceFile("__lit.ts", `(${value})`, ts.ScriptTarget.Latest, true);
1144
+ if (sf.statements.length !== 1)
1145
+ return null;
1146
+ const stmt = sf.statements[0];
1147
+ if (!ts.isExpressionStatement(stmt))
1148
+ return null;
1149
+ let expr = stmt.expression;
1150
+ while (ts.isParenthesizedExpression(expr))
1151
+ expr = expr.expression;
1152
+ return expr;
1153
+ }
1154
+ tsLiteralToGo(node, typeInfo) {
1155
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
1156
+ return `-${node.operand.text}`;
1157
+ }
1158
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
1159
+ return JSON.stringify(node.text);
1160
+ }
1161
+ if (ts.isNumericLiteral(node))
1162
+ return node.text;
1163
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
1164
+ return "true";
1165
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
1166
+ return "false";
1167
+ if (node.kind === ts.SyntaxKind.NullKeyword)
1168
+ return "nil";
1169
+ if (ts.isArrayLiteralExpression(node)) {
1170
+ if (node.elements.length === 0)
1171
+ return null;
1172
+ const elemType = typeInfo?.kind === "array" ? typeInfo.elementType : undefined;
1173
+ const sliceHeader = typeInfo?.kind === "array" ? this.typeInfoToGo(typeInfo) : "[]interface{}";
1174
+ const elems = [];
1175
+ for (const el of node.elements) {
1176
+ const go = this.tsLiteralToGo(el, elemType);
1177
+ if (go === null)
1178
+ return null;
1179
+ elems.push(go);
1180
+ }
1181
+ return `${sliceHeader}{${elems.join(", ")}}`;
1182
+ }
1183
+ if (ts.isObjectLiteralExpression(node)) {
1184
+ const goType = typeInfo ? this.typeInfoToGo(typeInfo) : "interface{}";
1185
+ const structFields = this.localStructFields.get(goType);
1186
+ if (!structFields)
1187
+ return null;
1188
+ const entries = [];
1189
+ for (const prop of node.properties) {
1190
+ if (!ts.isPropertyAssignment(prop))
1191
+ return null;
1192
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name) && !ts.isNumericLiteral(prop.name)) {
1193
+ return null;
1194
+ }
1195
+ const goField = structFields.get(prop.name.text);
1196
+ if (!goField)
1197
+ return null;
1198
+ const init = prop.initializer;
1199
+ if (ts.isObjectLiteralExpression(init) || ts.isArrayLiteralExpression(init)) {
1200
+ return null;
1201
+ }
1202
+ const go = this.tsLiteralToGo(init);
1203
+ if (go === null)
1204
+ return null;
1205
+ entries.push(`${goField}: ${go}`);
1206
+ }
1207
+ return `${goType}{${entries.join(", ")}}`;
1208
+ }
1209
+ return null;
1210
+ }
1026
1211
  typeInfoToGo(typeInfo, defaultValue) {
1027
1212
  switch (typeInfo.kind) {
1028
1213
  case "primitive":
@@ -1335,6 +1520,7 @@ ${goFields.join(`
1335
1520
  }
1336
1521
  return null;
1337
1522
  }
1523
+ static GO_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1338
1524
  static GO_INITIALISMS = new Set([
1339
1525
  "id",
1340
1526
  "url",
@@ -1541,9 +1727,23 @@ ${goFields.join(`
1541
1727
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
1542
1728
  if (currentLoopParam && name === currentLoopParam)
1543
1729
  return ".";
1730
+ if (this.isOuterLoopParam(name))
1731
+ return `$${name}`;
1544
1732
  if (this.loopVarRefCount.has(name))
1545
1733
  return `$${name}`;
1546
- return `.${this.capitalizeFieldName(name)}`;
1734
+ return this.rootFieldRef(name);
1735
+ }
1736
+ isOuterLoopParam(name) {
1737
+ const top = this.loopParamStack.length - 1;
1738
+ for (let i = 0;i < top; i++) {
1739
+ if (this.loopParamStack[i] === name)
1740
+ return true;
1741
+ }
1742
+ return false;
1743
+ }
1744
+ rootFieldRef(name) {
1745
+ const prefix = this.loopParamStack.length > 0 ? "$." : ".";
1746
+ return `${prefix}${this.capitalizeFieldName(name)}`;
1547
1747
  }
1548
1748
  literal(value, literalType) {
1549
1749
  if (literalType === "string")
@@ -1554,7 +1754,7 @@ ${goFields.join(`
1554
1754
  }
1555
1755
  call(callee, args, emit) {
1556
1756
  if (callee.kind === "identifier" && args.length === 0) {
1557
- return `.${this.capitalizeFieldName(callee.name)}`;
1757
+ return this.rootFieldRef(callee.name);
1558
1758
  }
1559
1759
  const path = identifierPath(callee);
1560
1760
  if (path && this.templatePrimitives[path]) {
@@ -1595,7 +1795,7 @@ ${goFields.join(`
1595
1795
  return templateBlock;
1596
1796
  }
1597
1797
  if (object.kind === "identifier" && this.propsObjectName && object.name === this.propsObjectName) {
1598
- return `.${this.capitalizeFieldName(property)}`;
1798
+ return this.rootFieldRef(property);
1599
1799
  }
1600
1800
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
1601
1801
  if (object.kind === "identifier" && currentLoopParam && object.name === currentLoopParam) {
@@ -2332,11 +2532,14 @@ Options:
2332
2532
  if (currentLoopParam && expr.name === currentLoopParam) {
2333
2533
  return plain(".");
2334
2534
  }
2535
+ if (this.isOuterLoopParam(expr.name)) {
2536
+ return plain(`$${expr.name}`);
2537
+ }
2335
2538
  if (this.loopVarRefCount.has(expr.name)) {
2336
2539
  return plain(`$${expr.name}`);
2337
2540
  }
2338
2541
  }
2339
- return plain(`.${this.capitalizeFieldName(expr.name)}`);
2542
+ return plain(this.rootFieldRef(expr.name));
2340
2543
  case "literal":
2341
2544
  if (expr.literalType === "string")
2342
2545
  return plain(`"${expr.value}"`);
@@ -2345,7 +2548,7 @@ Options:
2345
2548
  return plain(String(expr.value));
2346
2549
  case "call": {
2347
2550
  if (expr.callee.kind === "identifier" && expr.args.length === 0) {
2348
- return plain(`.${this.capitalizeFieldName(expr.callee.name)}`);
2551
+ return plain(this.rootFieldRef(expr.callee.name));
2349
2552
  }
2350
2553
  return plain(this.renderParsedExpr(expr));
2351
2554
  }
@@ -2356,7 +2559,7 @@ Options:
2356
2559
  return plain(result);
2357
2560
  }
2358
2561
  if (expr.object.kind === "identifier" && this.propsObjectName && expr.object.name === this.propsObjectName) {
2359
- return plain(`.${this.capitalizeFieldName(expr.property)}`);
2562
+ return plain(this.rootFieldRef(expr.property));
2360
2563
  }
2361
2564
  {
2362
2565
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
@@ -2502,6 +2705,7 @@ Options:
2502
2705
  }
2503
2706
  }
2504
2707
  const children = this.renderChildren(loop.children);
2708
+ const itemMarker = this.loopItemMarker(loop);
2505
2709
  for (const v of addedLoopVars) {
2506
2710
  const rc = (this.loopVarRefCount.get(v) ?? 1) - 1;
2507
2711
  if (rc <= 0)
@@ -2523,12 +2727,18 @@ Options:
2523
2727
  } else {
2524
2728
  filterCond = "true";
2525
2729
  }
2526
- const itemMarker2 = loop.bodyIsMultiRoot ? `{{bfComment "bf-loop-i"}}` : "";
2527
- return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}{{if ${filterCond}}}${itemMarker2}${children}{{end}}{{end}}{{bfComment "/loop:${loop.markerId}"}}`;
2730
+ return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}{{if ${filterCond}}}${itemMarker}${children}{{end}}{{end}}{{bfComment "/loop:${loop.markerId}"}}`;
2528
2731
  }
2529
- const itemMarker = loop.bodyIsMultiRoot ? `{{bfComment "bf-loop-i"}}` : "";
2530
2732
  return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`;
2531
2733
  }
2734
+ loopItemMarker(loop) {
2735
+ if (loop.bodyIsMultiRoot)
2736
+ return `{{bfComment "bf-loop-i"}}`;
2737
+ if (loop.bodyIsItemConditional && loop.key) {
2738
+ return `{{bfComment (printf "loop-i:%v" ${this.convertExpressionToGo(loop.key)})}}`;
2739
+ }
2740
+ return "";
2741
+ }
2532
2742
  findChildComponent(nodes) {
2533
2743
  for (const node of nodes) {
2534
2744
  if (node.type === "component") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/go-template",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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.5.0"
57
+ "@barefootjs/jsx": "0.5.2"
58
58
  }
59
59
  }