@barefootjs/jsx 0.24.1 → 0.26.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.
Files changed (46) hide show
  1. package/dist/adapters/dangerous-inner-html.d.ts +52 -24
  2. package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/errors.d.ts +1 -0
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/index.js +473 -165
  7. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
  9. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
  11. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +11 -5
  13. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
  15. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/reactivity.d.ts +24 -2
  18. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/types.d.ts +13 -0
  20. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  21. package/dist/to-locale-date-lowering.d.ts +31 -10
  22. package/dist/to-locale-date-lowering.d.ts.map +1 -1
  23. package/dist/types.d.ts +29 -5
  24. package/dist/types.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
  27. package/src/__tests__/nested-loop-conditional.test.ts +133 -0
  28. package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
  29. package/src/__tests__/reactive-factory-cross-file.test.ts +833 -0
  30. package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
  31. package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
  32. package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
  33. package/src/adapters/dangerous-inner-html.ts +101 -48
  34. package/src/analyzer.ts +607 -142
  35. package/src/errors.ts +4 -0
  36. package/src/ir-to-client-js/collect-elements.ts +28 -2
  37. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
  38. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
  39. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
  40. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
  41. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
  42. package/src/ir-to-client-js/reactivity.ts +61 -7
  43. package/src/ir-to-client-js/types.ts +13 -0
  44. package/src/rich-type-refusal.ts +3 -2
  45. package/src/to-locale-date-lowering.ts +50 -13
  46. package/src/types.ts +30 -5
@@ -500,3 +500,836 @@ export function DoublerSelfImporting() {
500
500
  expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
501
501
  })
502
502
  })
