@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.
Files changed (47) hide show
  1. package/dist/adapters/parsed-expr-emitter.d.ts +1 -1
  2. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  3. package/dist/analyzer-context.d.ts +22 -0
  4. package/dist/analyzer-context.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/errors.d.ts +0 -9
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/expression-parser.d.ts +1 -1
  9. package/dist/expression-parser.d.ts.map +1 -1
  10. package/dist/index.js +306 -38
  11. package/dist/ir-to-client-js/compute-inlinability.d.ts +2 -2
  12. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +6 -18
  14. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  16. package/dist/jsx-to-ir.d.ts.map +1 -1
  17. package/dist/types.d.ts +10 -3
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +3 -3
  20. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +13 -17
  21. package/src/__tests__/circular-dependency.audit.test.ts +19 -0
  22. package/src/__tests__/compiler-stress-1244.test.ts +6 -10
  23. package/src/__tests__/component-not-found.audit.test.ts +36 -0
  24. package/src/__tests__/doc-examples.test.ts +5 -6
  25. package/src/__tests__/invalid-component-name.audit.test.ts +38 -0
  26. package/src/__tests__/invalid-jsx-attribute.audit.test.ts +47 -0
  27. package/src/__tests__/invalid-jsx-expression.audit.test.ts +44 -0
  28. package/src/__tests__/invalid-signal-usage.audit.test.ts +72 -0
  29. package/src/__tests__/jsx-function-inlining.test.ts +281 -1
  30. package/src/__tests__/loop-fallback-wrap.test.ts +5 -5
  31. package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -4
  32. package/src/__tests__/props-type-mismatch.audit.test.ts +100 -0
  33. package/src/__tests__/staged-ir/10-stage-diagnostics.test.ts +83 -2
  34. package/src/__tests__/static-loop-csr-materialize.test.ts +6 -4
  35. package/src/__tests__/type-inference-failed.audit.test.ts +48 -0
  36. package/src/__tests__/unknown-signal.audit.test.ts +111 -0
  37. package/src/adapters/parsed-expr-emitter.ts +1 -1
  38. package/src/analyzer-context.ts +25 -0
  39. package/src/analyzer.ts +207 -1
  40. package/src/errors.ts +4 -26
  41. package/src/expression-parser.ts +8 -9
  42. package/src/ir-to-client-js/compute-inlinability.ts +2 -2
  43. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +3 -8
  44. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +6 -17
  45. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +5 -19
  46. package/src/jsx-to-ir.ts +215 -4
  47. 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('does not set isJsxFunction for functions with multiple returns', () => {
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('static prop array stays static', () => {
114
- // `items` is a non-destructured prop; the AST is just an
115
- // Identifier, no calls. Should still take the static path.
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).not.toMatch(/\bmapArray\s*\(/)
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*const __v = styleToCss\([\s\S]*?task\(\)\.id/)
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\(\(\) => \{ if \([^)]*c\(\)\.isOff[^)]*\)\s*[^.]+\.setAttribute\('data-off',\s*''\)/)
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\(\(\) => \{ const __v = styleToCss\(\{[\s\S]*?\}\); if \(__v != null\) __ta_s\d+\.setAttribute\('style'/,
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\(\(\) => \{ const __v = styleToCss\(\{[\s\S]*?\}\); if \(__v != null\) __ta_s\d+\.setAttribute\('style'/,
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 (not here)
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('safe-prop array (no init-scope local) does NOT emit the branch', () => {
81
- // `props.items` is directly usable in the CSR template no fallback
82
- // needed.
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).not.toMatch(/if \(!__iterEl\)/)
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())', () => {