@barefootjs/go-template 0.16.0 → 0.17.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.
Files changed (73) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +23 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +53 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/helper-inline.d.ts +31 -0
  6. package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
  7. package/dist/adapter/expr/url-builder.d.ts +23 -0
  8. package/dist/adapter/expr/url-builder.d.ts.map +1 -0
  9. package/dist/adapter/go-template-adapter.d.ts +291 -1070
  10. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  11. package/dist/adapter/index.js +2857 -2618
  12. package/dist/adapter/lib/compile-state.d.ts +116 -0
  13. package/dist/adapter/lib/compile-state.d.ts.map +1 -0
  14. package/dist/adapter/lib/constants.d.ts +11 -0
  15. package/dist/adapter/lib/constants.d.ts.map +1 -0
  16. package/dist/adapter/lib/go-emit.d.ts +117 -0
  17. package/dist/adapter/lib/go-emit.d.ts.map +1 -0
  18. package/dist/adapter/lib/go-naming.d.ts +39 -0
  19. package/dist/adapter/lib/go-naming.d.ts.map +1 -0
  20. package/dist/adapter/lib/ir-scope.d.ts +13 -0
  21. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  22. package/dist/adapter/lib/types.d.ts +165 -0
  23. package/dist/adapter/lib/types.d.ts.map +1 -0
  24. package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
  25. package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
  26. package/dist/adapter/memo/memo-compute.d.ts +124 -0
  27. package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
  28. package/dist/adapter/memo/memo-type.d.ts +38 -0
  29. package/dist/adapter/memo/memo-type.d.ts.map +1 -0
  30. package/dist/adapter/memo/memo-value.d.ts +64 -0
  31. package/dist/adapter/memo/memo-value.d.ts.map +1 -0
  32. package/dist/adapter/memo/template-interp.d.ts +43 -0
  33. package/dist/adapter/memo/template-interp.d.ts.map +1 -0
  34. package/dist/adapter/props/prop-types.d.ts +31 -0
  35. package/dist/adapter/props/prop-types.d.ts.map +1 -0
  36. package/dist/adapter/spread/spread-codegen.d.ts +40 -0
  37. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  38. package/dist/adapter/type/type-codegen.d.ts +25 -0
  39. package/dist/adapter/type/type-codegen.d.ts.map +1 -0
  40. package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
  41. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
  42. package/dist/adapter/value/value-lowering.d.ts +46 -0
  43. package/dist/adapter/value/value-lowering.d.ts.map +1 -0
  44. package/dist/build.js +2856 -2617
  45. package/dist/index.js +2857 -2618
  46. package/dist/test-render.d.ts.map +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/derived-state-memo.test.ts +155 -84
  49. package/src/__tests__/go-template-adapter.test.ts +118 -54
  50. package/src/__tests__/lowering-plugin.test.ts +109 -0
  51. package/src/__tests__/query-href.test.ts +179 -0
  52. package/src/adapter/analysis/component-tree.ts +174 -0
  53. package/src/adapter/emit-context.ts +59 -0
  54. package/src/adapter/expr/helper-inline.ts +274 -0
  55. package/src/adapter/expr/url-builder.ts +123 -0
  56. package/src/adapter/go-template-adapter.ts +2099 -5325
  57. package/src/adapter/lib/compile-state.ts +150 -0
  58. package/src/adapter/lib/constants.ts +24 -0
  59. package/src/adapter/lib/go-emit.ts +289 -0
  60. package/src/adapter/lib/go-naming.ts +84 -0
  61. package/src/adapter/lib/ir-scope.ts +31 -0
  62. package/src/adapter/lib/types.ts +182 -0
  63. package/src/adapter/memo/ctor-lowering.ts +267 -0
  64. package/src/adapter/memo/memo-compute.ts +451 -0
  65. package/src/adapter/memo/memo-type.ts +74 -0
  66. package/src/adapter/memo/memo-value.ts +197 -0
  67. package/src/adapter/memo/template-interp.ts +246 -0
  68. package/src/adapter/props/prop-types.ts +84 -0
  69. package/src/adapter/spread/spread-codegen.ts +458 -0
  70. package/src/adapter/type/type-codegen.ts +95 -0
  71. package/src/adapter/value/parsed-literal-to-go.ts +94 -0
  72. package/src/adapter/value/value-lowering.ts +162 -0
  73. package/src/test-render.ts +2 -14
