@barefootjs/cli 0.5.1 → 0.5.2

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.
@@ -12,6 +12,7 @@
12
12
 
13
13
  ## Reactivity
14
14
 
15
+ - [batch](https://barefootjs.dev/docs/reactivity/batch.md): Groups multiple signal writes so dependent effects and memos run once, after all writes complete.
15
16
  - [createEffect](https://barefootjs.dev/docs/reactivity/create-effect.md): Runs a function and re-runs it whenever its tracked signal dependencies change.
16
17
  - [createMemo](https://barefootjs.dev/docs/reactivity/create-memo.md): Creates a cached derived value that recomputes only when its dependencies change.
17
18
  - [createSignal](https://barefootjs.dev/docs/reactivity/create-signal.md): Creates a reactive getter/setter pair for managing state.
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: batch
3
+ description: Groups multiple signal writes so dependent effects and memos run once, after all writes complete.
4
+ ---
5
+
6
+ # batch
7
+
8
+ Groups multiple signal writes so that dependent effects and memos run **once**,
9
+ after all the writes inside the batch complete — instead of once per write.
10
+
11
+ ```tsx
12
+ import { batch } from '@barefootjs/client'
13
+
14
+ batch<T>(fn: () => T): T
15
+ ```
16
+
17
+ Returns the value produced by `fn`.
18
+
19
+ ## Default behavior (no batch)
20
+
21
+ BarefootJS propagates updates **synchronously**: each setter call immediately
22
+ re-runs every subscriber. This keeps reads-after-writes predictable — after a
23
+ setter returns, derived memos, effects, and the DOM already reflect the new value.
24
+
25
+ The cost is that writing N signals that share a subscriber re-runs that
26
+ subscriber N times, and the subscriber briefly observes intermediate states
27
+ where some signals are updated and others are not:
28
+
29
+ ```tsx
30
+ const [x, setX] = createSignal(40)
31
+ const [y, setY] = createSignal(60)
32
+
33
+ createEffect(() => {
34
+ // depends on both x and y
35
+ send({ x: x(), y: y() })
36
+ })
37
+
38
+ setX(70) // effect runs — observes x=70, y=60 (intermediate)
39
+ setY(30) // effect runs again — observes x=70, y=30
40
+ ```
41
+
42
+ Beyond its initial run on creation, the effect ran twice more — once per write —
43
+ and saw a transient `x=70, y=60` state.
44
+
45
+ ## With batch
46
+
47
+ ```tsx
48
+ batch(() => {
49
+ setX(70)
50
+ setY(30)
51
+ })
52
+ // effect runs once, observing x=70, y=30
53
+ ```
54
+
55
+ Inside `batch`, writes are collected and dependent subscribers are de-duplicated,
56
+ so each runs **exactly once** after the batch ends — and never observes an
57
+ intermediate, half-updated state.
58
+
59
+ ## When to use
60
+
61
+ When a single handler updates several signals that feed shared effects/memos,
62
+ `batch` collapses the work into one update pass — and keeps the subscriber from
63
+ running while a cross-field invariant is temporarily broken:
64
+
65
+ ```tsx
66
+ const reset = () => {
67
+ batch(() => {
68
+ setName('')
69
+ setEmail('')
70
+ setAge(0)
71
+ // ...20 more fields
72
+ })
73
+ // every subscriber ran once, not once-per-field
74
+ }
75
+ ```
76
+
77
+ ## Caveats
78
+
79
+ ### Derived values are stale *inside* the batch
80
+
81
+ `batch` defers the work that recomputes derived values. Plain signal reads return
82
+ the new value immediately, but **memos and effect-driven values stay stale until
83
+ the batch ends**:
84
+
85
+ ```tsx
86
+ const [n, setN] = createSignal(1)
87
+ const doubled = createMemo(() => n() * 2)
88
+
89
+ batch(() => {
90
+ setN(10)
91
+ n() // 10 — plain signal read is fresh
92
+ doubled() // 2 — STALE; the memo hasn't recomputed yet
93
+ })
94
+ doubled() // 20 — recomputed after the batch ends
95
+ ```
96
+
97
+ If you need the recomputed value, read it after the batch.
98
+
99
+ ### `await` escapes the batch
100
+
101
+ `batch` only covers the **synchronous** portion of `fn`. Wrapping an async
102
+ function in `batch` groups only the writes before the first `await` — everything
103
+ after runs ungrouped, and the promise `batch` returns is easy to leave floating.
104
+
105
+ Instead, wrap each synchronous group of writes in its own `batch`, with `await`
106
+ between the groups:
107
+
108
+ ```tsx
109
+ const onSubmit = async () => {
110
+ batch(() => {
111
+ setLoading(true)
112
+ setError(null)
113
+ })
114
+
115
+ try {
116
+ const result = await save()
117
+ batch(() => {
118
+ setLoading(false)
119
+ setResult(result)
120
+ })
121
+ } catch (err) {
122
+ batch(() => {
123
+ setLoading(false)
124
+ setError(err)
125
+ })
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Note
131
+
132
+ `batch` is an **opt-in** optimization. Forgetting it is never a correctness bug —
133
+ code still works, just with extra subscriber runs. Reach for `batch` when a
134
+ handler writes many signals that share subscribers, or when an effect must not
135
+ observe a partially-updated state.
@@ -9,7 +9,7 @@ All reactive primitives are imported from `@barefootjs/client`:
9
9
 
10
10
  ```tsx
11
11
  "use client"
12
- import { createSignal, createEffect, createMemo, onMount, onCleanup, untrack } from '@barefootjs/client'
12
+ import { createSignal, createEffect, createMemo, onMount, onCleanup, untrack, batch } from '@barefootjs/client'
13
13
  ```
14
14
 
15
15
  ## API Reference
@@ -22,6 +22,7 @@ import { createSignal, createEffect, createMemo, onMount, onCleanup, untrack } f
22
22
  | [`onMount`](./reactivity/on-mount.md) | Run once on component initialization |
23
23
  | [`onCleanup`](./reactivity/on-cleanup.md) | Register cleanup for effects and lifecycle |
24
24
  | [`untrack`](./reactivity/untrack.md) | Read signals without tracking dependencies |
25
+ | [`batch`](./reactivity/batch.md) | Group signal writes so subscribers run once |
25
26
 
26
27
  ## Guides
27
28
 
package/dist/index.js CHANGED
@@ -2450,6 +2450,24 @@ function getSourceLocation(node, sourceFile, filePath) {
2450
2450
  }
2451
2451
  };
2452
2452
  }
2453
+ function membersToProperties(members, sourceFile) {
2454
+ return members.filter(ts6.isPropertySignature).map((member) => ({
2455
+ name: propertyNameText(member.name, sourceFile),
2456
+ type: typeNodeToTypeInfo(member.type, sourceFile) ?? {
2457
+ kind: "unknown",
2458
+ raw: "unknown"
2459
+ },
2460
+ optional: !!member.questionToken,
2461
+ readonly: !!member.modifiers?.some(
2462
+ (m) => m.kind === ts6.SyntaxKind.ReadonlyKeyword
2463
+ )
2464
+ }));
2465
+ }
2466
+ function propertyNameText(name, sourceFile) {
2467
+ if (!name) return "";
2468
+ if (ts6.isStringLiteral(name) || ts6.isNumericLiteral(name)) return name.text;
2469
+ return name.getText(sourceFile);
2470
+ }
2453
2471
  function typeNodeToTypeInfo(typeNode, sourceFile) {
2454
2472
  if (!typeNode) return null;
2455
2473
  const raw = typeNode.getText(sourceFile);
@@ -2488,20 +2506,21 @@ function typeNodeToTypeInfo(typeNode, sourceFile) {
2488
2506
  return {
2489
2507
  kind: "object",
2490
2508
  raw,
2491
- properties: typeNode.members.filter(ts6.isPropertySignature).map((member) => ({
2492
- name: member.name?.getText(sourceFile) ?? "",
2493
- type: typeNodeToTypeInfo(member.type, sourceFile) ?? {
2494
- kind: "unknown",
2495
- raw: "unknown"
2496
- },
2497
- optional: !!member.questionToken,
2498
- readonly: !!member.modifiers?.some(
2499
- (m) => m.kind === ts6.SyntaxKind.ReadonlyKeyword
2500
- )
2501
- }))
2509
+ properties: membersToProperties(typeNode.members, sourceFile)
2502
2510
  };
2503
2511
  }
2504
2512
  if (ts6.isTypeReferenceNode(typeNode)) {
2513
+ const refName = ts6.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : "";
2514
+ if ((refName === "Array" || refName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
2515
+ return {
2516
+ kind: "array",
2517
+ raw,
2518
+ elementType: typeNodeToTypeInfo(typeNode.typeArguments[0], sourceFile) ?? {
2519
+ kind: "unknown",
2520
+ raw: "unknown"
2521
+ }
2522
+ };
2523
+ }
2505
2524
  return {
2506
2525
  kind: "interface",
2507
2526
  raw
@@ -3679,14 +3698,17 @@ function collectInterfaceDefinition(node, ctx2) {
3679
3698
  kind: "interface",
3680
3699
  name: node.name.text,
3681
3700
  definition: node.getText(ctx2.sourceFile),
3701
+ properties: membersToProperties(node.members, ctx2.sourceFile),
3682
3702
  loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
3683
3703
  });
3684
3704
  }
3685
3705
  function collectTypeAliasDefinition(node, ctx2) {
3706
+ const properties = ts7.isTypeLiteralNode(node.type) ? membersToProperties(node.type.members, ctx2.sourceFile) : void 0;
3686
3707
  ctx2.typeDefinitions.push({
3687
3708
  kind: "type",
3688
3709
  name: node.name.text,
3689
3710
  definition: node.getText(ctx2.sourceFile),
3711
+ properties,
3690
3712
  loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
3691
3713
  });
3692
3714
  }
@@ -5882,7 +5904,7 @@ function checkSupport(expr) {
5882
5904
  return {
5883
5905
  supported: false,
5884
5906
  level: "L5_UNSUPPORTED",
5885
- reason: `Higher-order method '${methodName}()' requires client-side evaluation. Use @client directive or pre-compute in Go.`
5907
+ reason: `Method '${methodName}()' has no template lowering and requires client-side evaluation. Wrap the expression in /* @client */ to defer it to hydration, or pre-compute the value before rendering.`
5886
5908
  };
5887
5909
  }
5888
5910
  }
@@ -6172,7 +6194,7 @@ var init_expression_parser = __esm({
6172
6194
  "some",
6173
6195
  "forEach",
6174
6196
  "flatMap",
6175
- "flat"
6197
+ "flat",
6176
6198
  // #1448 Tier A — Array methods. Each method PR adds the lowering
6177
6199
  // (typically a new `array-method` variant or runtime helper) and
6178
6200
  // removes its row here. See packages/adapter-tests/fixtures/methods/.
@@ -6202,6 +6224,34 @@ var init_expression_parser = __esm({
6202
6224
  // `bf_lower` / `bf_upper` (Go) and Perl's native `lc` / `uc` (Mojo).
6203
6225
  // `trim` lowers via the `array-method` IR + `bf_trim` (Go) and a
6204
6226
  // Perl regex strip (Mojo).
6227
+ //
6228
+ // #1448 follow-up — String methods that have NO lowering yet. These
6229
+ // were previously absent from this gate, so `isSupported` reported
6230
+ // them "supported" and the adapters emitted a raw method call
6231
+ // (`{{.Name.StartsWith "a"}}` on Go, `$name->{startsWith}('a')` on
6232
+ // Mojo) with no build diagnostic — a silent footgun that only
6233
+ // surfaced as a crash at template-render time. Listing them here
6234
+ // makes the build fail loudly with BF101 (the same treatment the
6235
+ // unsupported array methods above get), pointing users at the
6236
+ // `/* @client */` escape hatch. Each name drops off as its lowering
6237
+ // lands. See #1448 "Unsupported string methods" Tier B / Tier C.
6238
+ "split",
6239
+ "startsWith",
6240
+ "endsWith",
6241
+ "replace",
6242
+ "replaceAll",
6243
+ "repeat",
6244
+ "padStart",
6245
+ "padEnd",
6246
+ "charAt",
6247
+ "charCodeAt",
6248
+ "codePointAt",
6249
+ "normalize",
6250
+ "substring",
6251
+ "substr",
6252
+ "match",
6253
+ "matchAll",
6254
+ "search"
6205
6255
  ]);
6206
6256
  }
6207
6257
  });
@@ -9545,21 +9595,30 @@ function producesDomChild(node) {
9545
9595
  }
9546
9596
  function computeLoopSiblingOffsets(root) {
9547
9597
  const offsets = /* @__PURE__ */ new Map();
9548
- walkIR(root, null, {
9549
- element: ({ node: el, descend }) => {
9550
- let nonLoopCount = 0;
9551
- for (const child of el.children) {
9552
- if (child.type === "loop") {
9553
- if (nonLoopCount > 0) offsets.set(child, nonLoopCount);
9554
- } else if (producesDomChild(child)) {
9555
- nonLoopCount++;
9556
- }
9598
+ const recordChildren = (children) => {
9599
+ let nonLoopCount = 0;
9600
+ for (const child of children) {
9601
+ if (child.type === "loop") {
9602
+ if (nonLoopCount > 0) offsets.set(child, nonLoopCount);
9603
+ } else if (producesDomChild(child)) {
9604
+ nonLoopCount++;
9557
9605
  }
9558
- descend();
9559
9606
  }
9560
- // All container kinds (fragment / component / provider / async / loop /
9561
- // conditional / if-statement) rely on walkIR's default descent with the
9562
- // same scope. Leaves (text / expression / slot) are no-ops.
9607
+ };
9608
+ const containerVisit = ({ node, descend }) => {
9609
+ recordChildren(node.children);
9610
+ descend();
9611
+ };
9612
+ walkIR(root, null, {
9613
+ element: containerVisit,
9614
+ component: containerVisit,
9615
+ fragment: containerVisit,
9616
+ provider: containerVisit,
9617
+ async: containerVisit
9618
+ // `loop` / `conditional` / `if-statement` are not flat sibling
9619
+ // containers (their children are item bodies / branches), and leaves
9620
+ // (text / expression / slot) have no children — all rely on walkIR's
9621
+ // default descent with the same scope.
9563
9622
  });
9564
9623
  return offsets;
9565
9624
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "CLI for agent-driven UI component discovery and scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "typescript": "^5.0.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@barefootjs/jsx": "0.5.1",
34
+ "@barefootjs/jsx": "0.5.2",
35
35
  "@types/node": "^22.0.0"
36
36
  }
37
37
  }