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.d.ts ADDED
@@ -0,0 +1,339 @@
1
+ import { B as Binding } from './bindings-CYwoJpQb.js';
2
+ import { Signal } from '@preact/signals-core';
3
+
4
+ /**
5
+ * Per-list binding shape kept by `mount()` and consumed by both reconcile
6
+ * paths in `list-reconcile-snapshot.ts` and `list-reconcile-granular.ts`.
7
+ *
8
+ * Living in its own file (not re-exported through `list-reconcile.ts`) so
9
+ * the snapshot + granular paths can import it without creating a circular
10
+ * dependency on `list-reconcile.ts` itself. ESM handles cycles via function
11
+ * hoisting, but a cycle involving a non-function helper would be a temporal
12
+ * dead zone — extracting the binding shape + `endAnchor` here keeps the
13
+ * dependency graph acyclic.
14
+ */
15
+
16
+ interface BoundItem {
17
+ ref: object;
18
+ cacheKey: unknown;
19
+ html: string;
20
+ node: Element;
21
+ /** KF-294: this row's fine-grained binding specs (undefined/empty if none). */
22
+ bindings?: Binding[];
23
+ /**
24
+ * KF-294: live disposers for this row's bound effects. Set when the row node
25
+ * is wired (first-render inline in `mount()`, or `buildFreshNodes` during a
26
+ * reconcile) and called when the node is removed / the mount is torn down.
27
+ */
28
+ bindingDisposers?: Array<() => void>;
29
+ }
30
+ interface ListBinding {
31
+ liveParent: Element;
32
+ /**
33
+ * One entry per item currently mounted under `liveParent`, in order.
34
+ * Mirrors the segment's `items` length after each reconcile.
35
+ */
36
+ items: BoundItem[];
37
+ /**
38
+ * The list's `<!--kf-list:N-->` start marker, kept in the live DOM as
39
+ * a permanent anchor (KF-102 round 2). The marker stays put across
40
+ * static-surrounds diffs (it morphs as a comment node), so it gives
41
+ * the list reconciler a stable "begin" position even when surrounding
42
+ * siblings get inserted, removed, or reordered around the list.
43
+ *
44
+ * `endAnchor(binding)` derives the "insert at end of list" anchor from
45
+ * `marker.nextSibling` (empty list) or `items[last].node.nextSibling`
46
+ * (non-empty list) — picking up whatever the diff placed between the
47
+ * list and the parent's tail.
48
+ */
49
+ marker: Comment;
50
+ /**
51
+ * KF-173: once we've emitted the missing-key dev warning for this list,
52
+ * we suppress further warnings for the same binding. The flag is set
53
+ * inside `maybeWarnMissingRowKey()` and never cleared — the list is
54
+ * considered "decided" after the first row check.
55
+ */
56
+ warnedMissingKey?: boolean;
57
+ }
58
+
59
+ /**
60
+ * The dev-hook registry — kerf's single seam between production code and the
61
+ * opt-in development diagnostics.
62
+ *
63
+ * ## Why this exists
64
+ *
65
+ * kerf used to INFER whether it was running in development, by reading
66
+ * `globalThis.process?.env?.NODE_ENV` through `utils/devMode.ts`. That
67
+ * inference cannot be made correct, and it was wrong in the most common case:
68
+ * bundlers substitute the BARE `process.env.NODE_ENV` token and never create a
69
+ * `globalThis.process` object for browser targets, so the read returned
70
+ * `undefined` and `undefined !== 'production'` resolved to DEVELOPMENT inside
71
+ * production browser bundles.
72
+ *
73
+ * It also could not be fixed by rewriting the expression. Only the
74
+ * *production* answer can be made static: `X && false` folds to a constant for
75
+ * any side-effect-free `X`, while `X && true` does not. So any form that a
76
+ * bundler can eliminate is also a form that treats "no `process` binding" as
77
+ * production — which silently disables every warning in the no-build/CDN path
78
+ * and in browser dev bundles.
79
+ *
80
+ * The fix is to stop guessing. Every environment already has a correct,
81
+ * statically-foldable dev flag; what none of them offers is a way to hand that
82
+ * flag to a *library*. So the consumer writes the conditional, in their own
83
+ * code, with their own flag:
84
+ *
85
+ * ```js
86
+ * if (import.meta.env.DEV) await import('kerfjs/dev'); // Vite
87
+ * if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');
88
+ * ```
89
+ *
90
+ * Because that condition folds to `false` in the consumer's production build,
91
+ * the entire statement is eliminated and the dev chunk is never emitted or
92
+ * fetched. Installation IS the development signal — there is nothing left to
93
+ * detect, and no environment kerf can be wrong about.
94
+ *
95
+ * ## The contract
96
+ *
97
+ * Core modules never import a `dev-*` module. They read a nullable slot off
98
+ * `devHooks` and call through it:
99
+ *
100
+ * ```ts
101
+ * devHooks.listRebind?.(id, marker.parentElement as Element);
102
+ * ```
103
+ *
104
+ * When nothing is installed every slot is `undefined`, so the cost is one
105
+ * property read per call site and the `dev-*` modules are unreachable from the
106
+ * main entry — which is what lets a bundler drop them. This is why the gate
107
+ * lives at the CALL SITE rather than inside each warner: an unconditional call
108
+ * into a self-gating warner keeps the module reachable no matter how the gate
109
+ * is written, so no amount of dead-code elimination can reclaim it.
110
+ *
111
+ * Slots ending in `Enabled` are predicates rather than warnings. They exist for
112
+ * the handful of call sites that must decide whether to do *expensive
113
+ * preparatory work* — capturing the previous render's binding list, allocating
114
+ * a per-render `Map` — before there is anything to warn about. Core must check
115
+ * those before paying the cost, exactly as it checked the old `isOptedIn()`
116
+ * exports.
117
+ *
118
+ * Each warner keeps its own internal opt-in check (the `KERF_DEV_WARN_*` env
119
+ * reads). Installation decides whether the diagnostics are *present*; the
120
+ * individual warner still decides whether it is *switched on*.
121
+ *
122
+ * @see docs/11-dev-warnings.md
123
+ */
124
+
125
+ /** Per-mount / per-store one-shot dedup flag, owned by the caller in core. */
126
+ interface WarnOnceContext {
127
+ warned: boolean;
128
+ }
129
+ interface DevHooks {
130
+ /**
131
+ * Replaces `signal()`'s constructor so writes to never-subscribed signals can
132
+ * warn. Resolved at signal-CREATION time, so signals created before the dev
133
+ * entry is installed stay plain — see `signalsCreatedBeforeInstall`.
134
+ */
135
+ signalFactory?: <T>(value: T) => Signal<T>;
136
+ /**
137
+ * Wraps an `effect()` body so `delegate()` can detect that it is running
138
+ * inside one. Returns the body to actually run.
139
+ */
140
+ wrapEffect?: (fn: () => void | (() => void)) => () => void | (() => void);
141
+ delegateInEffect?: (fn: 'delegate' | 'delegateCapture') => void;
142
+ narrowSet?: (prev: unknown, next: unknown, ctx: WarnOnceContext) => void;
143
+ /** Deep read-only proxy for the `get()` snapshot, so stray writes throw. */
144
+ storeReadonly?: <T extends object>(state: T) => T;
145
+ /** Unwraps a proxy handed back through `set({ ...get() })`. */
146
+ storeToRaw?: <T>(next: T) => T;
147
+ listenerRebuild?: (rootEl: Element) => MutationObserver | null;
148
+ listIdShift?: (id: string) => void;
149
+ parserRepair?: (html: string) => void;
150
+ staleBindingEnabled?: () => boolean;
151
+ staleBinding?: (prevWired: readonly Binding[], current: readonly Binding[]) => void;
152
+ listInvariantsEnabled?: () => boolean;
153
+ listInvariants?: (rootEl: Element, bindings: ReadonlyMap<string, ListBinding>, expectedCounts?: ReadonlyMap<string, number>) => void;
154
+ valueOnlyRerender?: (prevHtml: string, nextHtml: string, ctx: WarnOnceContext) => void;
155
+ listRebind?: (id: string, liveParent: Element) => void;
156
+ eachInMorphSkip?: (id: string, liveParent: Element, rootEl: Element) => void;
157
+ missingRowKey?: (rowEl: Element, rowHtml: string, binding: {
158
+ warnedMissingKey?: boolean;
159
+ }) => void;
160
+ staleIndexEnabled?: () => boolean;
161
+ staleIndex?: (id: string) => void;
162
+ duplicateCacheKeys?: (id: string, segItems: readonly {
163
+ cacheKey: unknown;
164
+ }[]) => void;
165
+ /**
166
+ * When installed, a screened URL throws instead of warning-and-dropping.
167
+ * A slot rather than a boolean so the check stays uniform with the rest.
168
+ */
169
+ urlScreenThrow?: (message: string) => never;
170
+ }
171
+ /**
172
+ * The live slot table. Mutable by design — this is the fourth sanctioned
173
+ * module-level mutable location (Design rule 5), and like `store.ts:REGISTRY`
174
+ * it depends on there being exactly ONE copy at runtime. `tsup`'s
175
+ * `splitting: true` guarantees that: shared modules are promoted into a single
176
+ * chunk that both the main entry and the `kerfjs/dev` entry import.
177
+ */
178
+ declare const devHooks: DevHooks;
179
+ /**
180
+ * Install (or extend) the dev hooks. Called by the `kerfjs/dev` entry; not part
181
+ * of the public API surface.
182
+ *
183
+ * Merges rather than replaces, so a consumer can install the standard bundle
184
+ * and then override a single slot in a test.
185
+ */
186
+ declare function installDevHooks(hooks: DevHooks): void;
187
+ /**
188
+ * Remove every installed hook. Exists for test isolation — a suite that asserts
189
+ * the not-installed (production-shaped) path needs to get back to a clean slate
190
+ * without reloading modules.
191
+ */
192
+ declare function clearDevHooks(): void;
193
+
194
+ /**
195
+ * The switch layer for the opt-in diagnostics — the second of the two gates
196
+ * (§11.3.1). The first gate is installation: reaching this module at all means
197
+ * the consumer imported `kerfjs/dev`.
198
+ *
199
+ * Two sources feed one lookup, explicit-call-wins:
200
+ *
201
+ * 1. `enableWarnings({...})` — an in-memory map, set by the consumer through
202
+ * the `kerfjs/dev` entry. Explicit wins in BOTH directions, so
203
+ * `{ narrowSet: false }` silences a warning an ambient env var switched on.
204
+ * 2. `globalThis.process?.env?.KERF_DEV_*` — the environment, kept for Node,
205
+ * SSR, and CI, where exporting a variable is the natural way to turn a
206
+ * diagnostic on for one run.
207
+ *
208
+ * ## Why the env var could not be the only switch
209
+ *
210
+ * It is unreachable in the majority case. kerf is a browser framework, and a
211
+ * browser realm has no `process` object at all — so every one of these
212
+ * warnings was permanently off in exactly the environment (a Vite/webpack dev
213
+ * server) where a developer most wants them. A bundler `define` does not fix
214
+ * it either: the read goes through `globalThis.process` into a local binding,
215
+ * so nothing substitutes the `process.env.X` token. That indirection is not
216
+ * incidental — reading the bare token is what made kerf infer DEVELOPMENT
217
+ * inside production browser bundles.
218
+ *
219
+ * The consumer already holds the module at the moment they opt in
220
+ * (`const dev = await import('kerfjs/dev')`), so handing them a typed function
221
+ * there is both the most reachable and the most discoverable switch. The env
222
+ * vars stay because kerf's own suites and any CI run use them.
223
+ */
224
+ /**
225
+ * Which diagnostics to switch on. Every key is off unless you name it; passing
226
+ * `false` explicitly forces a warning off even when its env var is set.
227
+ *
228
+ * `invariants` is not a warning but the structural audit of kerf's own list
229
+ * bookkeeping: `true` reports violations with `console.warn`, `'throw'` raises
230
+ * them (what you want in a test suite, where a warning inside a passing test
231
+ * is invisible).
232
+ */
233
+ interface DevWarningOptions {
234
+ /** Imperative `addEventListener` on a node the morph later rebuilds. */
235
+ rebuiltListeners?: boolean;
236
+ /** A `.value` write to a signal that never had a subscriber. */
237
+ untrackedSignals?: boolean;
238
+ /** `defineStore` `set()` called with keys missing from the current state. */
239
+ narrowSet?: boolean;
240
+ /** `delegate()` called inside an `effect()` body — listeners stack up. */
241
+ delegateInEffect?: boolean;
242
+ /** An `each()` list under a `data-morph-skip` subtree. */
243
+ eachInMorphSkip?: boolean;
244
+ /** Two rows in one `each()` producing the same `cacheKey`. */
245
+ duplicateEachKeys?: boolean;
246
+ /** A fine-grained binding switching signal instance on the fast path. */
247
+ staleBinding?: boolean;
248
+ /** A re-render whose whole diff was values — candidates for bindings. */
249
+ valueOnlyRerender?: boolean;
250
+ /** An `each()` container rebuilt by the morph and self-healed. */
251
+ listRebind?: boolean;
252
+ /** A memoized row reused at a different index than it rendered at. */
253
+ staleIndex?: boolean;
254
+ /** Markup the HTML parser repaired (a block element inside a `<p>`). */
255
+ parserRepair?: boolean;
256
+ /** Structural audit of kerf's list bookkeeping: warn, or `'throw'`. */
257
+ invariants?: boolean | 'throw';
258
+ }
259
+
260
+ /**
261
+ * `kerfjs/dev` — the development diagnostics bundle.
262
+ *
263
+ * Importing this module installs kerf's dev-only behavior: the whole
264
+ * `KERF_DEV_WARN_*` warning family, the structural list invariants, the
265
+ * read-only store snapshot, and the throw-on-dangerous-URL screen. Importing
266
+ * nothing leaves every hook slot `undefined` and kerf runs in production shape.
267
+ *
268
+ * Put the import behind YOUR environment's dev flag, in YOUR code. That
269
+ * condition folds to `false` in your production build, so the whole statement
270
+ * is eliminated and this chunk is never emitted or fetched:
271
+ *
272
+ * ```js
273
+ * if (import.meta.env.DEV) await import('kerfjs/dev'); // Vite
274
+ * if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev'); // webpack / Node
275
+ * ```
276
+ *
277
+ * No-build / CDN consumers import it unconditionally from their dev page and
278
+ * simply leave it out of the production page — there is no bundler to fold the
279
+ * condition, and nothing for kerf to detect.
280
+ *
281
+ * ## Install ordering
282
+ *
283
+ * Every hook except one is read at CALL time (render, reconcile, `set()`,
284
+ * `delegate()`), so installing any time before your first `mount()` is enough.
285
+ *
286
+ * The exception is `signalFactory`: `signal()` picks its constructor when the
287
+ * signal is CREATED. Static imports are hoisted above a top-level
288
+ * `await import()`, so module-scope signals in imported modules are created
289
+ * before this module runs and the untracked-signal warning will not see them.
290
+ * To cover those, make `import 'kerfjs/dev'` the FIRST STATIC import of a
291
+ * dev-only entry file, then load the rest of your app.
292
+ *
293
+ * Opting into that warning prints this boundary once, so the gap is loud
294
+ * rather than silent. It cannot be closed by retro-fitting existing signals:
295
+ * `Signal.prototype`'s `value` accessor is non-configurable, and reaching live
296
+ * instances would require a per-signal registry that production would pay for.
297
+ *
298
+ * Installation decides whether the diagnostics are PRESENT; each individual
299
+ * warner still reads its own `KERF_DEV_WARN_*` env var to decide whether it is
300
+ * switched ON. That keeps the existing opt-in contract intact.
301
+ *
302
+ * @see docs/11-dev-warnings.md
303
+ */
304
+
305
+ /**
306
+ * Switch individual diagnostics on:
307
+ *
308
+ * ```js
309
+ * if (import.meta.env.DEV) {
310
+ * const dev = await import('kerfjs/dev');
311
+ * dev.enableWarnings({ staleBinding: true, narrowSet: true });
312
+ * }
313
+ * ```
314
+ *
315
+ * Installing this module makes the diagnostics PRESENT; this decides which are
316
+ * SWITCHED ON, so the console isn't flooded by warnings you didn't ask for. The
317
+ * `KERF_DEV_WARN_*` environment variables do the same thing for Node, SSR, and
318
+ * CI; an explicit call here wins over the environment in both directions, so
319
+ * `{ narrowSet: false }` silences an ambient var.
320
+ *
321
+ * **In a browser this function is the only switch that works.** A browser realm
322
+ * has no `process` object, and a bundler `define` cannot reach the read (it
323
+ * goes through `globalThis.process` into a local binding), so the environment
324
+ * variables are unreachable there.
325
+ *
326
+ * Call it as early as you can. Every diagnostic reads its switch at call time
327
+ * except the untracked-signal warning, which is chosen when a signal is
328
+ * CREATED — enabling that one prints its coverage boundary once.
329
+ */
330
+ declare function enableWarnings(options: DevWarningOptions): void;
331
+ /**
332
+ * The standard bundle of hooks this entry installs. Exported so kerf's own
333
+ * suites can drop back to the production shape (`clearDevHooks()`) and restore
334
+ * afterwards — re-importing this module would not re-run the install, since
335
+ * module evaluation happens once.
336
+ */
337
+ declare const DEV_HOOKS: DevHooks;
338
+
339
+ export { DEV_HOOKS, type DevHooks, type DevWarningOptions, clearDevHooks, devHooks, enableWarnings, installDevHooks };