@fluixi/dom 1.0.0-alpha.82 → 1.0.0-alpha.84

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 (183) hide show
  1. package/dist/cdn/dom-client.cjs +1427 -1
  2. package/dist/cdn/dom-client.d.ts +2 -2
  3. package/dist/cdn/dom-client.global.js +2 -1
  4. package/dist/cdn/dom-client.js +3 -3
  5. package/dist/cdn/dom-client.mjs +1430 -1
  6. package/dist/cdn/dom.cjs +3127 -1
  7. package/dist/cdn/dom.d.ts +2 -2
  8. package/dist/cdn/dom.global.js +2 -1
  9. package/dist/cdn/dom.js +4 -4
  10. package/dist/cdn/dom.mjs +3133 -1
  11. package/dist/index.cjs +3155 -1
  12. package/dist/index.js +2 -2
  13. package/dist/index.mjs +3149 -1
  14. package/dist/lib/component/index.cjs +128 -1
  15. package/dist/lib/component/index.mjs +105 -1
  16. package/dist/lib/component/modifiers.cjs +126 -1
  17. package/dist/lib/component/modifiers.d.ts +12 -0
  18. package/dist/lib/component/modifiers.d.ts.map +1 -1
  19. package/dist/lib/component/modifiers.js +12 -84
  20. package/dist/lib/component/modifiers.mjs +105 -1
  21. package/dist/lib/dom/delegate.cjs +60 -1
  22. package/dist/lib/dom/delegate.js +1 -1
  23. package/dist/lib/dom/delegate.mjs +39 -1
  24. package/dist/lib/dom/dynamic-element.cjs +949 -1
  25. package/dist/lib/dom/dynamic-element.d.ts +3 -3
  26. package/dist/lib/dom/dynamic-element.js +3 -3
  27. package/dist/lib/dom/dynamic-element.mjs +928 -1
  28. package/dist/lib/dom/hydration-state.cjs +93 -1
  29. package/dist/lib/dom/hydration-state.d.ts +1 -1
  30. package/dist/lib/dom/hydration-state.js +1 -1
  31. package/dist/lib/dom/hydration-state.mjs +72 -1
  32. package/dist/lib/dom/hydration.cjs +685 -1
  33. package/dist/lib/dom/hydration.js +8 -8
  34. package/dist/lib/dom/hydration.mjs +664 -1
  35. package/dist/lib/dom/index.cjs +2040 -1
  36. package/dist/lib/dom/index.js +1 -1
  37. package/dist/lib/dom/index.mjs +2020 -1
  38. package/dist/lib/dom/integration.cjs +71 -1
  39. package/dist/lib/dom/integration.d.ts.map +1 -1
  40. package/dist/lib/dom/integration.js +15 -6
  41. package/dist/lib/dom/integration.mjs +51 -1
  42. package/dist/lib/dom/island.cjs +790 -1
  43. package/dist/lib/dom/island.d.ts +1 -1
  44. package/dist/lib/dom/island.js +12 -12
  45. package/dist/lib/dom/island.mjs +769 -1
  46. package/dist/lib/dom/runtime.cjs +1432 -1
  47. package/dist/lib/dom/runtime.d.ts +71 -19
  48. package/dist/lib/dom/runtime.d.ts.map +1 -1
  49. package/dist/lib/dom/runtime.js +261 -100
  50. package/dist/lib/dom/runtime.mjs +1414 -1
  51. package/dist/lib/dom/server/fx-data.cjs +53 -1
  52. package/dist/lib/dom/server/fx-data.d.ts +5 -5
  53. package/dist/lib/dom/server/fx-data.js +6 -6
  54. package/dist/lib/dom/server/fx-data.mjs +32 -1
  55. package/dist/lib/dom/server/host.cjs +66 -1
  56. package/dist/lib/dom/server/host.d.ts +14 -2
  57. package/dist/lib/dom/server/host.d.ts.map +1 -1
  58. package/dist/lib/dom/server/host.js +38 -10
  59. package/dist/lib/dom/server/host.mjs +45 -1
  60. package/dist/lib/dom/server/index.cjs +759 -1
  61. package/dist/lib/dom/server/index.d.ts +1 -1
  62. package/dist/lib/dom/server/index.js +1 -1
  63. package/dist/lib/dom/server/index.mjs +743 -1
  64. package/dist/lib/dom/server/nodes.cjs +302 -1
  65. package/dist/lib/dom/server/nodes.d.ts +2 -2
  66. package/dist/lib/dom/server/nodes.d.ts.map +1 -1
  67. package/dist/lib/dom/server/nodes.js +4 -4
  68. package/dist/lib/dom/server/nodes.mjs +279 -1
  69. package/dist/lib/dom/server/parse-template.cjs +395 -1
  70. package/dist/lib/dom/server/parse-template.d.ts +1 -1
  71. package/dist/lib/dom/server/parse-template.js +4 -4
  72. package/dist/lib/dom/server/parse-template.mjs +372 -1
  73. package/dist/lib/dom/server/render.cjs +691 -1
  74. package/dist/lib/dom/server/render.d.ts +9 -1
  75. package/dist/lib/dom/server/render.d.ts.map +1 -1
  76. package/dist/lib/dom/server/render.js +25 -17
  77. package/dist/lib/dom/server/render.mjs +677 -1
  78. package/dist/lib/dom/server/request-context.cjs +110 -1
  79. package/dist/lib/dom/server/request-context.d.ts +29 -9
  80. package/dist/lib/dom/server/request-context.d.ts.map +1 -1
  81. package/dist/lib/dom/server/request-context.js +28 -8
  82. package/dist/lib/dom/server/request-context.mjs +89 -1
  83. package/dist/lib/dom/server/serialize.cjs +187 -1
  84. package/dist/lib/dom/server/serialize.js +3 -3
  85. package/dist/lib/dom/server/serialize.mjs +164 -1
  86. package/dist/lib/dom/server-renderer.cjs +760 -1
  87. package/dist/lib/dom/server-renderer.js +1 -1
  88. package/dist/lib/dom/server-renderer.mjs +745 -1
  89. package/dist/lib/dom/types.cjs +18 -1
  90. package/dist/lib/dom/utils.cjs +381 -1
  91. package/dist/lib/dom/utils.d.ts +1 -88
  92. package/dist/lib/dom/utils.d.ts.map +1 -1
  93. package/dist/lib/dom/utils.js +2 -99
  94. package/dist/lib/dom/utils.mjs +361 -1
  95. package/dist/lib/dom/versions.cjs +60 -1
  96. package/dist/lib/dom/versions.d.ts +3 -3
  97. package/dist/lib/dom/versions.d.ts.map +1 -1
  98. package/dist/lib/dom/versions.js +17 -5
  99. package/dist/lib/dom/versions.mjs +37 -1
  100. package/dist/lib/element/attributes.cjs +19 -1
  101. package/dist/lib/element/attributes.d.ts +7 -7
  102. package/dist/lib/element/attributes.js +1 -1
  103. package/dist/lib/element/attributes.mjs +1 -0
  104. package/dist/lib/element/index.cjs +19 -1
  105. package/dist/lib/element/index.mjs +1 -0
  106. package/dist/lib/element/types.cjs +18 -1
  107. package/dist/lib/element/types.d.ts +4 -5
  108. package/dist/lib/element/types.d.ts.map +1 -1
  109. package/dist/lib/element/types.js +0 -1
  110. package/dist/lib/flow/class-map.cjs +38 -1
  111. package/dist/lib/flow/class-map.d.ts +3 -3
  112. package/dist/lib/flow/class-map.js +3 -3
  113. package/dist/lib/flow/class-map.mjs +17 -1
  114. package/dist/lib/flow/client-only.cjs +57 -1
  115. package/dist/lib/flow/client-only.d.ts +4 -4
  116. package/dist/lib/flow/client-only.js +4 -4
  117. package/dist/lib/flow/client-only.mjs +36 -1
  118. package/dist/lib/flow/dynamic.cjs +53 -1
  119. package/dist/lib/flow/dynamic.d.ts +2 -2
  120. package/dist/lib/flow/dynamic.js +4 -4
  121. package/dist/lib/flow/dynamic.mjs +32 -1
  122. package/dist/lib/flow/error-boundary.cjs +43 -1
  123. package/dist/lib/flow/error-boundary.d.ts +2 -2
  124. package/dist/lib/flow/error-boundary.js +4 -4
  125. package/dist/lib/flow/error-boundary.mjs +22 -1
  126. package/dist/lib/flow/for.cjs +66 -1
  127. package/dist/lib/flow/for.d.ts +9 -9
  128. package/dist/lib/flow/for.d.ts.map +1 -1
  129. package/dist/lib/flow/for.js +2 -2
  130. package/dist/lib/flow/for.mjs +45 -1
  131. package/dist/lib/flow/index-flow.cjs +67 -1
  132. package/dist/lib/flow/index-flow.d.ts +5 -5
  133. package/dist/lib/flow/index-flow.js +4 -4
  134. package/dist/lib/flow/index-flow.mjs +46 -1
  135. package/dist/lib/flow/index.cjs +805 -1
  136. package/dist/lib/flow/index.mjs +792 -1
  137. package/dist/lib/flow/portal.cjs +538 -1
  138. package/dist/lib/flow/portal.mjs +519 -1
  139. package/dist/lib/flow/show.cjs +81 -1
  140. package/dist/lib/flow/show.d.ts +1 -1
  141. package/dist/lib/flow/show.d.ts.map +1 -1
  142. package/dist/lib/flow/show.js +12 -118
  143. package/dist/lib/flow/show.mjs +67 -1
  144. package/dist/lib/flow/style-map.cjs +43 -1
  145. package/dist/lib/flow/style-map.d.ts +3 -3
  146. package/dist/lib/flow/style-map.js +4 -4
  147. package/dist/lib/flow/style-map.mjs +22 -1
  148. package/dist/lib/flow/switch.cjs +91 -1
  149. package/dist/lib/flow/switch.d.ts +3 -3
  150. package/dist/lib/flow/switch.d.ts.map +1 -1
  151. package/dist/lib/flow/switch.js +7 -7
  152. package/dist/lib/flow/switch.mjs +70 -1
  153. package/dist/lib/flow/utils.cjs +44 -1
  154. package/dist/lib/flow/utils.d.ts +1 -1
  155. package/dist/lib/flow/utils.js +1 -1
  156. package/dist/lib/flow/utils.mjs +23 -1
  157. package/dist/lib/html.cjs +32 -1
  158. package/dist/lib/html.d.ts +6 -6
  159. package/dist/lib/html.js +5 -5
  160. package/dist/lib/html.mjs +11 -1
  161. package/dist/lib/index.cjs +3104 -1
  162. package/dist/lib/index.d.ts +0 -1
  163. package/dist/lib/index.d.ts.map +1 -1
  164. package/dist/lib/index.js +2 -3
  165. package/dist/lib/index.mjs +3097 -1
  166. package/dist/lib/shared/types.cjs +18 -1
  167. package/dist/lib/shared/types.d.ts +8 -8
  168. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  169. package/dist/version.generated.cjs +26 -1
  170. package/dist/version.generated.d.ts +1 -1
  171. package/dist/version.generated.js +2 -2
  172. package/dist/version.generated.mjs +5 -1
  173. package/package.json +7 -6
  174. package/dist/lib/jsx/control-flow/flow.cjs +0 -1
  175. package/dist/lib/jsx/control-flow/flow.d.ts +0 -2
  176. package/dist/lib/jsx/control-flow/flow.d.ts.map +0 -1
  177. package/dist/lib/jsx/control-flow/flow.js +0 -627
  178. package/dist/lib/jsx/control-flow/flow.mjs +0 -0
  179. package/dist/lib/jsx/control-flow/for-flow.cjs +0 -1
  180. package/dist/lib/jsx/control-flow/for-flow.d.ts +0 -2
  181. package/dist/lib/jsx/control-flow/for-flow.d.ts.map +0 -1
  182. package/dist/lib/jsx/control-flow/for-flow.js +0 -1
  183. package/dist/lib/jsx/control-flow/for-flow.mjs +0 -0
