@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
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* for destructures of non-recognised factory shapes.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { describe, test, expect } from 'bun:test'
|
|
13
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
|
|
14
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs'
|
|
15
|
+
import { tmpdir } from 'os'
|
|
16
|
+
import path from 'path'
|
|
14
17
|
import { analyzeComponent } from '../analyzer'
|
|
15
18
|
import { compileJSX } from '../compiler'
|
|
16
19
|
import { TestAdapter } from '../adapters/test-adapter'
|
|
@@ -482,3 +485,526 @@ describe('Object-return reactive factories (#2325)', () => {
|
|
|
482
485
|
expect(result.errors.find(e => e.code === 'BF110')).toBeUndefined()
|
|
483
486
|
})
|
|
484
487
|
})
|
|
488
|
+
|
|
489
|
+
describe('Guard-clause / nested-return factories decline (#2341 BUG-3)', () => {
|
|
490
|
+
test('T1: if-guard return declines with BF110, no inert splice (issue repro)', () => {
|
|
491
|
+
const source = `
|
|
492
|
+
'use client'
|
|
493
|
+
import { createSignal } from '@barefootjs/client'
|
|
494
|
+
function useBounded(initial: number, max: number) {
|
|
495
|
+
const [n, setN] = createSignal(initial)
|
|
496
|
+
const bump = () => setN(Math.min(n() + 1, max))
|
|
497
|
+
if (initial > max) {
|
|
498
|
+
return { n, bump }
|
|
499
|
+
}
|
|
500
|
+
return { n, bump }
|
|
501
|
+
}
|
|
502
|
+
export function Bounded() {
|
|
503
|
+
const { n, bump } = useBounded(0, 10)
|
|
504
|
+
return <button onClick={bump}>{n()}</button>
|
|
505
|
+
}
|
|
506
|
+
`
|
|
507
|
+
|
|
508
|
+
const ctx = analyzeComponent(source, 'Bounded.tsx')
|
|
509
|
+
expect(ctx.signals.length).toBe(0)
|
|
510
|
+
|
|
511
|
+
const result = compileJSX(source, 'Bounded.tsx', { adapter })
|
|
512
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
513
|
+
expect(bf110).toBeDefined()
|
|
514
|
+
expect(bf110!.message).toContain('useBounded')
|
|
515
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
516
|
+
if (clientJs) {
|
|
517
|
+
expect(clientJs.content).not.toContain('if (0 > 10)')
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
test('T2: return inside try/catch declines', () => {
|
|
522
|
+
const source = `
|
|
523
|
+
'use client'
|
|
524
|
+
import { createSignal } from '@barefootjs/client'
|
|
525
|
+
function useBounded(initial: number, max: number) {
|
|
526
|
+
const [n, setN] = createSignal(initial)
|
|
527
|
+
const bump = () => setN(Math.min(n() + 1, max))
|
|
528
|
+
try {
|
|
529
|
+
return { n, bump }
|
|
530
|
+
} catch {}
|
|
531
|
+
return { n, bump }
|
|
532
|
+
}
|
|
533
|
+
export function Bounded() {
|
|
534
|
+
const { n, bump } = useBounded(0, 10)
|
|
535
|
+
return <button onClick={bump}>{n()}</button>
|
|
536
|
+
}
|
|
537
|
+
`
|
|
538
|
+
|
|
539
|
+
const ctx = analyzeComponent(source, 'Bounded.tsx')
|
|
540
|
+
expect(ctx.signals.length).toBe(0)
|
|
541
|
+
|
|
542
|
+
const result = compileJSX(source, 'Bounded.tsx', { adapter })
|
|
543
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
544
|
+
expect(bf110).toBeDefined()
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
test('T3: return inside a for-loop declines', () => {
|
|
548
|
+
const source = `
|
|
549
|
+
'use client'
|
|
550
|
+
import { createSignal } from '@barefootjs/client'
|
|
551
|
+
function useBounded(initial: number, max: number, seed: number[]) {
|
|
552
|
+
const [n, setN] = createSignal(initial)
|
|
553
|
+
const bump = () => setN(Math.min(n() + 1, max))
|
|
554
|
+
for (const x of seed) {
|
|
555
|
+
if (x < 0) return { n, bump }
|
|
556
|
+
}
|
|
557
|
+
return { n, bump }
|
|
558
|
+
}
|
|
559
|
+
export function Bounded() {
|
|
560
|
+
const { n, bump } = useBounded(0, 10, [1, 2, 3])
|
|
561
|
+
return <button onClick={bump}>{n()}</button>
|
|
562
|
+
}
|
|
563
|
+
`
|
|
564
|
+
|
|
565
|
+
const ctx = analyzeComponent(source, 'Bounded.tsx')
|
|
566
|
+
expect(ctx.signals.length).toBe(0)
|
|
567
|
+
|
|
568
|
+
const result = compileJSX(source, 'Bounded.tsx', { adapter })
|
|
569
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
570
|
+
expect(bf110).toBeDefined()
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
test('T4: braced-arrow-callback returns do NOT decline (false-positive guard)', () => {
|
|
574
|
+
const source = `
|
|
575
|
+
'use client'
|
|
576
|
+
import { createSignal } from '@barefootjs/client'
|
|
577
|
+
function createCounter(initial: number) {
|
|
578
|
+
const [n, setN] = createSignal(initial)
|
|
579
|
+
const bump = () => { return setN(n() + 1) }
|
|
580
|
+
return { n, bump }
|
|
581
|
+
}
|
|
582
|
+
export function Counter() {
|
|
583
|
+
const { n, bump } = createCounter(0)
|
|
584
|
+
return <button onClick={bump}>{n()}</button>
|
|
585
|
+
}
|
|
586
|
+
`
|
|
587
|
+
|
|
588
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter })
|
|
589
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
590
|
+
const ctx = analyzeComponent(source, 'Counter.tsx')
|
|
591
|
+
expect(ctx.signals.length).toBe(1)
|
|
592
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
593
|
+
expect(clientJs).toContain('createSignal')
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test('T4b: return inside an object-literal method does NOT decline (Copilot review, PR #2342)', () => {
|
|
597
|
+
// The nested-return boundary check originally stopped only at
|
|
598
|
+
// FunctionDeclaration/FunctionExpression/ArrowFunction, so a `return`
|
|
599
|
+
// inside an object-literal method (or a class method/accessor) declared
|
|
600
|
+
// in the factory body was incorrectly counted toward the total, wrongly
|
|
601
|
+
// declassifying an otherwise-inlinable factory. ts.isFunctionLike
|
|
602
|
+
// (methods/accessors/constructors, not just plain functions) fixes it.
|
|
603
|
+
const source = `
|
|
604
|
+
'use client'
|
|
605
|
+
import { createSignal } from '@barefootjs/client'
|
|
606
|
+
function createLogger(initial: number) {
|
|
607
|
+
const [n, setN] = createSignal(initial)
|
|
608
|
+
const logger = {
|
|
609
|
+
describe() {
|
|
610
|
+
if (n() > 0) return 'positive'
|
|
611
|
+
return 'non-positive'
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
return { n, setN, logger }
|
|
615
|
+
}
|
|
616
|
+
export function Counter() {
|
|
617
|
+
const { n, setN, logger } = createLogger(0)
|
|
618
|
+
return <button onClick={() => setN(n() + 1)}>{logger.describe()}</button>
|
|
619
|
+
}
|
|
620
|
+
`
|
|
621
|
+
|
|
622
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter })
|
|
623
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
624
|
+
const ctx = analyzeComponent(source, 'Counter.tsx')
|
|
625
|
+
expect(ctx.signals.length).toBe(1)
|
|
626
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
627
|
+
expect(clientJs).toContain('createSignal')
|
|
628
|
+
expect(clientJs).toContain("'positive'")
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
describe('T5: cross-file guard-clause factory', () => {
|
|
632
|
+
let fixtureDir: string
|
|
633
|
+
|
|
634
|
+
beforeAll(() => {
|
|
635
|
+
fixtureDir = mkdtempSync(path.join(tmpdir(), 'bf-factory-guard-clause-'))
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
afterAll(() => {
|
|
639
|
+
rmSync(fixtureDir, { recursive: true, force: true })
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
function writeFixture(name: string, content: string): string {
|
|
643
|
+
const p = path.join(fixtureDir, name)
|
|
644
|
+
mkdirSync(path.dirname(p), { recursive: true })
|
|
645
|
+
writeFileSync(p, content, 'utf8')
|
|
646
|
+
return p
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
test('guard-clause factory in an imported helper declines with BF110 (reactive-shaped path)', () => {
|
|
650
|
+
writeFixture('hooks-bounded.tsx', `'use client'
|
|
651
|
+
import { createSignal } from '@barefootjs/client'
|
|
652
|
+
|
|
653
|
+
export function useBounded(initial: number, max: number) {
|
|
654
|
+
const [n, setN] = createSignal(initial)
|
|
655
|
+
const bump = () => setN(Math.min(n() + 1, max))
|
|
656
|
+
if (initial > max) {
|
|
657
|
+
return { n, bump }
|
|
658
|
+
}
|
|
659
|
+
return { n, bump }
|
|
660
|
+
}
|
|
661
|
+
`)
|
|
662
|
+
const consumerSource = `'use client'
|
|
663
|
+
import { useBounded } from './hooks-bounded'
|
|
664
|
+
|
|
665
|
+
export function Bounded() {
|
|
666
|
+
const { n, bump } = useBounded(0, 10)
|
|
667
|
+
return <button onClick={bump}>{n()}</button>
|
|
668
|
+
}
|
|
669
|
+
`
|
|
670
|
+
const consumerPath = writeFixture('bounded-consumer.tsx', consumerSource)
|
|
671
|
+
|
|
672
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
673
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
674
|
+
expect(bf110).toBeDefined()
|
|
675
|
+
expect(bf110!.message).toContain('does not match the inlinable factory shape')
|
|
676
|
+
})
|
|
677
|
+
})
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
describe('Real-world factory matrix (#2341)', () => {
|
|
681
|
+
test('M1: onMount/onCleanup inside a factory body', () => {
|
|
682
|
+
const source = `
|
|
683
|
+
'use client'
|
|
684
|
+
import { createSignal, onMount, onCleanup } from '@barefootjs/client'
|
|
685
|
+
|
|
686
|
+
function useTick() {
|
|
687
|
+
const [tick, setTick] = createSignal(0)
|
|
688
|
+
onMount(() => setTick(1))
|
|
689
|
+
onCleanup(() => setTick(0))
|
|
690
|
+
return { tick, setTick }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function Ticker() {
|
|
694
|
+
const { tick, setTick } = useTick()
|
|
695
|
+
return <button onClick={() => setTick(tick() + 1)}>{tick()}</button>
|
|
696
|
+
}
|
|
697
|
+
`
|
|
698
|
+
|
|
699
|
+
const ctx = analyzeComponent(source, 'Ticker.tsx')
|
|
700
|
+
expect(ctx.signals.length).toBe(1)
|
|
701
|
+
|
|
702
|
+
const result = compileJSX(source, 'Ticker.tsx', { adapter })
|
|
703
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
704
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
705
|
+
expect(clientJs).toContain('onMount(')
|
|
706
|
+
expect(clientJs).toContain('onCleanup(')
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
test('M2: createEffect inside a factory body', () => {
|
|
710
|
+
const source = `
|
|
711
|
+
'use client'
|
|
712
|
+
import { createSignal, createEffect } from '@barefootjs/client'
|
|
713
|
+
|
|
714
|
+
function useTitle(initial: number) {
|
|
715
|
+
const [count, setCount] = createSignal(initial)
|
|
716
|
+
createEffect(() => { document.title = String(count()) })
|
|
717
|
+
return [count, setCount] as const
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function TitleUpdater() {
|
|
721
|
+
const [count, setCount] = useTitle(0)
|
|
722
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
723
|
+
}
|
|
724
|
+
`
|
|
725
|
+
|
|
726
|
+
const result = compileJSX(source, 'TitleUpdater.tsx', { adapter })
|
|
727
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
728
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
729
|
+
expect(clientJs).toContain('createEffect')
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
test('M3: createDisposableEffect inside a factory body', () => {
|
|
733
|
+
const source = `
|
|
734
|
+
'use client'
|
|
735
|
+
import { createSignal, createDisposableEffect } from '@barefootjs/client'
|
|
736
|
+
|
|
737
|
+
function useDisposableTitle(initial: number) {
|
|
738
|
+
const [count, setCount] = createSignal(initial)
|
|
739
|
+
createDisposableEffect(() => { document.title = String(count()) })
|
|
740
|
+
return [count, setCount] as const
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function DisposableTitleUpdater() {
|
|
744
|
+
const [count, setCount] = useDisposableTitle(0)
|
|
745
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
746
|
+
}
|
|
747
|
+
`
|
|
748
|
+
|
|
749
|
+
const result = compileJSX(source, 'DisposableTitleUpdater.tsx', { adapter })
|
|
750
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
751
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
752
|
+
expect(clientJs).toContain('createDisposableEffect')
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
test('M4: realistic store (Sora useListStore scale) — 3 signals + 2 memos', () => {
|
|
756
|
+
const source = `
|
|
757
|
+
'use client'
|
|
758
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
759
|
+
|
|
760
|
+
function useListStore(initial: string[]) {
|
|
761
|
+
const [items, setItems] = createSignal(initial)
|
|
762
|
+
const [filter, setFilter] = createSignal('')
|
|
763
|
+
const [editing, setEditing] = createSignal('')
|
|
764
|
+
const visible = createMemo(() => items().filter(i => i.includes(filter())))
|
|
765
|
+
const count = createMemo(() => visible().length)
|
|
766
|
+
const clear = () => setItems([])
|
|
767
|
+
return { items, setItems, filter, setFilter, editing, setEditing, visible, count, clear }
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function ListView() {
|
|
771
|
+
const { items, setItems, filter, setFilter, editing, setEditing, visible, count, clear } = useListStore([])
|
|
772
|
+
return (
|
|
773
|
+
<div onClick={() => { setItems([...items(), 'x']); setFilter('a'); setEditing('x'); clear() }}>
|
|
774
|
+
{visible().length} / {count()} / {editing()}
|
|
775
|
+
</div>
|
|
776
|
+
)
|
|
777
|
+
}
|
|
778
|
+
`
|
|
779
|
+
|
|
780
|
+
const ctx = analyzeComponent(source, 'ListView.tsx')
|
|
781
|
+
expect(ctx.signals.length).toBe(3)
|
|
782
|
+
expect(ctx.memos.length).toBe(2)
|
|
783
|
+
|
|
784
|
+
const result = compileJSX(source, 'ListView.tsx', { adapter })
|
|
785
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
test('M5: identifier hygiene across 3 call sites of the same factory', () => {
|
|
789
|
+
// Each call site inlines a fresh copy of the factory body; the
|
|
790
|
+
// undestructured internal `setValue` must be suffix-renamed uniquely
|
|
791
|
+
// per call site (_bf0/_bf1/_bf2) while `value`/`bump` are renamed
|
|
792
|
+
// directly to each call site's own tuple names.
|
|
793
|
+
const source = `
|
|
794
|
+
'use client'
|
|
795
|
+
import { createSignal } from '@barefootjs/client'
|
|
796
|
+
|
|
797
|
+
function createPair(initial: number) {
|
|
798
|
+
const [value, setValue] = createSignal(initial)
|
|
799
|
+
const bump = () => setValue(value() + 1)
|
|
800
|
+
return [value, bump] as const
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function Triple() {
|
|
804
|
+
const [a, bumpA] = createPair(1)
|
|
805
|
+
const [b, bumpB] = createPair(2)
|
|
806
|
+
const [c, bumpC] = createPair(3)
|
|
807
|
+
return <button onClick={() => { bumpA(); bumpB(); bumpC() }}>{a()} {b()} {c()}</button>
|
|
808
|
+
}
|
|
809
|
+
`
|
|
810
|
+
|
|
811
|
+
const result = compileJSX(source, 'Triple.tsx', { adapter })
|
|
812
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
813
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
814
|
+
expect(clientJs.match(/createSignal\(/g)?.length).toBe(3)
|
|
815
|
+
expect(clientJs).toContain('_bf0')
|
|
816
|
+
expect(clientJs).toContain('_bf1')
|
|
817
|
+
expect(clientJs).toContain('_bf2')
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
test('M6: generic factory — type arguments do not disturb inlining', () => {
|
|
821
|
+
const source = `
|
|
822
|
+
'use client'
|
|
823
|
+
import { createSignal } from '@barefootjs/client'
|
|
824
|
+
|
|
825
|
+
function createBox<T>(initial: T) {
|
|
826
|
+
const [v, setV] = createSignal<T>(initial)
|
|
827
|
+
return { v, setV }
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function Box() {
|
|
831
|
+
const { v, setV } = createBox<string>('x')
|
|
832
|
+
return <button onClick={() => setV('y')}>{v()}</button>
|
|
833
|
+
}
|
|
834
|
+
`
|
|
835
|
+
|
|
836
|
+
const ctx = analyzeComponent(source, 'Box.tsx')
|
|
837
|
+
expect(ctx.signals.length).toBe(1)
|
|
838
|
+
|
|
839
|
+
const result = compileJSX(source, 'Box.tsx', { adapter })
|
|
840
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
test('M7: mixed reactive/non-reactive body — plain locals are also renamed correctly', () => {
|
|
844
|
+
const source = `
|
|
845
|
+
'use client'
|
|
846
|
+
import { createSignal } from '@barefootjs/client'
|
|
847
|
+
|
|
848
|
+
function createSession(initial: number) {
|
|
849
|
+
const startedAt = Date.now()
|
|
850
|
+
const label = \`session-\${startedAt}\`
|
|
851
|
+
const [count, setCount] = createSignal(initial)
|
|
852
|
+
const bump = () => { setCount(count() + 1); console.log(label) }
|
|
853
|
+
return [count, bump] as const
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export function Session() {
|
|
857
|
+
const [count, bump] = createSession(0)
|
|
858
|
+
return <button onClick={bump}>{count()}</button>
|
|
859
|
+
}
|
|
860
|
+
`
|
|
861
|
+
|
|
862
|
+
const result = compileJSX(source, 'Session.tsx', { adapter })
|
|
863
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
864
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
865
|
+
expect(clientJs).toContain('Date.now()')
|
|
866
|
+
expect(clientJs).toMatch(/startedAt_bf\d+/)
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
test('M8: default-value destructure of an object-return factory declines with BF111', () => {
|
|
870
|
+
const source = `
|
|
871
|
+
'use client'
|
|
872
|
+
import { createSignal } from '@barefootjs/client'
|
|
873
|
+
|
|
874
|
+
function createCounter(initial: number) {
|
|
875
|
+
const [count, setCount] = createSignal(initial)
|
|
876
|
+
return { count, setCount }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export function Counter() {
|
|
880
|
+
const { count = 5 } = createCounter(0)
|
|
881
|
+
return <p>{count}</p>
|
|
882
|
+
}
|
|
883
|
+
`
|
|
884
|
+
|
|
885
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter })
|
|
886
|
+
const bf111 = result.errors.find(e => e.code === 'BF111')
|
|
887
|
+
expect(bf111).toBeDefined()
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
test('M9: unknown destructured property declines with BF110', () => {
|
|
891
|
+
const source = `
|
|
892
|
+
'use client'
|
|
893
|
+
import { createSignal } from '@barefootjs/client'
|
|
894
|
+
|
|
895
|
+
function createCounter(initial: number) {
|
|
896
|
+
const [count, setCount] = createSignal(initial)
|
|
897
|
+
return { count, setCount }
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export function Counter() {
|
|
901
|
+
const { missing } = createCounter(0)
|
|
902
|
+
return <p>{missing}</p>
|
|
903
|
+
}
|
|
904
|
+
`
|
|
905
|
+
|
|
906
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter })
|
|
907
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
908
|
+
expect(bf110).toBeDefined()
|
|
909
|
+
expect(bf110!.message).toContain('missing')
|
|
910
|
+
expect(bf110!.message).toContain('not present in its return')
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
test('M10: whole-store (non-destructured) call is left untouched', () => {
|
|
914
|
+
// Pins the current fallback posture — a non-destructured factory call
|
|
915
|
+
// is not inlined at all, so none of the factory diagnostics apply.
|
|
916
|
+
// Whether it should eventually be double-invocation-safe is out of
|
|
917
|
+
// scope for #2341 (tracked separately in the issue).
|
|
918
|
+
const source = `
|
|
919
|
+
'use client'
|
|
920
|
+
import { createSignal } from '@barefootjs/client'
|
|
921
|
+
|
|
922
|
+
function createCounter(initial: number) {
|
|
923
|
+
const [count, setCount] = createSignal(initial)
|
|
924
|
+
return { count, setCount }
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function Counter() {
|
|
928
|
+
const store = createCounter(0)
|
|
929
|
+
return <p>{store.count}</p>
|
|
930
|
+
}
|
|
931
|
+
`
|
|
932
|
+
|
|
933
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter })
|
|
934
|
+
expect(result.errors.find(e => e.code?.startsWith('BF11'))).toBeUndefined()
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
test('M11: 3-element tuple return', () => {
|
|
938
|
+
const source = `
|
|
939
|
+
'use client'
|
|
940
|
+
import { createSignal } from '@barefootjs/client'
|
|
941
|
+
|
|
942
|
+
function createResettable(initial: number) {
|
|
943
|
+
const [value, setValue] = createSignal(initial)
|
|
944
|
+
const reset = () => setValue(initial)
|
|
945
|
+
return [value, setValue, reset] as const
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export function Resettable() {
|
|
949
|
+
const [count, setCount, resetCount] = createResettable(0)
|
|
950
|
+
return <button onClick={() => { setCount(count() + 1); resetCount() }}>{count()}</button>
|
|
951
|
+
}
|
|
952
|
+
`
|
|
953
|
+
|
|
954
|
+
const ctx = analyzeComponent(source, 'Resettable.tsx')
|
|
955
|
+
expect(ctx.signals.length).toBe(1)
|
|
956
|
+
|
|
957
|
+
const result = compileJSX(source, 'Resettable.tsx', { adapter })
|
|
958
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
959
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
960
|
+
expect(clientJs).toContain('count')
|
|
961
|
+
expect(clientJs).toContain('setCount')
|
|
962
|
+
expect(clientJs).toContain('resetCount')
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
test('M12: a complex (non-atomic) argument expression keeps its parens at the splice site', () => {
|
|
966
|
+
const source = `
|
|
967
|
+
'use client'
|
|
968
|
+
import { createSignal } from '@barefootjs/client'
|
|
969
|
+
|
|
970
|
+
function createCounter(initial: number) {
|
|
971
|
+
const [count, setCount] = createSignal(initial)
|
|
972
|
+
return [count, setCount] as const
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export function Counter() {
|
|
976
|
+
const a = 1
|
|
977
|
+
const [count, setCount] = createCounter(a > 0 ? a : 0)
|
|
978
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
979
|
+
}
|
|
980
|
+
`
|
|
981
|
+
|
|
982
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter })
|
|
983
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
984
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
985
|
+
expect(clientJs).toContain('(a > 0 ? a : 0)')
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
test('M13: SSR seed value flows through the inlined factory', () => {
|
|
989
|
+
const source = `
|
|
990
|
+
'use client'
|
|
991
|
+
import { createSignal } from '@barefootjs/client'
|
|
992
|
+
|
|
993
|
+
function createCounter(initial: number) {
|
|
994
|
+
const [count, setCount] = createSignal(initial)
|
|
995
|
+
return [count, setCount] as const
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
export function Counter() {
|
|
999
|
+
const [count, setCount] = createCounter(42)
|
|
1000
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
1001
|
+
}
|
|
1002
|
+
`
|
|
1003
|
+
|
|
1004
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter })
|
|
1005
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1006
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
1007
|
+
expect(template).toBeDefined()
|
|
1008
|
+
expect(template!.content).toContain('42')
|
|
1009
|
+
})
|
|
1010
|
+
})
|