@barefootjs/go-template 0.15.2 → 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 +2859 -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 +2857 -2616
- package/dist/index.js +2859 -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 +211 -60
- 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 +2100 -5316
- 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
|
@@ -42,12 +42,15 @@ runAdapterConformanceTests({
|
|
|
42
42
|
// `TemplateFuncMap`). Shapes the adapter intentionally refuses at
|
|
43
43
|
// build time are pinned in `expectedDiagnostics` below.
|
|
44
44
|
//
|
|
45
|
-
// `data-table`
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
45
|
+
// `data-table` — the last #1896 holdout — now renders to Hono byte
|
|
46
|
+
// parity on real Go and is fully un-skipped (run in CI). Its table
|
|
47
|
+
// body is a keyed `.map` over a `/* @client */`-sorted MEMO whose
|
|
48
|
+
// data is a module-const object array (a memo-derived dynamic loop of
|
|
49
|
+
// imported components); the nested-component slice constructor bakes
|
|
50
|
+
// it via loop-body children companion defines, a wrapper struct +
|
|
51
|
+
// constructor, and block-body memo folding (#1897). On Go it is
|
|
52
|
+
// un-skipped everywhere — including `skipMarkerConformance` below,
|
|
53
|
+
// since the SSR template emits the slot ids the IR declares.
|
|
51
54
|
//
|
|
52
55
|
// `search-params` (router v0.5) now renders on Go: `logical()` parenthesises
|
|
53
56
|
// its multi-token operands so `searchParams().get(k) ?? d` lowers to
|
|
@@ -223,8 +226,9 @@ describe('GoTemplateAdapter - searchParams() env-signal lowering (#1922)', () =>
|
|
|
223
226
|
test('lowers searchParams().get(k) to .SearchParams.Get and emits the struct binding', () => {
|
|
224
227
|
const adapter = new GoTemplateAdapter()
|
|
225
228
|
const ir = compileToIR(`
|
|
226
|
-
import {
|
|
229
|
+
import { createSearchParams } from '@barefootjs/client'
|
|
227
230
|
export function SortLabel() {
|
|
231
|
+
const [searchParams] = createSearchParams()
|
|
228
232
|
return <p>{searchParams().get('sort') ?? 'none'}</p>
|
|
229
233
|
}
|
|
230
234
|
`, adapter)
|
|
@@ -234,14 +238,15 @@ export function SortLabel() {
|
|
|
234
238
|
expect(types).toContain('SearchParams: in.SearchParams')
|
|
235
239
|
})
|
|
236
240
|
|
|
237
|
-
// An aliased
|
|
238
|
-
// call `sp()` still resolves to the canonical `.SearchParams` field
|
|
239
|
-
// generated struct field name is fixed, not derived from the JS
|
|
240
|
-
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', () => {
|
|
241
245
|
const adapter = new GoTemplateAdapter()
|
|
242
246
|
const ir = compileToIR(`
|
|
243
|
-
import {
|
|
247
|
+
import { createSearchParams } from '@barefootjs/client'
|
|
244
248
|
export function SortLabel() {
|
|
249
|
+
const [sp] = createSearchParams()
|
|
245
250
|
return <p>{sp().get('sort') ?? 'none'}</p>
|
|
246
251
|
}
|
|
247
252
|
`, adapter)
|
|
@@ -264,8 +269,9 @@ describe('GoTemplateAdapter - template-literal text lowering (#1933)', () => {
|
|
|
264
269
|
const adapter = new GoTemplateAdapter()
|
|
265
270
|
const ir = compileToIR(`
|
|
266
271
|
'use client'
|
|
267
|
-
import { createMemo,
|
|
272
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
268
273
|
export function StatusLine() {
|
|
274
|
+
const [searchParams] = createSearchParams()
|
|
269
275
|
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
270
276
|
return (
|
|
271
277
|
<div className="status">
|
|
@@ -732,6 +738,28 @@ export function Tags() {
|
|
|
732
738
|
)
|
|
733
739
|
})
|
|
734
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
|
+
|
|
735
763
|
test('synthesises a struct for an untyped object array and bakes it (#1680)', () => {
|
|
736
764
|
// An untyped object array has no element type to bake against. Rather
|
|
737
765
|
// than leave it nil (empty SSR loop), infer a struct from the literal's
|
|
@@ -1712,7 +1740,9 @@ export function TodoStatus() {
|
|
|
1712
1740
|
return <div>{todos().every(t => t.done)}</div>
|
|
1713
1741
|
}
|
|
1714
1742
|
`)
|
|
1715
|
-
|
|
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')
|
|
1716
1746
|
})
|
|
1717
1747
|
|
|
1718
1748
|
test('nested .filter(...).length in filter predicate lowers via len (bf_filter ...) (#1443 PR4)', () => {
|
|
@@ -1742,7 +1772,7 @@ export function TodoList() {
|
|
|
1742
1772
|
}
|
|
1743
1773
|
`, adapter)
|
|
1744
1774
|
expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
|
|
1745
|
-
expect(result.template).toContain('gt (len (
|
|
1775
|
+
expect(result.template).toContain('gt (len (bf_filter_eval .Tags')
|
|
1746
1776
|
// No degenerate fallbacks
|
|
1747
1777
|
expect(result.template).not.toContain('{{if false}}')
|
|
1748
1778
|
expect(result.template).not.toContain('[UNSUPPORTED-FILTER-EXPR]')
|
|
@@ -1773,7 +1803,7 @@ export function TodoList() {
|
|
|
1773
1803
|
)
|
|
1774
1804
|
}
|
|
1775
1805
|
`, adapter)
|
|
1776
|
-
expect(result.template).toContain('gt (len (
|
|
1806
|
+
expect(result.template).toContain('gt (len (bf_filter_eval .Tags')
|
|
1777
1807
|
expect(result.template).toContain('{{if not .Done}}')
|
|
1778
1808
|
})
|
|
1779
1809
|
|
|
@@ -1834,7 +1864,10 @@ export function ItemChecker() {
|
|
|
1834
1864
|
return <div>{items().find(t => t.done) ? 'Found' : 'Not found'}</div>
|
|
1835
1865
|
}
|
|
1836
1866
|
`)
|
|
1837
|
-
|
|
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')
|
|
1838
1871
|
expect(result.template).toContain('Found')
|
|
1839
1872
|
})
|
|
1840
1873
|
})
|
|
@@ -1852,7 +1885,10 @@ export function ItemChecker() {
|
|
|
1852
1885
|
return <div>{items().findLast(t => t.done) ? 'Found' : 'Not found'}</div>
|
|
1853
1886
|
}
|
|
1854
1887
|
`)
|
|
1855
|
-
|
|
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')
|
|
1856
1892
|
expect(result.template).toContain('Found')
|
|
1857
1893
|
})
|
|
1858
1894
|
|
|
@@ -1886,10 +1922,20 @@ export function ItemChecker() {
|
|
|
1886
1922
|
return <div>idx: {items().findLastIndex(t => t.done)}</div>
|
|
1887
1923
|
}
|
|
1888
1924
|
`)
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
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).
|
|
1893
1939
|
const result = compileAndGenerate(`
|
|
1894
1940
|
"use client"
|
|
1895
1941
|
import { createSignal } from "@barefootjs/client"
|
|
@@ -1901,10 +1947,9 @@ export function ItemFinder() {
|
|
|
1901
1947
|
return <div>{items().findLastIndex(t => t.price > 50 && t.active)}</div>
|
|
1902
1948
|
}
|
|
1903
1949
|
`)
|
|
1904
|
-
|
|
1905
|
-
expect(
|
|
1906
|
-
expect(result.template).toContain(
|
|
1907
|
-
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')
|
|
1908
1953
|
})
|
|
1909
1954
|
|
|
1910
1955
|
test('findLast() complex predicate in IR-level ternary works via preamble splitting', () => {
|
|
@@ -2425,7 +2470,9 @@ export function C() {
|
|
|
2425
2470
|
return <ul>{items().filter(({done}) => done).map(t => <li key={t.id}>{t.name}</li>)}</ul>
|
|
2426
2471
|
}`)
|
|
2427
2472
|
expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
|
|
2428
|
-
|
|
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')
|
|
2429
2476
|
})
|
|
2430
2477
|
|
|
2431
2478
|
test('.filter(function (x) { return x.done }).map(...) lowers cleanly', () => {
|
|
@@ -2438,7 +2485,9 @@ export function C() {
|
|
|
2438
2485
|
return <ul>{items().filter(function (x) { return x.done }).map(t => <li key={t.id}>{t.name}</li>)}</ul>
|
|
2439
2486
|
}`)
|
|
2440
2487
|
expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
|
|
2441
|
-
|
|
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')
|
|
2442
2491
|
})
|
|
2443
2492
|
})
|
|
2444
2493
|
|
|
@@ -2834,19 +2883,23 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
|
|
|
2834
2883
|
// #1448 Tier B — string → string, padded to a target width.
|
|
2835
2884
|
{ fixture: stringPadStartFixture, expect: 'bf_pad_start .Value 5 "0"' },
|
|
2836
2885
|
{ fixture: stringPadEndFixture, expect: 'bf_pad_end .Value 5 "."' },
|
|
2837
|
-
// #1448 Tier B — sort / toSorted.
|
|
2838
|
-
//
|
|
2839
|
-
//
|
|
2840
|
-
{ fixture: arraySortFieldAscFixture, expect: '
|
|
2841
|
-
{ fixture: arraySortFieldDescFixture, expect: '
|
|
2842
|
-
{ 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).
|
|
2843
2894
|
{ fixture: arraySortLocaleFixture, expect: 'bf_sort .Names "self" "" "string" "asc"' },
|
|
2844
|
-
// Multi-key (`||`-chain)
|
|
2845
|
-
//
|
|
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).
|
|
2846
2898
|
{ fixture: arraySortMultiKeyFixture, expect: 'bf_sort .Items "field" "Price" "numeric" "asc" "field" "Name" "string" "asc"' },
|
|
2847
|
-
// Relational-ternary comparator
|
|
2848
|
-
|
|
2849
|
-
{ 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' },
|
|
2850
2903
|
// #1448 Tier B — iteration shapes. These are loop-level
|
|
2851
2904
|
// patterns (range binding order), not helper function calls.
|
|
2852
2905
|
{ fixture: arrayEntriesFixture, expect: '{{range $i, $v := .Items}}' },
|
|
@@ -2891,14 +2944,14 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
|
|
|
2891
2944
|
// now listed in `UNSUPPORTED_METHODS`, so `isSupported` refuses them
|
|
2892
2945
|
// and `convertExpressionToGo` records BF101 — the same treatment the
|
|
2893
2946
|
// unsupported array methods already got. These tests pin that parity.
|
|
2894
|
-
describe('GoTemplateAdapter - #
|
|
2895
|
-
//
|
|
2896
|
-
//
|
|
2897
|
-
//
|
|
2898
|
-
//
|
|
2899
|
-
//
|
|
2900
|
-
//
|
|
2901
|
-
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', () => {
|
|
2902
2955
|
const adapter = new GoTemplateAdapter()
|
|
2903
2956
|
const ir = compileToIR(`
|
|
2904
2957
|
function C({ items }: { items: { id: number }[] }) {
|
|
@@ -2907,8 +2960,9 @@ function C({ items }: { items: { id: number }[] }) {
|
|
|
2907
2960
|
export { C }
|
|
2908
2961
|
`, adapter)
|
|
2909
2962
|
const template = adapter.generate(ir).template ?? ''
|
|
2910
|
-
expect(template).toContain('
|
|
2911
|
-
|
|
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")')
|
|
2912
2966
|
})
|
|
2913
2967
|
})
|
|
2914
2968
|
|
|
@@ -2949,20 +3003,32 @@ export { C }
|
|
|
2949
3003
|
return adapter.generate(ir).template ?? ''
|
|
2950
3004
|
}
|
|
2951
3005
|
|
|
2952
|
-
|
|
2953
|
-
|
|
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')
|
|
2954
3016
|
})
|
|
2955
3017
|
|
|
2956
|
-
test('.flatMap(i => i) emits
|
|
2957
|
-
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')
|
|
2958
3020
|
})
|
|
2959
3021
|
|
|
2960
|
-
test('.flatMap(i => [i.a, i.b]) emits
|
|
2961
|
-
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')
|
|
2962
3024
|
})
|
|
2963
3025
|
|
|
2964
|
-
|
|
2965
|
-
|
|
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')
|
|
2966
3032
|
})
|
|
2967
3033
|
})
|
|
2968
3034
|
|
|
@@ -3016,9 +3082,10 @@ export function C() {
|
|
|
3016
3082
|
// the no-initial-value form stays refused — JS throws on an empty
|
|
3017
3083
|
// array there, which a template can't mirror.
|
|
3018
3084
|
{ name: 'reduce (no init)', expr: `items().reduce((a, b) => a + b.n)`, badEmit: '.Reduce' },
|
|
3019
|
-
//
|
|
3020
|
-
//
|
|
3021
|
-
|
|
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.
|
|
3022
3089
|
// Lowered methods whose MEANINGFUL extra argument isn't lowered yet
|
|
3023
3090
|
// (#1448): the `fromIndex` of `.includes`/`.indexOf`/`.lastIndexOf`
|
|
3024
3091
|
// and the variadic `.concat`. The parser refuses these (silently
|
|
@@ -3183,3 +3250,87 @@ export function C() {
|
|
|
3183
3250
|
}
|
|
3184
3251
|
})
|
|
3185
3252
|
})
|
|
3253
|
+
|
|
3254
|
+
// =============================================================================
|
|
3255
|
+
// #1966 — `/* @client */` defers ATTRIBUTE bindings (not just child/text)
|
|
3256
|
+
// =============================================================================
|
|
3257
|
+
//
|
|
3258
|
+
// Before #1966 the directive was honoured for JSX child/text expressions
|
|
3259
|
+
// but silently ignored on attribute initializers: a Go-unsupported
|
|
3260
|
+
// predicate in `data-x={/* @client */ pred(x)}` still got lowered, raising
|
|
3261
|
+
// BF101/BF102 even though the author had explicitly opted out of SSR. That
|
|
3262
|
+
// made the BF102 remediation ("defer it with /* @client */") misleading for
|
|
3263
|
+
// attribute-only reactive state (the Calendar case in #1467 / PR #1965).
|
|
3264
|
+
//
|
|
3265
|
+
// The fix carries the existing `attr.clientOnly` flag (already set in
|
|
3266
|
+
// jsx-to-ir, already honoured by the client-JS reactive-attribute path —
|
|
3267
|
+
// CSR template omits the attr and a mount effect sets it) through to the
|
|
3268
|
+
// adapter: `renderAttributes` skips SSR emission for `clientOnly` attrs, so
|
|
3269
|
+
// the unsupported-expression lowering is never reached.
|
|
3270
|
+
describe('GoTemplateAdapter - #1966 @client defers attribute bindings', () => {
|
|
3271
|
+
function compileAttr(attrExpr: string) {
|
|
3272
|
+
const adapter = new GoTemplateAdapter()
|
|
3273
|
+
const result = compileJSX(`
|
|
3274
|
+
"use client"
|
|
3275
|
+
import { createSignal } from "@barefootjs/client"
|
|
3276
|
+
export function C() {
|
|
3277
|
+
const [sel] = createSignal(0)
|
|
3278
|
+
const [s] = createSignal("a")
|
|
3279
|
+
const pred = (n: number) => sel() === n
|
|
3280
|
+
return <div data-x={${attrExpr}}>hi</div>
|
|
3281
|
+
}
|
|
3282
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
3283
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
3284
|
+
return { errors: result.errors ?? [], template }
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
test('Go-lowerable predicate: bare emits data-x; @client omits it from SSR', () => {
|
|
3288
|
+
// `sel() === 1` lowers to `{{eq .Sel 1}}`, so the bare form is valid
|
|
3289
|
+
// Go and emits the attribute — the directive is what removes it.
|
|
3290
|
+
const bare = compileAttr('pred(1)')
|
|
3291
|
+
expect(bare.errors).toEqual([])
|
|
3292
|
+
expect(bare.template).toContain('data-x=')
|
|
3293
|
+
|
|
3294
|
+
const deferred = compileAttr('/* @client */ pred(1)')
|
|
3295
|
+
expect(deferred.errors).toEqual([])
|
|
3296
|
+
// Server omits the attribute; the client patches it on hydrate.
|
|
3297
|
+
expect(deferred.template).not.toContain('data-x')
|
|
3298
|
+
})
|
|
3299
|
+
|
|
3300
|
+
test('Go-unsupported predicate: bare raises BF101; @client clears it and omits the attr', () => {
|
|
3301
|
+
const bare = compileAttr('/[0-9]/.test(s())')
|
|
3302
|
+
expect(bare.errors.some(e => e.code === 'BF101' || e.code === 'BF102')).toBe(true)
|
|
3303
|
+
|
|
3304
|
+
const deferred = compileAttr('/* @client */ /[0-9]/.test(s())')
|
|
3305
|
+
// No BF101/BF102 — the lowering is never reached for a deferred attr.
|
|
3306
|
+
expect(deferred.errors).toEqual([])
|
|
3307
|
+
expect(deferred.template).not.toContain('data-x')
|
|
3308
|
+
})
|
|
3309
|
+
|
|
3310
|
+
test('@client attribute inside a keyed .map() loop body is also deferred', () => {
|
|
3311
|
+
const adapter = new GoTemplateAdapter()
|
|
3312
|
+
const result = compileJSX(`
|
|
3313
|
+
"use client"
|
|
3314
|
+
import { createSignal } from "@barefootjs/client"
|
|
3315
|
+
type Day = { n: number }
|
|
3316
|
+
export function C() {
|
|
3317
|
+
const [sel] = createSignal(0)
|
|
3318
|
+
const pred = (d: Day) => sel() === d.n
|
|
3319
|
+
const days: Day[] = [{ n: 1 }, { n: 2 }]
|
|
3320
|
+
return (
|
|
3321
|
+
<div>
|
|
3322
|
+
{days.map((day: Day) => (
|
|
3323
|
+
<div key={day.n} data-x={/* @client */ pred(day)}>cell</div>
|
|
3324
|
+
))}
|
|
3325
|
+
</div>
|
|
3326
|
+
)
|
|
3327
|
+
}
|
|
3328
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
3329
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
3330
|
+
expect(result.errors ?? []).toEqual([])
|
|
3331
|
+
// The loop still renders (the array is a static const) but the deferred
|
|
3332
|
+
// attribute is absent from the emitted `<div>` cell.
|
|
3333
|
+
expect(template).toContain('range')
|
|
3334
|
+
expect(template).not.toContain('data-x')
|
|
3335
|
+
})
|
|
3336
|
+
})
|
|
@@ -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
|
+
})
|