kerfjs 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ArraySignal } from './array-signal.js';
2
2
  import { SafeHtml } from './jsx-runtime.js';
3
3
  export { Fragment, isSafeHtml, raw } from './jsx-runtime.js';
4
- export { ReadonlySignal, Signal, batch, computed, effect, signal } from '@preact/signals-core';
4
+ import { Signal } from '@preact/signals-core';
5
+ export { ReadonlySignal, Signal, batch, computed, effect } from '@preact/signals-core';
5
6
  export { S as Store, d as defineStore, r as resetAllStores } from './testing-CdMgVVoI.js';
6
7
 
7
8
  /**
@@ -60,39 +61,104 @@ declare function delegate(rootEl: HTMLElement, type: string, selector: string, h
60
61
  declare function delegateCapture(rootEl: HTMLElement, type: string, selector: string, handler: Handler): () => void;
61
62
 
62
63
  /**
63
- * `each(items, render, key?)` — keyed list iteration with per-item memoisation.
64
+ * `each(items, render, cacheKey?)` — keyed list iteration with per-item memoization.
64
65
  *
65
66
  * Drops in as the body of a list-rendering JSX expression inside a `mount()`
66
67
  * render function. Returns a `SafeHtml` carrying a structured list segment,
67
68
  * so `mount()` can run a native keyed reconciler instead of the general-
68
69
  * purpose morph for these children.
69
70
  *
70
- * Two layers of optimisation:
71
+ * Two layers of optimization:
71
72
  *
72
- * 1. Per-item memoisation. `render(item)` is skipped for items whose object
73
- * identity (and optional `key`) are unchanged since the previous call.
74
- * Their HTML strings come from a `WeakMap` keyed by item reference. The
75
- * immutable-update style ("replace the row object" instead of "mutate it")
76
- * makes the cache work automatically.
73
+ * 1. Per-item memoization. `render(item)` is skipped for items whose object
74
+ * identity (and optional `cacheKey`) are unchanged since the previous
75
+ * call. Their HTML strings come from a `WeakMap` keyed by item reference.
76
+ * The immutable-update style ("replace the row object" instead of "mutate
77
+ * it") makes the cache work automatically.
77
78
  *
78
- * 2. Structural handoff. `mount()` recognises the list segment and bypasses
79
+ * 2. Structural handoff. `mount()` recognizes the list segment and bypasses
79
80
  * the parse-the-whole-table round trip: only fresh items get parsed (one
80
81
  * at a time, into the smallest detached element), and only changed rows
81
82
  * get patched in the live DOM. Unchanged rows are physically the same
82
83
  * nodes they were before — never visited.
83
84
  *
84
- * `key` covers the case where external state, not the item itself, drives
85
- * what the row should render (e.g. a "currently selected" id flips a CSS
86
- * class on one row). Same item identity but a different `key` value means
87
- * "re-render this item." If you don't pass `key`, only identity changes
88
- * invalidate.
85
+ * `cacheKey` is a passive comparator — it covers the case where external
86
+ * state, not the item itself, drives what the row should render (e.g. a
87
+ * "currently selected" id flips a CSS class on one row). Same item identity
88
+ * but a different `cacheKey` return value means "the cached HTML is stale —
89
+ * re-render this row." Not a reactive subscription: it's evaluated once per
90
+ * mount-effect run and compared against the previous run's return value. If
91
+ * you don't pass `cacheKey`, only object-identity changes invalidate the
92
+ * cache. (Renamed from `key` for clarity — it shared a name with React's
93
+ * `key` prop but has different semantics; the new name says what the
94
+ * parameter actually does. Positional callers — the canonical form — are
95
+ * unaffected.)
89
96
  *
90
97
  * Items must be objects (cache is a `WeakMap`); wrap primitives if you need
91
98
  * to iterate them. Each item's render output must produce exactly one
92
99
  * top-level element — the list reconciler binds one live DOM node per item.
93
100
  */
94
101
 
