@barefootjs/go-template 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -83,6 +83,7 @@ class GoTemplateAdapter extends BaseAdapter {
83
83
  errors = [];
84
84
  propsObjectName = null;
85
85
  restPropsName = null;
86
+ templateVarCounter = 0;
86
87
  localTypeNames = new Set;
87
88
  localTypeAliases = new Map;
88
89
  usesHtmlTemplate = false;
@@ -97,6 +98,7 @@ class GoTemplateAdapter extends BaseAdapter {
97
98
  generate(ir, options) {
98
99
  this.componentName = ir.metadata.componentName;
99
100
  this.errors = [];
101
+ this.templateVarCounter = 0;
100
102
  this.propsObjectName = ir.metadata.propsObjectName;
101
103
  this.restPropsName = ir.metadata.restPropsName ?? null;
102
104
  if (!options?.siblingTemplatesRegistered) {
@@ -436,7 +438,7 @@ ${goFields.join(`
436
438
  lines.push("\tScopeID string // Optional: if empty, random ID is generated");
437
439
  lines.push("\tBfParent string // Optional: parent scope id");
438
440
  lines.push("\tBfMount string // Optional: slot id in parent");
439
- const staticNested = nestedComponents.filter((n) => !n.isDynamic);
441
+ const inputNested = nestedComponents.filter((n) => !n.isDynamic || n.isPropDerived);
440
442
  const nestedArrayFields = new Set(nestedComponents.map((n) => `${n.name}s`));
441
443
  for (const param of ir.metadata.propsParams) {
442
444
  const fieldName = this.capitalizeFieldName(param.name);
@@ -445,7 +447,7 @@ ${goFields.join(`
445
447
  const goType = propTypeOverrides.get(param.name) ?? this.typeInfoToGo(param.type, param.defaultValue);
446
448
  lines.push(` ${fieldName} ${goType}`);
447
449
  }
448
- for (const nested of staticNested) {
450
+ for (const nested of inputNested) {
449
451
  lines.push(` ${nested.name}s []${nested.name}Input`);
450
452
  }
451
453
  const restPropsName = ir.metadata.restPropsName;
@@ -521,7 +523,7 @@ ${goFields.join(`
521
523
  lines.push(` ${fieldName} ${goType} \`json:"${jsonTag}"\``);
522
524
  }
523
525
  for (const nested of nestedComponents) {
524
- if (nested.isDynamic) {
526
+ if (nested.isDynamic && !nested.isPropDerived) {
525
527
  lines.push(` ${nested.name}s []${nested.name}Props \`json:"-"\``);
526
528
  } else {
527
529
  const jsonTag = this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`);
@@ -542,9 +544,9 @@ ${goFields.join(`
542
544
  generateNewPropsFunction(lines, ir, componentName, nestedComponents, spreadSlots) {
543
545
  const inputTypeName = `${componentName}Input`;
544
546
  const propsTypeName = `${componentName}Props`;
545
- const dynamicNested = nestedComponents.filter((n) => n.isDynamic);
547
+ const signalDynamicNested = nestedComponents.filter((n) => n.isDynamic && !n.isPropDerived);
546
548
  lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`);
547
- for (const nested of dynamicNested) {
549
+ for (const nested of signalDynamicNested) {
548
550
  const arrayField = `${nested.name}s`;
549
551
  lines.push(`//`);
550
552
  lines.push(`// NOTE: \`${arrayField}\` is populated by the route handler, not by`);
@@ -566,7 +568,7 @@ ${goFields.join(`
566
568
  lines.push(` scopeID = "${componentName}_" + randomID(6)`);
567
569
  lines.push("\t}");
568
570
  lines.push("");
569
- const staticNested = nestedComponents.filter((n) => !n.isDynamic);
571
+ const staticNested = nestedComponents.filter((n) => !n.isDynamic || n.isPropDerived);
570
572
  if (staticNested.length > 0) {
571
573
  for (const nested of staticNested) {
572
574
  const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`;
@@ -714,7 +716,8 @@ ${goFields.join(`
714
716
  if (!result.some((c) => c.name === loop.childComponent.name)) {
715
717
  result.push({
716
718
  ...loop.childComponent,
717
- isDynamic: !loop.isStaticArray
719
+ isDynamic: !loop.isStaticArray,
720
+ isPropDerived: !!loop.isPropDerivedArray
718
721
  });
719
722
  }
720
723
  }
@@ -1537,7 +1540,7 @@ ${goFields.join(`
1537
1540
  if (literalType === "string")
1538
1541
  return `"${value}"`;
1539
1542
  if (literalType === "null")
1540
- return '""';
1543
+ return "nil";
1541
1544
  return String(value);
1542
1545
  }
1543
1546
  call(callee, args, emit) {
@@ -1573,7 +1576,7 @@ ${goFields.join(`
1573
1576
  if (result)
1574
1577
  return result;
1575
1578
  }
1576
- if (object.kind === "higher-order" && object.method === "find") {
1579
+ if (object.kind === "higher-order" && (object.method === "find" || object.method === "findLast")) {
1577
1580
  const findResult = this.renderHigherOrderExpr(object, emit);
1578
1581
  if (findResult) {
1579
1582
  return `{{with ${findResult}}}{{.${this.capitalizeFieldName(property)}}}{{end}}`;
@@ -1677,7 +1680,7 @@ ${goFields.join(`
1677
1680
  const result = this.renderHigherOrderExpr(reconstructed, emit);
1678
1681
  if (result)
1679
1682
  return result;
1680
- if (method === "find" || method === "findIndex") {
1683
+ if (method === "find" || method === "findIndex" || method === "findLast" || method === "findLastIndex") {
1681
1684
  const templateBlock = this.renderFindTemplateBlock(reconstructed, emit);
1682
1685
  if (templateBlock)
1683
1686
  return templateBlock;
@@ -1817,12 +1820,17 @@ ${goFields.join(`
1817
1820
  const value = negated ? "false" : "true";
1818
1821
  return `bf_filter ${arrayExpr} "${field}" ${value}`;
1819
1822
  }
1820
- if (expr.method === "find" || expr.method === "findIndex") {
1823
+ if (expr.method === "find" || expr.method === "findIndex" || expr.method === "findLast" || expr.method === "findLastIndex") {
1821
1824
  const eqPred = this.extractEqualityPredicate(expr.predicate, expr.param, (e) => this.renderParsedExpr(e));
1822
1825
  if (!eqPred)
1823
1826
  return null;
1824
- const func = expr.method === "find" ? "bf_find" : "bf_find_index";
1825
- return `${func} ${arrayExpr} "${eqPred.field}" ${eqPred.value}`;
1827
+ const funcMap = {
1828
+ find: "bf_find",
1829
+ findIndex: "bf_find_index",
1830
+ findLast: "bf_find_last",
1831
+ findLastIndex: "bf_find_last_index"
1832
+ };
1833
+ return `${funcMap[expr.method]} ${arrayExpr} "${eqPred.field}" ${eqPred.value}`;
1826
1834
  }
1827
1835
  return null;
1828
1836
  }
@@ -1838,6 +1846,15 @@ ${goFields.join(`
1838
1846
  if (expr.method === "findIndex") {
1839
1847
  return `{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{$i}}{{break}}{{end}}{{end}}`;
1840
1848
  }
1849
+ if (expr.method === "findLast") {
1850
+ const v = `$bf_r${this.templateVarCounter++}`;
1851
+ const capture = propertyAccess ? `.${propertyAccess}` : ".";
1852
+ return `{{${v} := ""}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = ${capture}}}{{end}}{{end}}{{${v}}}`;
1853
+ }
1854
+ if (expr.method === "findLastIndex") {
1855
+ const v = `$bf_r${this.templateVarCounter++}`;
1856
+ return `{{${v} := -1}}{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{${v} = $i}}{{end}}{{end}}{{${v}}}`;
1857
+ }
1841
1858
  return null;
1842
1859
  }
1843
1860
  renderEverySomeTemplateBlock(expr, renderArray) {
@@ -1846,11 +1863,13 @@ ${goFields.join(`
1846
1863
  if (condition.includes("[UNSUPPORTED"))
1847
1864
  return null;
1848
1865
  if (expr.method === "every") {
1866
+ const v = `$bf_r${this.templateVarCounter++}`;
1849
1867
  const negated = this.negateGoCondition(condition);
1850
- return `{{$bf_result := true}}{{range ${arrayExpr}}}{{if ${negated}}}{{$bf_result = false}}{{break}}{{end}}{{end}}{{$bf_result}}`;
1868
+ return `{{${v} := true}}{{range ${arrayExpr}}}{{if ${negated}}}{{${v} = false}}{{break}}{{end}}{{end}}{{${v}}}`;
1851
1869
  }
1852
1870
  if (expr.method === "some") {
1853
- return `{{$bf_result := false}}{{range ${arrayExpr}}}{{if ${condition}}}{{$bf_result = true}}{{break}}{{end}}{{end}}{{$bf_result}}`;
1871
+ const v = `$bf_r${this.templateVarCounter++}`;
1872
+ return `{{${v} := false}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = true}}{{break}}{{end}}{{end}}{{${v}}}`;
1854
1873
  }
1855
1874
  return null;
1856
1875
  }
@@ -1879,6 +1898,22 @@ ${goFields.join(`
1879
1898
  needsParens(expr) {
1880
1899
  return expr.kind === "logical" || expr.kind === "unary" || expr.kind === "conditional";
1881
1900
  }
1901
+ splitPreamble(rendered) {
1902
+ if (!rendered.includes("{{"))
1903
+ return null;
1904
+ const lastOpen = rendered.lastIndexOf("{{");
1905
+ const lastClose = rendered.lastIndexOf("}}");
1906
+ if (lastOpen >= 0 && lastClose > lastOpen) {
1907
+ const candidate = rendered.substring(lastOpen + 2, lastClose);
1908
+ if (!candidate.startsWith("$"))
1909
+ return null;
1910
+ return {
1911
+ preamble: rendered.substring(0, lastOpen),
1912
+ expr: candidate
1913
+ };
1914
+ }
1915
+ return null;
1916
+ }
1882
1917
  renderBlockBodyCondition(statements, param) {
1883
1918
  const localVarMap = new Map;
1884
1919
  const paths = this.collectReturnPaths(statements, [], localVarMap, param);
@@ -1999,7 +2034,7 @@ ${goFields.join(`
1999
2034
  return `"${expr.value}"`;
2000
2035
  }
2001
2036
  if (expr.literalType === "null") {
2002
- return '""';
2037
+ return "nil";
2003
2038
  }
2004
2039
  return String(expr.value);
2005
2040
  case "member": {
@@ -2276,138 +2311,138 @@ Options:
2276
2311
  });
2277
2312
  return { condition: `false`, preamble: "" };
2278
2313
  }
2279
- const rendered = this.renderConditionExpr(parsed);
2280
- if (rendered.startsWith("{{")) {
2281
- const lastOpen = rendered.lastIndexOf("{{");
2282
- const lastClose = rendered.lastIndexOf("}}");
2283
- if (lastOpen >= 0 && lastClose > lastOpen) {
2284
- const preamble = rendered.substring(0, lastOpen);
2285
- const condition = rendered.substring(lastOpen + 2, lastClose);
2286
- return { condition, preamble };
2287
- }
2288
- }
2289
- return { condition: rendered, preamble: "" };
2314
+ const { preamble, expr: condition } = this.renderConditionExpr(parsed);
2315
+ return { condition, preamble };
2290
2316
  }
2291
2317
  renderConditionExpr(expr) {
2318
+ const plain = (e) => ({ preamble: "", expr: e });
2292
2319
  switch (expr.kind) {
2293
2320
  case "identifier":
2294
2321
  {
2295
2322
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2296
2323
  if (currentLoopParam && expr.name === currentLoopParam) {
2297
- return ".";
2324
+ return plain(".");
2298
2325
  }
2299
2326
  }
2300
- return `.${this.capitalizeFieldName(expr.name)}`;
2327
+ return plain(`.${this.capitalizeFieldName(expr.name)}`);
2301
2328
  case "literal":
2302
- if (expr.literalType === "string") {
2303
- return `"${expr.value}"`;
2304
- }
2305
- if (expr.literalType === "null") {
2306
- return '""';
2307
- }
2308
- return String(expr.value);
2329
+ if (expr.literalType === "string")
2330
+ return plain(`"${expr.value}"`);
2331
+ if (expr.literalType === "null")
2332
+ return plain("nil");
2333
+ return plain(String(expr.value));
2309
2334
  case "call": {
2310
2335
  if (expr.callee.kind === "identifier" && expr.args.length === 0) {
2311
- return `.${this.capitalizeFieldName(expr.callee.name)}`;
2336
+ return plain(`.${this.capitalizeFieldName(expr.callee.name)}`);
2312
2337
  }
2313
- return this.renderParsedExpr(expr);
2338
+ return plain(this.renderParsedExpr(expr));
2314
2339
  }
2315
2340
  case "member": {
2316
2341
  if (expr.property === "length" && expr.object.kind === "higher-order") {
2317
- const result = this.renderFilterLengthExpr(expr.object, (e) => this.renderConditionExpr(e));
2318
- if (result) {
2319
- return result;
2320
- }
2342
+ const result = this.renderFilterLengthExpr(expr.object, (e) => this.renderConditionExpr(e).expr);
2343
+ if (result)
2344
+ return plain(result);
2321
2345
  }
2322
2346
  if (expr.object.kind === "identifier" && this.propsObjectName && expr.object.name === this.propsObjectName) {
2323
- return `.${this.capitalizeFieldName(expr.property)}`;
2347
+ return plain(`.${this.capitalizeFieldName(expr.property)}`);
2324
2348
  }
2325
2349
  {
2326
2350
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
2327
2351
  if (expr.object.kind === "identifier" && currentLoopParam && expr.object.name === currentLoopParam) {
2328
- return `.${this.capitalizeFieldName(expr.property)}`;
2352
+ return plain(`.${this.capitalizeFieldName(expr.property)}`);
2329
2353
  }
2330
2354
  }
2331
2355
  const obj = this.renderConditionExpr(expr.object);
2332
2356
  if (expr.property === "length") {
2333
- return `len ${obj}`;
2357
+ return { preamble: obj.preamble, expr: `len ${obj.expr}` };
2334
2358
  }
2335
- return `${obj}.${this.capitalizeFieldName(expr.property)}`;
2359
+ return { preamble: obj.preamble, expr: `${obj.expr}.${this.capitalizeFieldName(expr.property)}` };
2336
2360
  }
2337
2361
  case "binary": {
2338
2362
  const leftNeedsParens = this.needsParensInGoTemplate(expr.left);
2339
- let left = this.renderConditionExpr(expr.left);
2340
- if (leftNeedsParens) {
2341
- left = `(${left})`;
2342
- }
2363
+ const leftResult = this.renderConditionExpr(expr.left);
2364
+ const left = leftNeedsParens ? `(${leftResult.expr})` : leftResult.expr;
2343
2365
  const rightNeedsParens = this.needsParensInGoTemplate(expr.right);
2344
- let right = this.renderConditionExpr(expr.right);
2345
- if (rightNeedsParens) {
2346
- right = `(${right})`;
2347
- }
2366
+ const rightResult = this.renderConditionExpr(expr.right);
2367
+ const right = rightNeedsParens ? `(${rightResult.expr})` : rightResult.expr;
2368
+ const preamble = leftResult.preamble + rightResult.preamble;
2369
+ let result;
2348
2370
  switch (expr.op) {
2349
2371
  case "===":
2350
2372
  case "==":
2351
- return `eq ${left} ${right}`;
2373
+ result = `eq ${left} ${right}`;
2374
+ break;
2352
2375
  case "!==":
2353
2376
  case "!=":
2354
- return `ne ${left} ${right}`;
2377
+ result = `ne ${left} ${right}`;
2378
+ break;
2355
2379
  case ">":
2356
- return `gt ${left} ${right}`;
2380
+ result = `gt ${left} ${right}`;
2381
+ break;
2357
2382
  case "<":
2358
- return `lt ${left} ${right}`;
2383
+ result = `lt ${left} ${right}`;
2384
+ break;
2359
2385
  case ">=":
2360
- return `ge ${left} ${right}`;
2386
+ result = `ge ${left} ${right}`;
2387
+ break;
2361
2388
  case "<=":
2362
- return `le ${left} ${right}`;
2389
+ result = `le ${left} ${right}`;
2390
+ break;
2363
2391
  case "+":
2364
- return `bf_add ${left} ${right}`;
2392
+ result = `bf_add ${left} ${right}`;
2393
+ break;
2365
2394
  case "-":
2366
- return `bf_sub ${left} ${right}`;
2395
+ result = `bf_sub ${left} ${right}`;
2396
+ break;
2367
2397
  case "*":
2368
- return `bf_mul ${left} ${right}`;
2398
+ result = `bf_mul ${left} ${right}`;
2399
+ break;
2369
2400
  case "/":
2370
- return `bf_div ${left} ${right}`;
2401
+ result = `bf_div ${left} ${right}`;
2402
+ break;
2371
2403
  default:
2372
- return `${left} ${expr.op} ${right}`;
2404
+ result = `${left} ${expr.op} ${right}`;
2373
2405
  }
2406
+ return { preamble, expr: result };
2374
2407
  }
2375
2408
  case "unary": {
2376
2409
  const arg = this.renderConditionExpr(expr.argument);
2377
- if (expr.op === "!") {
2378
- return `not ${arg}`;
2379
- }
2380
- if (expr.op === "-") {
2381
- return `bf_neg ${arg}`;
2382
- }
2410
+ if (expr.op === "!")
2411
+ return { preamble: arg.preamble, expr: `not ${arg.expr}` };
2412
+ if (expr.op === "-")
2413
+ return { preamble: arg.preamble, expr: `bf_neg ${arg.expr}` };
2383
2414
  return arg;
2384
2415
  }
2385
2416
  case "logical": {
2386
- const left = this.renderConditionExpr(expr.left);
2387
- const right = this.renderConditionExpr(expr.right);
2388
- const wrapLeft = this.needsParens(expr.left) ? `(${left})` : left;
2389
- const wrapRight = this.needsParens(expr.right) ? `(${right})` : right;
2390
- if (expr.op === "&&") {
2391
- return `and ${wrapLeft} ${wrapRight}`;
2392
- }
2393
- return `or ${wrapLeft} ${wrapRight}`;
2417
+ const leftResult = this.renderConditionExpr(expr.left);
2418
+ const rightResult = this.renderConditionExpr(expr.right);
2419
+ const preamble = leftResult.preamble + rightResult.preamble;
2420
+ const wrapLeft = this.needsParens(expr.left) ? `(${leftResult.expr})` : leftResult.expr;
2421
+ const wrapRight = this.needsParens(expr.right) ? `(${rightResult.expr})` : rightResult.expr;
2422
+ const result = expr.op === "&&" ? `and ${wrapLeft} ${wrapRight}` : `or ${wrapLeft} ${wrapRight}`;
2423
+ return { preamble, expr: result };
2394
2424
  }
2395
2425
  case "conditional": {
2396
2426
  const test = this.renderConditionExpr(expr.test);
2397
2427
  return test;
2398
2428
  }
2399
2429
  case "template-literal":
2400
- return this.renderParsedExpr(expr);
2430
+ return plain(this.renderParsedExpr(expr));
2401
2431
  case "arrow-fn":
2402
- return "[ARROW-FN]";
2403
- case "higher-order":
2404
- return this.renderParsedExpr(expr);
2432
+ return plain("[ARROW-FN]");
2433
+ case "higher-order": {
2434
+ const rendered = this.renderParsedExpr(expr);
2435
+ const split = this.splitPreamble(rendered);
2436
+ if (split)
2437
+ return split;
2438
+ return plain(rendered);
2439
+ }
2405
2440
  case "array-literal":
2406
- return this.renderParsedExpr(expr);
2441
+ return plain(this.renderParsedExpr(expr));
2407
2442
  case "array-method":
2408
- return this.renderParsedExpr(expr);
2443
+ return plain(this.renderParsedExpr(expr));
2409
2444
  case "unsupported":
2410
- return expr.raw;
2445
+ return plain(expr.raw);
2411
2446
  }
2412
2447
  }
2413
2448
  renderLoop(loop) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/go-template",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,11 +49,11 @@
49
49
  "directory": "packages/adapter-go-template"
50
50
  },
51
51
  "peerDependencies": {
52
- "@barefootjs/jsx": "0.1.1",
52
+ "@barefootjs/jsx": "0.1.3",
53
53
  "typescript": "^5.0.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@barefootjs/adapter-tests": "0.1.0",
57
- "@barefootjs/jsx": "0.1.1"
57
+ "@barefootjs/jsx": "0.1.3"
58
58
  }
59
59
  }
@@ -22,14 +22,6 @@ runAdapterConformanceTests({
22
22
  // `<!--bf-cond-start:sN-->` / `<!--bf-cond-end:sN-->` comment pairs)
23
23
  // is now collapsed by `normalizeHTML` in adapter-tests (#1266).
24
24
  //
25
- // `nullish-coalescing-jsx` / `return-nullish-coalescing` have a
26
- // separate semantic divergence: the Go template's `{{if ne .Banner
27
- // ""}}` condition treats an unset `Banner` (Go nil) as `!= ""` and
28
- // takes the truthy branch with empty content, while Hono's JS
29
- // `??` operator falls through to the JSX default. That's a Go-
30
- // adapter branch-selection bug — fixing it is out of scope for
31
- // #1266.
32
- //
33
25
  // `return-map` uses a `data-key` serialisation shape that differs
34
26
  // between Hono (runtime helper) and Go (template variable) in a
35
27
  // way that isn't structural — leaving it on `skipJsx` until a
@@ -43,8 +35,6 @@ runAdapterConformanceTests({
43
35
  // `BF104` at build time instead of silently emitting invalid
44
36
  // template syntax (#1266).
45
37
  skipJsx: [
46
- 'nullish-coalescing-jsx',
47
- 'return-nullish-coalescing',
48
38
  'return-map',
49
39
  // #1297 fixed the harness-side IR emission gate (multi-component
50
40
  // sources now emit one `ir` file per component, and the harness
@@ -1034,6 +1024,116 @@ export function ItemChecker() {
1034
1024
  })
1035
1025
  })
1036
1026
 
1027
+ describe('findLast/findLastIndex - adapter specific', () => {
1028
+ test('renders findLast() with equality predicate via bf_find_last', () => {
1029
+ const result = compileAndGenerate(`
1030
+ "use client"
1031
+ import { createSignal } from "@barefootjs/client"
1032
+
1033
+ type Item = { name: string; done: boolean }
1034
+
1035
+ export function ItemChecker() {
1036
+ const [items, setItems] = createSignal<Item[]>([])
1037
+ return <div>{items().findLast(t => t.done) ? 'Found' : 'Not found'}</div>
1038
+ }
1039
+ `)
1040
+ expect(result.template).toContain('bf_find_last .Items "Done" true')
1041
+ expect(result.template).toContain('Found')
1042
+ })
1043
+
1044
+ test('renders findLast() with complex predicate via range without break', () => {
1045
+ const result = compileAndGenerate(`
1046
+ "use client"
1047
+ import { createSignal } from "@barefootjs/client"
1048
+
1049
+ type Item = { price: number; category: string }
1050
+
1051
+ export function ItemFinder() {
1052
+ const [items, setItems] = createSignal<Item[]>([])
1053
+ const [type, setType] = createSignal('')
1054
+ return <div>{items().findLast(t => t.price > 100 && t.category === type())}</div>
1055
+ }
1056
+ `)
1057
+ expect(result.template).toContain('{{range')
1058
+ expect(result.template).toContain('$bf_r')
1059
+ expect(result.template).not.toContain('{{break}}')
1060
+ })
1061
+
1062
+ test('renders findLastIndex() with equality predicate via bf_find_last_index', () => {
1063
+ const result = compileAndGenerate(`
1064
+ "use client"
1065
+ import { createSignal } from "@barefootjs/client"
1066
+
1067
+ type Item = { name: string; done: boolean }
1068
+
1069
+ export function ItemChecker() {
1070
+ const [items, setItems] = createSignal<Item[]>([])
1071
+ return <div>idx: {items().findLastIndex(t => t.done)}</div>
1072
+ }
1073
+ `)
1074
+ expect(result.template).toContain('bf_find_last_index .Items "Done" true')
1075
+ })
1076
+
1077
+ test('renders findLastIndex() with complex predicate via range', () => {
1078
+ const result = compileAndGenerate(`
1079
+ "use client"
1080
+ import { createSignal } from "@barefootjs/client"
1081
+
1082
+ type Item = { price: number; active: boolean }
1083
+
1084
+ export function ItemFinder() {
1085
+ const [items, setItems] = createSignal<Item[]>([])
1086
+ return <div>{items().findLastIndex(t => t.price > 50 && t.active)}</div>
1087
+ }
1088
+ `)
1089
+ const varMatch = result.template.match(/(\$bf_r\d+) := -1/)
1090
+ expect(varMatch).not.toBeNull()
1091
+ expect(result.template).toContain(`${varMatch![1]} = $i`)
1092
+ expect(result.template).not.toContain('{{break}}')
1093
+ })
1094
+
1095
+ test('findLast() complex predicate in IR-level ternary works via preamble splitting', () => {
1096
+ const adapter = new GoTemplateAdapter()
1097
+ const ir = compileToIR(`
1098
+ "use client"
1099
+ import { createSignal } from "@barefootjs/client"
1100
+
1101
+ type Item = { price: number; category: string }
1102
+
1103
+ export function ItemFinder() {
1104
+ const [items, setItems] = createSignal<Item[]>([])
1105
+ const [type, setType] = createSignal('')
1106
+ return <div>{items().findLast(t => t.price > 100 && t.category === type()) ? 'yes' : 'no'}</div>
1107
+ }
1108
+ `, adapter)
1109
+ const output = adapter.generate(ir)
1110
+ expect(adapter.errors.filter(e => e.code === 'BF101')).toEqual([])
1111
+ expect(output.template).toMatch(/\$bf_r\d+ := ""/)
1112
+ expect(output.template).toContain('yes')
1113
+ })
1114
+
1115
+ test('findLast() complex predicate in binary expression compiles via preamble hoisting', () => {
1116
+ const adapter = new GoTemplateAdapter()
1117
+ const ir = compileToIR(`
1118
+ "use client"
1119
+ import { createSignal } from "@barefootjs/client"
1120
+
1121
+ type Item = { price: number; category: string }
1122
+
1123
+ export function ItemFinder() {
1124
+ const [items, setItems] = createSignal<Item[]>([])
1125
+ const [type, setType] = createSignal('')
1126
+ return <div class={items().findLast(t => t.price > 100 && t.category === type()) === 'special' ? 'highlight' : 'normal'}>test</div>
1127
+ }
1128
+ `, adapter)
1129
+ const output = adapter.generate(ir)
1130
+ expect(adapter.errors.filter(e => e.code === 'BF101')).toEqual([])
1131
+ expect(output.template).toMatch(/\$bf_r\d+ := ""/)
1132
+ expect(output.template).toContain('eq')
1133
+ expect(output.template).toContain('"special"')
1134
+ })
1135
+ })
1136
+
1037
1137
  describe('component root scope comment propagation', () => {
1038
1138
  test('component root in client component outputs bfScopeComment', () => {
1039
1139
  const result = compileAndGenerate(`