503
+
504
+ describe('Barrel re-exports (#2341 BUG-2)', () => {
505
+ beforeAll(() => {
506
+ writeFixture('barrel/useToggle.tsx', `'use client'
507
+ import { createSignal } from '@barefootjs/client'
508
+
509
+ export function useToggle(initial: boolean) {
510
+ const [on, setOn] = createSignal(initial)
511
+ return [on, setOn] as const
512
+ }
513
+ `)
514
+ writeFixture('barrel/index.ts', `export { useToggle } from './useToggle'
515
+ `)
516
+ })
517
+
518
+ test('B1: factory reached through a barrel index.ts inlines (issue repro)', () => {
519
+ // `../barrel` has no extension and is not itself a file — this also
520
+ // exercises directory-index resolution (`./hooks` -> `hooks/index.ts`),
521
+ // already supported by `resolveRelativeImportToFile`.
522
+ const consumerSource = `'use client'
523
+ import { useToggle } from '../barrel'
524
+
525
+ export function Switch() {
526
+ const [on, setOn] = useToggle(false)
527
+ return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
528
+ }
529
+ `
530
+ const consumerPath = writeFixture('barrel-consumer/Switch.tsx', consumerSource)
531
+
532
+ const ctx = analyzeComponent(consumerSource, consumerPath, 'Switch')
533
+ expect(ctx.signals.length).toBe(1)
534
+ expect(ctx.signals[0].getter).toBe('on')
535
+
536
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
537
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
538
+ const clientJs = result.files.find(f => f.type === 'clientJs')
539
+ expect(clientJs).toBeDefined()
540
+ expect(clientJs!.content).toContain('createSignal')
541
+ expect(clientJs!.content).not.toContain('../barrel')
542
+ expect(clientJs!.content).toMatch(/import\s*\{[^}]*createSignal[^}]*\}\s*from\s*'@barefootjs\/client\/runtime'/)
543
+ })
544
+
545
+ test('B2: barrel alias (export { useToggle as useFlip } from) inlines', () => {
546
+ writeFixture('barrel-alias/index.ts', `export { useToggle as useFlip } from '../barrel/useToggle'
547
+ `)
548
+ const consumerSource = `'use client'
549
+ import { useFlip } from '../barrel-alias'
550
+
551
+ export function FlipSwitch() {
552
+ const [on, setOn] = useFlip(false)
553
+ return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
554
+ }
555
+ `
556
+ const consumerPath = writeFixture('barrel-consumer/FlipSwitch.tsx', consumerSource)
557
+
558
+ const ctx = analyzeComponent(consumerSource, consumerPath, 'FlipSwitch')
559
+ expect(ctx.signals.length).toBe(1)
560
+
561
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
562
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
563
+ })
564
+
565
+ test('B3: barrel alias + consumer alias both resolve through the hop', () => {
566
+ const consumerSource = `'use client'
567
+ import { useFlip as flip } from '../barrel-alias'
568
+
569
+ export function FlipSwitch2() {
570
+ const [on, setOn] = flip(false)
571
+ return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
572
+ }
573
+ `
574
+ const consumerPath = writeFixture('barrel-consumer/FlipSwitch2.tsx', consumerSource)
575
+
576
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
577
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
578
+ const clientJs = result.files.find(f => f.type === 'clientJs')
579
+ expect(clientJs).toBeDefined()
580
+ expect(clientJs!.content).toContain('createSignal')
581
+ })
582
+
583
+ test('B4: export * from stays loud (BF110), never silently marked clean', () => {
584
+ writeFixture('barrel-star/index.ts', `export * from '../barrel/useToggle'
585
+ `)
586
+ const consumerSource = `'use client'
587
+ import { useToggle } from '../barrel-star'
588
+
589
+ export function StarConsumer() {
590
+ const [on, setOn] = useToggle(false)
591
+ return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
592
+ }
593
+ `
594
+ const consumerPath = writeFixture('barrel-consumer/StarConsumer.tsx', consumerSource)
595
+
596
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
597
+ const bf110 = result.errors.find(e => e.code === 'BF110')
598
+ expect(bf110).toBeDefined()
599
+ expect(bf110!.message).toContain('useToggle')
600
+ })
601
+
602
+ test('B5: self-referential barrel does not loop', () => {
603
+ writeFixture('loop/index.ts', `export { useToggle } from './index'
604
+ `)
605
+ const consumerSource = `'use client'
606
+ import { useToggle } from '../loop'
607
+
608
+ export function LoopConsumer() {
609
+ const [on, setOn] = useToggle(false)
610
+ return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
611
+ }
612
+ `
613
+ const consumerPath = writeFixture('barrel-consumer/LoopConsumer.tsx', consumerSource)
614
+
615
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
616
+ const bf110 = result.errors.find(e => e.code === 'BF110')
617
+ expect(bf110).toBeDefined()
618
+ })
619
+
620
+ test('B6: unresolvable re-export target stays loud, never cleanFactoryImports-silenced', () => {
621
+ writeFixture('barrel-missing/index.ts', `export { useToggle } from './missing'
622
+ `)
623
+ const consumerSource = `'use client'
624
+ import { useToggle } from '../barrel-missing'
625
+
626
+ export function MissingConsumer() {
627
+ const [on, setOn] = useToggle(false)
628
+ return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
629
+ }
630
+ `
631
+ const consumerPath = writeFixture('barrel-consumer/MissingConsumer.tsx', consumerSource)
632
+
633
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
634
+ const bf110 = result.errors.find(e => e.code === 'BF110')
635
+ expect(bf110).toBeDefined()
636
+ })
637
+
638
+ test('B7: module-scope capture through a barrel still declines with BF112 (defining-file anchor)', () => {
639
+ // If the capture check were (incorrectly) anchored to the barrel file
640
+ // instead of the file that actually DEFINES the factory, `readStored`/
641
+ // `KEY` (declared only in useStoredCounter.tsx, not in the barrel's
642
+ // index.ts) would never be found as a capture, and the factory would
643
+ // wrongly inline with a dangling `readStored()` reference (#2341 BUG-2).
644
+ writeFixture('barrel-capture/useStoredCounter.tsx', `'use client'
645
+ import { createSignal } from '@barefootjs/client'
646
+
647
+ const KEY = 'stored-value'
648
+
649
+ function readStored() {
650
+ return KEY.length
651
+ }
652
+
653
+ export function useStoredCounter() {
654
+ const [count, setCount] = createSignal(readStored())
655
+ return { count, setCount }
656
+ }
657
+ `)
658
+ writeFixture('barrel-capture/index.ts', `export { useStoredCounter } from './useStoredCounter'
659
+ `)
660
+ const consumerSource = `'use client'
661
+ import { useStoredCounter } from '../barrel-capture'
662
+
663
+ export function CaptureConsumer() {
664
+ const { count, setCount } = useStoredCounter()
665
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
666
+ }
667
+ `
668
+ const consumerPath = writeFixture('barrel-consumer/CaptureConsumer.tsx', consumerSource)
669
+
670
+ const ctx = analyzeComponent(consumerSource, consumerPath, 'CaptureConsumer')
671
+ expect(ctx.signals.length).toBe(0)
672
+
673
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
674
+ const bf112 = result.errors.find(e => e.code === 'BF112')
675
+ expect(bf112).toBeDefined()
676
+ expect(bf112!.message).toContain('readStored')
677
+ const clientJs = result.files.find(f => f.type === 'clientJs')
678
+ if (clientJs) {
679
+ expect(clientJs.content).not.toContain('readStored')
680
+ }
681
+ })
682
+
683
+ test('B8: re-provisioned helper import is anchored to the defining file, not the barrel', () => {
684
+ // The barrel (`hooks2barrel/nested/index.ts`) and the file that
685
+ // actually defines `useDouble` (`hooks2/useDouble.tsx`) live at
686
+ // DIFFERENT depths — anchoring the re-provisioned `doubleIt` import to
687
+ // the barrel's directory instead of the defining file's directory
688
+ // would resolve to a nonexistent path (#2341 BUG-2).
689
+ writeFixture('lib2/mathmod.ts', `export function doubleIt(x: number): number {
690
+ return x * 2
691
+ }
692
+ `)
693
+ writeFixture('hooks2/useDouble.tsx', `'use client'
694
+ import { createSignal } from '@barefootjs/client'
695
+ import { doubleIt } from '../lib2/mathmod'
696
+
697
+ export function useDouble(initial: number) {
698
+ const [value, setValue] = createSignal(doubleIt(initial))
699
+ const bump = () => setValue(doubleIt(value()))
700
+ return { value, bump }
701
+ }
702
+ `)
703
+ writeFixture('hooks2barrel/nested/index.ts', `export { useDouble } from '../../hooks2/useDouble'
704
+ `)
705
+ const consumerSource = `'use client'
706
+ import { useDouble } from '../../hooks2barrel/nested'
707
+
708
+ export function DoubleViaBarrel() {
709
+ const { value, bump } = useDouble(21)
710
+ return <button onClick={bump}>{value()}</button>
711
+ }
712
+ `
713
+ const consumerPath = writeFixture('components/deep/DoubleViaBarrel.tsx', consumerSource)
714
+
715
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
716
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
717
+ const clientJs = result.files.find(f => f.type === 'clientJs')
718
+ expect(clientJs).toBeDefined()
719
+ expect(clientJs!.content).toMatch(/from\s*'\.\.\/\.\.\/lib2\/mathmod'/)
720
+ expect(clientJs!.content).not.toContain('hooks2barrel')
721
+ })
722
+
723
+ test('B9: two barrel hops exceed MAX_REEXPORT_HOPS and stay loud', () => {
724
+ writeFixture('twohop-b/useToggle.tsx', `'use client'
725
+ import { createSignal } from '@barefootjs/client'
726
+
727
+ export function useToggle(initial: boolean) {
728
+ const [on, setOn] = createSignal(initial)
729
+ return [on, setOn] as const
730
+ }
731
+ `)
732
+ writeFixture('twohop-b/index.ts', `export { useToggle } from './useToggle'
733
+ `)
734
+ writeFixture('twohop-a/index.ts', `export { useToggle } from '../twohop-b'
735
+ `)
736
+ const consumerSource = `'use client'
737
+ import { useToggle } from '../twohop-a'
738
+
739
+ export function TwoHopConsumer() {
740
+ const [on, setOn] = useToggle(false)
741
+ return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
742
+ }
743
+ `
744
+ const consumerPath = writeFixture('barrel-consumer/TwoHopConsumer.tsx', consumerSource)
745
+
746
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
747
+ const bf110 = result.errors.find(e => e.code === 'BF110')
748
+ expect(bf110).toBeDefined()
749
+ })
750
+
751
+ test('B10: mixed barrel (own factory + re-export) inlines both', () => {
752
+ // Pins exportedFns-before-reexports lookup order: `useLocal` is defined
753
+ // directly in index.ts, `useToggle` only reaches it via a re-export.
754
+ writeFixture('mixed/useToggle.tsx', `'use client'
755
+ import { createSignal } from '@barefootjs/client'
756
+
757
+ export function useToggle(initial: boolean) {
758
+ const [on, setOn] = createSignal(initial)
759
+ return [on, setOn] as const
760
+ }
761
+ `)
762
+ writeFixture('mixed/index.ts', `'use client'
763
+ import { createSignal } from '@barefootjs/client'
764
+
765
+ export function useLocal(initial: number) {
766
+ const [count, setCount] = createSignal(initial)
767
+ return [count, setCount] as const
768
+ }
769
+
770
+ export { useToggle } from './useToggle'
771
+ `)
772
+ const consumerSource = `'use client'
773
+ import { useLocal, useToggle } from '../mixed'
774
+
775
+ export function MixedConsumer() {
776
+ const [count, setCount] = useLocal(0)
777
+ const [on, setOn] = useToggle(false)
778
+ return <button onClick={() => { setCount(count() + 1); setOn(!on()) }}>{count()} {on() ? 'on' : 'off'}</button>
779
+ }
780
+ `
781
+ const consumerPath = writeFixture('barrel-consumer/MixedConsumer.tsx', consumerSource)
782
+
783
+ const ctx = analyzeComponent(consumerSource, consumerPath, 'MixedConsumer')
784
+ expect(ctx.signals.length).toBe(2)
785
+
786
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
787
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
788
+ })
789
+ })
790
+
791
+ describe('Real-world factory matrix, cross-file (#2341)', () => {
792
+ test('M15: deep relative paths resolve and inline', () => {
793
+ writeFixture('deep/hooks/state/useCounter.tsx', `'use client'
794
+ import { createSignal } from '@barefootjs/client'
795
+
796
+ export function useCounter(initial: number) {
797
+ const [count, setCount] = createSignal(initial)
798
+ return [count, setCount] as const
799
+ }
800
+ `)
801
+ const consumerSource = `'use client'
802
+ import { useCounter } from '../../hooks/state/useCounter'
803
+
804
+ export function Counter() {
805
+ const [count, setCount] = useCounter(0)
806
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
807
+ }
808
+ `
809
+ const consumerPath = writeFixture('deep/components/pages/Counter.tsx', consumerSource)
810
+
811
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
812
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
813
+ const clientJs = result.files.find(f => f.type === 'clientJs')
814
+ expect(clientJs).toBeDefined()
815
+ expect(clientJs!.content).toContain('createSignal')
816
+ })
817
+
818
+ test('M16: two factories from two different modules compose in one component', () => {
819
+ writeFixture('compose/useA.tsx', `'use client'
820
+ import { createSignal } from '@barefootjs/client'
821
+
822
+ export function useA(initial: number) {
823
+ const [a, setA] = createSignal(initial)
824
+ return [a, setA] as const
825
+ }
826
+ `)
827
+ writeFixture('compose/useB.tsx', `'use client'
828
+ import { createSignal } from '@barefootjs/client'
829
+
830
+ export function useB(initial: number) {
831
+ const [b, setB] = createSignal(initial)
832
+ return [b, setB] as const
833
+ }
834
+ `)
835
+ const consumerSource = `'use client'
836
+ import { useA } from './useA'
837
+ import { useB } from './useB'
838
+
839
+ export function Composed() {
840
+ const [a, setA] = useA(1)
841
+ const [b, setB] = useB(2)
842
+ return <button onClick={() => { setA(a() + 1); setB(b() + 1) }}>{a()} {b()}</button>
843
+ }
844
+ `
845
+ const consumerPath = writeFixture('compose/Composed.tsx', consumerSource)
846
+
847
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
848
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
849
+ const clientJs = result.files.find(f => f.type === 'clientJs')
850
+ expect(clientJs).toBeDefined()
851
+ expect(clientJs!.content.match(/createSignal\(/g)?.length).toBe(2)
852
+ })
853
+
854
+ test('M17: onMount is provisioned from usage for a cross-file factory', () => {
855
+ writeFixture('matrix17/useTick.tsx', `'use client'
856
+ import { createSignal, onMount } from '@barefootjs/client'
857
+
858
+ export function useTick(initial: number) {
859
+ const [tick, setTick] = createSignal(initial)
860
+ onMount(() => setTick(initial))
861
+ return [tick, setTick] as const
862
+ }
863
+ `)
864
+ const consumerSource = `'use client'
865
+ import { useTick } from './useTick'
866
+
867
+ export function Ticker() {
868
+ const [tick, setTick] = useTick(0)
869
+ return <button onClick={() => setTick(tick() + 1)}>{tick()}</button>
870
+ }
871
+ `
872
+ const consumerPath = writeFixture('matrix17/Ticker.tsx', consumerSource)
873
+
874
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
875
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
876
+ const clientJs = result.files.find(f => f.type === 'clientJs')
877
+ expect(clientJs).toBeDefined()
878
+ expect(clientJs!.content).toMatch(/import\s*\{[^}]*onMount[^}]*\}\s*from\s*'@barefootjs\/client\/runtime'/)
879
+ })
880
+
881
+ test('M18: a type-only helper import does not trigger BF112 and is re-provisioned as `import type` (#2350)', () => {
882
+ writeFixture('matrix18/todo-types.ts', `export interface Todo {
883
+ id: number
884
+ text: string
885
+ }
886
+ `)
887
+ writeFixture('matrix18/useTodos.tsx', `'use client'
888
+ import { createSignal } from '@barefootjs/client'
889
+ import type { Todo } from './todo-types'
890
+
891
+ export function useTodos(initial: Todo[]) {
892
+ const [todos, setTodos] = createSignal<Todo[]>(initial)
893
+ return [todos, setTodos] as const
894
+ }
895
+ `)
896
+ const consumerSource = `'use client'
897
+ import { useTodos } from './useTodos'
898
+
899
+ export function TodoList() {
900
+ const [todos, setTodos] = useTodos([])
901
+ return <button onClick={() => setTodos([])}>{todos().length}</button>
902
+ }
903
+ `
904
+ const consumerPath = writeFixture('matrix18/TodoList.tsx', consumerSource)
905
+
906
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
907
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
908
+ expect(result.errors.find(e => e.code === 'BF112')).toBeUndefined()
909
+ // Re-provisioned into the still-typed rewritten source (#2350) so tsc on
910
+ // the compiled output can resolve `Todo` in the inlined `createSignal<Todo[]>` —
911
+ // otherwise this is exactly Sora's SavedList gap (see App.tsx's #2350 comment).
912
+ const template = result.files.find(f => f.type === 'markedTemplate')
913
+ expect(template).toBeDefined()
914
+ expect(template!.content).toMatch(/import\s+type\s*\{\s*Todo\s*\}\s*from\s*'\.\/todo-types'/)
915
+ // But never reaches the runtime bundle — type-only imports are erased
916
+ // before clientJs, same as any other TypeScript type annotation.
917
+ const clientJs = result.files.find(f => f.type === 'clientJs')
918
+ expect(clientJs).toBeDefined()
919
+ expect(clientJs!.content).not.toContain('./todo-types')
920
+ })
921
+
922
+ test('M18b: an already-imported type at the call site is not re-provisioned a second time (#2350)', () => {
923
+ // Mirrors Sora's App.tsx: the compiler doesn't re-provision a factory
924
+ // body's TYPE-only references on its own (extractFreeIdentifiersFromNode
925
+ // stops at type nodes), so Sora's App.tsx carries a manual
926
+ // `import type { SavedList }` for exactly this reason. Once #2350 adds
927
+ // re-provisioning, that manual import must dedupe against it, not
928
+ // produce a second, colliding `import type { SavedList }` line.
929
+ writeFixture('matrix18b/schema.ts', `export interface SavedList {
930
+ id: string
931
+ }
932
+ `)
933
+ writeFixture('matrix18b/useListStore.tsx', `'use client'
934
+ import { createSignal } from '@barefootjs/client'
935
+ import type { SavedList } from './schema'
936
+
937
+ export function useListStore() {
938
+ function emptyCard(): SavedList {
939
+ return { id: 'x' }
940
+ }
941
+ const [lists, setLists] = createSignal<SavedList[]>([emptyCard()])
942
+ return { lists, setLists }
943
+ }
944
+ `)
945
+ const consumerSource = `'use client'
946
+ import { useListStore } from './useListStore'
947
+ import type { SavedList } from './schema'
948
+
949
+ export function App() {
950
+ const { lists, setLists } = useListStore()
951
+ return <button onClick={() => setLists([])}>{lists().length}</button>
952
+ }
953
+ `
954
+ const consumerPath = writeFixture('matrix18b/App.tsx', consumerSource)
955
+
956
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
957
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
958
+ expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
959
+ const template = result.files.find(f => f.type === 'markedTemplate')
960
+ expect(template).toBeDefined()
961
+ const matches = template!.content.match(/import\s+type\s*\{\s*SavedList\s*\}/g) ?? []
962
+ expect(matches).toHaveLength(1)
963
+ })
964
+
965
+ test('M18c: a value need is never satisfied by an existing type-only import of the same name (#2350)', () => {
966
+ // The inverse of M18b: the entry file's own `import type { Shared }`
967
+ // has no runtime binding, so a factory needing `Shared` as a VALUE must
968
+ // still decline (BF113) rather than silently treat the type-only import
969
+ // as satisfying it — that would compile clean and crash at hydration
970
+ // (the same dangling-reference direction as #2341 BUG-2), the one
971
+ // failure mode this whole feature exists to avoid.
972
+ writeFixture('matrix18c/shared.ts', `export class Shared {
973
+ static make(): Shared { return new Shared() }
974
+ }
975
+ `)
976
+ writeFixture('matrix18c/useShared.tsx', `'use client'
977
+ import { createSignal } from '@barefootjs/client'
978
+ import { Shared } from './shared'
979
+
980
+ export function useShared() {
981
+ const [value, setValue] = createSignal(Shared.make())
982
+ return { value, setValue }
983
+ }
984
+ `)
985
+ const consumerSource = `'use client'
986
+ import { useShared } from './useShared'
987
+ import type { Shared } from './shared'
988
+
989
+ export function App() {
990
+ const { value, setValue } = useShared()
991
+ return <button onClick={() => setValue(null as unknown as Shared)}>{String(value())}</button>
992
+ }
993
+ `
994
+ const consumerPath = writeFixture('matrix18c/App.tsx', consumerSource)
995
+
996
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
997
+ const bf113 = result.errors.find(e => e.code === 'BF113')
998
+ expect(bf113).toBeDefined()
999
+ expect(bf113!.message).toContain('Shared')
1000
+ })
1001
+
1002
+ test('M18d: a helper VALUE import used only in type position is still re-provisioned (Copilot review, PR #2351)', () => {
1003
+ // `Shape` is a plain value import in the helper file — never wrapped in
1004
+ // `type`/`import type` — but the factory body only ever uses it as a
1005
+ // type annotation. The type-position walk must still find it via
1006
+ // moduleBindings.imported (not just importedTypes), or this regresses
1007
+ // to the exact #2350 gap for names that happen not to be type-only
1008
+ // imports in their OWN file.
1009
+ writeFixture('matrix18d/shape.ts', `export class Shape {
1010
+ area(): number { return 0 }
1011
+ }
1012
+ `)
1013
+ writeFixture('matrix18d/useShape.tsx', `'use client'
1014
+ import { createSignal } from '@barefootjs/client'
1015
+ import { Shape } from './shape'
1016
+
1017
+ export function useShape() {
1018
+ function makeDefault(): Shape {
1019
+ return new Shape()
1020
+ }
1021
+ const [shape, setShape] = createSignal<Shape>(makeDefault())
1022
+ return { shape, setShape }
1023
+ }
1024
+ `)
1025
+ const consumerSource = `'use client'
1026
+ import { useShape } from './useShape'
1027
+
1028
+ export function App() {
1029
+ const { shape, setShape } = useShape()
1030
+ return <button onClick={() => setShape(shape())}>{String(shape())}</button>
1031
+ }
1032
+ `
1033
+ const consumerPath = writeFixture('matrix18d/App.tsx', consumerSource)
1034
+
1035
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1036
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1037
+ const template = result.files.find(f => f.type === 'markedTemplate')
1038
+ expect(template).toBeDefined()
1039
+ // Re-provisioned as a normal VALUE import (not `import type`) — a value
1040
+ // import already brings the type into scope, and the value form is what
1041
+ // the helper file itself declared.
1042
+ expect(template!.content).toMatch(/import\s*\{\s*Shape\s*\}\s*from\s*'\.\/shape'/)
1043
+ expect(template!.content).not.toMatch(/import\s+type\s*\{\s*Shape\s*\}/)
1044
+ })
1045
+
1046
+ test('M18e: a generic type parameter is not misclassified as a module-scope type reference (Copilot review, PR #2351)', () => {
1047
+ // The factory's own <Item> type parameter shadows an unrelated
1048
+ // module-scope `Item` type import — referencing the PARAMETER inside
1049
+ // the factory body must not trigger re-provisioning of the import (it
1050
+ // isn't actually referenced at all).
1051
+ writeFixture('matrix18e/item-types.ts', `export interface Item {
1052
+ id: string
1053
+ }
1054
+ `)
1055
+ writeFixture('matrix18e/useBox.tsx', `'use client'
1056
+ import { createSignal } from '@barefootjs/client'
1057
+ import type { Item } from './item-types'
1058
+
1059
+ export function useBox<Item>(initial: Item) {
1060
+ const [value, setValue] = createSignal<Item>(initial)
1061
+ return { value, setValue }
1062
+ }
1063
+ `)
1064
+ const consumerSource = `'use client'
1065
+ import { useBox } from './useBox'
1066
+
1067
+ export function App() {
1068
+ const { value, setValue } = useBox(0)
1069
+ return <button onClick={() => setValue(1)}>{String(value())}</button>
1070
+ }
1071
+ `
1072
+ const consumerPath = writeFixture('matrix18e/App.tsx', consumerSource)
1073
+
1074
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1075
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1076
+ const template = result.files.find(f => f.type === 'markedTemplate')
1077
+ expect(template).toBeDefined()
1078
+ expect(template!.content).not.toContain('./item-types')
1079
+ })
1080
+
1081
+ test('M18f: a `typeof` reference to a helper value import is re-provisioned as a value import (Copilot review, PR #2351)', () => {
1082
+ // `typeof DEFAULT_SHAPE` is type position syntactically (a TypeQueryNode),
1083
+ // but the name it names — DEFAULT_SHAPE — is a VALUE import, and only its
1084
+ // TYPE is being borrowed here (the nested makeShape's return type). The
1085
+ // body never reads DEFAULT_SHAPE as a value directly, so this isolates
1086
+ // the TypeQueryNode path from the plain-value-walk path that already
1087
+ // covers a direct `DEFAULT_SHAPE` reference regardless of this fix.
1088
+ writeFixture('matrix18f/shapes.ts', `export const DEFAULT_SHAPE = { kind: 'circle' as const, radius: 1 }
1089
+ `)
1090
+ writeFixture('matrix18f/useShape.tsx', `'use client'
1091
+ import { createSignal } from '@barefootjs/client'
1092
+ import { DEFAULT_SHAPE } from './shapes'
1093
+
1094
+ export function useShape() {
1095
+ function makeShape(): typeof DEFAULT_SHAPE {
1096
+ return { kind: 'circle', radius: 1 }
1097
+ }
1098
+ const [shape, setShape] = createSignal(makeShape())
1099
+ return { shape, setShape }
1100
+ }
1101
+ `)
1102
+ const consumerSource = `'use client'
1103
+ import { useShape } from './useShape'
1104
+
1105
+ export function App() {
1106
+ const { shape, setShape } = useShape()
1107
+ return <button onClick={() => setShape({ kind: 'circle', radius: 2 })}>{shape().kind}</button>
1108
+ }
1109
+ `
1110
+ const consumerPath = writeFixture('matrix18f/App.tsx', consumerSource)
1111
+
1112
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1113
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1114
+ const template = result.files.find(f => f.type === 'markedTemplate')
1115
+ expect(template).toBeDefined()
1116
+ expect(template!.content).toMatch(/import\s*\{\s*DEFAULT_SHAPE\s*\}\s*from\s*'\.\/shapes'/)
1117
+ expect(template!.content).not.toMatch(/import\s+type\s*\{\s*DEFAULT_SHAPE\s*\}/)
1118
+ })
1119
+
1120
+ test('M18g: value and type-only re-provisioned imports are globally sorted by specifier, not grouped by kind (Copilot review, PR #2351)', () => {
1121
+ // A type-only need from 'a-types' and a value need from 'z-value' — a
1122
+ // two-separately-sorted-lists implementation would put ALL value lines
1123
+ // before ALL type lines regardless of specifier, landing 'z-value'
1124
+ // before 'a-types'. The correct order interleaves by specifier: 'a-types'
1125
+ // (type-only) first, then 'z-value' (value).
1126
+ writeFixture('matrix18g/a-types.ts', `export interface Early { id: string }
1127
+ `)
1128
+ writeFixture('matrix18g/z-value.ts', `export function makeLate(): number { return 1 }
1129
+ `)
1130
+ writeFixture('matrix18g/useBoth.tsx', `'use client'
1131
+ import { createSignal } from '@barefootjs/client'
1132
+ import type { Early } from './a-types'
1133
+ import { makeLate } from './z-value'
1134
+
1135
+ export function useBoth() {
1136
+ function makeEarly(): Early {
1137
+ return { id: 'x' }
1138
+ }
1139
+ const [n, setN] = createSignal(makeLate())
1140
+ const [e, setE] = createSignal(makeEarly())
1141
+ return { n, setN, e, setE }
1142
+ }
1143
+ `)
1144
+ const consumerSource = `'use client'
1145
+ import { useBoth } from './useBoth'
1146
+
1147
+ export function App() {
1148
+ const { n, setN, e, setE } = useBoth()
1149
+ return <button onClick={() => setN(n() + 1)}>{n()} {e().id}</button>
1150
+ }
1151
+ `
1152
+ const consumerPath = writeFixture('matrix18g/App.tsx', consumerSource)
1153
+
1154
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1155
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1156
+ const template = result.files.find(f => f.type === 'markedTemplate')
1157
+ expect(template).toBeDefined()
1158
+ const aTypesIndex = template!.content.indexOf("from './a-types'")
1159
+ const zValueIndex = template!.content.indexOf("from './z-value'")
1160
+ expect(aTypesIndex).toBeGreaterThan(-1)
1161
+ expect(zValueIndex).toBeGreaterThan(-1)
1162
+ expect(aTypesIndex).toBeLessThan(zValueIndex)
1163
+ })
1164
+
1165
+ test('M19: a colliding binding nested inside a JSX callback still triggers BF113', () => {
1166
+ // Pins collectEntryBindingNames's depth: the ONLY `doubleIt` binding in
1167
+ // the consumer file is declared inside an onClick callback, not at any
1168
+ // top level, yet the re-provisioning collision check must still find it
1169
+ // (an over-broad scan is required — a narrower one would silently
1170
+ // shadow the injected import at runtime instead of declining loudly).
1171
+ writeFixture('matrix19/lib/mathmod.ts', `export function doubleIt(x: number): number {
1172
+ return x * 2
1173
+ }
1174
+ `)
1175
+ writeFixture('matrix19/hooks/useDouble.tsx', `'use client'
1176
+ import { createSignal } from '@barefootjs/client'
1177
+ import { doubleIt } from '../lib/mathmod'
1178
+
1179
+ export function useDouble(initial: number) {
1180
+ const [value, setValue] = createSignal(doubleIt(initial))
1181
+ const bump = () => setValue(doubleIt(value()))
1182
+ return { value, bump }
1183
+ }
1184
+ `)
1185
+ const consumerSource = `'use client'
1186
+ import { useDouble } from '../hooks/useDouble'
1187
+
1188
+ export function Collide() {
1189
+ const { value, bump } = useDouble(21)
1190
+ return <button onClick={() => { const doubleIt = 1; bump(); console.log(doubleIt) }}>{value()}</button>
1191
+ }
1192
+ `
1193
+ const consumerPath = writeFixture('matrix19/components/Collide.tsx', consumerSource)
1194
+
1195
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1196
+ const bf113 = result.errors.find(e => e.code === 'BF113')
1197
+ expect(bf113).toBeDefined()
1198
+ expect(bf113!.message).toContain('doubleIt')
1199
+ })
1200
+
1201
+ test('M20: 3 call sites of one imported factory with distinct tuple caller names', () => {
1202
+ writeFixture('matrix20/useCounter.tsx', `'use client'
1203
+ import { createSignal } from '@barefootjs/client'
1204
+
1205
+ export function useCounter(initial: number) {
1206
+ const [count, setCount] = createSignal(initial)
1207
+ return [count, setCount] as const
1208
+ }
1209
+ `)
1210
+ const consumerSource = `'use client'
1211
+ import { useCounter } from './useCounter'
1212
+
1213
+ export function Triple() {
1214
+ const [a, setA] = useCounter(1)
1215
+ const [b, setB] = useCounter(2)
1216
+ const [c, setC] = useCounter(3)
1217
+ return <button onClick={() => { setA(a() + 1); setB(b() + 1); setC(c() + 1) }}>{a()} {b()} {c()}</button>
1218
+ }
1219
+ `
1220
+ const consumerPath = writeFixture('matrix20/Triple.tsx', consumerSource)
1221
+
1222
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1223
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1224
+ const clientJs = result.files.find(f => f.type === 'clientJs')
1225
+ expect(clientJs).toBeDefined()
1226
+ expect(clientJs!.content.match(/createSignal\(/g)?.length).toBe(3)
1227
+ for (const name of ['a', 'setA', 'b', 'setB', 'c', 'setC']) {
1228
+ expect(clientJs!.content).toContain(name)
1229
+ }
1230
+ })
1231
+
1232
+ test('M21: SSR through a barrel reflects the re-provisioned import (mirrors matrix 6)', () => {
1233
+ writeFixture('matrix21/lib2/mathmod.ts', `export function doubleIt(x: number): number {
1234
+ return x * 2
1235
+ }
1236
+ `)
1237
+ writeFixture('matrix21/hooks2/useDouble.tsx', `'use client'
1238
+ import { createSignal } from '@barefootjs/client'
1239
+ import { doubleIt } from '../lib2/mathmod'
1240
+
1241
+ export function useDouble(initial: number) {
1242
+ const [value, setValue] = createSignal(doubleIt(initial))
1243
+ const bump = () => setValue(doubleIt(value()))
1244
+ return { value, bump }
1245
+ }
1246
+ `)
1247
+ writeFixture('matrix21/hooks2barrel/index.ts', `export { useDouble } from '../hooks2/useDouble'
1248
+ `)
1249
+ const consumerSource = `'use client'
1250
+ import { useDouble } from '../hooks2barrel'
1251
+
1252
+ export function DoublerSSR() {
1253
+ const { value, bump } = useDouble(21)
1254
+ return <button onClick={bump}>{value()}</button>
1255
+ }
1256
+ `
1257
+ const consumerPath = writeFixture('matrix21/components/DoublerSSR.tsx', consumerSource)
1258
+
1259
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1260
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1261
+ const template = result.files.find(f => f.type === 'markedTemplate')
1262
+ expect(template).toBeDefined()
1263
+ expect(template!.content).toMatch(/import\s*\{\s*doubleIt\s*\}\s*from\s*'\.\.\/lib2\/mathmod'/)
1264
+ expect(template!.content).toContain('doubleIt(')
1265
+ })
1266
+
1267
+ test('M22: aliased factory import + aliased helper import both resolve correctly', () => {
1268
+ writeFixture('matrix22/lib/mathmod.ts', `export function doubleIt(x: number): number {
1269
+ return x * 2
1270
+ }
1271
+ `)
1272
+ writeFixture('matrix22/hooks/useDouble.tsx', `'use client'
1273
+ import { createSignal } from '@barefootjs/client'
1274
+ import { doubleIt as dbl } from '../lib/mathmod'
1275
+
1276
+ export function useDouble(initial: number) {
1277
+ const [value, setValue] = createSignal(dbl(initial))
1278
+ const bump = () => setValue(dbl(value()))
1279
+ return { value, bump }
1280
+ }
1281
+ `)
1282
+ const consumerSource = `'use client'
1283
+ import { useDouble as useDoubleAliased } from '../hooks/useDouble'
1284
+
1285
+ export function Doubler() {
1286
+ const { value, bump } = useDoubleAliased(21)
1287
+ return <button onClick={bump}>{value()}</button>
1288
+ }
1289
+ `
1290
+ const consumerPath = writeFixture('matrix22/components/Doubler.tsx', consumerSource)
1291
+
1292
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1293
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1294
+ const clientJs = result.files.find(f => f.type === 'clientJs')
1295
+ expect(clientJs).toBeDefined()
1296
+ // The re-provisioned import preserves the HELPER file's own local alias.
1297
+ expect(clientJs!.content).toMatch(/import\s*\{\s*doubleIt as dbl\s*\}/)
1298
+ })
1299
+
1300
+ test('M23: an already-satisfied import through a barrel dedupes (no BF113, one occurrence)', () => {
1301
+ writeFixture('matrix23/lib/mathmod.ts', `export function doubleIt(x: number): number {
1302
+ return x * 2
1303
+ }
1304
+ `)
1305
+ writeFixture('matrix23/hooks/useDouble.tsx', `'use client'
1306
+ import { createSignal } from '@barefootjs/client'
1307
+ import { doubleIt } from '../lib/mathmod'
1308
+
1309
+ export function useDouble(initial: number) {
1310
+ const [value, setValue] = createSignal(doubleIt(initial))
1311
+ const bump = () => setValue(doubleIt(value()))
1312
+ return { value, bump }
1313
+ }
1314
+ `)
1315
+ writeFixture('matrix23/hooks/index.ts', `export { useDouble } from './useDouble'
1316
+ `)
1317
+ const consumerSource = `'use client'
1318
+ import { useDouble } from '../hooks'
1319
+ import { doubleIt } from '../lib/mathmod'
1320
+
1321
+ export function DoublerSelfImporting() {
1322
+ const { value, bump } = useDouble(21)
1323
+ return <button onClick={() => { bump(); doubleIt(value()) }}>{value()}</button>
1324
+ }
1325
+ `
1326
+ const consumerPath = writeFixture('matrix23/components/DoublerSelfImporting.tsx', consumerSource)
1327
+
1328
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1329
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1330
+ expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
1331
+ const clientJs = result.files.find(f => f.type === 'clientJs')
1332
+ expect(clientJs).toBeDefined()
1333
+ expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
1334
+ })
1335
+ })