An adapter converts the compiler's IR into a template format your server can render.
#Role
- Phase 1 parses JSX → backend-agnostic
ComponentIR(JSON tree) - Phase 2a adapter converts IR → marked template in target language
- Phase 2b generates client JS from IR (no adapter involved)
ComponentIR (JSON)
↓
┌───────────────────────────────┐
│ TemplateAdapter │
│ │
│ renderElement() │
│ renderExpression() │
│ renderConditional() │
│ renderLoop() │
│ renderComponent() │
│ ... │
└───────────────────────────────┘
↓
Marked Template + optional typesEach IR node is translated into the target template language with hydration markers (bf-* attributes).
#`TemplateAdapter` Interface
interface TemplateAdapter {
name: string // Adapter identifier (e.g., 'hono', 'go-template')
extension: string // Output file extension (e.g., '.tsx', '.tmpl')
// Main entry point
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput
// Node rendering — one method per IR node type
renderNode(node: IRNode): string
renderElement(element: IRElement): string
renderExpression(expr: IRExpression): string
renderConditional(cond: IRConditional): string
renderLoop(loop: IRLoop): string
renderComponent(comp: IRComponent): string
// Hydration markers
renderScopeMarker(instanceIdExpr: string): string
renderSlotMarker(slotId: string): string
renderCondMarker(condId: string): string
// Optional: type generation for typed languages
generateTypes?(ir: ComponentIR): string | null
}#`generate()`
interface AdapterOutput {
template: string // The generated template code
types?: string // Optional generated types (Go structs, etc.)
extension: string // File extension for the output
}#`AdapterGenerateOptions`
interface AdapterGenerateOptions {
skipScriptRegistration?: boolean // For child components bundled in parent
scriptBaseName?: string // For non-default exports sharing a parent's client JS
}#Node rendering methods
| Method | IR Node | Responsibility |
|---|---|---|
renderElement() |
IRElement |
HTML elements with attributes, events, and hydration markers |
renderExpression() |
IRExpression |
Dynamic expressions (e.g., {count()}, {props.name}) |
renderConditional() |
IRConditional |
Ternaries and &&/` |
renderLoop() |
IRLoop |
.map(), .filter().map(), .sort().map() chains |
renderComponent() |
IRComponent |
Nested component invocations |
renderNode() |
IRNode |
Dispatcher — routes to the correct method based on node type |
#Hydration marker methods
| Method | Marker | Purpose |
|---|---|---|
renderScopeMarker() |
bf-s |
Component boundary for scoped hydration |
renderSlotMarker() |
bf |
Interactive element identifier |
renderCondMarker() |
bf-c |
Conditional block for DOM switching |
#`BaseAdapter` Class
BaseAdapter implements the TemplateAdapter interface with a renderChildren() utility:
abstract class BaseAdapter implements TemplateAdapter {
abstract name: string
abstract extension: string
// ... all abstract methods from TemplateAdapter
renderChildren(children: IRNode[]): string {
return children.map(child => this.renderNode(child)).join('')
}
}Extending BaseAdapter is optional.
#IR Node Types
Each adapter must handle all IR node types:
#`IRElement`
An HTML element with attributes, events, and children.
{
type: 'element'
tag: string // 'div', 'button', 'input', etc.
attrs: IRAttribute[] // Static and dynamic attributes
events: IREvent[] // Event handlers (onClick, onChange, etc.)
children: IRNode[] // Child nodes
slotId: string | null // Hydration slot ID (e.g., 's0')
needsScope: boolean // True if this is the component root
}#`IRExpression`
A dynamic expression in the template.
{
type: 'expression'
expr: string // The JS expression (e.g., 'count()', 'props.name')
reactive: boolean // True if the expression depends on signals
slotId: string | null // Slot ID for client updates
clientOnly?: boolean // True if wrapped in /* @client */
}#`IRConditional`
A ternary or logical expression that produces different output.
{
type: 'conditional'
condition: string // The JS condition
whenTrue: IRNode // Rendered when condition is true
whenFalse: IRNode // Rendered when condition is false
reactive: boolean // True if the condition depends on signals
slotId: string | null // Slot ID for DOM switching
}#`IRLoop`
An array iteration (.map(), optionally chained with .filter() or .sort()).
{
type: 'loop'
array: string // The array expression
param: string // Iterator parameter name
index: string | null // Index parameter name
children: IRNode[] // Loop body
isStaticArray: boolean // True if iterating a prop (not a signal)
filterPredicate?: {...} // For .filter().map() chains
sortComparator?: {...} // For .sort().map() chains
}#`IRComponent`
A nested component invocation.
{
type: 'component'
name: string // Component name (e.g., 'TodoItem')
props: IRProp[] // Props passed to the component
children: IRNode[] // Children (slots)
slotId: string | null // Slot ID if parent binds event handlers
}#Other node types
| Type | Description |
|---|---|
IRText |
Static text content |
IRFragment |
Fragment (<>...</>) wrapper |
IRSlot |
{children} or <Slot /> placeholder |
IRIfStatement |
Top-level if/else if/else blocks |
IRProvider |
Context provider wrapper |
IRTemplateLiteral |
Template literal expressions |
#Hydration Markers
bf-* attributes tell the client JS where to attach behavior:
| Marker | Example | Purpose |
|---|---|---|
bf-s |
<div bf-s="Counter_a1b2"> |
Component boundary — scopes all queries inside |
bf |
<p bf="s0"> |
Interactive element — target for effects and event handlers |
bf-c |
<div bf-c="s2"> |
Conditional block — target for DOM switching |
#Script Registration
Adapters register client JS during server rendering to ensure each script loads exactly once:
- Hono:
useRequestContext()collects script paths;BfScriptsrenders the<script>tags. - Go Template:
ScriptCollectortracks needed scripts; renders<script>tags at page end.
#Type Generation
For typed backends, generateTypes() produces type definitions alongside the template. The Go Template adapter generates:
- Go structs for component input and props types
- JSON tags for prop serialization
- Constructor functions like
New{Component}Props()with default values
// Generated by GoTemplateAdapter
type CounterInput struct {
Initial int `json:"initial"`
}
type CounterProps struct {
Initial int `json:"initial"`
ScopeID string `json:"scopeId"`
}
func NewCounterProps(input CounterInput) CounterProps {
return CounterProps{
Initial: input.Initial,
}
}Dynamically-typed adapters (like Hono) skip this.