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
package/dist/dev.js ADDED
@@ -0,0 +1,607 @@
1
+ import { truncateRowHtml } from './chunk-YHH7OUFA.js';
2
+ import { LIST_MARKER_PREFIX } from './chunk-GY4XV2UV.js';
3
+ import { installDevHooks } from './chunk-VVDJLWMP.js';
4
+ export { clearDevHooks, devHooks, installDevHooks } from './chunk-VVDJLWMP.js';
5
+ import { signal, Signal } from '@preact/signals-core';
6
+
7
+ // src/dev-warn-config.ts
8
+ var ENV_NAME = {
9
+ rebuiltListeners: "KERF_DEV_WARN_REBUILT_LISTENERS",
10
+ untrackedSignals: "KERF_DEV_WARN_UNTRACKED_SIGNALS",
11
+ narrowSet: "KERF_DEV_WARN_NARROW_SET",
12
+ delegateInEffect: "KERF_DEV_WARN_DELEGATE_IN_EFFECT",
13
+ eachInMorphSkip: "KERF_DEV_WARN_EACH_IN_MORPH_SKIP",
14
+ duplicateEachKeys: "KERF_DEV_WARN_DUPLICATE_EACH_KEYS",
15
+ staleBinding: "KERF_DEV_WARN_STALE_BINDING",
16
+ valueOnlyRerender: "KERF_DEV_WARN_VALUE_ONLY_RERENDER",
17
+ listRebind: "KERF_DEV_WARN_LIST_REBIND",
18
+ staleIndex: "KERF_DEV_WARN_STALE_INDEX",
19
+ parserRepair: "KERF_DEV_WARN_PARSER_REPAIR",
20
+ invariants: "KERF_DEV_INVARIANTS"
21
+ };
22
+ var overrides = /* @__PURE__ */ new Map();
23
+ function applyWarningOptions(options) {
24
+ for (const [key, value] of Object.entries(options)) {
25
+ const envName = ENV_NAME[key];
26
+ if (envName === void 0 || value === void 0) continue;
27
+ overrides.set(envName, value === false ? false : value === true ? "1" : value);
28
+ }
29
+ }
30
+ function devFlag(envName) {
31
+ const override = overrides.get(envName);
32
+ if (override !== void 0) return override === false ? void 0 : override;
33
+ const proc = globalThis.process;
34
+ return proc?.env?.[envName];
35
+ }
36
+
37
+ // src/dev-binding-warn.ts
38
+ var warnedHoles = /* @__PURE__ */ new Set();
39
+ function isOptedIn() {
40
+ return devFlag("KERF_DEV_WARN_STALE_BINDING") === "1";
41
+ }
42
+ function describeHole(b) {
43
+ return b.kind === "attr" ? `attr '${b.attr}' (id '${b.id}')` : `text (id '${b.id}')`;
44
+ }
45
+ function maybeWarnStaleBinding(prevWired, current) {
46
+ if (!isOptedIn()) return;
47
+ const n = Math.min(prevWired.length, current.length);
48
+ for (let i = 0; i < n; i++) {
49
+ const cur = current[i];
50
+ if (prevWired[i].signal === cur.signal) continue;
51
+ if (warnedHoles.has(cur.id)) continue;
52
+ warnedHoles.add(cur.id);
53
+ console.warn(
54
+ `kerf: fine-grained binding ${describeHole(cur)} switched to a different signal instance on a render whose static-surrounds HTML was byte-for-byte unchanged. On that fast path kerf keeps the original binding effect and does NOT re-bind, so this hole is now stale \u2014 it still tracks the FIRST signal instance and will not reflect the new one. Bind one computed that switches internally (e.g. class={computed(() => cond.value ? sigA.value : sigB.value)}) instead of switching which signal instance you bind (see docs/2-reactivity \xA72.9). Set KERF_DEV_WARN_STALE_BINDING=0 (or unset it) to silence this warning.`
55
+ );
56
+ }
57
+ }
58
+
59
+ // src/dev-delegate-warn.ts
60
+ var depth = 0;
61
+ var warned = false;
62
+ function isOptedIn2() {
63
+ return devFlag("KERF_DEV_WARN_DELEGATE_IN_EFFECT") === "1";
64
+ }
65
+ function enterEffect() {
66
+ depth++;
67
+ }
68
+ function exitEffect() {
69
+ depth--;
70
+ }
71
+ function isDevWarnDelegateInEffectEnabled() {
72
+ return isOptedIn2();
73
+ }
74
+ function warnIfInsideEffect(fn) {
75
+ if (!isOptedIn2()) return;
76
+ if (depth === 0) return;
77
+ if (warned) return;
78
+ warned = true;
79
+ console.warn(
80
+ `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.`
81
+ );
82
+ }
83
+
84
+ // src/dev-each-warn.ts
85
+ var warnedIds = /* @__PURE__ */ new Set();
86
+ function isOptedIn3() {
87
+ return devFlag("KERF_DEV_WARN_EACH_IN_MORPH_SKIP") === "1";
88
+ }
89
+ function hasMorphSkipAncestor(el, root) {
90
+ let ancestor = el.parentElement;
91
+ while (ancestor !== null && ancestor !== root) {
92
+ if (ancestor.dataset.morphSkip !== void 0) return true;
93
+ ancestor = ancestor.parentElement;
94
+ }
95
+ return false;
96
+ }
97
+ function maybeWarnEachInMorphSkip(id, liveParent, rootEl) {
98
+ if (!isOptedIn3()) return;
99
+ if (warnedIds.has(id)) return;
100
+ if (!hasMorphSkipAncestor(liveParent, rootEl)) return;
101
+ warnedIds.add(id);
102
+ console.warn(
103
+ `kerf: each() list '${id}' is inside a data-morph-skip subtree. The keyed reconciler still updates the list rows, but any static signal-reactive JSX inside the same skipped ancestor (e.g. <p>{count.value}</p>) is frozen \u2014 the morph never visits it. Remove data-morph-skip from any element that contains reactive JSX content and reserve it for truly library-owned hosts. Set KERF_DEV_WARN_EACH_IN_MORPH_SKIP=0 (or unset it) to silence this warning.`
104
+ );
105
+ }
106
+ var warnedDupIds = /* @__PURE__ */ new Set();
107
+ function isOptedInDupKeys() {
108
+ return devFlag("KERF_DEV_WARN_DUPLICATE_EACH_KEYS") === "1";
109
+ }
110
+ function maybeWarnDuplicateCacheKeys(id, segItems) {
111
+ if (!isOptedInDupKeys()) return;
112
+ if (warnedDupIds.has(id)) return;
113
+ const seen = /* @__PURE__ */ new Set();
114
+ for (const si of segItems) {
115
+ if (seen.has(si.cacheKey)) {
116
+ warnedDupIds.add(id);
117
+ console.warn(
118
+ `kerf: each() list '${id}' has duplicate cacheKey values (duplicate: ${String(si.cacheKey)}). The cacheKey function should return a unique value per row so kerf can tell apart items for memoization \u2014 duplicate values cause some rows to return stale cached HTML when external state that affects their render changes. Set KERF_DEV_WARN_DUPLICATE_EACH_KEYS=0 (or unset it) to silence this warning.`
119
+ );
120
+ return;
121
+ }
122
+ seen.add(si.cacheKey);
123
+ }
124
+ }
125
+
126
+ // src/dev-invariants.ts
127
+ function mode() {
128
+ const value = devFlag("KERF_DEV_INVARIANTS");
129
+ if (value === "throw") return "throw";
130
+ return value === "1" ? "warn" : "off";
131
+ }
132
+ function listInvariantsEnabled() {
133
+ return mode() !== "off";
134
+ }
135
+ function childIndexer() {
136
+ const cache = /* @__PURE__ */ new Map();
137
+ return (parent, node) => {
138
+ let index = cache.get(parent);
139
+ if (index === void 0) {
140
+ index = /* @__PURE__ */ new Map();
141
+ const kids = parent.childNodes;
142
+ for (let i = 0; i < kids.length; i++) index.set(kids[i], i);
143
+ cache.set(parent, index);
144
+ }
145
+ return index.get(node) ?? -1;
146
+ };
147
+ }
148
+ function describe(id, problem) {
149
+ return `kerf invariant violated after reconcile \u2014 each() list '${id}': ${problem}`;
150
+ }
151
+ function findListInvariantViolations(rootEl, bindings, expectedCounts) {
152
+ const problems = [];
153
+ const owners = /* @__PURE__ */ new Map();
154
+ const indexIn = childIndexer();
155
+ const spans = [];
156
+ for (const [id, binding] of bindings) {
157
+ const { marker, liveParent, items } = binding;
158
+ const expected = expectedCounts?.get(id);
159
+ if (expected !== void 0 && items.length !== expected) {
160
+ problems.push(describe(id, `holds ${items.length} row(s) but its source has ${expected} \u2014 the reconcile dropped or duplicated rows`));
161
+ }
162
+ if (!rootEl.contains(marker)) {
163
+ problems.push(describe(id, "its marker comment is no longer inside the mount root, so every future reconcile would mutate a detached tree"));
164
+ continue;
165
+ }
166
+ if (marker.data !== `${LIST_MARKER_PREFIX}${id}`) {
167
+ problems.push(describe(id, `its marker reads '${marker.data}' \u2014 the id is carried by a different marker node, so this binding describes another list`));
168
+ }
169
+ const markerIndex = indexIn(liveParent, marker);
170
+ if (markerIndex === -1) {
171
+ problems.push(describe(id, "its marker is not a child of the parent the binding records"));
172
+ continue;
173
+ }
174
+ let previousIndex = markerIndex;
175
+ let first = -1;
176
+ for (let i = 0; i < items.length; i++) {
177
+ const node = items[i].node;
178
+ const index = indexIn(liveParent, node);
179
+ if (index === -1) {
180
+ problems.push(describe(id, `bound row ${i} is not a child of the list's parent (<${liveParent.tagName.toLowerCase()}>)${node.isConnected ? " \u2014 it is attached elsewhere in the document" : " \u2014 it is detached"}`));
181
+ continue;
182
+ }
183
+ if (index <= previousIndex) {
184
+ problems.push(describe(id, `bound row ${i} appears at child position ${index}, which is not after the previous one (${previousIndex}) \u2014 the rows are out of order or have crossed the marker`));
185
+ }
186
+ previousIndex = index;
187
+ if (first === -1) first = index;
188
+ const owner = owners.get(node);
189
+ if (owner !== void 0) {
190
+ problems.push(describe(id, `bound row ${i} is also claimed by list '${owner}'`));
191
+ } else {
192
+ owners.set(node, id);
193
+ }
194
+ }
195
+ if (first !== -1) spans.push({ id, parent: liveParent, from: first, to: previousIndex });
196
+ }
197
+ for (let a = 0; a < spans.length; a++) {
198
+ for (let b = a + 1; b < spans.length; b++) {
199
+ if (spans[a].parent !== spans[b].parent) continue;
200
+ if (spans[a].from <= spans[b].to && spans[b].from <= spans[a].to) {
201
+ problems.push(describe(spans[a].id, `its rows (child positions ${spans[a].from}-${spans[a].to}) overlap those of list '${spans[b].id}' (${spans[b].from}-${spans[b].to}) in the same parent`));
202
+ }
203
+ }
204
+ }
205
+ return problems;
206
+ }
207
+ function maybeCheckListInvariants(rootEl, bindings, expectedCounts) {
208
+ const level = mode();
209
+ if (level === "off") return;
210
+ const problems = findListInvariantViolations(rootEl, bindings, expectedCounts);
211
+ if (problems.length === 0) return;
212
+ const report = `${problems.join("\n")}
213
+ This is a kerf bug, not an application one \u2014 please report it with the markup that produced it. Set KERF_DEV_INVARIANTS=1 to warn instead of throw, or unset it to disable these checks.`;
214
+ if (level === "throw") throw new Error(report);
215
+ console.warn(report);
216
+ }
217
+
218
+ // src/dev-list-index-warn.ts
219
+ var warnedIds2 = /* @__PURE__ */ new Set();
220
+ function isOptedInStaleIndex() {
221
+ return devFlag("KERF_DEV_WARN_STALE_INDEX") === "1";
222
+ }
223
+ function maybeWarnStaleIndex(id) {
224
+ if (warnedIds2.has(id)) return;
225
+ warnedIds2.add(id);
226
+ console.warn(
227
+ `kerf: each() list '${id}' reused a memoized row at a new index this render, but its render function takes an \`index\` argument. Rows are memoized by object identity, not by position, so a reorder or an insert/remove ahead of a surviving row serves that row's cached HTML \u2014 computed at its OLD index. A numbered list, zebra striping, or an "N of M" label will silently show the wrong value. If the row output depends on the index, fold it into the memo key: each(items, render, { cacheKey: (_, i) => i }) (combine with your own key if you have one). If the index is not used in the output, ignore this. Set KERF_DEV_WARN_STALE_INDEX=0 (or unset it) to silence this warning.`
228
+ );
229
+ }
230
+
231
+ // src/dev-list-key-warn.ts
232
+ function maybeWarnListIdShift(id) {
233
+ console.warn(
234
+ `kerf each(): list '${id}' is now a different list than it was last render. Lists without a key are identified by call order, so adding or removing an each() call before this one reassigns its identity \u2014 kerf rebuilds the list from scratch and its rows lose DOM identity, focus, scroll position and in-progress IME composition. Give the affected lists a stable key: each(items, render, { key: 'my-list' }). Keying the conditional list is usually enough \u2014 a keyed list does not take a call-order slot, so its siblings stop shifting too.`
235
+ );
236
+ }
237
+
238
+ // src/dev-list-rebind-warn.ts
239
+ var warnedIds3 = /* @__PURE__ */ new Set();
240
+ function isOptedIn4() {
241
+ return devFlag("KERF_DEV_WARN_LIST_REBIND") === "1";
242
+ }
243
+ function maybeWarnListRebind(id, liveParent) {
244
+ if (!isOptedIn4()) return;
245
+ if (warnedIds3.has(id)) return;
246
+ warnedIds3.add(id);
247
+ console.warn(
248
+ `kerf: each() list '${id}' had its container (<${liveParent.tagName.toLowerCase()}>) rebuilt by the morph this render \u2014 an ancestor's tag changed so the subtree was replaced, or a same-tag sibling positionally took the container's place. The list re-binds and repopulates automatically, but its rows were re-created from scratch: focus, scroll positions, in-progress IME composition, and any imperative listeners on the old row nodes are lost. If the rows should survive, give the LIST'S OWN container a stable id/data-key (keying the conditional sibling instead only helps when it is removed, not when it reappears) and keep the tags of the list's ancestors stable across renders. Set KERF_DEV_WARN_LIST_REBIND=0 (or unset it) to silence this warning.`
249
+ );
250
+ }
251
+
252
+ // src/dev-listener-warn.ts
253
+ var LISTENER_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.devListener");
254
+ var patched = false;
255
+ var warned2 = false;
256
+ function isOptedIn5() {
257
+ return devFlag("KERF_DEV_WARN_REBUILT_LISTENERS") === "1";
258
+ }
259
+ function findAddEventListenerProto() {
260
+ const probe = document.createElement("div");
261
+ let proto = Object.getPrototypeOf(probe);
262
+ while (!Object.prototype.hasOwnProperty.call(proto, "addEventListener")) {
263
+ proto = Object.getPrototypeOf(proto);
264
+ }
265
+ return proto;
266
+ }
267
+ function patchAddEventListenerOnce() {
268
+ if (patched) return;
269
+ patched = true;
270
+ const proto = findAddEventListenerProto();
271
+ const orig = proto.addEventListener;
272
+ proto.addEventListener = function(type, listener, options) {
273
+ if (this instanceof Element) {
274
+ this[LISTENER_MARKER] = true;
275
+ }
276
+ return orig.call(this, type, listener, options);
277
+ };
278
+ }
279
+ function hasMarkedListener(el) {
280
+ if (el[LISTENER_MARKER] === true) return true;
281
+ const stack = [];
282
+ for (let i = 0; i < el.children.length; i++) stack.push(el.children[i]);
283
+ while (stack.length > 0) {
284
+ const cur = stack.pop();
285
+ if (cur[LISTENER_MARKER] === true) return true;
286
+ for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
287
+ }
288
+ return false;
289
+ }
290
+ function emitWarning() {
291
+ if (warned2) return;
292
+ warned2 = true;
293
+ console.warn(
294
+ "kerf: a node inside a mount()-managed tree was removed/rebuilt while carrying an imperative addEventListener listener. The listener is gone with the old node. Use `delegate(rootEl, 'click', '[data-action=\"...\"]', handler)` so the listener lives on a stable ancestor and survives re-renders, or wrap the host in `data-morph-skip` if the subtree is library-owned (Monaco, xterm, D3 charts). Set KERF_DEV_WARN_REBUILT_LISTENERS=0 (or unset it) to silence this warning."
295
+ );
296
+ }
297
+ function installListenerRebuildWarn(rootEl) {
298
+ if (!isOptedIn5()) return null;
299
+ patchAddEventListenerOnce();
300
+ const observer = new MutationObserver((mutations) => {
301
+ if (warned2) return;
302
+ for (const m of mutations) {
303
+ for (let i = 0; i < m.removedNodes.length; i++) {
304
+ const removed = m.removedNodes[i];
305
+ if (!(removed instanceof Element)) continue;
306
+ if (hasMarkedListener(removed)) {
307
+ emitWarning();
308
+ return;
309
+ }
310
+ }
311
+ }
312
+ });
313
+ observer.observe(rootEl, { childList: true, subtree: true });
314
+ return observer;
315
+ }
316
+
317
+ // src/dev-parser-repair-warn.ts
318
+ var BLOCK_TAGS = [
319
+ "address",
320
+ "article",
321
+ "aside",
322
+ "blockquote",
323
+ "details",
324
+ "div",
325
+ "dl",
326
+ "fieldset",
327
+ "figcaption",
328
+ "figure",
329
+ "footer",
330
+ "form",
331
+ "h1",
332
+ "h2",
333
+ "h3",
334
+ "h4",
335
+ "h5",
336
+ "h6",
337
+ "header",
338
+ "hgroup",
339
+ "hr",
340
+ "main",
341
+ "menu",
342
+ "nav",
343
+ "ol",
344
+ "p",
345
+ "pre",
346
+ "section",
347
+ "table",
348
+ "ul"
349
+ ];
350
+ var SCAN = new RegExp(`<(/?p|${BLOCK_TAGS.join("|")})[\\s/>]`, "gi");
351
+ var warnedPairs = /* @__PURE__ */ new Set();
352
+ function isOptedIn6() {
353
+ return devFlag("KERF_DEV_WARN_PARSER_REPAIR") === "1";
354
+ }
355
+ function findParagraphRepair(html) {
356
+ SCAN.lastIndex = 0;
357
+ let inParagraph = false;
358
+ for (let m = SCAN.exec(html); m !== null; m = SCAN.exec(html)) {
359
+ const tag = m[1].toLowerCase();
360
+ if (tag === "/p") {
361
+ inParagraph = false;
362
+ continue;
363
+ }
364
+ if (tag === "p") {
365
+ if (inParagraph) return "p";
366
+ inParagraph = true;
367
+ continue;
368
+ }
369
+ if (inParagraph) return tag;
370
+ }
371
+ return null;
372
+ }
373
+ function maybeWarnParserRepair(html) {
374
+ if (!isOptedIn6()) return;
375
+ const tag = findParagraphRepair(html);
376
+ if (tag === null) return;
377
+ const pair = `p>${tag}`;
378
+ if (warnedPairs.has(pair)) return;
379
+ warnedPairs.add(pair);
380
+ console.warn(
381
+ `kerf: a <${tag}> inside a <p> will not survive parsing. <p> may contain only phrasing content, so the HTML parser closes it before a block-level child \u2014 your <p> ends up EMPTY and the <${tag}> (plus everything after it) becomes its sibling instead of its child. kerf then reconciles that repaired tree correctly, so updates still work; what you lose is the structure you wrote, along with any CSS or querySelector that assumed it. Use a <div> (or a phrasing element like <span>) in place of the <p>, or move the block content outside it. Set KERF_DEV_WARN_PARSER_REPAIR=0 (or unset it) to silence this warning.`
382
+ );
383
+ }
384
+
385
+ // src/dev-rerender-warn.ts
386
+ function isOptedIn7() {
387
+ return devFlag("KERF_DEV_WARN_VALUE_ONLY_RERENDER") === "1";
388
+ }
389
+ var ELEMENT_NODE = 1;
390
+ var TEXT_NODE = 3;
391
+ var COMMENT_NODE = 8;
392
+ function _isValueOnlyDiff(a, b) {
393
+ const an = a.childNodes;
394
+ const bn = b.childNodes;
395
+ if (an.length !== bn.length) return false;
396
+ for (let i = 0; i < an.length; i++) {
397
+ const x = an[i];
398
+ const y = bn[i];
399
+ if (x.nodeType !== y.nodeType) return false;
400
+ if (x.nodeType === ELEMENT_NODE) {
401
+ if (x.tagName !== y.tagName) return false;
402
+ if (!_isValueOnlyDiff(x, y)) return false;
403
+ } else if (x.nodeType === COMMENT_NODE) {
404
+ if (x.data !== y.data) return false;
405
+ } else if (x.nodeType !== TEXT_NODE) {
406
+ return false;
407
+ }
408
+ }
409
+ return true;
410
+ }
411
+ function maybeWarnValueOnlyRerender(prevHtml, nextHtml, ctx) {
412
+ if (ctx.warned || !isOptedIn7()) return;
413
+ const a = document.createElement("template");
414
+ const b = document.createElement("template");
415
+ a.innerHTML = prevHtml;
416
+ b.innerHTML = nextHtml;
417
+ if (!_isValueOnlyDiff(a.content, b.content)) return;
418
+ ctx.warned = true;
419
+ console.warn(
420
+ "kerf: this re-render changed only text content and attribute values \u2014 no structural change \u2014 so every changed hole could be a fine-grained binding instead. Values bind, structure re-renders: pass the signal/computed itself ({count}, class={sig}) rather than reading .value in the hole, and each change updates just that node with no render re-run (a mount whose render reads no .value never re-renders at all). See docs/2-reactivity \xA72.9. Set KERF_DEV_WARN_VALUE_ONLY_RERENDER=0 (or unset it) to silence this warning."
421
+ );
422
+ }
423
+
424
+ // src/dev-row-key-warn.ts
425
+ function maybeWarnMissingRowKey(rowEl, rowHtml, binding) {
426
+ if (binding.warnedMissingKey === true) return;
427
+ binding.warnedMissingKey = true;
428
+ if (rowEl.id !== "" || rowEl.hasAttribute("data-key")) return;
429
+ console.warn(
430
+ `kerf each(): the first row has no \`id\` or \`data-key\` attribute. Without one, rows match positionally \u2014 an insert/remove at the head shifts every row's identity, so focused inputs jump to the wrong row, mid-edit textareas swap content with their neighbor, and any per-row state silently follows the wrong item. Add \`data-key={item.id}\` (or set \`id\`) to the top-level element returned by the row render. Row HTML: ${JSON.stringify(truncateRowHtml(rowHtml))}`
431
+ );
432
+ }
433
+ 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.";
434
+ var DevSignal = class extends Signal {
435
+ __hasSubscriber = false;
436
+ __warned = false;
437
+ __constructed = false;
438
+ constructor(initial) {
439
+ super(initial, {
440
+ watched() {
441
+ this.__hasSubscriber = true;
442
+ }
443
+ });
444
+ this.__constructed = true;
445
+ }
446
+ get value() {
447
+ return super.value;
448
+ }
449
+ set value(v) {
450
+ super.value = v;
451
+ if (this.__constructed && !this.__hasSubscriber && !this.__warned) {
452
+ this.__warned = true;
453
+ console.warn(WARNING_MESSAGE);
454
+ }
455
+ }
456
+ };
457
+ function isDevWarnUntrackedEnabled() {
458
+ return devFlag("KERF_DEV_WARN_UNTRACKED_SIGNALS") === "1";
459
+ }
460
+ var coverageNoticeShown = false;
461
+ function noteUntrackedCoverage() {
462
+ if (coverageNoticeShown) return;
463
+ coverageNoticeShown = true;
464
+ console.warn(
465
+ "kerf: KERF_DEV_WARN_UNTRACKED_SIGNALS only covers signals created AFTER kerfjs/dev is installed. Static imports are hoisted above `await import('kerfjs/dev')`, so module-scope signals in the modules you import are created first and this warning cannot see them \u2014 you may get no warnings even where the bug exists. To cover them, make `import 'kerfjs/dev'` the FIRST STATIC import of a dev-only entry file (static imports evaluate in order), then load the rest of your app. Set KERF_DEV_WARN_UNTRACKED_SIGNALS=0 (or unset it) to silence this warning."
466
+ );
467
+ }
468
+
469
+ // src/dev-store-warn.ts
470
+ var WARNING_PREFIX = "kerf: defineStore.set() called with keys missing from the current state \u2014 ";
471
+ 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.";
472
+ function isOptedIn8() {
473
+ return devFlag("KERF_DEV_WARN_NARROW_SET") === "1";
474
+ }
475
+ function isPlainObjectState(v) {
476
+ if (v === null || typeof v !== "object") return false;
477
+ if (Array.isArray(v)) return false;
478
+ return true;
479
+ }
480
+ function maybeWarnNarrowSet(prev, next, ctx) {
481
+ if (ctx.warned) return;
482
+ if (!isOptedIn8()) return;
483
+ if (!isPlainObjectState(prev) || !isPlainObjectState(next)) return;
484
+ const missing = [];
485
+ for (const k of Object.keys(prev)) {
486
+ if (!(k in next)) missing.push(k);
487
+ }
488
+ if (missing.length === 0) return;
489
+ ctx.warned = true;
490
+ const keysList = missing.map((k) => `\`${k}\``).join(", ");
491
+ console.warn(`${WARNING_PREFIX}${keysList}${WARNING_SUFFIX}`);
492
+ }
493
+
494
+ // src/utils/devReadonly.ts
495
+ 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.";
496
+ var proxyToRaw = /* @__PURE__ */ new WeakMap();
497
+ var rawToProxy = /* @__PURE__ */ new WeakMap();
498
+ function isWrappable(v) {
499
+ if (v === null || typeof v !== "object") return false;
500
+ if (Array.isArray(v)) return true;
501
+ const proto = Object.getPrototypeOf(v);
502
+ return proto === Object.prototype || proto === null;
503
+ }
504
+ var handler = {
505
+ get(target, prop, receiver) {
506
+ const value = Reflect.get(target, prop, receiver);
507
+ return isWrappable(value) ? devReadonlyProxy(value) : value;
508
+ },
509
+ set() {
510
+ throw new TypeError(RULE_MESSAGE);
511
+ },
512
+ deleteProperty() {
513
+ throw new TypeError(RULE_MESSAGE);
514
+ },
515
+ defineProperty() {
516
+ throw new TypeError(RULE_MESSAGE);
517
+ }
518
+ };
519
+ function devReadonlyProxy(obj) {
520
+ if (proxyToRaw.has(obj)) return obj;
521
+ const cached = rawToProxy.get(obj);
522
+ if (cached) return cached;
523
+ const p = new Proxy(obj, handler);
524
+ rawToProxy.set(obj, p);
525
+ proxyToRaw.set(p, obj);
526
+ return p;
527
+ }
528
+ function toRaw(value) {
529
+ return unwrap(value);
530
+ }
531
+ function unwrap(v) {
532
+ if (v === null || typeof v !== "object") return v;
533
+ const raw = proxyToRaw.get(v);
534
+ if (raw !== void 0) return raw;
535
+ if (!isWrappable(v)) return v;
536
+ if (Array.isArray(v)) {
537
+ let changed2 = false;
538
+ const out2 = v.map((item) => {
539
+ const u = unwrap(item);
540
+ if (u !== item) changed2 = true;
541
+ return u;
542
+ });
543
+ return changed2 ? out2 : v;
544
+ }
545
+ let changed = false;
546
+ const src = v;
547
+ const out = {};
548
+ for (const k of Object.keys(src)) {
549
+ const u = unwrap(src[k]);
550
+ if (u !== src[k]) changed = true;
551
+ out[k] = u;
552
+ }
553
+ return changed ? out : v;
554
+ }
555
+
556
+ // src/dev.ts
557
+ function enableWarnings(options) {
558
+ applyWarningOptions(options);
559
+ if (isDevWarnUntrackedEnabled()) noteUntrackedCoverage();
560
+ }
561
+ var DEV_HOOKS = {
562
+ // --- reactive ---------------------------------------------------------
563
+ signalFactory: (value) => isDevWarnUntrackedEnabled() ? new DevSignal(value) : signal(value),
564
+ wrapEffect: (fn) => {
565
+ if (!isDevWarnDelegateInEffectEnabled()) return fn;
566
+ return () => {
567
+ enterEffect();
568
+ try {
569
+ return fn();
570
+ } finally {
571
+ exitEffect();
572
+ }
573
+ };
574
+ },
575
+ // --- delegate ---------------------------------------------------------
576
+ delegateInEffect: warnIfInsideEffect,
577
+ // --- store ------------------------------------------------------------
578
+ narrowSet: maybeWarnNarrowSet,
579
+ storeReadonly: devReadonlyProxy,
580
+ storeToRaw: toRaw,
581
+ // --- mount ------------------------------------------------------------
582
+ listenerRebuild: installListenerRebuildWarn,
583
+ listIdShift: maybeWarnListIdShift,
584
+ parserRepair: maybeWarnParserRepair,
585
+ staleBindingEnabled: isOptedIn,
586
+ staleBinding: maybeWarnStaleBinding,
587
+ listInvariantsEnabled,
588
+ listInvariants: maybeCheckListInvariants,
589
+ valueOnlyRerender: maybeWarnValueOnlyRerender,
590
+ listRebind: maybeWarnListRebind,
591
+ eachInMorphSkip: maybeWarnEachInMorphSkip,
592
+ missingRowKey: maybeWarnMissingRowKey,
593
+ // --- each -------------------------------------------------------------
594
+ staleIndexEnabled: isOptedInStaleIndex,
595
+ staleIndex: maybeWarnStaleIndex,
596
+ duplicateCacheKeys: maybeWarnDuplicateCacheKeys,
597
+ // --- urlScreen --------------------------------------------------------
598
+ urlScreenThrow: (message) => {
599
+ throw new Error(message);
600
+ }
601
+ };
602
+ installDevHooks(DEV_HOOKS);
603
+ if (isDevWarnUntrackedEnabled()) noteUntrackedCoverage();
604
+
605
+ export { DEV_HOOKS, enableWarnings };
606
+ //# sourceMappingURL=dev.js.map
607
+ //# sourceMappingURL=dev.js.map