import { B as Binding } from './bindings-CYwoJpQb.js'; import { Signal } from '@preact/signals-core'; /** * Per-list binding shape kept by `mount()` and consumed by both reconcile * paths in `list-reconcile-snapshot.ts` and `list-reconcile-granular.ts`. * * Living in its own file (not re-exported through `list-reconcile.ts`) so * the snapshot + granular paths can import it without creating a circular * dependency on `list-reconcile.ts` itself. ESM handles cycles via function * hoisting, but a cycle involving a non-function helper would be a temporal * dead zone — extracting the binding shape + `endAnchor` here keeps the * dependency graph acyclic. */ interface BoundItem { ref: object; cacheKey: unknown; html: string; node: Element; /** KF-294: this row's fine-grained binding specs (undefined/empty if none). */ bindings?: Binding[]; /** * KF-294: live disposers for this row's bound effects. Set when the row node * is wired (first-render inline in `mount()`, or `buildFreshNodes` during a * reconcile) and called when the node is removed / the mount is torn down. */ bindingDisposers?: Array<() => void>; } interface ListBinding { liveParent: Element; /** * One entry per item currently mounted under `liveParent`, in order. * Mirrors the segment's `items` length after each reconcile. */ items: BoundItem[]; /** * The list's `` start marker, kept in the live DOM as * a permanent anchor (KF-102 round 2). The marker stays put across * static-surrounds diffs (it morphs as a comment node), so it gives * the list reconciler a stable "begin" position even when surrounding * siblings get inserted, removed, or reordered around the list. * * `endAnchor(binding)` derives the "insert at end of list" anchor from * `marker.nextSibling` (empty list) or `items[last].node.nextSibling` * (non-empty list) — picking up whatever the diff placed between the * list and the parent's tail. */ marker: Comment; /** * KF-173: once we've emitted the missing-key dev warning for this list, * we suppress further warnings for the same binding. The flag is set * inside `maybeWarnMissingRowKey()` and never cleared — the list is * considered "decided" after the first row check. */ warnedMissingKey?: boolean; } /** * The dev-hook registry — kerf's single seam between production code and the * opt-in development diagnostics. * * ## Why this exists * * kerf used to INFER whether it was running in development, by reading * `globalThis.process?.env?.NODE_ENV` through `utils/devMode.ts`. That * inference cannot be made correct, and it was wrong in the most common case: * bundlers substitute the BARE `process.env.NODE_ENV` token and never create a * `globalThis.process` object for browser targets, so the read returned * `undefined` and `undefined !== 'production'` resolved to DEVELOPMENT inside * production browser bundles. * * It also could not be fixed by rewriting the expression. Only the * *production* answer can be made static: `X && false` folds to a constant for * any side-effect-free `X`, while `X && true` does not. So any form that a * bundler can eliminate is also a form that treats "no `process` binding" as * production — which silently disables every warning in the no-build/CDN path * and in browser dev bundles. * * The fix is to stop guessing. Every environment already has a correct, * statically-foldable dev flag; what none of them offers is a way to hand that * flag to a *library*. So the consumer writes the conditional, in their own * code, with their own flag: * * ```js * if (import.meta.env.DEV) await import('kerfjs/dev'); // Vite * if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev'); * ``` * * Because that condition folds to `false` in the consumer's production build, * the entire statement is eliminated and the dev chunk is never emitted or * fetched. Installation IS the development signal — there is nothing left to * detect, and no environment kerf can be wrong about. * * ## The contract * * Core modules never import a `dev-*` module. They read a nullable slot off * `devHooks` and call through it: * * ```ts * devHooks.listRebind?.(id, marker.parentElement as Element); * ``` * * When nothing is installed every slot is `undefined`, so the cost is one * property read per call site and the `dev-*` modules are unreachable from the * main entry — which is what lets a bundler drop them. This is why the gate * lives at the CALL SITE rather than inside each warner: an unconditional call * into a self-gating warner keeps the module reachable no matter how the gate * is written, so no amount of dead-code elimination can reclaim it. * * Slots ending in `Enabled` are predicates rather than warnings. They exist for * the handful of call sites that must decide whether to do *expensive * preparatory work* — capturing the previous render's binding list, allocating * a per-render `Map` — before there is anything to warn about. Core must check * those before paying the cost, exactly as it checked the old `isOptedIn()` * exports. * * Each warner keeps its own internal opt-in check (the `KERF_DEV_WARN_*` env * reads). Installation decides whether the diagnostics are *present*; the * individual warner still decides whether it is *switched on*. * * @see docs/11-dev-warnings.md */ /** Per-mount / per-store one-shot dedup flag, owned by the caller in core. */ interface WarnOnceContext { warned: boolean; } interface DevHooks { /** * Replaces `signal()`'s constructor so writes to never-subscribed signals can * warn. Resolved at signal-CREATION time, so signals created before the dev * entry is installed stay plain — see `signalsCreatedBeforeInstall`. */ signalFactory?: (value: T) => Signal; /** * Wraps an `effect()` body so `delegate()` can detect that it is running * inside one. Returns the body to actually run. */ wrapEffect?: (fn: () => void | (() => void)) => () => void | (() => void); delegateInEffect?: (fn: 'delegate' | 'delegateCapture') => void; narrowSet?: (prev: unknown, next: unknown, ctx: WarnOnceContext) => void; /** Deep read-only proxy for the `get()` snapshot, so stray writes throw. */ storeReadonly?: (state: T) => T; /** Unwraps a proxy handed back through `set({ ...get() })`. */ storeToRaw?: (next: T) => T; listenerRebuild?: (rootEl: Element) => MutationObserver | null; listIdShift?: (id: string) => void; parserRepair?: (html: string) => void; staleBindingEnabled?: () => boolean; staleBinding?: (prevWired: readonly Binding[], current: readonly Binding[]) => void; listInvariantsEnabled?: () => boolean; listInvariants?: (rootEl: Element, bindings: ReadonlyMap, expectedCounts?: ReadonlyMap) => void; valueOnlyRerender?: (prevHtml: string, nextHtml: string, ctx: WarnOnceContext) => void; listRebind?: (id: string, liveParent: Element) => void; eachInMorphSkip?: (id: string, liveParent: Element, rootEl: Element) => void; missingRowKey?: (rowEl: Element, rowHtml: string, binding: { warnedMissingKey?: boolean; }) => void; staleIndexEnabled?: () => boolean; staleIndex?: (id: string) => void; duplicateCacheKeys?: (id: string, segItems: readonly { cacheKey: unknown; }[]) => void; /** * When installed, a screened URL throws instead of warning-and-dropping. * A slot rather than a boolean so the check stays uniform with the rest. */ urlScreenThrow?: (message: string) => never; } /** * The live slot table. Mutable by design — this is the fourth sanctioned * module-level mutable location (Design rule 5), and like `store.ts:REGISTRY` * it depends on there being exactly ONE copy at runtime. `tsup`'s * `splitting: true` guarantees that: shared modules are promoted into a single * chunk that both the main entry and the `kerfjs/dev` entry import. */ declare const devHooks: DevHooks; /** * Install (or extend) the dev hooks. Called by the `kerfjs/dev` entry; not part * of the public API surface. * * Merges rather than replaces, so a consumer can install the standard bundle * and then override a single slot in a test. */ declare function installDevHooks(hooks: DevHooks): void; /** * Remove every installed hook. Exists for test isolation — a suite that asserts * the not-installed (production-shaped) path needs to get back to a clean slate * without reloading modules. */ declare function clearDevHooks(): void; /** * The switch layer for the opt-in diagnostics — the second of the two gates * (§11.3.1). The first gate is installation: reaching this module at all means * the consumer imported `kerfjs/dev`. * * Two sources feed one lookup, explicit-call-wins: * * 1. `enableWarnings({...})` — an in-memory map, set by the consumer through * the `kerfjs/dev` entry. Explicit wins in BOTH directions, so * `{ narrowSet: false }` silences a warning an ambient env var switched on. * 2. `globalThis.process?.env?.KERF_DEV_*` — the environment, kept for Node, * SSR, and CI, where exporting a variable is the natural way to turn a * diagnostic on for one run. * * ## Why the env var could not be the only switch * * It is unreachable in the majority case. kerf is a browser framework, and a * browser realm has no `process` object at all — so every one of these * warnings was permanently off in exactly the environment (a Vite/webpack dev * server) where a developer most wants them. A bundler `define` does not fix * it either: the read goes through `globalThis.process` into a local binding, * so nothing substitutes the `process.env.X` token. That indirection is not * incidental — reading the bare token is what made kerf infer DEVELOPMENT * inside production browser bundles. * * The consumer already holds the module at the moment they opt in * (`const dev = await import('kerfjs/dev')`), so handing them a typed function * there is both the most reachable and the most discoverable switch. The env * vars stay because kerf's own suites and any CI run use them. */ /** * Which diagnostics to switch on. Every key is off unless you name it; passing * `false` explicitly forces a warning off even when its env var is set. * * `invariants` is not a warning but the structural audit of kerf's own list * bookkeeping: `true` reports violations with `console.warn`, `'throw'` raises * them (what you want in a test suite, where a warning inside a passing test * is invisible). */ interface DevWarningOptions { /** Imperative `addEventListener` on a node the morph later rebuilds. */ rebuiltListeners?: boolean; /** A `.value` write to a signal that never had a subscriber. */ untrackedSignals?: boolean; /** `defineStore` `set()` called with keys missing from the current state. */ narrowSet?: boolean; /** `delegate()` called inside an `effect()` body — listeners stack up. */ delegateInEffect?: boolean; /** An `each()` list under a `data-morph-skip` subtree. */ eachInMorphSkip?: boolean; /** Two rows in one `each()` producing the same `cacheKey`. */ duplicateEachKeys?: boolean; /** A fine-grained binding switching signal instance on the fast path. */ staleBinding?: boolean; /** A re-render whose whole diff was values — candidates for bindings. */ valueOnlyRerender?: boolean; /** An `each()` container rebuilt by the morph and self-healed. */ listRebind?: boolean; /** A memoized row reused at a different index than it rendered at. */ staleIndex?: boolean; /** Markup the HTML parser repaired (a block element inside a `

`). */ parserRepair?: boolean; /** Structural audit of kerf's list bookkeeping: warn, or `'throw'`. */ invariants?: boolean | 'throw'; } /** * `kerfjs/dev` — the development diagnostics bundle. * * Importing this module installs kerf's dev-only behavior: the whole * `KERF_DEV_WARN_*` warning family, the structural list invariants, the * read-only store snapshot, and the throw-on-dangerous-URL screen. Importing * nothing leaves every hook slot `undefined` and kerf runs in production shape. * * Put the import behind YOUR environment's dev flag, in YOUR code. That * condition folds to `false` in your production build, so the whole statement * is eliminated and this chunk is never emitted or fetched: * * ```js * if (import.meta.env.DEV) await import('kerfjs/dev'); // Vite * if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev'); // webpack / Node * ``` * * No-build / CDN consumers import it unconditionally from their dev page and * simply leave it out of the production page — there is no bundler to fold the * condition, and nothing for kerf to detect. * * ## Install ordering * * Every hook except one is read at CALL time (render, reconcile, `set()`, * `delegate()`), so installing any time before your first `mount()` is enough. * * The exception is `signalFactory`: `signal()` picks its constructor when the * signal is CREATED. Static imports are hoisted above a top-level * `await import()`, so module-scope signals in imported modules are created * before this module runs and the untracked-signal warning will not see them. * To cover those, make `import 'kerfjs/dev'` the FIRST STATIC import of a * dev-only entry file, then load the rest of your app. * * Opting into that warning prints this boundary once, so the gap is loud * rather than silent. It cannot be closed by retro-fitting existing signals: * `Signal.prototype`'s `value` accessor is non-configurable, and reaching live * instances would require a per-signal registry that production would pay for. * * Installation decides whether the diagnostics are PRESENT; each individual * warner still reads its own `KERF_DEV_WARN_*` env var to decide whether it is * switched ON. That keeps the existing opt-in contract intact. * * @see docs/11-dev-warnings.md */ /** * Switch individual diagnostics on: * * ```js * if (import.meta.env.DEV) { * const dev = await import('kerfjs/dev'); * dev.enableWarnings({ staleBinding: true, narrowSet: true }); * } * ``` * * Installing this module makes the diagnostics PRESENT; this decides which are * SWITCHED ON, so the console isn't flooded by warnings you didn't ask for. The * `KERF_DEV_WARN_*` environment variables do the same thing for Node, SSR, and * CI; an explicit call here wins over the environment in both directions, so * `{ narrowSet: false }` silences an ambient var. * * **In a browser this function is the only switch that works.** A browser realm * has no `process` object, and a bundler `define` cannot reach the read (it * goes through `globalThis.process` into a local binding), so the environment * variables are unreachable there. * * Call it as early as you can. Every diagnostic reads its switch at call time * except the untracked-signal warning, which is chosen when a signal is * CREATED — enabling that one prints its coverage boundary once. */ declare function enableWarnings(options: DevWarningOptions): void; /** * The standard bundle of hooks this entry installs. Exported so kerf's own * suites can drop back to the production shape (`clearDevHooks()`) and restore * afterwards — re-importing this module would not re-run the install, since * module evaluation happens once. */ declare const DEV_HOOKS: DevHooks; export { DEV_HOOKS, type DevHooks, type DevWarningOptions, clearDevHooks, devHooks, enableWarnings, installDevHooks };