@barefootjs/jsx 0.23.0 → 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.
@@ -237,3 +237,848 @@ export function Config() {
237
237
  expect(objectResult.errors.find(e => e.code === 'BF111')).toBeUndefined()
238
238
  })
239
239
  })
240
+
241
+ describe('Cross-file factory import re-provisioning (#2332)', () => {
242
+ beforeAll(() => {
243
+ // lib/mathmod.ts — the third module, deliberately .ts and in a different
244
+ // directory than both hook and component so the relative-path math is real.
245
+ writeFixture('lib/mathmod.ts', `export function doubleIt(x: number): number {
246
+ return x * 2
247
+ }
248
+ `)
249
+ // hooks/useDouble.tsx — the helper: factory body calls an import.
250
+ writeFixture('hooks/useDouble.tsx', `'use client'
251
+ import { createSignal } from '@barefootjs/client'
252
+ import { doubleIt } from '../lib/mathmod'
253
+
254
+ export function useDouble(initial: number) {
255
+ const [value, setValue] = createSignal(doubleIt(initial))
256
+ const bump = () => setValue(doubleIt(value()))
257
+ return { value, bump }
258
+ }
259
+ `)
260
+ })
261
+
262
+ test('matrix 1: re-provisioned import, different directory depths', () => {
263
+ const consumerSource = `'use client'
264
+ import { useDouble } from '../hooks/useDouble'
265
+
266
+ export function Doubler() {
267
+ const { value, bump } = useDouble(21)
268
+ return <button onClick={bump}>{value()}</button>
269
+ }
270
+ `
271
+ const consumerPath = writeFixture('components/Doubler.tsx', consumerSource)
272
+ const ctx = analyzeComponent(consumerSource, consumerPath, 'Doubler')
273
+ expect(ctx.signals.length).toBe(1)
274
+ expect(ctx.signals[0].getter).toBe('value')
275
+
276
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
277
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
278
+ const clientJs = result.files.find(f => f.type === 'clientJs')
279
+ expect(clientJs).toBeDefined()
280
+ // Specifier rewritten relative to components/, not hooks/ — the off-by-one trap.
281
+ expect(clientJs!.content).toMatch(/import\s*\{\s*doubleIt\s*\}\s*from\s*'\.\.\/lib\/mathmod'/)
282
+ expect(clientJs!.content).not.toContain('../hooks/useDouble') // factory inlined, not imported
283
+ })
284
+
285
+ test('matrix 2: bare specifier passes through unchanged', () => {
286
+ writeFixture('hooks/useClamped.tsx', `'use client'
287
+ import { createSignal } from '@barefootjs/client'
288
+ import { clamp } from 'tiny-clamp'
289
+
290
+ export function useClamped(initial: number) {
291
+ const [value, setValue] = createSignal(clamp(initial))
292
+ const bump = () => setValue(clamp(value() + 1))
293
+ return { value, bump }
294
+ }
295
+ `)
296
+ const consumerSource = `'use client'
297
+ import { useClamped } from '../hooks/useClamped'
298
+
299
+ export function Clamped() {
300
+ const { value, bump } = useClamped(21)
301
+ return <button onClick={bump}>{value()}</button>
302
+ }
303
+ `
304
+ const consumerPath = writeFixture('components/Clamped.tsx', consumerSource)
305
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
306
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
307
+ const clientJs = result.files.find(f => f.type === 'clientJs')
308
+ expect(clientJs).toBeDefined()
309
+ // Bare specifiers skip resolution entirely, so the package need not exist on disk.
310
+ expect(clientJs!.content).toMatch(/import\s*\{\s*clamp\s*\}\s*from\s*'tiny-clamp'/)
311
+ })
312
+
313
+ test('matrix 3: dedupe across two factories sharing the same helper import', () => {
314
+ writeFixture('hooks/pair.tsx', `'use client'
315
+ import { createSignal } from '@barefootjs/client'
316
+ import { doubleIt } from '../lib/mathmod'
317
+
318
+ export function useA(initial: number) {
319
+ const [value, setValue] = createSignal(doubleIt(initial))
320
+ const bump = () => setValue(doubleIt(value()))
321
+ return { value, bump }
322
+ }
323
+
324
+ export function useB(initial: number) {
325
+ const [other, setOther] = createSignal(doubleIt(initial))
326
+ const bumpOther = () => setOther(doubleIt(other()))
327
+ return { other, bumpOther }
328
+ }
329
+ `)
330
+ const consumerSource = `'use client'
331
+ import { useA, useB } from '../hooks/pair'
332
+
333
+ export function Pair() {
334
+ const { value, bump } = useA(1)
335
+ const { other, bumpOther } = useB(2)
336
+ return <button onClick={() => { bump(); bumpOther() }}>{value()} / {other()}</button>
337
+ }
338
+ `
339
+ const consumerPath = writeFixture('components/Pair.tsx', consumerSource)
340
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
341
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
342
+ const clientJs = result.files.find(f => f.type === 'clientJs')
343
+ expect(clientJs).toBeDefined()
344
+ expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
345
+ })
346
+
347
+ test('re-provisioned imports are emitted in sorted order regardless of inlining order (Copilot review, PR #2338)', () => {
348
+ // `importsBySpecifier`/`inlinedFactories` are populated in AST-traversal/
349
+ // insertion order — deliberately adversarial here: the component calls
350
+ // the factory needing '../lib/zmod' BEFORE the one needing
351
+ // '../lib/amod', so insertion order is z-then-a. Emission must still be
352
+ // alphabetical (a-then-z), or the generated import block would be
353
+ // order-unstable across unrelated analyzer refactors.
354
+ writeFixture('lib/zmod.ts', `export function zHelper(x: number): number {
355
+ return x
356
+ }
357
+ `)
358
+ writeFixture('lib/amod.ts', `export function aHelper(x: number): number {
359
+ return x
360
+ }
361
+ `)
362
+ writeFixture('hooks/useZFactory.tsx', `'use client'
363
+ import { createSignal } from '@barefootjs/client'
364
+ import { zHelper } from '../lib/zmod'
365
+
366
+ export function useZFactory(initial: number) {
367
+ const [zValue, setZValue] = createSignal(zHelper(initial))
368
+ return { zValue, setZValue }
369
+ }
370
+ `)
371
+ writeFixture('hooks/useAFactory.tsx', `'use client'
372
+ import { createSignal } from '@barefootjs/client'
373
+ import { aHelper } from '../lib/amod'
374
+
375
+ export function useAFactory(initial: number) {
376
+ const [aValue, setAValue] = createSignal(aHelper(initial))
377
+ return { aValue, setAValue }
378
+ }
379
+ `)
380
+ const consumerSource = `'use client'
381
+ import { useZFactory } from '../hooks/useZFactory'
382
+ import { useAFactory } from '../hooks/useAFactory'
383
+
384
+ export function Ordered() {
385
+ const { zValue } = useZFactory(1)
386
+ const { aValue } = useAFactory(2)
387
+ return <p>{zValue()} / {aValue()}</p>
388
+ }
389
+ `
390
+ const consumerPath = writeFixture('components/Ordered.tsx', consumerSource)
391
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
392
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
393
+ const clientJs = result.files.find(f => f.type === 'clientJs')
394
+ expect(clientJs).toBeDefined()
395
+ const amodIndex = clientJs!.content.indexOf("from '../lib/amod'")
396
+ const zmodIndex = clientJs!.content.indexOf("from '../lib/zmod'")
397
+ expect(amodIndex).toBeGreaterThan(-1)
398
+ expect(zmodIndex).toBeGreaterThan(-1)
399
+ expect(amodIndex).toBeLessThan(zmodIndex)
400
+ })
401
+
402
+ test('matrix 4: local-name collision with a re-provisioned import declines with BF113', () => {
403
+ const consumerSource = `'use client'
404
+ import { useDouble } from '../hooks/useDouble'
405
+
406
+ function doubleIt(): number {
407
+ return 1
408
+ }
409
+
410
+ export function Collide() {
411
+ const { value, bump } = useDouble(21)
412
+ return <button onClick={bump}>{value()} {doubleIt()}</button>
413
+ }
414
+ `
415
+ const consumerPath = writeFixture('components/Collide.tsx', consumerSource)
416
+ const ctx = analyzeComponent(consumerSource, consumerPath, 'Collide')
417
+ expect(ctx.signals.length).toBe(0)
418
+
419
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
420
+ const bf113 = result.errors.find(e => e.code === 'BF113')
421
+ expect(bf113).toBeDefined()
422
+ expect(bf113!.message).toContain('doubleIt')
423
+ expect(bf113!.message).toContain('../lib/mathmod')
424
+ const clientJs = result.files.find(f => f.type === 'clientJs')
425
+ if (clientJs) {
426
+ expect(clientJs.content).not.toContain(`from '../lib/mathmod'`)
427
+ }
428
+ })
429
+
430
+ test('matrix 5b: default-import reference stays BF112, not re-provisioned', () => {
431
+ writeFixture('lib/mathmod-default.ts', `export default {
432
+ doubleIt(x: number): number {
433
+ return x * 2
434
+ },
435
+ }
436
+ `)
437
+ writeFixture('hooks/useDefault.tsx', `'use client'
438
+ import { createSignal } from '@barefootjs/client'
439
+ import mathmod from '../lib/mathmod-default'
440
+
441
+ export function useDefault(initial: number) {
442
+ const [value, setValue] = createSignal(mathmod.doubleIt(initial))
443
+ const bump = () => setValue(mathmod.doubleIt(value()))
444
+ return { value, bump }
445
+ }
446
+ `)
447
+ const consumerSource = `'use client'
448
+ import { useDefault } from '../hooks/useDefault'
449
+
450
+ export function Defaulter() {
451
+ const { value, bump } = useDefault(21)
452
+ return <button onClick={bump}>{value()}</button>
453
+ }
454
+ `
455
+ const consumerPath = writeFixture('components/Defaulter.tsx', consumerSource)
456
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
457
+ const bf112 = result.errors.find(e => e.code === 'BF112')
458
+ expect(bf112).toBeDefined()
459
+ expect(bf112!.message).toContain('mathmod')
460
+ expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
461
+ })
462
+
463
+ test('matrix 6: SSR output reflects the re-provisioned import', () => {
464
+ const consumerSource = `'use client'
465
+ import { useDouble } from '../hooks/useDouble'
466
+
467
+ export function DoublerSSR() {
468
+ const { value, bump } = useDouble(21)
469
+ return <button onClick={bump}>{value()}</button>
470
+ }
471
+ `
472
+ const consumerPath = writeFixture('components/DoublerSSR.tsx', consumerSource)
473
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
474
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
475
+ const template = result.files.find(f => f.type === 'markedTemplate')
476
+ expect(template).toBeDefined()
477
+ // The injected import is part of the same rewritten source both passes read:
478
+ // the adapter re-emits it via metadata.templateImports…
479
+ expect(template!.content).toMatch(/import\s*\{\s*doubleIt\s*\}\s*from\s*'\.\.\/lib\/mathmod'/)
480
+ // …and the inlined signal initializer calls it at SSR render.
481
+ expect(template!.content).toContain('doubleIt(')
482
+ })
483
+
484
+ test('already-satisfied import dedupes: no BF113, no duplicate declaration', () => {
485
+ const consumerSource = `'use client'
486
+ import { useDouble } from '../hooks/useDouble'
487
+ import { doubleIt } from '../lib/mathmod'
488
+
489
+ export function DoublerSelfImporting() {
490
+ const { value, bump } = useDouble(21)
491
+ return <button onClick={() => { bump(); doubleIt(value()) }}>{value()}</button>
492
+ }
493
+ `
494
+ const consumerPath = writeFixture('components/DoublerSelfImporting.tsx', consumerSource)
495
+ const result = compileJSX(consumerSource, consumerPath, { adapter })
496
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
497
+ expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
498
+ const clientJs = result.files.find(f => f.type === 'clientJs')
499
+ expect(clientJs).toBeDefined()
500
+ expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
501
+ })
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
+ })