@barefootjs/go-template 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +144 -14
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +449 -118
- package/dist/adapter/lib/compile-state.d.ts +27 -1
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/go-emit.d.ts +9 -0
- package/dist/adapter/lib/go-emit.d.ts.map +1 -1
- package/dist/adapter/lib/go-naming.d.ts +23 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts +54 -5
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/memo/memo-type.d.ts +10 -0
- package/dist/adapter/memo/memo-type.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 +449 -118
- 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 +468 -118
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +652 -129
- 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 +637 -140
- package/src/adapter/lib/compile-state.ts +31 -0
- package/src/adapter/lib/go-emit.ts +20 -0
- package/src/adapter/lib/go-naming.ts +29 -0
- package/src/adapter/memo/memo-compute.ts +228 -18
- package/src/adapter/memo/memo-type.ts +19 -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
- package/src/test-render.ts +46 -8
|
@@ -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,11 +60,12 @@ import {
|
|
|
59
60
|
emitAttrValue,
|
|
60
61
|
augmentInheritedPropAccesses,
|
|
61
62
|
collectContextConsumers,
|
|
62
|
-
|
|
63
|
+
isLowerableLoopDestructure,
|
|
63
64
|
type ContextConsumer,
|
|
64
65
|
collectModuleStringConsts as collectModuleStringConstsShared,
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
prepareLoweringMatchers,
|
|
67
|
+
envSignalReaderFor,
|
|
68
|
+
computeSsrSeedPlan,
|
|
67
69
|
} from '@barefootjs/jsx'
|
|
68
70
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
69
71
|
import { BF_REGION } from '@barefootjs/shared'
|
|
@@ -73,6 +75,7 @@ import {
|
|
|
73
75
|
GO_KEYWORDS,
|
|
74
76
|
capitalize,
|
|
75
77
|
capitalizeFieldName,
|
|
78
|
+
goFieldNameForKey,
|
|
76
79
|
slotIdToFieldSuffix,
|
|
77
80
|
loopKeyToGoFieldPath,
|
|
78
81
|
} from "./lib/go-naming.ts"
|
|
@@ -85,6 +88,7 @@ import {
|
|
|
85
88
|
emitReduceEval,
|
|
86
89
|
emitPredicateEval,
|
|
87
90
|
emitFlatMapEval,
|
|
91
|
+
emitMapEval,
|
|
88
92
|
stringTolerantEqOperands,
|
|
89
93
|
buildUnsupportedSuggestion,
|
|
90
94
|
GO_REMEDIATION_OPTIONS,
|
|
@@ -114,11 +118,12 @@ import {
|
|
|
114
118
|
jsLiteralToGo,
|
|
115
119
|
objectLiteralToGoMap,
|
|
116
120
|
} from "./value/value-lowering.ts"
|
|
121
|
+
import { parsedLiteralToGo } from "./value/parsed-literal-to-go.ts"
|
|
117
122
|
import { typeInfoToGo } from "./type/type-codegen.ts"
|
|
118
|
-
import { isBooleanMemo, isStringTernaryMemo } from "./memo/memo-type.ts"
|
|
123
|
+
import { isBooleanMemo, isListFilterMemo, isStringTernaryMemo } from "./memo/memo-type.ts"
|
|
119
124
|
import { lowerCtorExpr } from "./memo/ctor-lowering.ts"
|
|
120
125
|
import { resolveBlockBodyMemoModuleConst } from "./memo/memo-value.ts"
|
|
121
|
-
import { computeMemoInitialValue, computeMemoInitialValueOrNull } from "./memo/memo-compute.ts"
|
|
126
|
+
import { computeMemoInitialValue, computeMemoInitialValueOrNull, filterArmEarlierSiblingRefs } from "./memo/memo-compute.ts"
|
|
122
127
|
import { collectSpreadSlots, buildSpreadInitializer } from "./spread/spread-codegen.ts"
|
|
123
128
|
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames } from "./props/prop-types.ts"
|
|
124
129
|
|
|
@@ -226,11 +231,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
226
231
|
* wrapper's synthetic scalar field) instead of `.`. Innermost last.
|
|
227
232
|
*/
|
|
228
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[] = []
|
|
229
244
|
private loopVarRefCount: Map<string, number> = new Map()
|
|
230
245
|
/** Stack of destructure-param binding maps (binding name → Go accessor on the
|
|
231
|
-
* range var, e.g. `id` → `$__bf_item0.Id`, `rest` → `$__bf_item0
|
|
232
|
-
*
|
|
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`). */
|
|
233
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[] }>> = []
|
|
234
264
|
|
|
235
265
|
/**
|
|
236
266
|
* Cross-component child shapes, keyed by child component name. Populated via
|
|
@@ -272,7 +302,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
272
302
|
this.state.currentMemos = ir.metadata.memos ?? []
|
|
273
303
|
this.state.currentTypeDefinitions = ir.metadata.typeDefinitions ?? []
|
|
274
304
|
this.state.contextConsumers = collectContextConsumers(ir.metadata)
|
|
275
|
-
|
|
305
|
+
// Single authority (Package G): the plan already decided which signals are
|
|
306
|
+
// per-request env readers, in declaration order. `ir.metadata.ssrSeedPlan`
|
|
307
|
+
// may be absent for hand-built metadata (tests), hence the fallback to the
|
|
308
|
+
// same shared computation rather than a second, divergent derivation.
|
|
309
|
+
this.state.ssrSeedPlan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata)
|
|
310
|
+
this.state.envSignalReadersByLocal = new Map()
|
|
311
|
+
this.state.searchParamsLocals = new Set()
|
|
312
|
+
for (const step of this.state.ssrSeedPlan.steps) {
|
|
313
|
+
if (step.kind !== 'env-reader') continue
|
|
314
|
+
// `reader.methods` is a ReadonlySet — JSON round-tripping (adapter
|
|
315
|
+
// conformance harness) serializes it to `{}`, so re-resolve a live
|
|
316
|
+
// reader from the registry by key instead of trusting `step.reader`.
|
|
317
|
+
const reader = envSignalReaderFor(step.reader.key)
|
|
318
|
+
if (reader) this.state.envSignalReadersByLocal.set(step.name, reader)
|
|
319
|
+
if (step.reader.key === 'search') this.state.searchParamsLocals.add(step.name)
|
|
320
|
+
}
|
|
276
321
|
this.state.loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
277
322
|
augmentInheritedPropAccesses(ir)
|
|
278
323
|
}
|
|
@@ -458,9 +503,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
458
503
|
|
|
459
504
|
/**
|
|
460
505
|
* Register a child component's shape (see `childComponentShapes`). Call once
|
|
461
|
-
* 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.
|
|
462
510
|
*/
|
|
463
|
-
registerChildComponentShape(ir: ComponentIR): void {
|
|
511
|
+
registerChildComponentShape(ir: Pick<ComponentIR, 'metadata'>): void {
|
|
464
512
|
const name = ir.metadata.componentName
|
|
465
513
|
if (!name) return
|
|
466
514
|
const paramNames = new Set((ir.metadata.propsParams ?? []).map(p => p.name))
|
|
@@ -490,10 +538,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
490
538
|
return capitalizeFieldName(c.localName)
|
|
491
539
|
}
|
|
492
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
|
+
|
|
493
561
|
/** Go type for a context-consumer field, from its `createContext` default's type. */
|
|
494
562
|
private contextConsumerGoType(c: ContextConsumer): string {
|
|
495
563
|
if (typeof c.defaultValue === 'number') return 'int'
|
|
496
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{}'
|
|
497
572
|
return 'string'
|
|
498
573
|
}
|
|
499
574
|
|
|
@@ -502,6 +577,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
502
577
|
if (typeof c.defaultValue === 'number') return String(c.defaultValue)
|
|
503
578
|
if (typeof c.defaultValue === 'boolean') return String(c.defaultValue)
|
|
504
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{}{}'
|
|
505
588
|
return '""'
|
|
506
589
|
}
|
|
507
590
|
|
|
@@ -574,16 +657,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
574
657
|
* analyzer's structured properties (no definition-string parsing). Both the
|
|
575
658
|
* struct emitter and the object-literal baker consume it, so a baked literal
|
|
576
659
|
* can't name a field the struct lacks. A non-Go-identifier source key
|
|
577
|
-
* (`"data-
|
|
578
|
-
*
|
|
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).
|
|
579
666
|
*/
|
|
580
667
|
private structFieldsFor(td: TypeDefinition): Array<{ tsName: string; goName: string; goType: string }> {
|
|
581
668
|
const fields: Array<{ tsName: string; goName: string; goType: string }> = []
|
|
669
|
+
const seenGoNames = new Set<string>()
|
|
582
670
|
for (const prop of td.properties ?? []) {
|
|
583
|
-
|
|
671
|
+
const goName = goFieldNameForKey(prop.name)
|
|
672
|
+
if (seenGoNames.has(goName)) continue
|
|
673
|
+
seenGoNames.add(goName)
|
|
584
674
|
fields.push({
|
|
585
675
|
tsName: prop.name,
|
|
586
|
-
goName
|
|
676
|
+
goName,
|
|
587
677
|
goType: typeInfoToGo(this.emitCtx, prop.type),
|
|
588
678
|
})
|
|
589
679
|
}
|
|
@@ -911,10 +1001,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
911
1001
|
}
|
|
912
1002
|
|
|
913
1003
|
/** Collect static child instances from loop body children for the wrapper struct. */
|
|
914
|
-
private collectBodyChildInstances(
|
|
1004
|
+
private collectBodyChildInstances(
|
|
1005
|
+
bodyChildren: IRNode[],
|
|
1006
|
+
propsParams: ReadonlyArray<{ name: string }> = [],
|
|
1007
|
+
): StaticChildInstance[] {
|
|
915
1008
|
const result: StaticChildInstance[] = []
|
|
916
1009
|
for (const child of bodyChildren) {
|
|
917
|
-
this.collectStaticChildInstancesRecursive(child, result, false, new Map())
|
|
1010
|
+
this.collectStaticChildInstancesRecursive(child, result, false, new Map(), propsParams)
|
|
918
1011
|
}
|
|
919
1012
|
return result
|
|
920
1013
|
}
|
|
@@ -1000,6 +1093,38 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1000
1093
|
|
|
1001
1094
|
this.emitDynamicBodyWrappers(lines, ir, componentName, dynamicWithBody, propFallbackVars, emittedWrapperVars)
|
|
1002
1095
|
|
|
1096
|
+
// Sibling-memo hoisting (#2075/#2077 review finding 3): a filter-arm memo
|
|
1097
|
+
// whose predicate free vars reference an EARLIER sibling memo would
|
|
1098
|
+
// otherwise recompute that sibling's whole expression a second time
|
|
1099
|
+
// inline in its `bf.FilterEval` env map, duplicating it against the
|
|
1100
|
+
// sibling's own field. Two-pass: first collect which memos are
|
|
1101
|
+
// referenced this way, then hoist each into a local emitted once before
|
|
1102
|
+
// the `return`; the memo-field loop below (and the referencing memo's own
|
|
1103
|
+
// env-map entry, via `ctx.state.hoistedMemoLocals`) both reuse the local
|
|
1104
|
+
// instead of re-emitting the expression. Declared `var name Type = value`
|
|
1105
|
+
// (not `:=`) because an unresolved computation can fall back to the bare
|
|
1106
|
+
// `nil` literal, which `:=` can't type-infer.
|
|
1107
|
+
const memoPropsParamMap = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
1108
|
+
this.state.hoistedMemoLocals = new Map()
|
|
1109
|
+
const hoistNames = new Set<string>()
|
|
1110
|
+
for (const memo of ir.metadata.memos) {
|
|
1111
|
+
for (const name of filterArmEarlierSiblingRefs(this.emitCtx, memo, ir.metadata.signals, ir.metadata.propsParams)) {
|
|
1112
|
+
hoistNames.add(name)
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (hoistNames.size > 0) {
|
|
1116
|
+
for (const memo of ir.metadata.memos) {
|
|
1117
|
+
if (!hoistNames.has(memo.name)) continue
|
|
1118
|
+
const goType = this.inferMemoType(memo, ir.metadata.signals, memoPropsParamMap)
|
|
1119
|
+
const value = computeMemoInitialValue(this.emitCtx, memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars, goType)
|
|
1120
|
+
let localName = `memo${capitalizeFieldName(memo.name)}`
|
|
1121
|
+
while (GO_KEYWORDS.has(localName)) localName += '_'
|
|
1122
|
+
lines.push(`\tvar ${localName} ${goType} = ${value}`)
|
|
1123
|
+
this.state.hoistedMemoLocals.set(memo.name, localName)
|
|
1124
|
+
}
|
|
1125
|
+
lines.push('')
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1003
1128
|
lines.push(`\treturn ${propsTypeName}{`)
|
|
1004
1129
|
lines.push('\t\tScopeID: scopeID,')
|
|
1005
1130
|
// Host context, for when *this* component is itself a slot-attached child.
|
|
@@ -1097,11 +1222,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1097
1222
|
}
|
|
1098
1223
|
|
|
1099
1224
|
// Memo initial values (from signal initials). Prop-shadowing memos were
|
|
1100
|
-
// folded into the prop field above
|
|
1101
|
-
|
|
1225
|
+
// folded into the prop field above; a memo the pre-pass already hoisted
|
|
1226
|
+
// (above) reuses that local instead of recomputing its expression.
|
|
1102
1227
|
for (const memo of ir.metadata.memos) {
|
|
1103
1228
|
const fieldName = capitalizeFieldName(memo.name)
|
|
1104
1229
|
if (propFieldNames.has(fieldName)) continue
|
|
1230
|
+
const hoistedLocal = this.state.hoistedMemoLocals.get(memo.name)
|
|
1231
|
+
if (hoistedLocal) {
|
|
1232
|
+
lines.push(`\t\t${fieldName}: ${hoistedLocal},`)
|
|
1233
|
+
continue
|
|
1234
|
+
}
|
|
1105
1235
|
// Pass the inferred Go type so an unresolved computation zeroes to that
|
|
1106
1236
|
// type (`false` for a boolean memo), not the int `0`.
|
|
1107
1237
|
const goType = this.inferMemoType(memo, ir.metadata.signals, memoPropsParamMap)
|
|
@@ -1130,8 +1260,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1130
1260
|
for (const c of this.nonCollidingContextConsumers(takenInit)) {
|
|
1131
1261
|
const field = this.contextFieldName(c)
|
|
1132
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.
|
|
1133
1268
|
const defaulted =
|
|
1134
|
-
c.defaultValue === null ||
|
|
1269
|
+
c.defaultValue === null ||
|
|
1270
|
+
def === '""' ||
|
|
1271
|
+
def === '0' ||
|
|
1272
|
+
def === 'false' ||
|
|
1273
|
+
def === 'map[string]interface{}{}'
|
|
1135
1274
|
? `in.${field}`
|
|
1136
1275
|
: applyGoFallback(`in.${field}`, def)
|
|
1137
1276
|
lines.push(`\t\t${field}: ${defaulted},`)
|
|
@@ -1146,7 +1285,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1146
1285
|
}
|
|
1147
1286
|
|
|
1148
1287
|
private emitStaticChildInstances(lines: string[], ir: ComponentIR): void {
|
|
1149
|
-
const staticChildren = this.collectStaticChildInstances(ir.root)
|
|
1288
|
+
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
1150
1289
|
for (const child of staticChildren) {
|
|
1151
1290
|
lines.push(`\t\t${child.fieldName}: New${child.name}Props(${child.name}Input{`)
|
|
1152
1291
|
lines.push(`\t\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
@@ -1353,7 +1492,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1353
1492
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1354
1493
|
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
1355
1494
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
1356
|
-
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren
|
|
1495
|
+
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!, ir.metadata.propsParams)
|
|
1357
1496
|
|
|
1358
1497
|
for (const child of bodyChildInstances) {
|
|
1359
1498
|
const childVar = `child_${child.fieldName}`
|
|
@@ -1447,7 +1586,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1447
1586
|
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
1448
1587
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1449
1588
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
1450
|
-
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren
|
|
1589
|
+
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!, ir.metadata.propsParams)
|
|
1451
1590
|
|
|
1452
1591
|
// Child sub-component instances created once (identical scope IDs per row).
|
|
1453
1592
|
for (const child of bodyChildInstances) {
|
|
@@ -1772,7 +1911,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1772
1911
|
}
|
|
1773
1912
|
}
|
|
1774
1913
|
|
|
1775
|
-
const staticChildren = this.collectStaticChildInstances(ir.root)
|
|
1914
|
+
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
1776
1915
|
for (const child of staticChildren) {
|
|
1777
1916
|
lines.push(`\t${child.fieldName} ${child.name}Props \`json:"-"\``)
|
|
1778
1917
|
}
|
|
@@ -1796,9 +1935,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1796
1935
|
* components inside loops (handled by nestedComponents). Each instance carries
|
|
1797
1936
|
* its component `name`, `slotId`, `props`, and Go `fieldName`.
|
|
1798
1937
|
*/
|
|
1799
|
-
private collectStaticChildInstances(
|
|
1938
|
+
private collectStaticChildInstances(
|
|
1939
|
+
node: IRNode,
|
|
1940
|
+
propsParams: ReadonlyArray<{ name: string }> = [],
|
|
1941
|
+
): Array<StaticChildInstance> {
|
|
1800
1942
|
const result: StaticChildInstance[] = []
|
|
1801
|
-
this.collectStaticChildInstancesRecursive(node, result, false, new Map())
|
|
1943
|
+
this.collectStaticChildInstancesRecursive(node, result, false, new Map(), propsParams)
|
|
1802
1944
|
return result
|
|
1803
1945
|
}
|
|
1804
1946
|
|
|
@@ -1871,6 +2013,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1871
2013
|
result: StaticChildInstance[],
|
|
1872
2014
|
inLoop: boolean,
|
|
1873
2015
|
providerCtx: ReadonlyMap<string, string>,
|
|
2016
|
+
propsParams: ReadonlyArray<{ name: string }> = [],
|
|
1874
2017
|
): void {
|
|
1875
2018
|
if (node.type === 'component') {
|
|
1876
2019
|
const comp = node as IRComponent
|
|
@@ -1880,7 +2023,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1880
2023
|
// child components nested inside still get their slot fields.
|
|
1881
2024
|
if (comp.dynamicTag) {
|
|
1882
2025
|
for (const child of comp.children) {
|
|
1883
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2026
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1884
2027
|
}
|
|
1885
2028
|
return
|
|
1886
2029
|
}
|
|
@@ -1912,86 +2055,208 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1912
2055
|
// never contain components (any nested component renders a `{{template}}`
|
|
1913
2056
|
// action, which the bake extractors reject), so recursing is a no-op.
|
|
1914
2057
|
for (const child of effectiveChildren) {
|
|
1915
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2058
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1916
2059
|
}
|
|
1917
2060
|
}
|
|
1918
2061
|
// Recurse into Portal's children to find nested components
|
|
1919
2062
|
if (comp.name === 'Portal' && comp.children) {
|
|
1920
2063
|
for (const child of comp.children) {
|
|
1921
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2064
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1922
2065
|
}
|
|
1923
2066
|
}
|
|
1924
2067
|
} else if (node.type === 'loop') {
|
|
1925
2068
|
const loop = node as IRLoop
|
|
1926
|
-
//
|
|
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`).
|
|
1927
2077
|
for (const child of loop.children) {
|
|
1928
|
-
this.collectStaticChildInstancesRecursive(
|
|
2078
|
+
this.collectStaticChildInstancesRecursive(
|
|
2079
|
+
child,
|
|
2080
|
+
result,
|
|
2081
|
+
inLoop || !!loop.childComponent,
|
|
2082
|
+
providerCtx,
|
|
2083
|
+
propsParams,
|
|
2084
|
+
)
|
|
1929
2085
|
}
|
|
1930
2086
|
} else if (node.type === 'element') {
|
|
1931
2087
|
const element = node as IRElement
|
|
1932
2088
|
for (const child of element.children) {
|
|
1933
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2089
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1934
2090
|
}
|
|
1935
2091
|
} else if (node.type === 'fragment') {
|
|
1936
2092
|
const fragment = node as IRFragment
|
|
1937
2093
|
for (const child of fragment.children) {
|
|
1938
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2094
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1939
2095
|
}
|
|
1940
2096
|
} else if (node.type === 'conditional') {
|
|
1941
2097
|
const cond = node as IRConditional
|
|
1942
|
-
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop, providerCtx)
|
|
2098
|
+
this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop, providerCtx, propsParams)
|
|
1943
2099
|
if (cond.whenFalse) {
|
|
1944
|
-
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx)
|
|
2100
|
+
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx, propsParams)
|
|
1945
2101
|
}
|
|
1946
2102
|
} else if (node.type === 'if-statement') {
|
|
1947
2103
|
// An early-return if-statement root (e.g. an asChild split) keeps its
|
|
1948
2104
|
// subtrees in consequent/alternate — the non-asChild branch's nested icon
|
|
1949
2105
|
// needs its slot field like any other static child.
|
|
1950
2106
|
const stmt = node as IRIfStatement
|
|
1951
|
-
this.collectStaticChildInstancesRecursive(stmt.consequent, result, inLoop, providerCtx)
|
|
2107
|
+
this.collectStaticChildInstancesRecursive(stmt.consequent, result, inLoop, providerCtx, propsParams)
|
|
1952
2108
|
if (stmt.alternate) {
|
|
1953
|
-
this.collectStaticChildInstancesRecursive(stmt.alternate, result, inLoop, providerCtx)
|
|
2109
|
+
this.collectStaticChildInstancesRecursive(stmt.alternate, result, inLoop, providerCtx, propsParams)
|
|
1954
2110
|
}
|
|
1955
2111
|
} else if (node.type === 'provider') {
|
|
1956
2112
|
// SSR context propagation: record the provider's value against its context
|
|
1957
2113
|
// name and extend the active binding map for descendants. A literal value
|
|
1958
|
-
// lowers to a Go literal;
|
|
1959
|
-
//
|
|
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).
|
|
1960
2117
|
const p = node as IRProvider
|
|
1961
|
-
const childCtx = this.extendProviderContext(providerCtx, p)
|
|
2118
|
+
const childCtx = this.extendProviderContext(providerCtx, p, propsParams)
|
|
1962
2119
|
for (const child of p.children) {
|
|
1963
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, childCtx)
|
|
2120
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, childCtx, propsParams)
|
|
1964
2121
|
}
|
|
1965
2122
|
} else if (node.type === 'async') {
|
|
1966
2123
|
// Async fallback + children render server-side via the OOS
|
|
1967
2124
|
// protocol; static child components inside them still need slot
|
|
1968
2125
|
// fields on the parent struct.
|
|
1969
2126
|
const a = node as IRAsync
|
|
1970
|
-
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop, providerCtx)
|
|
2127
|
+
this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop, providerCtx, propsParams)
|
|
1971
2128
|
for (const child of a.children) {
|
|
1972
|
-
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2129
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx, propsParams)
|
|
1973
2130
|
}
|
|
1974
2131
|
}
|
|
1975
2132
|
}
|
|
1976
2133
|
|
|
1977
2134
|
/**
|
|
1978
2135
|
* Extend the active provider-context map with one `<Ctx.Provider value>`. A
|
|
1979
|
-
* string/number/boolean literal value is lowered to a Go literal
|
|
1980
|
-
*
|
|
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.
|
|
1981
2143
|
*/
|
|
1982
2144
|
private extendProviderContext(
|
|
1983
2145
|
current: ReadonlyMap<string, string>,
|
|
1984
2146
|
p: IRProvider,
|
|
2147
|
+
propsParams: ReadonlyArray<{ name: string }>,
|
|
1985
2148
|
): ReadonlyMap<string, string> {
|
|
1986
|
-
const v = p.valueProp?.value as
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
if (
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
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
|
|
1995
2260
|
}
|
|
1996
2261
|
|
|
1997
2262
|
/**
|
|
@@ -2095,10 +2360,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2095
2360
|
}
|
|
2096
2361
|
|
|
2097
2362
|
// A memo: when no pattern applies, return null so the caller OMITS the
|
|
2098
|
-
// field and Go's typed zero value applies.
|
|
2363
|
+
// field and Go's typed zero value applies. Seed the resolution stack
|
|
2364
|
+
// with this memo's own name — a fresh top-level computation, so
|
|
2365
|
+
// self-reference must be caught on the first recursion.
|
|
2099
2366
|
const memo = memos.find(m => m.name === getterName)
|
|
2100
2367
|
if (memo) {
|
|
2101
|
-
return computeMemoInitialValueOrNull(
|
|
2368
|
+
return computeMemoInitialValueOrNull(
|
|
2369
|
+
this.emitCtx, memo, signals, propsParams, undefined, new Set([memo.name]),
|
|
2370
|
+
)
|
|
2102
2371
|
}
|
|
2103
2372
|
}
|
|
2104
2373
|
|
|
@@ -2111,6 +2380,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2111
2380
|
signals: { getter: string; initialValue: string; type: TypeInfo }[],
|
|
2112
2381
|
propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>
|
|
2113
2382
|
): string {
|
|
2383
|
+
// A LIST-valued `.filter(arrow)` memo (#2075 — the blog PostList `visible`
|
|
2384
|
+
// shape) is a slice of the receiver's boxed elements, not a scalar.
|
|
2385
|
+
// Decided FIRST, ahead of the memo's declared `type` and every other
|
|
2386
|
+
// heuristic below: the analyzer's simple per-memo type inference doesn't
|
|
2387
|
+
// model `.filter`'s predicate shape and can land on a bogus primitive
|
|
2388
|
+
// (observed: `boolean`, from the predicate's own `!`/`||` structure),
|
|
2389
|
+
// which would otherwise sail through the `typeInfoToGo(memo.type) ===
|
|
2390
|
+
// 'interface{}'` gates below unchallenged. `bf.FilterEval` (the SSR
|
|
2391
|
+
// constructor lowering, memo-compute.ts) returns `[]any`; the template's
|
|
2392
|
+
// `range` / reflective field access handle the boxed elements the same
|
|
2393
|
+
// way it already handles other `interface{}`-typed slices.
|
|
2394
|
+
if (isListFilterMemo(memo)) return '[]any'
|
|
2395
|
+
|
|
2114
2396
|
// A template-literal memo always produces a string. Decide this first so a
|
|
2115
2397
|
// class-string `/` (e.g. `ring-ring/50`) doesn't trip the arithmetic
|
|
2116
2398
|
// heuristic below into `int`. The analyzer classified the body shape from
|
|
@@ -2775,10 +3057,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2775
3057
|
if (objHO && (objHO.method === 'find' || objHO.method === 'findLast')) {
|
|
2776
3058
|
const findResult = this.renderHigherOrderExpr(objHO, emit)
|
|
2777
3059
|
if (findResult) {
|
|
2778
|
-
return `{{with ${findResult}}}{{.${
|
|
3060
|
+
return `{{with ${findResult}}}{{.${goFieldNameForKey(property)}}}{{end}}`
|
|
2779
3061
|
}
|
|
2780
3062
|
const templateBlock = this.renderFindTemplateBlock(
|
|
2781
|
-
objHO, emit,
|
|
3063
|
+
objHO, emit, goFieldNameForKey(property),
|
|
2782
3064
|
)
|
|
2783
3065
|
if (templateBlock) return templateBlock
|
|
2784
3066
|
}
|
|
@@ -2789,6 +3071,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2789
3071
|
return this.rootFieldRef(property)
|
|
2790
3072
|
}
|
|
2791
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
|
+
|
|
2792
3090
|
// Static property access on a module object-literal const
|
|
2793
3091
|
// (`variantClasses.ghost` in a class template literal) resolves at compile
|
|
2794
3092
|
// time — same lookup as the bracket-index form in
|
|
@@ -2803,14 +3101,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2803
3101
|
|
|
2804
3102
|
// Inside a loop, the loop param variable refers to the current item
|
|
2805
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.
|
|
2806
3112
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
2807
3113
|
if (object.kind === 'identifier' && currentLoopParam && object.name === currentLoopParam) {
|
|
2808
|
-
return `.${
|
|
3114
|
+
return `.${goFieldNameForKey(property)}`
|
|
2809
3115
|
}
|
|
2810
3116
|
|
|
2811
3117
|
const obj = emit(object)
|
|
2812
3118
|
if (property === 'length') return `len ${obj}`
|
|
2813
|
-
return `${obj}.${
|
|
3119
|
+
return `${obj}.${goFieldNameForKey(property)}`
|
|
2814
3120
|
}
|
|
2815
3121
|
|
|
2816
3122
|
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
@@ -2957,11 +3263,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2957
3263
|
}
|
|
2958
3264
|
|
|
2959
3265
|
objectLiteral(_properties: ObjectLiteralProperty[], raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
2960
|
-
//
|
|
2961
|
-
//
|
|
2962
|
-
//
|
|
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
|
|
2963
3281
|
// `objectLiteralToGoMap` lowering, not here.
|
|
2964
|
-
|
|
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 `""`
|
|
2965
3296
|
}
|
|
2966
3297
|
|
|
2967
3298
|
/** Set of predicate (boolean-callback) higher-order methods. */
|
|
@@ -3077,6 +3408,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3077
3408
|
return this.pushCallbackBF101(method, true)
|
|
3078
3409
|
}
|
|
3079
3410
|
|
|
3411
|
+
// Value-producing `.map(cb)` (#2073): eval-only. (The JSX-returning
|
|
3412
|
+
// `.map` is an IRLoop upstream and never reaches this dispatch.)
|
|
3413
|
+
if (method === 'map') {
|
|
3414
|
+
const evalForm = emitMapEval(recv, body, params[0] ?? '_', emit)
|
|
3415
|
+
if (evalForm !== null) return evalForm
|
|
3416
|
+
return this.pushCallbackBF101(method, true)
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3080
3419
|
return this.pushCallbackBF101(method, true)
|
|
3081
3420
|
}
|
|
3082
3421
|
|
|
@@ -3249,7 +3588,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3249
3588
|
}
|
|
3250
3589
|
}
|
|
3251
3590
|
|
|
3252
|
-
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
|
+
}
|
|
3253
3608
|
// `.flat(depth?)` → `bf_flat <recv> <depth>`. `Infinity` lowers to the `-1`
|
|
3254
3609
|
// sentinel (flatten fully); a finite depth flattens that many levels
|
|
3255
3610
|
// (`0` = shallow copy).
|
|
@@ -3657,6 +4012,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3657
4012
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
3658
4013
|
return `$.${capitalizeFieldName(expr.callee.name)}`
|
|
3659
4014
|
}
|
|
4015
|
+
// A nested callback method call (`other.some(r => …)`) reaching this
|
|
4016
|
+
// arm has no Go template form in filter context — the fallthrough
|
|
4017
|
+
// below renders only the callee and silently DROPS the arrow argument,
|
|
4018
|
+
// changing predicate semantics (#2038). The one faithful nested shape
|
|
4019
|
+
// (`.filter(cb).length` → `len (bf_filter_eval …)`) is intercepted at
|
|
4020
|
+
// the `member` arm before this node is visited; everything else must
|
|
4021
|
+
// be loud.
|
|
4022
|
+
if (asCallbackMethodCall(expr) !== null) {
|
|
4023
|
+
return this.refuseFilterExprNode(expr)
|
|
4024
|
+
}
|
|
3660
4025
|
const result = this.renderFilterExpr(expr.callee, param, localVarMap)
|
|
3661
4026
|
if (this.filterExprUnsupported) return 'false'
|
|
3662
4027
|
return result
|
|
@@ -3721,31 +4086,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3721
4086
|
return `or (${left}) (${right})`
|
|
3722
4087
|
}
|
|
3723
4088
|
|
|
3724
|
-
default:
|
|
3725
|
-
// The filter predicate body contains a node kind we can't lower to a
|
|
3726
|
-
// template action
|
|
3727
|
-
//
|
|
3728
|
-
|
|
3729
|
-
// flag so parent branches return `false` instead of wrapping the
|
|
3730
|
-
// sentinel into `false.Length` / `gt false.Length 0` etc.: the build
|
|
3731
|
-
// fails on BF101 anyway, but the emitted template must stay
|
|
3732
|
-
// syntactically valid so `text/template` parsing doesn't cascade into
|
|
3733
|
-
// confusing secondary errors.
|
|
3734
|
-
this.filterExprUnsupported = true
|
|
3735
|
-
this.state.errors.push({
|
|
3736
|
-
code: 'BF101',
|
|
3737
|
-
severity: 'error',
|
|
3738
|
-
message: `Filter predicate contains an expression that cannot be lowered to a Go template action: ${exprToString(expr)}`,
|
|
3739
|
-
loc: this.makeLoc(),
|
|
3740
|
-
suggestion: {
|
|
3741
|
-
message: 'Options:\n1. Use /* @client */ for client-side evaluation\n2. Rewrite the predicate to avoid nested higher-order methods (`.filter()` / `.map()` / etc. inside the predicate body)',
|
|
3742
|
-
},
|
|
3743
|
-
})
|
|
3744
|
-
return 'false'
|
|
3745
|
-
}
|
|
4089
|
+
default:
|
|
4090
|
+
// The filter predicate body contains a node kind we can't lower to a
|
|
4091
|
+
// Go template action. Shared refusal with the nested-callback guard in
|
|
4092
|
+
// the `call` arm (#2038).
|
|
4093
|
+
return this.refuseFilterExprNode(expr)
|
|
3746
4094
|
}
|
|
3747
4095
|
}
|
|
3748
4096
|
|
|
4097
|
+
/**
|
|
4098
|
+
* Refuse a filter-predicate node that has no Go template lowering. Surfaces
|
|
4099
|
+
* BF101 with the offending expression and sets the recursion-wide
|
|
4100
|
+
* `filterExprUnsupported` flag so parent branches return `false` instead of
|
|
4101
|
+
* wrapping the sentinel into `false.Length` / `gt false.Length 0` etc.: the
|
|
4102
|
+
* build fails on BF101 anyway, but the emitted template must stay
|
|
4103
|
+
* syntactically valid so `text/template` parsing doesn't cascade into
|
|
4104
|
+
* confusing secondary errors.
|
|
4105
|
+
*/
|
|
4106
|
+
private refuseFilterExprNode(expr: ParsedExpr): string {
|
|
4107
|
+
this.filterExprUnsupported = true
|
|
4108
|
+
this.state.errors.push({
|
|
4109
|
+
code: 'BF101',
|
|
4110
|
+
severity: 'error',
|
|
4111
|
+
message: `Filter predicate contains an expression that cannot be lowered to a Go template action: ${exprToString(expr)}`,
|
|
4112
|
+
loc: this.makeLoc(),
|
|
4113
|
+
suggestion: {
|
|
4114
|
+
message: 'Options:\n1. Use /* @client */ for client-side evaluation\n2. Rewrite the predicate to avoid nested higher-order methods (`.filter()` / `.map()` / etc. inside the predicate body)',
|
|
4115
|
+
},
|
|
4116
|
+
})
|
|
4117
|
+
return 'false'
|
|
4118
|
+
}
|
|
4119
|
+
|
|
3749
4120
|
/**
|
|
3750
4121
|
* Whether a ParsedExpr renders as a Go template function call (so it needs
|
|
3751
4122
|
* parentheses around it).
|
|
@@ -4319,20 +4690,78 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4319
4690
|
}
|
|
4320
4691
|
|
|
4321
4692
|
/**
|
|
4322
|
-
*
|
|
4323
|
-
*
|
|
4324
|
-
*
|
|
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.
|
|
4325
4704
|
*/
|
|
4326
|
-
private
|
|
4327
|
-
|
|
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.
|
|
4732
|
+
*/
|
|
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}`
|
|
4328
4743
|
for (const b of loop.paramBindings ?? []) {
|
|
4329
|
-
|
|
4330
|
-
|
|
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})`)
|
|
4331
4749
|
} else {
|
|
4332
|
-
|
|
4750
|
+
bindings.set(b.name, parent)
|
|
4751
|
+
restExcludes.set(b.name, { parent, excludeKeys: b.rest.exclude.map(k => k.key) })
|
|
4333
4752
|
}
|
|
4334
4753
|
}
|
|
4335
|
-
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
|
|
4336
4765
|
}
|
|
4337
4766
|
|
|
4338
4767
|
renderLoop(loop: IRLoop): string {
|
|
@@ -4351,15 +4780,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4351
4780
|
//
|
|
4352
4781
|
// Check the IR's structured `paramBindings` field rather than string-matching
|
|
4353
4782
|
// `loop.param`: Phase 1 populates it iff the param is a destructure pattern,
|
|
4354
|
-
// and the structured check is robust to formatting variants.
|
|
4355
|
-
//
|
|
4356
|
-
//
|
|
4357
|
-
//
|
|
4358
|
-
// `rest.flag`
|
|
4359
|
-
//
|
|
4360
|
-
//
|
|
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.
|
|
4361
4792
|
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0)
|
|
4362
|
-
const supportableDestructure = destructure &&
|
|
4793
|
+
const supportableDestructure = destructure && isLowerableLoopDestructure(loop)
|
|
4363
4794
|
if (destructure && !supportableDestructure) {
|
|
4364
4795
|
this.state.errors.push({
|
|
4365
4796
|
code: 'BF104',
|
|
@@ -4376,6 +4807,42 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4376
4807
|
})
|
|
4377
4808
|
}
|
|
4378
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
|
+
|
|
4379
4846
|
let goArray = this.convertExpressionToGo(loop.array)
|
|
4380
4847
|
const param = loop.param
|
|
4381
4848
|
let index = loop.index || '_'
|
|
@@ -4396,11 +4863,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4396
4863
|
rangeValue = '_'
|
|
4397
4864
|
}
|
|
4398
4865
|
|
|
4399
|
-
// If the loop
|
|
4400
|
-
// which carries a ScopeID per item (TodoItem → `.TodoItems`).
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
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`
|
|
4404
4878
|
}
|
|
4405
4879
|
|
|
4406
4880
|
this.inLoop = true
|
|
@@ -4418,7 +4892,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4418
4892
|
if (supportableDestructure) {
|
|
4419
4893
|
// Bindings resolve against the synthetic `$__bf_item` range var; don't push
|
|
4420
4894
|
// a loop param (the param is a pattern, not a name).
|
|
4421
|
-
this.
|
|
4895
|
+
const built = this.buildDestructureBindingMap(loop, rangeValue)
|
|
4896
|
+
this.loopBindingStack.push(built.bindings)
|
|
4897
|
+
this.loopRestExcludeStack.push(built.restExcludes)
|
|
4422
4898
|
pushedBindingMap = true
|
|
4423
4899
|
this.loopParamStack.push('')
|
|
4424
4900
|
if (rangeIndex !== '_') {
|
|
@@ -4443,7 +4919,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4443
4919
|
this.loopScalarItemStack.push(
|
|
4444
4920
|
this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null,
|
|
4445
4921
|
)
|
|
4922
|
+
this.loopWrapperStack.push(!!loop.childComponent)
|
|
4446
4923
|
const children = this.renderChildren(loop.children)
|
|
4924
|
+
this.loopWrapperStack.pop()
|
|
4447
4925
|
this.loopScalarItemStack.pop()
|
|
4448
4926
|
// Build the per-item anchor marker while the loop param is still on the
|
|
4449
4927
|
// stack, so a `bodyIsItemConditional` key expression resolves against the
|
|
@@ -4456,7 +4934,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4456
4934
|
else this.loopVarRefCount.set(v, rc)
|
|
4457
4935
|
}
|
|
4458
4936
|
this.loopParamStack.pop()
|
|
4459
|
-
if (pushedBindingMap)
|
|
4937
|
+
if (pushedBindingMap) {
|
|
4938
|
+
this.loopBindingStack.pop()
|
|
4939
|
+
this.loopRestExcludeStack.pop()
|
|
4940
|
+
}
|
|
4460
4941
|
this.inLoop = false
|
|
4461
4942
|
|
|
4462
4943
|
// Apply sort if present: wrap the array in a sort pipeline before `range`.
|
|
@@ -4512,24 +4993,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4512
4993
|
return ''
|
|
4513
4994
|
}
|
|
4514
4995
|
|
|
4515
|
-
/** Find the first component child in a list of nodes. */
|
|
4516
|
-
private findChildComponent(nodes: IRNode[]): IRComponent | null {
|
|
4517
|
-
for (const node of nodes) {
|
|
4518
|
-
if (node.type === 'component') {
|
|
4519
|
-
return node as IRComponent
|
|
4520
|
-
}
|
|
4521
|
-
if (node.type === 'element' && (node as IRElement).children) {
|
|
4522
|
-
const found = this.findChildComponent((node as IRElement).children)
|
|
4523
|
-
if (found) return found
|
|
4524
|
-
}
|
|
4525
|
-
if (node.type === 'fragment' && (node as IRFragment).children) {
|
|
4526
|
-
const found = this.findChildComponent((node as IRFragment).children)
|
|
4527
|
-
if (found) return found
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
return null
|
|
4531
|
-
}
|
|
4532
|
-
|
|
4533
4996
|
/**
|
|
4534
4997
|
* When `comp`'s JSX children contain template actions (nested components,
|
|
4535
4998
|
* dynamic text) — i.e. none of the static bake paths in
|
|
@@ -4603,13 +5066,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4603
5066
|
|
|
4604
5067
|
// In Go templates, components are rendered via {{template "name" data}}.
|
|
4605
5068
|
let templateCall: string
|
|
4606
|
-
if (this.inLoop) {
|
|
4607
|
-
//
|
|
4608
|
-
//
|
|
4609
|
-
//
|
|
4610
|
-
//
|
|
4611
|
-
//
|
|
4612
|
-
//
|
|
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.
|
|
4613
5078
|
const loopBodyDefine = this.queueLoopBodyChildrenDefine(comp)
|
|
4614
5079
|
if (loopBodyDefine) {
|
|
4615
5080
|
// Scalar-item loop: feed the body define the wrapper's `.BfLoopItem` (the
|
|
@@ -4622,6 +5087,24 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4622
5087
|
} else {
|
|
4623
5088
|
templateCall = `{{template "${comp.name}" .}}`
|
|
4624
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}" .}}`
|
|
4625
5108
|
} else if (comp.slotId) {
|
|
4626
5109
|
// Static children with slotId: unique field name based on slotId.
|
|
4627
5110
|
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
@@ -4801,16 +5284,30 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4801
5284
|
// Property access through the loop param (`t.attrs`) is already handled
|
|
4802
5285
|
// by the member-expression path that returns `.Attrs`.
|
|
4803
5286
|
//
|
|
4804
|
-
//
|
|
4805
|
-
//
|
|
4806
|
-
//
|
|
4807
|
-
// `
|
|
4808
|
-
//
|
|
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.
|
|
4809
5293
|
const trimmed = value.expr.trim()
|
|
4810
5294
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
4811
5295
|
if (currentLoopParam && trimmed === currentLoopParam) {
|
|
4812
5296
|
return `{{bf_spread_attrs .}}`
|
|
4813
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
|
+
}
|
|
4814
5311
|
const goExpr = this.convertExpressionToGo(value.expr)
|
|
4815
5312
|
// `convertExpressionToGo` already pushes BF101 for unsupported
|
|
4816
5313
|
// expressions and returns `""`; pass through so the template still
|