@barefootjs/go-template 0.33.4 → 0.34.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/emit-context.d.ts +12 -0
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/expr/url-builder.d.ts +53 -1
- package/dist/adapter/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +153 -7
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +253 -99
- 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 +261 -103
- package/dist/render-divergences.d.ts +10 -0
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +420 -214
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +333 -9
- package/src/__tests__/lowering-plugin.test.ts +38 -0
- package/src/__tests__/query-href.test.ts +126 -0
- package/src/adapter/emit-context.ts +14 -0
- package/src/adapter/expr/url-builder.ts +114 -8
- package/src/adapter/go-template-adapter.ts +514 -107
- 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 +19 -0
- package/src/render-divergences.ts +12 -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'
|
|
@@ -125,7 +127,7 @@ import { analyzeBakeableStaticChildLoop, scalarToGoLiteral, type BakedStaticChil
|
|
|
125
127
|
import { analyzeBakeableStaticElementLoop } from "./analysis/static-element-loop-bake.ts"
|
|
126
128
|
import type { GoEmitContext } from "./emit-context.ts"
|
|
127
129
|
import { inlineLocalHelperCall } from "./expr/helper-inline.ts"
|
|
128
|
-
import { lowerRegisteredCall, lowerTernary } from "./expr/url-builder.ts"
|
|
130
|
+
import { lowerRegisteredAttrCall, lowerRegisteredCall, lowerRegisteredCallNode, lowerTernary } from "./expr/url-builder.ts"
|
|
129
131
|
import {
|
|
130
132
|
convertInitialValue,
|
|
131
133
|
jsLiteralToGo,
|
|
@@ -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 (
|
|
@@ -2398,6 +2636,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2398
2636
|
lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`)
|
|
2399
2637
|
for (const nested of signalDynamicNested) {
|
|
2400
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)
|
|
2401
2644
|
lines.push(`//`)
|
|
2402
2645
|
lines.push(`// NOTE: \`${arrayField}\` is populated by the route handler, not by`)
|
|
2403
2646
|
lines.push(`// New${componentName}Props — the SSR template iterates over it`)
|
|
@@ -2405,9 +2648,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2405
2648
|
lines.push(`// assign it before passing the props to your renderer. Example:`)
|
|
2406
2649
|
lines.push(`//`)
|
|
2407
2650
|
lines.push(`// props := New${componentName}Props(${inputTypeName}{ /* ... */ })`)
|
|
2408
|
-
lines.push(`// props.${arrayField} = make([]${
|
|
2651
|
+
lines.push(`// props.${arrayField} = make([]${declaredName}Props, len(items))`)
|
|
2409
2652
|
lines.push(`// for i, item := range items {`)
|
|
2410
|
-
lines.push(`// props.${arrayField}[i] = New${
|
|
2653
|
+
lines.push(`// props.${arrayField}[i] = New${declaredName}Props(${declaredName}Input{ /* fields */ })`)
|
|
2411
2654
|
lines.push(`// props.${arrayField}[i].BfParent = props.ScopeID`)
|
|
2412
2655
|
lines.push(`// props.${arrayField}[i].BfMount = "${nested.slotId}"`)
|
|
2413
2656
|
lines.push(`// }`)
|
|
@@ -2470,10 +2713,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2470
2713
|
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
2471
2714
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
2472
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)
|
|
2473
2720
|
|
|
2474
2721
|
for (const child of bodyChildInstances) {
|
|
2475
2722
|
const childVar = `child_${child.fieldName}`
|
|
2476
|
-
|
|
2723
|
+
const childDeclaredName = this.resolveChildName(child.name)
|
|
2724
|
+
lines.push(`\t${childVar} := New${childDeclaredName}Props(${childDeclaredName}Input{`)
|
|
2477
2725
|
lines.push(`\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
2478
2726
|
lines.push(`\t\tBfParent: scopeID,`)
|
|
2479
2727
|
lines.push(`\t\tBfMount: "${child.slotId}",`)
|
|
@@ -2493,7 +2741,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2493
2741
|
lines.push(`\t${varName} := make([]${wrapperType}, len(${dataVar}))`)
|
|
2494
2742
|
lines.push(`\tfor i, item := range ${dataVar} {`)
|
|
2495
2743
|
lines.push(`\t\t${varName}[i] = ${wrapperType}{`)
|
|
2496
|
-
lines.push(`\t\t\t${
|
|
2744
|
+
lines.push(`\t\t\t${declaredName}Props: New${declaredName}Props(${declaredName}Input{`)
|
|
2497
2745
|
lines.push(`\t\t\t\tBfParent: scopeID,`)
|
|
2498
2746
|
lines.push(`\t\t\t\tBfMount: "${nested.slotId}",`)
|
|
2499
2747
|
// Loop-body component's own static props. `key` → BfDataKey below; children
|
|
@@ -2564,11 +2812,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2564
2812
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
2565
2813
|
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
2566
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)
|
|
2567
2818
|
|
|
2568
2819
|
// Child sub-component instances created once (identical scope IDs per row).
|
|
2569
2820
|
for (const child of bodyChildInstances) {
|
|
2570
2821
|
const childVar = `child_${child.fieldName}`
|
|
2571
|
-
|
|
2822
|
+
const childDeclaredName = this.resolveChildName(child.name)
|
|
2823
|
+
lines.push(`\t${childVar} := New${childDeclaredName}Props(${childDeclaredName}Input{`)
|
|
2572
2824
|
lines.push(`\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
2573
2825
|
lines.push(`\t\tBfParent: scopeID,`)
|
|
2574
2826
|
lines.push(`\t\tBfMount: "${child.slotId}",`)
|
|
@@ -2587,7 +2839,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2587
2839
|
lines.push(`\t${varName} := make([]${wrapperType}, len(bakedData))`)
|
|
2588
2840
|
lines.push(`\tfor i, item := range bakedData {`)
|
|
2589
2841
|
lines.push(`\t\t${varName}[i] = ${wrapperType}{`)
|
|
2590
|
-
lines.push(`\t\t\t${
|
|
2842
|
+
lines.push(`\t\t\t${declaredName}Props: New${declaredName}Props(${declaredName}Input{`)
|
|
2591
2843
|
lines.push(`\t\t\t\tBfParent: scopeID,`)
|
|
2592
2844
|
lines.push(`\t\t\t\tBfMount: "${nested.slotId}",`)
|
|
2593
2845
|
lines.push(`\t\t\t}),`)
|
|
@@ -2715,20 +2967,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2715
2967
|
visit(prop.type, desiredName, prop.name)
|
|
2716
2968
|
}
|
|
2717
2969
|
const fields = this.structFieldsFor(typeInfo)
|
|
2718
|
-
this.
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
loc: SYNTH_TYPE_LOC,
|
|
2725
|
-
})
|
|
2726
|
-
const goFields = fields.map(
|
|
2727
|
-
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).`,
|
|
2728
2976
|
)
|
|
2729
|
-
lines.push(`// ${desiredName} is a synthesised type for an anonymous object type (#2674).`)
|
|
2730
|
-
lines.push(`type ${desiredName} struct {\n${goFields.join('\n')}\n}`)
|
|
2731
|
-
lines.push('')
|
|
2732
2977
|
}
|
|
2733
2978
|
|
|
2734
2979
|
const visitArrayElem = (elemType: TypeInfo | undefined, parentName: string, propName: string): void => {
|
|
@@ -2781,28 +3026,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2781
3026
|
}
|
|
2782
3027
|
|
|
2783
3028
|
private emitSynthStructs(lines: string[], ir: ComponentIR, componentName: string): void {
|
|
2784
|
-
// Synthesise a struct for each untyped object-array signal
|
|
2785
|
-
//
|
|
2786
|
-
//
|
|
2787
|
-
//
|
|
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.
|
|
2788
3036
|
this.state.synthStructTypes = new Map<string, TypeInfo>()
|
|
2789
3037
|
for (const signal of ir.metadata.signals) {
|
|
2790
3038
|
if (signal.envReader) continue // env signal has no bakeable initial shape (#2057)
|
|
2791
3039
|
const synth = this.synthesizeStructFromSignal(signal, componentName)
|
|
2792
3040
|
if (!synth) continue
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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))
|
|
2806
3056
|
}
|
|
2807
3057
|
}
|
|
2808
3058
|
|
|
@@ -3082,10 +3332,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3082
3332
|
// real — a second same-named field here is a Go compile error
|
|
3083
3333
|
// ("redeclared"), not just dead code.
|
|
3084
3334
|
if (this.isOrphanedClientOnlyNested(nested)) continue
|
|
3085
|
-
// 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.
|
|
3086
3341
|
const elemType = nested.bodyChildren?.length
|
|
3087
3342
|
? this.loopBodyWrapperName(componentName, nested)
|
|
3088
|
-
: `${nested.name}Props`
|
|
3343
|
+
: `${this.resolveChildName(nested.name)}Props`
|
|
3089
3344
|
if (nested.isDynamic && !nested.isPropDerived) {
|
|
3090
3345
|
// Dynamic signal-array loops are template-only.
|
|
3091
3346
|
lines.push(`\t${nested.name}s []${elemType} \`json:"-"\``)
|
|
@@ -3129,7 +3384,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3129
3384
|
|
|
3130
3385
|
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
3131
3386
|
for (const child of staticChildren) {
|
|
3132
|
-
|
|
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:"-"\``)
|
|
3133
3393
|
}
|
|
3134
3394
|
|
|
3135
3395
|
// Top-level intrinsic-element spreads: each gets a `Spread_<slotId>
|
|
@@ -3879,6 +4139,55 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3879
4139
|
return resolveSignalParsedThroughSeedPlan(this.state, signal)
|
|
3880
4140
|
}
|
|
3881
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
|
+
|
|
3882
4191
|
/**
|
|
3883
4192
|
* Parse a signal-time initial value of the form `props.X ?? <literal>` —
|
|
3884
4193
|
* or, for destructured components, `x ?? <literal>` — into the source prop
|
|
@@ -4537,6 +4846,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4537
4846
|
* rebinds. Outside any loop the root *is* the dot, so we emit `.Field`.
|
|
4538
4847
|
*/
|
|
4539
4848
|
private rootFieldRef(name: string): string {
|
|
4849
|
+
this.state.templateReadRootFields.add(name)
|
|
4540
4850
|
const prefix = this.inLoop ? '$.' : '.'
|
|
4541
4851
|
return `${prefix}${capitalizeFieldName(name)}`
|
|
4542
4852
|
}
|
|
@@ -4587,6 +4897,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4587
4897
|
return `"${escapeGoString(value)}"`
|
|
4588
4898
|
}
|
|
4589
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
|
+
|
|
4590
4915
|
/**
|
|
4591
4916
|
* Inline a module-level numeric const (`const TRACK = 8`) as its literal
|
|
4592
4917
|
* value. Only a plain numeric initializer qualifies — anything computed or
|
|
@@ -4598,9 +4923,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4598
4923
|
if (this.isCurrentLoopItem(name)) return null
|
|
4599
4924
|
if (this.loopVarRefCount.has(name)) return null
|
|
4600
4925
|
if (this.isOuterLoopParam(name)) return null
|
|
4601
|
-
const c = this.
|
|
4602
|
-
(k) => k.name === name && k.isModule && !k.containsArrow,
|
|
4603
|
-
)
|
|
4926
|
+
const c = this.findModuleConst(name)
|
|
4604
4927
|
if (!c || c.value === undefined) return null
|
|
4605
4928
|
// `value` is reconstructed from source text, so a valid TS literal may carry
|
|
4606
4929
|
// numeric separators (`100_000`). Strip them between digits, then accept a
|
|
@@ -4609,6 +4932,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4609
4932
|
return /^-?\d+(\.\d+)?$/.test(v) ? v : null
|
|
4610
4933
|
}
|
|
4611
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
|
+
|
|
4612
4952
|
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
4613
4953
|
if (literalType === 'string') return `"${value}"`
|
|
4614
4954
|
if (literalType === 'null') return 'nil'
|
|
@@ -4616,6 +4956,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4616
4956
|
}
|
|
4617
4957
|
|
|
4618
4958
|
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
4959
|
+
// #2842: a registered lowering (the built-in `queryHref`, or any userland
|
|
4960
|
+
// plugin) must win in EVERY position this shared dispatcher reaches — a
|
|
4961
|
+
// ternary branch, a template-literal interpolation, a binary operand —
|
|
4962
|
+
// not only when the call is the whole expression (`convertExpressionToGo`'s
|
|
4963
|
+
// own early return). Same precedence as that top-level path, where the
|
|
4964
|
+
// registry is consulted before signal-read resolution and template
|
|
4965
|
+
// primitives.
|
|
4966
|
+
const lowered = lowerRegisteredCallNode(this.emitCtx, callee, args)
|
|
4967
|
+
if (lowered !== null) return lowered
|
|
4619
4968
|
// Signal call: count() -> .Count (or $.Count inside a loop). An env-signal
|
|
4620
4969
|
// binding (`searchParams()`, or an aliased `sp()`) resolves to the canonical
|
|
4621
4970
|
// `.SearchParams` field regardless of the JS name.
|
|
@@ -5794,8 +6143,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5794
6143
|
// A local variable mapped to a signal.
|
|
5795
6144
|
const signal = localVarMap.get(expr.name)
|
|
5796
6145
|
if (signal) {
|
|
6146
|
+
// Root-scope read (#2700's BF101 gate needs it in
|
|
6147
|
+
// `templateReadRootFields`) — `$.` is hardcoded rather than
|
|
6148
|
+
// routed through `rootFieldRef`'s `this.inLoop`-conditional
|
|
6149
|
+
// prefix because a filter predicate must always escape back to
|
|
6150
|
+
// root regardless of loop nesting; call it only for its
|
|
6151
|
+
// registration side effect and keep the prefix here (pullfrog
|
|
6152
|
+
// review, PR #2818).
|
|
6153
|
+
this.rootFieldRef(signal)
|
|
5797
6154
|
return `$.${capitalizeFieldName(signal)}`
|
|
5798
6155
|
}
|
|
6156
|
+
this.rootFieldRef(expr.name)
|
|
5799
6157
|
return `.${capitalizeFieldName(expr.name)}`
|
|
5800
6158
|
}
|
|
5801
6159
|
|
|
@@ -5840,8 +6198,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5840
6198
|
if (expr.callee.kind === 'member' && expr.callee.object.kind === 'identifier' && expr.callee.object.name === param) {
|
|
5841
6199
|
return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`
|
|
5842
6200
|
}
|
|
5843
|
-
// Signal calls: `filter()` -> `$.Filter
|
|
6201
|
+
// Signal calls: `filter()` -> `$.Filter`. Same registration-only
|
|
6202
|
+
// `rootFieldRef` call as the `identifier` case above, for the same
|
|
6203
|
+
// reason (#2700's BF101 gate; pullfrog review, PR #2818).
|
|
5844
6204
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
6205
|
+
this.rootFieldRef(expr.callee.name)
|
|
5845
6206
|
return `$.${capitalizeFieldName(expr.callee.name)}`
|
|
5846
6207
|
}
|
|
5847
6208
|
// A nested callback method call (`other.some(r => …)`) reaching this
|
|
@@ -6204,7 +6565,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6204
6565
|
private loopRowChildPropOverrides(
|
|
6205
6566
|
comp: IRComponent,
|
|
6206
6567
|
): { args: string; helper: 'bf_with_props' | 'bf_reprops' } | null {
|
|
6207
|
-
|
|
6568
|
+
// #2822: every cross-file map below is keyed by the child's own
|
|
6569
|
+
// declared name, not the caller-local alias — see `importAliases`.
|
|
6570
|
+
// Diagnostics still name `comp.name` (what the user actually wrote in
|
|
6571
|
+
// the JSX) since that's the more useful reference for the reader.
|
|
6572
|
+
const declaredName = this.resolveChildName(comp.name)
|
|
6573
|
+
const childShape = this.childComponentShapes.get(declaredName)
|
|
6208
6574
|
const args: string[] = []
|
|
6209
6575
|
// Set by the derived-field check below when at least one overridden prop
|
|
6210
6576
|
// feeds a constructor-derived field AND the child can rebuild itself.
|
|
@@ -6235,12 +6601,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6235
6601
|
// one-shot value on every row. Re-run the constructor per row when the
|
|
6236
6602
|
// child has a rebuilder; refuse when it doesn't.
|
|
6237
6603
|
{
|
|
6238
|
-
const derived = this.childDerivedFieldDeps.get(
|
|
6604
|
+
const derived = this.childDerivedFieldDeps.get(declaredName)
|
|
6239
6605
|
const overriddenField = capitalizeFieldName(prop.name)
|
|
6240
6606
|
const staleField = derived
|
|
6241
6607
|
? [...derived].find(([, deps]) => deps.has(overriddenField))?.[0]
|
|
6242
6608
|
: undefined
|
|
6243
|
-
if (staleField && !this.childRepropsReady.has(
|
|
6609
|
+
if (staleField && !this.childRepropsReady.has(declaredName)) {
|
|
6244
6610
|
this.state.errors.push({
|
|
6245
6611
|
code: 'BF101',
|
|
6246
6612
|
severity: 'error',
|
|
@@ -6257,9 +6623,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6257
6623
|
// The rebuilder is emitted into THIS component's type block, not the
|
|
6258
6624
|
// child's — only here do we know it is actually needed. First parent
|
|
6259
6625
|
// to claim a child owns the registration, so two parents overriding
|
|
6260
|
-
// the same child don't both emit an `init()` for it.
|
|
6261
|
-
|
|
6262
|
-
|
|
6626
|
+
// the same child don't both emit an `init()` for it. Keyed by the
|
|
6627
|
+
// child's DECLARED name (#2822) — `emitRepropsRegistration` below
|
|
6628
|
+
// builds Go type references (`<Name>Props`/`<Name>Input`) from this
|
|
6629
|
+
// same key, and only the declared name has real types to match.
|
|
6630
|
+
if (!this.repropsOwner.has(declaredName)) {
|
|
6631
|
+
this.repropsOwner.set(declaredName, this.state.componentName)
|
|
6263
6632
|
}
|
|
6264
6633
|
}
|
|
6265
6634
|
}
|
|
@@ -6324,7 +6693,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6324
6693
|
// (#2457). Stays LOCAL even though the child's Input field is
|
|
6325
6694
|
// caller-facing. Falls back to the capitalized attribute for a
|
|
6326
6695
|
// cross-file child this run's pre-pass never registered.
|
|
6327
|
-
const fieldName = this.childPropFieldNames.get(
|
|
6696
|
+
const fieldName = this.childPropFieldNames.get(declaredName)?.get(prop.name) ?? capitalizeFieldName(prop.name)
|
|
6328
6697
|
args.push(`${JSON.stringify(fieldName)} ${wrapIfMultiToken(go)}`)
|
|
6329
6698
|
}
|
|
6330
6699
|
if (args.length === 0) return null
|
|
@@ -7378,7 +7747,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7378
7747
|
*/
|
|
7379
7748
|
private queueDynamicPropDefine(comp: IRComponent): string | null {
|
|
7380
7749
|
const args: string[] = []
|
|
7381
|
-
|
|
7750
|
+
// #2822: cross-file shapes/field-name maps are keyed by the child's own
|
|
7751
|
+
// declared name, not the caller-local alias — see `importAliases`.
|
|
7752
|
+
const declaredName = this.resolveChildName(comp.name)
|
|
7753
|
+
const childShape = this.childComponentShapes.get(declaredName)
|
|
7382
7754
|
for (const prop of comp.props) {
|
|
7383
7755
|
if (prop.value.kind !== 'jsx-children' || prop.name === 'children') continue
|
|
7384
7756
|
const children = prop.value.children
|
|
@@ -7419,7 +7791,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7419
7791
|
// render. `childPropFieldNames` resolves this exact hazard for the
|
|
7420
7792
|
// sibling `bf_with_props` call site (`loopRowChildPropOverrides`,
|
|
7421
7793
|
// below) — mirrored here.
|
|
7422
|
-
const fieldName = this.childPropFieldNames.get(
|
|
7794
|
+
const fieldName = this.childPropFieldNames.get(declaredName)?.get(prop.name) ?? capitalizeFieldName(prop.name)
|
|
7423
7795
|
args.push(`${JSON.stringify(fieldName)} (bf_tmpl ${JSON.stringify(name)} .)`)
|
|
7424
7796
|
}
|
|
7425
7797
|
return args.length > 0 ? args.join(' ') : null
|
|
@@ -7470,6 +7842,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7470
7842
|
}
|
|
7471
7843
|
|
|
7472
7844
|
// In Go templates, components are rendered via {{template "name" data}}.
|
|
7845
|
+
// #2822: the STRING passed to `{{template "..."}}` (and the `bf_reprops`
|
|
7846
|
+
// registry-key argument below) must be the child's own DECLARED name —
|
|
7847
|
+
// that's what the child's own compiled Go file registers its
|
|
7848
|
+
// `{{define "..."}}` block under. A field-access expression like
|
|
7849
|
+
// `.${comp.name}${suffix}` stays keyed by `comp.name` (the caller-local
|
|
7850
|
+
// alias) — it names THIS parent's own struct field, declared and read
|
|
7851
|
+
// consistently within this one parent's generated code, with no
|
|
7852
|
+
// cross-file identity to match.
|
|
7853
|
+
const declaredName = this.resolveChildName(comp.name)
|
|
7473
7854
|
let templateCall: string
|
|
7474
7855
|
if (this.inLoop && (this.loopWrapperStack[this.loopWrapperStack.length - 1] ?? false)) {
|
|
7475
7856
|
// Wrapper-slice loop (body IS this component): `.` is the wrapper struct
|
|
@@ -7488,9 +7869,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7488
7869
|
const bodyData = this.loopScalarItemStack[this.loopScalarItemStack.length - 1]
|
|
7489
7870
|
? '.BfLoopItem'
|
|
7490
7871
|
: '.'
|
|
7491
|
-
templateCall = `{{template "${
|
|
7872
|
+
templateCall = `{{template "${declaredName}" (bf_with_children . (bf_tmpl "${loopBodyDefine}" ${bodyData}))}}`
|
|
7492
7873
|
} else {
|
|
7493
|
-
templateCall = `{{template "${
|
|
7874
|
+
templateCall = `{{template "${declaredName}" .}}`
|
|
7494
7875
|
}
|
|
7495
7876
|
} else if (this.inLoop && comp.slotId) {
|
|
7496
7877
|
// Non-wrapper loop (component nested inside an element item, #2130):
|
|
@@ -7516,16 +7897,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7516
7897
|
// the row's children last, on the rebuilt value.
|
|
7517
7898
|
const base = overrides
|
|
7518
7899
|
? overrides.helper === 'bf_reprops'
|
|
7519
|
-
? `(bf_reprops ${JSON.stringify(
|
|
7900
|
+
? `(bf_reprops ${JSON.stringify(declaredName)} $.${comp.name}${suffix} ${overrides.args})`
|
|
7520
7901
|
: `(bf_with_props $.${comp.name}${suffix} ${overrides.args})`
|
|
7521
7902
|
: `$.${comp.name}${suffix}`
|
|
7522
7903
|
templateCall = loopBodyDefine
|
|
7523
|
-
? `{{template "${
|
|
7524
|
-
: `{{template "${
|
|
7904
|
+
? `{{template "${declaredName}" (bf_with_children ${base} (bf_tmpl "${loopBodyDefine}" .))}}`
|
|
7905
|
+
: `{{template "${declaredName}" ${base}}}`
|
|
7525
7906
|
} else if (this.inLoop) {
|
|
7526
7907
|
// Loop-nested component without a slotId: no parent field to route
|
|
7527
7908
|
// through — legacy passthrough of the current dot.
|
|
7528
|
-
templateCall = `{{template "${
|
|
7909
|
+
templateCall = `{{template "${declaredName}" .}}`
|
|
7529
7910
|
} else if (comp.slotId) {
|
|
7530
7911
|
// Static children with slotId: unique field name based on slotId.
|
|
7531
7912
|
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
@@ -7541,11 +7922,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7541
7922
|
? `(bf_with_props .${comp.name}${suffix} ${propArgs})`
|
|
7542
7923
|
: `.${comp.name}${suffix}`
|
|
7543
7924
|
templateCall = childrenDefine
|
|
7544
|
-
? `{{template "${
|
|
7545
|
-
: `{{template "${
|
|
7925
|
+
? `{{template "${declaredName}" (bf_with_children ${base} (bf_tmpl "${childrenDefine}" .))}}`
|
|
7926
|
+
: `{{template "${declaredName}" ${base}}}`
|
|
7546
7927
|
} else {
|
|
7547
7928
|
// Static children without slotId: fall back to .ComponentName.
|
|
7548
|
-
templateCall = `{{template "${
|
|
7929
|
+
templateCall = `{{template "${declaredName}" .${comp.name}}}`
|
|
7549
7930
|
}
|
|
7550
7931
|
|
|
7551
7932
|
// A root component in a client component needs a scope comment for the
|
|
@@ -7649,10 +8030,30 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7649
8030
|
? value.expr
|
|
7650
8031
|
: value.expr.slice(0, value.expr.indexOf('?')).trim(),
|
|
7651
8032
|
)
|
|
7652
|
-
|
|
7653
|
-
|
|
8033
|
+
// #2842 / #2743: a `query` guard-list consequent (queryHref) takes
|
|
8034
|
+
// the whole-attribute `bf_attr` route INSIDE the `{{if}}`, so
|
|
8035
|
+
// html/template's URL-context inference never percent-encodes it —
|
|
8036
|
+
// the same route the direct-call and non-undefined-ternary shapes
|
|
8037
|
+
// take. The consequent is passed alone (not the ternary): omission
|
|
8038
|
+
// needs this `{{if}}` wrapper, which `bf_attr` can't express on its
|
|
8039
|
+
// own. Any other consequent (a `helper-call` plugin, an unmatched
|
|
8040
|
+
// call, a member/literal) keeps the ordinary `name="{{…}}"` form —
|
|
8041
|
+
// `call()` now consults the registry too, so a plugin call renders
|
|
8042
|
+
// its `bf_<helper>` pipeline there instead of invalid Go syntax.
|
|
8043
|
+
const attrConsequent = lowerRegisteredAttrCall(this.emitCtx, name, parsed.consequent)
|
|
8044
|
+
const body = attrConsequent !== null
|
|
8045
|
+
? attrConsequent
|
|
8046
|
+
: `${name}="{{${this.renderParsedExpr(parsed.consequent)}}}"`
|
|
7654
8047
|
return `${preamble}{{if ${goCond}}}${body}{{end}}`
|
|
7655
8048
|
}
|
|
8049
|
+
// #2743 follow-up (pullfrog review on #2841): a `query` guard-list
|
|
8050
|
+
// value (queryHref) reachable through EITHER branch of this ternary
|
|
8051
|
+
// still needs the whole-attribute `bf_attr` route — otherwise the
|
|
8052
|
+
// pipeline below lands inside the ordinary `name="{{...}}"` wrapper
|
|
8053
|
+
// and html/template's URL-context inference still percent-encodes
|
|
8054
|
+
// whichever branch wins at render time. See `lowerRegisteredAttrCall`.
|
|
8055
|
+
const attrTernary = lowerRegisteredAttrCall(this.emitCtx, name, parsed)
|
|
8056
|
+
if (attrTernary !== null) return attrTernary
|
|
7656
8057
|
// #2335: the ternary lowers to the pipeline-position `(bf_ternary …)`
|
|
7657
8058
|
// value (no longer a `{{if}}…{{end}}` fragment), so wrap it in a single
|
|
7658
8059
|
// `{{…}}` action inside the attribute string — `name="{{bf_ternary …}}"`.
|
|
@@ -7662,6 +8063,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7662
8063
|
// Inline Go template syntax with embedded `{{...}}` actions.
|
|
7663
8064
|
return `${name}="${this.renderParsedExpr(parsed)}"`
|
|
7664
8065
|
}
|
|
8066
|
+
// #2743: a `query` guard-list value (queryHref) emits the WHOLE
|
|
8067
|
+
// attribute via `bf_attr` (template.HTMLAttr) so html/template's
|
|
8068
|
+
// URL-context inference on the attribute name never percent-encodes
|
|
8069
|
+
// the base. See `lowerRegisteredAttrCall`.
|
|
8070
|
+
const attrAction = lowerRegisteredAttrCall(this.emitCtx, name, parsed)
|
|
8071
|
+
if (attrAction !== null) return attrAction
|
|
7665
8072
|
// Nullish-attribute omission: when the attribute value is a BARE reference
|
|
7666
8073
|
// to a nillable (`interface{}`) prop field, guard emission on `ne .X nil`
|
|
7667
8074
|
// so an unset optional prop drops the attribute entirely instead of
|