95
- declare function each<T extends object>(items: readonly T[] | ArraySignal<T>, render: (item: T, index: number) => SafeHtml | string, key?: (item: T, index: number) => unknown): SafeHtml;
102
+ declare function each<T extends object>(items: readonly T[] | ArraySignal<T>, render: (item: T, index: number) => SafeHtml | string, cacheKey?: (item: T, index: number) => unknown): SafeHtml;
103
+
104
+ /**
105
+ * `morph(liveRoot, template)` — minimum-mutation DOM reconciliation.
106
+ *
107
+ * Public primitive (KF-150): one-shot reconciliation against an
108
+ * already-populated element. `mount()` first paints by writing
109
+ * `innerHTML`; `morph()` is the "I have a live tree and a template,
110
+ * reconcile them in place" sibling. Use it for SSR hydration of static
111
+ * fragments, page-refresh diffs, third-party widget remounts, etc.
112
+ *
113
+ * Replaces our previous dependency on `morphdom`. The algorithm is the
114
+ * classic two-tree walk: match children by key (id, then data-key, then
115
+ * positional same-tag), morph matches in place, insert / remove / clone
116
+ * the rest. Specialized for what kerf needs:
117
+ *
118
+ * - `childrenOnly` is always true; the live root is never replaced.
119
+ * - Per-element short-circuits: `data-morph-skip` (library-owned, leave
120
+ * element AND subtree verbatim), `data-morph-skip-children` (KF-152 —
121
+ * morph attrs on the element, leave its subtree verbatim — for
122
+ * client-hydrated slots whose loading/state classes still need to flow
123
+ * through), and `isEqualNode` (byte-identical, no work needed).
124
+ * - **`data-morph-preserve`** (KF-151) is honored in the trailing-removal
125
+ * pass: an unmatched live element with this attribute is skipped instead
126
+ * of removed. Lets imperatively-injected nodes (autoplay `<video>`s,
127
+ * tour-widget tooltips, analytics pixels) survive across renders without
128
+ * having to `data-morph-skip` the entire parent.
129
+ * - **`ownedItems`** is the set of element nodes owned by an `each()`
130
+ * list reconciler. The morph skips them in every children walk —
131
+ * they're not added to the keyed-lookup map, the from-cursor advances
132
+ * past them, and the trailing-removal pass leaves them in place. This
133
+ * lets each() coexist with non-list siblings inside the same parent
134
+ * (KF-102 round 2): the morph still walks the parent's children to
135
+ * reconcile siblings, but never disturbs list rows. The parameter is an
136
+ * internal coordination channel between `mount()` and the reconciler;
137
+ * public callers should omit it (the default empty set is correct for
138
+ * any tree that isn't being managed by an active `mount()`).
139
+ * - Focused text inputs (`<input>`/`<textarea>`) keep their value +
140
+ * selection across the morph; focused `[contenteditable]` keeps its
141
+ * entire subtree (typed content + caret + multi-range selection).
142
+ *
143
+ * Algorithm credit: based on the design of
144
+ * https://github.com/patrick-steele-idem/morphdom by Patrick Steele-Idem
145
+ * (MIT licensed). Reimplemented here so kerf can specialize the hot paths
146
+ * (segment-aware list dispatch, lighter callback surface) and drop the
147
+ * runtime dependency. Original copyright preserved in `LICENSE`.
148
+ */
149
+
150
+ /**
151
+ * Reconcile the children of `liveRoot` to match `template`.
152
+ *
153
+ * `template` can be an `Element` (used directly), a `SafeHtml`, or a raw
154
+ * HTML string — the latter two are stringified and parsed into a transient
155
+ * element whose tag matches `liveRoot`. The active text-entry / focused-
156
+ * contenteditable preservation rules apply in all cases.
157
+ *
158
+ * `ownedItems` is an internal coordination channel for `mount()`'s list
159
+ * reconciler; public callers should omit it.
160
+ */
161
+ declare function morph(liveRoot: Element, template: Element | SafeHtml | string, ownedItems?: ReadonlySet<Element>): void;
96
162
 
97
163
  /**
98
164
  * `mount(rootEl, render)` — kerf's render primitive.
@@ -106,7 +172,7 @@ declare function each<T extends object>(items: readonly T[] | ArraySignal<T>, re
106
172
  * Two phases per render:
107
173
  *
108
174
  * - Static surrounds (everything outside `each()` lists): kerf's native
109
- * `diff()` reconciler walks a freshly-built template against the live
175
+ * `morph()` reconciler walks a freshly-built template against the live
110
176
  * tree. Conventions: id/data-key matching, `data-morph-skip`, focus
111
177
  * preservation.
112
178
  *
@@ -147,6 +213,20 @@ declare function each<T extends object>(items: readonly T[] | ArraySignal<T>, re
147
213
  type MountResult = SafeHtml | string | number | boolean | null | undefined;
148
214
  declare function mount(rootEl: HTMLElement, render: () => MountResult): () => void;
149
215
 
216
+ /**
217
+ * Re-exports of `@preact/signals-core`. Lets the rest of the codebase depend
218
+ * on `'./reactive.js'` without naming the underlying lib, so swapping it out
219
+ * later (or fronting it with a hand-rolled implementation) is a one-file
220
+ * change.
221
+ *
222
+ * The `signal()` factory is dev-gated: when `NODE_ENV !== 'production'` and
223
+ * `KERF_DEV_WARN_UNTRACKED_SIGNALS === '1'`, it returns a `DevSignal` that
224
+ * warns on writes to signals with no subscribers (KF-176). Off by default;
225
+ * production always returns the bare `@preact/signals-core` signal.
226
+ */
227
+
228
+ declare function signal<T>(value?: T): Signal<T>;
229
+
150
230
  /**
151
231
  * `toElement(jsx)` — JSX → DOM, with SVG-aware namespace handling.
152
232
  *
@@ -163,4 +243,4 @@ declare function mount(rootEl: HTMLElement, render: () => MountResult): () => vo
163
243
 
164
244
  declare function toElement(jsx: SafeHtml | string): Element;
165
245
 
166
- export { type MountResult, SafeHtml, delegate, delegateCapture, each, mount, toElement };
246
+ export { type MountResult, SafeHtml, delegate, delegateCapture, each, morph, mount, signal, toElement };