react-fine-grained-signals 0.1.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.
@@ -0,0 +1,730 @@
1
+ import { a as effect, o as isSignal } from "./base-BZTnpU8U.js";
2
+ import { o as useSignalValue } from "./hooks-BFIWh7AD.js";
3
+ import { Fragment, createElement, useLayoutEffect, useRef } from "react";
4
+ //#region src/runtime/jsx.ts
5
+ /** The small set of DOM properties that support direct signal bindings. */
6
+ const REACTIVE_PROP_NAMES = /* @__PURE__ */ new Set([
7
+ "title",
8
+ "id",
9
+ "className",
10
+ "hidden",
11
+ "disabled",
12
+ "style",
13
+ "value",
14
+ "checked"
15
+ ]);
16
+ function isControlledTwoWayProp(tagName, name) {
17
+ if (name === "value") return tagName === "input" || tagName === "textarea" || tagName === "select";
18
+ if (name === "checked") return tagName === "input";
19
+ return false;
20
+ }
21
+ function resolveBindingKind(tagName, name) {
22
+ if (name === "style") return "style";
23
+ if (isControlledTwoWayProp(tagName, name)) {
24
+ if (name === "value") return tagName === "select" ? "select-value" : "text-value";
25
+ return "checked";
26
+ }
27
+ return "prop";
28
+ }
29
+ function isTwoWayBindingKind(kind) {
30
+ return kind === "select-value" || kind === "text-value" || kind === "checked";
31
+ }
32
+ const UNCONTROLLED_PROP_NAMES = {
33
+ value: "defaultValue",
34
+ checked: "defaultChecked"
35
+ };
36
+ const UNITLESS_CSS_PROPERTIES = /* @__PURE__ */ new Set([
37
+ "animationIterationCount",
38
+ "aspectRatio",
39
+ "borderImageOutset",
40
+ "borderImageSlice",
41
+ "borderImageWidth",
42
+ "boxFlex",
43
+ "boxFlexGroup",
44
+ "boxOrdinalGroup",
45
+ "columnCount",
46
+ "columns",
47
+ "flex",
48
+ "flexGrow",
49
+ "flexPositive",
50
+ "flexShrink",
51
+ "flexNegative",
52
+ "flexOrder",
53
+ "gridArea",
54
+ "gridColumn",
55
+ "gridColumnEnd",
56
+ "gridColumnSpan",
57
+ "gridColumnStart",
58
+ "gridRow",
59
+ "gridRowEnd",
60
+ "gridRowSpan",
61
+ "gridRowStart",
62
+ "fontWeight",
63
+ "lineClamp",
64
+ "lineHeight",
65
+ "opacity",
66
+ "order",
67
+ "orphans",
68
+ "scale",
69
+ "tabSize",
70
+ "WebkitLineClamp",
71
+ "widows",
72
+ "zIndex",
73
+ "zoom"
74
+ ]);
75
+ const SVG_ELEMENTS = [
76
+ "svg",
77
+ "animate",
78
+ "animateMotion",
79
+ "animateTransform",
80
+ "circle",
81
+ "clipPath",
82
+ "defs",
83
+ "desc",
84
+ "ellipse",
85
+ "feBlend",
86
+ "feColorMatrix",
87
+ "feComponentTransfer",
88
+ "feComposite",
89
+ "feConvolveMatrix",
90
+ "feDiffuseLighting",
91
+ "feDisplacementMap",
92
+ "feDistantLight",
93
+ "feDropShadow",
94
+ "feFlood",
95
+ "feFuncA",
96
+ "feFuncB",
97
+ "feFuncG",
98
+ "feFuncR",
99
+ "feGaussianBlur",
100
+ "feImage",
101
+ "feMerge",
102
+ "feMergeNode",
103
+ "feMorphology",
104
+ "feOffset",
105
+ "fePointLight",
106
+ "feSpecularLighting",
107
+ "feSpotLight",
108
+ "feTile",
109
+ "feTurbulence",
110
+ "filter",
111
+ "foreignObject",
112
+ "g",
113
+ "image",
114
+ "line",
115
+ "linearGradient",
116
+ "marker",
117
+ "mask",
118
+ "metadata",
119
+ "mpath",
120
+ "path",
121
+ "pattern",
122
+ "polygon",
123
+ "polyline",
124
+ "radialGradient",
125
+ "rect",
126
+ "set",
127
+ "stop",
128
+ "switch",
129
+ "symbol",
130
+ "text",
131
+ "textPath",
132
+ "tspan",
133
+ "use",
134
+ "view"
135
+ ];
136
+ const MATHML_ELEMENTS = [
137
+ "annotation",
138
+ "annotation-xml",
139
+ "maction",
140
+ "math",
141
+ "merror",
142
+ "mfrac",
143
+ "mi",
144
+ "mmultiscripts",
145
+ "mn",
146
+ "mo",
147
+ "mover",
148
+ "mpadded",
149
+ "mphantom",
150
+ "mprescripts",
151
+ "mroot",
152
+ "mrow",
153
+ "ms",
154
+ "mspace",
155
+ "msqrt",
156
+ "mstyle",
157
+ "msub",
158
+ "msubsup",
159
+ "msup",
160
+ "mtable",
161
+ "mtd",
162
+ "mtext",
163
+ "mtr",
164
+ "munder",
165
+ "munderover",
166
+ "semantics"
167
+ ];
168
+ const NON_HTML_HOST_ELEMENTS = /* @__PURE__ */ new Set([...SVG_ELEMENTS, ...MATHML_ELEMENTS]);
169
+ const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
170
+ /**
171
+ * A leaf that lets React own reconciliation while its signal dependency stays
172
+ * local to the leaf. A signal resolving to another signal/array is normalized
173
+ * again so nested reactive children also work.
174
+ */
175
+ function SignalValue({ source }) {
176
+ return normalizeChild(useSignalValue(source));
177
+ }
178
+ function normalizeChild(value) {
179
+ if (isSignal(value)) return createElement(SignalValue, { source: value });
180
+ if (Array.isArray(value)) return value.map(normalizeChild);
181
+ return value;
182
+ }
183
+ function isReactiveHostProp(name, value) {
184
+ if (!isSignal(value)) return false;
185
+ return REACTIVE_PROP_NAMES.has(name) || name.startsWith("data-") || name.startsWith("aria-");
186
+ }
187
+ function readInitialValue(source) {
188
+ return source.peek();
189
+ }
190
+ /**
191
+ * Reads a signal on behalf of one of this module's direct DOM bindings (see
192
+ * the module doc: these write straight to the DOM from an `effect()` installed
193
+ * alongside the element's ref, bypassing React's render, so the owning
194
+ * component never re-renders). A `computed()` whose getter throws caches and
195
+ * rethrows that error on every read (see `computed()` in src/core/base.ts) —
196
+ * if `source` is such a computed and it starts failing after the binding is
197
+ * already mounted, an unguarded read here would throw synchronously out of
198
+ * `effect()`'s callback body. Since 41c77ea that no longer escapes the flush —
199
+ * `effect()` catches it and routes it through `reportEffectError`
200
+ * (src/core/base.ts), so the write that triggered it still completes and the
201
+ * rest of the effect queue still runs. But that containment is whole-callback:
202
+ * it abandons the remainder of this binding's callback for the cycle and
203
+ * reports a generic "an effect() callback threw", with no idea which binding
204
+ * failed or that the DOM was left mid-update.
205
+ *
206
+ * Catching here keeps a failure local to this one binding: the DOM write for
207
+ * this cycle is skipped (the DOM is left at its last successful value) and
208
+ * the failure is reported with `console.error(message, { cause: error })` —
209
+ * assert against `mock.calls[i][1].cause` in tests, the same shape used by
210
+ * `computed()`'s own error report (src/core/base.ts) and by
211
+ * `render-tracking.ts`. Reporting is intentional here too: a direct binding
212
+ * has no Error Boundary or other surface to fall back on, and silence would
213
+ * mean a binding that mysteriously stops updating with zero trace.
214
+ *
215
+ * Reported at most once per contiguous run of failures ("episode"): `episode`
216
+ * latches after the first report and is cleared the moment a read next
217
+ * succeeds, so a later, distinct failure episode logs again. This keeps a
218
+ * `computed()` that fails on every keystroke from spamming one `console.error`
219
+ * per keystroke.
220
+ */
221
+ function readBoundSignal(read, episode) {
222
+ try {
223
+ const value = read();
224
+ episode.hasReported = false;
225
+ return {
226
+ ok: true,
227
+ value
228
+ };
229
+ } catch (error) {
230
+ if (!episode.hasReported) {
231
+ episode.hasReported = true;
232
+ console.error("react-fine-grained-signals: a direct signal binding's read threw; skipping this update and leaving the DOM at its last value.", { cause: error });
233
+ }
234
+ return { ok: false };
235
+ }
236
+ }
237
+ /**
238
+ * Reads via `readBoundSignal` and, on success, hands the value to `apply`; a
239
+ * failed read is already reported by `readBoundSignal`, so this just skips
240
+ * the write. Shared by every read-and-apply call site below, including the
241
+ * one that isn't itself an `effect()` — the MutationObserver-triggered
242
+ * re-apply in `bindSelectValue`, which reads outside the reactive graph via
243
+ * `.peek()`.
244
+ */
245
+ function applyBoundSignal(read, apply, episode) {
246
+ const result = readBoundSignal(read, episode);
247
+ if (result.ok) apply(result.value);
248
+ }
249
+ /**
250
+ * The common case built on `applyBoundSignal`: subscribe to `source.value`
251
+ * inside an `effect()` and re-run `apply` whenever it changes. Centralizes
252
+ * the pattern that used to be hand-rolled at each binding site — declare an
253
+ * `episode`, then `effect(() => { const read = readBoundSignal(...); if
254
+ * (!read.ok) return; <apply the value> })` — so the read-and-skip contract
255
+ * can't drift between sites. An `episode` may be passed in to share a
256
+ * failure latch with a sibling read site (see `bindSelectValue`); otherwise
257
+ * each binding gets its own.
258
+ */
259
+ function createBindingEffect(source, apply, episode = { hasReported: false }) {
260
+ return effect(() => applyBoundSignal(() => source.value, apply, episode));
261
+ }
262
+ function setAttribute(node, name, value) {
263
+ if (value == null) node.removeAttribute(name);
264
+ else node.setAttribute(name, String(value));
265
+ }
266
+ function setDomProp(node, name, value) {
267
+ if (node.namespaceURI !== HTML_NAMESPACE) {
268
+ setAttribute(node, name === "className" ? "class" : name, value);
269
+ return;
270
+ }
271
+ switch (name) {
272
+ case "title":
273
+ node.title = value == null ? "" : String(value);
274
+ return;
275
+ case "id":
276
+ node.id = value == null ? "" : String(value);
277
+ return;
278
+ case "className":
279
+ node.className = value == null ? "" : String(value);
280
+ return;
281
+ case "hidden":
282
+ if (value === "until-found") setAttribute(node, "hidden", value);
283
+ else node.hidden = Boolean(value);
284
+ return;
285
+ case "disabled":
286
+ if ("disabled" in node) node.disabled = Boolean(value);
287
+ else if (value == null || value === false) node.removeAttribute(name);
288
+ else setAttribute(node, name, value);
289
+ return;
290
+ default: setAttribute(node, name, value);
291
+ }
292
+ }
293
+ /**
294
+ * Writes `value`/`checked` on a controlled-two-way element, skipping the DOM
295
+ * write when it already holds the value being written. Effects re-run on
296
+ * every keystroke (the signal changed because `onChange` just wrote it), so
297
+ * without this guard every keystroke would re-set a property the DOM already
298
+ * has, moving the caret and disrupting IME composition for no reason.
299
+ */
300
+ function setControlledProp(node, name, value) {
301
+ if (name === "value") {
302
+ const select = node;
303
+ if (select.tagName === "SELECT" && select.multiple) {
304
+ setMultiSelectValue(select, value);
305
+ return;
306
+ }
307
+ const next = value == null ? "" : String(value);
308
+ const input = node;
309
+ if (input.value !== next) input.value = next;
310
+ return;
311
+ }
312
+ const next = Boolean(value);
313
+ const input = node;
314
+ if (input.checked !== next) input.checked = next;
315
+ }
316
+ /**
317
+ * `<select multiple>` does not support assigning `.value` directly — the DOM
318
+ * setter only ever selects a single option, silently corrupting the
319
+ * selection instead of erroring. Its documented API for a multi-value select
320
+ * is per-`<option>` `.selected`, so an array-valued signal (React's own
321
+ * typing for a multi-select) is applied that way instead.
322
+ */
323
+ function setMultiSelectValue(select, value) {
324
+ const values = new Set(Array.isArray(value) ? value.map(String) : value == null ? [] : [String(value)]);
325
+ for (const option of select.options) {
326
+ const selected = values.has(option.value);
327
+ if (option.selected !== selected) option.selected = selected;
328
+ }
329
+ }
330
+ /**
331
+ * A `<select>` whose matching `<option>` does not exist yet (for example when
332
+ * the options themselves are rendered from a signal or other state, mounted
333
+ * after this one) silently ends up with nothing selected: the DOM neither
334
+ * errors nor retroactively applies `.value`/`.selected` once a matching
335
+ * `<option>` is later added. The per-value effect below only reruns when the
336
+ * bound signal itself changes, so it cannot see that the option list changed
337
+ * out from under it. A MutationObserver on the select's subtree re-applies
338
+ * the signal's current value whenever its `<option>` list changes, closing
339
+ * that gap without requiring the signal to change too.
340
+ */
341
+ function bindSelectValue(select, source) {
342
+ const episode = { hasReported: false };
343
+ const apply = (value) => setControlledProp(select, "value", value);
344
+ const stopEffect = createBindingEffect(source, apply, episode);
345
+ const observer = new MutationObserver(() => {
346
+ applyBoundSignal(() => source.peek(), apply, episode);
347
+ });
348
+ observer.observe(select, {
349
+ childList: true,
350
+ subtree: true
351
+ });
352
+ return () => {
353
+ stopEffect();
354
+ observer.disconnect();
355
+ };
356
+ }
357
+ /**
358
+ * Forcing a `value` write while an IME composition is in progress can abort
359
+ * the composition outright, independent of whether the string being written
360
+ * happens to match what was typed — the write-skip guard in
361
+ * `setControlledProp` only protects the same-value echo case, not this one.
362
+ * `compositionstart`/`compositionend` listeners on the node itself track
363
+ * composition state directly, regardless of whether the component declares
364
+ * its own composition handlers, and a write requested while composing is
365
+ * deferred until composition ends instead of applied immediately.
366
+ */
367
+ function bindTextValue(node, source) {
368
+ let composing = false;
369
+ let hasPending = false;
370
+ let pending;
371
+ const episode = { hasReported: false };
372
+ const onCompositionStart = () => {
373
+ composing = true;
374
+ };
375
+ const onCompositionEnd = () => {
376
+ composing = false;
377
+ if (hasPending) {
378
+ hasPending = false;
379
+ setControlledProp(node, "value", pending);
380
+ }
381
+ };
382
+ node.addEventListener("compositionstart", onCompositionStart);
383
+ node.addEventListener("compositionend", onCompositionEnd);
384
+ const stopEffect = createBindingEffect(source, (next) => {
385
+ if (composing) {
386
+ hasPending = true;
387
+ pending = next;
388
+ return;
389
+ }
390
+ setControlledProp(node, "value", next);
391
+ }, episode);
392
+ return () => {
393
+ stopEffect();
394
+ node.removeEventListener("compositionstart", onCompositionStart);
395
+ node.removeEventListener("compositionend", onCompositionEnd);
396
+ };
397
+ }
398
+ function clearStyleProperty(style, key) {
399
+ if (key.startsWith("--")) style.removeProperty(key);
400
+ else style[key] = "";
401
+ }
402
+ function setStyleProperty(style, key, value) {
403
+ if (value == null) {
404
+ clearStyleProperty(style, key);
405
+ return;
406
+ }
407
+ if (key.startsWith("--")) {
408
+ style.setProperty(key, String(value));
409
+ return;
410
+ }
411
+ style[key] = typeof value === "number" && !UNITLESS_CSS_PROPERTIES.has(key) ? `${value}px` : String(value);
412
+ }
413
+ /**
414
+ * Applies a whole style object to an element, clearing keys that were present
415
+ * in a previous call but are absent from this one. Only the coarse
416
+ * `style={signal}` form is bound this way — an object whose individual entries
417
+ * are themselves signals is out of scope (see docs/direct-binding-value-checked-style.md).
418
+ */
419
+ function applyStyle(node, value, previousKeys) {
420
+ const nextStyle = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
421
+ const nextKeys = Object.keys(nextStyle);
422
+ const nextKeySet = new Set(nextKeys);
423
+ for (const key of previousKeys) if (!nextKeySet.has(key)) clearStyleProperty(node.style, key);
424
+ for (const key of nextKeys) setStyleProperty(node.style, key, nextStyle[key]);
425
+ return nextKeys;
426
+ }
427
+ function applyRef(ref, node) {
428
+ if (typeof ref === "function") return ref(node);
429
+ if (ref != null) ref.current = node;
430
+ }
431
+ /** A binding's identity for the re-render diff: same name, source, and kind. */
432
+ function isSameBinding(a, b) {
433
+ return b !== void 0 && a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
434
+ }
435
+ /**
436
+ * Subscribes a single binding to `node` and returns its teardown. Split out of
437
+ * the ref callback so `createReactiveHostBinder` can rebuild one binding on
438
+ * its own, without disturbing the siblings that did not change.
439
+ *
440
+ * `initialStyleKeys` seeds a `"style"` binding's own `previousKeys` — passed
441
+ * by `syncBindings` when this binding is replacing a disposed style binding,
442
+ * so the fresh one starts already knowing which CSS properties are actually
443
+ * on the node, instead of starting from an empty set and never clearing them.
444
+ * Ignored for every other kind.
445
+ */
446
+ function subscribeBinding(node, name, source, kind, initialStyleKeys) {
447
+ switch (kind) {
448
+ case "style": {
449
+ let previousKeys = initialStyleKeys ?? [];
450
+ return {
451
+ dispose: createBindingEffect(source, (value) => {
452
+ previousKeys = applyStyle(node, value, previousKeys);
453
+ }),
454
+ getStyleKeys: () => previousKeys
455
+ };
456
+ }
457
+ case "select-value": return {
458
+ dispose: bindSelectValue(node, source),
459
+ getStyleKeys: void 0
460
+ };
461
+ case "text-value": return {
462
+ dispose: bindTextValue(node, source),
463
+ getStyleKeys: void 0
464
+ };
465
+ case "checked": return {
466
+ dispose: createBindingEffect(source, (value) => setControlledProp(node, name, value)),
467
+ getStyleKeys: void 0
468
+ };
469
+ case "prop": return {
470
+ dispose: createBindingEffect(source, (value) => setDomProp(node, name, value)),
471
+ getStyleKeys: void 0
472
+ };
473
+ }
474
+ }
475
+ function mountBinding(node, binding, initialStyleKeys) {
476
+ const { dispose, getStyleKeys } = subscribeBinding(node, binding[0], binding[1], binding[2], initialStyleKeys);
477
+ return {
478
+ binding,
479
+ dispose,
480
+ getStyleKeys
481
+ };
482
+ }
483
+ /**
484
+ * Owns everything one mounted `ReactiveHost` attaches to its DOM node: the
485
+ * user's own ref, plus one live subscription per binding.
486
+ *
487
+ * The ref callback is created once per binder, and a binder is created once
488
+ * per mounted `ReactiveHost`, so React only ever sees a single ref identity
489
+ * for the lifetime of that element. That is load-bearing: React responds to a
490
+ * *changed* callback-ref identity by detaching the old ref and attaching the
491
+ * new one on the very same, unchanged DOM node. While the ref closure was
492
+ * rebuilt on every render — which it was, since `transformProps` hands
493
+ * `ReactiveHost` a freshly allocated `bindings` array each time — every
494
+ * unrelated re-render of the owning component (any state, context, or parent
495
+ * update anywhere above this element) silently disconnected and recreated
496
+ * `bindSelectValue`'s `MutationObserver`, removed and re-added
497
+ * `bindTextValue`'s composition listeners *along with the closure-local
498
+ * `composing`/`pending` state they guard* — resetting `composing` to `false`
499
+ * mid-composition and letting the next write stomp in-flight IME input —
500
+ * resubscribed every binding's `effect()`, and called the user's own ref with
501
+ * `null` and then the identical node again.
502
+ *
503
+ * Pinning the identity moves that reconciliation here: `sync` runs from
504
+ * `ReactiveHost`'s layout effect after every commit and diffs the render's
505
+ * bindings against the mounted ones by `(name, source, kind)`, tearing down
506
+ * and rebuilding only the entries that actually changed. What is left in the
507
+ * ref callback is exactly the part React's attach/detach protocol has to
508
+ * drive: which node is current, and full teardown when there no longer is one.
509
+ *
510
+ * The split is safe because of how React orders a commit. A host element's ref
511
+ * is attached during the layout phase *before* the layout effects of the
512
+ * component that rendered it, so `sync` always finds the node already
513
+ * attached; and a ref is only ever (re)attached on a fiber React re-rendered,
514
+ * which is also the only way `ReactiveHost`'s dependency-less layout effect
515
+ * can fail to re-run — so no attach can slip past a `sync`.
516
+ */
517
+ function createReactiveHostBinder() {
518
+ let node = null;
519
+ let mounted = [];
520
+ let attachedUserRef;
521
+ let userCleanup;
522
+ let activeCleanup;
523
+ const attachUserRef = (target, userRef) => {
524
+ attachedUserRef = userRef;
525
+ userCleanup = applyRef(userRef, target);
526
+ };
527
+ const detachUserRef = () => {
528
+ const cleanup = userCleanup;
529
+ const detached = attachedUserRef;
530
+ userCleanup = void 0;
531
+ attachedUserRef = void 0;
532
+ if (typeof cleanup === "function") cleanup();
533
+ else applyRef(detached, null);
534
+ };
535
+ const disposeActive = () => {
536
+ const cleanup = activeCleanup;
537
+ activeCleanup = void 0;
538
+ cleanup?.();
539
+ };
540
+ const ref = (target) => {
541
+ disposeActive();
542
+ if (target == null) return;
543
+ node = target;
544
+ let isDisposed = false;
545
+ const cleanup = () => {
546
+ if (isDisposed) return;
547
+ isDisposed = true;
548
+ if (activeCleanup === cleanup) activeCleanup = void 0;
549
+ for (const binding of mounted) binding.dispose();
550
+ mounted = [];
551
+ detachUserRef();
552
+ node = null;
553
+ };
554
+ activeCleanup = cleanup;
555
+ return cleanup;
556
+ };
557
+ const syncBindings = (target, bindings) => {
558
+ if (mounted.length === bindings.length && mounted.every((entry, index) => isSameBinding(entry.binding, bindings[index]))) return;
559
+ const reusable = /* @__PURE__ */ new Map();
560
+ for (const entry of mounted) reusable.set(entry.binding[0], entry);
561
+ const reused = bindings.map((binding) => {
562
+ const candidate = reusable.get(binding[0]);
563
+ if (candidate === void 0 || !isSameBinding(candidate.binding, binding)) return void 0;
564
+ reusable.delete(binding[0]);
565
+ return candidate;
566
+ });
567
+ const staleStyleKeys = /* @__PURE__ */ new Map();
568
+ for (const stale of reusable.values()) if (stale.getStyleKeys !== void 0) staleStyleKeys.set(stale.binding[0], stale.getStyleKeys());
569
+ for (const stale of reusable.values()) stale.dispose();
570
+ mounted = bindings.map((binding, index) => reused[index] ?? mountBinding(target, binding, staleStyleKeys.get(binding[0])));
571
+ };
572
+ return {
573
+ ref,
574
+ sync(bindings, userRef) {
575
+ const target = node;
576
+ if (target == null) return;
577
+ if (userRef !== attachedUserRef) {
578
+ detachUserRef();
579
+ attachUserRef(target, userRef);
580
+ }
581
+ syncBindings(target, bindings);
582
+ }
583
+ };
584
+ }
585
+ /**
586
+ * Hands `ReactiveHost` its one stable ref callback and drives the binder's
587
+ * post-commit reconciliation.
588
+ *
589
+ * The layout effect deliberately declares no dependency array and returns no
590
+ * cleanup: it must run after every commit (that is what makes it impossible
591
+ * for a ref attach to happen without a following `sync`), and its teardown
592
+ * belongs to the ref callback, which React already invokes on unmount, on
593
+ * node replacement, and on StrictMode's ref replay. Giving it a cleanup here
594
+ * would tear the whole element down again on every re-render — precisely the
595
+ * churn this exists to remove.
596
+ */
597
+ function useReactiveHostBinder(bindings, userRef) {
598
+ const binderRef = useRef(void 0);
599
+ if (binderRef.current === void 0) binderRef.current = createReactiveHostBinder();
600
+ const binder = binderRef.current;
601
+ useLayoutEffect(() => {
602
+ binder.sync(bindings, userRef);
603
+ });
604
+ return binder.ref;
605
+ }
606
+ /**
607
+ * A host element with DOM-only signal subscriptions attached via its ref.
608
+ *
609
+ * The subscriptions themselves are owned by a per-instance binder
610
+ * (`useReactiveHostBinder`) rather than rebuilt inline here, so this
611
+ * component's ref prop keeps one identity for the element's whole lifetime
612
+ * and an unrelated re-render costs nothing — see `createReactiveHostBinder`.
613
+ *
614
+ * `children` arrives as ReactiveHost's own top-level prop (see
615
+ * `createJsxWrapper` below) rather than folded into `props`/`hostProps`,
616
+ * purely so the *outer* `factory(ReactiveHost, { ..., children }, key)` call
617
+ * that constructs this element — the very same real `jsx`/`jsxs`/`jsxDEV`
618
+ * that would have validated the original host element's children had this
619
+ * wrapper not intercepted it — runs React's dev-mode key validation on it.
620
+ * That validation is a flag React stamps onto each child element itself
621
+ * (`element._store.validated`), not onto the array or onto whichever
622
+ * component currently holds it, so it survives being read back out of props
623
+ * here and re-embedded via `createElement` below. Skip this indirection —
624
+ * i.e. leave `children` folded into `hostProps` before any real jsx/jsxs call
625
+ * ever sees it — and a signal-bound host element with 2+ static, unkeyed JSX
626
+ * children spuriously trips React's "missing key" warning: `createElement`'s
627
+ * children-as-prop path never validates children (only its children-as-rest-
628
+ * args path does), and neither did the `factory(ReactiveHost, ...)` call
629
+ * itself, since `children` wasn't its own prop at that call site.
630
+ */
631
+ function ReactiveHost({ elementType, props, bindings, children }) {
632
+ const { ref: userRef, ...hostProps } = props;
633
+ const ref = useReactiveHostBinder(bindings, userRef);
634
+ return createElement(elementType, {
635
+ ...hostProps,
636
+ children,
637
+ ref
638
+ });
639
+ }
640
+ /**
641
+ * Scans a native host element's props for anything reactive (see
642
+ * `isReactiveHostProp`). Kept separate from building the transformed props
643
+ * copy in `transformHostProps` so that function can decide whether a copy is
644
+ * needed at all before ever allocating one.
645
+ */
646
+ function findHostBindings(props, tagName) {
647
+ const bindings = [];
648
+ for (const [name, value] of Object.entries(props)) if (isReactiveHostProp(name, value)) bindings.push([
649
+ name,
650
+ value,
651
+ resolveBindingKind(tagName, name)
652
+ ]);
653
+ return bindings;
654
+ }
655
+ /**
656
+ * Transforms a native host element's (`<div>`, `<svg>`, ...) props: replaces
657
+ * any directly-bound prop with its initial, non-reactive value (`bindings`
658
+ * carries the reactive ones for `ReactiveHost` to mount later — see
659
+ * `createJsxWrapper`) and normalizes a signal/array `children` the same way
660
+ * `Fragment` does.
661
+ *
662
+ * The overwhelmingly common host element has neither. Scanning first and only
663
+ * allocating a copy when something is actually found means a plain
664
+ * `<div className="card">…</div>` costs one prop walk and zero extra
665
+ * allocations — the original props object is returned as-is (see the
666
+ * `bindings.length === 0 && !childrenNeedNormalization` branch below), and
667
+ * `createJsxWrapper` passes it straight to `factory` unchanged.
668
+ */
669
+ function transformHostProps(type, input) {
670
+ const inputProps = input ?? {};
671
+ const bindings = !NON_HTML_HOST_ELEMENTS.has(type) ? findHostBindings(inputProps, type) : [];
672
+ const rawChildren = inputProps.children;
673
+ const childrenNeedNormalization = "children" in inputProps && (isSignal(rawChildren) || Array.isArray(rawChildren));
674
+ if (bindings.length === 0 && !childrenNeedNormalization) return {
675
+ props: inputProps,
676
+ bindings
677
+ };
678
+ const props = { ...inputProps };
679
+ if (childrenNeedNormalization) props.children = normalizeChild(rawChildren);
680
+ for (const [name, value, kind] of bindings) {
681
+ const uncontrolledName = isTwoWayBindingKind(kind) ? UNCONTROLLED_PROP_NAMES[name] : void 0;
682
+ if (uncontrolledName !== void 0) {
683
+ delete props[name];
684
+ props[uncontrolledName] = readInitialValue(value);
685
+ } else props[name] = readInitialValue(value);
686
+ }
687
+ return {
688
+ props,
689
+ bindings
690
+ };
691
+ }
692
+ /**
693
+ * `Fragment`'s only prop worth transforming is `children`, and only when it
694
+ * is itself a signal or array — `normalizeChild` is the identity for
695
+ * anything else (see its definition) — so a plain `<>...</>` with ordinary
696
+ * children is passed straight through, uncopied, the same way a custom
697
+ * component is below.
698
+ */
699
+ function transformFragmentProps(input) {
700
+ const props = input;
701
+ if (props == null || !("children" in props) || !isSignal(props.children) && !Array.isArray(props.children)) return input;
702
+ return {
703
+ ...props,
704
+ children: normalizeChild(props.children)
705
+ };
706
+ }
707
+ /** Creates a JSX wrapper while letting each module supply React's JSX factory. */
708
+ function createJsxWrapper(factory) {
709
+ return (type, input, key) => {
710
+ if (typeof type === "string") {
711
+ const { props, bindings } = transformHostProps(type, input);
712
+ if (bindings.length > 0) {
713
+ const { children, ...hostProps } = props;
714
+ return factory(ReactiveHost, {
715
+ elementType: type,
716
+ props: hostProps,
717
+ bindings,
718
+ children
719
+ }, key);
720
+ }
721
+ return factory(type, props, key);
722
+ }
723
+ if (type === Fragment) return factory(type, transformFragmentProps(input), key);
724
+ return factory(type, input, key);
725
+ };
726
+ }
727
+ //#endregion
728
+ export { createJsxWrapper as t };
729
+
730
+ //# sourceMappingURL=jsx-D9U6dSwL.js.map