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.
- package/LICENSE +21 -0
- package/README.ja.md +53 -0
- package/README.md +54 -0
- package/dist/base-BZTnpU8U.js +286 -0
- package/dist/base-BZTnpU8U.js.map +1 -0
- package/dist/base-Bx5ahfW2.d.ts +24 -0
- package/dist/hooks-BFIWh7AD.js +914 -0
- package/dist/hooks-BFIWh7AD.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +4 -0
- package/dist/jsx-D9U6dSwL.js +730 -0
- package/dist/jsx-D9U6dSwL.js.map +1 -0
- package/dist/jsx-Xp5q2y97.d.ts +33 -0
- package/dist/jsx-dev-runtime.d.ts +7 -0
- package/dist/jsx-dev-runtime.js +11 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.d.ts +9 -0
- package/dist/jsx-runtime.js +11 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/render-tracking-DCzw57BW.js +96 -0
- package/dist/render-tracking-DCzw57BW.js.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +2 -0
- package/dist/use-signals-8i2B-94o.js +173 -0
- package/dist/use-signals-8i2B-94o.js.map +1 -0
- package/dist/use-signals-CBDbbAbV.d.ts +24 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.js +91 -0
- package/dist/utils.js.map +1 -0
- package/package.json +114 -0
|
@@ -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
|