@barefootjs/go-template 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/go-template-adapter.d.ts +222 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +516 -26
- package/dist/build.js +516 -26
- package/dist/index.js +516 -26
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +273 -22
- package/src/adapter/go-template-adapter.ts +907 -24
- package/src/test-render.ts +189 -10
|
@@ -58,6 +58,10 @@ import {
|
|
|
58
58
|
emitParsedExpr,
|
|
59
59
|
emitIRNode,
|
|
60
60
|
emitAttrValue,
|
|
61
|
+
augmentInheritedPropAccesses,
|
|
62
|
+
parseRecordIndexAccess,
|
|
63
|
+
collectContextConsumers,
|
|
64
|
+
type ContextConsumer,
|
|
61
65
|
} from '@barefootjs/jsx'
|
|
62
66
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
63
67
|
|
|
@@ -100,6 +104,24 @@ interface StaticChildInstance {
|
|
|
100
104
|
* through the parent's `{{.Children}}` read; those cases stay on
|
|
101
105
|
* the existing drop path. */
|
|
102
106
|
childrenHtml: string | null
|
|
107
|
+
/**
|
|
108
|
+
* Context values from enclosing `<Ctx.Provider value>` ancestors
|
|
109
|
+
* (`createContext` identifier → Go value literal), wired into this child
|
|
110
|
+
* slot's input against its own context-consumer fields. Empty/undefined when
|
|
111
|
+
* the child isn't under any provider. (#1297)
|
|
112
|
+
*/
|
|
113
|
+
contextBindings?: ReadonlyMap<string, string>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Cross-component shape of a child component the parent renders (#checkbox).
|
|
118
|
+
* `paramNames` are the child's declared `propsParams`; `restBagField` is the
|
|
119
|
+
* Go field name of the child's open-ended rest bag (`Capitalize(restPropsName)`),
|
|
120
|
+
* or null when the child has no `...props` rest spread.
|
|
121
|
+
*/
|
|
122
|
+
interface ChildComponentShape {
|
|
123
|
+
paramNames: Set<string>
|
|
124
|
+
restBagField: string | null
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
/**
|
|
@@ -419,6 +441,24 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
419
441
|
* `template.HTML(...)`; toggles the `"html/template"` import. */
|
|
420
442
|
private usesHtmlTemplate: boolean = false
|
|
421
443
|
|
|
444
|
+
/** Set during type generation when any emit references
|
|
445
|
+
* `fmt.Sprint(...)` — e.g. a `Record<staticKeys, scalar>[propKey]`
|
|
446
|
+
* indexed-map spread value (#checkbox); toggles the `"fmt"` import. */
|
|
447
|
+
private usesFmt: boolean = false
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Cross-component child shapes (#checkbox), keyed by child component name.
|
|
451
|
+
* Populated out-of-band via `registerChildComponentShape` before the parent
|
|
452
|
+
* component's `generateTypes` runs, so the static-child-init codegen can
|
|
453
|
+
* route an attribute that is NOT a declared param of the child
|
|
454
|
+
* (`<CheckIcon data-slot=.../>`) into the child's rest bag
|
|
455
|
+
* (`Capitalize(restPropsName)` map field) instead of emitting an invalid
|
|
456
|
+
* hyphenated top-level field (`Data-slot:`). A child with no rest bag and
|
|
457
|
+
* an unknown attr is left as-is so the existing field path / Go compile
|
|
458
|
+
* error still surfaces.
|
|
459
|
+
*/
|
|
460
|
+
private childComponentShapes: Map<string, ChildComponentShape> = new Map()
|
|
461
|
+
|
|
422
462
|
/**
|
|
423
463
|
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
424
464
|
* file top-level), keyed by name → resolved literal value. Populated at
|
|
@@ -434,6 +474,40 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
434
474
|
*/
|
|
435
475
|
private moduleStringConsts: Map<string, string> = new Map()
|
|
436
476
|
|
|
477
|
+
/**
|
|
478
|
+
* All local constants (module + function-scope) from the IR, retained for
|
|
479
|
+
* the lifetime of `generate()` so the memo-computation path can resolve
|
|
480
|
+
* `Record`-index lookups (`variantClasses[variant]`) without re-threading the
|
|
481
|
+
* full `ir` through every helper. Reset at `generate()` entry.
|
|
482
|
+
*/
|
|
483
|
+
private localConstants: IRMetadata['localConstants'] = []
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* `useContext(...)` consumers in the component being generated. Each becomes
|
|
487
|
+
* a struct field defaulted to the `createContext` default, which an enclosing
|
|
488
|
+
* `<Ctx.Provider value>` overwrites for descendant child slots. Reset at
|
|
489
|
+
* `generate()` / `generateTypes()` entry. (#1297)
|
|
490
|
+
*/
|
|
491
|
+
private contextConsumers: ContextConsumer[] = []
|
|
492
|
+
|
|
493
|
+
/** Child component name → the contexts it consumes (cross-component, for provider wiring). */
|
|
494
|
+
private childContextConsumers: Map<string, ContextConsumer[]> = new Map()
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Set of prop NAMES whose resolved Go struct-field type is exactly
|
|
498
|
+
* `interface{}` — i.e. nillable. Populated at `generate()` entry from
|
|
499
|
+
* the SAME per-prop Go-type computation `generatePropsStruct` /
|
|
500
|
+
* `generateInputStruct` use (`propTypeOverrides` + `typeInfoToGo`), so
|
|
501
|
+
* it can't drift from the actual field types. Used by
|
|
502
|
+
* `elementAttrEmitter.emitExpression` to omit a dynamic attribute whose
|
|
503
|
+
* value is a bare reference to a nillable prop when that prop is nil
|
|
504
|
+
* (Hono-style nullish-attribute omission: an `undefined`-valued
|
|
505
|
+
* attribute is dropped rather than rendered as `attr=""`). Concrete
|
|
506
|
+
* (`string`/`int`/`bool`) fields are never in this set and always emit
|
|
507
|
+
* unconditionally, matching Hono's `value=""` / `data-count="0"`.
|
|
508
|
+
*/
|
|
509
|
+
private nillablePropNames: Set<string> = new Set()
|
|
510
|
+
|
|
437
511
|
constructor(options: GoTemplateAdapterOptions = {}) {
|
|
438
512
|
super()
|
|
439
513
|
this.options = {
|
|
@@ -455,6 +529,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
455
529
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
456
530
|
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
457
531
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
532
|
+
this.localConstants = ir.metadata.localConstants ?? []
|
|
533
|
+
this.contextConsumers = collectContextConsumers(ir.metadata)
|
|
534
|
+
// (#checkbox) Enumerate inherited-attribute accesses (props-object pattern)
|
|
535
|
+
// before computing the nillable set / rendering, so the synthetic params
|
|
536
|
+
// participate in attribute omission and field binding uniformly. Shared
|
|
537
|
+
// with the Mojo adapter (single source of truth in `@barefootjs/jsx`).
|
|
538
|
+
augmentInheritedPropAccesses(ir)
|
|
539
|
+
this.nillablePropNames = this.collectNillablePropNames(ir)
|
|
458
540
|
|
|
459
541
|
// Surface loop-body usages of components imported from sibling
|
|
460
542
|
// .tsx files. The adapter emits `{{template "X" .}}` for these,
|
|
@@ -746,8 +828,70 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
746
828
|
return `{{if .Scripts}}${registrations.join('')}{{end}}\n`
|
|
747
829
|
}
|
|
748
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Register a child component's shape (#checkbox) so a parent's
|
|
833
|
+
* static-child-init codegen can route non-param attributes into the child's
|
|
834
|
+
* rest bag rather than emitting an invalid hyphenated top-level field. Call
|
|
835
|
+
* once per known child IR (siblings in the same source, auto-inferred
|
|
836
|
+
* `../<name>` imports) before generating the parent's types. Idempotent.
|
|
837
|
+
*/
|
|
838
|
+
registerChildComponentShape(ir: ComponentIR): void {
|
|
839
|
+
const name = ir.metadata.componentName
|
|
840
|
+
if (!name) return
|
|
841
|
+
const paramNames = new Set((ir.metadata.propsParams ?? []).map(p => p.name))
|
|
842
|
+
const restPropsName = ir.metadata.restPropsName ?? null
|
|
843
|
+
const restBagField = restPropsName ? this.capitalizeFieldName(restPropsName) : null
|
|
844
|
+
this.childComponentShapes.set(name, { paramNames, restBagField })
|
|
845
|
+
// Record the contexts this child consumes so a parent wrapping it in
|
|
846
|
+
// `<Ctx.Provider value>` can set the matching field on the child's slot input.
|
|
847
|
+
this.childContextConsumers.set(name, collectContextConsumers(ir.metadata))
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** Go field name for a `useContext` consumer (the capitalized local binding). */
|
|
851
|
+
private contextFieldName(c: ContextConsumer): string {
|
|
852
|
+
return this.capitalizeFieldName(c.localName)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Go type for a context-consumer field, from its `createContext` default's type. */
|
|
856
|
+
private contextConsumerGoType(c: ContextConsumer): string {
|
|
857
|
+
if (typeof c.defaultValue === 'number') return 'int'
|
|
858
|
+
if (typeof c.defaultValue === 'boolean') return 'bool'
|
|
859
|
+
return 'string'
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Go literal for a context-consumer's default value (the `createContext` arg). */
|
|
863
|
+
private contextConsumerGoDefault(c: ContextConsumer): string {
|
|
864
|
+
if (typeof c.defaultValue === 'number') return String(c.defaultValue)
|
|
865
|
+
if (typeof c.defaultValue === 'boolean') return String(c.defaultValue)
|
|
866
|
+
if (typeof c.defaultValue === 'string') return `"${this.escapeGoString(c.defaultValue)}"`
|
|
867
|
+
return '""'
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Context-consumer fields that don't collide with an already-emitted prop /
|
|
872
|
+
* signal / memo field. The template reads them as `{{.Field}}` (the local
|
|
873
|
+
* `useContext` binding lowered to a root field); the struct must carry them.
|
|
874
|
+
*/
|
|
875
|
+
private nonCollidingContextConsumers(taken: ReadonlySet<string>): ContextConsumer[] {
|
|
876
|
+
return this.contextConsumers.filter(c => !taken.has(this.contextFieldName(c)))
|
|
877
|
+
}
|
|
878
|
+
|
|
749
879
|
generateTypes(ir: ComponentIR): string | null {
|
|
750
880
|
this.usesHtmlTemplate = false
|
|
881
|
+
this.usesFmt = false
|
|
882
|
+
// (#checkbox) Mirror `generate()`: enumerate inherited-attribute accesses
|
|
883
|
+
// so the Input/Props structs expose `ClassName`/`ID`/`Disabled` fields the
|
|
884
|
+
// template + caller bind against. `generateTypes` runs on a separately
|
|
885
|
+
// round-tripped IR, so this must be applied here too; the method is
|
|
886
|
+
// idempotent. `propsObjectName` is needed by the scan.
|
|
887
|
+
this.propsObjectName = ir.metadata.propsObjectName
|
|
888
|
+
augmentInheritedPropAccesses(ir)
|
|
889
|
+
// Mirror `generate()`: the `NewXxxProps` initializer computes memo SSR
|
|
890
|
+
// values, which inline module string consts and resolve `Record`-index
|
|
891
|
+
// lookups — both need the const tables populated on this standalone entry.
|
|
892
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
893
|
+
this.localConstants = ir.metadata.localConstants ?? []
|
|
894
|
+
this.contextConsumers = collectContextConsumers(ir.metadata)
|
|
751
895
|
const lines: string[] = []
|
|
752
896
|
|
|
753
897
|
const componentName = ir.metadata.componentName
|
|
@@ -840,6 +984,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
840
984
|
header.push(`package ${this.options.packageName}`)
|
|
841
985
|
header.push('')
|
|
842
986
|
header.push('import (')
|
|
987
|
+
// Go's import block is conventionally sorted; emit in lexical order
|
|
988
|
+
// (`fmt` < `html/template` < `math/rand`).
|
|
989
|
+
if (this.usesFmt) header.push('\t"fmt"')
|
|
843
990
|
if (this.usesHtmlTemplate) header.push('\t"html/template"')
|
|
844
991
|
header.push('\t"math/rand"')
|
|
845
992
|
header.push('')
|
|
@@ -1065,6 +1212,40 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1065
1212
|
return overrides
|
|
1066
1213
|
}
|
|
1067
1214
|
|
|
1215
|
+
/**
|
|
1216
|
+
* Resolve a prop param's Go struct-field type using the SAME logic
|
|
1217
|
+
* `generatePropsStruct` / `generateInputStruct` use for the field
|
|
1218
|
+
* declaration: a `propTypeOverrides` entry (signal-inferred override)
|
|
1219
|
+
* wins, otherwise `typeInfoToGo(param.type, param.defaultValue)`.
|
|
1220
|
+
* Factored out so the nillable-field set (`collectNillablePropNames`)
|
|
1221
|
+
* can't drift from the actual emitted field types.
|
|
1222
|
+
*/
|
|
1223
|
+
private resolvePropGoType(
|
|
1224
|
+
param: IRMetadata['propsParams'][number],
|
|
1225
|
+
propTypeOverrides: Map<string, string>,
|
|
1226
|
+
): string {
|
|
1227
|
+
return propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Build the set of prop NAMES whose resolved Go field type is exactly
|
|
1232
|
+
* `interface{}` (nillable). Uses the same `propTypeOverrides` +
|
|
1233
|
+
* `resolvePropGoType` pipeline as the struct generators, so a prop
|
|
1234
|
+
* that ends up `interface{}` on the Props struct — and only such a
|
|
1235
|
+
* prop — is treated as nillable for Hono-style attribute omission.
|
|
1236
|
+
* Concrete (`string`/`int`/`bool`/`[]T`/struct) types are excluded.
|
|
1237
|
+
*/
|
|
1238
|
+
private collectNillablePropNames(ir: ComponentIR): Set<string> {
|
|
1239
|
+
const propTypeOverrides = this.buildPropTypeOverrides(ir)
|
|
1240
|
+
const nillable = new Set<string>()
|
|
1241
|
+
for (const param of ir.metadata.propsParams) {
|
|
1242
|
+
if (this.resolvePropGoType(param, propTypeOverrides) === 'interface{}') {
|
|
1243
|
+
nillable.add(param.name)
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return nillable
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1068
1249
|
/**
|
|
1069
1250
|
* Generate Input struct for a component
|
|
1070
1251
|
*/
|
|
@@ -1096,7 +1277,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1096
1277
|
for (const param of ir.metadata.propsParams) {
|
|
1097
1278
|
const fieldName = this.capitalizeFieldName(param.name)
|
|
1098
1279
|
if (nestedArrayFields.has(fieldName)) continue
|
|
1099
|
-
const goType =
|
|
1280
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides)
|
|
1100
1281
|
lines.push(`\t${fieldName} ${goType}`)
|
|
1101
1282
|
}
|
|
1102
1283
|
|
|
@@ -1105,6 +1286,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1105
1286
|
lines.push(`\t${nested.name}s []${nested.name}Input`)
|
|
1106
1287
|
}
|
|
1107
1288
|
|
|
1289
|
+
// `useContext` consumer fields — settable by an enclosing provider on the
|
|
1290
|
+
// parent's side; default applied in NewXxxProps.
|
|
1291
|
+
const takenInput = new Set(ir.metadata.propsParams.map(p => this.capitalizeFieldName(p.name)))
|
|
1292
|
+
for (const c of this.nonCollidingContextConsumers(takenInput)) {
|
|
1293
|
+
lines.push(`\t${this.contextFieldName(c)} ${this.contextConsumerGoType(c)}`)
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1108
1296
|
// (#1407 follow-up) Input-side bag field for restPropsName spreads.
|
|
1109
1297
|
// The destructured-rest pattern
|
|
1110
1298
|
// (`function({a, ...rest}: P) { <el {...rest}/> }`) surfaces
|
|
@@ -1169,7 +1357,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1169
1357
|
const fieldName = this.capitalizeFieldName(param.name)
|
|
1170
1358
|
// Skip if this field will be replaced by a typed array for nested components
|
|
1171
1359
|
if (nestedArrayFields.has(fieldName)) continue
|
|
1172
|
-
const goType =
|
|
1360
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides)
|
|
1173
1361
|
const jsonTag = this.toJsonTag(param.name)
|
|
1174
1362
|
lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
|
|
1175
1363
|
propFieldNames.add(fieldName)
|
|
@@ -1237,6 +1425,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1237
1425
|
lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
|
|
1238
1426
|
}
|
|
1239
1427
|
|
|
1428
|
+
// `useContext` consumer fields (skip names already taken by a prop /
|
|
1429
|
+
// signal / memo field).
|
|
1430
|
+
const takenProps = new Set<string>([
|
|
1431
|
+
...ir.metadata.propsParams.map(p => this.capitalizeFieldName(p.name)),
|
|
1432
|
+
...ir.metadata.signals.map(s => this.capitalizeFieldName(s.getter)),
|
|
1433
|
+
...ir.metadata.memos.map(m => this.capitalizeFieldName(m.name)),
|
|
1434
|
+
])
|
|
1435
|
+
for (const c of this.nonCollidingContextConsumers(takenProps)) {
|
|
1436
|
+
const jsonTag = this.toJsonTag(c.localName)
|
|
1437
|
+
lines.push(`\t${this.contextFieldName(c)} ${this.contextConsumerGoType(c)} \`json:"${jsonTag}"\``)
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1240
1440
|
// Add array fields for nested components (for template rendering)
|
|
1241
1441
|
for (const nested of nestedComponents) {
|
|
1242
1442
|
if (nested.isDynamic && !nested.isPropDerived) {
|
|
@@ -1416,12 +1616,34 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1416
1616
|
}
|
|
1417
1617
|
|
|
1418
1618
|
// Add memo initial values (computed from signal initial values)
|
|
1619
|
+
const memoPropsParamMap = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
1419
1620
|
for (const memo of ir.metadata.memos) {
|
|
1420
1621
|
const fieldName = this.capitalizeFieldName(memo.name)
|
|
1421
|
-
|
|
1622
|
+
// (#checkbox) Pass the memo's inferred Go type so an unresolved
|
|
1623
|
+
// computation falls back to that type's zero value (`false` for a
|
|
1624
|
+
// boolean memo like `isChecked`), not the int `0`.
|
|
1625
|
+
const goType = this.inferMemoType(memo, ir.metadata.signals, memoPropsParamMap)
|
|
1626
|
+
const memoValue = this.computeMemoInitialValue(memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars, goType)
|
|
1422
1627
|
lines.push(`\t\t${fieldName}: ${memoValue},`)
|
|
1423
1628
|
}
|
|
1424
1629
|
|
|
1630
|
+
// `useContext` consumer fields: default to the `createContext` default
|
|
1631
|
+
// when the caller (a provider) didn't set them.
|
|
1632
|
+
const takenInit = new Set<string>([
|
|
1633
|
+
...ir.metadata.propsParams.map(p => this.capitalizeFieldName(p.name)),
|
|
1634
|
+
...ir.metadata.signals.map(s => this.capitalizeFieldName(s.getter)),
|
|
1635
|
+
...ir.metadata.memos.map(m => this.capitalizeFieldName(m.name)),
|
|
1636
|
+
])
|
|
1637
|
+
for (const c of this.nonCollidingContextConsumers(takenInit)) {
|
|
1638
|
+
const field = this.contextFieldName(c)
|
|
1639
|
+
const def = this.contextConsumerGoDefault(c)
|
|
1640
|
+
const defaulted =
|
|
1641
|
+
c.defaultValue === null || def === '""' || def === '0' || def === 'false'
|
|
1642
|
+
? `in.${field}`
|
|
1643
|
+
: this.applyGoFallback(`in.${field}`, def)
|
|
1644
|
+
lines.push(`\t\t${field}: ${defaulted},`)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1425
1647
|
// Add static child component instances
|
|
1426
1648
|
const staticChildren = this.collectStaticChildInstances(ir.root)
|
|
1427
1649
|
for (const child of staticChildren) {
|
|
@@ -1431,15 +1653,49 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1431
1653
|
// own NewProps (BfParent/BfMount fields).
|
|
1432
1654
|
lines.push(`\t\t\tBfParent: scopeID,`)
|
|
1433
1655
|
lines.push(`\t\t\tBfMount: "${child.slotId}",`)
|
|
1656
|
+
// SSR context propagation: if this child is wrapped in a `<Ctx.Provider
|
|
1657
|
+
// value>` it consumes, set its context-consumer field to the provider
|
|
1658
|
+
// value (else the child's own NewProps applies the `createContext`
|
|
1659
|
+
// default). (#1297)
|
|
1660
|
+
if (child.contextBindings) {
|
|
1661
|
+
for (const consumer of this.childContextConsumers.get(child.name) ?? []) {
|
|
1662
|
+
const goVal = child.contextBindings.get(consumer.contextName)
|
|
1663
|
+
if (goVal !== undefined) {
|
|
1664
|
+
lines.push(`\t\t\t${this.contextFieldName(consumer)}: ${goVal},`)
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
// (#checkbox) Cross-component shape lookup: an attribute that is NOT a
|
|
1669
|
+
// declared param of the child but the child has a `...props` rest bag
|
|
1670
|
+
// (`<CheckIcon data-slot=.../>`, where CheckIcon's params are
|
|
1671
|
+
// `size`/`className` and the rest binding is `props`) must be routed
|
|
1672
|
+
// into the child's rest-bag map field — emitting `Data-slot:` as a
|
|
1673
|
+
// top-level Go field is a syntax error (hyphen). `restBagEntries`
|
|
1674
|
+
// collects `"jsx-attr-name": goValue` pairs for that map.
|
|
1675
|
+
const childShape = this.childComponentShapes.get(child.name)
|
|
1676
|
+
const restBagEntries: string[] = []
|
|
1677
|
+
// Emit a child input field, OR collect it as a rest-bag entry when the
|
|
1678
|
+
// attr isn't a declared child param and a rest bag exists.
|
|
1679
|
+
const emitChildField = (jsxName: string, goValue: string): void => {
|
|
1680
|
+
if (
|
|
1681
|
+
childShape &&
|
|
1682
|
+
childShape.restBagField &&
|
|
1683
|
+
!childShape.paramNames.has(jsxName)
|
|
1684
|
+
) {
|
|
1685
|
+
restBagEntries.push(`${JSON.stringify(jsxName)}: ${goValue}`)
|
|
1686
|
+
return
|
|
1687
|
+
}
|
|
1688
|
+
lines.push(`\t\t\t${this.capitalizeFieldName(jsxName)}: ${goValue},`)
|
|
1689
|
+
}
|
|
1434
1690
|
// Add prop values
|
|
1435
1691
|
for (const prop of child.props) {
|
|
1436
1692
|
switch (prop.value.kind) {
|
|
1437
1693
|
case 'literal':
|
|
1438
|
-
|
|
1694
|
+
emitChildField(prop.name, this.goLiteral(prop.value.value))
|
|
1439
1695
|
break
|
|
1440
1696
|
case 'boolean-shorthand':
|
|
1441
1697
|
case 'boolean-attr':
|
|
1442
|
-
|
|
1698
|
+
emitChildField(prop.name, 'true')
|
|
1443
1699
|
break
|
|
1444
1700
|
case 'expression':
|
|
1445
1701
|
case 'spread':
|
|
@@ -1458,7 +1714,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1458
1714
|
const goExpr = this.templatePartsToGoCode(parts, ir.metadata.propsParams)
|
|
1459
1715
|
if (goExpr !== null) {
|
|
1460
1716
|
// Parts path succeeded — emit and move on.
|
|
1461
|
-
|
|
1717
|
+
emitChildField(prop.name, goExpr)
|
|
1462
1718
|
break
|
|
1463
1719
|
}
|
|
1464
1720
|
// Parts exist but templatePartsToGoCode opted out (unsupported
|
|
@@ -1476,7 +1732,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1476
1732
|
ir.metadata.propsParams
|
|
1477
1733
|
)
|
|
1478
1734
|
if (resolvedValue !== null) {
|
|
1479
|
-
|
|
1735
|
+
emitChildField(prop.name, resolvedValue)
|
|
1480
1736
|
}
|
|
1481
1737
|
break
|
|
1482
1738
|
}
|
|
@@ -1485,6 +1741,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1485
1741
|
break
|
|
1486
1742
|
}
|
|
1487
1743
|
}
|
|
1744
|
+
// (#checkbox) Emit the collected rest-bag entries as the child's
|
|
1745
|
+
// open-ended bag field (`Props: map[string]any{...}`), matching how the
|
|
1746
|
+
// child's `NewXxxProps` maps `in.Props` onto its `Spread_<N>` field.
|
|
1747
|
+
if (childShape?.restBagField && restBagEntries.length > 0) {
|
|
1748
|
+
lines.push(
|
|
1749
|
+
`\t\t\t${childShape.restBagField}: map[string]any{${restBagEntries.join(', ')}},`,
|
|
1750
|
+
)
|
|
1751
|
+
}
|
|
1488
1752
|
// Pass through JSX children as the child slot's `Children` input.
|
|
1489
1753
|
// Two paths:
|
|
1490
1754
|
// 1. Plain text (`<Button>+1</Button>`) → quote with JSON.stringify
|
|
@@ -1596,7 +1860,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1596
1860
|
*/
|
|
1597
1861
|
private collectStaticChildInstances(node: IRNode): Array<StaticChildInstance> {
|
|
1598
1862
|
const result: StaticChildInstance[] = []
|
|
1599
|
-
this.collectStaticChildInstancesRecursive(node, result, false)
|
|
1863
|
+
this.collectStaticChildInstancesRecursive(node, result, false, new Map())
|
|
1600
1864
|
return result
|
|
1601
1865
|
}
|
|
1602
1866
|
|
|
@@ -1637,7 +1901,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1637
1901
|
private collectStaticChildInstancesRecursive(
|
|
1638
1902
|
node: IRNode,
|
|
1639
1903
|
result: StaticChildInstance[],
|
|
1640
|
-
inLoop: boolean
|
|
1904
|
+
inLoop: boolean,
|
|
1905
|
+
providerCtx: ReadonlyMap<string, string>,
|
|
1641
1906
|
): void {
|
|
1642
1907
|
if (node.type === 'component') {
|
|
1643
1908
|
const comp = node as IRComponent
|
|
@@ -1647,7 +1912,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1647
1912
|
// child components nested inside still get their slot fields.
|
|
1648
1913
|
if (comp.dynamicTag) {
|
|
1649
1914
|
for (const child of comp.children) {
|
|
1650
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1915
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1651
1916
|
}
|
|
1652
1917
|
return
|
|
1653
1918
|
}
|
|
@@ -1662,57 +1927,79 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1662
1927
|
fieldName: `${comp.name}${suffix}`,
|
|
1663
1928
|
childrenText: this.extractTextChildren(comp.children),
|
|
1664
1929
|
childrenHtml: this.extractHtmlChildren(comp.children),
|
|
1930
|
+
contextBindings: providerCtx.size > 0 ? providerCtx : undefined,
|
|
1665
1931
|
})
|
|
1666
1932
|
}
|
|
1667
1933
|
// Recurse into Portal's children to find nested components
|
|
1668
1934
|
if (comp.name === 'Portal' && comp.children) {
|
|
1669
1935
|
for (const child of comp.children) {
|
|
1670
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1936
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1671
1937
|
}
|
|
1672
1938
|
}
|
|
1673
1939
|
} else if (node.type === 'loop') {
|
|
1674
1940
|
const loop = node as IRLoop
|
|
1675
1941
|
// Mark children as inside loop
|
|
1676
1942
|
for (const child of loop.children) {
|
|
1677
|
-
this.collectStaticChildInstancesRecursive(child, result, true)
|
|
1943
|
+
this.collectStaticChildInstancesRecursive(child, result, true, providerCtx)
|
|
1678
1944
|
}
|
|
1679
1945
|
} else if (node.type === 'element') {
|
|
1680
1946
|
const element = node as IRElement
|
|
1681
1947
|
for (const child of element.children) {
|
|
1682
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1948
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1683
1949
|
}
|
|
1684
1950
|
} else if (node.type === 'fragment') {
|
|
1685
1951
|
const fragment = node as IRFragment
|
|
1686
1952
|
for (const child of fragment.children) {
|
|
1687
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1953
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1688
1954
|
}
|
|
1689
1955
|
} else if (node.type === 'conditional') {
|
|
1690
1956
|
const cond = node as IRConditional
|
|
1691
|
-
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop)
|
|
1957
|
+
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop, providerCtx)
|
|
1692
1958
|
if (cond.whenFalse) {
|
|
1693
|
-
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop)
|
|
1959
|
+
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx)
|
|
1694
1960
|
}
|
|
1695
1961
|
} else if (node.type === 'provider') {
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1962
|
+
// SSR context propagation: record the provider's value against its
|
|
1963
|
+
// context name and extend the active binding map for descendants. A
|
|
1964
|
+
// literal value lowers to a Go literal; a non-literal is left unbound
|
|
1965
|
+
// (the consumer keeps its default). (#1297)
|
|
1700
1966
|
const p = node as IRProvider
|
|
1967
|
+
const childCtx = this.extendProviderContext(providerCtx, p)
|
|
1701
1968
|
for (const child of p.children) {
|
|
1702
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1969
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, childCtx)
|
|
1703
1970
|
}
|
|
1704
1971
|
} else if (node.type === 'async') {
|
|
1705
1972
|
// Async fallback + children render server-side via the OOS
|
|
1706
1973
|
// protocol; static child components inside them still need slot
|
|
1707
1974
|
// fields on the parent struct.
|
|
1708
1975
|
const a = node as IRAsync
|
|
1709
|
-
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop)
|
|
1976
|
+
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop, providerCtx)
|
|
1710
1977
|
for (const child of a.children) {
|
|
1711
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1978
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1712
1979
|
}
|
|
1713
1980
|
}
|
|
1714
1981
|
}
|
|
1715
1982
|
|
|
1983
|
+
/**
|
|
1984
|
+
* Extend the active provider-context map with one `<Ctx.Provider value>`. A
|
|
1985
|
+
* string/number/boolean literal value is lowered to a Go literal; any other
|
|
1986
|
+
* shape is skipped (the descendant consumer keeps its `createContext` default).
|
|
1987
|
+
*/
|
|
1988
|
+
private extendProviderContext(
|
|
1989
|
+
current: ReadonlyMap<string, string>,
|
|
1990
|
+
p: IRProvider,
|
|
1991
|
+
): ReadonlyMap<string, string> {
|
|
1992
|
+
const v = p.valueProp?.value as { kind?: string; value?: unknown } | undefined
|
|
1993
|
+
if (!v || v.kind !== 'literal') return current
|
|
1994
|
+
let goLit: string | null = null
|
|
1995
|
+
if (typeof v.value === 'string') goLit = `"${this.escapeGoString(v.value)}"`
|
|
1996
|
+
else if (typeof v.value === 'number' || typeof v.value === 'boolean') goLit = String(v.value)
|
|
1997
|
+
if (goLit === null) return current
|
|
1998
|
+
const next = new Map(current)
|
|
1999
|
+
next.set(p.contextName, goLit)
|
|
2000
|
+
return next
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1716
2003
|
/**
|
|
1717
2004
|
* Collect top-level (non-loop) JSX intrinsic-element spread slots
|
|
1718
2005
|
* from the IR (#1407). Loop-internal spreads are skipped — they
|
|
@@ -1922,6 +2209,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1922
2209
|
ir: ComponentIR,
|
|
1923
2210
|
): string | null {
|
|
1924
2211
|
const trimmed = spreadExpr.trim()
|
|
2212
|
+
// Conditional inline-object spread:
|
|
2213
|
+
// `{...(COND ? { 'k': v } : {})}` (either branch possibly `{}`).
|
|
2214
|
+
// Lower to an immediately-invoked func literal that conditionally
|
|
2215
|
+
// builds the bag, so the falsy branch yields an empty map (the key
|
|
2216
|
+
// is OMITTED rather than rendered as `k=""` — `SpreadAttrs` does
|
|
2217
|
+
// NOT filter empty strings). Returns null for any shape it can't
|
|
2218
|
+
// faithfully convert so the caller falls back to BF101 (#textarea).
|
|
2219
|
+
const conditional = this.buildConditionalSpreadInitializer(trimmed, ir)
|
|
2220
|
+
if (conditional !== undefined) return conditional
|
|
1925
2221
|
// Signal-getter call: `attrs()` — pluck the signal's initialValue
|
|
1926
2222
|
// and translate the JS object literal to a Go map literal.
|
|
1927
2223
|
const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed)
|
|
@@ -1970,10 +2266,219 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1970
2266
|
if (ir.metadata.restPropsName === trimmed) {
|
|
1971
2267
|
return `in.${this.capitalizeFieldName(trimmed)}`
|
|
1972
2268
|
}
|
|
2269
|
+
// 4. Function-scope local const holding a conditional inline-object
|
|
2270
|
+
// spread: `const sizeAttrs = size ? {…} : {}` then `{...sizeAttrs}`
|
|
2271
|
+
// (#checkbox / icon). Resolve the identifier to its initializer
|
|
2272
|
+
// text and route through the conditional-spread lowering. Only
|
|
2273
|
+
// function-scope (`!isModule`) consts qualify — a module const is
|
|
2274
|
+
// a different shape, and the resolved initializer must itself be a
|
|
2275
|
+
// conditional-of-object-literals (else `buildConditionalSpreadInitializer`
|
|
2276
|
+
// returns undefined and we fall through to BF101). Guard against a
|
|
2277
|
+
// const that resolves to another bare identifier (loop / non-literal).
|
|
2278
|
+
const localConst = (ir.metadata.localConstants ?? []).find(
|
|
2279
|
+
c => c.name === trimmed && !c.isModule,
|
|
2280
|
+
)
|
|
2281
|
+
if (localConst?.value !== undefined) {
|
|
2282
|
+
const initTrimmed = localConst.value.trim()
|
|
2283
|
+
// Reject a const resolving to a bare identifier to avoid an
|
|
2284
|
+
// unbounded resolution loop / non-literal forwarding.
|
|
2285
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
2286
|
+
const resolved = this.buildConditionalSpreadInitializer(initTrimmed, ir)
|
|
2287
|
+
// `undefined` → not a conditional-spread shape; fall through to
|
|
2288
|
+
// BF101. `null` → that shape but unconvertible; also BF101.
|
|
2289
|
+
if (resolved) return resolved
|
|
2290
|
+
if (resolved === null) return null
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
1973
2293
|
}
|
|
1974
2294
|
return null
|
|
1975
2295
|
}
|
|
1976
2296
|
|
|
2297
|
+
/**
|
|
2298
|
+
* Lower a conditional inline-object spread bag value:
|
|
2299
|
+
* `(COND ? { 'aria-describedby': describedBy } : {})`
|
|
2300
|
+
* into an immediately-invoked Go func literal that conditionally
|
|
2301
|
+
* builds the map (so the falsy branch OMITS the key rather than
|
|
2302
|
+
* rendering it as an empty string, which `SpreadAttrs` does not
|
|
2303
|
+
* filter):
|
|
2304
|
+
*
|
|
2305
|
+
* func() map[string]any {
|
|
2306
|
+
* if in.DescribedBy != nil && in.DescribedBy != "" {
|
|
2307
|
+
* return map[string]any{"aria-describedby": in.DescribedBy}
|
|
2308
|
+
* }
|
|
2309
|
+
* return map[string]any{}
|
|
2310
|
+
* }()
|
|
2311
|
+
*
|
|
2312
|
+
* Returns:
|
|
2313
|
+
* - `undefined` when the expression is NOT a parenthesized ternary
|
|
2314
|
+
* of object literals — the caller falls through to other shapes.
|
|
2315
|
+
* - `null` when it IS that shape but a part can't be faithfully
|
|
2316
|
+
* converted (non-static key, unsupported condition, …) — the
|
|
2317
|
+
* caller raises BF101.
|
|
2318
|
+
* - the Go IIFE string when fully convertible.
|
|
2319
|
+
*/
|
|
2320
|
+
private buildConditionalSpreadInitializer(
|
|
2321
|
+
spreadExpr: string,
|
|
2322
|
+
ir: ComponentIR,
|
|
2323
|
+
): string | null | undefined {
|
|
2324
|
+
const expr = this.parseLiteralExpression(spreadExpr)
|
|
2325
|
+
if (!expr || !ts.isConditionalExpression(expr)) return undefined
|
|
2326
|
+
const whenTrue = this.unwrapParens(expr.whenTrue)
|
|
2327
|
+
const whenFalse = this.unwrapParens(expr.whenFalse)
|
|
2328
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
2329
|
+
return undefined
|
|
2330
|
+
}
|
|
2331
|
+
// Condition → Go bool against `in.`, type-aware on the prop.
|
|
2332
|
+
const goCond = this.conditionToGoBool(expr.condition, ir)
|
|
2333
|
+
if (goCond === null) return null
|
|
2334
|
+
const trueMap = this.objectLiteralToGoSpreadMap(whenTrue, ir)
|
|
2335
|
+
const falseMap = this.objectLiteralToGoSpreadMap(whenFalse, ir)
|
|
2336
|
+
if (trueMap === null || falseMap === null) return null
|
|
2337
|
+
return (
|
|
2338
|
+
`func() map[string]any {\n` +
|
|
2339
|
+
`\t\tif ${goCond} {\n` +
|
|
2340
|
+
`\t\t\treturn ${trueMap}\n` +
|
|
2341
|
+
`\t\t}\n` +
|
|
2342
|
+
`\t\treturn ${falseMap}\n` +
|
|
2343
|
+
`\t}()`
|
|
2344
|
+
)
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/** Strip redundant parenthesised wrappers off a TS expression. */
|
|
2348
|
+
private unwrapParens(node: ts.Expression): ts.Expression {
|
|
2349
|
+
let e = node
|
|
2350
|
+
while (ts.isParenthesizedExpression(e)) e = e.expression
|
|
2351
|
+
return e
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* Convert a conditional-spread condition expression to a Go bool in
|
|
2356
|
+
* the `in.` context. Supports a bare prop identifier (`describedBy`)
|
|
2357
|
+
* and its negation (`!describedBy`), type-aware on the prop:
|
|
2358
|
+
* string → `in.X != ""`
|
|
2359
|
+
* boolean → `in.X`
|
|
2360
|
+
* number → `in.X != 0`
|
|
2361
|
+
* unknown / interface{} → `in.X != nil && in.X != ""`
|
|
2362
|
+
* (faithful JS string-truthiness for an interface holding a
|
|
2363
|
+
* string — textarea's `describedBy` resolves to interface{}).
|
|
2364
|
+
* Returns null for any other shape (caller → BF101).
|
|
2365
|
+
*/
|
|
2366
|
+
private conditionToGoBool(
|
|
2367
|
+
condition: ts.Expression,
|
|
2368
|
+
ir: ComponentIR,
|
|
2369
|
+
): string | null {
|
|
2370
|
+
let node = this.unwrapParens(condition)
|
|
2371
|
+
let negate = false
|
|
2372
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
|
|
2373
|
+
negate = true
|
|
2374
|
+
node = this.unwrapParens(node.operand)
|
|
2375
|
+
}
|
|
2376
|
+
if (!ts.isIdentifier(node)) return null
|
|
2377
|
+
const param = ir.metadata.propsParams.find(p => p.name === node.text)
|
|
2378
|
+
if (!param) return null
|
|
2379
|
+
const field = `in.${this.capitalizeFieldName(param.name)}`
|
|
2380
|
+
const prim = param.type.kind === 'primitive' ? param.type.primitive : undefined
|
|
2381
|
+
let truthy: string
|
|
2382
|
+
if (prim === 'boolean') {
|
|
2383
|
+
truthy = field
|
|
2384
|
+
} else if (prim === 'number') {
|
|
2385
|
+
truthy = `${field} != 0`
|
|
2386
|
+
} else if (prim === 'string') {
|
|
2387
|
+
truthy = `${field} != ""`
|
|
2388
|
+
} else {
|
|
2389
|
+
// unknown / interface{}: the runtime value may be a string, number,
|
|
2390
|
+
// bool, etc., so a string-biased `!= ""` test would diverge from JS
|
|
2391
|
+
// truthiness (e.g. an `interface{}` holding `0` or `false` is falsy in
|
|
2392
|
+
// JS but `!= ""` reads true). Route through `bf.Truthy`, the exported
|
|
2393
|
+
// `Boolean(x)` equivalent, for a faithful check (Copilot review #1752).
|
|
2394
|
+
truthy = `bf.Truthy(${field})`
|
|
2395
|
+
}
|
|
2396
|
+
if (!negate) return truthy
|
|
2397
|
+
// Negation: wrap so `!` applies to the whole truthiness test.
|
|
2398
|
+
if (prim === 'boolean') return `!${field}`
|
|
2399
|
+
if (prim === 'number') return `${field} == 0`
|
|
2400
|
+
if (prim === 'string') return `${field} == ""`
|
|
2401
|
+
return `!bf.Truthy(${field})`
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Convert a static object literal (`{ 'aria-describedby': describedBy }`)
|
|
2406
|
+
* into a Go `map[string]any{...}` literal for a conditional spread.
|
|
2407
|
+
* Only static string/identifier keys are allowed; values resolve
|
|
2408
|
+
* prop-identifier references to `in.FieldName` and string literals to
|
|
2409
|
+
* Go string literals. Returns null for any computed/spread/dynamic
|
|
2410
|
+
* key or unsupported value (caller → BF101). Empty object → `map[string]any{}`.
|
|
2411
|
+
*/
|
|
2412
|
+
private objectLiteralToGoSpreadMap(
|
|
2413
|
+
obj: ts.ObjectLiteralExpression,
|
|
2414
|
+
ir: ComponentIR,
|
|
2415
|
+
): string | null {
|
|
2416
|
+
const entries: string[] = []
|
|
2417
|
+
for (const prop of obj.properties) {
|
|
2418
|
+
if (!ts.isPropertyAssignment(prop)) return null
|
|
2419
|
+
let key: string
|
|
2420
|
+
if (ts.isIdentifier(prop.name)) {
|
|
2421
|
+
key = prop.name.text
|
|
2422
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
2423
|
+
key = prop.name.text
|
|
2424
|
+
} else {
|
|
2425
|
+
return null
|
|
2426
|
+
}
|
|
2427
|
+
const val = this.unwrapParens(prop.initializer)
|
|
2428
|
+
let goVal: string
|
|
2429
|
+
if (ts.isStringLiteral(val) || ts.isNoSubstitutionTemplateLiteral(val)) {
|
|
2430
|
+
goVal = JSON.stringify(val.text)
|
|
2431
|
+
} else if (ts.isIdentifier(val)) {
|
|
2432
|
+
const param = ir.metadata.propsParams.find(p => p.name === val.text)
|
|
2433
|
+
if (!param) return null
|
|
2434
|
+
goVal = `in.${this.capitalizeFieldName(param.name)}`
|
|
2435
|
+
} else {
|
|
2436
|
+
const indexed = this.recordIndexAccessToGoMap(val, ir)
|
|
2437
|
+
if (indexed === null) return null
|
|
2438
|
+
goVal = indexed
|
|
2439
|
+
}
|
|
2440
|
+
entries.push(`${JSON.stringify(key)}: ${goVal}`)
|
|
2441
|
+
}
|
|
2442
|
+
return `map[string]any{${entries.join(', ')}}`
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
/**
|
|
2446
|
+
* Lower a spread-object VALUE of the form `IDENT[KEY]` where:
|
|
2447
|
+
* - `IDENT` resolves via `localConstants` to a MODULE-scope object
|
|
2448
|
+
* literal whose property values are all scalar (number/string)
|
|
2449
|
+
* literals under static (string-literal or identifier) keys
|
|
2450
|
+
* (a `Record<staticKeys, scalar>` map like `sizeMap`), AND
|
|
2451
|
+
* - `KEY` is a bare identifier that is a prop.
|
|
2452
|
+
* Emits an inline indexed Go map:
|
|
2453
|
+
* `map[string]any{"sm": 16, ...}[fmt.Sprint(in.Size)]`
|
|
2454
|
+
* (`fmt.Sprint` coerces the `interface{}`/typed prop to the map's
|
|
2455
|
+
* string key space — sets `usesFmt` so the `"fmt"` import is added).
|
|
2456
|
+
*
|
|
2457
|
+
* Returns the Go string when convertible, else `null` (caller → BF101)
|
|
2458
|
+
* for any non-scalar value, non-static key, or non-prop index so
|
|
2459
|
+
* unrelated shapes don't regress. (#checkbox / icon `sizeMap[size]`.)
|
|
2460
|
+
*/
|
|
2461
|
+
private recordIndexAccessToGoMap(
|
|
2462
|
+
val: ts.Expression,
|
|
2463
|
+
ir: ComponentIR,
|
|
2464
|
+
): string | null {
|
|
2465
|
+
// Shared structural parse (single source of truth in `@barefootjs/jsx`);
|
|
2466
|
+
// this wrapper only does the Go-specific emit from the structured result.
|
|
2467
|
+
const parsed = parseRecordIndexAccess(
|
|
2468
|
+
val,
|
|
2469
|
+
ir.metadata.localConstants ?? [],
|
|
2470
|
+
ir.metadata.propsParams,
|
|
2471
|
+
)
|
|
2472
|
+
if (!parsed) return null
|
|
2473
|
+
const entries = parsed.entries.map(e => {
|
|
2474
|
+
const mapVal = e.value.kind === 'number' ? e.value.text : JSON.stringify(e.value.text)
|
|
2475
|
+
return `${JSON.stringify(e.key)}: ${mapVal}`
|
|
2476
|
+
})
|
|
2477
|
+
this.usesFmt = true
|
|
2478
|
+
const field = `in.${this.capitalizeFieldName(parsed.indexPropName)}`
|
|
2479
|
+
return `map[string]any{${entries.join(', ')}}[fmt.Sprint(${field})]`
|
|
2480
|
+
}
|
|
2481
|
+
|
|
1977
2482
|
/**
|
|
1978
2483
|
* Convert JavaScript initial value to Go value for NewXxxProps function.
|
|
1979
2484
|
* References to props params are converted to in.FieldName format.
|
|
@@ -2383,11 +2888,232 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2383
2888
|
* referenced prop, substitute it for `in.FieldName` so the memo
|
|
2384
2889
|
* inherits the signal-time `??` fallback.
|
|
2385
2890
|
*/
|
|
2891
|
+
/**
|
|
2892
|
+
* (#checkbox) Compute the SSR initial value of a template-literal memo as a
|
|
2893
|
+
* Go `string` expression. The memo computation looks like
|
|
2894
|
+
* `() => `${a} ${b} ${props.className ?? ''} grid place-content-center``.
|
|
2895
|
+
*
|
|
2896
|
+
* Each quasi (literal text span) becomes a Go string literal; each
|
|
2897
|
+
* interpolation is resolved:
|
|
2898
|
+
* - an identifier naming a module string const → its inlined literal
|
|
2899
|
+
* (covers both pure-string and `[...].join(' ')` consts);
|
|
2900
|
+
* - `props.<name> ?? '<fallback>'` or bare `props.<name>` → `in.<Field>`
|
|
2901
|
+
* when `<name>` is a known prop param (typed `string`); the `?? ''`
|
|
2902
|
+
* fallback maps to Go's zero value for an unset string field, matching
|
|
2903
|
+
* the Hono reference's empty-string result.
|
|
2904
|
+
*
|
|
2905
|
+
* Returns the `"a" + in.Field + " grid..."` concatenation, or null when the
|
|
2906
|
+
* computation isn't a single template literal or any interpolation isn't
|
|
2907
|
+
* representable (so the caller keeps its existing pattern matching).
|
|
2908
|
+
*/
|
|
2909
|
+
private computeTemplateLiteralMemoInitialValue(
|
|
2910
|
+
computation: string,
|
|
2911
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
2912
|
+
): string | null {
|
|
2913
|
+
const sf = ts.createSourceFile(
|
|
2914
|
+
'__memo.ts',
|
|
2915
|
+
`const __x = (${computation});`,
|
|
2916
|
+
ts.ScriptTarget.Latest,
|
|
2917
|
+
/*setParentNodes*/ false,
|
|
2918
|
+
)
|
|
2919
|
+
const stmt = sf.statements[0]
|
|
2920
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
2921
|
+
let init = stmt.declarationList.declarations[0]?.initializer
|
|
2922
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
2923
|
+
if (!init || !ts.isArrowFunction(init)) return null
|
|
2924
|
+
let body = init.body as ts.Node
|
|
2925
|
+
while (ts.isParenthesizedExpression(body as ts.Expression)) {
|
|
2926
|
+
body = (body as ts.ParenthesizedExpression).expression
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
// Block-bodied arrow (the Toggle `classes` memo): collect leading
|
|
2930
|
+
// `const X = props.Y ?? 'lit'` key bindings, then resolve against the
|
|
2931
|
+
// single returned template literal. The bindings let `variantClasses[variant]`
|
|
2932
|
+
// resolve `variant` to prop `variant` with its `'default'` fallback key.
|
|
2933
|
+
const localKeyBindings = new Map<string, { propName: string; defaultLiteral?: string }>()
|
|
2934
|
+
if (ts.isBlock(body)) {
|
|
2935
|
+
let returned: ts.Node | null = null
|
|
2936
|
+
for (const s of body.statements) {
|
|
2937
|
+
if (ts.isVariableStatement(s)) {
|
|
2938
|
+
for (const d of s.declarationList.declarations) {
|
|
2939
|
+
if (!ts.isIdentifier(d.name) || !d.initializer) continue
|
|
2940
|
+
const binding = this.parseLocalKeyBinding(d.initializer)
|
|
2941
|
+
if (binding) localKeyBindings.set(d.name.text, binding)
|
|
2942
|
+
}
|
|
2943
|
+
} else if (ts.isReturnStatement(s) && s.expression) {
|
|
2944
|
+
returned = s.expression
|
|
2945
|
+
} else if (ts.isExpressionStatement(s) || ts.isEmptyStatement(s)) {
|
|
2946
|
+
// ignore
|
|
2947
|
+
} else {
|
|
2948
|
+
return null // unsupported statement shape — bail to existing patterns
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
if (!returned) return null
|
|
2952
|
+
body = returned
|
|
2953
|
+
while (ts.isParenthesizedExpression(body as ts.Expression)) {
|
|
2954
|
+
body = (body as ts.ParenthesizedExpression).expression
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
if (!ts.isTemplateExpression(body) && !ts.isNoSubstitutionTemplateLiteral(body)) {
|
|
2959
|
+
return null
|
|
2960
|
+
}
|
|
2961
|
+
const propNames = new Set(propsParams.map(p => p.name))
|
|
2962
|
+
const escGo = (s: string) => `"${this.escapeGoString(s)}"`
|
|
2963
|
+
const segments: string[] = []
|
|
2964
|
+
|
|
2965
|
+
if (ts.isNoSubstitutionTemplateLiteral(body)) {
|
|
2966
|
+
return escGo(body.text)
|
|
2967
|
+
}
|
|
2968
|
+
// head + each span
|
|
2969
|
+
if (body.head.text) segments.push(escGo(body.head.text))
|
|
2970
|
+
for (const span of body.templateSpans) {
|
|
2971
|
+
const resolved = this.resolveTemplateInterpolation(span.expression, propNames, localKeyBindings)
|
|
2972
|
+
if (resolved === null) return null
|
|
2973
|
+
segments.push(resolved)
|
|
2974
|
+
if (span.literal.text) segments.push(escGo(span.literal.text))
|
|
2975
|
+
}
|
|
2976
|
+
if (segments.length === 0) return '""'
|
|
2977
|
+
return segments.join(' + ')
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/**
|
|
2981
|
+
* (#checkbox) Resolve one `${expr}` interpolation of a template-literal memo
|
|
2982
|
+
* to a Go string expression, or null when unsupported. See
|
|
2983
|
+
* `computeTemplateLiteralMemoInitialValue` for the supported shapes.
|
|
2984
|
+
*/
|
|
2985
|
+
private resolveTemplateInterpolation(
|
|
2986
|
+
expr: ts.Expression,
|
|
2987
|
+
propNames: Set<string>,
|
|
2988
|
+
localKeyBindings: ReadonlyMap<string, { propName: string; defaultLiteral?: string }> = new Map(),
|
|
2989
|
+
): string | null {
|
|
2990
|
+
let node: ts.Expression = expr
|
|
2991
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
2992
|
+
|
|
2993
|
+
// Identifier → module string const inline.
|
|
2994
|
+
if (ts.isIdentifier(node)) {
|
|
2995
|
+
const inlined = this.resolveModuleStringConst(node.text)
|
|
2996
|
+
if (inlined !== null) return inlined
|
|
2997
|
+
return null
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// `recordConst[key]` → inline indexed Go map (the Toggle `classes` memo's
|
|
3001
|
+
// `variantClasses[variant]` / `sizeClasses[size]`).
|
|
3002
|
+
if (ts.isElementAccessExpression(node)) {
|
|
3003
|
+
const indexed = this.recordIndexInterpolationToGo(node, propNames, localKeyBindings)
|
|
3004
|
+
if (indexed !== null) return indexed
|
|
3005
|
+
return null
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// `props.X ?? '...'` — string-typed prop with an empty-string fallback.
|
|
3009
|
+
if (
|
|
3010
|
+
ts.isBinaryExpression(node) &&
|
|
3011
|
+
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken
|
|
3012
|
+
) {
|
|
3013
|
+
const right = node.right
|
|
3014
|
+
const isEmptyStr =
|
|
3015
|
+
(ts.isStringLiteral(right) || ts.isNoSubstitutionTemplateLiteral(right)) &&
|
|
3016
|
+
right.text === ''
|
|
3017
|
+
const propName = this.propsAccessName(node.left)
|
|
3018
|
+
if (propName && propNames.has(propName) && isEmptyStr) {
|
|
3019
|
+
// Unset string field is "" in Go — same as `?? ''`.
|
|
3020
|
+
return `in.${this.capitalizeFieldName(propName)}`
|
|
3021
|
+
}
|
|
3022
|
+
return null
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// Bare `props.X` (string-typed prop).
|
|
3026
|
+
const propName = this.propsAccessName(node)
|
|
3027
|
+
if (propName && propNames.has(propName)) {
|
|
3028
|
+
return `in.${this.capitalizeFieldName(propName)}`
|
|
3029
|
+
}
|
|
3030
|
+
return null
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
/**
|
|
3034
|
+
* Parse a memo-local `const X = …` initializer into a `Record`-index key
|
|
3035
|
+
* binding: `props.Y ?? 'lit'` → `{ propName: 'Y', defaultLiteral: 'lit' }`,
|
|
3036
|
+
* or bare `props.Y` → `{ propName: 'Y' }`. Returns null for any other shape
|
|
3037
|
+
* (a literal const, a call, etc.) so it simply isn't registered as a key.
|
|
3038
|
+
*/
|
|
3039
|
+
private parseLocalKeyBinding(
|
|
3040
|
+
init: ts.Expression,
|
|
3041
|
+
): { propName: string; defaultLiteral?: string } | null {
|
|
3042
|
+
let node: ts.Expression = init
|
|
3043
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
3044
|
+
if (
|
|
3045
|
+
ts.isBinaryExpression(node) &&
|
|
3046
|
+
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken
|
|
3047
|
+
) {
|
|
3048
|
+
const propName = this.propsAccessName(node.left)
|
|
3049
|
+
const right = node.right
|
|
3050
|
+
if (
|
|
3051
|
+
propName &&
|
|
3052
|
+
(ts.isStringLiteral(right) || ts.isNoSubstitutionTemplateLiteral(right))
|
|
3053
|
+
) {
|
|
3054
|
+
return { propName, defaultLiteral: right.text }
|
|
3055
|
+
}
|
|
3056
|
+
return null
|
|
3057
|
+
}
|
|
3058
|
+
const propName = this.propsAccessName(node)
|
|
3059
|
+
if (propName) return { propName }
|
|
3060
|
+
return null
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
/**
|
|
3064
|
+
* Lower a `recordConst[key]` interpolation to an inline indexed Go map,
|
|
3065
|
+
* emitting `map[string]string{…}[fmt.Sprint(in.Field)]` (or `map[string]any`
|
|
3066
|
+
* for mixed values). `key` is a bare prop or a memo-local const bound to
|
|
3067
|
+
* `props.X ?? 'default'` (resolved via `localKeyBindings`); a `'default'`
|
|
3068
|
+
* fallback also maps `""` to that entry, so an unset prop (Go zero value `""`)
|
|
3069
|
+
* renders the default — matching Hono's `props.X ?? 'default'`. Returns null
|
|
3070
|
+
* for any non-record / non-resolvable key so the caller falls through.
|
|
3071
|
+
*/
|
|
3072
|
+
private recordIndexInterpolationToGo(
|
|
3073
|
+
node: ts.ElementAccessExpression,
|
|
3074
|
+
propNames: Set<string>,
|
|
3075
|
+
localKeyBindings: ReadonlyMap<string, { propName: string; defaultLiteral?: string }>,
|
|
3076
|
+
): string | null {
|
|
3077
|
+
const parsed = parseRecordIndexAccess(
|
|
3078
|
+
node,
|
|
3079
|
+
this.localConstants ?? [],
|
|
3080
|
+
[...propNames].map(name => ({ name })),
|
|
3081
|
+
name => localKeyBindings.get(name) ?? null,
|
|
3082
|
+
)
|
|
3083
|
+
if (!parsed) return null
|
|
3084
|
+
const goVal = (v: { kind: 'number' | 'string'; text: string }) =>
|
|
3085
|
+
v.kind === 'number' ? v.text : JSON.stringify(v.text)
|
|
3086
|
+
const entries = parsed.entries.map(e => `${JSON.stringify(e.key)}: ${goVal(e.value)}`)
|
|
3087
|
+
if (parsed.defaultKey !== undefined) {
|
|
3088
|
+
const def = parsed.entries.find(e => e.key === parsed.defaultKey)
|
|
3089
|
+
if (def) entries.unshift(`"": ${goVal(def.value)}`)
|
|
3090
|
+
}
|
|
3091
|
+
const allString = parsed.entries.every(e => e.value.kind === 'string')
|
|
3092
|
+
const mapType = allString ? 'map[string]string' : 'map[string]any'
|
|
3093
|
+
this.usesFmt = true
|
|
3094
|
+
return `${mapType}{${entries.join(', ')}}[fmt.Sprint(in.${this.capitalizeFieldName(parsed.indexPropName)})]`
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
/**
|
|
3098
|
+
* If `node` is a `<propsObjectName>.<name>` access, return `<name>`, else
|
|
3099
|
+
* null. Used to recognize props-object reads inside memo interpolations.
|
|
3100
|
+
*/
|
|
3101
|
+
private propsAccessName(node: ts.Expression): string | null {
|
|
3102
|
+
if (!ts.isPropertyAccessExpression(node)) return null
|
|
3103
|
+
if (!ts.isIdentifier(node.expression)) return null
|
|
3104
|
+
if (!this.propsObjectName || node.expression.text !== this.propsObjectName) return null
|
|
3105
|
+
return node.name.text
|
|
3106
|
+
}
|
|
3107
|
+
|
|
2386
3108
|
private computeMemoInitialValue(
|
|
2387
3109
|
memo: { name: string; computation: string; deps: string[] },
|
|
2388
3110
|
signals: { getter: string; initialValue: string }[],
|
|
2389
3111
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
2390
3112
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = GoTemplateAdapter.EMPTY_PROP_FALLBACK_VARS,
|
|
3113
|
+
// (#checkbox) Go type of the memo field; used to pick the zero-value
|
|
3114
|
+
// fallback for an unresolved computation (`false` for `bool`, `""` for
|
|
3115
|
+
// `string`, else the historical `0`).
|
|
3116
|
+
goType?: string,
|
|
2391
3117
|
): string {
|
|
2392
3118
|
const computation = memo.computation
|
|
2393
3119
|
// Helper to pick the hoisted var (if any) or fall back to `in.X`.
|
|
@@ -2397,6 +3123,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2397
3123
|
return `in.${this.capitalizeFieldName(propName)}`
|
|
2398
3124
|
}
|
|
2399
3125
|
|
|
3126
|
+
// (#checkbox) Pattern: () => `...${expr}...` — a template-literal memo
|
|
3127
|
+
// (the classes memo: `${baseClasses} ${focusClasses} ... ${props.className
|
|
3128
|
+
// ?? ''} grid place-content-center`). Build a Go string concatenation that
|
|
3129
|
+
// inlines module string consts (incl. `[...].join(' ')` consts resolved by
|
|
3130
|
+
// `resolveModuleStringConst`) and resolves `props.X ?? ''` / bare `props.X`
|
|
3131
|
+
// to the corresponding `in.Field`. Returns null when any interpolation
|
|
3132
|
+
// isn't representable, so the existing patterns below still apply.
|
|
3133
|
+
const tmplMemo = this.computeTemplateLiteralMemoInitialValue(computation, propsParams)
|
|
3134
|
+
if (tmplMemo !== null) return tmplMemo
|
|
3135
|
+
|
|
2400
3136
|
// Pattern: () => dep() * N or () => dep() + N etc.
|
|
2401
3137
|
const arithmeticMatch = computation.match(/\(\)\s*=>\s*(\w+)\(\)\s*([*+\-/])\s*(\d+)/)
|
|
2402
3138
|
if (arithmeticMatch) {
|
|
@@ -2475,10 +3211,42 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2475
3211
|
}
|
|
2476
3212
|
}
|
|
2477
3213
|
|
|
2478
|
-
// Default:
|
|
3214
|
+
// Default: zero value for the memo's Go type (#checkbox). A boolean memo
|
|
3215
|
+
// (`isChecked`) renders `false`, a string memo `""`; otherwise the
|
|
3216
|
+
// historical int `0`.
|
|
3217
|
+
if (goType === 'bool') return 'false'
|
|
3218
|
+
if (goType === 'string') return '""'
|
|
2479
3219
|
return '0'
|
|
2480
3220
|
}
|
|
2481
3221
|
|
|
3222
|
+
/**
|
|
3223
|
+
* Whether a memo is an arrow whose result is a template literal — either a
|
|
3224
|
+
* concise body (`() => \`…\``) or a block body whose `return` is one.
|
|
3225
|
+
*/
|
|
3226
|
+
private isTemplateLiteralMemo(computation: string): boolean {
|
|
3227
|
+
const sf = ts.createSourceFile(
|
|
3228
|
+
'__memo.ts', `const __x = (${computation});`, ts.ScriptTarget.Latest, /*setParentNodes*/ false,
|
|
3229
|
+
)
|
|
3230
|
+
const stmt = sf.statements[0]
|
|
3231
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return false
|
|
3232
|
+
let init = stmt.declarationList.declarations[0]?.initializer
|
|
3233
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
3234
|
+
if (!init || !ts.isArrowFunction(init)) return false
|
|
3235
|
+
let body = init.body as ts.Node
|
|
3236
|
+
while (ts.isParenthesizedExpression(body as ts.Expression)) {
|
|
3237
|
+
body = (body as ts.ParenthesizedExpression).expression
|
|
3238
|
+
}
|
|
3239
|
+
if (ts.isBlock(body)) {
|
|
3240
|
+
const ret = body.statements.find(ts.isReturnStatement)
|
|
3241
|
+
if (!ret || !ret.expression) return false
|
|
3242
|
+
body = ret.expression
|
|
3243
|
+
while (ts.isParenthesizedExpression(body as ts.Expression)) {
|
|
3244
|
+
body = (body as ts.ParenthesizedExpression).expression
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return ts.isTemplateExpression(body) || ts.isNoSubstitutionTemplateLiteral(body)
|
|
3248
|
+
}
|
|
3249
|
+
|
|
2482
3250
|
/**
|
|
2483
3251
|
* Infer the Go type for a memo based on its computation and dependencies.
|
|
2484
3252
|
*/
|
|
@@ -2487,6 +3255,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2487
3255
|
signals: { getter: string; initialValue: string; type: TypeInfo }[],
|
|
2488
3256
|
propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>
|
|
2489
3257
|
): string {
|
|
3258
|
+
// A template-literal memo always produces a string. Decide this first so a
|
|
3259
|
+
// class-string `/` (e.g. `ring-ring/50`) doesn't trip the arithmetic
|
|
3260
|
+
// heuristic below into `int`.
|
|
3261
|
+
if (this.isTemplateLiteralMemo(memo.computation)) return 'string'
|
|
3262
|
+
|
|
2490
3263
|
// Check if computation involves multiplication (*) - likely number
|
|
2491
3264
|
if (memo.computation.includes('*') || memo.computation.includes('/') ||
|
|
2492
3265
|
memo.computation.includes('+') || memo.computation.includes('-')) {
|
|
@@ -2514,10 +3287,58 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2514
3287
|
}
|
|
2515
3288
|
}
|
|
2516
3289
|
|
|
3290
|
+
// (#checkbox) Boolean memo: a comparison/negation/ternary whose dependency
|
|
3291
|
+
// signals are all boolean (`isChecked = isControlled() ? controlledChecked()
|
|
3292
|
+
// : internalChecked()`). Inferring `bool` makes the field render `false`
|
|
3293
|
+
// (not the int `0`) for `aria-checked={isChecked()}`, matching Hono's SSR
|
|
3294
|
+
// initial value. Only fires when the declared memo type is unknown so an
|
|
3295
|
+
// explicitly-typed memo still wins.
|
|
3296
|
+
if (this.typeInfoToGo(memo.type) === 'interface{}' && this.isBooleanMemo(memo, signals, propsParamMap)) {
|
|
3297
|
+
return 'bool'
|
|
3298
|
+
}
|
|
3299
|
+
|
|
2517
3300
|
// Default to the memo's declared type
|
|
2518
3301
|
return this.typeInfoToGo(memo.type)
|
|
2519
3302
|
}
|
|
2520
3303
|
|
|
3304
|
+
/**
|
|
3305
|
+
* (#checkbox) Heuristic: does this memo evaluate to a boolean? True when its
|
|
3306
|
+
* computation is a comparison (`!==`/`===`/`!=`/`==`), a negation (`!x`), or
|
|
3307
|
+
* a ternary whose branches are all boolean signals/props. Used to pick `bool`
|
|
3308
|
+
* (zero value `false`) over the int `0` default for the SSR initial value.
|
|
3309
|
+
*/
|
|
3310
|
+
private isBooleanMemo(
|
|
3311
|
+
memo: { computation: string; deps: string[] },
|
|
3312
|
+
signals: { getter: string; initialValue: string; type: TypeInfo }[],
|
|
3313
|
+
propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>,
|
|
3314
|
+
): boolean {
|
|
3315
|
+
const c = memo.computation
|
|
3316
|
+
if (/(!==|===|!=(?!=)|==(?!=))/.test(c)) return true
|
|
3317
|
+
if (/=>\s*!/.test(c)) return true
|
|
3318
|
+
// Ternary `() => cond() ? a() : b()` — boolean when both branches are
|
|
3319
|
+
// boolean-resolving getters (signals whose value is boolean, or boolean
|
|
3320
|
+
// props).
|
|
3321
|
+
const isBoolGetter = (name: string): boolean => {
|
|
3322
|
+
const sig = signals.find(s => s.getter === name)
|
|
3323
|
+
if (sig) {
|
|
3324
|
+
if (this.typeInfoToGo(sig.type) === 'bool') return true
|
|
3325
|
+
// Signal initialised from `props.X ?? false` / a boolean prop.
|
|
3326
|
+
if (/\?\?\s*(true|false)\b/.test(sig.initialValue)) return true
|
|
3327
|
+
const propName = this.extractPropNameFromInitialValue(sig.initialValue) ?? sig.initialValue
|
|
3328
|
+
const prop = propsParamMap.get(propName)
|
|
3329
|
+
if (prop && this.typeInfoToGo(prop.type, prop.defaultValue) === 'bool') return true
|
|
3330
|
+
return false
|
|
3331
|
+
}
|
|
3332
|
+
const prop = propsParamMap.get(name)
|
|
3333
|
+
return !!prop && this.typeInfoToGo(prop.type, prop.defaultValue) === 'bool'
|
|
3334
|
+
}
|
|
3335
|
+
const ternary = c.match(/=>\s*\w+\(\)\s*\?\s*(\w+)\(\)\s*:\s*(\w+)\(\)/)
|
|
3336
|
+
if (ternary) {
|
|
3337
|
+
return isBoolGetter(ternary[1]) && isBoolGetter(ternary[2])
|
|
3338
|
+
}
|
|
3339
|
+
return false
|
|
3340
|
+
}
|
|
3341
|
+
|
|
2521
3342
|
/**
|
|
2522
3343
|
* Infer Go type from a JavaScript value literal.
|
|
2523
3344
|
*/
|
|
@@ -3002,9 +3823,49 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3002
3823
|
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
3003
3824
|
return init.text
|
|
3004
3825
|
}
|
|
3826
|
+
// (#checkbox) `[...string literals].join(SEP)` — a const that flattens an
|
|
3827
|
+
// array of pure string literals to a single string at module load (e.g.
|
|
3828
|
+
// Checkbox's `stateClasses = [...].join(' ')`). Evaluate it statically so
|
|
3829
|
+
// the const inlines byte-for-byte like the Hono reference. Only fires when
|
|
3830
|
+
// every array element is a string/no-substitution-template literal and the
|
|
3831
|
+
// separator is a string-literal argument (or omitted → default `,`).
|
|
3832
|
+
const joined = this.evalStringArrayJoin(init)
|
|
3833
|
+
if (joined !== null) return joined
|
|
3005
3834
|
return null
|
|
3006
3835
|
}
|
|
3007
3836
|
|
|
3837
|
+
/**
|
|
3838
|
+
* (#checkbox) Statically evaluate `[<string literals>].join(<sep?>)`.
|
|
3839
|
+
* Returns the joined string, or null when the shape doesn't match (non-call,
|
|
3840
|
+
* non-`.join`, non-array receiver, any non-string-literal element, or a
|
|
3841
|
+
* non-string-literal separator). Comments/whitespace between elements are
|
|
3842
|
+
* irrelevant — the TS parser already discarded them.
|
|
3843
|
+
*/
|
|
3844
|
+
private evalStringArrayJoin(node: ts.Expression): string | null {
|
|
3845
|
+
if (!ts.isCallExpression(node)) return null
|
|
3846
|
+
const callee = node.expression
|
|
3847
|
+
if (!ts.isPropertyAccessExpression(callee)) return null
|
|
3848
|
+
if (callee.name.text !== 'join') return null
|
|
3849
|
+
let recv: ts.Expression = callee.expression
|
|
3850
|
+
while (ts.isParenthesizedExpression(recv)) recv = recv.expression
|
|
3851
|
+
if (!ts.isArrayLiteralExpression(recv)) return null
|
|
3852
|
+
const parts: string[] = []
|
|
3853
|
+
for (const el of recv.elements) {
|
|
3854
|
+
if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) {
|
|
3855
|
+
parts.push(el.text)
|
|
3856
|
+
} else {
|
|
3857
|
+
return null
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
let sep = ','
|
|
3861
|
+
if (node.arguments.length >= 1) {
|
|
3862
|
+
const arg = node.arguments[0]
|
|
3863
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) sep = arg.text
|
|
3864
|
+
else return null
|
|
3865
|
+
}
|
|
3866
|
+
return parts.join(sep)
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3008
3869
|
/**
|
|
3009
3870
|
* Resolve an identifier to its inlined Go string literal when it names a
|
|
3010
3871
|
* module pure-string const. Returns the Go template literal form
|
|
@@ -4886,6 +5747,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4886
5747
|
// Inline Go template syntax with embedded `{{...}}` actions.
|
|
4887
5748
|
return `${name}="${this.renderParsedExpr(parsed)}"`
|
|
4888
5749
|
}
|
|
5750
|
+
// Hono-style nullish-attribute omission (#textarea rows): when the
|
|
5751
|
+
// attribute value is a BARE reference to a nillable (`interface{}`)
|
|
5752
|
+
// prop field, guard emission on `ne .X nil` so an unset optional
|
|
5753
|
+
// prop drops the attribute entirely instead of rendering `attr=""`.
|
|
5754
|
+
// Hono omits `undefined`/`null`-valued attributes; this restores
|
|
5755
|
+
// parity. Scope is deliberately narrow — bare identifiers only — so
|
|
5756
|
+
// member exprs, calls, ternaries, template literals, and
|
|
5757
|
+
// concrete-typed props (which are never nil) are unaffected and
|
|
5758
|
+
// still emit `attr=""` / `attr="0"` exactly as Hono does.
|
|
5759
|
+
const bareId = value.expr.trim()
|
|
5760
|
+
// Normalize a props-object access (`props.id`) to its bare prop name
|
|
5761
|
+
// (`id`) so the nillable set — which is keyed by bare prop name —
|
|
5762
|
+
// matches the SolidJS-style props-object pattern too, not just
|
|
5763
|
+
// destructured params (#checkbox `id={props.id}`).
|
|
5764
|
+
const propName =
|
|
5765
|
+
this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`)
|
|
5766
|
+
? bareId.slice(this.propsObjectName.length + 1)
|
|
5767
|
+
: bareId
|
|
5768
|
+
if (/^[A-Za-z_$][\w$]*$/.test(propName) && this.nillablePropNames.has(propName)) {
|
|
5769
|
+
const field = `.${this.capitalizeFieldName(propName)}`
|
|
5770
|
+
return `{{if ne ${field} nil}}${name}="{{${this.convertExpressionToGo(value.expr)}}}"{{end}}`
|
|
5771
|
+
}
|
|
4889
5772
|
return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`
|
|
4890
5773
|
},
|
|
4891
5774
|
emitBooleanAttr: (_value, name) => name,
|