kerfjs 0.12.1 → 0.13.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 +12 -0
- package/ai/cursorrules +13 -12
- package/ai/manifest.json +5 -5
- package/ai/skill.md +13 -12
- package/dist/array-signal.js +1 -1
- package/dist/{chunk-IBVKW6WU.js → chunk-4TJEO4AO.js} +3 -3
- package/dist/{chunk-IBVKW6WU.js.map → chunk-4TJEO4AO.js.map} +1 -1
- package/dist/chunk-N4KF3GD2.js +79 -0
- package/dist/chunk-N4KF3GD2.js.map +1 -0
- package/dist/index.d.ts +14 -6
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/testing.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-UU2YJEJY.js +0 -41
- package/dist/chunk-UU2YJEJY.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
- KF-238 — New opt-in dev-warn `KERF_DEV_WARN_DELEGATE_IN_EFFECT=1` fires once per process when `delegate()` or `delegateCapture()` is called inside an `effect()` body. Every effect re-run executes its body fresh, so a `delegate()` call inside the body installs a NEW root listener on each re-run; the effect's disposer cleans up the reactive subscription but not the side-effects the body produced, so listener count grows linearly with signal churn and each listener pins its handler closure. Implementation: `reactive.ts`'s `effect()` factory now wraps the user body in `enterEffect()` / `exitEffect()` calls (in a `try`/`finally` so a throwing body still decrements the counter) when the env var is set; `delegate.ts` calls `warnIfInsideEffect()` at the top of both helpers. Production (`NODE_ENV=production`) short-circuits before the wrap, so the bare `coreEffect` re-export stays the default path. New module `src/dev-delegate-warn.ts`; new test `tests/unit/dev-delegate-warn.internal.test.ts` (9 cases). Pairs with KF-237's docs gotchas section (§5.3 "When capturing the disposer still isn't enough" — scenario 2).
|
|
11
|
+
- KF-237 — `docs/5-event-delegation.md` §5.3 gains a "When capturing the disposer still isn't enough" subsection covering five scenarios where capturing the disposer is necessary but not sufficient: `delegate()` rooted on a node inside a morph-managed tree (root at the outer `mount()` instead), `delegate()` called inside `effect()` (per-rerun listener stack — see KF-238 dev warn), `delegate()` on `toElement()` output that's later `replaceChildren()`-ed, disposer variables overwritten by reassignment, and nested-root confusion where the stable parent fools an AI into thinking a transient child is page-lifetime. Each scenario has wrong / right code pairs. Cross-linked from `docs/8-api-reference.md`'s `delegate()` entry, Hard Rule 5 in `docs/ai/usage-guide.md`, and two new rows in the common-errors table.
|
|
12
|
+
- KF-236 — The `chat`, `todomvc`, `kanban`, and `markdown-editor` example apps now prefix their bare `delegate()` / `delegateCapture()` calls with the `void` opt-out sigil and carry an inline comment explaining the page-lifetime intent (matching the `counter-store` pattern shipped in KF-234). Downstream consumers who copy these examples and enable `kerfjs.configs.recommended` no longer see warnings from `kerfjs/require-delegate-disposer` on the canonical source.
|
|
13
|
+
- KF-235 — `eslint-plugin-kerfjs` adds `kerfjs/require-delegate-disposer` (`warn` in recommended; plugin bumped to v0.13.0) — flags `delegate(...)` / `delegateCapture(...)` calls whose `() => void` return value is discarded (parent is an `ExpressionStatement`). The listener closure pins `rootEl`, `handler`, and everything the handler closes over, so an undisposed delegate on a transient root (modal, route view, mount swap, dynamic widget) leaks both the listener and the app graph it references; re-mount cycles stack listeners linearly. Accepts assignments, returns, array/object literals, argument positions, and `void` as an explicit-discard sigil; standard `eslint-disable-next-line` works for one-off page-lifetime exceptions. Severity `warn` to give downstream code an audit window — will promote to `error` after one or two releases. Pairs with KF-234's docs rewrite.
|
|
14
|
+
- KF-234 — Delegate-disposer guidance rewritten (`docs/5-event-delegation.md` §5.3, mirrored in `docs/8-api-reference.md`). The prior wording said discarding the disposer is "usually fine" — that's only true for genuinely page-lifetime registrations (root is `document.body`, attached once at startup, never torn down), and was dangerously presumptuous everywhere else. New rule: **capture the disposer when the delegate's scope is shorter than the page** (modals, route views, mount swaps, dynamic widgets). The listener closure pins `rootEl`, `handler`, and everything the handler closes over, so an undisposed delegate on a transient root leaks the listener AND the app graph it references; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. The `cart-htmx` example gained an inline annotation flagging its transient-root pattern; the `counter-store` example gained an inline annotation flagging its page-lifetime exception. The AI configs (`docs/ai/usage-guide.md`, `kerf.cursorrules`, `kerf.claude-skill.md`) pick up a new Hard Rule 5; `kerf-skill-version` bumped 1.1.1 → 1.2.0 (`ai/` bundle regenerated). See KF-235 for the upcoming `require-delegate-disposer` eslint rule that will mechanically enforce this.
|
|
10
15
|
- KF-232 — `toElement()` no longer throws on multi-root inputs (`<><svg/> label</>`, two icons side by side, `text<svg/>`) and no longer silently drops sibling content. The return type widens to `Element | DocumentFragment`: single-root inputs still return an `Element` (XML-validated for `<svg>` roots, namespace-fixed for orphan SVG fragments); multi-root inputs return a `DocumentFragment` containing every top-level node — text and elements alike. Callers using the result with `appendChild` / `replaceChildren` / `append` keep working without changes (those APIs splat a `DocumentFragment`'s children into the parent and empty the fragment), so `parent.replaceChildren(toElement(<>{ICON} label</>))` now does the obvious thing — parent gets the SVG and the text. Callers that downcast the result (e.g. `as HTMLDivElement`) keep working since `Element | DocumentFragment` is still assignable via `as`, but the more precise types they intend to assert are now an `instanceof Element` guard or `if ('tagName' in result)` away.
|
|
11
16
|
- KF-230 — `eslint-plugin-kerfjs` adds `kerfjs/prefer-attr-selector` (`warn` in recommended; plugin bumped to v0.11.0) — flags `delegate(_, _, '[name="value"]', _)` literal selectors and recommends `attr('name', 'value').selector`. Routing JSX `{...spec.attrs}` and the delegate target through one typed source means a rename can't desync the two. Conservative AST match: compound selectors (`[a="x"][b="y"]`), tag-qualified (`button[data-action="x"]`), bare-presence (`[data-new]`), and class / id selectors are left alone — those aren't 1:1 swaps for `attr()`. Severity `warn` because the literal form still works at runtime.
|
|
12
17
|
- KF-230 — `scripts/check-docs-examples.mjs` gains Check 3 (example doc/source import mirroring). For every paired `site/src/content/docs/examples/complete/<name>.md` ↔ `site/src/examples/complete/<name>/main.tsx`, every named kerfjs import the source pulls in must also appear in at least one kerf-import statement in the doc page. Catches "release adds a new public API (`attr()`, …) and updates the example source, but the rendered doc excerpt still uses the pre-release pattern" — the v0.11.0 release shipped exactly this drift across TodoMVC + chat doc pages, 12 migration pages, 7 reactivity-demo sections, and `docs/ai/usage-guide.md`, all corrected in the same ticket.
|
|
@@ -19,6 +24,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
19
24
|
- `kerfjs` npm package now bundles the drop-in AI-assistant configs at `ai/skill.md`, `ai/cursorrules`, and `ai/manifest.json` — so `npm install kerfjs` lands them directly on disk instead of asking consumers to find them on GitHub. The repo-root `kerf.claude-skill.md` / `kerf.cursorrules` remain the source of truth; `ai/` is regenerated by `scripts/sync-ai-bundle.mjs` and kept honest by the new `npm run check:ai-bundle-in-sync` gate (wired into `npm run check`). Each bundled file carries a `kerf-skill-version` line + a `KERF-APP-CANONICAL-END` marker so the new ESLint rule can detect drift and auto-fix only the kerf-maintained section while preserving consumer customizations below the marker. See `docs/12-ai-assistant-configs.md`.
|
|
20
25
|
- `eslint-plugin-kerfjs` v0.9.0 adds `kerfjs/ai-assistant-configs` (`warn` in recommended config) — once per lint pass, checks the consumer's `.claude/skills/kerf-app/SKILL.md` and `.cursorrules` against the bundled `kerfjs/ai/manifest.json`. Reports `missing` / `stale` / `forked` states; `eslint --fix` writes the bundled canonical above the `KERF-APP-CANONICAL-END` marker and preserves the consumer's append zone below it (the "versioned-section preservation" strategy). Granular disable via `['warn', { claude: false, cursor: true }]`.
|
|
21
26
|
|
|
27
|
+
## [0.13.0] - 2026-05-23
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
- Add `KERF_DEV_WARN_DELEGATE_IN_EFFECT` dev warning to catch `delegate()` calls inside reactive effects
|
|
31
|
+
- New `require-delegate-disposer` ESLint rule flags `delegate()` calls whose disposer is discarded
|
|
32
|
+
- Document `delegate()` disposer gotchas and canonical cleanup patterns
|
|
33
|
+
|
|
22
34
|
## [0.12.1] - 2026-05-22
|
|
23
35
|
|
|
24
36
|
|
package/ai/cursorrules
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- kerf-skill-version: 1.
|
|
1
|
+
<!-- kerf-skill-version: 1.2.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
|
|
@@ -12,7 +12,7 @@ You are writing a UI in kerf — a ~6.1 KB reactive framework (6.5 KB with `arra
|
|
|
12
12
|
- Install with `npm install kerfjs`.
|
|
13
13
|
- `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`.
|
|
14
14
|
- Vite / esbuild need no extra config.
|
|
15
|
-
- Recommended: also install `eslint-plugin-kerfjs` (`npm install --save-dev eslint-plugin-kerfjs`) and add `kerfjs.configs.recommended` to the project's eslint config. It enforces
|
|
15
|
+
- Recommended: also install `eslint-plugin-kerfjs` (`npm install --save-dev eslint-plugin-kerfjs`) and add `kerfjs.configs.recommended` to the project's eslint config. It enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time so violations surface as IDE squiggles before any code runs.
|
|
16
16
|
|
|
17
17
|
## Public API — one import path
|
|
18
18
|
|
|
@@ -55,15 +55,16 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
55
55
|
2. **Diff keys are `id` then `data-key`.** Lists must set `data-key={item.id}` per item, otherwise the diff matches by position and you lose focus / cursor / identity on insert/delete.
|
|
56
56
|
3. **Escape hatches:** `data-morph-skip` (element + subtree preserved verbatim — for Monaco / xterm / D3); `data-morph-skip-children` (attrs morph, subtree preserved — for client-hydrated slots whose host classes change); `data-morph-preserve` (element survives the trailing-removal pass — for imperatively-injected nodes like autoplay videos).
|
|
57
57
|
4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
|
|
58
|
-
5. **
|
|
59
|
-
6. **
|
|
60
|
-
7. **
|
|
61
|
-
8. **
|
|
62
|
-
9. **
|
|
63
|
-
10.
|
|
64
|
-
11.
|
|
65
|
-
12. **
|
|
66
|
-
13.
|
|
58
|
+
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
|
+
6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
|
|
60
|
+
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. **Signal reads must happen INSIDE the render function** to be tracked. `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Move the read inside.
|
|
62
|
+
9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
63
|
+
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root instead.
|
|
64
|
+
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.
|
|
65
|
+
12. **Custom-element types: declaration-merge into `kerfjs/jsx-runtime`**, NOT into a global JSX namespace. Pattern: `declare module 'kerfjs/jsx-runtime' { namespace JSX { interface IntrinsicElements { 'my-tag': KerfCustomElement & { foo?: string } } } }`.
|
|
66
|
+
13. **Each `each()` row must produce exactly one top-level element.** The reconciler binds one live DOM node per item — multi-root or empty rows throw with a row-precise error. Wrap multiple roots in one parent.
|
|
67
|
+
14. **`each()` is for DYNAMIC lists. Use `.map()` for static structural arrays** (constant `COLUMNS` / `TABS` / settings sections) whose row render reads signals. `each()` memoizes per-item HTML by object identity; constant items never change identity, so the cache hits forever, the row render is never re-invoked, and signal reads inside it silently stop tracking. Outer `.map()` for the static frame + inner `each()` for the dynamic sub-list is the idiomatic shape.
|
|
67
68
|
|
|
68
69
|
## Decision-making axes
|
|
69
70
|
|
|
@@ -133,7 +134,7 @@ morph(liveCard, '<article class="card">…</article>');
|
|
|
133
134
|
- SVG renders as broken / namespaceless markup → use `mount` (HTML path) or `toElement` (SVG-aware), not `innerHTML`.
|
|
134
135
|
- Library widget destroyed on every render → wrap host in `data-morph-skip`; mount the library imperatively after first render.
|
|
135
136
|
- `each(): row render at index N produced K top-level elements` → wrap multiple roots in one parent.
|
|
136
|
-
- 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
|
|
137
|
+
- 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.
|
|
137
138
|
|
|
138
139
|
## Server / SSR
|
|
139
140
|
|
package/ai/manifest.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kerfjsVersion": "0.
|
|
2
|
+
"kerfjsVersion": "0.13.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.2.0",
|
|
10
|
+
"sha256": "76550ec18da8e063123bfabc575d9ad3a43583d41a498c6e4b4be03c17dbd188"
|
|
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.2.0",
|
|
18
|
+
"sha256": "dcf2fc03f5dfca3b9bfea8cfefc65ac8ce31e1c601c613e91af69b6195c5e267"
|
|
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.2.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Building apps with kerf
|
|
@@ -17,7 +17,7 @@ kerf is a ~11 KB reactive UI framework (~12 KB with `arraySignal`): signals + DO
|
|
|
17
17
|
- Install: `npm install kerfjs`
|
|
18
18
|
- `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`
|
|
19
19
|
- Vite / esbuild need no extra config.
|
|
20
|
-
- Recommended companion: `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to the project's eslint config. Enforces
|
|
20
|
+
- Recommended companion: `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to the project's eslint config. Enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time — useful as a self-correction signal when authoring kerf code.
|
|
21
21
|
|
|
22
22
|
## Public API — one import path
|
|
23
23
|
|
|
@@ -63,15 +63,16 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
63
63
|
- `data-morph-skip-children` — attrs on the host morph, subtree preserved. For client-hydrated slots whose loading/state classes need to flow through.
|
|
64
64
|
- `data-morph-preserve` — element survives the trailing-removal pass even when the new template doesn't emit it. For imperatively-injected children (autoplay video, tooltip overlay, analytics pixel). Does NOT block a keyed-match move.
|
|
65
65
|
4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
|
|
66
|
-
5. **
|
|
67
|
-
6. **
|
|
68
|
-
7. **
|
|
69
|
-
8. **
|
|
70
|
-
9. **
|
|
71
|
-
10.
|
|
72
|
-
11.
|
|
73
|
-
12. **
|
|
74
|
-
13.
|
|
66
|
+
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
|
+
6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
|
|
68
|
+
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. **Signal reads must happen INSIDE the render function** to be tracked. `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. Move the read inside.
|
|
70
|
+
9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
71
|
+
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root.
|
|
72
|
+
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.
|
|
73
|
+
12. **Custom-element types: declaration-merge into `kerfjs/jsx-runtime`**, NOT into a global JSX namespace. Pattern: `declare module 'kerfjs/jsx-runtime' { namespace JSX { interface IntrinsicElements { 'my-tag': KerfCustomElement & { foo?: string } } } }`.
|
|
74
|
+
13. **Each `each()` row must produce exactly one top-level element.** Multi-root or empty rows throw a row-precise error. Wrap multiple roots in one parent.
|
|
75
|
+
14. **`each()` is for DYNAMIC lists. Use `.map()` for static structural arrays** (constant `COLUMNS` / `TABS` / settings sections) whose row render reads signals. `each()` memoizes per-item HTML by object identity; constant items never change identity, so the cache hits forever, the row render is never re-invoked, and signal reads inside it silently stop tracking. Outer `.map()` for the static frame + inner `each()` for the dynamic sub-list is the idiomatic shape.
|
|
75
76
|
|
|
76
77
|
## Decision-making axes
|
|
77
78
|
|
|
@@ -145,7 +146,7 @@ morph(liveCard, '<article class="card">…</article>');
|
|
|
145
146
|
| Library widget destroyed on every render | host reachable by the morph | Wrap host in `data-morph-skip`; mount the library imperatively after first render |
|
|
146
147
|
| `<my-tag>` fails to typecheck | declaration merging targeted global JSX | Use `declare module 'kerfjs/jsx-runtime' { namespace JSX { … } }` instead |
|
|
147
148
|
| `each(): row render at index N produced K top-level elements` | row returned multiple sibling elements or zero | Wrap them in one parent so the row renders exactly one element |
|
|
148
|
-
| 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
|
|
149
|
+
| 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 |
|
|
149
150
|
|
|
150
151
|
## Workflow guidance
|
|
151
152
|
|
package/dist/array-signal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { signal } from './chunk-
|
|
1
|
+
import { signal } from './chunk-N4KF3GD2.js';
|
|
2
2
|
|
|
3
3
|
// src/dev-store-warn.ts
|
|
4
4
|
var WARNING_PREFIX = "kerf: defineStore.set() called with keys missing from the current state \u2014 ";
|
|
@@ -66,5 +66,5 @@ function clearStoreRegistry() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export { clearStoreRegistry, defineStore, resetAllStores };
|
|
69
|
-
//# sourceMappingURL=chunk-
|
|
70
|
-
//# sourceMappingURL=chunk-
|
|
69
|
+
//# sourceMappingURL=chunk-4TJEO4AO.js.map
|
|
70
|
+
//# sourceMappingURL=chunk-4TJEO4AO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/dev-store-warn.ts","../src/store.ts"],"names":[],"mappings":"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","file":"chunk-IBVKW6WU.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\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 const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n if (proc?.env?.NODE_ENV === 'production') return false;\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 * `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';\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\nconst IS_DEV: boolean = (() => {\n const proc = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process;\n return proc?.env?.NODE_ENV !== 'production';\n})();\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 const set = (next: TState): void => {\n maybeWarnNarrowSet(internal.value, next, warnCtx);\n internal.value = next;\n };\n // In dev, freeze the snapshot returned to actions so that\n // `get().count = 42`-style mutations (a documented Rule 8 violation) throw\n // a native `TypeError: Cannot assign to read only property` instead of\n // silently landing on the underlying state without notifying subscribers.\n // Production keeps the bare reference for zero overhead. Read NODE_ENV via\n // globalThis so the source works untouched in browsers (no bare `process`).\n const get = (): Readonly<TState> => {\n const v = internal.value;\n if (IS_DEV && v !== null && typeof v === 'object') {\n Object.freeze(v);\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/dev-store-warn.ts","../src/store.ts"],"names":[],"mappings":"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","file":"chunk-4TJEO4AO.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\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 const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n if (proc?.env?.NODE_ENV === 'production') return false;\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 * `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';\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\nconst IS_DEV: boolean = (() => {\n const proc = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process;\n return proc?.env?.NODE_ENV !== 'production';\n})();\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 const set = (next: TState): void => {\n maybeWarnNarrowSet(internal.value, next, warnCtx);\n internal.value = next;\n };\n // In dev, freeze the snapshot returned to actions so that\n // `get().count = 42`-style mutations (a documented Rule 8 violation) throw\n // a native `TypeError: Cannot assign to read only property` instead of\n // silently landing on the underlying state without notifying subscribers.\n // Production keeps the bare reference for zero overhead. Read NODE_ENV via\n // globalThis so the source works untouched in browsers (no bare `process`).\n const get = (): Readonly<TState> => {\n const v = internal.value;\n if (IS_DEV && v !== null && typeof v === 'object') {\n Object.freeze(v);\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"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { signal as signal$1, effect as effect$1, Signal } from '@preact/signals-core';
|
|
2
|
+
export { batch, computed } from '@preact/signals-core';
|
|
3
|
+
|
|
4
|
+
// src/reactive.ts
|
|
5
|
+
|
|
6
|
+
// src/dev-delegate-warn.ts
|
|
7
|
+
var depth = 0;
|
|
8
|
+
var warned = false;
|
|
9
|
+
function isOptedIn() {
|
|
10
|
+
const proc = globalThis.process;
|
|
11
|
+
if (proc?.env?.NODE_ENV === "production") return false;
|
|
12
|
+
return proc?.env?.KERF_DEV_WARN_DELEGATE_IN_EFFECT === "1";
|
|
13
|
+
}
|
|
14
|
+
function enterEffect() {
|
|
15
|
+
depth++;
|
|
16
|
+
}
|
|
17
|
+
function exitEffect() {
|
|
18
|
+
depth--;
|
|
19
|
+
}
|
|
20
|
+
function isDevWarnDelegateInEffectEnabled() {
|
|
21
|
+
return isOptedIn();
|
|
22
|
+
}
|
|
23
|
+
function warnIfInsideEffect(fn) {
|
|
24
|
+
if (!isOptedIn()) return;
|
|
25
|
+
if (depth === 0) return;
|
|
26
|
+
if (warned) return;
|
|
27
|
+
warned = true;
|
|
28
|
+
console.warn(
|
|
29
|
+
`kerf: ${fn}() was called inside an effect() body. Every effect re-run installs a fresh root listener; the effect disposer cleans up the reactive subscription but not the listeners, so listener count grows linearly with signal churn and each listener pins its handler closure. Register the delegate once at module or setup scope and gate behavior on the signal *inside the handler* where the read is free. See docs/5-event-delegation.md \xA75.3 "When capturing the disposer still isn't enough". Set KERF_DEV_WARN_DELEGATE_IN_EFFECT=0 (or unset it) to silence this warning.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
var WARNING_MESSAGE = "kerf: signal was written but has no subscribers. Did you read `.value` outside of a render fn / effect()? Hoisted reads do not subscribe, so subsequent writes will not re-render. Move the read inside mount()'s render fn or effect() callback. Set KERF_DEV_WARN_UNTRACKED_SIGNALS=0 (or unset it) to silence this warning.";
|
|
33
|
+
var DevSignal = class extends Signal {
|
|
34
|
+
__hasSubscriber = false;
|
|
35
|
+
__warned = false;
|
|
36
|
+
__constructed = false;
|
|
37
|
+
constructor(initial) {
|
|
38
|
+
super(initial, {
|
|
39
|
+
watched() {
|
|
40
|
+
this.__hasSubscriber = true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this.__constructed = true;
|
|
44
|
+
}
|
|
45
|
+
get value() {
|
|
46
|
+
return super.value;
|
|
47
|
+
}
|
|
48
|
+
set value(v) {
|
|
49
|
+
super.value = v;
|
|
50
|
+
if (this.__constructed && !this.__hasSubscriber && !this.__warned) {
|
|
51
|
+
this.__warned = true;
|
|
52
|
+
console.warn(WARNING_MESSAGE);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
function isDevWarnUntrackedEnabled() {
|
|
57
|
+
const proc = globalThis.process;
|
|
58
|
+
if (proc?.env?.NODE_ENV === "production") return false;
|
|
59
|
+
return proc?.env?.KERF_DEV_WARN_UNTRACKED_SIGNALS === "1";
|
|
60
|
+
}
|
|
61
|
+
function signal(value) {
|
|
62
|
+
if (isDevWarnUntrackedEnabled()) return new DevSignal(value);
|
|
63
|
+
return signal$1(value);
|
|
64
|
+
}
|
|
65
|
+
function effect(fn) {
|
|
66
|
+
if (!isDevWarnDelegateInEffectEnabled()) return effect$1(fn);
|
|
67
|
+
return effect$1(() => {
|
|
68
|
+
enterEffect();
|
|
69
|
+
try {
|
|
70
|
+
return fn();
|
|
71
|
+
} finally {
|
|
72
|
+
exitEffect();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { effect, signal, warnIfInsideEffect };
|
|
78
|
+
//# sourceMappingURL=chunk-N4KF3GD2.js.map
|
|
79
|
+
//# sourceMappingURL=chunk-N4KF3GD2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dev-delegate-warn.ts","../src/dev-signal.ts","../src/reactive.ts"],"names":["coreSignal","coreEffect"],"mappings":";;;;;;AAyBA,IAAI,KAAA,GAAQ,CAAA;AACZ,IAAI,MAAA,GAAS,KAAA;AAEb,SAAS,SAAA,GAAqB;AAC5B,EAAA,MAAM,OAAQ,UAAA,CAA0E,OAAA;AACxF,EAAA,IAAI,IAAA,EAAM,GAAA,EAAK,QAAA,KAAa,YAAA,EAAc,OAAO,KAAA;AACjD,EAAA,OAAO,IAAA,EAAM,KAAK,gCAAA,KAAqC,GAAA;AACzD;AAGO,SAAS,WAAA,GAAoB;AAClC,EAAA,KAAA,EAAA;AACF;AAGO,SAAS,UAAA,GAAmB;AACjC,EAAA,KAAA,EAAA;AACF;AAGO,SAAS,gCAAA,GAA4C;AAC1D,EAAA,OAAO,SAAA,EAAU;AACnB;AAQO,SAAS,mBAAmB,EAAA,EAA0C;AAC3E,EAAA,IAAI,CAAC,WAAU,EAAG;AAClB,EAAA,IAAI,UAAU,CAAA,EAAG;AACjB,EAAA,IAAI,MAAA,EAAQ;AACZ,EAAA,MAAA,GAAS,IAAA;AACT,EAAA,OAAA,CAAQ,IAAA;AAAA,IACN,SAAS,EAAE,CAAA,gjBAAA;AAAA,GAOb;AACF;AC3CA,IAAM,eAAA,GACF,gUAAA;AAMG,IAAM,SAAA,GAAN,cAA2B,MAAA,CAAU;AAAA,EAClC,eAAA,GAAkB,KAAA;AAAA,EAClB,QAAA,GAAW,KAAA;AAAA,EACX,aAAA,GAAgB,KAAA;AAAA,EAExB,YAAY,OAAA,EAAa;AACvB,IAAA,KAAA,CAAM,OAAA,EAAc;AAAA,MAClB,OAAA,GAAyB;AACvB,QAAC,KAAiD,eAAA,GAAkB,IAAA;AAAA,MACtE;AAAA,KACD,CAAA;AACD,IAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AAAA,EACvB;AAAA,EAEA,IAAa,KAAA,GAAW;AAAE,IAAA,OAAO,KAAA,CAAM,KAAA;AAAA,EAAO;AAAA,EAC9C,IAAa,MAAM,CAAA,EAAM;AACvB,IAAA,KAAA,CAAM,KAAA,GAAQ,CAAA;AACd,IAAA,IAAI,KAAK,aAAA,IAAiB,CAAC,KAAK,eAAA,IAAmB,CAAC,KAAK,QAAA,EAAU;AACjE,MAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,MAAA,OAAA,CAAQ,KAAK,eAAe,CAAA;AAAA,IAC9B;AAAA,EACF;AACF,CAAA;AAEO,SAAS,yBAAA,GAAqC;AACnD,EAAA,MAAM,OAAQ,UAAA,CAA0E,OAAA;AACxF,EAAA,IAAI,IAAA,EAAM,GAAA,EAAK,QAAA,KAAa,YAAA,EAAc,OAAO,KAAA;AACjD,EAAA,OAAO,IAAA,EAAM,KAAK,+BAAA,KAAoC,GAAA;AACxD;AC9BO,SAAS,OAAU,KAAA,EAAsB;AAC9C,EAAA,IAAI,yBAAA,EAA0B,EAAG,OAAO,IAAI,UAAa,KAAU,CAAA;AACnE,EAAA,OAAOA,SAAW,KAAU,CAAA;AAC9B;AAEO,SAAS,OAAO,EAAA,EAA2C;AAChE,EAAA,IAAI,CAAC,gCAAA,EAAiC,EAAG,OAAOC,SAAW,EAAE,CAAA;AAC7D,EAAA,OAAOA,SAAW,MAAM;AACtB,IAAA,WAAA,EAAY;AACZ,IAAA,IAAI;AACF,MAAA,OAAO,EAAA,EAAG;AAAA,IACZ,CAAA,SAAE;AACA,MAAA,UAAA,EAAW;AAAA,IACb;AAAA,EACF,CAAC,CAAA;AACH","file":"chunk-N4KF3GD2.js","sourcesContent":["/**\n * Dev-mode warning for `delegate()` / `delegateCapture()` calls that run\n * inside an `effect()` body (KERF_DEV_WARN_DELEGATE_IN_EFFECT=1).\n *\n * Why the pattern matters: every effect re-run executes its body fresh, which\n * means a `delegate()` call inside the body installs a NEW root listener on\n * each re-run. The effect's disposer cleans up the reactive subscription but\n * not the side-effects the body produced — so previous listeners stay\n * attached, the per-listener closure pins `rootEl` / `handler` / everything\n * the handler closes over, and listener count grows linearly with signal\n * churn. Structurally identical to the addEventListener-inside-mount foot-gun\n * (Hard Rule 4) but doesn't *look* like it.\n *\n * Static analysis can't reliably detect \"inside an effect\" without flow\n * information (effect() is just a function call), so the canonical defense\n * is this runtime opt-in warning. When enabled, `reactive.ts`'s `effect()`\n * wrapper increments a module-level counter before invoking the user body\n * and decrements after; `delegate.ts` checks the counter and fires the\n * warning once total.\n *\n * Production behavior is unchanged for zero runtime cost — the env-var check\n * short-circuits before any state is touched, and the wrapper in\n * `reactive.ts` only wraps when the gate is on.\n */\n\nlet depth = 0;\nlet warned = false;\n\nfunction isOptedIn(): boolean {\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n if (proc?.env?.NODE_ENV === 'production') return false;\n return proc?.env?.KERF_DEV_WARN_DELEGATE_IN_EFFECT === '1';\n}\n\n/** Called by the `effect()` wrapper in `reactive.ts` before running the user body. */\nexport function enterEffect(): void {\n depth++;\n}\n\n/** Called by the `effect()` wrapper in `reactive.ts` after the user body returns or throws. */\nexport function exitEffect(): void {\n depth--;\n}\n\n/** Public re-export of the env-var check so `reactive.ts` can decide whether to wrap. */\nexport function isDevWarnDelegateInEffectEnabled(): boolean {\n return isOptedIn();\n}\n\n/**\n * Called at the top of `delegate()` and `delegateCapture()`. If the call is\n * happening inside an `effect()` body (depth > 0) AND the env var is on, fire\n * a one-shot warning. The `fn` argument is the name of the caller for the\n * message (\"delegate\" vs \"delegateCapture\").\n */\nexport function warnIfInsideEffect(fn: 'delegate' | 'delegateCapture'): void {\n if (!isOptedIn()) return;\n if (depth === 0) return;\n if (warned) return;\n warned = true;\n console.warn(\n `kerf: ${fn}() was called inside an effect() body. `\n + 'Every effect re-run installs a fresh root listener; the effect disposer cleans up the '\n + 'reactive subscription but not the listeners, so listener count grows linearly with signal '\n + 'churn and each listener pins its handler closure. Register the delegate once at module '\n + 'or setup scope and gate behavior on the signal *inside the handler* where the read is free. '\n + 'See docs/5-event-delegation.md §5.3 \"When capturing the disposer still isn\\'t enough\". '\n + 'Set KERF_DEV_WARN_DELEGATE_IN_EFFECT=0 (or unset it) to silence this warning.',\n );\n}\n\n/** Test helper — resets the one-shot dedup flag and depth counter for unit tests. */\nexport function _resetWarnedForTests(): void {\n warned = false;\n depth = 0;\n}\n","/**\n * Dev-mode signal subclass with subscriber tracking (KF-176). When the\n * dev-warn opt-in is enabled, `signal()` returns a `DevSignal` that emits a\n * one-shot `console.warn` the first time `.value` is written to an instance\n * that has never had a subscriber attached. This surfaces the canonical\n * Rule 7 violation (read `.value` outside a render fn / effect — the read\n * doesn't subscribe, so subsequent writes silently fail to re-render) at\n * the moment the user makes the wrong write, instead of leaving them to\n * notice that their UI never updates.\n *\n * The gate is `process.env.NODE_ENV !== 'production'` AND\n * `KERF_DEV_WARN_UNTRACKED_SIGNALS === '1'`. Off by default because the\n * heuristic produces false positives for purely imperative signals (used as\n * mutable cells with no UI consumer); opt-in is the right shape until a\n * sharper heuristic is found. Production behavior is unchanged for zero\n * runtime cost.\n *\n * The subclass uses signals-core's `SignalOptions.watched` callback to set a\n * per-instance `__hasSubscriber` flag — fired by signals-core when the first\n * subscriber attaches. We never clear the flag on `unwatched`, so a signal\n * that *was* subscribed at some point won't warn even if its subscribers\n * later detach.\n */\n\nimport { Signal } from '@preact/signals-core';\n\nconst WARNING_MESSAGE\n = 'kerf: signal was written but has no subscribers. '\n + 'Did you read `.value` outside of a render fn / effect()? '\n + 'Hoisted reads do not subscribe, so subsequent writes will not re-render. '\n + 'Move the read inside mount()\\'s render fn or effect() callback. '\n + 'Set KERF_DEV_WARN_UNTRACKED_SIGNALS=0 (or unset it) to silence this warning.';\n\nexport class DevSignal<T> extends Signal<T> {\n private __hasSubscriber = false;\n private __warned = false;\n private __constructed = false;\n\n constructor(initial?: T) {\n super(initial as T, {\n watched(this: Signal<T>) {\n (this as unknown as { __hasSubscriber: boolean }).__hasSubscriber = true;\n },\n });\n this.__constructed = true;\n }\n\n override get value(): T { return super.value; }\n override set value(v: T) {\n super.value = v;\n if (this.__constructed && !this.__hasSubscriber && !this.__warned) {\n this.__warned = true;\n console.warn(WARNING_MESSAGE);\n }\n }\n}\n\nexport function isDevWarnUntrackedEnabled(): boolean {\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n if (proc?.env?.NODE_ENV === 'production') return false;\n return proc?.env?.KERF_DEV_WARN_UNTRACKED_SIGNALS === '1';\n}\n","/**\n * Re-exports of `@preact/signals-core`. Lets the rest of the codebase depend\n * on `'./reactive.js'` without naming the underlying lib, so swapping it out\n * later (or fronting it with a hand-rolled implementation) is a one-file\n * change.\n *\n * Two dev-gated wrappers sit in front of the bare re-exports:\n *\n * - `signal()` returns a `DevSignal` when `KERF_DEV_WARN_UNTRACKED_SIGNALS=1`\n * (KF-176) — warns on writes to signals with no subscribers.\n *\n * - `effect()` wraps the user body in `enterEffect()` / `exitEffect()` calls\n * when `KERF_DEV_WARN_DELEGATE_IN_EFFECT=1` so `delegate()` can detect when\n * it's running inside an effect body and fire the appropriate warning.\n *\n * Both gates short-circuit on `NODE_ENV === 'production'` — production\n * always sees the bare `@preact/signals-core` exports with zero overhead.\n */\n\nimport { effect as coreEffect,type Signal,signal as coreSignal } from '@preact/signals-core';\n\nimport { enterEffect, exitEffect, isDevWarnDelegateInEffectEnabled } from './dev-delegate-warn.js';\nimport { DevSignal, isDevWarnUntrackedEnabled } from './dev-signal.js';\n\nexport {\n batch,\n computed,\n type ReadonlySignal,\n type Signal,\n} from '@preact/signals-core';\n\nexport function signal<T>(value?: T): Signal<T> {\n if (isDevWarnUntrackedEnabled()) return new DevSignal<T>(value as T) as Signal<T>;\n return coreSignal(value as T);\n}\n\nexport function effect(fn: () => void | (() => void)): () => void {\n if (!isDevWarnDelegateInEffectEnabled()) return coreEffect(fn);\n return coreEffect(() => {\n enterEffect();\n try {\n return fn();\n } finally {\n exitEffect();\n }\n });\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ArraySignal } from './array-signal.js';
|
|
|
2
2
|
import { SafeHtml } from './jsx-runtime.js';
|
|
3
3
|
export { Fragment, isSafeHtml, raw } from './jsx-runtime.js';
|
|
4
4
|
import { Signal } from '@preact/signals-core';
|
|
5
|
-
export { ReadonlySignal, Signal, batch, computed
|
|
5
|
+
export { ReadonlySignal, Signal, batch, computed } from '@preact/signals-core';
|
|
6
6
|
export { S as Store, d as defineStore, r as resetAllStores } from './testing-DNEY7wi3.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -301,13 +301,21 @@ declare function mount(rootEl: HTMLElement, render: () => MountResult): () => vo
|
|
|
301
301
|
* later (or fronting it with a hand-rolled implementation) is a one-file
|
|
302
302
|
* change.
|
|
303
303
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
304
|
+
* Two dev-gated wrappers sit in front of the bare re-exports:
|
|
305
|
+
*
|
|
306
|
+
* - `signal()` returns a `DevSignal` when `KERF_DEV_WARN_UNTRACKED_SIGNALS=1`
|
|
307
|
+
* (KF-176) — warns on writes to signals with no subscribers.
|
|
308
|
+
*
|
|
309
|
+
* - `effect()` wraps the user body in `enterEffect()` / `exitEffect()` calls
|
|
310
|
+
* when `KERF_DEV_WARN_DELEGATE_IN_EFFECT=1` so `delegate()` can detect when
|
|
311
|
+
* it's running inside an effect body and fire the appropriate warning.
|
|
312
|
+
*
|
|
313
|
+
* Both gates short-circuit on `NODE_ENV === 'production'` — production
|
|
314
|
+
* always sees the bare `@preact/signals-core` exports with zero overhead.
|
|
308
315
|
*/
|
|
309
316
|
|
|
310
317
|
declare function signal<T>(value?: T): Signal<T>;
|
|
318
|
+
declare function effect(fn: () => void | (() => void)): () => void;
|
|
311
319
|
|
|
312
320
|
/**
|
|
313
321
|
* `toElement(jsx)` — JSX → DOM, with SVG-aware namespace handling.
|
|
@@ -330,4 +338,4 @@ declare function signal<T>(value?: T): Signal<T>;
|
|
|
330
338
|
|
|
331
339
|
declare function toElement(jsx: SafeHtml | string): Element | DocumentFragment;
|
|
332
340
|
|
|
333
|
-
export { type AttrSpec, type MountResult, SafeHtml, attr, delegate, delegateCapture, each, morph, mount, signal, toElement };
|
|
341
|
+
export { type AttrSpec, type MountResult, SafeHtml, attr, delegate, delegateCapture, each, effect, morph, mount, signal, toElement };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isSafeHtml, listSafeHtml, flattenWithoutListItems, collectLists, flatten, granularListSafeHtml } from './chunk-4VT4YZOO.js';
|
|
2
2
|
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-4VT4YZOO.js';
|
|
3
|
-
export { defineStore, resetAllStores } from './chunk-
|
|
4
|
-
import { effect } from './chunk-
|
|
5
|
-
export { batch, computed, effect, signal } from './chunk-
|
|
3
|
+
export { defineStore, resetAllStores } from './chunk-4TJEO4AO.js';
|
|
4
|
+
import { warnIfInsideEffect, effect } from './chunk-N4KF3GD2.js';
|
|
5
|
+
export { batch, computed, effect, signal } from './chunk-N4KF3GD2.js';
|
|
6
6
|
|
|
7
7
|
// src/attrSelector.ts
|
|
8
8
|
function cssEscapeIdent(value) {
|
|
@@ -96,6 +96,7 @@ function assertValidSelector(selector, fn) {
|
|
|
96
96
|
}
|
|
97
97
|
function delegate(rootEl, type, selector, handler) {
|
|
98
98
|
assertValidSelector(selector, "delegate");
|
|
99
|
+
warnIfInsideEffect("delegate");
|
|
99
100
|
const listener = (event) => {
|
|
100
101
|
const target = event.target;
|
|
101
102
|
if (!(target instanceof Element)) return;
|
|
@@ -112,6 +113,7 @@ function delegate(rootEl, type, selector, handler) {
|
|
|
112
113
|
}
|
|
113
114
|
function delegateCapture(rootEl, type, selector, handler) {
|
|
114
115
|
assertValidSelector(selector, "delegateCapture");
|
|
116
|
+
warnIfInsideEffect("delegateCapture");
|
|
115
117
|
const listener = (event) => {
|
|
116
118
|
const target = event.target;
|
|
117
119
|
if (!(target instanceof Element)) return;
|