@@ -1,10 +1,9 @@
1
1
  /**
2
- * @fileoverview Core Reactive DOM Runtime
3
2
  * @module @fluixi/dom/runtime
4
3
  *
5
4
  * Fine-grained reactive DOM manipulation utilities inspired by SolidJS.
6
5
  * Key design: Event delegation uses `$$eventname` properties on DOM nodes
7
- * (the SolidJS pattern) simpler and more reliable than Map-based lookup.
6
+ * (the SolidJS pattern): simpler and more reliable than Map-based lookup.
8
7
  *
9
8
  * Critical fixes applied:
10
9
  * - insertContent array branch: nested signal content cleanup via onCleanup
@@ -18,8 +17,8 @@ import { stampVersions, warnOnVersionSkew } from './versions.js';
18
17
  import { isComponent } from '../component/modifiers.js';
19
18
  // Reactive primitives come straight from the signal core (the globalThis-singleton graph makes
20
19
  // this duplication-safe). The integration bridge is only for framework-provided glue that is
21
- // NOT in the reactive core (e.g. createComponent).
22
- import { batch, createEffect, createRenderEffect, createRoot, onCleanup, readChildren } from '@fluixi/reactive/signal';
20
+ // not in the reactive core (e.g. createComponent).
21
+ import { batch, createEffect, createRenderEffect, createRoot, onCleanup, readChildren, reportReactiveBinding, reportReactiveDirective, untrack } from '@fluixi/reactive/signal';
23
22
  import { isServer, getServerNodes, isDomNode, isDomElement, isDomText, } from './server/host.js';
24
23
  import { hydration, getHydrationWalker, isAdopted, runHydrationAt, ascendPast, } from './hydration-state.js';
25
24
  function getElementFingerprint(el) {
@@ -71,24 +70,34 @@ export const generateID = () => {
71
70
  // ============================================================================
72
71
  const EMPTY_OBJ = {};
73
72
  const EMPTY_ARR = [];
73
+ /** Where devtools leaves the source position of a hole. Absent in a production build. */
74
+ const HOLE_TAG = '__fx_at';
75
+ /** Where an element's own source position goes, for devtools to read back. */
76
+ const LOC_TAG = '__fx_loc';
77
+ /** Only ever called with a position when the compiler recorded one. */
78
+ function located(element, at) {
79
+ if (at)
80
+ element[LOC_TAG] = at;
81
+ return element;
82
+ }
74
83
  // Template cache
75
84
  const templateCache = new Map();
76
85
  // ============================================================================
77
- // Event Delegation SolidJS $$eventname property pattern
86
+ // Event Delegation. SolidJS $$eventname property pattern
78
87
  //
79
88
  // Handlers are stored directly on DOM nodes as `node.$$click = handler`.
80
- // The capture listener reads `node.$$click` directly no Map lookups needed.
89
+ // The capture listener reads `node.$$click` directly, no Map lookups needed.
81
90
  // This approach correctly handles e.preventDefault() and stopPropagation.
82
91
  // ============================================================================
83
92
  const delegatedSet = new Set();
