@barefootjs/jsx 0.3.0 → 0.5.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.
@@ -474,7 +474,133 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-
474
474
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
475
475
  `;
476
476
 
477
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L75(no label) 1`] = `
477
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L69 Multi-key: sort by price, break ties by name 1`] = `
478
+ "import { $, $t, createComponent, createEffect, createSignal, hydrate, initChild, mapArray, renderChild } from '@barefootjs/client/runtime'
479
+
480
+ export function initTodoItem(__scope, _p = {}) {
481
+ if (!__scope) return
482
+ const __scopeId = __scope.getAttribute('bf-s')
483
+
484
+ const [_s0] = $t(__scope, 's0')
485
+
486
+ createEffect(() => {
487
+ const __val = String(_p.todo)
488
+ if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
489
+ })
490
+
491
+ }
492
+
493
+ hydrate('TodoItem__b3c36eee', { init: initTodoItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.todo)}<!--/--></li>\` })
494
+ export function TodoItem(_p, __bfKey) { return createComponent('TodoItem__b3c36eee', _p, __bfKey) }
495
+ export function initItem(__scope, _p = {}) {
496
+ if (!__scope) return
497
+ const __scopeId = __scope.getAttribute('bf-s')
498
+
499
+ const [_s0] = $t(__scope, 's0')
500
+
501
+ createEffect(() => {
502
+ const __val = String(_p.item)
503
+ if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
504
+ })
505
+
506
+ }
507
+
508
+ hydrate('Item__b3c36eee', { init: initItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.item)}<!--/--></li>\` })
509
+ export function Item(_p, __bfKey) { return createComponent('Item__b3c36eee', _p, __bfKey) }
510
+ function initDashboard() {}
511
+
512
+ hydrate('Dashboard__b3c36eee', { init: initDashboard, template: (_p) => \`<div>D</div>\` })
513
+ export function Dashboard(_p, __bfKey) { return createComponent('Dashboard__b3c36eee', _p, __bfKey) }
514
+ export function initExample(__scope, _p = {}) {
515
+ if (!__scope) return
516
+ const __scopeId = __scope.getAttribute('bf-s')
517
+
518
+ const children = _p.children
519
+ const [count, setCount] = createSignal(0)
520
+ const [isLoggedIn] = createSignal(false)
521
+ const [todos] = createSignal([])
522
+ const [items] = createSignal([])
523
+ const [filter] = createSignal('all')
524
+ const [accepted] = createSignal(false)
525
+ const [text, setText] = createSignal('')
526
+
527
+ const [_s1] = $(__scope, 's1')
528
+
529
+ mapArray(() => items().toSorted((a, b) => a.price - b.price || a.name.localeCompare(b.name)), _s1, (item) => String(item.id), (item, __idx, __existing) => {
530
+ if (__existing) { initChild('Item__b3c36eee', __existing, { get item() { return item() } }); return __existing }
531
+ return createComponent('Item__b3c36eee', { get item() { return item() } }, item().id)
532
+ }, 'l0')
533
+
534
+ }
535
+
536
+ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).toSorted((a, b) => a.price - b.price || a.name.localeCompare(b.name)).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
537
+ export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
538
+ `;
539
+
540
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L74 — ✅ Relational ternary 1`] = `
541
+ "import { $, $t, createComponent, createEffect, createSignal, hydrate, initChild, mapArray, renderChild } from '@barefootjs/client/runtime'
542
+
543
+ export function initTodoItem(__scope, _p = {}) {
544
+ if (!__scope) return
545
+ const __scopeId = __scope.getAttribute('bf-s')
546
+
547
+ const [_s0] = $t(__scope, 's0')
548
+
549
+ createEffect(() => {
550
+ const __val = String(_p.todo)
551
+ if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
552
+ })
553
+
554
+ }
555
+
556
+ hydrate('TodoItem__b3c36eee', { init: initTodoItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.todo)}<!--/--></li>\` })
557
+ export function TodoItem(_p, __bfKey) { return createComponent('TodoItem__b3c36eee', _p, __bfKey) }
558
+ export function initItem(__scope, _p = {}) {
559
+ if (!__scope) return
560
+ const __scopeId = __scope.getAttribute('bf-s')
561
+
562
+ const [_s0] = $t(__scope, 's0')
563
+
564
+ createEffect(() => {
565
+ const __val = String(_p.item)
566
+ if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
567
+ })
568
+
569
+ }
570
+
571
+ hydrate('Item__b3c36eee', { init: initItem, template: (_p) => \`<li bf="s1"><!--bf:s0-->\${String(_p.item)}<!--/--></li>\` })
572
+ export function Item(_p, __bfKey) { return createComponent('Item__b3c36eee', _p, __bfKey) }
573
+ function initDashboard() {}
574
+
575
+ hydrate('Dashboard__b3c36eee', { init: initDashboard, template: (_p) => \`<div>D</div>\` })
576
+ export function Dashboard(_p, __bfKey) { return createComponent('Dashboard__b3c36eee', _p, __bfKey) }
577
+ export function initExample(__scope, _p = {}) {
578
+ if (!__scope) return
579
+ const __scopeId = __scope.getAttribute('bf-s')
580
+
581
+ const children = _p.children
582
+ const [count, setCount] = createSignal(0)
583
+ const [isLoggedIn] = createSignal(false)
584
+ const [todos] = createSignal([])
585
+ const [items] = createSignal([])
586
+ const [filter] = createSignal('all')
587
+ const [accepted] = createSignal(false)
588
+ const [text, setText] = createSignal('')
589
+
590
+ const [_s1] = $(__scope, 's1')
591
+
592
+ mapArray(() => items().toSorted((a, b) => a.price > b.price ? 1 : -1), _s1, (item) => String(item.id), (item, __idx, __existing) => {
593
+ if (__existing) { initChild('Item__b3c36eee', __existing, { get item() { return item() } }); return __existing }
594
+ return createComponent('Item__b3c36eee', { get item() { return item() } }, item().id)
595
+ }, 'l0')
596
+
597
+ }
598
+
599
+ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).toSorted((a, b) => a.price > b.price ? 1 : -1).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
600
+ export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
601
+ `;
602
+
603
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L85 — (no label) 1`] = `
478
604
  "import { $, $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
