kerfjs 2.0.0 → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +8 -0
  3. package/ai/cursorrules +11 -1
  4. package/ai/manifest.json +5 -5
  5. package/ai/skill.md +11 -1
  6. package/dist/array-signal.d.ts +7 -1
  7. package/dist/array-signal.js +11 -2
  8. package/dist/array-signal.js.map +1 -1
  9. package/dist/bindings-CYwoJpQb.d.ts +60 -0
  10. package/dist/chunk-3APBEVHF.js +20 -0
  11. package/dist/chunk-3APBEVHF.js.map +1 -0
  12. package/dist/chunk-GY4XV2UV.js +73 -0
  13. package/dist/chunk-GY4XV2UV.js.map +1 -0
  14. package/dist/{chunk-RYZHZBHE.js → chunk-JVVU2RQO.js} +15 -79
  15. package/dist/chunk-JVVU2RQO.js.map +1 -0
  16. package/dist/chunk-QIP723L4.js +15 -0
  17. package/dist/chunk-QIP723L4.js.map +1 -0
  18. package/dist/chunk-SAYPJ6XR.js +43 -0
  19. package/dist/chunk-SAYPJ6XR.js.map +1 -0
  20. package/dist/chunk-VVDJLWMP.js +14 -0
  21. package/dist/chunk-VVDJLWMP.js.map +1 -0
  22. package/dist/chunk-YHH7OUFA.js +58 -0
  23. package/dist/chunk-YHH7OUFA.js.map +1 -0
  24. package/dist/dev.d.ts +339 -0
  25. package/dist/dev.js +607 -0
  26. package/dist/dev.js.map +1 -0
  27. package/dist/html.d.ts +1 -0
  28. package/dist/html.js +4 -2
  29. package/dist/html.js.map +1 -1
  30. package/dist/index.d.ts +49 -11
  31. package/dist/index.js +358 -340
  32. package/dist/index.js.map +1 -1
  33. package/dist/jsx-runtime.d.ts +16 -60
  34. package/dist/jsx-runtime.js +4 -2
  35. package/dist/testing.js +3 -2
  36. package/llms.txt +2 -2
  37. package/package.json +22 -11
  38. package/dist/chunk-KFUDM3VP.js +0 -131
  39. package/dist/chunk-KFUDM3VP.js.map +0 -1
  40. package/dist/chunk-NU7YHYEV.js +0 -90
  41. package/dist/chunk-NU7YHYEV.js.map +0 -1
  42. package/dist/chunk-RYZHZBHE.js.map +0 -1
@@ -1,4 +1,5 @@
1
- import { ReadonlySignal, Signal } from '@preact/signals-core';
1
+ import { ReadonlySignal } from '@preact/signals-core';
2
+ import { B as Binding } from './bindings-CYwoJpQb.js';
2
3
 
