@barefootjs/go-template 0.14.0 → 0.15.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/go-template-adapter.d.ts +295 -9
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +1282 -137
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1283 -136
- package/dist/index.js +1282 -137
- package/dist/test-render.d.ts +8 -0
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/build.test.ts +40 -1
- package/src/__tests__/derived-state-memo.test.ts +371 -0
- package/src/__tests__/go-template-adapter.test.ts +267 -7
- package/src/adapter/go-template-adapter.ts +2205 -188
- package/src/build.ts +4 -0
- package/src/test-render.ts +109 -27
package/dist/test-render.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface RenderOptions {
|
|
|
17
17
|
props?: Record<string, unknown>;
|
|
18
18
|
/** Additional component files (filename → source) */
|
|
19
19
|
components?: Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Pre-compiled child SSR modules (import specifier → path) — consumed
|
|
22
|
+
* by the Hono renderer; here only the KEYS matter (#1896): they carry
|
|
23
|
+
* EVERY specifier a sibling is reachable by (`@ui/components/ui/icon`
|
|
24
|
+
* and `../icon`), whereas `components` keys carry one per sibling, so
|
|
25
|
+
* the sibling-import recognition uses both.
|
|
26
|
+
*/
|
|
27
|
+
componentModules?: Record<string, string>;
|
|
20
28
|
/**
|
|
21
29
|
* Explicit component to render when `source` declares multiple
|
|
22
30
|
* exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,iBAAiB,CAAA;AAsBnE,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAgCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,eAAe,CAAA;IACxB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACzC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAyVvF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,12 +48,15 @@
|
|
|
48
48
|
"url": "https://github.com/piconic-ai/barefootjs",
|
|
49
49
|
"directory": "packages/adapter-go-template"
|
|
50
50
|
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@barefootjs/shared": "0.15.0"
|
|
53
|
+
},
|
|
51
54
|
"peerDependencies": {
|
|
52
55
|
"@barefootjs/jsx": ">=0.2.0",
|
|
53
56
|
"typescript": "^5.0.0"
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
59
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
57
|
-
"@barefootjs/jsx": "0.
|
|
60
|
+
"@barefootjs/jsx": "0.15.0"
|
|
58
61
|
}
|
|
59
62
|
}
|
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { deduplicateGoTypes } from '../build'
|
|
2
|
+
import { combineGoTypes, deduplicateGoTypes } from '../build'
|
|
3
|
+
|
|
4
|
+
describe('combineGoTypes stdlib imports', () => {
|
|
5
|
+
// The combined types file strips each component's own import block and
|
|
6
|
+
// rebuilds one header, pulling in stdlib packages only when the merged code
|
|
7
|
+
// references them. A `searchParams()`-backed constructor emits
|
|
8
|
+
// `strings.TrimRight(in.Base, "/")`, so the header must include "strings" or
|
|
9
|
+
// the generated file fails to compile (`undefined: strings`).
|
|
10
|
+
test('adds "strings" import when a constructor uses strings.*', () => {
|
|
11
|
+
const types = new Map<string, string>([
|
|
12
|
+
[
|
|
13
|
+
'Foo',
|
|
14
|
+
[
|
|
15
|
+
'package main',
|
|
16
|
+
'',
|
|
17
|
+
'import (',
|
|
18
|
+
'\t"strings"',
|
|
19
|
+
')',
|
|
20
|
+
'',
|
|
21
|
+
'func NewFooProps(in FooInput) FooProps {',
|
|
22
|
+
'\treturn FooProps{Root: strings.TrimRight(in.Base, "/")}',
|
|
23
|
+
'}',
|
|
24
|
+
].join('\n'),
|
|
25
|
+
],
|
|
26
|
+
])
|
|
27
|
+
const result = combineGoTypes({ types, packageName: 'main' })
|
|
28
|
+
expect(result).toContain('\t"strings"')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('omits "strings" import when unused', () => {
|
|
32
|
+
const types = new Map<string, string>([
|
|
33
|
+
[
|
|
34
|
+
'Foo',
|
|
35
|
+
['package main', '', 'func NewFooProps(in FooInput) FooProps {', '\treturn FooProps{}', '}'].join('\n'),
|
|
36
|
+
],
|
|
37
|
+
])
|
|
38
|
+
const result = combineGoTypes({ types, packageName: 'main' })
|
|
39
|
+
expect(result).not.toContain('"strings"')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
3
42
|
|
|
4
43
|
describe('deduplicateGoTypes', () => {
|
|
5
44
|
test('deduplicates NewXxxProps with single-line doc comment', () => {
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived-state SSR for the Go adapter (PostList blocker, #1897 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* Capability A: an object-returning block-body memo that derives from
|
|
5
|
+
* `searchParams()` must compute a NON-nil `map[string]interface{}` in
|
|
6
|
+
* `NewXxxProps`, so the template's `.Params.Sort` / `.Params.Tag` field
|
|
7
|
+
* accesses resolve at execute time instead of reading a nil map.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
|
|
11
|
+
import { GoTemplateAdapter } from '../adapter/go-template-adapter'
|
|
12
|
+
|
|
13
|
+
function generate(src: string) {
|
|
14
|
+
const adapter = new GoTemplateAdapter()
|
|
15
|
+
const result = compileJSX(src.trimStart(), 'T.tsx', { adapter, outputIR: true })
|
|
16
|
+
const irFile = result.files.find(f => f.type === 'ir')
|
|
17
|
+
if (!irFile) throw new Error('no IR')
|
|
18
|
+
const ir = JSON.parse(irFile.content) as ComponentIR
|
|
19
|
+
return adapter.generate(ir)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('Capability A: object-returning searchParams memo → computed map field', () => {
|
|
23
|
+
const SRC = `
|
|
24
|
+
'use client'
|
|
25
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
26
|
+
const SORT_KEYS = ['date', 'title', 'tag']
|
|
27
|
+
const asSortKey = (raw) => (SORT_KEYS.includes(raw) ? raw : 'date')
|
|
28
|
+
export function P() {
|
|
29
|
+
const params = createMemo(() => {
|
|
30
|
+
const sp = searchParams()
|
|
31
|
+
return { sort: asSortKey(sp.get('sort')), tag: sp.get('tag') ?? '' }
|
|
32
|
+
})
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<span>{params().sort}</span>
|
|
36
|
+
<span>{params().tag}</span>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
test('NewProps computes the map instead of nil', () => {
|
|
43
|
+
const { types } = generate(SRC)
|
|
44
|
+
expect(types).not.toContain('Params: nil')
|
|
45
|
+
expect(types).toContain('Params: map[string]interface{}{')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('object keys are capitalized to match template field access', () => {
|
|
49
|
+
const { types } = generate(SRC)
|
|
50
|
+
expect(types).toContain('"Sort":')
|
|
51
|
+
expect(types).toContain('"Tag":')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('searchParams().get is lowered to the SSR SearchParams field', () => {
|
|
55
|
+
const { types } = generate(SRC)
|
|
56
|
+
expect(types).toContain('in.SearchParams.Get("sort")')
|
|
57
|
+
expect(types).toContain('in.SearchParams.Get("tag")')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('module helper asSortKey is inlined with bf.Includes over the const array', () => {
|
|
61
|
+
const { types } = generate(SRC)
|
|
62
|
+
expect(types).toContain('bf.Includes([]string{"date", "title", "tag"}')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('template still reads .Params.Sort / .Params.Tag (unchanged field access)', () => {
|
|
66
|
+
const { template } = generate(SRC)
|
|
67
|
+
expect(template).toContain('.Params.Sort')
|
|
68
|
+
expect(template).toContain('.Params.Tag')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// A ternary whose condition is string-valued (`sp.get('tag') ? … : …`) is
|
|
72
|
+
// truthy in JS but `if "<string>"` does not compile in Go. The lowerer must
|
|
73
|
+
// fall back to nil rather than emit invalid code (#1941 review).
|
|
74
|
+
test('string-valued ternary condition falls back to nil, never invalid Go', () => {
|
|
75
|
+
const src = `
|
|
76
|
+
'use client'
|
|
77
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
78
|
+
export function P() {
|
|
79
|
+
const params = createMemo(() => {
|
|
80
|
+
const sp = searchParams()
|
|
81
|
+
return { label: sp.get('tag') ? 'has' : 'none' }
|
|
82
|
+
})
|
|
83
|
+
return <div>{params().label}</div>
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
const { types } = generate(src)
|
|
87
|
+
expect(types).toContain('Params: nil')
|
|
88
|
+
// Must NOT emit a string as a Go bool condition.
|
|
89
|
+
expect(types).not.toContain('if in.SearchParams.Get')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// A block with control flow (an early `return` inside an `if`, plus the final
|
|
93
|
+
// return) must fall back to nil — not silently lower the final return as if it
|
|
94
|
+
// were unconditional, which would change SSR semantics (#1941 review).
|
|
95
|
+
test('block-body memo with control flow falls back to nil', () => {
|
|
96
|
+
const src = `
|
|
97
|
+
'use client'
|
|
98
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
99
|
+
export function P() {
|
|
100
|
+
const params = createMemo(() => {
|
|
101
|
+
const sp = searchParams()
|
|
102
|
+
if (sp.get('x')) return { sort: 'early' }
|
|
103
|
+
return { sort: sp.get('sort') ?? '' }
|
|
104
|
+
})
|
|
105
|
+
return <div>{params().sort}</div>
|
|
106
|
+
}
|
|
107
|
+
`
|
|
108
|
+
const { types } = generate(src)
|
|
109
|
+
expect(types).toContain('Params: nil')
|
|
110
|
+
expect(types).not.toContain('Params: map[string]interface{}{')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('Capability B: inline local pure helper calls at attribute call sites', () => {
|
|
115
|
+
test('sortClass(k) inlines to a conditional, not a .SortClass method call', () => {
|
|
116
|
+
const src = `
|
|
117
|
+
'use client'
|
|
118
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
119
|
+
const SORT_KEYS = ['date', 'title', 'tag']
|
|
120
|
+
const asSortKey = (raw) => (SORT_KEYS.includes(raw) ? raw : 'date')
|
|
121
|
+
export function P() {
|
|
122
|
+
const params = createMemo(() => {
|
|
123
|
+
const sp = searchParams()
|
|
124
|
+
return { sort: asSortKey(sp.get('sort')), tag: sp.get('tag') ?? '' }
|
|
125
|
+
})
|
|
126
|
+
const sortClass = (k) => (params().sort === k ? 'sort on' : 'sort')
|
|
127
|
+
return <a className={sortClass('date')}>date</a>
|
|
128
|
+
}
|
|
129
|
+
`
|
|
130
|
+
const { template } = generate(src)
|
|
131
|
+
expect(template).not.toContain('.SortClass')
|
|
132
|
+
// `.Params.Sort` is interface{} (a map value), so `eq` coerces it via
|
|
133
|
+
// `bf_string` before comparing to the string literal.
|
|
134
|
+
expect(template).toContain(
|
|
135
|
+
'class="{{if eq (bf_string .Params.Sort) "date"}}sort on{{else}}sort{{end}}"',
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('tagClass(t) inlines inside a loop, resolving the loop var and root memo', () => {
|
|
140
|
+
const src = `
|
|
141
|
+
'use client'
|
|
142
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
143
|
+
export function P(props: { tags: string[] }) {
|
|
144
|
+
const params = createMemo(() => {
|
|
145
|
+
const sp = searchParams()
|
|
146
|
+
return { tag: sp.get('tag') ?? '' }
|
|
147
|
+
})
|
|
148
|
+
const tagClass = (t) => (params().tag === t ? 'tag on' : 'tag')
|
|
149
|
+
return <div>{props.tags.map((t) => <a key={t} className={tagClass(t)}>#{t}</a>)}</div>
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
const { template } = generate(src)
|
|
153
|
+
expect(template).not.toContain('.TagClass')
|
|
154
|
+
// params() is a root memo (→ $.Params) and t is the loop var (→ .)
|
|
155
|
+
expect(template).toContain('{{if eq $.Params.Tag .}}tag on{{else}}tag{{end}}')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('a helper that delegates to a non-URL-builder local helper is not inlined', () => {
|
|
159
|
+
const src = `
|
|
160
|
+
'use client'
|
|
161
|
+
import { createSignal } from '@barefootjs/client'
|
|
162
|
+
export function P() {
|
|
163
|
+
const [sig] = createSignal('x')
|
|
164
|
+
const label = (k) => (sig() === k ? 'on' : 'off')
|
|
165
|
+
const wrap = (k) => '[' + label(k) + ']'
|
|
166
|
+
return <a className={wrap('y')}>x</a>
|
|
167
|
+
}
|
|
168
|
+
`
|
|
169
|
+
const { template } = generate(src)
|
|
170
|
+
// wrap delegates to label (a local helper) and isn't a URL builder → not
|
|
171
|
+
// inlined; falls back to the method-call form.
|
|
172
|
+
expect(template).toContain('.Wrap')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// A compound argument must keep its precedence when spliced into the body —
|
|
176
|
+
// `sig() === <param>` with arg `a ?? b` must not become `sig() === a ?? b`
|
|
177
|
+
// (#1943 review). The substituted arg is parenthesized.
|
|
178
|
+
test('compound call argument is parenthesized (precedence preserved)', () => {
|
|
179
|
+
const src = `
|
|
180
|
+
'use client'
|
|
181
|
+
import { createSignal } from '@barefootjs/client'
|
|
182
|
+
export function P(props: { a?: string; b?: string }) {
|
|
183
|
+
const [sig] = createSignal('x')
|
|
184
|
+
const cls = (k) => (sig() === k ? 'on' : 'off')
|
|
185
|
+
return <a className={cls(props.a ?? props.b)}>x</a>
|
|
186
|
+
}
|
|
187
|
+
`
|
|
188
|
+
const { template } = generate(src)
|
|
189
|
+
expect(template).not.toContain('.Cls')
|
|
190
|
+
// `===` must stay the outer operation (`eq .Sig …`). Without parenthesizing
|
|
191
|
+
// the arg, `sig() === props.a ?? props.b` would bind as
|
|
192
|
+
// `(sig() === props.a) ?? props.b` → an outer `{{if or …}}`. This matches
|
|
193
|
+
// what a direct `sig() === (props.a ?? props.b)` lowers to.
|
|
194
|
+
expect(template).toContain('{{if eq .Sig')
|
|
195
|
+
expect(template).not.toContain('{{if or')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// The splicer is scope-blind, so a helper whose body contains a nested
|
|
199
|
+
// function is NOT inlined (avoids shadowing / param-position corruption) —
|
|
200
|
+
// it falls back to the method-call form (#1943 review).
|
|
201
|
+
test('helper with a nested function scope is not inlined', () => {
|
|
202
|
+
const src = `
|
|
203
|
+
'use client'
|
|
204
|
+
import { createSignal } from '@barefootjs/client'
|
|
205
|
+
export function P(props: { xs: string[] }) {
|
|
206
|
+
const [sig] = createSignal('x')
|
|
207
|
+
const has = (k) => (props.xs.some((x) => x === k) ? 'on' : 'off')
|
|
208
|
+
return <a className={has('y')}>x</a>
|
|
209
|
+
}
|
|
210
|
+
`
|
|
211
|
+
const { template } = generate(src)
|
|
212
|
+
// Not inlined → stays as the (un-backed) method-call form.
|
|
213
|
+
expect(template).toContain('.Has')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('Capability C2: URL-builder helpers → bf_query + derived Root field', () => {
|
|
218
|
+
const SRC = `
|
|
219
|
+
'use client'
|
|
220
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
221
|
+
export function P(props: { base: string }) {
|
|
222
|
+
const params = createMemo(() => {
|
|
223
|
+
const sp = searchParams()
|
|
224
|
+
return { sort: sp.get('sort') ?? '', tag: sp.get('tag') ?? '' }
|
|
225
|
+
})
|
|
226
|
+
const base = (props.base ?? '').replace(/\\/+$/, '')
|
|
227
|
+
const root = base || '/'
|
|
228
|
+
const hrefFor = (sort: string, tag: string) => {
|
|
229
|
+
const u = new URLSearchParams()
|
|
230
|
+
if (sort !== 'date') u.set('sort', sort)
|
|
231
|
+
if (tag) u.set('tag', tag)
|
|
232
|
+
const s = u.toString()
|
|
233
|
+
return s ? \`\${root}?\${s}\` : root
|
|
234
|
+
}
|
|
235
|
+
const sortHref = (k) => hrefFor(k, params().tag)
|
|
236
|
+
const tagHref = (t) => hrefFor(params().sort, t)
|
|
237
|
+
return (
|
|
238
|
+
<div>
|
|
239
|
+
<a href={sortHref('title')}>s</a>
|
|
240
|
+
{props.base ? <a href={tagHref('go')}>t</a> : null}
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
`
|
|
245
|
+
|
|
246
|
+
test('sortHref/tagHref lower to bf_query, not a .SortHref method call', () => {
|
|
247
|
+
const { template } = generate(SRC)
|
|
248
|
+
expect(template).not.toContain('.SortHref')
|
|
249
|
+
expect(template).not.toContain('.TagHref')
|
|
250
|
+
expect(template).toContain('bf_query .Root')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('guarded set() calls become bool include triples', () => {
|
|
254
|
+
const { template } = generate(SRC)
|
|
255
|
+
// sort !== 'date' guard (runtime sort case via tagHref) + tag truthiness.
|
|
256
|
+
expect(template).toContain('(ne (bf_string .Params.Sort) "date") "sort"')
|
|
257
|
+
expect(template).toContain('(ne .Params.Tag "") "tag"')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('derived `root` const becomes a computed Root field', () => {
|
|
261
|
+
const { types } = generate(SRC)
|
|
262
|
+
expect(types).toContain('Root string `json:"-"`')
|
|
263
|
+
expect(types).toContain(
|
|
264
|
+
'Root: func() string { v := strings.TrimRight(in.Base, "/"); if v != "" { return v }; return "/" }(),',
|
|
265
|
+
)
|
|
266
|
+
expect(types).toContain('"strings"')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// A `&&` guard is NOT a Go bool (Go's `and` returns an operand); it must be
|
|
270
|
+
// truthiness-wrapped so `bf_query`'s `include` receives a real bool (#1945 review).
|
|
271
|
+
test('a && guard is wrapped to a bool, not passed as bare `and`', () => {
|
|
272
|
+
const src = `
|
|
273
|
+
'use client'
|
|
274
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
275
|
+
export function P(props: { base: string }) {
|
|
276
|
+
const params = createMemo(() => { const sp = searchParams(); return { tag: sp.get('tag') ?? '' } })
|
|
277
|
+
const root = (props.base ?? '') || '/'
|
|
278
|
+
const hrefFor = (sort: string, tag: string) => {
|
|
279
|
+
const u = new URLSearchParams()
|
|
280
|
+
if (sort && tag) u.set('both', sort)
|
|
281
|
+
return u.toString() ? \`\${root}?\${u}\` : root
|
|
282
|
+
}
|
|
283
|
+
const h = (k) => hrefFor(k, params().tag)
|
|
284
|
+
return <a href={h('x')}>x</a>
|
|
285
|
+
}
|
|
286
|
+
`
|
|
287
|
+
const { template } = generate(src)
|
|
288
|
+
expect(template).toContain('(ne (and "x" .Params.Tag) "")')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// A URL-builder helper whose return isn't the conditional with-query/base
|
|
292
|
+
// shape must not be lowered to bf_query (#1945 review) — it falls back to the
|
|
293
|
+
// method-call form.
|
|
294
|
+
test('a builder returning a non-conditional shape is not lowered to bf_query', () => {
|
|
295
|
+
const src = `
|
|
296
|
+
'use client'
|
|
297
|
+
import { searchParams } from '@barefootjs/client'
|
|
298
|
+
export function P() {
|
|
299
|
+
const bad = (sort: string) => { const u = new URLSearchParams(); u.set('s', sort); return u.toString() }
|
|
300
|
+
return <a href={bad('x')}>x</a>
|
|
301
|
+
}
|
|
302
|
+
`
|
|
303
|
+
const { template } = generate(src)
|
|
304
|
+
expect(template).not.toContain('bf_query')
|
|
305
|
+
expect(template).toContain('.Bad')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// A non-string derived const referenced by the template must not be emitted
|
|
309
|
+
// as a `string` field with a non-string initializer (#1945 review).
|
|
310
|
+
test('a numeric derived const is not emitted as a string field', () => {
|
|
311
|
+
const src = `
|
|
312
|
+
'use client'
|
|
313
|
+
export function P(props: { count: number }) {
|
|
314
|
+
const n = props.count + 1
|
|
315
|
+
return <span>{n}</span>
|
|
316
|
+
}
|
|
317
|
+
`
|
|
318
|
+
const { types } = generate(src)
|
|
319
|
+
expect(types).not.toContain('N string')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// `||`/`??` evaluate to one operand, so a non-string left makes the result
|
|
323
|
+
// non-string even when the right is a string literal (#1945 review).
|
|
324
|
+
test('a `?? ""` over a non-string is not emitted as a string field', () => {
|
|
325
|
+
const src = `
|
|
326
|
+
'use client'
|
|
327
|
+
export function P(props: { count: number }) {
|
|
328
|
+
const c = props.count ?? ''
|
|
329
|
+
return <span>{c}</span>
|
|
330
|
+
}
|
|
331
|
+
`
|
|
332
|
+
const { types } = generate(src)
|
|
333
|
+
expect(types).not.toContain('C string')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('Capability D: array-memo .length → handler-filled loop slice count', () => {
|
|
338
|
+
test('visible().length lowers to len .<Slice>, not the nil memo field', () => {
|
|
339
|
+
const src = `
|
|
340
|
+
'use client'
|
|
341
|
+
import { createMemo, searchParams } from '@barefootjs/client'
|
|
342
|
+
import { Row } from './Row'
|
|
343
|
+
export function P(props: { items: { id: string }[] }) {
|
|
344
|
+
const params = createMemo(() => {
|
|
345
|
+
const sp = searchParams()
|
|
346
|
+
return { tag: sp.get('tag') ?? '' }
|
|
347
|
+
})
|
|
348
|
+
const visible = createMemo(() => {
|
|
349
|
+
const { tag } = params()
|
|
350
|
+
return props.items.filter((p) => !tag || p.id === tag)
|
|
351
|
+
})
|
|
352
|
+
return (
|
|
353
|
+
<div>
|
|
354
|
+
<span>{visible().length} / {props.items.length} shown</span>
|
|
355
|
+
<ul>
|
|
356
|
+
{visible().map((p) => (
|
|
357
|
+
<Row key={p.id} id={p.id} />
|
|
358
|
+
))}
|
|
359
|
+
</ul>
|
|
360
|
+
</div>
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
`
|
|
364
|
+
const { template } = generate(src)
|
|
365
|
+
expect(template).not.toContain('len .Visible')
|
|
366
|
+
// The loop over visible() is handler-filled as `.Rows`; the count reuses it.
|
|
367
|
+
expect(template).toContain('len .Rows')
|
|
368
|
+
// props.items.length is unaffected.
|
|
369
|
+
expect(template).toContain('len .Items')
|
|
370
|
+
})
|
|
371
|
+
})
|