@barefootjs/go-template 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/go-template-adapter.d.ts +295 -9
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +1282 -137
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1283 -136
- package/dist/index.js +1282 -137
- package/dist/test-render.d.ts +8 -0
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/build.test.ts +40 -1
- package/src/__tests__/derived-state-memo.test.ts +371 -0
- package/src/__tests__/go-template-adapter.test.ts +267 -7
- package/src/adapter/go-template-adapter.ts +2205 -188
- package/src/build.ts +4 -0
- package/src/test-render.ts +109 -27
|
@@ -64,8 +64,11 @@ import {
|
|
|
64
64
|
collectContextConsumers,
|
|
65
65
|
isLowerableObjectRestDestructure,
|
|
66
66
|
type ContextConsumer,
|
|
67
|
+
collectModuleStringConsts as collectModuleStringConstsShared,
|
|
68
|
+
searchParamsLocalNames
|
|
67
69
|
} from '@barefootjs/jsx'
|
|
68
70
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
71
|
+
import { BF_REGION } from '@barefootjs/shared'
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
74
|
* Go-template adapter's IRNode render context. Only `isRootOfClientComponent`
|
|
@@ -88,6 +91,18 @@ interface NestedComponentInfo extends IRLoopChildComponent {
|
|
|
88
91
|
* name (`item`), so the loop-child init can stamp `data-key` per item. */
|
|
89
92
|
loopKey?: string
|
|
90
93
|
loopParam?: string
|
|
94
|
+
/** The loop body component's JSX children (e.g. the 4 `<TableCell>` nodes
|
|
95
|
+
* inside `<TableRow>` in data-table's `.map(payment => <TableRow>…</TableRow>)`).
|
|
96
|
+
* Non-empty when the loop body component has children that need a companion
|
|
97
|
+
* define rendered via `bf_with_children` + `bf_tmpl`. (#1897) */
|
|
98
|
+
bodyChildren?: IRNode[]
|
|
99
|
+
/** The loop's array expression for baking (e.g. `sortedData()`) */
|
|
100
|
+
loopArray?: string
|
|
101
|
+
/** The enclosing loop's `markerId` (e.g. `l0`) for unique naming */
|
|
102
|
+
loopMarkerId?: string
|
|
103
|
+
/** The loop item's TS type (`Payment` from `sortedData().map(payment => …)`),
|
|
104
|
+
* resolved to Go struct fields for the wrapper struct's datum fields. */
|
|
105
|
+
loopItemType?: TypeInfo | null
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
interface StaticChildInstance {
|
|
@@ -182,6 +197,19 @@ interface PropFallbackVar {
|
|
|
182
197
|
zeroLiteral: string
|
|
183
198
|
}
|
|
184
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Scope for `lowerCtorExpr` — lowering a JS expression to Go in the
|
|
202
|
+
* `NewXxxProps` constructor context (#1897 PostList derived state).
|
|
203
|
+
*/
|
|
204
|
+
interface CtorLowerEnv {
|
|
205
|
+
/** Local names bound to `searchParams()` (`const sp = searchParams()`). */
|
|
206
|
+
searchParamsVars: Set<string>
|
|
207
|
+
/** Helper-param name → its already-lowered Go argument, for inlining. */
|
|
208
|
+
params: Map<string, string>
|
|
209
|
+
/** Component-scope const names currently being inlined (cycle guard). */
|
|
210
|
+
consts?: Set<string>
|
|
211
|
+
}
|
|
212
|
+
|
|
185
213
|
export interface GoTemplateAdapterOptions {
|
|
186
214
|
/** Go package name for generated types (default: 'components') */
|
|
187
215
|
packageName?: string
|
|
@@ -388,13 +416,49 @@ function buildUnsupportedSuggestion(support: SupportResult): string {
|
|
|
388
416
|
return `${support.reason}\n\n${GO_REMEDIATION_OPTIONS}`
|
|
389
417
|
}
|
|
390
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Parenthesize a compound Go template argument (`or .Checked false`) so a
|
|
421
|
+
* primitive call reads it as ONE argument — unwrapped, the parser splits
|
|
422
|
+
* it into three and `bf_string` fails with "want 1 got 3" (#1896,
|
|
423
|
+
* DropdownMenuCheckboxItem's `String(props.checked ?? false)`).
|
|
424
|
+
*/
|
|
425
|
+
function wrapGoArg(arg: string): string {
|
|
426
|
+
if (!/\s/.test(arg)) return arg
|
|
427
|
+
if (arg.startsWith('(') && arg.endsWith(')')) return arg
|
|
428
|
+
return `(${arg})`
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Make an equality comparison string-tolerant when exactly one side is a
|
|
433
|
+
* Go string literal (#1896): JS `sorted === 'asc'` is loosely false for
|
|
434
|
+
* `sorted = false`, but Go's template `eq` ERRORS on bool-vs-string
|
|
435
|
+
* (`incompatible types for comparison` — DataTableColumnHeader's
|
|
436
|
+
* `'asc' | 'desc' | false` union prop). Routing the non-literal side
|
|
437
|
+
* through `bf_string` preserves JS comparison semantics for every
|
|
438
|
+
* concrete type while keeping same-kind comparisons untouched.
|
|
439
|
+
*/
|
|
440
|
+
function stringTolerantEqOperands(l: string, r: string): [string, string] {
|
|
441
|
+
const isStrLit = (x: string) => /^"(?:[^"\\]|\\.)*"$/.test(x)
|
|
442
|
+
if (isStrLit(l) === isStrLit(r)) return [l, r]
|
|
443
|
+
// Keep `wrapGoArg`'s parentheses: a compound operand must reach
|
|
444
|
+
// `bf_string` as ONE argument — `(bf_string (or .Placement "top"))`,
|
|
445
|
+
// not `(bf_string or .Placement "top")` (which the template parser
|
|
446
|
+
// reads as three arguments and fails at runtime).
|
|
447
|
+
const wrap = (x: string) => (isStrLit(x) ? x : `(bf_string ${wrapGoArg(x)})`)
|
|
448
|
+
return [wrap(l), wrap(r)]
|
|
449
|
+
}
|
|
450
|
+
|
|
391
451
|
const GO_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
392
|
-
'JSON.stringify': { arity: 1, emit: (args) => `bf_json ${args[0]}` },
|
|
393
|
-
'String': { arity: 1, emit: (args) => `bf_string ${args[0]}` },
|
|
394
|
-
'Number': { arity: 1, emit: (args) => `bf_number ${args[0]}` },
|
|
395
|
-
'Math.floor': { arity: 1, emit: (args) => `bf_floor ${args[0]}` },
|
|
396
|
-
'Math.ceil': { arity: 1, emit: (args) => `bf_ceil ${args[0]}` },
|
|
397
|
-
'Math.round': { arity: 1, emit: (args) => `bf_round ${args[0]}` },
|
|
452
|
+
'JSON.stringify': { arity: 1, emit: (args) => `bf_json ${wrapGoArg(args[0])}` },
|
|
453
|
+
'String': { arity: 1, emit: (args) => `bf_string ${wrapGoArg(args[0])}` },
|
|
454
|
+
'Number': { arity: 1, emit: (args) => `bf_number ${wrapGoArg(args[0])}` },
|
|
455
|
+
'Math.floor': { arity: 1, emit: (args) => `bf_floor ${wrapGoArg(args[0])}` },
|
|
456
|
+
'Math.ceil': { arity: 1, emit: (args) => `bf_ceil ${wrapGoArg(args[0])}` },
|
|
457
|
+
'Math.round': { arity: 1, emit: (args) => `bf_round ${wrapGoArg(args[0])}` },
|
|
458
|
+
// Two-arg forms only; an N-arg `Math.min(a, b, c)` falls through to the
|
|
459
|
+
// standard BF101 unsupported-call diagnostic via the arity gate.
|
|
460
|
+
'Math.min': { arity: 2, emit: (args) => `bf_min ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
|
|
461
|
+
'Math.max': { arity: 2, emit: (args) => `bf_max ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
|
|
398
462
|
}
|
|
399
463
|
|
|
400
464
|
export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter, IRNodeEmitter<GoRenderCtx> {
|
|
@@ -467,6 +531,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
467
531
|
private componentName: string = ''
|
|
468
532
|
private options: Required<GoTemplateAdapterOptions>
|
|
469
533
|
private inLoop: boolean = false
|
|
534
|
+
/**
|
|
535
|
+
* Companion `{{define "<Component>__children_<slot>"}}` blocks queued
|
|
536
|
+
* while rendering the template body (#1896): JSX children passed to an
|
|
537
|
+
* imported child component that contain template actions (nested
|
|
538
|
+
* components, dynamic text) can't be baked to a static `Children`
|
|
539
|
+
* string — they render through a per-call-site define executed via
|
|
540
|
+
* `bf_tmpl` with the PARENT's data, and the result is injected into
|
|
541
|
+
* the child props with `bf_with_children`. Flushed after the main
|
|
542
|
+
* define in `generate()`.
|
|
543
|
+
*/
|
|
544
|
+
private pendingChildrenDefines: Array<{ name: string; content: string }> = []
|
|
470
545
|
private loopParamStack: string[] = []
|
|
471
546
|
private loopVarRefCount: Map<string, number> = new Map()
|
|
472
547
|
/** Stack of destructure-param binding maps (binding name → Go accessor on the
|
|
@@ -475,6 +550,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
475
550
|
* refusing with BF104. (#1310) */
|
|
476
551
|
private loopBindingStack: Array<Map<string, string>> = []
|
|
477
552
|
private errors: CompilerError[] = []
|
|
553
|
+
/** The current IR's memos, stashed like `localConstants` so nested memo
|
|
554
|
+
* resolution (a ternary memo whose condition is another memo, #1896)
|
|
555
|
+
* can recurse without threading the list through every signature. */
|
|
556
|
+
private currentMemos: Array<{ name: string; computation: string; deps: string[] }> = []
|
|
478
557
|
private propsObjectName: string | null = null
|
|
479
558
|
/**
|
|
480
559
|
* Component-scoped rest binding identifier (`function({ a, ...rest }: P)`
|
|
@@ -504,6 +583,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
504
583
|
* initial-value baker.
|
|
505
584
|
*/
|
|
506
585
|
private synthStructTypes: Map<string, TypeInfo> = new Map()
|
|
586
|
+
/** Full type definitions from the current IR, stashed for loop-datum field resolution (#1897). */
|
|
587
|
+
private currentTypeDefinitions: TypeDefinition[] = []
|
|
507
588
|
|
|
508
589
|
/** Set during type generation when any emit references
|
|
509
590
|
* `template.HTML(...)`; toggles the `"html/template"` import. */
|
|
@@ -562,6 +643,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
562
643
|
*/
|
|
563
644
|
private contextConsumers: ContextConsumer[] = []
|
|
564
645
|
|
|
646
|
+
/**
|
|
647
|
+
* (#1922) Local binding names the request-scoped `searchParams()` env signal
|
|
648
|
+
* is imported under (handles `import { searchParams as sp }`). A zero-arg call
|
|
649
|
+
* on one of these names lowers to the canonical `.SearchParams` field
|
|
650
|
+
* regardless of the JS alias. Set at `generate()` / `generateTypes()` entry.
|
|
651
|
+
*/
|
|
652
|
+
private searchParamsLocals: Set<string> = new Set()
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Names of component-scope arrow-const helpers (`const sortClass = …`),
|
|
656
|
+
* eligible for call-site inlining (#1897). Precomputed per component so the
|
|
657
|
+
* inliner can skip the AST parse for the common non-helper expression.
|
|
658
|
+
*/
|
|
659
|
+
private localHelperNames: Set<string> = new Set()
|
|
660
|
+
|
|
661
|
+
/** Set when a constructor-context lowering emits a `strings.` call (#1897), so
|
|
662
|
+
* `strings` is added to the generated types file's import block. */
|
|
663
|
+
private needsStringsImport = false
|
|
664
|
+
|
|
665
|
+
/** Component-scope derived consts referenced by the template during rendering
|
|
666
|
+
* (e.g. `root` → `.Root`). `generateTypes` emits a computed field for each
|
|
667
|
+
* that's resolvable and non-colliding (#1897). */
|
|
668
|
+
private referencedDerivedConsts: Set<string> = new Set()
|
|
669
|
+
|
|
670
|
+
/** Array-memo name → the handler-filled loop slice field its `.map()` feeds
|
|
671
|
+
* (e.g. `visible` → `PostListItems`). Lets `<memo>().length` lower to
|
|
672
|
+
* `len .<Slice>` instead of `len .<Memo>` (a nil/unset memo field) — the
|
|
673
|
+
* slice IS the rendered (filtered) items, so its length is the count (#1897). */
|
|
674
|
+
private memoBackedLoopSlice: Map<string, string> = new Map()
|
|
675
|
+
|
|
565
676
|
/** Child component name → the contexts it consumes (cross-component, for provider wiring). */
|
|
566
677
|
private childContextConsumers: Map<string, ContextConsumer[]> = new Map()
|
|
567
678
|
|
|
@@ -597,12 +708,20 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
597
708
|
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput {
|
|
598
709
|
this.componentName = ir.metadata.componentName
|
|
599
710
|
this.errors = []
|
|
711
|
+
this.referencedDerivedConsts = new Set()
|
|
600
712
|
this.templateVarCounter = 0
|
|
713
|
+
this.pendingChildrenDefines = []
|
|
601
714
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
602
715
|
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
603
716
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
604
717
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
718
|
+
this.localHelperNames = new Set(
|
|
719
|
+
this.localConstants.filter(c => !c.isModule && c.containsArrow).map(c => c.name),
|
|
720
|
+
)
|
|
721
|
+
this.currentMemos = ir.metadata.memos ?? []
|
|
722
|
+
this.currentTypeDefinitions = ir.metadata.typeDefinitions ?? []
|
|
605
723
|
this.contextConsumers = collectContextConsumers(ir.metadata)
|
|
724
|
+
this.searchParamsLocals = searchParamsLocalNames(ir.metadata)
|
|
606
725
|
// (#checkbox) Enumerate inherited-attribute accesses (props-object pattern)
|
|
607
726
|
// before computing the nillable set / rendering, so the synthetic params
|
|
608
727
|
// participate in attribute omission and field binding uniformly. Shared
|
|
@@ -639,6 +758,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
639
758
|
const isIfStatement = ir.root.type === 'if-statement'
|
|
640
759
|
|
|
641
760
|
this.rootScopeNodes = collectRootScopeNodes(ir.root)
|
|
761
|
+
// Map each array memo that backs a loop (`<memo>().map(...)`) to that loop's
|
|
762
|
+
// handler-filled slice field, so `<memo>().length` can lower to the slice's
|
|
763
|
+
// length (#1897 PostList status count). Built before rendering — the
|
|
764
|
+
// `.length` reference can appear before the loop in source order.
|
|
765
|
+
this.memoBackedLoopSlice = new Map()
|
|
766
|
+
for (const nested of this.findNestedComponents(ir.root)) {
|
|
767
|
+
const memoName = this.extractMemoNameFromLoopArray(nested.loopArray)
|
|
768
|
+
if (memoName) this.memoBackedLoopSlice.set(memoName, `${nested.name}s`)
|
|
769
|
+
}
|
|
642
770
|
const templateBody = isIfStatement
|
|
643
771
|
? this.renderIfStatement(ir.root as IRIfStatement, { isRootOfClientComponent: hasInteractivity })
|
|
644
772
|
: this.renderNode(ir.root, { isRootOfClientComponent: hasInteractivity && isRootComponent })
|
|
@@ -648,7 +776,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
648
776
|
? ''
|
|
649
777
|
: this.generateScriptRegistrations(ir, options?.scriptBaseName)
|
|
650
778
|
|
|
651
|
-
|
|
779
|
+
let template = `{{define "${this.componentName}"}}\n${scriptRegistrations}${templateBody}\n{{end}}\n`
|
|
780
|
+
// Flush the companion children defines (#1896) — they execute with
|
|
781
|
+
// the parent's data via `bf_tmpl`, so they belong to this
|
|
782
|
+
// component's template output.
|
|
783
|
+
for (const d of this.pendingChildrenDefines) {
|
|
784
|
+
template += `{{define "${d.name}"}}${d.content}{{end}}\n`
|
|
785
|
+
}
|
|
652
786
|
const types = this.generateTypes(ir)
|
|
653
787
|
|
|
654
788
|
// Merge collected errors into IR errors
|
|
@@ -958,13 +1092,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
958
1092
|
// round-tripped IR, so this must be applied here too; the method is
|
|
959
1093
|
// idempotent. `propsObjectName` is needed by the scan.
|
|
960
1094
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
1095
|
+
// Mirror `generate()` for the rest binding too (#1896):
|
|
1096
|
+
// `classifySpreadBagSource` decides whether a `{...props}` slot is an
|
|
1097
|
+
// Input-side bag by comparing against `this.restPropsName`. Without
|
|
1098
|
+
// this line the stash holds whichever component `generate()` ran
|
|
1099
|
+
// LAST — in a multi-component file (`tabs/index.tsx`) that is a
|
|
1100
|
+
// different component, so the Input struct dropped its `Props`
|
|
1101
|
+
// bag field while `NewXxxProps` (which reads the per-IR metadata)
|
|
1102
|
+
// still emitted `Spread_N: in.Props` — `in.Props undefined`.
|
|
1103
|
+
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
961
1104
|
augmentInheritedPropAccesses(ir)
|
|
962
1105
|
// Mirror `generate()`: the `NewXxxProps` initializer computes memo SSR
|
|
963
1106
|
// values, which inline module string consts and resolve `Record`-index
|
|
964
1107
|
// lookups — both need the const tables populated on this standalone entry.
|
|
965
1108
|
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
966
1109
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
1110
|
+
this.localHelperNames = new Set(
|
|
1111
|
+
this.localConstants.filter(c => !c.isModule && c.containsArrow).map(c => c.name),
|
|
1112
|
+
)
|
|
1113
|
+
this.currentMemos = ir.metadata.memos ?? []
|
|
1114
|
+
this.currentTypeDefinitions = ir.metadata.typeDefinitions ?? []
|
|
967
1115
|
this.contextConsumers = collectContextConsumers(ir.metadata)
|
|
1116
|
+
this.searchParamsLocals = searchParamsLocalNames(ir.metadata)
|
|
968
1117
|
const lines: string[] = []
|
|
969
1118
|
|
|
970
1119
|
const componentName = ir.metadata.componentName
|
|
@@ -1029,6 +1178,52 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1029
1178
|
// Find nested components (loops with childComponent)
|
|
1030
1179
|
const nestedComponents = this.findNestedComponents(ir.root)
|
|
1031
1180
|
|
|
1181
|
+
// (#1897) When a loop's `itemType` is null, resolve the element type
|
|
1182
|
+
// from the source array so wrapper structs get correct datum fields.
|
|
1183
|
+
// Two cases:
|
|
1184
|
+
// 1. Memo-derived: `sortedData()` → resolve through the memo's SSR
|
|
1185
|
+
// path to the module const it returns (block-body memo baking).
|
|
1186
|
+
// 2. Direct module const: `payments` → look up the constant directly.
|
|
1187
|
+
for (const nested of nestedComponents) {
|
|
1188
|
+
if (nested.loopItemType || !nested.loopArray) continue
|
|
1189
|
+
|
|
1190
|
+
// Case 1: memo-derived loop array (`sortedData()`)
|
|
1191
|
+
const memoName = this.extractMemoNameFromLoopArray(nested.loopArray)
|
|
1192
|
+
if (memoName) {
|
|
1193
|
+
const memo = ir.metadata.memos.find(m => m.name === memoName)
|
|
1194
|
+
if (memo) {
|
|
1195
|
+
const blockReturn = this.resolveBlockBodyMemoModuleConst(
|
|
1196
|
+
memo.computation, ir.metadata.signals,
|
|
1197
|
+
)
|
|
1198
|
+
if (blockReturn) {
|
|
1199
|
+
const constant = (ir.metadata.localConstants ?? []).find(
|
|
1200
|
+
c => c.name === blockReturn.constName && c.origin?.scope === 'module',
|
|
1201
|
+
)
|
|
1202
|
+
if (constant?.type?.elementType) {
|
|
1203
|
+
nested.loopItemType = constant.type.elementType
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
continue
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Case 2: direct module-const array reference (`payments`)
|
|
1211
|
+
const directConst = (ir.metadata.localConstants ?? []).find(
|
|
1212
|
+
c => c.name === nested.loopArray && c.origin?.scope === 'module',
|
|
1213
|
+
)
|
|
1214
|
+
if (directConst?.type?.elementType) {
|
|
1215
|
+
nested.loopItemType = directConst.type.elementType
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// (#1897) Generate wrapper structs for loop body components with JSX children.
|
|
1220
|
+
// The wrapper embeds the child component's Props and adds datum fields from
|
|
1221
|
+
// the loop's item type + static child instances from the body children.
|
|
1222
|
+
for (const nested of nestedComponents) {
|
|
1223
|
+
if (!nested.bodyChildren || nested.bodyChildren.length === 0) continue
|
|
1224
|
+
this.generateLoopBodyWrapperStruct(lines, componentName, nested)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1032
1227
|
// Build prop type overrides from signal types
|
|
1033
1228
|
const propTypeOverrides = this.buildPropTypeOverrides(ir)
|
|
1034
1229
|
|
|
@@ -1045,10 +1240,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1045
1240
|
this.generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots)
|
|
1046
1241
|
|
|
1047
1242
|
// Generate Props struct for main component
|
|
1243
|
+
this.needsStringsImport = false
|
|
1048
1244
|
this.generatePropsStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots)
|
|
1049
1245
|
|
|
1050
1246
|
// Generate NewXxxProps function
|
|
1051
|
-
this.generateNewPropsFunction(lines, ir, componentName, nestedComponents, spreadSlots)
|
|
1247
|
+
this.generateNewPropsFunction(lines, ir, componentName, nestedComponents, spreadSlots, propTypeOverrides)
|
|
1052
1248
|
|
|
1053
1249
|
// Imports come at the top, but `usesHtmlTemplate` is only known
|
|
1054
1250
|
// after the body has been generated. Compose package + imports +
|
|
@@ -1062,6 +1258,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1062
1258
|
if (this.usesFmt) header.push('\t"fmt"')
|
|
1063
1259
|
if (this.usesHtmlTemplate) header.push('\t"html/template"')
|
|
1064
1260
|
header.push('\t"math/rand"')
|
|
1261
|
+
if (this.needsStringsImport) header.push('\t"strings"')
|
|
1065
1262
|
header.push('')
|
|
1066
1263
|
header.push('\tbf "github.com/barefootjs/runtime/bf"')
|
|
1067
1264
|
header.push(')')
|
|
@@ -1319,6 +1516,39 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1319
1516
|
return nillable
|
|
1320
1517
|
}
|
|
1321
1518
|
|
|
1519
|
+
/**
|
|
1520
|
+
* Whether the component reads the request-scoped `searchParams()`
|
|
1521
|
+
* environment signal (router v0.5, #1922). Detected from `searchParamsLocals`
|
|
1522
|
+
* — the binding names the shared `searchParamsLocalNames` helper found at
|
|
1523
|
+
* `generate()` / `generateTypes()` entry, covering any local name (including
|
|
1524
|
+
* an aliased `import { searchParams as sp }`). When non-empty the generated
|
|
1525
|
+
* structs carry a `SearchParams bf.SearchParams` binding the route handler
|
|
1526
|
+
* fills per request and the template reads via `.SearchParams.Get "key"`.
|
|
1527
|
+
*
|
|
1528
|
+
* Guarded against a name collision with a user prop / signal / memo also
|
|
1529
|
+
* called `searchParams`: that author owns the `SearchParams` field, so the
|
|
1530
|
+
* env-signal field is dropped (the reference would resolve to their value).
|
|
1531
|
+
*/
|
|
1532
|
+
private usesSearchParams(ir: ComponentIR): boolean {
|
|
1533
|
+
if (this.searchParamsLocals.size === 0) return false
|
|
1534
|
+
// Every other source that contributes a Props/Input struct field: a
|
|
1535
|
+
// collision on `SearchParams` would redeclare the field and break the Go
|
|
1536
|
+
// compile. Covers props, signals, memos, `useContext` consumers, and the
|
|
1537
|
+
// `{...rest}` bag — the same field-producing sets the struct emitters draw
|
|
1538
|
+
// from. When the author already owns `SearchParams`, drop the env-signal
|
|
1539
|
+
// field (their binding lowers to the same `.SearchParams` reference).
|
|
1540
|
+
const taken = new Set<string>([
|
|
1541
|
+
...ir.metadata.propsParams.map(p => this.capitalizeFieldName(p.name)),
|
|
1542
|
+
...ir.metadata.signals.map(s => this.capitalizeFieldName(s.getter)),
|
|
1543
|
+
...ir.metadata.memos.map(m => this.capitalizeFieldName(m.name)),
|
|
1544
|
+
...this.contextConsumers.map(c => this.contextFieldName(c)),
|
|
1545
|
+
])
|
|
1546
|
+
if (ir.metadata.restPropsName) {
|
|
1547
|
+
taken.add(this.capitalizeFieldName(ir.metadata.restPropsName))
|
|
1548
|
+
}
|
|
1549
|
+
return !taken.has('SearchParams')
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1322
1552
|
/**
|
|
1323
1553
|
* Generate Input struct for a component
|
|
1324
1554
|
*/
|
|
@@ -1339,6 +1569,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1339
1569
|
lines.push('\tBfParent string // Optional: parent scope id')
|
|
1340
1570
|
lines.push('\tBfMount string // Optional: slot id in parent')
|
|
1341
1571
|
|
|
1572
|
+
// (#1922) Request-scoped `searchParams()` binding. The route handler
|
|
1573
|
+
// builds it from the request URL (`bf.NewSearchParams(r.URL.RawQuery)`);
|
|
1574
|
+
// the zero value is an empty query, so an omitted field resolves every
|
|
1575
|
+
// `.Get` to "" — the author's `?? default` then renders.
|
|
1576
|
+
if (this.usesSearchParams(ir)) {
|
|
1577
|
+
lines.push('\tSearchParams bf.SearchParams // Optional: request query for searchParams()')
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1342
1580
|
// Static + prop-derived nested components appear in Input;
|
|
1343
1581
|
// signal-backed dynamic ones are template-only
|
|
1344
1582
|
const inputNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
|
|
@@ -1423,6 +1661,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1423
1661
|
// Add Scripts field for dynamic script collection
|
|
1424
1662
|
lines.push('\tScripts *bf.ScriptCollector `json:"-"`')
|
|
1425
1663
|
|
|
1664
|
+
// (#1922) Request-scoped `searchParams()` SSR value. Read by the
|
|
1665
|
+
// template as `.SearchParams.Get "key"`. Not serialised for hydration
|
|
1666
|
+
// (`json:"-"`) — the client re-reads `window.location.search` itself.
|
|
1667
|
+
if (this.usesSearchParams(ir)) {
|
|
1668
|
+
lines.push('\tSearchParams bf.SearchParams `json:"-"`')
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1426
1671
|
// Collect nested component array field names to skip from propsParams
|
|
1427
1672
|
const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
|
|
1428
1673
|
|
|
@@ -1492,15 +1737,35 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1492
1737
|
lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
|
|
1493
1738
|
}
|
|
1494
1739
|
|
|
1495
|
-
// Add memos to Props (they are computed values needed for SSR)
|
|
1740
|
+
// Add memos to Props (they are computed values needed for SSR).
|
|
1741
|
+
// Skip a memo whose name collides with an already-emitted prop field
|
|
1742
|
+
// (#1896): `const className = createMemo(() => props.className ?? '')`
|
|
1743
|
+
// — common in site/ui (AccordionContent, PaginationLink) — would
|
|
1744
|
+
// otherwise redeclare `ClassName`. Both readers (the memo usage and
|
|
1745
|
+
// the inherited `props.className` access) lower to the same `.Field`,
|
|
1746
|
+
// so the prop field carries the value; the memo's `?? fallback` is
|
|
1747
|
+
// folded into the prop's initializer in `generateNewPropsFunction`.
|
|
1496
1748
|
for (const memo of ir.metadata.memos) {
|
|
1497
1749
|
const fieldName = this.capitalizeFieldName(memo.name)
|
|
1750
|
+
if (propFieldNames.has(fieldName)) continue
|
|
1498
1751
|
const jsonTag = this.toJsonTag(memo.name)
|
|
1499
1752
|
// Memos that depend on number signals are usually numbers
|
|
1500
1753
|
const goType = this.inferMemoType(memo, ir.metadata.signals, propsParamMap)
|
|
1501
1754
|
lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
|
|
1502
1755
|
}
|
|
1503
1756
|
|
|
1757
|
+
// (#1897 PostList) Computed fields for component-scope derived string consts
|
|
1758
|
+
// the template references (e.g. `root = base || '/'`). Not serialised — the
|
|
1759
|
+
// route handler doesn't supply them; `NewXxxProps` computes them.
|
|
1760
|
+
const takenForDerivedConsts = new Set<string>([
|
|
1761
|
+
...ir.metadata.propsParams.map(p => this.capitalizeFieldName(p.name)),
|
|
1762
|
+
...ir.metadata.signals.map(s => this.capitalizeFieldName(s.getter)),
|
|
1763
|
+
...ir.metadata.memos.map(m => this.capitalizeFieldName(m.name)),
|
|
1764
|
+
])
|
|
1765
|
+
for (const f of this.computeDerivedConstFields(takenForDerivedConsts)) {
|
|
1766
|
+
lines.push(`\t${f.name} string \`json:"-"\``)
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1504
1769
|
// `useContext` consumer fields (skip names already taken by a prop /
|
|
1505
1770
|
// signal / memo field).
|
|
1506
1771
|
const takenProps = new Set<string>([
|
|
@@ -1515,14 +1780,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1515
1780
|
|
|
1516
1781
|
// Add array fields for nested components (for template rendering)
|
|
1517
1782
|
for (const nested of nestedComponents) {
|
|
1783
|
+
// (#1897) Loop body with JSX children → use the wrapper struct type
|
|
1784
|
+
const elemType = nested.bodyChildren?.length
|
|
1785
|
+
? this.loopBodyWrapperName(componentName, nested)
|
|
1786
|
+
: `${nested.name}Props`
|
|
1518
1787
|
if (nested.isDynamic && !nested.isPropDerived) {
|
|
1519
1788
|
// Dynamic signal array loops: template-only, not in JSON
|
|
1520
|
-
lines.push(`\t${nested.name}s []${
|
|
1789
|
+
lines.push(`\t${nested.name}s []${elemType} \`json:"-"\``)
|
|
1521
1790
|
} else {
|
|
1522
1791
|
// Static arrays and prop-derived dynamic arrays: include in JSON
|
|
1523
1792
|
// so the client can hydrate via mapArray or forEach
|
|
1524
1793
|
const jsonTag = this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`)
|
|
1525
|
-
lines.push(`\t${nested.name}s []${
|
|
1794
|
+
lines.push(`\t${nested.name}s []${elemType} \`json:"${jsonTag}"\``)
|
|
1526
1795
|
}
|
|
1527
1796
|
}
|
|
1528
1797
|
|
|
@@ -1549,6 +1818,90 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1549
1818
|
lines.push('')
|
|
1550
1819
|
}
|
|
1551
1820
|
|
|
1821
|
+
/**
|
|
1822
|
+
* (#1897) Generate a wrapper struct for loop body components with JSX children.
|
|
1823
|
+
* The wrapper embeds the child component's Props, adds datum fields from the
|
|
1824
|
+
* loop's item type, and adds static child instance fields for sub-components
|
|
1825
|
+
* within the loop body children (e.g. the `TableCell` instances inside
|
|
1826
|
+
* `<TableRow>…</TableRow>`).
|
|
1827
|
+
*/
|
|
1828
|
+
private generateLoopBodyWrapperStruct(
|
|
1829
|
+
lines: string[],
|
|
1830
|
+
parentComponentName: string,
|
|
1831
|
+
nested: NestedComponentInfo,
|
|
1832
|
+
): void {
|
|
1833
|
+
const wrapperName = this.loopBodyWrapperName(parentComponentName, nested)
|
|
1834
|
+
|
|
1835
|
+
// Resolve datum fields from the loop's item type
|
|
1836
|
+
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
1837
|
+
|
|
1838
|
+
// Collect static child instances from the body children
|
|
1839
|
+
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!)
|
|
1840
|
+
|
|
1841
|
+
lines.push(`// ${wrapperName} wraps ${nested.name}Props with per-row loop datum`)
|
|
1842
|
+
lines.push(`// fields and child component slots for the loop body children. (#1897)`)
|
|
1843
|
+
lines.push(`type ${wrapperName} struct {`)
|
|
1844
|
+
lines.push(`\t${nested.name}Props`)
|
|
1845
|
+
for (const f of datumFields) {
|
|
1846
|
+
lines.push(`\t${f.goName} ${f.goType} \`json:"-"\``)
|
|
1847
|
+
}
|
|
1848
|
+
for (const child of bodyChildInstances) {
|
|
1849
|
+
lines.push(`\t${child.fieldName} ${child.name}Props \`json:"-"\``)
|
|
1850
|
+
}
|
|
1851
|
+
lines.push('}')
|
|
1852
|
+
lines.push('')
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/** Extract a memo name from a loop array expression like `sortedData()` → `sortedData`. */
|
|
1856
|
+
private extractMemoNameFromLoopArray(loopArray: string | undefined): string | null {
|
|
1857
|
+
if (!loopArray) return null
|
|
1858
|
+
const match = loopArray.match(/^(\w+)\(\)$/)
|
|
1859
|
+
return match ? match[1] : null
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/** Stable name for the wrapper struct: `<Parent><Child>L<Marker>Ctx` */
|
|
1863
|
+
private loopBodyWrapperName(parentName: string, nested: NestedComponentInfo): string {
|
|
1864
|
+
return `${parentName}${nested.name}L${nested.loopMarkerId ?? '0'}Ctx`
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/** Resolve a loop item's TypeInfo to Go struct fields for the wrapper. */
|
|
1868
|
+
private resolveLoopDatumFields(
|
|
1869
|
+
itemType: TypeInfo | null | undefined,
|
|
1870
|
+
): Array<{ tsName: string; goName: string; goType: string }> {
|
|
1871
|
+
if (!itemType) return []
|
|
1872
|
+
const typeName = itemType.raw?.replace(/\[\]$/, '') ?? itemType.raw
|
|
1873
|
+
if (!typeName) return []
|
|
1874
|
+
for (const td of this.currentTypeDefinitions) {
|
|
1875
|
+
if (td.name === typeName) {
|
|
1876
|
+
const fields: Array<{ tsName: string; goName: string; goType: string }> = []
|
|
1877
|
+
for (const prop of td.properties ?? []) {
|
|
1878
|
+
if (!GoTemplateAdapter.GO_IDENTIFIER.test(prop.name)) continue
|
|
1879
|
+
fields.push({
|
|
1880
|
+
tsName: prop.name,
|
|
1881
|
+
goName: this.capitalizeFieldName(prop.name),
|
|
1882
|
+
goType: this.typeInfoToGo(prop.type),
|
|
1883
|
+
})
|
|
1884
|
+
}
|
|
1885
|
+
if (fields.length > 0) return fields
|
|
1886
|
+
break
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
const structFields = this.localStructFields.get(typeName)
|
|
1890
|
+
if (structFields) {
|
|
1891
|
+
return Array.from(structFields, ([tsName, goName]) => ({ tsName, goName, goType: 'interface{}' }))
|
|
1892
|
+
}
|
|
1893
|
+
return []
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/** Collect static child instances from loop body children for the wrapper struct. */
|
|
1897
|
+
private collectBodyChildInstances(bodyChildren: IRNode[]): StaticChildInstance[] {
|
|
1898
|
+
const result: StaticChildInstance[] = []
|
|
1899
|
+
for (const child of bodyChildren) {
|
|
1900
|
+
this.collectStaticChildInstancesRecursive(child, result, false, new Map())
|
|
1901
|
+
}
|
|
1902
|
+
return result
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1552
1905
|
/**
|
|
1553
1906
|
* Generate NewXxxProps function
|
|
1554
1907
|
*/
|
|
@@ -1557,7 +1910,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1557
1910
|
ir: ComponentIR,
|
|
1558
1911
|
componentName: string,
|
|
1559
1912
|
nestedComponents: NestedComponentInfo[],
|
|
1560
|
-
spreadSlots: SpreadSlotInfo[]
|
|
1913
|
+
spreadSlots: SpreadSlotInfo[],
|
|
1914
|
+
propTypeOverrides: Map<string, string>,
|
|
1561
1915
|
): void {
|
|
1562
1916
|
const inputTypeName = `${componentName}Input`
|
|
1563
1917
|
const propsTypeName = `${componentName}Props`
|
|
@@ -1569,7 +1923,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1569
1923
|
// field, the SSR template iterates over it, but
|
|
1570
1924
|
// `NewTodoAppProps(TodoAppInput{Initial: ...})` returns it empty
|
|
1571
1925
|
// and the page renders a blank list (#1442 echo TodoApp repro).
|
|
1572
|
-
|
|
1926
|
+
// (#1897) Split dynamic nested: components with body children get auto-populated
|
|
1927
|
+
// from baked memo data; components without stay handler-populated.
|
|
1928
|
+
const dynamicWithBody = nestedComponents.filter(
|
|
1929
|
+
n => n.isDynamic && !n.isPropDerived && n.bodyChildren && n.bodyChildren.length > 0,
|
|
1930
|
+
)
|
|
1931
|
+
const signalDynamicNested = nestedComponents.filter(
|
|
1932
|
+
n => n.isDynamic && !n.isPropDerived && !(n.bodyChildren && n.bodyChildren.length > 0),
|
|
1933
|
+
)
|
|
1573
1934
|
lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`)
|
|
1574
1935
|
for (const nested of signalDynamicNested) {
|
|
1575
1936
|
const arrayField = `${nested.name}s`
|
|
@@ -1598,27 +1959,103 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1598
1959
|
// Signal-backed dynamic arrays are set manually by the handler.
|
|
1599
1960
|
const staticNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
|
|
1600
1961
|
|
|
1601
|
-
//
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1962
|
+
// Track which wrapper vars have been emitted so the return struct can
|
|
1963
|
+
// conditionally include them (both static-with-body and dynamic-with-body
|
|
1964
|
+
// paths may emit wrappers).
|
|
1965
|
+
const emittedWrapperVars = new Set<string>()
|
|
1966
|
+
|
|
1967
|
+
// (#1897) Split static nested into those with and without body children.
|
|
1968
|
+
// Static loops with body children backed by module consts bake the data
|
|
1969
|
+
// directly (like the dynamic-with-body path) because Input items don't
|
|
1970
|
+
// carry the datum fields the wrapper struct needs.
|
|
1971
|
+
const staticWithBody = staticNested.filter(
|
|
1972
|
+
n => n.bodyChildren && n.bodyChildren.length > 0,
|
|
1973
|
+
)
|
|
1974
|
+
const staticWithoutBody = staticNested.filter(
|
|
1975
|
+
n => !n.bodyChildren || n.bodyChildren.length === 0,
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
// Handle static nested components WITHOUT body children (original path)
|
|
1979
|
+
for (const nested of staticWithoutBody) {
|
|
1980
|
+
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1981
|
+
lines.push(`\t${varName} := make([]${nested.name}Props, len(in.${nested.name}s))`)
|
|
1982
|
+
lines.push(`\tfor i, item := range in.${nested.name}s {`)
|
|
1983
|
+
lines.push(`\t\t${varName}[i] = New${nested.name}Props(item)`)
|
|
1984
|
+
lines.push(`\t\t${varName}[i].BfParent = scopeID`)
|
|
1985
|
+
lines.push(`\t\t${varName}[i].BfMount = "${nested.slotId}"`)
|
|
1986
|
+
const keyField = loopKeyToGoFieldPath(nested.loopKey, nested.loopParam)
|
|
1987
|
+
if (keyField) {
|
|
1988
|
+
lines.push(`\t\t${varName}[i].BfDataKey = fmt.Sprint(${keyField})`)
|
|
1989
|
+
this.usesFmt = true
|
|
1990
|
+
}
|
|
1991
|
+
lines.push('\t}')
|
|
1992
|
+
lines.push('')
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// (#1897) Handle static nested components WITH body children.
|
|
1996
|
+
// Bake the module-const array into the constructor so wrapper structs
|
|
1997
|
+
// get their datum fields directly from the data, not from Input items
|
|
1998
|
+
// (Input items only carry child-component params, not loop datum fields).
|
|
1999
|
+
for (const nested of staticWithBody) {
|
|
2000
|
+
const loopArray = nested.loopArray
|
|
2001
|
+
const moduleConst = loopArray
|
|
2002
|
+
? (ir.metadata.localConstants ?? []).find(
|
|
2003
|
+
c => c.name === loopArray && c.origin?.scope === 'module' && c.value && c.type,
|
|
2004
|
+
)
|
|
2005
|
+
: null
|
|
2006
|
+
const bakedValue = moduleConst?.type
|
|
2007
|
+
? this.convertInitialValue(moduleConst.value!, moduleConst.type, ir.metadata.propsParams)
|
|
2008
|
+
: null
|
|
2009
|
+
if (!bakedValue || bakedValue === 'nil' || bakedValue === '0') continue
|
|
2010
|
+
|
|
2011
|
+
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
2012
|
+
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
2013
|
+
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
2014
|
+
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!)
|
|
2015
|
+
|
|
2016
|
+
for (const child of bodyChildInstances) {
|
|
2017
|
+
const childVar = `child_${child.fieldName}`
|
|
2018
|
+
lines.push(`\t${childVar} := New${child.name}Props(${child.name}Input{`)
|
|
2019
|
+
lines.push(`\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
2020
|
+
lines.push(`\t\tBfParent: scopeID,`)
|
|
2021
|
+
lines.push(`\t\tBfMount: "${child.slotId}",`)
|
|
2022
|
+
for (const prop of child.props) {
|
|
2023
|
+
if (prop.value.kind === 'literal') {
|
|
2024
|
+
lines.push(`\t\t${this.capitalizeFieldName(prop.name)}: ${this.goLiteral(prop.value.value)},`)
|
|
2025
|
+
} else if (prop.value.kind === 'boolean-shorthand' || prop.value.kind === 'boolean-attr') {
|
|
2026
|
+
lines.push(`\t\t${this.capitalizeFieldName(prop.name)}: true,`)
|
|
2027
|
+
}
|
|
1618
2028
|
}
|
|
1619
|
-
lines.push(
|
|
1620
|
-
|
|
1621
|
-
|
|
2029
|
+
lines.push(`\t})`)
|
|
2030
|
+
}
|
|
2031
|
+
if (bodyChildInstances.length > 0) lines.push('')
|
|
2032
|
+
|
|
2033
|
+
const dataVar = `${varName}Data`
|
|
2034
|
+
lines.push(`\t${dataVar} := ${bakedValue}`)
|
|
2035
|
+
lines.push(`\t${varName} := make([]${wrapperType}, len(${dataVar}))`)
|
|
2036
|
+
lines.push(`\tfor i, item := range ${dataVar} {`)
|
|
2037
|
+
lines.push(`\t\t${varName}[i] = ${wrapperType}{`)
|
|
2038
|
+
lines.push(`\t\t\t${nested.name}Props: New${nested.name}Props(${nested.name}Input{`)
|
|
2039
|
+
lines.push(`\t\t\t\tBfParent: scopeID,`)
|
|
2040
|
+
lines.push(`\t\t\t\tBfMount: "${nested.slotId}",`)
|
|
2041
|
+
lines.push(`\t\t\t}),`)
|
|
2042
|
+
for (const f of datumFields) {
|
|
2043
|
+
lines.push(`\t\t\t${f.goName}: item.${f.goName},`)
|
|
2044
|
+
}
|
|
2045
|
+
for (const child of bodyChildInstances) {
|
|
2046
|
+
lines.push(`\t\t\t${child.fieldName}: child_${child.fieldName},`)
|
|
2047
|
+
}
|
|
2048
|
+
lines.push(`\t\t}`)
|
|
2049
|
+
lines.push(`\t\t${varName}[i].BfParent = scopeID`)
|
|
2050
|
+
lines.push(`\t\t${varName}[i].BfMount = "${nested.slotId}"`)
|
|
2051
|
+
const keyField = loopKeyToGoFieldPath(nested.loopKey, nested.loopParam)
|
|
2052
|
+
if (keyField) {
|
|
2053
|
+
lines.push(`\t\t${varName}[i].BfDataKey = fmt.Sprint(${keyField})`)
|
|
2054
|
+
this.usesFmt = true
|
|
2055
|
+
}
|
|
2056
|
+
lines.push('\t}')
|
|
2057
|
+
lines.push('')
|
|
2058
|
+
emittedWrapperVars.add(varName)
|
|
1622
2059
|
}
|
|
1623
2060
|
|
|
1624
2061
|
// (#1423) Collect signal-time prop fallbacks: when a signal is
|
|
@@ -1638,12 +2075,80 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1638
2075
|
}
|
|
1639
2076
|
if (propFallbackVars.size > 0) lines.push('')
|
|
1640
2077
|
|
|
2078
|
+
// (#1897) Build wrapper items for dynamic loop body components whose array
|
|
2079
|
+
// bakes to a module-const via a memo. Creates the wrapper slice with
|
|
2080
|
+
// embedded child Props + datum fields + static sub-component instances.
|
|
2081
|
+
const propsParamMap = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
2082
|
+
for (const nested of dynamicWithBody) {
|
|
2083
|
+
const memoName = this.extractMemoNameFromLoopArray(nested.loopArray)
|
|
2084
|
+
if (!memoName) continue
|
|
2085
|
+
const memo = ir.metadata.memos.find(m => m.name === memoName)
|
|
2086
|
+
if (!memo) continue
|
|
2087
|
+
|
|
2088
|
+
const goType = this.inferMemoType(memo, ir.metadata.signals, propsParamMap)
|
|
2089
|
+
const bakedValue = this.computeMemoInitialValue(
|
|
2090
|
+
memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars, goType,
|
|
2091
|
+
)
|
|
2092
|
+
if (bakedValue === 'nil' || bakedValue === '0') continue
|
|
2093
|
+
|
|
2094
|
+
const wrapperType = this.loopBodyWrapperName(componentName, nested)
|
|
2095
|
+
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
2096
|
+
const datumFields = this.resolveLoopDatumFields(nested.loopItemType)
|
|
2097
|
+
const bodyChildInstances = this.collectBodyChildInstances(nested.bodyChildren!)
|
|
2098
|
+
|
|
2099
|
+
// Create child sub-component instances once (identical scope IDs across rows)
|
|
2100
|
+
for (const child of bodyChildInstances) {
|
|
2101
|
+
const childVar = `child_${child.fieldName}`
|
|
2102
|
+
lines.push(`\t${childVar} := New${child.name}Props(${child.name}Input{`)
|
|
2103
|
+
lines.push(`\t\tScopeID: scopeID + "_${child.slotId}",`)
|
|
2104
|
+
lines.push(`\t\tBfParent: scopeID,`)
|
|
2105
|
+
lines.push(`\t\tBfMount: "${child.slotId}",`)
|
|
2106
|
+
for (const prop of child.props) {
|
|
2107
|
+
if (prop.value.kind === 'literal') {
|
|
2108
|
+
lines.push(`\t\t${this.capitalizeFieldName(prop.name)}: ${this.goLiteral(prop.value.value)},`)
|
|
2109
|
+
} else if (prop.value.kind === 'boolean-shorthand' || prop.value.kind === 'boolean-attr') {
|
|
2110
|
+
lines.push(`\t\t${this.capitalizeFieldName(prop.name)}: true,`)
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
lines.push(`\t})`)
|
|
2114
|
+
}
|
|
2115
|
+
if (bodyChildInstances.length > 0) lines.push('')
|
|
2116
|
+
|
|
2117
|
+
lines.push(`\tbakedData := ${bakedValue}`)
|
|
2118
|
+
lines.push(`\t${varName} := make([]${wrapperType}, len(bakedData))`)
|
|
2119
|
+
lines.push(`\tfor i, item := range bakedData {`)
|
|
2120
|
+
lines.push(`\t\t${varName}[i] = ${wrapperType}{`)
|
|
2121
|
+
lines.push(`\t\t\t${nested.name}Props: New${nested.name}Props(${nested.name}Input{`)
|
|
2122
|
+
lines.push(`\t\t\t\tBfParent: scopeID,`)
|
|
2123
|
+
lines.push(`\t\t\t\tBfMount: "${nested.slotId}",`)
|
|
2124
|
+
lines.push(`\t\t\t}),`)
|
|
2125
|
+
for (const f of datumFields) {
|
|
2126
|
+
lines.push(`\t\t\t${f.goName}: item.${f.goName},`)
|
|
2127
|
+
}
|
|
2128
|
+
for (const child of bodyChildInstances) {
|
|
2129
|
+
lines.push(`\t\t\t${child.fieldName}: child_${child.fieldName},`)
|
|
2130
|
+
}
|
|
2131
|
+
lines.push(`\t\t}`)
|
|
2132
|
+
const keyField = loopKeyToGoFieldPath(nested.loopKey, nested.loopParam)
|
|
2133
|
+
if (keyField) {
|
|
2134
|
+
lines.push(`\t\t${varName}[i].BfDataKey = fmt.Sprint(${keyField})`)
|
|
2135
|
+
this.usesFmt = true
|
|
2136
|
+
}
|
|
2137
|
+
lines.push(`\t}`)
|
|
2138
|
+
lines.push('')
|
|
2139
|
+
emittedWrapperVars.add(varName)
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1641
2142
|
lines.push(`\treturn ${propsTypeName}{`)
|
|
1642
2143
|
lines.push('\t\tScopeID: scopeID,')
|
|
1643
2144
|
// (#1249) Forward host context for when *this* component is itself a
|
|
1644
2145
|
// slot-attached child of an outer page/component.
|
|
1645
2146
|
lines.push('\t\tBfParent: in.BfParent,')
|
|
1646
2147
|
lines.push('\t\tBfMount: in.BfMount,')
|
|
2148
|
+
// (#1922) Forward the request-scoped searchParams() binding unchanged.
|
|
2149
|
+
if (this.usesSearchParams(ir)) {
|
|
2150
|
+
lines.push('\t\tSearchParams: in.SearchParams,')
|
|
2151
|
+
}
|
|
1647
2152
|
|
|
1648
2153
|
// Collect nested component array field names
|
|
1649
2154
|
const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
|
|
@@ -1654,6 +2159,31 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1654
2159
|
// doesn't silently shadow the JSX-side default. The same logic
|
|
1655
2160
|
// applies for signal-side fallbacks (`createSignal(props.X ?? N)`)
|
|
1656
2161
|
// via the hoisted variable from `propFallbackVars` (#1423).
|
|
2162
|
+
// (#1896) A memo that shadows a prop of the same name
|
|
2163
|
+
// (`const className = createMemo(() => props.className ?? '')`)
|
|
2164
|
+
// shares the prop's struct field (see `generatePropsStruct`). Fold
|
|
2165
|
+
// the memo's `?? fallback` into the prop's own initializer so the
|
|
2166
|
+
// SSR value matches the memo semantics when the caller omits it.
|
|
2167
|
+
const memoFallbacks = new Map<string, { goFallback: string; goType: string }>()
|
|
2168
|
+
for (const memo of ir.metadata.memos) {
|
|
2169
|
+
const stripped = memo.computation.replace(/^\(\)\s*=>\s*/, '')
|
|
2170
|
+
const m = this.extractPropFallback(stripped)
|
|
2171
|
+
if (!m) continue
|
|
2172
|
+
if (this.capitalizeFieldName(m.propName) !== this.capitalizeFieldName(memo.name)) continue
|
|
2173
|
+
// `applyGoFallback` emits a string-typed zero-value check; folding
|
|
2174
|
+
// onto a prop whose Input field resolved to `interface{}` (e.g. a
|
|
2175
|
+
// string-literal-union type the resolver can't narrow —
|
|
2176
|
+
// PaginationLink's `size`) would not compile. Such props keep the
|
|
2177
|
+
// plain `in.<Field>` assignment.
|
|
2178
|
+
const param = ir.metadata.propsParams.find(
|
|
2179
|
+
p => this.capitalizeFieldName(p.name) === this.capitalizeFieldName(memo.name),
|
|
2180
|
+
)
|
|
2181
|
+
if (!param) continue
|
|
2182
|
+
const goType = this.resolvePropGoType(param, propTypeOverrides)
|
|
2183
|
+
if (goType !== 'string' && goType !== 'interface{}') continue
|
|
2184
|
+
memoFallbacks.set(this.capitalizeFieldName(memo.name), { goFallback: m.goFallback, goType })
|
|
2185
|
+
}
|
|
2186
|
+
|
|
1657
2187
|
const propFieldNames = new Set<string>()
|
|
1658
2188
|
for (const param of ir.metadata.propsParams) {
|
|
1659
2189
|
const fieldName = this.capitalizeFieldName(param.name)
|
|
@@ -1662,9 +2192,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1662
2192
|
if (hoisted) {
|
|
1663
2193
|
lines.push(`\t\t${fieldName}: ${hoisted.varName},`)
|
|
1664
2194
|
} else {
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
|
|
2195
|
+
const paramDefault = this.goPropDefault(param.defaultValue)
|
|
2196
|
+
const memoFold = memoFallbacks.get(fieldName)
|
|
2197
|
+
if (paramDefault !== null) {
|
|
2198
|
+
lines.push(`\t\t${fieldName}: ${this.applyGoFallback(`in.${fieldName}`, paramDefault)},`)
|
|
2199
|
+
} else if (memoFold !== undefined && memoFold.goType === 'string') {
|
|
2200
|
+
lines.push(`\t\t${fieldName}: ${this.applyGoFallback(`in.${fieldName}`, memoFold.goFallback)},`)
|
|
2201
|
+
} else if (memoFold !== undefined) {
|
|
2202
|
+
// interface{} field (#1896, PaginationLink's `size ?? 'icon'`):
|
|
2203
|
+
// applyGoFallback's string zero-check doesn't compile here, so
|
|
2204
|
+
// emit a nil/empty-tolerant wrapper instead.
|
|
2205
|
+
lines.push(
|
|
2206
|
+
`\t\t${fieldName}: func() interface{} { v := interface{}(in.${fieldName}); if v == nil || v == "" { return ${memoFold.goFallback} }; return v }(),`,
|
|
2207
|
+
)
|
|
1668
2208
|
} else {
|
|
1669
2209
|
lines.push(`\t\t${fieldName}: in.${fieldName},`)
|
|
1670
2210
|
}
|
|
@@ -1692,16 +2232,24 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1692
2232
|
}
|
|
1693
2233
|
}
|
|
1694
2234
|
|
|
1695
|
-
// Add nested component arrays (static
|
|
1696
|
-
|
|
2235
|
+
// Add nested component arrays (static without body always emitted;
|
|
2236
|
+
// static with body + dynamic with body use emittedWrapperVars guard)
|
|
2237
|
+
for (const nested of staticWithoutBody) {
|
|
2238
|
+
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
2239
|
+
lines.push(`\t\t${nested.name}s: ${varName},`)
|
|
2240
|
+
}
|
|
2241
|
+
for (const nested of [...staticWithBody, ...dynamicWithBody]) {
|
|
1697
2242
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
2243
|
+
if (!emittedWrapperVars.has(varName)) continue
|
|
1698
2244
|
lines.push(`\t\t${nested.name}s: ${varName},`)
|
|
1699
2245
|
}
|
|
1700
2246
|
|
|
1701
|
-
// Add memo initial values (computed from signal initial values)
|
|
2247
|
+
// Add memo initial values (computed from signal initial values).
|
|
2248
|
+
// Prop-shadowing memos were folded into the prop field above (#1896).
|
|
1702
2249
|
const memoPropsParamMap = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
1703
2250
|
for (const memo of ir.metadata.memos) {
|
|
1704
2251
|
const fieldName = this.capitalizeFieldName(memo.name)
|
|
2252
|
+
if (propFieldNames.has(fieldName)) continue
|
|
1705
2253
|
// (#checkbox) Pass the memo's inferred Go type so an unresolved
|
|
1706
2254
|
// computation falls back to that type's zero value (`false` for a
|
|
1707
2255
|
// boolean memo like `isChecked`), not the int `0`.
|
|
@@ -1710,6 +2258,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1710
2258
|
lines.push(`\t\t${fieldName}: ${memoValue},`)
|
|
1711
2259
|
}
|
|
1712
2260
|
|
|
2261
|
+
// (#1897 PostList) Initialise computed derived-const fields
|
|
2262
|
+
// (e.g. `Root: func() string { … }()`), matching `generatePropsStruct`.
|
|
2263
|
+
const takenDerivedInit = new Set<string>([
|
|
2264
|
+
...ir.metadata.propsParams.map(p => this.capitalizeFieldName(p.name)),
|
|
2265
|
+
...ir.metadata.signals.map(s => this.capitalizeFieldName(s.getter)),
|
|
2266
|
+
...ir.metadata.memos.map(m => this.capitalizeFieldName(m.name)),
|
|
2267
|
+
])
|
|
2268
|
+
for (const f of this.computeDerivedConstFields(takenDerivedInit)) {
|
|
2269
|
+
lines.push(`\t\t${f.name}: ${f.init},`)
|
|
2270
|
+
}
|
|
2271
|
+
|
|
1713
2272
|
// `useContext` consumer fields: default to the `createContext` default
|
|
1714
2273
|
// when the caller (a provider) didn't set them.
|
|
1715
2274
|
const takenInit = new Set<string>([
|
|
@@ -1768,6 +2327,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1768
2327
|
restBagEntries.push(`${JSON.stringify(jsxName)}: ${goValue}`)
|
|
1769
2328
|
return
|
|
1770
2329
|
}
|
|
2330
|
+
// A hyphenated attribute (`aria-label`) can't be a Go struct
|
|
2331
|
+
// field, and with no rest bag on the child there is nowhere to
|
|
2332
|
+
// route it — the child has no `{...props}` spread, so the Hono
|
|
2333
|
+
// reference drops it on the child's root too. Skip rather than
|
|
2334
|
+
// emit invalid Go (#1896, data-table's selection sibling).
|
|
2335
|
+
if (jsxName.includes('-')) return
|
|
1771
2336
|
lines.push(`\t\t\t${this.capitalizeFieldName(jsxName)}: ${goValue},`)
|
|
1772
2337
|
}
|
|
1773
2338
|
// Add prop values
|
|
@@ -1907,12 +2472,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1907
2472
|
if (loop.childComponent) {
|
|
1908
2473
|
// Check for duplicates
|
|
1909
2474
|
if (!result.some(c => c.name === loop.childComponent!.name)) {
|
|
2475
|
+
const hasBodyChildren = loop.childComponent.children.length > 0
|
|
1910
2476
|
result.push({
|
|
1911
2477
|
...loop.childComponent,
|
|
1912
2478
|
isDynamic: !loop.isStaticArray,
|
|
1913
2479
|
isPropDerived: !!loop.isPropDerivedArray,
|
|
1914
2480
|
loopKey: loop.key ?? undefined,
|
|
1915
2481
|
loopParam: loop.param ?? undefined,
|
|
2482
|
+
bodyChildren: hasBodyChildren ? loop.childComponent.children : undefined,
|
|
2483
|
+
loopArray: loop.array,
|
|
2484
|
+
loopMarkerId: loop.markerId,
|
|
2485
|
+
loopItemType: loop.itemType,
|
|
1916
2486
|
})
|
|
1917
2487
|
}
|
|
1918
2488
|
}
|
|
@@ -1935,6 +2505,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1935
2505
|
if (cond.whenFalse) {
|
|
1936
2506
|
this.collectNestedComponents(cond.whenFalse, result)
|
|
1937
2507
|
}
|
|
2508
|
+
} else if (node.type === 'if-statement') {
|
|
2509
|
+
const stmt = node as IRIfStatement
|
|
2510
|
+
this.collectNestedComponents(stmt.consequent, result)
|
|
2511
|
+
if (stmt.alternate) {
|
|
2512
|
+
this.collectNestedComponents(stmt.alternate, result)
|
|
2513
|
+
}
|
|
2514
|
+
} else if (node.type === 'component') {
|
|
2515
|
+
// (#1896) JSX children passed to an imported component render via
|
|
2516
|
+
// a companion define with the PARENT's data, so a keyed loop
|
|
2517
|
+
// nested inside them (DataTablePreviewDemo's `sortedData().map(…)`
|
|
2518
|
+
// inside `<TableBody>`) needs its `<Name>s` slice on THIS
|
|
2519
|
+
// component's props like any other nested loop.
|
|
2520
|
+
const comp = node as IRComponent
|
|
2521
|
+
for (const child of comp.children) {
|
|
2522
|
+
this.collectNestedComponents(child, result)
|
|
2523
|
+
}
|
|
1938
2524
|
}
|
|
1939
2525
|
}
|
|
1940
2526
|
|
|
@@ -2070,6 +2656,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2070
2656
|
childrenScopedHtmlExpr: this.extractScopedHtmlChildren(effectiveChildren),
|
|
2071
2657
|
contextBindings: providerCtx.size > 0 ? providerCtx : undefined,
|
|
2072
2658
|
})
|
|
2659
|
+
// (#1896) Action-bearing JSX children render through a companion
|
|
2660
|
+
// define with the PARENT's data (see `queueDynamicChildrenDefine`),
|
|
2661
|
+
// so component instances nested inside them need their own
|
|
2662
|
+
// `<Name>SlotN` fields + constructor inits on THIS component's
|
|
2663
|
+
// props. Statically-baked children never contain components
|
|
2664
|
+
// (any nested component renders a `{{template}}` action, which
|
|
2665
|
+
// the bake extractors reject), so recursing is a no-op for them.
|
|
2666
|
+
for (const child of effectiveChildren) {
|
|
2667
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop, providerCtx)
|
|
2668
|
+
}
|
|
2073
2669
|
}
|
|
2074
2670
|
// Recurse into Portal's children to find nested components
|
|
2075
2671
|
if (comp.name === 'Portal' && comp.children) {
|
|
@@ -2099,6 +2695,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2099
2695
|
if (cond.whenFalse) {
|
|
2100
2696
|
this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop, providerCtx)
|
|
2101
2697
|
}
|
|
2698
|
+
} else if (node.type === 'if-statement') {
|
|
2699
|
+
// (#1896) An early-return if-statement root (AccordionTrigger's
|
|
2700
|
+
// asChild split) keeps its subtrees in consequent/alternate — the
|
|
2701
|
+
// non-asChild branch's ChevronDownIcon needs its slot field like
|
|
2702
|
+
// any other static child.
|
|
2703
|
+
const stmt = node as IRIfStatement
|
|
2704
|
+
this.collectStaticChildInstancesRecursive(stmt.consequent, result, inLoop, providerCtx)
|
|
2705
|
+
if (stmt.alternate) {
|
|
2706
|
+
this.collectStaticChildInstancesRecursive(stmt.alternate, result, inLoop, providerCtx)
|
|
2707
|
+
}
|
|
2102
2708
|
} else if (node.type === 'provider') {
|
|
2103
2709
|
// SSR context propagation: record the provider's value against its
|
|
2104
2710
|
// context name and extend the active binding map for descendants. A
|
|
@@ -2974,8 +3580,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2974
3580
|
}
|
|
2975
3581
|
const lines: string[] = []
|
|
2976
3582
|
lines.push('func() string {')
|
|
2977
|
-
|
|
2978
|
-
|
|
3583
|
+
// `fmt.Sprint` is type-tolerant: the key field is `interface{}`
|
|
3584
|
+
// when the analyzer typed the prop, but a `string` when the
|
|
3585
|
+
// shared inherited-prop augmentation synthesised it (#1896) — a
|
|
3586
|
+
// `.(string)` assertion compiles for the former only.
|
|
3587
|
+
this.usesFmt = true
|
|
3588
|
+
lines.push(`\t\t\tswitch fmt.Sprint(in.${fieldName}) {`)
|
|
2979
3589
|
for (const [k, v] of caseEntries) {
|
|
2980
3590
|
lines.push(`\t\t\tcase ${JSON.stringify(k)}: return ${JSON.stringify(v)}`)
|
|
2981
3591
|
}
|
|
@@ -2999,6 +3609,31 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2999
3609
|
memos: { name: string; computation: string; deps: string[] }[],
|
|
3000
3610
|
propsParams: { name: string }[]
|
|
3001
3611
|
): string | null {
|
|
3612
|
+
// `getter() === 'lit'` / `!==` as a child-instance prop value
|
|
3613
|
+
// (#1896 — accordion's `open={openItem() === 'item-1'}`): resolves
|
|
3614
|
+
// to a Go bool when the signal's initial value is a string literal.
|
|
3615
|
+
const cmpMatch = expr.match(
|
|
3616
|
+
/^(\w+)\(\)\s*([!=]==?)\s*(?:'([^']*)'|(-?\d+(?:\.\d+)?))\s*$/,
|
|
3617
|
+
)
|
|
3618
|
+
if (cmpMatch) {
|
|
3619
|
+
const [, depName, op, strLit, numLit] = cmpMatch
|
|
3620
|
+
const signal = signals.find(sg => sg.getter === depName)
|
|
3621
|
+
const init = signal?.initialValue.trim()
|
|
3622
|
+
const initMatch = init !== undefined
|
|
3623
|
+
? /^(?:'([^'\\]*)'|(-?\d+(?:\.\d+)?))$/.exec(init)
|
|
3624
|
+
: null
|
|
3625
|
+
if (initMatch) {
|
|
3626
|
+
const initVal = initMatch[1] ?? initMatch[2]
|
|
3627
|
+
const litVal = strLit ?? numLit
|
|
3628
|
+
// Same-kind comparison only (string vs string, number vs number).
|
|
3629
|
+
const sameKind = (initMatch[1] !== undefined) === (strLit !== undefined)
|
|
3630
|
+
if (sameKind) {
|
|
3631
|
+
const equal = initVal === litVal
|
|
3632
|
+
return String(op.startsWith('!') ? !equal : equal)
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3002
3637
|
// Match signal/memo getter calls like count(), doubled()
|
|
3003
3638
|
const getterMatch = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\(\)$/)
|
|
3004
3639
|
if (getterMatch) {
|
|
@@ -3010,10 +3645,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3010
3645
|
return this.convertInitialValue(signal.initialValue, signal.type, propsParams)
|
|
3011
3646
|
}
|
|
3012
3647
|
|
|
3013
|
-
// Check if it's a memo
|
|
3648
|
+
// Check if it's a memo. Use the pattern-matching core: when no
|
|
3649
|
+
// pattern applies, return null so the caller OMITS the field and
|
|
3650
|
+
// Go's typed zero value applies (#1896).
|
|
3014
3651
|
const memo = memos.find(m => m.name === getterName)
|
|
3015
3652
|
if (memo) {
|
|
3016
|
-
return this.
|
|
3653
|
+
return this.computeMemoInitialValueOrNull(memo, signals, propsParams)
|
|
3017
3654
|
}
|
|
3018
3655
|
}
|
|
3019
3656
|
|
|
@@ -3256,6 +3893,47 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3256
3893
|
// `string`, else the historical `0`).
|
|
3257
3894
|
goType?: string,
|
|
3258
3895
|
): string {
|
|
3896
|
+
const resolved = this.computeMemoInitialValueOrNull(
|
|
3897
|
+
memo, signals, propsParams, propFallbackVars,
|
|
3898
|
+
)
|
|
3899
|
+
if (resolved !== null) return resolved
|
|
3900
|
+
// Default: zero value for the memo's Go type (#checkbox). A boolean memo
|
|
3901
|
+
// (`isChecked`) renders `false`, a string memo `""`; reference types
|
|
3902
|
+
// (map / slice / interface / pointer) take `nil` — `0` is not
|
|
3903
|
+
// assignable to them and broke the struct literal for a
|
|
3904
|
+
// block-bodied string-building memo whose type inferred to
|
|
3905
|
+
// `map[string]any` (#1896, select-demo's `summary`). Other types
|
|
3906
|
+
// keep the historical int `0`.
|
|
3907
|
+
if (goType === 'bool') return 'false'
|
|
3908
|
+
if (goType === 'string') return '""'
|
|
3909
|
+
if (
|
|
3910
|
+
goType !== undefined &&
|
|
3911
|
+
(goType.startsWith('map[') ||
|
|
3912
|
+
goType.startsWith('[]') ||
|
|
3913
|
+
goType.startsWith('*') ||
|
|
3914
|
+
goType.includes('interface{}') ||
|
|
3915
|
+
goType === 'any')
|
|
3916
|
+
) {
|
|
3917
|
+
return 'nil'
|
|
3918
|
+
}
|
|
3919
|
+
return '0'
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
/**
|
|
3923
|
+
* Pattern-matching core of `computeMemoInitialValue`: returns the
|
|
3924
|
+
* memo's SSR initial value as a Go expression, or `null` when no
|
|
3925
|
+
* pattern applies. Callers that have a typed field to fill use the
|
|
3926
|
+
* zero-value-defaulting wrapper above; callers that can simply OMIT
|
|
3927
|
+
* the field (a child-instance prop init — Go's zero values then apply
|
|
3928
|
+
* with the right type for free) use this directly (#1896,
|
|
3929
|
+
* data-table's `Checked: 0` into a bool field).
|
|
3930
|
+
*/
|
|
3931
|
+
private computeMemoInitialValueOrNull(
|
|
3932
|
+
memo: { name: string; computation: string; deps: string[] },
|
|
3933
|
+
signals: { getter: string; initialValue: string }[],
|
|
3934
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
3935
|
+
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = GoTemplateAdapter.EMPTY_PROP_FALLBACK_VARS,
|
|
3936
|
+
): string | null {
|
|
3259
3937
|
const computation = memo.computation
|
|
3260
3938
|
// Helper to pick the hoisted var (if any) or fall back to `in.X`.
|
|
3261
3939
|
const propRef = (propName: string): string => {
|
|
@@ -3274,6 +3952,71 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3274
3952
|
const tmplMemo = this.computeTemplateLiteralMemoInitialValue(computation, propsParams)
|
|
3275
3953
|
if (tmplMemo !== null) return tmplMemo
|
|
3276
3954
|
|
|
3955
|
+
// Pattern: () => getter() === 'lit' / !== 'lit' — a selection memo
|
|
3956
|
+
// (#1896, tabs' `isAccountSelected = createMemo(() => activeTab() ===
|
|
3957
|
+
// 'account')`). Resolves to a Go bool when the signal's initial value
|
|
3958
|
+
// is itself a string literal.
|
|
3959
|
+
const eqMatch = computation.match(
|
|
3960
|
+
/^\(\)\s*=>\s*(\w+)\(\)\s*([!=]==?)\s*'([^']*)'\s*$/,
|
|
3961
|
+
)
|
|
3962
|
+
if (eqMatch) {
|
|
3963
|
+
const [, depName, op, lit] = eqMatch
|
|
3964
|
+
const signal = signals.find(sg => sg.getter === depName)
|
|
3965
|
+
const initLit = signal ? /^'([^'\\]*)'$/.exec(signal.initialValue.trim()) : null
|
|
3966
|
+
if (initLit) {
|
|
3967
|
+
const equal = initLit[1] === lit
|
|
3968
|
+
return String(op.startsWith('!') ? !equal : equal)
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
// Pattern: () => props.X ?? false — a boolean passthrough memo
|
|
3973
|
+
// (#1896, SelectItem's `isDisabled`). Go's bool zero value IS the
|
|
3974
|
+
// `?? false` fallback, so the raw input field carries the memo
|
|
3975
|
+
// semantics exactly.
|
|
3976
|
+
const boolPassthrough = computation.match(
|
|
3977
|
+
/^\(\)\s*=>\s*props\.(\w+)\s*\?\?\s*false\s*$/,
|
|
3978
|
+
)
|
|
3979
|
+
if (boolPassthrough) {
|
|
3980
|
+
return propRef(boolPassthrough[1])
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
// Pattern: () => cond() ? A : B where each branch is a module string
|
|
3984
|
+
// const or a string literal, and `cond` is a signal/memo this
|
|
3985
|
+
// resolver can already evaluate (#1896, SelectItem's
|
|
3986
|
+
// `stateClasses`). Emits a Go conditional so the SSR value tracks
|
|
3987
|
+
// the caller's input rather than a baked branch.
|
|
3988
|
+
const ternMatch = computation.match(
|
|
3989
|
+
/^\(\)\s*=>\s*(\w+)\(\)\s*\?\s*([\w$]+|'[^']*')\s*:\s*([\w$]+|'[^']*')\s*$/,
|
|
3990
|
+
)
|
|
3991
|
+
if (ternMatch) {
|
|
3992
|
+
const [, condName, whenTrue, whenFalse] = ternMatch
|
|
3993
|
+
const resolveBranch = (b: string): string | null => {
|
|
3994
|
+
const lit = /^'([^']*)'$/.exec(b)
|
|
3995
|
+
if (lit) return JSON.stringify(lit[1])
|
|
3996
|
+
const constVal = this.moduleStringConsts.get(b)
|
|
3997
|
+
return constVal !== undefined ? JSON.stringify(constVal) : null
|
|
3998
|
+
}
|
|
3999
|
+
const t = resolveBranch(whenTrue)
|
|
4000
|
+
const f = resolveBranch(whenFalse)
|
|
4001
|
+
if (t !== null && f !== null) {
|
|
4002
|
+
let condGo: string | null = null
|
|
4003
|
+
const condSignal = signals.find(sg => sg.getter === condName)
|
|
4004
|
+
if (condSignal) {
|
|
4005
|
+
condGo = this.getSignalInitialValueAsGo(condSignal.initialValue, propsParams, propFallbackVars)
|
|
4006
|
+
} else {
|
|
4007
|
+
const condMemo = (this.currentMemos ?? []).find(m => m.name === condName)
|
|
4008
|
+
if (condMemo) {
|
|
4009
|
+
condGo = this.computeMemoInitialValueOrNull(condMemo, signals, propsParams, propFallbackVars)
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
if (condGo === 'true') return t
|
|
4013
|
+
if (condGo === 'false') return f
|
|
4014
|
+
if (condGo !== null) {
|
|
4015
|
+
return `func() string { if ${condGo} { return ${t} }; return ${f} }()`
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
|
|
3277
4020
|
// Pattern: () => dep() * N or () => dep() + N etc.
|
|
3278
4021
|
const arithmeticMatch = computation.match(/\(\)\s*=>\s*(\w+)\(\)\s*([*+\-/])\s*(\d+)/)
|
|
3279
4022
|
if (arithmeticMatch) {
|
|
@@ -3352,55 +4095,486 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3352
4095
|
}
|
|
3353
4096
|
}
|
|
3354
4097
|
|
|
3355
|
-
//
|
|
3356
|
-
// (
|
|
3357
|
-
//
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
4098
|
+
// (#1897) Pattern: block-body memo that early-returns a module-const array
|
|
4099
|
+
// when a guard signal is falsy — `() => { const k = getter(); if (!k)
|
|
4100
|
+
// return MODULE_ARRAY; return /* @client */ ... }`. When the signal starts
|
|
4101
|
+
// null, the SSR value is the module-const array. The constant's literal
|
|
4102
|
+
// value (not the identifier) is passed to the baker so `jsLiteralToGo`
|
|
4103
|
+
// can reduce it to a Go slice.
|
|
4104
|
+
const blockReturn = this.resolveBlockBodyMemoModuleConst(computation, signals)
|
|
4105
|
+
if (blockReturn !== null && blockReturn.constValue && blockReturn.constType) {
|
|
4106
|
+
return this.convertInitialValue(
|
|
4107
|
+
blockReturn.constValue,
|
|
4108
|
+
blockReturn.constType,
|
|
4109
|
+
propsParams,
|
|
4110
|
+
)
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
// (#1897 PostList) Pattern: an object-returning block-body memo derived from
|
|
4114
|
+
// `searchParams()` — `() => { const sp = searchParams(); return { sort:
|
|
4115
|
+
// asSortKey(sp.get('sort')), tag: sp.get('tag') ?? '' } }`. Compute a Go
|
|
4116
|
+
// `map[string]interface{}` whose values are lowered from the request query,
|
|
4117
|
+
// so `.Params.Sort` / `.Params.Tag` resolve at execute time instead of
|
|
4118
|
+
// reading a nil map. Returns null for any unsupported shape (→ nil fallback,
|
|
4119
|
+
// no regression).
|
|
4120
|
+
const objMemo = this.computeObjectMemoInitialValue(computation)
|
|
4121
|
+
if (objMemo !== null) return objMemo
|
|
4122
|
+
|
|
4123
|
+
return null
|
|
3361
4124
|
}
|
|
3362
4125
|
|
|
3363
4126
|
/**
|
|
3364
|
-
*
|
|
3365
|
-
*
|
|
4127
|
+
* (#1897) Recognises a block-body memo whose SSR path returns a module-const
|
|
4128
|
+
* array when the guard signal starts falsy:
|
|
4129
|
+
* `() => { const k = getter(); if (!k) return MODULE_CONST; … }`
|
|
4130
|
+
* Returns the constant's name and inferred type, or null.
|
|
3366
4131
|
*/
|
|
3367
|
-
private
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
if (!
|
|
3383
|
-
body =
|
|
3384
|
-
|
|
3385
|
-
|
|
4132
|
+
private resolveBlockBodyMemoModuleConst(
|
|
4133
|
+
computation: string,
|
|
4134
|
+
signals: { getter: string; initialValue: string }[],
|
|
4135
|
+
): { constName: string; constValue: string | undefined; constType: TypeInfo | undefined } | null {
|
|
4136
|
+
try {
|
|
4137
|
+
const sf = ts.createSourceFile(
|
|
4138
|
+
'__memo.ts',
|
|
4139
|
+
`const __x = (${computation});`,
|
|
4140
|
+
ts.ScriptTarget.Latest,
|
|
4141
|
+
false,
|
|
4142
|
+
)
|
|
4143
|
+
const stmt = sf.statements[0]
|
|
4144
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
4145
|
+
let init = stmt.declarationList.declarations[0]?.initializer
|
|
4146
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
4147
|
+
if (!init || !ts.isArrowFunction(init)) return null
|
|
4148
|
+
const body = init.body
|
|
4149
|
+
if (!ts.isBlock(body)) return null
|
|
4150
|
+
|
|
4151
|
+
// Walk the block's statements collecting:
|
|
4152
|
+
// const <varName> = <signalGetter>() → varToSignal map
|
|
4153
|
+
// if (!<varName>) return <moduleConst> → match varName back to signal
|
|
4154
|
+
const varToSignal = new Map<string, string>()
|
|
4155
|
+
let guardSignalGetter: string | null = null
|
|
4156
|
+
let returnedConst: string | null = null
|
|
4157
|
+
|
|
4158
|
+
for (const s of body.statements) {
|
|
4159
|
+
if (ts.isVariableStatement(s)) {
|
|
4160
|
+
for (const decl of s.declarationList.declarations) {
|
|
4161
|
+
if (
|
|
4162
|
+
ts.isIdentifier(decl.name) &&
|
|
4163
|
+
decl.initializer &&
|
|
4164
|
+
ts.isCallExpression(decl.initializer) &&
|
|
4165
|
+
ts.isIdentifier(decl.initializer.expression)
|
|
4166
|
+
) {
|
|
4167
|
+
const callee = decl.initializer.expression.text
|
|
4168
|
+
if (signals.some(sg => sg.getter === callee)) {
|
|
4169
|
+
varToSignal.set(decl.name.text, callee)
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
if (
|
|
4175
|
+
ts.isIfStatement(s) &&
|
|
4176
|
+
ts.isPrefixUnaryExpression(s.expression) &&
|
|
4177
|
+
s.expression.operator === ts.SyntaxKind.ExclamationToken &&
|
|
4178
|
+
ts.isIdentifier(s.expression.operand)
|
|
4179
|
+
) {
|
|
4180
|
+
const guardVar = s.expression.operand.text
|
|
4181
|
+
const signalGetter = varToSignal.get(guardVar)
|
|
4182
|
+
if (!signalGetter) continue
|
|
4183
|
+
const thenBlock = ts.isBlock(s.thenStatement)
|
|
4184
|
+
? s.thenStatement.statements
|
|
4185
|
+
: [s.thenStatement]
|
|
4186
|
+
for (const rs of thenBlock) {
|
|
4187
|
+
if (
|
|
4188
|
+
ts.isReturnStatement(rs) &&
|
|
4189
|
+
rs.expression &&
|
|
4190
|
+
ts.isIdentifier(rs.expression)
|
|
4191
|
+
) {
|
|
4192
|
+
guardSignalGetter = signalGetter
|
|
4193
|
+
returnedConst = rs.expression.text
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
if (guardSignalGetter && returnedConst) break
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
if (!guardSignalGetter || !returnedConst) return null
|
|
4201
|
+
|
|
4202
|
+
// The guard signal must start falsy (null, '', 0, false)
|
|
4203
|
+
const guardSignal = signals.find(sg => sg.getter === guardSignalGetter)
|
|
4204
|
+
if (!guardSignal) return null
|
|
4205
|
+
const iv = guardSignal.initialValue.trim()
|
|
4206
|
+
if (iv !== 'null' && iv !== "''" && iv !== '""' && iv !== '0' && iv !== 'false') {
|
|
4207
|
+
return null
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
// The returned identifier must be a module-scope constant
|
|
4211
|
+
const constant = this.localConstants.find(
|
|
4212
|
+
c => c.name === returnedConst && c.origin?.scope === 'module',
|
|
4213
|
+
)
|
|
4214
|
+
if (!constant) return null
|
|
4215
|
+
|
|
4216
|
+
return {
|
|
4217
|
+
constName: constant.name,
|
|
4218
|
+
constValue: constant.value,
|
|
4219
|
+
constType: constant.type ?? undefined,
|
|
3386
4220
|
}
|
|
4221
|
+
} catch {
|
|
4222
|
+
return null
|
|
3387
4223
|
}
|
|
3388
|
-
return ts.isTemplateExpression(body) || ts.isNoSubstitutionTemplateLiteral(body)
|
|
3389
4224
|
}
|
|
3390
4225
|
|
|
3391
4226
|
/**
|
|
3392
|
-
*
|
|
4227
|
+
* (#1897 PostList) Compute the SSR value of an object-returning block-body
|
|
4228
|
+
* memo derived from `searchParams()`:
|
|
4229
|
+
* () => { const sp = searchParams(); return { sort: asSortKey(sp.get('sort')),
|
|
4230
|
+
* tag: sp.get('tag') ?? '' } }
|
|
4231
|
+
* Emits a Go `map[string]interface{}{ "Sort": …, "Tag": … }` whose values are
|
|
4232
|
+
* lowered from the request query (see `lowerCtorExpr`). Keys are capitalized to
|
|
4233
|
+
* match the template's `.Params.<Field>` map access. Returns null for any shape
|
|
4234
|
+
* the lowerer can't represent, so the caller falls back to a nil map.
|
|
3393
4235
|
*/
|
|
3394
|
-
private
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
//
|
|
3400
|
-
//
|
|
3401
|
-
//
|
|
3402
|
-
|
|
3403
|
-
|
|
4236
|
+
private computeObjectMemoInitialValue(computation: string): string | null {
|
|
4237
|
+
const arrow = this.parseLiteralExpression(computation)
|
|
4238
|
+
if (!arrow || !ts.isArrowFunction(arrow) || !ts.isBlock(arrow.body)) return null
|
|
4239
|
+
|
|
4240
|
+
// Accept only a strict shape: zero or more `const`/`let` declarations
|
|
4241
|
+
// followed by exactly one `return { … }` as the LAST statement. Any other
|
|
4242
|
+
// statement kind — `if`, an early/extra `return`, a loop, a bare call —
|
|
4243
|
+
// means the block has control flow this resolver can't reason about, so we
|
|
4244
|
+
// bail to the nil fallback rather than silently lowering one of several
|
|
4245
|
+
// returns (#1941 review). Along the way, collect `const <v> = searchParams()`
|
|
4246
|
+
// bindings (the env for `<v>.get('k')`).
|
|
4247
|
+
const searchParamsVars = new Set<string>()
|
|
4248
|
+
let retObj: ts.ObjectLiteralExpression | null = null
|
|
4249
|
+
const statements = arrow.body.statements
|
|
4250
|
+
for (let i = 0; i < statements.length; i++) {
|
|
4251
|
+
const s = statements[i]
|
|
4252
|
+
if (ts.isVariableStatement(s)) {
|
|
4253
|
+
for (const d of s.declarationList.declarations) {
|
|
4254
|
+
if (
|
|
4255
|
+
ts.isIdentifier(d.name) &&
|
|
4256
|
+
d.initializer &&
|
|
4257
|
+
ts.isCallExpression(d.initializer) &&
|
|
4258
|
+
ts.isIdentifier(d.initializer.expression) &&
|
|
4259
|
+
d.initializer.arguments.length === 0 &&
|
|
4260
|
+
this.searchParamsLocals.has(d.initializer.expression.text)
|
|
4261
|
+
) {
|
|
4262
|
+
searchParamsVars.add(d.name.text)
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
continue
|
|
4266
|
+
}
|
|
4267
|
+
if (ts.isReturnStatement(s)) {
|
|
4268
|
+
// The return must be the last statement (so it's the only one) and
|
|
4269
|
+
// return an object literal.
|
|
4270
|
+
if (i !== statements.length - 1 || !s.expression) return null
|
|
4271
|
+
let e: ts.Expression = s.expression
|
|
4272
|
+
while (ts.isParenthesizedExpression(e)) e = e.expression
|
|
4273
|
+
if (!ts.isObjectLiteralExpression(e)) return null
|
|
4274
|
+
retObj = e
|
|
4275
|
+
continue
|
|
4276
|
+
}
|
|
4277
|
+
// Any other statement kind → control flow we don't model → bail.
|
|
4278
|
+
return null
|
|
4279
|
+
}
|
|
4280
|
+
if (!retObj || retObj.properties.length === 0) return null
|
|
4281
|
+
|
|
4282
|
+
const env: CtorLowerEnv = { searchParamsVars, params: new Map() }
|
|
4283
|
+
const entries: string[] = []
|
|
4284
|
+
for (const prop of retObj.properties) {
|
|
4285
|
+
if (!ts.isPropertyAssignment(prop)) return null
|
|
4286
|
+
const key =
|
|
4287
|
+
ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null
|
|
4288
|
+
if (!key) return null
|
|
4289
|
+
const go = this.lowerCtorExpr(prop.initializer, env)
|
|
4290
|
+
if (go === null) return null
|
|
4291
|
+
entries.push(`"${this.capitalizeFieldName(key)}": ${go}`)
|
|
4292
|
+
}
|
|
4293
|
+
return `map[string]interface{}{\n\t\t${entries.join(',\n\t\t')},\n\t}`
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
/**
|
|
4297
|
+
* Lower a JS expression to a Go expression in the `NewXxxProps` constructor
|
|
4298
|
+
* context. This is Go *code*, not template syntax — so a search-param read
|
|
4299
|
+
* becomes `in.SearchParams.Get("k")` (method call), not the template's
|
|
4300
|
+
* `.SearchParams.Get "k"`. Supports the narrow surface derived-state memos
|
|
4301
|
+
* need: string/number literals, `<sp>.get('k')`, `<arr>.includes(<x>)`,
|
|
4302
|
+
* module arrow-helper inlining, `<expr> ?? <fallback>`, and string ternaries.
|
|
4303
|
+
* Returns null for anything else so the caller can fall back safely.
|
|
4304
|
+
*/
|
|
4305
|
+
private lowerCtorExpr(node: ts.Expression, env: CtorLowerEnv): string | null {
|
|
4306
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
4307
|
+
|
|
4308
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
4309
|
+
return JSON.stringify(node.text)
|
|
4310
|
+
}
|
|
4311
|
+
if (ts.isNumericLiteral(node)) return node.text
|
|
4312
|
+
|
|
4313
|
+
// Identifier: a substituted helper param, a module string const, or a
|
|
4314
|
+
// component-scope derived const inlined recursively (e.g. `root` → its
|
|
4315
|
+
// `base || '/'` value).
|
|
4316
|
+
if (ts.isIdentifier(node)) {
|
|
4317
|
+
const sub = env.params.get(node.text)
|
|
4318
|
+
if (sub !== undefined) return sub
|
|
4319
|
+
const c = this.localConstants.find(lc => lc.name === node.text)
|
|
4320
|
+
if (c?.value !== undefined) {
|
|
4321
|
+
if (c.isModule) {
|
|
4322
|
+
const lit = this.parseLiteralExpression(c.value)
|
|
4323
|
+
if (lit && (ts.isStringLiteral(lit) || ts.isNoSubstitutionTemplateLiteral(lit))) {
|
|
4324
|
+
return JSON.stringify(lit.text)
|
|
4325
|
+
}
|
|
4326
|
+
return null
|
|
4327
|
+
}
|
|
4328
|
+
// Component-scope const: inline its computed value, guarding cycles.
|
|
4329
|
+
if (env.consts?.has(node.text)) return null
|
|
4330
|
+
const inner = this.parseLiteralExpression(c.value)
|
|
4331
|
+
if (!inner) return null
|
|
4332
|
+
return this.lowerCtorExpr(inner, {
|
|
4333
|
+
...env,
|
|
4334
|
+
consts: new Set([...(env.consts ?? []), node.text]),
|
|
4335
|
+
})
|
|
4336
|
+
}
|
|
4337
|
+
return null
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
// `props.<X>` → the constructor input field `in.<X>`.
|
|
4341
|
+
if (
|
|
4342
|
+
ts.isPropertyAccessExpression(node) &&
|
|
4343
|
+
ts.isIdentifier(node.expression) &&
|
|
4344
|
+
node.expression.text === this.propsObjectName
|
|
4345
|
+
) {
|
|
4346
|
+
return `in.${this.capitalizeFieldName(node.name.text)}`
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
4350
|
+
const method = node.expression.name.text
|
|
4351
|
+
const recv = node.expression.expression
|
|
4352
|
+
// `<sp>.get('k')` where <sp> is bound to searchParams().
|
|
4353
|
+
if (
|
|
4354
|
+
method === 'get' &&
|
|
4355
|
+
ts.isIdentifier(recv) &&
|
|
4356
|
+
env.searchParamsVars.has(recv.text) &&
|
|
4357
|
+
node.arguments.length === 1 &&
|
|
4358
|
+
ts.isStringLiteral(node.arguments[0])
|
|
4359
|
+
) {
|
|
4360
|
+
return `in.SearchParams.Get(${JSON.stringify(node.arguments[0].text)})`
|
|
4361
|
+
}
|
|
4362
|
+
// `<arr>.includes(<x>)` → bf.Includes(<[]string{…}>, <x>) (bool).
|
|
4363
|
+
if (method === 'includes' && node.arguments.length === 1) {
|
|
4364
|
+
const arr = this.lowerCtorStringArray(recv)
|
|
4365
|
+
const elem = this.lowerCtorExpr(node.arguments[0], env)
|
|
4366
|
+
if (arr !== null && elem !== null) return `bf.Includes(${arr}, ${elem})`
|
|
4367
|
+
return null
|
|
4368
|
+
}
|
|
4369
|
+
// `<s>.replace(/\/+$/, '')` — strip trailing slashes → strings.TrimRight.
|
|
4370
|
+
// Only this exact trailing-slash regex is recognized (a general regex
|
|
4371
|
+
// replace would need Go's regexp; out of scope).
|
|
4372
|
+
if (
|
|
4373
|
+
method === 'replace' &&
|
|
4374
|
+
node.arguments.length === 2 &&
|
|
4375
|
+
node.arguments[0].kind === ts.SyntaxKind.RegularExpressionLiteral &&
|
|
4376
|
+
(node.arguments[0] as ts.Node).getText() === '/\\/+$/' &&
|
|
4377
|
+
(ts.isStringLiteral(node.arguments[1]) ||
|
|
4378
|
+
ts.isNoSubstitutionTemplateLiteral(node.arguments[1])) &&
|
|
4379
|
+
node.arguments[1].text === ''
|
|
4380
|
+
) {
|
|
4381
|
+
const recvGo = this.lowerCtorExpr(recv, env)
|
|
4382
|
+
if (recvGo === null) return null
|
|
4383
|
+
this.needsStringsImport = true
|
|
4384
|
+
return `strings.TrimRight(${recvGo}, "/")`
|
|
4385
|
+
}
|
|
4386
|
+
return null
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
// `helper(<args>)` where helper is a module arrow const → inline its body.
|
|
4390
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
4391
|
+
const fnConst = this.localConstants.find(
|
|
4392
|
+
lc => lc.name === (node.expression as ts.Identifier).text && lc.isModule,
|
|
4393
|
+
)
|
|
4394
|
+
if (fnConst?.value) {
|
|
4395
|
+
const fn = this.parseLiteralExpression(fnConst.value)
|
|
4396
|
+
if (
|
|
4397
|
+
fn &&
|
|
4398
|
+
ts.isArrowFunction(fn) &&
|
|
4399
|
+
!ts.isBlock(fn.body) &&
|
|
4400
|
+
fn.parameters.length === node.arguments.length
|
|
4401
|
+
) {
|
|
4402
|
+
const params = new Map(env.params)
|
|
4403
|
+
for (let i = 0; i < fn.parameters.length; i++) {
|
|
4404
|
+
const p = fn.parameters[i]
|
|
4405
|
+
if (!ts.isIdentifier(p.name)) return null
|
|
4406
|
+
const argGo = this.lowerCtorExpr(node.arguments[i], env)
|
|
4407
|
+
if (argGo === null) return null
|
|
4408
|
+
params.set(p.name.text, argGo)
|
|
4409
|
+
}
|
|
4410
|
+
return this.lowerCtorExpr(fn.body, { searchParamsVars: env.searchParamsVars, params })
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
return null
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
// `<expr> ?? <fallback>`
|
|
4417
|
+
if (
|
|
4418
|
+
ts.isBinaryExpression(node) &&
|
|
4419
|
+
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken
|
|
4420
|
+
) {
|
|
4421
|
+
const left = this.lowerCtorExpr(node.left, env)
|
|
4422
|
+
if (left === null) return null
|
|
4423
|
+
let r: ts.Expression = node.right
|
|
4424
|
+
while (ts.isParenthesizedExpression(r)) r = r.expression
|
|
4425
|
+
// `?? ''` is a no-op: SearchParams.Get already returns "" for a missing key.
|
|
4426
|
+
if ((ts.isStringLiteral(r) || ts.isNoSubstitutionTemplateLiteral(r)) && r.text === '') {
|
|
4427
|
+
return left
|
|
4428
|
+
}
|
|
4429
|
+
const right = this.lowerCtorExpr(node.right, env)
|
|
4430
|
+
if (right === null) return null
|
|
4431
|
+
return `func() string { v := ${left}; if v != "" { return v }; return ${right} }()`
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
// `<expr> || <fallback>` (string `||` — falsy = empty, like `?? ''` but the
|
|
4435
|
+
// left can itself be empty): `base || '/'`.
|
|
4436
|
+
if (
|
|
4437
|
+
ts.isBinaryExpression(node) &&
|
|
4438
|
+
node.operatorToken.kind === ts.SyntaxKind.BarBarToken
|
|
4439
|
+
) {
|
|
4440
|
+
const left = this.lowerCtorExpr(node.left, env)
|
|
4441
|
+
const right = this.lowerCtorExpr(node.right, env)
|
|
4442
|
+
if (left === null || right === null) return null
|
|
4443
|
+
return `func() string { v := ${left}; if v != "" { return v }; return ${right} }()`
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
// `<cond> ? <t> : <f>` (string result)
|
|
4447
|
+
if (ts.isConditionalExpression(node)) {
|
|
4448
|
+
// The condition must be lowered as a *boolean* (`lowerCtorCond`), not a
|
|
4449
|
+
// value: a string-valued JS condition like `sp.get('tag') ? a : b` is
|
|
4450
|
+
// truthy in JS, but `if "<string>"` does not compile in Go — such shapes
|
|
4451
|
+
// return null so the memo falls back to nil rather than emitting invalid
|
|
4452
|
+
// code (#1941 review).
|
|
4453
|
+
const cond = this.lowerCtorCond(node.condition, env)
|
|
4454
|
+
const t = this.lowerCtorExpr(node.whenTrue, env)
|
|
4455
|
+
const f = this.lowerCtorExpr(node.whenFalse, env)
|
|
4456
|
+
if (cond !== null && t !== null && f !== null) {
|
|
4457
|
+
return `func() string { if ${cond} { return ${t} }; return ${f} }()`
|
|
4458
|
+
}
|
|
4459
|
+
return null
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
return null
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
/**
|
|
4466
|
+
* Lower a JS expression used as a *boolean* condition to a Go bool expression,
|
|
4467
|
+
* or null when it is not provably boolean. Distinct from `lowerCtorExpr`,
|
|
4468
|
+
* which lowers value expressions: a string-valued condition (`sp.get('tag')`)
|
|
4469
|
+
* is truthy in JS but `if "<string>"` does not compile in Go, so anything not
|
|
4470
|
+
* known to yield a Go bool must fall back to null (#1941 review).
|
|
4471
|
+
*/
|
|
4472
|
+
private lowerCtorCond(node: ts.Expression, env: CtorLowerEnv): string | null {
|
|
4473
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
4474
|
+
|
|
4475
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return 'true'
|
|
4476
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return 'false'
|
|
4477
|
+
|
|
4478
|
+
// `!<cond>`
|
|
4479
|
+
if (
|
|
4480
|
+
ts.isPrefixUnaryExpression(node) &&
|
|
4481
|
+
node.operator === ts.SyntaxKind.ExclamationToken
|
|
4482
|
+
) {
|
|
4483
|
+
const inner = this.lowerCtorCond(node.operand, env)
|
|
4484
|
+
return inner === null ? null : `!(${inner})`
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
// `<a> && <b>` / `<a> || <b>` — both operands must themselves be boolean.
|
|
4488
|
+
if (ts.isBinaryExpression(node)) {
|
|
4489
|
+
const op =
|
|
4490
|
+
node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
4491
|
+
? '&&'
|
|
4492
|
+
: node.operatorToken.kind === ts.SyntaxKind.BarBarToken
|
|
4493
|
+
? '||'
|
|
4494
|
+
: null
|
|
4495
|
+
if (op) {
|
|
4496
|
+
const l = this.lowerCtorCond(node.left, env)
|
|
4497
|
+
const r = this.lowerCtorCond(node.right, env)
|
|
4498
|
+
return l !== null && r !== null ? `(${l} ${op} ${r})` : null
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
// `<arr>.includes(<x>)` is the one value-shape `lowerCtorExpr` lowers to a
|
|
4503
|
+
// Go bool (`bf.Includes(...)`); reuse it for that case only.
|
|
4504
|
+
if (
|
|
4505
|
+
ts.isCallExpression(node) &&
|
|
4506
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
4507
|
+
node.expression.name.text === 'includes'
|
|
4508
|
+
) {
|
|
4509
|
+
return this.lowerCtorExpr(node, env)
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
return null
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
/**
|
|
4516
|
+
* Resolve a string-array expression (a `['a','b']` literal, or a module const
|
|
4517
|
+
* bound to one) to a Go `[]string{…}` literal, or null when it isn't a pure
|
|
4518
|
+
* string-array. Used by `lowerCtorExpr` for `<arr>.includes(<x>)`.
|
|
4519
|
+
*/
|
|
4520
|
+
private lowerCtorStringArray(node: ts.Expression): string | null {
|
|
4521
|
+
let arr: ts.Expression | null = node
|
|
4522
|
+
if (ts.isIdentifier(node)) {
|
|
4523
|
+
const c = this.localConstants.find(lc => lc.name === node.text && lc.isModule)
|
|
4524
|
+
if (!c?.value) return null
|
|
4525
|
+
arr = this.parseLiteralExpression(c.value)
|
|
4526
|
+
}
|
|
4527
|
+
if (!arr || !ts.isArrayLiteralExpression(arr)) return null
|
|
4528
|
+
const elems: string[] = []
|
|
4529
|
+
for (const el of arr.elements) {
|
|
4530
|
+
if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) {
|
|
4531
|
+
elems.push(JSON.stringify(el.text))
|
|
4532
|
+
} else return null
|
|
4533
|
+
}
|
|
4534
|
+
return `[]string{${elems.join(', ')}}`
|
|
4535
|
+
}
|
|
4536
|
+
|
|
4537
|
+
/**
|
|
4538
|
+
* Whether a memo is an arrow whose result is a template literal — either a
|
|
4539
|
+
* concise body (`() => \`…\``) or a block body whose `return` is one.
|
|
4540
|
+
*/
|
|
4541
|
+
private isTemplateLiteralMemo(computation: string): boolean {
|
|
4542
|
+
const sf = ts.createSourceFile(
|
|
4543
|
+
'__memo.ts', `const __x = (${computation});`, ts.ScriptTarget.Latest, /*setParentNodes*/ false,
|
|
4544
|
+
)
|
|
4545
|
+
const stmt = sf.statements[0]
|
|
4546
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return false
|
|
4547
|
+
let init = stmt.declarationList.declarations[0]?.initializer
|
|
4548
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
4549
|
+
if (!init || !ts.isArrowFunction(init)) return false
|
|
4550
|
+
let body = init.body as ts.Node
|
|
4551
|
+
while (ts.isParenthesizedExpression(body as ts.Expression)) {
|
|
4552
|
+
body = (body as ts.ParenthesizedExpression).expression
|
|
4553
|
+
}
|
|
4554
|
+
if (ts.isBlock(body)) {
|
|
4555
|
+
const ret = body.statements.find(ts.isReturnStatement)
|
|
4556
|
+
if (!ret || !ret.expression) return false
|
|
4557
|
+
body = ret.expression
|
|
4558
|
+
while (ts.isParenthesizedExpression(body as ts.Expression)) {
|
|
4559
|
+
body = (body as ts.ParenthesizedExpression).expression
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
return ts.isTemplateExpression(body) || ts.isNoSubstitutionTemplateLiteral(body)
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
/**
|
|
4566
|
+
* Infer the Go type for a memo based on its computation and dependencies.
|
|
4567
|
+
*/
|
|
4568
|
+
private inferMemoType(
|
|
4569
|
+
memo: { name: string; computation: string; type: TypeInfo; deps: string[] },
|
|
4570
|
+
signals: { getter: string; initialValue: string; type: TypeInfo }[],
|
|
4571
|
+
propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>
|
|
4572
|
+
): string {
|
|
4573
|
+
// A template-literal memo always produces a string. Decide this first so a
|
|
4574
|
+
// class-string `/` (e.g. `ring-ring/50`) doesn't trip the arithmetic
|
|
4575
|
+
// heuristic below into `int`.
|
|
4576
|
+
if (this.isTemplateLiteralMemo(memo.computation)) return 'string'
|
|
4577
|
+
|
|
3404
4578
|
// Check if computation involves multiplication (*) - likely number
|
|
3405
4579
|
if (memo.computation.includes('*') || memo.computation.includes('/') ||
|
|
3406
4580
|
memo.computation.includes('+') || memo.computation.includes('-')) {
|
|
@@ -3438,6 +4612,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3438
4612
|
return 'bool'
|
|
3439
4613
|
}
|
|
3440
4614
|
|
|
4615
|
+
// (#1897) Block-body memo returning a module-const array: use the
|
|
4616
|
+
// constant's array type instead of the memo's generic `object`.
|
|
4617
|
+
const blockReturn = this.resolveBlockBodyMemoModuleConst(memo.computation, signals)
|
|
4618
|
+
if (blockReturn?.constType?.kind === 'array') {
|
|
4619
|
+
return this.typeInfoToGo(blockReturn.constType)
|
|
4620
|
+
}
|
|
4621
|
+
|
|
3441
4622
|
// Default to the memo's declared type
|
|
3442
4623
|
return this.typeInfoToGo(memo.type)
|
|
3443
4624
|
}
|
|
@@ -3816,6 +4997,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3816
4997
|
if (element.slotId) {
|
|
3817
4998
|
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`
|
|
3818
4999
|
}
|
|
5000
|
+
// Page-lifecycle boundary lowered from `<Region>` (spec/router.md). The id
|
|
5001
|
+
// is a deterministic static string (`<file scope>:<index>`), so it emits as
|
|
5002
|
+
// a plain literal attribute — no Go-template interpolation.
|
|
5003
|
+
if (element.regionId) {
|
|
5004
|
+
hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`
|
|
5005
|
+
}
|
|
3819
5006
|
|
|
3820
5007
|
const voidElements = [
|
|
3821
5008
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
@@ -3839,12 +5026,32 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3839
5026
|
return ''
|
|
3840
5027
|
}
|
|
3841
5028
|
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
//
|
|
3845
|
-
//
|
|
5029
|
+
// Let `convertExpressionToGo` report the `ParsedExpr` it already builds (if
|
|
5030
|
+
// it gets that far) so the wrap decision below reuses that single parse —
|
|
5031
|
+
// no extra `ts.createSourceFile`, and no parse at all for expressions it
|
|
5032
|
+
// resolves via early returns (`null`/`undefined`, inlined consts) on the
|
|
5033
|
+
// `bf build` hot path.
|
|
5034
|
+
const classify: { parsed?: ParsedExpr } = {}
|
|
5035
|
+
const goExpr = this.convertExpressionToGo(expr.expr, classify)
|
|
5036
|
+
|
|
5037
|
+
// If the lowered expression is already template text, don't wrap it again
|
|
5038
|
+
// in {{...}} (double-wrapping). Two distinct shapes reach here:
|
|
5039
|
+
// - whole-expression blocks that START with `{{` (a `{{with ...}}` /
|
|
5040
|
+
// `{{if ...}}` chain from a nested ternary), and
|
|
5041
|
+
// - template literals, which lower via `templateLiteral()` to a MIX of
|
|
5042
|
+
// literal text and actions (` · #${tag}` → ` · #{{.Tag}}`). Those don't
|
|
5043
|
+
// start with `{{`, so a plain `startsWith` test let them fall through to
|
|
5044
|
+
// the wrap below and produced `{{ · #{{.Tag}}}}` — invalid
|
|
5045
|
+
// `html/template` syntax that panics at parse time (#1933, blog PostList
|
|
5046
|
+
// status line).
|
|
5047
|
+
//
|
|
5048
|
+
// `isTemplateFragment` makes this decision structurally (a `{{`-leading
|
|
5049
|
+
// action block or a template-literal kind), not by substring-matching `{{`:
|
|
5050
|
+
// a bare string literal that merely CONTAINS `{{` (JSX `{"{{"}` → Go expr
|
|
5051
|
+
// `"{{"`) is neither — it must still be wrapped so html/template evaluates
|
|
5052
|
+
// and escapes the string instead of emitting the raw quotes (#1937 review).
|
|
3846
5053
|
// Use comment markers instead of <span> to avoid changing DOM structure.
|
|
3847
|
-
if (goExpr.
|
|
5054
|
+
if (this.isTemplateFragment(goExpr, classify.parsed?.kind)) {
|
|
3848
5055
|
if (expr.slotId) {
|
|
3849
5056
|
return `{{bfTextStart "${expr.slotId}"}}${goExpr}{{bfTextEnd}}`
|
|
3850
5057
|
}
|
|
@@ -3860,6 +5067,41 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3860
5067
|
return `{{${goExpr}}}`
|
|
3861
5068
|
}
|
|
3862
5069
|
|
|
5070
|
+
/**
|
|
5071
|
+
* Decide whether a lowered Go string is ALREADY a self-contained template
|
|
5072
|
+
* fragment — i.e. it carries its own `{{...}}` actions and so must NOT be
|
|
5073
|
+
* re-wrapped in another `{{...}}` (doing so yields `{{ {{...}} }}`, which
|
|
5074
|
+
* `html/template` rejects at parse time). Single source of truth for the
|
|
5075
|
+
* wrap-or-not decision across `renderExpression`, `templateLiteral`, and the
|
|
5076
|
+
* static-string / attribute interpolation paths.
|
|
5077
|
+
*
|
|
5078
|
+
* The decision is STRUCTURAL, deliberately NOT a `{{` substring scan: literal
|
|
5079
|
+
* text and Go string literals are ambiguous to scan — `5" ${x}` lowers to
|
|
5080
|
+
* `5" {{.X}}` while JS `{"{{"}` lowers to the Go literal `"{{"`; both mix
|
|
5081
|
+
* quotes and braces, so no scan can tell them apart. Two — and only two —
|
|
5082
|
+
* structural shapes are fragments:
|
|
5083
|
+
*
|
|
5084
|
+
* 1. A pure action block (`{{if}}` / `{{with}}` / `{{range}}` from a ternary,
|
|
5085
|
+
* a `find().prop`, a `filter().length`, …). The emitter prepends NO
|
|
5086
|
+
* literal text to these, so they ALWAYS start with `{{`; a leading `{{`
|
|
5087
|
+
* is therefore an unambiguous structural marker for this whole class.
|
|
5088
|
+
* 2. A template literal — the ONLY source form that interleaves author
|
|
5089
|
+
* literal text with `{{...}}` actions (` · #${tag}` → ` · #{{.Tag}}`), so
|
|
5090
|
+
* it may begin with literal text and is detected by its parsed `kind`.
|
|
5091
|
+
*
|
|
5092
|
+
* Everything else is a bare pipeline (`.Foo`, `len .X`, `bf_arr …`) — even one
|
|
5093
|
+
* whose value contains `{{` inside a Go string literal — and MUST be wrapped.
|
|
5094
|
+
*
|
|
5095
|
+
* Invariant (enforced by the `template-fragment invariant` tests): no
|
|
5096
|
+
* non-template-literal fragment ever begins with literal text, so case 1's
|
|
5097
|
+
* `startsWith('{{')` is complete. If a future emitter prepends literal text to
|
|
5098
|
+
* an action block those tests fail — fix it by giving that shape a parsed kind
|
|
5099
|
+
* this helper can key off, exactly as template literals are handled here.
|
|
5100
|
+
*/
|
|
5101
|
+
private isTemplateFragment(go: string, kind?: ParsedExpr['kind']): boolean {
|
|
5102
|
+
return go.startsWith('{{') || kind === 'template-literal'
|
|
5103
|
+
}
|
|
5104
|
+
|
|
3863
5105
|
/**
|
|
3864
5106
|
* Render a client-only conditional as comment markers.
|
|
3865
5107
|
* Used when @client directive is applied to an unsupported conditional.
|
|
@@ -3888,6 +5130,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3888
5130
|
// ===========================================================================
|
|
3889
5131
|
|
|
3890
5132
|
identifier(name: string): string {
|
|
5133
|
+
// `undefined` / `null` inside a larger expression tree (a ternary
|
|
5134
|
+
// branch like `props.isActive ? 'page' : undefined`, #1896
|
|
5135
|
+
// pagination) renders as the empty string — the top-level
|
|
5136
|
+
// `convertExpressionToGo` short-circuit doesn't see nested ones.
|
|
5137
|
+
if (name === 'undefined' || name === 'null') return '""'
|
|
3891
5138
|
// Module pure-string const (e.g. `const baseClasses = '...'` used in a
|
|
3892
5139
|
// className template literal): inline the literal value rather than
|
|
3893
5140
|
// emit `{{.BaseClasses}}` against a Props field that never exists.
|
|
@@ -3901,6 +5148,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3901
5148
|
}
|
|
3902
5149
|
const inlined = this.resolveModuleStringConst(name)
|
|
3903
5150
|
if (inlined !== null) return inlined
|
|
5151
|
+
// Module numeric const (e.g. `const TRACK = 8` used in a width expression):
|
|
5152
|
+
// inline the literal value rather than emit `{{.TRACK}}` against a Props
|
|
5153
|
+
// field that never exists. Mirrors the string-const inlining above.
|
|
5154
|
+
const inlinedNum = this.resolveModuleNumericConst(name)
|
|
5155
|
+
if (inlinedNum !== null) return inlinedNum
|
|
3904
5156
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
3905
5157
|
if (currentLoopParam && name === currentLoopParam) return '.'
|
|
3906
5158
|
// An *outer* loop's value variable (we're in a nested loop) is in scope as
|
|
@@ -3908,7 +5160,101 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3908
5160
|
// the inner dot no longer refers to it, and it's not a root field. (#1677)
|
|
3909
5161
|
if (this.isOuterLoopParam(name)) return `$${name}`
|
|
3910
5162
|
if (this.loopVarRefCount.has(name)) return `$${name}`
|
|
3911
|
-
|
|
5163
|
+
// (#1897) A bare reference to a component-scope derived const (e.g. `root`)
|
|
5164
|
+
// lowers to `.Root`; note it so `generateTypes` emits a computed field.
|
|
5165
|
+
if (this.localConstants.some(c => c.name === name && !c.isModule && !c.containsArrow)) {
|
|
5166
|
+
this.referencedDerivedConsts.add(name)
|
|
5167
|
+
}
|
|
5168
|
+
// Env-signal binding (incl. an alias) → canonical `.SearchParams` (#1922).
|
|
5169
|
+
return this.searchParamsFieldRef(name) ?? this.rootFieldRef(name)
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
/**
|
|
5173
|
+
* (#1897 PostList) Compute the Go struct fields for component-scope derived
|
|
5174
|
+
* string consts referenced by the template (e.g. `root = base || '/'`). Each
|
|
5175
|
+
* is lowered to a constructor-context Go expression via `lowerCtorExpr`, with
|
|
5176
|
+
* its dependency consts inlined. Skips names that collide with an existing
|
|
5177
|
+
* field (`takenFieldNames`) or that the lowerer can't represent.
|
|
5178
|
+
*/
|
|
5179
|
+
private computeDerivedConstFields(
|
|
5180
|
+
takenFieldNames: ReadonlySet<string>,
|
|
5181
|
+
): { name: string; init: string }[] {
|
|
5182
|
+
const fields: { name: string; init: string }[] = []
|
|
5183
|
+
for (const name of this.referencedDerivedConsts) {
|
|
5184
|
+
const fieldName = this.capitalizeFieldName(name)
|
|
5185
|
+
if (takenFieldNames.has(fieldName)) continue
|
|
5186
|
+
const c = this.localConstants.find(lc => lc.name === name && !lc.isModule && lc.value)
|
|
5187
|
+
if (!c?.value) continue
|
|
5188
|
+
const expr = this.parseLiteralExpression(c.value)
|
|
5189
|
+
if (!expr) continue
|
|
5190
|
+
// The field is typed `string`; only emit when the value is provably a Go
|
|
5191
|
+
// string, so a numeric/other const referenced in the template can't be
|
|
5192
|
+
// assigned into a string field (#1945 review).
|
|
5193
|
+
if (!this.isStringExpr(expr, new Set())) continue
|
|
5194
|
+
const init = this.lowerCtorExpr(expr, {
|
|
5195
|
+
searchParamsVars: new Set(),
|
|
5196
|
+
params: new Map(),
|
|
5197
|
+
consts: new Set([name]),
|
|
5198
|
+
})
|
|
5199
|
+
if (init === null) continue
|
|
5200
|
+
fields.push({ name: fieldName, init })
|
|
5201
|
+
}
|
|
5202
|
+
return fields
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
/**
|
|
5206
|
+
* Conservative check that a JS expression is *definitely* string-valued —
|
|
5207
|
+
* used to gate derived-const field emission (the field is typed `string`).
|
|
5208
|
+
* Recognizes string/template literals, string-returning methods (`.replace`,
|
|
5209
|
+
* `.trim`, … and `searchParams().get`), `+` / `||` / `??` where a branch is
|
|
5210
|
+
* string-valued, and a component-const reference to such a value. Anything
|
|
5211
|
+
* unproven (numbers, `props.X`, calls it doesn't know) returns false.
|
|
5212
|
+
*/
|
|
5213
|
+
private isStringExpr(node: ts.Expression, seen: Set<string>): boolean {
|
|
5214
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
5215
|
+
if (
|
|
5216
|
+
ts.isStringLiteral(node) ||
|
|
5217
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
5218
|
+
ts.isTemplateExpression(node)
|
|
5219
|
+
) {
|
|
5220
|
+
return true
|
|
5221
|
+
}
|
|
5222
|
+
if (ts.isBinaryExpression(node)) {
|
|
5223
|
+
const op = node.operatorToken.kind
|
|
5224
|
+
// `+`: a string on *either* side forces string concatenation.
|
|
5225
|
+
if (op === ts.SyntaxKind.PlusToken) {
|
|
5226
|
+
return this.isStringExpr(node.left, seen) || this.isStringExpr(node.right, seen)
|
|
5227
|
+
}
|
|
5228
|
+
// `||` / `??` evaluate to *one* operand, so the result is only provably a
|
|
5229
|
+
// string when *both* sides are (`props.count ?? ''` is not — it can be the
|
|
5230
|
+
// number) (#1945 review).
|
|
5231
|
+
if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) {
|
|
5232
|
+
return this.isStringExpr(node.left, seen) && this.isStringExpr(node.right, seen)
|
|
5233
|
+
}
|
|
5234
|
+
return false
|
|
5235
|
+
}
|
|
5236
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
5237
|
+
const m = node.expression.name.text
|
|
5238
|
+
const STRING_METHODS = new Set([
|
|
5239
|
+
'replace', 'trim', 'trimStart', 'trimEnd', 'toLowerCase', 'toUpperCase',
|
|
5240
|
+
'slice', 'substring', 'substr', 'padStart', 'padEnd', 'concat', 'repeat', 'get',
|
|
5241
|
+
])
|
|
5242
|
+
return STRING_METHODS.has(m)
|
|
5243
|
+
}
|
|
5244
|
+
if (ts.isConditionalExpression(node)) {
|
|
5245
|
+
return (
|
|
5246
|
+
this.isStringExpr(node.whenTrue, seen) && this.isStringExpr(node.whenFalse, seen)
|
|
5247
|
+
)
|
|
5248
|
+
}
|
|
5249
|
+
if (ts.isIdentifier(node)) {
|
|
5250
|
+
if (seen.has(node.text)) return false
|
|
5251
|
+
const c = this.localConstants.find(lc => lc.name === node.text && !lc.isModule && lc.value)
|
|
5252
|
+
if (c?.value) {
|
|
5253
|
+
const inner = this.parseLiteralExpression(c.value)
|
|
5254
|
+
if (inner) return this.isStringExpr(inner, new Set([...seen, node.text]))
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
return false
|
|
3912
5258
|
}
|
|
3913
5259
|
|
|
3914
5260
|
/**
|
|
@@ -3937,6 +5283,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3937
5283
|
return `${prefix}${this.capitalizeFieldName(name)}`
|
|
3938
5284
|
}
|
|
3939
5285
|
|
|
5286
|
+
/**
|
|
5287
|
+
* (#1922) When `name` is a local binding of the `searchParams()` env signal,
|
|
5288
|
+
* resolve it to the canonical `.SearchParams` field — not `.<Capitalized
|
|
5289
|
+
* name>` — so an aliased `import { searchParams as sp }` (`sp()`) reaches the
|
|
5290
|
+
* same struct field the generator emits. Returns null for any other name so
|
|
5291
|
+
* callers fall back to their normal field-ref lowering.
|
|
5292
|
+
*/
|
|
5293
|
+
private searchParamsFieldRef(name: string): string | null {
|
|
5294
|
+
return this.searchParamsLocals.has(name) ? this.rootFieldRef('searchParams') : null
|
|
5295
|
+
}
|
|
5296
|
+
|
|
3940
5297
|
/**
|
|
3941
5298
|
* Build the module pure-string-const map from the IR's localConstants.
|
|
3942
5299
|
* A const qualifies only when it is module-scope (`isModule`) and its
|
|
@@ -3947,49 +5304,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3947
5304
|
* pure compile-time string can be safely inlined byte-for-byte.
|
|
3948
5305
|
*/
|
|
3949
5306
|
private collectModuleStringConsts(constants: IRMetadata['localConstants']): Map<string, string> {
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
const literal = this.parsePureStringLiteral(c.value)
|
|
3955
|
-
if (literal !== null) map.set(c.name, literal)
|
|
3956
|
-
}
|
|
3957
|
-
return map
|
|
5307
|
+
// Single source of truth shared with the Mojo / Xslate adapters
|
|
5308
|
+
// (fixed-point resolution incl. composed template-literal consts and
|
|
5309
|
+
// `[...].join(sep)` — #1896 / #1897).
|
|
5310
|
+
return collectModuleStringConstsShared(constants)
|
|
3958
5311
|
}
|
|
3959
5312
|
|
|
3960
|
-
|
|
3961
|
-
* Parse a const initializer's source text. Returns the unescaped string
|
|
3962
|
-
* value when the whole initializer is a single string literal (or a
|
|
3963
|
-
* no-substitution template literal), else `null`. Uses the TS parser so
|
|
3964
|
-
* escapes/quotes are resolved exactly as JS would, matching the value
|
|
3965
|
-
* the Hono reference inlines at runtime.
|
|
3966
|
-
*/
|
|
3967
|
-
private parsePureStringLiteral(source: string): string | null {
|
|
3968
|
-
const sf = ts.createSourceFile(
|
|
3969
|
-
'__const.ts',
|
|
3970
|
-
`const __x = (${source});`,
|
|
3971
|
-
ts.ScriptTarget.Latest,
|
|
3972
|
-
/*setParentNodes*/ false,
|
|
3973
|
-
)
|
|
3974
|
-
const stmt = sf.statements[0]
|
|
3975
|
-
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
3976
|
-
const decl = stmt.declarationList.declarations[0]
|
|
3977
|
-
let init = decl?.initializer
|
|
3978
|
-
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
3979
|
-
if (!init) return null
|
|
3980
|
-
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
3981
|
-
return init.text
|
|
3982
|
-
}
|
|
3983
|
-
// (#checkbox) `[...string literals].join(SEP)` — a const that flattens an
|
|
3984
|
-
// array of pure string literals to a single string at module load (e.g.
|
|
3985
|
-
// Checkbox's `stateClasses = [...].join(' ')`). Evaluate it statically so
|
|
3986
|
-
// the const inlines byte-for-byte like the Hono reference. Only fires when
|
|
3987
|
-
// every array element is a string/no-substitution-template literal and the
|
|
3988
|
-
// separator is a string-literal argument (or omitted → default `,`).
|
|
3989
|
-
const joined = this.evalStringArrayJoin(init)
|
|
3990
|
-
if (joined !== null) return joined
|
|
3991
|
-
return null
|
|
3992
|
-
}
|
|
5313
|
+
|
|
3993
5314
|
|
|
3994
5315
|
/**
|
|
3995
5316
|
* (#checkbox) Statically evaluate `[<string literals>].join(<sep?>)`.
|
|
@@ -3998,31 +5319,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3998
5319
|
* non-string-literal separator). Comments/whitespace between elements are
|
|
3999
5320
|
* irrelevant — the TS parser already discarded them.
|
|
4000
5321
|
*/
|
|
4001
|
-
private evalStringArrayJoin(node: ts.Expression): string | null {
|
|
4002
|
-
if (!ts.isCallExpression(node)) return null
|
|
4003
|
-
const callee = node.expression
|
|
4004
|
-
if (!ts.isPropertyAccessExpression(callee)) return null
|
|
4005
|
-
if (callee.name.text !== 'join') return null
|
|
4006
|
-
let recv: ts.Expression = callee.expression
|
|
4007
|
-
while (ts.isParenthesizedExpression(recv)) recv = recv.expression
|
|
4008
|
-
if (!ts.isArrayLiteralExpression(recv)) return null
|
|
4009
|
-
const parts: string[] = []
|
|
4010
|
-
for (const el of recv.elements) {
|
|
4011
|
-
if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) {
|
|
4012
|
-
parts.push(el.text)
|
|
4013
|
-
} else {
|
|
4014
|
-
return null
|
|
4015
|
-
}
|
|
4016
|
-
}
|
|
4017
|
-
let sep = ','
|
|
4018
|
-
if (node.arguments.length >= 1) {
|
|
4019
|
-
const arg = node.arguments[0]
|
|
4020
|
-
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) sep = arg.text
|
|
4021
|
-
else return null
|
|
4022
|
-
}
|
|
4023
|
-
return parts.join(sep)
|
|
4024
|
-
}
|
|
4025
|
-
|
|
4026
5322
|
/**
|
|
4027
5323
|
* Resolve an identifier to its inlined Go string literal when it names a
|
|
4028
5324
|
* module pure-string const. Returns the Go template literal form
|
|
@@ -4043,6 +5339,31 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4043
5339
|
return `"${this.escapeGoString(value)}"`
|
|
4044
5340
|
}
|
|
4045
5341
|
|
|
5342
|
+
/**
|
|
5343
|
+
* Inline a module-level numeric const (`const TRACK = 8`) as its literal
|
|
5344
|
+
* value. Only a plain numeric initializer qualifies — anything computed or
|
|
5345
|
+
* non-numeric falls through to the normal field/ident resolution. Scoped to
|
|
5346
|
+
* module consts (like the string variant) and guarded against loop vars so a
|
|
5347
|
+
* range variable that shadows a const name still wins.
|
|
5348
|
+
*/
|
|
5349
|
+
private resolveModuleNumericConst(name: string): string | null {
|
|
5350
|
+
if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
|
|
5351
|
+
return null
|
|
5352
|
+
}
|
|
5353
|
+
if (this.loopVarRefCount.has(name)) return null
|
|
5354
|
+
if (this.isOuterLoopParam(name)) return null
|
|
5355
|
+
const c = this.localConstants.find(
|
|
5356
|
+
(k) => k.name === name && k.isModule && !k.containsArrow,
|
|
5357
|
+
)
|
|
5358
|
+
if (!c || c.value === undefined) return null
|
|
5359
|
+
// `value` is reconstructed from source text, so a valid TS literal may carry
|
|
5360
|
+
// numeric separators (`100_000`). Strip them between digits, then accept a
|
|
5361
|
+
// plain decimal / float; Go template numeric literals don't allow `_`, so
|
|
5362
|
+
// the stripped form is what gets emitted.
|
|
5363
|
+
const v = c.value.trim().replace(/(?<=\d)_(?=\d)/g, '')
|
|
5364
|
+
return /^-?\d+(\.\d+)?$/.test(v) ? v : null
|
|
5365
|
+
}
|
|
5366
|
+
|
|
4046
5367
|
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
4047
5368
|
if (literalType === 'string') return `"${value}"`
|
|
4048
5369
|
if (literalType === 'null') return 'nil'
|
|
@@ -4050,9 +5371,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4050
5371
|
}
|
|
4051
5372
|
|
|
4052
5373
|
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
4053
|
-
// Signal call: count() -> .Count (or $.Count inside a loop, #1677)
|
|
5374
|
+
// Signal call: count() -> .Count (or $.Count inside a loop, #1677).
|
|
5375
|
+
// An env-signal binding (`searchParams()`, or an aliased `sp()`) resolves to
|
|
5376
|
+
// the canonical `.SearchParams` field regardless of the JS name (#1922).
|
|
4054
5377
|
if (callee.kind === 'identifier' && args.length === 0) {
|
|
4055
|
-
return this.rootFieldRef(callee.name)
|
|
5378
|
+
return this.searchParamsFieldRef(callee.name) ?? this.rootFieldRef(callee.name)
|
|
4056
5379
|
}
|
|
4057
5380
|
// Array methods (`.join` and any others added to ArrayMethod, #1443)
|
|
4058
5381
|
// are lifted into the `array-method` IR kind at parse time, so
|
|
@@ -4104,6 +5427,24 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4104
5427
|
if (result) return result
|
|
4105
5428
|
}
|
|
4106
5429
|
|
|
5430
|
+
// `<memo>().length` where the memo's `.map()` feeds a handler-filled loop
|
|
5431
|
+
// slice → `len .<Slice>` (#1897 PostList: `visible().length` →
|
|
5432
|
+
// `len .PostListItems`). The slice holds the rendered (filtered) items, so
|
|
5433
|
+
// its length is the count — unlike the memo's own field, which is unset.
|
|
5434
|
+
if (
|
|
5435
|
+
property === 'length' &&
|
|
5436
|
+
object.kind === 'call' &&
|
|
5437
|
+
object.callee.kind === 'identifier' &&
|
|
5438
|
+
object.args.length === 0
|
|
5439
|
+
) {
|
|
5440
|
+
const slice = this.memoBackedLoopSlice.get(object.callee.name)
|
|
5441
|
+
if (slice) {
|
|
5442
|
+
// Root field, so reach it through `$.` inside a loop (#1677).
|
|
5443
|
+
const prefix = this.loopParamStack.length > 0 ? '$.' : '.'
|
|
5444
|
+
return `len ${prefix}${slice}`
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
|
|
4107
5448
|
// find().property / findLast().property → {{with bf_find ...}}{{.Property}}{{end}}
|
|
4108
5449
|
if (object.kind === 'higher-order' && (object.method === 'find' || object.method === 'findLast')) {
|
|
4109
5450
|
const findResult = this.renderHigherOrderExpr(object, emit)
|
|
@@ -4122,6 +5463,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4122
5463
|
return this.rootFieldRef(property)
|
|
4123
5464
|
}
|
|
4124
5465
|
|
|
5466
|
+
// Static property access on a module object-literal const
|
|
5467
|
+
// (`variantClasses.ghost` in a class template literal, #1896
|
|
5468
|
+
// pagination) resolves at compile time — same lookup as the
|
|
5469
|
+
// bracket-index form in `resolveStaticRecordLiteralIndex`, reached
|
|
5470
|
+
// here when the access is nested inside a larger ParsedExpr tree.
|
|
5471
|
+
if (object.kind === 'identifier') {
|
|
5472
|
+
const staticValue = this.resolveStaticRecordLiteralIndex(
|
|
5473
|
+
`${object.name}.${property}`,
|
|
5474
|
+
)
|
|
5475
|
+
if (staticValue !== null) return staticValue
|
|
5476
|
+
}
|
|
5477
|
+
|
|
4125
5478
|
// Inside a loop, the loop param variable refers to the current item
|
|
4126
5479
|
// (dot). e.g. `msg.role` inside `{{range $_, $msg := .Messages}}` → `.Role`
|
|
4127
5480
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
@@ -4134,34 +5487,56 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4134
5487
|
return `${obj}.${this.capitalizeFieldName(property)}`
|
|
4135
5488
|
}
|
|
4136
5489
|
|
|
5490
|
+
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
5491
|
+
// Go's `index` builtin: `index $arr $i`. Both operands render
|
|
5492
|
+
// through the same emitter so a loop-variable / arithmetic index
|
|
5493
|
+
// lowers correctly. A multi-token operand (`bf_add $i 1`) must be
|
|
5494
|
+
// parenthesised or Go parses it as extra `index` arguments. #1897
|
|
5495
|
+
// (`selected()[index]`). (data-table's broader keyed-loop SSR stays
|
|
5496
|
+
// tracked under #1896.)
|
|
5497
|
+
return `index ${wrapIfMultiToken(emit(object))} ${wrapIfMultiToken(emit(index))}`
|
|
5498
|
+
}
|
|
5499
|
+
|
|
4137
5500
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
4138
5501
|
const l = emit(left)
|
|
4139
5502
|
const r = emit(right)
|
|
5503
|
+
// Every Go form below is a prefix function call (`bf_mul a b`, `gt a b`,
|
|
5504
|
+
// `eq a b`), so a COMPOUND operand must be parenthesised or the template
|
|
5505
|
+
// parser folds its tokens into the call's argument list — e.g. nested
|
|
5506
|
+
// arithmetic `(elapsed / TRACK) * 100` would emit `bf_mul bf_div .Elapsed
|
|
5507
|
+
// .TRACK 100`, handing `bf_mul` four args. `wrapIfMultiToken` is a no-op
|
|
5508
|
+
// for single tokens and quoted literals, so simple operands are untouched.
|
|
5509
|
+
const wl = wrapIfMultiToken(l)
|
|
5510
|
+
const wr = wrapIfMultiToken(r)
|
|
4140
5511
|
switch (op) {
|
|
4141
5512
|
case '===':
|
|
4142
|
-
case '==':
|
|
4143
|
-
|
|
5513
|
+
case '==': {
|
|
5514
|
+
const [el, er] = stringTolerantEqOperands(l, r)
|
|
5515
|
+
return `eq ${wrapIfMultiToken(el)} ${wrapIfMultiToken(er)}`
|
|
5516
|
+
}
|
|
4144
5517
|
case '!==':
|
|
4145
|
-
case '!=':
|
|
4146
|
-
|
|
5518
|
+
case '!=': {
|
|
5519
|
+
const [el, er] = stringTolerantEqOperands(l, r)
|
|
5520
|
+
return `ne ${wrapIfMultiToken(el)} ${wrapIfMultiToken(er)}`
|
|
5521
|
+
}
|
|
4147
5522
|
case '>':
|
|
4148
|
-
return `gt ${
|
|
5523
|
+
return `gt ${wl} ${wr}`
|
|
4149
5524
|
case '<':
|
|
4150
|
-
return `lt ${
|
|
5525
|
+
return `lt ${wl} ${wr}`
|
|
4151
5526
|
case '>=':
|
|
4152
|
-
return `ge ${
|
|
5527
|
+
return `ge ${wl} ${wr}`
|
|
4153
5528
|
case '<=':
|
|
4154
|
-
return `le ${
|
|
5529
|
+
return `le ${wl} ${wr}`
|
|
4155
5530
|
case '+':
|
|
4156
|
-
return `bf_add ${
|
|
5531
|
+
return `bf_add ${wl} ${wr}`
|
|
4157
5532
|
case '-':
|
|
4158
|
-
return `bf_sub ${
|
|
5533
|
+
return `bf_sub ${wl} ${wr}`
|
|
4159
5534
|
case '*':
|
|
4160
|
-
return `bf_mul ${
|
|
5535
|
+
return `bf_mul ${wl} ${wr}`
|
|
4161
5536
|
case '/':
|
|
4162
|
-
return `bf_div ${
|
|
5537
|
+
return `bf_div ${wl} ${wr}`
|
|
4163
5538
|
case '%':
|
|
4164
|
-
return `bf_mod ${
|
|
5539
|
+
return `bf_mod ${wl} ${wr}`
|
|
4165
5540
|
default:
|
|
4166
5541
|
return `${l} ${op} ${r}`
|
|
4167
5542
|
}
|
|
@@ -4180,10 +5555,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4180
5555
|
right: ParsedExpr,
|
|
4181
5556
|
emit: (e: ParsedExpr) => string,
|
|
4182
5557
|
): string {
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
5558
|
+
// Go's `and`/`or` are prefix builtins, so every operand that renders to
|
|
5559
|
+
// more than one token (a method/function call like `.SearchParams.Get
|
|
5560
|
+
// "sort"`, an arithmetic `bf_add a b`, a comparison `eq a b`, a nested
|
|
5561
|
+
// `not …` / `or …`) must be parenthesised or it degrades into extra
|
|
5562
|
+
// sibling args of the enclosing `and`/`or`. `wrapIfMultiToken` is the
|
|
5563
|
+
// file-wide idiom for exactly this (every other prefix-helper emitter
|
|
5564
|
+
// composes operands through it); a bare field ref / quoted literal stays
|
|
5565
|
+
// uncluttered. This is what makes `searchParams().get(k) ?? d` lower to
|
|
5566
|
+
// `or (.SearchParams.Get "sort") "none"` instead of the broken
|
|
5567
|
+
// `or .SearchParams.Get "sort" "none"` (#1922).
|
|
5568
|
+
const wrapLeft = wrapIfMultiToken(emit(left))
|
|
5569
|
+
const wrapRight = wrapIfMultiToken(emit(right))
|
|
4187
5570
|
if (op === '&&') return `and ${wrapLeft} ${wrapRight}`
|
|
4188
5571
|
return `or ${wrapLeft} ${wrapRight}`
|
|
4189
5572
|
}
|
|
@@ -4212,7 +5595,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4212
5595
|
if (part.type === 'string') {
|
|
4213
5596
|
result += part.value
|
|
4214
5597
|
} else {
|
|
4215
|
-
|
|
5598
|
+
// A nested ternary emits a complete `{{if …}}…{{end}}` action
|
|
5599
|
+
// chain (see `conditional` above) — wrapping it again produces
|
|
5600
|
+
// `{{{{if …}}` and a template parse error (#1896, the tooltip
|
|
5601
|
+
// open/closed class ternary with module-const branches). Same
|
|
5602
|
+
// `isTemplateFragment` guard as `renderExpression`.
|
|
5603
|
+
const e = emit(part.expr)
|
|
5604
|
+
result += this.isTemplateFragment(e, part.expr.kind) ? e : `{{${e}}}`
|
|
4216
5605
|
}
|
|
4217
5606
|
}
|
|
4218
5607
|
return result
|
|
@@ -4398,6 +5787,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4398
5787
|
const recv = emit(object)
|
|
4399
5788
|
return `bf_trim ${wrapIfMultiToken(recv)}`
|
|
4400
5789
|
}
|
|
5790
|
+
case 'toFixed': {
|
|
5791
|
+
// `.toFixed(digits?)` → `bf_to_fixed` (`fmt.Sprintf("%.*f", …)`).
|
|
5792
|
+
// Default 0 digits when the argument is omitted. #1897.
|
|
5793
|
+
const recv = emit(object)
|
|
5794
|
+
const digits = args.length >= 1 ? emit(args[0]) : '0'
|
|
5795
|
+
return `bf_to_fixed ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(digits)}`
|
|
5796
|
+
}
|
|
4401
5797
|
case 'split': {
|
|
4402
5798
|
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
|
|
4403
5799
|
// `[]any`. No separator → the whole string as a single element
|
|
@@ -5258,7 +6654,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5258
6654
|
/**
|
|
5259
6655
|
* Convert a JS expression to Go template syntax.
|
|
5260
6656
|
*/
|
|
5261
|
-
private convertExpressionToGo(jsExpr: string): string {
|
|
6657
|
+
private convertExpressionToGo(jsExpr: string, out?: { parsed?: ParsedExpr }): string {
|
|
5262
6658
|
const trimmed = jsExpr.trim()
|
|
5263
6659
|
|
|
5264
6660
|
// Handle null/undefined specially
|
|
@@ -5266,6 +6662,59 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5266
6662
|
return '""'
|
|
5267
6663
|
}
|
|
5268
6664
|
|
|
6665
|
+
// `IDENT['key']` over a module object-literal const with a STRING
|
|
6666
|
+
// LITERAL key is a fully static lookup — resolve it at compile time
|
|
6667
|
+
// (#1896). The generic member lowering below would otherwise
|
|
6668
|
+
// capitalize the bracket access into a field reference and emit an
|
|
6669
|
+
// invalid hyphenated path (`strokePaths['chevron-down']` →
|
|
6670
|
+
// `.StrokePaths.Chevron-down`, a template parse error).
|
|
6671
|
+
const staticIndexed = this.resolveStaticRecordLiteralIndex(trimmed)
|
|
6672
|
+
if (staticIndexed !== null) {
|
|
6673
|
+
return staticIndexed
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6676
|
+
// A bare identifier bound to a literal const inlines at compile time
|
|
6677
|
+
// (#1896 — pagination's `Page {currentPage()} of {totalPages}`:
|
|
6678
|
+
// `totalPages` is a function-scope `const totalPages = 5`, not a
|
|
6679
|
+
// prop, so the generic lowering would reference a nonexistent
|
|
6680
|
+
// `.TotalPages` field). Only pure numeric / single-quoted-string
|
|
6681
|
+
// initializers qualify; anything else may be runtime-dependent.
|
|
6682
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
|
|
6683
|
+
const litConst = (this.localConstants ?? []).find(c => c.name === trimmed)
|
|
6684
|
+
if (litConst?.value !== undefined) {
|
|
6685
|
+
const v = litConst.value.trim()
|
|
6686
|
+
if (/^-?\d+(\.\d+)?$/.test(v)) return v
|
|
6687
|
+
const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v)
|
|
6688
|
+
if (strLit) return JSON.stringify(strLit[1])
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
|
|
6692
|
+
// (#1897 PostList) A local URL-builder helper (`hrefFor`, or `sortHref` /
|
|
6693
|
+
// `tagHref` delegating to it) lowers to a `bf_query` action — there is no Go
|
|
6694
|
+
// method backing a `.SortHref "date"` call. Tried before the generic inliner
|
|
6695
|
+
// because these helpers are block-bodied / delegate, which the inliner skips.
|
|
6696
|
+
const urlBuilt = this.lowerUrlBuilderHelperCall(trimmed)
|
|
6697
|
+
if (urlBuilt !== null) return urlBuilt
|
|
6698
|
+
|
|
6699
|
+
// Inline a call to a local, expression-bodied helper arrow
|
|
6700
|
+
// (`sortClass(k)` / `tagClass(t)`) by substituting its params with the call
|
|
6701
|
+
// args and lowering the resulting expression. There is no Go method backing
|
|
6702
|
+
// a `.SortClass "date"` call, so the call site must carry the computation
|
|
6703
|
+
// (`{{if eq .Params.Sort "date"}}sort on{{else}}sort{{end}}`). Only self-
|
|
6704
|
+
// contained helpers are inlined; one that delegates to another local helper
|
|
6705
|
+
// (e.g. `sortHref` → `hrefFor`) is left for a later capability.
|
|
6706
|
+
const inlined = this.inlineLocalHelperCall(trimmed)
|
|
6707
|
+
if (inlined !== null) {
|
|
6708
|
+
return this.convertExpressionToGo(inlined, out)
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
// Parse only here — *after* the early returns above, which resolve
|
|
6712
|
+
// `null`/`undefined`, static record indexes, and inlined literal consts
|
|
6713
|
+
// without a parse. The result is reported to the caller via `out` below
|
|
6714
|
+
// (after the support gate) so `renderExpression` can classify the
|
|
6715
|
+
// expression (template literal vs. not) off this single `parseExpression`,
|
|
6716
|
+
// with no extra `ts.createSourceFile` on the `bf build` hot path and no
|
|
6717
|
+
// parse at all for the early-return shapes.
|
|
5269
6718
|
const parsed = parseExpression(trimmed)
|
|
5270
6719
|
const support = isSupported(parsed)
|
|
5271
6720
|
|
|
@@ -5280,13 +6729,429 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5280
6729
|
message: buildUnsupportedSuggestion(support),
|
|
5281
6730
|
},
|
|
5282
6731
|
})
|
|
5283
|
-
// Return empty string - Go template comments must be separate actions
|
|
6732
|
+
// Return empty string - Go template comments must be separate actions.
|
|
6733
|
+
// Deliberately leave `out.parsed` unset here: the sentinel `""` must take
|
|
6734
|
+
// the normal wrap path in `renderExpression` (→ `{{""}}`), not the
|
|
6735
|
+
// template-literal "already template text" path — otherwise an
|
|
6736
|
+
// unsupported interpolation (`template-literal` kind) would emit `""`
|
|
6737
|
+
// outside an action and render literal quotes into the HTML (#1937 review).
|
|
5284
6738
|
return `""`
|
|
5285
6739
|
}
|
|
5286
6740
|
|
|
6741
|
+
// Report the supported parse to the caller (template-literal classification
|
|
6742
|
+
// for `renderExpression`) only after the support gate, so the wrap-skip path
|
|
6743
|
+
// can never trigger on the error sentinel above.
|
|
6744
|
+
if (out) out.parsed = parsed
|
|
6745
|
+
|
|
5287
6746
|
return this.renderParsedExpr(parsed)
|
|
5288
6747
|
}
|
|
5289
6748
|
|
|
6749
|
+
/**
|
|
6750
|
+
* (#1897 PostList) If `jsExpr` is a call to a local, expression-bodied helper
|
|
6751
|
+
* arrow const (`sortClass(k)` / `tagClass(t)`), return its body with the call
|
|
6752
|
+
* args substituted for the params (re-emitted to source), so the caller can
|
|
6753
|
+
* lower the inlined expression. Returns null when `jsExpr` is not such a call,
|
|
6754
|
+
* or when the helper delegates to another local helper (e.g. `sortHref` →
|
|
6755
|
+
* `hrefFor`) — those need their own lowering capability and must not be
|
|
6756
|
+
* half-inlined here. Substitution is AST-based (no string matching) so it is
|
|
6757
|
+
* shadowing- and member-name-safe.
|
|
6758
|
+
*/
|
|
6759
|
+
private inlineLocalHelperCall(jsExpr: string): string | null {
|
|
6760
|
+
if (this.localHelperNames.size === 0) return null
|
|
6761
|
+
// Fast path: skip the AST parse unless the expression begins with a known
|
|
6762
|
+
// helper name applied as a call (`<helper>(`). The leading-identifier match
|
|
6763
|
+
// is an unambiguous prefix — the real shape check is the AST parse below.
|
|
6764
|
+
const head = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(jsExpr)
|
|
6765
|
+
if (!head || !this.localHelperNames.has(head[1])) return null
|
|
6766
|
+
|
|
6767
|
+
const call = this.parseLiteralExpression(jsExpr)
|
|
6768
|
+
if (!call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) return null
|
|
6769
|
+
// A spread arg (`f(...xs)`) can't map to positional params by text splice.
|
|
6770
|
+
if (call.arguments.some(a => ts.isSpreadElement(a))) return null
|
|
6771
|
+
const fnConst = this.localConstants.find(
|
|
6772
|
+
c => c.name === (call.expression as ts.Identifier).text && !c.isModule && c.value,
|
|
6773
|
+
)
|
|
6774
|
+
if (!fnConst?.value) return null
|
|
6775
|
+
const fn = this.parseLiteralExpression(fnConst.value)
|
|
6776
|
+
if (!fn || !ts.isArrowFunction(fn) || ts.isBlock(fn.body)) return null
|
|
6777
|
+
if (fn.parameters.length !== call.arguments.length) return null
|
|
6778
|
+
// Don't half-inline a helper that calls another local helper.
|
|
6779
|
+
if (this.bodyCallsLocalHelper(fn.body)) return null
|
|
6780
|
+
|
|
6781
|
+
const subs = new Map<string, string>()
|
|
6782
|
+
for (let i = 0; i < fn.parameters.length; i++) {
|
|
6783
|
+
const p = fn.parameters[i]
|
|
6784
|
+
if (!ts.isIdentifier(p.name)) return null
|
|
6785
|
+
// Parenthesize the arg so its own precedence survives the splice into the
|
|
6786
|
+
// body (e.g. `x === <param>` with arg `a || b` must not become
|
|
6787
|
+
// `x === a || b`).
|
|
6788
|
+
subs.set(p.name.text, `(${call.arguments[i].getText()})`)
|
|
6789
|
+
}
|
|
6790
|
+
// The splicer is scope-blind, so reject bodies where a textual identifier
|
|
6791
|
+
// replacement could be wrong: nested function scopes (shadowing / param
|
|
6792
|
+
// positions) and object shorthand keys that are params.
|
|
6793
|
+
if (!this.isSpliceSafeHelperBody(fn.body, new Set(subs.keys()))) return null
|
|
6794
|
+
return this.substituteHelperParams(fn.body, subs)
|
|
6795
|
+
}
|
|
6796
|
+
|
|
6797
|
+
/**
|
|
6798
|
+
* Guard for `substituteHelperParams`: the span splicer replaces identifiers by
|
|
6799
|
+
* name without scope tracking, so it is only safe on bodies free of
|
|
6800
|
+
* constructs where a param name could appear in a position the splice can't
|
|
6801
|
+
* handle — a nested function scope (shadowing or its own parameters), or an
|
|
6802
|
+
* object shorthand key (`{ k }`, which can't be rewritten to `{ (arg) }`).
|
|
6803
|
+
*/
|
|
6804
|
+
private isSpliceSafeHelperBody(body: ts.Node, paramNames: ReadonlySet<string>): boolean {
|
|
6805
|
+
let safe = true
|
|
6806
|
+
const visit = (n: ts.Node): void => {
|
|
6807
|
+
if (!safe) return
|
|
6808
|
+
if (
|
|
6809
|
+
ts.isArrowFunction(n) ||
|
|
6810
|
+
ts.isFunctionExpression(n) ||
|
|
6811
|
+
ts.isFunctionDeclaration(n)
|
|
6812
|
+
) {
|
|
6813
|
+
safe = false
|
|
6814
|
+
return
|
|
6815
|
+
}
|
|
6816
|
+
if (ts.isShorthandPropertyAssignment(n) && paramNames.has(n.name.text)) {
|
|
6817
|
+
safe = false
|
|
6818
|
+
return
|
|
6819
|
+
}
|
|
6820
|
+
ts.forEachChild(n, visit)
|
|
6821
|
+
}
|
|
6822
|
+
visit(body)
|
|
6823
|
+
return safe
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
/**
|
|
6827
|
+
* True when `body` contains a call to a *local* (component-scope) arrow helper
|
|
6828
|
+
* const — the signal that inlining `body` here would only push the problem to
|
|
6829
|
+
* another un-lowered helper (e.g. `sortHref`'s body calls `hrefFor`).
|
|
6830
|
+
*/
|
|
6831
|
+
private bodyCallsLocalHelper(body: ts.Node): boolean {
|
|
6832
|
+
let found = false
|
|
6833
|
+
const visit = (n: ts.Node): void => {
|
|
6834
|
+
if (found) return
|
|
6835
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
6836
|
+
const name = n.expression.text
|
|
6837
|
+
if (
|
|
6838
|
+
this.localConstants.some(c => c.name === name && !c.isModule && c.containsArrow)
|
|
6839
|
+
) {
|
|
6840
|
+
found = true
|
|
6841
|
+
return
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
ts.forEachChild(n, visit)
|
|
6845
|
+
}
|
|
6846
|
+
visit(body)
|
|
6847
|
+
return found
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6850
|
+
/**
|
|
6851
|
+
* Re-emit `body` to source with each identifier named in `subs` replaced by
|
|
6852
|
+
* the substitution's source text. Implemented as span splicing over `body`'s
|
|
6853
|
+
* own source (single source file — args are passed as text, not cross-file
|
|
6854
|
+
* AST nodes, which would corrupt a printer keyed to `body`'s source). The walk
|
|
6855
|
+
* skips non-value identifier positions — the property NAME in `a.b` and a
|
|
6856
|
+
* plain object-literal key in `{ k: … }` — so a param sharing a name with a
|
|
6857
|
+
* member or key is left untouched. (`isSpliceSafeHelperBody` has already
|
|
6858
|
+
* rejected nested functions and `{ k }` shorthand keys.)
|
|
6859
|
+
*/
|
|
6860
|
+
private substituteHelperParams(
|
|
6861
|
+
body: ts.Expression,
|
|
6862
|
+
subs: ReadonlyMap<string, string>,
|
|
6863
|
+
): string {
|
|
6864
|
+
const sf = body.getSourceFile()
|
|
6865
|
+
const base = body.getStart(sf)
|
|
6866
|
+
// Collect replacement spans (relative to the body's start), right-to-left.
|
|
6867
|
+
const repls: { start: number; end: number; text: string }[] = []
|
|
6868
|
+
const visit = (node: ts.Node): void => {
|
|
6869
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
6870
|
+
visit(node.expression) // skip `.name`
|
|
6871
|
+
return
|
|
6872
|
+
}
|
|
6873
|
+
if (ts.isPropertyAssignment(node)) {
|
|
6874
|
+
// A plain identifier/string key isn't a value position; only a computed
|
|
6875
|
+
// key (`[expr]`) is. Visit the initializer (and computed key) only.
|
|
6876
|
+
if (ts.isComputedPropertyName(node.name)) visit(node.name)
|
|
6877
|
+
visit(node.initializer)
|
|
6878
|
+
return
|
|
6879
|
+
}
|
|
6880
|
+
if (ts.isIdentifier(node)) {
|
|
6881
|
+
const sub = subs.get(node.text)
|
|
6882
|
+
if (sub !== undefined) {
|
|
6883
|
+
repls.push({ start: node.getStart(sf) - base, end: node.getEnd() - base, text: sub })
|
|
6884
|
+
return
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
ts.forEachChild(node, visit)
|
|
6888
|
+
}
|
|
6889
|
+
visit(body)
|
|
6890
|
+
|
|
6891
|
+
let text = body.getText(sf)
|
|
6892
|
+
for (const r of repls.sort((a, b) => b.start - a.start)) {
|
|
6893
|
+
text = text.slice(0, r.start) + r.text + text.slice(r.end)
|
|
6894
|
+
}
|
|
6895
|
+
return text
|
|
6896
|
+
}
|
|
6897
|
+
|
|
6898
|
+
/**
|
|
6899
|
+
* (#1897 PostList) Lower a call to a local URL-builder helper to a `bf_query`
|
|
6900
|
+
* template expression. Handles two shapes:
|
|
6901
|
+
* - the builder itself — `(sort, tag) => { const u = new URLSearchParams();
|
|
6902
|
+
* if (sort !== 'date') u.set('sort', sort); if (tag) u.set('tag', tag);
|
|
6903
|
+
* return u.toString() ? \`${root}?${u}\` : root }` — substitute the call
|
|
6904
|
+
* args for params and emit `bf_query <base> (<guard>) "key" <value> …`;
|
|
6905
|
+
* - a pass-through delegate — `(k) => hrefFor(k, params().tag)` — substitute
|
|
6906
|
+
* and recurse on the delegated call.
|
|
6907
|
+
* Returns null for anything else (→ existing lowering / method-call fallback).
|
|
6908
|
+
*/
|
|
6909
|
+
private lowerUrlBuilderHelperCall(jsExpr: string): string | null {
|
|
6910
|
+
if (this.localHelperNames.size === 0) return null
|
|
6911
|
+
const head = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(jsExpr)
|
|
6912
|
+
if (!head || !this.localHelperNames.has(head[1])) return null
|
|
6913
|
+
|
|
6914
|
+
const call = this.parseLiteralExpression(jsExpr)
|
|
6915
|
+
if (!call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) return null
|
|
6916
|
+
if (call.arguments.some(a => ts.isSpreadElement(a))) return null
|
|
6917
|
+
const fnConst = this.localConstants.find(
|
|
6918
|
+
c => c.name === (call.expression as ts.Identifier).text && !c.isModule && c.value,
|
|
6919
|
+
)
|
|
6920
|
+
if (!fnConst?.value) return null
|
|
6921
|
+
const fn = this.parseLiteralExpression(fnConst.value)
|
|
6922
|
+
if (!fn || !ts.isArrowFunction(fn) || fn.parameters.length !== call.arguments.length) return null
|
|
6923
|
+
|
|
6924
|
+
const subs = new Map<string, string>()
|
|
6925
|
+
for (let i = 0; i < fn.parameters.length; i++) {
|
|
6926
|
+
const p = fn.parameters[i]
|
|
6927
|
+
if (!ts.isIdentifier(p.name)) return null
|
|
6928
|
+
subs.set(p.name.text, `(${call.arguments[i].getText()})`)
|
|
6929
|
+
}
|
|
6930
|
+
|
|
6931
|
+
// Shape 1: the helper is itself a URLSearchParams builder.
|
|
6932
|
+
const shape = this.extractUrlBuilder(fn)
|
|
6933
|
+
if (shape) return this.emitUrlBuilder(shape, subs)
|
|
6934
|
+
|
|
6935
|
+
// Shape 2: a pass-through delegate to another local URL-builder helper.
|
|
6936
|
+
if (!ts.isBlock(fn.body)) {
|
|
6937
|
+
let body: ts.Expression = fn.body
|
|
6938
|
+
while (ts.isParenthesizedExpression(body)) body = body.expression
|
|
6939
|
+
if (
|
|
6940
|
+
ts.isCallExpression(body) &&
|
|
6941
|
+
ts.isIdentifier(body.expression) &&
|
|
6942
|
+
this.localHelperNames.has(body.expression.text)
|
|
6943
|
+
) {
|
|
6944
|
+
return this.lowerUrlBuilderHelperCall(this.substituteHelperParams(body, subs))
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
return null
|
|
6948
|
+
}
|
|
6949
|
+
|
|
6950
|
+
/**
|
|
6951
|
+
* Extract the `bf_query` shape from a `(…) => { const u = new URLSearchParams();
|
|
6952
|
+
* [if (G)] u.set(K, V); …; return <s> ? … : <base> }` helper, or null when the
|
|
6953
|
+
* block doesn't match that exact builder idiom.
|
|
6954
|
+
*/
|
|
6955
|
+
private extractUrlBuilder(
|
|
6956
|
+
arrow: ts.ArrowFunction,
|
|
6957
|
+
): { base: ts.Expression; sets: { guard: ts.Expression | null; key: string; value: ts.Expression }[] } | null {
|
|
6958
|
+
if (!ts.isBlock(arrow.body)) return null
|
|
6959
|
+
let builderVar: string | null = null
|
|
6960
|
+
let base: ts.Expression | null = null
|
|
6961
|
+
const sets: { guard: ts.Expression | null; key: string; value: ts.Expression }[] = []
|
|
6962
|
+
|
|
6963
|
+
for (const s of arrow.body.statements) {
|
|
6964
|
+
if (ts.isVariableStatement(s)) {
|
|
6965
|
+
for (const d of s.declarationList.declarations) {
|
|
6966
|
+
if (
|
|
6967
|
+
ts.isIdentifier(d.name) &&
|
|
6968
|
+
d.initializer &&
|
|
6969
|
+
ts.isNewExpression(d.initializer) &&
|
|
6970
|
+
ts.isIdentifier(d.initializer.expression) &&
|
|
6971
|
+
d.initializer.expression.text === 'URLSearchParams' &&
|
|
6972
|
+
(d.initializer.arguments?.length ?? 0) === 0
|
|
6973
|
+
) {
|
|
6974
|
+
builderVar = d.name.text
|
|
6975
|
+
}
|
|
6976
|
+
// Other locals (`const s = u.toString()`) are inert — ignored.
|
|
6977
|
+
}
|
|
6978
|
+
continue
|
|
6979
|
+
}
|
|
6980
|
+
// `if (G) u.set(K, V)` (no else).
|
|
6981
|
+
if (ts.isIfStatement(s) && !s.elseStatement && builderVar) {
|
|
6982
|
+
const set = this.matchUrlSet(s.thenStatement, builderVar)
|
|
6983
|
+
if (!set) return null
|
|
6984
|
+
sets.push({ guard: s.expression, key: set.key, value: set.value })
|
|
6985
|
+
continue
|
|
6986
|
+
}
|
|
6987
|
+
// Unguarded `u.set(K, V)`.
|
|
6988
|
+
if (ts.isExpressionStatement(s) && builderVar) {
|
|
6989
|
+
const set = this.matchUrlSet(s, builderVar)
|
|
6990
|
+
if (set) {
|
|
6991
|
+
sets.push({ guard: null, key: set.key, value: set.value })
|
|
6992
|
+
continue
|
|
6993
|
+
}
|
|
6994
|
+
return null
|
|
6995
|
+
}
|
|
6996
|
+
// `return <s> ? `${base}?…` : <base>` — the builder's return must be the
|
|
6997
|
+
// conditional that picks between the with-query and bare-base URL; its
|
|
6998
|
+
// `whenFalse` (no-query) branch is the base. Any other return shape isn't
|
|
6999
|
+
// this idiom (#1945 review) — bail to the method-call fallback.
|
|
7000
|
+
if (ts.isReturnStatement(s) && s.expression) {
|
|
7001
|
+
let e: ts.Expression = s.expression
|
|
7002
|
+
while (ts.isParenthesizedExpression(e)) e = e.expression
|
|
7003
|
+
if (!ts.isConditionalExpression(e)) return null
|
|
7004
|
+
base = e.whenFalse
|
|
7005
|
+
continue
|
|
7006
|
+
}
|
|
7007
|
+
return null
|
|
7008
|
+
}
|
|
7009
|
+
if (!builderVar || !base || sets.length === 0) return null
|
|
7010
|
+
return { base, sets }
|
|
7011
|
+
}
|
|
7012
|
+
|
|
7013
|
+
/**
|
|
7014
|
+
* Match `<builderVar>.set('literalKey', <value>)` — as a statement or an
|
|
7015
|
+
* if-then — returning the key text and value node, else null.
|
|
7016
|
+
*/
|
|
7017
|
+
private matchUrlSet(
|
|
7018
|
+
node: ts.Node,
|
|
7019
|
+
builderVar: string,
|
|
7020
|
+
): { key: string; value: ts.Expression } | null {
|
|
7021
|
+
const stmt = ts.isBlock(node) ? (node.statements.length === 1 ? node.statements[0] : null) : node
|
|
7022
|
+
if (!stmt || !ts.isExpressionStatement(stmt)) return null
|
|
7023
|
+
const call = stmt.expression
|
|
7024
|
+
if (
|
|
7025
|
+
ts.isCallExpression(call) &&
|
|
7026
|
+
ts.isPropertyAccessExpression(call.expression) &&
|
|
7027
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
7028
|
+
call.expression.expression.text === builderVar &&
|
|
7029
|
+
call.expression.name.text === 'set' &&
|
|
7030
|
+
call.arguments.length === 2 &&
|
|
7031
|
+
ts.isStringLiteral(call.arguments[0])
|
|
7032
|
+
) {
|
|
7033
|
+
return { key: call.arguments[0].text, value: call.arguments[1] }
|
|
7034
|
+
}
|
|
7035
|
+
return null
|
|
7036
|
+
}
|
|
7037
|
+
|
|
7038
|
+
/**
|
|
7039
|
+
* Emit `bf_query <base> (<guard>) "key" <value> …` from an extracted builder
|
|
7040
|
+
* shape, lowering each part (with the call args substituted for params) via
|
|
7041
|
+
* the normal expression / condition lowering. Unguarded sets use `true`.
|
|
7042
|
+
*/
|
|
7043
|
+
private emitUrlBuilder(
|
|
7044
|
+
shape: { base: ts.Expression; sets: { guard: ts.Expression | null; key: string; value: ts.Expression }[] },
|
|
7045
|
+
subs: ReadonlyMap<string, string>,
|
|
7046
|
+
): string | null {
|
|
7047
|
+
const lowerExpr = (n: ts.Expression): string =>
|
|
7048
|
+
this.convertExpressionToGo(this.substituteHelperParams(n, subs))
|
|
7049
|
+
const baseGo = wrapIfMultiToken(lowerExpr(shape.base))
|
|
7050
|
+
const parts: string[] = [baseGo]
|
|
7051
|
+
for (const set of shape.sets) {
|
|
7052
|
+
const guardGo = set.guard ? this.lowerUrlGuard(set.guard, subs) : 'true'
|
|
7053
|
+
parts.push(`(${guardGo})`)
|
|
7054
|
+
parts.push(JSON.stringify(set.key))
|
|
7055
|
+
parts.push(wrapIfMultiToken(lowerExpr(set.value)))
|
|
7056
|
+
}
|
|
7057
|
+
return `bf_query ${parts.join(' ')}`
|
|
7058
|
+
}
|
|
7059
|
+
|
|
7060
|
+
/**
|
|
7061
|
+
* Lower a `u.set()` guard to a Go *bool* for `bf_query`'s `include` argument.
|
|
7062
|
+
* A comparison / logical / negation / bool-literal already yields a bool
|
|
7063
|
+
* (`convertConditionToGo`); a bare value (`if (tag)`) is JS string-truthiness,
|
|
7064
|
+
* lowered to `ne <value> ""`. The arg must be a real bool — `bf_query` type-
|
|
7065
|
+
* asserts it, so Go-template truthiness (`{{if x}}`) is not enough.
|
|
7066
|
+
*/
|
|
7067
|
+
private lowerUrlGuard(guard: ts.Expression, subs: ReadonlyMap<string, string>): string {
|
|
7068
|
+
let g = guard
|
|
7069
|
+
while (ts.isParenthesizedExpression(g)) g = g.expression
|
|
7070
|
+
// Comparisons, `!x`, and bool literals lower to a Go bool via the condition
|
|
7071
|
+
// lowering. `&&` / `||` do NOT qualify: Go's `and`/`or` return one of their
|
|
7072
|
+
// operands (a string for a truthiness guard like `tag && other`), not a
|
|
7073
|
+
// bool — so they take the truthiness-wrap path below, yielding
|
|
7074
|
+
// `ne (and …) ""`, an actual bool (#1945 review).
|
|
7075
|
+
const isBoolShape =
|
|
7076
|
+
(ts.isBinaryExpression(g) &&
|
|
7077
|
+
[
|
|
7078
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
7079
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
7080
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
7081
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
7082
|
+
ts.SyntaxKind.LessThanToken,
|
|
7083
|
+
ts.SyntaxKind.GreaterThanToken,
|
|
7084
|
+
ts.SyntaxKind.LessThanEqualsToken,
|
|
7085
|
+
ts.SyntaxKind.GreaterThanEqualsToken,
|
|
7086
|
+
].includes(g.operatorToken.kind)) ||
|
|
7087
|
+
(ts.isPrefixUnaryExpression(g) && g.operator === ts.SyntaxKind.ExclamationToken) ||
|
|
7088
|
+
g.kind === ts.SyntaxKind.TrueKeyword ||
|
|
7089
|
+
g.kind === ts.SyntaxKind.FalseKeyword
|
|
7090
|
+
if (isBoolShape) {
|
|
7091
|
+
return this.convertConditionToGo(this.substituteHelperParams(guard, subs)).condition
|
|
7092
|
+
}
|
|
7093
|
+
const valueGo = wrapIfMultiToken(
|
|
7094
|
+
this.convertExpressionToGo(this.substituteHelperParams(guard, subs)),
|
|
7095
|
+
)
|
|
7096
|
+
return `ne ${valueGo} ""`
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
/**
|
|
7100
|
+
* Resolve `IDENT['key']` / `IDENT["key"]` where `IDENT` is a
|
|
7101
|
+
* module-scope object-literal const and the key is a string literal —
|
|
7102
|
+
* a compile-time-static lookup (the icon registry's
|
|
7103
|
+
* `strokePaths['chevron-down']`, #1896). Returns the looked-up value
|
|
7104
|
+
* as a Go literal (quoted string / bare number) usable inside a
|
|
7105
|
+
* template action, or `null` for any other shape so the caller falls
|
|
7106
|
+
* through to the generic lowering. The prop-keyed variant of the same
|
|
7107
|
+
* pattern lives in `parseRecordIndexAccess` (shared with Mojo); this
|
|
7108
|
+
* helper covers the literal-key case that parse rejects.
|
|
7109
|
+
*/
|
|
7110
|
+
private resolveStaticRecordLiteralIndex(jsExpr: string): string | null {
|
|
7111
|
+
const m =
|
|
7112
|
+
/^([A-Za-z_$][\w$]*)\[\s*(?:'([^']*)'|"([^"]*)")\s*\]$/.exec(jsExpr) ??
|
|
7113
|
+
// Property-access form of the same static lookup
|
|
7114
|
+
// (`variantClasses.ghost`, #1896 pagination) — only when the base
|
|
7115
|
+
// resolves to a module object-literal const below, so ordinary
|
|
7116
|
+
// props/locals never match.
|
|
7117
|
+
/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(jsExpr)
|
|
7118
|
+
if (!m) return null
|
|
7119
|
+
const key = m[2] ?? m[3]
|
|
7120
|
+
const constInfo = (this.localConstants ?? []).find(
|
|
7121
|
+
c => c.name === m[1] && c.isModule,
|
|
7122
|
+
)
|
|
7123
|
+
if (constInfo?.value === undefined) return null
|
|
7124
|
+
const sf = ts.createSourceFile(
|
|
7125
|
+
'__rec.ts',
|
|
7126
|
+
`(${constInfo.value})`,
|
|
7127
|
+
ts.ScriptTarget.Latest,
|
|
7128
|
+
/* setParentNodes */ true,
|
|
7129
|
+
)
|
|
7130
|
+
if (sf.statements.length !== 1) return null
|
|
7131
|
+
const stmt = sf.statements[0]
|
|
7132
|
+
if (!ts.isExpressionStatement(stmt)) return null
|
|
7133
|
+
let parsed: ts.Expression = stmt.expression
|
|
7134
|
+
while (ts.isParenthesizedExpression(parsed)) parsed = parsed.expression
|
|
7135
|
+
if (!ts.isObjectLiteralExpression(parsed)) return null
|
|
7136
|
+
for (const prop of parsed.properties) {
|
|
7137
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
7138
|
+
const name = prop.name
|
|
7139
|
+
const propKey =
|
|
7140
|
+
ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)
|
|
7141
|
+
? name.text
|
|
7142
|
+
: null
|
|
7143
|
+
if (propKey !== key) continue
|
|
7144
|
+
let v: ts.Expression = prop.initializer
|
|
7145
|
+
while (ts.isParenthesizedExpression(v)) v = v.expression
|
|
7146
|
+
if (ts.isNumericLiteral(v)) return v.text
|
|
7147
|
+
if (ts.isStringLiteral(v) || ts.isNoSubstitutionTemplateLiteral(v)) {
|
|
7148
|
+
return JSON.stringify(v.text)
|
|
7149
|
+
}
|
|
7150
|
+
return null
|
|
7151
|
+
}
|
|
7152
|
+
return null
|
|
7153
|
+
}
|
|
7154
|
+
|
|
5290
7155
|
/**
|
|
5291
7156
|
* Create a source location for error reporting.
|
|
5292
7157
|
*/
|
|
@@ -5525,6 +7390,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5525
7390
|
return { preamble: obj.preamble, expr: `${obj.expr}.${this.capitalizeFieldName(expr.property)}` }
|
|
5526
7391
|
}
|
|
5527
7392
|
|
|
7393
|
+
case 'index-access': {
|
|
7394
|
+
// Go's `index` builtin: `index $arr $i`. A multi-token operand
|
|
7395
|
+
// (`bf_add $i 1`) must be parenthesised or Go parses it as extra
|
|
7396
|
+
// `index` arguments. #1897.
|
|
7397
|
+
const obj = this.renderConditionExpr(expr.object)
|
|
7398
|
+
const idx = this.renderConditionExpr(expr.index)
|
|
7399
|
+
return {
|
|
7400
|
+
preamble: obj.preamble + idx.preamble,
|
|
7401
|
+
expr: `index ${wrapIfMultiToken(obj.expr)} ${wrapIfMultiToken(idx.expr)}`,
|
|
7402
|
+
}
|
|
7403
|
+
}
|
|
7404
|
+
|
|
5528
7405
|
case 'binary': {
|
|
5529
7406
|
const leftNeedsParens = this.needsParensInGoTemplate(expr.left)
|
|
5530
7407
|
const leftResult = this.renderConditionExpr(expr.left)
|
|
@@ -5539,11 +7416,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5539
7416
|
let result: string
|
|
5540
7417
|
switch (expr.op) {
|
|
5541
7418
|
case '===':
|
|
5542
|
-
case '==':
|
|
5543
|
-
|
|
7419
|
+
case '==': {
|
|
7420
|
+
const [el, er] = stringTolerantEqOperands(left, right)
|
|
7421
|
+
result = `eq ${el} ${er}`; break
|
|
7422
|
+
}
|
|
5544
7423
|
case '!==':
|
|
5545
|
-
case '!=':
|
|
5546
|
-
|
|
7424
|
+
case '!=': {
|
|
7425
|
+
const [el, er] = stringTolerantEqOperands(left, right)
|
|
7426
|
+
result = `ne ${el} ${er}`; break
|
|
7427
|
+
}
|
|
5547
7428
|
case '>':
|
|
5548
7429
|
result = `gt ${left} ${right}`; break
|
|
5549
7430
|
case '<':
|
|
@@ -5831,6 +7712,61 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5831
7712
|
return null
|
|
5832
7713
|
}
|
|
5833
7714
|
|
|
7715
|
+
/**
|
|
7716
|
+
* When `comp`'s JSX children contain template actions (nested
|
|
7717
|
+
* components, dynamic text) — i.e. none of the static bake paths in
|
|
7718
|
+
* `collectStaticChildInstances` apply — render them into a companion
|
|
7719
|
+
* define and return its name; `renderComponent` then routes the child
|
|
7720
|
+
* call through `bf_with_children` + `bf_tmpl` (#1896). Returns null
|
|
7721
|
+
* for childless or statically-bakeable children, which keep the
|
|
7722
|
+
* constructor-baked `Children` value.
|
|
7723
|
+
*/
|
|
7724
|
+
private queueDynamicChildrenDefine(comp: IRComponent): string | null {
|
|
7725
|
+
const effectiveChildren = comp.children.length > 0
|
|
7726
|
+
? comp.children
|
|
7727
|
+
: this.jsxChildrenPropNodes(comp.props)
|
|
7728
|
+
if (effectiveChildren.length === 0) return null
|
|
7729
|
+
if (this.extractTextChildren(effectiveChildren) !== null) return null
|
|
7730
|
+
if (this.extractHtmlChildren(effectiveChildren) !== null) return null
|
|
7731
|
+
if (this.extractScopedHtmlChildren(effectiveChildren) !== null) return null
|
|
7732
|
+
const name = `${this.componentName}__children_${comp.slotId}`
|
|
7733
|
+
if (!this.pendingChildrenDefines.some(d => d.name === name)) {
|
|
7734
|
+
this.pendingChildrenDefines.push({
|
|
7735
|
+
name,
|
|
7736
|
+
content: this.renderChildren(effectiveChildren),
|
|
7737
|
+
})
|
|
7738
|
+
}
|
|
7739
|
+
return name
|
|
7740
|
+
}
|
|
7741
|
+
|
|
7742
|
+
/**
|
|
7743
|
+
* (#1897) Queue a companion define for a loop body component's JSX children.
|
|
7744
|
+
* Like `queueDynamicChildrenDefine` but temporarily exits the `inLoop`
|
|
7745
|
+
* context so nested component calls render with the normal `.NameSlotN`
|
|
7746
|
+
* field-access pattern (the fields live on the wrapper struct that the
|
|
7747
|
+
* companion define receives as its data context). The loop param stack
|
|
7748
|
+
* stays intact so datum-field references (`payment.id` → `.Id`) still
|
|
7749
|
+
* resolve.
|
|
7750
|
+
*/
|
|
7751
|
+
private queueLoopBodyChildrenDefine(comp: IRComponent): string | null {
|
|
7752
|
+
const effectiveChildren = comp.children.length > 0
|
|
7753
|
+
? comp.children
|
|
7754
|
+
: this.jsxChildrenPropNodes(comp.props)
|
|
7755
|
+
if (effectiveChildren.length === 0) return null
|
|
7756
|
+
if (this.extractTextChildren(effectiveChildren) !== null) return null
|
|
7757
|
+
if (this.extractHtmlChildren(effectiveChildren) !== null) return null
|
|
7758
|
+
if (this.extractScopedHtmlChildren(effectiveChildren) !== null) return null
|
|
7759
|
+
const name = `${this.componentName}__loop_children_${comp.slotId}`
|
|
7760
|
+
if (!this.pendingChildrenDefines.some(d => d.name === name)) {
|
|
7761
|
+
const wasInLoop = this.inLoop
|
|
7762
|
+
this.inLoop = false
|
|
7763
|
+
const content = this.renderChildren(effectiveChildren)
|
|
7764
|
+
this.inLoop = wasInLoop
|
|
7765
|
+
this.pendingChildrenDefines.push({ name, content })
|
|
7766
|
+
}
|
|
7767
|
+
return name
|
|
7768
|
+
}
|
|
7769
|
+
|
|
5834
7770
|
renderComponent(comp: IRComponent, ctx?: { isRootOfClientComponent?: boolean }): string {
|
|
5835
7771
|
// Handle Portal component specially - collect content for body end
|
|
5836
7772
|
if (comp.name === 'Portal') {
|
|
@@ -5852,12 +7788,25 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5852
7788
|
// In Go templates, components are rendered using {{template "name" data}}
|
|
5853
7789
|
let templateCall: string
|
|
5854
7790
|
if (this.inLoop) {
|
|
5855
|
-
// Loop
|
|
5856
|
-
|
|
7791
|
+
// (#1897) Loop body component with JSX children: render children through
|
|
7792
|
+
// a companion define so `bf_with_children` injects them at template
|
|
7793
|
+
// execution time. Temporarily exit loop context so nested component
|
|
7794
|
+
// calls (e.g. TableCell inside TableRow) use the normal non-loop
|
|
7795
|
+
// rendering path (`.TableCellSlotN` fields on the wrapper struct),
|
|
7796
|
+
// while the loop param stack stays intact so datum references resolve.
|
|
7797
|
+
const loopBodyDefine = this.queueLoopBodyChildrenDefine(comp)
|
|
7798
|
+
if (loopBodyDefine) {
|
|
7799
|
+
templateCall = `{{template "${comp.name}" (bf_with_children . (bf_tmpl "${loopBodyDefine}" .))}}`
|
|
7800
|
+
} else {
|
|
7801
|
+
templateCall = `{{template "${comp.name}" .}}`
|
|
7802
|
+
}
|
|
5857
7803
|
} else if (comp.slotId) {
|
|
5858
7804
|
// Static children with slotId: use unique field name based on slotId
|
|
5859
7805
|
const suffix = slotIdToFieldSuffix(comp.slotId)
|
|
5860
|
-
|
|
7806
|
+
const childrenDefine = this.queueDynamicChildrenDefine(comp)
|
|
7807
|
+
templateCall = childrenDefine
|
|
7808
|
+
? `{{template "${comp.name}" (bf_with_children .${comp.name}${suffix} (bf_tmpl "${childrenDefine}" .))}}`
|
|
7809
|
+
: `{{template "${comp.name}" .${comp.name}${suffix}}}`
|
|
5861
7810
|
} else {
|
|
5862
7811
|
// Static children without slotId: fallback to .ComponentName
|
|
5863
7812
|
templateCall = `{{template "${comp.name}" .${comp.name}}}`
|
|
@@ -5948,10 +7897,40 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5948
7897
|
}
|
|
5949
7898
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
5950
7899
|
const { condition: goCond, preamble } = this.convertConditionToGo(value.expr)
|
|
5951
|
-
|
|
7900
|
+
// ARIA attributes are string-valued ("true"/"false"), not HTML5
|
|
7901
|
+
// presence booleans — Hono renders the truthy presence form as
|
|
7902
|
+
// `aria-x="true"` (#1896, SelectItem's
|
|
7903
|
+
// `aria-disabled={isDisabled() || undefined}`).
|
|
7904
|
+
const body = name.startsWith('aria-') ? `${name}="true"` : name
|
|
7905
|
+
return `${preamble}{{if ${goCond}}}${body}{{end}}`
|
|
5952
7906
|
}
|
|
5953
7907
|
const parsed = parseExpression(value.expr.trim())
|
|
5954
|
-
if (parsed.kind === 'conditional'
|
|
7908
|
+
if (parsed.kind === 'conditional') {
|
|
7909
|
+
// A ternary whose falsy branch is `undefined` / `null` OMITS the
|
|
7910
|
+
// attribute entirely on Hono (`aria-current={props.isActive ?
|
|
7911
|
+
// 'page' : undefined}`, #1896 pagination) — wrap the whole
|
|
7912
|
+
// attribute in the condition instead of rendering `attr=""`.
|
|
7913
|
+
const undef = (e: ParsedExpr): boolean =>
|
|
7914
|
+
(e.kind === 'identifier' && (e.name === 'undefined' || e.name === 'null')) ||
|
|
7915
|
+
(e.kind === 'literal' && (e.value === null || e.value === undefined))
|
|
7916
|
+
const test = parsed.test
|
|
7917
|
+
if (undef(parsed.alternate) && !undef(parsed.consequent)) {
|
|
7918
|
+
const { condition: goCond, preamble } = this.convertConditionToGo(
|
|
7919
|
+
// Re-render the test from its source span via the parsed tree. If it
|
|
7920
|
+
// lowered to a self-contained action block, re-parse the whole
|
|
7921
|
+
// ternary source; otherwise slice off the test portion.
|
|
7922
|
+
this.isTemplateFragment(this.renderParsedExpr(test), test.kind)
|
|
7923
|
+
? value.expr
|
|
7924
|
+
: value.expr.slice(0, value.expr.indexOf('?')).trim(),
|
|
7925
|
+
)
|
|
7926
|
+
const valueExpr = this.renderParsedExpr(parsed.consequent)
|
|
7927
|
+
const body = `${name}="{{${valueExpr}}}"`
|
|
7928
|
+
return `${preamble}{{if ${goCond}}}${body}{{end}}`
|
|
7929
|
+
}
|
|
7930
|
+
// Inline Go template syntax with embedded `{{...}}` actions.
|
|
7931
|
+
return `${name}="${this.renderParsedExpr(parsed)}"`
|
|
7932
|
+
}
|
|
7933
|
+
if (parsed.kind === 'template-literal') {
|
|
5955
7934
|
// Inline Go template syntax with embedded `{{...}}` actions.
|
|
5956
7935
|
return `${name}="${this.renderParsedExpr(parsed)}"`
|
|
5957
7936
|
}
|
|
@@ -5977,7 +7956,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5977
7956
|
const field = `.${this.capitalizeFieldName(propName)}`
|
|
5978
7957
|
return `{{if ne ${field} nil}}${name}="{{${this.convertExpressionToGo(value.expr)}}}"{{end}}`
|
|
5979
7958
|
}
|
|
5980
|
-
|
|
7959
|
+
// Lower once; if the result is already a self-contained action block (e.g.
|
|
7960
|
+
// an inlined `sortClass(k)` → `{{if …}}…{{end}}`, #1897), embed it as-is
|
|
7961
|
+
// rather than double-wrapping it in another `{{…}}`.
|
|
7962
|
+
const exprOut: { parsed?: ParsedExpr } = {}
|
|
7963
|
+
const go = this.convertExpressionToGo(value.expr, exprOut)
|
|
7964
|
+
return this.isTemplateFragment(go, exprOut.parsed?.kind)
|
|
7965
|
+
? `${name}="${go}"`
|
|
7966
|
+
: `${name}="{{${go}}}"`
|
|
5981
7967
|
},
|
|
5982
7968
|
emitBooleanAttr: (_value, name) => name,
|
|
5983
7969
|
// Spread attributes (`<div {...attrs()} />`) lower through the
|
|
@@ -6127,7 +8113,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6127
8113
|
}
|
|
6128
8114
|
const inner = s.slice(open + 2, close).trim()
|
|
6129
8115
|
if (inner) {
|
|
6130
|
-
out
|
|
8116
|
+
// Thread the parsed kind out of `convertExpressionToGo` (reusing its
|
|
8117
|
+
// single parse) so a nested template literal here is handled too.
|
|
8118
|
+
const cls: { parsed?: ParsedExpr } = {}
|
|
8119
|
+
const goExpr = this.convertExpressionToGo(inner, cls)
|
|
8120
|
+
const parsed = cls.parsed
|
|
8121
|
+
if (parsed?.kind === 'template-literal') {
|
|
8122
|
+
// Attribute context: a template literal lowers to literal text
|
|
8123
|
+
// interleaved with `{{...}}` actions. That literal text sits OUTSIDE
|
|
8124
|
+
// any action, so — unlike the `{{...}}` actions, which Go escapes for
|
|
8125
|
+
// the attribute context at render time — it bypasses escaping. A `"`,
|
|
8126
|
+
// `<`, or `&` in a UnoCSS arbitrary value (`content-["x"]`) would then
|
|
8127
|
+
// break the surrounding `class="..."`. Escape each string part with
|
|
8128
|
+
// `escapeAttrText`, keeping interpolations as fragment-aware actions
|
|
8129
|
+
// (#1937 review). Mirrors the `templateLiteral` emitter, plus escaping.
|
|
8130
|
+
for (const part of parsed.parts) {
|
|
8131
|
+
if (part.type === 'string') {
|
|
8132
|
+
out += this.escapeAttrText(part.value)
|
|
8133
|
+
} else {
|
|
8134
|
+
const e = this.renderParsedExpr(part.expr)
|
|
8135
|
+
out += this.isTemplateFragment(e, part.expr.kind) ? e : `{{${e}}}`
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8138
|
+
} else {
|
|
8139
|
+
// Same `isTemplateFragment` guard as `renderExpression` (#1896): a
|
|
8140
|
+
// ternary lowers to a complete `{{if}}` action chain — don't re-wrap.
|
|
8141
|
+
out += this.isTemplateFragment(goExpr, parsed?.kind) ? goExpr : `{{${goExpr}}}`
|
|
8142
|
+
}
|
|
6131
8143
|
} else {
|
|
6132
8144
|
out += s.slice(open, close + 1)
|
|
6133
8145
|
}
|
|
@@ -6174,7 +8186,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6174
8186
|
// case matches; consumers shouldn't rely on a default fallback
|
|
6175
8187
|
// here (the JSX-side `variant = 'default'` default already
|
|
6176
8188
|
// shows up via the per-prop fallback in `NewXxxProps`).
|
|
6177
|
-
const
|
|
8189
|
+
const rawKeyExpr = this.convertExpressionToGo(part.key)
|
|
8190
|
+
// A compound key (`props.placement ?? 'top'` → `or .Placement
|
|
8191
|
+
// "top"`) must be parenthesized inside `eq` — unwrapped, Go's
|
|
8192
|
+
// template parser reads `eq or .Placement "top" "left"` as four
|
|
8193
|
+
// arguments with a zero-arg `or` (#1896, tooltip placement).
|
|
8194
|
+
const keyExpr = /\s/.test(rawKeyExpr) ? `(${rawKeyExpr})` : rawKeyExpr
|
|
6178
8195
|
const caseEntries = Object.entries(part.cases)
|
|
6179
8196
|
if (caseEntries.length === 0) continue
|
|
6180
8197
|
const branches = caseEntries.map(([k, v], i) => {
|