#Pipeline Overview
┌─────────────────────────────────────────────────────┐
│ JSX Source (.tsx with "use client") │
└──────────────────────┬──────────────────────────────┘
↓
┌──────────────────────┴──────────────────────────────┐
│ 1. Analyzer (analyzer.ts) │
│ Single-pass AST visitor │
│ Extracts: signals, memos, effects, props, types │
└──────────────────────┬──────────────────────────────┘
↓
┌──────────────────────┴──────────────────────────────┐
│ 2. JSX → IR (jsx-to-ir.ts) │
│ Transforms JSX AST to IR node tree │
│ Assigns slotIds, detects reactivity │
└──────────────────────┬──────────────────────────────┘
↓
┌────────────┴────────────┐
↓ ↓
┌─────────┴─────────┐ ┌───────────┴──────────┐
│ 3a. Adapter │ │ 3b. IR → Client JS │
│ IR → Template │ │ ir-to-client-js/ │
│ (e.g., Hono JSX) │ │ Hydration code │
└────────────────────┘ └──────────────────────┘#Entry Points
// Async — reads files from disk
compileJSX(entryPath: string, readFile: ReadFileFn, options: CompileOptions): Promise<CompileResult>
// Sync — source string input
compileJSX(source: string, filePath: string, options: CompileOptions): CompileResultBoth support multi-component files.
#Phase 1: Analysis
The analyzer (analyzer.ts) performs a single-pass AST walk using TypeScript's compiler API.
#Extracted Data
| Category | Data | Example |
|---|---|---|
| Signals | getter/setter names, initial value, type | [count, setCount] = createSignal(0) |
| Memos | name, computation expression, type | doubled = createMemo(() => count() * 2) |
| Effects | effect body | createEffect(() => { ... }) |
| onMounts | callback body | onMount(() => { ... }) |
| Props | parameter style, type info, defaults | (props: ButtonProps) or ({ label }: Props) |
| Imports | source, specifiers | import { createSignal } from '@barefootjs/client' |
| Constants | name, value, dependencies | const baseClass = 'btn' |
| Functions | name, body, parameters | function handleClick() { ... } |
| Types | interfaces, type aliases | interface ButtonProps { ... } |
| JSX Return | the return statement's JSX | return <button>...</button> |
| Conditional Returns | early returns inside if blocks |
if (loading) return <Spinner /> |
#`"use client"` Validation
Files with reactive APIs but no "use client" emit BF001:
error[BF001]: 'use client' directive required for components with createSignal#Props Destructuring Detection
// ⚠️ BF043: Destructuring captures values once — may lose reactivity
function Child({ count }: Props) { ... }
// ✅ No warning — direct access maintains reactivity
function Child(props: Props) { ... }Suppress with // @bf-ignore props-destructuring.
#Phase 2: JSX → IR
jsxToIR (jsx-to-ir.ts) transforms the analyzed JSX AST into the IR node tree.
#Reactivity Detection
Two-tier strategy to determine if an expression is reactive:
TypeChecker path — Walks the AST and checks each node's type for the
Reactive<T>brand viachecker.getTypeAtLocation(). This detects all reactive getters: signals, memos, and library-provided reactive accessors (e.g.,FieldReturn.error,FormReturn.isSubmitting).Regex fallback — Pattern-matches known signal/memo names and props references. Used when the TypeChecker cannot resolve imported types.
Reactive if any match:
- A
Reactive<T>-branded type (via TypeChecker) - A signal getter:
count()— regex pattern\bcount\s*\( - A memo:
doubled()— same pattern - A props reference:
props.value— per-prop name matching (excludeschildren) - A local constant derived from any of the above (taint analysis)
#Slot ID Assignment
Elements receive a slotId when they have:
- Event handlers (
onClick,onInput, etc.) - Dynamic children (reactive expressions, loops, conditionals)
- Reactive attributes (
class={expr()},value={signal()}) - Refs (
ref={callback}) - Component references (always need initialization)
#Filter/Sort Chain Parsing
.filter() and .sort() chains before .map() are parsed for template-level evaluation:
{todos().filter(t => !t.done).sort((a, b) => a.date - b.date).map(t => (
<li>{t.name}</li>
))}Simple patterns compile for template-level evaluation. Complex patterns trigger BF021. See Error Codes.
#Auto Scope Wrapping
If the IR root is a Provider with no wrapper element, the compiler wraps it in <div style="display:contents"> for scope identification during hydration.
#Phase 3a: Template Generation (Adapter)
See Adapter Architecture. Each adapter handles:
renderElement()— HTML elements with hydration markersrenderExpression()— Dynamic values in the target template languagerenderConditional()— Template-level conditionalsrenderLoop()— Template-level iteration (with filter/sort if supported)renderComponent()— Child component includes
#Phase 3b: Client JS Generation
#1. Element Collection
| Category | Description | Example |
|---|---|---|
interactiveElements |
Elements with event handlers | <button onClick={...}> |
dynamicElements |
Elements with reactive text | <span>{count()}</span> |
conditionalElements |
Ternary/logical conditionals | {open() ? <A/> : <B/>} |
loopElements |
Array .map() loops |
{items().map(...)} |
refElements |
Elements with ref callbacks | <input ref={inputRef}> |
reactiveAttrs |
Elements with reactive attributes | <div class={cls()}> |
clientOnlyElements |
/* @client */ expressions |
Skipped during SSR |
#2. Dependency Resolution
// "Early" constants — no reactive deps, emitted first
const baseClass = 'btn'
const THRESHOLD = 10
// "Late" constants — reference signals/memos, emitted after signal creation
const displayValue = `Count: ${count()}`#3. Controlled Signal Detection
When a signal name matches a prop name:
function Switch(props: Props) {
const [checked, setChecked] = createSignal(props.checked ?? false)
// ^^^^^^^ matches props.checked
}A sync effect is generated:
createEffect(() => {
const __val = _p.checked
if (__val !== undefined) setChecked(__val)
})#4. Code Generation Order
import { $, $t, createEffect, createMemo, createSignal, hydrate, onMount } from '@barefootjs/client'
export function initCounter(__scope, _p = {}) {
if (!__scope) return
// 1. Early constants (no reactive deps)
const baseClass = 'counter'
// 2. Local functions / handlers (before signals so signal initializers
// can reference them, e.g., createSignal(toArray(_p.x)))
const handleClick = () => { setCount(n => n + 1) }
// 3. Signals, memos, controlled signal sync, and late constants
const [count, setCount] = createSignal(_p.initial ?? 0)
createEffect(() => { // controlled signal sync
const __val = _p.initial
if (__val !== undefined) setCount(__val)
})
const doubled = createMemo(() => count() * 2)
// 4. Element references (always destructured, always returns array)
// $() — regular elements: querySelector('[bf="id"]') within scope
// $t() — text nodes: find comment marker <!--bf:id-->
const [_s3] = $(__scope, 's3')
const [_s0, _s2] = $t(__scope, 's0', 's2')
// 5. Dynamic text updates
createEffect(() => {
const __val = count()
if (_s0 && !__val?.__isSlot) _s0.nodeValue = String(__val ?? '')
})
// 6. Reactive attribute updates
createEffect(() => {
if (_s3) { _s3.disabled = !!(count() > 10) }
})
// 7. Conditional updates (insert with comment markers)
// insert(__scope, 's4', () => isOpen(), trueBranch, falseBranch)
// 8. Loop updates (mapArray with reconciliation)
// mapArray(() => items(), _s5, null, (item, idx, existing) => { ... })
// 9. Event handlers
if (_s3) _s3.addEventListener('click', handleClick)
// 10. Reactive prop bindings / child component props
// 11. Ref callbacks
// 12. User-defined effects and onMounts
createEffect(() => { console.log('Count changed:', count()) })
onMount(() => { console.log('Mounted') })
// 13. Provider setup and child component initialization
}
// Registration: hydrate() registers the component and initializes all
// instances on the page. Template inclusion depends on two factors:
// 1. Static template (no signal deps) → always included
// 2. CSR fallback template → only when used as a child component
// Top-level-only components with signals skip template to save bytes.
hydrate('Counter', {
init: initCounter,
template: (_p) => `<div><p bf="s1">Count: <!--bf:s0-->${(0)}<!--/--></p>...</div>`
})#5. Import Detection
Only used imports are included:
import { $, $t, createEffect, createMemo, createSignal, hydrate, onMount } from '@barefootjs/client'#6. Template Registration
Static template — No signal-dependent expressions:
hydrate('Button', {
init: initButton,
template: (_p) => `<button ${(_p.className ?? '') != null ? 'class="' + (_p.className ?? '') + '"' : ''} bf="s0">${_p.children}</button>`
})CSR fallback template — Component has signals but is used as a child in the same file:
// StatusBadge is used by Dashboard in the same file → gets CSR fallback
hydrate('StatusBadge', {
init: initStatusBadge,
template: (_p) => `<span bf="s0">${_p.active ? 'on' : 'off'}</span>`
})
// Dashboard is top-level only → no template (saves bytes)
hydrate('Dashboard', { init: initDashboard })No template — Top-level-only components with signals. Hydrated from server HTML only.
#Multi-Component Files
Two-pass approach for multi-component files:
- Pass 1 — Detect exports, analyze, and generate IR for each
- Between passes — Build
usedAsChildset viacollectComponentNamesFromIR() - Pass 2 — Generate templates and client JS; only child components get CSR fallback templates
- Merge templates (deduplicate imports/types) and client JS (combine imports)
// Both compiled from the same file
export function Button(props: ButtonProps) { ... }
export function IconButton(props: IconButtonProps) { ... }#Debugging Tips
#View the IR
const result = compileJSX(source, 'file.tsx', { adapter })
console.log(JSON.stringify(result.ir, null, 2))#View generated client JS
console.log(result.clientJs)