479
605
 
480
606
  export function initTodoItem(__scope, _p = {}) {
@@ -536,7 +662,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div><button bf="s0"
536
662
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
537
663
  `;
538
664
 
539
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L121 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
665
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L131 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
540
666
  "import { $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
541
667
 
542
668
  export function initTodoItem(__scope, _p = {}) {
@@ -599,7 +725,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
599
725
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
600
726
  `;
601
727
 
602
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L124 — ✅ Use /* @client */ 1`] = `
728
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L134 — ✅ Use /* @client */ 1`] = `
603
729
  "import { $t, createComponent, createEffect, createSignal, hydrate, updateClientMarker } from '@barefootjs/client/runtime'
604
730
 
605
731
  export function initTodoItem(__scope, _p = {}) {
@@ -660,7 +786,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-
660
786
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
661
787
  `;
662
788
 
663
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L141 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
789
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L151 — ❌ BF101 on Go/Mojo; works on Hono 1`] = `
664
790
  "import { $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
665
791
 
666
792
  export function initTodoItem(__scope, _p = {}) {
@@ -723,7 +849,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
723
849
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
724
850
  `;
725
851
 
726
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L144 — ✅ Use arrow functions for adapter portability 1`] = `
852
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L154 — ✅ Use arrow functions for adapter portability 1`] = `
727
853
  "import { $t, createComponent, createEffect, createSignal, hydrate } from '@barefootjs/client/runtime'
728
854
 
729
855
  export function initTodoItem(__scope, _p = {}) {
@@ -786,7 +912,7 @@ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf:
786
912
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
787
913
  `;
788
914
 
789
- exports[`docs/core/rendering/jsx-compatibility.md doc-examples L158 — ✅ Use /* @client */ 1`] = `
915
+ exports[`docs/core/rendering/jsx-compatibility.md doc-examples L168 — ✅ Use /* @client */ 1`] = `
790
916
  "import { $, $t, createComponent, createEffect, createSignal, hydrate, initChild, mapArray, renderChild } from '@barefootjs/client/runtime'
791
917
 
792
918
  export function initTodoItem(__scope, _p = {}) {
@@ -838,14 +964,14 @@ export function initExample(__scope, _p = {}) {
838
964
 
839
965
  const [_s1] = $(__scope, 's1')
840
966
 
841
- mapArray(() => items().sort((a, b) => a.name > b.name ? 1 : -1), _s1, (item) => String(item.id), (item, __idx, __existing) => {
967
+ mapArray(() => items().sort((a, b) => { const an = a.name; return an > b.name ? 1 : -1 }), _s1, (item) => String(item.id), (item, __idx, __existing) => {
842
968
  if (__existing) { initChild('Item__b3c36eee', __existing, { get item() { return item() } }); return __existing }
843
969
  return createComponent('Item__b3c36eee', { get item() { return item() } }, item().id)
844
970
  }, 'l0')
845
971
 
846
972
  }
847
973
 
848
- hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).sort((a, b) => a.name > b.name ? 1 : -1).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
974
+ hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s1"><!--bf-loop:l0-->\${([]).sort((a, b) => { const an = a.name; return an > b.name ? 1 : -1 }).map((item) => \`\${renderChild('Item__b3c36eee', {item: item}, item.id)}\`).join('')}<!--bf-/loop:l0--></div>\` })
849
975
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
850
976
  `;
851
977
 
@@ -201,3 +201,84 @@ describe('auto-defer brand-package reactive bindings (#1638)', () => {
201
201
  expect(templateBody).toContain('data-count=')
202
202
  })
203
203
  })
