@barefootjs/cli 0.35.5 → 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
  ---
@@ -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>
@@ -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.