@barefootjs/go-template 0.17.1 → 0.18.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/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +134 -14
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +228 -82
- package/dist/adapter/lib/go-naming.d.ts +23 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts +5 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/adapter/value/parsed-literal-to-go.d.ts +13 -4
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
- package/dist/build.js +228 -82
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +247 -82
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +388 -145
- package/src/__tests__/lowering-plugin.test.ts +56 -0
- package/src/adapter/expr/url-builder.ts +14 -3
- package/src/adapter/go-template-adapter.ts +511 -109
- package/src/adapter/lib/go-naming.ts +29 -0
- package/src/adapter/type/type-codegen.ts +22 -3
- package/src/adapter/value/parsed-literal-to-go.ts +82 -9
- package/src/conformance-pins.ts +135 -0
- package/src/index.ts +1 -0
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
IRAsync,
|
|
32
32
|
IRMetadata,
|
|
33
33
|
TemplatePrimitiveRegistry,
|
|
34
|
+
LoopBindingPathSegment,
|
|
34
35
|
} from '@barefootjs/jsx'
|
|
35
36
|
import {
|
|
36
37
|
BaseAdapter,
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
emitAttrValue,
|
|
60
61
|
augmentInheritedPropAccesses,
|
|
61
62
|
collectContextConsumers,
|
|
62
|
-
|
|
63
|
+
isLowerableLoopDestructure,
|
|
63
64
|
type ContextConsumer,
|
|
64
65
|
collectModuleStringConsts as collectModuleStringConstsShared,
|
|
65
66
|
prepareLoweringMatchers,
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
GO_KEYWORDS,
|
|
75
76
|
capitalize,
|
|
76
77
|
capitalizeFieldName,
|
|
78
|
+
goFieldNameForKey,
|
|
77
79
|
slotIdToFieldSuffix,
|
|
78
80
|
loopKeyToGoFieldPath,
|
|
79
81
|
} from "./lib/go-naming.ts"
|
|
@@ -116,6 +118,7 @@ import {
|
|
|
116
118
|
jsLiteralToGo,
|
|
117
119
|
objectLiteralToGoMap,
|
|
118
120
|
} from "./value/value-lowering.ts"
|
|
121
|
+
import { parsedLiteralToGo } from "./value/parsed-literal-to-go.ts"
|
|
119
122
|
import { typeInfoToGo } from "./type/type-codegen.ts"
|
|
120
123
|
import { isBooleanMemo, isListFilterMemo, isStringTernaryMemo } from "./memo/memo-type.ts"
|
|
121
124
|
import { lowerCtorExpr } from "./memo/ctor-lowering.ts"
|
|
@@ -228,11 +231,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
228
231
|
* wrapper's synthetic scalar field) instead of `.`. Innermost last.
|
|
229
232
|
*/
|
|
230
233
|
private loopScalarItemStack: boolean[] = []
|
|
234
|
+
/**
|
|
235
|
+
* Per-loop: true when the loop body IS a single component
|
|
236
|
+
* (`loop.childComponent`), i.e. the range iterates the `.{Name}s` wrapper
|
|
237
|
+
* slice and `.` inside the body is a wrapper struct that embeds the child's
|
|
238
|
+
* Props. False for a component merely nested inside an element item
|
|
239
|
+
* (`<li><Badge/></li>`, #2130), where `.` is the raw datum and the child's
|
|
240
|
+
* props live on the PARENT's once-per-slot instance (`$.{Name}SlotN`).
|
|
241
|
+
* Innermost last.
|
|
242
|
+
*/
|
|
243
|
+
private loopWrapperStack: boolean[] = []
|
|
231
244
|
private loopVarRefCount: Map<string, number> = new Map()
|
|
232
245
|
/** Stack of destructure-param binding maps (binding name → Go accessor on the
|
|
233
|
-
* range var, e.g. `id` → `$__bf_item0.Id`, `rest` → `$__bf_item0
|
|
234
|
-
*
|
|
246
|
+
* range var, e.g. `id` → `$__bf_item0.Id`, `rest` → `$__bf_item0`, an
|
|
247
|
+
* array-rest → `(bf_slice $__bf_item0 1)`, a nested/index path →
|
|
248
|
+
* `(index $__bf_item0.Cells 0)`). Innermost last. Lets `.map(({ id, ...rest
|
|
249
|
+
* })` / `.map(([k, v]) =>` / nested-path destructure resolve instead of
|
|
250
|
+
* BF104 (#2087 Phase B — see `buildDestructureBindingMap`). */
|
|
235
251
|
private loopBindingStack: Array<Map<string, string>> = []
|
|
252
|
+
/**
|
|
253
|
+
* Stack of object-rest exclude-key maps, parallel to `loopBindingStack`
|
|
254
|
+
* (same push/pop points, innermost last). Only object-rest bindings appear
|
|
255
|
+
* here — keyed by binding name, each entry carries the PARENT accessor
|
|
256
|
+
* (same value as `loopBindingStack`'s entry for that name) plus the sibling
|
|
257
|
+
* keys the destructure pattern already pulled out. `emitSpread` consults
|
|
258
|
+
* this for a `{...rest}` spread onto an intrinsic element, so the residual
|
|
259
|
+
* omits exactly the keys the pattern destructured — member reads
|
|
260
|
+
* (`rest.flag`) don't need it and keep resolving through `loopBindingStack`
|
|
261
|
+
* alone (#2087 Phase B).
|
|
262
|
+
*/
|
|
263
|
+
private loopRestExcludeStack: Array<Map<string, { parent: string; excludeKeys: string[] }>> = []
|
|
236
264
|
|
|
237
265
|
/**
|
|
238
266
|
* Cross-component child shapes, keyed by child component name. Populated via
|
|
@@ -475,9 +503,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
475
503
|
|
|
476
504
|
/**
|
|
477
505
|
* Register a child component's shape (see `childComponentShapes`). Call once
|
|
478
|
-
* per known child IR before the parent's `generateTypes`. Idempotent.
|
|
506
|
+
* per known child IR before the parent's `generateTypes`. Idempotent. Takes
|
|
507
|
+
* only the IR's `metadata` so orchestrators that never build the full IR
|
|
508
|
+
* (the CLI's cross-file shape pre-pass, #2131) can register from a bare
|
|
509
|
+
* analyzer pass — a full `ComponentIR` still satisfies it structurally.
|
|
479
510
|
*/
|
|
480
|
-
registerChildComponentShape(ir: ComponentIR): void {
|
|
511
|
+
registerChildComponentShape(ir: Pick<ComponentIR, 'metadata'>): void {
|
|
481
512
|
const name = ir.metadata.componentName
|
|
482
513
|
if (!name) return
|
|
483
514
|
const paramNames = new Set((ir.metadata.propsParams ?? []).map(p => p.name))
|
|
@@ -507,10 +538,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
507
538
|
return capitalizeFieldName(c.localName)
|
|
508
539
|
}
|
|
509
540
|
|
|
541
|
+
/**
|
|
542
|
+
* True when `node` is (or is a member access rooted in) a `useContext`
|
|
543
|
+
* local with an object-shaped `createContext` default — see `member()`'s
|
|
544
|
+
* `bf_get` branch. Recurses through non-computed member chains: once a
|
|
545
|
+
* chain is rooted in a map, every further `.property` down the chain reads
|
|
546
|
+
* off an `interface{}` value with no static struct to fall back to, so it
|
|
547
|
+
* stays map-rooted for `bf_get` too.
|
|
548
|
+
*/
|
|
549
|
+
private isMapRootedContextChain(node: ParsedExpr): boolean {
|
|
550
|
+
if (node.kind === 'identifier') {
|
|
551
|
+
return this.state.contextConsumers.some(
|
|
552
|
+
c => c.localName === node.name && c.defaultKind === 'object',
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
if (node.kind === 'member' && !node.computed) {
|
|
556
|
+
return this.isMapRootedContextChain(node.object)
|
|
557
|
+
}
|
|
558
|
+
return false
|
|
559
|
+
}
|
|
560
|
+
|
|
510
561
|
/** Go type for a context-consumer field, from its `createContext` default's type. */
|
|
511
562
|
private contextConsumerGoType(c: ContextConsumer): string {
|
|
512
563
|
if (typeof c.defaultValue === 'number') return 'int'
|
|
513
564
|
if (typeof c.defaultValue === 'boolean') return 'bool'
|
|
565
|
+
// An OBJECT-shaped default (`createContext<{ config: X }>({ config: {} })`,
|
|
566
|
+
// #2087) needs a real map type, not the scalar `string` fallback below: a
|
|
567
|
+
// descendant's `ctx.config.label` read lowers through `bf_get` (see
|
|
568
|
+
// `member()`), which needs an actual `map[string]interface{}` receiver to
|
|
569
|
+
// walk — a `string` field crashes real `go run` execution with `can't
|
|
570
|
+
// evaluate field Config in type string`.
|
|
571
|
+
if (c.defaultKind === 'object') return 'map[string]interface{}'
|
|
514
572
|
return 'string'
|
|
515
573
|
}
|
|
516
574
|
|
|
@@ -519,6 +577,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
519
577
|
if (typeof c.defaultValue === 'number') return String(c.defaultValue)
|
|
520
578
|
if (typeof c.defaultValue === 'boolean') return String(c.defaultValue)
|
|
521
579
|
if (typeof c.defaultValue === 'string') return `"${escapeGoString(c.defaultValue)}"`
|
|
580
|
+
// Object-shaped default: a real (nil-safe) empty map — see
|
|
581
|
+
// `contextConsumerGoType`. The `createContext` argument's actual nested
|
|
582
|
+
// shape (`{ config: {} }`) is never baked key-for-key here: a consumer
|
|
583
|
+
// only ever reads this DEFAULT when no enclosing Provider ran, and
|
|
584
|
+
// `bf_get` (getFieldValue) already returns nil safely for any missing/
|
|
585
|
+
// absent key off an empty map, so `ctx.config.label ?? 'none'` resolves
|
|
586
|
+
// to `'none'` regardless of how deep the default's own nesting goes.
|
|
587
|
+
if (c.defaultKind === 'object') return 'map[string]interface{}{}'
|
|
522
588
|
return '""'
|
|
523
589
|
}
|
|
524
590
|
|
|
@@ -591,16 +657,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
591
657
|
* analyzer's structured properties (no definition-string parsing). Both the
|
|
592
658
|
* struct emitter and the object-literal baker consume it, so a baked literal
|
|
593
659
|
* can't name a field the struct lacks. A non-Go-identifier source key
|
|
594
|
-
* (`"data-
|
|
595
|
-
*
|
|
660
|
+
* (`"data-priority"`, a numeric key) still gets a field via
|
|
661
|
+
* `goFieldNameForKey`'s segment-splitting PascalCase (#2087 Phase B —
|
|
662
|
+
* `rest-destructure-object-spread-in-map`'s `'data-priority'` residual key
|
|
663
|
+
* needs a real field to bake into, or the whole literal defers to nil); a
|
|
664
|
+
* dedup guard drops a later key that sanitizes to a Go name already taken
|
|
665
|
+
* (rare, but two fields can't share one Go identifier).
|
|
596
666
|
*/
|
|
597
667
|
private structFieldsFor(td: TypeDefinition): Array<{ tsName: string; goName: string; goType: string }> {
|
|
598
668
|
const fields: Array<{ tsName: string; goName: string; goType: string }> = []
|
|
669
|
+
const seenGoNames = new Set<string>()
|
|
599
670
|
for (const prop of td.properties ?? []) {
|
|
600
|
-
|
|
671
|
+
const goName = goFieldNameForKey(prop.name)
|
|
672
|
+
if (seenGoNames.has(goName)) continue
|
|
673
|
+
seenGoNames.add(goName)
|
|
601
674
|
fields.push({
|
|
602
675
|
tsName: prop.name,
|
|
603
|
-
goName
|
|
676
|
+
goName,
|
|
604
677
|
goType: typeInfoToGo(this.emitCtx, prop.type),
|
|
605
678
|
})
|
|
606
679
|
}
|
|
@@ -928,10 +1001,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
928
1001
|
}
|
|
929
1002
|
|
|
930
1003
|
/** Collect static child instances from loop body children for the wrapper struct. */
|
|
931
|
-
private collectBodyChildInstances(
|
|
1004
|
+
private collectBodyChildInstances(
|
|
1005
|
+
bodyChildren: IRNode[],
|
|
1006
|
+
propsParams: ReadonlyArray<{ name: string }> = [],
|
|
1007
|
+
): StaticChildInstance[] {
|
|
932
1008
|
const result: StaticChildInstance[] = []
|
|
933
1009
|
for (const child of bodyChildren) {
|
|
934
|
-
this.collectStaticChildInstancesRecursive(child, result, false, new Map())
|
|
1010
|
+
this.collectStaticChildInstancesRecursive(child, result, false, new Map(), propsParams)
|
|
935
1011
|
}
|
|
936
1012
|
return result
|
|
937
1013
|
}
|
|
@@ -1184,8 +1260,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1184
1260
|
for (const c of this.nonCollidingContextConsumers(takenInit)) {
|
|
1185
1261
|
const field = this.contextFieldName(c)
|
|
1186
1262
|
const def = this.contextConsumerGoDefault(c)
|
|
1263
|
+
// A `map[string]interface{}` field's Go zero value (nil) is already a
|
|
1264
|
+
// safely-readable "no value" (`bf_get` / `getFieldValue` treats a nil
|
|
1265
|
+
// map exactly like an empty one, per `reflect.Value.MapIndex`'s
|
|
1266
|
+
// documented nil-map behavior) — no `applyGoFallback` wrapper needed,
|
|
1267
|
+
// same as the other zero-value sentinels below.
|
|
1187
1268
|
const defaulted =
|
|
1188
|
-
c.defaultValue === null ||
|
|
1269
|
+
c.defaultValue === null ||
|
|
1270
|
+
def === '""' ||
|
|
1271
|
+
def === '0' ||
|
|
1272
|
+
def === 'false' ||
|
|
1273
|
+
def === 'map[string]interface{}{}'
|
|
1189
1274
|
? `in.${field}`
|
|
1190
1275
|
: applyGoFallback(`in.${field}`, def)
|
|
1191
1276
|
lines.push(`\t\t${field}: ${defaulted},`)
|
|
@@ -1200,7 +1285,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1200
1285
|
}
|
|
1201
1286
|
|
|
1202
1287
|
private emitStaticChildInstances(lines: string[], ir: ComponentIR): void {
|
|
1203
|
-
const staticChildren = this.collectStaticChildInstances(ir.root)
|
|
1288
|
+
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
1204
1289
|
for (const child of staticChildren) {
|
|
1205
1290
|
lines.push(`\t\t${child.fieldName}: New${child.name}Props(${child.name}Input{`)
|
|
1206
1291
|
lines.push(`\t\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
@@ -1407,7 +1492,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1407
1492
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1408
1493
|
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
1409
1494
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
1410
|
-
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren
|
|
1495
|
+
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!, ir.metadata.propsParams)
|
|
1411
1496
|
|
|
1412
1497
|
for (const child of bodyChildInstances) {
|
|
1413
1498
|
const childVar = `child_${child.fieldName}`
|
|
@@ -1501,7 +1586,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1501
1586
|
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
1502
1587
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1503
1588
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
1504
|
-
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren
|
|
1589
|
+
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!, ir.metadata.propsParams)
|
|
1505
1590
|
|
|
1506
1591
|
// Child sub-component instances created once (identical scope IDs per row).
|
|
1507
1592
|
for (const child of bodyChildInstances) {
|
|
@@ -1826,7 +1911,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1826
1911
|
}
|
|
1827
1912
|
}
|
|
1828
1913
|
|
|
1829
|
-
const staticChildren = this.collectStaticChildInstances(ir.root)
|
|
1914
|
+
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
1830
1915
|
for (const child of staticChildren) {
|
|
1831
1916
|
lines.push(`\t${child.fieldName} ${child.name}Props \`json:"-"\``)
|
|
1832
1917
|
}
|
|
@@ -1850,9 +1935,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1850
1935
|
* components inside loops (handled by nestedComponents). Each instance carries
|
|
1851
1936
|
* its component `name`, `slotId`, `props`, and Go `fieldName`.
|
|
1852
1937
|
*/
|
|
1853
|
-
private collectStaticChildInstances(
|
|
1938
|
+
private collectStaticChildInstances(
|
|
1939
|
+
node: IRNode,
|
|
1940
|
+
propsParams: ReadonlyArray<{ name: string }> = [],
|
|
1941
|
+
): Array<StaticChildInstance> {
|
|
1854
1942
|
const result: StaticChildInstance[] = []
|
|
1855
|
-
this.collectStaticChildInstancesRecursive(node, result, false, new Map())
|
|
1943
|
+
this.collectStaticChildInstancesRecursive(node, result, false, new Map(), propsParams)
|
|
1856
1944
|
return result
|
|
1857
1945
|
}
|
|
1858
1946
|
|
|
@@ -1925,6 +2013,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1925
2013
|
result: StaticChildInstance[],
|
|
1926
2014
|
inLoop: boolean,
|
|
1927
2015
|
providerCtx: ReadonlyMap<string, string>,
|
|
2016
|
+
propsParams: ReadonlyArray<{ name: string }> = [],
|
|
1928
2017
|
): void {
|
|
1929
2018
|
if (node.type === 'component') {
|
|
1930
2019
|
const comp = node as IRComponent
|
|
@@ -1934,7 +2023,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1934
2023
|
// child components nested inside still get their slot fields.
|
|
1935
2024
|
if (comp.dynamicTag) {
|
|
1936
2025
|
for (const child of comp.children) {
|
|
1937
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2026
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1938
2027
|
}
|
|
1939
2028
|
return
|
|
1940
2029
|
}
|
|
@@ -1966,86 +2055,208 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1966
2055
|
// never contain components (any nested component renders a `{{template}}`
|
|
1967
2056
|
// action, which the bake extractors reject), so recursing is a no-op.
|
|
1968
2057
|
for (const child of effectiveChildren) {
|
|
1969
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2058
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1970
2059
|
}
|
|
1971
2060
|
}
|
|
1972
2061
|
// Recurse into Portal's children to find nested components
|
|
1973
2062
|
if (comp.name === 'Portal' && comp.children) {
|
|
1974
2063
|
for (const child of comp.children) {
|
|
1975
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2064
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1976
2065
|
}
|
|
1977
2066
|
}
|
|
1978
2067
|
} else if (node.type === 'loop') {
|
|
1979
2068
|
const loop = node as IRLoop
|
|
1980
|
-
//
|
|
2069
|
+
// A loop whose body IS a single component is the nestedComponents /
|
|
2070
|
+
// wrapper-slice machinery's case — mark its subtree as in-loop so the
|
|
2071
|
+
// component (and its body children, which live on the wrapper struct)
|
|
2072
|
+
// are skipped here. A component merely nested inside an element item
|
|
2073
|
+
// (`<li><Badge/></li>`, #2130) gets NO wrapper slice: keep collecting,
|
|
2074
|
+
// so it registers a normal once-per-slot instance (shared across rows,
|
|
2075
|
+
// like the wrapper's `bodyChildInstances`); per-item content reaches it
|
|
2076
|
+
// through the loop-body children define (see `renderComponent`).
|
|
1981
2077
|
for (const child of loop.children) {
|
|
1982
|
-
this.collectStaticChildInstancesRecursive(
|
|
2078
|
+
this.collectStaticChildInstancesRecursive(
|
|
2079
|
+
child,
|
|
2080
|
+
result,
|
|
2081
|
+
inLoop || !!loop.childComponent,
|
|
2082
|
+
providerCtx,
|
|
2083
|
+
propsParams,
|
|
2084
|
+
)
|
|
1983
2085
|
}
|
|
1984
2086
|
} else if (node.type === 'element') {
|
|
1985
2087
|
const element = node as IRElement
|
|
1986
2088
|
for (const child of element.children) {
|
|
1987
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2089
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1988
2090
|
}
|
|
1989
2091
|
} else if (node.type === 'fragment') {
|
|
1990
2092
|
const fragment = node as IRFragment
|
|
1991
2093
|
for (const child of fragment.children) {
|
|
1992
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2094
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1993
2095
|
}
|
|
1994
2096
|
} else if (node.type === 'conditional') {
|
|
1995
2097
|
const cond = node as IRConditional
|
|
1996
|
-
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop, providerCtx)
|
|
2098
|
+
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop, providerCtx, propsParams)
|
|
1997
2099
|
if (cond.whenFalse) {
|
|
1998
|
-
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx)
|
|
2100
|
+
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx, propsParams)
|
|
1999
2101
|
}
|
|
2000
2102
|
} else if (node.type === 'if-statement') {
|
|
2001
2103
|
// An early-return if-statement root (e.g. an asChild split) keeps its
|
|
2002
2104
|
// subtrees in consequent/alternate — the non-asChild branch's nested icon
|
|
2003
2105
|
// needs its slot field like any other static child.
|
|
2004
2106
|
const stmt = node as IRIfStatement
|
|
2005
|
-
this.collectStaticChildInstancesRecursive(stmt.consequent, result, inLoop, providerCtx)
|
|
2107
|
+
this.collectStaticChildInstancesRecursive(stmt.consequent, result, inLoop, providerCtx, propsParams)
|
|
2006
2108
|
if (stmt.alternate) {
|
|
2007
|
-
this.collectStaticChildInstancesRecursive(stmt.alternate, result, inLoop, providerCtx)
|
|
2109
|
+
this.collectStaticChildInstancesRecursive(stmt.alternate, result, inLoop, providerCtx, propsParams)
|
|
2008
2110
|
}
|
|
2009
2111
|
} else if (node.type === 'provider') {
|
|
2010
2112
|
// SSR context propagation: record the provider's value against its context
|
|
2011
2113
|
// name and extend the active binding map for descendants. A literal value
|
|
2012
|
-
// lowers to a Go literal;
|
|
2013
|
-
//
|
|
2114
|
+
// lowers to a Go literal; an object-literal value lowers to a Go map when
|
|
2115
|
+
// every member is a supported shape (#2087 — see `extendProviderContext`);
|
|
2116
|
+
// anything else is left unbound (the consumer keeps its default).
|
|
2014
2117
|
const p = node as IRProvider
|
|
2015
|
-
const childCtx = this.extendProviderContext(providerCtx, p)
|
|
2118
|
+
const childCtx = this.extendProviderContext(providerCtx, p, propsParams)
|
|
2016
2119
|
for (const child of p.children) {
|
|
2017
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, childCtx)
|
|
2120
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, childCtx, propsParams)
|
|
2018
2121
|
}
|
|
2019
2122
|
} else if (node.type === 'async') {
|
|
2020
2123
|
// Async fallback + children render server-side via the OOS
|
|
2021
2124
|
// protocol; static child components inside them still need slot
|
|
2022
2125
|
// fields on the parent struct.
|
|
2023
2126
|
const a = node as IRAsync
|
|
2024
|
-
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop, providerCtx)
|
|
2127
|
+
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop, providerCtx, propsParams)
|
|
2025
2128
|
for (const child of a.children) {
|
|
2026
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2129
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
2027
2130
|
}
|
|
2028
2131
|
}
|
|
2029
2132
|
}
|
|
2030
2133
|
|
|
2031
2134
|
/**
|
|
2032
2135
|
* Extend the active provider-context map with one `<Ctx.Provider value>`. A
|
|
2033
|
-
* string/number/boolean literal value is lowered to a Go literal
|
|
2034
|
-
*
|
|
2136
|
+
* string/number/boolean literal value is lowered to a Go literal. An
|
|
2137
|
+
* OBJECT-LITERAL value (#2087's chart shape — `value={{ config: props.config
|
|
2138
|
+
* ?? {} }}`) lowers to a `map[string]interface{}` Go expression via
|
|
2139
|
+
* `providerObjectValueToGoMap` when every member is a supported shape; any
|
|
2140
|
+
* other shape (including a partially-supported object literal) is skipped —
|
|
2141
|
+
* the descendant consumer keeps its `createContext` default, unchanged from
|
|
2142
|
+
* before this method understood objects at all.
|
|
2035
2143
|
*/
|
|
2036
2144
|
private extendProviderContext(
|
|
2037
2145
|
current: ReadonlyMap<string, string>,
|
|
2038
2146
|
p: IRProvider,
|
|
2147
|
+
propsParams: ReadonlyArray<{ name: string }>,
|
|
2039
2148
|
): ReadonlyMap<string, string> {
|
|
2040
|
-
const v = p.valueProp?.value as
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
if (
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2149
|
+
const v = p.valueProp?.value as
|
|
2150
|
+
| { kind?: string; value?: unknown; parsed?: ParsedExpr }
|
|
2151
|
+
| undefined
|
|
2152
|
+
if (!v) return current
|
|
2153
|
+
if (v.kind === 'literal') {
|
|
2154
|
+
let goLit: string | null = null
|
|
2155
|
+
if (typeof v.value === 'string') goLit = `"${escapeGoString(v.value)}"`
|
|
2156
|
+
else if (typeof v.value === 'number' || typeof v.value === 'boolean') goLit = String(v.value)
|
|
2157
|
+
if (goLit === null) return current
|
|
2158
|
+
const next = new Map(current)
|
|
2159
|
+
next.set(p.contextName, goLit)
|
|
2160
|
+
return next
|
|
2161
|
+
}
|
|
2162
|
+
if (v.kind === 'expression' && v.parsed) {
|
|
2163
|
+
const goMap = this.providerObjectValueToGoMap(v.parsed, propsParams)
|
|
2164
|
+
if (goMap !== null) {
|
|
2165
|
+
const next = new Map(current)
|
|
2166
|
+
next.set(p.contextName, goMap)
|
|
2167
|
+
return next
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
return current
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Lower a `<Ctx.Provider value={{ … }}>` object literal to a Go
|
|
2175
|
+
* `map[string]interface{}{…}` expression, for binding into a static child's
|
|
2176
|
+
* context-consumer field (`emitStaticChildInstances`). Keys keep their
|
|
2177
|
+
* SOURCE (JS-cased) names — the consumer reads them back via `bf_get`
|
|
2178
|
+
* (`member()`), which is case-tolerant (`getFieldValue`, bf.go), so casing
|
|
2179
|
+
* doesn't have to match a capitalized Go field.
|
|
2180
|
+
*
|
|
2181
|
+
* Every member must lower through `lowerProviderMapMemberValue` for the
|
|
2182
|
+
* WHOLE object to lower — a single unsupported member (a getter, a
|
|
2183
|
+
* function, an expression outside the narrow surface below) fails the
|
|
2184
|
+
* whole value and returns `null`, exactly like the pre-#2087 refusal (the
|
|
2185
|
+
* consumer then keeps its `createContext` default).
|
|
2186
|
+
*/
|
|
2187
|
+
private providerObjectValueToGoMap(
|
|
2188
|
+
parsed: ParsedExpr,
|
|
2189
|
+
propsParams: ReadonlyArray<{ name: string }>,
|
|
2190
|
+
): string | null {
|
|
2191
|
+
if (parsed.kind !== 'object-literal') return null
|
|
2192
|
+
const entries: string[] = []
|
|
2193
|
+
for (const prop of parsed.properties) {
|
|
2194
|
+
if (prop.shorthand) return null
|
|
2195
|
+
const goVal = this.lowerProviderMapMemberValue(prop.value, propsParams)
|
|
2196
|
+
if (goVal === null) return null
|
|
2197
|
+
entries.push(`${JSON.stringify(prop.key)}: ${goVal}`)
|
|
2198
|
+
}
|
|
2199
|
+
if (entries.length === 0) return null
|
|
2200
|
+
return `map[string]interface{}{${entries.join(', ')}}`
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/**
|
|
2204
|
+
* Lower one member VALUE of a provider object literal to a Go expression.
|
|
2205
|
+
* Supports:
|
|
2206
|
+
* - a pure literal / nested literal array-or-object (same machinery
|
|
2207
|
+
* `objectLiteralToGoMap` uses for an inline object passed to a child's
|
|
2208
|
+
* optional object prop);
|
|
2209
|
+
* - `props.<X> ?? {}` (#2087's exact chart shape) — an optional prop with
|
|
2210
|
+
* an empty-object fallback. The referenced prop's OWN Go field is
|
|
2211
|
+
* `interface{}` (a `Record<string, X>` type-alias prop never becomes a
|
|
2212
|
+
* named Go struct — see `typeInfoToGo`'s `interface` case — so it can't
|
|
2213
|
+
* be typed `map[string]interface{}` without risking an unrelated
|
|
2214
|
+
* `ui/compat.lock.json` diff across every other `interface{}`-typed
|
|
2215
|
+
* prop). Recovering the caller-supplied map goes through the runtime's
|
|
2216
|
+
* `bf.AsMap` normalizer rather than a bare
|
|
2217
|
+
* `.(map[string]interface{})` type assertion: an `interface{}` field
|
|
2218
|
+
* can legally hold ANY string-keyed map kind — a Go handler modelling
|
|
2219
|
+
* `Record<string, string>` naturally passes `map[string]string` — and
|
|
2220
|
+
* the single-type assertion would silently drop those values (#2111
|
|
2221
|
+
* review). `bf.AsMap` returns nil for nil / typed-nil / non-map
|
|
2222
|
+
* values, so the emitted fallback still lands on a real, empty map,
|
|
2223
|
+
* matching JS `??`'s "assign the real value when present, else `{}`"
|
|
2224
|
+
* semantics.
|
|
2225
|
+
* Anything else (a getter/callback member, an unresolvable expression)
|
|
2226
|
+
* returns `null`, failing the WHOLE containing object (see
|
|
2227
|
+
* `providerObjectValueToGoMap`).
|
|
2228
|
+
*/
|
|
2229
|
+
private lowerProviderMapMemberValue(
|
|
2230
|
+
node: ParsedExpr,
|
|
2231
|
+
propsParams: ReadonlyArray<{ name: string }>,
|
|
2232
|
+
): string | null {
|
|
2233
|
+
if (node.kind === 'object-literal') return objectLiteralToGoMap(this.emitCtx, node)
|
|
2234
|
+
const literal = parsedLiteralToGo(this.emitCtx, node)
|
|
2235
|
+
if (literal !== null) return literal
|
|
2236
|
+
if (
|
|
2237
|
+
node.kind === 'logical' &&
|
|
2238
|
+
node.op === '??' &&
|
|
2239
|
+
node.right.kind === 'object-literal' &&
|
|
2240
|
+
node.right.properties.length === 0 &&
|
|
2241
|
+
node.left.kind === 'member' &&
|
|
2242
|
+
!node.left.computed &&
|
|
2243
|
+
node.left.object.kind === 'identifier' &&
|
|
2244
|
+
node.left.object.name === this.state.propsObjectName
|
|
2245
|
+
) {
|
|
2246
|
+
// Bound to a local so the narrowed `member` shape survives into the
|
|
2247
|
+
// closure below — TS drops property-path narrowing (`node.left.kind
|
|
2248
|
+
// === 'member'`) across a nested arrow function boundary.
|
|
2249
|
+
const propName = node.left.property
|
|
2250
|
+
if (propsParams.some(param => param.name === propName)) {
|
|
2251
|
+
const fieldRef = `in.${capitalizeFieldName(propName)}`
|
|
2252
|
+
return (
|
|
2253
|
+
`func() map[string]interface{} { ` +
|
|
2254
|
+
`if m := bf.AsMap(${fieldRef}); m != nil { return m }; ` +
|
|
2255
|
+
`return map[string]interface{}{} }()`
|
|
2256
|
+
)
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
return null
|
|
2049
2260
|
}
|
|
2050
2261
|
|
|
2051
2262
|
/**
|
|
@@ -2846,10 +3057,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2846
3057
|
if (objHO && (objHO.method === 'find' || objHO.method === 'findLast')) {
|
|
2847
3058
|
const findResult = this.renderHigherOrderExpr(objHO, emit)
|
|
2848
3059
|
if (findResult) {
|
|
2849
|
-
return `{{with ${findResult}}}{{.${
|
|
3060
|
+
return `{{with ${findResult}}}{{.${goFieldNameForKey(property)}}}{{end}}`
|
|
2850
3061
|
}
|
|
2851
3062
|
const templateBlock = this.renderFindTemplateBlock(
|
|
2852
|
-
objHO, emit,
|
|
3063
|
+
objHO, emit, goFieldNameForKey(property),
|
|
2853
3064
|
)
|
|
2854
3065
|
if (templateBlock) return templateBlock
|
|
2855
3066
|
}
|
|
@@ -2860,6 +3071,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2860
3071
|
return this.rootFieldRef(property)
|
|
2861
3072
|
}
|
|
2862
3073
|
|
|
3074
|
+
// A member chain rooted in a `useContext` local whose `createContext`
|
|
3075
|
+
// default is object-shaped (#2087 — `defaultKind === 'object'`) reads off
|
|
3076
|
+
// a `map[string]interface{}` field, not a struct: plain Go template dot
|
|
3077
|
+
// access (`.Ctx.Config`) does an EXACT-string `MapIndex`, so it would
|
|
3078
|
+
// only resolve a capitalized key the provider never bakes (see
|
|
3079
|
+
// `providerObjectValueToGoMap`, which keeps SOURCE/JS-cased keys). Route
|
|
3080
|
+
// through `bf_get` (the runtime's case-tolerant `getFieldValue`, bf.go)
|
|
3081
|
+
// instead, recursively — once a chain is map-rooted every further
|
|
3082
|
+
// `.property` is ALSO opaque (the map's value type is `interface{}`, so
|
|
3083
|
+
// there's no static struct to fall back to), hence checking the object
|
|
3084
|
+
// rather than just the top-level identifier.
|
|
3085
|
+
if (this.isMapRootedContextChain(object)) {
|
|
3086
|
+
const objGo = emit(object)
|
|
3087
|
+
return `bf_get ${wrapIfMultiToken(objGo)} ${JSON.stringify(property)}`
|
|
3088
|
+
}
|
|
3089
|
+
|
|
2863
3090
|
// Static property access on a module object-literal const
|
|
2864
3091
|
// (`variantClasses.ghost` in a class template literal) resolves at compile
|
|
2865
3092
|
// time — same lookup as the bracket-index form in
|
|
@@ -2874,14 +3101,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2874
3101
|
|
|
2875
3102
|
// Inside a loop, the loop param variable refers to the current item
|
|
2876
3103
|
// (dot). e.g. `msg.role` inside `{{range $_, $msg := .Messages}}` → `.Role`
|
|
3104
|
+
//
|
|
3105
|
+
// Field names route through `goFieldNameForKey` — the same sanitizer the
|
|
3106
|
+
// struct/map bake side uses — NOT bare `capitalizeFieldName`: a
|
|
3107
|
+
// non-identifier property (`meta["data-x"]`, parsed as computed member
|
|
3108
|
+
// access) would otherwise emit `.Data-x`, which is not even valid Go
|
|
3109
|
+
// template syntax, let alone a reachable field (PR #2089 review). For
|
|
3110
|
+
// identifier keys (snake_case included) the two functions agree, so
|
|
3111
|
+
// nothing previously reachable changes shape.
|
|
2877
3112
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
2878
3113
|
if (object.kind === 'identifier' && currentLoopParam && object.name === currentLoopParam) {
|
|
2879
|
-
return `.${
|
|
3114
|
+
return `.${goFieldNameForKey(property)}`
|
|
2880
3115
|
}
|
|
2881
3116
|
|
|
2882
3117
|
const obj = emit(object)
|
|
2883
3118
|
if (property === 'length') return `len ${obj}`
|
|
2884
|
-
return `${obj}.${
|
|
3119
|
+
return `${obj}.${goFieldNameForKey(property)}`
|
|
2885
3120
|
}
|
|
2886
3121
|
|
|
2887
3122
|
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
@@ -3028,11 +3263,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3028
3263
|
}
|
|
3029
3264
|
|
|
3030
3265
|
objectLiteral(_properties: ObjectLiteralProperty[], raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
3031
|
-
//
|
|
3032
|
-
//
|
|
3033
|
-
//
|
|
3266
|
+
// The shared `isSupported` gate now admits an EMPTY object literal
|
|
3267
|
+
// (`?? {}`) as `??`'s right operand (expression-parser.ts, `logical`
|
|
3268
|
+
// case) — every other adapter has a native `{}` dict/hashref literal to
|
|
3269
|
+
// emit here, but `text/template` has none, so this dispatcher is the
|
|
3270
|
+
// one place left to refuse the shape. Unlike the sibling adapters, Go
|
|
3271
|
+
// can't silently fall back to a safe sentinel text: `this.unsupported`'s
|
|
3272
|
+
// `[UNSUPPORTED: …]` marker would be spliced into a Go template action
|
|
3273
|
+
// (e.g. as an `or`/`and` operand) and break template parsing, and the
|
|
3274
|
+
// shared gate no longer reports BF101 for this shape itself (it now
|
|
3275
|
+
// considers `x ?? {}` supported). So self-report BF101 here — mirroring
|
|
3276
|
+
// the other self-contained refusals in this file (`pushCallbackBF101`,
|
|
3277
|
+
// `refuseFilterExprNode`) — and return the safe `""` string sentinel,
|
|
3278
|
+
// which is always a valid Go template value in every position `??`'s
|
|
3279
|
+
// result can land in. Object values that DO reach a Go map today
|
|
3280
|
+
// (signal/const inits, spread bags) go through the dedicated
|
|
3034
3281
|
// `objectLiteralToGoMap` lowering, not here.
|
|
3035
|
-
|
|
3282
|
+
// `raw` is the whole top-level expression's source text (threaded
|
|
3283
|
+
// unchanged through `convertNode`, same convention every `unsupported`
|
|
3284
|
+
// node relies on for its diagnostic) — e.g. `props.config ?? {}`, not
|
|
3285
|
+
// just the `{}` sub-node — so it reads naturally in the message below.
|
|
3286
|
+
this.state.errors.push({
|
|
3287
|
+
code: 'BF101',
|
|
3288
|
+
severity: 'error',
|
|
3289
|
+
message: `Expression not supported: ${raw}`,
|
|
3290
|
+
loc: this.makeLoc(),
|
|
3291
|
+
suggestion: {
|
|
3292
|
+
message: `Go templates have no object/map literal syntax, so the \`?? {}\` fallback can't render server-side. ${GO_REMEDIATION_OPTIONS}`,
|
|
3293
|
+
},
|
|
3294
|
+
})
|
|
3295
|
+
return `""`
|
|
3036
3296
|
}
|
|
3037
3297
|
|
|
3038
3298
|
/** Set of predicate (boolean-callback) higher-order methods. */
|
|
@@ -3328,7 +3588,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3328
3588
|
}
|
|
3329
3589
|
}
|
|
3330
3590
|
|
|
3331
|
-
flatMethod(
|
|
3591
|
+
flatMethod(
|
|
3592
|
+
object: ParsedExpr,
|
|
3593
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
3594
|
+
emit: (e: ParsedExpr) => string,
|
|
3595
|
+
): string {
|
|
3596
|
+
if (typeof depth === 'object') {
|
|
3597
|
+
// Dynamic depth (#2094) → `bf_flat_dynamic <recv> <renderedDepthExpr>`,
|
|
3598
|
+
// a SEPARATE runtime helper from `bf_flat` below. `bf_flat`'s `-1`
|
|
3599
|
+
// argument is a compile-time SENTINEL meaning "flatten fully"
|
|
3600
|
+
// (`Infinity` normalised at parse time); a genuinely dynamic render-time
|
|
3601
|
+
// value of `-1` means the JS-correct OPPOSITE (no flatten — negative
|
|
3602
|
+
// depth never recurses). Reusing `bf_flat` for both would silently
|
|
3603
|
+
// invert that case, so the dynamic form routes through `FlatDynamicDepth`
|
|
3604
|
+
// (`bf.go`), which performs full `ToIntegerOrInfinity` coercion from
|
|
3605
|
+
// scratch on whatever value the rendered expression evaluates to.
|
|
3606
|
+
return `bf_flat_dynamic ${wrapIfMultiToken(emit(object))} ${wrapIfMultiToken(emit(depth.expr))}`
|
|
3607
|
+
}
|
|
3332
3608
|
// `.flat(depth?)` → `bf_flat <recv> <depth>`. `Infinity` lowers to the `-1`
|
|
3333
3609
|
// sentinel (flatten fully); a finite depth flattens that many levels
|
|
3334
3610
|
// (`0` = shallow copy).
|
|
@@ -4414,20 +4690,78 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4414
4690
|
}
|
|
4415
4691
|
|
|
4416
4692
|
/**
|
|
4417
|
-
*
|
|
4418
|
-
*
|
|
4419
|
-
*
|
|
4693
|
+
* Walk a destructure binding's structured `segments` path (#2087 Phase A) into
|
|
4694
|
+
* a Go accessor over `base`. A `field` step appends `.<GoFieldName>` — dot
|
|
4695
|
+
* access resolves against a struct field OR a map key (Go template's
|
|
4696
|
+
* `evalField` supports both), and `goFieldNameForKey` produces a valid Go
|
|
4697
|
+
* identifier regardless of whether the SOURCE key was one (`cells` → `Cells`,
|
|
4698
|
+
* `data-priority` → `DataPriority`) — Go dot-syntax only requires the EMITTED
|
|
4699
|
+
* name to be a legal identifier, not the original JS key. An `index` step
|
|
4700
|
+
* has no dot-notation form, so it wraps in Go's `index` builtin
|
|
4701
|
+
* (`(index <acc> N)`); the wrap also parenthesises the whole accessor so a
|
|
4702
|
+
* following step or an outer composition (`len (...)`) can't swallow it as
|
|
4703
|
+
* extra call arguments.
|
|
4704
|
+
*/
|
|
4705
|
+
private buildSegmentAccessor(base: string, segments: readonly LoopBindingPathSegment[]): string {
|
|
4706
|
+
let acc = base
|
|
4707
|
+
for (const seg of segments) {
|
|
4708
|
+
acc = seg.kind === 'field' ? `${acc}.${goFieldNameForKey(seg.key)}` : `(index ${acc} ${seg.index})`
|
|
4709
|
+
}
|
|
4710
|
+
return acc
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
/**
|
|
4714
|
+
* Map each destructure binding to its Go accessor on the range var (#2087
|
|
4715
|
+
* Phase B — `isLowerableLoopDestructure` admits every shape below):
|
|
4716
|
+
*
|
|
4717
|
+
* - fixed binding (any depth): the FULL `segments` path over the range var
|
|
4718
|
+
* (`id` → `$item.Id`, `head` in `cells: [head]` → `(index $item.Cells 0)`,
|
|
4719
|
+
* `k` in `[k, v]` → `(index $item 0)`).
|
|
4720
|
+
* - array-rest (`[first, ...tail]`): the PARENT `segments` prefix wrapped in
|
|
4721
|
+
* `bf_slice` (`tail` → `(bf_slice $item 1)`) — this IS the binding's whole
|
|
4722
|
+
* value, so it composes correctly both bare and under `.length` (`len
|
|
4723
|
+
* (bf_slice $item 1)`, via the `member()` emitter's generic `len <obj>`
|
|
4724
|
+
* arm).
|
|
4725
|
+
* - object-rest (`{ id, ...rest }`): the bare PARENT accessor, so a member
|
|
4726
|
+
* read (`rest.flag`) renders `$item.Flag` — the simplest lowering that
|
|
4727
|
+
* keeps the already-green `rest-destructure-object-in-map` fixture byte-
|
|
4728
|
+
* exact. A `{...rest}` SPREAD needs the residual (item minus the
|
|
4729
|
+
* destructured siblings), which this map alone can't express; that case
|
|
4730
|
+
* is tracked separately in the returned `restExcludes` map and consumed
|
|
4731
|
+
* by `emitSpread`'s `bf_omit` lowering, not through this binding value.
|
|
4420
4732
|
*/
|
|
4421
|
-
private buildDestructureBindingMap(
|
|
4422
|
-
|
|
4733
|
+
private buildDestructureBindingMap(
|
|
4734
|
+
loop: IRLoop,
|
|
4735
|
+
rangeVar: string,
|
|
4736
|
+
): {
|
|
4737
|
+
bindings: Map<string, string>
|
|
4738
|
+
restExcludes: Map<string, { parent: string; excludeKeys: string[] }>
|
|
4739
|
+
} {
|
|
4740
|
+
const bindings = new Map<string, string>()
|
|
4741
|
+
const restExcludes = new Map<string, { parent: string; excludeKeys: string[] }>()
|
|
4742
|
+
const base = `$${rangeVar}`
|
|
4423
4743
|
for (const b of loop.paramBindings ?? []) {
|
|
4424
|
-
|
|
4425
|
-
|
|
4744
|
+
const parent = this.buildSegmentAccessor(base, b.segments ?? [])
|
|
4745
|
+
if (!b.rest) {
|
|
4746
|
+
bindings.set(b.name, parent)
|
|
4747
|
+
} else if (b.rest.kind === 'array') {
|
|
4748
|
+
bindings.set(b.name, `(bf_slice ${parent} ${b.rest.from})`)
|
|
4426
4749
|
} else {
|
|
4427
|
-
|
|
4750
|
+
bindings.set(b.name, parent)
|
|
4751
|
+
restExcludes.set(b.name, { parent, excludeKeys: b.rest.exclude.map(k => k.key) })
|
|
4428
4752
|
}
|
|
4429
4753
|
}
|
|
4430
|
-
return
|
|
4754
|
+
return { bindings, restExcludes }
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
/** Innermost-first lookup mirroring `loopBindingStack`'s search in
|
|
4758
|
+
* `identifier()`, but over the object-rest exclude-key side table. */
|
|
4759
|
+
private lookupRestExclude(name: string): { parent: string; excludeKeys: string[] } | undefined {
|
|
4760
|
+
for (let i = this.loopRestExcludeStack.length - 1; i >= 0; i--) {
|
|
4761
|
+
const info = this.loopRestExcludeStack[i].get(name)
|
|
4762
|
+
if (info) return info
|
|
4763
|
+
}
|
|
4764
|
+
return undefined
|
|
4431
4765
|
}
|
|
4432
4766
|
|
|
4433
4767
|
renderLoop(loop: IRLoop): string {
|
|
@@ -4446,15 +4780,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4446
4780
|
//
|
|
4447
4781
|
// Check the IR's structured `paramBindings` field rather than string-matching
|
|
4448
4782
|
// `loop.param`: Phase 1 populates it iff the param is a destructure pattern,
|
|
4449
|
-
// and the structured check is robust to formatting variants.
|
|
4450
|
-
//
|
|
4451
|
-
//
|
|
4452
|
-
//
|
|
4453
|
-
// `rest.flag`
|
|
4454
|
-
//
|
|
4455
|
-
//
|
|
4783
|
+
// and the structured check is robust to formatting variants. `isLowerableLoopDestructure`
|
|
4784
|
+
// (#2087 Phase A/B) admits fixed bindings at ANY depth (`.field`, array-index,
|
|
4785
|
+
// nested combinations of either — `buildSegmentAccessor` walks the structured
|
|
4786
|
+
// `segments` path), array-rest (`[a, ...t]` → `bf_slice`), and object-rest
|
|
4787
|
+
// whose every use is a member read (`rest.flag`) or a `{...rest}` spread onto
|
|
4788
|
+
// an intrinsic element (→ `bf_omit`, see `emitSpread`). Still refused (BF104):
|
|
4789
|
+
// a bare-value object-rest use (`String(rest)`, `{rest}`, spread onto a
|
|
4790
|
+
// component), and a `.filter().map(destructure)` chain — those need either the
|
|
4791
|
+
// actual residual object or a filter-param retarget this lowering doesn't do.
|
|
4456
4792
|
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0)
|
|
4457
|
-
const supportableDestructure = destructure &&
|
|
4793
|
+
const supportableDestructure = destructure && isLowerableLoopDestructure(loop)
|
|
4458
4794
|
if (destructure && !supportableDestructure) {
|
|
4459
4795
|
this.state.errors.push({
|
|
4460
4796
|
code: 'BF104',
|
|
@@ -4471,6 +4807,42 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4471
4807
|
})
|
|
4472
4808
|
}
|
|
4473
4809
|
|
|
4810
|
+
// A loop array that's a bare reference to a FUNCTION-SCOPE local const with
|
|
4811
|
+
// a computed initializer (`const entries = Object.entries(props.x ??
|
|
4812
|
+
// {}).filter(...)`) can't be bound as a Go template value: module-scope
|
|
4813
|
+
// consts (`isModule`) are a different, already-working case (statically
|
|
4814
|
+
// evaluated and seeded into the render context elsewhere), but a
|
|
4815
|
+
// function-scope local only reaches the template at all via
|
|
4816
|
+
// `computeDerivedConstFields`'s STRING-typed derived-const lowering
|
|
4817
|
+
// (`isStringExpr`) — an array-typed initializer like this one never
|
|
4818
|
+
// qualifies, so `identifier()` would still emit `.Entries` (the naive
|
|
4819
|
+
// `rootFieldRef` fallback) while `generateTypes` emits no such field.
|
|
4820
|
+
// `html/template` resolves struct fields dynamically at EXECUTE time, not
|
|
4821
|
+
// at Go compile time, so left unchecked this fails loudly only when the
|
|
4822
|
+
// template actually runs (`can't evaluate field Entries in type
|
|
4823
|
+
// ...Props`) instead of at build time. Pre-existing, general limitation,
|
|
4824
|
+
// orthogonal to #2087's destructure-binding work — newly reachable in this
|
|
4825
|
+
// adapter's test corpus only because the widened destructure gate (#2087
|
|
4826
|
+
// Phase A/B) no longer refuses `static-array-from-props`'s `([emoji,
|
|
4827
|
+
// users]) => ...` param first. Cross-adapter policy: Jinja / ERB apply the
|
|
4828
|
+
// same narrow check in their own `renderLoop` (see `jinja-adapter.ts`).
|
|
4829
|
+
const arrayName = loop.array.trim()
|
|
4830
|
+
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
4831
|
+
const arrayConst = this.state.localConstants.find(c => c.name === arrayName)
|
|
4832
|
+
if (arrayConst && !arrayConst.isModule && arrayConst.parsed && !this.isStringExpr(arrayConst.parsed, new Set())) {
|
|
4833
|
+
this.state.errors.push({
|
|
4834
|
+
code: 'BF101',
|
|
4835
|
+
severity: 'error',
|
|
4836
|
+
message: `Loop array \`${arrayName}\` is a local computed value (\`${arrayConst.value}\`) that the Go template adapter cannot bind as a template variable — only a string-derived local resolves to a generated struct field.`,
|
|
4837
|
+
loc: loop.loc ?? this.makeLoc(),
|
|
4838
|
+
suggestion: {
|
|
4839
|
+
message:
|
|
4840
|
+
'Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client.',
|
|
4841
|
+
},
|
|
4842
|
+
})
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4474
4846
|
let goArray = this.convertExpressionToGo(loop.array)
|
|
4475
4847
|
const param = loop.param
|
|
4476
4848
|
let index = loop.index || '_'
|
|
@@ -4491,11 +4863,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4491
4863
|
rangeValue = '_'
|
|
4492
4864
|
}
|
|
4493
4865
|
|
|
4494
|
-
// If the loop
|
|
4495
|
-
// which carries a ScopeID per item (TodoItem → `.TodoItems`).
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4866
|
+
// If the loop body IS a single component, range over `.{ComponentName}s`,
|
|
4867
|
+
// which carries a ScopeID per item (TodoItem → `.TodoItems`). Gate on the
|
|
4868
|
+
// IR's `loop.childComponent` — the SAME condition `findNestedComponents`
|
|
4869
|
+
// uses to generate that slice field — not on a deep search of the body:
|
|
4870
|
+
// a component merely nested inside an element item (`<li><Badge/></li>`)
|
|
4871
|
+
// generates NO `.Badges` slice, so retargeting the range at it 500s at
|
|
4872
|
+
// render with `can't evaluate field Badges in type *XxxProps` (#2130).
|
|
4873
|
+
// Such loops keep iterating the real collection; the nested child renders
|
|
4874
|
+
// through the parent's once-per-slot instance (see `renderComponent`'s
|
|
4875
|
+
// non-wrapper in-loop branch).
|
|
4876
|
+
if (loop.childComponent) {
|
|
4877
|
+
goArray = `.${loop.childComponent.name}s`
|
|
4499
4878
|
}
|
|
4500
4879
|
|
|
4501
4880
|
this.inLoop = true
|
|
@@ -4513,7 +4892,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4513
4892
|
if (supportableDestructure) {
|
|
4514
4893
|
// Bindings resolve against the synthetic `$__bf_item` range var; don't push
|
|
4515
4894
|
// a loop param (the param is a pattern, not a name).
|
|
4516
|
-
this.
|
|
4895
|
+
const built = this.buildDestructureBindingMap(loop, rangeValue)
|
|
4896
|
+
this.loopBindingStack.push(built.bindings)
|
|
4897
|
+
this.loopRestExcludeStack.push(built.restExcludes)
|
|
4517
4898
|
pushedBindingMap = true
|
|
4518
4899
|
this.loopParamStack.push('')
|
|
4519
4900
|
if (rangeIndex !== '_') {
|
|
@@ -4538,7 +4919,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4538
4919
|
this.loopScalarItemStack.push(
|
|
4539
4920
|
this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null,
|
|
4540
4921
|
)
|
|
4922
|
+
this.loopWrapperStack.push(!!loop.childComponent)
|
|
4541
4923
|
const children = this.renderChildren(loop.children)
|
|
4924
|
+
this.loopWrapperStack.pop()
|
|
4542
4925
|
this.loopScalarItemStack.pop()
|
|
4543
4926
|
// Build the per-item anchor marker while the loop param is still on the
|
|
4544
4927
|
// stack, so a `bodyIsItemConditional` key expression resolves against the
|
|
@@ -4551,7 +4934,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4551
4934
|
else this.loopVarRefCount.set(v, rc)
|
|
4552
4935
|
}
|
|
4553
4936
|
this.loopParamStack.pop()
|
|
4554
|
-
if (pushedBindingMap)
|
|
4937
|
+
if (pushedBindingMap) {
|
|
4938
|
+
this.loopBindingStack.pop()
|
|
4939
|
+
this.loopRestExcludeStack.pop()
|
|
4940
|
+
}
|
|
4555
4941
|
this.inLoop = false
|
|
4556
4942
|
|
|
4557
4943
|
// Apply sort if present: wrap the array in a sort pipeline before `range`.
|
|
@@ -4607,24 +4993,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4607
4993
|
return ''
|
|
4608
4994
|
}
|
|
4609
4995
|
|
|
4610
|
-
/** Find the first component child in a list of nodes. */
|
|
4611
|
-
private findChildComponent(nodes: IRNode[]): IRComponent | null {
|
|
4612
|
-
for (const node of nodes) {
|
|
4613
|
-
if (node.type === 'component') {
|
|
4614
|
-
return node as IRComponent
|
|
4615
|
-
}
|
|
4616
|
-
if (node.type === 'element' && (node as IRElement).children) {
|
|
4617
|
-
const found = this.findChildComponent((node as IRElement).children)
|
|
4618
|
-
if (found) return found
|
|
4619
|
-
}
|
|
4620
|
-
if (node.type === 'fragment' && (node as IRFragment).children) {
|
|
4621
|
-
const found = this.findChildComponent((node as IRFragment).children)
|
|
4622
|
-
if (found) return found
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
return null
|
|
4626
|
-
}
|
|
4627
|
-
|
|
4628
4996
|
/**
|
|
4629
4997
|
* When `comp`'s JSX children contain template actions (nested components,
|
|
4630
4998
|
* dynamic text) — i.e. none of the static bake paths in
|
|
@@ -4698,13 +5066,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4698
5066
|
|
|
4699
5067
|
// In Go templates, components are rendered via {{template "name" data}}.
|
|
4700
5068
|
let templateCall: string
|
|
4701
|
-
if (this.inLoop) {
|
|
4702
|
-
//
|
|
4703
|
-
//
|
|
4704
|
-
//
|
|
4705
|
-
//
|
|
4706
|
-
//
|
|
4707
|
-
//
|
|
5069
|
+
if (this.inLoop && (this.loopWrapperStack[this.loopWrapperStack.length - 1] ?? false)) {
|
|
5070
|
+
// Wrapper-slice loop (body IS this component): `.` is the wrapper struct
|
|
5071
|
+
// embedding the child's Props. Loop body component with JSX children:
|
|
5072
|
+
// render children through a companion define so `bf_with_children`
|
|
5073
|
+
// injects them at template execution time. Temporarily exit loop context
|
|
5074
|
+
// so nested component calls (e.g. TableCell inside TableRow) use the
|
|
5075
|
+
// normal non-loop rendering path (`.TableCellSlotN` fields on the
|
|
5076
|
+
// wrapper struct), while the loop param stack stays intact so datum
|
|
5077
|
+
// references resolve.
|
|
4708
5078
|
const loopBodyDefine = this.queueLoopBodyChildrenDefine(comp)
|
|
4709
5079
|
if (loopBodyDefine) {
|
|
4710
5080
|
// Scalar-item loop: feed the body define the wrapper's `.BfLoopItem` (the
|
|
@@ -4717,6 +5087,24 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4717
5087
|
} else {
|
|
4718
5088
|
templateCall = `{{template "${comp.name}" .}}`
|
|
4719
5089
|
}
|
|
5090
|
+
} else if (this.inLoop && comp.slotId) {
|
|
5091
|
+
// Non-wrapper loop (component nested inside an element item, #2130):
|
|
5092
|
+
// the range iterates the REAL collection, so `.` is the raw datum and
|
|
5093
|
+
// carries none of the child's props. Call through the parent's
|
|
5094
|
+
// once-per-slot instance via the root context (`$` — the define's own
|
|
5095
|
+
// data, i.e. the parent's Props), injecting per-item content through a
|
|
5096
|
+
// loop-body children define executed with the datum (`.`). The shared
|
|
5097
|
+
// instance means identical child scope IDs across rows — the same
|
|
5098
|
+
// contract the wrapper machinery's `bodyChildInstances` already uses.
|
|
5099
|
+
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
5100
|
+
const loopBodyDefine = this.queueLoopBodyChildrenDefine(comp)
|
|
5101
|
+
templateCall = loopBodyDefine
|
|
5102
|
+
? `{{template "${comp.name}" (bf_with_children $.${comp.name}${suffix} (bf_tmpl "${loopBodyDefine}" .))}}`
|
|
5103
|
+
: `{{template "${comp.name}" $.${comp.name}${suffix}}}`
|
|
5104
|
+
} else if (this.inLoop) {
|
|
5105
|
+
// Loop-nested component without a slotId: no parent field to route
|
|
5106
|
+
// through — legacy passthrough of the current dot.
|
|
5107
|
+
templateCall = `{{template "${comp.name}" .}}`
|
|
4720
5108
|
} else if (comp.slotId) {
|
|
4721
5109
|
// Static children with slotId: unique field name based on slotId.
|
|
4722
5110
|
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
@@ -4896,16 +5284,30 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4896
5284
|
// Property access through the loop param (`t.attrs`) is already handled
|
|
4897
5285
|
// by the member-expression path that returns `.Attrs`.
|
|
4898
5286
|
//
|
|
4899
|
-
//
|
|
4900
|
-
//
|
|
4901
|
-
//
|
|
4902
|
-
// `
|
|
4903
|
-
//
|
|
5287
|
+
// A per-item spread whose operand is a plain object/array field read
|
|
5288
|
+
// (not a destructure-rest binding) is otherwise gated on whether the
|
|
5289
|
+
// enclosing signal's literal initial value bakes at all — see
|
|
5290
|
+
// `parsed-literal-to-go.ts`'s nested object/array property support
|
|
5291
|
+
// (#2087), which lifted the flat-object-only restriction for the
|
|
5292
|
+
// destructure-residual fixtures below.
|
|
4904
5293
|
const trimmed = value.expr.trim()
|
|
4905
5294
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
4906
5295
|
if (currentLoopParam && trimmed === currentLoopParam) {
|
|
4907
5296
|
return `{{bf_spread_attrs .}}`
|
|
4908
5297
|
}
|
|
5298
|
+
// Destructure object-rest spread onto an element (`{...rest}`, #2087
|
|
5299
|
+
// Phase B): `rest` alone would spread the WHOLE item (every field,
|
|
5300
|
+
// including the ones the pattern already destructured out as `id` /
|
|
5301
|
+
// `title`) — route through `bf_omit` instead so the residual excludes
|
|
5302
|
+
// exactly those sibling keys. Only a spread whose expr is the BARE
|
|
5303
|
+
// rest name matches (`isLowerableLoopDestructure`'s admitted shape);
|
|
5304
|
+
// anything else (`{...fn(rest)}`) already refused at the IR gate.
|
|
5305
|
+
const restInfo = this.lookupRestExclude(trimmed)
|
|
5306
|
+
if (restInfo) {
|
|
5307
|
+
const excludeArgs = restInfo.excludeKeys.map(k => JSON.stringify(k)).join(' ')
|
|
5308
|
+
const omitArgs = excludeArgs ? `${restInfo.parent} ${excludeArgs}` : restInfo.parent
|
|
5309
|
+
return `{{bf_spread_attrs (bf_omit ${omitArgs})}}`
|
|
5310
|
+
}
|
|
4909
5311
|
const goExpr = this.convertExpressionToGo(value.expr)
|
|
4910
5312
|
// `convertExpressionToGo` already pushes BF101 for unsupported
|
|
4911
5313
|
// expressions and returns `""`; pass through so the template still
|