204
+
205
+ describe('client hydrate template defers brand conditionals (#1645)', () => {
206
+ // The `template: (_p) => ...` lambda runs at module scope when the
207
+ // component is client-rendered via `createComponent` (not when hydrating
208
+ // existing SSR DOM). It cannot reproduce per-instance `createForm` state,
209
+ // so an auto-deferred conditional must emit empty cond markers — exactly
210
+ // like the SSR adapter — and let `init`'s `insert()` populate the branch.
211
+
212
+ test('non-inlinable createForm (onSubmit): no undefined.field, emits cond markers', () => {
213
+ const { templateBody } = compileWithBrand(`
214
+ 'use client'
215
+ import { createForm } from './_form-defs'
216
+
217
+ export function SignupForm() {
218
+ const form = createForm({
219
+ onSubmit: async (data) => { await fetch('/signup', { method: 'POST', body: JSON.stringify(data) }) },
220
+ })
221
+ const email = form.field('email')
222
+ return (
223
+ <form>
224
+ <input value={email.value()} />
225
+ {email.error() && <p>{email.error()}</p>}
226
+ </form>
227
+ )
228
+ }
229
+ `)
230
+
231
+ // Never re-derive the form at module scope: `undefined.field(...)` throws.
232
+ expect(templateBody).not.toContain('undefined.field')
233
+ expect(templateBody).not.toContain('.field(')
234
+ // The deferred conditional collapses to the same empty markers SSR emits.
235
+ expect(templateBody).toMatch(/<!--bf-cond-start:s\d+--><!--bf-cond-end:s\d+-->/)
236
+ })
237
+
238
+ test('inlinable createForm (no onSubmit): no re-inlined createForm in template', () => {
239
+ const { templateBody } = compileWithBrand(`
240
+ 'use client'
241
+ import { createForm } from './_form-defs'
242
+
243
+ export function SignupForm() {
244
+ const form = createForm({ defaultValues: { email: '' } })
245
+ const email = form.field('email')
246
+ return (
247
+ <form>
248
+ <input value={email.value()} />
249
+ {email.error() && <p>{email.error()}</p>}
250
+ </form>
251
+ )
252
+ }
253
+ `)
254
+
255
+ // A re-inlined `createForm({...})` would build a throwaway instance on
256
+ // every template render (error always '', never the live instance).
257
+ expect(templateBody).not.toContain('createForm(')
258
+ expect(templateBody).not.toContain('undefined.field')
259
+ expect(templateBody).toMatch(/<!--bf-cond-start:s\d+--><!--bf-cond-end:s\d+-->/)
260
+ })
261
+
262
+ test('init still wires the deferred conditional via insert()', () => {
263
+ const { initBody } = compileWithBrand(`
264
+ 'use client'
265
+ import { createForm } from './_form-defs'
266
+
267
+ export function SignupForm() {
268
+ const form = createForm()
269
+ const email = form.field('email')
270
+ return (
271
+ <form>
272
+ <input value={email.value()} />
273
+ {email.error() && <p>{email.error()}</p>}
274
+ </form>
275
+ )
276
+ }
277
+ `)
278
+
279
+ // The reactive binding lives in init (where `email`/`form` are in scope),
280
+ // not in the module-scope template lambda.
281
+ expect(initBody).toMatch(/insert\(/)
282
+ expect(initBody).toContain('email.error()')
283
+ })
284
+ })
@@ -729,4 +729,80 @@ describe('child components inside .map() (#344)', () => {
729
729
  expect(content).toContain('children[__idx]')
730
730
  expect(content).not.toContain('children[__idx + ')
731
731
  })
732
+
733
+ test('nested .map() with multiple inner components emits unique __compEl bindings (#1664)', () => {
734
+ const source = `
735
+ 'use client'
736
+
737
+ export function Picker() {
738
+ const GROUPS = [
739
+ { id: 'a', items: [{ id: 'x', label: 'X' }] },
740
+ ]
741
+ return (
742
+ <div>
743
+ {GROUPS.map(group => (
744
+ <div key={group.id}>
745
+ {group.items.map(it => (
746
+ <div key={it.id}>
747
+ <SelectItem value={it.id}>{it.label}</SelectItem>
748
+ <SelectIcon name={it.id} />
749
+ </div>
750
+ ))}
751
+ </div>
752
+ ))}
753
+ </div>
754
+ )
755
+ }
756
+ `
757
+ const result = compileJSX(source, 'Picker.tsx', { adapter })
758
+ expect(result.errors).toHaveLength(0)
759
+
760
+ const clientJs = result.files.find(f => f.type === 'clientJs')
761
+ expect(clientJs).toBeDefined()
762
+ const content = clientJs!.content
763
+
764
+ // Both inner-loop components must be initialised.
765
+ expect(content).toContain("initChild('SelectItem'")
766
+ expect(content).toContain("initChild('SelectIcon'")
767
+
768
+ // No re-declaration of `__compEl` in the shared inner-forEach scope:
769
+ // each comp must use a uniquely-suffixed binding.
770
+ expect(content).toContain('__compEl0')
771
+ expect(content).toContain('__compEl1')
772
+
773
+ // The bug threw "Identifier '__compEl' has already been declared" — the
774
+ // unsuffixed binding must not appear when multiple comps share a scope.
775
+ expect(content).not.toContain('const __compEl =')
776
+ })
777
+
778
+ test('nested .map() with a single inner component keeps the plain __compEl binding (#1664)', () => {
779
+ const source = `
780
+ 'use client'
781
+
782
+ export function Picker() {
783
+ const GROUPS = [
784
+ { id: 'a', items: [{ id: 'x', label: 'X' }] },
785
+ ]
786
+ return (
787
+ <div>
788
+ {GROUPS.map(group => (
789
+ <div key={group.id}>
790
+ {group.items.map(it => (
791
+ <SelectItem key={it.id} value={it.id}>{it.label}</SelectItem>
792
+ ))}
793
+ </div>
794
+ ))}
795
+ </div>
796
+ )
797
+ }
798
+ `
799
+ const result = compileJSX(source, 'Picker.tsx', { adapter })
800
+ expect(result.errors).toHaveLength(0)
801
+
802
+ const content = result.files.find(f => f.type === 'clientJs')!.content
803
+ expect(content).toContain("initChild('SelectItem'")
804
+ // Single comp keeps the unsuffixed name.
805
+ expect(content).toContain('const __compEl =')
806
+ expect(content).not.toContain('__compEl0')
807
+ })
732
808
  })
