@barefootjs/go-template 0.7.0 → 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 +80 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +117 -2
- package/dist/build.js +117 -2
- package/dist/index.js +117 -2
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +94 -3
- package/src/adapter/go-template-adapter.ts +221 -2
|
@@ -123,6 +123,20 @@ export declare class GoTemplateAdapter extends BaseAdapter implements ParsedExpr
|
|
|
123
123
|
* indexed lookups / memos / signals are deliberately excluded.
|
|
124
124
|
*/
|
|
125
125
|
private moduleStringConsts;
|
|
126
|
+
/**
|
|
127
|
+
* Set of prop NAMES whose resolved Go struct-field type is exactly
|
|
128
|
+
* `interface{}` — i.e. nillable. Populated at `generate()` entry from
|
|
129
|
+
* the SAME per-prop Go-type computation `generatePropsStruct` /
|
|
130
|
+
* `generateInputStruct` use (`propTypeOverrides` + `typeInfoToGo`), so
|
|
131
|
+
* it can't drift from the actual field types. Used by
|
|
132
|
+
* `elementAttrEmitter.emitExpression` to omit a dynamic attribute whose
|
|
133
|
+
* value is a bare reference to a nillable prop when that prop is nil
|
|
134
|
+
* (Hono-style nullish-attribute omission: an `undefined`-valued
|
|
135
|
+
* attribute is dropped rather than rendered as `attr=""`). Concrete
|
|
136
|
+
* (`string`/`int`/`bool`) fields are never in this set and always emit
|
|
137
|
+
* unconditionally, matching Hono's `value=""` / `data-count="0"`.
|
|
138
|
+
*/
|
|
139
|
+
private nillablePropNames;
|
|
126
140
|
constructor(options?: GoTemplateAdapterOptions);
|
|
127
141
|
/**
|
|
128
142
|
* Generate template output for a component.
|
|
@@ -233,6 +247,24 @@ export declare class GoTemplateAdapter extends BaseAdapter implements ParsedExpr
|
|
|
233
247
|
* the signal's type annotation may be more specific than the prop's TypeInfo.
|
|
234
248
|
*/
|
|
235
249
|
private buildPropTypeOverrides;
|
|
250
|
+
/**
|
|
251
|
+
* Resolve a prop param's Go struct-field type using the SAME logic
|
|
252
|
+
* `generatePropsStruct` / `generateInputStruct` use for the field
|
|
253
|
+
* declaration: a `propTypeOverrides` entry (signal-inferred override)
|
|
254
|
+
* wins, otherwise `typeInfoToGo(param.type, param.defaultValue)`.
|
|
255
|
+
* Factored out so the nillable-field set (`collectNillablePropNames`)
|
|
256
|
+
* can't drift from the actual emitted field types.
|
|
257
|
+
*/
|
|
258
|
+
private resolvePropGoType;
|
|
259
|
+
/**
|
|
260
|
+
* Build the set of prop NAMES whose resolved Go field type is exactly
|
|
261
|
+
* `interface{}` (nillable). Uses the same `propTypeOverrides` +
|
|
262
|
+
* `resolvePropGoType` pipeline as the struct generators, so a prop
|
|
263
|
+
* that ends up `interface{}` on the Props struct — and only such a
|
|
264
|
+
* prop — is treated as nillable for Hono-style attribute omission.
|
|
265
|
+
* Concrete (`string`/`int`/`bool`/`[]T`/struct) types are excluded.
|
|
266
|
+
*/
|
|
267
|
+
private collectNillablePropNames;
|
|
236
268
|
/**
|
|
237
269
|
* Generate Input struct for a component
|
|
238
270
|
*/
|
|
@@ -354,6 +386,54 @@ export declare class GoTemplateAdapter extends BaseAdapter implements ParsedExpr
|
|
|
354
386
|
* narrowed BF101 with the offending expression.
|
|
355
387
|
*/
|
|
356
388
|
private buildSpreadInitializer;
|
|
389
|
+
/**
|
|
390
|
+
* Lower a conditional inline-object spread bag value:
|
|
391
|
+
* `(COND ? { 'aria-describedby': describedBy } : {})`
|
|
392
|
+
* into an immediately-invoked Go func literal that conditionally
|
|
393
|
+
* builds the map (so the falsy branch OMITS the key rather than
|
|
394
|
+
* rendering it as an empty string, which `SpreadAttrs` does not
|
|
395
|
+
* filter):
|
|
396
|
+
*
|
|
397
|
+
* func() map[string]any {
|
|
398
|
+
* if in.DescribedBy != nil && in.DescribedBy != "" {
|
|
399
|
+
* return map[string]any{"aria-describedby": in.DescribedBy}
|
|
400
|
+
* }
|
|
401
|
+
* return map[string]any{}
|
|
402
|
+
* }()
|
|
403
|
+
*
|
|
404
|
+
* Returns:
|
|
405
|
+
* - `undefined` when the expression is NOT a parenthesized ternary
|
|
406
|
+
* of object literals — the caller falls through to other shapes.
|
|
407
|
+
* - `null` when it IS that shape but a part can't be faithfully
|
|
408
|
+
* converted (non-static key, unsupported condition, …) — the
|
|
409
|
+
* caller raises BF101.
|
|
410
|
+
* - the Go IIFE string when fully convertible.
|
|
411
|
+
*/
|
|
412
|
+
private buildConditionalSpreadInitializer;
|
|
413
|
+
/** Strip redundant parenthesised wrappers off a TS expression. */
|
|
414
|
+
private unwrapParens;
|
|
415
|
+
/**
|
|
416
|
+
* Convert a conditional-spread condition expression to a Go bool in
|
|
417
|
+
* the `in.` context. Supports a bare prop identifier (`describedBy`)
|
|
418
|
+
* and its negation (`!describedBy`), type-aware on the prop:
|
|
419
|
+
* string → `in.X != ""`
|
|
420
|
+
* boolean → `in.X`
|
|
421
|
+
* number → `in.X != 0`
|
|
422
|
+
* unknown / interface{} → `in.X != nil && in.X != ""`
|
|
423
|
+
* (faithful JS string-truthiness for an interface holding a
|
|
424
|
+
* string — textarea's `describedBy` resolves to interface{}).
|
|
425
|
+
* Returns null for any other shape (caller → BF101).
|
|
426
|
+
*/
|
|
427
|
+
private conditionToGoBool;
|
|
428
|
+
/**
|
|
429
|
+
* Convert a static object literal (`{ 'aria-describedby': describedBy }`)
|
|
430
|
+
* into a Go `map[string]any{...}` literal for a conditional spread.
|
|
431
|
+
* Only static string/identifier keys are allowed; values resolve
|
|
432
|
+
* prop-identifier references to `in.FieldName` and string literals to
|
|
433
|
+
* Go string literals. Returns null for any computed/spread/dynamic
|
|
434
|
+
* key or unsupported value (caller → BF101). Empty object → `map[string]any{}`.
|
|
435
|
+
*/
|
|
436
|
+
private objectLiteralToGoSpreadMap;
|
|
357
437
|
/**
|
|
358
438
|
* Convert JavaScript initial value to Go value for NewXxxProps function.
|
|
359
439
|
* References to props params are converted to in.FieldName format.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"go-template-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/go-template-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,WAAW,EACX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EAEN,WAAW,EACX,UAAU,EACV,MAAM,EAON,UAAU,EAEV,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,YAAY,EACZ,aAAa,EACb,UAAU,EACV,OAAO,EAEP,yBAAyB,EAC1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAE3B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,UAAU,EAWhB,MAAM,iBAAiB,CAAA;AAGxB;;;;;GAKG;AACH,KAAK,WAAW,GAAG;IACjB,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC,CAAA;AA2ED,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAEzB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AA+JD,qBAAa,iBAAkB,SAAQ,WAAY,YAAW,iBAAiB,EAAE,aAAa,CAAC,WAAW,CAAC;IACzG,IAAI,SAAgB;IACpB,SAAS,SAAU;IAGnB,kBAAkB,EAAG,cAAc,CAAS;IAQ5C,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,qBAAqB,CAAQ;IAErC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,kBAAkB,EAAE,yBAAyB,CAG1C;IAEH;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAGtC;IAEH,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,eAAe,CAAiC;IACxD,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,eAAe,CAAsB;IAC7C;;;;;;OAMG;IACH,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,kBAAkB,CAAY;IACtC,sFAAsF;IACtF,OAAO,CAAC,cAAc,CAAyB;IAC/C,kFAAkF;IAClF,OAAO,CAAC,gBAAgB,CAAiC;IACzD;;;;OAIG;IACH,OAAO,CAAC,iBAAiB,CAA8C;IACvE;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB,CAAmC;IAE3D;sEACkE;IAClE,OAAO,CAAC,gBAAgB,CAAiB;IAEzC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,kBAAkB,CAAiC;IAE3D,YAAY,OAAO,GAAE,wBAA6B,EAOjD;IAED;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"go-template-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/go-template-adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter/index.js
CHANGED
|
@@ -118,6 +118,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
118
118
|
synthStructTypes = new Map;
|
|
119
119
|
usesHtmlTemplate = false;
|
|
120
120
|
moduleStringConsts = new Map;
|
|
121
|
+
nillablePropNames = new Set;
|
|
121
122
|
constructor(options = {}) {
|
|
122
123
|
super();
|
|
123
124
|
this.options = {
|
|
@@ -133,6 +134,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
133
134
|
this.propsObjectName = ir.metadata.propsObjectName;
|
|
134
135
|
this.restPropsName = ir.metadata.restPropsName ?? null;
|
|
135
136
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
137
|
+
this.nillablePropNames = this.collectNillablePropNames(ir);
|
|
136
138
|
if (!options?.siblingTemplatesRegistered) {
|
|
137
139
|
this.checkImportedLoopChildComponents(ir);
|
|
138
140
|
}
|
|
@@ -568,6 +570,19 @@ ${goFields.join(`
|
|
|
568
570
|
}
|
|
569
571
|
return overrides;
|
|
570
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
|
+
}
|
|
571
586
|
generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
|
|
572
587
|
const inputTypeName = `${componentName}Input`;
|
|
573
588
|
lines.push(`// ${inputTypeName} is the user-facing input type.`);
|
|
@@ -581,7 +596,7 @@ ${goFields.join(`
|
|
|
581
596
|
const fieldName = this.capitalizeFieldName(param.name);
|
|
582
597
|
if (nestedArrayFields.has(fieldName))
|
|
583
598
|
continue;
|
|
584
|
-
const goType =
|
|
599
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides);
|
|
585
600
|
lines.push(` ${fieldName} ${goType}`);
|
|
586
601
|
}
|
|
587
602
|
for (const nested of inputNested) {
|
|
@@ -620,7 +635,7 @@ ${goFields.join(`
|
|
|
620
635
|
const fieldName = this.capitalizeFieldName(param.name);
|
|
621
636
|
if (nestedArrayFields.has(fieldName))
|
|
622
637
|
continue;
|
|
623
|
-
const goType =
|
|
638
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides);
|
|
624
639
|
const jsonTag = this.toJsonTag(param.name);
|
|
625
640
|
lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
|
|
626
641
|
propFieldNames.add(fieldName);
|
|
@@ -1094,6 +1109,9 @@ ${goFields.join(`
|
|
|
1094
1109
|
}
|
|
1095
1110
|
buildSpreadInitializer(spreadExpr, ir) {
|
|
1096
1111
|
const trimmed = spreadExpr.trim();
|
|
1112
|
+
const conditional = this.buildConditionalSpreadInitializer(trimmed, ir);
|
|
1113
|
+
if (conditional !== undefined)
|
|
1114
|
+
return conditional;
|
|
1097
1115
|
const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed);
|
|
1098
1116
|
if (callMatch) {
|
|
1099
1117
|
const getterName = callMatch[1];
|
|
@@ -1120,6 +1138,98 @@ ${goFields.join(`
|
|
|
1120
1138
|
}
|
|
1121
1139
|
return null;
|
|
1122
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
|
+
}
|
|
1123
1233
|
convertInitialValue(value, typeInfo, propsParams) {
|
|
1124
1234
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
|
|
1125
1235
|
if (propsParams?.some((p) => p.name === value)) {
|
|
@@ -2968,6 +3078,11 @@ ${children}`;
|
|
|
2968
3078
|
if (parsed.kind === "conditional" || parsed.kind === "template-literal") {
|
|
2969
3079
|
return `${name}="${this.renderParsedExpr(parsed)}"`;
|
|
2970
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
|
+
}
|
|
2971
3086
|
return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`;
|
|
2972
3087
|
},
|
|
2973
3088
|
emitBooleanAttr: (_value, name) => name,
|
package/dist/build.js
CHANGED
|
@@ -458,6 +458,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
458
458
|
synthStructTypes = new Map;
|
|
459
459
|
usesHtmlTemplate = false;
|
|
460
460
|
moduleStringConsts = new Map;
|
|
461
|
+
nillablePropNames = new Set;
|
|
461
462
|
constructor(options = {}) {
|
|
462
463
|
super();
|
|
463
464
|
this.options = {
|
|
@@ -473,6 +474,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
473
474
|
this.propsObjectName = ir.metadata.propsObjectName;
|
|
474
475
|
this.restPropsName = ir.metadata.restPropsName ?? null;
|
|
475
476
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
477
|
+
this.nillablePropNames = this.collectNillablePropNames(ir);
|
|
476
478
|
if (!options?.siblingTemplatesRegistered) {
|
|
477
479
|
this.checkImportedLoopChildComponents(ir);
|
|
478
480
|
}
|
|
@@ -908,6 +910,19 @@ ${goFields.join(`
|
|
|
908
910
|
}
|
|
909
911
|
return overrides;
|
|
910
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
|
+
}
|
|
911
926
|
generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
|
|
912
927
|
const inputTypeName = `${componentName}Input`;
|
|
913
928
|
lines.push(`// ${inputTypeName} is the user-facing input type.`);
|
|
@@ -921,7 +936,7 @@ ${goFields.join(`
|
|
|
921
936
|
const fieldName = this.capitalizeFieldName(param.name);
|
|
922
937
|
if (nestedArrayFields.has(fieldName))
|
|
923
938
|
continue;
|
|
924
|
-
const goType =
|
|
939
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides);
|
|
925
940
|
lines.push(` ${fieldName} ${goType}`);
|
|
926
941
|
}
|
|
927
942
|
for (const nested of inputNested) {
|
|
@@ -960,7 +975,7 @@ ${goFields.join(`
|
|
|
960
975
|
const fieldName = this.capitalizeFieldName(param.name);
|
|
961
976
|
if (nestedArrayFields.has(fieldName))
|
|
962
977
|
continue;
|
|
963
|
-
const goType =
|
|
978
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides);
|
|
964
979
|
const jsonTag = this.toJsonTag(param.name);
|
|
965
980
|
lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
|
|
966
981
|
propFieldNames.add(fieldName);
|
|
@@ -1434,6 +1449,9 @@ ${goFields.join(`
|
|
|
1434
1449
|
}
|
|
1435
1450
|
buildSpreadInitializer(spreadExpr, ir) {
|
|
1436
1451
|
const trimmed = spreadExpr.trim();
|
|
1452
|
+
const conditional = this.buildConditionalSpreadInitializer(trimmed, ir);
|
|
1453
|
+
if (conditional !== undefined)
|
|
1454
|
+
return conditional;
|
|
1437
1455
|
const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed);
|
|
1438
1456
|
if (callMatch) {
|
|
1439
1457
|
const getterName = callMatch[1];
|
|
@@ -1460,6 +1478,98 @@ ${goFields.join(`
|
|
|
1460
1478
|
}
|
|
1461
1479
|
return null;
|
|
1462
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
|
+
}
|
|
1463
1573
|
convertInitialValue(value, typeInfo, propsParams) {
|
|
1464
1574
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
|
|
1465
1575
|
if (propsParams?.some((p) => p.name === value)) {
|
|
@@ -3308,6 +3418,11 @@ ${children}`;
|
|
|
3308
3418
|
if (parsed.kind === "conditional" || parsed.kind === "template-literal") {
|
|
3309
3419
|
return `${name}="${this.renderParsedExpr(parsed)}"`;
|
|
3310
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
|
+
}
|
|
3311
3426
|
return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`;
|
|
3312
3427
|
},
|
|
3313
3428
|
emitBooleanAttr: (_value, name) => name,
|
package/dist/index.js
CHANGED
|
@@ -118,6 +118,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
118
118
|
synthStructTypes = new Map;
|
|
119
119
|
usesHtmlTemplate = false;
|
|
120
120
|
moduleStringConsts = new Map;
|
|
121
|
+
nillablePropNames = new Set;
|
|
121
122
|
constructor(options = {}) {
|
|
122
123
|
super();
|
|
123
124
|
this.options = {
|
|
@@ -133,6 +134,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
133
134
|
this.propsObjectName = ir.metadata.propsObjectName;
|
|
134
135
|
this.restPropsName = ir.metadata.restPropsName ?? null;
|
|
135
136
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
137
|
+
this.nillablePropNames = this.collectNillablePropNames(ir);
|
|
136
138
|
if (!options?.siblingTemplatesRegistered) {
|
|
137
139
|
this.checkImportedLoopChildComponents(ir);
|
|
138
140
|
}
|
|
@@ -568,6 +570,19 @@ ${goFields.join(`
|
|
|
568
570
|
}
|
|
569
571
|
return overrides;
|
|
570
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
|
+
}
|
|
571
586
|
generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
|
|
572
587
|
const inputTypeName = `${componentName}Input`;
|
|
573
588
|
lines.push(`// ${inputTypeName} is the user-facing input type.`);
|
|
@@ -581,7 +596,7 @@ ${goFields.join(`
|
|
|
581
596
|
const fieldName = this.capitalizeFieldName(param.name);
|
|
582
597
|
if (nestedArrayFields.has(fieldName))
|
|
583
598
|
continue;
|
|
584
|
-
const goType =
|
|
599
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides);
|
|
585
600
|
lines.push(` ${fieldName} ${goType}`);
|
|
586
601
|
}
|
|
587
602
|
for (const nested of inputNested) {
|
|
@@ -620,7 +635,7 @@ ${goFields.join(`
|
|
|
620
635
|
const fieldName = this.capitalizeFieldName(param.name);
|
|
621
636
|
if (nestedArrayFields.has(fieldName))
|
|
622
637
|
continue;
|
|
623
|
-
const goType =
|
|
638
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides);
|
|
624
639
|
const jsonTag = this.toJsonTag(param.name);
|
|
625
640
|
lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
|
|
626
641
|
propFieldNames.add(fieldName);
|
|
@@ -1094,6 +1109,9 @@ ${goFields.join(`
|
|
|
1094
1109
|
}
|
|
1095
1110
|
buildSpreadInitializer(spreadExpr, ir) {
|
|
1096
1111
|
const trimmed = spreadExpr.trim();
|
|
1112
|
+
const conditional = this.buildConditionalSpreadInitializer(trimmed, ir);
|
|
1113
|
+
if (conditional !== undefined)
|
|
1114
|
+
return conditional;
|
|
1097
1115
|
const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed);
|
|
1098
1116
|
if (callMatch) {
|
|
1099
1117
|
const getterName = callMatch[1];
|
|
@@ -1120,6 +1138,98 @@ ${goFields.join(`
|
|
|
1120
1138
|
}
|
|
1121
1139
|
return null;
|
|
1122
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
|
+
}
|
|
1123
1233
|
convertInitialValue(value, typeInfo, propsParams) {
|
|
1124
1234
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
|
|
1125
1235
|
if (propsParams?.some((p) => p.name === value)) {
|
|
@@ -2968,6 +3078,11 @@ ${children}`;
|
|
|
2968
3078
|
if (parsed.kind === "conditional" || parsed.kind === "template-literal") {
|
|
2969
3079
|
return `${name}="${this.renderParsedExpr(parsed)}"`;
|
|
2970
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
|
+
}
|
|
2971
3086
|
return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`;
|
|
2972
3087
|
},
|
|
2973
3088
|
emitBooleanAttr: (_value, name) => name,
|
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
|
}
|
|
@@ -80,12 +80,19 @@ runAdapterConformanceTests({
|
|
|
80
80
|
// parity for the `site/ui` corpus is Phase 3, so these participate only
|
|
81
81
|
// in Hono SSR conformance + the fixture-hydrate runtime layer for now.
|
|
82
82
|
// (`label` and `kbd` are static helpers; `toggle` / `switch` /
|
|
83
|
-
// `checkbox` carry uncontrolled state; `input`
|
|
84
|
-
//
|
|
83
|
+
// `checkbox` carry uncontrolled state; `input` is a pass-through
|
|
84
|
+
// native control.)
|
|
85
|
+
//
|
|
86
|
+
// `textarea` now participates: its conditional inline-object spread
|
|
87
|
+
// (`{...(describedBy ? {...} : {})}`) lowers via
|
|
88
|
+
// `buildConditionalSpreadInitializer`, and its optional
|
|
89
|
+
// `rows={rows}` attribute is omitted when nil via the nillable-field
|
|
90
|
+
// guard in `elementAttrEmitter.emitExpression`
|
|
91
|
+
// (`{{if ne .Rows nil}}rows="{{.Rows}}"{{end}}`), matching Hono's
|
|
92
|
+
// nullish-attribute omission.
|
|
85
93
|
'toggle',
|
|
86
94
|
'switch',
|
|
87
95
|
'checkbox',
|
|
88
|
-
'textarea',
|
|
89
96
|
'kbd',
|
|
90
97
|
],
|
|
91
98
|
// Per-fixture build-time contracts for shapes the Go template
|
|
@@ -818,6 +825,90 @@ export function List() {
|
|
|
818
825
|
})
|
|
819
826
|
})
|
|
820
827
|
|
|
828
|
+
describe('conditional inline-object spread (textarea aria-describedby)', () => {
|
|
829
|
+
// `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to an
|
|
830
|
+
// IIFE-of-maps in `NewXxxProps` so the falsy branch OMITS the key
|
|
831
|
+
// (SpreadAttrs does not filter empty strings). The fixture only
|
|
832
|
+
// exercises the falsy branch; this pins the TRUTHY branch.
|
|
833
|
+
test('lowers to a conditional map IIFE and keeps the {{bf_spread_attrs}} template', () => {
|
|
834
|
+
const source = `
|
|
835
|
+
function Box({ describedBy }: { describedBy?: string }) {
|
|
836
|
+
return <div {...(describedBy ? { 'aria-describedby': describedBy } : {})} />
|
|
837
|
+
}
|
|
838
|
+
`
|
|
839
|
+
const { template, types } = compileAndGenerate(source)
|
|
840
|
+
// Template emission is unchanged from the proven {...props} path.
|
|
841
|
+
expect(template).toContain('{{bf_spread_attrs .Spread_0}}')
|
|
842
|
+
// The bag value is a conditional map built in NewBoxProps. The
|
|
843
|
+
// prop type is unresolved (interface{}), so the condition routes
|
|
844
|
+
// through `bf.Truthy` for a faithful JS `Boolean(x)` test.
|
|
845
|
+
expect(types).toContain('Spread_0: func() map[string]any {')
|
|
846
|
+
expect(types).toContain('if bf.Truthy(in.DescribedBy) {')
|
|
847
|
+
expect(types).toContain('return map[string]any{"aria-describedby": in.DescribedBy}')
|
|
848
|
+
expect(types).toContain('return map[string]any{}')
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
test('resolves the object value reference and key for a second prop', () => {
|
|
852
|
+
const source = `
|
|
853
|
+
function Box({ label }: { label: string }) {
|
|
854
|
+
return <div {...(label ? { 'data-label': label } : {})} />
|
|
855
|
+
}
|
|
856
|
+
`
|
|
857
|
+
const { types } = compileAndGenerate(source)
|
|
858
|
+
// The condition prop and the value reference resolve to `in.<Field>`,
|
|
859
|
+
// the static key is preserved. (The analyzer surfaces these
|
|
860
|
+
// destructured props as `unknown`/`interface{}`, so the condition
|
|
861
|
+
// routes through `bf.Truthy` for a faithful JS truthiness test.)
|
|
862
|
+
expect(types).toContain('if bf.Truthy(in.Label) {')
|
|
863
|
+
expect(types).toContain('return map[string]any{"data-label": in.Label}')
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
test('refuses a non-identifier condition with BF101 (out-of-shape fallback)', () => {
|
|
867
|
+
const adapter = new GoTemplateAdapter()
|
|
868
|
+
const source = `
|
|
869
|
+
function Box({ a, b }: { a?: string; b?: string }) {
|
|
870
|
+
return <div {...(a === b ? { 'data-x': a } : {})} />
|
|
871
|
+
}
|
|
872
|
+
`
|
|
873
|
+
const ir = compileToIR(source, adapter)
|
|
874
|
+
adapter.generate(ir)
|
|
875
|
+
const errs = (adapter as unknown as { errors: { code: string }[] }).errors
|
|
876
|
+
expect(errs.some(e => e.code === 'BF101')).toBe(true)
|
|
877
|
+
})
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
describe('nullish optional-attribute omission (textarea rows)', () => {
|
|
881
|
+
// An optional, no-default prop whose Go field type resolves to
|
|
882
|
+
// `interface{}` (nillable) is emitted with a `ne .X nil` guard so an
|
|
883
|
+
// unset value DROPS the attribute instead of rendering `attr=""` —
|
|
884
|
+
// matching Hono's nullish-attribute omission. Concrete/defaulted
|
|
885
|
+
// props are never nil and stay unconditional.
|
|
886
|
+
test('guards a nillable optional attr with {{if ne .X nil}}', () => {
|
|
887
|
+
const source = `
|
|
888
|
+
function C({ rows }: { rows?: number }) {
|
|
889
|
+
return <textarea rows={rows} />
|
|
890
|
+
}
|
|
891
|
+
`
|
|
892
|
+
const { template } = compileAndGenerate(source)
|
|
893
|
+
expect(template).toContain('{{if ne .Rows nil}}rows="{{.Rows}}"{{end}}')
|
|
894
|
+
// Must NOT emit the bare unconditional form.
|
|
895
|
+
expect(template).not.toMatch(/(?<!if ne \.Rows nil}})rows="\{\{\.Rows\}\}"/)
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
test('leaves a concrete/defaulted attr unconditional (scope did not widen)', () => {
|
|
899
|
+
const source = `
|
|
900
|
+
function C({ value = '' }: { value?: string }) {
|
|
901
|
+
return <textarea value={value} />
|
|
902
|
+
}
|
|
903
|
+
`
|
|
904
|
+
const { template } = compileAndGenerate(source)
|
|
905
|
+
// `value` has a destructure default → concrete `string` field →
|
|
906
|
+
// never nil → emitted unconditionally, exactly like Hono's value="".
|
|
907
|
+
expect(template).toContain('value="{{.Value}}"')
|
|
908
|
+
expect(template).not.toContain('if ne .Value nil')
|
|
909
|
+
})
|
|
910
|
+
})
|
|
911
|
+
|
|
821
912
|
describe('loop body outer-scope references (#1677)', () => {
|
|
822
913
|
test('references an outer signal inside a loop via $ root scope, not the element', () => {
|
|
823
914
|
// Inside `{{range $_, $t := .Items}}` the dot is rebound to the loop
|
|
@@ -434,6 +434,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
434
434
|
*/
|
|
435
435
|
private moduleStringConsts: Map<string, string> = new Map()
|
|
436
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Set of prop NAMES whose resolved Go struct-field type is exactly
|
|
439
|
+
* `interface{}` — i.e. nillable. Populated at `generate()` entry from
|
|
440
|
+
* the SAME per-prop Go-type computation `generatePropsStruct` /
|
|
441
|
+
* `generateInputStruct` use (`propTypeOverrides` + `typeInfoToGo`), so
|
|
442
|
+
* it can't drift from the actual field types. Used by
|
|
443
|
+
* `elementAttrEmitter.emitExpression` to omit a dynamic attribute whose
|
|
444
|
+
* value is a bare reference to a nillable prop when that prop is nil
|
|
445
|
+
* (Hono-style nullish-attribute omission: an `undefined`-valued
|
|
446
|
+
* attribute is dropped rather than rendered as `attr=""`). Concrete
|
|
447
|
+
* (`string`/`int`/`bool`) fields are never in this set and always emit
|
|
448
|
+
* unconditionally, matching Hono's `value=""` / `data-count="0"`.
|
|
449
|
+
*/
|
|
450
|
+
private nillablePropNames: Set<string> = new Set()
|
|
451
|
+
|
|
437
452
|
constructor(options: GoTemplateAdapterOptions = {}) {
|
|
438
453
|
super()
|
|
439
454
|
this.options = {
|
|
@@ -455,6 +470,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
455
470
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
456
471
|
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
457
472
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
473
|
+
this.nillablePropNames = this.collectNillablePropNames(ir)
|
|
458
474
|
|
|
459
475
|
// Surface loop-body usages of components imported from sibling
|
|
460
476
|
// .tsx files. The adapter emits `{{template "X" .}}` for these,
|
|
@@ -1065,6 +1081,40 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1065
1081
|
return overrides
|
|
1066
1082
|
}
|
|
1067
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* Resolve a prop param's Go struct-field type using the SAME logic
|
|
1086
|
+
* `generatePropsStruct` / `generateInputStruct` use for the field
|
|
1087
|
+
* declaration: a `propTypeOverrides` entry (signal-inferred override)
|
|
1088
|
+
* wins, otherwise `typeInfoToGo(param.type, param.defaultValue)`.
|
|
1089
|
+
* Factored out so the nillable-field set (`collectNillablePropNames`)
|
|
1090
|
+
* can't drift from the actual emitted field types.
|
|
1091
|
+
*/
|
|
1092
|
+
private resolvePropGoType(
|
|
1093
|
+
param: IRMetadata['propsParams'][number],
|
|
1094
|
+
propTypeOverrides: Map<string, string>,
|
|
1095
|
+
): string {
|
|
1096
|
+
return propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Build the set of prop NAMES whose resolved Go field type is exactly
|
|
1101
|
+
* `interface{}` (nillable). Uses the same `propTypeOverrides` +
|
|
1102
|
+
* `resolvePropGoType` pipeline as the struct generators, so a prop
|
|
1103
|
+
* that ends up `interface{}` on the Props struct — and only such a
|
|
1104
|
+
* prop — is treated as nillable for Hono-style attribute omission.
|
|
1105
|
+
* Concrete (`string`/`int`/`bool`/`[]T`/struct) types are excluded.
|
|
1106
|
+
*/
|
|
1107
|
+
private collectNillablePropNames(ir: ComponentIR): Set<string> {
|
|
1108
|
+
const propTypeOverrides = this.buildPropTypeOverrides(ir)
|
|
1109
|
+
const nillable = new Set<string>()
|
|
1110
|
+
for (const param of ir.metadata.propsParams) {
|
|
1111
|
+
if (this.resolvePropGoType(param, propTypeOverrides) === 'interface{}') {
|
|
1112
|
+
nillable.add(param.name)
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return nillable
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1068
1118
|
/**
|
|
1069
1119
|
* Generate Input struct for a component
|
|
1070
1120
|
*/
|
|
@@ -1096,7 +1146,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1096
1146
|
for (const param of ir.metadata.propsParams) {
|
|
1097
1147
|
const fieldName = this.capitalizeFieldName(param.name)
|
|
1098
1148
|
if (nestedArrayFields.has(fieldName)) continue
|
|
1099
|
-
const goType =
|
|
1149
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides)
|
|
1100
1150
|
lines.push(`\t${fieldName} ${goType}`)
|
|
1101
1151
|
}
|
|
1102
1152
|
|
|
@@ -1169,7 +1219,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1169
1219
|
const fieldName = this.capitalizeFieldName(param.name)
|
|
1170
1220
|
// Skip if this field will be replaced by a typed array for nested components
|
|
1171
1221
|
if (nestedArrayFields.has(fieldName)) continue
|
|
1172
|
-
const goType =
|
|
1222
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides)
|
|
1173
1223
|
const jsonTag = this.toJsonTag(param.name)
|
|
1174
1224
|
lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
|
|
1175
1225
|
propFieldNames.add(fieldName)
|
|
@@ -1922,6 +1972,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1922
1972
|
ir: ComponentIR,
|
|
1923
1973
|
): string | null {
|
|
1924
1974
|
const trimmed = spreadExpr.trim()
|
|
1975
|
+
// Conditional inline-object spread:
|
|
1976
|
+
// `{...(COND ? { 'k': v } : {})}` (either branch possibly `{}`).
|
|
1977
|
+
// Lower to an immediately-invoked func literal that conditionally
|
|
1978
|
+
// builds the bag, so the falsy branch yields an empty map (the key
|
|
1979
|
+
// is OMITTED rather than rendered as `k=""` — `SpreadAttrs` does
|
|
1980
|
+
// NOT filter empty strings). Returns null for any shape it can't
|
|
1981
|
+
// faithfully convert so the caller falls back to BF101 (#textarea).
|
|
1982
|
+
const conditional = this.buildConditionalSpreadInitializer(trimmed, ir)
|
|
1983
|
+
if (conditional !== undefined) return conditional
|
|
1925
1984
|
// Signal-getter call: `attrs()` — pluck the signal's initialValue
|
|
1926
1985
|
// and translate the JS object literal to a Go map literal.
|
|
1927
1986
|
const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed)
|
|
@@ -1974,6 +2033,152 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1974
2033
|
return null
|
|
1975
2034
|
}
|
|
1976
2035
|
|
|
2036
|
+
/**
|
|
2037
|
+
* Lower a conditional inline-object spread bag value:
|
|
2038
|
+
* `(COND ? { 'aria-describedby': describedBy } : {})`
|
|
2039
|
+
* into an immediately-invoked Go func literal that conditionally
|
|
2040
|
+
* builds the map (so the falsy branch OMITS the key rather than
|
|
2041
|
+
* rendering it as an empty string, which `SpreadAttrs` does not
|
|
2042
|
+
* filter):
|
|
2043
|
+
*
|
|
2044
|
+
* func() map[string]any {
|
|
2045
|
+
* if in.DescribedBy != nil && in.DescribedBy != "" {
|
|
2046
|
+
* return map[string]any{"aria-describedby": in.DescribedBy}
|
|
2047
|
+
* }
|
|
2048
|
+
* return map[string]any{}
|
|
2049
|
+
* }()
|
|
2050
|
+
*
|
|
2051
|
+
* Returns:
|
|
2052
|
+
* - `undefined` when the expression is NOT a parenthesized ternary
|
|
2053
|
+
* of object literals — the caller falls through to other shapes.
|
|
2054
|
+
* - `null` when it IS that shape but a part can't be faithfully
|
|
2055
|
+
* converted (non-static key, unsupported condition, …) — the
|
|
2056
|
+
* caller raises BF101.
|
|
2057
|
+
* - the Go IIFE string when fully convertible.
|
|
2058
|
+
*/
|
|
2059
|
+
private buildConditionalSpreadInitializer(
|
|
2060
|
+
spreadExpr: string,
|
|
2061
|
+
ir: ComponentIR,
|
|
2062
|
+
): string | null | undefined {
|
|
2063
|
+
const expr = this.parseLiteralExpression(spreadExpr)
|
|
2064
|
+
if (!expr || !ts.isConditionalExpression(expr)) return undefined
|
|
2065
|
+
const whenTrue = this.unwrapParens(expr.whenTrue)
|
|
2066
|
+
const whenFalse = this.unwrapParens(expr.whenFalse)
|
|
2067
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
2068
|
+
return undefined
|
|
2069
|
+
}
|
|
2070
|
+
// Condition → Go bool against `in.`, type-aware on the prop.
|
|
2071
|
+
const goCond = this.conditionToGoBool(expr.condition, ir)
|
|
2072
|
+
if (goCond === null) return null
|
|
2073
|
+
const trueMap = this.objectLiteralToGoSpreadMap(whenTrue, ir)
|
|
2074
|
+
const falseMap = this.objectLiteralToGoSpreadMap(whenFalse, ir)
|
|
2075
|
+
if (trueMap === null || falseMap === null) return null
|
|
2076
|
+
return (
|
|
2077
|
+
`func() map[string]any {\n` +
|
|
2078
|
+
`\t\tif ${goCond} {\n` +
|
|
2079
|
+
`\t\t\treturn ${trueMap}\n` +
|
|
2080
|
+
`\t\t}\n` +
|
|
2081
|
+
`\t\treturn ${falseMap}\n` +
|
|
2082
|
+
`\t}()`
|
|
2083
|
+
)
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/** Strip redundant parenthesised wrappers off a TS expression. */
|
|
2087
|
+
private unwrapParens(node: ts.Expression): ts.Expression {
|
|
2088
|
+
let e = node
|
|
2089
|
+
while (ts.isParenthesizedExpression(e)) e = e.expression
|
|
2090
|
+
return e
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
/**
|
|
2094
|
+
* Convert a conditional-spread condition expression to a Go bool in
|
|
2095
|
+
* the `in.` context. Supports a bare prop identifier (`describedBy`)
|
|
2096
|
+
* and its negation (`!describedBy`), type-aware on the prop:
|
|
2097
|
+
* string → `in.X != ""`
|
|
2098
|
+
* boolean → `in.X`
|
|
2099
|
+
* number → `in.X != 0`
|
|
2100
|
+
* unknown / interface{} → `in.X != nil && in.X != ""`
|
|
2101
|
+
* (faithful JS string-truthiness for an interface holding a
|
|
2102
|
+
* string — textarea's `describedBy` resolves to interface{}).
|
|
2103
|
+
* Returns null for any other shape (caller → BF101).
|
|
2104
|
+
*/
|
|
2105
|
+
private conditionToGoBool(
|
|
2106
|
+
condition: ts.Expression,
|
|
2107
|
+
ir: ComponentIR,
|
|
2108
|
+
): string | null {
|
|
2109
|
+
let node = this.unwrapParens(condition)
|
|
2110
|
+
let negate = false
|
|
2111
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
|
|
2112
|
+
negate = true
|
|
2113
|
+
node = this.unwrapParens(node.operand)
|
|
2114
|
+
}
|
|
2115
|
+
if (!ts.isIdentifier(node)) return null
|
|
2116
|
+
const param = ir.metadata.propsParams.find(p => p.name === node.text)
|
|
2117
|
+
if (!param) return null
|
|
2118
|
+
const field = `in.${this.capitalizeFieldName(param.name)}`
|
|
2119
|
+
const prim = param.type.kind === 'primitive' ? param.type.primitive : undefined
|
|
2120
|
+
let truthy: string
|
|
2121
|
+
if (prim === 'boolean') {
|
|
2122
|
+
truthy = field
|
|
2123
|
+
} else if (prim === 'number') {
|
|
2124
|
+
truthy = `${field} != 0`
|
|
2125
|
+
} else if (prim === 'string') {
|
|
2126
|
+
truthy = `${field} != ""`
|
|
2127
|
+
} else {
|
|
2128
|
+
// unknown / interface{}: the runtime value may be a string, number,
|
|
2129
|
+
// bool, etc., so a string-biased `!= ""` test would diverge from JS
|
|
2130
|
+
// truthiness (e.g. an `interface{}` holding `0` or `false` is falsy in
|
|
2131
|
+
// JS but `!= ""` reads true). Route through `bf.Truthy`, the exported
|
|
2132
|
+
// `Boolean(x)` equivalent, for a faithful check (Copilot review #1752).
|
|
2133
|
+
truthy = `bf.Truthy(${field})`
|
|
2134
|
+
}
|
|
2135
|
+
if (!negate) return truthy
|
|
2136
|
+
// Negation: wrap so `!` applies to the whole truthiness test.
|
|
2137
|
+
if (prim === 'boolean') return `!${field}`
|
|
2138
|
+
if (prim === 'number') return `${field} == 0`
|
|
2139
|
+
if (prim === 'string') return `${field} == ""`
|
|
2140
|
+
return `!bf.Truthy(${field})`
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/**
|
|
2144
|
+
* Convert a static object literal (`{ 'aria-describedby': describedBy }`)
|
|
2145
|
+
* into a Go `map[string]any{...}` literal for a conditional spread.
|
|
2146
|
+
* Only static string/identifier keys are allowed; values resolve
|
|
2147
|
+
* prop-identifier references to `in.FieldName` and string literals to
|
|
2148
|
+
* Go string literals. Returns null for any computed/spread/dynamic
|
|
2149
|
+
* key or unsupported value (caller → BF101). Empty object → `map[string]any{}`.
|
|
2150
|
+
*/
|
|
2151
|
+
private objectLiteralToGoSpreadMap(
|
|
2152
|
+
obj: ts.ObjectLiteralExpression,
|
|
2153
|
+
ir: ComponentIR,
|
|
2154
|
+
): string | null {
|
|
2155
|
+
const entries: string[] = []
|
|
2156
|
+
for (const prop of obj.properties) {
|
|
2157
|
+
if (!ts.isPropertyAssignment(prop)) return null
|
|
2158
|
+
let key: string
|
|
2159
|
+
if (ts.isIdentifier(prop.name)) {
|
|
2160
|
+
key = prop.name.text
|
|
2161
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
2162
|
+
key = prop.name.text
|
|
2163
|
+
} else {
|
|
2164
|
+
return null
|
|
2165
|
+
}
|
|
2166
|
+
const val = this.unwrapParens(prop.initializer)
|
|
2167
|
+
let goVal: string
|
|
2168
|
+
if (ts.isStringLiteral(val) || ts.isNoSubstitutionTemplateLiteral(val)) {
|
|
2169
|
+
goVal = JSON.stringify(val.text)
|
|
2170
|
+
} else if (ts.isIdentifier(val)) {
|
|
2171
|
+
const param = ir.metadata.propsParams.find(p => p.name === val.text)
|
|
2172
|
+
if (!param) return null
|
|
2173
|
+
goVal = `in.${this.capitalizeFieldName(param.name)}`
|
|
2174
|
+
} else {
|
|
2175
|
+
return null
|
|
2176
|
+
}
|
|
2177
|
+
entries.push(`${JSON.stringify(key)}: ${goVal}`)
|
|
2178
|
+
}
|
|
2179
|
+
return `map[string]any{${entries.join(', ')}}`
|
|
2180
|
+
}
|
|
2181
|
+
|
|
1977
2182
|
/**
|
|
1978
2183
|
* Convert JavaScript initial value to Go value for NewXxxProps function.
|
|
1979
2184
|
* References to props params are converted to in.FieldName format.
|
|
@@ -4886,6 +5091,20 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4886
5091
|
// Inline Go template syntax with embedded `{{...}}` actions.
|
|
4887
5092
|
return `${name}="${this.renderParsedExpr(parsed)}"`
|
|
4888
5093
|
}
|
|
5094
|
+
// Hono-style nullish-attribute omission (#textarea rows): when the
|
|
5095
|
+
// attribute value is a BARE reference to a nillable (`interface{}`)
|
|
5096
|
+
// prop field, guard emission on `ne .X nil` so an unset optional
|
|
5097
|
+
// prop drops the attribute entirely instead of rendering `attr=""`.
|
|
5098
|
+
// Hono omits `undefined`/`null`-valued attributes; this restores
|
|
5099
|
+
// parity. Scope is deliberately narrow — bare identifiers only — so
|
|
5100
|
+
// member exprs, calls, ternaries, template literals, and
|
|
5101
|
+
// concrete-typed props (which are never nil) are unaffected and
|
|
5102
|
+
// still emit `attr=""` / `attr="0"` exactly as Hono does.
|
|
5103
|
+
const bareId = value.expr.trim()
|
|
5104
|
+
if (this.nillablePropNames.has(bareId)) {
|
|
5105
|
+
const field = `.${this.capitalizeFieldName(bareId)}`
|
|
5106
|
+
return `{{if ne ${field} nil}}${name}="{{${this.convertExpressionToGo(value.expr)}}}"{{end}}`
|
|
5107
|
+
}
|
|
4889
5108
|
return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`
|
|
4890
5109
|
},
|
|
4891
5110
|
emitBooleanAttr: (_value, name) => name,
|