@barefootjs/cli 0.1.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/docs/core/README.md +84 -0
- package/dist/docs/core/adapters/adapter-architecture.md +262 -0
- package/dist/docs/core/adapters/csr.md +78 -0
- package/dist/docs/core/adapters/custom-adapter.md +357 -0
- package/dist/docs/core/adapters/go-template-adapter.md +269 -0
- package/dist/docs/core/adapters/hono-adapter.md +199 -0
- package/dist/docs/core/adapters.md +37 -0
- package/dist/docs/core/advanced/code-splitting.md +142 -0
- package/dist/docs/core/advanced/compiler-internals.md +331 -0
- package/dist/docs/core/advanced/error-codes.md +261 -0
- package/dist/docs/core/advanced/ir-schema.md +65 -0
- package/dist/docs/core/advanced/performance.md +115 -0
- package/dist/docs/core/advanced/xyflow-browser-bundle.md +69 -0
- package/dist/docs/core/advanced.md +11 -0
- package/dist/docs/core/components/children-slots.md +150 -0
- package/dist/docs/core/components/component-authoring.md +207 -0
- package/dist/docs/core/components/context-api.md +236 -0
- package/dist/docs/core/components/portals.md +192 -0
- package/dist/docs/core/components/props-type-safety.md +97 -0
- package/dist/docs/core/components/styling.md +37 -0
- package/dist/docs/core/components.md +19 -0
- package/dist/docs/core/core-concepts/ai-native.md +83 -0
- package/dist/docs/core/core-concepts/backend-freedom.md +31 -0
- package/dist/docs/core/core-concepts/how-it-works.md +147 -0
- package/dist/docs/core/core-concepts/mpa-style.md +36 -0
- package/dist/docs/core/core-concepts/reactivity.md +35 -0
- package/dist/docs/core/core-concepts.md +28 -0
- package/dist/docs/core/introduction.md +87 -0
- package/dist/docs/core/llms.txt +53 -0
- package/dist/docs/core/quick-start.mdx +160 -0
- package/dist/docs/core/reactivity/create-effect.md +125 -0
- package/dist/docs/core/reactivity/create-memo.md +91 -0
- package/dist/docs/core/reactivity/create-signal.md +128 -0
- package/dist/docs/core/reactivity/on-cleanup.md +67 -0
- package/dist/docs/core/reactivity/on-mount.md +70 -0
- package/dist/docs/core/reactivity/props-reactivity.md +91 -0
- package/dist/docs/core/reactivity/untrack.md +69 -0
- package/dist/docs/core/reactivity.md +28 -0
- package/dist/docs/core/rendering/client-directive.md +82 -0
- package/dist/docs/core/rendering/fragment.md +43 -0
- package/dist/docs/core/rendering/jsx-compatibility.md +163 -0
- package/dist/docs/core/rendering.md +14 -0
- package/dist/index.js +25490 -0
- package/dist/tokens.json +74 -0
- package/package.json +37 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Writing a Custom Adapter
|
|
3
|
+
description: Step-by-step guide to building a custom adapter using the TestAdapter as a reference.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing a Custom Adapter
|
|
7
|
+
|
|
8
|
+
Build a custom adapter using the `TestAdapter` (`packages/jsx/src/adapters/test-adapter.ts`) as reference — a minimal working adapter that generates JSX output.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Step 1: Implement `TemplateAdapter`
|
|
12
|
+
|
|
13
|
+
Extend `BaseAdapter` or implement `TemplateAdapter` directly:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import type {
|
|
17
|
+
ComponentIR,
|
|
18
|
+
IRNode,
|
|
19
|
+
IRElement,
|
|
20
|
+
IRText,
|
|
21
|
+
IRExpression,
|
|
22
|
+
IRConditional,
|
|
23
|
+
IRLoop,
|
|
24
|
+
IRComponent,
|
|
25
|
+
IRFragment,
|
|
26
|
+
ParamInfo,
|
|
27
|
+
} from '../types'
|
|
28
|
+
import { type AdapterOutput, BaseAdapter } from './interface'
|
|
29
|
+
|
|
30
|
+
export class TestAdapter extends BaseAdapter {
|
|
31
|
+
name = 'test'
|
|
32
|
+
extension = '.test.tsx'
|
|
33
|
+
|
|
34
|
+
private componentName: string = ''
|
|
35
|
+
|
|
36
|
+
generate(ir: ComponentIR): AdapterOutput {
|
|
37
|
+
this.componentName = ir.metadata.componentName
|
|
38
|
+
|
|
39
|
+
const imports = this.generateImports(ir)
|
|
40
|
+
const types = this.generateTypes(ir)
|
|
41
|
+
const component = this.generateComponent(ir)
|
|
42
|
+
|
|
43
|
+
const template = [imports, types, component].filter(Boolean).join('\n\n')
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
template,
|
|
47
|
+
types: types || undefined,
|
|
48
|
+
extension: this.extension,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ... node rendering methods (see below)
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Step 2: Implement `renderNode()`
|
|
57
|
+
|
|
58
|
+
Route each IR node to the correct rendering method:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
renderNode(node: IRNode): string {
|
|
62
|
+
switch (node.type) {
|
|
63
|
+
case 'element': return this.renderElement(node)
|
|
64
|
+
case 'text': return (node as IRText).value
|
|
65
|
+
case 'expression': return this.renderExpression(node)
|
|
66
|
+
case 'conditional': return this.renderConditional(node)
|
|
67
|
+
case 'loop': return this.renderLoop(node)
|
|
68
|
+
case 'component': return this.renderComponent(node)
|
|
69
|
+
case 'fragment': return this.renderChildren((node as IRFragment).children)
|
|
70
|
+
case 'slot': return '{children}'
|
|
71
|
+
default: return ''
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Step 3: Implement Element Rendering
|
|
77
|
+
|
|
78
|
+
Render the tag, attributes, hydration markers, and children:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
renderElement(element: IRElement): string {
|
|
82
|
+
const tag = element.tag
|
|
83
|
+
const attrs = this.renderAttributes(element)
|
|
84
|
+
const children = this.renderChildren(element.children)
|
|
85
|
+
|
|
86
|
+
let hydrationAttrs = ''
|
|
87
|
+
if (element.needsScope) {
|
|
88
|
+
hydrationAttrs += ' bf-s={__scopeId}'
|
|
89
|
+
}
|
|
90
|
+
if (element.slotId) {
|
|
91
|
+
hydrationAttrs += ` bf="${element.slotId}"`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (children) {
|
|
95
|
+
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
96
|
+
} else {
|
|
97
|
+
return `<${tag}${attrs}${hydrationAttrs} />`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Attributes
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
private renderAttributes(element: IRElement): string {
|
|
106
|
+
const parts: string[] = []
|
|
107
|
+
|
|
108
|
+
for (const attr of element.attrs) {
|
|
109
|
+
const attrName = attr.name === 'class' ? 'className' : attr.name
|
|
110
|
+
|
|
111
|
+
if (attr.name === '...') {
|
|
112
|
+
parts.push(`{...${attr.value}}`)
|
|
113
|
+
} else if (attr.value === null) {
|
|
114
|
+
parts.push(attrName) // Boolean attribute
|
|
115
|
+
} else if (attr.dynamic) {
|
|
116
|
+
parts.push(`${attrName}={${attr.value}}`)
|
|
117
|
+
} else {
|
|
118
|
+
parts.push(`${attrName}="${attr.value}"`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Event handlers — render as no-op stubs for SSR
|
|
123
|
+
for (const event of element.events) {
|
|
124
|
+
const handlerName = `on${event.name.charAt(0).toUpperCase()}${event.name.slice(1)}`
|
|
125
|
+
parts.push(`${handlerName}={() => {}}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : ''
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The TestAdapter renders event handlers as no-op stubs for JSX. Non-JSX adapters omit them — handlers exist only in client JS.
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
## Step 4: Implement Expression Rendering
|
|
136
|
+
|
|
137
|
+
Reactive expressions with a `slotId` get a hydration marker for client JS updates:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
renderExpression(expr: IRExpression): string {
|
|
141
|
+
if (expr.expr === 'null' || expr.expr === 'undefined') {
|
|
142
|
+
return 'null'
|
|
143
|
+
}
|
|
144
|
+
if (expr.reactive && expr.slotId) {
|
|
145
|
+
return `<span bf="${expr.slotId}">{${expr.expr}}</span>`
|
|
146
|
+
}
|
|
147
|
+
return `{${expr.expr}}`
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Non-JSX adapters convert expressions to the target language (e.g., `count()` → `{{.Count}}`).
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Step 5: Implement Conditional Rendering
|
|
155
|
+
|
|
156
|
+
Ternaries pass through in JSX adapters:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
renderConditional(cond: IRConditional): string {
|
|
160
|
+
const whenTrue = this.renderNode(cond.whenTrue)
|
|
161
|
+
const whenFalse = this.renderNode(cond.whenFalse)
|
|
162
|
+
|
|
163
|
+
return `{${cond.condition} ? ${whenTrue} : ${whenFalse || 'null'}}`
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Input (JSX):**
|
|
168
|
+
```tsx
|
|
169
|
+
{isActive ? <span>Active</span> : <span>Inactive</span>}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Output (TestAdapter):**
|
|
173
|
+
```tsx
|
|
174
|
+
{isActive ? <span>Active</span> : <span>Inactive</span>}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Non-JSX adapters translate to the target conditional syntax (e.g., `{{if .IsActive}}...{{else}}...{{end}}`).
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
## Step 6: Implement Loop Rendering
|
|
181
|
+
|
|
182
|
+
`.map()` calls stay as JSX:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
renderLoop(loop: IRLoop): string {
|
|
186
|
+
const indexParam = loop.index ? `, ${loop.index}` : ''
|
|
187
|
+
const children = this.renderChildren(loop.children)
|
|
188
|
+
|
|
189
|
+
return `{${loop.array}.map((${loop.param}${indexParam}) => ${children})}`
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Input (JSX):**
|
|
194
|
+
```tsx
|
|
195
|
+
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Output (TestAdapter):**
|
|
199
|
+
```tsx
|
|
200
|
+
{items.map((item) => <li key={item.id}>{item.name}</li>)}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Non-JSX adapters translate to the target iteration syntax (e.g., `{{range .Items}}...{{end}}`).
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
## Step 7: Implement Component Rendering
|
|
207
|
+
|
|
208
|
+
Pass the parent's scope ID to nested components:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
renderComponent(comp: IRComponent): string {
|
|
212
|
+
const props = this.renderComponentProps(comp)
|
|
213
|
+
const children = this.renderChildren(comp.children)
|
|
214
|
+
|
|
215
|
+
const scopeAttr = ' __bfScope={__scopeId}'
|
|
216
|
+
|
|
217
|
+
if (children) {
|
|
218
|
+
return `<${comp.name}${props}${scopeAttr}>${children}</${comp.name}>`
|
|
219
|
+
} else {
|
|
220
|
+
return `<${comp.name}${props}${scopeAttr} />`
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Step 8: Implement Hydration Markers
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
renderScopeMarker(instanceIdExpr: string): string {
|
|
229
|
+
return `bf-s={${instanceIdExpr}}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
renderSlotMarker(slotId: string): string {
|
|
233
|
+
return `bf="${slotId}"`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
renderCondMarker(condId: string): string {
|
|
237
|
+
return `bf-c="${condId}"`
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Step 9: Generate Signal Initializers
|
|
242
|
+
|
|
243
|
+
Signal getters return initial values during SSR via stub functions:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
private generateSignalInitializers(ir: ComponentIR): string {
|
|
247
|
+
const lines: string[] = []
|
|
248
|
+
|
|
249
|
+
for (const signal of ir.metadata.signals) {
|
|
250
|
+
lines.push(` const ${signal.getter} = () => ${signal.initialValue}`)
|
|
251
|
+
lines.push(` const ${signal.setter} = () => {}`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const memo of ir.metadata.memos) {
|
|
255
|
+
lines.push(` const ${memo.name} = ${memo.computation}`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return lines.join('\n')
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`const [count, setCount] = createSignal(initial)` becomes:
|
|
263
|
+
```typescript
|
|
264
|
+
const count = () => initial // getter returns initial value
|
|
265
|
+
const setCount = () => {} // setter is a no-op on the server
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Optional: Type Generation
|
|
269
|
+
|
|
270
|
+
For typed backends, implement `generateTypes()`:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
generateTypes(ir: ComponentIR): string | null {
|
|
274
|
+
const lines: string[] = []
|
|
275
|
+
|
|
276
|
+
const propsTypeName = ir.metadata.propsType?.raw
|
|
277
|
+
if (propsTypeName) {
|
|
278
|
+
lines.push(`type ${this.componentName}PropsWithHydration = ${propsTypeName} & {`)
|
|
279
|
+
lines.push(' __instanceId?: string')
|
|
280
|
+
lines.push(' __bfScope?: string')
|
|
281
|
+
lines.push('}')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return lines.length > 0 ? lines.join('\n') : null
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
For dynamically-typed backends, return `null`.
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
## Testing
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { compileJsxToIR } from '@barefootjs/jsx'
|
|
295
|
+
import { TestAdapter } from './test-adapter'
|
|
296
|
+
|
|
297
|
+
const source = `
|
|
298
|
+
"use client"
|
|
299
|
+
import { createSignal } from '@barefootjs/client'
|
|
300
|
+
|
|
301
|
+
export function Counter({ initial = 0 }: { initial?: number }) {
|
|
302
|
+
const [count, setCount] = createSignal(initial)
|
|
303
|
+
return (
|
|
304
|
+
<div>
|
|
305
|
+
<p>{count()}</p>
|
|
306
|
+
<button onClick={() => setCount(n => n + 1)}>+1</button>
|
|
307
|
+
</div>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
`
|
|
311
|
+
|
|
312
|
+
const ir = compileJsxToIR(source)
|
|
313
|
+
const adapter = new TestAdapter()
|
|
314
|
+
const output = adapter.generate(ir)
|
|
315
|
+
|
|
316
|
+
console.log(output.template)
|
|
317
|
+
// export function Counter({ __instanceId, ... }) {
|
|
318
|
+
// const __scopeId = ...
|
|
319
|
+
// const count = () => 0
|
|
320
|
+
//
|
|
321
|
+
// return (
|
|
322
|
+
// <div bf-s={__scopeId}>
|
|
323
|
+
// <span bf="s1">Count: {bfText("s0")}{count()}{bfTextEnd()}</span>
|
|
324
|
+
// <button bf="s2" onClick={() => {}}>+1</button>
|
|
325
|
+
// </div>
|
|
326
|
+
// )
|
|
327
|
+
// }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
## Checklist
|
|
332
|
+
|
|
333
|
+
Ensure you handle:
|
|
334
|
+
|
|
335
|
+
- [ ] All IR node types (`element`, `text`, `expression`, `conditional`, `loop`, `component`, `fragment`, `slot`)
|
|
336
|
+
- [ ] Hydration markers (`bf-s`, `bf`, `bf-c`) on interactive elements
|
|
337
|
+
- [ ] Static vs. dynamic attributes
|
|
338
|
+
- [ ] Boolean HTML attributes (`disabled`, `checked`, etc.)
|
|
339
|
+
- [ ] Spread attributes (`{...props}`)
|
|
340
|
+
- [ ] Signal getter stubs for server-side initial values
|
|
341
|
+
- [ ] Nested component scope passing
|
|
342
|
+
- [ ] Props serialization (`bf-p` attribute) for client hydration
|
|
343
|
+
- [ ] Script registration for client JS loading
|
|
344
|
+
- [ ] `/* @client */` directive (skip client-only expressions server-side)
|
|
345
|
+
|
|
346
|
+
Production adapters also handle:
|
|
347
|
+
|
|
348
|
+
- [ ] Void HTML elements (`<input>`, `<br>`, etc.) — no closing tag
|
|
349
|
+
- [ ] Expression translation to the target template language
|
|
350
|
+
- [ ] Type generation for typed backend languages
|
|
351
|
+
- [ ] `if-statement` and `provider` IR node types
|
|
352
|
+
|
|
353
|
+
### Reference Implementations
|
|
354
|
+
|
|
355
|
+
- **TestAdapter** (`packages/jsx/src/adapters/test-adapter.ts`) — Minimal working adapter used throughout this guide
|
|
356
|
+
- **HonoAdapter** (`packages/adapter-hono/src/adapter/hono-adapter.ts`) — Production JSX-to-JSX adapter with script collection via Hono's request context
|
|
357
|
+
- **GoTemplateAdapter** (`packages/adapter-go-template/src/adapter/go-template-adapter.ts`) — Production adapter with expression translation, type generation, and array method mapping
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Go Template Adapter
|
|
3
|
+
description: Generate Go html/template files and type definitions from the compiler's IR.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Go Template Adapter
|
|
7
|
+
|
|
8
|
+
Generates Go `html/template` files (`.tmpl`) and type definitions (`_types.go`) from the compiler's IR.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npm install @barefootjs/go-template
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Basic Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { compile } from '@barefootjs/jsx'
|
|
19
|
+
import { GoTemplateAdapter } from '@barefootjs/go-template'
|
|
20
|
+
|
|
21
|
+
const adapter = new GoTemplateAdapter()
|
|
22
|
+
const result = compile(source, { adapter })
|
|
23
|
+
|
|
24
|
+
// result.template → .tmpl file content
|
|
25
|
+
// result.types → _types.go file content
|
|
26
|
+
// result.clientJs → .client.js file content
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const adapter = new GoTemplateAdapter({
|
|
34
|
+
packageName: 'views',
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Option | Type | Default | Description |
|
|
39
|
+
|--------|------|---------|-------------|
|
|
40
|
+
| `packageName` | `string` | `'components'` | Go package name for generated type files |
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Output Format
|
|
44
|
+
|
|
45
|
+
### Server Component
|
|
46
|
+
|
|
47
|
+
**Source:**
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
export function Greeting(props: { name: string }) {
|
|
51
|
+
return <p>Hello, {props.name}!</p>
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Output (.tmpl):**
|
|
56
|
+
|
|
57
|
+
```go-template
|
|
58
|
+
{{define "Greeting"}}
|
|
59
|
+
<p bf-s="{{bfScopeAttr .}}" {{bfPropsAttr .}} bf="s1">
|
|
60
|
+
Hello, {{bfTextStart "s0"}}{{.Name}}{{bfTextEnd}}!
|
|
61
|
+
</p>
|
|
62
|
+
{{end}}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `bfScopeAttr` — generates the `bf-s` scope ID
|
|
66
|
+
- `bfPropsAttr` — serializes props for client hydration
|
|
67
|
+
- `bfTextStart` / `bfTextEnd` — text node markers (rendered as `<!--bf:s0-->...<!--/-->`)
|
|
68
|
+
|
|
69
|
+
### Client Component
|
|
70
|
+
|
|
71
|
+
**Source:**
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
"use client"
|
|
75
|
+
import { createSignal } from '@barefootjs/client'
|
|
76
|
+
|
|
77
|
+
export function Counter(props: { initial?: number }) {
|
|
78
|
+
const [count, setCount] = createSignal(props.initial ?? 0)
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div>
|
|
82
|
+
<span>Count: {count()}</span>
|
|
83
|
+
<button onClick={() => setCount(n => n + 1)}>+1</button>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Output (.tmpl):**
|
|
90
|
+
|
|
91
|
+
```go-template
|
|
92
|
+
{{define "Counter"}}
|
|
93
|
+
{{if .Scripts}}{{.Scripts.Register "/static/client/barefoot.js"}}{{.Scripts.Register "/static/client/Counter.client.js"}}{{end}}
|
|
94
|
+
<div bf-s="{{bfScopeAttr .}}" {{bfPropsAttr .}}>
|
|
95
|
+
<span bf="s1">Count: {{bfTextStart "s0"}}{{.Count}}{{bfTextEnd}}</span>
|
|
96
|
+
<button bf="s2">+1</button>
|
|
97
|
+
</div>
|
|
98
|
+
{{end}}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Expression Translation
|
|
103
|
+
|
|
104
|
+
### Property Access
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
props.name → .Name
|
|
108
|
+
props.user.email → .User.Email
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Field names are automatically capitalized to follow Go conventions.
|
|
112
|
+
|
|
113
|
+
### Comparisons
|
|
114
|
+
|
|
115
|
+
| JavaScript | Go Template |
|
|
116
|
+
|-----------|-------------|
|
|
117
|
+
| `a === b` | `eq .A .B` |
|
|
118
|
+
| `a !== b` | `ne .A .B` |
|
|
119
|
+
| `a > b` | `gt .A .B` |
|
|
120
|
+
| `a < b` | `lt .A .B` |
|
|
121
|
+
| `a >= b` | `ge .A .B` |
|
|
122
|
+
| `a <= b` | `le .A .B` |
|
|
123
|
+
|
|
124
|
+
### Arithmetic
|
|
125
|
+
|
|
126
|
+
| JavaScript | Go Template |
|
|
127
|
+
|-----------|-------------|
|
|
128
|
+
| `a + b` | `bf_add .A .B` |
|
|
129
|
+
| `a - b` | `bf_sub .A .B` |
|
|
130
|
+
| `a * b` | `bf_mul .A .B` |
|
|
131
|
+
| `a / b` | `bf_div .A .B` |
|
|
132
|
+
|
|
133
|
+
### Logical Operators
|
|
134
|
+
|
|
135
|
+
| JavaScript | Go Template |
|
|
136
|
+
|-----------|-------------|
|
|
137
|
+
| `a && b` | `and .A .B` |
|
|
138
|
+
| `a \|\| b` | `or .A .B` |
|
|
139
|
+
| `!a` | `not .A` |
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## Array Methods
|
|
143
|
+
|
|
144
|
+
### `.map()`
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
{items().map(item => <li key={item}>{item}</li>)}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```go-template
|
|
151
|
+
{{range $_, $item := .Items}}
|
|
152
|
+
<li>{{bfTextStart "s0"}}{{.Item}}{{bfTextEnd}}</li>
|
|
153
|
+
{{end}}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `.filter().map()`
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
{items().filter(item => item.active).map(item => <li key={item.id}>{item.name}</li>)}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```go-template
|
|
163
|
+
{{range $_, $item := .Items}}{{if .Active}}
|
|
164
|
+
<li>{{bfTextStart "s0"}}{{.Item.Name}}{{bfTextEnd}}</li>
|
|
165
|
+
{{end}}{{end}}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For complex filter predicates, the adapter generates template block functions.
|
|
169
|
+
|
|
170
|
+
### `.sort().map()` / `.toSorted().map()`
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
{items.toSorted((a, b) => a.priority - b.priority).map(t => <li key={t.id}>{t.name}</li>)}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```go-template
|
|
177
|
+
{{range bf_sort .Items "Priority" "asc"}}
|
|
178
|
+
<li>{{bfTextStart "s0"}}{{.Name}}{{bfTextEnd}}</li>
|
|
179
|
+
{{end}}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Other Array Methods
|
|
183
|
+
|
|
184
|
+
| JavaScript | Go Template |
|
|
185
|
+
|-----------|-------------|
|
|
186
|
+
| `arr.find(fn)` | `bf_find` |
|
|
187
|
+
| `arr.findIndex(fn)` | `bf_find_index` |
|
|
188
|
+
| `arr.every(fn)` | `bf_every` |
|
|
189
|
+
| `arr.some(fn)` | `bf_some` |
|
|
190
|
+
| `arr.length` | `len .Arr` |
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## Type Generation
|
|
194
|
+
|
|
195
|
+
For each component, the adapter generates:
|
|
196
|
+
|
|
197
|
+
1. **Input struct** — external API
|
|
198
|
+
2. **Props struct** — internal representation (includes hydration fields)
|
|
199
|
+
3. **Constructor** — `New{Component}Props()` with defaults
|
|
200
|
+
|
|
201
|
+
### Type Mapping
|
|
202
|
+
|
|
203
|
+
| TypeScript | Go |
|
|
204
|
+
|-----------|-----|
|
|
205
|
+
| `string` | `string` |
|
|
206
|
+
| `number` | `int` (or `float64` for decimals) |
|
|
207
|
+
| `boolean` | `bool` |
|
|
208
|
+
| `T[]` | `[]T` |
|
|
209
|
+
| `T \| undefined` | Pointer type `*T` or zero value |
|
|
210
|
+
| Object type | Named struct |
|
|
211
|
+
|
|
212
|
+
### Nested Components
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
export function TodoList({ items }: { items: TodoItem[] }) {
|
|
216
|
+
return (
|
|
217
|
+
<ul>
|
|
218
|
+
{items.map(item => <TodoItem key={item.id} {...item} />)}
|
|
219
|
+
</ul>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Conditional Rendering
|
|
225
|
+
|
|
226
|
+
Ternaries become `{{if}}...{{else}}...{{end}}`:
|
|
227
|
+
|
|
228
|
+
**Source:**
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
{loggedIn() ? <span>Welcome back!</span> : <span>Please log in</span>}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Output:**
|
|
235
|
+
|
|
236
|
+
```go-template
|
|
237
|
+
{{if .LoggedIn}}<span bf-c="s0">Welcome back!</span>{{else}}<span bf-c="s0">Please log in</span>{{end}}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Element branches use `bf-c` for conditional markers. Text-only ternaries use `bfComment` markers instead.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Script Registration
|
|
244
|
+
|
|
245
|
+
Client components register their scripts via the `.Scripts` interface:
|
|
246
|
+
|
|
247
|
+
```go-template
|
|
248
|
+
{{if .Scripts}}{{.Scripts.Register "/static/client/barefoot.js"}}{{.Scripts.Register "/static/client/Counter.client.js"}}{{end}}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The `ScriptCollector` tracks needed scripts and renders `<script>` tags at page end. Each script loads at most once.
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
## Go Helper Functions
|
|
255
|
+
|
|
256
|
+
These helper functions must be in the Go template `FuncMap`:
|
|
257
|
+
|
|
258
|
+
| Function | Purpose |
|
|
259
|
+
|----------|---------|
|
|
260
|
+
| `bf_add`, `bf_sub`, `bf_mul`, `bf_div` | Arithmetic operations |
|
|
261
|
+
| `bf_neg` | Unary negation |
|
|
262
|
+
| `bf_filter` | Filter a slice by field/value |
|
|
263
|
+
| `bf_sort` | Sort a slice by field/direction |
|
|
264
|
+
| `bf_find`, `bf_find_index` | Find element/index in a slice |
|
|
265
|
+
| `bf_every`, `bf_some` | Test if all/any elements match |
|
|
266
|
+
| `bf_json` | JSON-encode a value for props serialization |
|
|
267
|
+
| `bf_concat` | String concatenation |
|
|
268
|
+
|
|
269
|
+
Provided by the BarefootJS Go runtime package.
|