@barefootjs/go-template 0.8.0 → 0.9.1
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 +142 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +402 -27
- package/dist/build.js +402 -27
- package/dist/index.js +402 -27
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +189 -29
- package/src/adapter/go-template-adapter.ts +689 -25
- 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,25 @@ 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
|
+
|
|
437
496
|
/**
|
|
438
497
|
* Set of prop NAMES whose resolved Go struct-field type is exactly
|
|
439
498
|
* `interface{}` — i.e. nillable. Populated at `generate()` entry from
|
|
@@ -470,6 +529,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
470
529
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
471
530
|
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
472
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)
|
|
473
539
|
this.nillablePropNames = this.collectNillablePropNames(ir)
|
|
474
540
|
|
|
475
541
|
// Surface loop-body usages of components imported from sibling
|
|
@@ -762,8 +828,70 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
762
828
|
return `{{if .Scripts}}${registrations.join('')}{{end}}\n`
|
|
763
829
|
}
|
|
764
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
|
+
|
|
765
879
|
generateTypes(ir: ComponentIR): string | null {
|
|
766
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)
|
|
767
895
|
const lines: string[] = []
|
|
768
896
|
|
|
769
897
|
const componentName = ir.metadata.componentName
|
|
@@ -856,6 +984,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
856
984
|
header.push(`package ${this.options.packageName}`)
|
|
857
985
|
header.push('')
|
|
858
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"')
|
|
859
990
|
if (this.usesHtmlTemplate) header.push('\t"html/template"')
|
|
860
991
|
header.push('\t"math/rand"')
|
|
861
992
|
header.push('')
|
|
@@ -1155,6 +1286,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1155
1286
|
lines.push(`\t${nested.name}s []${nested.name}Input`)
|
|
1156
1287
|
}
|
|
1157
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
|
+
|
|
1158
1296
|
// (#1407 follow-up) Input-side bag field for restPropsName spreads.
|
|
1159
1297
|
// The destructured-rest pattern
|
|
1160
1298
|
// (`function({a, ...rest}: P) { <el {...rest}/> }`) surfaces
|
|
@@ -1287,6 +1425,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1287
1425
|
lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
|
|
1288
1426
|
}
|
|
1289
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
|
+
|
|
1290
1440
|
// Add array fields for nested components (for template rendering)
|
|
1291
1441
|
for (const nested of nestedComponents) {
|
|
1292
1442
|
if (nested.isDynamic && !nested.isPropDerived) {
|
|
@@ -1466,12 +1616,34 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1466
1616
|
}
|
|
1467
1617
|
|
|
1468
1618
|
// Add memo initial values (computed from signal initial values)
|
|
1619
|
+
const memoPropsParamMap = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
1469
1620
|
for (const memo of ir.metadata.memos) {
|
|
1470
1621
|
const fieldName = this.capitalizeFieldName(memo.name)
|
|
1471
|
-
|
|
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)
|
|
1472
1627
|
lines.push(`\t\t${fieldName}: ${memoValue},`)
|
|
1473
1628
|
}
|
|
1474
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
|
+
|
|
1475
1647
|
// Add static child component instances
|
|
1476
1648
|
const staticChildren = this.collectStaticChildInstances(ir.root)
|
|
1477
1649
|
for (const child of staticChildren) {
|
|
@@ -1481,15 +1653,49 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1481
1653
|
// own NewProps (BfParent/BfMount fields).
|
|
1482
1654
|
lines.push(`\t\t\tBfParent: scopeID,`)
|
|
1483
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
|
+
}
|
|
1484
1690
|
// Add prop values
|
|
1485
1691
|
for (const prop of child.props) {
|
|
1486
1692
|
switch (prop.value.kind) {
|
|
1487
1693
|
case 'literal':
|
|
1488
|
-
|
|
1694
|
+
emitChildField(prop.name, this.goLiteral(prop.value.value))
|
|
1489
1695
|
break
|
|
1490
1696
|
case 'boolean-shorthand':
|
|
1491
1697
|
case 'boolean-attr':
|
|
1492
|
-
|
|
1698
|
+
emitChildField(prop.name, 'true')
|
|
1493
1699
|
break
|
|
1494
1700
|
case 'expression':
|
|
1495
1701
|
case 'spread':
|
|
@@ -1508,7 +1714,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1508
1714
|
const goExpr = this.templatePartsToGoCode(parts, ir.metadata.propsParams)
|
|
1509
1715
|
if (goExpr !== null) {
|
|
1510
1716
|
// Parts path succeeded — emit and move on.
|
|
1511
|
-
|
|
1717
|
+
emitChildField(prop.name, goExpr)
|
|
1512
1718
|
break
|
|
1513
1719
|
}
|
|
1514
1720
|
// Parts exist but templatePartsToGoCode opted out (unsupported
|
|
@@ -1526,7 +1732,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1526
1732
|
ir.metadata.propsParams
|
|
1527
1733
|
)
|
|
1528
1734
|
if (resolvedValue !== null) {
|
|
1529
|
-
|
|
1735
|
+
emitChildField(prop.name, resolvedValue)
|
|
1530
1736
|
}
|
|
1531
1737
|
break
|
|
1532
1738
|
}
|
|
@@ -1535,6 +1741,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1535
1741
|
break
|
|
1536
1742
|
}
|
|
1537
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
|
+
}
|
|
1538
1752
|
// Pass through JSX children as the child slot's `Children` input.
|
|
1539
1753
|
// Two paths:
|
|
1540
1754
|
// 1. Plain text (`<Button>+1</Button>`) → quote with JSON.stringify
|
|
@@ -1646,7 +1860,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1646
1860
|
*/
|
|
1647
1861
|
private collectStaticChildInstances(node: IRNode): Array<StaticChildInstance> {
|
|
1648
1862
|
const result: StaticChildInstance[] = []
|
|
1649
|
-
this.collectStaticChildInstancesRecursive(node, result, false)
|
|
1863
|
+
this.collectStaticChildInstancesRecursive(node, result, false, new Map())
|
|
1650
1864
|
return result
|
|
1651
1865
|
}
|
|
1652
1866
|
|
|
@@ -1687,7 +1901,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1687
1901
|
private collectStaticChildInstancesRecursive(
|
|
1688
1902
|
node: IRNode,
|
|
1689
1903
|
result: StaticChildInstance[],
|
|
1690
|
-
inLoop: boolean
|
|
1904
|
+
inLoop: boolean,
|
|
1905
|
+
providerCtx: ReadonlyMap<string, string>,
|
|
1691
1906
|
): void {
|
|
1692
1907
|
if (node.type === 'component') {
|
|
1693
1908
|
const comp = node as IRComponent
|
|
@@ -1697,7 +1912,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1697
1912
|
// child components nested inside still get their slot fields.
|
|
1698
1913
|
if (comp.dynamicTag) {
|
|
1699
1914
|
for (const child of comp.children) {
|
|
1700
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1915
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1701
1916
|
}
|
|
1702
1917
|
return
|
|
1703
1918
|
}
|
|
@@ -1712,57 +1927,79 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1712
1927
|
fieldName: `${comp.name}${suffix}`,
|
|
1713
1928
|
childrenText: this.extractTextChildren(comp.children),
|
|
1714
1929
|
childrenHtml: this.extractHtmlChildren(comp.children),
|
|
1930
|
+
contextBindings: providerCtx.size > 0 ? providerCtx : undefined,
|
|
1715
1931
|
})
|
|
1716
1932
|
}
|
|
1717
1933
|
// Recurse into Portal's children to find nested components
|
|
1718
1934
|
if (comp.name === 'Portal' && comp.children) {
|
|
1719
1935
|
for (const child of comp.children) {
|
|
1720
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1936
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1721
1937
|
}
|
|
1722
1938
|
}
|
|
1723
1939
|
} else if (node.type === 'loop') {
|
|
1724
1940
|
const loop = node as IRLoop
|
|
1725
1941
|
// Mark children as inside loop
|
|
1726
1942
|
for (const child of loop.children) {
|
|
1727
|
-
this.collectStaticChildInstancesRecursive(child, result, true)
|
|
1943
|
+
this.collectStaticChildInstancesRecursive(child, result, true, providerCtx)
|
|
1728
1944
|
}
|
|
1729
1945
|
} else if (node.type === 'element') {
|
|
1730
1946
|
const element = node as IRElement
|
|
1731
1947
|
for (const child of element.children) {
|
|
1732
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1948
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1733
1949
|
}
|
|
1734
1950
|
} else if (node.type === 'fragment') {
|
|
1735
1951
|
const fragment = node as IRFragment
|
|
1736
1952
|
for (const child of fragment.children) {
|
|
1737
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1953
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1738
1954
|
}
|
|
1739
1955
|
} else if (node.type === 'conditional') {
|
|
1740
1956
|
const cond = node as IRConditional
|
|
1741
|
-
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop)
|
|
1957
|
+
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop, providerCtx)
|
|
1742
1958
|
if (cond.whenFalse) {
|
|
1743
|
-
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop)
|
|
1959
|
+
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx)
|
|
1744
1960
|
}
|
|
1745
1961
|
} else if (node.type === 'provider') {
|
|
1746
|
-
//
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
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)
|
|
1750
1966
|
const p = node as IRProvider
|
|
1967
|
+
const childCtx = this.extendProviderContext(providerCtx, p)
|
|
1751
1968
|
for (const child of p.children) {
|
|
1752
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1969
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, childCtx)
|
|
1753
1970
|
}
|
|
1754
1971
|
} else if (node.type === 'async') {
|
|
1755
1972
|
// Async fallback + children render server-side via the OOS
|
|
1756
1973
|
// protocol; static child components inside them still need slot
|
|
1757
1974
|
// fields on the parent struct.
|
|
1758
1975
|
const a = node as IRAsync
|
|
1759
|
-
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop)
|
|
1976
|
+
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop, providerCtx)
|
|
1760
1977
|
for (const child of a.children) {
|
|
1761
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1978
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
1762
1979
|
}
|
|
1763
1980
|
}
|
|
1764
1981
|
}
|
|
1765
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
|
+
|
|
1766
2003
|
/**
|
|
1767
2004
|
* Collect top-level (non-loop) JSX intrinsic-element spread slots
|
|
1768
2005
|
* from the IR (#1407). Loop-internal spreads are skipped — they
|
|
@@ -2029,6 +2266,30 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2029
2266
|
if (ir.metadata.restPropsName === trimmed) {
|
|
2030
2267
|
return `in.${this.capitalizeFieldName(trimmed)}`
|
|
2031
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
|
+
}
|
|
2032
2293
|
}
|
|
2033
2294
|
return null
|
|
2034
2295
|
}
|
|
@@ -2172,13 +2433,52 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2172
2433
|
if (!param) return null
|
|
2173
2434
|
goVal = `in.${this.capitalizeFieldName(param.name)}`
|
|
2174
2435
|
} else {
|
|
2175
|
-
|
|
2436
|
+
const indexed = this.recordIndexAccessToGoMap(val, ir)
|
|
2437
|
+
if (indexed === null) return null
|
|
2438
|
+
goVal = indexed
|
|
2176
2439
|
}
|
|
2177
2440
|
entries.push(`${JSON.stringify(key)}: ${goVal}`)
|
|
2178
2441
|
}
|
|
2179
2442
|
return `map[string]any{${entries.join(', ')}}`
|
|
2180
2443
|
}
|
|
2181
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
|
+
|
|
2182
2482
|
/**
|
|
2183
2483
|
* Convert JavaScript initial value to Go value for NewXxxProps function.
|
|
2184
2484
|
* References to props params are converted to in.FieldName format.
|
|
@@ -2588,11 +2888,232 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2588
2888
|
* referenced prop, substitute it for `in.FieldName` so the memo
|
|
2589
2889
|
* inherits the signal-time `??` fallback.
|
|
2590
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
|
+
|
|
2591
3108
|
private computeMemoInitialValue(
|
|
2592
3109
|
memo: { name: string; computation: string; deps: string[] },
|
|
2593
3110
|
signals: { getter: string; initialValue: string }[],
|
|
2594
3111
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
2595
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,
|
|
2596
3117
|
): string {
|
|
2597
3118
|
const computation = memo.computation
|
|
2598
3119
|
// Helper to pick the hoisted var (if any) or fall back to `in.X`.
|
|
@@ -2602,6 +3123,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2602
3123
|
return `in.${this.capitalizeFieldName(propName)}`
|
|
2603
3124
|
}
|
|
2604
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
|
+
|
|
2605
3136
|
// Pattern: () => dep() * N or () => dep() + N etc.
|
|
2606
3137
|
const arithmeticMatch = computation.match(/\(\)\s*=>\s*(\w+)\(\)\s*([*+\-/])\s*(\d+)/)
|
|
2607
3138
|
if (arithmeticMatch) {
|
|
@@ -2680,10 +3211,42 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2680
3211
|
}
|
|
2681
3212
|
}
|
|
2682
3213
|
|
|
2683
|
-
// 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 '""'
|
|
2684
3219
|
return '0'
|
|
2685
3220
|
}
|
|
2686
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
|
+
|
|
2687
3250
|
/**
|
|
2688
3251
|
* Infer the Go type for a memo based on its computation and dependencies.
|
|
2689
3252
|
*/
|
|
@@ -2692,6 +3255,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2692
3255
|
signals: { getter: string; initialValue: string; type: TypeInfo }[],
|
|
2693
3256
|
propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>
|
|
2694
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
|
+
|
|
2695
3263
|
// Check if computation involves multiplication (*) - likely number
|
|
2696
3264
|
if (memo.computation.includes('*') || memo.computation.includes('/') ||
|
|
2697
3265
|
memo.computation.includes('+') || memo.computation.includes('-')) {
|
|
@@ -2719,10 +3287,58 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2719
3287
|
}
|
|
2720
3288
|
}
|
|
2721
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
|
+
|
|
2722
3300
|
// Default to the memo's declared type
|
|
2723
3301
|
return this.typeInfoToGo(memo.type)
|
|
2724
3302
|
}
|
|
2725
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
|
+
|
|
2726
3342
|
/**
|
|
2727
3343
|
* Infer Go type from a JavaScript value literal.
|
|
2728
3344
|
*/
|
|
@@ -3207,9 +3823,49 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3207
3823
|
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
3208
3824
|
return init.text
|
|
3209
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
|
|
3210
3834
|
return null
|
|
3211
3835
|
}
|
|
3212
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
|
+
|
|
3213
3869
|
/**
|
|
3214
3870
|
* Resolve an identifier to its inlined Go string literal when it names a
|
|
3215
3871
|
* module pure-string const. Returns the Go template literal form
|
|
@@ -5101,8 +5757,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5101
5757
|
// concrete-typed props (which are never nil) are unaffected and
|
|
5102
5758
|
// still emit `attr=""` / `attr="0"` exactly as Hono does.
|
|
5103
5759
|
const bareId = value.expr.trim()
|
|
5104
|
-
|
|
5105
|
-
|
|
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)}`
|
|
5106
5770
|
return `{{if ne ${field} nil}}${name}="{{${this.convertExpressionToGo(value.expr)}}}"{{end}}`
|
|
5107
5771
|
}
|
|
5108
5772
|
return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`
|