@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/adapter/go-template-adapter.d.ts +122 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +191 -3
- package/dist/build.js +191 -3
- package/dist/index.js +191 -3
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +124 -10
- package/src/__tests__/slot-dynamic-tag.test.ts +79 -0
- package/src/adapter/go-template-adapter.ts +380 -3
- package/src/test-render.ts +33 -1
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
57
|
+
"@barefootjs/jsx": "0.8.0"
|
|
58
58
|
}
|
|
59
59
|
}
|