84
- // Events that don't bubble must use direct addEventListener
93
+ // Events that don't bubble: must use direct addEventListener
85
94
  const NON_DELEGATED = new Set(['scroll', 'focus', 'blur', 'load', 'error', 'resize', 'mouseenter', 'mouseleave', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture']);
86
95
  /**
87
96
  * Register global delegated event listener for the given event names.
88
97
  * Stores handlers as `element.$$eventname = handler` properties.
89
98
  */
90
99
  export function delegateEvents(eventNames) {
91
- if (isServer)
100
+ if (isServer())
92
101
  return; // no document to attach to; events are inert in SSR
93
102
  for (const name of eventNames) {
94
103
  if (delegatedSet.has(name) || NON_DELEGATED.has(name))
@@ -115,12 +124,12 @@ function delegatedEventHandler(e) {
115
124
  }
116
125
  }
117
126
  /**
118
- * Add a delegated event handler stores as $$eventname property
127
+ * Add a delegated event handler: stores as $$eventname property
119
128
  */
120
129
  export function addDelegatedEventListener(element, name, handler) {
121
130
  element[`$$${name}`] = handler;
122
131
  if (NON_DELEGATED.has(name)) {
123
- // Non-bubbling event (focus/blur/pointerenter/…): document delegation can't observe it, so
132
+ // Non-bubbling event (focus/blur/pointerenter/...): document delegation can't observe it, so
124
133
  // attach a direct listener once. A stable forwarder reads $$name so updating the handler
125
134
  // doesn't churn addEventListener/removeEventListener.
126
135
  const key = `__fx_${name}`;
@@ -163,9 +172,9 @@ export function isFunction(value) {
163
172
  return typeof value === 'function';
164
173
  }
165
174
  function createTextNode(value, anchor = false) {
166
- if (isServer)
175
+ if (isServer())
167
176
  return getServerNodes().createText(value);
168
- // Anchors/placeholders (insert markers) are client-only insertion points the server
177
+ // Anchors/placeholders (insert markers) are client-only insertion points, the server
169
178
  // serialized nothing for them, so they must never adopt a server text node. Only real
170
179
  // content goes through the hydration walker.
171
180
  if (hydration.active && !anchor) {
@@ -176,7 +185,7 @@ function createTextNode(value, anchor = false) {
176
185
  return document.createTextNode(value);
177
186
  }
178
187
  function createMarker(value) {
179
- if (isServer)
188
+ if (isServer())
180
189
  return getServerNodes().createComment(value);
181
190
  if (hydration.active) {
182
191
  const node = getHydrationWalker()?.hydrateMarker(value);
@@ -185,18 +194,34 @@ function createMarker(value) {
185
194
  }
186
195
  return document.createComment(value);
187
196
  }
188
- export function createNativeElement(tag, isSVG = false) {
189
- if (isServer)
197
+ /**
198
+ * Create an element, or adopt the server's during hydration.
199
+ *
200
+ * One of the calls the compiler emits, so its three modes are the whole of how a
201
+ * template becomes DOM. Server rendering routes to the realm's node factory and returns
202
+ * a string-serializable stand-in rather than a real element. Hydration asks the walker
203
+ * for the next element the server wrote and reuses it. Otherwise it creates one.
204
+ *
205
+ * `at` is a dev-only source position, stamped onto the node for devtools. A production
206
+ * build passes none and carries no marker.
207
+ */
208
+ export function createNativeElement(tag, isSVG = false, at) {
209
+ if (isServer())
190
210
  return getServerNodes().createElement(tag, isSVG);
191
211
  if (hydration.active) {
192
212
  const node = getHydrationWalker()?.hydrateElement(tag);
193
213
  if (node)
194
- return node;
214
+ return at ? located(node, at) : node;
195
215
  }
196
- return isSVG
197
- ? document.createElementNS(SVG_NAMESPACE, tag)
198
- : document.createElement(tag);
216
+ return located(isSVG ? document.createElementNS(SVG_NAMESPACE, tag) : document.createElement(tag), at);
199
217
  }
218
+ /**
219
+ * Parse static template HTML once and cache it by that string.
220
+ *
221
+ * Every clone of a template comes from one parse, which is most of why a compiled
222
+ * template is cheap. The cache is keyed by the markup rather than by identity, so two
223
+ * modules emitting the same HTML share the parse.
224
+ */
200
225
  export function createTemplate(html, isSVG = false) {
201
226
  const cached = templateCache.get(html);
202
227
  if (cached)
@@ -216,17 +241,17 @@ export function createTemplate(html, isSVG = false) {
216
241
  * needs it.
217
242
  *
218
243
  * The three cases mirror {@link createNativeElement}, which is the point: a template has
219
- * to behave like the elements it replaces, or it works only in client-rendered apps
244
+ * to behave like the elements it replaces, or it works only in client-rendered apps,
220
245
  * which is exactly how it failed before this existed.
221
246
  *
222
- * - **server** carried as text and written out verbatim. Parsing it into elements
247
+ * - **server**: carried as text and written out verbatim. Parsing it into elements
223
248
  * only to serialize them straight back would be pure cost.
224
- * - **hydrating** adopt the server's existing subtree and skip past it. The subtree
249
+ * - **hydrating**: adopt the server's existing subtree and skip past it. The subtree
225
250
  * is inert, so there is nothing inside to wire up.
226
- * - **client** parse once (cached by HTML) and clone per instance.
251
+ * - **client**: parse once (cached by HTML) and clone per instance.
227
252
  */
228
- export function templateNode(html, tag, holes = false, isSVG = false) {
229
- if (isServer) {
253
+ export function templateNode(html, tag, holes = false, isSVG = false, at) {
254
+ if (isServer()) {
230
255
  // With holes the server needs real nodes for the hole content to be inserted into;
231
256
  // without them the string can be written out untouched, which is strictly cheaper.
232
257
  const nodes = getServerNodes();
@@ -237,16 +262,38 @@ export function templateNode(html, tag, holes = false, isSVG = false) {
237
262
  // through the adopted tree, whose structure matches the template by construction.
238
263
  const node = getHydrationWalker()?.hydrateStatic(tag);
239
264
  if (node)
240
- return node;
265
+ return stampPositions(node, at);
241
266
  }
242
- return cloneTemplate(createTemplate(html, isSVG), isSVG).firstChild;
267
+ return stampPositions(cloneTemplate(createTemplate(html, isSVG), isSVG).firstChild, at);
268
+ }
269
+ /**
270
+ * Write each element's own source position onto it.
271
+ *
272
+ * The whole template is one string, so a source map traces every element in it back to the
273
+ * line the root opens at: devtools would name the component and nothing finer. This is the
274
+ * one moment the two line up: the tree holds exactly the elements the compiler listed, in
275
+ * the same order, because holes are filled after this returns.
276
+ *
277
+ * Only reached when the compiler emitted positions, which it does for a dev build.
278
+ */
279
+ function stampPositions(node, at) {
280
+ const root = node;
281
+ if (!at || typeof root?.querySelectorAll !== 'function')
282
+ return node;
283
+ const all = [root, ...root.querySelectorAll('*')];
284
+ for (let i = 0; i < all.length && i * 2 + 1 < at.pos.length; i++) {
285
+ const line = at.pos[i * 2];
286
+ if (line)
287
+ all[i][LOC_TAG] = { file: at.file, line, column: at.pos[i * 2 + 1] };
288
+ }
289
+ return node;
243
290
  }
244
291
  /**
245
292
  * Evaluate a hole's content with the hydration cursor placed at what the server rendered
246
293
  * for *that hole*.
247
294
  *
248
295
  * Without this, only the hole's top level adopts. `templateNode` moves the cursor past the
249
- * whole template the subtree is inert, so there is nothing in it to walk which leaves
296
+ * whole template, the subtree is inert, so there is nothing in it to walk, which leaves
250
297
  * the cursor in the wrong place for anything the hole builds internally. A component in a
251
298
  * hole would then recreate its own elements: no duplication, but every server node thrown
252
299
  * away, which is most of what hydration is for.
@@ -257,22 +304,35 @@ export function holeScope(start, produce) {
257
304
  if (!hydration.active)
258
305
  return produce();
259
306
  // `produce` is called with the cursor already placed, because the value may be built
260
- // eagerly `props.children` is a getter, and reading it constructs its nodes there and
307
+ // eagerly: `props.children` is a getter, and reading it constructs its nodes there and
261
308
  // then. Wrapping the result instead would be too late: the DOM would already exist,
262
309
  // built against whatever cursor happened to be current, and the server's nodes would be
263
310
  // left behind as duplicates.
264
311
  const value = runHydrationAt(start.nextSibling, produce);
265
312
  // A memo or accessor builds nothing until it is read, which happens inside `insert`'s
266
- // effect so it needs the cursor placed again at that moment. The guard matters: once
313
+ // effect, so it needs the cursor placed again at that moment. The guard matters: once
267
314
  // hydration is over, later updates must render normally rather than re-enter it.
268
315
  if (typeof value === 'function') {
269
316
  const accessor = value;
270
- return ((...args) => hydration.active
317
+ const wrapped = (...args) => hydration.active
271
318
  ? runHydrationAt(start.nextSibling, () => accessor(...args))
272
- : accessor(...args));
319
+ : accessor(...args);
320
+ return carryHoleTag(accessor, wrapped);
273
321
  }
274
322
  return value;
275
323
  }
324
+ /**
325
+ * The compiler tags a hole's accessor with where it was written, and devtools reads the tag
326
+ * off whatever finally reaches `insert`. Under hydration that is this wrapper, not the
327
+ * accessor, so the tag has to come along: otherwise the binding reports the frame it was
328
+ * created from, which is a line in this file.
329
+ */
330
+ function carryHoleTag(from, to) {
331
+ const tag = from[HOLE_TAG];
332
+ if (tag !== undefined)
333
+ to[HOLE_TAG] = tag;
334
+ return to;
335
+ }
276
336
  /**
277
337
  * The closing marker for a hole opened at `start`.
278
338
  *
@@ -293,7 +353,7 @@ export function holeEnd(start) {
293
353
  *
294
354
  * On a fresh clone the markers are adjacent and this is empty, so `insert` builds the
295
355
  * content. When hydrating, the server's rendered content sits between them and is handed
296
- * back `insertExpression` then reuses those nodes (setting `.data` on a text node
356
+ * back: `insertExpression` then reuses those nodes (setting `.data` on a text node
297
357
  * rather than creating one), which is what adoption means here.
298
358
  *
299
359
  * Paired markers are what make this exact: a hole's content is *by definition* the nodes
@@ -331,12 +391,12 @@ export function fnName(fn) {
331
391
  return fn._name || fn.name || 'anonymous';
332
392
  }
333
393
  /**
334
- * iterateFn resolves a value by calling through plain function wrappers.
394
+ * iterateFn: resolves a value by calling through plain function wrappers.
335
395
  *
336
396
  * Must stop at signals/memos/resources ($SIGNAL marker) and
337
397
  * component thunks (fluixi-component marker). Without this:
338
398
  * - components returning memos get unwrapped to static values, losing reactivity
339
- * - lazy component createMemo re-triggers C(props) infinite recursion
399
+ * - lazy component createMemo re-triggers C(props) -> infinite recursion
340
400
  */
341
401
  export function iterateFn(item) {
342
402
  // Stop immediately if it's a signal, memo, or resource
@@ -347,7 +407,7 @@ export function iterateFn(item) {
347
407
  return item;
348
408
  if (typeof item !== 'function')
349
409
  return item;
350
- // Unwrap plain function wrappers stop at signals and component thunks
410
+ // Unwrap plain function wrappers: stop at signals and component thunks
351
411
  let content = item();
352
412
  while (typeof content === 'function' &&
353
413
  !isSignal(content) &&
@@ -365,13 +425,18 @@ const MAX_INSERT_DEPTH = 200;
365
425
  // _createInsertEffect
366
426
  //
367
427
  // Creates a reactive render effect for DOM insertion.
368
- // Uses a componentCache to ensure component thunks are called at most once
428
+ // Uses a componentCache to ensure component thunks are called at most once:
369
429
  // same thunk function reference always returns the same output across re-runs.
370
430
  // ============================================================================
371
431
  function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCurrent) {
372
432
  let current = initialCurrent;
373
433
  const componentCache = new Map();
374
434
  return createRenderEffect(() => {
435
+ // The accessor, never its result: it carries the compiler's hole position, and calling it
436
+ // again here builds the dom twice, during hydration, a duplicated subtree.
437
+ //
438
+ // Every run, because what this effect writes into moves as the value does.
439
+ reportReactiveBinding(marker ?? parent, 'content', undefined, getAccessor);
375
440
  let value;
376
441
  // ── 0. If accessor itself is a component thunk (not a signal/memo), call it once via cache ──
377
442
  // must check !isSignal to avoid caching reactive values that are also
@@ -441,9 +506,22 @@ function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCur
441
506
  // ============================================================================
442
507
  // insert
443
508
  // ============================================================================
509
+ /**
510
+ * Place content in a parent, and keep it current when the content is reactive.
511
+ *
512
+ * The other half of the compiler's output. `accessor` may be a value or a function: a
513
+ * function is read inside a render effect, so the position updates on its own when
514
+ * anything it read changes. That is where fine-grained updating actually happens, since
515
+ * only this position is touched rather than the subtree around it.
516
+ *
517
+ * `marker` is the node the content sits before, which is what makes several holes in one
518
+ * parent independent. `init` is what the position already holds, empty on a fresh clone
519
+ * and the server's nodes when hydrating, so hydration reconciles against them instead of
520
+ * building a second copy.
521
+ */
444
522
  export function insert(parent, accessor, marker, init) {
445
523
  if (_insertDepth > MAX_INSERT_DEPTH) {
446
- console.error('[insert] Exceeded max call depth possible infinite loop.');
524
+ console.error('[insert] Exceeded max call depth, possible infinite loop.');
447
525
  return () => null;
448
526
  }
449
527
  if (marker !== undefined && !marker) {
@@ -462,7 +540,7 @@ export function insert(parent, accessor, marker, init) {
462
540
  if (typeof accessor !== 'function' && !isReactiveArray(accessor)) {
463
541
  _insertDepth++;
464
542
  try {
465
- // `current` not null. The caller passes what is already in this slot, and for a
543
+ // `current`, not null. The caller passes what is already in this slot, and for a
466
544
  // hydrating template hole that is the server's own nodes (`holeContent`). Dropping it
467
545
  // meant a static value never adopted: insertExpression saw an empty slot and appended
468
546
  // a second copy beside the server's, so `{cmd}` rendered the string twice.
@@ -547,14 +625,14 @@ export function insertExpression(parent, value, marker, current, componentCache)
547
625
  }
548
626
  else if (!isAdopted(parent, node)) {
549
627
  // during hydration createTextNode reuses the in-place server text node and
550
- // already advanced the cursor don't re-insert (that would reorder).
628
+ // already advanced the cursor: don't re-insert (that would reorder).
551
629
  insertBefore(parent, node, marker);
552
630
  }
553
631
  return node;
554
632
  }
555
633
  if (isDomNode(value)) {
556
634
  // Hydration: the node was already produced from the server DOM (reused by
557
- // createNativeElement) and sits in `parent` in place adopt it without
635
+ // createNativeElement) and sits in `parent` in place: adopt it without
558
636
  // re-inserting, and ascend the cursor past its (already-walked) subtree.
559
637
  if (isAdopted(parent, value)) {
560
638
  ascendPast(value);
@@ -598,13 +676,13 @@ export function insertExpression(parent, value, marker, current, componentCache)
598
676
  const reactiveSlots = [];
599
677
  flattenArray(value, array, parent, marker, true, componentCache, reactiveSlots);
600
678
  const reconciled = reconcileArrays(parent, currentArray, array, marker);
601
- // Markers are now in the DOM wire a per-slot reactive insert. insert() sets up a nested
679
+ // Markers are now in the DOM: wire a per-slot reactive insert. insert() sets up a nested
602
680
  // render effect that re-inserts the member's content before its marker on change, so a bare
603
681
  // control-flow component return (<Show>/<Switch>/<For>) updates in place.
604
682
  //
605
683
  // The per-slot content lives OUTSIDE the reconcile array (only the marker is tracked), so if
606
- // this enclosing effect re-runs with a shorter array e.g. a <For> item dropped, or its child
607
- // resolved to a memo (Provider -> component) reconcile removes the marker but would orphan the
684
+ // this enclosing effect re-runs with a shorter array: e.g. a <For> item dropped, or its child
685
+ // resolved to a memo (Provider -> component): reconcile removes the marker but would orphan the
608
686
  // content. Register a cleanup that removes the slot's nodes when its effect is torn down. This
609
687
  // only fires when the enclosing effect re-runs (the array itself changed); an in-place update
610
688
  // (e.g. <Show> toggling) never re-runs this effect, so it keeps updating in place.
@@ -630,12 +708,12 @@ export function insertExpression(parent, value, marker, current, componentCache)
630
708
  }
631
709
  return cleanChildren(parent, current, marker);
632
710
  }
633
- // flattenArray flatten a children array to DOM nodes. A signal whose result is a
711
+ // flattenArray: flatten a children array to DOM nodes. A signal whose result is a
634
712
  // component thunk (e.g. a router memo returning a SuspenseThunk) is recursed through
635
713
  // flattenArray, not read inline, so the thunk goes through componentCache and stays
636
714
  // stable across navigations.
637
715
  function flattenArray(arr, result, parent, marker, track = true, componentCache,
638
- // When provided, a reactive member is NOT read inline (which would subscribe the enclosing
716
+ // When provided, a reactive member is not read inline (which would subscribe the enclosing
639
717
  // render effect and re-render the whole subtree on change). Instead it emits a placeholder
640
718
  // marker and records itself here; the array caller wires a fine-grained insert() per slot
641
719
  // after the markers are placed. This is what makes a bare control-flow component return
@@ -656,14 +734,14 @@ reactiveSlots) {
656
734
  if (typeof item === 'function') {
657
735
  let resolved;
658
736
  if (isComponent(item) && !isSignal(item)) {
659
- // Pure component thunk (e.g. () => <div>) call exactly once and cache so
737
+ // Pure component thunk (e.g. () => <div>): call exactly once and cache so
660
738
  // re-runs of the enclosing render effect return the same DOM nodes (keeps
661
739
  // focus etc. stable across navigations).
662
740
  // must check !isSignal. A memo stamped as a component (e.g. an
663
741
  // Outlet/Routes/Suspense output returned via $component/asComponent) is BOTH
664
742
  // isComponent AND isSignal; caching it would freeze its first value (so a
665
- // boundary that later switches from a single node to an array fallback ->
666
- // children would never update). Read those reactively in the else branch.
743
+ // boundary that later switches from a single node to an array: fallback ->
744
+ // children: would never update). Read those reactively in the else branch.
667
745
  if (!componentCache)
668
746
  componentCache = new Map();
669
747
  if (!componentCache.has(item))
@@ -671,17 +749,17 @@ reactiveSlots) {
671
749
  resolved = componentCache.get(item);
672
750
  }
673
751
  else {
674
- // Signal/memo (or memo-stamped component) read to track reactivity.
752
+ // Signal/memo (or memo-stamped component): read to track reactivity.
675
753
  resolved = item();
676
754
  }
677
755
  // Unwrap plain wrappers; stop at signals and component thunks.
678
- // Cache calls by function reference a plain function returned by a
756
+ // Cache calls by function reference, a plain function returned by a
679
757
  // signal/memo (e.g. `createMemo(() => cond ? () => <JSX> : ...)`) is a
680
758
  // stable reference as long as the memo doesn't re-evaluate. Calling it
681
759
  // once and caching the result means re-runs of the outer render effect
682
760
  // (triggered by unrelated signals like `activeLevel()`) hit the cache and
683
761
  // return the same DOM nodes. reconcileArrays then sees identical references
684
- // and skips the slot the focused <input> is never removed.
762
+ // and skips the slot, the focused <input> is never removed.
685
763
  while (typeof resolved === 'function' &&
686
764
  !isPromise(resolved) &&
687
765
  !isSignal(resolved) &&
@@ -705,8 +783,8 @@ reactiveSlots) {
705
783
  reactiveSlots.push({ marker: slot, sig: resolved });
706
784
  continue;
707
785
  }
708
- // Read the signal tracked inside the enclosing render effect.
709
- // Use flattenArray recursively (NOT normalizeValue) so
786
+ // Read the signal: tracked inside the enclosing render effect.
787
+ // Use flattenArray recursively (not normalizeValue) so
710
788
  // component thunks inside the result go through componentCache.
711
789
  let val = resolved();
712
790
  while (typeof val === 'function' && isSignal(val))
@@ -720,7 +798,7 @@ reactiveSlots) {
720
798
  flattenArray([val], result, parent, marker, track, componentCache);
721
799
  continue;
722
800
  }
723
- // Other function type skip
801
+ // Other function type: skip
724
802
  continue;
725
803
  }
726
804
  // resolved is not a function
@@ -786,15 +864,15 @@ reactiveSlots) {
786
864
  // ============================================================================
787
865
  function reconcileArrays(parent, a, b, marker) {
788
866
  // Pre-process: clone any node already owned by a different parent.
789
- // The Babel plugin hoists static elements to module-level the same node
867
+ // The Babel plugin hoists static elements to module-level, the same node
790
868
  // can appear in `b` for multiple parents simultaneously. Clone to avoid
791
869
  // stealing it from its current owner; future reconciles track the clone.
792
870
  //
793
- // A DocumentFragment is NOT such an owner. `cloneTemplate` hands back the clone still
871
+ // A DocumentFragment is not such an owner. `cloneTemplate` hands back the clone still
794
872
  // sitting in its fragment, so a template-cloned element arrives here with a non-null
795
873
  // parent that nothing else refers to. Cloning it again threw away the node whose props
796
- // had just been applied `cloneNode` copies attributes but not the event-handler
797
- // properties so an element built from a template lost its handlers the moment it was
874
+ // had just been applied: `cloneNode` copies attributes but not the event-handler
875
+ // properties, so an element built from a template lost its handlers the moment it was
798
876
  // inserted as part of an array. Moving it out of the fragment is exactly right.
799
877
  for (let i = 0; i < b.length; i++) {
800
878
  const owner = b[i].parentNode;
@@ -811,18 +889,18 @@ function reconcileArrays(parent, a, b, marker) {
811
889
  ? a[aEnd - 1].nextSibling ?? undefined
812
890
  : marker, map = null;
813
891
  while (aStart < aEnd || bStart < bEnd) {
814
- // 1. Common prefix already in the right place, skip.
892
+ // 1. Common prefix: already in the right place, skip.
815
893
  if (a[aStart] === b[bStart]) {
816
894
  aStart++;
817
895
  bStart++;
818
896
  continue;
819
897
  }
820
- // 2. Common suffix already in the right place, shrink window.
898
+ // 2. Common suffix: already in the right place, shrink window.
821
899
  while (aEnd > aStart && bEnd > bStart && a[aEnd - 1] === b[bEnd - 1]) {
822
900
  aEnd--;
823
901
  bEnd--;
824
902
  }
825
- // 3. Old list exhausted insert remaining new nodes.
903
+ // 3. Old list exhausted: insert remaining new nodes.
826
904
  if (aEnd === aStart) {
827
905
  const node = bEnd < bLength
828
906
  ? bStart
@@ -832,7 +910,7 @@ function reconcileArrays(parent, a, b, marker) {
832
910
  while (bStart < bEnd)
833
911
  insertBefore(parent, b[bStart++], node);
834
912
  }
835
- // 4. New list exhausted remove remaining old nodes.
913
+ // 4. New list exhausted: remove remaining old nodes.
836
914
  else if (bEnd === bStart) {
837
915
  while (aStart < aEnd) {
838
916
  if ((!map || !map.has(a[aStart])) && a[aStart].parentNode === parent)
@@ -870,7 +948,7 @@ function reconcileArrays(parent, a, b, marker) {
870
948
  insertBefore(parent, b[bStart++], node);
871
949
  }
872
950
  else {
873
- // Guard replaceChild: if a[aStart] was moved out by another effect, insert instead.
951
+ // Guard replaceChild, if a[aStart] was moved out by another effect, insert instead.
874
952
  if (a[aStart].parentNode === parent) {
875
953
  parent.replaceChild(b[bStart++], a[aStart++]);
876
954
  }
@@ -965,10 +1043,41 @@ export function addNativeEventListener(element, name, value) {
965
1043
  element.addEventListener(name, handler, options);
966
1044
  return () => element.removeEventListener(name, handler, options);
967
1045
  }
1046
+ /**
1047
+ * Name each key of a class or style object as the directive it came from.
1048
+ *
1049
+ * `class:a class:b` are merged by the compiler into one classList prop, so the keys are the
1050
+ * only record that two directives were written.
1051
+ */
1052
+ /**
1053
+ * A directive the compiler had to name, because the runtime sees only what it lowers to.
1054
+ * `bind:value` arrives as an ordinary value and an input handler.
1055
+ */
1056
+ function reportMarked(element, accessor) {
1057
+ if (!accessor || typeof accessor !== 'function')
1058
+ return;
1059
+ const written = accessor[HOLE_TAG]?.name;
1060
+ if (!written)
1061
+ return;
1062
+ const colon = written.indexOf(':');
1063
+ if (colon === -1)
1064
+ return;
1065
+ reportReactiveDirective(element, written.slice(0, colon), written.slice(colon + 1), accessor);
1066
+ }
1067
+ function reportKeys(element, kind, value) {
1068
+ const from = typeof value === 'function' ? untrack(() => value()) : value;
1069
+ if (!from || typeof from !== 'object')
1070
+ return;
1071
+ for (const key of Object.keys(from))
1072
+ reportReactiveDirective(element, kind, key);
1073
+ }
968
1074
  export function applyUse(element, value) {
969
1075
  const call = (dir, accessor) => {
970
- if (typeof dir === 'function')
971
- dir(element, accessor ?? (() => undefined));
1076
+ if (typeof dir !== 'function')
1077
+ return;
1078
+ // Named so a watcher can tell one directive from another. Nothing else reads it.
1079
+ reportReactiveDirective(element, 'use', dir.name || 'use', accessor);
1080
+ dir(element, accessor ?? (() => undefined));
972
1081
  };
973
1082
  if (typeof value === 'function') {
974
1083
  call(value);
@@ -989,11 +1098,13 @@ export function applyUse(element, value) {
989
1098
  }
990
1099
  }
991
1100
  export function setDynamicAttribute(element, name, accessor, isSVG = false) {
1101
+ reportMarked(element, accessor);
992
1102
  if (typeof accessor !== 'function') {
993
1103
  setAttribute(element, name, accessor, isSVG);
994
1104
  return () => { };
995
1105
  }
996
1106
  return createRenderEffect(() => {
1107
+ reportReactiveBinding(element, 'attribute', name, accessor);
997
1108
  const value = typeof accessor === 'function' ? accessor() : accessor;
998
1109
  setAttribute(element, name, value, isSVG);
999
1110
  });
@@ -1003,7 +1114,9 @@ export function setDynamicProperty(element, name, accessor) {
1003
1114
  setProperty(element, name, accessor);
1004
1115
  return () => { };
1005
1116
  }
1117
+ reportMarked(element, accessor);
1006
1118
  return createRenderEffect(() => {
1119
+ reportReactiveBinding(element, 'property', name, accessor);
1007
1120
  const value = typeof accessor === 'function' ? accessor() : accessor;
1008
1121
  setProperty(element, name, value);
1009
1122
  });
@@ -1016,6 +1129,7 @@ export function setDynamicBoolAttribute(element, name, accessor) {
1016
1129
  return () => { };
1017
1130
  }
1018
1131
  return createRenderEffect(() => {
1132
+ reportReactiveBinding(element, 'attribute', name, accessor);
1019
1133
  const value = accessor();
1020
1134
  setAttribute(element, name, !!value);
1021
1135
  });
@@ -1029,9 +1143,12 @@ export function setClassName(element, value) {
1029
1143
  return;
1030
1144
  }
1031
1145
  if (typeof value === 'function') {
1032
- // A render effect, like every other attribute binding `spread` already wraps
1146
+ // A render effect, like every other attribute binding: `spread` already wraps
1033
1147
  // class this way, so going through `applyProps` shouldn't schedule it later.
1034
- createRenderEffect(() => setClassName(element, value()));
1148
+ createRenderEffect(() => {
1149
+ reportReactiveBinding(element, 'attribute', 'class', value);
1150
+ setClassName(element, value());
1151
+ });
1035
1152
  return;
1036
1153
  }
1037
1154
  if (typeof value === 'string') {
@@ -1055,7 +1172,7 @@ export function setClassName(element, value) {
1055
1172
  element.setAttribute('class', String(value));
1056
1173
  }
1057
1174
  /**
1058
- * Apply a `classList`-style map `{ 'a b': condition }` toggling each class on
1175
+ * Apply a `classList`-style map `{ 'a b': condition }`, toggling each class on
1059
1176
  * the element. A condition may be a value or a reactive accessor; when
1060
1177
  * `disposers` is provided, accessor entries are wired with a render effect so
1061
1178
  * they update fine-grained (Solid-style `classList`).
@@ -1105,6 +1222,19 @@ export function setStyle(element, _value) {
1105
1222
  // ============================================================================
1106
1223
  // spread
1107
1224
  // ============================================================================
1225
+ /**
1226
+ * Apply a props object to an element, and re-apply what changes.
1227
+ *
1228
+ * Handles every prop kind the compiler can emit: attributes, properties, events,
1229
+ * `class`/`style` in their object and string forms, `ref` and `use:`. Each reactive prop
1230
+ * gets its own effect, so a change touches one attribute rather than re-running the
1231
+ * element.
1232
+ *
1233
+ * `prevProps` is what was applied last, which is how a removed key is cleared rather
1234
+ * than left behind. A getter-backed prop stays reactive: the value is read inside the
1235
+ * effect, not when the object was built, which is what keeps `splitProps` and a
1236
+ * polymorphic wrapper live.
1237
+ */
1108
1238
  export function spread(options) {
1109
1239
  const { element, props, prevProps = EMPTY_OBJ, isSVG = false, skipChildren = false, } = options;
1110
1240
  if (!element || !isDomElement(element)) {
@@ -1117,7 +1247,7 @@ export function spread(options) {
1117
1247
  }
1118
1248
  // Trust the element's real namespace over the compile-time isSVG flag: the codegen only marks
1119
1249
  // some SVG elements (e.g. <svg>/<defs>) and misses nested ones (<linearGradient>, <stop>). For
1120
- // an actually-SVG element, a key like `x1` IS `in element` (read-only SVGAnimatedLength), so the
1250
+ // an actually-SVG element, a key like `x1` is `in element` (read-only SVGAnimatedLength), so the
1121
1251
  // property branch below would throw "Cannot set property x1". Detecting SVG by namespaceURI keeps
1122
1252
  // those on the setAttribute path. (Matters under hydration, where we adopt the real, correctly
1123
1253
  // namespaced server node.)
@@ -1151,22 +1281,28 @@ export function spread(options) {
1151
1281
  continue;
1152
1282
  }
1153
1283
  // Forced bindings: prop:x always assigns the property, attr:x always sets the
1154
- // attribute, bool:x toggles a boolean attribute skipping the heuristic below.
1284
+ // attribute, bool:x toggles a boolean attribute, skipping the heuristic below.
1155
1285
  if (key.startsWith('prop:')) {
1286
+ reportReactiveDirective(element, 'prop', key.slice(5), value);
1156
1287
  disposers.push(setDynamicProperty(element, key.slice(5), value));
1157
1288
  continue;
1158
1289
  }
1159
1290
  if (key.startsWith('attr:')) {
1291
+ reportReactiveDirective(element, 'attr', key.slice(5), value);
1160
1292
  disposers.push(setDynamicAttribute(element, key.slice(5), value, elementIsSVG));
1161
1293
  continue;
1162
1294
  }
1163
1295
  if (key.startsWith('bool:')) {
1296
+ reportReactiveDirective(element, 'bool', key.slice(5), value);
1164
1297
  disposers.push(setDynamicBoolAttribute(element, key.slice(5), value));
1165
1298
  continue;
1166
1299
  }
1167
1300
  if (key === 'class' || key === 'className') {
1168
1301
  if (typeof value === 'function') {
1169
- disposers.push(createRenderEffect(() => setClassName(element, value())));
1302
+ disposers.push(createRenderEffect(() => {
1303
+ reportReactiveBinding(element, 'attribute', 'class', value);
1304
+ setClassName(element, value());
1305
+ }));
1170
1306
  }
1171
1307
  else {
1172
1308
  setClassName(element, value);
@@ -1174,8 +1310,12 @@ export function spread(options) {
1174
1310
  continue;
1175
1311
  }
1176
1312
  if (key === 'classList') {
1313
+ reportKeys(element, 'class', value);
1177
1314
  if (typeof value === 'function') {
1178
- disposers.push(createRenderEffect(() => setClassList(element, value())));
1315
+ disposers.push(createRenderEffect(() => {
1316
+ reportReactiveBinding(element, 'attribute', 'class', value);
1317
+ setClassList(element, value());
1318
+ }));
1179
1319
  }
1180
1320
  else {
1181
1321
  setClassList(element, value, disposers);
@@ -1183,8 +1323,12 @@ export function spread(options) {
1183
1323
  continue;
1184
1324
  }
1185
1325
  if (key === 'style') {
1326
+ reportKeys(element, 'style', value);
1186
1327
  if (typeof value === 'function') {
1187
- disposers.push(createRenderEffect(() => setStyle(element, value())));
1328
+ disposers.push(createRenderEffect(() => {
1329
+ reportReactiveBinding(element, 'attribute', 'style', value);
1330
+ setStyle(element, value());
1331
+ }));
1188
1332
  }
1189
1333
  else {
1190
1334
  setStyle(element, value);
@@ -1207,12 +1351,12 @@ export function spread(options) {
1207
1351
  : setDynamicAttribute(element, key, value, elementIsSVG));
1208
1352
  }
1209
1353
  else {
1210
- // A plain, non-function value may still be backed by a reactive GETTER —
1354
+ // A plain, non-function value may still be backed by a reactive getter:
1211
1355
  // component props compile to `get x() {...}`, and splitProps/mergeProps
1212
1356
  // forward those getters. Reading `props[key]` once (above) snapshots the
1213
1357
  // current value and drops the dependency, so the DOM never updates. Re-read
1214
1358
  // it inside a render effect so the getter's reads are tracked. (A truly
1215
- // static value just runs the effect once with no deps negligible.)
1359
+ // static value just runs the effect once with no deps: negligible.)
1216
1360
  disposers.push(createRenderEffect(() => asProperty
1217
1361
  ? setProperty(element, key, props[key])
1218
1362
  : setAttribute(element, key, props[key], elementIsSVG)));
@@ -1255,16 +1399,19 @@ export function applyProps(element, props, isSVG = false) {
1255
1399
  addNativeEventListener(element, key.slice(3), val);
1256
1400
  continue;
1257
1401
  }
1258
- // Forced bindings see spread().
1402
+ // Forced bindings: see spread().
1259
1403
  if (key.startsWith('prop:')) {
1404
+ reportReactiveDirective(element, 'prop', key.slice(5), val);
1260
1405
  setDynamicProperty(element, key.slice(5), val);
1261
1406
  continue;
1262
1407
  }
1263
1408
  if (key.startsWith('attr:')) {
1409
+ reportReactiveDirective(element, 'attr', key.slice(5), val);
1264
1410
  setDynamicAttribute(element, key.slice(5), val, isSVG);
1265
1411
  continue;
1266
1412
  }
1267
1413
  if (key.startsWith('bool:')) {
1414
+ reportReactiveDirective(element, 'bool', key.slice(5), val);
1268
1415
  setDynamicBoolAttribute(element, key.slice(5), val);
1269
1416
  continue;
1270
1417
  }
@@ -1272,7 +1419,7 @@ export function applyProps(element, props, isSVG = false) {
1272
1419
  const name = key.slice(2).toLowerCase();
1273
1420
  if (typeof val === 'function') {
1274
1421
  if (NON_DELEGATED.has(name)) {
1275
- // Non-bubbling events (blur, focus, scroll): store handler on the element
1422
+ // Non-bubbling events (blur, focus, scroll...): store handler on the element
1276
1423
  // and attach a single stable forwarder listener. This avoids
1277
1424
  // addEventListener/removeEventListener churn when a reactive scope re-runs,
1278
1425
  // which would silently drop the listener via onCleanup before re-adding it.
@@ -1298,27 +1445,35 @@ export function applyProps(element, props, isSVG = false) {
1298
1445
  continue;
1299
1446
  }
1300
1447
  if (key === 'classList') {
1448
+ reportKeys(element, 'class', val);
1301
1449
  if (typeof val === 'function')
1302
- createRenderEffect(() => setClassList(element, val()));
1450
+ createRenderEffect(() => {
1451
+ reportReactiveBinding(element, 'attribute', 'class', val);
1452
+ setClassList(element, val());
1453
+ });
1303
1454
  else
1304
1455
  setClassList(element, val);
1305
1456
  continue;
1306
1457
  }
1307
1458
  if (key === 'style') {
1308
1459
  // An accessor reaches applyProps whenever a component forwards a computed
1309
- // style through a props object rather than as a compiled JSX attribute
1460
+ // style through a props object rather than as a compiled JSX attribute,
1310
1461
  // which is what every polymorphic/`as` wrapper does. class and classList
1311
1462
  // already handled that; style applied once and never updated.
1463
+ reportKeys(element, 'style', val);
1312
1464
  if (typeof val === 'function')
1313
- createRenderEffect(() => setStyle(element, val()));
1465
+ createRenderEffect(() => {
1466
+ reportReactiveBinding(element, 'attribute', 'style', val);
1467
+ setStyle(element, val());
1468
+ });
1314
1469
  else
1315
1470
  setStyle(element, val);
1316
1471
  continue;
1317
1472
  }
1318
1473
  if (key === 'innerHTML') {
1319
- // Accept a static string OR a reactive accessor. A function is wrapped in an effect so
1474
+ // Accept a static string or a reactive accessor. A function is wrapped in an effect so
1320
1475
  // updates apply; a plain value is set once. (The previous code always called val(), which
1321
- // threw on a static string wiping content during hydration.)
1476
+ // threw on a static string, wiping content during hydration.)
1322
1477
  if (typeof val === 'function') {
1323
1478
  const dispose = createEffect(() => {
1324
1479
  let v = val();
@@ -1333,7 +1488,7 @@ export function applyProps(element, props, isSVG = false) {
1333
1488
  }
1334
1489
  continue;
1335
1490
  }
1336
- // Special case: <input>/<textarea> value use DOM property assignment, not
1491
+ // Special case: <input>/<textarea> value: use DOM property assignment, not
1337
1492
  // setAttribute. Skip reactive updates while the element has focus so user
1338
1493
  // input is never interrupted and focus is never lost due to signal writes.
1339
1494
  if (key === 'value' && (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) {
@@ -1343,7 +1498,7 @@ export function applyProps(element, props, isSVG = false) {
1343
1498
  // console.log('apply inpit props', v, element);
1344
1499
  while (typeof v === 'function' && v.length === 0)
1345
1500
  v = v();
1346
- // Only update if element is NOT currently focused don't fight user input
1501
+ // Only update if element is not currently focused: don't fight user input
1347
1502
  if (document.activeElement !== element) {
1348
1503
  element.value = v ?? '';
1349
1504
  }
@@ -1369,7 +1524,7 @@ export function applyProps(element, props, isSVG = false) {
1369
1524
  let v = val();
1370
1525
  while (typeof v === 'function' && v.length === 0)
1371
1526
  v = v();
1372
- // Only update if element is NOT currently focused don't fight user input
1527
+ // Only update if element is not currently focused: don't fight user input
1373
1528
  if (document.activeElement !== element) {
1374
1529
  element.checked = v ?? '';
1375
1530
  }
@@ -1377,7 +1532,7 @@ export function applyProps(element, props, isSVG = false) {
1377
1532
  onCleanup(() => { dispose(); });
1378
1533
  }
1379
1534
  else {
1380
- // Plain value possibly behind a reactive getter re-read live. See the
1535
+ // Plain value possibly behind a reactive getter: re-read live. See the
1381
1536
  // value branch above.
1382
1537
  createRenderEffect(() => {
1383
1538
  if (document.activeElement !== element) {
@@ -1387,8 +1542,8 @@ export function applyProps(element, props, isSVG = false) {
1387
1542
  }
1388
1543
  continue;
1389
1544
  }
1390
- // Special case: <select value=...> must assign .value as DOM property
1391
- // AFTER options are in the DOM. We defer the initial run via queueMicrotask
1545
+ // Special case: <select value=...>: must assign .value as DOM property
1546
+ // after options are in the DOM. We defer the initial run via queueMicrotask
1392
1547
  // (children/options are inserted synchronously after applyProps returns).
1393
1548
  if (key === 'value' && element.tagName === 'SELECT') {
1394
1549
  if (typeof val === 'function') {
@@ -1408,7 +1563,7 @@ export function applyProps(element, props, isSVG = false) {
1408
1563
  onCleanup(() => { dispose(); });
1409
1564
  }
1410
1565
  else {
1411
- // Plain value possibly behind a reactive getter re-read live, keeping the
1566
+ // Plain value possibly behind a reactive getter: re-read live, keeping the
1412
1567
  // first assignment deferred until the options are in the DOM.
1413
1568
  let firstRun = true;
1414
1569
  createRenderEffect(() => {
@@ -1448,19 +1603,19 @@ export function applyProps(element, props, isSVG = false) {
1448
1603
  }
1449
1604
  }
1450
1605
  /**
1451
- * Merge several (possibly reactive) prop sources into one, last-source-wins but a later
1452
- * source's `undefined` does NOT clobber an earlier defined value; it falls through to it. This
1606
+ * Merge several (possibly reactive) prop sources into one, last-source-wins, but a later
1607
+ * source's `undefined` does not clobber an earlier defined value; it falls through to it. This
1453
1608
  * is what makes `mergeProps(defaults, props)` work, and why the compiler can emit
1454
1609
  * `mergeProps(...spreads, baseProps)` for JSX spreads (`<div {...a} {...b} />`) without a missing
1455
1610
  * `b.x` wiping `a.x`.
1456
1611
  *
1457
1612
  * Like Solid's mergeProps (and `splitProps` above), the result is a Proxy that resolves each key
1458
- * LAZILY off the sources getter/signal-backed props stay live (re-read on every access, not
1613
+ * LAZILY off the sources: getter/signal-backed props stay live (re-read on every access, not
1459
1614
  * snapshotted), and the key set is the union across all sources (so dynamic prop objects work).
1460
1615
  */
1461
1616
  export function mergeProps(...sources) {
1462
1617
  // Last defined (non-undefined) value wins; scan sources from the end so a later `undefined`
1463
- // falls through to an earlier source. Reads `source[key]` live each call no snapshot.
1618
+ // falls through to an earlier source. Reads `source[key]` live each call, no snapshot.
1464
1619
  const resolve = (key) => {
1465
1620
  for (let i = sources.length - 1; i >= 0; i--) {
1466
1621
  const s = sources[i];
@@ -1493,7 +1648,7 @@ export function mergeProps(...sources) {
1493
1648
  }
1494
1649
  /**
1495
1650
  * Split a (possibly reactive) props object into groups of keys plus a `rest`,
1496
- * preserving reactivity each returned object is a Proxy whose `get` forwards
1651
+ * preserving reactivity, each returned object is a Proxy whose `get` forwards
1497
1652
  * to the original `props`, so a getter/signal-backed prop stays live.
1498
1653
  *
1499
1654
  * const [local, rest] = splitProps(props, ['class', 'id']);
@@ -1536,6 +1691,12 @@ export function splitProps(props, ...keys) {
1536
1691
  export function handleRef(ref, element) {
1537
1692
  if (!ref)
1538
1693
  return;
1694
+ // Which variable ends up holding the element is the question a ref raises, and only the
1695
+ // compiler's mark on the value can answer it.
1696
+ // The compiler's mark names the variable it is stored in; a function's own name is the
1697
+ // next best thing.
1698
+ const held = ref[HOLE_TAG]?.name;
1699
+ reportReactiveDirective(element, 'ref', held || (typeof ref === 'function' ? ref.name || 'ref' : 'ref'), ref);
1539
1700
  // Swapped-argument guard
1540
1701
  if (isDomNode(ref)) {
1541
1702
  const actualRef = element;
@@ -1624,7 +1785,7 @@ export function isPromise(v) {
1624
1785
  v instanceof Promise);
1625
1786
  }
1626
1787
  // ============================================================================
1627
- // isAccessor deprecated, kept for backward compat
1788
+ // isAccessor: deprecated, kept for backward compat
1628
1789
  // ============================================================================
1629
1790
  export function isAccessor(value) {
1630
1791
  return typeof value === 'function' && value.length === 0 && isSignal(value);
@@ -1641,7 +1802,7 @@ export function render(element, container, context) {
1641
1802
  // This is critical for context propagation (Provider/useContext).
1642
1803
  createRoot((dispose) => {
1643
1804
  batch(() => {
1644
- // Clear container (but not when hydrating we reuse the server DOM).
1805
+ // Clear container (but not when hydrating: we reuse the server DOM).
1645
1806
  if (!hydration.active) {
1646
1807
  container.textContent = '';
1647
1808
  }
@@ -1667,15 +1828,15 @@ export function render(element, container, context) {
1667
1828
  * Normalize what `bind:` was given into a `[read, write]` pair.
1668
1829
  *
1669
1830
  * `createSignal` hands back that pair already; `signal` hands back `{ get, set }`. Both
1670
- * are the same primitive, so `bind:value` has to take either steering people to the
1831
+ * are the same primitive, so `bind:value` has to take either, steering people to the
1671
1832
  * new API and then refusing it in a directive would be its own kind of broken.
1672
1833
  */
1673
1834
  export function bindPair(source) {
1674
1835
  if (Array.isArray(source))
1675
1836
  return source;
1676
- // `signal()` is the accessor itself with `set` attached.
1837
+ // `signal(...)` is the accessor itself with `set` attached.
1677
1838
  if (typeof source === 'function' && typeof source.set === 'function') {
1678
1839
  return [source, source.set];
1679
1840
  }
1680
- throw new TypeError('[fluixi] bind: needs a signal either `signal()` or the `[get, set]` pair from `createSignal()`.');
1841
+ throw new TypeError('[fluixi] bind: needs a signal, either `signal(...)` or the `[get, set]` pair from `createSignal(...)`.');
1681
1842
  }