3
4
  /**
4
5
  * JSX intrinsic-element types — kerf's per-tag attribute contracts.
@@ -692,63 +693,6 @@ interface KerfBuiltinIntrinsicElements {
692
693
  };
693
694
  }
694
695
 
695
- /**
696
- * Fine-grained signal bindings (KF-294 spike).
697
- *
698
- * When a `Signal` is interpolated straight into a JSX attribute
699
- * (`class={sig}`) or a text child (`{sig}`) INSIDE a `mount()` render, the
700
- * JSX runtime stops stringifying it. Instead it emits a marker into the HTML
701
- * string and records a binding here; after the string is parsed to DOM, a
702
- * wiring pass attaches one `effect` per binding that writes straight to the
703
- * live node. A later change to that signal then updates the node WITHOUT
704
- * re-running the render function or walking the list reconciler.
705
- *
706
- * This reuses the "marker in string, wire up after parse" mechanism the
707
- * keyed-list reconciler already uses for `<!--kf-list:{id}-->` markers.
708
- *
709
- * TWO SCOPES of binding, with disjoint marker namespaces so their wiring
710
- * passes never collide:
711
- *
712
- * - GLOBAL holes — signals in the static surrounds (outside any `each()`
713
- * row). Markers: `data-kfb` attribute / `<!--kfb:{id}-->` comment. Ids come
714
- * from the mount render context's counter; wired by `wireBindings()` over
715
- * the whole mount root; disposed/re-wired by `mount()` each render.
716
- *
717
- * - ROW holes — signals inside an `each()` row. Markers: `data-kfbrow`
718
- * attribute / `<!--kfbr:{id}-->` comment. Ids are row-LOCAL (reset per row)
719
- * so they stay stable and collision-free as rows are inserted/removed/moved.
720
- * Captured per row by `captureRowBindings()`, carried on the list segment
721
- * item, and wired/disposed by the list reconciler at each row node's
722
- * create/remove — so a binding's lifetime tracks its row node's lifetime,
723
- * and row reorders (which reuse the same node) are free.
724
- *
725
- * Outside a `mount()` render (SSR / `SafeHtml.toString()`) neither scope is
726
- * active: the runtime snapshots `signal.value` and emits no markers, so server
727
- * output is correct and legacy `.toString()` callers are unaffected.
728
- *
729
- * Module-level mutable state note: `context` / `rowSink` (plus `rowCounter`,
730
- * the row-hole id counter that resets with each row capture) are a third
731
- * sanctioned module-level mutable location (alongside `store.ts:REGISTRY` and
732
- * `each.ts:context`). They hold the current render's binding sinks and are set
733
- * / cleared by `mount()` and `each()` around the render calls. The only other
734
- * module-level container here is `insertedTextNodes`, a WeakMap keyed on text
735
- * marker comments — a pure cache whose entries die with their nodes (GC-tied
736
- * lifetime), so it carries no cross-render semantics.
737
- */
738
-
739
- interface AttrBinding {
740
- kind: 'attr';
741
- id: string;
742
- attr: string;
743
- signal: Signal<unknown>;
744
- }
745
- interface TextBinding {
746
- kind: 'text';
747
- id: string;
748
- signal: Signal<unknown>;
749
- }
750
- type Binding = AttrBinding | TextBinding;
751
-
752
696
  /**
753
697
  * `Segment` — kerf's structured render output.
754
698
  *
@@ -809,6 +753,18 @@ interface ListSegment {
809
753
  * patches are present.
810
754
  */
811
755
  patches?: ArrayPatchInternal[];
756
+ /**
757
+ * KF-388: the identity of the data this list renders — the `arraySignal`
758
+ * instance, or `undefined` for a plain array.
759
+ *
760
+ * A list's `id` is its call-order index, so a render that changes how many
761
+ * `each()` calls precede this one hands this segment a DIFFERENT list's
762
+ * binding. Patches are only meaningful against the binding they were queued
763
+ * for, so the reconciler compares this against the binding's recorded source
764
+ * before trusting the patch queue. It is an identity check, not a value
765
+ * check — the instance is never read.
766
+ */
767
+ source?: object;
812
768
  }
813
769
  /**
814
770
  * Internal patch shape used inside list segments. Mirrors `ArrayPatch<T>`
@@ -906,7 +862,7 @@ declare function raw(html: string): SafeHtml;
906
862
  * Internal: build a `SafeHtml` representing a list segment. Used by
907
863
  * `each()` so the JSX runtime is the sole owner of `SafeHtml` construction.
908
864
  */
909
- declare function listSafeHtml(id: string, items: ListSegment['items']): SafeHtml;
865
+ declare function listSafeHtml(id: string, items: ListSegment['items'], source?: object): SafeHtml;
910
866
  /**
911
867
  * Internal: build a `SafeHtml` representing a granular list segment with
912
868
  * patches (KF-92). The reconciler applies the patches to the existing
@@ -919,7 +875,7 @@ declare function listSafeHtml(id: string, items: ListSegment['items']): SafeHtml
919
875
  * already carries a `html` string, and the reconciler does no further
920
876
  * row rendering.
921
877
  */
