@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.
- package/dist/adapter/analysis/component-tree.d.ts +23 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +53 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/helper-inline.d.ts +31 -0
- package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
- package/dist/adapter/expr/url-builder.d.ts +23 -0
- package/dist/adapter/expr/url-builder.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +291 -1070
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +2857 -2618
- package/dist/adapter/lib/compile-state.d.ts +116 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +11 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/go-emit.d.ts +117 -0
- package/dist/adapter/lib/go-emit.d.ts.map +1 -0
- package/dist/adapter/lib/go-naming.d.ts +39 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +13 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +165 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
- package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
- package/dist/adapter/memo/memo-compute.d.ts +124 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
- package/dist/adapter/memo/memo-type.d.ts +38 -0
- package/dist/adapter/memo/memo-type.d.ts.map +1 -0
- package/dist/adapter/memo/memo-value.d.ts +64 -0
- package/dist/adapter/memo/memo-value.d.ts.map +1 -0
- package/dist/adapter/memo/template-interp.d.ts +43 -0
- package/dist/adapter/memo/template-interp.d.ts.map +1 -0
- package/dist/adapter/props/prop-types.d.ts +31 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +40 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/type/type-codegen.d.ts +25 -0
- package/dist/adapter/type/type-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
- package/dist/adapter/value/value-lowering.d.ts +46 -0
- package/dist/adapter/value/value-lowering.d.ts.map +1 -0
- package/dist/build.js +2856 -2617
- package/dist/index.js +2857 -2618
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +155 -84
- package/src/__tests__/go-template-adapter.test.ts +118 -54
- package/src/__tests__/lowering-plugin.test.ts +109 -0
- package/src/__tests__/query-href.test.ts +179 -0
- package/src/adapter/analysis/component-tree.ts +174 -0
- package/src/adapter/emit-context.ts +59 -0
- package/src/adapter/expr/helper-inline.ts +274 -0
- package/src/adapter/expr/url-builder.ts +123 -0
- package/src/adapter/go-template-adapter.ts +2099 -5325
- package/src/adapter/lib/compile-state.ts +150 -0
- package/src/adapter/lib/constants.ts +24 -0
- package/src/adapter/lib/go-emit.ts +289 -0
- package/src/adapter/lib/go-naming.ts +84 -0
- package/src/adapter/lib/ir-scope.ts +31 -0
- package/src/adapter/lib/types.ts +182 -0
- package/src/adapter/memo/ctor-lowering.ts +267 -0
- package/src/adapter/memo/memo-compute.ts +451 -0
- package/src/adapter/memo/memo-type.ts +74 -0
- package/src/adapter/memo/memo-value.ts +197 -0
- package/src/adapter/memo/template-interp.ts +246 -0
- package/src/adapter/props/prop-types.ts +84 -0
- package/src/adapter/spread/spread-codegen.ts +458 -0
- package/src/adapter/type/type-codegen.ts +95 -0
- package/src/adapter/value/parsed-literal-to-go.ts +94 -0
- package/src/adapter/value/value-lowering.ts +162 -0
- 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 {
|
|
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
|
|
241
|
-
// call `sp()` still resolves to the canonical `.SearchParams` field
|
|
242
|
-
// generated struct field name is fixed, not derived from the JS
|
|
243
|
-
test('aliased
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
|
|
1908
|
-
expect(
|
|
1909
|
-
expect(result.template).toContain(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2841
|
-
//
|
|
2842
|
-
//
|
|
2843
|
-
{ fixture: arraySortFieldAscFixture, expect: '
|
|
2844
|
-
{ fixture: arraySortFieldDescFixture, expect: '
|
|
2845
|
-
{ fixture: arraySortPrimitiveFixture, expect: '
|
|
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)
|
|
2848
|
-
//
|
|
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
|
|
2851
|
-
|
|
2852
|
-
{ fixture:
|
|
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 - #
|
|
2898
|
-
//
|
|
2899
|
-
//
|
|
2900
|
-
//
|
|
2901
|
-
//
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
2904
|
-
test('reduce over
|
|
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('
|
|
2914
|
-
|
|
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
|
-
|
|
2956
|
-
|
|
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
|
|
2960
|
-
expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain('
|
|
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
|
|
2964
|
-
expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain('
|
|
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
|
-
|
|
2968
|
-
|
|
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
|
-
//
|
|
3023
|
-
//
|
|
3024
|
-
|
|
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
|
+
})
|