@barefootjs/go-template 0.14.0 → 0.15.1

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.
@@ -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;AAqBnE,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;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA6RvF"}
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.14.0",
3
+ "version": "0.15.1",
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.1"
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.14.0"
60
+ "@barefootjs/jsx": "0.15.1"
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
+ })