@@ -226,8 +226,9 @@ describe('GoTemplateAdapter - searchParams() env-signal lowering (#1922)', () =>
226
226
  test('lowers searchParams().get(k) to .SearchParams.Get and emits the struct binding', () => {
227
227
  const adapter = new GoTemplateAdapter()
228
228
  const ir = compileToIR(`
229
- import { searchParams } from '@barefootjs/client'
229
+ import { createSearchParams } from '@barefootjs/client'
230
230
  export function SortLabel() {
231
+ const [searchParams] = createSearchParams()
231
232
  return <p>{searchParams().get('sort') ?? 'none'}</p>
232
233
  }
233
234
  `, adapter)
@@ -237,14 +238,15 @@ export function SortLabel() {
237
238
  expect(types).toContain('SearchParams: in.SearchParams')
238
239
  })
239
240
 
240
- // An aliased import binds the env signal to a different local name; the
241
- // call `sp()` still resolves to the canonical `.SearchParams` field (the
242
- // generated struct field name is fixed, not derived from the JS alias).
243
- test('aliased import (`searchParams as sp`) resolves sp() to canonical .SearchParams', () => {
241
+ // An aliased destructured getter binds the env signal to a different local
242
+ // name; the call `sp()` still resolves to the canonical `.SearchParams` field
243
+ // (the generated struct field name is fixed, not derived from the JS name).
244
+ test('aliased env-signal getter (`const [sp] = createSearchParams()`) resolves sp() to canonical .SearchParams', () => {
244
245
  const adapter = new GoTemplateAdapter()
245
246
  const ir = compileToIR(`
246
- import { searchParams as sp } from '@barefootjs/client'
247
+ import { createSearchParams } from '@barefootjs/client'
247
248
  export function SortLabel() {
249
+ const [sp] = createSearchParams()
248
250
  return <p>{sp().get('sort') ?? 'none'}</p>
249
251
  }
250
252
  `, adapter)
@@ -267,8 +269,9 @@ describe('GoTemplateAdapter - template-literal text lowering (#1933)', () => {
267
269
  const adapter = new GoTemplateAdapter()
268
270
  const ir = compileToIR(`
269
271
  'use client'
270
- import { createMemo, searchParams } from '@barefootjs/client'
272
+ import { createMemo, createSearchParams } from '@barefootjs/client'
271
273
  export function StatusLine() {
274
+ const [searchParams] = createSearchParams()
272
275
  const tag = createMemo(() => searchParams().get('tag') ?? '')
273
276
  return (
274
277
  <div className="status">
@@ -735,6 +738,28 @@ export function Tags() {
735
738
  )
736
739
  })
737
740
 
741
+ test('bakes a numeric scalar array from the carried tree (Roadmap A-3)', () => {
742
+ // The analyzer carries `SignalInfo.parsed`, so the scalar-array bake
743
+ // reads the structured tree (`parsedLiteralToGo`) instead of re-parsing
744
+ // the value string with `ts.createSourceFile`. Each numeric element uses
745
+ // the carried raw token (= `NumericLiteral.text`), so the bake matches
746
+ // the fallback byte-for-byte — including TS's `.text` normalisation of
747
+ // `1e3` → `1000` and `0x10` → `16`.
748
+ const adapter = new GoTemplateAdapter()
749
+ const ir = compileToIR(`
750
+ "use client"
751
+ import { createSignal } from "@barefootjs/client"
752
+
753
+ export function Nums() {
754
+ const [ns] = createSignal<number[]>([1, 2, 1e3, 0x10])
755
+ return <ul>{ns().map((n) => <li key={n}>{n}</li>)}</ul>
756
+ }
757
+ `)
758
+ expect(adapter.generate(ir).types!).toContain(
759
+ 'Ns: []int{1, 2, 1000, 16},',
760
+ )
761
+ })
762
+
738
763
  test('synthesises a struct for an untyped object array and bakes it (#1680)', () => {
739
764
  // An untyped object array has no element type to bake against. Rather
740
765
  // than leave it nil (empty SSR loop), infer a struct from the literal's
@@ -1715,7 +1740,9 @@ export function TodoStatus() {
1715
1740
  return <div>{todos().every(t => t.done)}</div>
1716
1741
  }
1717
1742
  `)
1718
- expect(result.template).toContain('bf_every .Todos "Done"')
1743
+ // #2018 P2: `.every(t => t.done)` now lowers through the evaluator —
1744
+ // the predicate body travels as serialized ParsedExpr JSON.
1745
+ expect(result.template).toContain('bf_every_eval .Todos')
1719
1746
  })
1720
1747
 
1721
1748
  test('nested .filter(...).length in filter predicate lowers via len (bf_filter ...) (#1443 PR4)', () => {
@@ -1745,7 +1772,7 @@ export function TodoList() {
1745
1772
  }
1746
1773
  `, adapter)
1747
1774
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
1748
- expect(result.template).toContain('gt (len (bf_filter .Tags "Active" true)) 0')
1775
+ expect(result.template).toContain('gt (len (bf_filter_eval .Tags')
1749
1776
  // No degenerate fallbacks
1750
1777
  expect(result.template).not.toContain('{{if false}}')
1751
1778
  expect(result.template).not.toContain('[UNSUPPORTED-FILTER-EXPR]')
@@ -1776,7 +1803,7 @@ export function TodoList() {
1776
1803
  )
1777
1804
  }
1778
1805
  `, adapter)
1779
- expect(result.template).toContain('gt (len (bf_filter .Tags "Active" true)) 0')
1806
+ expect(result.template).toContain('gt (len (bf_filter_eval .Tags')
1780
1807
  expect(result.template).toContain('{{if not .Done}}')
1781
1808
  })
1782
1809
 
@@ -1837,7 +1864,10 @@ export function ItemChecker() {
1837
1864
  return <div>{items().find(t => t.done) ? 'Found' : 'Not found'}</div>
1838
1865
  }
1839
1866
  `)
1840
- expect(result.template).toContain('bf_find .Items "Done" true')
1867
+ // #2018 P2: `.find(t => t.done)` lowers through the evaluator; the
1868
+ // trailing `true` is the forward-search flag (find / findIndex).
1869
+ expect(result.template).toContain('bf_find_eval .Items')
1870
+ expect(result.template).toContain('true bf_env')
1841
1871
  expect(result.template).toContain('Found')
1842
1872
  })
1843
1873
  })
@@ -1855,7 +1885,10 @@ export function ItemChecker() {
1855
1885
  return <div>{items().findLast(t => t.done) ? 'Found' : 'Not found'}</div>
1856
1886
  }
1857
1887
  `)
1858
- expect(result.template).toContain('bf_find_last .Items "Done" true')
1888
+ // #2018 P2: `.findLast(t => t.done)` shares `bf_find_eval` with `.find`,
1889
+ // distinguished by the `false` (backward) forward-flag.
1890
+ expect(result.template).toContain('bf_find_eval .Items')
1891
+ expect(result.template).toContain('false bf_env')
1859
1892
  expect(result.template).toContain('Found')
1860
1893
  })
1861
1894
 
@@ -1889,10 +1922,20 @@ export function ItemChecker() {
1889
1922
  return <div>idx: {items().findLastIndex(t => t.done)}</div>
1890
1923
  }
1891
1924
  `)
1892
- expect(result.template).toContain('bf_find_last_index .Items "Done" true')
1893
- })
1894
-
1895
- test('renders findLastIndex() with complex predicate via range', () => {
1925
+ // #2018 P2: `.findLastIndex` lowers via `bf_find_index_eval` with the
1926
+ // `false` (backward) forward-flag.
1927
+ expect(result.template).toContain('bf_find_index_eval .Items')
1928
+ expect(result.template).toContain('false bf_env')
1929
+ })
1930
+
1931
+ test('renders findLastIndex() with a pure complex predicate via the evaluator', () => {
1932
+ // #2018 P2: a pure (call-free) complex predicate like
1933
+ // `t.price > 50 && t.active` is no longer confined to the
1934
+ // field-equality catalogue — it serializes to a ParsedExpr and lowers
1935
+ // through `bf_find_index_eval` (backward `false` flag), replacing the
1936
+ // old `{{range}}` $bf_r accumulator. The range fallback is now reserved
1937
+ // for predicates the evaluator can't model (e.g. a signal-getter call,
1938
+ // covered by the findLast test above).
1896
1939
  const result = compileAndGenerate(`
1897
1940
  "use client"
1898
1941
  import { createSignal } from "@barefootjs/client"
@@ -1904,10 +1947,9 @@ export function ItemFinder() {
1904
1947
  return <div>{items().findLastIndex(t => t.price > 50 && t.active)}</div>
1905
1948
  }
1906
1949
  `)
1907
- const varMatch = result.template.match(/(\$bf_r\d+) := -1/)
1908
- expect(varMatch).not.toBeNull()
1909
- expect(result.template).toContain(`${varMatch![1]} = $i`)
1910
- expect(result.template).not.toContain('{{break}}')
1950
+ expect(result.template).toContain('bf_find_index_eval .Items')
1951
+ expect(result.template).toContain('false bf_env')
1952
+ expect(result.template).not.toContain('$bf_r')
1911
1953
  })
1912
1954
 
1913
1955
  test('findLast() complex predicate in IR-level ternary works via preamble splitting', () => {
@@ -2428,7 +2470,9 @@ export function C() {
2428
2470
  return <ul>{items().filter(({done}) => done).map(t => <li key={t.id}>{t.name}</li>)}</ul>
2429
2471
  }`)
2430
2472
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
2431
- expect(result.template).toContain('bf_filter .Items "Done" true')
2473
+ // #2018 P2: the normalised `_t => _t.done` predicate lowers through the
2474
+ // evaluator (`bf_filter_eval`).
2475
+ expect(result.template).toContain('bf_filter_eval .Items')
2432
2476
  })
