@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.
- package/dist/adapters/dangerous-inner-html.d.ts +52 -24
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +473 -165
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +11 -5
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +24 -2
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +13 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +31 -10
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +29 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
- package/src/__tests__/nested-loop-conditional.test.ts +133 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
- package/src/__tests__/reactive-factory-cross-file.test.ts +833 -0
- package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
- package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
- package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
- package/src/adapters/dangerous-inner-html.ts +101 -48
- package/src/analyzer.ts +607 -142
- package/src/errors.ts +4 -0
- package/src/ir-to-client-js/collect-elements.ts +28 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
- package/src/ir-to-client-js/reactivity.ts +61 -7
- package/src/ir-to-client-js/types.ts +13 -0
- package/src/rich-type-refusal.ts +3 -2
- package/src/to-locale-date-lowering.ts +50 -13
- 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
|
+
})
|