@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.
- package/dist/docs/core/llms.txt +1 -0
- package/dist/docs/core/reactivity/batch.md +135 -0
- package/dist/docs/core/reactivity.md +2 -1
- package/dist/index.js +85 -26
- package/package.json +2 -2
package/dist/docs/core/llms.txt
CHANGED
|
@@ -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
|
|
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: `
|
|
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
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
if (
|
|
9553
|
-
|
|
9554
|
-
|
|
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
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
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.
|
|
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.
|
|
34
|
+
"@barefootjs/jsx": "0.5.2",
|
|
35
35
|
"@types/node": "^22.0.0"
|
|
36
36
|
}
|
|
37
37
|
}
|