@barefootjs/jsx 0.1.3 → 1.0.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/debug.d.ts +66 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/html-constants.d.ts +4 -9
- package/dist/html-constants.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8628 -8071
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +15 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +5 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +2 -8
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +135 -0
- package/src/__tests__/boolean-attributes.test.ts +2 -1
- package/src/__tests__/conditional-branch-reactive-text.test.ts +108 -0
- package/src/__tests__/debug.test.ts +422 -9
- package/src/__tests__/doc-examples.test.ts +7 -0
- package/src/__tests__/ir-provider.test.ts +98 -0
- package/src/__tests__/rewrite-destructured-props.test.ts +73 -0
- package/src/debug.ts +637 -32
- package/src/html-constants.ts +4 -27
- package/src/index.ts +6 -1
- package/src/ir-to-client-js/collect-elements.ts +3 -0
- package/src/ir-to-client-js/emit-reactive.ts +5 -5
- package/src/ir-to-client-js/html-template.ts +97 -11
- package/src/ir-to-client-js/types.ts +6 -0
- package/src/ir-to-client-js/utils.ts +4 -65
- package/src/jsx-to-ir.ts +92 -17
- package/src/prop-rewrite.ts +6 -2
- package/src/types.ts +21 -0
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
import { describe, test, expect } from 'bun:test'
|
|
9
9
|
import {
|
|
10
10
|
buildComponentGraph,
|
|
11
|
+
buildEventSummary,
|
|
12
|
+
buildComponentAnalysis,
|
|
13
|
+
buildLoopSummary,
|
|
11
14
|
traceUpdatePath,
|
|
12
15
|
formatComponentGraph,
|
|
13
16
|
formatUpdatePath,
|
|
17
|
+
formatEventSummary,
|
|
18
|
+
formatLoopSummary,
|
|
14
19
|
formatSignalTrace,
|
|
15
20
|
generateStaticTrace,
|
|
16
21
|
graphToJSON,
|
|
@@ -615,8 +620,6 @@ describe('DomBinding classification (#944)', () => {
|
|
|
615
620
|
})
|
|
616
621
|
|
|
617
622
|
test('formatComponentGraph marks fallback bindings with ~ prefix', () => {
|
|
618
|
-
// The visual marker is the primary UX for `bf debug graph`.
|
|
619
|
-
// Guard the prefix format so `why-wrap` output doesn't silently drift.
|
|
620
623
|
const source = `
|
|
621
624
|
'use client'
|
|
622
625
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -631,14 +634,424 @@ describe('DomBinding classification (#944)', () => {
|
|
|
631
634
|
const graph = buildComponentGraph(source, 'Page.tsx')
|
|
632
635
|
const output = formatComponentGraph(graph)
|
|
633
636
|
// Fallback text binding for formatTitle(page) — marked with '~'.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
expect(output).toMatch(/dom bindings:[\s\S]*?
|
|
638
|
-
// No fallback marker on the reactive binding's line.
|
|
637
|
+
// New format uses JSX preview: `~ <h1>{formatTitle(page)}</h1>`
|
|
638
|
+
expect(output).toMatch(/~ .*formatTitle/)
|
|
639
|
+
// Reactive text binding for count() — no tilde prefix.
|
|
640
|
+
expect(output).toMatch(/dom bindings:[\s\S]*?count/)
|
|
639
641
|
const lines = output.split('\n')
|
|
640
|
-
const countLine = lines.find(l => l.includes('count') && l.includes('
|
|
642
|
+
const countLine = lines.find(l => l.includes('count()') && l.includes('<h1>'))
|
|
641
643
|
expect(countLine).toBeDefined()
|
|
642
|
-
expect(countLine!).not.toMatch(
|
|
644
|
+
expect(countLine!).not.toMatch(/^.*~ /)
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
// =============================================================================
|
|
649
|
+
// Source locations + JSX previews on DomBinding (#1611 TODO 2)
|
|
650
|
+
// =============================================================================
|
|
651
|
+
|
|
652
|
+
describe('DomBinding source locations and JSX previews', () => {
|
|
653
|
+
test('text bindings carry loc and parent-tag JSX preview', () => {
|
|
654
|
+
const graph = buildComponentGraph(counterSource, 'Counter.tsx')
|
|
655
|
+
const textBinding = graph.domBindings.find(d => d.type === 'text')
|
|
656
|
+
expect(textBinding).toBeDefined()
|
|
657
|
+
expect(textBinding!.loc).toBeDefined()
|
|
658
|
+
expect(textBinding!.loc!.start.line).toBeGreaterThan(0)
|
|
659
|
+
expect(textBinding!.jsxPreview).toContain('<button>')
|
|
660
|
+
expect(textBinding!.jsxPreview).toContain('count()')
|
|
661
|
+
expect(textBinding!.jsxPreview).toContain('</button>')
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
test('attribute bindings carry loc and element-tag JSX preview', () => {
|
|
665
|
+
const graph = buildComponentGraph(sliderLikeSource, 'RangeInput.tsx')
|
|
666
|
+
const styleBinding = graph.domBindings.find(d => d.label === 'style')
|
|
667
|
+
expect(styleBinding).toBeDefined()
|
|
668
|
+
expect(styleBinding!.loc).toBeDefined()
|
|
669
|
+
expect(styleBinding!.jsxPreview).toMatch(/<div style=\{/)
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
test('event bindings carry loc and JSX preview', () => {
|
|
673
|
+
const graph = buildComponentGraph(counterSource, 'Counter.tsx')
|
|
674
|
+
const eventBinding = graph.domBindings.find(d => d.type === 'event')
|
|
675
|
+
expect(eventBinding).toBeDefined()
|
|
676
|
+
expect(eventBinding!.loc).toBeDefined()
|
|
677
|
+
expect(eventBinding!.jsxPreview).toContain('<button')
|
|
678
|
+
expect(eventBinding!.jsxPreview).toContain('onClick')
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test('loop bindings carry loc and JSX preview with param', () => {
|
|
682
|
+
const source = `
|
|
683
|
+
'use client'
|
|
684
|
+
import { createSignal } from '@barefootjs/client'
|
|
685
|
+
|
|
686
|
+
export function List() {
|
|
687
|
+
const [items, setItems] = createSignal([1, 2, 3])
|
|
688
|
+
return <ul>{items().map(item => <li>{item}</li>)}</ul>
|
|
689
|
+
}
|
|
690
|
+
`
|
|
691
|
+
const graph = buildComponentGraph(source, 'List.tsx')
|
|
692
|
+
const loopBinding = graph.domBindings.find(d => d.type === 'loop')
|
|
693
|
+
expect(loopBinding).toBeDefined()
|
|
694
|
+
expect(loopBinding!.jsxPreview).toContain('.map(item')
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
test('conditional bindings carry loc and JSX preview', () => {
|
|
698
|
+
const source = `
|
|
699
|
+
'use client'
|
|
700
|
+
import { createSignal } from '@barefootjs/client'
|
|
701
|
+
|
|
702
|
+
export function Toggle() {
|
|
703
|
+
const [show, setShow] = createSignal(true)
|
|
704
|
+
return <div>{show() ? <span>on</span> : <span>off</span>}</div>
|
|
705
|
+
}
|
|
706
|
+
`
|
|
707
|
+
const graph = buildComponentGraph(source, 'Toggle.tsx')
|
|
708
|
+
const condBinding = graph.domBindings.find(d => d.type === 'conditional')
|
|
709
|
+
expect(condBinding).toBeDefined()
|
|
710
|
+
expect(condBinding!.jsxPreview).toContain('show()')
|
|
711
|
+
expect(condBinding!.jsxPreview).toContain('? ... : ...')
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
test('component prop bindings carry loc and JSX preview', () => {
|
|
715
|
+
const source = `
|
|
716
|
+
'use client'
|
|
717
|
+
import { createSignal } from '@barefootjs/client'
|
|
718
|
+
import { Card } from './Card'
|
|
719
|
+
|
|
720
|
+
export function Page() {
|
|
721
|
+
const [title, setTitle] = createSignal('hello')
|
|
722
|
+
return <Card title={title()} />
|
|
723
|
+
}
|
|
724
|
+
`
|
|
725
|
+
const graph = buildComponentGraph(source, 'Page.tsx')
|
|
726
|
+
const propBinding = graph.domBindings.find(d => d.label === 'Card.title')
|
|
727
|
+
expect(propBinding).toBeDefined()
|
|
728
|
+
expect(propBinding!.jsxPreview).toContain('<Card title=')
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
test('formatComponentGraph includes source locations', () => {
|
|
732
|
+
const graph = buildComponentGraph(counterSource, 'Counter.tsx')
|
|
733
|
+
const output = formatComponentGraph(graph)
|
|
734
|
+
expect(output).toMatch(/at Counter\.tsx:\d+/)
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
test('graphToJSON includes loc and jsxPreview fields', () => {
|
|
738
|
+
const graph = buildComponentGraph(counterSource, 'Counter.tsx')
|
|
739
|
+
const json = graphToJSON(graph) as { domBindings: Array<{ loc?: object; jsxPreview?: string }> }
|
|
740
|
+
const binding = json.domBindings.find(d => (d as any).type === 'text')
|
|
741
|
+
expect(binding).toBeDefined()
|
|
742
|
+
expect(binding!.loc).toBeDefined()
|
|
743
|
+
expect(binding!.jsxPreview).toBeDefined()
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
// =============================================================================
|
|
748
|
+
// Event analysis (bf debug events)
|
|
749
|
+
// =============================================================================
|
|
750
|
+
|
|
751
|
+
describe('buildEventSummary', () => {
|
|
752
|
+
test('extracts direct setter calls from event handlers', () => {
|
|
753
|
+
const summary = buildEventSummary(counterSource, 'Counter.tsx')
|
|
754
|
+
expect(summary.componentName).toBe('Counter')
|
|
755
|
+
expect(summary.events.length).toBeGreaterThanOrEqual(1)
|
|
756
|
+
const clickEvent = summary.events.find(e => e.eventName === 'onClick')
|
|
757
|
+
expect(clickEvent).toBeDefined()
|
|
758
|
+
expect(clickEvent!.elementTag).toBe('button')
|
|
759
|
+
expect(clickEvent!.setterCalls.some(s => s.setter === 'setCount')).toBe(true)
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
test('extracts events from a component with multiple handlers', () => {
|
|
763
|
+
const source = `
|
|
764
|
+
'use client'
|
|
765
|
+
import { createSignal } from '@barefootjs/client'
|
|
766
|
+
|
|
767
|
+
export function SearchPanel() {
|
|
768
|
+
const [query, setQuery] = createSignal('')
|
|
769
|
+
const [category, setCategory] = createSignal('all')
|
|
770
|
+
return (
|
|
771
|
+
<div>
|
|
772
|
+
<input type="search" onInput={(e) => setQuery(e.target.value)} />
|
|
773
|
+
<select name="category" onChange={(e) => setCategory(e.target.value)}>
|
|
774
|
+
<option value="all">All</option>
|
|
775
|
+
</select>
|
|
776
|
+
<p>{query()}</p>
|
|
777
|
+
</div>
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
`
|
|
781
|
+
const summary = buildEventSummary(source, 'SearchPanel.tsx')
|
|
782
|
+
expect(summary.events).toHaveLength(2)
|
|
783
|
+
const inputEvent = summary.events.find(e => e.elementTag === 'input')
|
|
784
|
+
expect(inputEvent).toBeDefined()
|
|
785
|
+
expect(inputEvent!.elementContext).toBe('input search')
|
|
786
|
+
expect(inputEvent!.setterCalls[0].setter).toBe('setQuery')
|
|
787
|
+
expect(inputEvent!.setterCalls[0].signal).toBe('query')
|
|
788
|
+
|
|
789
|
+
const selectEvent = summary.events.find(e => e.elementTag === 'select')
|
|
790
|
+
expect(selectEvent).toBeDefined()
|
|
791
|
+
expect(selectEvent!.elementContext).toBe('select category')
|
|
792
|
+
expect(selectEvent!.setterCalls[0].setter).toBe('setCategory')
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
test('resolves setters through local functions', () => {
|
|
796
|
+
const source = `
|
|
797
|
+
'use client'
|
|
798
|
+
import { createSignal } from '@barefootjs/client'
|
|
799
|
+
|
|
800
|
+
export function TodoApp() {
|
|
801
|
+
const [todos, setTodos] = createSignal([])
|
|
802
|
+
|
|
803
|
+
function addTodo(text: string) {
|
|
804
|
+
setTodos(prev => [...prev, { text, done: false }])
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return (
|
|
808
|
+
<button onClick={() => addTodo('new')}>Add</button>
|
|
809
|
+
)
|
|
810
|
+
}
|
|
811
|
+
`
|
|
812
|
+
const summary = buildEventSummary(source, 'TodoApp.tsx')
|
|
813
|
+
expect(summary.events).toHaveLength(1)
|
|
814
|
+
const click = summary.events[0]
|
|
815
|
+
expect(click.setterCalls).toHaveLength(1)
|
|
816
|
+
expect(click.setterCalls[0].setter).toBe('setTodos')
|
|
817
|
+
expect(click.setterCalls[0].signal).toBe('todos')
|
|
818
|
+
expect(click.setterCalls[0].via).toBe('addTodo')
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
test('includes component prop events (Button.onClick)', () => {
|
|
822
|
+
const source = `
|
|
823
|
+
'use client'
|
|
824
|
+
import { createSignal } from '@barefootjs/client'
|
|
825
|
+
import { Button } from './Button'
|
|
826
|
+
|
|
827
|
+
export function ResetPanel() {
|
|
828
|
+
const [count, setCount] = createSignal(0)
|
|
829
|
+
return (
|
|
830
|
+
<div>
|
|
831
|
+
<p>{count()}</p>
|
|
832
|
+
<Button onClick={() => setCount(0)}>Reset</Button>
|
|
833
|
+
</div>
|
|
834
|
+
)
|
|
835
|
+
}
|
|
836
|
+
`
|
|
837
|
+
const summary = buildEventSummary(source, 'ResetPanel.tsx')
|
|
838
|
+
const btnEvent = summary.events.find(e => e.isComponentProp)
|
|
839
|
+
expect(btnEvent).toBeDefined()
|
|
840
|
+
expect(btnEvent!.elementTag).toBe('Button')
|
|
841
|
+
expect(btnEvent!.elementContext).toBe('Reset Button')
|
|
842
|
+
expect(btnEvent!.setterCalls[0].setter).toBe('setCount')
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
test('returns empty events for stateless component', () => {
|
|
846
|
+
const source = `
|
|
847
|
+
export function Card(props: { title: string }) {
|
|
848
|
+
return <div>{props.title}</div>
|
|
849
|
+
}
|
|
850
|
+
`
|
|
851
|
+
const summary = buildEventSummary(source, 'Card.tsx')
|
|
852
|
+
expect(summary.events).toHaveLength(0)
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
test('includes source location on events', () => {
|
|
856
|
+
const summary = buildEventSummary(counterSource, 'Counter.tsx')
|
|
857
|
+
expect(summary.events.length).toBeGreaterThan(0)
|
|
858
|
+
const event = summary.events[0]
|
|
859
|
+
expect(event.loc).toBeDefined()
|
|
860
|
+
expect(event.loc.start.line).toBeGreaterThan(0)
|
|
861
|
+
})
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
describe('formatEventSummary', () => {
|
|
865
|
+
test('produces readable output with setter and update info', () => {
|
|
866
|
+
const source = `
|
|
867
|
+
'use client'
|
|
868
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
869
|
+
|
|
870
|
+
export function Counter() {
|
|
871
|
+
const [count, setCount] = createSignal(0)
|
|
872
|
+
const doubled = createMemo(() => count() * 2)
|
|
873
|
+
return (
|
|
874
|
+
<div>
|
|
875
|
+
<button onClick={() => setCount(n => n + 1)}>+</button>
|
|
876
|
+
<span>{count()}</span>
|
|
877
|
+
<span>{doubled()}</span>
|
|
878
|
+
</div>
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
`
|
|
882
|
+
const summary = buildEventSummary(source, 'Counter.tsx')
|
|
883
|
+
const graph = buildComponentGraph(source, 'Counter.tsx')
|
|
884
|
+
const output = formatEventSummary(summary, graph)
|
|
885
|
+
|
|
886
|
+
expect(output).toContain('Counter')
|
|
887
|
+
expect(output).toContain('onClick')
|
|
888
|
+
expect(output).toContain('setCount')
|
|
889
|
+
expect(output).toContain('updates:')
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
test('shows indirect setter resolution via local functions', () => {
|
|
893
|
+
const source = `
|
|
894
|
+
'use client'
|
|
895
|
+
import { createSignal } from '@barefootjs/client'
|
|
896
|
+
|
|
897
|
+
export function App() {
|
|
898
|
+
const [items, setItems] = createSignal([])
|
|
899
|
+
function addItem(text: string) {
|
|
900
|
+
setItems(prev => [...prev, text])
|
|
901
|
+
}
|
|
902
|
+
return (
|
|
903
|
+
<div>
|
|
904
|
+
<button onClick={() => addItem('test')}>Add</button>
|
|
905
|
+
<ul>{items().map(i => <li>{i}</li>)}</ul>
|
|
906
|
+
</div>
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
`
|
|
910
|
+
const summary = buildEventSummary(source, 'App.tsx')
|
|
911
|
+
const graph = buildComponentGraph(source, 'App.tsx')
|
|
912
|
+
const output = formatEventSummary(summary, graph)
|
|
913
|
+
|
|
914
|
+
expect(output).toContain('addItem -> setItems')
|
|
915
|
+
})
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
describe('buildComponentAnalysis', () => {
|
|
919
|
+
test('returns both graph and IR', () => {
|
|
920
|
+
const analysis = buildComponentAnalysis(counterSource, 'Counter.tsx')
|
|
921
|
+
expect(analysis.graph.componentName).toBe('Counter')
|
|
922
|
+
expect(analysis.ir.root).toBeDefined()
|
|
923
|
+
expect(analysis.ir.metadata.signals).toHaveLength(1)
|
|
924
|
+
})
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
// =============================================================================
|
|
928
|
+
// Loop analysis (bf debug loops)
|
|
929
|
+
// =============================================================================
|
|
930
|
+
|
|
931
|
+
describe('buildLoopSummary', () => {
|
|
932
|
+
test('extracts loop with key, bindings, and handlers', () => {
|
|
933
|
+
const source = `
|
|
934
|
+
'use client'
|
|
935
|
+
import { createSignal } from '@barefootjs/client'
|
|
936
|
+
|
|
937
|
+
export function ItemList() {
|
|
938
|
+
const [items, setItems] = createSignal([{ id: 1, name: 'A' }])
|
|
939
|
+
const [selectedId, setSelectedId] = createSignal(0)
|
|
940
|
+
return (
|
|
941
|
+
<ul>
|
|
942
|
+
{items().map(item => (
|
|
943
|
+
<li key={item.id} class={selectedId() === item.id ? 'active' : ''}>
|
|
944
|
+
<span>{item.name}</span>
|
|
945
|
+
<button onClick={() => setSelectedId(item.id)}>Select</button>
|
|
946
|
+
</li>
|
|
947
|
+
))}
|
|
948
|
+
</ul>
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
`
|
|
952
|
+
const summary = buildLoopSummary(source, 'ItemList.tsx')
|
|
953
|
+
expect(summary.loops).toHaveLength(1)
|
|
954
|
+
const loop = summary.loops[0]
|
|
955
|
+
expect(loop.array).toContain('items()')
|
|
956
|
+
expect(loop.param).toBe('item')
|
|
957
|
+
expect(loop.key).toBe('item.id')
|
|
958
|
+
|
|
959
|
+
const classBinding = loop.bindings.find(b => b.name === 'class')
|
|
960
|
+
expect(classBinding).toBeDefined()
|
|
961
|
+
expect(classBinding!.deps).toContain('selectedId')
|
|
962
|
+
expect(classBinding!.deps).toContain('item')
|
|
963
|
+
|
|
964
|
+
const textBinding = loop.bindings.find(b => b.kind === 'text')
|
|
965
|
+
expect(textBinding).toBeDefined()
|
|
966
|
+
expect(textBinding!.deps).toContain('item')
|
|
967
|
+
expect(textBinding!.elementContext).toBe('span')
|
|
968
|
+
|
|
969
|
+
const clickEvent = loop.bindings.find(b => b.kind === 'event')
|
|
970
|
+
expect(clickEvent).toBeDefined()
|
|
971
|
+
expect(clickEvent!.deps).toContain('item')
|
|
972
|
+
})
|
|
973
|
+
|
|
974
|
+
test('returns empty for component without loops', () => {
|
|
975
|
+
const summary = buildLoopSummary(counterSource, 'Counter.tsx')
|
|
976
|
+
expect(summary.loops).toHaveLength(0)
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
test('includes source location', () => {
|
|
980
|
+
const summary = buildLoopSummary(todoSource, 'TodoList.tsx')
|
|
981
|
+
expect(summary.loops.length).toBeGreaterThan(0)
|
|
982
|
+
expect(summary.loops[0].loc.start.line).toBeGreaterThan(0)
|
|
983
|
+
})
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
describe('formatLoopSummary', () => {
|
|
987
|
+
test('produces readable output', () => {
|
|
988
|
+
const source = `
|
|
989
|
+
'use client'
|
|
990
|
+
import { createSignal } from '@barefootjs/client'
|
|
991
|
+
|
|
992
|
+
export function List() {
|
|
993
|
+
const [items, setItems] = createSignal([{ id: 1, text: 'hello' }])
|
|
994
|
+
return (
|
|
995
|
+
<ul>
|
|
996
|
+
{items().map(item => (
|
|
997
|
+
<li key={item.id}>
|
|
998
|
+
<span>{item.text}</span>
|
|
999
|
+
</li>
|
|
1000
|
+
))}
|
|
1001
|
+
</ul>
|
|
1002
|
+
)
|
|
1003
|
+
}
|
|
1004
|
+
`
|
|
1005
|
+
const summary = buildLoopSummary(source, 'List.tsx')
|
|
1006
|
+
const output = formatLoopSummary(summary)
|
|
1007
|
+
expect(output).toContain('1 loop(s)')
|
|
1008
|
+
expect(output).toContain('.map(item)')
|
|
1009
|
+
expect(output).toContain('key: item.id')
|
|
1010
|
+
expect(output).toContain('item')
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
test('includes index parameter in format output', () => {
|
|
1014
|
+
const source = `
|
|
1015
|
+
'use client'
|
|
1016
|
+
import { createSignal } from '@barefootjs/client'
|
|
1017
|
+
|
|
1018
|
+
export function IndexList() {
|
|
1019
|
+
const [items, setItems] = createSignal(['a', 'b', 'c'])
|
|
1020
|
+
return (
|
|
1021
|
+
<ul>
|
|
1022
|
+
{items().map((item, i) => (
|
|
1023
|
+
<li><span>{i}: {item}</span></li>
|
|
1024
|
+
))}
|
|
1025
|
+
</ul>
|
|
1026
|
+
)
|
|
1027
|
+
}
|
|
1028
|
+
`
|
|
1029
|
+
const summary = buildLoopSummary(source, 'IndexList.tsx')
|
|
1030
|
+
expect(summary.loops).toHaveLength(1)
|
|
1031
|
+
const output = formatLoopSummary(summary)
|
|
1032
|
+
expect(output).toContain('.map(item, i)')
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
test('handles destructured loop params via paramBindings', () => {
|
|
1036
|
+
const source = `
|
|
1037
|
+
'use client'
|
|
1038
|
+
import { createSignal } from '@barefootjs/client'
|
|
1039
|
+
|
|
1040
|
+
export function ConfigList() {
|
|
1041
|
+
const [entries, setEntries] = createSignal([['key1', { label: 'A' }]])
|
|
1042
|
+
return (
|
|
1043
|
+
<ul>
|
|
1044
|
+
{entries().map(([key, cfg]) => (
|
|
1045
|
+
<li><span>{key}: {cfg.label}</span></li>
|
|
1046
|
+
))}
|
|
1047
|
+
</ul>
|
|
1048
|
+
)
|
|
1049
|
+
}
|
|
1050
|
+
`
|
|
1051
|
+
const summary = buildLoopSummary(source, 'ConfigList.tsx')
|
|
1052
|
+
expect(summary.loops).toHaveLength(1)
|
|
1053
|
+
const loop = summary.loops[0]
|
|
1054
|
+
const textBindings = loop.bindings.filter(b => b.kind === 'text')
|
|
1055
|
+
expect(textBindings.length).toBeGreaterThan(0)
|
|
643
1056
|
})
|
|
644
1057
|
})
|
|
@@ -314,6 +314,13 @@ const PAGES: PageSpec[] = [
|
|
|
314
314
|
// tie each ❌ snippet to its parent `### BFxxx —` H3.
|
|
315
315
|
{ path: 'core/advanced/performance.md' },
|
|
316
316
|
{ path: 'core/reactivity.md' },
|
|
317
|
+
{
|
|
318
|
+
path: 'core/reactivity/shared-state.md',
|
|
319
|
+
pageSkip: (body: string) => {
|
|
320
|
+
if (/\bapp\.get\b/.test(body)) return 'server route example (not a component)'
|
|
321
|
+
return undefined
|
|
322
|
+
},
|
|
323
|
+
},
|
|
317
324
|
{ path: 'core/introduction.mdx' },
|
|
318
325
|
]
|
|
319
326
|
|
|
@@ -403,3 +403,101 @@ describe('Context.Provider JSX', () => {
|
|
|
403
403
|
expect(error?.severity).toBe('error')
|
|
404
404
|
})
|
|
405
405
|
})
|
|
406
|
+
|
|
407
|
+
describe('Context API constraints (#1607)', () => {
|
|
408
|
+
test('useContext without "use client" triggers BF001', () => {
|
|
409
|
+
const source = `
|
|
410
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
411
|
+
|
|
412
|
+
const Ctx = createContext()
|
|
413
|
+
|
|
414
|
+
export function Consumer() {
|
|
415
|
+
const handleMount = (el: HTMLElement) => {
|
|
416
|
+
const ctx = useContext(Ctx)
|
|
417
|
+
}
|
|
418
|
+
return <div ref={handleMount} />
|
|
419
|
+
}
|
|
420
|
+
`
|
|
421
|
+
|
|
422
|
+
const result = compileJSX(source, 'Consumer.tsx', { adapter })
|
|
423
|
+
const bf001 = result.errors.find(e => e.code === ErrorCodes.MISSING_USE_CLIENT)
|
|
424
|
+
expect(bf001).toBeDefined()
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('useContext with "use client" compiles without errors', () => {
|
|
428
|
+
const source = `
|
|
429
|
+
'use client'
|
|
430
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
431
|
+
|
|
432
|
+
const Ctx = createContext()
|
|
433
|
+
|
|
434
|
+
export function Consumer() {
|
|
435
|
+
const handleMount = (el: HTMLElement) => {
|
|
436
|
+
const ctx = useContext(Ctx)
|
|
437
|
+
}
|
|
438
|
+
return <div ref={handleMount} />
|
|
439
|
+
}
|
|
440
|
+
`
|
|
441
|
+
|
|
442
|
+
const result = compileJSX(source, 'Consumer.tsx', { adapter })
|
|
443
|
+
expect(result.errors).toHaveLength(0)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('useContext import is rewritten to runtime path in client JS', () => {
|
|
447
|
+
const source = `
|
|
448
|
+
'use client'
|
|
449
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
450
|
+
|
|
451
|
+
const Ctx = createContext()
|
|
452
|
+
|
|
453
|
+
export function Consumer() {
|
|
454
|
+
const handleMount = (el: HTMLElement) => {
|
|
455
|
+
const ctx = useContext(Ctx)
|
|
456
|
+
}
|
|
457
|
+
return <div ref={handleMount} />
|
|
458
|
+
}
|
|
459
|
+
`
|
|
460
|
+
|
|
461
|
+
const result = compileJSX(source, 'Consumer.tsx', { adapter })
|
|
462
|
+
expect(result.errors).toHaveLength(0)
|
|
463
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!
|
|
464
|
+
expect(clientJs).toBeDefined()
|
|
465
|
+
expect(clientJs.content).toContain("from '@barefootjs/client/runtime'")
|
|
466
|
+
expect(clientJs.content).toContain('useContext')
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test('same-file provider + consumer compiles with provideContext before useContext', () => {
|
|
470
|
+
const source = `
|
|
471
|
+
'use client'
|
|
472
|
+
import { createContext, useContext, createSignal } from '@barefootjs/client'
|
|
473
|
+
|
|
474
|
+
const ThemeContext = createContext('light')
|
|
475
|
+
|
|
476
|
+
export function ThemeProvider(props) {
|
|
477
|
+
return (
|
|
478
|
+
<ThemeContext.Provider value={props.theme}>
|
|
479
|
+
<ThemedButton />
|
|
480
|
+
</ThemeContext.Provider>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function ThemedButton() {
|
|
485
|
+
const handleMount = (el: HTMLButtonElement) => {
|
|
486
|
+
const theme = useContext(ThemeContext)
|
|
487
|
+
el.className = theme === 'dark' ? 'btn-dark' : 'btn-light'
|
|
488
|
+
}
|
|
489
|
+
return <button ref={handleMount}>click</button>
|
|
490
|
+
}
|
|
491
|
+
`
|
|
492
|
+
|
|
493
|
+
const result = compileJSX(source, 'Theme.tsx', { adapter })
|
|
494
|
+
expect(result.errors).toHaveLength(0)
|
|
495
|
+
|
|
496
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!
|
|
497
|
+
const provideIdx = clientJs.content.indexOf('provideContext(ThemeContext')
|
|
498
|
+
const useIdx = clientJs.content.indexOf('useContext(ThemeContext')
|
|
499
|
+
expect(provideIdx).toBeGreaterThan(-1)
|
|
500
|
+
expect(useIdx).toBeGreaterThan(-1)
|
|
501
|
+
expect(provideIdx).toBeLessThan(useIdx)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { compileJSX } from '../index'
|
|
3
|
+
import { HonoAdapter } from '../../../adapter-hono/src/adapter/index'
|
|
4
|
+
|
|
5
|
+
function compileClientJs(source: string): string {
|
|
6
|
+
const result = compileJSX(source, 'test.tsx', { adapter: new HonoAdapter() })
|
|
7
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
8
|
+
if (!clientJs) throw new Error('No client JS emitted')
|
|
9
|
+
return clientJs.content
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('prop substitution in reactive attr expressions', () => {
|
|
13
|
+
test('does NOT replace prop name inside double-quoted strings in class attr', () => {
|
|
14
|
+
const source = `
|
|
15
|
+
"use client"
|
|
16
|
+
const sizeClasses: Record<string, string> = { default: "h-9", icon: "size-9" }
|
|
17
|
+
function Comp({ size = 'default' }: { size?: string }) {
|
|
18
|
+
return <div className={\`base \${sizeClasses[size]}\`} />
|
|
19
|
+
}
|
|
20
|
+
export { Comp }
|
|
21
|
+
`
|
|
22
|
+
const js = compileClientJs(source)
|
|
23
|
+
expect(js).toContain('"size-9"')
|
|
24
|
+
expect(js).not.toContain('(_p.size ?? \'default\')-9')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('does NOT replace prop name in CSS selector strings', () => {
|
|
28
|
+
const source = `
|
|
29
|
+
"use client"
|
|
30
|
+
function Comp({ size = 'default' }: { size?: string }) {
|
|
31
|
+
return <button className={\`[&_svg:not([class*="size-"])]:size-4 \${size}\`} />
|
|
32
|
+
}
|
|
33
|
+
export { Comp }
|
|
34
|
+
`
|
|
35
|
+
const js = compileClientJs(source)
|
|
36
|
+
expect(js).toContain('[class*="size-"]')
|
|
37
|
+
expect(js).toContain(':size-4')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('rewrites prop name in interpolation positions', () => {
|
|
41
|
+
const source = `
|
|
42
|
+
"use client"
|
|
43
|
+
function Comp({ size = 'default' }: { size?: string }) {
|
|
44
|
+
return <div className={\`cls-\${size}\`} />
|
|
45
|
+
}
|
|
46
|
+
export { Comp }
|
|
47
|
+
`
|
|
48
|
+
const js = compileClientJs(source)
|
|
49
|
+
expect(js).toContain('_p.size')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('Button-like pattern: CSS selectors, variant maps, and prop refs', () => {
|
|
53
|
+
const source = `
|
|
54
|
+
"use client"
|
|
55
|
+
const base = 'inline-flex [&_svg:not([class*="size-"])]:size-4'
|
|
56
|
+
const variants: Record<string, string> = { default: "bg-primary", outline: "border" }
|
|
57
|
+
const sizes: Record<string, string> = { default: "h-9", icon: "size-9", "icon-sm": "size-8" }
|
|
58
|
+
function Btn({ variant = 'default', size = 'default', className = '' }: { variant?: string, size?: string, className?: string }) {
|
|
59
|
+
return <button className={\`\${base} \${variants[variant]} \${sizes[size]} \${className}\`} />
|
|
60
|
+
}
|
|
61
|
+
export { Btn }
|
|
62
|
+
`
|
|
63
|
+
const js = compileClientJs(source)
|
|
64
|
+
expect(js).toContain('[class*="size-"]')
|
|
65
|
+
expect(js).toContain(':size-4')
|
|
66
|
+
expect(js).toContain('"size-9"')
|
|
67
|
+
expect(js).toContain('"size-8"')
|
|
68
|
+
expect(js).toContain('_p.variant')
|
|
69
|
+
expect(js).toContain('_p.size')
|
|
70
|
+
expect(js).toContain('_p.className')
|
|
71
|
+
expect(js).not.toMatch(/\(_p\.size[^)]*\)-[0-9]/)
|
|
72
|
+
})
|
|
73
|
+
})
|