922
- declare function granularListSafeHtml(id: string, items: ListSegment['items'], patches: NonNullable<ListSegment['patches']>): SafeHtml;
878
+ declare function granularListSafeHtml(id: string, items: ListSegment['items'], patches: NonNullable<ListSegment['patches']>, source?: object): SafeHtml;
923
879
  type Child = SafeHtml | string | number | boolean | null | undefined | ReadonlySignal<unknown>;
924
880
  type Children = Child | Children[];
925
881
  interface Props {
@@ -1,4 +1,6 @@
1
- export { Fragment, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw } from './chunk-RYZHZBHE.js';
2
- import './chunk-NU7YHYEV.js';
1
+ export { Fragment, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw } from './chunk-JVVU2RQO.js';
2
+ import './chunk-GY4XV2UV.js';
3
+ import './chunk-3APBEVHF.js';
4
+ import './chunk-VVDJLWMP.js';
3
5
  //# sourceMappingURL=jsx-runtime.js.map
4
6
  //# sourceMappingURL=jsx-runtime.js.map
package/dist/testing.js CHANGED
@@ -1,4 +1,5 @@
1
- export { clearStoreRegistry } from './chunk-KFUDM3VP.js';
2
- import './chunk-NU7YHYEV.js';
1
+ export { clearStoreRegistry } from './chunk-SAYPJ6XR.js';
2
+ import './chunk-3APBEVHF.js';
3
+ import './chunk-VVDJLWMP.js';
3
4
  //# sourceMappingURL=testing.js.map
4
5
  //# sourceMappingURL=testing.js.map
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A tiny (~11 KB minified + gzipped including its one runtime dependency `@preact/signals-core`; ~12 KB with `arraySignal`) reactive UI framework — fine-grained signals + DOM morphing + JSX. No virtual DOM, no compiler. Apply the smallest possible cut to update your DOM.
4
4
 
5
- kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "list"/"mixed" segments where `each(...)` was used) and reconciles it against the live tree with a custom segment-aware morph. Static surrounds go through a general-purpose tree-morph; list contents go through a keyed reconciler that operates directly on live children — partial-update on huge lists is O(changes), not O(rows). Reactivity is provided by [@preact/signals-core](https://github.com/preactjs/signals). It pairs well with server-rendered HTML, embedded widgets, and any UI where preserving focus / selection across re-renders matters. Public API is one import: `signal`, `computed`, `effect`, `batch`, `defineStore`, `resetAllStores`, `mount`, `morph`, `each`, `attr`, `delegate`, `delegateCapture`, `toElement`, `SafeHtml`, `isSafeHtml`, `raw`, `Fragment`. (Two more subpaths: `kerfjs/testing` exposes `clearStoreRegistry` for unit-test isolation; `kerfjs/jsx-runtime` exposes the typed JSX building blocks for declaration-merging custom-element types.) An optional subpath at `kerfjs/array-signal` adds `arraySignal()` — a granular keyed-list signal whose patch events let `each()` reconcile in O(patches) instead of O(N). Another optional subpath at `kerfjs/html` adds the `html` tagged template — JSX-identical runtime semantics with no JSX transform, so CDN/importmap projects can author kerf UIs with literally no build step.
5
+ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "list"/"mixed" segments where `each(...)` was used) and reconciles it against the live tree with a custom segment-aware morph. Static surrounds go through a general-purpose tree-morph; list contents go through a keyed reconciler that operates directly on live children — partial-update on huge lists is O(changes), not O(rows). Reactivity is provided by [@preact/signals-core](https://github.com/preactjs/signals). It pairs well with server-rendered HTML, embedded widgets, and any UI where preserving focus / selection across re-renders matters. Public API is one import: `signal`, `computed`, `effect`, `batch`, `defineStore`, `resetAllStores`, `mount`, `morph`, `each`, `attr`, `delegate`, `delegateCapture`, `toElement`, `SafeHtml`, `isSafeHtml`, `raw`, `Fragment`. (Two more subpaths: `kerfjs/testing` exposes `clearStoreRegistry` for unit-test isolation; `kerfjs/jsx-runtime` exposes the typed JSX building blocks for declaration-merging custom-element types.) An optional subpath at `kerfjs/array-signal` adds `arraySignal()` — a granular keyed-list signal whose patch events let `each()` reconcile in O(patches) instead of O(N). An optional subpath at `kerfjs/dev` installs the development diagnostics — kerf does NOT infer dev mode, so you import it behind your own build's dev flag (`if (import.meta.env.DEV) await import('kerfjs/dev');`); omitting it is production and sheds ~4.7 KB min+gzip. Another optional subpath at `kerfjs/html` adds the `html` tagged template — JSX-identical runtime semantics with no JSX transform, so CDN/importmap projects can author kerf UIs with literally no build step.
6
6
 
7
7
  ## For humans new to the codebase
8
8
 
@@ -30,7 +30,7 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
30
30
  - [API reference](https://github.com/brianwestphal/kerf/blob/main/docs/8-api-reference.md): every export, every option.
31
31
  - [Live demo](https://github.com/brianwestphal/kerf/blob/main/docs/9-live-demo.md): the GitHub Pages deploy of `examples/reactivity-demo`.
32
32
  - [Migrating](https://github.com/brianwestphal/kerf/blob/main/docs/10-migrating.md): the `/kerf/migrating/` comparison hub — coming-from-React/Alpine/Lit/vanjs pages with side-by-side todo-list translations.
33
- - [Dev-mode warnings](https://github.com/brianwestphal/kerf/blob/main/docs/11-dev-warnings.md): the opt-in `KERF_DEV_WARN_*` env-gated dev-warn family (rebuilt listeners, untracked signals, narrow store sets, delegate-in-effect, each-in-morph-skip, duplicate keys, value-only re-renders, stale bindings) and the rules each new warning must follow.
33
+ - [Dev-mode warnings](https://github.com/brianwestphal/kerf/blob/main/docs/11-dev-warnings.md): the opt-in `KERF_DEV_WARN_*` env-gated dev-warn family (rebuilt listeners, untracked signals, narrow store sets, delegate-in-effect, each-in-morph-skip, duplicate keys, value-only re-renders, stale bindings), how the diagnostics are installed via the `kerfjs/dev` subpath rather than inferred from the environment, and the rules each new warning must follow.
34
34
  - [AI-assistant configs](https://github.com/brianwestphal/kerf/blob/main/docs/12-ai-assistant-configs.md): how the drop-in Claude Code skill + Cursor rules ship inside the `kerfjs` npm package at `ai/skill.md` / `ai/cursorrules` / `ai/manifest.json`, the version + marker contract for customization preservation, and the `kerfjs/ai-assistant-configs` ESLint rule that surfaces drift on every lint pass.
35
35
  - [Component packages](https://github.com/brianwestphal/kerf/blob/main/docs/13-component-packages.md): building and publishing reusable kerf components as npm packages — the no-instance component model, per-instance state via factories, event/cleanup patterns, and `kerfjs`-as-peer-dependency packaging modeled on `eslint-plugin-kerfjs`. Scaffold one with `npm create kerf-component@latest <dir>` (the `create-kerf-component` initializer).
36
36
  - [Feature coverage](https://github.com/brianwestphal/kerf/blob/main/docs/14-feature-coverage.md): the per-behavior coverage axis orthogonal to line coverage — an index mapping each behavior (especially list-reconciler *state transitions*) to its guarding test, enforced by `npm run check:features`.
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "kerfjs",
3
- "version": "2.0.0",
3
+ "version": "3.0.0-beta.1",
4
4
  "description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",
5
5
  "type": "module",
6
- "sideEffects": false,
6
+ "sideEffects": [
7
+ "./dist/dev.js",
8
+ "./src/dev.ts"
9
+ ],
7
10
  "license": "MIT",
8
11
  "author": "Brian Westphal <brian.westphal@bleugris.com>",
9
12
  "homepage": "https://brianwestphal.github.io/kerf/",
@@ -53,6 +56,10 @@
53
56
  "types": "./dist/array-signal.d.ts",
54
57
  "import": "./dist/array-signal.js"
55
58
  },
59
+ "./dev": {
60
+ "types": "./dist/dev.d.ts",
61
+ "import": "./dist/dev.js"
62
+ },
56
63
  "./html": {
57
64
  "types": "./dist/html.d.ts",
58
65
  "import": "./dist/html.js"
@@ -76,20 +83,22 @@
76
83
  "test:integration": "vitest run tests/integration --coverage",
77
84
  "test:dist": "npm run build && vitest run --config vitest.config.dist.ts",
78
85
  "test:dist:full": "npm run build && vitest run --config vitest.config.dist-full.ts",
79
- "test:dist:jsx-typing": "npm run build && tsc -p tests/dist/jsx-typing/tsconfig.json",
80
- "test:dist:examples": "npm run build && tsc -p site/src/examples/complete/tsconfig.json",
81
- "test:dist:scaffold-typing": "npm run build && tsc -p tests/dist/scaffold-typing/tsconfig.json",
86
+ "test:dist:jsx-typing": "npm run build && node node_modules/typescript7/bin/tsc -p tests/dist/jsx-typing/tsconfig.json",
87
+ "test:dist:examples": "npm run build && node node_modules/typescript7/bin/tsc -p site/src/examples/complete/tsconfig.json",
88
+ "test:dist:scaffold-typing": "npm run build && node node_modules/typescript7/bin/tsc -p tests/dist/scaffold-typing/tsconfig.json",
82
89
  "test:browser": "npm run build && playwright test",
83
90
  "bench:micro": "vitest bench --run --config vitest.config.bench.ts",
84
91
  "bench:serve": "bash bench/site.sh",
85
92
  "lint": "eslint src tests",
86
- "typecheck": "tsc --noEmit",
87
- "check": "npm run lint && npm run typecheck && node scripts/check-doc-test-inventory.mjs && node scripts/check-doc-api-coverage.mjs && node scripts/check-feature-coverage.mjs && node scripts/check-ai-bundle.mjs && npm test && npm run build && node scripts/check-doc-api-signatures.mjs && vitest run --config vitest.config.dist.ts && vitest run --config vitest.config.dist-full.ts && tsc -p tests/dist/jsx-typing/tsconfig.json && tsc -p site/src/examples/complete/tsconfig.json && tsc -p tests/dist/scaffold-typing/tsconfig.json && node scripts/check-docs-examples.mjs",
93
+ "typecheck": "node node_modules/typescript7/bin/tsc --noEmit",
94
+ "check": "npm run lint && npm run typecheck && node scripts/check-doc-test-inventory.mjs && node scripts/check-doc-api-coverage.mjs && node scripts/check-doc-site-tickets.mjs && node scripts/check-feature-coverage.mjs && node scripts/check-ai-bundle.mjs && npm test && npm run build && node scripts/check-bundle-size.mjs && node scripts/check-doc-api-signatures.mjs && vitest run --config vitest.config.dist.ts && vitest run --config vitest.config.dist-full.ts && node node_modules/typescript7/bin/tsc -p tests/dist/jsx-typing/tsconfig.json && node node_modules/typescript7/bin/tsc -p site/src/examples/complete/tsconfig.json && node node_modules/typescript7/bin/tsc -p tests/dist/scaffold-typing/tsconfig.json && node scripts/check-docs-examples.mjs",
88
95
  "check:docs:examples": "node scripts/check-docs-examples.mjs",
89
96
  "check:full": "npm run check && playwright test",
90
97
  "check:docs:test-inventory": "node scripts/check-doc-test-inventory.mjs",
91
98
  "check:docs:api-coverage": "node scripts/check-doc-api-coverage.mjs",
99
+ "check:docs:site-tickets": "node scripts/check-doc-site-tickets.mjs",
92
100
  "check:docs:api-signatures": "npm run build && node scripts/check-doc-api-signatures.mjs",
101
+ "fuzz:soak": "node scripts/fuzz-soak.mjs",
93
102
  "check:features": "node scripts/check-feature-coverage.mjs",
94
103
  "check:ai-bundle-in-sync": "node scripts/check-ai-bundle.mjs",
95
104
  "ai-bundle:sync": "node scripts/sync-ai-bundle.mjs",
@@ -103,7 +112,8 @@
103
112
  "example:reactivity-demo:build": "cd examples/reactivity-demo && npm install && npm run build",
104
113
  "site:dev": "cd site && npm install && npm run dev",
105
114
  "site:dev:hmr": "cd site && npm install && npm run dev:hmr",
106
- "site:build": "cd site && npm install && npm run build"
115
+ "site:build": "cd site && npm install && npm run build",
116
+ "check:bundle-size": "npm run build && node scripts/check-bundle-size.mjs"
107
117
  },
108
118
  "dependencies": {
109
119
  "@preact/signals-core": "^1.14.1"
@@ -112,8 +122,8 @@
112
122
  "@playwright/test": "^1.59.1",
113
123
  "@types/jsdom": "^28.0.1",
114
124
  "@types/node": "^22.10.0",
115
- "@typescript-eslint/eslint-plugin": "^8.18.0",
116
- "@typescript-eslint/parser": "^8.18.0",
125
+ "@typescript-eslint/eslint-plugin": "^8.65.0",
126
+ "@typescript-eslint/parser": "^8.65.0",
117
127
  "@vitest/coverage-v8": "^3.0.0",
118
128
  "domotion-svg": "^0.21.1",
119
129
  "eslint": "^9.16.0",
@@ -124,7 +134,8 @@
124
134
  "husky": "^9.1.7",
125
135
  "jsdom": "^29.1.1",
126
136
  "tsup": "^8.3.0",
127
- "typescript": "^5.7.0",
137
+ "typescript": "^6.0.3",
138
+ "typescript7": "npm:typescript@^7.0.2",
128
139
  "vitest": "^3.0.0"
129
140
  }
130
141
  }
@@ -1,131 +0,0 @@
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
@@ -1 +0,0 @@
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"]}
@@ -1,90 +0,0 @@
1
- import { Signal, signal as signal$1, effect as effect$1 } from '@preact/signals-core';
2
- export { batch, computed } from '@preact/signals-core';
3
-
4
- // src/reactive.ts
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
-
14
- // src/dev-delegate-warn.ts
15
- var depth = 0;
16
- var warned = false;
17
- function isOptedIn() {
18
- if (!isDevMode()) return false;
19
- const proc = globalThis.process;
20
- return proc?.env?.KERF_DEV_WARN_DELEGATE_IN_EFFECT === "1";
21
- }
22
- function enterEffect() {
23
- depth++;
24
- }
25
- function exitEffect() {
26
- depth--;
27
- }
28
- function isDevWarnDelegateInEffectEnabled() {
29
- return isOptedIn();
30
- }
31
- function warnIfInsideEffect(fn) {
32
- if (!isOptedIn()) return;
33
- if (depth === 0) return;
34
- if (warned) return;
35
- warned = true;
36
- console.warn(
37
- `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.`
38
- );
39
- }
40
- 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.";
41
- var DevSignal = class extends Signal {
42
- __hasSubscriber = false;
43
- __warned = false;
44
- __constructed = false;
45
- constructor(initial) {
46
- super(initial, {
47
- watched() {
48
- this.__hasSubscriber = true;
49
- }
50
- });
51
- this.__constructed = true;
52
- }
53
- get value() {
54
- return super.value;
55
- }
56
- set value(v) {
57
- super.value = v;
58
- if (this.__constructed && !this.__hasSubscriber && !this.__warned) {
59
- this.__warned = true;
60
- console.warn(WARNING_MESSAGE);
61
- }
62
- }
63
- };
64
- function isDevWarnUntrackedEnabled() {
65
- if (!isDevMode()) return false;
66
- const proc = globalThis.process;
67
- return proc?.env?.KERF_DEV_WARN_UNTRACKED_SIGNALS === "1";
68
- }
69
- function isSignal(value) {
70
- return value instanceof Signal;
71
- }
72
- function signal(value) {
73
- if (isDevWarnUntrackedEnabled()) return new DevSignal(value);
74
- return signal$1(value);
75
- }
76
- function effect(fn) {
77
- if (!isDevWarnDelegateInEffectEnabled()) return effect$1(fn);
78
- return effect$1(() => {
79
- enterEffect();
80
- try {
81
- return fn();
82
- } finally {
83
- exitEffect();
84
- }
85
- });
86
- }
87
-
88
- export { effect, isDevMode, isSignal, signal, warnIfInsideEffect };
89
- //# sourceMappingURL=chunk-NU7YHYEV.js.map
90
- //# sourceMappingURL=chunk-NU7YHYEV.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/devMode.ts","../src/dev-delegate-warn.ts","../src/dev-signal.ts","../src/reactive.ts"],"names":["Signal","coreSignal","coreEffect"],"mappings":"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","file":"chunk-NU7YHYEV.js","sourcesContent":["/**\n * Shared dev-mode gate. One primary export: `isDevMode()`. Every dev-only\n * behavior in kerf — the `defineStore` `get()` snapshot freeze, the each()\n * row-key warning, and the opt-in `KERF_DEV_WARN_*` warning family — routes\n * its \"is this a development build?\" decision through here.\n *\n * Two inputs, override-wins precedence:\n *\n * 1. `globalThis.KERF_DEV` — explicit runtime override. When set to a boolean\n * it WINS unconditionally: `false` forces production behavior (no store\n * freeze, no dev warnings) even under `NODE_ENV=development`; `true` forces\n * development behavior even under `NODE_ENV=production`. Read lazily (at\n * call time, never memoized at import) so a no-bundler consumer loading\n * kerf from a CDN can set it once before mounting and have it take effect.\n *\n * 2. `process.env.NODE_ENV` — the default when no override is present.\n * Development is ON unless `NODE_ENV === 'production'`. Read through\n * `globalThis.process` so the source runs untouched in a browser that has\n * no `process` binding. Keeping this branch is what lets a bundler that\n * statically substitutes `NODE_ENV` continue to dead-code-eliminate the\n * dev paths for bundled production consumers exactly as before.\n *\n * Why the override matters: a no-bundler consumer (importmap, no build step)\n * has no `process`, so without an override the NODE_ENV branch resolves to\n * development-ON — which is the correct, unchanged default. Previously that\n * consumer had NO way to turn it off, leaving the store freeze and warning\n * machinery permanently active in their production deployment. Setting\n * `globalThis.KERF_DEV = false` before mount is the escape hatch.\n *\n * Perf: the reads are a handful of optional-chained property accesses, as\n * cheap as a boolean read. Hot-path callers that ran a cached boolean before\n * (the store `get()` freeze) keep caching the first result per instance rather\n * than probing on every call.\n */\nexport function isDevMode(): boolean {\n const override = (globalThis as { KERF_DEV?: unknown }).KERF_DEV;\n if (typeof override === 'boolean') return override;\n const proc = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process;\n return proc?.env?.NODE_ENV !== 'production';\n}\n","/**\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\nimport { isDevMode } from './utils/devMode.js';\n\nlet depth = 0;\nlet warned = false;\n\nfunction 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_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 `isDevMode()` (NODE_ENV, or a `globalThis.KERF_DEV` override\n * when set) AND `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\nimport { isDevMode } from './utils/devMode.js';\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 if (!isDevMode()) return false;\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\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 when `isDevMode()` is false (i.e. under\n * `NODE_ENV === 'production'`, or a `globalThis.KERF_DEV = false` override) —\n * production always sees the bare `@preact/signals-core` exports with zero\n * overhead.\n */\n\nimport { effect as coreEffect,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 Signal,\n} from '@preact/signals-core';\n\n/**\n * Runtime type guard for a `@preact/signals-core` signal (both `signal()`\n * values and `computed()` values are `Signal` instances). Used by the JSX\n * runtime (KF-294) to detect a signal handed straight into an attribute or\n * text hole — the trigger for a fine-grained binding rather than a snapshot\n * stringify.\n */\nexport function isSignal(value: unknown): value is Signal<unknown> {\n return value instanceof Signal;\n}\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"]}