@barefootjs/jsx 0.1.1 → 0.1.3
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/parsed-expr-emitter.d.ts +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +22 -0
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +0 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +306 -38
- package/dist/ir-to-client-js/compute-inlinability.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +6 -18
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +10 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +13 -17
- package/src/__tests__/circular-dependency.audit.test.ts +19 -0
- package/src/__tests__/compiler-stress-1244.test.ts +6 -10
- package/src/__tests__/component-not-found.audit.test.ts +36 -0
- package/src/__tests__/doc-examples.test.ts +5 -6
- package/src/__tests__/invalid-component-name.audit.test.ts +38 -0
- package/src/__tests__/invalid-jsx-attribute.audit.test.ts +47 -0
- package/src/__tests__/invalid-jsx-expression.audit.test.ts +44 -0
- package/src/__tests__/invalid-signal-usage.audit.test.ts +72 -0
- package/src/__tests__/jsx-function-inlining.test.ts +281 -1
- package/src/__tests__/loop-fallback-wrap.test.ts +5 -5
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -4
- package/src/__tests__/props-type-mismatch.audit.test.ts +100 -0
- package/src/__tests__/staged-ir/10-stage-diagnostics.test.ts +83 -2
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -4
- package/src/__tests__/type-inference-failed.audit.test.ts +48 -0
- package/src/__tests__/unknown-signal.audit.test.ts +111 -0
- package/src/adapters/parsed-expr-emitter.ts +1 -1
- package/src/analyzer-context.ts +25 -0
- package/src/analyzer.ts +207 -1
- package/src/errors.ts +4 -26
- package/src/expression-parser.ts +8 -9
- package/src/ir-to-client-js/compute-inlinability.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +3 -8
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +6 -17
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +5 -19
- package/src/jsx-to-ir.ts +215 -4
- package/src/types.ts +11 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BF012 `INVALID_SIGNAL_USAGE` deletion audit.
|
|
3
|
+
*
|
|
4
|
+
* BF012 was reserved for "Invalid signal usage" — a vague placeholder
|
|
5
|
+
* with no defined scenario and no emission site. Every concrete
|
|
6
|
+
* signal-misuse pattern already has a dedicated diagnostic:
|
|
7
|
+
*
|
|
8
|
+
* - BF011: module-level createSignal / createMemo
|
|
9
|
+
* - BF044: signal getter passed without calling it
|
|
10
|
+
* - BF060: reactive binding in template scope
|
|
11
|
+
*
|
|
12
|
+
* This file proves the compiler handles signal-related edge cases
|
|
13
|
+
* without needing BF012.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, test, expect } from 'bun:test'
|
|
17
|
+
import { analyzeComponent } from '../analyzer'
|
|
18
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
19
|
+
import { ErrorCodes } from '../errors'
|
|
20
|
+
|
|
21
|
+
function compileToIR(source: string) {
|
|
22
|
+
const ctx = analyzeComponent(source, '/tmp/Test.tsx')
|
|
23
|
+
const ir = jsxToIR(ctx)
|
|
24
|
+
return { ctx, ir, errors: ctx.errors }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('BF012 INVALID_SIGNAL_USAGE — deletion audit', () => {
|
|
28
|
+
test('valid signal usage produces no errors', () => {
|
|
29
|
+
const src = `'use client'
|
|
30
|
+
import { createSignal } from '@barefootjs/client'
|
|
31
|
+
export function Counter() {
|
|
32
|
+
const [count, setCount] = createSignal(0)
|
|
33
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
34
|
+
}
|
|
35
|
+
`
|
|
36
|
+
const { errors } = compileToIR(src)
|
|
37
|
+
expect(errors).toHaveLength(0)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('module-level signal is caught by BF011, not BF012', () => {
|
|
41
|
+
const src = `'use client'
|
|
42
|
+
import { createSignal } from '@barefootjs/client'
|
|
43
|
+
const [count, setCount] = createSignal(0)
|
|
44
|
+
export function Counter() {
|
|
45
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
const ctx = analyzeComponent(src, '/tmp/Counter.tsx', 'Counter')
|
|
49
|
+
const codes = ctx.errors.map(e => e.code)
|
|
50
|
+
expect(codes).toContain(ErrorCodes.SIGNAL_OUTSIDE_COMPONENT)
|
|
51
|
+
expect(codes).not.toContain('BF012')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('signal getter passed without calling is caught by BF044, not BF012', () => {
|
|
55
|
+
const src = `'use client'
|
|
56
|
+
import { createSignal } from '@barefootjs/client'
|
|
57
|
+
export function Counter() {
|
|
58
|
+
const [count, setCount] = createSignal(0)
|
|
59
|
+
return <div value={count} />
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
const { errors } = compileToIR(src)
|
|
63
|
+
const codes = errors.map(e => e.code)
|
|
64
|
+
expect(codes).toContain(ErrorCodes.SIGNAL_GETTER_NOT_CALLED)
|
|
65
|
+
expect(codes).not.toContain('BF012')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('BF012 code no longer exists in ErrorCodes', () => {
|
|
69
|
+
const allCodes = Object.values(ErrorCodes)
|
|
70
|
+
expect(allCodes).not.toContain('BF012')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -105,7 +105,7 @@ describe('JSX function inlining (#569)', () => {
|
|
|
105
105
|
expect(fn!.isJsxFunction).toBe(true)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
test('
|
|
108
|
+
test('multi-return function is stored in jsxMultiReturnFunctions (not jsxFunctions)', () => {
|
|
109
109
|
const source = `
|
|
110
110
|
'use client'
|
|
111
111
|
|
|
@@ -121,6 +121,11 @@ describe('JSX function inlining (#569)', () => {
|
|
|
121
121
|
const ctx = analyzeComponent(source, 'MyComponent.tsx')
|
|
122
122
|
|
|
123
123
|
expect(ctx.jsxFunctions.has('renderItem')).toBe(false)
|
|
124
|
+
expect(ctx.jsxMultiReturnFunctions.has('renderItem')).toBe(true)
|
|
125
|
+
const info = ctx.jsxMultiReturnFunctions.get('renderItem')!
|
|
126
|
+
expect(info.params).toEqual(['active'])
|
|
127
|
+
expect(info.branches).toHaveLength(1)
|
|
128
|
+
expect(info.fallback).not.toBeNull()
|
|
124
129
|
})
|
|
125
130
|
|
|
126
131
|
test('does not set isJsxFunction for non-JSX returning functions', () => {
|
|
@@ -422,4 +427,279 @@ describe('JSX function inlining (#569)', () => {
|
|
|
422
427
|
expect(template!.content).toContain('table')
|
|
423
428
|
})
|
|
424
429
|
})
|
|
430
|
+
|
|
431
|
+
describe('multi-return JSX function inlining', () => {
|
|
432
|
+
test('if/else helper is inlined as conditional IR', () => {
|
|
433
|
+
const source = `
|
|
434
|
+
'use client'
|
|
435
|
+
import { createSignal } from '@barefootjs/client'
|
|
436
|
+
|
|
437
|
+
export function StatusDisplay() {
|
|
438
|
+
const [status, setStatus] = createSignal('ok')
|
|
439
|
+
|
|
440
|
+
function renderBadge(s: string) {
|
|
441
|
+
if (s === 'error') return <span class="error">Error</span>
|
|
442
|
+
return <span class="ok">OK</span>
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return <div>{renderBadge(status())}</div>
|
|
446
|
+
}
|
|
447
|
+
`
|
|
448
|
+
|
|
449
|
+
const ctx = analyzeComponent(source, 'StatusDisplay.tsx')
|
|
450
|
+
expect(ctx.jsxMultiReturnFunctions.has('renderBadge')).toBe(true)
|
|
451
|
+
|
|
452
|
+
const ir = jsxToIR(ctx)
|
|
453
|
+
expect(ir).not.toBeNull()
|
|
454
|
+
|
|
455
|
+
// Verify IR contains a conditional node (not an opaque expression)
|
|
456
|
+
function findConditional(node: any): any {
|
|
457
|
+
if (node?.type === 'conditional') return node
|
|
458
|
+
for (const child of node?.children ?? []) {
|
|
459
|
+
const found = findConditional(child)
|
|
460
|
+
if (found) return found
|
|
461
|
+
}
|
|
462
|
+
return null
|
|
463
|
+
}
|
|
464
|
+
const cond = findConditional(ir)
|
|
465
|
+
expect(cond).not.toBeNull()
|
|
466
|
+
expect(cond.type).toBe('conditional')
|
|
467
|
+
|
|
468
|
+
const result = compileJSX(source, 'StatusDisplay.tsx', { adapter })
|
|
469
|
+
expect(result.errors).toHaveLength(0)
|
|
470
|
+
|
|
471
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
472
|
+
expect(template).toContain('error')
|
|
473
|
+
expect(template).toContain('OK')
|
|
474
|
+
// Helper function should NOT appear verbatim — it's inlined
|
|
475
|
+
expect(template).not.toMatch(/function renderBadge/)
|
|
476
|
+
|
|
477
|
+
// Client JS should not contain the raw helper function with JSX
|
|
478
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
479
|
+
if (clientJs) {
|
|
480
|
+
expect(clientJs.content).not.toMatch(/function renderBadge/)
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('if/else if/else chain inlines to nested conditionals', () => {
|
|
485
|
+
const source = `
|
|
486
|
+
'use client'
|
|
487
|
+
|
|
488
|
+
export function Display(props: { level: string }) {
|
|
489
|
+
function renderLevel(lvl: string) {
|
|
490
|
+
if (lvl === 'high') return <span class="high">High</span>
|
|
491
|
+
if (lvl === 'mid') return <span class="mid">Mid</span>
|
|
492
|
+
return <span class="low">Low</span>
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return <div>{renderLevel(props.level)}</div>
|
|
496
|
+
}
|
|
497
|
+
`
|
|
498
|
+
|
|
499
|
+
const result = compileJSX(source, 'Display.tsx', { adapter })
|
|
500
|
+
expect(result.errors).toHaveLength(0)
|
|
501
|
+
|
|
502
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
503
|
+
expect(template).toContain('High')
|
|
504
|
+
expect(template).toContain('Mid')
|
|
505
|
+
expect(template).toContain('Low')
|
|
506
|
+
expect(template).not.toMatch(/function renderLevel/)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test('guard clause (early return null) is handled', () => {
|
|
510
|
+
const source = `
|
|
511
|
+
'use client'
|
|
512
|
+
import { createSignal } from '@barefootjs/client'
|
|
513
|
+
|
|
514
|
+
export function App() {
|
|
515
|
+
const [show, setShow] = createSignal(true)
|
|
516
|
+
|
|
517
|
+
function renderOptional(visible: boolean) {
|
|
518
|
+
if (!visible) return null
|
|
519
|
+
return <strong>Visible</strong>
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return <div>{renderOptional(show())}</div>
|
|
523
|
+
}
|
|
524
|
+
`
|
|
525
|
+
|
|
526
|
+
const result = compileJSX(source, 'App.tsx', { adapter })
|
|
527
|
+
expect(result.errors).toHaveLength(0)
|
|
528
|
+
|
|
529
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
530
|
+
expect(template).toContain('Visible')
|
|
531
|
+
expect(template).not.toMatch(/function renderOptional/)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
test('arrow function with multi-return is inlined', () => {
|
|
535
|
+
const source = `
|
|
536
|
+
'use client'
|
|
537
|
+
|
|
538
|
+
export function App(props: { type: string }) {
|
|
539
|
+
const renderIcon = (t: string) => {
|
|
540
|
+
if (t === 'star') return <span>★</span>
|
|
541
|
+
return <span>○</span>
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return <div>{renderIcon(props.type)}</div>
|
|
545
|
+
}
|
|
546
|
+
`
|
|
547
|
+
|
|
548
|
+
const ctx = analyzeComponent(source, 'App.tsx')
|
|
549
|
+
expect(ctx.jsxMultiReturnFunctions.has('renderIcon')).toBe(true)
|
|
550
|
+
|
|
551
|
+
const result = compileJSX(source, 'App.tsx', { adapter })
|
|
552
|
+
expect(result.errors).toHaveLength(0)
|
|
553
|
+
|
|
554
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
555
|
+
expect(template).toContain('★')
|
|
556
|
+
expect(template).toContain('○')
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
test('param substitution works in both condition and branches', () => {
|
|
560
|
+
const source = `
|
|
561
|
+
'use client'
|
|
562
|
+
|
|
563
|
+
export function App(props: { name: string }) {
|
|
564
|
+
function greet(who: string) {
|
|
565
|
+
if (who === 'world') return <span>Hello World!</span>
|
|
566
|
+
return <span>Hi {who}!</span>
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return <div>{greet(props.name)}</div>
|
|
570
|
+
}
|
|
571
|
+
`
|
|
572
|
+
|
|
573
|
+
const result = compileJSX(source, 'App.tsx', { adapter })
|
|
574
|
+
expect(result.errors).toHaveLength(0)
|
|
575
|
+
|
|
576
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
577
|
+
// The param 'who' should be replaced with 'props.name' in the condition
|
|
578
|
+
expect(template).toContain('props.name')
|
|
579
|
+
expect(template).not.toMatch(/\bwho\b/)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test('switch/case helper is inlined as nested conditional', () => {
|
|
583
|
+
const source = `
|
|
584
|
+
'use client'
|
|
585
|
+
|
|
586
|
+
export function App(props: { icon: string }) {
|
|
587
|
+
function renderIcon(name: string) {
|
|
588
|
+
switch (name) {
|
|
589
|
+
case 'home': return <span>🏠</span>
|
|
590
|
+
case 'star': return <span>⭐</span>
|
|
591
|
+
default: return <span>?</span>
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return <div>{renderIcon(props.icon)}</div>
|
|
596
|
+
}
|
|
597
|
+
`
|
|
598
|
+
|
|
599
|
+
const ctx = analyzeComponent(source, 'App.tsx')
|
|
600
|
+
expect(ctx.jsxMultiReturnFunctions.has('renderIcon')).toBe(true)
|
|
601
|
+
const info = ctx.jsxMultiReturnFunctions.get('renderIcon')!
|
|
602
|
+
expect(info.branches).toHaveLength(2)
|
|
603
|
+
expect(info.fallback).not.toBeNull()
|
|
604
|
+
expect(info.switchDiscriminant).toBeDefined()
|
|
605
|
+
|
|
606
|
+
const result = compileJSX(source, 'App.tsx', { adapter })
|
|
607
|
+
expect(result.errors).toHaveLength(0)
|
|
608
|
+
|
|
609
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
610
|
+
expect(template).toContain('🏠')
|
|
611
|
+
expect(template).toContain('⭐')
|
|
612
|
+
expect(template).toContain('?')
|
|
613
|
+
expect(template).not.toMatch(/function renderIcon/)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
test('switch with null default is handled', () => {
|
|
617
|
+
const source = `
|
|
618
|
+
'use client'
|
|
619
|
+
|
|
620
|
+
export function App(props: { status: string }) {
|
|
621
|
+
function renderStatus(s: string) {
|
|
622
|
+
switch (s) {
|
|
623
|
+
case 'ok': return <span class="ok">OK</span>
|
|
624
|
+
default: return null
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return <div>{renderStatus(props.status)}</div>
|
|
629
|
+
}
|
|
630
|
+
`
|
|
631
|
+
|
|
632
|
+
const result = compileJSX(source, 'App.tsx', { adapter })
|
|
633
|
+
expect(result.errors).toHaveLength(0)
|
|
634
|
+
|
|
635
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
636
|
+
expect(template).toContain('OK')
|
|
637
|
+
expect(template).not.toMatch(/function renderStatus/)
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
test('switch with side-effect discriminant is NOT inlined', () => {
|
|
641
|
+
const source = `
|
|
642
|
+
'use client'
|
|
643
|
+
|
|
644
|
+
export function App() {
|
|
645
|
+
function getValue(): string { return 'a' }
|
|
646
|
+
function renderByValue(v: string) {
|
|
647
|
+
switch (getValue()) {
|
|
648
|
+
case 'a': return <span>A</span>
|
|
649
|
+
default: return <span>B</span>
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return <div>{renderByValue('x')}</div>
|
|
654
|
+
}
|
|
655
|
+
`
|
|
656
|
+
|
|
657
|
+
const ctx = analyzeComponent(source, 'App.tsx')
|
|
658
|
+
// Should NOT be registered because discriminant is a call expression
|
|
659
|
+
expect(ctx.jsxMultiReturnFunctions.has('renderByValue')).toBe(false)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
test('switch without default clause is NOT inlined', () => {
|
|
663
|
+
const source = `
|
|
664
|
+
'use client'
|
|
665
|
+
|
|
666
|
+
export function App(props: { icon: string }) {
|
|
667
|
+
function renderIcon(name: string) {
|
|
668
|
+
switch (name) {
|
|
669
|
+
case 'home': return <span>🏠</span>
|
|
670
|
+
case 'star': return <span>⭐</span>
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return <div>{renderIcon(props.icon)}</div>
|
|
675
|
+
}
|
|
676
|
+
`
|
|
677
|
+
|
|
678
|
+
const ctx = analyzeComponent(source, 'App.tsx')
|
|
679
|
+
expect(ctx.jsxMultiReturnFunctions.has('renderIcon')).toBe(false)
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
test('if/else with trailing return does not overwrite else fallback', () => {
|
|
683
|
+
const source = `
|
|
684
|
+
'use client'
|
|
685
|
+
|
|
686
|
+
export function App(props: { mode: string }) {
|
|
687
|
+
function renderMode(m: string) {
|
|
688
|
+
if (m === 'a') return <span>A</span>
|
|
689
|
+
else return <span>B</span>
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return <div>{renderMode(props.mode)}</div>
|
|
693
|
+
}
|
|
694
|
+
`
|
|
695
|
+
|
|
696
|
+
const result = compileJSX(source, 'App.tsx', { adapter })
|
|
697
|
+
expect(result.errors).toHaveLength(0)
|
|
698
|
+
|
|
699
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
700
|
+
// The else fallback should be <span>B</span>, not overwritten
|
|
701
|
+
expect(template).toContain('A')
|
|
702
|
+
expect(template).toContain('B')
|
|
703
|
+
})
|
|
704
|
+
})
|
|
425
705
|
})
|
|
@@ -110,9 +110,10 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
110
110
|
expect(clientJs).not.toMatch(/\breconcileList\s*\(/)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
test('
|
|
114
|
-
// `items` is a
|
|
115
|
-
//
|
|
113
|
+
test('prop array compiles to mapArray (#1586)', () => {
|
|
114
|
+
// `props.items` is a direct prop reference — props are always
|
|
115
|
+
// potentially reactive (parent may pass signal getters), so the
|
|
116
|
+
// loop must use mapArray for reconciliation.
|
|
116
117
|
const source = `
|
|
117
118
|
export function List(props: { items: string[] }) {
|
|
118
119
|
return (
|
|
@@ -124,8 +125,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
124
125
|
`
|
|
125
126
|
|
|
126
127
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
127
|
-
expect(clientJs).
|
|
128
|
-
expect(clientJs).not.toMatch(/\breconcileList\s*\(/)
|
|
128
|
+
expect(clientJs).toMatch(/\bmapArray\s*\(/)
|
|
129
129
|
})
|
|
130
130
|
|
|
131
131
|
test('unrecognised-call chain with filter now reconciles', () => {
|
|
@@ -64,7 +64,7 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
64
64
|
// Both effects must run inside the inner mapArray's renderItem (so
|
|
65
65
|
// they capture `task` as the inner-loop accessor — `task()` rather
|
|
66
66
|
// than the module-level closure).
|
|
67
|
-
expect(content).toMatch(/createEffect\(\(\) => \{\s
|
|
67
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?styleToCss\([\s\S]*?task\(\)\.id/)
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
test('non-style reactive attribute (className) wires up too', () => {
|
|
@@ -141,7 +141,7 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
141
141
|
|
|
142
142
|
// `data-off` uses the truthy-check shape (no `__v != null` for this
|
|
143
143
|
// attribute) so a concrete `false` removes the attribute.
|
|
144
|
-
expect(content).toMatch(/createEffect\(\(\) => \{
|
|
144
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?if \(c\(\)\.isOff\)\s*\S+\.setAttribute\('data-off',\s*''\)/)
|
|
145
145
|
// aria-* keeps the explicit "true" value per WAI-ARIA.
|
|
146
146
|
expect(content).toContain("setAttribute('aria-pressed', 'true')")
|
|
147
147
|
})
|
|
@@ -234,7 +234,7 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
234
234
|
// to subscribe to the per-item accessor). The bug we're locking
|
|
235
235
|
// down is purely in the createEffect emission, so scope to that.
|
|
236
236
|
const effectMatch = content.match(
|
|
237
|
-
/createEffect\(\(\) => \{
|
|
237
|
+
/createEffect\(\(\) => \{[\s\S]*?styleToCss\(\{[\s\S]*?\}\)[\s\S]*?__ta_s\d+\.setAttribute\('style'/,
|
|
238
238
|
)
|
|
239
239
|
expect(effectMatch).not.toBeNull()
|
|
240
240
|
const effectBody = effectMatch![0]
|
|
@@ -283,7 +283,7 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
283
283
|
expect(result.errors).toHaveLength(0)
|
|
284
284
|
const content = result.files.find((f) => f.type === 'clientJs')!.content
|
|
285
285
|
const effectMatch = content.match(
|
|
286
|
-
/createEffect\(\(\) => \{
|
|
286
|
+
/createEffect\(\(\) => \{[\s\S]*?styleToCss\(\{[\s\S]*?\}\)[\s\S]*?__ta_s\d+\.setAttribute\('style'/,
|
|
287
287
|
)
|
|
288
288
|
expect(effectMatch).not.toBeNull()
|
|
289
289
|
const effectBody = effectMatch![0]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BF031 `PROPS_TYPE_MISMATCH` deletion audit.
|
|
3
|
+
*
|
|
4
|
+
* BF031 was reserved for "Props type mismatch" but was never emitted.
|
|
5
|
+
* TypeScript's own type-checking catches props type mismatches at the
|
|
6
|
+
* language level (e.g., passing a string where a number is expected).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import ts from 'typescript'
|
|
11
|
+
import path from 'path'
|
|
12
|
+
import { analyzeComponent } from '../analyzer'
|
|
13
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
14
|
+
import { ErrorCodes } from '../errors'
|
|
15
|
+
|
|
16
|
+
function compileToIR(source: string) {
|
|
17
|
+
const ctx = analyzeComponent(source, '/tmp/Test.tsx')
|
|
18
|
+
const ir = jsxToIR(ctx)
|
|
19
|
+
return { ctx, ir, errors: ctx.errors }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getSemanticDiagnostics(source: string) {
|
|
23
|
+
const baseDir = path.resolve(__dirname)
|
|
24
|
+
const filePath = path.join(baseDir, '_props-mismatch-virtual.tsx')
|
|
25
|
+
|
|
26
|
+
const virtualFiles = new Map<string, string>()
|
|
27
|
+
virtualFiles.set(filePath, source)
|
|
28
|
+
|
|
29
|
+
const compilerOptions: ts.CompilerOptions = {
|
|
30
|
+
target: ts.ScriptTarget.Latest,
|
|
31
|
+
module: ts.ModuleKind.ESNext,
|
|
32
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
33
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
34
|
+
jsxImportSource: 'react',
|
|
35
|
+
strict: true,
|
|
36
|
+
noEmit: true,
|
|
37
|
+
skipLibCheck: true,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultHost = ts.createCompilerHost(compilerOptions)
|
|
41
|
+
|
|
42
|
+
const host: ts.CompilerHost = {
|
|
43
|
+
...defaultHost,
|
|
44
|
+
getSourceFile(fileName, languageVersion) {
|
|
45
|
+
const resolved = path.resolve(fileName)
|
|
46
|
+
const content = virtualFiles.get(resolved)
|
|
47
|
+
if (content !== undefined) {
|
|
48
|
+
return ts.createSourceFile(fileName, content, languageVersion, true)
|
|
49
|
+
}
|
|
50
|
+
return defaultHost.getSourceFile(fileName, languageVersion)
|
|
51
|
+
},
|
|
52
|
+
fileExists(fileName) {
|
|
53
|
+
const resolved = path.resolve(fileName)
|
|
54
|
+
if (virtualFiles.has(resolved)) return true
|
|
55
|
+
return defaultHost.fileExists(fileName)
|
|
56
|
+
},
|
|
57
|
+
readFile(fileName) {
|
|
58
|
+
const resolved = path.resolve(fileName)
|
|
59
|
+
const content = virtualFiles.get(resolved)
|
|
60
|
+
if (content !== undefined) return content
|
|
61
|
+
return defaultHost.readFile(fileName)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const program = ts.createProgram([filePath], compilerOptions, host)
|
|
66
|
+
return program.getSemanticDiagnostics(program.getSourceFile(filePath)!)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('BF031 PROPS_TYPE_MISMATCH — deletion audit', () => {
|
|
70
|
+
test('correct props compile without errors', () => {
|
|
71
|
+
const src = `
|
|
72
|
+
interface Props { count: number; label: string }
|
|
73
|
+
export function Display(props: Props) {
|
|
74
|
+
return <div>{props.label}: {props.count}</div>
|
|
75
|
+
}
|
|
76
|
+
`
|
|
77
|
+
const { errors } = compileToIR(src)
|
|
78
|
+
expect(errors).toHaveLength(0)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('TypeScript catches props type mismatches', () => {
|
|
82
|
+
const src = `
|
|
83
|
+
function Child(props: { count: number }) {
|
|
84
|
+
return <div>{props.count}</div>
|
|
85
|
+
}
|
|
86
|
+
export function Parent() {
|
|
87
|
+
return <Child count={"not a number"} />
|
|
88
|
+
}
|
|
89
|
+
`
|
|
90
|
+
const diagnostics = getSemanticDiagnostics(src)
|
|
91
|
+
expect(diagnostics.length).toBeGreaterThanOrEqual(1)
|
|
92
|
+
const messages = diagnostics.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
|
|
93
|
+
expect(messages.some(m => m.includes('number') || m.includes('not assignable'))).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('BF031 code no longer exists in ErrorCodes', () => {
|
|
97
|
+
const allCodes = Object.values(ErrorCodes)
|
|
98
|
+
expect(allCodes).not.toContain('BF031')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* BF060: signal/memo getter referenced from template scope
|
|
18
18
|
* BF061: init-scope local referenced from template scope
|
|
19
|
-
* BF062: cross-stage await — emitted at Phase 1 dispatcher (
|
|
19
|
+
* BF062: cross-stage await — emitted at Phase 1 dispatcher (jsx-to-ir.ts)
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { describe, test, expect } from 'bun:test'
|
|
@@ -24,7 +24,7 @@ import { ErrorCodes } from '../../errors'
|
|
|
24
24
|
import { recordStageDiagnostics } from '../../ir-to-client-js/compute-inlinability'
|
|
25
25
|
import type { ConstantInfo, CompilerError } from '../../types'
|
|
26
26
|
import type { RelocateDecision } from '../../relocate'
|
|
27
|
-
import { compile } from './helpers'
|
|
27
|
+
import { compile, expectValidJs } from './helpers'
|
|
28
28
|
|
|
29
29
|
const dummyLoc = {
|
|
30
30
|
file: 'Test.tsx',
|
|
@@ -622,3 +622,84 @@ describe('compileJSX surfaces stage-violation diagnostics by default', () => {
|
|
|
622
622
|
expect(errors.find(e => e.startsWith('[BF061]'))).toBeUndefined()
|
|
623
623
|
})
|
|
624
624
|
})
|
|
625
|
+
|
|
626
|
+
describe('BF062: AwaitExpression in template scope', () => {
|
|
627
|
+
test('await in JSX child position emits BF062', () => {
|
|
628
|
+
const { errors } = compile(`
|
|
629
|
+
'use client'
|
|
630
|
+
|
|
631
|
+
interface Props {}
|
|
632
|
+
|
|
633
|
+
export async function Foo(_props: Props) {
|
|
634
|
+
const result = await fetch('/api')
|
|
635
|
+
return <div>{await result.json()}</div>
|
|
636
|
+
}
|
|
637
|
+
`)
|
|
638
|
+
|
|
639
|
+
const bf062 = errors.find(e => e.startsWith('[BF062]'))
|
|
640
|
+
expect(bf062).toBeDefined()
|
|
641
|
+
expect(bf062).toContain('AwaitExpression')
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
test('await in component body (not JSX) does NOT emit BF062', () => {
|
|
645
|
+
const { errors } = compile(`
|
|
646
|
+
'use client'
|
|
647
|
+
|
|
648
|
+
interface Props {}
|
|
649
|
+
|
|
650
|
+
export async function Foo(_props: Props) {
|
|
651
|
+
const data = await fetch('/api')
|
|
652
|
+
return <div>loaded</div>
|
|
653
|
+
}
|
|
654
|
+
`)
|
|
655
|
+
|
|
656
|
+
expect(errors.find(e => e.startsWith('[BF062]'))).toBeUndefined()
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
test('await in JSX attribute position emits BF062', () => {
|
|
660
|
+
const { errors } = compile(`
|
|
661
|
+
'use client'
|
|
662
|
+
|
|
663
|
+
interface Props {}
|
|
664
|
+
|
|
665
|
+
export async function Foo(_props: Props) {
|
|
666
|
+
return <div data-x={await fetch('/api')}>hi</div>
|
|
667
|
+
}
|
|
668
|
+
`)
|
|
669
|
+
|
|
670
|
+
const bf062 = errors.find(e => e.startsWith('[BF062]'))
|
|
671
|
+
expect(bf062).toBeDefined()
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
test('emitted client JS remains parseable after BF062', () => {
|
|
675
|
+
const { errors, clientJs } = compile(`
|
|
676
|
+
'use client'
|
|
677
|
+
|
|
678
|
+
interface Props {}
|
|
679
|
+
|
|
680
|
+
export async function Foo(_props: Props) {
|
|
681
|
+
const result = await fetch('/api')
|
|
682
|
+
return <div>{await result.json()}</div>
|
|
683
|
+
}
|
|
684
|
+
`)
|
|
685
|
+
|
|
686
|
+
expect(errors.find(e => e.startsWith('[BF062]'))).toBeDefined()
|
|
687
|
+
expectValidJs(clientJs)
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
test('nested await inside scalar expression emits BF062', () => {
|
|
691
|
+
const { errors, clientJs } = compile(`
|
|
692
|
+
'use client'
|
|
693
|
+
|
|
694
|
+
interface Props {}
|
|
695
|
+
|
|
696
|
+
export async function Foo(_props: Props) {
|
|
697
|
+
return <div>{String(await fetch('/api'))}</div>
|
|
698
|
+
}
|
|
699
|
+
`)
|
|
700
|
+
|
|
701
|
+
const bf062 = errors.find(e => e.startsWith('[BF062]'))
|
|
702
|
+
expect(bf062).toBeDefined()
|
|
703
|
+
expectValidJs(clientJs)
|
|
704
|
+
})
|
|
705
|
+
})
|
|
@@ -77,9 +77,10 @@ describe('#1247 — static-loop CSR self-heal', () => {
|
|
|
77
77
|
expect(clientJs).not.toMatch(/if \(!__iterEl\)/)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
test('
|
|
81
|
-
// `props.items` is
|
|
82
|
-
//
|
|
80
|
+
test('direct prop array uses mapArray, not static forEach (#1586)', () => {
|
|
81
|
+
// `props.items` is a direct prop reference — props are always
|
|
82
|
+
// potentially reactive, so the compiler promotes to mapArray.
|
|
83
|
+
// mapArray handles CSR mount natively (no materialize needed).
|
|
83
84
|
const source = `
|
|
84
85
|
'use client'
|
|
85
86
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -94,7 +95,8 @@ describe('#1247 — static-loop CSR self-heal', () => {
|
|
|
94
95
|
}
|
|
95
96
|
`
|
|
96
97
|
const clientJs = getClientJs(source, 'PropList.tsx')
|
|
97
|
-
expect(clientJs).
|
|
98
|
+
expect(clientJs).toMatch(/\bmapArray\s*\(/)
|
|
99
|
+
expect(clientJs).not.toMatch(/\.forEach\s*\(/)
|
|
98
100
|
})
|
|
99
101
|
|
|
100
102
|
test('materialize branch uses raw destructured param refs (no __bfItem())', () => {
|