kerfjs 1.0.1 → 2.0.0
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/CHANGELOG.md +32 -0
- package/LICENSE +29 -1
- package/README.md +45 -17
- package/ai/cursorrules +21 -6
- package/ai/manifest.json +5 -5
- package/ai/skill.md +23 -6
- package/dist/array-signal.js +1 -1
- package/dist/chunk-KFUDM3VP.js +131 -0
- package/dist/chunk-KFUDM3VP.js.map +1 -0
- package/dist/{chunk-4E26PO2C.js → chunk-NU7YHYEV.js} +13 -5
- package/dist/chunk-NU7YHYEV.js.map +1 -0
- package/dist/{chunk-QNYOMGI4.js → chunk-RYZHZBHE.js} +74 -17
- package/dist/chunk-RYZHZBHE.js.map +1 -0
- package/dist/html.d.ts +50 -0
- package/dist/html.js +156 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +70 -34
- package/dist/index.js +232 -126
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +96 -55
- package/dist/jsx-runtime.js +2 -2
- package/dist/testing.js +2 -2
- package/llms.txt +6 -5
- package/package.json +7 -2
- package/dist/chunk-4E26PO2C.js.map +0 -1
- package/dist/chunk-7SKIIA5P.js +0 -70
- package/dist/chunk-7SKIIA5P.js.map +0 -1
- package/dist/chunk-QNYOMGI4.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.0.0] - 2026-07-23
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
- `delegateCapture()` now uses `closest()`-style walk-up matching like `delegate()`, passing the matched ancestor to the handler; pass `{ match: 'direct' }` (new `DelegateOptions`) to restore exact-element matching.
|
|
14
|
+
- The dangerous-URL screen (`javascript:`, `vbscript:`, script-executing `data:` URLs) now throws an error in development instead of only warning. Production behavior is unchanged: warn and drop the attribute.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
- New `kerfjs/html` tagged template: author kerf UIs with no build step (CDN / importmap, no JSX transform) with runtime semantics identical to JSX — signal holes become fine-grained bindings, attributes and text are escaped and URL-screened the same way.
|
|
18
|
+
- New no-build example app, **live-poll**, served exactly as authored — an importmap plus one `html`-templated module, with view-source showing the app.
|
|
19
|
+
- Added `<filter>` to the typed JSX intrinsic elements, so SVG filters compile in JSX-authored code.
|
|
20
|
+
- Fully-bound mounts are now a documented, test-pinned guarantee: a render that reads no signal `.value` runs exactly once, forever — every update is a direct per-node write.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
- Controlled form state now survives user interaction: `checked`, `value`, and `selected` DOM properties are synced when the reconciler mutates those attributes, so a clicked checkbox or typed-into input no longer ignores later updates.
|
|
24
|
+
- Fixed stale fine-grained bindings after `arraySignal.update()`: in-place row updates now re-wire bindings whose signal instance changed, instead of leaving effects reading the old row object forever.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
- New runtime dev-mode override: set `globalThis.KERF_DEV = false` (or `true`) to control dev mode without a bundler — CDN/importmap apps are no longer stuck in dev mode in production.
|
|
28
|
+
- Two new opt-in dev warnings: `KERF_DEV_WARN_STALE_BINDING` flags bindings that silently go stale on the byte-equal fast path, and `KERF_DEV_WARN_VALUE_ONLY_RERENDER` flags re-renders whose only changes could have been fine-grained bindings.
|
|
29
|
+
- `defineStore`'s dev-mode `get()` snapshot now returns a deep read-only proxy instead of freezing the live state: nested mutations like `get().nested.x = 1` are caught too, and they throw a descriptive `TypeError` rather than failing silently.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
- Docs repositioned around the "values bind, structure re-renders" idiom as the primary way to render dynamic values, across the overview, reactivity guide, and AI assistant configs.
|
|
33
|
+
- New guide covering the no-build authoring path and example app, plus a documentation-wide accuracy pass (delegate capture semantics, `effect()` cleanup returns, URL-screen behavior, and more).
|
|
34
|
+
|
|
35
|
+
## [1.0.2] - 2026-07-22
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
- Fixed the row-selector demo animation 404ing on the published site — the missing SVG capture is now generated and committed.
|
|
40
|
+
|
|
9
41
|
## [1.0.1] - 2026-07-22
|
|
10
42
|
|
|
11
43
|
|
package/LICENSE
CHANGED
|
@@ -25,7 +25,7 @@ SOFTWARE.
|
|
|
25
25
|
|
|
26
26
|
## Acknowledgements
|
|
27
27
|
|
|
28
|
-
`src/
|
|
28
|
+
`src/morph.ts` re-implements the DOM-reconciliation algorithm of
|
|
29
29
|
[morphdom](https://github.com/patrick-steele-idem/morphdom) by Patrick
|
|
30
30
|
Steele-Idem, which is also distributed under the MIT License:
|
|
31
31
|
|
|
@@ -51,3 +51,31 @@ Steele-Idem, which is also distributed under the MIT License:
|
|
|
51
51
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
52
52
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
53
53
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
`src/attrSelector.ts`'s `cssEscapeIdent` adapts the CSS identifier escaping of
|
|
58
|
+
the [CSS.escape polyfill](https://github.com/mathiasbynens/CSS.escape) by
|
|
59
|
+
Mathias Bynens, which is also distributed under the MIT License:
|
|
60
|
+
|
|
61
|
+
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
|
62
|
+
|
|
63
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
64
|
+
a copy of this software and associated documentation files (the
|
|
65
|
+
"Software"), to deal in the Software without restriction, including
|
|
66
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
67
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
68
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
69
|
+
the following conditions:
|
|
70
|
+
|
|
71
|
+
The above copyright notice and this permission notice shall be
|
|
72
|
+
included in all copies or substantial portions of the Software.
|
|
73
|
+
|
|
74
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
75
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
76
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
77
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
78
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
79
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
80
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
81
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -35,17 +35,17 @@ That's it. Your JSX renders to HTML strings, kerf's native diff applies the mini
|
|
|
35
35
|
|
|
36
36
|
2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it *is* the DOM.
|
|
37
37
|
|
|
38
|
-
3. **
|
|
38
|
+
3. **Values bind, structure re-renders.** Hand a signal *itself* into a JSX hole — `class={selectedId}` or `{status}` — and kerf binds that one node directly: when the signal changes, only that attribute or text node updates, with no render re-run and no list reconcile. A selection flip on a 10,000-row table touches exactly one class. Taken to its logical end: a mount whose render reads no `.value` at all runs **exactly once, forever** — every subsequent update flows through the per-hole bindings. Read `.value` in the render only when the *structure* depends on it (conditionals, list shape).
|
|
39
39
|
|
|
40
40
|
4. **Focus, selection, listeners survive re-renders — even mid-list.** The reconciler morphs instead of rebuilding, so caret position, selection range, IME composition, and delegated listeners survive every re-render. Keyed lists get the same treatment: same-identity rows are updated *in place* rather than recreated, so a row reorder or a single-cell edit no longer blows away focus, scroll, or an in-flight animation the way node replacement does.
|
|
41
41
|
|
|
42
|
-
5. **Safe by default.** Text and attribute values are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` / script-carrying `data:` dropped), inline `on*` handlers are rejected outright, and the same screening covers the fine-grained bound path — so untrusted data stays inert even when kerf is dropped into someone else's page. `raw()` is the explicit, auditable opt-out.
|
|
42
|
+
5. **Safe by default.** Text and attribute values are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` / script-carrying `data:` dropped), inline `on*` handlers are rejected outright, and the same screening covers the fine-grained bound path — so untrusted data stays inert even when kerf is dropped into someone else's page. The URL screen fails loudly at your desk (throws in development) and degrades safely in the field (warns and drops in production). `raw()` is the explicit, auditable opt-out.
|
|
43
43
|
|
|
44
|
-
6. **Small public API.** ~17 exports from the main barrel (plus `arraySignal` on
|
|
44
|
+
6. **Small public API.** ~17 exports from the main barrel (plus `arraySignal` and the `html` tagged template on their own subpaths). No hooks, no lifecycle, no per-instance state. Components are plain functions that return JSX.
|
|
45
45
|
|
|
46
|
-
7. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain.
|
|
46
|
+
7. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain. And with the `html` tagged template (`import { html } from 'kerfjs/html'` — identical runtime semantics to JSX), a CDN / importmap project needs no build step at all.
|
|
47
47
|
|
|
48
|
-
8. **Grown-up tooling around a tiny core.** An [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) that enforces the hard rules at edit time, a `create-kerf-component` scaffold for publishable component packages, drop-in AI-assistant configs, and side-by-side migration guides for a dozen-plus frameworks — none of which grows the core runtime past ~11 KB.
|
|
48
|
+
8. **Grown-up tooling around a tiny core.** An [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) that enforces the hard rules at edit time, an opt-in family of `KERF_DEV_WARN_*` runtime warnings that catch the classic mistakes in development (with zero production cost), a `create-kerf-component` scaffold for publishable component packages, drop-in AI-assistant configs, and side-by-side migration guides for a dozen-plus frameworks — none of which grows the core runtime past ~11 KB.
|
|
49
49
|
|
|
50
50
|
## When to use Kerf
|
|
51
51
|
|
|
@@ -91,16 +91,12 @@ mount(root, () => (
|
|
|
91
91
|
<div>
|
|
92
92
|
<h1>Cart ({cart.state.value.items.length})</h1>
|
|
93
93
|
<ul>
|
|
94
|
-
{each(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</li>
|
|
101
|
-
),
|
|
102
|
-
(item) => item.id,
|
|
103
|
-
)}
|
|
94
|
+
{each(cart.state.value.items, (item) => (
|
|
95
|
+
<li data-key={item.id}>
|
|
96
|
+
{item.name}
|
|
97
|
+
<button data-action="remove" data-id={item.id}>×</button>
|
|
98
|
+
</li>
|
|
99
|
+
))}
|
|
104
100
|
</ul>
|
|
105
101
|
<p>Doubled count: {doubled.value}</p>
|
|
106
102
|
</div>
|
|
@@ -112,6 +108,17 @@ delegate(root, 'click', '[data-action="remove"]', (_e, btn) => {
|
|
|
112
108
|
});
|
|
113
109
|
```
|
|
114
110
|
|
|
111
|
+
The stringly-typed `'[data-action="remove"]'` pair above can be made rename-safe with the `attr()` helper — declare the attribute once and use it on both sides:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { attr } from 'kerfjs';
|
|
115
|
+
|
|
116
|
+
const REMOVE = attr('data-action', 'remove'); // pre-escaped name/value/selector
|
|
117
|
+
|
|
118
|
+
<button {...REMOVE.attrs} data-id={item.id}>×</button>; // in JSX
|
|
119
|
+
delegate(root, 'click', REMOVE.selector, (_e, btn) => { /* … */ }); // in delegation
|
|
120
|
+
```
|
|
121
|
+
|
|
115
122
|
### Fine-grained updates: bind a signal into a hole
|
|
116
123
|
|
|
117
124
|
Inside a `mount()`, hand a signal *itself* (not its `.value`) into an attribute or text position and kerf wires that hole straight to the signal — the render function never re-runs and the list reconciler never walks:
|
|
@@ -128,7 +135,9 @@ mount(root, () => (
|
|
|
128
135
|
status.value = 'saving'; // updates the class + the text node directly — no re-render
|
|
129
136
|
```
|
|
130
137
|
|
|
131
|
-
The headline use is external state driving one spot: a `selectedId` flipping a single row's class inside a 10,000-row `each()` list touches exactly that one node, no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value.
|
|
138
|
+
The headline use is external state driving one spot: a `selectedId` flipping a single row's class inside a 10,000-row `each()` list touches exactly that one node, no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value.
|
|
139
|
+
|
|
140
|
+
This is kerf's guiding idiom — *values bind, structure re-renders*: pass the signal itself wherever a hole is just a value, and read `.value` in the render function only where the JSX structure depends on it. A render that reads no `.value` runs exactly once; from then on every update is a direct write to the node it concerns. See [`docs/2-reactivity.md`](./docs/2-reactivity.md) §2.9.
|
|
132
141
|
|
|
133
142
|
### Long keyed lists: `arraySignal`
|
|
134
143
|
|
|
@@ -164,6 +173,25 @@ morph(liveCard, raw(htmlFromServer)); // SafeHtml
|
|
|
164
173
|
|
|
165
174
|
Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the `<details>` / `<dialog>` user-agent-owned `open` rule all carry over. Use it for SSR-fragment hydration, page-refresh diffs, third-party widget remounts. See [`docs/4-render.md`](./docs/4-render.md) §4.4.3.
|
|
166
175
|
|
|
176
|
+
### No build step at all: the `html` tagged template
|
|
177
|
+
|
|
178
|
+
"No compiler" isn't just a JSX story. The `html` tagged template from `kerfjs/html` has **identical runtime semantics to JSX** — escaping, boolean/nullish attribute rules, URL screening, `on*` rejection, fine-grained signal bindings, `each()` composition — with no transform, so a plain `<script type="module">` on a CDN / importmap page is a complete kerf app:
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<script type="module">
|
|
182
|
+
import { signal, mount, each } from 'https://esm.sh/kerfjs';
|
|
183
|
+
import { html } from 'https://esm.sh/kerfjs/html';
|
|
184
|
+
|
|
185
|
+
const items = signal([{ id: 1, label: 'no build step' }]);
|
|
186
|
+
|
|
187
|
+
mount(document.getElementById('app'), () => html`
|
|
188
|
+
<ul>${each(items.value, (i) => html`<li id="${i.id}">${i.label}</li>`)}</ul>
|
|
189
|
+
`);
|
|
190
|
+
</script>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Attribute names are written verbatim (`class`, not `className`), and holes are only legal in text positions or as a complete attribute value — anything ambiguous throws with an actionable message. Static template parts parse once per call site. See [`docs/6-jsx-runtime.md`](./docs/6-jsx-runtime.md) §6.11 — or the [live-poll example](https://brianwestphal.github.io/kerf/examples/complete/live-poll/), a complete app served exactly as authored: no bundler ever touches it.
|
|
194
|
+
|
|
167
195
|
## Install
|
|
168
196
|
|
|
169
197
|
```bash
|
|
@@ -225,7 +253,7 @@ A *kerf* is the narrow strip of material a saw blade removes when cutting — th
|
|
|
225
253
|
|
|
226
254
|
## Status
|
|
227
255
|
|
|
228
|
-
|
|
256
|
+
Stable — the public API follows semver. See [CHANGELOG.md](./CHANGELOG.md) for the current version and what's shipped.
|
|
229
257
|
|
|
230
258
|
## Sponsor
|
|
231
259
|
|
package/ai/cursorrules
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- kerf-skill-version: 1.
|
|
1
|
+
<!-- kerf-skill-version: 1.8.0 -->
|
|
2
2
|
# kerf.cursorrules — rules for building apps with kerf
|
|
3
3
|
#
|
|
4
4
|
# Drop this file into your project as `.cursorrules` (Cursor will pick it
|
|
@@ -42,12 +42,13 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
42
42
|
| `morph(liveRoot, template)` | one-shot reconcile against an already-populated element (SSR hydration, page-refresh diffs). Template can be `Element`, `SafeHtml`, or HTML string |
|
|
43
43
|
| `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on object identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
|
|
44
44
|
| `delegate(root, type, sel, h)` | one listener at the root, walks `closest(selector)` from target |
|
|
45
|
-
| `delegateCapture(root, type, sel, h)` | capture-phase escape hatch
|
|
45
|
+
| `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
|
|
46
46
|
| `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
|
|
47
47
|
| `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
|
|
48
48
|
| `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
|
|
49
49
|
| `raw(html)` | inject pre-escaped HTML |
|
|
50
50
|
| `arraySignal(initial?)` | granular keyed-list signal at `kerfjs/array-signal` subpath; `each()` reconciles in O(patches) |
|
|
51
|
+
| `` html`…` `` | tagged template at `kerfjs/html` subpath — JSX-identical runtime semantics with NO build step (CDN/importmap projects). Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
|
|
51
52
|
|
|
52
53
|
## Hard rules — get these right on the first try
|
|
53
54
|
|
|
@@ -58,7 +59,7 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
58
59
|
5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
|
|
59
60
|
6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
|
|
60
61
|
7. **No `<MyComponent />` semantics with hooks.** Components are plain functions returning JSX. State lives in module-scope signals or stores, never in component closures.
|
|
61
|
-
8. **
|
|
62
|
+
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX *structure* depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`KERF_DEV_WARN_STALE_BINDING=1` detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders that could have been bindings.
|
|
62
63
|
9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
63
64
|
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root instead.
|
|
64
65
|
11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise (single-row edits, append-to-end). For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
|
|
@@ -73,7 +74,7 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
73
74
|
**Events.**
|
|
74
75
|
- Originates inside the mount tree → `delegate(rootEl, type, sel, handler)`. Originates outside (window-level keyboard, online/offline, beforeunload) → native `window.addEventListener` at module top-level.
|
|
75
76
|
- Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, `el.setPointerCapture(e.pointerId)`. Subsequent `pointermove` / `pointerup` redirect to the captured element and `delegate(rootEl, 'pointermove', '[data-card]', …)` still picks them up. Don't reach for `window.addEventListener` for in-mount-tree gestures.
|
|
76
|
-
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or strict element-match
|
|
77
|
+
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or capture-phase interception → `delegateCapture()` (also `closest()`-matched by default). Need strict element-match? Add `{ match: 'direct' }` on either helper.
|
|
77
78
|
|
|
78
79
|
**Lists.**
|
|
79
80
|
- Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
|
|
@@ -89,18 +90,22 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
89
90
|
**Raw HTML.**
|
|
90
91
|
- User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
|
|
91
92
|
- Author-controlled trusted HTML → `raw(html)` directly.
|
|
93
|
+
- Dangerous URLs (`javascript:`/`vbscript:`/script-executing `data:`) on `href`/`src`/`xlink:href`/`formaction`/`action`/`data` are dropped — kerf THROWS in dev, WARNS + drops in prod. Sanitize user URLs upstream; wrap an intentional trusted one in `raw(url)` to bypass the screen in both modes.
|
|
92
94
|
|
|
93
95
|
## Canonical patterns
|
|
94
96
|
|
|
95
97
|
```tsx
|
|
96
|
-
// Signal + mount
|
|
98
|
+
// Signal + mount. THE core idiom — values bind, structure re-renders:
|
|
99
|
+
// pass the signal ITSELF into a value hole ({count}, class={sig}) so kerf
|
|
100
|
+
// updates that one node directly with no render re-run; read `.value` only
|
|
101
|
+
// when the JSX STRUCTURE depends on the signal (conditionals, list shape).
|
|
97
102
|
const count = signal(0);
|
|
98
103
|
const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
|
|
99
104
|
|
|
100
105
|
mount(document.getElementById('app')!, () => (
|
|
101
106
|
<div>
|
|
102
107
|
<button {...ACTIONS.inc.attrs}>+</button>
|
|
103
|
-
<span>{count
|
|
108
|
+
<span>{count}</span>
|
|
104
109
|
</div>
|
|
105
110
|
));
|
|
106
111
|
delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
|
|
@@ -124,6 +129,15 @@ const cart = defineStore({
|
|
|
124
129
|
// One-shot reconcile against existing DOM (no signals)
|
|
125
130
|
morph(liveCard, '<article class="card">…</article>');
|
|
126
131
|
|
|
132
|
+
// No build step (CDN / importmap): the html tagged template instead of JSX.
|
|
133
|
+
// Same runtime semantics as JSX; write real HTML attribute names; a hole must
|
|
134
|
+
// be a text position or a COMPLETE attribute value (partial values throw).
|
|
135
|
+
import { html } from 'kerfjs/html';
|
|
136
|
+
mount(rootEl, () => html`
|
|
137
|
+
<div class="${cls}">Count: ${count}</div>
|
|
138
|
+
<ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
|
|
139
|
+
`);
|
|
140
|
+
|
|
127
141
|
// Fine-grained binding (opt-in): pass the signal/computed ITSELF into a hole
|
|
128
142
|
// so a change updates ONLY that node (no render re-run, no reconcile). For a
|
|
129
143
|
// hot spot driven by an external signal (selection class) — not everywhere.
|
|
@@ -150,6 +164,7 @@ mount(listEl, () => (
|
|
|
150
164
|
- Drag/drop / state change has no visible effect, only stuff *outside* `each()` updates → you used `each(STATIC_ARRAY, …)` whose row render reads signals. Replace the outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14.
|
|
151
165
|
- Row-enter CSS animation no longer replays when only a row's *content* changed (kerf ≥ 0.15.0) → 0.15.0+ morphs a same-identity, same-position row *in place* instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired; the intentional flip side is that focus, scroll, IME, and in-progress transitions now survive). Key the animation on a state-class toggle, not element creation; to force a remount, churn the row's identity (new object ref / `data-key`).
|
|
152
166
|
- Want a hot spot to update without re-running the whole render → fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there.
|
|
167
|
+
- `` html`` ``: partial attribute values are not supported → in `kerfjs/html` templates a hole must be the COMPLETE attribute value. Replace `class="a ${b}"` with a pre-built string (`` class="${`a ${b}`}" ``) or, for a bound attribute, `class="${computed(() => `a ${b.value}`)}"`.
|
|
153
168
|
|
|
154
169
|
## Server / SSR
|
|
155
170
|
|
package/ai/manifest.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kerfjsVersion": "
|
|
2
|
+
"kerfjsVersion": "2.0.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"name": "skill",
|
|
6
6
|
"source": "kerf.claude-skill.md",
|
|
7
7
|
"bundle": "ai/skill.md",
|
|
8
8
|
"dest": ".claude/skills/kerf-app/SKILL.md",
|
|
9
|
-
"version": "1.
|
|
10
|
-
"sha256": "
|
|
9
|
+
"version": "1.8.0",
|
|
10
|
+
"sha256": "76b8a88bc7aaf9d50e6f3eee7b57a976b3279127f5d4970599aef1f839a06655"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "cursorrules",
|
|
14
14
|
"source": "kerf.cursorrules",
|
|
15
15
|
"bundle": "ai/cursorrules",
|
|
16
16
|
"dest": ".cursorrules",
|
|
17
|
-
"version": "1.
|
|
18
|
-
"sha256": "
|
|
17
|
+
"version": "1.8.0",
|
|
18
|
+
"sha256": "f91402df009941b4198ea659498f458d2433843d47a3b524b0ca5e2d3d2fc949"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
}
|
package/ai/skill.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kerf-app
|
|
3
3
|
description: Build UIs in the kerf reactive framework (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
|
|
4
|
-
kerf-skill-version: 1.
|
|
4
|
+
kerf-skill-version: 1.8.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Building apps with kerf
|
|
@@ -47,12 +47,13 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
47
47
|
| `morph(liveRoot, template)` | one-shot reconcile against a populated element (SSR hydration, page-refresh diffs). Template = `Element`, `SafeHtml`, or HTML string |
|
|
48
48
|
| `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
|
|
49
49
|
| `delegate(root, type, sel, h)` | one listener at the root; `closest(selector)` walk from target |
|
|
50
|
-
| `delegateCapture(root, type, sel, h)` | capture-phase escape hatch; `target.matches()`
|
|
50
|
+
| `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
|
|
51
51
|
| `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
|
|
52
52
|
| `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
|
|
53
53
|
| `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
|
|
54
54
|
| `raw(html)` | inject pre-escaped HTML |
|
|
55
55
|
| `arraySignal(initial?)` | granular keyed-list signal (subpath `kerfjs/array-signal`); `each()` reconciles in O(patches) |
|
|
56
|
+
| `` html`…` `` | tagged template (subpath `kerfjs/html`) — JSX-identical runtime semantics with NO build step, for CDN/importmap projects. Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
|
|
56
57
|
|
|
57
58
|
## Hard rules — every AI assistant gets these wrong at least once
|
|
58
59
|
|
|
@@ -66,7 +67,7 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
66
67
|
5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
|
|
67
68
|
6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
|
|
68
69
|
7. **Components are plain functions.** `<MyComponent props />` works — the JSX runtime calls `MyComponent(props)` and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures.
|
|
69
|
-
8. **
|
|
70
|
+
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX *structure* depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `KERF_DEV_WARN_STALE_BINDING=1`). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders whose only differences were text/attribute values.
|
|
70
71
|
9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
71
72
|
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root.
|
|
72
73
|
11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise. For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
|
|
@@ -81,7 +82,7 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
81
82
|
**Events.**
|
|
82
83
|
- Originates inside the mount tree → `delegate(rootEl, type, sel, handler)`. Originates outside (window-level keyboard, online/offline, beforeunload) → native `window.addEventListener` at module top-level.
|
|
83
84
|
- Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, `el.setPointerCapture(e.pointerId)`. Subsequent `pointermove` / `pointerup` redirect to the captured element and `delegate(rootEl, 'pointermove', '[data-card]', …)` still picks them up. Don't reach for `window.addEventListener` for in-mount-tree gestures.
|
|
84
|
-
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or strict element-match
|
|
85
|
+
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or capture-phase interception → `delegateCapture()` (also `closest()`-matched by default). Need strict element-match? Add `{ match: 'direct' }` on either helper.
|
|
85
86
|
|
|
86
87
|
**Lists.**
|
|
87
88
|
- Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
|
|
@@ -98,17 +99,23 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
98
99
|
- User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
|
|
99
100
|
- Author-controlled trusted HTML → `raw(html)` directly.
|
|
100
101
|
|
|
102
|
+
**Dangerous URLs.** `javascript:`/`vbscript:`/script-executing `data:` values on `href`/`src`/`xlink:href`/`formaction`/`action`/`data` are dropped — kerf **throws in dev**, **warns + drops in prod**. Sanitize user URLs upstream; wrap an intentional trusted one in `raw(url)` (bypasses the screen in both modes).
|
|
103
|
+
|
|
101
104
|
## Canonical patterns
|
|
102
105
|
|
|
103
106
|
```tsx
|
|
104
|
-
// Pattern 1: signal + mount + delegate
|
|
107
|
+
// Pattern 1: signal + mount + delegate.
|
|
108
|
+
// THE core idiom — values bind, structure re-renders: pass the signal ITSELF
|
|
109
|
+
// into a value hole ({count}, class={sig}) so kerf updates that one node
|
|
110
|
+
// directly with no render re-run; read `.value` only when the JSX STRUCTURE
|
|
111
|
+
// depends on the signal (conditionals, list shape).
|
|
105
112
|
const count = signal(0);
|
|
106
113
|
const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
|
|
107
114
|
|
|
108
115
|
mount(document.getElementById('app')!, () => (
|
|
109
116
|
<div>
|
|
110
117
|
<button {...ACTIONS.inc.attrs}>+</button>
|
|
111
|
-
<span>{count
|
|
118
|
+
<span>{count}</span>
|
|
112
119
|
</div>
|
|
113
120
|
));
|
|
114
121
|
delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
|
|
@@ -146,6 +153,15 @@ mount(listEl, () => (
|
|
|
146
153
|
</ul>
|
|
147
154
|
));
|
|
148
155
|
// selectedId.value = 3 → only the ~2 affected <li> class attrs update.
|
|
156
|
+
|
|
157
|
+
// Pattern 6: no build step (CDN / importmap) — the html tagged template
|
|
158
|
+
// instead of JSX. Same runtime semantics; real HTML attribute names; a hole
|
|
159
|
+
// must be a text position or a COMPLETE attribute value (partials throw).
|
|
160
|
+
import { html } from 'kerfjs/html';
|
|
161
|
+
mount(rootEl, () => html`
|
|
162
|
+
<div class="${cls}">Count: ${count}</div>
|
|
163
|
+
<ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
|
|
164
|
+
`);
|
|
149
165
|
```
|
|
150
166
|
|
|
151
167
|
## Diagnosing common errors
|
|
@@ -163,6 +179,7 @@ mount(listEl, () => (
|
|
|
163
179
|
| Drag/drop / state change has no visible effect; only elements *outside* `each()` update | Used `each(STATIC_ARRAY, …)` whose row render reads signals. Items never change identity → cache hits forever → row render never re-invoked → signal reads stop tracking | Replace outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14 |
|
|
164
180
|
| Row-enter CSS animation no longer replays when only a row's *content* changed (kerf ≥ 0.15.0) | 0.15.0+ morphs a same-identity, same-position row *in place* instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired). Intentional flip side: focus, scroll, IME, and in-progress transitions now survive | Key the animation on a state-class toggle, not element creation. To force a remount, churn the row's identity (new object ref / `data-key`) so the reconciler replaces the node |
|
|
165
181
|
| Want a hot spot to update without re-running the whole render | Fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there |
|
|
182
|
+
| `` html`` ``: partial attribute values are not supported | In `kerfjs/html` templates a hole must be the COMPLETE attribute value | Build the full string first (`` class="${`a ${b}`}" ``), or bind `class="${computed(() => `a ${b.value}`)}"` for a reactive one |
|
|
166
183
|
|
|
167
184
|
## Workflow guidance
|
|
168
185
|
|
package/dist/array-signal.js
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { signal, isDevMode } from './chunk-NU7YHYEV.js';
|
|
2
|
+
|
|
3
|
+
// src/dev-store-warn.ts
|
|
4
|
+
var WARNING_PREFIX = "kerf: defineStore.set() called with keys missing from the current state \u2014 ";
|
|
5
|
+
var WARNING_SUFFIX = ". set() REPLACES state; the missing keys will be undefined after this call. Use `set({ ...get(), ...next })` to merge instead, or update each call site to pass the full state. Set KERF_DEV_WARN_NARROW_SET=0 (or unset it) to silence this warning.";
|
|
6
|
+
function isOptedIn() {
|
|
7
|
+
if (!isDevMode()) return false;
|
|
8
|
+
const proc = globalThis.process;
|
|
9
|
+
return proc?.env?.KERF_DEV_WARN_NARROW_SET === "1";
|
|
10
|
+
}
|
|
11
|
+
function isPlainObjectState(v) {
|
|
12
|
+
if (v === null || typeof v !== "object") return false;
|
|
13
|
+
if (Array.isArray(v)) return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
function maybeWarnNarrowSet(prev, next, ctx) {
|
|
17
|
+
if (ctx.warned) return;
|
|
18
|
+
if (!isOptedIn()) return;
|
|
19
|
+
if (!isPlainObjectState(prev) || !isPlainObjectState(next)) return;
|
|
20
|
+
const missing = [];
|
|
21
|
+
for (const k of Object.keys(prev)) {
|
|
22
|
+
if (!(k in next)) missing.push(k);
|
|
23
|
+
}
|
|
24
|
+
if (missing.length === 0) return;
|
|
25
|
+
ctx.warned = true;
|
|
26
|
+
const keysList = missing.map((k) => `\`${k}\``).join(", ");
|
|
27
|
+
console.warn(`${WARNING_PREFIX}${keysList}${WARNING_SUFFIX}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/utils/devReadonly.ts
|
|
31
|
+
var RULE_MESSAGE = "kerf: store state is read-only \u2014 all writes must go through actions (build a new state object and pass it to `set()`). Mutating the object returned by `get()` is a Rule 8 violation and never notifies subscribers.";
|
|
32
|
+
var proxyToRaw = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
var rawToProxy = /* @__PURE__ */ new WeakMap();
|
|
34
|
+
function isWrappable(v) {
|
|
35
|
+
if (v === null || typeof v !== "object") return false;
|
|
36
|
+
if (Array.isArray(v)) return true;
|
|
37
|
+
const proto = Object.getPrototypeOf(v);
|
|
38
|
+
return proto === Object.prototype || proto === null;
|
|
39
|
+
}
|
|
40
|
+
var handler = {
|
|
41
|
+
get(target, prop, receiver) {
|
|
42
|
+
const value = Reflect.get(target, prop, receiver);
|
|
43
|
+
return isWrappable(value) ? devReadonlyProxy(value) : value;
|
|
44
|
+
},
|
|
45
|
+
set() {
|
|
46
|
+
throw new TypeError(RULE_MESSAGE);
|
|
47
|
+
},
|
|
48
|
+
deleteProperty() {
|
|
49
|
+
throw new TypeError(RULE_MESSAGE);
|
|
50
|
+
},
|
|
51
|
+
defineProperty() {
|
|
52
|
+
throw new TypeError(RULE_MESSAGE);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function devReadonlyProxy(obj) {
|
|
56
|
+
if (proxyToRaw.has(obj)) return obj;
|
|
57
|
+
const cached = rawToProxy.get(obj);
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
const p = new Proxy(obj, handler);
|
|
60
|
+
rawToProxy.set(obj, p);
|
|
61
|
+
proxyToRaw.set(p, obj);
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
function toRaw(value) {
|
|
65
|
+
return unwrap(value);
|
|
66
|
+
}
|
|
67
|
+
function unwrap(v) {
|
|
68
|
+
if (v === null || typeof v !== "object") return v;
|
|
69
|
+
const raw = proxyToRaw.get(v);
|
|
70
|
+
if (raw !== void 0) return raw;
|
|
71
|
+
if (!isWrappable(v)) return v;
|
|
72
|
+
if (Array.isArray(v)) {
|
|
73
|
+
let changed2 = false;
|
|
74
|
+
const out2 = v.map((item) => {
|
|
75
|
+
const u = unwrap(item);
|
|
76
|
+
if (u !== item) changed2 = true;
|
|
77
|
+
return u;
|
|
78
|
+
});
|
|
79
|
+
return changed2 ? out2 : v;
|
|
80
|
+
}
|
|
81
|
+
let changed = false;
|
|
82
|
+
const src = v;
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const k of Object.keys(src)) {
|
|
85
|
+
const u = unwrap(src[k]);
|
|
86
|
+
if (u !== src[k]) changed = true;
|
|
87
|
+
out[k] = u;
|
|
88
|
+
}
|
|
89
|
+
return changed ? out : v;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/store.ts
|
|
93
|
+
var REGISTRY = [];
|
|
94
|
+
function defineStore(spec) {
|
|
95
|
+
const internal = signal(spec.initial());
|
|
96
|
+
const warnCtx = { warned: false };
|
|
97
|
+
let devGate;
|
|
98
|
+
const isDev = () => devGate ??= isDevMode();
|
|
99
|
+
const set = (next) => {
|
|
100
|
+
const raw = isDev() ? toRaw(next) : next;
|
|
101
|
+
maybeWarnNarrowSet(internal.value, raw, warnCtx);
|
|
102
|
+
internal.value = raw;
|
|
103
|
+
};
|
|
104
|
+
const get = () => {
|
|
105
|
+
const v = internal.value;
|
|
106
|
+
if (isDev() && v !== null && typeof v === "object") {
|
|
107
|
+
return devReadonlyProxy(v);
|
|
108
|
+
}
|
|
109
|
+
return v;
|
|
110
|
+
};
|
|
111
|
+
const actions = spec.actions(set, get);
|
|
112
|
+
const store = {
|
|
113
|
+
state: internal,
|
|
114
|
+
actions,
|
|
115
|
+
reset() {
|
|
116
|
+
internal.value = spec.initial();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
REGISTRY.push(store);
|
|
120
|
+
return store;
|
|
121
|
+
}
|
|
122
|
+
function resetAllStores() {
|
|
123
|
+
for (const s of REGISTRY) s.reset();
|
|
124
|
+
}
|
|
125
|
+
function clearStoreRegistry() {
|
|
126
|
+
REGISTRY.length = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { clearStoreRegistry, defineStore, resetAllStores };
|
|
130
|
+
//# sourceMappingURL=chunk-KFUDM3VP.js.map
|
|
131
|
+
//# sourceMappingURL=chunk-KFUDM3VP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dev-store-warn.ts","../src/utils/devReadonly.ts","../src/store.ts"],"names":["changed","out"],"mappings":"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","file":"chunk-KFUDM3VP.js","sourcesContent":["/**\n * Dev-mode warning for partial-set violations of Hard Rule 8 (KF-212). When\n * the opt-in env var `KERF_DEV_WARN_NARROW_SET=1` is set in a non-production\n * build, `defineStore`'s `set()` calls `maybeWarnNarrowSet(prev, next, ctx)`\n * before assigning. If `next` is a plain object whose own-keys are a strict\n * subset of `prev`'s own-keys, a one-shot `console.warn` fires naming the\n * missing keys and pointing at the canonical `set({ ...get(), ...next })`\n * merge fix.\n *\n * Why opt-in: narrow-set IS legal — sometimes you want to replace state with\n * a smaller shape (a reset() that drops keys, a feature-flag-driven schema\n * change). The warn is the right shape for the canonical bug (\"I wrote\n * `set({filter})` against a replace-semantics store and wiped items+editingId\")\n * but produces false positives for intentional shape changes. Opt-in keeps\n * the warning available to dev/CI environments that want the diagnostic\n * without surprising existing projects.\n *\n * Trigger condition: ANY key in `prev` missing from `next` — strictly broader\n * than \"fewer keys total.\" A `set({a, c})` against `cur = {a, b}` (same count,\n * different keys) also wipes `b`, so it warns. The original partial-set bug\n * shape was always \"at least one key from current is missing in next\"; the\n * key-count check in the original ticket sketch was an early-exit\n * optimization, not the semantic gate.\n *\n * Skips: non-object cur/next (booleans, numbers, strings — no \"keys\" to\n * miss), null/undefined either side, and arrays either side (shrinking-array\n * replacement is normal, not a partial set).\n *\n * Per-store one-shot dedup: each store warns at most once across its\n * lifetime — matches the KF-174 / KF-176 pattern of \"tell the developer\n * about the rule violation once, then trust them to fix it.\" The dedup\n * scope is the store, not the module, so a second store can still warn\n * if it independently hits the same bug.\n *\n * Production behavior is unchanged for zero runtime cost (the env-var read\n * short-circuits before any per-set work runs).\n */\n\nimport { isDevMode } from './utils/devMode.js';\n\nexport interface NarrowSetWarnContext {\n /** Set once per store; the warner reads/writes this to enforce the per-store one-shot dedup. */\n warned: boolean;\n}\n\nconst WARNING_PREFIX\n = 'kerf: defineStore.set() called with keys missing from the current state — ';\nconst WARNING_SUFFIX\n = '. set() REPLACES state; the missing keys will be undefined after this call. '\n + 'Use `set({ ...get(), ...next })` to merge instead, or update each call site to pass the full state. '\n + 'Set KERF_DEV_WARN_NARROW_SET=0 (or unset it) to silence this warning.';\n\nexport function isOptedIn(): boolean {\n if (!isDevMode()) return false;\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n return proc?.env?.KERF_DEV_WARN_NARROW_SET === '1';\n}\n\nfunction isPlainObjectState(v: unknown): v is Record<string, unknown> {\n if (v === null || typeof v !== 'object') return false;\n if (Array.isArray(v)) return false;\n return true;\n}\n\nexport function maybeWarnNarrowSet(\n prev: unknown,\n next: unknown,\n ctx: NarrowSetWarnContext,\n): void {\n if (ctx.warned) return;\n if (!isOptedIn()) return;\n if (!isPlainObjectState(prev) || !isPlainObjectState(next)) return;\n\n const missing: string[] = [];\n for (const k of Object.keys(prev)) {\n if (!(k in next)) missing.push(k);\n }\n if (missing.length === 0) return;\n\n ctx.warned = true;\n const keysList = missing.map((k) => `\\`${k}\\``).join(', ');\n console.warn(`${WARNING_PREFIX}${keysList}${WARNING_SUFFIX}`);\n}\n\n/**\n * Test helper — resets the per-store `warned` flag on a context so a\n * subsequent test in the same module can re-exercise the first-warning\n * path. Not exported from the public barrel; the unit-test file imports it\n * directly via the relative path.\n */\nexport function _resetWarnContext(ctx: NarrowSetWarnContext): void {\n ctx.warned = false;\n}\n","/**\n * Dev-only deep read-only guard for `defineStore`'s `get()` snapshot.\n *\n * Replaces the older `Object.freeze(get())` guard, which had three problems:\n * it mutated (froze) the LIVE state object as a side effect of a read, so\n * external references that later legitimately mutated it threw in dev but not\n * prod; it was shallow (`get().nested.x = 1` slipped through); and freezing on\n * read is surprising. This module instead wraps the returned reference in a\n * lazy `Proxy` that:\n *\n * - throws a store-rule-specific `TypeError` on any write (`set` /\n * `deleteProperty` / `defineProperty`) — mutating the object returned by\n * `get()` is a Rule 8 violation (all writes go through actions), and now it\n * is a loud throw instead of a silent desync;\n * - lazily wraps plain-object / array property values in the SAME proxy on\n * access (deep coverage, O(1) per access, no clones), so `get().nested.x = 1`\n * also throws;\n * - leaves primitives, functions, and exotic objects (Date, Map, …) as-is, so\n * `instanceof`, `JSON.stringify`, spread, `Object.keys`, and array iteration\n * all behave exactly as on the raw object.\n *\n * The live state object is never frozen or mutated, so an external reference to\n * it stays writable. This guard is DEV-ONLY: production returns the raw\n * reference and never constructs a proxy, so its perf and semantics are\n * byte-identical to a bare object.\n *\n * `toRaw()` reverses the wrapping so a state object DERIVED from `get()` output\n * (e.g. `set({ ...get(), count: 1 })`, whose nested values are proxies handed\n * back by the `get` trap) is stored as a plain object — the internal signal\n * must never hold a Proxy.\n */\n\nconst RULE_MESSAGE\n = 'kerf: store state is read-only — all writes must go through actions '\n + '(build a new state object and pass it to `set()`). Mutating the object '\n + 'returned by `get()` is a Rule 8 violation and never notifies subscribers.';\n\n/** proxy → its raw target. Lets `toRaw()` unwrap a value derived from `get()`. */\nconst proxyToRaw = new WeakMap<object, object>();\n/** raw → its proxy. Stable proxy identity + avoids re-wrapping the same object. */\nconst rawToProxy = new WeakMap<object, object>();\n\n/** Only plain objects and arrays are wrapped; exotic objects pass through untouched. */\nfunction isWrappable(v: unknown): v is object {\n if (v === null || typeof v !== 'object') return false;\n if (Array.isArray(v)) return true;\n const proto = Object.getPrototypeOf(v) as unknown;\n return proto === Object.prototype || proto === null;\n}\n\nconst handler: ProxyHandler<object> = {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver) as unknown;\n return isWrappable(value) ? devReadonlyProxy(value) : value;\n },\n set() {\n throw new TypeError(RULE_MESSAGE);\n },\n deleteProperty() {\n throw new TypeError(RULE_MESSAGE);\n },\n defineProperty() {\n throw new TypeError(RULE_MESSAGE);\n },\n};\n\n/** Wrap `obj` in the dev read-only proxy (idempotent, identity-stable per raw). */\nexport function devReadonlyProxy<T extends object>(obj: T): T {\n if (proxyToRaw.has(obj)) return obj; // already a proxy — don't double-wrap\n const cached = rawToProxy.get(obj);\n if (cached) return cached as T;\n const p = new Proxy(obj, handler) as T;\n rawToProxy.set(obj, p as object);\n proxyToRaw.set(p as object, obj);\n return p;\n}\n\n/**\n * Deep-unwrap any dev read-only proxies out of `value`, preserving structural\n * sharing: returns the SAME reference when nothing was a proxy, and only\n * allocates along the path to a proxy it actually replaces. Raw targets are\n * fully plain (proxies are never stored), so unwrapping one is deep-clean.\n */\nexport function toRaw<T>(value: T): T {\n return unwrap(value) as T;\n}\n\nfunction unwrap(v: unknown): unknown {\n if (v === null || typeof v !== 'object') return v;\n const raw = proxyToRaw.get(v);\n if (raw !== undefined) return raw; // a proxy → its fully-plain raw target\n if (!isWrappable(v)) return v; // exotic object — leave as-is\n\n if (Array.isArray(v)) {\n let changed = false;\n const out = v.map((item) => {\n const u = unwrap(item);\n if (u !== item) changed = true;\n return u;\n });\n return changed ? out : v;\n }\n\n let changed = false;\n const src = v as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(src)) {\n const u = unwrap(src[k]);\n if (u !== src[k]) changed = true;\n out[k] = u;\n }\n return changed ? out : v;\n}\n","/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport { maybeWarnNarrowSet, type NarrowSetWarnContext } from './dev-store-warn.js';\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\nimport { isDevMode } from './utils/devMode.js';\nimport { devReadonlyProxy, toRaw } from './utils/devReadonly.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (set: (next: TState) => void, get: () => Readonly<TState>) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n // KF-212: per-store one-shot dedup for the opt-in narrow-set warning.\n // Default off; consumers opt in via `KERF_DEV_WARN_NARROW_SET=1` in dev.\n // Production behavior is unchanged — `maybeWarnNarrowSet` short-circuits\n // on the env-var read before any per-set work runs.\n const warnCtx: NarrowSetWarnContext = { warned: false };\n\n // The dev gate is resolved once per store, lazily on first `set()`/`get()`\n // (so a runtime `globalThis.KERF_DEV` override set before mount is honored),\n // then cached — the prod hot path stays a bare boolean read, never a\n // per-call env probe.\n let devGate: boolean | undefined;\n const isDev = (): boolean => (devGate ??= isDevMode());\n\n const set = (next: TState): void => {\n // In dev, `next` may carry proxies handed back by the `get()` trap (e.g.\n // `set({ ...get(), count: 1 })`). Unwrap them so the internal signal only\n // ever holds a plain object — the narrow-set warning and every consumer\n // read see raw state, never a Proxy. Prod stores the bare reference.\n const raw = isDev() ? toRaw(next) : next;\n maybeWarnNarrowSet(internal.value, raw, warnCtx);\n internal.value = raw;\n };\n // In dev, wrap the reference returned to actions in a deep read-only Proxy so\n // that `get().count = 42` / `get().nested.x = 1` (documented Rule 8\n // violations) throw a `TypeError` instead of silently landing on the\n // underlying state without notifying subscribers. The live state object is\n // never frozen or mutated, so external references to it stay writable.\n // Production returns the bare reference for zero overhead (no proxy).\n const get = (): Readonly<TState> => {\n const v = internal.value;\n if (isDev() && v !== null && typeof v === 'object') {\n return devReadonlyProxy(v as TState & object);\n }\n return v;\n };\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n"]}
|
|
@@ -3,12 +3,20 @@ export { batch, computed } from '@preact/signals-core';
|
|
|
3
3
|
|
|
4
4
|
// src/reactive.ts
|
|
5
5
|
|
|
6
|
+
// src/utils/devMode.ts
|
|
7
|
+
function isDevMode() {
|
|
8
|
+
const override = globalThis.KERF_DEV;
|
|
9
|
+
if (typeof override === "boolean") return override;
|
|
10
|
+
const proc = globalThis.process;
|
|
11
|
+
return proc?.env?.NODE_ENV !== "production";
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
// src/dev-delegate-warn.ts
|
|
7
15
|
var depth = 0;
|
|
8
16
|
var warned = false;
|
|
9
17
|
function isOptedIn() {
|
|
18
|
+
if (!isDevMode()) return false;
|
|
10
19
|
const proc = globalThis.process;
|
|
11
|
-
if (proc?.env?.NODE_ENV === "production") return false;
|
|
12
20
|
return proc?.env?.KERF_DEV_WARN_DELEGATE_IN_EFFECT === "1";
|
|
13
21
|
}
|
|
14
22
|
function enterEffect() {
|
|
@@ -54,8 +62,8 @@ var DevSignal = class extends Signal {
|
|
|
54
62
|
}
|
|
55
63
|
};
|
|
56
64
|
function isDevWarnUntrackedEnabled() {
|
|
65
|
+
if (!isDevMode()) return false;
|
|
57
66
|
const proc = globalThis.process;
|
|
58
|
-
if (proc?.env?.NODE_ENV === "production") return false;
|
|
59
67
|
return proc?.env?.KERF_DEV_WARN_UNTRACKED_SIGNALS === "1";
|
|
60
68
|
}
|
|
61
69
|
function isSignal(value) {
|
|
@@ -77,6 +85,6 @@ function effect(fn) {
|
|
|
77
85
|
});
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
export { effect, isSignal, signal, warnIfInsideEffect };
|
|
81
|
-
//# sourceMappingURL=chunk-
|
|
82
|
-
//# sourceMappingURL=chunk-
|
|
88
|
+
export { effect, isDevMode, isSignal, signal, warnIfInsideEffect };
|
|
89
|
+
//# sourceMappingURL=chunk-NU7YHYEV.js.map
|
|
90
|
+
//# sourceMappingURL=chunk-NU7YHYEV.js.map
|