@barefootjs/go-template 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2672 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __toESM = (mod, isNodeMode, target) => {
7
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
8
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
9
+ for (let key of __getOwnPropNames(mod))
10
+ if (!__hasOwnProp.call(to, key))
11
+ __defProp(to, key, {
12
+ get: () => mod[key],
13
+ enumerable: true
14
+ });
15
+ return to;
16
+ };
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, {
20
+ get: all[name],
21
+ enumerable: true,
22
+ configurable: true,
23
+ set: (newValue) => all[name] = () => newValue
24
+ });
25
+ };
26
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
27
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
28
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
29
+ }) : x)(function(x) {
30
+ if (typeof require !== "undefined")
31
+ return require.apply(this, arguments);
32
+ throw Error('Dynamic require of "' + x + '" is not supported');
33
+ });
34
+
35
+ // src/adapter/go-template-adapter.ts
36
+ import ts from "typescript";
37
+ import {
38
+ BaseAdapter,
39
+ isBooleanAttr,
40
+ parseExpression,
41
+ isSupported,
42
+ exprToString,
43
+ identifierPath,
44
+ emitParsedExpr,
45
+ emitIRNode,
46
+ emitAttrValue
47
+ } from "@barefootjs/jsx";
48
+ import { findInterpolationEnd } from "@barefootjs/jsx/scanner";
49
+ function wrapIfMultiToken(rendered) {
50
+ if (rendered.startsWith("(") && rendered.endsWith(")"))
51
+ return rendered;
52
+ if (rendered.startsWith('"') && rendered.endsWith('"'))
53
+ return rendered;
54
+ if (/\s/.test(rendered))
55
+ return `(${rendered})`;
56
+ return rendered;
57
+ }
58
+ function emitBfSort(recv, c) {
59
+ const keyKind = c.key.kind;
60
+ const keyName = c.key.kind === "field" ? capitalize(c.key.field) : "";
61
+ return `bf_sort ${wrapIfMultiToken(recv)} "${keyKind}" "${keyName}" "${c.type}" "${c.direction}"`;
62
+ }
63
+ function capitalize(s) {
64
+ return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
65
+ }
66
+ function slotIdToFieldSuffix(slotId) {
67
+ const cleanId = slotId.startsWith("^") ? slotId.slice(1) : slotId;
68
+ const match = cleanId.match(/^s(\d+)$/);
69
+ if (match) {
70
+ return `Slot${match[1]}`;
71
+ }
72
+ return cleanId.replace("slot_", "Slot");
73
+ }
74
+ var GO_TEMPLATE_PRIMITIVES = {
75
+ "JSON.stringify": { arity: 1, emit: (args) => `bf_json ${args[0]}` },
76
+ String: { arity: 1, emit: (args) => `bf_string ${args[0]}` },
77
+ Number: { arity: 1, emit: (args) => `bf_number ${args[0]}` },
78
+ "Math.floor": { arity: 1, emit: (args) => `bf_floor ${args[0]}` },
79
+ "Math.ceil": { arity: 1, emit: (args) => `bf_ceil ${args[0]}` },
80
+ "Math.round": { arity: 1, emit: (args) => `bf_round ${args[0]}` }
81
+ };
82
+
83
+ class GoTemplateAdapter extends BaseAdapter {
84
+ name = "go-template";
85
+ extension = ".tmpl";
86
+ filterExprDepth = 0;
87
+ filterExprUnsupported = false;
88
+ templatePrimitives = Object.fromEntries(Object.entries(GO_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
89
+ templatePrimitiveArities = Object.fromEntries(Object.entries(GO_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.arity]));
90
+ componentName = "";
91
+ options;
92
+ inLoop = false;
93
+ loopParamStack = [];
94
+ errors = [];
95
+ propsObjectName = null;
96
+ restPropsName = null;
97
+ localTypeNames = new Set;
98
+ localTypeAliases = new Map;
99
+ usesHtmlTemplate = false;
100
+ constructor(options = {}) {
101
+ super();
102
+ this.options = {
103
+ packageName: options.packageName ?? "components",
104
+ clientJsBasePath: options.clientJsBasePath ?? "/static/client/",
105
+ barefootJsPath: options.barefootJsPath ?? "/static/client/barefoot.js"
106
+ };
107
+ }
108
+ generate(ir, options) {
109
+ this.componentName = ir.metadata.componentName;
110
+ this.errors = [];
111
+ this.propsObjectName = ir.metadata.propsObjectName;
112
+ this.restPropsName = ir.metadata.restPropsName ?? null;
113
+ if (!options?.siblingTemplatesRegistered) {
114
+ this.checkImportedLoopChildComponents(ir);
115
+ }
116
+ const hasInteractivity = this.hasClientInteractivity(ir);
117
+ const isRootComponent = ir.root.type === "component";
118
+ const isIfStatement = ir.root.type === "if-statement";
119
+ const templateBody = isIfStatement ? this.renderIfStatement(ir.root, { isRootOfClientComponent: hasInteractivity }) : this.renderNode(ir.root, { isRootOfClientComponent: hasInteractivity && isRootComponent });
120
+ const scriptRegistrations = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
121
+ const template = `{{define "${this.componentName}"}}
122
+ ${scriptRegistrations}${templateBody}
123
+ {{end}}
124
+ `;
125
+ const types = this.generateTypes(ir);
126
+ if (this.errors.length > 0) {
127
+ ir.errors.push(...this.errors);
128
+ }
129
+ const sections = {
130
+ imports: "",
131
+ types: "",
132
+ component: template,
133
+ defaultExport: ""
134
+ };
135
+ return {
136
+ template,
137
+ sections,
138
+ types: types || undefined,
139
+ extension: this.extension
140
+ };
141
+ }
142
+ hasClientInteractivity(ir) {
143
+ if (ir.metadata.signals.length > 0)
144
+ return true;
145
+ if (ir.metadata.effects.length > 0)
146
+ return true;
147
+ if (ir.metadata.onMounts.length > 0)
148
+ return true;
149
+ if (this.hasEventsInTree(ir.root))
150
+ return true;
151
+ if (this.findChildComponentNames(ir.root).size > 0)
152
+ return true;
153
+ return false;
154
+ }
155
+ hasEventsInTree(node) {
156
+ if (node.type === "element") {
157
+ const element = node;
158
+ if (element.events.length > 0)
159
+ return true;
160
+ for (const child of element.children) {
161
+ if (this.hasEventsInTree(child))
162
+ return true;
163
+ }
164
+ } else if (node.type === "fragment") {
165
+ const fragment = node;
166
+ for (const child of fragment.children) {
167
+ if (this.hasEventsInTree(child))
168
+ return true;
169
+ }
170
+ } else if (node.type === "conditional") {
171
+ const cond = node;
172
+ if (this.hasEventsInTree(cond.whenTrue))
173
+ return true;
174
+ if (cond.whenFalse && this.hasEventsInTree(cond.whenFalse))
175
+ return true;
176
+ } else if (node.type === "loop") {
177
+ const loop = node;
178
+ for (const child of loop.children) {
179
+ if (this.hasEventsInTree(child))
180
+ return true;
181
+ }
182
+ } else if (node.type === "if-statement") {
183
+ const ifStmt = node;
184
+ if (this.hasEventsInTree(ifStmt.consequent))
185
+ return true;
186
+ if (ifStmt.alternate && this.hasEventsInTree(ifStmt.alternate))
187
+ return true;
188
+ }
189
+ return false;
190
+ }
191
+ findChildComponentNames(node) {
192
+ const names = new Set;
193
+ this.collectChildComponentNames(node, names);
194
+ return names;
195
+ }
196
+ collectChildComponentNames(node, names) {
197
+ if (node.type === "component") {
198
+ const comp = node;
199
+ names.add(comp.name);
200
+ } else if (node.type === "element") {
201
+ const element = node;
202
+ for (const child of element.children) {
203
+ this.collectChildComponentNames(child, names);
204
+ }
205
+ } else if (node.type === "fragment") {
206
+ const fragment = node;
207
+ for (const child of fragment.children) {
208
+ this.collectChildComponentNames(child, names);
209
+ }
210
+ } else if (node.type === "conditional") {
211
+ const cond = node;
212
+ this.collectChildComponentNames(cond.whenTrue, names);
213
+ if (cond.whenFalse) {
214
+ this.collectChildComponentNames(cond.whenFalse, names);
215
+ }
216
+ } else if (node.type === "loop") {
217
+ const loop = node;
218
+ for (const child of loop.children) {
219
+ this.collectChildComponentNames(child, names);
220
+ }
221
+ } else if (node.type === "if-statement") {
222
+ const ifStmt = node;
223
+ this.collectChildComponentNames(ifStmt.consequent, names);
224
+ if (ifStmt.alternate) {
225
+ this.collectChildComponentNames(ifStmt.alternate, names);
226
+ }
227
+ }
228
+ }
229
+ checkImportedLoopChildComponents(ir) {
230
+ const relativeImports = new Set;
231
+ for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
232
+ if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
233
+ continue;
234
+ if (imp.isTypeOnly)
235
+ continue;
236
+ for (const spec of imp.specifiers) {
237
+ relativeImports.add(spec.alias ?? spec.name);
238
+ }
239
+ }
240
+ if (relativeImports.size === 0)
241
+ return;
242
+ const visit = (node, inLoop) => {
243
+ switch (node.type) {
244
+ case "component": {
245
+ const comp = node;
246
+ if (inLoop && relativeImports.has(comp.name)) {
247
+ this.errors.push({
248
+ code: "BF103",
249
+ severity: "error",
250
+ message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Go template adapter emits a cross-template call ({{template "${comp.name}" .}}); the child template must be registered on the same *template.Template instance at render time.`,
251
+ loc: comp.loc ?? this.makeLoc(),
252
+ suggestion: {
253
+ message: `Options:
254
+ ` + ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting {{define "${comp.name}"}} on the same *template.Template instance at render time.
255
+ ` + ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.
256
+ ` + ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`
257
+ }
258
+ });
259
+ }
260
+ for (const child of comp.children)
261
+ visit(child, inLoop);
262
+ break;
263
+ }
264
+ case "element": {
265
+ const el = node;
266
+ for (const child of el.children)
267
+ visit(child, inLoop);
268
+ break;
269
+ }
270
+ case "fragment": {
271
+ const frag = node;
272
+ for (const child of frag.children)
273
+ visit(child, inLoop);
274
+ break;
275
+ }
276
+ case "conditional": {
277
+ const cond = node;
278
+ visit(cond.whenTrue, inLoop);
279
+ if (cond.whenFalse)
280
+ visit(cond.whenFalse, inLoop);
281
+ break;
282
+ }
283
+ case "loop": {
284
+ const loop = node;
285
+ for (const child of loop.children)
286
+ visit(child, true);
287
+ break;
288
+ }
289
+ case "if-statement": {
290
+ const stmt = node;
291
+ visit(stmt.consequent, inLoop);
292
+ if (stmt.alternate)
293
+ visit(stmt.alternate, inLoop);
294
+ break;
295
+ }
296
+ case "provider": {
297
+ const p = node;
298
+ for (const child of p.children)
299
+ visit(child, inLoop);
300
+ break;
301
+ }
302
+ case "async": {
303
+ const a = node;
304
+ visit(a.fallback, inLoop);
305
+ for (const child of a.children)
306
+ visit(child, inLoop);
307
+ break;
308
+ }
309
+ }
310
+ };
311
+ visit(ir.root, false);
312
+ }
313
+ generateScriptRegistrations(ir, scriptBaseName) {
314
+ const hasInteractivity = this.hasClientInteractivity(ir);
315
+ if (!hasInteractivity) {
316
+ return "";
317
+ }
318
+ const registrations = [];
319
+ registrations.push(`{{.Scripts.Register "${this.options.barefootJsPath}"}}`);
320
+ const scriptName = scriptBaseName || ir.metadata.componentName;
321
+ registrations.push(`{{.Scripts.Register "${this.options.clientJsBasePath}${scriptName}.client.js"}}`);
322
+ return `{{if .Scripts}}${registrations.join("")}{{end}}
323
+ `;
324
+ }
325
+ generateTypes(ir) {
326
+ this.usesHtmlTemplate = false;
327
+ const lines = [];
328
+ const componentName = ir.metadata.componentName;
329
+ this.localTypeNames = new Set;
330
+ this.localTypeAliases = new Map;
331
+ for (const td of ir.metadata.typeDefinitions) {
332
+ if (td.name === "Props" || td.name === `${componentName}Props`)
333
+ continue;
334
+ if (td.name.endsWith("Props"))
335
+ continue;
336
+ this.localTypeNames.add(td.name);
337
+ if (td.definition.match(/^type \w+ = ('[^']*'(\s*\|\s*'[^']*')*)/)) {
338
+ this.localTypeAliases.set(td.name, "string");
339
+ }
340
+ }
341
+ for (const td of ir.metadata.typeDefinitions) {
342
+ if (td.name === "Props" || td.name === `${componentName}Props`)
343
+ continue;
344
+ if (td.name.endsWith("Props"))
345
+ continue;
346
+ const goStruct = this.typeDefinitionToGo(td);
347
+ if (goStruct) {
348
+ lines.push(goStruct);
349
+ lines.push("");
350
+ }
351
+ }
352
+ const nestedComponents = this.findNestedComponents(ir.root);
353
+ const propTypeOverrides = this.buildPropTypeOverrides(ir);
354
+ const spreadSlots = this.collectSpreadSlots(ir.root);
355
+ this.generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots);
356
+ this.generatePropsStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots);
357
+ this.generateNewPropsFunction(lines, ir, componentName, nestedComponents, spreadSlots);
358
+ const header = [];
359
+ header.push(`package ${this.options.packageName}`);
360
+ header.push("");
361
+ header.push("import (");
362
+ if (this.usesHtmlTemplate)
363
+ header.push('\t"html/template"');
364
+ header.push('\t"math/rand"');
365
+ header.push("");
366
+ header.push('\tbf "github.com/barefootjs/runtime/bf"');
367
+ header.push(")");
368
+ header.push("");
369
+ return [...header, ...lines].join(`
370
+ `);
371
+ }
372
+ typeDefinitionToGo(td) {
373
+ const def = td.definition;
374
+ if (def.match(/^type \w+ = ('[^']*'(\s*\|\s*'[^']*')*)/)) {
375
+ return `// ${td.name} is a string type.
376
+ type ${td.name} = string`;
377
+ }
378
+ const bodyMatch = def.match(/(?:type \w+ = |interface \w+ )\{([\s\S]*)\}/);
379
+ if (!bodyMatch)
380
+ return null;
381
+ const body = bodyMatch[1];
382
+ const goFields = [];
383
+ const fieldEntries = body.split(/[;\n]/).map((s) => s.trim()).filter(Boolean);
384
+ for (const entry of fieldEntries) {
385
+ const fieldMatch = entry.match(/^(\w+)\??\s*:\s*(.+)$/);
386
+ if (!fieldMatch)
387
+ continue;
388
+ const [, fieldName, tsType] = fieldMatch;
389
+ const goFieldName = this.capitalizeFieldName(fieldName);
390
+ const goType = this.tsTypeStringToGo(tsType.trim());
391
+ const jsonTag = this.toJsonTag(fieldName);
392
+ goFields.push(` ${goFieldName} ${goType} \`json:"${jsonTag}"\``);
393
+ }
394
+ if (goFields.length === 0)
395
+ return null;
396
+ return `// ${td.name} represents a ${td.name.toLowerCase()}.
397
+ type ${td.name} struct {
398
+ ${goFields.join(`
399
+ `)}
400
+ }`;
401
+ }
402
+ tsTypeStringToGo(tsType) {
403
+ const t = tsType.trim();
404
+ if (t === "number")
405
+ return "int";
406
+ if (t === "string")
407
+ return "string";
408
+ if (t === "boolean" || t === "bool")
409
+ return "bool";
410
+ if (t.endsWith("[]")) {
411
+ const elem = t.slice(0, -2);
412
+ return `[]${this.tsTypeStringToGo(elem)}`;
413
+ }
414
+ const arrayMatch = t.match(/^Array<(.+)>$/);
415
+ if (arrayMatch)
416
+ return `[]${this.tsTypeStringToGo(arrayMatch[1])}`;
417
+ if (this.localTypeNames.has(t))
418
+ return t;
419
+ return "interface{}";
420
+ }
421
+ buildPropTypeOverrides(ir) {
422
+ const overrides = new Map;
423
+ for (const signal of ir.metadata.signals) {
424
+ const propNames = [signal.initialValue];
425
+ const extracted = this.extractPropNameFromInitialValue(signal.initialValue);
426
+ if (extracted)
427
+ propNames.push(extracted);
428
+ for (const propName of propNames) {
429
+ const param = ir.metadata.propsParams.find((p) => p.name === propName);
430
+ if (!param)
431
+ continue;
432
+ const propGoType = this.typeInfoToGo(param.type, param.defaultValue);
433
+ if (propGoType.includes("interface{}")) {
434
+ const signalGoType = this.typeInfoToGo(signal.type, signal.initialValue);
435
+ if (!signalGoType.includes("interface{}")) {
436
+ overrides.set(propName, signalGoType);
437
+ }
438
+ }
439
+ }
440
+ }
441
+ return overrides;
442
+ }
443
+ generateInputStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
444
+ const inputTypeName = `${componentName}Input`;
445
+ lines.push(`// ${inputTypeName} is the user-facing input type.`);
446
+ lines.push(`type ${inputTypeName} struct {`);
447
+ lines.push("\tScopeID string // Optional: if empty, random ID is generated");
448
+ lines.push("\tBfParent string // Optional: parent scope id");
449
+ lines.push("\tBfMount string // Optional: slot id in parent");
450
+ const staticNested = nestedComponents.filter((n) => !n.isDynamic);
451
+ const nestedArrayFields = new Set(nestedComponents.map((n) => `${n.name}s`));
452
+ for (const param of ir.metadata.propsParams) {
453
+ const fieldName = this.capitalizeFieldName(param.name);
454
+ if (nestedArrayFields.has(fieldName))
455
+ continue;
456
+ const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
457
+ lines.push(` ${fieldName} ${goType}`);
458
+ }
459
+ for (const nested of staticNested) {
460
+ lines.push(` ${nested.name}s []${nested.name}Input`);
461
+ }
462
+ const restPropsName = ir.metadata.restPropsName;
463
+ if (restPropsName) {
464
+ const seen = new Set;
465
+ for (const slot of spreadSlots) {
466
+ if (slot.bagSource !== "input-bag")
467
+ continue;
468
+ const fieldName = this.capitalizeFieldName(restPropsName);
469
+ if (seen.has(fieldName))
470
+ continue;
471
+ seen.add(fieldName);
472
+ const jsonTag = this.toJsonTag(restPropsName);
473
+ lines.push(` ${fieldName} map[string]any \`json:"${jsonTag}"\``);
474
+ }
475
+ }
476
+ lines.push("}");
477
+ lines.push("");
478
+ }
479
+ generatePropsStruct(lines, ir, componentName, nestedComponents, propTypeOverrides, spreadSlots) {
480
+ const propsTypeName = `${componentName}Props`;
481
+ lines.push(`// ${propsTypeName} is the props type for the ${componentName} component.`);
482
+ lines.push(`type ${propsTypeName} struct {`);
483
+ lines.push('\tScopeID string `json:"scopeID"`');
484
+ lines.push('\tBfIsRoot bool `json:"-"`');
485
+ lines.push('\tBfIsChild bool `json:"-"`');
486
+ lines.push('\tBfParent string `json:"-"`');
487
+ lines.push('\tBfMount string `json:"-"`');
488
+ lines.push('\tScripts *bf.ScriptCollector `json:"-"`');
489
+ const nestedArrayFields = new Set(nestedComponents.map((n) => `${n.name}s`));
490
+ const propFieldNames = new Set;
491
+ for (const param of ir.metadata.propsParams) {
492
+ const fieldName = this.capitalizeFieldName(param.name);
493
+ if (nestedArrayFields.has(fieldName))
494
+ continue;
495
+ const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
496
+ const jsonTag = this.toJsonTag(param.name);
497
+ lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
498
+ propFieldNames.add(fieldName);
499
+ }
500
+ const propsParamMap = new Map(ir.metadata.propsParams.map((p) => [p.name, p]));
501
+ for (const signal of ir.metadata.signals) {
502
+ const fieldName = this.capitalizeFieldName(signal.getter);
503
+ if (propFieldNames.has(fieldName))
504
+ continue;
505
+ const jsonTag = this.toJsonTag(signal.getter);
506
+ let goType;
507
+ let referencedProp = propsParamMap.get(signal.initialValue);
508
+ if (!referencedProp) {
509
+ const propName = this.extractPropNameFromInitialValue(signal.initialValue);
510
+ if (propName)
511
+ referencedProp = propsParamMap.get(propName);
512
+ }
513
+ if (referencedProp) {
514
+ const propGoType = this.typeInfoToGo(referencedProp.type, referencedProp.defaultValue);
515
+ const signalGoType = this.typeInfoToGo(signal.type, signal.initialValue);
516
+ if (propGoType.includes("interface{}")) {
517
+ goType = signalGoType;
518
+ } else if (!signalGoType.includes("interface{}") && signalGoType !== propGoType) {
519
+ goType = signalGoType;
520
+ } else {
521
+ goType = propGoType;
522
+ }
523
+ } else {
524
+ goType = this.typeInfoToGo(signal.type, signal.initialValue);
525
+ }
526
+ lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
527
+ }
528
+ for (const memo of ir.metadata.memos) {
529
+ const fieldName = this.capitalizeFieldName(memo.name);
530
+ const jsonTag = this.toJsonTag(memo.name);
531
+ const goType = this.inferMemoType(memo, ir.metadata.signals, propsParamMap);
532
+ lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
533
+ }
534
+ for (const nested of nestedComponents) {
535
+ if (nested.isDynamic) {
536
+ lines.push(` ${nested.name}s []${nested.name}Props \`json:"-"\``);
537
+ } else {
538
+ const jsonTag = this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`);
539
+ lines.push(` ${nested.name}s []${nested.name}Props \`json:"${jsonTag}"\``);
540
+ }
541
+ }
542
+ const staticChildren = this.collectStaticChildInstances(ir.root);
543
+ for (const child of staticChildren) {
544
+ lines.push(` ${child.fieldName} ${child.name}Props \`json:"-"\``);
545
+ }
546
+ for (const slot of spreadSlots) {
547
+ const jsonTag = this.toJsonTag(slot.slotId);
548
+ lines.push(` ${slot.slotId} map[string]any \`json:"${jsonTag}"\``);
549
+ }
550
+ lines.push("}");
551
+ lines.push("");
552
+ }
553
+ generateNewPropsFunction(lines, ir, componentName, nestedComponents, spreadSlots) {
554
+ const inputTypeName = `${componentName}Input`;
555
+ const propsTypeName = `${componentName}Props`;
556
+ const dynamicNested = nestedComponents.filter((n) => n.isDynamic);
557
+ lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`);
558
+ for (const nested of dynamicNested) {
559
+ const arrayField = `${nested.name}s`;
560
+ lines.push(`//`);
561
+ lines.push(`// NOTE: \`${arrayField}\` is populated by the route handler, not by`);
562
+ lines.push(`// New${componentName}Props — the SSR template iterates over it`);
563
+ lines.push(`// dynamically (\`.${arrayField}\`). Build the slice from your source data and`);
564
+ lines.push(`// assign it before passing the props to your renderer. Example:`);
565
+ lines.push(`//`);
566
+ lines.push(`// props := New${componentName}Props(${inputTypeName}{ /* ... */ })`);
567
+ lines.push(`// props.${arrayField} = make([]${nested.name}Props, len(items))`);
568
+ lines.push(`// for i, item := range items {`);
569
+ lines.push(`// props.${arrayField}[i] = New${nested.name}Props(${nested.name}Input{ /* fields */ })`);
570
+ lines.push(`// props.${arrayField}[i].BfParent = props.ScopeID`);
571
+ lines.push(`// props.${arrayField}[i].BfMount = "${nested.slotId}"`);
572
+ lines.push(`// }`);
573
+ }
574
+ lines.push(`func New${componentName}Props(in ${inputTypeName}) ${propsTypeName} {`);
575
+ lines.push("\tscopeID := in.ScopeID");
576
+ lines.push('\tif scopeID == "" {');
577
+ lines.push(` scopeID = "${componentName}_" + randomID(6)`);
578
+ lines.push("\t}");
579
+ lines.push("");
580
+ const staticNested = nestedComponents.filter((n) => !n.isDynamic);
581
+ if (staticNested.length > 0) {
582
+ for (const nested of staticNested) {
583
+ const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`;
584
+ lines.push(` ${varName} := make([]${nested.name}Props, len(in.${nested.name}s))`);
585
+ lines.push(` for i, item := range in.${nested.name}s {`);
586
+ lines.push(` ${varName}[i] = New${nested.name}Props(item)`);
587
+ lines.push(` ${varName}[i].BfParent = scopeID`);
588
+ lines.push(` ${varName}[i].BfMount = "${nested.slotId}"`);
589
+ lines.push("\t}");
590
+ lines.push("");
591
+ }
592
+ }
593
+ const propFallbackVars = this.collectPropFallbackVars(ir);
594
+ for (const [, info] of propFallbackVars) {
595
+ lines.push(` ${info.varName} := in.${info.fieldName}`);
596
+ lines.push(` if ${info.varName} == ${info.zeroLiteral} {`);
597
+ lines.push(` ${info.varName} = ${info.goFallback}`);
598
+ lines.push(` }`);
599
+ }
600
+ if (propFallbackVars.size > 0)
601
+ lines.push("");
602
+ lines.push(` return ${propsTypeName}{`);
603
+ lines.push("\t\tScopeID: scopeID,");
604
+ lines.push("\t\tBfParent: in.BfParent,");
605
+ lines.push("\t\tBfMount: in.BfMount,");
606
+ const nestedArrayFields = new Set(nestedComponents.map((n) => `${n.name}s`));
607
+ const propFieldNames = new Set;
608
+ for (const param of ir.metadata.propsParams) {
609
+ const fieldName = this.capitalizeFieldName(param.name);
610
+ if (nestedArrayFields.has(fieldName))
611
+ continue;
612
+ const hoisted = propFallbackVars.get(param.name);
613
+ if (hoisted) {
614
+ lines.push(` ${fieldName}: ${hoisted.varName},`);
615
+ } else {
616
+ const fallback = this.goPropDefault(param.defaultValue);
617
+ if (fallback !== null) {
618
+ lines.push(` ${fieldName}: ${this.applyGoFallback(`in.${fieldName}`, fallback)},`);
619
+ } else {
620
+ lines.push(` ${fieldName}: in.${fieldName},`);
621
+ }
622
+ }
623
+ propFieldNames.add(fieldName);
624
+ }
625
+ for (const signal of ir.metadata.signals) {
626
+ const fieldName = this.capitalizeFieldName(signal.getter);
627
+ if (propFieldNames.has(fieldName))
628
+ continue;
629
+ const fallbackMatch = this.extractPropFallback(signal.initialValue);
630
+ const hoisted = fallbackMatch ? propFallbackVars.get(fallbackMatch.propName) : undefined;
631
+ if (hoisted) {
632
+ lines.push(` ${fieldName}: ${hoisted.varName},`);
633
+ } else {
634
+ const initialValue = this.convertInitialValue(signal.initialValue, signal.type, ir.metadata.propsParams);
635
+ lines.push(` ${fieldName}: ${initialValue},`);
636
+ }
637
+ }
638
+ for (const nested of staticNested) {
639
+ const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`;
640
+ lines.push(` ${nested.name}s: ${varName},`);
641
+ }
642
+ for (const memo of ir.metadata.memos) {
643
+ const fieldName = this.capitalizeFieldName(memo.name);
644
+ const memoValue = this.computeMemoInitialValue(memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars);
645
+ lines.push(` ${fieldName}: ${memoValue},`);
646
+ }
647
+ const staticChildren = this.collectStaticChildInstances(ir.root);
648
+ for (const child of staticChildren) {
649
+ lines.push(` ${child.fieldName}: New${child.name}Props(${child.name}Input{`);
650
+ lines.push(` ScopeID: scopeID + "_${child.slotId}",`);
651
+ lines.push(` BfParent: scopeID,`);
652
+ lines.push(` BfMount: "${child.slotId}",`);
653
+ for (const prop of child.props) {
654
+ switch (prop.value.kind) {
655
+ case "literal":
656
+ lines.push(` ${this.capitalizeFieldName(prop.name)}: ${this.goLiteral(prop.value.value)},`);
657
+ break;
658
+ case "boolean-shorthand":
659
+ case "boolean-attr":
660
+ lines.push(` ${this.capitalizeFieldName(prop.name)}: true,`);
661
+ break;
662
+ case "expression":
663
+ case "spread":
664
+ case "template": {
665
+ const parts = prop.value.kind === "template" || prop.value.kind === "expression" ? prop.value.parts : undefined;
666
+ if (parts) {
667
+ const goExpr = this.templatePartsToGoCode(parts, ir.metadata.propsParams);
668
+ if (goExpr !== null) {
669
+ lines.push(` ${this.capitalizeFieldName(prop.name)}: ${goExpr},`);
670
+ break;
671
+ }
672
+ }
673
+ const exprText = prop.value.kind === "template" ? "" : prop.value.expr;
674
+ if (!exprText)
675
+ break;
676
+ const resolvedValue = this.resolveDynamicPropValue(exprText, ir.metadata.signals, ir.metadata.memos, ir.metadata.propsParams);
677
+ if (resolvedValue !== null) {
678
+ lines.push(` ${this.capitalizeFieldName(prop.name)}: ${resolvedValue},`);
679
+ }
680
+ break;
681
+ }
682
+ case "jsx-children":
683
+ break;
684
+ }
685
+ }
686
+ if (child.childrenText !== null) {
687
+ lines.push(` Children: ${JSON.stringify(child.childrenText)},`);
688
+ } else if (child.childrenHtml !== null) {
689
+ this.usesHtmlTemplate = true;
690
+ lines.push(` Children: template.HTML(${JSON.stringify(child.childrenHtml)}),`);
691
+ }
692
+ lines.push(` }),`);
693
+ }
694
+ for (const slot of spreadSlots) {
695
+ const goExpr = this.buildSpreadInitializer(slot.expr, ir);
696
+ if (goExpr) {
697
+ lines.push(` ${slot.slotId}: ${goExpr},`);
698
+ } else {
699
+ this.errors.push({
700
+ code: "BF101",
701
+ severity: "error",
702
+ message: `JSX spread '{...${slot.expr}}' on an intrinsic element has no Go template lowering. Supported shapes: signal-getter calls (attrs()), destructured-prop identifiers ({ extras }: P with {...extras}), SolidJS-style props identifier ((props: P) with {...props}), rest-prop identifiers ({...rest}: P with {...rest})`,
703
+ loc: this.makeLoc(),
704
+ suggestion: {
705
+ message: "Pre-compute the spread bag as a discrete prop, or expand the spread into per-attribute props at the call site."
706
+ }
707
+ });
708
+ }
709
+ }
710
+ lines.push("\t}");
711
+ lines.push("}");
712
+ }
713
+ toJsonTag(name) {
714
+ return name.charAt(0).toLowerCase() + name.slice(1);
715
+ }
716
+ findNestedComponents(node) {
717
+ const result = [];
718
+ this.collectNestedComponents(node, result);
719
+ return result;
720
+ }
721
+ collectNestedComponents(node, result) {
722
+ if (node.type === "loop") {
723
+ const loop = node;
724
+ if (loop.childComponent) {
725
+ if (!result.some((c) => c.name === loop.childComponent.name)) {
726
+ result.push({
727
+ ...loop.childComponent,
728
+ isDynamic: !loop.isStaticArray
729
+ });
730
+ }
731
+ }
732
+ for (const child of loop.children) {
733
+ this.collectNestedComponents(child, result);
734
+ }
735
+ } else if (node.type === "element") {
736
+ const element = node;
737
+ for (const child of element.children) {
738
+ this.collectNestedComponents(child, result);
739
+ }
740
+ } else if (node.type === "fragment") {
741
+ const fragment = node;
742
+ for (const child of fragment.children) {
743
+ this.collectNestedComponents(child, result);
744
+ }
745
+ } else if (node.type === "conditional") {
746
+ const cond = node;
747
+ this.collectNestedComponents(cond.whenTrue, result);
748
+ if (cond.whenFalse) {
749
+ this.collectNestedComponents(cond.whenFalse, result);
750
+ }
751
+ }
752
+ }
753
+ collectStaticChildInstances(node) {
754
+ const result = [];
755
+ this.collectStaticChildInstancesRecursive(node, result, false);
756
+ return result;
757
+ }
758
+ extractTextChildren(children) {
759
+ if (children.length === 0)
760
+ return null;
761
+ let out = "";
762
+ for (const child of children) {
763
+ if (child.type !== "text")
764
+ return null;
765
+ out += child.value;
766
+ }
767
+ return out;
768
+ }
769
+ extractHtmlChildren(children) {
770
+ if (children.length === 0)
771
+ return null;
772
+ if (children.every((c) => c.type === "text"))
773
+ return null;
774
+ const html = this.renderChildren(children);
775
+ if (html.includes("{{"))
776
+ return null;
777
+ return html;
778
+ }
779
+ collectStaticChildInstancesRecursive(node, result, inLoop) {
780
+ if (node.type === "component") {
781
+ const comp = node;
782
+ if (comp.name !== "Portal" && !inLoop && comp.slotId) {
783
+ const suffix = slotIdToFieldSuffix(comp.slotId);
784
+ result.push({
785
+ name: comp.name,
786
+ slotId: comp.slotId,
787
+ props: comp.props,
788
+ fieldName: `${comp.name}${suffix}`,
789
+ childrenText: this.extractTextChildren(comp.children),
790
+ childrenHtml: this.extractHtmlChildren(comp.children)
791
+ });
792
+ }
793
+ if (comp.name === "Portal" && comp.children) {
794
+ for (const child of comp.children) {
795
+ this.collectStaticChildInstancesRecursive(child, result, inLoop);
796
+ }
797
+ }
798
+ } else if (node.type === "loop") {
799
+ const loop = node;
800
+ for (const child of loop.children) {
801
+ this.collectStaticChildInstancesRecursive(child, result, true);
802
+ }
803
+ } else if (node.type === "element") {
804
+ const element = node;
805
+ for (const child of element.children) {
806
+ this.collectStaticChildInstancesRecursive(child, result, inLoop);
807
+ }
808
+ } else if (node.type === "fragment") {
809
+ const fragment = node;
810
+ for (const child of fragment.children) {
811
+ this.collectStaticChildInstancesRecursive(child, result, inLoop);
812
+ }
813
+ } else if (node.type === "conditional") {
814
+ const cond = node;
815
+ this.collectStaticChildInstancesRecursive(cond.whenTrue, result, inLoop);
816
+ if (cond.whenFalse) {
817
+ this.collectStaticChildInstancesRecursive(cond.whenFalse, result, inLoop);
818
+ }
819
+ } else if (node.type === "provider") {
820
+ const p = node;
821
+ for (const child of p.children) {
822
+ this.collectStaticChildInstancesRecursive(child, result, inLoop);
823
+ }
824
+ } else if (node.type === "async") {
825
+ const a = node;
826
+ this.collectStaticChildInstancesRecursive(a.fallback, result, inLoop);
827
+ for (const child of a.children) {
828
+ this.collectStaticChildInstancesRecursive(child, result, inLoop);
829
+ }
830
+ }
831
+ }
832
+ collectSpreadSlots(node) {
833
+ const result = [];
834
+ this.collectSpreadSlotsRecursive(node, result);
835
+ return result;
836
+ }
837
+ classifySpreadBagSource(spreadExpr) {
838
+ const trimmed = spreadExpr.trim();
839
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && this.restPropsName === trimmed) {
840
+ return "input-bag";
841
+ }
842
+ return "inline";
843
+ }
844
+ collectSpreadSlotsRecursive(node, result) {
845
+ if (node.type === "element") {
846
+ const element = node;
847
+ for (const attr of element.attrs) {
848
+ if (attr.value.kind !== "spread")
849
+ continue;
850
+ if (!attr.value.slotId)
851
+ continue;
852
+ result.push({
853
+ slotId: attr.value.slotId,
854
+ expr: attr.value.expr,
855
+ templateExpr: attr.value.templateExpr,
856
+ bagSource: this.classifySpreadBagSource(attr.value.expr)
857
+ });
858
+ }
859
+ for (const child of element.children) {
860
+ this.collectSpreadSlotsRecursive(child, result);
861
+ }
862
+ return;
863
+ }
864
+ if (node.type === "fragment") {
865
+ const fragment = node;
866
+ for (const child of fragment.children) {
867
+ this.collectSpreadSlotsRecursive(child, result);
868
+ }
869
+ return;
870
+ }
871
+ if (node.type === "conditional") {
872
+ const cond = node;
873
+ this.collectSpreadSlotsRecursive(cond.whenTrue, result);
874
+ if (cond.whenFalse)
875
+ this.collectSpreadSlotsRecursive(cond.whenFalse, result);
876
+ return;
877
+ }
878
+ if (node.type === "if-statement") {
879
+ const stmt = node;
880
+ this.collectSpreadSlotsRecursive(stmt.consequent, result);
881
+ if (stmt.alternate)
882
+ this.collectSpreadSlotsRecursive(stmt.alternate, result);
883
+ return;
884
+ }
885
+ if (node.type === "component") {
886
+ const comp = node;
887
+ for (const child of comp.children) {
888
+ this.collectSpreadSlotsRecursive(child, result);
889
+ }
890
+ return;
891
+ }
892
+ if (node.type === "provider") {
893
+ const p = node;
894
+ for (const child of p.children) {
895
+ this.collectSpreadSlotsRecursive(child, result);
896
+ }
897
+ return;
898
+ }
899
+ if (node.type === "async") {
900
+ const a = node;
901
+ this.collectSpreadSlotsRecursive(a.fallback, result);
902
+ for (const child of a.children) {
903
+ this.collectSpreadSlotsRecursive(child, result);
904
+ }
905
+ return;
906
+ }
907
+ }
908
+ parseJsObjectLiteralToGoMap(jsText) {
909
+ const sf = ts.createSourceFile("inline.ts", `(${jsText})`, ts.ScriptTarget.Latest, true);
910
+ if (sf.statements.length !== 1)
911
+ return null;
912
+ const stmt = sf.statements[0];
913
+ if (!ts.isExpressionStatement(stmt))
914
+ return null;
915
+ let expr = stmt.expression;
916
+ while (ts.isParenthesizedExpression(expr))
917
+ expr = expr.expression;
918
+ if (!ts.isObjectLiteralExpression(expr))
919
+ return null;
920
+ const entries = [];
921
+ for (const prop of expr.properties) {
922
+ if (!ts.isPropertyAssignment(prop))
923
+ return null;
924
+ let key;
925
+ if (ts.isIdentifier(prop.name)) {
926
+ key = prop.name.text;
927
+ } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
928
+ key = prop.name.text;
929
+ } else {
930
+ return null;
931
+ }
932
+ const val = prop.initializer;
933
+ let goVal;
934
+ if (ts.isStringLiteral(val) || ts.isNoSubstitutionTemplateLiteral(val)) {
935
+ goVal = JSON.stringify(val.text);
936
+ } else if (ts.isNumericLiteral(val)) {
937
+ goVal = val.text;
938
+ } else if (ts.isPrefixUnaryExpression(val) && (val.operator === ts.SyntaxKind.MinusToken || val.operator === ts.SyntaxKind.PlusToken) && ts.isNumericLiteral(val.operand)) {
939
+ const sign = val.operator === ts.SyntaxKind.MinusToken ? "-" : "";
940
+ goVal = `${sign}${val.operand.text}`;
941
+ } else if (val.kind === ts.SyntaxKind.TrueKeyword) {
942
+ goVal = "true";
943
+ } else if (val.kind === ts.SyntaxKind.FalseKeyword) {
944
+ goVal = "false";
945
+ } else if (val.kind === ts.SyntaxKind.NullKeyword) {
946
+ goVal = "nil";
947
+ } else {
948
+ return null;
949
+ }
950
+ entries.push(`${JSON.stringify(key)}: ${goVal}`);
951
+ }
952
+ return `map[string]any{${entries.join(", ")}}`;
953
+ }
954
+ buildSpreadInitializer(spreadExpr, ir) {
955
+ const trimmed = spreadExpr.trim();
956
+ const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed);
957
+ if (callMatch) {
958
+ const getterName = callMatch[1];
959
+ const signal = ir.metadata.signals.find((s) => s.getter === getterName);
960
+ if (signal && signal.initialValue) {
961
+ const goMap = this.parseJsObjectLiteralToGoMap(signal.initialValue);
962
+ if (goMap)
963
+ return goMap;
964
+ }
965
+ return null;
966
+ }
967
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
968
+ const param = ir.metadata.propsParams.find((p) => p.name === trimmed);
969
+ if (param) {
970
+ return `in.${this.capitalizeFieldName(param.name)}`;
971
+ }
972
+ if (ir.metadata.propsObjectName === trimmed) {
973
+ const entries = ir.metadata.propsParams.map((p) => `${JSON.stringify(p.name)}: in.${this.capitalizeFieldName(p.name)}`);
974
+ return `map[string]any{${entries.join(", ")}}`;
975
+ }
976
+ if (ir.metadata.restPropsName === trimmed) {
977
+ return `in.${this.capitalizeFieldName(trimmed)}`;
978
+ }
979
+ }
980
+ return null;
981
+ }
982
+ convertInitialValue(value, typeInfo, propsParams) {
983
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
984
+ if (propsParams?.some((p) => p.name === value)) {
985
+ return `in.${this.capitalizeFieldName(value)}`;
986
+ }
987
+ }
988
+ const propName = this.extractPropNameFromInitialValue(value);
989
+ if (propName && propsParams?.some((p) => p.name === propName)) {
990
+ return `in.${this.capitalizeFieldName(propName)}`;
991
+ }
992
+ if (typeInfo.kind === "primitive") {
993
+ if (typeInfo.primitive === "boolean") {
994
+ return value === "true" ? "true" : "false";
995
+ }
996
+ if (typeInfo.primitive === "number") {
997
+ if (/^\d+$/.test(value))
998
+ return value;
999
+ if (/^\d+\.\d+$/.test(value))
1000
+ return value;
1001
+ return "0";
1002
+ }
1003
+ if (typeInfo.primitive === "string") {
1004
+ if (value.startsWith("'") || value.endsWith("'")) {
1005
+ return value.replace(/'/g, '"');
1006
+ }
1007
+ if (value.startsWith('"') && value.endsWith('"')) {
1008
+ return value;
1009
+ }
1010
+ return '""';
1011
+ }
1012
+ }
1013
+ if (typeInfo.kind === "array") {
1014
+ if (value === "[]" || value === "null" || value === "undefined") {
1015
+ return "nil";
1016
+ }
1017
+ return "nil";
1018
+ }
1019
+ if (typeInfo.kind === "interface" && typeInfo.raw) {
1020
+ const aliasBase = this.localTypeAliases.get(typeInfo.raw);
1021
+ if (aliasBase === "string") {
1022
+ if (value.startsWith("'") || value.startsWith('"')) {
1023
+ return value.replace(/'/g, '"');
1024
+ }
1025
+ return '""';
1026
+ }
1027
+ }
1028
+ return "nil";
1029
+ }
1030
+ typeInfoToGo(typeInfo, defaultValue) {
1031
+ switch (typeInfo.kind) {
1032
+ case "primitive":
1033
+ switch (typeInfo.primitive) {
1034
+ case "string":
1035
+ return "string";
1036
+ case "number":
1037
+ return "int";
1038
+ case "boolean":
1039
+ return "bool";
1040
+ default:
1041
+ return "interface{}";
1042
+ }
1043
+ case "array":
1044
+ if (typeInfo.elementType) {
1045
+ return `[]${this.typeInfoToGo(typeInfo.elementType)}`;
1046
+ }
1047
+ return "[]interface{}";
1048
+ case "object":
1049
+ return "map[string]interface{}";
1050
+ case "interface":
1051
+ if (typeInfo.raw && this.localTypeNames.has(typeInfo.raw)) {
1052
+ return typeInfo.raw;
1053
+ }
1054
+ if (typeInfo.raw) {
1055
+ const resolved = this.tsTypeStringToGo(typeInfo.raw);
1056
+ if (resolved !== "interface{}")
1057
+ return resolved;
1058
+ }
1059
+ return "interface{}";
1060
+ case "unknown":
1061
+ if (defaultValue !== undefined) {
1062
+ return this.inferTypeFromValue(defaultValue);
1063
+ }
1064
+ return "interface{}";
1065
+ default:
1066
+ return "interface{}";
1067
+ }
1068
+ }
1069
+ getSignalInitialValueAsGo(initialValue, propsParams, propFallbackVars = GoTemplateAdapter.EMPTY_PROP_FALLBACK_VARS) {
1070
+ if (propsParams.some((p) => p.name === initialValue)) {
1071
+ const hoisted = propFallbackVars.get(initialValue);
1072
+ if (hoisted)
1073
+ return hoisted.varName;
1074
+ return `in.${this.capitalizeFieldName(initialValue)}`;
1075
+ }
1076
+ const propName = this.extractPropNameFromInitialValue(initialValue);
1077
+ if (propName && propsParams.some((p) => p.name === propName)) {
1078
+ const hoisted = propFallbackVars.get(propName);
1079
+ if (hoisted)
1080
+ return hoisted.varName;
1081
+ return `in.${this.capitalizeFieldName(propName)}`;
1082
+ }
1083
+ if (/^-?\d+$/.test(initialValue)) {
1084
+ return initialValue;
1085
+ }
1086
+ if (/^-?\d+\.\d+$/.test(initialValue)) {
1087
+ return initialValue;
1088
+ }
1089
+ if (initialValue === "true" || initialValue === "false") {
1090
+ return initialValue;
1091
+ }
1092
+ if (initialValue.startsWith("'") && initialValue.endsWith("'") || initialValue.startsWith('"') && initialValue.endsWith('"')) {
1093
+ return initialValue.replace(/'/g, '"');
1094
+ }
1095
+ return "0";
1096
+ }
1097
+ templatePartsToGoCode(parts, propsParams) {
1098
+ const segments = [];
1099
+ for (const part of parts) {
1100
+ if (part.type === "string") {
1101
+ segments.push(JSON.stringify(part.value));
1102
+ continue;
1103
+ }
1104
+ if (part.type === "lookup") {
1105
+ const keyExpr = part.key.trim();
1106
+ const param = propsParams.find((p) => p.name === keyExpr);
1107
+ if (!param)
1108
+ return null;
1109
+ const fieldName = this.capitalizeFieldName(keyExpr);
1110
+ const caseEntries = Object.entries(part.cases);
1111
+ if (caseEntries.length === 0) {
1112
+ segments.push('""');
1113
+ continue;
1114
+ }
1115
+ const lines = [];
1116
+ lines.push("func() string {");
1117
+ lines.push(` k, _ := in.${fieldName}.(string)`);
1118
+ lines.push("\t\t\tswitch k {");
1119
+ for (const [k, v] of caseEntries) {
1120
+ lines.push(` case ${JSON.stringify(k)}: return ${JSON.stringify(v)}`);
1121
+ }
1122
+ lines.push("\t\t\t}");
1123
+ lines.push('\t\t\treturn ""');
1124
+ lines.push("\t\t}()");
1125
+ segments.push(lines.join(`
1126
+ `));
1127
+ continue;
1128
+ }
1129
+ return null;
1130
+ }
1131
+ if (segments.length === 0)
1132
+ return '""';
1133
+ return segments.join(" + ");
1134
+ }
1135
+ resolveDynamicPropValue(expr, signals, memos, propsParams) {
1136
+ const getterMatch = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\(\)$/);
1137
+ if (getterMatch) {
1138
+ const getterName = getterMatch[1];
1139
+ const signal = signals.find((s) => s.getter === getterName);
1140
+ if (signal) {
1141
+ return this.convertInitialValue(signal.initialValue, signal.type, propsParams);
1142
+ }
1143
+ const memo = memos.find((m) => m.name === getterName);
1144
+ if (memo) {
1145
+ return this.computeMemoInitialValue(memo, signals, propsParams);
1146
+ }
1147
+ }
1148
+ return null;
1149
+ }
1150
+ computeMemoInitialValue(memo, signals, propsParams, propFallbackVars = GoTemplateAdapter.EMPTY_PROP_FALLBACK_VARS) {
1151
+ const computation = memo.computation;
1152
+ const propRef = (propName) => {
1153
+ const hoisted = propFallbackVars.get(propName);
1154
+ if (hoisted)
1155
+ return hoisted.varName;
1156
+ return `in.${this.capitalizeFieldName(propName)}`;
1157
+ };
1158
+ const arithmeticMatch = computation.match(/\(\)\s*=>\s*(\w+)\(\)\s*([*+\-/])\s*(\d+)/);
1159
+ if (arithmeticMatch) {
1160
+ const [, depName, operator, operand] = arithmeticMatch;
1161
+ const signal = signals.find((s) => s.getter === depName);
1162
+ if (signal) {
1163
+ const signalInitial = this.getSignalInitialValueAsGo(signal.initialValue, propsParams, propFallbackVars);
1164
+ return `${signalInitial} ${operator} ${operand}`;
1165
+ }
1166
+ }
1167
+ const propsArithmeticMatch = computation.match(/\(\)\s*=>\s*props\.(\w+)\s*([*+\-/])\s*(\d+)/);
1168
+ if (propsArithmeticMatch) {
1169
+ const [, propName, operator, operand] = propsArithmeticMatch;
1170
+ const param = propsParams.find((p) => p.name === propName);
1171
+ if (param) {
1172
+ const hoisted = propFallbackVars.get(propName);
1173
+ if (hoisted)
1174
+ return `${hoisted.varName} ${operator} ${operand}`;
1175
+ const fieldName = this.capitalizeFieldName(propName);
1176
+ if (param.type) {
1177
+ const goType = this.typeInfoToGo(param.type, param.defaultValue);
1178
+ if (goType === "interface{}")
1179
+ return `in.${fieldName}.(int) ${operator} ${operand}`;
1180
+ }
1181
+ return `in.${fieldName} ${operator} ${operand}`;
1182
+ }
1183
+ }
1184
+ const simpleMatch = computation.match(/\(\)\s*=>\s*(\w+)\(\)$/);
1185
+ if (simpleMatch) {
1186
+ const [, depName] = simpleMatch;
1187
+ const signal = signals.find((s) => s.getter === depName);
1188
+ if (signal) {
1189
+ return this.getSignalInitialValueAsGo(signal.initialValue, propsParams, propFallbackVars);
1190
+ }
1191
+ }
1192
+ const propsSimpleMatch = computation.match(/\(\)\s*=>\s*props\.(\w+)$/);
1193
+ if (propsSimpleMatch) {
1194
+ const [, propName] = propsSimpleMatch;
1195
+ const param = propsParams.find((p) => p.name === propName);
1196
+ if (param) {
1197
+ return propRef(propName);
1198
+ }
1199
+ }
1200
+ const varArithmeticMatch = computation.match(/\(\)\s*=>\s*(\w+)\s*([*+\-/])\s*(\d+)/);
1201
+ if (varArithmeticMatch) {
1202
+ const [, varName, operator, operand] = varArithmeticMatch;
1203
+ const param = propsParams.find((p) => p.name === varName);
1204
+ if (param) {
1205
+ const fieldName = this.capitalizeFieldName(varName);
1206
+ if (param.type) {
1207
+ const goType = this.typeInfoToGo(param.type, param.defaultValue);
1208
+ if (goType === "interface{}")
1209
+ return `in.${fieldName}.(int) ${operator} ${operand}`;
1210
+ }
1211
+ return `in.${fieldName} ${operator} ${operand}`;
1212
+ }
1213
+ }
1214
+ const varSimpleMatch = computation.match(/\(\)\s*=>\s*(\w+)$/);
1215
+ if (varSimpleMatch) {
1216
+ const [, varName] = varSimpleMatch;
1217
+ const param = propsParams.find((p) => p.name === varName);
1218
+ if (param) {
1219
+ return `in.${this.capitalizeFieldName(varName)}`;
1220
+ }
1221
+ }
1222
+ return "0";
1223
+ }
1224
+ inferMemoType(memo, signals, propsParamMap) {
1225
+ if (memo.computation.includes("*") || memo.computation.includes("/") || memo.computation.includes("+") || memo.computation.includes("-")) {
1226
+ for (const dep of memo.deps) {
1227
+ const signal = signals.find((s) => s.getter === dep);
1228
+ if (signal) {
1229
+ let referencedProp = propsParamMap.get(signal.initialValue);
1230
+ if (!referencedProp) {
1231
+ const propName = this.extractPropNameFromInitialValue(signal.initialValue);
1232
+ if (propName)
1233
+ referencedProp = propsParamMap.get(propName);
1234
+ }
1235
+ if (referencedProp) {
1236
+ const propType = this.typeInfoToGo(referencedProp.type, referencedProp.defaultValue);
1237
+ if (propType === "int" || propType === "float64") {
1238
+ return "int";
1239
+ }
1240
+ }
1241
+ const signalType = this.typeInfoToGo(signal.type, signal.initialValue);
1242
+ if (signalType === "int" || signalType === "float64") {
1243
+ return "int";
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ return this.typeInfoToGo(memo.type);
1249
+ }
1250
+ inferTypeFromValue(value) {
1251
+ if (value === "true" || value === "false")
1252
+ return "bool";
1253
+ if (/^-?\d+$/.test(value))
1254
+ return "int";
1255
+ if (/^-?\d+\.\d+$/.test(value))
1256
+ return "float64";
1257
+ if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
1258
+ return "string";
1259
+ }
1260
+ if (value === '""' || value === "''")
1261
+ return "string";
1262
+ if (value.startsWith("["))
1263
+ return "[]interface{}";
1264
+ return "interface{}";
1265
+ }
1266
+ static EMPTY_PROP_FALLBACK_VARS = new Map;
1267
+ collectPropFallbackVars(ir) {
1268
+ const result = new Map;
1269
+ const localTaken = new Set(["scopeID"]);
1270
+ for (const nested of this.findNestedComponents(ir.root)) {
1271
+ localTaken.add(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`);
1272
+ }
1273
+ for (const signal of ir.metadata.signals) {
1274
+ const match = this.extractPropFallback(signal.initialValue);
1275
+ if (!match)
1276
+ continue;
1277
+ if (result.has(match.propName))
1278
+ continue;
1279
+ const param = ir.metadata.propsParams.find((p) => p.name === match.propName);
1280
+ if (!param)
1281
+ continue;
1282
+ if (this.goPropDefault(param.defaultValue) !== null)
1283
+ continue;
1284
+ const fieldName = this.capitalizeFieldName(match.propName);
1285
+ let zeroLiteral;
1286
+ if (match.goFallback === "true" || match.goFallback === "false") {
1287
+ zeroLiteral = "false";
1288
+ } else if (/^-?\d+(\.\d+)?$/.test(match.goFallback)) {
1289
+ zeroLiteral = "0";
1290
+ } else if (match.goFallback.startsWith('"')) {
1291
+ zeroLiteral = '""';
1292
+ } else {
1293
+ continue;
1294
+ }
1295
+ if (match.goFallback === zeroLiteral)
1296
+ continue;
1297
+ if (zeroLiteral === "0" && Number(match.goFallback) === 0)
1298
+ continue;
1299
+ let varName = match.propName;
1300
+ while (localTaken.has(varName) || GoTemplateAdapter.GO_KEYWORDS.has(varName)) {
1301
+ varName += "_";
1302
+ }
1303
+ localTaken.add(varName);
1304
+ result.set(match.propName, { varName, fieldName, goFallback: match.goFallback, zeroLiteral });
1305
+ }
1306
+ return result;
1307
+ }
1308
+ extractPropFallback(initialValue) {
1309
+ if (!this.propsObjectName)
1310
+ return null;
1311
+ const trimmed = initialValue.trim();
1312
+ const name = this.propsObjectName;
1313
+ const re = new RegExp(`^${name}\\.(\\w+)\\s*\\?\\?\\s*(.+)$`);
1314
+ const m = trimmed.match(re);
1315
+ if (!m)
1316
+ return null;
1317
+ const goFallback = this.goPropDefault(m[2].trim());
1318
+ if (goFallback === null)
1319
+ return null;
1320
+ return { propName: m[1], goFallback };
1321
+ }
1322
+ extractPropNameFromInitialValue(initialValue) {
1323
+ if (!this.propsObjectName)
1324
+ return null;
1325
+ const trimmed = initialValue.trim();
1326
+ const name = this.propsObjectName;
1327
+ const direct = new RegExp(`^${name}\\.(\\w+)(?:\\s*(?:\\?\\?|\\|\\|)\\s*.+)?$`);
1328
+ const m1 = trimmed.match(direct);
1329
+ if (m1)
1330
+ return m1[1];
1331
+ const wrapped = new RegExp(`^\\(${name}\\.(\\w+)\\s*(?:\\?\\?|\\|\\|)\\s*[^)]+\\)(.*)$`);
1332
+ const m2 = trimmed.match(wrapped);
1333
+ if (m2) {
1334
+ const tail = m2[2];
1335
+ if (/^\s*\.(length|size|some|every|includes|indexOf|findIndex|lastIndexOf)\b/.test(tail)) {
1336
+ return null;
1337
+ }
1338
+ return m2[1];
1339
+ }
1340
+ return null;
1341
+ }
1342
+ static GO_INITIALISMS = new Set([
1343
+ "id",
1344
+ "url",
1345
+ "http",
1346
+ "https",
1347
+ "api",
1348
+ "json",
1349
+ "xml",
1350
+ "html",
1351
+ "css",
1352
+ "sql",
1353
+ "ip",
1354
+ "tcp",
1355
+ "udp",
1356
+ "dns",
1357
+ "ssh",
1358
+ "tls",
1359
+ "ssl",
1360
+ "uri",
1361
+ "uid",
1362
+ "uuid",
1363
+ "ascii",
1364
+ "utf8",
1365
+ "eof",
1366
+ "grpc",
1367
+ "rpc",
1368
+ "cpu",
1369
+ "gpu",
1370
+ "ram",
1371
+ "os"
1372
+ ]);
1373
+ static GO_KEYWORDS = new Set([
1374
+ "break",
1375
+ "case",
1376
+ "chan",
1377
+ "const",
1378
+ "continue",
1379
+ "default",
1380
+ "defer",
1381
+ "else",
1382
+ "fallthrough",
1383
+ "for",
1384
+ "func",
1385
+ "go",
1386
+ "goto",
1387
+ "if",
1388
+ "import",
1389
+ "interface",
1390
+ "map",
1391
+ "package",
1392
+ "range",
1393
+ "return",
1394
+ "select",
1395
+ "struct",
1396
+ "switch",
1397
+ "type",
1398
+ "var"
1399
+ ]);
1400
+ capitalizeFieldName(name) {
1401
+ if (!name)
1402
+ return name;
1403
+ if (GoTemplateAdapter.GO_INITIALISMS.has(name.toLowerCase())) {
1404
+ return name.toUpperCase();
1405
+ }
1406
+ return name.charAt(0).toUpperCase() + name.slice(1);
1407
+ }
1408
+ goPropDefault(defaultValue) {
1409
+ if (!defaultValue)
1410
+ return null;
1411
+ const trimmed = defaultValue.trim();
1412
+ if (trimmed === "")
1413
+ return null;
1414
+ if (trimmed === "true" || trimmed === "false")
1415
+ return trimmed;
1416
+ if (/^-?\d+(\.\d+)?$/.test(trimmed))
1417
+ return trimmed;
1418
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"')) {
1419
+ const body = trimmed.slice(1, -1);
1420
+ return JSON.stringify(body);
1421
+ }
1422
+ return null;
1423
+ }
1424
+ applyGoFallback(ref, fallback) {
1425
+ if (fallback === "true" || fallback === "false") {
1426
+ return fallback === "true" ? `(${ref} || true)` : ref;
1427
+ }
1428
+ if (/^-?\d+(\.\d+)?$/.test(fallback)) {
1429
+ if (fallback === "0")
1430
+ return ref;
1431
+ return `func() int { if ${ref} == 0 { return ${fallback} }; return ${ref} }()`;
1432
+ }
1433
+ return `func() string { if ${ref} == "" { return ${fallback} }; return ${ref} }()`;
1434
+ }
1435
+ goLiteral(value) {
1436
+ if (value === "true" || value === "false")
1437
+ return value;
1438
+ if (/^-?\d+(\.\d+)?$/.test(value))
1439
+ return value;
1440
+ if (value.startsWith("'") && value.endsWith("'")) {
1441
+ return `"${value.slice(1, -1)}"`;
1442
+ }
1443
+ if (value.startsWith('"') && value.endsWith('"')) {
1444
+ return value;
1445
+ }
1446
+ return `"${value}"`;
1447
+ }
1448
+ renderNode(node, ctx) {
1449
+ return emitIRNode(node, this, ctx ?? {});
1450
+ }
1451
+ emitElement(node, _ctx, _emit) {
1452
+ return this.renderElement(node);
1453
+ }
1454
+ emitText(node) {
1455
+ return node.value;
1456
+ }
1457
+ emitExpression(node) {
1458
+ return this.renderExpression(node);
1459
+ }
1460
+ emitConditional(node, _ctx, _emit) {
1461
+ return this.renderConditional(node);
1462
+ }
1463
+ emitLoop(node, _ctx, _emit) {
1464
+ return this.renderLoop(node);
1465
+ }
1466
+ emitComponent(node, ctx, _emit) {
1467
+ return this.renderComponent(node, ctx);
1468
+ }
1469
+ emitFragment(node, _ctx, _emit) {
1470
+ return this.renderFragment(node);
1471
+ }
1472
+ emitSlot(node) {
1473
+ return this.renderSlot(node);
1474
+ }
1475
+ emitIfStatement(node, ctx, _emit) {
1476
+ return this.renderIfStatement(node, ctx);
1477
+ }
1478
+ emitProvider(node, _ctx, _emit) {
1479
+ return this.renderChildren(node.children);
1480
+ }
1481
+ emitAsync(node, _ctx, _emit) {
1482
+ return this.renderAsync(node);
1483
+ }
1484
+ renderElement(element) {
1485
+ const tag = element.tag;
1486
+ const attrs = this.renderAttributes(element);
1487
+ const children = this.renderChildren(element.children);
1488
+ let hydrationAttrs = "";
1489
+ if (element.needsScope) {
1490
+ hydrationAttrs += ` ${this.renderScopeMarker(".ScopeID")}`;
1491
+ }
1492
+ if (element.slotId) {
1493
+ hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
1494
+ }
1495
+ const voidElements = [
1496
+ "area",
1497
+ "base",
1498
+ "br",
1499
+ "col",
1500
+ "embed",
1501
+ "hr",
1502
+ "img",
1503
+ "input",
1504
+ "link",
1505
+ "meta",
1506
+ "param",
1507
+ "source",
1508
+ "track",
1509
+ "wbr"
1510
+ ];
1511
+ if (voidElements.includes(tag.toLowerCase())) {
1512
+ return `<${tag}${attrs}${hydrationAttrs}>`;
1513
+ }
1514
+ return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
1515
+ }
1516
+ renderExpression(expr) {
1517
+ if (expr.clientOnly) {
1518
+ if (expr.slotId) {
1519
+ return `{{bfComment "client:${expr.slotId}"}}`;
1520
+ }
1521
+ return "";
1522
+ }
1523
+ const goExpr = this.convertExpressionToGo(expr.expr);
1524
+ if (goExpr.startsWith("{{")) {
1525
+ if (expr.slotId) {
1526
+ return `{{bfTextStart "${expr.slotId}"}}${goExpr}{{bfTextEnd}}`;
1527
+ }
1528
+ return goExpr;
1529
+ }
1530
+ if (expr.slotId) {
1531
+ return `{{bfTextStart "${expr.slotId}"}}{{${goExpr}}}{{bfTextEnd}}`;
1532
+ }
1533
+ return `{{${goExpr}}}`;
1534
+ }
1535
+ renderClientOnlyConditional(cond) {
1536
+ if (cond.slotId) {
1537
+ return `{{bfComment "cond-start:${cond.slotId}"}}{{bfComment "cond-end:${cond.slotId}"}}`;
1538
+ }
1539
+ return "";
1540
+ }
1541
+ renderParsedExpr(expr) {
1542
+ return emitParsedExpr(expr, this);
1543
+ }
1544
+ identifier(name) {
1545
+ return `.${this.capitalizeFieldName(name)}`;
1546
+ }
1547
+ literal(value, literalType) {
1548
+ if (literalType === "string")
1549
+ return `"${value}"`;
1550
+ if (literalType === "null")
1551
+ return '""';
1552
+ return String(value);
1553
+ }
1554
+ call(callee, args, emit) {
1555
+ if (callee.kind === "identifier" && args.length === 0) {
1556
+ return `.${this.capitalizeFieldName(callee.name)}`;
1557
+ }
1558
+ const path = identifierPath(callee);
1559
+ if (path && this.templatePrimitives[path]) {
1560
+ const expected = this.templatePrimitiveArities[path];
1561
+ if (expected === undefined || args.length === expected) {
1562
+ const renderedArgs = args.map(emit);
1563
+ return `(${this.templatePrimitives[path](renderedArgs)})`;
1564
+ }
1565
+ this.errors.push({
1566
+ code: "BF101",
1567
+ severity: "error",
1568
+ message: `templatePrimitive '${path}' expects ${expected} arg(s), got ${args.length}`,
1569
+ loc: this.makeLoc(),
1570
+ suggestion: {
1571
+ message: `Call '${path}' with exactly ${expected} argument(s), or wrap the JSX expression in /* @client */ to defer evaluation.`
1572
+ }
1573
+ });
1574
+ }
1575
+ const calleeStr = emit(callee);
1576
+ if (args.length === 0)
1577
+ return calleeStr;
1578
+ const argsStr = args.map(emit).join(" ");
1579
+ return `${calleeStr} ${argsStr}`;
1580
+ }
1581
+ member(object, property, _computed, emit) {
1582
+ if (property === "length" && object.kind === "higher-order") {
1583
+ const result = this.renderFilterLengthExpr(object, emit);
1584
+ if (result)
1585
+ return result;
1586
+ }
1587
+ if (object.kind === "higher-order" && object.method === "find") {
1588
+ const findResult = this.renderHigherOrderExpr(object, emit);
1589
+ if (findResult) {
1590
+ return `{{with ${findResult}}}{{.${this.capitalizeFieldName(property)}}}{{end}}`;
1591
+ }
1592
+ const templateBlock = this.renderFindTemplateBlock(object, emit, this.capitalizeFieldName(property));
1593
+ if (templateBlock)
1594
+ return templateBlock;
1595
+ }
1596
+ if (object.kind === "identifier" && this.propsObjectName && object.name === this.propsObjectName) {
1597
+ return `.${this.capitalizeFieldName(property)}`;
1598
+ }
1599
+ const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
1600
+ if (object.kind === "identifier" && currentLoopParam && object.name === currentLoopParam) {
1601
+ return `.${this.capitalizeFieldName(property)}`;
1602
+ }
1603
+ const obj = emit(object);
1604
+ if (property === "length")
1605
+ return `len ${obj}`;
1606
+ return `${obj}.${this.capitalizeFieldName(property)}`;
1607
+ }
1608
+ binary(op, left, right, emit) {
1609
+ const l = emit(left);
1610
+ const r = emit(right);
1611
+ switch (op) {
1612
+ case "===":
1613
+ case "==":
1614
+ return `eq ${l} ${r}`;
1615
+ case "!==":
1616
+ case "!=":
1617
+ return `ne ${l} ${r}`;
1618
+ case ">":
1619
+ return `gt ${l} ${r}`;
1620
+ case "<":
1621
+ return `lt ${l} ${r}`;
1622
+ case ">=":
1623
+ return `ge ${l} ${r}`;
1624
+ case "<=":
1625
+ return `le ${l} ${r}`;
1626
+ case "+":
1627
+ return `bf_add ${l} ${r}`;
1628
+ case "-":
1629
+ return `bf_sub ${l} ${r}`;
1630
+ case "*":
1631
+ return `bf_mul ${l} ${r}`;
1632
+ case "/":
1633
+ return `bf_div ${l} ${r}`;
1634
+ case "%":
1635
+ return `bf_mod ${l} ${r}`;
1636
+ default:
1637
+ return `${l} ${op} ${r}`;
1638
+ }
1639
+ }
1640
+ unary(op, argument, emit) {
1641
+ const arg = emit(argument);
1642
+ if (op === "!")
1643
+ return `not ${arg}`;
1644
+ if (op === "-")
1645
+ return `bf_neg ${arg}`;
1646
+ return arg;
1647
+ }
1648
+ logical(op, left, right, emit) {
1649
+ const l = emit(left);
1650
+ const r = emit(right);
1651
+ const wrapLeft = this.needsParens(left) ? `(${l})` : l;
1652
+ const wrapRight = this.needsParens(right) ? `(${r})` : r;
1653
+ if (op === "&&")
1654
+ return `and ${wrapLeft} ${wrapRight}`;
1655
+ return `or ${wrapLeft} ${wrapRight}`;
1656
+ }
1657
+ conditional(test, consequent, alternate, emit) {
1658
+ const t = emit(test);
1659
+ const c = this.renderConditionalBranch(consequent);
1660
+ const a = this.renderConditionalBranch(alternate);
1661
+ return `{{if ${t}}}${c}{{else}}${a}{{end}}`;
1662
+ }
1663
+ templateLiteral(parts, emit) {
1664
+ let result = "";
1665
+ for (const part of parts) {
1666
+ if (part.type === "string") {
1667
+ result += part.value;
1668
+ } else {
1669
+ result += `{{${emit(part.expr)}}}`;
1670
+ }
1671
+ }
1672
+ return result;
1673
+ }
1674
+ arrowFn(param, _body, _emit) {
1675
+ return `[ARROW-FN: ${param} => ...]`;
1676
+ }
1677
+ arrayLiteral(elements, emit) {
1678
+ if (elements.length === 0)
1679
+ return "bf_arr";
1680
+ const parts = elements.map((el) => {
1681
+ const rendered = emit(el);
1682
+ return rendered.includes(" ") ? `(${rendered})` : rendered;
1683
+ });
1684
+ return `bf_arr ${parts.join(" ")}`;
1685
+ }
1686
+ higherOrder(method, object, param, predicate, emit) {
1687
+ const reconstructed = { kind: "higher-order", method, object, param, predicate };
1688
+ const result = this.renderHigherOrderExpr(reconstructed, emit);
1689
+ if (result)
1690
+ return result;
1691
+ if (method === "find" || method === "findIndex") {
1692
+ const templateBlock = this.renderFindTemplateBlock(reconstructed, emit);
1693
+ if (templateBlock)
1694
+ return templateBlock;
1695
+ }
1696
+ if (method === "every" || method === "some") {
1697
+ const templateBlock = this.renderEverySomeTemplateBlock(reconstructed, emit);
1698
+ if (templateBlock)
1699
+ return templateBlock;
1700
+ }
1701
+ this.errors.push({
1702
+ code: "BF101",
1703
+ severity: "error",
1704
+ message: `Higher-order method '.${method}' shape cannot be lowered to a Go template action`,
1705
+ loc: this.makeLoc(),
1706
+ suggestion: {
1707
+ message: `Options:
1708
+ 1. Use @client directive for client-side evaluation
1709
+ 2. Pre-compute the value in Go code`
1710
+ }
1711
+ });
1712
+ return `""`;
1713
+ }
1714
+ arrayMethod(method, object, args, emit) {
1715
+ switch (method) {
1716
+ case "join": {
1717
+ const obj = emit(object);
1718
+ const sep = emit(args[0]);
1719
+ return `bf_join (${obj}) ${wrapIfMultiToken(sep)}`;
1720
+ }
1721
+ case "includes": {
1722
+ const obj = emit(object);
1723
+ const needle = emit(args[0]);
1724
+ return `bf_includes ${wrapIfMultiToken(obj)} ${wrapIfMultiToken(needle)}`;
1725
+ }
1726
+ case "indexOf":
1727
+ case "lastIndexOf": {
1728
+ const fn = method === "indexOf" ? "bf_index_of" : "bf_last_index_of";
1729
+ const obj = emit(object);
1730
+ const needle = emit(args[0]);
1731
+ return `${fn} ${wrapIfMultiToken(obj)} ${wrapIfMultiToken(needle)}`;
1732
+ }
1733
+ case "at": {
1734
+ const obj = emit(object);
1735
+ const idx = emit(args[0]);
1736
+ return `bf_at ${wrapIfMultiToken(obj)} ${wrapIfMultiToken(idx)}`;
1737
+ }
1738
+ case "concat": {
1739
+ const a = emit(object);
1740
+ const b = emit(args[0]);
1741
+ return `bf_concat ${wrapIfMultiToken(a)} ${wrapIfMultiToken(b)}`;
1742
+ }
1743
+ case "slice": {
1744
+ const recv = emit(object);
1745
+ const start = emit(args[0]);
1746
+ if (args.length === 1) {
1747
+ return `bf_slice ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(start)}`;
1748
+ }
1749
+ const end = emit(args[1]);
1750
+ return `bf_slice ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(start)} ${wrapIfMultiToken(end)}`;
1751
+ }
1752
+ case "reverse":
1753
+ case "toReversed": {
1754
+ const recv = emit(object);
1755
+ return `bf_reverse ${wrapIfMultiToken(recv)}`;
1756
+ }
1757
+ case "toLowerCase": {
1758
+ const recv = emit(object);
1759
+ return `bf_lower ${wrapIfMultiToken(recv)}`;
1760
+ }
1761
+ case "toUpperCase": {
1762
+ const recv = emit(object);
1763
+ return `bf_upper ${wrapIfMultiToken(recv)}`;
1764
+ }
1765
+ case "trim": {
1766
+ const recv = emit(object);
1767
+ return `bf_trim ${wrapIfMultiToken(recv)}`;
1768
+ }
1769
+ default: {
1770
+ const _exhaustive = method;
1771
+ throw new Error(`Go arrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
1772
+ }
1773
+ }
1774
+ }
1775
+ sortMethod(method, object, comparator, emit) {
1776
+ return emitBfSort(emit(object), comparator);
1777
+ }
1778
+ unsupported(raw, _reason) {
1779
+ return `[UNSUPPORTED: ${raw}]`;
1780
+ }
1781
+ extractFieldPredicate(pred, param) {
1782
+ if (pred.kind === "member" && pred.object.kind === "identifier" && pred.object.name === param) {
1783
+ return { field: this.capitalizeFieldName(pred.property), negated: false };
1784
+ }
1785
+ if (pred.kind === "unary" && pred.op === "!" && pred.argument.kind === "member") {
1786
+ const mem = pred.argument;
1787
+ if (mem.object.kind === "identifier" && mem.object.name === param) {
1788
+ return { field: this.capitalizeFieldName(mem.property), negated: true };
1789
+ }
1790
+ }
1791
+ return { field: null, negated: false };
1792
+ }
1793
+ extractEqualityPredicate(pred, param, renderValue) {
1794
+ if (pred.kind === "member" && pred.object.kind === "identifier" && pred.object.name === param) {
1795
+ return { field: this.capitalizeFieldName(pred.property), value: "true" };
1796
+ }
1797
+ if (pred.kind === "unary" && pred.op === "!" && pred.argument.kind === "member") {
1798
+ const mem = pred.argument;
1799
+ if (mem.object.kind === "identifier" && mem.object.name === param) {
1800
+ return { field: this.capitalizeFieldName(mem.property), value: "false" };
1801
+ }
1802
+ }
1803
+ if (pred.kind === "binary" && (pred.op === "===" || pred.op === "==")) {
1804
+ if (pred.left.kind === "member" && pred.left.object.kind === "identifier" && pred.left.object.name === param) {
1805
+ return { field: this.capitalizeFieldName(pred.left.property), value: renderValue(pred.right) };
1806
+ }
1807
+ if (pred.right.kind === "member" && pred.right.object.kind === "identifier" && pred.right.object.name === param) {
1808
+ return { field: this.capitalizeFieldName(pred.right.property), value: renderValue(pred.left) };
1809
+ }
1810
+ }
1811
+ return null;
1812
+ }
1813
+ renderHigherOrderExpr(expr, renderArray) {
1814
+ const arrayExpr = renderArray(expr.object);
1815
+ if (expr.method === "every" || expr.method === "some") {
1816
+ const { field } = this.extractFieldPredicate(expr.predicate, expr.param);
1817
+ if (!field)
1818
+ return null;
1819
+ return expr.method === "every" ? `bf_every ${arrayExpr} "${field}"` : `bf_some ${arrayExpr} "${field}"`;
1820
+ }
1821
+ if (expr.method === "filter") {
1822
+ if (expr.predicate.kind === "identifier" && expr.predicate.name === expr.param) {
1823
+ return `bf_filter_truthy (${arrayExpr})`;
1824
+ }
1825
+ const { field, negated } = this.extractFieldPredicate(expr.predicate, expr.param);
1826
+ if (!field)
1827
+ return null;
1828
+ const value = negated ? "false" : "true";
1829
+ return `bf_filter ${arrayExpr} "${field}" ${value}`;
1830
+ }
1831
+ if (expr.method === "find" || expr.method === "findIndex") {
1832
+ const eqPred = this.extractEqualityPredicate(expr.predicate, expr.param, (e) => this.renderParsedExpr(e));
1833
+ if (!eqPred)
1834
+ return null;
1835
+ const func = expr.method === "find" ? "bf_find" : "bf_find_index";
1836
+ return `${func} ${arrayExpr} "${eqPred.field}" ${eqPred.value}`;
1837
+ }
1838
+ return null;
1839
+ }
1840
+ renderFindTemplateBlock(expr, renderArray, propertyAccess) {
1841
+ const arrayExpr = renderArray(expr.object);
1842
+ const condition = this.renderFilterExpr(expr.predicate, expr.param);
1843
+ if (condition.includes("[UNSUPPORTED"))
1844
+ return null;
1845
+ if (expr.method === "find") {
1846
+ const output = propertyAccess ? `{{.${propertyAccess}}}` : "{{.}}";
1847
+ return `{{range ${arrayExpr}}}{{if ${condition}}}${output}{{break}}{{end}}{{end}}`;
1848
+ }
1849
+ if (expr.method === "findIndex") {
1850
+ return `{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{$i}}{{break}}{{end}}{{end}}`;
1851
+ }
1852
+ return null;
1853
+ }
1854
+ renderEverySomeTemplateBlock(expr, renderArray) {
1855
+ const arrayExpr = renderArray(expr.object);
1856
+ const condition = this.renderFilterExpr(expr.predicate, expr.param);
1857
+ if (condition.includes("[UNSUPPORTED"))
1858
+ return null;
1859
+ if (expr.method === "every") {
1860
+ const negated = this.negateGoCondition(condition);
1861
+ return `{{$bf_result := true}}{{range ${arrayExpr}}}{{if ${negated}}}{{$bf_result = false}}{{break}}{{end}}{{end}}{{$bf_result}}`;
1862
+ }
1863
+ if (expr.method === "some") {
1864
+ return `{{$bf_result := false}}{{range ${arrayExpr}}}{{if ${condition}}}{{$bf_result = true}}{{break}}{{end}}{{end}}{{$bf_result}}`;
1865
+ }
1866
+ return null;
1867
+ }
1868
+ negateGoCondition(condition) {
1869
+ const goFuncPattern = /^(eq|ne|gt|lt|ge|le|and|or|not|bf_)\b/;
1870
+ if (goFuncPattern.test(condition)) {
1871
+ return `not (${condition})`;
1872
+ }
1873
+ return `not ${condition}`;
1874
+ }
1875
+ renderFilterLengthExpr(filterExpr, renderArray) {
1876
+ if (filterExpr.method !== "filter") {
1877
+ return null;
1878
+ }
1879
+ const { field, negated } = this.extractFieldPredicate(filterExpr.predicate, filterExpr.param);
1880
+ if (!field) {
1881
+ return null;
1882
+ }
1883
+ const arrayExpr = renderArray(filterExpr.object);
1884
+ const value = negated ? "false" : "true";
1885
+ return `len (bf_filter ${arrayExpr} "${field}" ${value})`;
1886
+ }
1887
+ renderPredicateCondition(pred, param) {
1888
+ return this.renderFilterExpr(pred, param);
1889
+ }
1890
+ needsParens(expr) {
1891
+ return expr.kind === "logical" || expr.kind === "unary" || expr.kind === "conditional";
1892
+ }
1893
+ renderBlockBodyCondition(statements, param) {
1894
+ const localVarMap = new Map;
1895
+ const paths = this.collectReturnPaths(statements, [], localVarMap, param);
1896
+ if (paths.length === 0) {
1897
+ return "true";
1898
+ }
1899
+ if (paths.length === 1) {
1900
+ const path = paths[0];
1901
+ return this.buildSinglePathCondition(path, param, localVarMap);
1902
+ }
1903
+ return this.buildOrCondition(paths, param, localVarMap);
1904
+ }
1905
+ collectReturnPaths(statements, currentConditions, localVarMap, param) {
1906
+ const paths = [];
1907
+ for (const stmt of statements) {
1908
+ if (stmt.kind === "var-decl") {
1909
+ if (stmt.init.kind === "call" && stmt.init.callee.kind === "identifier") {
1910
+ localVarMap.set(stmt.name, stmt.init.callee.name);
1911
+ }
1912
+ } else if (stmt.kind === "return") {
1913
+ paths.push({
1914
+ conditions: [...currentConditions],
1915
+ result: stmt.value
1916
+ });
1917
+ break;
1918
+ } else if (stmt.kind === "if") {
1919
+ const thenConditions = [...currentConditions, stmt.condition];
1920
+ const thenPaths = this.collectReturnPaths(stmt.consequent, thenConditions, localVarMap, param);
1921
+ paths.push(...thenPaths);
1922
+ if (stmt.alternate) {
1923
+ const negatedCondition = { kind: "unary", op: "!", argument: stmt.condition };
1924
+ const elseConditions = [...currentConditions, negatedCondition];
1925
+ const elsePaths = this.collectReturnPaths(stmt.alternate, elseConditions, localVarMap, param);
1926
+ paths.push(...elsePaths);
1927
+ } else {
1928
+ const negatedCondition = { kind: "unary", op: "!", argument: stmt.condition };
1929
+ currentConditions.push(negatedCondition);
1930
+ }
1931
+ }
1932
+ }
1933
+ return paths;
1934
+ }
1935
+ buildSinglePathCondition(path, param, localVarMap) {
1936
+ if (path.result.kind === "literal" && path.result.literalType === "boolean") {
1937
+ if (path.result.value === true) {
1938
+ if (path.conditions.length === 0) {
1939
+ return "true";
1940
+ }
1941
+ return this.renderConditionsAnd(path.conditions, param, localVarMap);
1942
+ } else {
1943
+ return "false";
1944
+ }
1945
+ }
1946
+ if (path.conditions.length === 0) {
1947
+ return this.renderFilterExpr(path.result, param, localVarMap);
1948
+ }
1949
+ const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap);
1950
+ const resultPart = this.renderFilterExpr(path.result, param, localVarMap);
1951
+ return `and (${condPart}) (${resultPart})`;
1952
+ }
1953
+ buildOrCondition(paths, param, localVarMap) {
1954
+ const parts = [];
1955
+ for (const path of paths) {
1956
+ if (path.result.kind === "literal" && path.result.literalType === "boolean" && path.result.value === false) {
1957
+ continue;
1958
+ }
1959
+ const pathCond = this.buildSinglePathCondition(path, param, localVarMap);
1960
+ if (pathCond !== "false") {
1961
+ parts.push(pathCond);
1962
+ }
1963
+ }
1964
+ if (parts.length === 0) {
1965
+ return "false";
1966
+ }
1967
+ if (parts.length === 1) {
1968
+ return parts[0];
1969
+ }
1970
+ return `or ${parts.map((p) => `(${p})`).join(" ")}`;
1971
+ }
1972
+ renderConditionsAnd(conditions, param, localVarMap) {
1973
+ if (conditions.length === 0) {
1974
+ return "true";
1975
+ }
1976
+ if (conditions.length === 1) {
1977
+ return this.renderFilterExpr(conditions[0], param, localVarMap);
1978
+ }
1979
+ const parts = conditions.map((c) => this.renderFilterExpr(c, param, localVarMap));
1980
+ let result = parts[parts.length - 1];
1981
+ for (let i = parts.length - 2;i >= 0; i--) {
1982
+ result = `and (${parts[i]}) (${result})`;
1983
+ }
1984
+ return result;
1985
+ }
1986
+ renderFilterExpr(expr, param, localVarMap = new Map) {
1987
+ if (this.filterExprDepth === 0)
1988
+ this.filterExprUnsupported = false;
1989
+ this.filterExprDepth++;
1990
+ try {
1991
+ return this.renderFilterExprNode(expr, param, localVarMap);
1992
+ } finally {
1993
+ this.filterExprDepth--;
1994
+ }
1995
+ }
1996
+ renderFilterExprNode(expr, param, localVarMap) {
1997
+ switch (expr.kind) {
1998
+ case "identifier": {
1999
+ if (expr.name === param) {
2000
+ return ".";
2001
+ }
2002
+ const signal = localVarMap.get(expr.name);
2003
+ if (signal) {
2004
+ return `$.${this.capitalizeFieldName(signal)}`;
2005
+ }
2006
+ return `.${this.capitalizeFieldName(expr.name)}`;
2007
+ }
2008
+ case "literal":
2009
+ if (expr.literalType === "string") {
2010
+ return `"${expr.value}"`;
2011
+ }
2012
+ if (expr.literalType === "null") {
2013
+ return '""';
2014
+ }
2015
+ return String(expr.value);
2016
+ case "member": {
2017
+ if (expr.object.kind === "identifier" && expr.object.name === param) {
2018
+ return `.${this.capitalizeFieldName(expr.property)}`;
2019
+ }
2020
+ if (expr.property === "length" && expr.object.kind === "higher-order" && expr.object.method === "filter") {
2021
+ const lenExpr = this.renderFilterLengthExpr(expr.object, (e) => this.renderFilterExpr(e, param, localVarMap));
2022
+ if (lenExpr)
2023
+ return `(${lenExpr})`;
2024
+ }
2025
+ const obj = this.renderFilterExpr(expr.object, param, localVarMap);
2026
+ if (this.filterExprUnsupported)
2027
+ return "false";
2028
+ return `${obj}.${this.capitalizeFieldName(expr.property)}`;
2029
+ }
2030
+ case "call": {
2031
+ if (expr.callee.kind === "member" && expr.callee.object.kind === "identifier" && expr.callee.object.name === param) {
2032
+ return `.${this.capitalizeFieldName(expr.callee.property)}`;
2033
+ }
2034
+ if (expr.callee.kind === "identifier" && expr.args.length === 0) {
2035
+ return `$.${this.capitalizeFieldName(expr.callee.name)}`;
2036
+ }
2037
+ const result = this.renderFilterExpr(expr.callee, param, localVarMap);
2038
+ if (this.filterExprUnsupported)
2039
+ return "false";
2040
+ return result;
2041
+ }
2042
+ case "unary": {
2043
+ const arg = this.renderFilterExpr(expr.argument, param, localVarMap);
2044
+ if (this.filterExprUnsupported)
2045
+ return "false";
2046
+ if (expr.op === "!") {
2047
+ const needsParens = this.isGoFunctionCall(expr.argument);
2048
+ return needsParens ? `not (${arg})` : `not ${arg}`;
2049
+ }
2050
+ if (expr.op === "-") {
2051
+ return `bf_neg ${arg}`;
2052
+ }
2053
+ return arg;
2054
+ }
2055
+ case "binary": {
2056
+ const left = this.renderFilterExpr(expr.left, param, localVarMap);
2057
+ if (this.filterExprUnsupported)
2058
+ return "false";
2059
+ const right = this.renderFilterExpr(expr.right, param, localVarMap);
2060
+ if (this.filterExprUnsupported)
2061
+ return "false";
2062
+ switch (expr.op) {
2063
+ case "===":
2064
+ case "==":
2065
+ return `eq ${left} ${right}`;
2066
+ case "!==":
2067
+ case "!=":
2068
+ return `ne ${left} ${right}`;
2069
+ case ">":
2070
+ return `gt ${left} ${right}`;
2071
+ case "<":
2072
+ return `lt ${left} ${right}`;
2073
+ case ">=":
2074
+ return `ge ${left} ${right}`;
2075
+ case "<=":
2076
+ return `le ${left} ${right}`;
2077
+ case "+":
2078
+ return `bf_add ${left} ${right}`;
2079
+ case "-":
2080
+ return `bf_sub ${left} ${right}`;
2081
+ case "*":
2082
+ return `bf_mul ${left} ${right}`;
2083
+ case "/":
2084
+ return `bf_div ${left} ${right}`;
2085
+ default:
2086
+ return `${left} ${expr.op} ${right}`;
2087
+ }
2088
+ }
2089
+ case "logical": {
2090
+ const left = this.renderFilterExpr(expr.left, param, localVarMap);
2091
+ if (this.filterExprUnsupported)
2092
+ return "false";
2093
+ const right = this.renderFilterExpr(expr.right, param, localVarMap);
2094
+ if (this.filterExprUnsupported)
2095
+ return "false";
2096
+ if (expr.op === "&&") {
2097
+ return `and (${left}) (${right})`;
2098
+ }
2099
+ return `or (${left}) (${right})`;
2100
+ }
2101
+ default: {
2102
+ this.filterExprUnsupported = true;
2103
+ this.errors.push({
2104
+ code: "BF101",
2105
+ severity: "error",
2106
+ message: `Filter predicate contains an expression that cannot be lowered to a Go template action: ${exprToString(expr)}`,
2107
+ loc: this.makeLoc(),
2108
+ suggestion: {
2109
+ message: "Options:\n1. Use /* @client */ for client-side evaluation\n2. Rewrite the predicate to avoid nested higher-order methods (`.filter()` / `.map()` / etc. inside the predicate body)"
2110
+ }
2111
+ });
2112
+ return "false";
2113
+ }
2114
+ }
2115
+ }
2116
+ isGoFunctionCall(expr) {
2117
+ switch (expr.kind) {
2118
+ case "binary":
2119
+ return ["===", "==", "!==", "!=", ">", "<", ">=", "<="].includes(expr.op);
2120
+ case "logical":
2121
+ return true;
2122
+ case "unary":
2123
+ return true;
2124
+ case "member":
2125
+ return expr.property === "length";
2126
+ default:
2127
+ return false;
2128
+ }
2129
+ }
2130
+ renderConditionalBranch(expr) {
2131
+ if (expr.kind === "literal" && expr.literalType === "string") {
2132
+ return String(expr.value);
2133
+ }
2134
+ if (expr.kind === "conditional") {
2135
+ const test = this.renderParsedExpr(expr.test);
2136
+ const consequent = this.renderConditionalBranch(expr.consequent);
2137
+ const alternate = this.renderConditionalBranch(expr.alternate);
2138
+ return `{{if ${test}}}${consequent}{{else}}${alternate}{{end}}`;
2139
+ }
2140
+ return `{{${this.renderParsedExpr(expr)}}}`;
2141
+ }
2142
+ needsParensInGoTemplate(expr) {
2143
+ switch (expr.kind) {
2144
+ case "member":
2145
+ return expr.property === "length";
2146
+ case "binary":
2147
+ return ["+", "-", "*", "/", "%"].includes(expr.op);
2148
+ case "unary":
2149
+ return expr.op === "-";
2150
+ default:
2151
+ return false;
2152
+ }
2153
+ }
2154
+ convertExpressionToGo(jsExpr) {
2155
+ const trimmed = jsExpr.trim();
2156
+ if (trimmed === "null" || trimmed === "undefined") {
2157
+ return '""';
2158
+ }
2159
+ const parsed = parseExpression(trimmed);
2160
+ const support = isSupported(parsed);
2161
+ if (!support.supported) {
2162
+ this.errors.push({
2163
+ code: "BF101",
2164
+ severity: "error",
2165
+ message: `Expression not supported: ${trimmed}`,
2166
+ loc: this.makeLoc(),
2167
+ suggestion: {
2168
+ message: support.reason ? `${support.reason}
2169
+
2170
+ Options:
2171
+ 1. Use @client directive for client-side evaluation
2172
+ 2. Pre-compute the value in Go code` : `Options:
2173
+ 1. Use @client directive for client-side evaluation
2174
+ 2. Pre-compute the value in Go code`
2175
+ }
2176
+ });
2177
+ return `""`;
2178
+ }
2179
+ return this.renderParsedExpr(parsed);
2180
+ }
2181
+ makeLoc() {
2182
+ return {
2183
+ file: this.componentName + ".tsx",
2184
+ start: { line: 1, column: 0 },
2185
+ end: { line: 1, column: 0 }
2186
+ };
2187
+ }
2188
+ renderIfStatement(ifStmt, ctx) {
2189
+ const { condition: goCondition, preamble } = this.convertConditionToGo(ifStmt.condition);
2190
+ const consequent = this.renderNode(ifStmt.consequent, ctx);
2191
+ let result = `${preamble}{{if ${goCondition}}}${consequent}`;
2192
+ if (ifStmt.alternate) {
2193
+ if (ifStmt.alternate.type === "if-statement") {
2194
+ const altIfStmt = ifStmt.alternate;
2195
+ const { condition: altCondition, preamble: altPreamble } = this.convertConditionToGo(altIfStmt.condition);
2196
+ if (altPreamble) {
2197
+ this.errors.push({
2198
+ code: "BF102",
2199
+ severity: "error",
2200
+ message: `Complex predicate in else-if is not supported: ${altIfStmt.condition}`,
2201
+ loc: this.makeLoc(),
2202
+ suggestion: {
2203
+ message: `Options:
2204
+ 1. Use @client directive for client-side evaluation
2205
+ 2. Pre-compute the value in Go code`
2206
+ }
2207
+ });
2208
+ }
2209
+ const altConsequent = this.renderNode(altIfStmt.consequent, ctx);
2210
+ result += `{{else if ${altCondition}}}${altConsequent}`;
2211
+ if (altIfStmt.alternate) {
2212
+ const altElse = this.renderNode(altIfStmt.alternate, ctx);
2213
+ result += `{{else}}${altElse}`;
2214
+ }
2215
+ } else {
2216
+ const alternate = this.renderNode(ifStmt.alternate, ctx);
2217
+ result += `{{else}}${alternate}`;
2218
+ }
2219
+ }
2220
+ result += "{{end}}";
2221
+ return result;
2222
+ }
2223
+ renderConditional(cond) {
2224
+ if (cond.clientOnly) {
2225
+ return this.renderClientOnlyConditional(cond);
2226
+ }
2227
+ const { condition: goCondition, preamble } = this.convertConditionToGo(cond.condition);
2228
+ const whenTrue = this.renderNode(cond.whenTrue);
2229
+ if (cond.slotId) {
2230
+ const whenTrueWrapped = this.wrapWithCondMarker(whenTrue, cond.slotId);
2231
+ let result2 = `${preamble}{{if ${goCondition}}}${whenTrueWrapped}`;
2232
+ if (cond.whenFalse) {
2233
+ if (cond.whenFalse.type === "expression") {
2234
+ const exprNode = cond.whenFalse;
2235
+ if (exprNode.expr === "null" || exprNode.expr === "undefined") {
2236
+ const emptyMarkers = `{{bfComment "cond-start:${cond.slotId}"}}{{bfComment "cond-end:${cond.slotId}"}}`;
2237
+ result2 += `{{else}}${emptyMarkers}`;
2238
+ } else {
2239
+ const whenFalse = this.renderNode(cond.whenFalse);
2240
+ const whenFalseWrapped = this.wrapWithCondMarker(whenFalse, cond.slotId);
2241
+ result2 += `{{else}}${whenFalseWrapped}`;
2242
+ }
2243
+ } else {
2244
+ const whenFalse = this.renderNode(cond.whenFalse);
2245
+ const whenFalseWrapped = this.wrapWithCondMarker(whenFalse, cond.slotId);
2246
+ result2 += `{{else}}${whenFalseWrapped}`;
2247
+ }
2248
+ }
2249
+ result2 += "{{end}}";
2250
+ return result2;
2251
+ }
2252
+ let result = `${preamble}{{if ${goCondition}}}${whenTrue}`;
2253
+ if (cond.whenFalse && cond.whenFalse.type !== "expression") {
2254
+ const whenFalse = this.renderNode(cond.whenFalse);
2255
+ if (whenFalse && whenFalse !== '{{""}}') {
2256
+ result += `{{else}}${whenFalse}`;
2257
+ }
2258
+ } else if (cond.whenFalse && cond.whenFalse.type === "expression") {
2259
+ const exprNode = cond.whenFalse;
2260
+ if (exprNode.expr !== "null" && exprNode.expr !== "undefined") {
2261
+ const whenFalse = this.renderNode(cond.whenFalse);
2262
+ if (whenFalse && whenFalse !== '{{""}}') {
2263
+ result += `{{else}}${whenFalse}`;
2264
+ }
2265
+ }
2266
+ }
2267
+ result += "{{end}}";
2268
+ return result;
2269
+ }
2270
+ convertConditionToGo(jsCondition) {
2271
+ const trimmed = jsCondition.trim();
2272
+ const parsed = parseExpression(trimmed);
2273
+ const support = isSupported(parsed);
2274
+ if (!support.supported) {
2275
+ this.errors.push({
2276
+ code: "BF102",
2277
+ severity: "error",
2278
+ message: `Condition not supported: ${trimmed}`,
2279
+ loc: this.makeLoc(),
2280
+ suggestion: {
2281
+ message: support.reason ? `${support.reason}
2282
+
2283
+ Options:
2284
+ 1. Use @client directive for client-side evaluation
2285
+ 2. Pre-compute the value in Go code` : "Expression contains unsupported syntax"
2286
+ }
2287
+ });
2288
+ return { condition: `false`, preamble: "" };
2289
+ }
2290
+ const rendered = this.renderConditionExpr(parsed);
2291
+ if (rendered.startsWith("{{")) {
2292
+ const lastOpen = rendered.lastIndexOf("{{");
2293
+ const lastClose = rendered.lastIndexOf("}}");
2294
+ if (lastOpen >= 0 && lastClose > lastOpen) {
2295
+ const preamble = rendered.substring(0, lastOpen);
2296
+ const condition = rendered.substring(lastOpen + 2, lastClose);
2297
+ return { condition, preamble };
2298
+ }
2299
+ }
2300
+ return { condition: rendered, preamble: "" };
2301
+ }
2302
+ renderConditionExpr(expr) {
2303
+ switch (expr.kind) {
2304
+ case "identifier":
2305
+ {
2306
+ const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2307
+ if (currentLoopParam && expr.name === currentLoopParam) {
2308
+ return ".";
2309
+ }
2310
+ }
2311
+ return `.${this.capitalizeFieldName(expr.name)}`;
2312
+ case "literal":
2313
+ if (expr.literalType === "string") {
2314
+ return `"${expr.value}"`;
2315
+ }
2316
+ if (expr.literalType === "null") {
2317
+ return '""';
2318
+ }
2319
+ return String(expr.value);
2320
+ case "call": {
2321
+ if (expr.callee.kind === "identifier" && expr.args.length === 0) {
2322
+ return `.${this.capitalizeFieldName(expr.callee.name)}`;
2323
+ }
2324
+ return this.renderParsedExpr(expr);
2325
+ }
2326
+ case "member": {
2327
+ if (expr.property === "length" && expr.object.kind === "higher-order") {
2328
+ const result = this.renderFilterLengthExpr(expr.object, (e) => this.renderConditionExpr(e));
2329
+ if (result) {
2330
+ return result;
2331
+ }
2332
+ }
2333
+ if (expr.object.kind === "identifier" && this.propsObjectName && expr.object.name === this.propsObjectName) {
2334
+ return `.${this.capitalizeFieldName(expr.property)}`;
2335
+ }
2336
+ {
2337
+ const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2338
+ if (expr.object.kind === "identifier" && currentLoopParam && expr.object.name === currentLoopParam) {
2339
+ return `.${this.capitalizeFieldName(expr.property)}`;
2340
+ }
2341
+ }
2342
+ const obj = this.renderConditionExpr(expr.object);
2343
+ if (expr.property === "length") {
2344
+ return `len ${obj}`;
2345
+ }
2346
+ return `${obj}.${this.capitalizeFieldName(expr.property)}`;
2347
+ }
2348
+ case "binary": {
2349
+ const leftNeedsParens = this.needsParensInGoTemplate(expr.left);
2350
+ let left = this.renderConditionExpr(expr.left);
2351
+ if (leftNeedsParens) {
2352
+ left = `(${left})`;
2353
+ }
2354
+ const rightNeedsParens = this.needsParensInGoTemplate(expr.right);
2355
+ let right = this.renderConditionExpr(expr.right);
2356
+ if (rightNeedsParens) {
2357
+ right = `(${right})`;
2358
+ }
2359
+ switch (expr.op) {
2360
+ case "===":
2361
+ case "==":
2362
+ return `eq ${left} ${right}`;
2363
+ case "!==":
2364
+ case "!=":
2365
+ return `ne ${left} ${right}`;
2366
+ case ">":
2367
+ return `gt ${left} ${right}`;
2368
+ case "<":
2369
+ return `lt ${left} ${right}`;
2370
+ case ">=":
2371
+ return `ge ${left} ${right}`;
2372
+ case "<=":
2373
+ return `le ${left} ${right}`;
2374
+ case "+":
2375
+ return `bf_add ${left} ${right}`;
2376
+ case "-":
2377
+ return `bf_sub ${left} ${right}`;
2378
+ case "*":
2379
+ return `bf_mul ${left} ${right}`;
2380
+ case "/":
2381
+ return `bf_div ${left} ${right}`;
2382
+ default:
2383
+ return `${left} ${expr.op} ${right}`;
2384
+ }
2385
+ }
2386
+ case "unary": {
2387
+ const arg = this.renderConditionExpr(expr.argument);
2388
+ if (expr.op === "!") {
2389
+ return `not ${arg}`;
2390
+ }
2391
+ if (expr.op === "-") {
2392
+ return `bf_neg ${arg}`;
2393
+ }
2394
+ return arg;
2395
+ }
2396
+ case "logical": {
2397
+ const left = this.renderConditionExpr(expr.left);
2398
+ const right = this.renderConditionExpr(expr.right);
2399
+ const wrapLeft = this.needsParens(expr.left) ? `(${left})` : left;
2400
+ const wrapRight = this.needsParens(expr.right) ? `(${right})` : right;
2401
+ if (expr.op === "&&") {
2402
+ return `and ${wrapLeft} ${wrapRight}`;
2403
+ }
2404
+ return `or ${wrapLeft} ${wrapRight}`;
2405
+ }
2406
+ case "conditional": {
2407
+ const test = this.renderConditionExpr(expr.test);
2408
+ return test;
2409
+ }
2410
+ case "template-literal":
2411
+ return this.renderParsedExpr(expr);
2412
+ case "arrow-fn":
2413
+ return "[ARROW-FN]";
2414
+ case "higher-order":
2415
+ return this.renderParsedExpr(expr);
2416
+ case "array-literal":
2417
+ return this.renderParsedExpr(expr);
2418
+ case "array-method":
2419
+ return this.renderParsedExpr(expr);
2420
+ case "unsupported":
2421
+ return expr.raw;
2422
+ }
2423
+ }
2424
+ renderLoop(loop) {
2425
+ if (loop.clientOnly) {
2426
+ return `{{bfComment "loop:${loop.markerId}"}}{{bfComment "/loop:${loop.markerId}"}}`;
2427
+ }
2428
+ if (loop.paramBindings && loop.paramBindings.length > 0) {
2429
+ this.errors.push({
2430
+ code: "BF104",
2431
+ severity: "error",
2432
+ message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Go template adapter cannot lower — Go's \`{{range}}\` only supports single-name bindings.`,
2433
+ loc: loop.loc ?? this.makeLoc(),
2434
+ suggestion: {
2435
+ message: `Options:
2436
+ ` + ` 1. Rename the parameter to a single name and access tuple elements with index syntax in the body (e.g. \`entry => entry[0]\` instead of \`([k, v]) => ...\`).
2437
+ ` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
2438
+ ` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
2439
+ }
2440
+ });
2441
+ }
2442
+ let goArray = this.convertExpressionToGo(loop.array);
2443
+ const param = loop.param;
2444
+ const index = loop.index || "_";
2445
+ const childComponent = this.findChildComponent(loop.children);
2446
+ if (childComponent) {
2447
+ goArray = `.${childComponent.name}s`;
2448
+ }
2449
+ this.inLoop = true;
2450
+ this.loopParamStack.push(param);
2451
+ const children = this.renderChildren(loop.children);
2452
+ this.loopParamStack.pop();
2453
+ this.inLoop = false;
2454
+ if (loop.sortComparator) {
2455
+ goArray = `(${emitBfSort(goArray, loop.sortComparator)})`;
2456
+ }
2457
+ if (loop.filterPredicate) {
2458
+ let filterCond;
2459
+ if (loop.filterPredicate.blockBody) {
2460
+ filterCond = this.renderBlockBodyCondition(loop.filterPredicate.blockBody, loop.filterPredicate.param);
2461
+ } else if (loop.filterPredicate.predicate) {
2462
+ filterCond = this.renderPredicateCondition(loop.filterPredicate.predicate, loop.filterPredicate.param);
2463
+ } else {
2464
+ filterCond = "true";
2465
+ }
2466
+ const itemMarker2 = loop.bodyIsMultiRoot ? `{{bfComment "bf-loop-i"}}` : "";
2467
+ return `{{bfComment "loop:${loop.markerId}"}}{{range $${index}, $${param} := ${goArray}}}{{if ${filterCond}}}${itemMarker2}${children}{{end}}{{end}}{{bfComment "/loop:${loop.markerId}"}}`;
2468
+ }
2469
+ const itemMarker = loop.bodyIsMultiRoot ? `{{bfComment "bf-loop-i"}}` : "";
2470
+ return `{{bfComment "loop:${loop.markerId}"}}{{range $${index}, $${param} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`;
2471
+ }
2472
+ findChildComponent(nodes) {
2473
+ for (const node of nodes) {
2474
+ if (node.type === "component") {
2475
+ return node;
2476
+ }
2477
+ if (node.type === "element" && node.children) {
2478
+ const found = this.findChildComponent(node.children);
2479
+ if (found)
2480
+ return found;
2481
+ }
2482
+ if (node.type === "fragment" && node.children) {
2483
+ const found = this.findChildComponent(node.children);
2484
+ if (found)
2485
+ return found;
2486
+ }
2487
+ }
2488
+ return null;
2489
+ }
2490
+ renderComponent(comp, ctx) {
2491
+ if (comp.name === "Portal") {
2492
+ return this.renderPortalComponent(comp);
2493
+ }
2494
+ let templateCall;
2495
+ if (this.inLoop) {
2496
+ templateCall = `{{template "${comp.name}" .}}`;
2497
+ } else if (comp.slotId) {
2498
+ const suffix = slotIdToFieldSuffix(comp.slotId);
2499
+ templateCall = `{{template "${comp.name}" .${comp.name}${suffix}}}`;
2500
+ } else {
2501
+ templateCall = `{{template "${comp.name}" .${comp.name}}}`;
2502
+ }
2503
+ if (ctx?.isRootOfClientComponent) {
2504
+ return `{{bfScopeComment .}}${templateCall}`;
2505
+ }
2506
+ return templateCall;
2507
+ }
2508
+ renderPortalComponent(comp) {
2509
+ const children = this.renderChildren(comp.children);
2510
+ const escapedContent = children.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
2511
+ if (children.includes("{{")) {
2512
+ return `{{.Portals.Add .ScopeID (bfPortalHTML . "${escapedContent}")}}`;
2513
+ }
2514
+ return `{{.Portals.Add .ScopeID "${escapedContent}"}}`;
2515
+ }
2516
+ renderFragment(fragment) {
2517
+ const children = this.renderChildren(fragment.children);
2518
+ if (fragment.needsScopeComment) {
2519
+ return `{{bfScopeComment .}}${children}`;
2520
+ }
2521
+ return children;
2522
+ }
2523
+ renderSlot(slot) {
2524
+ const slotName = slot.name === "default" ? "children" : slot.name;
2525
+ return `{{block "${slotName}" .}}{{end}}`;
2526
+ }
2527
+ renderAsync(node) {
2528
+ const fallback = this.renderNode(node.fallback);
2529
+ const children = this.renderChildren(node.children);
2530
+ return `{{bfAsyncBoundary "${node.id}" "${this.escapeGoString(fallback)}"}}
2531
+ ${children}`;
2532
+ }
2533
+ escapeGoString(s) {
2534
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2535
+ }
2536
+ elementAttrEmitter = {
2537
+ emitLiteral: (value, name) => `${name}="${value.value}"`,
2538
+ emitExpression: (value, name) => {
2539
+ if (isBooleanAttr(name) || value.presenceOrUndefined) {
2540
+ const { condition: goCond, preamble } = this.convertConditionToGo(value.expr);
2541
+ return `${preamble}{{if ${goCond}}}${name}{{end}}`;
2542
+ }
2543
+ const parsed = parseExpression(value.expr.trim());
2544
+ if (parsed.kind === "conditional" || parsed.kind === "template-literal") {
2545
+ return `${name}="${this.renderParsedExpr(parsed)}"`;
2546
+ }
2547
+ return `${name}="{{${this.convertExpressionToGo(value.expr)}}}"`;
2548
+ },
2549
+ emitBooleanAttr: (_value, name) => name,
2550
+ emitSpread: (value) => {
2551
+ if (!value.slotId) {
2552
+ this.errors.push({
2553
+ code: "BF101",
2554
+ severity: "error",
2555
+ message: `JSX spread '{...${value.expr}}' on an intrinsic element has no Go template lowering (missing slot id)`,
2556
+ loc: this.makeLoc(),
2557
+ suggestion: {
2558
+ message: "This usually means a closed-type rest-prop spread was unexpectedly routed through the bag path — file a bug with the source."
2559
+ }
2560
+ });
2561
+ return "";
2562
+ }
2563
+ if (this.inLoop) {
2564
+ const trimmed = value.expr.trim();
2565
+ const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2566
+ if (currentLoopParam && trimmed === currentLoopParam) {
2567
+ return `{{bf_spread_attrs .}}`;
2568
+ }
2569
+ const goExpr = this.convertExpressionToGo(value.expr);
2570
+ return `{{bf_spread_attrs ${goExpr}}}`;
2571
+ }
2572
+ return `{{bf_spread_attrs .${value.slotId}}}`;
2573
+ },
2574
+ emitTemplate: (value, name) => `${name}="${this.renderTemplateLiteralParts(value.parts)}"`,
2575
+ emitBooleanShorthand: () => "",
2576
+ emitJsxChildren: () => ""
2577
+ };
2578
+ renderAttributes(element) {
2579
+ const parts = [];
2580
+ for (const attr of element.attrs) {
2581
+ let attrName;
2582
+ if (attr.name === "className")
2583
+ attrName = "class";
2584
+ else if (attr.name === "key")
2585
+ attrName = "data-key";
2586
+ else
2587
+ attrName = attr.name;
2588
+ const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
2589
+ if (lowered)
2590
+ parts.push(lowered);
2591
+ }
2592
+ return parts.length > 0 ? " " + parts.join(" ") : "";
2593
+ }
2594
+ substituteJsInterpolations(s) {
2595
+ let out = "";
2596
+ let i = 0;
2597
+ while (i < s.length) {
2598
+ const open = s.indexOf("${", i);
2599
+ if (open === -1) {
2600
+ out += this.escapeAttrText(s.slice(i));
2601
+ break;
2602
+ }
2603
+ out += this.escapeAttrText(s.slice(i, open));
2604
+ const close = findInterpolationEnd(s, open + 2);
2605
+ if (close === -1) {
2606
+ out += this.escapeAttrText(s.slice(open));
2607
+ break;
2608
+ }
2609
+ const inner = s.slice(open + 2, close).trim();
2610
+ if (inner) {
2611
+ out += `{{${this.convertExpressionToGo(inner)}}}`;
2612
+ } else {
2613
+ out += s.slice(open, close + 1);
2614
+ }
2615
+ i = close + 1;
2616
+ }
2617
+ return out;
2618
+ }
2619
+ escapeAttrText(s) {
2620
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2621
+ }
2622
+ renderTemplateLiteralParts(parts) {
2623
+ let output = "";
2624
+ for (const part of parts) {
2625
+ if (part.type === "string") {
2626
+ output += this.substituteJsInterpolations(part.value);
2627
+ } else if (part.type === "ternary") {
2628
+ const { condition: goCond, preamble } = this.convertConditionToGo(part.condition);
2629
+ output += `${preamble}{{if ${goCond}}}${part.whenTrue}{{else}}${part.whenFalse}{{end}}`;
2630
+ } else if (part.type === "lookup") {
2631
+ const keyExpr = this.convertExpressionToGo(part.key);
2632
+ const caseEntries = Object.entries(part.cases);
2633
+ if (caseEntries.length === 0)
2634
+ continue;
2635
+ const branches = caseEntries.map(([k, v], i) => {
2636
+ const head = i === 0 ? "{{if" : "{{else if";
2637
+ return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${v}`;
2638
+ });
2639
+ output += branches.join("") + "{{end}}";
2640
+ }
2641
+ }
2642
+ return output;
2643
+ }
2644
+ renderScopeMarker(_instanceIdExpr) {
2645
+ return `bf-s="{{bfScopeAttr .}}" {{bfHydrationAttrs .}} {{bfPropsAttr .}}`;
2646
+ }
2647
+ renderSlotMarker(slotId) {
2648
+ return `bf="${slotId}"`;
2649
+ }
2650
+ renderCondMarker(condId) {
2651
+ return `bf-c="${condId}"`;
2652
+ }
2653
+ wrapWithCondMarker(content, condId) {
2654
+ if (content.startsWith("<")) {
2655
+ const match = content.match(/^<(\w+)/);
2656
+ if (match) {
2657
+ const tag = match[1];
2658
+ const trimmed = content.trim();
2659
+ const isSingle = new RegExp(`</${tag}>\\s*$`).test(trimmed) || /^<\w+[^>]*\/>$/.test(trimmed);
2660
+ if (isSingle) {
2661
+ return content.replace(`<${match[1]}`, `<${match[1]} ${this.renderCondMarker(condId)}`);
2662
+ }
2663
+ }
2664
+ }
2665
+ return `{{bfComment "cond-start:${condId}"}}${content}{{bfComment "cond-end:${condId}"}}`;
2666
+ }
2667
+ }
2668
+ var goTemplateAdapter = new GoTemplateAdapter;
2669
+ export {
2670
+ goTemplateAdapter,
2671
+ GoTemplateAdapter
2672
+ };