@barefootjs/cli 0.26.3 → 0.27.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/advanced/compiler-internals.md +8 -7
- package/dist/docs/core/advanced/performance.md +1 -1
- package/dist/docs/core/core-concepts/how-it-works.mdx +23 -7
- package/dist/docs/core/introduction.mdx +6 -4
- package/dist/docs/core/rendering/client-directive.md +24 -7
- package/dist/docs/core/rendering/jsx-compatibility.md +18 -1
- package/dist/index.js +761 -130
- package/package.json +4 -4
|
@@ -195,7 +195,7 @@ createEffect(() => {
|
|
|
195
195
|
### 4. Code Generation Order
|
|
196
196
|
|
|
197
197
|
```javascript
|
|
198
|
-
import { $,
|
|
198
|
+
import { $, createEffect, createMemo, createSignal, escapeTextOrNode, hydrate, lazySlots, onMount } from '@barefootjs/client/runtime'
|
|
199
199
|
|
|
200
200
|
export function initCounter(__scope, _p = {}) {
|
|
201
201
|
if (!__scope) return
|
|
@@ -216,15 +216,16 @@ export function initCounter(__scope, _p = {}) {
|
|
|
216
216
|
const doubled = createMemo(() => count() * 2)
|
|
217
217
|
|
|
218
218
|
// 4. Element references (always destructured, always returns array)
|
|
219
|
-
// $()
|
|
220
|
-
// $t() — text nodes: find comment marker <!--bf:id-->
|
|
219
|
+
// $() — regular elements: querySelector('[bf="id"]') within scope
|
|
221
220
|
const [_s3] = $(__scope, 's3')
|
|
222
|
-
const [_s0, _s2] = $t(__scope, 's0', 's2')
|
|
223
221
|
|
|
224
|
-
// 5. Dynamic
|
|
222
|
+
// 5. Dynamic content updates — the compiler emits a claim plan (data)
|
|
223
|
+
// per slot; lazySlots() claims the <!--bf:id-->…<!--/--> marker
|
|
224
|
+
// range once on first write, then writes through the held reference
|
|
225
|
+
const __bfw_s0 = lazySlots(__scope, [{ id: 's0', kind: 'markup', path: [] }])
|
|
225
226
|
createEffect(() => {
|
|
226
227
|
const __val = count()
|
|
227
|
-
|
|
228
|
+
__bfw_s0('s0', escapeTextOrNode(__val))
|
|
228
229
|
})
|
|
229
230
|
|
|
230
231
|
// 6. Reactive attribute updates
|
|
@@ -266,7 +267,7 @@ hydrate('Counter', {
|
|
|
266
267
|
Only used imports are included:
|
|
267
268
|
|
|
268
269
|
```javascript
|
|
269
|
-
import { $,
|
|
270
|
+
import { $, createEffect, createMemo, createSignal, escapeTextOrNode, hydrate, lazySlots, onMount } from '@barefootjs/client/runtime'
|
|
270
271
|
```
|
|
271
272
|
|
|
272
273
|
### 6. Template Registration
|
|
@@ -51,7 +51,7 @@ The compiler detects static arrays and skips reconciliation:
|
|
|
51
51
|
const tabs = ['Home', 'About', 'Contact']
|
|
52
52
|
{tabs.map(tab => <Tab label={tab} />)}
|
|
53
53
|
|
|
54
|
-
// Dynamic —
|
|
54
|
+
// Dynamic — keyed reconciliation (mapArray) needed
|
|
55
55
|
const [items, setItems] = createSignal([...])
|
|
56
56
|
{items().map(item => <Item key={item.id} data={item} />)}
|
|
57
57
|
```
|
|
@@ -82,7 +82,7 @@ export function Counter({ __instanceId, ... }) {
|
|
|
82
82
|
Client JS (Phase 2b):
|
|
83
83
|
|
|
84
84
|
```js
|
|
85
|
-
import { $,
|
|
85
|
+
import { $, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
|
|
86
86
|
|
|
87
87
|
export function initCounter(__scope, _p = {}) {
|
|
88
88
|
if (!__scope) return
|
|
@@ -90,11 +90,13 @@ export function initCounter(__scope, _p = {}) {
|
|
|
90
90
|
const [count, setCount] = createSignal(0)
|
|
91
91
|
|
|
92
92
|
const [_s1] = $(__scope, 's1') // element lookup
|
|
93
|
-
const [_s0] = $t(__scope, 's0') // text node lookup
|
|
94
93
|
|
|
94
|
+
// Content slot: claimed lazily from the <!--bf:s0-->…<!--/--> marker
|
|
95
|
+
// pair on first write, then updated through the held reference
|
|
96
|
+
const __bfw_s0 = lazySlots(__scope, [{ id: 's0', kind: 'markup', path: [] }])
|
|
95
97
|
createEffect(() => {
|
|
96
98
|
const __val = count()
|
|
97
|
-
|
|
99
|
+
__bfw_s0('s0', escapeTextOrNode(__val))
|
|
98
100
|
})
|
|
99
101
|
|
|
100
102
|
if (_s1) _s1.addEventListener('click', () => { setCount(n => n + 1) })
|
|
@@ -102,7 +104,7 @@ export function initCounter(__scope, _p = {}) {
|
|
|
102
104
|
|
|
103
105
|
hydrate('Counter', {
|
|
104
106
|
init: initCounter,
|
|
105
|
-
template: (_p) => `<button bf="s1"> Count: <!--bf:s0-->${(0)}<!--/--></button>`
|
|
107
|
+
template: (_p) => `<button bf="s1"> Count: <!--bf:s0-->${escapeText((0))}<!--/--></button>`
|
|
106
108
|
})
|
|
107
109
|
```
|
|
108
110
|
|
|
@@ -135,9 +137,11 @@ Marker-driven hydration attaches behavior to server-rendered HTML.
|
|
|
135
137
|
4. Init function runs per scope — signals, effects, handlers
|
|
136
138
|
5. Runtime tracks scopes to prevent double initialization
|
|
137
139
|
|
|
138
|
-
### Scoped Queries
|
|
140
|
+
### Scoped Queries and Slot Claiming
|
|
139
141
|
|
|
140
|
-
`$()` and
|
|
142
|
+
Element lookups (`$()`) and content-slot claims (`claimSlots()` /
|
|
143
|
+
`lazySlots()`) both operate within a scope, excluding child component
|
|
144
|
+
scopes:
|
|
141
145
|
|
|
142
146
|
```html
|
|
143
147
|
<div bf-s="TodoApp_x1">
|
|
@@ -148,4 +152,16 @@ Marker-driven hydration attaches behavior to server-rendered HTML.
|
|
|
148
152
|
</div>
|
|
149
153
|
```
|
|
150
154
|
|
|
151
|
-
`$(__scope, 's0')` in TodoApp finds `<h1>`, not the `<span>` inside
|
|
155
|
+
`$(__scope, 's0')` in TodoApp finds `<h1>`, not the `<span>` inside
|
|
156
|
+
TodoItem. The `~` prefix marks a child scope excluded from parent
|
|
157
|
+
queries.
|
|
158
|
+
|
|
159
|
+
Dynamic *content* (reactive text and markup regions) is not looked up
|
|
160
|
+
per update. Instead the compiler emits a **claim plan** — a data
|
|
161
|
+
description of each slot (`{ id, kind, path }`) — and the runtime claims
|
|
162
|
+
the slot's DOM position **once**: `lazySlots()` scans for the slot's
|
|
163
|
+
`<!--bf:id-->…<!--/-->` marker pair on the first write (skipping child
|
|
164
|
+
scopes, same as `$()`), holds the reference, and every later write goes
|
|
165
|
+
through that held reference with no re-scanning. `claimSlots()` is the
|
|
166
|
+
eager variant used where content may be mutated before the first write
|
|
167
|
+
(streaming, portals).
|
|
@@ -63,7 +63,7 @@ export function Counter({ __instanceId, ... }) {
|
|
|
63
63
|
**Client script** — Wires up only the interactive parts:
|
|
64
64
|
|
|
65
65
|
```js
|
|
66
|
-
import { $,
|
|
66
|
+
import { $, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
|
|
67
67
|
|
|
68
68
|
export function initCounter(__scope, _p = {}) {
|
|
69
69
|
if (!__scope) return
|
|
@@ -71,11 +71,13 @@ export function initCounter(__scope, _p = {}) {
|
|
|
71
71
|
const [count, setCount] = createSignal(0)
|
|
72
72
|
|
|
73
73
|
const [_s1] = $(__scope, 's1') // find element with bf="s1"
|
|
74
|
-
const [_s0] = $t(__scope, 's0') // find text node at <!--bf:s0-->
|
|
75
74
|
|
|
75
|
+
// Claim the content slot between <!--bf:s0--> and <!--/--> on first
|
|
76
|
+
// write; later writes reuse the claimed reference (no re-scanning)
|
|
77
|
+
const __bfw_s0 = lazySlots(__scope, [{ id: 's0', kind: 'markup', path: [] }])
|
|
76
78
|
createEffect(() => {
|
|
77
79
|
const __val = count()
|
|
78
|
-
|
|
80
|
+
__bfw_s0('s0', escapeTextOrNode(__val))
|
|
79
81
|
})
|
|
80
82
|
|
|
81
83
|
if (_s1) _s1.addEventListener('click', () => { setCount(n => n + 1) })
|
|
@@ -83,6 +85,6 @@ export function initCounter(__scope, _p = {}) {
|
|
|
83
85
|
|
|
84
86
|
hydrate('Counter', {
|
|
85
87
|
init: initCounter,
|
|
86
|
-
template: (_p) => `<button bf="s1"> Count: <!--bf:s0-->${(0)}<!--/--></button>`
|
|
88
|
+
template: (_p) => `<button bf="s1"> Count: <!--bf:s0-->${escapeText((0))}<!--/--></button>`
|
|
87
89
|
})
|
|
88
90
|
```
|
|
@@ -32,23 +32,40 @@ See [JSX Compatibility — Limitations](./jsx-compatibility.md#limitations) for
|
|
|
32
32
|
|
|
33
33
|
## How It Works
|
|
34
34
|
|
|
35
|
-
The compiler skips template generation for the expression. The
|
|
35
|
+
The compiler skips template generation for the expression: the server can never evaluate it, so the SSR-rendered width is always zero. The client claims a slot for it and writes the real value once the browser evaluates the expression — the general case behind both compiled shapes below (`spec/slot-unification.md` §4).
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**Claimed slot (the common case):** a marker pair is still emitted so the client has an anchor comment to claim against.
|
|
38
38
|
|
|
39
39
|
```html
|
|
40
|
-
<!--
|
|
40
|
+
<!-- server output -->
|
|
41
|
+
<!--bf:s0--><!--/--> items left
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
```js
|
|
45
|
+
// client JS
|
|
46
|
+
{ const __bfw_s0 = lazySlots(__scope, [{ id: 's0', kind: 'text', path: [] }])
|
|
47
|
+
createEffect(() => {
|
|
48
|
+
__bfw_s0('s0', todos().filter(t => !t.done).length)
|
|
49
|
+
}) }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Markerless elision (Step B):** when the expression is the ONLY content of its own element — not adjacent to other text/expressions, and not inside a loop or conditional branch — the compiler proves a static child-index path to the slot's position and drops the marker pair entirely from both SSR and CSR output. `<strong>{/* @client */ todos().filter(t => !t.done).length}</strong>` from the [TodoApp example](https://github.com/piconic-ai/barefootjs/blob/main/integrations/shared/components/TodoApp.tsx) qualifies:
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<!-- server output -->
|
|
56
|
+
<strong bf="s1"></strong>
|
|
57
|
+
```
|
|
44
58
|
|
|
45
59
|
```js
|
|
46
|
-
//
|
|
60
|
+
// client JS
|
|
61
|
+
{ const __bfw_s0 = lazySlots(__scope, [{ id: 's0', kind: 'text', path: [0, 0], markerless: true }])
|
|
47
62
|
createEffect(() => {
|
|
48
|
-
|
|
49
|
-
})
|
|
63
|
+
__bfw_s0('s0', todos().filter(t => !t.done).length)
|
|
64
|
+
}) }
|
|
50
65
|
```
|
|
51
66
|
|
|
67
|
+
Either way, the claim happens lazily on the first write — nothing is touched until the effect actually runs — and every later write goes through the held reference, never re-scanning the DOM (`packages/client/src/runtime/claim-slots.ts`).
|
|
68
|
+
|
|
52
69
|
|
|
53
70
|
## Examples
|
|
54
71
|
|
|
@@ -52,6 +52,21 @@ return <div>...</div>
|
|
|
52
52
|
))}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
`.flatMap()` expands nested collections into a flat run of keyed elements. A pure projection body — the nested `.map()` as the whole body, expression or single-`return` block — compiles on every adapter; a body with statements before the projection (early returns, `const`s) runs as JS on JS-runtime adapters and needs [`/* @client */`](./client-directive.md) on DSL backends:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// ✅ Projection — works on every adapter
|
|
59
|
+
{todos().flatMap(todo => todo.tags.map(tag => (
|
|
60
|
+
<li key={`${todo.id}:${tag}`}>{tag}</li>
|
|
61
|
+
)))}
|
|
62
|
+
|
|
63
|
+
// ✅ Statement body — JS-runtime adapters; /* @client */ on Go/Mojo etc.
|
|
64
|
+
{todos().flatMap(todo => {
|
|
65
|
+
if (todo.hidden) return []
|
|
66
|
+
return todo.tags.map(tag => <li key={`${todo.id}:${tag}`}>{tag}</li>)
|
|
67
|
+
})}
|
|
68
|
+
```
|
|
69
|
+
|
|
55
70
|
`.sort()` and `.toSorted()` can be chained with `.map()` and `.filter()`:
|
|
56
71
|
|
|
57
72
|
```tsx
|
|
@@ -105,7 +120,9 @@ Some JavaScript expressions cannot be translated into marked template syntax. Wh
|
|
|
105
120
|
|---|---|---|
|
|
106
121
|
| `.filter()` with destructured param (`({done}) => done`) | works (runs as JS) | **BF101** |
|
|
107
122
|
| `.filter()` with `function` keyword callback | works | **BF101** |
|
|
108
|
-
| `.reduce()`, `.forEach()`, `.flatMap()` | works | **BF101** |
|
|
123
|
+
| `.reduce()`, `.forEach()`, value-returning `.flatMap()` with an off-catalogue projection | works | **BF101** |
|
|
124
|
+
| JSX-returning `.flatMap()` projection (`items.flatMap(it => it.tags.map(tag => <li key={...}/>))`) | works | works |
|
|
125
|
+
| JSX-returning `.flatMap()` with a statement body (early `return`, `const` before the projection) | works (runs as JS) | **BF021** |
|
|
109
126
|
| Nested `.filter()` / `.map()` in a filter predicate (`x => x.tags.filter(...).length > 0`) | works | works |
|
|
110
127
|
| Nested `.some()` / `.find()` / `.reduce()` in a filter predicate | works | **BF101** |
|
|
111
128
|
| Sort comparator that's a multi-statement block body or `localeCompare(b, locale, opts)` | works (runs as JS) | **BF021** |
|