@barefootjs/go-template 0.33.3 → 0.33.6
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/emit-context.d.ts +12 -0
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +171 -7
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +237 -86
- package/dist/adapter/lib/compile-state.d.ts +17 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +251 -90
- package/dist/render-divergences.d.ts +20 -0
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +319 -149
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +342 -7
- package/src/adapter/emit-context.ts +14 -0
- package/src/adapter/go-template-adapter.ts +568 -103
- package/src/adapter/lib/compile-state.ts +18 -0
- package/src/adapter/value/parsed-literal-to-go.ts +11 -7
- package/src/adapter/value/value-lowering.ts +17 -0
- package/src/conformance-pins.ts +36 -0
- package/src/render-divergences.ts +22 -6
- package/src/test-render.ts +32 -15
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
TemplatePrimitiveRegistry,
|
|
35
35
|
LoopBindingPathSegment,
|
|
36
36
|
LoopBindingSource,
|
|
37
|
+
ConstantInfo,
|
|
37
38
|
} from '@barefootjs/jsx'
|
|
38
39
|
import {
|
|
39
40
|
BaseAdapter,
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
collectLoopBoundNames,
|
|
79
80
|
evaluateStaticLiteral,
|
|
80
81
|
BindingScope,
|
|
82
|
+
buildImportAliasMap,
|
|
81
83
|
} from '@barefootjs/jsx'
|
|
82
84
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
83
85
|
import { BF_REGION, escapeHtml, resolveJsxChildrenProp } from '@barefootjs/shared'
|
|
@@ -257,6 +259,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
257
259
|
extractPropFallback: (initialValue, preParsed) => this.extractPropFallback(initialValue, preParsed),
|
|
258
260
|
extractCollisionDerivation: (parsed) => this.extractCollisionDerivation(parsed),
|
|
259
261
|
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
|
|
262
|
+
resolveModuleNumericConst: (name) => this.resolveModuleNumericConst(name),
|
|
263
|
+
resolveModuleBooleanConst: (name) => this.resolveModuleBooleanConst(name),
|
|
260
264
|
}
|
|
261
265
|
|
|
262
266
|
/** Diagnostics from the current compile (backed by `CompileState`); `generate()` also merges these into `ir.errors`. */
|
|
@@ -386,6 +390,44 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
386
390
|
/** Child component name → the contexts it consumes (cross-component, for provider wiring). */
|
|
387
391
|
private childContextConsumers: Map<string, ContextConsumer[]> = new Map()
|
|
388
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Local alias -> declared/exported name for imported components (#2822,
|
|
395
|
+
* the SSR-side counterpart of #2777's client-JS registry-key fix). A
|
|
396
|
+
* child referenced under an import alias (`import { Foo as Bar }`,
|
|
397
|
+
* `<Bar/>`) has an `IRComponent.name` of `Bar` (the caller-local JSX tag
|
|
398
|
+
* name), but every cross-file lookup this adapter does against the
|
|
399
|
+
* child's OWN registered identity — `childComponentShapes` /
|
|
400
|
+
* `childContextConsumers` / `childDerivedFieldDeps` / `childPropFieldNames`
|
|
401
|
+
* / `childRepropsReady` (all populated from the CHILD's own
|
|
402
|
+
* `ir.metadata.componentName` via `registerChildComponentShape` or the
|
|
403
|
+
* child's own `generate()`/`generateTypes()` pass), the `New<Name>Props`
|
|
404
|
+
* constructor + `<Name>Input`/`<Name>Props` TYPE names, and the
|
|
405
|
+
* `{{template "<Name>" ...}}` cross-template call — must resolve through
|
|
406
|
+
* to `Foo`, the declared name, or they silently miss (a Go compile error
|
|
407
|
+
* for the constructor/type case, a `no such template` render error for
|
|
408
|
+
* the `{{template}}` case). Built once per compile from
|
|
409
|
+
* `ir.metadata.imports` via the shared `buildImportAliasMap`
|
|
410
|
+
* (`@barefootjs/jsx`) and read through `resolveChildName`.
|
|
411
|
+
*
|
|
412
|
+
* Deliberately NOT applied to a component's own PARENT-PRIVATE struct
|
|
413
|
+
* field name (`${comp.name}${suffix}`, `.${comp.name}` field access) —
|
|
414
|
+
* that field is declared and read using the SAME `comp.name` expression
|
|
415
|
+
* within this one parent's own generated code, so it stays internally
|
|
416
|
+
* consistent under the caller-local alias with no cross-file identity to
|
|
417
|
+
* match.
|
|
418
|
+
*/
|
|
419
|
+
private importAliases: Map<string, string> = new Map()
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Resolve a component reference's `IRComponent.name` (the caller-local
|
|
423
|
+
* JSX tag / import alias) to the name the referenced child's OWN module
|
|
424
|
+
* registers its Go template/type/constructor under. Identity for an
|
|
425
|
+
* un-aliased reference. See `importAliases`.
|
|
426
|
+
*/
|
|
427
|
+
private resolveChildName(name: string): string {
|
|
428
|
+
return this.importAliases.get(name) ?? name
|
|
429
|
+
}
|
|
430
|
+
|
|
389
431
|
|
|
390
432
|
constructor(options: GoTemplateAdapterOptions = {}) {
|
|
391
433
|
super()
|
|
@@ -405,6 +447,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
405
447
|
private primeCompileState(ir: ComponentIR): void {
|
|
406
448
|
this.state.propsObjectName = ir.metadata.propsObjectName
|
|
407
449
|
this.state.restPropsName = ir.metadata.restPropsName ?? null
|
|
450
|
+
// #2822: this component's OWN import-alias map (local alias -> declared
|
|
451
|
+
// name), read by `resolveChildName` at every cross-file lookup/codegen
|
|
452
|
+
// site below. Re-primed on every `generate()`/`generateTypes()` call —
|
|
453
|
+
// harmless per this method's own docstring, since what matters is that
|
|
454
|
+
// the LAST prime before a given `ir`'s template body actually renders is
|
|
455
|
+
// that same `ir`'s own imports (true here: `generate(ir)` primes then
|
|
456
|
+
// renders `ir`'s body synchronously before any other IR is primed).
|
|
457
|
+
this.importAliases = buildImportAliasMap(ir.metadata.imports ?? [])
|
|
408
458
|
// Inline-object-typed props (`cfg: { id: number }`) bake as
|
|
409
459
|
// `map[string]interface{}`; `member()` routes a nested access on them
|
|
410
460
|
// through `bf_get` rather than an exact-case dot path (#2299).
|
|
@@ -487,6 +537,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
487
537
|
this.state.componentName = ir.metadata.componentName
|
|
488
538
|
this.state.errors = []
|
|
489
539
|
this.state.referencedDerivedConsts = new Set()
|
|
540
|
+
this.state.templateReadRootFields = new Set()
|
|
490
541
|
this.state.templateVarCounter = 0
|
|
491
542
|
this.state.pendingChildrenDefines = []
|
|
492
543
|
this.scope = BindingScope.EMPTY
|
|
@@ -532,7 +583,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
532
583
|
for (const d of this.state.pendingChildrenDefines) {
|
|
533
584
|
template += `{{define "${d.name}"}}${d.content}{{end}}\n`
|
|
534
585
|
}
|
|
535
|
-
const types = this.generateTypes(ir)
|
|
586
|
+
const types = this.generateTypes(ir, true)
|
|
536
587
|
|
|
537
588
|
if (this.state.errors.length > 0) {
|
|
538
589
|
ir.errors.push(...this.state.errors)
|
|
@@ -1070,9 +1121,25 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1070
1121
|
return desired
|
|
1071
1122
|
}
|
|
1072
1123
|
|
|
1073
|
-
|
|
1124
|
+
/**
|
|
1125
|
+
* `preserveTemplateReadRootFields` is set ONLY by `generate()`'s own
|
|
1126
|
+
* internal call below — `templateReadRootFields` is an observation log
|
|
1127
|
+
* `renderNode` populated moments ago while rendering THIS SAME
|
|
1128
|
+
* component's template body, and this call needs to read that log back,
|
|
1129
|
+
* not a freshly emptied one. Every other caller (the standalone public
|
|
1130
|
+
* entry point `test-render.ts` calls directly on an already-`generate()`d
|
|
1131
|
+
* adapter for a sibling/child IR, and this file's own unit tests) omits
|
|
1132
|
+
* it and gets the set reset fresh — otherwise a stale log left over from
|
|
1133
|
+
* whichever OTHER component `generate()` rendered last would silently
|
|
1134
|
+
* narrow #2700's BF101 refusal to a false negative (pullfrog review, PR
|
|
1135
|
+
* #2818).
|
|
1136
|
+
*/
|
|
1137
|
+
generateTypes(ir: ComponentIR, preserveTemplateReadRootFields = false): string | null {
|
|
1074
1138
|
this.state.usesHtmlTemplate = false
|
|
1075
1139
|
this.state.usesFmt = false
|
|
1140
|
+
if (!preserveTemplateReadRootFields) {
|
|
1141
|
+
this.state.templateReadRootFields = new Set()
|
|
1142
|
+
}
|
|
1076
1143
|
// Prime identically to `generate()` so the standalone `generateTypes` entry
|
|
1077
1144
|
// can't drift the structs (e.g. a `{...props}` bag field in one entry only).
|
|
1078
1145
|
this.primeCompileState(ir)
|
|
@@ -1331,17 +1398,29 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1331
1398
|
}
|
|
1332
1399
|
|
|
1333
1400
|
/**
|
|
1334
|
-
* Synthesise a Go struct
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1401
|
+
* Synthesise a Go struct (plus, per #2800, one nested struct for each
|
|
1402
|
+
* array-of-objects field, recursively) from an untyped object-array
|
|
1403
|
+
* signal's inline initial value, or `null` (caller keeps
|
|
1404
|
+
* `[]interface{}`/`nil`). Requires: untyped array type; a non-empty array
|
|
1405
|
+
* literal of object literals; every element sharing the same
|
|
1406
|
+
* Go-identifier key set; every value EITHER a scalar literal with a
|
|
1407
|
+
* per-key-consistent Go type (mixed int/float64 widens to float64) OR an
|
|
1408
|
+
* array of object literals (recursed the same way). Any deviation, or a
|
|
1409
|
+
* name collision with an existing type, returns `null` for the WHOLE
|
|
1410
|
+
* signal — `parsedLiteralToGo` (`value-lowering.ts`) already defers the
|
|
1411
|
+
* whole array the moment one element fails to bake, so a partial struct
|
|
1412
|
+
* (e.g. only the scalar fields) would synthesize a type nothing could
|
|
1413
|
+
* ever fully populate; not worth a second, more permissive code path.
|
|
1414
|
+
*
|
|
1415
|
+
* Returns the synthesized structs in DEPENDENCY ORDER — nested structs
|
|
1416
|
+
* before the struct(s) that reference them by name — so a caller
|
|
1417
|
+
* registering them in list order never references an undeclared Go type.
|
|
1418
|
+
* The signal's own top-level struct is always the LAST entry.
|
|
1340
1419
|
*/
|
|
1341
1420
|
private synthesizeStructFromSignal(
|
|
1342
1421
|
signal: { getter: string; type: TypeInfo; initialValue: string; parsed?: ParsedExpr },
|
|
1343
1422
|
componentName: string,
|
|
1344
|
-
): { name: string; fields: Array<{ tsName: string; goName: string; goType: string }
|
|
1423
|
+
): Array<{ name: string; fields: Array<{ tsName: string; goName: string; goType: string }>; properties: PropertyInfo[] }> | null {
|
|
1345
1424
|
// Only untyped arrays: typed (`Item[]`) / scalar (`string[]`) elements bake
|
|
1346
1425
|
// through the normal path.
|
|
1347
1426
|
if (signal.type.kind !== 'array') return null
|
|
@@ -1351,12 +1430,40 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1351
1430
|
const node = signal.parsed
|
|
1352
1431
|
if (!node || node.kind !== 'array-literal' || node.elements.length === 0) return null
|
|
1353
1432
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1433
|
+
const name = `${componentName}${capitalizeFieldName(signal.getter)}Item`
|
|
1434
|
+
return this.synthesizeStructsFromElements(node.elements, name)
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* The recursive core of `synthesizeStructFromSignal`: given a list of
|
|
1439
|
+
* object-literal elements known to share ONE shape, and the Go struct
|
|
1440
|
+
* name to assign that shape, returns the synthesized struct(s) — this
|
|
1441
|
+
* shape's own struct last, any nested array-of-objects field's struct(s)
|
|
1442
|
+
* before it — or `null` on any shape this fast path doesn't bake.
|
|
1443
|
+
*
|
|
1444
|
+
* A nested array-of-objects field is validated and shaped from the FLAT
|
|
1445
|
+
* concatenation of that key's elements across every row (not just the
|
|
1446
|
+
* first row) — a later row's own object shape for that key must still
|
|
1447
|
+
* agree, but the nested struct's field set is inferred from every row's
|
|
1448
|
+
* contribution so no row's data is silently dropped from the type.
|
|
1449
|
+
*/
|
|
1450
|
+
private synthesizeStructsFromElements(
|
|
1451
|
+
elements: ParsedExpr[],
|
|
1452
|
+
name: string,
|
|
1453
|
+
): Array<{ name: string; fields: Array<{ tsName: string; goName: string; goType: string }>; properties: PropertyInfo[] }> | null {
|
|
1454
|
+
// Don't shadow an existing (user-defined or already-synthesised) type.
|
|
1455
|
+
if (this.state.localTypeNames.has(name)) return null
|
|
1456
|
+
|
|
1457
|
+
type PropShape = { kind: 'scalar'; goType: string } | { kind: 'nested-array' }
|
|
1458
|
+
|
|
1459
|
+
// Field order + per-key shape from the first element; every other
|
|
1460
|
+
// element must match exactly (same keys, same shape KIND per key).
|
|
1356
1461
|
const order: string[] = []
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1462
|
+
const shapes = new Map<string, PropShape>()
|
|
1463
|
+
const nestedElements = new Map<string, ParsedExpr[]>()
|
|
1464
|
+
|
|
1465
|
+
for (let i = 0; i < elements.length; i++) {
|
|
1466
|
+
const el = elements[i]
|
|
1360
1467
|
if (el.kind !== 'object-literal') return null
|
|
1361
1468
|
const seen = new Set<string>()
|
|
1362
1469
|
for (const prop of el.properties) {
|
|
@@ -1367,36 +1474,124 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1367
1474
|
if (prop.shorthand) return null
|
|
1368
1475
|
const key = prop.key
|
|
1369
1476
|
if (!GO_IDENTIFIER.test(key)) return null
|
|
1477
|
+
seen.add(key)
|
|
1478
|
+
|
|
1479
|
+
const isNestedArray =
|
|
1480
|
+
prop.value.kind === 'array-literal' &&
|
|
1481
|
+
prop.value.elements.every(e => e.kind === 'object-literal')
|
|
1482
|
+
|
|
1483
|
+
if (isNestedArray) {
|
|
1484
|
+
const prevShape = shapes.get(key)
|
|
1485
|
+
if (prevShape === undefined) {
|
|
1486
|
+
if (i !== 0) return null // key absent from the first element → shape differs
|
|
1487
|
+
order.push(key)
|
|
1488
|
+
shapes.set(key, { kind: 'nested-array' })
|
|
1489
|
+
nestedElements.set(key, [])
|
|
1490
|
+
} else if (prevShape.kind !== 'nested-array') {
|
|
1491
|
+
return null // this key is a scalar in some rows, a nested array in others
|
|
1492
|
+
}
|
|
1493
|
+
nestedElements.get(key)!.push(...(prop.value as { elements: ParsedExpr[] }).elements)
|
|
1494
|
+
continue
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1370
1497
|
const goType = this.scalarParsedGoType(prop.value)
|
|
1371
1498
|
if (!goType) return null
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
if (i !== 0) return null // key absent from the first element → shape differs
|
|
1499
|
+
const prevShape = shapes.get(key)
|
|
1500
|
+
if (prevShape === undefined) {
|
|
1501
|
+
if (i !== 0) return null
|
|
1376
1502
|
order.push(key)
|
|
1377
|
-
|
|
1503
|
+
shapes.set(key, { kind: 'scalar', goType })
|
|
1504
|
+
} else if (prevShape.kind !== 'scalar') {
|
|
1505
|
+
return null
|
|
1378
1506
|
} else {
|
|
1379
|
-
const merged = this.mergeScalarGoType(
|
|
1507
|
+
const merged = this.mergeScalarGoType(prevShape.goType, goType)
|
|
1380
1508
|
if (!merged) return null
|
|
1381
|
-
|
|
1509
|
+
shapes.set(key, { kind: 'scalar', goType: merged })
|
|
1382
1510
|
}
|
|
1383
1511
|
}
|
|
1384
1512
|
// A first-element key missing here → shape differs.
|
|
1385
1513
|
if (seen.size !== order.length) return null
|
|
1386
1514
|
}
|
|
1387
1515
|
|
|
1388
|
-
const name
|
|
1389
|
-
|
|
1390
|
-
|
|
1516
|
+
const nestedStructs: Array<{ name: string; fields: Array<{ tsName: string; goName: string; goType: string }>; properties: PropertyInfo[] }> = []
|
|
1517
|
+
const fields: Array<{ tsName: string; goName: string; goType: string }> = []
|
|
1518
|
+
const properties: PropertyInfo[] = []
|
|
1391
1519
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
tsName: key,
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1520
|
+
for (const key of order) {
|
|
1521
|
+
const shape = shapes.get(key)!
|
|
1522
|
+
if (shape.kind === 'scalar') {
|
|
1523
|
+
fields.push({ tsName: key, goName: capitalizeFieldName(key), goType: shape.goType })
|
|
1524
|
+
properties.push({ name: key, type: this.scalarGoTypeToTypeInfo(shape.goType), optional: false, readonly: false })
|
|
1525
|
+
continue
|
|
1526
|
+
}
|
|
1527
|
+
const nestedList = nestedElements.get(key)!
|
|
1528
|
+
// Every row's array for this key was empty — no row's data to infer
|
|
1529
|
+
// a shape from (distinct from a MISSING key, already ruled out
|
|
1530
|
+
// above); matches the top-level empty-array rule this function
|
|
1531
|
+
// already applies to the signal's own outer array.
|
|
1532
|
+
if (nestedList.length === 0) return null
|
|
1533
|
+
const nestedName = `${name}${capitalizeFieldName(key)}Item`
|
|
1534
|
+
const nested = this.synthesizeStructsFromElements(nestedList, nestedName)
|
|
1535
|
+
if (!nested) return null
|
|
1536
|
+
nestedStructs.push(...nested)
|
|
1537
|
+
fields.push({ tsName: key, goName: capitalizeFieldName(key), goType: `[]${nestedName}` })
|
|
1538
|
+
properties.push({ name: key, type: this.synthSliceTypeInfo(nestedName), optional: false, readonly: false })
|
|
1399
1539
|
}
|
|
1540
|
+
|
|
1541
|
+
return [...nestedStructs, { name, fields, properties }]
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/** `PropertyInfo.type` for a scalar Go field type — consumed only as a
|
|
1545
|
+
* defensive/consistent fill; `parsedLiteralToGo`'s object branch never
|
|
1546
|
+
* looks up a SCALAR property's declared type (only array/object ones),
|
|
1547
|
+
* so this never actually gates a bake, unlike `synthSliceTypeInfo`. */
|
|
1548
|
+
private scalarGoTypeToTypeInfo(goType: string): TypeInfo {
|
|
1549
|
+
if (goType === 'string') return { kind: 'primitive', raw: 'string', primitive: 'string' }
|
|
1550
|
+
if (goType === 'bool') return { kind: 'primitive', raw: 'boolean', primitive: 'boolean' }
|
|
1551
|
+
return { kind: 'primitive', raw: 'number', primitive: 'number' }
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* `TypeInfo` for "an array of the named synthesized struct" — the exact
|
|
1556
|
+
* shape both `emitSynthStructs`'s `synthStructTypes` entry (the signal's
|
|
1557
|
+
* OWN field type) and a nested array-of-objects field's `PropertyInfo`
|
|
1558
|
+
* need, so `parsed-literal-to-go.ts`'s `structPropertyType` resolves a
|
|
1559
|
+
* nested array property the identical way it resolves the signal's own
|
|
1560
|
+
* top-level type.
|
|
1561
|
+
*/
|
|
1562
|
+
private synthSliceTypeInfo(name: string): TypeInfo {
|
|
1563
|
+
return { kind: 'array', raw: `${name}[]`, elementType: { kind: 'interface', raw: name } }
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* Register a synthesized struct (fields as Go source lines, `properties`
|
|
1568
|
+
* for `structPropertyType`'s nested-type lookups) and emit its
|
|
1569
|
+
* declaration — the one register+emit sequence shared by
|
|
1570
|
+
* `emitSynthPropStructs.visitObject` (anonymous TS object types, #2674)
|
|
1571
|
+
* and `emitSynthStructs` (untyped object-array signals, #2800), so
|
|
1572
|
+
* localTypeNames/localStructFields/currentTypeDefinitions registration
|
|
1573
|
+
* can't drift between the two synthesis call sites.
|
|
1574
|
+
*/
|
|
1575
|
+
private registerSynthStruct(
|
|
1576
|
+
lines: string[],
|
|
1577
|
+
name: string,
|
|
1578
|
+
fields: Array<{ tsName: string; goName: string; goType: string }>,
|
|
1579
|
+
properties: PropertyInfo[],
|
|
1580
|
+
comment: string,
|
|
1581
|
+
): void {
|
|
1582
|
+
this.state.localTypeNames.add(name)
|
|
1583
|
+
this.state.localStructFields.set(name, new Map(fields.map(f => [f.tsName, f.goName])))
|
|
1584
|
+
this.state.currentTypeDefinitions.push({
|
|
1585
|
+
kind: 'type',
|
|
1586
|
+
name,
|
|
1587
|
+
definition: '',
|
|
1588
|
+
properties,
|
|
1589
|
+
loc: SYNTH_TYPE_LOC,
|
|
1590
|
+
})
|
|
1591
|
+
const goFields = fields.map(f => `\t${f.goName} ${f.goType} \`json:"${this.toJsonTag(f.tsName)}"\``)
|
|
1592
|
+
lines.push(comment)
|
|
1593
|
+
lines.push(`type ${name} struct {\n${goFields.join('\n')}\n}`)
|
|
1594
|
+
lines.push('')
|
|
1400
1595
|
}
|
|
1401
1596
|
|
|
1402
1597
|
/**
|
|
@@ -1524,7 +1719,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1524
1719
|
nested.loopParam,
|
|
1525
1720
|
nested.loopKey,
|
|
1526
1721
|
)) continue
|
|
1527
|
-
|
|
1722
|
+
// #2822 follow-up: field NAME stays alias-keyed (parent-private, this
|
|
1723
|
+
// Input struct's own field — read as `in.${nested.name}s` throughout
|
|
1724
|
+
// this file), but the element TYPE is the child's own cross-file
|
|
1725
|
+
// `<Name>Input` struct — see `importAliases`.
|
|
1726
|
+
lines.push(`\t${nested.name}s []${this.resolveChildName(nested.name)}Input`)
|
|
1528
1727
|
}
|
|
1529
1728
|
|
|
1530
1729
|
// `useContext` consumer fields — settable by an enclosing provider; default
|
|
@@ -1595,11 +1794,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1595
1794
|
const wrapperName = this.loopBodyWrapperName(parentComponentName, nested)
|
|
1596
1795
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
1597
1796
|
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!)
|
|
1598
|
-
|
|
1599
|
-
|
|
1797
|
+
// #2822 follow-up: this is a Go EMBEDDED (anonymous) field — its field
|
|
1798
|
+
// name IS its type name, so unlike `child.fieldName` the alias-keyed
|
|
1799
|
+
// field name can't be kept separate from the type here. The whole
|
|
1800
|
+
// token must be the child's own cross-file DECLARED name everywhere
|
|
1801
|
+
// this embedded field is declared or literal-initialized (below, and
|
|
1802
|
+
// every `${declaredName}Props: New${declaredName}Props(...)` composite
|
|
1803
|
+
// literal site) — see `importAliases`.
|
|
1804
|
+
const declaredName = this.resolveChildName(nested.name)
|
|
1805
|
+
|
|
1806
|
+
lines.push(`// ${wrapperName} wraps ${declaredName}Props with per-row loop datum`)
|
|
1600
1807
|
lines.push(`// fields and child component slots for the loop body children. (#1897)`)
|
|
1601
1808
|
lines.push(`type ${wrapperName} struct {`)
|
|
1602
|
-
lines.push(`\t${
|
|
1809
|
+
lines.push(`\t${declaredName}Props`)
|
|
1603
1810
|
for (const f of datumFields) {
|
|
1604
1811
|
lines.push(`\t${f.goName} ${f.goType} \`json:"-"\``)
|
|
1605
1812
|
}
|
|
@@ -1610,7 +1817,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1610
1817
|
lines.push(`\tBfLoopItem ${scalarLoopType} \`json:"-"\``)
|
|
1611
1818
|
}
|
|
1612
1819
|
for (const child of bodyChildInstances) {
|
|
1613
|
-
|
|
1820
|
+
// #2822: field NAME stays alias-keyed (parent-private); the TYPE must
|
|
1821
|
+
// name the child's own declared Go type — see `importAliases`.
|
|
1822
|
+
lines.push(`\t${child.fieldName} ${this.resolveChildName(child.name)}Props \`json:"-"\``)
|
|
1614
1823
|
}
|
|
1615
1824
|
lines.push('}')
|
|
1616
1825
|
lines.push('')
|
|
@@ -1759,6 +1968,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1759
1968
|
// Static nested WITHOUT body children.
|
|
1760
1969
|
for (const nested of staticWithoutBody) {
|
|
1761
1970
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1971
|
+
// #2822 follow-up: the constructor/type names below are cross-file
|
|
1972
|
+
// (the child's own `New<Name>Props`/`<Name>Props`/`<Name>Input`) and
|
|
1973
|
+
// must resolve to the child's declared name; `varName` (this local)
|
|
1974
|
+
// and `in.${nested.name}s` (this parent's own Input field, read
|
|
1975
|
+
// below) stay alias-keyed — see `importAliases`.
|
|
1976
|
+
const declaredName = this.resolveChildName(nested.name)
|
|
1762
1977
|
// #2208: a static loop whose ARRAY SOURCE is itself fully-static
|
|
1763
1978
|
// (`const items = [{ label: 'Alpha' }, ...]`) has no caller input to
|
|
1764
1979
|
// wait for — every item's props/data-key are already known at
|
|
@@ -1776,10 +1991,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1776
1991
|
)
|
|
1777
1992
|
: null
|
|
1778
1993
|
if (baked) {
|
|
1779
|
-
lines.push(`\t${varName} := make([]${
|
|
1994
|
+
lines.push(`\t${varName} := make([]${declaredName}Props, ${baked.items.length})`)
|
|
1780
1995
|
baked.items.forEach((item, i) => {
|
|
1781
1996
|
const fields = item.inputFields.map(f => `${f.goField}: ${f.goValue}`).join(', ')
|
|
1782
|
-
lines.push(`\t${varName}[${i}] = New${
|
|
1997
|
+
lines.push(`\t${varName}[${i}] = New${declaredName}Props(${declaredName}Input{${fields}})`)
|
|
1783
1998
|
lines.push(`\t${varName}[${i}].BfParent = scopeID`)
|
|
1784
1999
|
lines.push(`\t${varName}[${i}].BfMount = "${nested.slotId}"`)
|
|
1785
2000
|
if (item.dataKey !== null) {
|
|
@@ -1789,9 +2004,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1789
2004
|
lines.push('')
|
|
1790
2005
|
continue
|
|
1791
2006
|
}
|
|
1792
|
-
lines.push(`\t${varName} := make([]${
|
|
2007
|
+
lines.push(`\t${varName} := make([]${declaredName}Props, len(in.${nested.name}s))`)
|
|
1793
2008
|
lines.push(`\tfor i, item := range in.${nested.name}s {`)
|
|
1794
|
-
lines.push(`\t\t${varName}[i] = New${
|
|
2009
|
+
lines.push(`\t\t${varName}[i] = New${declaredName}Props(item)`)
|
|
1795
2010
|
lines.push(`\t\t${varName}[i].BfParent = scopeID`)
|
|
1796
2011
|
lines.push(`\t\t${varName}[i].BfMount = "${nested.slotId}"`)
|
|
1797
2012
|
const keyField = loopKeyToGoFieldPath(nested.loopKey, nested.loopParam)
|
|
@@ -1964,8 +2179,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1964
2179
|
// Bake against the synthesised struct type if one was inferred for this
|
|
1965
2180
|
// untyped object-array signal, else the signal's own type.
|
|
1966
2181
|
const bakeType = this.state.synthStructTypes.get(signal.getter) ?? signal.type
|
|
2182
|
+
const resolvedParsed = this.resolvedSignalParsed(signal)
|
|
1967
2183
|
const initialValue = convertInitialValue(this.emitCtx, signal.initialValue, bakeType, ir.metadata.propsParams, signal.parsed)
|
|
1968
2184
|
lines.push(`\t\t${fieldName}: ${initialValue},`)
|
|
2185
|
+
if (resolvedParsed?.kind === 'object-literal' && jsLiteralToGo(this.emitCtx, bakeType, resolvedParsed) === null) {
|
|
2186
|
+
const step = this.state.ssrSeedPlan.steps.find(s => s.kind === 'derived' && s.origin === 'signal' && s.name === signal.getter)
|
|
2187
|
+
if (step?.kind === 'derived') this.refuseUnbakeableDerivedObjectLiteral(signal.getter, signal.loc, step.frees)
|
|
2188
|
+
}
|
|
1969
2189
|
}
|
|
1970
2190
|
}
|
|
1971
2191
|
|
|
@@ -1997,6 +2217,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1997
2217
|
const goType = this.inferMemoType(memo, ir.metadata.signals, memoPropsParamMap)
|
|
1998
2218
|
const memoValue = computeMemoInitialValue(this.emitCtx, memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars, goType)
|
|
1999
2219
|
lines.push(`\t\t${fieldName}: ${memoValue},`)
|
|
2220
|
+
// No #2700 refusal check here (unlike the signal loop above): the
|
|
2221
|
+
// analyzer deliberately never sets `MemoInfo.parsed` to an
|
|
2222
|
+
// `object-literal` for an object-returning memo body (`() => ({…})`,
|
|
2223
|
+
// `analyzer.ts`'s own docstring — "isn't lowered from the parsed tree
|
|
2224
|
+
// yet") — a pre-existing, unrelated exclusion this fix doesn't touch.
|
|
2225
|
+
// Detecting the shape without `parsed` would mean re-parsing
|
|
2226
|
+
// `memo.computation` as text, which the repo's own convention (see
|
|
2227
|
+
// CLAUDE.md, "Never parse imports... with regex or string matching")
|
|
2228
|
+
// rules out. #2700's own reproduction and fixture are signal-only;
|
|
2229
|
+
// a memo-side refusal is left for whoever lands the "Roadmap A" memo
|
|
2230
|
+
// object-literal `parsed` support this comment references.
|
|
2000
2231
|
}
|
|
2001
2232
|
|
|
2002
2233
|
// Computed derived-const fields (`Root: func() string { … }()`), matching
|
|
@@ -2182,7 +2413,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2182
2413
|
private emitStaticChildInstances(lines: string[], ir: ComponentIR): void {
|
|
2183
2414
|
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
2184
2415
|
for (const child of staticChildren) {
|
|
2185
|
-
|
|
2416
|
+
// #2822: the constructor/type names and every cross-file shape lookup
|
|
2417
|
+
// below must resolve to the child's own DECLARED name — `New<Name>Props`
|
|
2418
|
+
// / `<Name>Input` are types/functions the child's OWN generated Go file
|
|
2419
|
+
// defines under that name, not the caller-local alias. `child.fieldName`
|
|
2420
|
+
// (this PARENT's own struct field) stays keyed by the alias — declared
|
|
2421
|
+
// and read consistently within this one parent's generated code.
|
|
2422
|
+
const declaredName = this.resolveChildName(child.name)
|
|
2423
|
+
lines.push(`\t\t${child.fieldName}: New${declaredName}Props(${declaredName}Input{`)
|
|
2186
2424
|
lines.push(`\t\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
2187
2425
|
lines.push(`\t\t\tBfParent: scopeID,`)
|
|
2188
2426
|
lines.push(`\t\t\tBfMount: "${child.slotId}",`)
|
|
@@ -2190,7 +2428,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2190
2428
|
// consumes gets the provider value set on its consumer field (else its own
|
|
2191
2429
|
// NewProps applies the `createContext` default).
|
|
2192
2430
|
if (child.contextBindings) {
|
|
2193
|
-
for (const consumer of this.childContextConsumers.get(
|
|
2431
|
+
for (const consumer of this.childContextConsumers.get(declaredName) ?? []) {
|
|
2194
2432
|
const goVal = child.contextBindings.get(consumer.contextName)
|
|
2195
2433
|
if (goVal !== undefined) {
|
|
2196
2434
|
lines.push(`\t\t\t${this.contextFieldName(consumer)}: ${goVal},`)
|
|
@@ -2200,7 +2438,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2200
2438
|
// Non-param attrs route into the child's rest bag (see
|
|
2201
2439
|
// `childComponentShapes`); `restBagEntries` collects `"jsx-attr-name":
|
|
2202
2440
|
// goValue` pairs for that map.
|
|
2203
|
-
const childShape = this.childComponentShapes.get(
|
|
2441
|
+
const childShape = this.childComponentShapes.get(declaredName)
|
|
2204
2442
|
const restBagEntries: string[] = []
|
|
2205
2443
|
const emitChildField = (jsxName: string, goValue: string): void => {
|
|
2206
2444
|
if (
|
|
@@ -2342,6 +2580,20 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2342
2580
|
this.state.usesHtmlTemplate = true
|
|
2343
2581
|
emitChildField(prop.name, `template.HTML(${scopedHtml})`)
|
|
2344
2582
|
}
|
|
2583
|
+
// Else: none of the three bake attempts produced a static
|
|
2584
|
+
// Go string — the value contains a template action that
|
|
2585
|
+
// survived (#2746) or is otherwise genuinely dynamic
|
|
2586
|
+
// (#2703). No field is emitted here; `queueDynamicPropDefine`
|
|
2587
|
+
// (called from `renderComponent`'s static-call-site branch)
|
|
2588
|
+
// detects the SAME unbakeable shape and delivers it at
|
|
2589
|
+
// template-execution time via `bf_with_props` + `bf_tmpl`,
|
|
2590
|
+
// mirroring how the reserved `children` field's null case
|
|
2591
|
+
// just above is filled in by `bf_with_children`. A
|
|
2592
|
+
// component nested in a loop row never reaches this
|
|
2593
|
+
// function (`collectStaticChildInstancesRecursive` only
|
|
2594
|
+
// collects `!inLoop` components), so there is no dynamic
|
|
2595
|
+
// named-prop delivery for that shape yet — out of scope
|
|
2596
|
+
// here, tracked separately if it turns up.
|
|
2345
2597
|
}
|
|
2346
2598
|
}
|
|
2347
2599
|
}
|
|
@@ -2384,6 +2636,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2384
2636
|
lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`)
|
|
2385
2637
|
for (const nested of signalDynamicNested) {
|
|
2386
2638
|
const arrayField = `${nested.name}s`
|
|
2639
|
+
// #2822 follow-up: `arrayField` names THIS parent's own field
|
|
2640
|
+
// (alias-keyed, stays as-is); the constructor/type names in the
|
|
2641
|
+
// example below are the child's own cross-file symbols — resolve
|
|
2642
|
+
// for doc accuracy (cosmetic; not compiled) — see `importAliases`.
|
|
2643
|
+
const declaredName = this.resolveChildName(nested.name)
|
|
2387
2644
|
lines.push(`//`)
|
|
2388
2645
|
lines.push(`// NOTE: \`${arrayField}\` is populated by the route handler, not by`)
|
|
2389
2646
|
lines.push(`// New${componentName}Props — the SSR template iterates over it`)
|
|
@@ -2391,9 +2648,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2391
2648
|
lines.push(`// assign it before passing the props to your renderer. Example:`)
|
|
2392
2649
|
lines.push(`//`)
|
|
2393
2650
|
lines.push(`// props := New${componentName}Props(${inputTypeName}{ /* ... */ })`)
|
|
2394
|
-
lines.push(`// props.${arrayField} = make([]${
|
|
2651
|
+
lines.push(`// props.${arrayField} = make([]${declaredName}Props, len(items))`)
|
|
2395
2652
|
lines.push(`// for i, item := range items {`)
|
|
2396
|
-
lines.push(`// props.${arrayField}[i] = New${
|
|
2653
|
+
lines.push(`// props.${arrayField}[i] = New${declaredName}Props(${declaredName}Input{ /* fields */ })`)
|
|
2397
2654
|
lines.push(`// props.${arrayField}[i].BfParent = props.ScopeID`)
|
|
2398
2655
|
lines.push(`// props.${arrayField}[i].BfMount = "${nested.slotId}"`)
|
|
2399
2656
|
lines.push(`// }`)
|
|
@@ -2456,10 +2713,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2456
2713
|
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
2457
2714
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
2458
2715
|
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!, ir.metadata.propsParams)
|
|
2716
|
+
// #2822 follow-up: matches `generateLoopBodyWrapperStruct`'s embedded
|
|
2717
|
+
// field — the composite-literal key here must be the SAME declared
|
|
2718
|
+
// name the wrapper struct's embedded field was declared under.
|
|
2719
|
+
const declaredName = this.resolveChildName(nested.name)
|
|
2459
2720
|
|
|
2460
2721
|
for (const child of bodyChildInstances) {
|
|
2461
2722
|
const childVar = `child_${child.fieldName}`
|
|
2462
|
-
|
|
2723
|
+
const childDeclaredName = this.resolveChildName(child.name)
|
|
2724
|
+
lines.push(`\t${childVar} := New${childDeclaredName}Props(${childDeclaredName}Input{`)
|
|
2463
2725
|
lines.push(`\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
2464
2726
|
lines.push(`\t\tBfParent: scopeID,`)
|
|
2465
2727
|
lines.push(`\t\tBfMount: "${child.slotId}",`)
|
|
@@ -2479,7 +2741,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2479
2741
|
lines.push(`\t${varName} := make([]${wrapperType}, len(${dataVar}))`)
|
|
2480
2742
|
lines.push(`\tfor i, item := range ${dataVar} {`)
|
|
2481
2743
|
lines.push(`\t\t${varName}[i] = ${wrapperType}{`)
|
|
2482
|
-
lines.push(`\t\t\t${
|
|
2744
|
+
lines.push(`\t\t\t${declaredName}Props: New${declaredName}Props(${declaredName}Input{`)
|
|
2483
2745
|
lines.push(`\t\t\t\tBfParent: scopeID,`)
|
|
2484
2746
|
lines.push(`\t\t\t\tBfMount: "${nested.slotId}",`)
|
|
2485
2747
|
// Loop-body component's own static props. `key` → BfDataKey below; children
|
|
@@ -2550,11 +2812,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2550
2812
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
2551
2813
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
2552
2814
|
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!, ir.metadata.propsParams)
|
|
2815
|
+
// #2822 follow-up: matches `generateLoopBodyWrapperStruct`'s embedded
|
|
2816
|
+
// field — see the identical comment in `emitStaticBodyWrappers`.
|
|
2817
|
+
const declaredName = this.resolveChildName(nested.name)
|
|
2553
2818
|
|
|
2554
2819
|
// Child sub-component instances created once (identical scope IDs per row).
|
|
2555
2820
|
for (const child of bodyChildInstances) {
|
|
2556
2821
|
const childVar = `child_${child.fieldName}`
|
|
2557
|
-
|
|
2822
|
+
const childDeclaredName = this.resolveChildName(child.name)
|
|
2823
|
+
lines.push(`\t${childVar} := New${childDeclaredName}Props(${childDeclaredName}Input{`)
|
|
2558
2824
|
lines.push(`\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
2559
2825
|
lines.push(`\t\tBfParent: scopeID,`)
|
|
2560
2826
|
lines.push(`\t\tBfMount: "${child.slotId}",`)
|
|
@@ -2573,7 +2839,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2573
2839
|
lines.push(`\t${varName} := make([]${wrapperType}, len(bakedData))`)
|
|
2574
2840
|
lines.push(`\tfor i, item := range bakedData {`)
|
|
2575
2841
|
lines.push(`\t\t${varName}[i] = ${wrapperType}{`)
|
|
2576
|
-
lines.push(`\t\t\t${
|
|
2842
|
+
lines.push(`\t\t\t${declaredName}Props: New${declaredName}Props(${declaredName}Input{`)
|
|
2577
2843
|
lines.push(`\t\t\t\tBfParent: scopeID,`)
|
|
2578
2844
|
lines.push(`\t\t\t\tBfMount: "${nested.slotId}",`)
|
|
2579
2845
|
lines.push(`\t\t\t}),`)
|
|
@@ -2701,20 +2967,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2701
2967
|
visit(prop.type, desiredName, prop.name)
|
|
2702
2968
|
}
|
|
2703
2969
|
const fields = this.structFieldsFor(typeInfo)
|
|
2704
|
-
this.
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
loc: SYNTH_TYPE_LOC,
|
|
2711
|
-
})
|
|
2712
|
-
const goFields = fields.map(
|
|
2713
|
-
f => `\t${f.goName} ${f.goType} \`json:"${this.toJsonTag(f.tsName)}"\``,
|
|
2970
|
+
this.registerSynthStruct(
|
|
2971
|
+
lines,
|
|
2972
|
+
desiredName,
|
|
2973
|
+
fields,
|
|
2974
|
+
typeInfo.properties ?? [],
|
|
2975
|
+
`// ${desiredName} is a synthesised type for an anonymous object type (#2674).`,
|
|
2714
2976
|
)
|
|
2715
|
-
lines.push(`// ${desiredName} is a synthesised type for an anonymous object type (#2674).`)
|
|
2716
|
-
lines.push(`type ${desiredName} struct {\n${goFields.join('\n')}\n}`)
|
|
2717
|
-
lines.push('')
|
|
2718
2977
|
}
|
|
2719
2978
|
|
|
2720
2979
|
const visitArrayElem = (elemType: TypeInfo | undefined, parentName: string, propName: string): void => {
|
|
@@ -2767,28 +3026,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2767
3026
|
}
|
|
2768
3027
|
|
|
2769
3028
|
private emitSynthStructs(lines: string[], ir: ComponentIR, componentName: string): void {
|
|
2770
|
-
// Synthesise a struct for each untyped object-array signal
|
|
2771
|
-
//
|
|
2772
|
-
//
|
|
2773
|
-
//
|
|
3029
|
+
// Synthesise a struct for each untyped object-array signal (plus, per
|
|
3030
|
+
// #2800, one nested struct per array-of-objects field, recursively) and
|
|
3031
|
+
// emit them, so the signal field can be typed `[]Synth` and its inline
|
|
3032
|
+
// items baked (the loop body reaches each item via struct field
|
|
3033
|
+
// access). Registered via `registerSynthStruct` so the baker resolves
|
|
3034
|
+
// every level's element type the same way it resolves a #2674
|
|
3035
|
+
// anonymous-type struct.
|
|
2774
3036
|
this.state.synthStructTypes = new Map<string, TypeInfo>()
|
|
2775
3037
|
for (const signal of ir.metadata.signals) {
|
|
2776
3038
|
if (signal.envReader) continue // env signal has no bakeable initial shape (#2057)
|
|
2777
3039
|
const synth = this.synthesizeStructFromSignal(signal, componentName)
|
|
2778
3040
|
if (!synth) continue
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
3041
|
+
// Nested-first order (`synthesizeStructFromSignal`'s contract): a
|
|
3042
|
+
// struct referencing an earlier entry by name is always registered
|
|
3043
|
+
// after it, so no declaration ever forward-references an
|
|
3044
|
+
// undeclared Go type.
|
|
3045
|
+
for (const s of synth) {
|
|
3046
|
+
this.registerSynthStruct(
|
|
3047
|
+
lines,
|
|
3048
|
+
s.name,
|
|
3049
|
+
s.fields,
|
|
3050
|
+
s.properties,
|
|
3051
|
+
`// ${s.name} is a synthesised element type for the ${signal.getter} signal.`,
|
|
3052
|
+
)
|
|
3053
|
+
}
|
|
3054
|
+
const top = synth[synth.length - 1]
|
|
3055
|
+
this.state.synthStructTypes.set(signal.getter, this.synthSliceTypeInfo(top.name))
|
|
2792
3056
|
}
|
|
2793
3057
|
}
|
|
2794
3058
|
|
|
@@ -3068,10 +3332,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3068
3332
|
// real — a second same-named field here is a Go compile error
|
|
3069
3333
|
// ("redeclared"), not just dead code.
|
|
3070
3334
|
if (this.isOrphanedClientOnlyNested(nested)) continue
|
|
3071
|
-
// Loop body with JSX children → use the wrapper struct type
|
|
3335
|
+
// Loop body with JSX children → use the wrapper struct type (a
|
|
3336
|
+
// parent-private name — the child's own Props type only appears
|
|
3337
|
+
// INSIDE it as the embedded field, already resolved in
|
|
3338
|
+
// `generateLoopBodyWrapperStruct`). Loop body IS just the bare child
|
|
3339
|
+
// component → this element type IS the child's own cross-file Props
|
|
3340
|
+
// type directly — #2822 follow-up: resolve to the declared name.
|
|
3072
3341
|
const elemType = nested.bodyChildren?.length
|
|
3073
3342
|
? this.loopBodyWrapperName(componentName, nested)
|
|
3074
|
-
: `${nested.name}Props`
|
|
3343
|
+
: `${this.resolveChildName(nested.name)}Props`
|
|
3075
3344
|
if (nested.isDynamic && !nested.isPropDerived) {
|
|
3076
3345
|
// Dynamic signal-array loops are template-only.
|
|
3077
3346
|
lines.push(`\t${nested.name}s []${elemType} \`json:"-"\``)
|
|
@@ -3115,7 +3384,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3115
3384
|
|
|
3116
3385
|
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
3117
3386
|
for (const child of staticChildren) {
|
|
3118
|
-
|
|
3387
|
+
// #2822: the field's NAME stays keyed by the caller-local alias
|
|
3388
|
+
// (`child.fieldName`, declared and read consistently within this
|
|
3389
|
+
// parent's own generated code — see `importAliases`'s docstring), but
|
|
3390
|
+
// its TYPE is a real Go type only the child's own file defines, under
|
|
3391
|
+
// the child's DECLARED name.
|
|
3392
|
+
lines.push(`\t${child.fieldName} ${this.resolveChildName(child.name)}Props \`json:"-"\``)
|
|
3119
3393
|
}
|
|
3120
3394
|
|
|
3121
3395
|
// Top-level intrinsic-element spreads: each gets a `Spread_<slotId>
|
|
@@ -3865,6 +4139,55 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3865
4139
|
return resolveSignalParsedThroughSeedPlan(this.state, signal)
|
|
3866
4140
|
}
|
|
3867
4141
|
|
|
4142
|
+
/**
|
|
4143
|
+
* #2700: a `derived`-classified signal (`SsrSeedPlan`'s classification —
|
|
4144
|
+
* an object literal whose free identifiers ALL resolve in scope, e.g.
|
|
4145
|
+
* `createSignal({ ...base, done: true })`) whose value the
|
|
4146
|
+
* constructor-time baker (`convertInitialValue`) can't reproduce silently
|
|
4147
|
+
* keeps the field's Go zero value — that baker is static-only
|
|
4148
|
+
* (identifier/member/call operands defer, `parsed-literal-to-go.ts`'s own
|
|
4149
|
+
* docstring). Signal-only: the analyzer deliberately never sets
|
|
4150
|
+
* `MemoInfo.parsed` to an `object-literal` for an object-returning memo
|
|
4151
|
+
* body (`analyzer.ts`'s own docstring — "isn't lowered from the parsed
|
|
4152
|
+
* tree yet"), so there is no structural way to reach this check for a
|
|
4153
|
+
* memo without re-parsing `computation` as text, which the repo's own
|
|
4154
|
+
* convention rules out.
|
|
4155
|
+
*
|
|
4156
|
+
* Deferring silently is harmless UNLESS the SSR template actually reads
|
|
4157
|
+
* the field: a signal that only feeds a spread-attrs bag never reaches
|
|
4158
|
+
* here as a false positive because spread bags bake through their own
|
|
4159
|
+
* `.Spread_<slot>` route (`emitSpreadBagInits`), never `rootFieldRef` —
|
|
4160
|
+
* so `templateReadRootFields` (populated while `generate()` rendered the
|
|
4161
|
+
* template, which always precedes `generateTypes`'s call into this
|
|
4162
|
+
* method) is the exact structural proxy for "the zero value would
|
|
4163
|
+
* actually surface," not merely "the bake failed."
|
|
4164
|
+
*
|
|
4165
|
+
* Scoped to a NON-EMPTY free set on purpose, not every deferred bake: a
|
|
4166
|
+
* fully-static object literal (`createSignal({ id: 'row-1' })`) hits the
|
|
4167
|
+
* same `nil` fallback today but is a separate, untracked silent-divergence
|
|
4168
|
+
* shape with no fixture of its own — loud-ifying it here would silently
|
|
4169
|
+
* widen this fix beyond #2700's actual reproduction (a literal that
|
|
4170
|
+
* references a live prop/signal), so it's left for its own issue instead.
|
|
4171
|
+
*/
|
|
4172
|
+
private refuseUnbakeableDerivedObjectLiteral(
|
|
4173
|
+
name: string,
|
|
4174
|
+
loc: SourceLocation,
|
|
4175
|
+
frees: readonly string[],
|
|
4176
|
+
): void {
|
|
4177
|
+
if (frees.length === 0) return
|
|
4178
|
+
if (!this.state.templateReadRootFields.has(name)) return
|
|
4179
|
+
this.state.errors.push({
|
|
4180
|
+
code: 'BF101',
|
|
4181
|
+
severity: 'error',
|
|
4182
|
+
message: `Signal '${name}' is seeded from an object literal that references live value(s) (${frees.join(', ')}) — the Go template adapter bakes object-typed signal values into Go source at New${this.state.componentName}Props time, and that baker is static-only (identifier/member/call operands defer), so the SSR template's read of it would see the Go zero value instead of the derived object.`,
|
|
4183
|
+
loc,
|
|
4184
|
+
suggestion: {
|
|
4185
|
+
message: `Wrap each SSR read of '${name}()' in /* @client */ so it renders on the client instead, or pass the already-derived object in as a prop.`,
|
|
4186
|
+
escape: [{ kind: 'client-directive' }],
|
|
4187
|
+
},
|
|
4188
|
+
})
|
|
4189
|
+
}
|
|
4190
|
+
|
|
3868
4191
|
/**
|
|
3869
4192
|
* Parse a signal-time initial value of the form `props.X ?? <literal>` —
|
|
3870
4193
|
* or, for destructured components, `x ?? <literal>` — into the source prop
|
|
@@ -4523,6 +4846,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4523
4846
|
* rebinds. Outside any loop the root *is* the dot, so we emit `.Field`.
|
|
4524
4847
|
*/
|
|
4525
4848
|
private rootFieldRef(name: string): string {
|
|
4849
|
+
this.state.templateReadRootFields.add(name)
|
|
4526
4850
|
const prefix = this.inLoop ? '$.' : '.'
|
|
4527
4851
|
return `${prefix}${capitalizeFieldName(name)}`
|
|
4528
4852
|
}
|
|
@@ -4573,6 +4897,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4573
4897
|
return `"${escapeGoString(value)}"`
|
|
4574
4898
|
}
|
|
4575
4899
|
|
|
4900
|
+
/**
|
|
4901
|
+
* The single module-const lookup shared by `resolveModuleNumericConst`
|
|
4902
|
+
* and `resolveModuleBooleanConst` — they differ only in which literal
|
|
4903
|
+
* SHAPE they accept from the same "plain module-level const" search, not
|
|
4904
|
+
* in how that search is performed. A second inline lookup per resolver
|
|
4905
|
+
* would grow `binding-scope-ratchet.test.ts`'s shrink-only floor for this
|
|
4906
|
+
* file (already at 5) for a shape variance the callers can express
|
|
4907
|
+
* themselves instead.
|
|
4908
|
+
*/
|
|
4909
|
+
private findModuleConst(name: string): ConstantInfo | undefined {
|
|
4910
|
+
return this.state.localConstants.find(
|
|
4911
|
+
(k) => k.name === name && k.isModule && !k.containsArrow,
|
|
4912
|
+
)
|
|
4913
|
+
}
|
|
4914
|
+
|
|
4576
4915
|
/**
|
|
4577
4916
|
* Inline a module-level numeric const (`const TRACK = 8`) as its literal
|
|
4578
4917
|
* value. Only a plain numeric initializer qualifies — anything computed or
|
|
@@ -4584,9 +4923,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4584
4923
|
if (this.isCurrentLoopItem(name)) return null
|
|
4585
4924
|
if (this.loopVarRefCount.has(name)) return null
|
|
4586
4925
|
if (this.isOuterLoopParam(name)) return null
|
|
4587
|
-
const c = this.
|
|
4588
|
-
(k) => k.name === name && k.isModule && !k.containsArrow,
|
|
4589
|
-
)
|
|
4926
|
+
const c = this.findModuleConst(name)
|
|
4590
4927
|
if (!c || c.value === undefined) return null
|
|
4591
4928
|
// `value` is reconstructed from source text, so a valid TS literal may carry
|
|
4592
4929
|
// numeric separators (`100_000`). Strip them between digits, then accept a
|
|
@@ -4595,6 +4932,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4595
4932
|
return /^-?\d+(\.\d+)?$/.test(v) ? v : null
|
|
4596
4933
|
}
|
|
4597
4934
|
|
|
4935
|
+
/**
|
|
4936
|
+
* Inline a module-level boolean const (`const OPEN = true`) as its Go
|
|
4937
|
+
* literal (`true`/`false`). Only a plain `true`/`false` initializer
|
|
4938
|
+
* qualifies (#2815) — mirrors `resolveModuleNumericConst`'s shape, sharing
|
|
4939
|
+
* its lookup rather than adding a second `.find(` (see
|
|
4940
|
+
* `findModuleConst`'s docstring).
|
|
4941
|
+
*/
|
|
4942
|
+
private resolveModuleBooleanConst(name: string): string | null {
|
|
4943
|
+
if (this.isCurrentLoopItem(name)) return null
|
|
4944
|
+
if (this.loopVarRefCount.has(name)) return null
|
|
4945
|
+
if (this.isOuterLoopParam(name)) return null
|
|
4946
|
+
const c = this.findModuleConst(name)
|
|
4947
|
+
if (!c || c.value === undefined) return null
|
|
4948
|
+
const v = c.value.trim()
|
|
4949
|
+
return v === 'true' || v === 'false' ? v : null
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4598
4952
|
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
4599
4953
|
if (literalType === 'string') return `"${value}"`
|
|
4600
4954
|
if (literalType === 'null') return 'nil'
|
|
@@ -5780,8 +6134,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5780
6134
|
// A local variable mapped to a signal.
|
|
5781
6135
|
const signal = localVarMap.get(expr.name)
|
|
5782
6136
|
if (signal) {
|
|
6137
|
+
// Root-scope read (#2700's BF101 gate needs it in
|
|
6138
|
+
// `templateReadRootFields`) — `$.` is hardcoded rather than
|
|
6139
|
+
// routed through `rootFieldRef`'s `this.inLoop`-conditional
|
|
6140
|
+
// prefix because a filter predicate must always escape back to
|
|
6141
|
+
// root regardless of loop nesting; call it only for its
|
|
6142
|
+
// registration side effect and keep the prefix here (pullfrog
|
|
6143
|
+
// review, PR #2818).
|
|
6144
|
+
this.rootFieldRef(signal)
|
|
5783
6145
|
return `$.${capitalizeFieldName(signal)}`
|
|
5784
6146
|
}
|
|
6147
|
+
this.rootFieldRef(expr.name)
|
|
5785
6148
|
return `.${capitalizeFieldName(expr.name)}`
|
|
5786
6149
|
}
|
|
5787
6150
|
|
|
@@ -5826,8 +6189,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5826
6189
|
if (expr.callee.kind === 'member' && expr.callee.object.kind === 'identifier' && expr.callee.object.name === param) {
|
|
5827
6190
|
return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`
|
|
5828
6191
|
}
|
|
5829
|
-
// Signal calls: `filter()` -> `$.Filter
|
|
6192
|
+
// Signal calls: `filter()` -> `$.Filter`. Same registration-only
|
|
6193
|
+
// `rootFieldRef` call as the `identifier` case above, for the same
|
|
6194
|
+
// reason (#2700's BF101 gate; pullfrog review, PR #2818).
|
|
5830
6195
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
6196
|
+
this.rootFieldRef(expr.callee.name)
|
|
5831
6197
|
return `$.${capitalizeFieldName(expr.callee.name)}`
|
|
5832
6198
|
}
|
|
5833
6199
|
// A nested callback method call (`other.some(r => …)`) reaching this
|
|
@@ -6190,7 +6556,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6190
6556
|
private loopRowChildPropOverrides(
|
|
6191
6557
|
comp: IRComponent,
|
|
6192
6558
|
): { args: string; helper: 'bf_with_props' | 'bf_reprops' } | null {
|
|
6193
|
-
|
|
6559
|
+
// #2822: every cross-file map below is keyed by the child's own
|
|
6560
|
+
// declared name, not the caller-local alias — see `importAliases`.
|
|
6561
|
+
// Diagnostics still name `comp.name` (what the user actually wrote in
|
|
6562
|
+
// the JSX) since that's the more useful reference for the reader.
|
|
6563
|
+
const declaredName = this.resolveChildName(comp.name)
|
|
6564
|
+
const childShape = this.childComponentShapes.get(declaredName)
|
|
6194
6565
|
const args: string[] = []
|
|
6195
6566
|
// Set by the derived-field check below when at least one overridden prop
|
|
6196
6567
|
// feeds a constructor-derived field AND the child can rebuild itself.
|
|
@@ -6221,12 +6592,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6221
6592
|
// one-shot value on every row. Re-run the constructor per row when the
|
|
6222
6593
|
// child has a rebuilder; refuse when it doesn't.
|
|
6223
6594
|
{
|
|
6224
|
-
const derived = this.childDerivedFieldDeps.get(
|
|
6595
|
+
const derived = this.childDerivedFieldDeps.get(declaredName)
|
|
6225
6596
|
const overriddenField = capitalizeFieldName(prop.name)
|
|
6226
6597
|
const staleField = derived
|
|
6227
6598
|
? [...derived].find(([, deps]) => deps.has(overriddenField))?.[0]
|
|
6228
6599
|
: undefined
|
|
6229
|
-
if (staleField && !this.childRepropsReady.has(
|
|
6600
|
+
if (staleField && !this.childRepropsReady.has(declaredName)) {
|
|
6230
6601
|
this.state.errors.push({
|
|
6231
6602
|
code: 'BF101',
|
|
6232
6603
|
severity: 'error',
|
|
@@ -6243,9 +6614,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6243
6614
|
// The rebuilder is emitted into THIS component's type block, not the
|
|
6244
6615
|
// child's — only here do we know it is actually needed. First parent
|
|
6245
6616
|
// to claim a child owns the registration, so two parents overriding
|
|
6246
|
-
// the same child don't both emit an `init()` for it.
|
|
6247
|
-
|
|
6248
|
-
|
|
6617
|
+
// the same child don't both emit an `init()` for it. Keyed by the
|
|
6618
|
+
// child's DECLARED name (#2822) — `emitRepropsRegistration` below
|
|
6619
|
+
// builds Go type references (`<Name>Props`/`<Name>Input`) from this
|
|
6620
|
+
// same key, and only the declared name has real types to match.
|
|
6621
|
+
if (!this.repropsOwner.has(declaredName)) {
|
|
6622
|
+
this.repropsOwner.set(declaredName, this.state.componentName)
|
|
6249
6623
|
}
|
|
6250
6624
|
}
|
|
6251
6625
|
}
|
|
@@ -6310,7 +6684,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6310
6684
|
// (#2457). Stays LOCAL even though the child's Input field is
|
|
6311
6685
|
// caller-facing. Falls back to the capitalized attribute for a
|
|
6312
6686
|
// cross-file child this run's pre-pass never registered.
|
|
6313
|
-
const fieldName = this.childPropFieldNames.get(
|
|
6687
|
+
const fieldName = this.childPropFieldNames.get(declaredName)?.get(prop.name) ?? capitalizeFieldName(prop.name)
|
|
6314
6688
|
args.push(`${JSON.stringify(fieldName)} ${wrapIfMultiToken(go)}`)
|
|
6315
6689
|
}
|
|
6316
6690
|
if (args.length === 0) return null
|
|
@@ -7305,7 +7679,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7305
7679
|
* `mapArrayAnchored` can hydrate items that render no element.
|
|
7306
7680
|
*/
|
|
7307
7681
|
private loopItemMarker(loop: { bodyIsMultiRoot?: boolean; bodyIsItemConditional?: boolean; key?: string | null }): string {
|
|
7308
|
-
|
|
7682
|
+
// `bfComment` prepends `bf-`, so the argument is `"loop-i"`, not
|
|
7683
|
+
// `"bf-loop-i"` (which would double the prefix to `<!--bf-bf-loop-i-->`
|
|
7684
|
+
// — the same latent bug #2763's fixture caught on the Hono adapter).
|
|
7685
|
+
if (loop.bodyIsMultiRoot) return `{{bfComment "loop-i"}}`
|
|
7309
7686
|
if (loop.bodyIsItemConditional && loop.key) {
|
|
7310
7687
|
// `bfComment` prepends `bf-`, so `printf "loop-i:%v"` yields
|
|
7311
7688
|
// `<!--bf-loop-i:KEY-->`. The key expression resolves against the current
|
|
@@ -7342,6 +7719,75 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7342
7719
|
return name
|
|
7343
7720
|
}
|
|
7344
7721
|
|
|
7722
|
+
/**
|
|
7723
|
+
* #2703/B2: extend `queueDynamicChildrenDefine`'s companion-define delivery
|
|
7724
|
+
* from the reserved `children` slot to NAMED jsx-children props (any
|
|
7725
|
+
* JSX-valued prop other than `children`, e.g. `header={<strong>Title</strong>}`).
|
|
7726
|
+
* A named prop whose value can't be baked into a static Go string by
|
|
7727
|
+
* `emitStaticChildInstances`'s bake chain (`extractTextChildren` /
|
|
7728
|
+
* `extractHtmlChildren` / `extractScopedHtmlChildren`, all null) is queued
|
|
7729
|
+
* here instead, the same way; the caller composes the result into
|
|
7730
|
+
* `bf_with_props` + `bf_tmpl` at the STATIC (non-loop) call site — the one
|
|
7731
|
+
* shape `emitStaticChildInstances` bakes for at all (a component nested in
|
|
7732
|
+
* a loop row never reaches `collectStaticChildInstances`).
|
|
7733
|
+
*
|
|
7734
|
+
* Returns the flat `"FieldName" (bf_tmpl "name" .) ...` argument list ready
|
|
7735
|
+
* to splice into `bf_with_props`, or null when no named prop needs it —
|
|
7736
|
+
* batches every prop that needs dynamic delivery into ONE `bf_with_props`
|
|
7737
|
+
* call rather than nesting one per prop.
|
|
7738
|
+
*/
|
|
7739
|
+
private queueDynamicPropDefine(comp: IRComponent): string | null {
|
|
7740
|
+
const args: string[] = []
|
|
7741
|
+
// #2822: cross-file shapes/field-name maps are keyed by the child's own
|
|
7742
|
+
// declared name, not the caller-local alias — see `importAliases`.
|
|
7743
|
+
const declaredName = this.resolveChildName(comp.name)
|
|
7744
|
+
const childShape = this.childComponentShapes.get(declaredName)
|
|
7745
|
+
for (const prop of comp.props) {
|
|
7746
|
+
if (prop.value.kind !== 'jsx-children' || prop.name === 'children') continue
|
|
7747
|
+
const children = prop.value.children
|
|
7748
|
+
if (this.extractTextChildren(children) !== null) continue
|
|
7749
|
+
if (this.extractHtmlChildren(children) !== null) continue
|
|
7750
|
+
if (this.extractScopedHtmlChildren(children) !== null) continue
|
|
7751
|
+
// A prop routed into the child's rest bag (no declared param —
|
|
7752
|
+
// `loopRowChildPropOverrides`'s identical guard, above) has no named
|
|
7753
|
+
// Go field for `bf_with_props`/`WithProps` to target at all; `WithProps`
|
|
7754
|
+
// silently no-ops on an unmatched field name. Refuse loudly instead of
|
|
7755
|
+
// delivering the value against a field name that can never land —
|
|
7756
|
+
// this shape had no dynamic-delivery route before this method existed
|
|
7757
|
+
// either (the blanket BF101 this method's caller replaced was
|
|
7758
|
+
// unconditional for any unbakeable named prop), so this keeps it loud
|
|
7759
|
+
// rather than trading that refusal for a silent dropped render.
|
|
7760
|
+
if (childShape?.restBagField && !childShape.paramNames.has(prop.name)) {
|
|
7761
|
+
this.state.errors.push({
|
|
7762
|
+
code: 'BF101',
|
|
7763
|
+
severity: 'error',
|
|
7764
|
+
message: `JSX-valued prop '${prop.name}' on <${comp.name}> is dynamic and routes into the child's rest-bag prop ('${childShape.restBagField}') rather than a declared field — there is no named Go struct field for 'bf_with_props' to target`,
|
|
7765
|
+
loc: prop.loc,
|
|
7766
|
+
})
|
|
7767
|
+
continue
|
|
7768
|
+
}
|
|
7769
|
+
const name = `${this.state.componentName}__prop_${prop.name}_${comp.slotId}`
|
|
7770
|
+
if (!this.state.pendingChildrenDefines.some(d => d.name === name)) {
|
|
7771
|
+
this.state.pendingChildrenDefines.push({
|
|
7772
|
+
name,
|
|
7773
|
+
content: this.renderChildren(children),
|
|
7774
|
+
})
|
|
7775
|
+
}
|
|
7776
|
+
// `bf_with_props`/`WithProps` patches the child's Props struct, keyed
|
|
7777
|
+
// by the child's own LOCAL destructured field name — not necessarily
|
|
7778
|
+
// the caller's JSX attribute name (`function Card({ header: h })`
|
|
7779
|
+
// reads `h`, whose field is `H`, not `Header`). `WithProps` silently
|
|
7780
|
+
// no-ops on an unmatched field name, so skipping this lookup would
|
|
7781
|
+
// turn an aliased prop's dynamic delivery into a silent dropped
|
|
7782
|
+
// render. `childPropFieldNames` resolves this exact hazard for the
|
|
7783
|
+
// sibling `bf_with_props` call site (`loopRowChildPropOverrides`,
|
|
7784
|
+
// below) — mirrored here.
|
|
7785
|
+
const fieldName = this.childPropFieldNames.get(declaredName)?.get(prop.name) ?? capitalizeFieldName(prop.name)
|
|
7786
|
+
args.push(`${JSON.stringify(fieldName)} (bf_tmpl ${JSON.stringify(name)} .)`)
|
|
7787
|
+
}
|
|
7788
|
+
return args.length > 0 ? args.join(' ') : null
|
|
7789
|
+
}
|
|
7790
|
+
|
|
7345
7791
|
/**
|
|
7346
7792
|
* Queue a companion define for a loop body component's JSX children. Like
|
|
7347
7793
|
* `queueDynamicChildrenDefine` but temporarily exits the `inLoop` context so
|
|
@@ -7387,6 +7833,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7387
7833
|
}
|
|
7388
7834
|
|
|
7389
7835
|
// In Go templates, components are rendered via {{template "name" data}}.
|
|
7836
|
+
// #2822: the STRING passed to `{{template "..."}}` (and the `bf_reprops`
|
|
7837
|
+
// registry-key argument below) must be the child's own DECLARED name —
|
|
7838
|
+
// that's what the child's own compiled Go file registers its
|
|
7839
|
+
// `{{define "..."}}` block under. A field-access expression like
|
|
7840
|
+
// `.${comp.name}${suffix}` stays keyed by `comp.name` (the caller-local
|
|
7841
|
+
// alias) — it names THIS parent's own struct field, declared and read
|
|
7842
|
+
// consistently within this one parent's generated code, with no
|
|
7843
|
+
// cross-file identity to match.
|
|
7844
|
+
const declaredName = this.resolveChildName(comp.name)
|
|
7390
7845
|
let templateCall: string
|
|
7391
7846
|
if (this.inLoop && (this.loopWrapperStack[this.loopWrapperStack.length - 1] ?? false)) {
|
|
7392
7847
|
// Wrapper-slice loop (body IS this component): `.` is the wrapper struct
|
|
@@ -7405,9 +7860,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7405
7860
|
const bodyData = this.loopScalarItemStack[this.loopScalarItemStack.length - 1]
|
|
7406
7861
|
? '.BfLoopItem'
|
|
7407
7862
|
: '.'
|
|
7408
|
-
templateCall = `{{template "${
|
|
7863
|
+
templateCall = `{{template "${declaredName}" (bf_with_children . (bf_tmpl "${loopBodyDefine}" ${bodyData}))}}`
|
|
7409
7864
|
} else {
|
|
7410
|
-
templateCall = `{{template "${
|
|
7865
|
+
templateCall = `{{template "${declaredName}" .}}`
|
|
7411
7866
|
}
|
|
7412
7867
|
} else if (this.inLoop && comp.slotId) {
|
|
7413
7868
|
// Non-wrapper loop (component nested inside an element item, #2130):
|
|
@@ -7433,26 +7888,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7433
7888
|
// the row's children last, on the rebuilt value.
|
|
7434
7889
|
const base = overrides
|
|
7435
7890
|
? overrides.helper === 'bf_reprops'
|
|
7436
|
-
? `(bf_reprops ${JSON.stringify(
|
|
7891
|
+
? `(bf_reprops ${JSON.stringify(declaredName)} $.${comp.name}${suffix} ${overrides.args})`
|
|
7437
7892
|
: `(bf_with_props $.${comp.name}${suffix} ${overrides.args})`
|
|
7438
7893
|
: `$.${comp.name}${suffix}`
|
|
7439
7894
|
templateCall = loopBodyDefine
|
|
7440
|
-
? `{{template "${
|
|
7441
|
-
: `{{template "${
|
|
7895
|
+
? `{{template "${declaredName}" (bf_with_children ${base} (bf_tmpl "${loopBodyDefine}" .))}}`
|
|
7896
|
+
: `{{template "${declaredName}" ${base}}}`
|
|
7442
7897
|
} else if (this.inLoop) {
|
|
7443
7898
|
// Loop-nested component without a slotId: no parent field to route
|
|
7444
7899
|
// through — legacy passthrough of the current dot.
|
|
7445
|
-
templateCall = `{{template "${
|
|
7900
|
+
templateCall = `{{template "${declaredName}" .}}`
|
|
7446
7901
|
} else if (comp.slotId) {
|
|
7447
7902
|
// Static children with slotId: unique field name based on slotId.
|
|
7448
7903
|
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
7449
7904
|
const childrenDefine = this.queueDynamicChildrenDefine(comp)
|
|
7905
|
+
// #2703/B2: a NAMED jsx-children prop (any JSX-valued prop other than
|
|
7906
|
+
// `children`) that couldn't be baked into the constructor gets the
|
|
7907
|
+
// same treatment `children` already has — delivered at the call site
|
|
7908
|
+
// via a companion define. Props helper stays INNER, same ordering
|
|
7909
|
+
// rule as the loop-nested branch above: `bf_with_children` applies
|
|
7910
|
+
// last, on the props-patched value.
|
|
7911
|
+
const propArgs = this.queueDynamicPropDefine(comp)
|
|
7912
|
+
const base = propArgs
|
|
7913
|
+
? `(bf_with_props .${comp.name}${suffix} ${propArgs})`
|
|
7914
|
+
: `.${comp.name}${suffix}`
|
|
7450
7915
|
templateCall = childrenDefine
|
|
7451
|
-
? `{{template "${
|
|
7452
|
-
: `{{template "${
|
|
7916
|
+
? `{{template "${declaredName}" (bf_with_children ${base} (bf_tmpl "${childrenDefine}" .))}}`
|
|
7917
|
+
: `{{template "${declaredName}" ${base}}}`
|
|
7453
7918
|
} else {
|
|
7454
7919
|
// Static children without slotId: fall back to .ComponentName.
|
|
7455
|
-
templateCall = `{{template "${
|
|
7920
|
+
templateCall = `{{template "${declaredName}" .${comp.name}}}`
|
|
7456
7921
|
}
|
|
7457
7922
|
|
|
7458
7923
|
// A root component in a client component needs a scope comment for the
|