bitwrench 2.0.32 → 2.1.1
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/README.md +211 -125
- package/dist/bitwrench-bccl.cjs.js +349 -188
- package/dist/bitwrench-bccl.cjs.min.js +2 -39
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +349 -188
- package/dist/bitwrench-bccl.esm.min.js +2 -39
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +349 -188
- package/dist/bitwrench-bccl.umd.min.js +2 -39
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +17 -6
- package/dist/bitwrench-code-edit.cjs.min.js +2 -20
- package/dist/bitwrench-code-edit.es5.js +8 -3
- package/dist/bitwrench-code-edit.es5.min.js +2 -19
- package/dist/bitwrench-code-edit.esm.js +17 -6
- package/dist/bitwrench-code-edit.esm.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.js +17 -6
- package/dist/bitwrench-code-edit.umd.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +2492 -1628
- package/dist/bitwrench-lean.cjs.min.js +2 -80
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +2740 -1838
- package/dist/bitwrench-lean.es5.min.js +2 -49
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +2492 -1628
- package/dist/bitwrench-lean.esm.min.js +2 -80
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +2492 -1628
- package/dist/bitwrench-lean.umd.min.js +2 -80
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-color.cjs.js +251 -0
- package/dist/bitwrench-util-color.cjs.min.js +3 -0
- package/dist/bitwrench-util-color.es5.js +256 -0
- package/dist/bitwrench-util-color.es5.min.js +3 -0
- package/dist/bitwrench-util-color.esm.js +241 -0
- package/dist/bitwrench-util-color.esm.min.js +3 -0
- package/dist/bitwrench-util-color.umd.js +257 -0
- package/dist/bitwrench-util-color.umd.min.js +3 -0
- package/dist/bitwrench-util-css.cjs.js +2 -1
- package/dist/bitwrench-util-css.cjs.min.js +2 -21
- package/dist/bitwrench-util-css.es5.js +2 -1
- package/dist/bitwrench-util-css.es5.min.js +2 -20
- package/dist/bitwrench-util-css.esm.js +2 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -19
- package/dist/bitwrench-util-css.umd.js +2 -1
- package/dist/bitwrench-util-css.umd.min.js +2 -20
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +2826 -1801
- package/dist/bitwrench.cjs.min.js +2 -99
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +403 -479
- package/dist/bitwrench.d.ts +70 -73
- package/dist/bitwrench.es5.js +3106 -2020
- package/dist/bitwrench.es5.min.js +2 -67
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +2826 -1801
- package/dist/bitwrench.esm.min.js +2 -99
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +2826 -1801
- package/dist/bitwrench.umd.min.js +2 -99
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +222 -134
- package/dist/bwserve.cjs.js +289 -282
- package/dist/bwserve.d.ts +157 -0
- package/dist/bwserve.esm.js +290 -283
- package/dist/sri.json +54 -46
- package/docs/README.md +6 -3
- package/docs/app-patterns.md +7 -6
- package/docs/bitwrench-for-wasm.md +53 -54
- package/docs/bitwrench-mcp.md +2 -2
- package/docs/bitwrench-northstar-principles.md +406 -0
- package/docs/bitwrench-taco-schema-discussion.md +2 -2
- package/docs/bitwrench_api.md +191 -106
- package/docs/bitwrench_typescript_usage.md +5 -5
- package/docs/bw-attach.md +29 -75
- package/docs/bwserve.md +200 -168
- package/docs/cli.md +36 -12
- package/docs/component-cheatsheet.md +2 -2
- package/docs/component-library.md +4 -4
- package/docs/component-lifecycle.md +234 -0
- package/docs/drift-lint.md +268 -0
- package/docs/framework-translation-table.md +4 -4
- package/docs/llm-bitwrench-guide.md +60 -50
- package/docs/routing.md +11 -13
- package/docs/state-management.md +110 -109
- package/docs/taco-format.md +13 -14
- package/docs/theming.md +13 -3
- package/docs/thinking-in-bitwrench.md +858 -983
- package/docs/tutorial-bwserve.md +37 -36
- package/docs/tutorial-embedded.md +10 -21
- package/docs/tutorial-website.md +2 -2
- package/package.json +38 -7
- package/readme.html +262 -161
- package/src/bitwrench-bccl-entry.js +2 -2
- package/src/bitwrench-bccl.js +346 -185
- package/src/bitwrench-code-edit.js +16 -5
- package/src/bitwrench-color-utils.js +117 -181
- package/src/bitwrench-file-ops.js +2 -2
- package/src/bitwrench-lean.js +4 -3
- package/src/bitwrench-router.js +5 -2
- package/src/bitwrench-styles.js +420 -504
- package/src/bitwrench-util-color.js +240 -0
- package/src/bitwrench-util-css.js +1 -0
- package/src/bitwrench-utils.js +4 -0
- package/src/bitwrench.d.ts +70 -73
- package/src/bitwrench.h +5 -0
- package/src/bitwrench.js +1939 -933
- package/src/bwserve/attach.js +0 -1
- package/src/bwserve/bwclient.js +172 -32
- package/src/bwserve/bwshell.js +0 -4
- package/src/bwserve/client.js +59 -220
- package/src/bwserve/index.js +59 -26
- package/src/bwserve.d.ts +157 -0
- package/src/bwserve.h +5 -0
- package/src/cli/attach.js +12 -75
- package/src/cli/convert.js +2 -2
- package/src/cli/serve.js +37 -35
- package/src/generate-css.js +1 -1
- package/src/mcp/knowledge.js +4 -4
- package/src/mcp/live.js +21 -13
- package/src/mcp/tools.js +0 -1
- package/src/version.js +3 -7
package/docs/cli.md
CHANGED
|
@@ -189,32 +189,56 @@ bwcli serve -v
|
|
|
189
189
|
Your app sends bwserve protocol messages (JSON) to the input port. All connected browsers update in real time.
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
|
-
# Patch a value:
|
|
192
|
+
# Patch a value (v2.1 wire protocol — ref, discriminated fields, v:1 stamp):
|
|
193
193
|
curl -X POST http://localhost:9000 \
|
|
194
194
|
-H "Content-Type: application/json" \
|
|
195
|
-
-d '{"type":"patch","
|
|
195
|
+
-d '{"v":1,"type":"patch","ref":"temp","text":"23.5 C"}'
|
|
196
196
|
|
|
197
197
|
# Batch update:
|
|
198
198
|
curl -X POST http://localhost:9000 \
|
|
199
|
-
-d '{"type":"batch","ops":[
|
|
200
|
-
{"type":"patch","
|
|
201
|
-
{"type":"patch","
|
|
199
|
+
-d '{"v":1,"type":"batch","ops":[
|
|
200
|
+
{"v":1,"type":"patch","ref":"temp","text":"23.5"},
|
|
201
|
+
{"v":1,"type":"patch","ref":"humidity","text":"67%"}
|
|
202
202
|
]}'
|
|
203
203
|
|
|
204
204
|
# r-prefix relaxed JSON is also accepted (for C/embedded):
|
|
205
|
-
curl -X POST http://localhost:9000 -d "r{'type':'patch','
|
|
205
|
+
curl -X POST http://localhost:9000 -d "r{'v':1,'type':'patch','ref':'temp','text':'23.5'}"
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
### Interactive Commands
|
|
209
|
+
|
|
210
|
+
Messages with a `"command"` field are interactive -- they return a JSON response (not broadcast). Use these to inspect or query connected clients:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# List connected clients
|
|
214
|
+
curl -X POST http://localhost:9000 -d '{"command":"clients"}'
|
|
215
|
+
|
|
216
|
+
# Eval a JS expression in the browser
|
|
217
|
+
curl -X POST http://localhost:9000 -d '{"command":"query","code":"document.title"}'
|
|
218
|
+
|
|
219
|
+
# DOM tree snapshot
|
|
220
|
+
curl -X POST http://localhost:9000 -d '{"command":"tree","selector":"#app","depth":2}'
|
|
221
|
+
|
|
222
|
+
# Take a screenshot (requires --allow-screenshot)
|
|
223
|
+
curl -X POST http://localhost:9000 -d '{"command":"screenshot"}'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
When no `clientId` is specified, the command routes to the most recently connected client.
|
|
227
|
+
|
|
208
228
|
### Options
|
|
209
229
|
|
|
210
230
|
```
|
|
211
|
-
bwcli serve [options]
|
|
231
|
+
bwcli serve [dir] [options]
|
|
212
232
|
|
|
213
233
|
Options:
|
|
214
234
|
-p, --port <number> Web port for browsers (default: 8080)
|
|
215
|
-
|
|
235
|
+
-l, --listen <number> Input port for protocol messages (default: 9000)
|
|
236
|
+
-b, --bind <address> Host/address to bind to (default: 0.0.0.0)
|
|
216
237
|
--stdin Read messages from stdin instead of input port
|
|
217
238
|
-t, --theme <name> Theme preset or hex colors
|
|
239
|
+
--title <string> Page title (default: "bwcli serve")
|
|
240
|
+
--no-dir-list Disable directory listings
|
|
241
|
+
--allow-screenshot Enable client.screenshot() capability
|
|
218
242
|
--open Open browser on start
|
|
219
243
|
-v, --verbose Verbose output (shows all messages)
|
|
220
244
|
-h, --help Print help
|
|
@@ -232,7 +256,7 @@ Both strict JSON and r-prefix relaxed JSON are accepted on the input port. See [
|
|
|
232
256
|
|
|
233
257
|
## The `bwcli attach` subcommand — Remote Debugging REPL
|
|
234
258
|
|
|
235
|
-
`bwcli attach` provides a built-in terminal-based debugger for any bitwrench page. It starts a bwserve instance and waits for a browser to connect via a drop-in `<script>` tag. Once connected, you get an interactive REPL for
|
|
259
|
+
`bwcli attach` provides a built-in terminal-based debugger for any bitwrench page. It starts a bwserve instance and waits for a browser to connect via a drop-in `<script>` tag. Once connected, you get an interactive REPL for inspecting the DOM, mounting components, taking screenshots, and listening to events.
|
|
236
260
|
|
|
237
261
|
### Usage
|
|
238
262
|
|
|
@@ -265,15 +289,13 @@ The drop-in script automatically loads bitwrench if it's not already on the page
|
|
|
265
289
|
Once connected, you get a `bw>` prompt:
|
|
266
290
|
|
|
267
291
|
```
|
|
268
|
-
bw> document.title # Evaluate JS expression
|
|
269
292
|
bw> /tree #app 2 # Show DOM tree
|
|
270
293
|
bw> /screenshot body page.png # Capture screenshot (requires --allow-screenshot)
|
|
271
294
|
bw> /mount #app card {"title":"Hi"} # Mount BCCL component
|
|
272
|
-
bw> /render #app {"t":"h1","c":"Hi"} # Render TACO
|
|
295
|
+
bw> /render #app {"t":"h1","c":"Hi"} # Render TACO at selector
|
|
273
296
|
bw> /patch counter 42 # Update element text
|
|
274
297
|
bw> /listen button click # Watch DOM events
|
|
275
298
|
bw> /unlisten button click # Stop watching
|
|
276
|
-
bw> /exec alert('hello') # Execute JS (fire-and-forget)
|
|
277
299
|
bw> /clients # List connected clients
|
|
278
300
|
bw> /help # Command reference
|
|
279
301
|
bw> /quit # Exit
|
|
@@ -291,6 +313,8 @@ Options:
|
|
|
291
313
|
-h, --help Print help
|
|
292
314
|
```
|
|
293
315
|
|
|
316
|
+
Bare JS evaluation and the `/exec` command were removed in v2.1. Non-slash input is no longer evaluated. Use the slash commands above for all interactions.
|
|
317
|
+
|
|
294
318
|
For the complete guide, see [bwcli attach documentation](bw-attach.md).
|
|
295
319
|
|
|
296
320
|
## Page layout
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Component Cheat Sheet
|
|
2
2
|
|
|
3
3
|
> **Before you write custom TACO for a common UI pattern, check this list.**
|
|
4
|
-
> Bitwrench ships ready-made `make*()` factories. Each returns a
|
|
4
|
+
> Bitwrench ships ready-made `make*()` factories. Each returns a plain TACO object.
|
|
5
5
|
|
|
6
6
|
## Full Component Table
|
|
7
7
|
|
|
@@ -92,7 +92,7 @@ bw.message('#my-carousel', 'goToSlide', 2);
|
|
|
92
92
|
bw.message('.bw_uuid_abc123', 'next');
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
See [State Management --
|
|
95
|
+
See [State Management -- Component Handles](state-management.md#component-handles) for the full handle/slots guide.
|
|
96
96
|
|
|
97
97
|
## How to Use Slots
|
|
98
98
|
|
|
@@ -334,7 +334,7 @@ bw.makeButton({
|
|
|
334
334
|
text: 'Click Me',
|
|
335
335
|
variant: 'primary', // 'primary' | 'secondary' | 'success' | 'danger' |
|
|
336
336
|
// 'warning' | 'info' | 'light' | 'dark' |
|
|
337
|
-
// '
|
|
337
|
+
// 'outline_primary' | 'outline_secondary' | ...
|
|
338
338
|
size: '', // 'sm' or 'lg'
|
|
339
339
|
disabled: false,
|
|
340
340
|
onclick: function() {},
|
|
@@ -1049,7 +1049,7 @@ Build your own with `o.handle` and `o.slots`:
|
|
|
1049
1049
|
}
|
|
1050
1050
|
```
|
|
1051
1051
|
|
|
1052
|
-
See [State Management --
|
|
1052
|
+
See [State Management -- Component Handles](state-management.md#component-handles) for the full guide.
|
|
1053
1053
|
|
|
1054
1054
|
---
|
|
1055
1055
|
|
|
@@ -1068,7 +1068,7 @@ Most components accept a `variant` prop. The available variants are:
|
|
|
1068
1068
|
| `light` | Light background |
|
|
1069
1069
|
| `dark` | Dark background |
|
|
1070
1070
|
|
|
1071
|
-
Buttons also support outline variants: `
|
|
1071
|
+
Buttons also support outline variants: `outline_primary`, `outline_secondary`, etc.
|
|
1072
1072
|
|
|
1073
1073
|
## Composition
|
|
1074
1074
|
|
|
@@ -1078,7 +1078,7 @@ Because every factory returns a TACO object, you compose components with standar
|
|
|
1078
1078
|
// Functions as component factories
|
|
1079
1079
|
function userRow(user) {
|
|
1080
1080
|
return {
|
|
1081
|
-
t: 'div', a: { class: '
|
|
1081
|
+
t: 'div', a: { class: 'bw_bccl_card' }, c: [
|
|
1082
1082
|
bw.makeAvatar({ initials: user.name[0], size: 'sm' }),
|
|
1083
1083
|
{ t: 'span', c: user.name },
|
|
1084
1084
|
bw.makeBadge({ text: user.role, variant: 'info' })
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Component Lifecycle Walkthrough
|
|
2
|
+
|
|
3
|
+
A stats card, taken through every phase of a component's life — define, create,
|
|
4
|
+
mount, update, unmount — using the v2.1 API. If you read one document after the
|
|
5
|
+
README, read this one: it is the shortest complete tour of how bitwrench
|
|
6
|
+
components actually work.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Phase 1: Define
|
|
11
|
+
|
|
12
|
+
A stats card that displays a label and a formatted number.
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
function makeStatsCard(config) {
|
|
16
|
+
return {
|
|
17
|
+
t: 'div', a: { class: 'stats-card' },
|
|
18
|
+
c: [
|
|
19
|
+
{ t: 'h3', a: { class: 'card-title' }, c: config.title || '' },
|
|
20
|
+
{ t: 'span', a: { class: 'card-value' }, c: formatValue(config.value) }
|
|
21
|
+
],
|
|
22
|
+
o: {
|
|
23
|
+
type: 'stats-card',
|
|
24
|
+
state: { value: config.value || 0 },
|
|
25
|
+
slots: { title: '.card-title', value: '.card-value' },
|
|
26
|
+
handle: {
|
|
27
|
+
update: function(el, data) {
|
|
28
|
+
if (data.value !== undefined) {
|
|
29
|
+
el._bw_state.value = data.value;
|
|
30
|
+
el.bw.setValue(formatValue(data.value));
|
|
31
|
+
}
|
|
32
|
+
if (data.title !== undefined) {
|
|
33
|
+
el.bw.setTitle(data.title);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatValue(v) {
|
|
42
|
+
return '$' + Number(v).toLocaleString();
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
What we have: a factory function that returns a TACO. Plain data (minus the
|
|
47
|
+
handle function). The factory captures `config` but does NOT create DOM, wire
|
|
48
|
+
lifecycle, or touch the document. This is just an object.
|
|
49
|
+
|
|
50
|
+
**What goes where:**
|
|
51
|
+
|
|
52
|
+
- `t, a, c` — structure (tag, attributes, content)
|
|
53
|
+
- `o.type` — typed discovery: elements get a `bw_is_component_stats-card` class,
|
|
54
|
+
findable via `querySelectorAll`
|
|
55
|
+
- `o.state` — local mutable state (value), initialized from config
|
|
56
|
+
- `o.slots` — cached DOM targets. Auto-generates `el.bw.setTitle()`,
|
|
57
|
+
`el.bw.getTitle()`, `el.bw.setValue()`, `el.bw.getValue()`
|
|
58
|
+
- `o.handle.update` — the public "give me new data" method. Does formatting
|
|
59
|
+
logic, then delegates to slot setters for the DOM update
|
|
60
|
+
|
|
61
|
+
**Slot vs handle interaction:** slots generate simple setters that replace DOM
|
|
62
|
+
content. The `update` handle method adds logic (formatting) then calls slot
|
|
63
|
+
setters. Both live on `el.bw`. They don't collide because they have different
|
|
64
|
+
names: `setValue` (slot) vs `update` (handle).
|
|
65
|
+
|
|
66
|
+
The handle named `update` is special: `bw.update(ref, data)` dispatches to
|
|
67
|
+
`el.bw.update(data)` automatically. Define an `update` handle and your
|
|
68
|
+
component works with the dispatch API for free.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Phase 2+3: Create + Hydrate
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
var taco = makeStatsCard({ title: 'Revenue', value: 50000 });
|
|
76
|
+
var node = bw.create(taco);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After this call, `node` is a **detached** DOM element:
|
|
80
|
+
|
|
81
|
+
- `node.tagName` === `'DIV'`
|
|
82
|
+
- `node.className` includes `stats-card bw_lc bw_is_component bw_uuid_xxxx`
|
|
83
|
+
- `node._bw_state` === `{ value: 50000 }`
|
|
84
|
+
- `node.bw.update` exists (bound handle method)
|
|
85
|
+
- `node.bw.setValue` / `node.bw.setTitle` exist (slot setters, cached selectors)
|
|
86
|
+
- `node.bw.getValue` / `node.bw.getTitle` exist (slot getters)
|
|
87
|
+
- The h3 contains `Revenue`, the span contains `$50,000`
|
|
88
|
+
- **NOT in the document.** Not registered. `o.mounted` has NOT fired.
|
|
89
|
+
|
|
90
|
+
You rarely call `bw.create()` directly — this is shown for understanding.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Phase 4: Mount
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
var el = bw.mount('#dashboard', taco);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`bw.mount()` unmounts any previous children of `#dashboard`, creates the
|
|
101
|
+
element, appends it, and runs the mount pass. It returns the root element.
|
|
102
|
+
(`bw.DOM()` is the same function under its other name.)
|
|
103
|
+
|
|
104
|
+
After mount:
|
|
105
|
+
|
|
106
|
+
- Element is in the document inside `#dashboard`
|
|
107
|
+
- Its UUID is registered — findable via `bw.el('bw_uuid_xxxx')`
|
|
108
|
+
- `o.mounted` would have fired if this component had one (it doesn't)
|
|
109
|
+
- Element is alive: `el.bw.update({ value: 75000 })` works
|
|
110
|
+
|
|
111
|
+
**Try it in devtools:** select the card in the Elements panel and type
|
|
112
|
+
`$0._bw_state` — the component's state, no browser extension required.
|
|
113
|
+
`$0.bw` shows its public methods.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Phase 5: Update
|
|
118
|
+
|
|
119
|
+
Five ways, ordered by cost and coupling:
|
|
120
|
+
|
|
121
|
+
### Direct handle method call (cheapest)
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
el.bw.update({ value: 75000 });
|
|
125
|
+
// card shows '$75,000'; el._bw_state.value === 75000
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Slot setter (bypasses the handle's logic)
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
el.bw.setValue('SOLD OUT');
|
|
132
|
+
// card shows 'SOLD OUT' (raw, no formatting)
|
|
133
|
+
// el._bw_state.value is NOT updated — slot setters don't touch state
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This is intentional. Slot setters are low-level; handle methods are the public
|
|
137
|
+
API. If you call `setValue()` directly, keeping state consistent is on you.
|
|
138
|
+
|
|
139
|
+
### Dispatch via bw.update
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
bw.update(el, { value: 100000, title: 'Profit' });
|
|
143
|
+
// dispatches to el.bw.update({ ... })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Works identically by UUID, without holding the element reference:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
var uuid = bw.getUUID(el);
|
|
150
|
+
bw.update(uuid, { value: 100000 });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Dispatch via bw.message (explicit method name)
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
bw.message(el, 'update', { value: 200000 });
|
|
157
|
+
// same as el.bw.update({ value: 200000 })
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Pub/sub (decoupled)
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
bw.sub('revenue:change', function(data) {
|
|
164
|
+
el.bw.update(data);
|
|
165
|
+
}, el);
|
|
166
|
+
|
|
167
|
+
// elsewhere:
|
|
168
|
+
bw.pub('revenue:change', { value: 999999 });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The third argument to `bw.sub()` ties the subscription to the element — when
|
|
172
|
+
the element is unmounted, the subscription is removed automatically.
|
|
173
|
+
|
|
174
|
+
Note what this component never needs: `bw.refresh()`. It has no `o.render`,
|
|
175
|
+
because every update it supports is surgical. Reach for `o.render` +
|
|
176
|
+
`bw.refresh(el)` when the component's *structure* changes with state, not just
|
|
177
|
+
its values.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Phase 6: Unmount
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
bw.remove(el);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This unmounts and removes the element. The unmount pass:
|
|
188
|
+
|
|
189
|
+
- Fires `o.unmount` if it existed (ours doesn't)
|
|
190
|
+
- Unregisters the UUID
|
|
191
|
+
- Unsubscribes all pub/sub subscriptions tied to this element
|
|
192
|
+
- Deletes `_bw_state`, `_bw_render`, `_bw_type`, and `el.bw`
|
|
193
|
+
- Removes the element from the DOM
|
|
194
|
+
|
|
195
|
+
After this, `el` is an inert DOM node with no bitwrench properties.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## The factory as reusable template
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
var revenue = bw.mount('#stats', makeStatsCard({ title: 'Revenue', value: 50000 }));
|
|
203
|
+
var users = bw.mount('#stats', makeStatsCard({ title: 'Users', value: 1234 }));
|
|
204
|
+
var orders = bw.mount('#stats', makeStatsCard({ title: 'Orders', value: 89 }));
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Three independent instances. Each has its own state, its own UUID, its own slot
|
|
208
|
+
targets. The handle function is shared by reference, but `el` (the first
|
|
209
|
+
argument) is bound per instance. State is per instance (`el._bw_state`).
|
|
210
|
+
|
|
211
|
+
To update all three at once, publish:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
bw.pub('dashboard:refresh', newData);
|
|
215
|
+
// each card subscribed to this topic updates independently
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## The design in one paragraph
|
|
221
|
+
|
|
222
|
+
The TACO was consumed at mount time — bitwrench does not keep a copy of it.
|
|
223
|
+
Everything the component is now lives on the DOM element itself: its state
|
|
224
|
+
(`el._bw_state`), its public API (`el.bw`), its identity (`bw_uuid_*` class).
|
|
225
|
+
There is no shadow tree to reconcile and no framework instance to look up.
|
|
226
|
+
That's why updates are explicit and cheap — you (or the component itself) talk
|
|
227
|
+
directly to the element — and why any component can be inspected, driven, or
|
|
228
|
+
debugged from the browser console with no tooling.
|
|
229
|
+
|
|
230
|
+
## See also
|
|
231
|
+
|
|
232
|
+
- [State Management](state-management.md) — the full component model, `o.render` + `bw.refresh()`
|
|
233
|
+
- [Thinking in Bitwrench §7](thinking-in-bitwrench.md) — lifecycle options and the update cost spectrum
|
|
234
|
+
- [Component Library](component-library.md) — the built-in BCCL factories, which follow this exact pattern
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# drift-lint -- keeping the docs honest
|
|
2
|
+
|
|
3
|
+
`tools/drift-lint.js` is a repo-specific consistency checker. Where ESLint asks
|
|
4
|
+
"is this code well-formed?", drift-lint asks **"do the docs still tell the truth?"**
|
|
5
|
+
|
|
6
|
+
It exists because documentation rots silently. Code that calls a removed function
|
|
7
|
+
fails a test; a doc that *teaches* a removed function fails a user, months later,
|
|
8
|
+
with no error pointing back here. The v2.1 alignment release cleaned up exactly
|
|
9
|
+
this kind of rot -- drift-lint is what makes that cleanup permanent instead of a
|
|
10
|
+
one-time event. Every API removal, rename, or terminology ruling gets encoded as
|
|
11
|
+
a rule, and from then on the stale form physically cannot reach a release.
|
|
12
|
+
|
|
13
|
+
It also extracts the real public API from `src/` at runtime and cross-references
|
|
14
|
+
every `bw.XXX()` call in scanned files against it. If a doc references a function
|
|
15
|
+
that doesn't exist in the source, drift-lint catches it -- no manual rule needed.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm run lint:drift # run all rules
|
|
21
|
+
node tools/drift-lint.js # same thing, direct
|
|
22
|
+
node tools/drift-lint.js --verbose # show honored ignore blocks and config excludes
|
|
23
|
+
node tools/drift-lint.js --list-rules # print all rules with descriptions
|
|
24
|
+
node tools/drift-lint.js --help # CLI help
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## When it runs
|
|
28
|
+
|
|
29
|
+
| Trigger | Why there |
|
|
30
|
+
|---------|-----------|
|
|
31
|
+
| `npm run lint:drift` | Direct invocation |
|
|
32
|
+
| `posttest` (after every `npm test`) | Cheapest possible feedback loop |
|
|
33
|
+
| `postbuild` (after every `npm run build`) | Generated artifacts (e.g. `readme.html`) are scanned fresh |
|
|
34
|
+
| `tools/release.js` step 4 | A stale doc cannot ship |
|
|
35
|
+
|
|
36
|
+
It scans ~120 files in well under a second. No browsers, no network, no flake --
|
|
37
|
+
it is always safe to make blocking.
|
|
38
|
+
|
|
39
|
+
## What it scans
|
|
40
|
+
|
|
41
|
+
- **Directories (recursive):** `docs/`, `pages/`, `examples/`, `embedded_python/`
|
|
42
|
+
- **Root files:** `README.md`, `CONTRIBUTING.md`, `ABOUT.md`, `readme.html`, `llms.txt`, `agents.md`
|
|
43
|
+
- **Extensions:** `.md`, `.html`, `.js`, `.py`, `.sh`, `.ts`, `.txt`
|
|
44
|
+
- **Skipped:** `node_modules`, `dist`, `coverage`, `.git`, and `dev/` (internal
|
|
45
|
+
notes are allowed to discuss stale patterns — the SUPERSEDED archives live there)
|
|
46
|
+
|
|
47
|
+
`src/` is deliberately out of scope: working code is ESLint's jurisdiction, and
|
|
48
|
+
several rules (like the wire-format field renames) would false-positive against
|
|
49
|
+
the implementation that has to handle both forms.
|
|
50
|
+
|
|
51
|
+
`readme.html` is scanned even though it is generated: if someone fixes
|
|
52
|
+
`README.md` but forgets `npm run build:readme`, the mirror goes stale — which is
|
|
53
|
+
itself drift, and the postbuild hook catches it.
|
|
54
|
+
|
|
55
|
+
## Rule kinds
|
|
56
|
+
|
|
57
|
+
### Name rules
|
|
58
|
+
|
|
59
|
+
A token that should no longer appear. The simplest and most common kind:
|
|
60
|
+
|
|
61
|
+
<!-- drift-lint:ignore-start: documenting the rule format requires showing banned tokens -->
|
|
62
|
+
```javascript
|
|
63
|
+
{
|
|
64
|
+
id: 'toggleStyles',
|
|
65
|
+
pattern: /toggleStyles/g,
|
|
66
|
+
message: 'bw.toggleStyles() → bw.toggleThemeMode()',
|
|
67
|
+
contextExclude: [
|
|
68
|
+
/removed|was removed|renamed|no longer|SUPERSEDED/i
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
<!-- drift-lint:ignore-end -->
|
|
73
|
+
|
|
74
|
+
Fields:
|
|
75
|
+
|
|
76
|
+
| Field | Meaning |
|
|
77
|
+
|-------|---------|
|
|
78
|
+
| `id` | Short name, shown in the failure report |
|
|
79
|
+
| `pattern` | Regex tested line by line |
|
|
80
|
+
| `message` | What to do instead — write it as `old → new` |
|
|
81
|
+
| `contextExclude` | Regexes that exempt a matching line (e.g. a removal note legitimately names the removed API) |
|
|
82
|
+
| `fileFilter` | Only scan matching paths (e.g. `/\.md$/`) |
|
|
83
|
+
| `fileExclude` | Skip matching paths entirely |
|
|
84
|
+
|
|
85
|
+
### Structural rules
|
|
86
|
+
|
|
87
|
+
An anti-pattern *shape* rather than a banned name — every individual token is
|
|
88
|
+
fine, the combination is wrong. Structural rules add two fields: `followedBy`
|
|
89
|
+
(evidence regex) and `within` (line window). The rule fires when the anchor
|
|
90
|
+
matches and the evidence appears within N lines:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
{
|
|
94
|
+
id: 'mounted-event-wiring',
|
|
95
|
+
pattern: /\bmounted\s*:/, // anchor
|
|
96
|
+
followedBy: /\.addEventListener\(\s*['"](?:click|input|...)['"]/, // evidence
|
|
97
|
+
within: 3,
|
|
98
|
+
message: 'event handler wired in o.mounted is lost on bw.refresh() — use a: { onclick: fn }',
|
|
99
|
+
fileFilter: /\.(html|js)$/,
|
|
100
|
+
contextExclude: [
|
|
101
|
+
/['"`].*addEventListener.*['"`]/, // quoted demo code in comparison pages
|
|
102
|
+
/window\.addEventListener|document\.addEventListener/ // page-level listeners are fine
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For structural rules, `contextExclude` is applied to the **evidence line** —
|
|
108
|
+
that's where false-positive context lives (a quoted code string, a
|
|
109
|
+
window-level listener).
|
|
110
|
+
|
|
111
|
+
This rule enforces the #1 documented mistake in the lifecycle model: handlers
|
|
112
|
+
attached via `addEventListener` inside `o.mounted` are silently lost when the
|
|
113
|
+
component re-renders via `bw.refresh()`. Handlers belong in `a: { onclick: fn }`,
|
|
114
|
+
which bitwrench re-attaches on every render.
|
|
115
|
+
|
|
116
|
+
### API rules
|
|
117
|
+
|
|
118
|
+
The stale-api rule is different from name and structural rules: it has no
|
|
119
|
+
hand-written pattern. Instead, drift-lint extracts the real public API from
|
|
120
|
+
`src/` at startup and checks every `bw.XXX()` call in scanned files against it.
|
|
121
|
+
|
|
122
|
+
What it extracts:
|
|
123
|
+
- `bw.XXX = function` assignments from all `src/bitwrench*.js` files
|
|
124
|
+
- Object-literal properties on the `bw` object (e.g. `getVersion`, `version`)
|
|
125
|
+
- `export function XXX` from `bitwrench-bccl.js` (become `bw.makeXxx`)
|
|
126
|
+
- `export function XXX` from `bitwrench-color-utils.js` (become `bw.XXX`)
|
|
127
|
+
|
|
128
|
+
What it skips:
|
|
129
|
+
- `el.bw.XXX()` -- handle/slot method calls on component instances
|
|
130
|
+
- `bw._xxx()` -- private/internal names
|
|
131
|
+
- Lines mentioning "removed", "deprecated", "renamed", etc.
|
|
132
|
+
- Generic naming patterns like `bw.makeXxx()` (convention, not a literal call)
|
|
133
|
+
|
|
134
|
+
This rule requires no maintenance. When you add, rename, or remove a public
|
|
135
|
+
function in source, the rule automatically picks up the change. If a doc still
|
|
136
|
+
references the old name, drift-lint flags it on the next run.
|
|
137
|
+
|
|
138
|
+
Run `node tools/drift-lint.js --list-rules` to see the current API count.
|
|
139
|
+
|
|
140
|
+
## The ignore pragma
|
|
141
|
+
|
|
142
|
+
Sometimes a scanned file must legitimately contain a banned pattern — this
|
|
143
|
+
document is the canonical example. Others: documenting how old versions worked,
|
|
144
|
+
counter-examples shown for discussion, quoting other frameworks' APIs. Wrap the
|
|
145
|
+
block and **give a reason**:
|
|
146
|
+
|
|
147
|
+
```markdown
|
|
148
|
+
<!-- drift-lint:ignore-start: comparison table quotes the old 2.0 API on purpose -->
|
|
149
|
+
...exempt content...
|
|
150
|
+
<!-- drift-lint:ignore-end -->
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Any comment style works — the scanner matches the pragma text itself, so `//`,
|
|
154
|
+
`#`, `/* */`, and `<!-- -->` are all fine:
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
// drift-lint:ignore-start: demo intentionally shows the WRONG pattern for teaching
|
|
158
|
+
...
|
|
159
|
+
// drift-lint:ignore-end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For a single line, use `ignore-next-line` instead of a block:
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
// drift-lint:ignore-next-line: comparing bitwrench to reactive frameworks
|
|
166
|
+
const comparison = "Unlike reactive frameworks, bitwrench uses explicit updates";
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Guard rails, so exemptions can't silently swallow more than intended:
|
|
170
|
+
|
|
171
|
+
- `ignore-start` without a reason → **warning** (the run still passes — a
|
|
172
|
+
hotfix shouldn't be blocked on prose — but the nag prints on every run
|
|
173
|
+
until a reason is added. Pragmas without context become archaeology:
|
|
174
|
+
months later nobody knows what the exemption was protecting.)
|
|
175
|
+
- `ignore-start` never closed → **error** (prevents accidentally exempting the rest of a file)
|
|
176
|
+
- `ignore-end` without a start → **error**
|
|
177
|
+
- nested `ignore-start` → **error**
|
|
178
|
+
- `node tools/drift-lint.js --verbose` lists every honored block with its
|
|
179
|
+
file, line range, and reason — audit them occasionally; each one is debt
|
|
180
|
+
- the summary line always shows the honored-block count
|
|
181
|
+
|
|
182
|
+
Prefer `contextExclude` on the rule over pragmas in files: an exclusion encodes
|
|
183
|
+
*why* a context is acceptable once, centrally; pragmas scatter exemptions
|
|
184
|
+
through the tree. Use the pragma when a single file has a unique, legitimate
|
|
185
|
+
need the rule shouldn't generalize.
|
|
186
|
+
|
|
187
|
+
## Config file (.drift-lint-config.json)
|
|
188
|
+
|
|
189
|
+
<!-- drift-lint:ignore-start: documenting the config feature requires showing rule IDs and example patterns -->
|
|
190
|
+
For files that structurally and permanently contain a banned pattern (e.g. a
|
|
191
|
+
design philosophy doc that will always discuss reactive systems comparatively),
|
|
192
|
+
use the config file instead of scattering pragmas:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"exclude": {
|
|
197
|
+
"reactive-self": ["docs/bitwrench-northstar-principles.md"],
|
|
198
|
+
"*": ["docs/legacy-migration-guide.md"]
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
<!-- drift-lint:ignore-end -->
|
|
203
|
+
|
|
204
|
+
- Keys are rule IDs (from the `id` field in RULES). `"*"` excludes from all rules.
|
|
205
|
+
- Values are arrays of relative paths (from repo root, forward slashes).
|
|
206
|
+
- `--verbose` reports which config excludes are active.
|
|
207
|
+
|
|
208
|
+
Use config excludes when a file's relationship to a rule is structural — the
|
|
209
|
+
file will always legitimately contain the pattern, and re-wording to avoid it
|
|
210
|
+
would make the document worse. Use pragmas for isolated one-off exemptions
|
|
211
|
+
within an otherwise-scanned file.
|
|
212
|
+
|
|
213
|
+
## Adding a rule
|
|
214
|
+
|
|
215
|
+
When you remove, rename, or re-decide something user-facing:
|
|
216
|
+
|
|
217
|
+
1. Add the rule to `RULES` in `tools/drift-lint.js` in the same commit as the
|
|
218
|
+
change. The rule *is* the enforcement half of the decision.
|
|
219
|
+
2. Write the message as the migration: `old → new`, not just "don't".
|
|
220
|
+
3. Run `npm run lint:drift` — it will list every place the old form survives.
|
|
221
|
+
Fix them all in the same commit if feasible.
|
|
222
|
+
4. Add `contextExclude` entries only for patterns of legitimate use you actually
|
|
223
|
+
observed, not hypothetical ones.
|
|
224
|
+
5. If a doc must keep the old form (removal notes, migration guides,
|
|
225
|
+
comparison tables), prefer a `contextExclude`; reach for the pragma only
|
|
226
|
+
for one-off cases.
|
|
227
|
+
|
|
228
|
+
## Current rule inventory
|
|
229
|
+
|
|
230
|
+
<!-- drift-lint:ignore-start: the inventory table must name the banned patterns it documents -->
|
|
231
|
+
|
|
232
|
+
| Rule | Catches | Correct form |
|
|
233
|
+
|------|---------|--------------|
|
|
234
|
+
| `client.render` / `client.exec` / `client.query` / `client.register` | 2.0 bwserve client APIs | `client.mount()`; exec/query/register removed |
|
|
235
|
+
| `bw.createDOM` | 2.0 name | `bw.create()` |
|
|
236
|
+
| `bw.cleanup` | 2.0 name | `bw.unmount()` |
|
|
237
|
+
| `bw.component` | removed API | TACO `o:` options |
|
|
238
|
+
| `data-bw-action` | 2.0 action attribute | `bw_act_*` CSS class |
|
|
239
|
+
| `allowExec` / `--allow-exec` / `/exec` | removed exec surface | removed in v2.1 |
|
|
240
|
+
| `wire:target` / `wire:node` | 2.0 wire field names | `"ref"` / `"taco"` |
|
|
241
|
+
| `parseRJSON` | 2.0 name | `bw.parseJSONFlex()` |
|
|
242
|
+
| `levels-taxonomy` | "Level 0/1/2" component taxonomy | descriptive stage names |
|
|
243
|
+
| `toggleStyles` | removed API | `bw.toggleThemeMode()` |
|
|
244
|
+
| `bw_card-bare` / `bw_btn-bare` | pre-rename BCCL class names in docs | `bw_bccl_card` / `bw_bccl_btn` (the bare forms remain valid as stylesheet classes) |
|
|
245
|
+
| `bw-container` | hyphen class form | `bw_container` (underscore canonical) |
|
|
246
|
+
| `normalizeClass` | never-shipped API | remove reference |
|
|
247
|
+
| `three-level` | stale component-model count | describe stages, don't count |
|
|
248
|
+
| `outline-hyphen` | hyphen variant spelling | `outline_primary` etc. |
|
|
249
|
+
| `reactive-self` | "reactive" as self-description | "explicit stateful" / "state + explicit re-render" (comparative uses are excluded) |
|
|
250
|
+
| `getHandle` | removed API | handles live on `el.bw` directly |
|
|
251
|
+
| `update-as-rerender` | prose claiming `bw.update()` re-renders | `bw.update()` dispatches; re-render is `bw.refresh()` |
|
|
252
|
+
| `mounted-event-wiring` | structural: DOM event handlers wired in `o.mounted` | `a: { onclick: fn }` |
|
|
253
|
+
| `stale-api` | `bw.XXX()` in docs where XXX is not in the public API | auto-detected from `src/` -- no manual rule needed |
|
|
254
|
+
|
|
255
|
+
<!-- drift-lint:ignore-end -->
|
|
256
|
+
|
|
257
|
+
## Design constraints
|
|
258
|
+
|
|
259
|
+
- **Zero dependencies, zero flake.** Plain-string scanning only. If a rule
|
|
260
|
+
needs an AST, it belongs in ESLint; if it needs a browser, it belongs in
|
|
261
|
+
Playwright. Drift-lint stays fast enough that nobody is ever tempted to
|
|
262
|
+
skip it.
|
|
263
|
+
- **Every rule earns its place by having fired at least once.** Rules encode
|
|
264
|
+
real drift that actually happened (or a decision actually made), not
|
|
265
|
+
hypothetical hygiene.
|
|
266
|
+
- **False positives are rule bugs.** If a legitimate line trips a rule, fix
|
|
267
|
+
the rule (`contextExclude`), don't pragma around it — the next legitimate
|
|
268
|
+
use will trip it again.
|