2433
2477
 
2434
2478
  test('.filter(function (x) { return x.done }).map(...) lowers cleanly', () => {
@@ -2441,7 +2485,9 @@ export function C() {
2441
2485
  return <ul>{items().filter(function (x) { return x.done }).map(t => <li key={t.id}>{t.name}</li>)}</ul>
2442
2486
  }`)
2443
2487
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
2444
- expect(result.template).toContain('bf_filter .Items "Done" true')
2488
+ // #2018 P2: the normalised `_t => _t.done` predicate lowers through the
2489
+ // evaluator (`bf_filter_eval`).
2490
+ expect(result.template).toContain('bf_filter_eval .Items')
2445
2491
  })
2446
2492
  })
2447
2493
 
@@ -2837,19 +2883,23 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
2837
2883
  // #1448 Tier B — string → string, padded to a target width.
2838
2884
  { fixture: stringPadStartFixture, expect: 'bf_pad_start .Value 5 "0"' },
2839
2885
  { fixture: stringPadEndFixture, expect: 'bf_pad_end .Value 5 "."' },
2840
- // #1448 Tier B — sort / toSorted. Loop-chained shapes wrap the
2841
- // iterable in `bf_sort .Items <kind> <key> <type> <dir>`;
2842
- // standalone shapes inline the helper at the call site.
2843
- { fixture: arraySortFieldAscFixture, expect: 'bf_sort .Items "field" "Price" "numeric" "asc"' },
2844
- { fixture: arraySortFieldDescFixture, expect: 'bf_sort .Items "field" "Price" "numeric" "desc"' },
2845
- { fixture: arraySortPrimitiveFixture, expect: 'bf_sort .Nums "self" "" "numeric" "asc"' },
2886
+ // #1448 Tier B — sort / toSorted. Both the standalone shapes and the
2887
+ // `.sort().map()` loop-hoist now lower through the evaluator (#2018 P1/P3):
2888
+ // the comparator body travels as serialized ParsedExpr to `bf_sort_eval`.
2889
+ { fixture: arraySortFieldAscFixture, expect: 'bf_sort_eval .Items' },
2890
+ { fixture: arraySortFieldDescFixture, expect: 'bf_sort_eval .Items' },
2891
+ { fixture: arraySortPrimitiveFixture, expect: 'bf_sort_eval .Nums' },
2892
+ // localeCompare can't be evaluated, so it keeps the legacy `bf_sort` path
2893
+ // (both standalone and loop-hoist fall back).
2846
2894
  { fixture: arraySortLocaleFixture, expect: 'bf_sort .Names "self" "" "string" "asc"' },
2847
- // Multi-key (`||`-chain): one 4-string group per comparison key,
2848
- // applied in priority order as tie-breakers.
2895
+ // Multi-key (`||`-chain) here ends in a `localeCompare`, so the whole
2896
+ // comparator falls back to the structured `bf_sort` (one 4-string group
2897
+ // per comparison key, applied in priority order as tie-breakers).
2849
2898
  { fixture: arraySortMultiKeyFixture, expect: 'bf_sort .Items "field" "Price" "numeric" "asc" "field" "Name" "string" "asc"' },
2850
- // Relational-ternary comparator lowers to a single `auto` key.
2851
- { fixture: arraySortTernaryFixture, expect: 'bf_sort .Items "field" "Rank" "auto" "asc"' },
2852
- { fixture: arrayToSortedFixture, expect: 'bf_sort .Nums "self" "" "numeric" "asc"' },
2899
+ // Relational-ternary comparator a pure body, so it lowers via the
2900
+ // evaluator like the other field sorts.
2901
+ { fixture: arraySortTernaryFixture, expect: 'bf_sort_eval .Items' },
2902
+ { fixture: arrayToSortedFixture, expect: 'bf_sort_eval .Nums' },
2853
2903
  // #1448 Tier B — iteration shapes. These are loop-level
2854
2904
  // patterns (range binding order), not helper function calls.
2855
2905
  { fixture: arrayEntriesFixture, expect: '{{range $i, $v := .Items}}' },
@@ -2894,14 +2944,14 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
2894
2944
  // now listed in `UNSUPPORTED_METHODS`, so `isSupported` refuses them
2895
2945
  // and `convertExpressionToGo` records BF101 — the same treatment the
2896
2946
  // unsupported array methods already got. These tests pin that parity.
2897
- describe('GoTemplateAdapter - #1448 Tier C reduce field capitalisation', () => {
2898
- // #1728 review: `bf_reduce`'s projected field name must use the same
2899
- // initialism-aware capitalisation the adapter applies when generating
2900
- // struct fields (`capitalizeFieldName`), or the runtime reflect lookup
2901
- // misses the exported field (`id` struct `ID`, not `Id`) and folds a
2902
- // zero value. Pin the emitted key so a regression to plain
2903
- // first-letter capitalisation fails here.
2904
- test('reduce over an initialism field emits the Go-initialism key (ID, not Id)', () => {
2947
+ describe('GoTemplateAdapter - #2018 reduce via the evaluator', () => {
2948
+ // A standalone `.reduce(fn, init)` now lowers through the evaluator: the
2949
+ // reducer body is serialized to ParsedExpr JSON and folded by `bf_reduce_eval`.
2950
+ // The body carries the JS field name (`x.id`); the runtime field reader
2951
+ // (`getFieldValue`) resolves it case-variantly against the Go struct field
2952
+ // (`ID`) at render time, so no compile-time capitalisation is needed (the
2953
+ // former #1728 initialism gotcha is moot on this path).
2954
+ test('reduce over a field lowers to bf_reduce_eval with the serialized body', () => {
2905
2955
  const adapter = new GoTemplateAdapter()
2906
2956
  const ir = compileToIR(`
2907
2957
  function C({ items }: { items: { id: number }[] }) {
@@ -2910,8 +2960,9 @@ function C({ items }: { items: { id: number }[] }) {
2910
2960
  export { C }
2911
2961
  `, adapter)
2912
2962
  const template = adapter.generate(ir).template ?? ''
2913
- expect(template).toContain('bf_reduce .Items "+" "field" "ID" "numeric" "0"')
2914
- expect(template).not.toContain('"Id"')
2963
+ expect(template).toContain('bf_reduce_eval .Items')
2964
+ // The seed is threaded through bf_number; the body is the serialized tree.
2965
+ expect(template).toContain('(bf_number "0")')
2915
2966
  })
2916
2967
  })
2917
2968
 
@@ -2952,20 +3003,32 @@ export { C }
2952
3003
  return adapter.generate(ir).template ?? ''
2953
3004
  }
2954
3005
 
2955
- test('.flatMap(i => i.field) emits bf_flat_map with the Go-capitalised field', () => {
2956
- expect(emitFlatMap('rows.flatMap(i => i.tags).join(" ")')).toContain('bf_flat_map .Rows "field" "Tags"')
3006
+ // #2018 P3: `.flatMap(proj)` lowers through the evaluator the projection
3007
+ // body serializes to ParsedExpr JSON and `bf_flat_map_eval` flattens the
3008
+ // results one level. The raw (lowercase) field name travels in the JSON; the
3009
+ // runtime field reader resolves it case-insensitively against Go data.
3010
+ // (The serialized projection JSON is embedded in a Go-template double-quoted
3011
+ // string — its quotes are backslash-escaped — so these pins assert the helper
3012
+ // + receiver; the projection JSON itself is verified by the render
3013
+ // conformance fixtures + the runtime TestFlatMapEval.)
3014
+ test('.flatMap(i => i.field) emits bf_flat_map_eval', () => {
3015
+ expect(emitFlatMap('rows.flatMap(i => i.tags).join(" ")')).toContain('bf_flat_map_eval .Rows')
2957
3016
  })
2958
3017
 
2959
- test('.flatMap(i => i) emits the self projection', () => {
2960
- expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain('bf_flat_map .Rows "self" ""')
3018
+ test('.flatMap(i => i) emits bf_flat_map_eval (self projection)', () => {
3019
+ expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain('bf_flat_map_eval .Rows')
2961
3020
  })
2962
3021
 
2963
- test('.flatMap(i => [i.a, i.b]) emits bf_flat_map_tuple with capitalised leaves', () => {
2964
- expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain('bf_flat_map_tuple .Rows "field" "A" "field" "B"')
3022
+ test('.flatMap(i => [i.a, i.b]) emits bf_flat_map_eval (array-literal projection)', () => {
3023
+ expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain('bf_flat_map_eval .Rows')
2965
3024
  })
2966
3025
 
2967
- test('tuple self + field leaves', () => {
2968
- expect(emitFlatMap('rows.flatMap(i => [i, i.a]).join(" ")')).toContain('bf_flat_map_tuple .Rows "self" "" "field" "A"')
3026
+ // (#2018 P5) A tuple with a string-literal element lowers the same way: the
3027
+ // whole array-literal body serializes and `bf_flat_map_eval` evaluates it per
3028
+ // item, so a non-member element no longer refuses (it did under the structured
3029
+ // `bf_flat_map_tuple` catalogue).
3030
+ test('.flatMap(i => [i.name, "x"]) emits bf_flat_map_eval (literal element)', () => {
3031
+ expect(emitFlatMap('rows.flatMap(i => [i.name, "x"]).join(" ")')).toContain('bf_flat_map_eval .Rows')
2969
3032
  })
2970
3033
  })
2971
3034
 
@@ -3019,9 +3082,10 @@ export function C() {
3019
3082
  // the no-initial-value form stays refused — JS throws on an empty
3020
3083
  // array there, which a template can't mirror.
3021
3084
  { name: 'reduce (no init)', expr: `items().reduce((a, b) => a + b.n)`, badEmit: '.Reduce' },
3022
- // Self / field / field-tuple `.flatMap` now lowers (#1448 Tier C); a
3023
- // tuple with a non-leaf element (here a string literal) stays refused.
3024
- { name: 'flatMap (literal element)', expr: `items().flatMap(i => [i.name, "x"])`, badEmit: '.FlatMap' },
3085
+ // (#2018 P5) The literal-element tuple `.flatMap(i => [i.name, "x"])` now
3086
+ // lowers through the evaluator (`bf_flat_map_eval` serializes the array-
3087
+ // literal body and flattens), so it is no longer refused pinned positively
3088
+ // in the `.flatMap` describe block alongside the `[i.a, i.b]` form.
3025
3089
  // Lowered methods whose MEANINGFUL extra argument isn't lowered yet
3026
3090
  // (#1448): the `fromIndex` of `.includes`/`.indexOf`/`.lastIndexOf`
3027
3091
  // and the variadic `.concat`. The parser refuses these (silently
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Registry → adapter end-to-end (#2057). Every lowering — first-party built-ins
3
+ * (like `queryHref`) and userland plugins alike — flows through the one
4
+ * lowering-plugin *registry*. This test guarantees that seam works all the way
5
+ * through: a SAMPLE plugin (registered via the public `registerLoweringPlugin`)
6
+ * recognises a call from an arbitrary package and returns a backend-neutral
7
+ * `guard-list` node, and the Go adapter renders it via its own `bf_query`
8
+ * mapping — no adapter code knows the sample plugin exists.
9
+ *
10
+ * The plugin reuses the `'query'` helper id so it exercises the exact same
11
+ * renderer path as the built-in `queryHref` plugin, proving that a userland
12
+ * plugin and a default-registered built-in are indistinguishable to the adapter
13
+ * (which is the whole point of the neutral-node layer).
14
+ */
15
+ import { describe, test, expect, afterEach } from 'bun:test'
16
+ import {
17
+ compileJSX,
18
+ registerLoweringPlugin,
19
+ getLoweringPlugins,
20
+ __resetLoweringPluginsForTest,
21
+ type ComponentIR,
22
+ type LoweringPlugin,
23
+ } from '@barefootjs/jsx'
24
+ import { GoTemplateAdapter } from '../adapter/go-template-adapter'
25
+
26
+ function generate(src: string) {
27
+ const adapter = new GoTemplateAdapter()
28
+ const result = compileJSX(src.trimStart(), 'T.tsx', { adapter, outputIR: true })
29
+ const irFile = result.files.find(f => f.type === 'ir')
30
+ if (!irFile) throw new Error('no IR')
31
+ const ir = JSON.parse(irFile.content) as ComponentIR
32
+ return adapter.generate(ir)
33
+ }
34
+
35
+ // A userland-style plugin: active only when the component imports `demoUrl` from
36
+ // `@sample/pkg`, lowering `demoUrl(base, { key: value })` to a neutral
37
+ // `guard-list` on the `'query'` helper. It reuses the sub-parts of the parsed
38
+ // call directly (base = first arg, one triple per object property) so no
39
+ // adapter syntax leaks into the plugin.
40
+ const samplePlugin: LoweringPlugin = {
41
+ name: 'sample-demo-url',
42
+ prepare(metadata) {
43
+ const spec = metadata.imports
44
+ .filter(i => i.source === '@sample/pkg' && !i.isTypeOnly)
45
+ .flatMap(i => i.specifiers)
46
+ .filter(s => !s.isTypeOnly && !s.isNamespace && !s.isDefault)
47
+ .find(s => s.name === 'demoUrl')
48
+ if (!spec) return null
49
+ const local = spec.alias ?? spec.name // the name it's bound under in this file
50
+ return (callee, args) => {
51
+ if (callee.kind !== 'identifier' || callee.name !== local) return null
52
+ const [base, obj] = args
53
+ if (!base || obj?.kind !== 'object-literal') return null
54
+ return {
55
+ kind: 'guard-list',
56
+ helper: 'query',
57
+ base,
58
+ triples: obj.properties.map(p => ({ guard: null, key: p.key, value: p.value })),
59
+ }
60
+ }
61
+ },
62
+ }
63
+
64
+ afterEach(() => {
65
+ __resetLoweringPluginsForTest(getLoweringPlugins().filter(p => p.name !== 'sample-demo-url'))
66
+ })
67
+
68
+ describe('lowering-plugin registry → Go adapter (#2057)', () => {
69
+ test('a registered plugin lowers its call to the neutral node the adapter renders', () => {
70
+ registerLoweringPlugin(samplePlugin)
71
+ const src = `
72
+ 'use client'
73
+ import { demoUrl } from '@sample/pkg'
74
+ export function P(props: { base: string; tag: string }) {
75
+ return <a href={demoUrl(props.base, { tag: props.tag })}>x</a>
76
+ }
77
+ `
78
+ const { template } = generate(src)
79
+ // Same render path as built-in queryHref: guard-list → bf_query.
80
+ expect(template).toContain('bf_query .Base (true) "tag" .Tag')
81
+ })
82
+
83
+ test('without the plugin registered the same call falls back to the generic lowering', () => {
84
+ // No registerLoweringPlugin call — the registry is empty for this component.
85
+ const src = `
86
+ 'use client'
87
+ import { demoUrl } from '@sample/pkg'
88
+ export function P(props: { base: string; tag: string }) {
89
+ return <a href={demoUrl(props.base, { tag: props.tag })}>x</a>
90
+ }
91
+ `
92
+ const { template } = generate(src)
93
+ expect(template).not.toContain('bf_query')
94
+ })
95
+
96
+ test('the plugin is inert when the component does not import from its package', () => {
97
+ registerLoweringPlugin(samplePlugin)
98
+ const src = `
99
+ 'use client'
100
+ import { queryHref } from '@barefootjs/client'
101
+ export function P(props: { base: string; tag: string }) {
102
+ return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
103
+ }
104
+ `
105
+ const { template } = generate(src)
106
+ // Built-in queryHref still lowers; the sample plugin simply isn't active.
107
+ expect(template).toContain('bf_query .Base (true) "tag" .Tag')
108
+ })
109
+ })
@@ -0,0 +1,179 @@
1
+ /**
2
+ * `queryHref(base, { … })` → `bf_query` lowering for the Go adapter (#2042).
3
+ *
4
+ * The pure functional URL builder is a structured `call` + `object-literal` in
5
+ * the IR, so the adapter lowers it directly — no block-body recognizer, no
6
+ * re-parse. The lowering emits `(include) "key" value` triples and lets the
7
+ * `bf_query` runtime helper own the non-empty check (so it can also append array
8
+ * values member-by-member, #2048): a plain `key: v` → `(true) "key" v`; a
9
+ * conditional `key: cond ? a : undefined` → `(cond) "key" a` (the helper drops an
10
+ * empty `a`). The full value semantics are conformance-tested against
11
+ * URLSearchParams in the shared golden vectors (TestHelperVectors, fn "query").
12
+ */
13
+ import { describe, test, expect } from 'bun:test'
14
+ import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
15
+ import { GoTemplateAdapter } from '../adapter/go-template-adapter'
16
+
17
+ function generate(src: string) {
18
+ const adapter = new GoTemplateAdapter()
19
+ const result = compileJSX(src.trimStart(), 'T.tsx', { adapter, outputIR: true })
20
+ const irFile = result.files.find(f => f.type === 'ir')
21
+ if (!irFile) throw new Error('no IR')
22
+ const ir = JSON.parse(irFile.content) as ComponentIR
23
+ return adapter.generate(ir)
24
+ }
25
+
26
+ describe('queryHref → bf_query (#2042)', () => {
27
+ test('a plain value passes a `true` include — bf_query drops it if empty', () => {
28
+ const src = `
29
+ 'use client'
30
+ import { queryHref } from '@barefootjs/client'
31
+ export function P(props: { base: string; tag: string }) {
32
+ return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
33
+ }
34
+ `
35
+ const { template } = generate(src)
36
+ expect(template).toContain('bf_query .Base (true) "tag" .Tag')
37
+ expect(template).not.toContain('.QueryHref')
38
+ })
39
+
40
+ test('a conditional include lowers to `(cond)` — the helper applies the non-empty check', () => {
41
+ const src = `
42
+ 'use client'
43
+ import { queryHref } from '@barefootjs/client'
44
+ export function P(props: { base: string; sort: string; tag: string }) {
45
+ return (
46
+ <a href={queryHref(props.base, {
47
+ sort: props.sort !== 'date' ? props.sort : undefined,
48
+ tag: props.tag,
49
+ })}>x</a>
50
+ )
51
+ }
52
+ `
53
+ const { template } = generate(src)
54
+ expect(template).toContain(
55
+ 'bf_query .Base (ne (bf_string .Sort) "date") "sort" .Sort (true) "tag" .Tag',
56
+ )
57
+ // The `ne consequent ""` non-empty check is no longer folded into the
58
+ // include — bf_query owns it (so it can also append array values).
59
+ expect(template).not.toContain('(ne .Sort "")')
60
+ })
61
+
62
+ // A `&&` / `||` guard is NOT a comparison, so `lowerUrlGuard` can't emit it as
63
+ // a bare Go bool — `and`/`or` return one of their operands (a string), which
64
+ // `bf_query` type-asserts against. It must take the truthiness-wrap path,
65
+ // `ne (and …) ""`, yielding a real bool.
66
+ test('a `&&` guard is wrapped to a bool — `ne (and …) ""`, not a bare `and`', () => {
67
+ const src = `
68
+ 'use client'
69
+ import { queryHref } from '@barefootjs/client'
70
+ export function P(props: { base: string; a: string; b: string }) {
71
+ return <a href={queryHref(props.base, { both: props.a && props.b ? props.a : undefined })}>x</a>
72
+ }
73
+ `
74
+ const { template } = generate(src)
75
+ expect(template).toContain('bf_query .Base (ne (and .A .B) "") "both" .A')
76
+ })
77
+
78
+ test('null / empty-string alternates are both treated as the omit branch', () => {
79
+ const src = `
80
+ 'use client'
81
+ import { queryHref } from '@barefootjs/client'
82
+ export function P(props: { base: string; mode: string; a: string; b: string }) {
83
+ return <a href={queryHref(props.base, {
84
+ a: props.mode !== 'off' ? props.a : '',
85
+ b: props.mode !== 'off' ? props.b : null,
86
+ })}>x</a>
87
+ }
88
+ `
89
+ const { template } = generate(src)
90
+ // Both '' and null alternates fold to the same conditional-include form.
91
+ expect(template).toContain('(ne (bf_string .Mode) "off") "a" .A')
92
+ expect(template).toContain('(ne (bf_string .Mode) "off") "b" .B')
93
+ })
94
+
95
+ test('an array value lowers the slice expression; bf_query appends its members', () => {
96
+ const src = `
97
+ 'use client'
98
+ import { queryHref } from '@barefootjs/client'
99
+ export function P(props: { base: string; tags: string[] }) {
100
+ return <a href={queryHref(props.base, { tag: props.tags })}>x</a>
101
+ }
102
+ `
103
+ const { template } = generate(src)
104
+ // The value is the raw slice field; member-append + non-empty omit happen in
105
+ // the helper at render time (verified against URLSearchParams in the golden
106
+ // vectors). The old `ne value ""` fold would have been invalid Go here.
107
+ expect(template).toContain('bf_query .Base (true) "tag" .Tags')
108
+ })
109
+
110
+ test('a conditional array value keeps the guard and passes the slice', () => {
111
+ const src = `
112
+ 'use client'
113
+ import { queryHref } from '@barefootjs/client'
114
+ export function P(props: { base: string; on: string; tags: string[] }) {
115
+ return <a href={queryHref(props.base, { tag: props.on !== '' ? props.tags : undefined })}>x</a>
116
+ }
117
+ `
118
+ const { template } = generate(src)
119
+ expect(template).toContain('bf_query .Base (ne (bf_string .On) "") "tag" .Tags')
120
+ })
121
+
122
+ test('an aliased import is still recognised', () => {
123
+ const src = `
124
+ 'use client'
125
+ import { queryHref as qh } from '@barefootjs/client'
126
+ export function P(props: { base: string; tag: string }) {
127
+ return <a href={qh(props.base, { tag: props.tag })}>x</a>
128
+ }
129
+ `
130
+ const { template } = generate(src)
131
+ expect(template).toContain('bf_query .Base')
132
+ expect(template).toContain('"tag" .Tag')
133
+ })
134
+
135
+ test('a param-free expression-bodied helper wrapping queryHref inlines + lowers', () => {
136
+ const src = `
137
+ 'use client'
138
+ import { queryHref } from '@barefootjs/client'
139
+ export function P(props: { base: string }) {
140
+ const homeHref = () => queryHref(props.base, { view: 'home' })
141
+ return <a href={homeHref()}>x</a>
142
+ }
143
+ `
144
+ const { template } = generate(src)
145
+ expect(template).toContain('bf_query .Base (true) "view" "home"')
146
+ expect(template).not.toContain('.HomeHref')
147
+ })
148
+
149
+ // Known limitation (pre-existing, not queryHref-specific): the generic helper
150
+ // inliner declines a body whose object literal references the helper's params,
151
+ // because an object literal lowers opaquely from its `raw` source — so the
152
+ // param can't be substituted. queryHref's idiom is the direct call, so this is
153
+ // a minor gap; helper-delegation ergonomics are a follow-up (#2042).
154
+ test('a helper whose params-object references a param is not yet inlined (falls back)', () => {
155
+ const src = `
156
+ 'use client'
157
+ import { queryHref } from '@barefootjs/client'
158
+ export function P(props: { base: string }) {
159
+ const hrefFor = (s: string) => queryHref(props.base, { sort: s })
160
+ return <a href={hrefFor('title')}>x</a>
161
+ }
162
+ `
163
+ const { template } = generate(src)
164
+ expect(template).not.toContain('bf_query')
165
+ expect(template).toContain('.HrefFor "title"')
166
+ })
167
+
168
+ test('a dynamic (non-literal) params object falls back to the generic lowering', () => {
169
+ const src = `
170
+ 'use client'
171
+ import { queryHref } from '@barefootjs/client'
172
+ export function P(props: { base: string; q: Record<string, string> }) {
173
+ return <a href={queryHref(props.base, props.q)}>x</a>
174
+ }
175
+ `
176
+ const { template } = generate(src)
177
+ expect(template).not.toContain('bf_query')
178
+ })
179
+ })