@barefootjs/cli 0.35.4 → 0.35.6

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.
@@ -60,7 +60,7 @@
60
60
 
61
61
  - [IR Schema Reference](./advanced/ir-schema.md) — Node types, metadata, hydration markers
62
62
  - [Compiler Internals](./advanced/compiler-internals.md) — Pipeline phases, reactivity analysis, code generation
63
- - [Error Codes Reference](./advanced/error-codes.md) — All BF001–BF043 errors with solutions
63
+ - [Error Codes Reference](./advanced/error-codes.md) — All BF-prefixed errors with solutions
64
64
  - [Performance Optimization](./advanced/performance.md) — Minimal client JS, fast hydration, efficient reactivity
65
65
 
66
66
  ---
@@ -126,7 +126,7 @@ export function Counter(__allProps: { initial?: number } & { __instanceId?: stri
126
126
  A build-time post-processing step injects `useRequestContext()` calls into generated templates. `BfScripts` renders the collected `<script>` tags:
127
127
 
128
128
  ```tsx
129
- import { BfScripts } from '@barefootjs/hono'
129
+ import { BfScripts } from '@barefootjs/hono/scripts'
130
130
 
131
131
  export function Layout({ children }) {
132
132
  return (
@@ -212,3 +212,158 @@ export default defineConfig({
212
212
  }),
213
213
  })
214
214
  ```
215
+
216
+ ## Adding to an Existing Project
217
+
218
+ `npm create barefootjs@latest` scaffolds the layout above from scratch; this section retrofits it onto an existing Hono + Cloudflare Workers app that already renders with `hono/jsx-renderer` and deploys with `wrangler deploy`. The end state is the same as the scaffold's, so a freshly scaffolded project is a valid reference to diff against when something doesn't line up.
219
+
220
+ ### What changes
221
+
222
+ `barefoot()` compiles **individual components** — each `.tsx` under `components` becomes an SSR template under `templates` plus hydration JS under `build.outDir`. It never bundles the server app into a single file. For a project that currently does that with a whole-app Vite bundler (e.g. `@hono/vite-build`), the consequence is:
223
+
224
+ | | Before | After |
225
+ |---|---|---|
226
+ | Server build | Bundler plugin emits a single-file SSR bundle | Step removed; nothing replaces it |
227
+ | `wrangler.jsonc` `main` | The pre-bundled output | The uncompiled entry file — wrangler's own esbuild bundles it at dev/deploy time |
228
+ | `vite build` | Produces the server bundle | Produces compiled templates + hydration JS only |
229
+
230
+ `barefoot()` takes the bundler plugin's place in `vite.config.ts`, and any `package.json` script or CI step that referenced the old bundle path goes with it.
231
+
232
+ **Paths in this section are the scaffold's** (`components/`, `dist/components`, `public/components`, `server.tsx`, `renderer.tsx`). Substitute your own. Two that are easy to copy literally by mistake:
233
+
234
+ - `wrangler.jsonc` `main` must be your app's actual Hono entry file (e.g. `src/index.tsx`). Copying `server.tsx` as-is fails immediately with `The entry-point file at "server.tsx" was not found`.
235
+ - `import { renderer } from './renderer'` assumes `jsxRenderer` lives in its own module, as in the scaffold. If yours is defined inline in the entry file, edit it there and skip the import — there's no need to split it out.
236
+
237
+ ### 1. Install
238
+
239
+ ```sh
240
+ npm install @barefootjs/client @barefootjs/hono @barefootjs/jsx @barefootjs/shared
241
+ npm install -D @barefootjs/vite
242
+ ```
243
+
244
+ > **Peer dependencies.** `@barefootjs/hono` peer-depends on `vite ^6.0.0` and `@barefootjs/jsx` on `typescript ^5.0.0`. A project already on a newer major of either (e.g. `vite@8`, `typescript@7`) fails `npm install` with an `ERESOLVE` error. Either install with `--legacy-peer-deps` or pin `typescript` to `^5`. The `typescript` range is the one that matters: a newer `vite` alone works in practice, but a newer `typescript` makes `vite build` crash at config-load time with `TypeError: ts.createPrinter is not a function` — an opaque runtime error, not a version check — because `@barefootjs/vite` calls the `typescript` package's compiler API directly.
245
+
246
+ ### 2. Config files
247
+
248
+ #### `tsconfig.json`
249
+
250
+ ```json
251
+ {
252
+ "compilerOptions": {
253
+ "jsx": "react-jsx",
254
+ "jsxImportSource": "@barefootjs/hono/jsx",
255
+ "baseUrl": ".",
256
+ "paths": {
257
+ "@/components/*": ["./dist/components/*", "./components/*"]
258
+ }
259
+ },
260
+ "exclude": ["node_modules", "dist/components"]
261
+ }
262
+ ```
263
+
264
+ `@/components/*` has two targets, tried in order by both TypeScript and wrangler: the compiled SSR template under `dist/components/` (carrying the hydration markers from Output Format above) whenever it exists, otherwise the raw source under `components/` so editor tooling and type-checking resolve before the first build. Excluding `dist/components` keeps the compiled copy out of type-checking. Everything else already in `compilerOptions` (`types`, `strict`, …) stays as it is.
265
+
266
+ #### `vite.config.ts`
267
+
268
+ ```ts
269
+ // vite.config.ts
270
+ import { dirname, resolve } from 'node:path'
271
+ import { fileURLToPath } from 'node:url'
272
+ import { defineConfig } from 'vite'
273
+ import { barefoot } from '@barefootjs/hono/vite'
274
+
275
+ const HERE = dirname(fileURLToPath(import.meta.url))
276
+
277
+ export default defineConfig({
278
+ // Public URL prefix of the client build. Must match the path Workers
279
+ // Assets serves `build.outDir` under (wrangler.jsonc below).
280
+ base: '/components/',
281
+ resolve: {
282
+ // Required, not optional: Vite's dev-server dependency pre-scan runs
283
+ // before this plugin's `transform` hook and ignores tsconfig `paths`.
284
+ // Points at the SOURCE tree, never at `dist/components`.
285
+ alias: {
286
+ '@/components': resolve(HERE, 'components'),
287
+ },
288
+ },
289
+ // `build.outDir` sits inside `public/`, which Workers Assets already
290
+ // serves as-is. Vite's default `publicDir` would copy the rest of
291
+ // `public/` into `public/components` on every build for nothing.
292
+ publicDir: false,
293
+ build: {
294
+ outDir: 'public/components', // hydration JS → served by Workers Assets
295
+ emptyOutDir: true,
296
+ },
297
+ plugins: barefoot({
298
+ components: ['components'], // .tsx source directories to compile
299
+ templates: 'dist/components', // compiled SSR templates → imported by the server, never served
300
+ }),
301
+ })
302
+ ```
303
+
304
+ Everything not shown (hashing, chunking, minification, dev server) is stock Vite. `adapterOptions`, `assets`, and `assetsOutputFile` (see Options above) are not needed for a retrofit.
305
+
306
+ #### `wrangler.jsonc`
307
+
308
+ ```jsonc
309
+ {
310
+ "$schema": "node_modules/wrangler/config-schema.json",
311
+ "name": "my-app",
312
+ // Your existing uncompiled Hono entry (see "Paths" above). Wrangler
313
+ // bundles it and follows the tsconfig `paths` mapping into dist/components/.
314
+ "main": "server.tsx",
315
+ "compatibility_date": "2025-01-01",
316
+ // Must cover `build.outDir` from vite.config.ts and must not include
317
+ // `templates` — see Deploying to Cloudflare Workers above.
318
+ "assets": {
319
+ "directory": "./public"
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### 3. Wire the app
325
+
326
+ **Render `<BfScripts />` once** (from `@barefootjs/hono/scripts`) in the layout your `jsxRenderer` already defines — not inside individual components. It emits the `<script>` tags loading each compiled component's hydration JS exactly once per page, however many instances are rendered (see Script Collection above). Existing `ContextRenderer` augmentation and stylesheet links stay as they are.
327
+
328
+ ```tsx
329
+ import { jsxRenderer } from 'hono/jsx-renderer'
330
+ import { BfScripts } from '@barefootjs/hono/scripts'
331
+
332
+ export const renderer = jsxRenderer(({ children, title }) => (
333
+ <html lang="en">
334
+ <head>
335
+ <meta charset="UTF-8" />
336
+ <title>{title ?? 'My app'}</title>
337
+ </head>
338
+ <body>
339
+ {children}
340
+ <BfScripts />
341
+ </body>
342
+ </html>
343
+ ))
344
+ ```
345
+
346
+ **Import components through the alias**, not a relative path into the source tree, so the dist-first resolution from `tsconfig.json` applies:
347
+
348
+ ```tsx
349
+ // server.tsx
350
+ import { Hono } from 'hono'
351
+ import { renderer } from './renderer'
352
+ import { Counter } from '@/components/Counter'
353
+
354
+ const app = new Hono()
355
+ app.use('*', renderer)
356
+ app.get('/', (c) => c.render(<main><Counter /></main>, { title: 'My app' }))
357
+ export default app
358
+ ```
359
+
360
+ Existing plain Hono JSX components can stay put and move under `components/` one at a time — only files under a configured `components` directory are compiled.
361
+
362
+ ### 4. Build and verify
363
+
364
+ ```sh
365
+ npx vite build # writes dist/components/ (templates) and public/components/ (hydration JS)
366
+ npx wrangler dev # or: npx wrangler deploy
367
+ ```
368
+
369
+ Load a page that renders a converted `"use client"` component: its element carries a `bf` marker attribute (see Output Format above) and it responds to interaction. During development, run `vite dev` alongside `wrangler dev` so `dist/components/` keeps regenerating on change.
@@ -18,16 +18,20 @@ JSX Source
18
18
 
19
19
  ## Available Adapters
20
20
 
21
- | Adapter | Output | Backend | Package |
22
- |---------|--------|---------|---------|
23
- | [`HonoAdapter`](./adapters/hono-adapter.md) | `.tsx` | Hono / JSX-based servers | `@barefootjs/hono` |
24
- | [`GoTemplateAdapter`](./adapters/go-template-adapter.md) | `.tmpl` + `_types.go` | Go `html/template` | `@barefootjs/go-template` |
25
- | [Perl](./adapters/perl-adapter.md) | `.ep` / `.tx` | Mojolicious, Text::Xslate (PSGI/Plack) | `@barefootjs/mojolicious`, `@barefootjs/xslate` |
26
- | [Ruby](./adapters/ruby-adapter.md) | `.erb` | stdlib ERB (any Rack app — Sinatra, Rails) | `@barefootjs/erb` |
27
- | [Python](./adapters/python-adapter.md) | `.jinja` | Jinja2 (Flask, Django, bare WSGI) | `@barefootjs/jinja` |
28
- | [PHP](./adapters/php-adapter.md) | `.twig` / `.blade.php` | Twig (Slim, plain PHP), Laravel Blade (`illuminate/view` standalone) | `@barefootjs/twig`, `@barefootjs/blade` |
29
- | [Rust](./adapters/rust-adapter.md) | `.j2` | minijinja (axum, actix-web, warp) | `@barefootjs/rust` |
30
- | [CSR](./adapters/csr.md) | — (client-rendered) | None (browser-only) | `@barefootjs/client` |
21
+ <!-- ADAPTER-TABLE:START (auto-generated by scripts/generate-adapter-docs.ts do not edit by hand) -->
22
+ | Language | Backend | Package |
23
+ |----------|---------|---------|
24
+ | TypeScript | [Hono](./adapters/hono-adapter.md) | `@barefootjs/hono` |
25
+ | Go | [html/template](./adapters/go-template-adapter.md) | `@barefootjs/go-template` |
26
+ | Perl | [Mojolicious](./adapters/perl-adapter.md) | `@barefootjs/mojolicious` |
27
+ | Perl | [Text::Xslate](./adapters/perl-adapter.md) | `@barefootjs/xslate` |
28
+ | Ruby | [ERB](./adapters/ruby-adapter.md) | `@barefootjs/erb` |
29
+ | Python | [Jinja2](./adapters/python-adapter.md) | `@barefootjs/jinja` |
30
+ | PHP | [Twig](./adapters/php-adapter.md) | `@barefootjs/twig` |
31
+ | PHP | [Laravel Blade](./adapters/php-adapter.md) | `@barefootjs/blade` |
32
+ | Rust | [minijinja](./adapters/rust-adapter.md) | `@barefootjs/rust` |
33
+ | — | [CSR (browser only)](./adapters/csr.md) | `@barefootjs/client` |
34
+ <!-- ADAPTER-TABLE:END -->
31
35
 
32
36
  > CSR is not an IR→template adapter. It renders components directly in the browser using client-side template functions — use it when the server can't (or shouldn't) emit the initial HTML.
33
37
 
@@ -75,17 +75,32 @@ Files with reactive APIs but no `"use client"` emit **BF001**:
75
75
  error[BF001]: 'use client' directive required for components with createSignal
76
76
  ```
77
77
 
78
- ### Props Destructuring Detection
78
+ ### Destructured Props Stay Live
79
79
 
80
80
  ```tsx
81
- // ⚠️ BF043: Destructuring captures values once — may lose reactivity
82
81
  function Child({ count }: Props) { ... }
83
82
 
84
- // No warning direct access maintains reactivity
83
+ // Compiles identically to direct access:
85
84
  function Child(props: Props) { ... }
86
85
  ```
87
86
 
88
- Suppress with `// @bf-ignore props-destructuring`.
87
+ Both forms record the same `propsParams` metadata in Phase 1. Phase 2 (client JS emission)
88
+ rewrites every value-position read of a name bound by the destructured PARAMETER — inside
89
+ effects, memos, handlers, text, reactive attributes, anywhere in the generated `init*` body —
90
+ to a live `_p.<key>` read, the same read `props.xxx` compiles to. See
91
+ `rewriteDestructuredPropReads` (`ir-to-client-js/rewrite-destructured-props.ts`) and
92
+ `livePropReadExpr` (`props-binding.ts`).
93
+
94
+ Destructuring in the function BODY (`const { count } = props`, or the equivalent `const count
95
+ = props.count`) is a pure single-prop alias — the analyzer's IR can't tell the two shapes
96
+ apart, and treats them identically — and is ALSO rewritten live, via the same
97
+ `rewriteDestructuredPropReads` door's `rewriteBodyAliasReads` branch
98
+ (`resolveBodyPropAliases`, `props-binding.ts`): the local's own extraction is deleted from the
99
+ emitted body (it would otherwise shadow the very references the rewrite is trying to make
100
+ live) and every reference to it becomes `_p.count`, same as the parameter form. This does NOT
101
+ apply once the local does a real computation of its own (`const doubled = count * 2` stays an
102
+ ordinary once-evaluated local) or is reassigned (`let { count } = props` is never treated as
103
+ a live alias).
89
104
 
90
105
  ---
91
106
 
@@ -317,64 +317,41 @@ See [JSX Compatibility](../rendering/jsx-compatibility.md) for the full worked e
317
317
 
318
318
  ---
319
319
 
320
- ## Component Errors (BF043–BF049)
320
+ ## Component Errors (BF044–BF049)
321
321
 
322
- <a id="bf043"></a>
323
-
324
- ### BF043 Props Destructuring (Warning)
325
-
326
- **Trigger:** Props destructured in function parameter.
327
-
328
- ```tsx
329
- // ⚠️ BF043
330
- function Child({ count }: Props) {
331
- return <span>{count}</span> // count is captured once
332
- }
333
- ```
334
-
335
- ```
336
- warning[BF043]: Destructuring props in function parameters captures values once.
337
- = help: Use `props.count` for reactive access, or suppress with // @bf-ignore props-destructuring
338
- ```
339
-
340
- **Fix options:**
341
-
342
- 1. Use direct props access:
343
-
344
- ```tsx
345
- function Child(props: Props) {
346
- return <span>{props.count}</span> // Reactive
347
- }
348
- ```
349
-
350
- 2. Suppress if intentional (static initial value):
351
-
352
- ```tsx
353
- // @bf-ignore props-destructuring
354
- function Child({ initialCount }: Props) {
355
- const [count, setCount] = createSignal(initialCount)
356
- return <span>{count()}</span>
357
- }
358
- ```
322
+ <!--
323
+ BF043 (Props Destructuring warning) is retired: destructured props read
324
+ live, the same as `props.xxx` access, so there is no reactivity difference
325
+ left to warn about. See [Props Reactivity](../reactivity/props-reactivity.md).
326
+ -->
359
327
 
360
328
  <a id="bf044"></a>
361
329
 
362
330
  ### BF044 — Signal/Memo Getter Not Called
363
331
 
364
- **Trigger:** Signal/memo getter passed without calling it.
332
+ **Trigger:** Signal/memo getter passed without calling it in a RENDERED
333
+ position — a DOM element attribute or a JSX text child, where the value
334
+ becomes literal output.
365
335
 
366
336
  ```tsx
367
337
  // ❌ BF044
368
- <Child count={count} /> // Passing getter function, not the value
338
+ <div count={count} /> // Passing getter function, not the value
369
339
  ```
370
340
 
371
341
  **Fix:**
372
342
 
373
343
  ```tsx
374
344
  // ✅ Fixed
375
- <Child count={count()} />
345
+ <div count={count()} />
376
346
  ```
377
347
 
348
+ **Not triggered on a component prop:** `<Child count={count} />` compiles —
349
+ a component prop is an opaque value handed to the child, not rendered
350
+ output, and passing a live getter there is this codebase's deliberate
351
+ Context-Provider idiom (the child calls it at its own read site). See
352
+ [`spec/compiler.md`'s BF044 section](https://github.com/piconic-ai/barefootjs/blob/main/spec/compiler.md#signalmemo-getter-not-called-bf044)
353
+ for the full rule.
354
+
378
355
  <a id="bf049"></a>
379
356
 
380
357
  ### BF049 — Rich-Typed Prop Not Hydratable
@@ -457,25 +434,6 @@ export function Page() {
457
434
 
458
435
  ---
459
436
 
460
- ## Suppressing Warnings
461
-
462
- Suppress with `@bf-ignore`:
463
-
464
- ```tsx
465
- // @bf-ignore props-destructuring
466
- function Component({ checked }: Props) {
467
- // Warning suppressed
468
- }
469
- ```
470
-
471
- **Available rules:**
472
-
473
- | Rule ID | Error Code | Description |
474
- |---------|------------|-------------|
475
- | `props-destructuring` | BF043 | Props destructuring in function parameters |
476
-
477
- ---
478
-
479
437
  ## Error Code Quick Reference
480
438
 
481
439
  | Code | Severity | Description |
@@ -486,7 +444,6 @@ function Component({ checked }: Props) {
486
444
  | BF013 | Error | Reactive primitive called through an unresolved namespace import |
487
445
  | BF021 | Error | Unsupported JSX pattern for SSR |
488
446
  | BF023 | Error | Missing key in list |
489
- | BF043 | Warning | Props destructuring breaks reactivity |
490
447
  | BF044 | Error | Signal/memo getter passed without calling it |
491
448
  | BF049 | Error | Rich-typed prop read by client code cannot survive hydration |
492
449
  | BF054 | Error | Built-in `<Async>` / `<Region>` used without `@barefootjs/client` import |
@@ -35,10 +35,11 @@ function Button(props: { variant?: 'default' | 'primary'; children?: Child }) {
35
35
  }
36
36
  ```
37
37
 
38
- For initial-value-only props, default parameter syntax works. Add `@bf-ignore` to suppress the `BF043` destructuring warning:
38
+ For initial-value-only props, default parameter syntax works destructuring is fully
39
+ reactive (see [Props Reactivity](../reactivity/props-reactivity.md)), so there is nothing to
40
+ suppress:
39
41
 
40
42
  ```tsx
41
- // @bf-ignore props-destructuring
42
43
  function Counter({ initial = 0 }: { initial?: number }) {
43
44
  const [count, setCount] = createSignal(initial)
44
45
  return <button onClick={() => setCount(n => n + 1)}>{count()}</button>
@@ -13,19 +13,21 @@ BarefootJS compiles JSX at build time into your backend's native template format
13
13
  JSX → IR (backend-agnostic) → Adapter → Template
14
14
  ```
15
15
 
16
- | Language | Adapter | Notes |
17
- |----------|---------|-------|
18
- | TypeScript | [HonoAdapter](../adapters/hono-adapter.md) | Hono / JSX-based TS servers |
19
- | TypeScript | [TestAdapter](https://github.com/piconic-ai/barefootjs/tree/main/packages/test) | IR-based component testing |
20
- | Go | [GoTemplateAdapter](../adapters/go-template-adapter.md) | `html/template` |
21
- | Perl | [MojoliciousAdapter](https://github.com/piconic-ai/barefootjs/tree/main/packages/adapter-mojolicious) | Mojolicious EP templates |
22
-
23
- ### Planned
24
-
25
- | Language | Adapter |
26
- |----------|---------|
27
- | Rust | (TBD) |
28
- | Python | Jinja2Adapter |
29
- | Ruby | ERBAdapter |
16
+ <!-- ADAPTER-TABLE:START (auto-generated by scripts/generate-adapter-docs.ts do not edit by hand) -->
17
+ | Language | Backend | Package |
18
+ |----------|---------|---------|
19
+ | TypeScript | [Hono](../adapters/hono-adapter.md) | `@barefootjs/hono` |
20
+ | Go | [html/template](../adapters/go-template-adapter.md) | `@barefootjs/go-template` |
21
+ | Perl | [Mojolicious](../adapters/perl-adapter.md) | `@barefootjs/mojolicious` |
22
+ | Perl | [Text::Xslate](../adapters/perl-adapter.md) | `@barefootjs/xslate` |
23
+ | Ruby | [ERB](../adapters/ruby-adapter.md) | `@barefootjs/erb` |
24
+ | Python | [Jinja2](../adapters/python-adapter.md) | `@barefootjs/jinja` |
25
+ | PHP | [Twig](../adapters/php-adapter.md) | `@barefootjs/twig` |
26
+ | PHP | [Laravel Blade](../adapters/php-adapter.md) | `@barefootjs/blade` |
27
+ | Rust | [minijinja](../adapters/rust-adapter.md) | `@barefootjs/rust` |
28
+ | | [CSR (browser only)](../adapters/csr.md) | `@barefootjs/client` |
29
+ <!-- ADAPTER-TABLE:END -->
30
+
31
+ TypeScript also has an [IR-based `TestAdapter`](https://github.com/piconic-ai/barefootjs/tree/main/packages/test) for browser-free component testing — it isn't a serving-time backend, so it's outside the table above. See [Adapters](../adapters.md) for the full picture, including which adapters share one engine-agnostic runtime.
30
32
 
31
33
  The IR contract is stable. You can [write a custom adapter](../adapters/custom-adapter.md) for any backend.
@@ -88,7 +88,7 @@ export function Counter(props: CounterProps) {
88
88
  }
89
89
  ```
90
90
 
91
- See [Client Directive](./rendering/client-directive.md), [`createSignal`](./reactivity/create-signal.md), and [`createMemo`](./reactivity/create-memo.md) for what each piece does. Props are read via `props.initial`, not destructured destructuring captures the value once and breaks reactivity, so the compiler emits warning [`BF043`](./advanced/error-codes.md) when it sees that form on a `"use client"` component. [Props Reactivity](./reactivity/props-reactivity.md) covers the full rule.
91
+ See [Client Directive](./rendering/client-directive.md), [`createSignal`](./reactivity/create-signal.md), and [`createMemo`](./reactivity/create-memo.md) for what each piece does. Props are read via `props.initial` here, but destructuring them in the function parameter (`function Counter({ initial }: CounterProps)`) works identically both forms compile to the same live read, so pick whichever reads better. [Props Reactivity](./reactivity/props-reactivity.md) covers the full rule.
92
92
 
93
93
  The Counter is mounted in `server.tsx`:
94
94
 
@@ -5,10 +5,13 @@ description: How prop access patterns determine whether reactive updates propaga
5
5
 
6
6
  # Props Reactivity
7
7
 
8
- **How you access props determines whether updates propagate.** The compiler wraps dynamic prop expressions in getters.
8
+ **`props.xxx` access, destructuring the props parameter, and destructuring the props object
9
+ inside the function body are all equally reactive.** The compiler wraps dynamic prop
10
+ expressions in getters, and every value-position read of a prop — written any of the three
11
+ ways — compiles to a live read of that getter.
9
12
 
10
13
 
11
- ## Direct Access — Reactive
14
+ ## Direct Access
12
15
 
13
16
  `props.xxx` maintains reactivity. Each access calls the underlying getter:
14
17
 
@@ -22,55 +25,87 @@ function Display(props: { value: number }) {
22
25
  ```
23
26
 
24
27
 
25
- ## Destructuring Captures Once
28
+ ## Destructuring In The Parameter
26
29
 
27
- Destructuring calls the getter once and stores the result. The value does not update:
30
+ Destructuring the props **parameter** is also fully reactive. Every reference to a
31
+ destructured prop name — in a `createEffect` body, a `createMemo` computation, an event
32
+ handler, a reactive attribute, plain text, anywhere — compiles to the same live read
33
+ `props.xxx` would:
28
34
 
29
35
  ```tsx
30
36
  function Display({ value }: { value: number }) {
31
37
  createEffect(() => {
32
- console.log(value) // Stale captured at component init
38
+ console.log(value) // Re-runs when parent updates value
33
39
  })
34
40
  return <span>{value}</span>
35
41
  }
36
42
  ```
37
43
 
38
- The compiler emits `BF043` when it detects props destructuring in a client component:
44
+ There is no local variable holding a stale, captured-at-mount copy `value` above compiles
45
+ to a live read of the parent's getter at every reference site, the same as `props.value`
46
+ would. A destructure default (`{ value = 0 }`) is evaluated live too: it re-applies on every
47
+ read, not just once at mount.
39
48
 
40
- ```
41
- warning[BF043]: Props destructuring breaks reactivity
42
-
43
- --> src/components/Display.tsx:1:18
44
- |
45
- 1 | function Display({ value }: { value: number }) {
46
- | ^^^^^^^^^
47
- |
48
- = help: Access props via `props.value` to maintain reactivity
49
+
50
+ ## Destructuring In The Body
51
+
52
+ Destructuring inside the function body is also fully reactive, as long as the destructured
53
+ name is a **pure alias** of a single prop — nothing computed from it:
54
+
55
+ ```tsx
56
+ function Display(props: { value: number }) {
57
+ const { value } = props
58
+ createEffect(() => {
59
+ console.log(value) // Re-runs when parent updates value
60
+ })
61
+ return <span>{value}</span>
62
+ }
49
63
  ```
50
64
 
51
- Suppress with `@bf-ignore` when capturing intentionally (e.g., initial values):
65
+ The compiler recognizes `const { value } = props` (and the equivalent `const value =
66
+ props.value`) as a pure passthrough, drops the local extraction entirely, and rewrites every
67
+ reference to `value` to a live `props.value` read instead — the same rewrite the parameter
68
+ form gets. A destructure default (`const { value = 0 } = props`) and a renamed binding
69
+ (`const { value: v } = props`) are both covered the same way.
70
+
71
+ This does NOT apply once the local does its own computation, or is reassigned:
52
72
 
53
73
  ```tsx
54
- // @bf-ignore props-destructuring
55
- function Counter({ initial }: { initial: number }) {
56
- const [count, setCount] = createSignal(initial)
57
- return <button onClick={() => setCount(n => n + 1)}>{count()}</button>
74
+ function Display(props: { value: number }) {
75
+ const { value } = props
76
+ const doubled = value * 2 // `doubled` is an ordinary once-evaluated local
77
+ let { count } = props
78
+ count += 1 // `count` is reassigned, so it can't be a live alias either
79
+ return <span>{doubled}</span>
58
80
  }
59
81
  ```
60
82
 
83
+ `doubled` is a real computation, not a passthrough — it is an ordinary local, evaluated once
84
+ at its declaration (same as it would be with any other access pattern). A `let` binding is
85
+ never treated as a live alias, since rewriting a later assignment to it would mean silently
86
+ writing through to the caller's prop.
61
87
 
62
- ## When Destructuring Is Safe
63
88
 
64
- Destructuring is safe for **initial values** of local state and for values that never change (`id`, static labels).
89
+ ## When To Prefer Which
90
+
91
+ The three reactive forms — `props.xxx`, parameter destructuring, and body destructuring —
92
+ behave identically at runtime, so the choice between them is style, not correctness:
93
+
94
+ - Destructuring (parameter or body) reads naturally and is usually the better default for
95
+ components with a handful of named props.
96
+ - `props.xxx` avoids repeating a long prop list at the call site, and is the natural fit for
97
+ a component that mostly forwards its props (`...rest`) rather than naming each one.
65
98
 
66
99
 
67
100
  ## Summary
68
101
 
69
- | Pattern | Reactive? | Use when |
70
- |---------|-----------|----------|
71
- | `props.value` | Yes | You need live updates from parent |
72
- | `const { value } = props` | No | Value is used once (e.g., initial state) |
73
- | `createSignal(props.value)` | `props.value` is reactive, signal is independent | Creating local state from a prop |
102
+ | Pattern | Reactive? |
103
+ |---------|-----------|
104
+ | `props.value` | Yes |
105
+ | `function C({ value }: Props)` parameter destructuring | Yes |
106
+ | `const { value } = props` body destructuring (pure alias) | Yes |
107
+ | `const doubled = value * 2` — a computation, not a pure alias | No, evaluated once at declaration (same as any other once-evaluated local) |
108
+ | `createSignal(props.value)` | `props.value` is reactive, the signal it seeds is independent thereafter |
74
109
 
75
110
 
76
111
  ## How It Works
@@ -85,7 +120,10 @@ The compiler transforms dynamic prop expressions into getters:
85
120
  { get value() { return count() } }
86
121
  ```
87
122
 
88
- - `props.value` calls getter calls `count()` dependency tracked
89
- - `const { value } = props` calls getter once stores the number → no further tracking
90
-
91
- This is the same model as SolidJS. If you are coming from React, this is the key behavioral difference.
123
+ `props.value` calls the getter directly. A `value` destructured in the PARAMETER, or aliased
124
+ by a pure body destructure, compiles to the exact same getter call at every reference site
125
+ the compiler rewrites each one, rather than binding a plain local that would only read the
126
+ getter once. This is the same reactive-getter model as SolidJS; unlike SolidJS, BarefootJS
127
+ performs that rewrite for both the parameter form and the body-alias form, so there is no
128
+ "don't destructure props" caveat left — only the ordinary rule that a real computation
129
+ (`value * 2`) is evaluated once, same as it would be anywhere else in the function.