@barefootjs/jsx 0.24.1 → 0.25.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.
@@ -500,3 +500,585 @@ 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 not re-provisioned', () => {
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
+ const clientJs = result.files.find(f => f.type === 'clientJs')
910
+ expect(clientJs).toBeDefined()
911
+ expect(clientJs!.content).not.toContain('./todo-types')
912
+ })
913
+
914
+ test('M19: a colliding binding nested inside a JSX callback still triggers BF113', () => {
915
+ // Pins collectEntryBindingNames's depth: the ONLY `doubleIt` binding in
916
+ // the consumer file is declared inside an onClick callback, not at any
917
+ // top level, yet the re-provisioning collision check must still find it
918
+ // (an over-broad scan is required — a narrower one would silently
919
+ // shadow the injected import at runtime instead of declining loudly).
920
+ writeFixture('matrix19/lib/mathmod.ts', `export function doubleIt(x: number): number {
921
+ return x * 2
922
+ }
923
+ `)
924
+ writeFixture('matrix19/hooks/useDouble.tsx', `'use client'
925
+ import { createSignal } from '@barefootjs/client'
926
+ import { doubleIt } from '../lib/mathmod'
927
+
928
+ export function useDouble(initial: number) {
929
+ const [value, setValue] = createSignal(doubleIt(initial))
930
+ const bump = () => setValue(doubleIt(value()))
931
+ return { value, bump }
932
+ }
933
+ `)
934
+ const consumerSource = `'use client'
935
+ import { useDouble } from '../hooks/useDouble'
936
+
937
+ export function Collide() {
938
+ const { value, bump } = useDouble(21)
939
+ return <button onClick={() => { const doubleIt = 1; bump(); console.log(doubleIt) }}>{value()}</button>
940
+ }
941
+ `
942
+ const consumerPath = writeFixture('matrix19/components/Collide.tsx', consumerSource)
943
+
944
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
945
+ const bf113 = result.errors.find(e => e.code === 'BF113')
946
+ expect(bf113).toBeDefined()
947
+ expect(bf113!.message).toContain('doubleIt')
948
+ })
949
+
950
+ test('M20: 3 call sites of one imported factory with distinct tuple caller names', () => {
951
+ writeFixture('matrix20/useCounter.tsx', `'use client'
952
+ import { createSignal } from '@barefootjs/client'
953
+
954
+ export function useCounter(initial: number) {
955
+ const [count, setCount] = createSignal(initial)
956
+ return [count, setCount] as const
957
+ }
958
+ `)
959
+ const consumerSource = `'use client'
960
+ import { useCounter } from './useCounter'
961
+
962
+ export function Triple() {
963
+ const [a, setA] = useCounter(1)
964
+ const [b, setB] = useCounter(2)
965
+ const [c, setC] = useCounter(3)
966
+ return <button onClick={() => { setA(a() + 1); setB(b() + 1); setC(c() + 1) }}>{a()} {b()} {c()}</button>
967
+ }
968
+ `
969
+ const consumerPath = writeFixture('matrix20/Triple.tsx', consumerSource)
970
+
971
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
972
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
973
+ const clientJs = result.files.find(f => f.type === 'clientJs')
974
+ expect(clientJs).toBeDefined()
975
+ expect(clientJs!.content.match(/createSignal\(/g)?.length).toBe(3)
976
+ for (const name of ['a', 'setA', 'b', 'setB', 'c', 'setC']) {
977
+ expect(clientJs!.content).toContain(name)
978
+ }
979
+ })
980
+
981
+ test('M21: SSR through a barrel reflects the re-provisioned import (mirrors matrix 6)', () => {
982
+ writeFixture('matrix21/lib2/mathmod.ts', `export function doubleIt(x: number): number {
983
+ return x * 2
984
+ }
985
+ `)
986
+ writeFixture('matrix21/hooks2/useDouble.tsx', `'use client'
987
+ import { createSignal } from '@barefootjs/client'
988
+ import { doubleIt } from '../lib2/mathmod'
989
+
990
+ export function useDouble(initial: number) {
991
+ const [value, setValue] = createSignal(doubleIt(initial))
992
+ const bump = () => setValue(doubleIt(value()))
993
+ return { value, bump }
994
+ }
995
+ `)
996
+ writeFixture('matrix21/hooks2barrel/index.ts', `export { useDouble } from '../hooks2/useDouble'
997
+ `)
998
+ const consumerSource = `'use client'
999
+ import { useDouble } from '../hooks2barrel'
1000
+
1001
+ export function DoublerSSR() {
1002
+ const { value, bump } = useDouble(21)
1003
+ return <button onClick={bump}>{value()}</button>
1004
+ }
1005
+ `
1006
+ const consumerPath = writeFixture('matrix21/components/DoublerSSR.tsx', consumerSource)
1007
+
1008
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1009
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1010
+ const template = result.files.find(f => f.type === 'markedTemplate')
1011
+ expect(template).toBeDefined()
1012
+ expect(template!.content).toMatch(/import\s*\{\s*doubleIt\s*\}\s*from\s*'\.\.\/lib2\/mathmod'/)
1013
+ expect(template!.content).toContain('doubleIt(')
1014
+ })
1015
+
1016
+ test('M22: aliased factory import + aliased helper import both resolve correctly', () => {
1017
+ writeFixture('matrix22/lib/mathmod.ts', `export function doubleIt(x: number): number {
1018
+ return x * 2
1019
+ }
1020
+ `)
1021
+ writeFixture('matrix22/hooks/useDouble.tsx', `'use client'
1022
+ import { createSignal } from '@barefootjs/client'
1023
+ import { doubleIt as dbl } from '../lib/mathmod'
1024
+
1025
+ export function useDouble(initial: number) {
1026
+ const [value, setValue] = createSignal(dbl(initial))
1027
+ const bump = () => setValue(dbl(value()))
1028
+ return { value, bump }
1029
+ }
1030
+ `)
1031
+ const consumerSource = `'use client'
1032
+ import { useDouble as useDoubleAliased } from '../hooks/useDouble'
1033
+
1034
+ export function Doubler() {
1035
+ const { value, bump } = useDoubleAliased(21)
1036
+ return <button onClick={bump}>{value()}</button>
1037
+ }
1038
+ `
1039
+ const consumerPath = writeFixture('matrix22/components/Doubler.tsx', consumerSource)
1040
+
1041
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1042
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1043
+ const clientJs = result.files.find(f => f.type === 'clientJs')
1044
+ expect(clientJs).toBeDefined()
1045
+ // The re-provisioned import preserves the HELPER file's own local alias.
1046
+ expect(clientJs!.content).toMatch(/import\s*\{\s*doubleIt as dbl\s*\}/)
1047
+ })
1048
+
1049
+ test('M23: an already-satisfied import through a barrel dedupes (no BF113, one occurrence)', () => {
1050
+ writeFixture('matrix23/lib/mathmod.ts', `export function doubleIt(x: number): number {
1051
+ return x * 2
1052
+ }
1053
+ `)
1054
+ writeFixture('matrix23/hooks/useDouble.tsx', `'use client'
1055
+ import { createSignal } from '@barefootjs/client'
1056
+ import { doubleIt } from '../lib/mathmod'
1057
+
1058
+ export function useDouble(initial: number) {
1059
+ const [value, setValue] = createSignal(doubleIt(initial))
1060
+ const bump = () => setValue(doubleIt(value()))
1061
+ return { value, bump }
1062
+ }
1063
+ `)
1064
+ writeFixture('matrix23/hooks/index.ts', `export { useDouble } from './useDouble'
1065
+ `)
1066
+ const consumerSource = `'use client'
1067
+ import { useDouble } from '../hooks'
1068
+ import { doubleIt } from '../lib/mathmod'
1069
+
1070
+ export function DoublerSelfImporting() {
1071
+ const { value, bump } = useDouble(21)
1072
+ return <button onClick={() => { bump(); doubleIt(value()) }}>{value()}</button>
1073
+ }
1074
+ `
1075
+ const consumerPath = writeFixture('matrix23/components/DoublerSelfImporting.tsx', consumerSource)
1076
+
1077
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
1078
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
1079
+ expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
1080
+ const clientJs = result.files.find(f => f.type === 'clientJs')
1081
+ expect(clientJs).toBeDefined()
1082
+ expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
1083
+ })
1084
+ })