@@ -0,0 +1,75 @@
1
+ /**
2
+ * renderImportMapHtml tests
3
+ *
4
+ * The shared importmap-snippet renderer turns a parsed `barefoot-externals.json`
5
+ * into the `<script type="importmap">` (+ `<link rel="modulepreload">`) HTML that
6
+ * `bf build` emits as `barefoot-importmap.html` for template-string adapters
7
+ * (issue #1644). This is the single source of truth for that snippet.
8
+ */
9
+ import { describe, test, expect } from 'bun:test'
10
+ import { renderImportMapHtml } from '../import-map'
11
+
12
+ function parseImportMap(html: string): Record<string, string> {
13
+ const match = html.match(/<script type="importmap">(.*?)<\/script>/s)
14
+ if (!match) throw new Error(`no importmap in: ${html}`)
15
+ // Decode the < escape the renderer applies before parsing.
16
+ return JSON.parse(match[1]).imports
17
+ }
18
+
19
+ describe('renderImportMapHtml', () => {
20
+ test('emits the manifest importmap imports verbatim', () => {
21
+ const html = renderImportMapHtml({
22
+ importmap: {
23
+ imports: {
24
+ '@barefootjs/client': '/components/barefoot.js',
25
+ '@barefootjs/client/runtime': '/components/barefoot.js',
26
+ zod: 'https://esm.sh/zod@4.4.3',
27
+ },
28
+ },
29
+ preloads: [],
30
+ })
31
+ expect(parseImportMap(html)).toEqual({
32
+ '@barefootjs/client': '/components/barefoot.js',
33
+ '@barefootjs/client/runtime': '/components/barefoot.js',
34
+ zod: 'https://esm.sh/zod@4.4.3',
35
+ })
36
+ expect(html).not.toContain('modulepreload')
37
+ })
38
+
39
+ test('emits modulepreload links with crossorigin for manifest preloads (#1648)', () => {
40
+ const html = renderImportMapHtml({
41
+ importmap: { imports: {} },
42
+ preloads: ['/components/form.js', 'https://esm.sh/zod@4.4.3'],
43
+ })
44
+ expect(html).toContain('<link rel="modulepreload" href="/components/form.js" crossorigin>')
45
+ expect(html).toContain('<link rel="modulepreload" href="https://esm.sh/zod@4.4.3" crossorigin>')
46
+ })
47
+
48
+ test('reads defensively from a partial manifest', () => {
49
+ expect(parseImportMap(renderImportMapHtml({}))).toEqual({})
50
+ expect(renderImportMapHtml({})).not.toContain('modulepreload')
51
+ })
52
+
53
+ test('ends with a trailing newline (template-include friendly)', () => {
54
+ expect(renderImportMapHtml({ importmap: { imports: {} } }).endsWith('\n')).toBe(true)
55
+ })
56
+
57
+ test('escapes < in the importmap JSON so a URL cannot break out of the script', () => {
58
+ const html = renderImportMapHtml({
59
+ importmap: { imports: { evil: 'https://x/</script><script>alert(1)</script>' } },
60
+ })
61
+ // The literal closing tag must not appear before the importmap's own.
62
+ const importmapClose = html.indexOf('</script>')
63
+ expect(html.slice(0, importmapClose)).not.toContain('</script>')
64
+ // But the value still round-trips through JSON.parse.
65
+ expect(parseImportMap(html).evil).toBe('https://x/</script><script>alert(1)</script>')
66
+ })
67
+
68
+ test('escapes double quotes and angle brackets in preload hrefs', () => {
69
+ const html = renderImportMapHtml({
70
+ preloads: ['/components/"onerror=alert(1).js'],
71
+ })
72
+ expect(html).not.toContain('"onerror=alert(1)')
73
+ expect(html).toContain('&quot;onerror=alert(1)')
74
+ })
75
+ })