@remix-run/ui 0.0.0 → 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.
Files changed (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,1722 @@
1
+ import type { Component, ComponentHandle, FrameContent, FrameHandle } from './component.ts'
2
+ import { createComponent, Frame } from './component.ts'
3
+ import type { Frame as FrameInstance, FrameRuntime } from './frame.ts'
4
+ import { createFrame } from './frame.ts'
5
+ import { createRangeRoot } from './vdom.ts'
6
+ import type {
7
+ ComponentNode,
8
+ CommittedComponentNode,
9
+ CommittedHostNode,
10
+ CommittedTextNode,
11
+ HostNode,
12
+ TextNode,
13
+ VNode,
14
+ VNodeType,
15
+ } from './vnode.ts'
16
+ import {
17
+ isCommittedComponentNode,
18
+ isComponentNode,
19
+ isCommittedHostNode,
20
+ isCommittedTextNode,
21
+ isFragmentNode,
22
+ isHostNode,
23
+ isTextNode,
24
+ findContextFromAncestry,
25
+ } from './vnode.ts'
26
+ import { invariant } from './invariant.ts'
27
+ import { diffHostProps } from './diff-props.ts'
28
+ import type { StyleManager } from '../style/index.ts'
29
+ import type { ElementProps } from './jsx.ts'
30
+ import { skipComments, logHydrationMismatch } from './client-entries.ts'
31
+ import type { Scheduler } from './scheduler.ts'
32
+ import { toVNode } from './to-vnode.ts'
33
+ import {
34
+ bindMixinRuntime,
35
+ cancelPendingMixinRemoval,
36
+ dispatchMixinBeforeUpdate,
37
+ dispatchMixinCommit,
38
+ getMixinRuntimeSignal,
39
+ prepareMixinRemoval,
40
+ resolveMixedProps,
41
+ teardownMixins,
42
+ type MixinRuntimeBinding,
43
+ type MixinRuntimeState,
44
+ } from './mixins/mixin.ts'
45
+
46
+ const SVG_NS = 'http://www.w3.org/2000/svg'
47
+
48
+ // Internal diffing flags (modeled after Preact)
49
+ const INSERT_VNODE = 1 << 0
50
+ const MATCHED = 1 << 1
51
+
52
+ let idCounter = 0
53
+ let persistedRemovalToken = 0
54
+ const persistedMixinNodes = new Set<CommittedHostNode>()
55
+ let activeSchedulerUpdateParents: ParentNode[] | undefined
56
+
57
+ // Compute SVG context for a node based on its parent and type.
58
+ // Returns true if the node is within an SVG subtree, false otherwise.
59
+ function getSvgContext(vParent: VNode, nodeType: VNodeType): boolean {
60
+ // Only host elements (strings) can affect SVG context
61
+ if (typeof nodeType === 'string') {
62
+ // svg element creates SVG context
63
+ if (nodeType === 'svg') return true
64
+ // foreignObject switches back to HTML context
65
+ if (nodeType === 'foreignObject') return false
66
+ }
67
+ // Otherwise inherit from parent
68
+ return vParent._svg ?? false
69
+ }
70
+
71
+ function getHostProps(node: HostNode | CommittedHostNode): ElementProps {
72
+ return node._mixedProps ?? node.props
73
+ }
74
+
75
+ function markNodePersistedByMixins(node: CommittedHostNode, domParent: ParentNode, token: number) {
76
+ node._persistedByMixins = true
77
+ node._persistedParentByMixins = domParent
78
+ node._persistedRemovalToken = token
79
+ persistedMixinNodes.add(node)
80
+ bindMixinRuntime(node._mixState as MixinRuntimeState | undefined, undefined)
81
+ }
82
+
83
+ function unmarkNodePersistedByMixins(node: CommittedHostNode) {
84
+ node._persistedByMixins = false
85
+ node._persistedParentByMixins = undefined
86
+ node._persistedRemovalToken = undefined
87
+ persistedMixinNodes.delete(node)
88
+ }
89
+
90
+ function findMatchingPersistedMixinNode(
91
+ type: string,
92
+ key: string | undefined,
93
+ domParent: ParentNode,
94
+ ): CommittedHostNode | null {
95
+ if (key == null) return null
96
+ for (let node of persistedMixinNodes) {
97
+ if (node._persistedParentByMixins !== domParent) continue
98
+ if (node.type !== type) continue
99
+ if (node.key !== key) continue
100
+ return node
101
+ }
102
+ return null
103
+ }
104
+
105
+ type ControlledReflectionState = {
106
+ disposed: boolean
107
+ listenersAttached: boolean
108
+ pendingRestoreVersion: number
109
+ managesValue: boolean
110
+ managesChecked: boolean
111
+ hasControlledValue: boolean
112
+ controlledValue: unknown
113
+ hasControlledChecked: boolean
114
+ controlledChecked: unknown
115
+ onInput: () => void
116
+ onChange: () => void
117
+ }
118
+
119
+ function shouldRestoreControlledReflectionOnInput(
120
+ node: CommittedHostNode,
121
+ state: ControlledReflectionState,
122
+ ): boolean {
123
+ // Some controls dispatch `input` before `change` for the same interaction.
124
+ // When checked/value state is typically handled on `change`, restoring on the
125
+ // earlier `input` can race and clobber the value observed by app handlers.
126
+ if (state.hasControlledChecked) return false
127
+ if (node.type === 'select') return false
128
+ return true
129
+ }
130
+
131
+ function ensureControlledReflection(
132
+ node: CommittedHostNode,
133
+ scheduler: Scheduler,
134
+ ): ControlledReflectionState {
135
+ let existing = node._controlledState as ControlledReflectionState | undefined
136
+ if (existing) return existing
137
+
138
+ let state: ControlledReflectionState = {
139
+ disposed: false,
140
+ listenersAttached: false,
141
+ pendingRestoreVersion: 0,
142
+ managesValue: false,
143
+ managesChecked: false,
144
+ hasControlledValue: false,
145
+ controlledValue: undefined,
146
+ hasControlledChecked: false,
147
+ controlledChecked: undefined,
148
+ onInput: () => {
149
+ if (!shouldRestoreControlledReflectionOnInput(node, state)) return
150
+ scheduleControlledRestore(node, state)
151
+ },
152
+ onChange: () => {
153
+ scheduleControlledRestore(node, state)
154
+ },
155
+ }
156
+
157
+ node._controlledState = state
158
+ scheduler.enqueueTasks([
159
+ () => {
160
+ if (state.disposed) return
161
+ node._dom.addEventListener('input', state.onInput)
162
+ node._dom.addEventListener('change', state.onChange)
163
+ state.listenersAttached = true
164
+ },
165
+ ])
166
+ return state
167
+ }
168
+
169
+ function syncControlledReflection(node: CommittedHostNode, props: ElementProps): void {
170
+ let state = node._controlledState as ControlledReflectionState | undefined
171
+ if (!state || state.disposed) return
172
+
173
+ state.managesValue = canManageValue(node.type, node._dom)
174
+ state.managesChecked = canReflectProperty(node._dom, 'checked')
175
+ state.hasControlledValue = state.managesValue && hasControlledValueProp(props)
176
+ state.controlledValue = props.value
177
+ state.hasControlledChecked = state.managesChecked && hasControlledCheckedProp(props)
178
+ state.controlledChecked = props.checked
179
+ state.pendingRestoreVersion++
180
+ }
181
+
182
+ function shouldTrackControlledReflection(props: ElementProps): boolean {
183
+ return hasControlledValueProp(props) || hasControlledCheckedProp(props)
184
+ }
185
+
186
+ function scheduleControlledRestore(
187
+ node: CommittedHostNode,
188
+ state: ControlledReflectionState,
189
+ ): void {
190
+ if (state.disposed) return
191
+ let version = ++state.pendingRestoreVersion
192
+ queueMicrotask(() => {
193
+ if (state.disposed) return
194
+ if (state.pendingRestoreVersion !== version) return
195
+ restoreControlledReflections(node, state)
196
+ })
197
+ }
198
+
199
+ function restoreControlledReflections(
200
+ node: CommittedHostNode,
201
+ state: ControlledReflectionState,
202
+ ): void {
203
+ let element = node._dom
204
+ if (state.hasControlledValue && readDomProp(element, 'value') !== state.controlledValue) {
205
+ setPropertyReflection(element, 'value', state.controlledValue)
206
+ }
207
+ if (state.hasControlledChecked && readDomProp(element, 'checked') !== state.controlledChecked) {
208
+ setPropertyReflection(element, 'checked', state.controlledChecked)
209
+ }
210
+ }
211
+
212
+ function teardownControlledReflection(node: CommittedHostNode): void {
213
+ let state = node._controlledState as ControlledReflectionState | undefined
214
+ if (!state) return
215
+ state.disposed = true
216
+ state.pendingRestoreVersion++
217
+ if (state.listenersAttached) {
218
+ node._dom.removeEventListener('input', state.onInput)
219
+ node._dom.removeEventListener('change', state.onChange)
220
+ state.listenersAttached = false
221
+ }
222
+ }
223
+
224
+ function canManageValue(type: string, element: Element): boolean {
225
+ if (type === 'progress') return false
226
+ return canReflectProperty(element, 'value')
227
+ }
228
+
229
+ function hasControlledValueProp(props: ElementProps): boolean {
230
+ return 'value' in props && props.value !== undefined
231
+ }
232
+
233
+ function hasControlledCheckedProp(props: ElementProps): boolean {
234
+ return 'checked' in props && props.checked !== undefined
235
+ }
236
+
237
+ function canReflectProperty(
238
+ element: Element,
239
+ key: string,
240
+ ): element is Element & Record<string, unknown> {
241
+ return key in element && !key.includes('-')
242
+ }
243
+
244
+ function readDomProp(element: Element, key: string): unknown {
245
+ if (!canReflectProperty(element, key)) return undefined
246
+ return element[key]
247
+ }
248
+
249
+ function setPropertyReflection(element: Element, key: string, value: unknown): void {
250
+ if (!canReflectProperty(element, key)) return
251
+ element[key] = value == null ? '' : value
252
+ }
253
+
254
+ function resolveNodeMixProps(
255
+ node: HostNode,
256
+ frame: FrameHandle,
257
+ scheduler: Scheduler,
258
+ state?: MixinRuntimeState,
259
+ ): ElementProps {
260
+ let mix = node.props.mix
261
+ if (state == null && (mix == null || (Array.isArray(mix) && mix.length === 0))) {
262
+ node._mixState = undefined
263
+ node._mixedProps = node.props
264
+ return node.props
265
+ }
266
+
267
+ let resolved = resolveMixedProps({
268
+ hostType: node.type,
269
+ frame,
270
+ scheduler,
271
+ getContext: (type: Component | string | symbol) => {
272
+ if (typeof type !== 'function') {
273
+ return undefined
274
+ }
275
+
276
+ return findContextFromAncestry(node, type as Component)
277
+ },
278
+ props: node.props,
279
+ state,
280
+ })
281
+ node._mixState = resolved.state
282
+ node._mixedProps = resolved.props
283
+ return resolved.props
284
+ }
285
+
286
+ function enqueueMixinBindingUpdate(
287
+ this: MixinRuntimeBinding,
288
+ done: (signal: AbortSignal) => void,
289
+ ): void {
290
+ let node = this.target as CommittedHostNode
291
+ let state = node._mixState as MixinRuntimeState | undefined
292
+ this.scheduler.enqueueWork([
293
+ () => {
294
+ if (state?.aborted) {
295
+ done(getMixinRuntimeSignal(state))
296
+ return
297
+ }
298
+
299
+ dispatchMixinBeforeUpdate(state)
300
+ let prevProps = getHostProps(node)
301
+ let nextProps = resolveNodeMixProps(node, this.frame, this.scheduler, state)
302
+ diffHostProps(prevProps, nextProps, this.node)
303
+
304
+ dispatchMixinCommit(state)
305
+ done(state ? getMixinRuntimeSignal(state) : AbortSignal.abort())
306
+ },
307
+ ])
308
+ }
309
+
310
+ function bindNodeMixRuntime(
311
+ node: CommittedHostNode,
312
+ frame: FrameHandle,
313
+ scheduler: Scheduler,
314
+ styles: StyleManager,
315
+ reclaimed: boolean = false,
316
+ parent?: ParentNode,
317
+ ) {
318
+ let state = node._mixState as MixinRuntimeState | undefined
319
+ bindMixinRuntime(
320
+ state,
321
+ {
322
+ node: node._dom,
323
+ parent: parent ?? (node._dom.parentNode as ParentNode),
324
+ key: node.key,
325
+ target: node,
326
+ frame,
327
+ scheduler,
328
+ enqueueUpdate: enqueueMixinBindingUpdate,
329
+ },
330
+ { dispatchReclaimed: reclaimed },
331
+ )
332
+ }
333
+
334
+ function isHeadHostNode(node: HostNode): boolean {
335
+ return node.type.toLowerCase() === 'head'
336
+ }
337
+
338
+ function getDocumentHead(domParent: ParentNode): HTMLHeadElement | null {
339
+ if (domParent instanceof Document) {
340
+ return domParent.head
341
+ }
342
+ if (domParent instanceof Node) {
343
+ return domParent.ownerDocument?.head ?? null
344
+ }
345
+ return null
346
+ }
347
+
348
+ export function diffVNodes(
349
+ curr: VNode | null,
350
+ next: VNode,
351
+ domParent: ParentNode,
352
+ frame: FrameHandle,
353
+ scheduler: Scheduler,
354
+ styles: StyleManager,
355
+ vParent: VNode,
356
+ rootTarget: EventTarget,
357
+ anchor?: Node,
358
+ rootCursor?: Node | null,
359
+ ): Node | null | undefined {
360
+ next._parent = vParent // set parent for initial render context lookups
361
+ next._svg = getSvgContext(vParent, next.type)
362
+
363
+ // new
364
+ if (curr === null) {
365
+ return insert(
366
+ next,
367
+ domParent,
368
+ frame,
369
+ scheduler,
370
+ styles,
371
+ vParent,
372
+ rootTarget,
373
+ anchor,
374
+ rootCursor,
375
+ )
376
+ }
377
+
378
+ if (curr.type !== next.type) {
379
+ replace(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)
380
+ return rootCursor
381
+ }
382
+
383
+ if (isCommittedTextNode(curr) && isTextNode(next)) {
384
+ diffText(curr, next, vParent)
385
+ return rootCursor
386
+ }
387
+
388
+ if (isCommittedHostNode(curr) && isHostNode(next)) {
389
+ diffHost(curr, next, frame, scheduler, styles, vParent, rootTarget)
390
+ return rootCursor
391
+ }
392
+
393
+ if (isCommittedComponentNode(curr) && isComponentNode(next)) {
394
+ diffComponent(curr, next, frame, scheduler, styles, domParent, vParent, rootTarget)
395
+ return rootCursor
396
+ }
397
+
398
+ if (isFragmentNode(curr) && isFragmentNode(next)) {
399
+ diffChildren(
400
+ curr._children,
401
+ next._children,
402
+ domParent,
403
+ frame,
404
+ scheduler,
405
+ styles,
406
+ vParent,
407
+ rootTarget,
408
+ undefined,
409
+ anchor,
410
+ )
411
+ return rootCursor
412
+ }
413
+
414
+ if (curr.type === Frame && next.type === Frame) {
415
+ diffFrame(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)
416
+ return rootCursor
417
+ }
418
+
419
+ invariant(false, 'Unexpected diff case')
420
+ }
421
+
422
+ function replace(
423
+ curr: VNode,
424
+ next: VNode,
425
+ domParent: ParentNode,
426
+ frame: FrameHandle,
427
+ scheduler: Scheduler,
428
+ styles: StyleManager,
429
+ vParent: VNode,
430
+ rootTarget: EventTarget,
431
+ anchor?: Node,
432
+ ) {
433
+ let currAnchor = findFirstDomAnchor(curr)
434
+ if (currAnchor && currAnchor.parentNode === domParent) {
435
+ let replacementAnchor = document.createComment('rmx:replace')
436
+ domParent.insertBefore(replacementAnchor, currAnchor)
437
+ try {
438
+ remove(curr, domParent, scheduler, styles)
439
+ insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
440
+ } finally {
441
+ replacementAnchor.parentNode?.removeChild(replacementAnchor)
442
+ }
443
+ return
444
+ }
445
+
446
+ let replacementAnchor = findNextSiblingDomAnchor(curr, vParent) ?? anchor
447
+ remove(curr, domParent, scheduler, styles)
448
+ insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
449
+ }
450
+
451
+ function diffHost(
452
+ curr: CommittedHostNode,
453
+ next: HostNode,
454
+ frame: FrameHandle,
455
+ scheduler: Scheduler,
456
+ styles: StyleManager,
457
+ vParent: VNode,
458
+ rootTarget: EventTarget,
459
+ ) {
460
+ let mixState = curr._mixState as MixinRuntimeState | undefined
461
+ let currProps = getHostProps(curr)
462
+ let nextProps = resolveNodeMixProps(next, frame, scheduler, mixState)
463
+ if (shouldDispatchInlineMixinLifecycle(curr._dom)) {
464
+ dispatchMixinBeforeUpdate(next._mixState as MixinRuntimeState | undefined)
465
+ }
466
+
467
+ // Handle innerHTML prop BEFORE diffChildren to avoid clearing children
468
+ if (nextProps.innerHTML != null) {
469
+ // innerHTML is set, update it if changed
470
+ if (currProps.innerHTML !== nextProps.innerHTML) {
471
+ curr._dom.innerHTML = nextProps.innerHTML as string
472
+ }
473
+ } else if (currProps.innerHTML != null) {
474
+ // innerHTML was removed, clear it before adding children
475
+ curr._dom.innerHTML = ''
476
+ }
477
+
478
+ diffChildren(
479
+ curr._children,
480
+ next._children,
481
+ curr._dom,
482
+ frame,
483
+ scheduler,
484
+ styles,
485
+ next,
486
+ rootTarget,
487
+ )
488
+ diffHostProps(currProps, nextProps, curr._dom)
489
+
490
+ next._dom = curr._dom
491
+ next._parent = vParent
492
+ next._controller = curr._controller
493
+ next._controlledState = curr._controlledState
494
+
495
+ if (next._controlledState || shouldTrackControlledReflection(nextProps)) {
496
+ ensureControlledReflection(next as CommittedHostNode, scheduler)
497
+ syncControlledReflection(next as CommittedHostNode, nextProps)
498
+ }
499
+
500
+ bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
501
+ if (shouldDispatchInlineMixinLifecycle(curr._dom)) {
502
+ scheduler.enqueueCommitPhase([
503
+ () => dispatchMixinCommit(next._mixState as MixinRuntimeState | undefined),
504
+ ])
505
+ }
506
+
507
+ return
508
+ }
509
+
510
+ function setupHostNode(node: HostNode, dom: Element, scheduler: Scheduler): void {
511
+ node._dom = dom
512
+ let props = getHostProps(node)
513
+ let committedNode = node as CommittedHostNode
514
+
515
+ if (shouldTrackControlledReflection(props)) {
516
+ ensureControlledReflection(committedNode, scheduler)
517
+ syncControlledReflection(committedNode, props)
518
+ }
519
+ }
520
+
521
+ function diffText(curr: CommittedTextNode, next: TextNode, vParent: VNode) {
522
+ if (curr._text !== next._text) {
523
+ curr._dom.textContent = next._text
524
+ }
525
+ next._dom = curr._dom
526
+ next._parent = vParent
527
+ }
528
+
529
+ function insert(
530
+ node: VNode,
531
+ domParent: ParentNode,
532
+ frame: FrameHandle,
533
+ scheduler: Scheduler,
534
+ styles: StyleManager,
535
+ vParent: VNode,
536
+ rootTarget: EventTarget,
537
+ anchor?: Node,
538
+ cursor?: Node | null,
539
+ ): Node | null | undefined {
540
+ node._parent = vParent // set parent for initial render context lookups
541
+ node._svg = getSvgContext(vParent, node.type)
542
+
543
+ // Stop hydration if cursor has reached the anchor (end boundary)
544
+ // Check BEFORE skipComments to prevent escaping range root markers
545
+ if (cursor && anchor && cursor === anchor) {
546
+ cursor = null
547
+ }
548
+
549
+ cursor =
550
+ node.type === Frame
551
+ ? skipCommentsExceptFrameStart(cursor ?? null)
552
+ : skipComments(cursor ?? null)
553
+
554
+ // Also check after skipComments in case we skipped past the anchor
555
+ if (cursor && anchor && cursor === anchor) {
556
+ cursor = null
557
+ }
558
+
559
+ let doInsert = anchor
560
+ ? (dom: Node) => domParent.insertBefore(dom, anchor)
561
+ : (dom: Node) => domParent.appendChild(dom)
562
+
563
+ if (isTextNode(node)) {
564
+ if (cursor instanceof Text) {
565
+ node._parent = vParent
566
+ // Handle text node consolidation: server renders adjacent text as single node
567
+ // e.g., <span>Hello {world}</span> → server: "Hello world", client: ["Hello ", "world"]
568
+ if (cursor.data !== node._text) {
569
+ if (cursor.data.startsWith(node._text) && node._text.length < cursor.data.length) {
570
+ // Consolidation case: split the text node at the boundary
571
+ // cursor becomes the first part (node._text), remainder is returned for next vnode
572
+ let remainder = cursor.splitText(node._text.length)
573
+ node._dom = cursor
574
+ return remainder
575
+ }
576
+ // Genuine mismatch - correct it
577
+ logHydrationMismatch('text mismatch', cursor.data, node._text)
578
+ cursor.data = node._text
579
+ }
580
+ node._dom = cursor
581
+ return cursor.nextSibling
582
+ }
583
+ let dom = document.createTextNode(node._text)
584
+ node._dom = dom
585
+ node._parent = vParent
586
+ doInsert(dom)
587
+ return cursor
588
+ }
589
+
590
+ if (isHostNode(node)) {
591
+ let hostProps = resolveNodeMixProps(node, frame, scheduler)
592
+
593
+ if (isHeadHostNode(node)) {
594
+ let targetHead = getDocumentHead(domParent)
595
+ if (targetHead) {
596
+ let childCursor = cursor
597
+ if (cursor instanceof Element && cursor.tagName.toLowerCase() === 'head') {
598
+ childCursor = cursor.firstChild
599
+ let nextCursor = cursor.nextSibling
600
+ if (cursor !== targetHead) {
601
+ while (cursor.firstChild) {
602
+ targetHead.appendChild(cursor.firstChild)
603
+ }
604
+ cursor.remove()
605
+ }
606
+ cursor = nextCursor
607
+ }
608
+
609
+ // Render explicit <head> children directly into document.head.
610
+ diffChildren(
611
+ null,
612
+ node._children,
613
+ targetHead,
614
+ frame,
615
+ scheduler,
616
+ styles,
617
+ node,
618
+ rootTarget,
619
+ childCursor,
620
+ )
621
+ diffHostProps({}, hostProps, targetHead)
622
+ setupHostNode(node, targetHead, scheduler)
623
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
624
+ return cursor
625
+ }
626
+ }
627
+
628
+ // Check for matching mixin-persisted node that can be reclaimed
629
+ let persistedNode = findMatchingPersistedMixinNode(node.type, node.key, domParent)
630
+ if (persistedNode) {
631
+ reclaimPersistedMixinNode(persistedNode, node, frame, scheduler, styles, vParent, rootTarget)
632
+ return cursor
633
+ }
634
+
635
+ if (cursor instanceof Element) {
636
+ // SVG elements have case-sensitive tag names (e.g. linearGradient, clipPath)
637
+ // HTML elements are case-insensitive, so we lowercase for comparison
638
+ let cursorTag = node._svg ? cursor.tagName : cursor.tagName.toLowerCase()
639
+ if (cursorTag === node.type) {
640
+ let nextCursor = cursor.nextSibling
641
+ diffHostProps({}, hostProps, cursor)
642
+
643
+ // Handle innerHTML prop
644
+ if (hostProps.innerHTML != null) {
645
+ cursor.innerHTML = hostProps.innerHTML as string
646
+ } else {
647
+ let childCursor = cursor.firstChild
648
+ // Ignore excess nodes - browser extensions may inject content
649
+ diffChildren(
650
+ null,
651
+ node._children,
652
+ cursor,
653
+ frame,
654
+ scheduler,
655
+ styles,
656
+ node,
657
+ rootTarget,
658
+ childCursor,
659
+ )
660
+ }
661
+
662
+ setupHostNode(node, cursor, scheduler)
663
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
664
+ return nextCursor
665
+ } else {
666
+ // Type mismatch - try single-advance retry to handle browser extension injections
667
+ // at the start of containers. Skip this node and try the next sibling once.
668
+ let nextSibling = skipComments(cursor.nextSibling)
669
+ if (nextSibling instanceof Element) {
670
+ let nextTag = node._svg ? nextSibling.tagName : nextSibling.tagName.toLowerCase()
671
+ if (nextTag === node.type) {
672
+ let nextCursor = nextSibling.nextSibling
673
+ // Found a match after skipping - adopt it and leave skipped node in place
674
+ diffHostProps({}, hostProps, nextSibling)
675
+
676
+ if (hostProps.innerHTML != null) {
677
+ nextSibling.innerHTML = hostProps.innerHTML as string
678
+ } else {
679
+ let childCursor = nextSibling.firstChild
680
+ diffChildren(
681
+ null,
682
+ node._children,
683
+ nextSibling,
684
+ frame,
685
+ scheduler,
686
+ styles,
687
+ node,
688
+ rootTarget,
689
+ childCursor,
690
+ )
691
+ }
692
+
693
+ setupHostNode(node, nextSibling, scheduler)
694
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
695
+ return nextCursor
696
+ }
697
+ }
698
+ // Retry failed - log mismatch and create new element (don't remove mismatched nodes)
699
+ logHydrationMismatch('tag', cursorTag, node.type)
700
+ cursor = undefined // stop hydration for this tree
701
+ }
702
+ }
703
+ let dom = node._svg
704
+ ? document.createElementNS(SVG_NS, node.type)
705
+ : document.createElement(node.type)
706
+ diffHostProps({}, hostProps, dom)
707
+
708
+ // Handle innerHTML prop
709
+ if (hostProps.innerHTML != null) {
710
+ dom.innerHTML = hostProps.innerHTML as string
711
+ } else {
712
+ diffChildren(null, node._children, dom, frame, scheduler, styles, node, rootTarget)
713
+ }
714
+
715
+ setupHostNode(node, dom, scheduler)
716
+ bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
717
+ doInsert(dom)
718
+ return cursor
719
+ }
720
+
721
+ if (isFragmentNode(node)) {
722
+ // Insert fragment children in order before the same anchor
723
+ for (let child of node._children) {
724
+ cursor = insert(
725
+ child,
726
+ domParent,
727
+ frame,
728
+ scheduler,
729
+ styles,
730
+ vParent,
731
+ rootTarget,
732
+ anchor,
733
+ cursor,
734
+ )
735
+ }
736
+ return cursor
737
+ }
738
+
739
+ if (isComponentNode(node)) {
740
+ return diffComponent(
741
+ null,
742
+ node,
743
+ frame,
744
+ scheduler,
745
+ styles,
746
+ domParent,
747
+ vParent,
748
+ rootTarget,
749
+ anchor,
750
+ cursor,
751
+ )
752
+ }
753
+
754
+ if (node.type === Frame) {
755
+ return insertFrame(
756
+ node,
757
+ domParent,
758
+ frame,
759
+ scheduler,
760
+ styles,
761
+ vParent,
762
+ rootTarget,
763
+ anchor,
764
+ cursor,
765
+ )
766
+ }
767
+
768
+ invariant(false, 'Unexpected node type')
769
+ }
770
+
771
+ function diffFrame(
772
+ curr: VNode,
773
+ next: VNode,
774
+ domParent: ParentNode,
775
+ frame: FrameHandle,
776
+ scheduler: Scheduler,
777
+ styles: StyleManager,
778
+ vParent: VNode,
779
+ rootTarget: EventTarget,
780
+ anchor?: Node,
781
+ ): void {
782
+ let currSrc = getFrameSrc(curr)
783
+ let nextSrc = getFrameSrc(next)
784
+ let currName = getFrameName(curr)
785
+ let nextName = getFrameName(next)
786
+
787
+ if (currName !== nextName) {
788
+ let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor
789
+ remove(curr, domParent, scheduler, styles)
790
+ insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)
791
+ return
792
+ }
793
+
794
+ // If the frame hasn't resolved yet, preserve existing cancel/remount behavior
795
+ // so pending streams from the old src cannot take over the new src.
796
+ if (currSrc !== nextSrc && !curr._frameResolved) {
797
+ let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor
798
+ remove(curr, domParent, scheduler, styles)
799
+ insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)
800
+ return
801
+ }
802
+
803
+ next._rangeStart = curr._rangeStart
804
+ next._rangeEnd = curr._rangeEnd
805
+ next._frameInstance = curr._frameInstance
806
+ next._frameFallbackRoot = curr._frameFallbackRoot
807
+ next._frameResolveToken = curr._frameResolveToken
808
+ next._frameResolved = curr._frameResolved
809
+ next._parent = vParent
810
+
811
+ if (currSrc !== nextSrc) {
812
+ let frameInstance = next._frameInstance as FrameInstance | undefined
813
+ if (frameInstance) {
814
+ frameInstance.handle.src = nextSrc
815
+ }
816
+
817
+ let runtime = getFrameRuntime(frame)
818
+ if (runtime) {
819
+ resolveClientFrame(next, runtime)
820
+ }
821
+ }
822
+
823
+ if (!next._frameResolved && next._frameFallbackRoot) {
824
+ next._frameFallbackRoot.render(next.props?.fallback ?? null)
825
+ }
826
+ }
827
+
828
+ function insertFrame(
829
+ node: VNode,
830
+ domParent: ParentNode,
831
+ frame: FrameHandle,
832
+ scheduler: Scheduler,
833
+ styles: StyleManager,
834
+ vParent: VNode,
835
+ rootTarget: EventTarget,
836
+ anchor?: Node,
837
+ cursor?: Node | null,
838
+ ): Node | null | undefined {
839
+ let runtime = getFrameRuntime(frame)
840
+ if (!runtime || (runtime as { canResolveFrames?: boolean }).canResolveFrames === false) {
841
+ throw new Error(
842
+ 'Cannot render <Frame /> without frame runtime. Use run() or pass frameInit to createRoot/createRangeRoot.',
843
+ )
844
+ }
845
+
846
+ // Hydration path: adopt server-rendered frame markers and reuse the existing
847
+ // frame instance created during createSubFrames().
848
+ if (isFrameStartComment(cursor)) {
849
+ let start = cursor
850
+ let end = findFrameEndComment(start)
851
+ if (end) {
852
+ node._rangeStart = start
853
+ node._rangeEnd = end
854
+ node._parent = vParent
855
+ node._frameResolveToken = 0
856
+ node._frameResolveController = undefined
857
+ node._frameFallbackRoot = undefined
858
+ node._frameResolved = true
859
+
860
+ let frameId = getFrameIdFromComment(start)
861
+ let marker = frameId ? runtime.data.f?.[frameId] : undefined
862
+ let src = marker?.src ?? getFrameSrc(node)
863
+ let instance = runtime.frameInstances.get(start)
864
+ if (!instance) {
865
+ instance = createFrame([start, end], {
866
+ name: getFrameName(node),
867
+ src,
868
+ marker: frameId && marker ? { ...marker, id: frameId } : undefined,
869
+ errorTarget: runtime.errorTarget,
870
+ loadModule: runtime.loadModule,
871
+ resolveFrame: runtime.resolveFrame,
872
+ pendingClientEntries: runtime.pendingClientEntries,
873
+ scheduler: runtime.scheduler,
874
+ data: runtime.data,
875
+ moduleCache: runtime.moduleCache,
876
+ moduleLoads: runtime.moduleLoads,
877
+ frameInstances: runtime.frameInstances,
878
+ namedFrames: runtime.namedFrames,
879
+ })
880
+ runtime.frameInstances.set(start, instance)
881
+ }
882
+
883
+ node._frameInstance = instance
884
+
885
+ return end.nextSibling
886
+ }
887
+ }
888
+
889
+ let start = document.createComment(` rmx:f:${randomFrameId()} `)
890
+ let end = document.createComment(' /rmx:f ')
891
+ let doInsert = anchor
892
+ ? (dom: Node) => domParent.insertBefore(dom, anchor)
893
+ : (dom: Node) => domParent.appendChild(dom)
894
+
895
+ doInsert(start)
896
+ doInsert(end)
897
+
898
+ node._rangeStart = start
899
+ node._rangeEnd = end
900
+ node._parent = vParent
901
+
902
+ let fallbackRoot = createRangeRoot([start, end], {
903
+ frame,
904
+ styleManager: styles,
905
+ })
906
+ fallbackRoot.render(node.props?.fallback ?? null)
907
+ node._frameFallbackRoot = fallbackRoot
908
+ node._frameResolved = false
909
+ node._frameResolveToken = 0
910
+
911
+ let instance = createFrame([start, end], {
912
+ name: getFrameName(node),
913
+ src: getFrameSrc(node),
914
+ errorTarget: runtime.errorTarget,
915
+ loadModule: runtime.loadModule,
916
+ resolveFrame: runtime.resolveFrame,
917
+ pendingClientEntries: runtime.pendingClientEntries,
918
+ scheduler: runtime.scheduler,
919
+ data: runtime.data,
920
+ moduleCache: runtime.moduleCache,
921
+ moduleLoads: runtime.moduleLoads,
922
+ frameInstances: runtime.frameInstances,
923
+ namedFrames: runtime.namedFrames,
924
+ })
925
+ node._frameInstance = instance
926
+ runtime.frameInstances.set(start, instance)
927
+
928
+ resolveClientFrame(node, runtime)
929
+
930
+ return cursor
931
+ }
932
+
933
+ function resolveClientFrame(node: VNode, runtime: FrameRuntime): void {
934
+ let frameSrc = getFrameSrc(node)
935
+ let instance = node._frameInstance as FrameInstance | undefined
936
+ if (!instance) return
937
+
938
+ let token = (node._frameResolveToken ?? 0) + 1
939
+ node._frameResolveToken = token
940
+ node._frameResolveController?.abort()
941
+ let resolveController = new AbortController()
942
+ node._frameResolveController = resolveController
943
+
944
+ Promise.resolve(runtime.resolveFrame(frameSrc, resolveController.signal))
945
+ .then(async (content) => {
946
+ if (node._frameResolveToken !== token || resolveController.signal.aborted) return
947
+ node._frameFallbackRoot?.dispose()
948
+ node._frameFallbackRoot = undefined
949
+ let nextContent = asAbortableFrameContent(content, resolveController.signal)
950
+ await instance.render(nextContent, { signal: resolveController.signal })
951
+ if (node._frameResolveToken !== token || resolveController.signal.aborted) return
952
+ node._frameResolved = true
953
+ })
954
+ .catch(() => {})
955
+ .finally(() => {
956
+ if (node._frameResolveController === resolveController) {
957
+ node._frameResolveController = undefined
958
+ }
959
+ })
960
+ }
961
+
962
+ function disposeFrameResources(node: VNode): void {
963
+ node._frameResolveToken = (node._frameResolveToken ?? 0) + 1
964
+ node._frameResolveController?.abort()
965
+ node._frameResolveController = undefined
966
+ node._frameFallbackRoot?.dispose()
967
+ node._frameFallbackRoot = undefined
968
+
969
+ let frameInstance = node._frameInstance as FrameInstance | undefined
970
+ if (frameInstance) {
971
+ frameInstance.dispose()
972
+ node._frameInstance = undefined
973
+ }
974
+ }
975
+
976
+ function asAbortableFrameContent(content: FrameContent, signal: AbortSignal): FrameContent {
977
+ if (!(content instanceof ReadableStream)) return content
978
+ return createAbortableReadableStream(content, signal)
979
+ }
980
+
981
+ function createAbortableReadableStream(
982
+ source: ReadableStream<Uint8Array>,
983
+ signal: AbortSignal,
984
+ ): ReadableStream<Uint8Array> {
985
+ let reader = source.getReader()
986
+ let aborted = false
987
+
988
+ let onAbort = () => {
989
+ aborted = true
990
+ void reader.cancel(signal.reason)
991
+ }
992
+
993
+ if (signal.aborted) onAbort()
994
+ else signal.addEventListener('abort', onAbort, { once: true })
995
+
996
+ return new ReadableStream<Uint8Array>({
997
+ async pull(controller) {
998
+ if (aborted) {
999
+ controller.close()
1000
+ return
1001
+ }
1002
+
1003
+ let removeAbortReadListener: undefined | (() => void)
1004
+ let abortRead = new Promise<ReadableStreamReadResult<Uint8Array>>((resolve) => {
1005
+ if (signal.aborted) {
1006
+ resolve({ done: true, value: undefined as unknown as Uint8Array })
1007
+ return
1008
+ }
1009
+ let onAbortRead = () => {
1010
+ resolve({ done: true, value: undefined as unknown as Uint8Array })
1011
+ }
1012
+ removeAbortReadListener = () => signal.removeEventListener('abort', onAbortRead)
1013
+ signal.addEventListener('abort', onAbortRead, { once: true })
1014
+ })
1015
+
1016
+ let { done, value } = await Promise.race([reader.read(), abortRead])
1017
+ removeAbortReadListener?.()
1018
+
1019
+ if (done) {
1020
+ controller.close()
1021
+ return
1022
+ }
1023
+ controller.enqueue(value)
1024
+ },
1025
+ cancel(reason) {
1026
+ signal.removeEventListener('abort', onAbort)
1027
+ return reader.cancel(reason)
1028
+ },
1029
+ })
1030
+ }
1031
+
1032
+ function removeFrameDomRange(node: VNode, domParent: ParentNode): void {
1033
+ let start = node._rangeStart
1034
+ let end = node._rangeEnd
1035
+ if (!(start instanceof Comment) || !(end instanceof Comment)) return
1036
+
1037
+ let cursor: Node | null = start
1038
+ while (cursor) {
1039
+ let nextSibling: Node | null = cursor.nextSibling
1040
+ if (cursor.parentNode === domParent) {
1041
+ domParent.removeChild(cursor)
1042
+ }
1043
+ if (cursor === end) break
1044
+ cursor = nextSibling
1045
+ }
1046
+
1047
+ node._rangeStart = undefined
1048
+ node._rangeEnd = undefined
1049
+ }
1050
+
1051
+ function getFrameRuntime(frame: FrameHandle): FrameRuntime | undefined {
1052
+ return frame.$runtime as FrameRuntime | undefined
1053
+ }
1054
+
1055
+ function getFrameSrc(node: VNode): string {
1056
+ let src = node.props?.src
1057
+ invariant(typeof src === 'string' && src.length > 0, '<Frame /> requires a src prop')
1058
+ return src
1059
+ }
1060
+
1061
+ function getFrameName(node: VNode): string | undefined {
1062
+ let name = node.props?.name
1063
+ return typeof name === 'string' && name.length > 0 ? name : undefined
1064
+ }
1065
+
1066
+ function randomFrameId(): string {
1067
+ return `f${crypto.randomUUID().slice(0, 8)}`
1068
+ }
1069
+
1070
+ function skipCommentsExceptFrameStart(cursor: Node | null): Node | null {
1071
+ while (cursor && cursor.nodeType === Node.COMMENT_NODE) {
1072
+ if (isFrameStartComment(cursor)) return cursor
1073
+ cursor = cursor.nextSibling
1074
+ }
1075
+ return cursor
1076
+ }
1077
+
1078
+ function isFrameStartComment(node: Node | null | undefined): node is Comment {
1079
+ return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
1080
+ }
1081
+
1082
+ function isFrameEndComment(node: Node | null | undefined): node is Comment {
1083
+ return node instanceof Comment && node.data.trim() === '/rmx:f'
1084
+ }
1085
+
1086
+ function getFrameIdFromComment(comment: Comment): string | undefined {
1087
+ let text = comment.data.trim()
1088
+ if (!text.startsWith('rmx:f:')) return undefined
1089
+ return text.slice('rmx:f:'.length)
1090
+ }
1091
+
1092
+ function findFrameEndComment(start: Comment): Comment | null {
1093
+ let depth = 1
1094
+ let node: Node | null = start.nextSibling
1095
+
1096
+ while (node) {
1097
+ if (isFrameStartComment(node)) depth++
1098
+ else if (isFrameEndComment(node)) {
1099
+ depth--
1100
+ if (depth === 0) return node
1101
+ }
1102
+ node = node.nextSibling
1103
+ }
1104
+
1105
+ return null
1106
+ }
1107
+
1108
+ export function renderComponent(
1109
+ handle: ComponentHandle,
1110
+ currContent: VNode | null,
1111
+ next: ComponentNode,
1112
+ domParent: ParentNode,
1113
+ frame: FrameHandle,
1114
+ scheduler: Scheduler,
1115
+ styles: StyleManager,
1116
+ rootTarget: EventTarget,
1117
+ vParent?: VNode,
1118
+ anchor?: Node,
1119
+ cursor?: Node | null,
1120
+ ): Node | null | undefined {
1121
+ let [element, tasks] = handle.render(next.props)
1122
+ let content = toVNode(element)
1123
+ let newCursor = diffVNodes(
1124
+ currContent,
1125
+ content,
1126
+ domParent,
1127
+ frame,
1128
+ scheduler,
1129
+ styles,
1130
+ next,
1131
+ rootTarget,
1132
+ anchor,
1133
+ cursor,
1134
+ )
1135
+ next._content = content
1136
+ next._handle = handle
1137
+ next._parent = vParent
1138
+
1139
+ let committed = next as CommittedComponentNode
1140
+ handle.setScheduleUpdate(() => {
1141
+ scheduler.enqueue(committed, domParent)
1142
+ })
1143
+
1144
+ scheduler.enqueueTasks(tasks)
1145
+
1146
+ return newCursor
1147
+ }
1148
+
1149
+ function diffComponent(
1150
+ curr: CommittedComponentNode | null,
1151
+ next: ComponentNode,
1152
+ frame: FrameHandle,
1153
+ scheduler: Scheduler,
1154
+ styles: StyleManager,
1155
+ domParent: ParentNode,
1156
+ vParent: VNode,
1157
+ rootTarget: EventTarget,
1158
+ anchor?: Node,
1159
+ cursor?: Node | null,
1160
+ ): Node | null | undefined {
1161
+ if (curr === null) {
1162
+ let componentId = vParent._pendingHydrationComponentId
1163
+ if (componentId) {
1164
+ vParent._pendingHydrationComponentId = undefined
1165
+ } else {
1166
+ componentId = `c${++idCounter}`
1167
+ }
1168
+ next._handle = createComponent({
1169
+ id: componentId,
1170
+ frame,
1171
+ type: next.type,
1172
+ getContext: (type: Component) => findContextFromAncestry(vParent, type),
1173
+ getFrameByName(name: string) {
1174
+ let runtime = getFrameRuntime(frame)
1175
+ return runtime?.namedFrames.get(name)
1176
+ },
1177
+ getTopFrame() {
1178
+ let runtime = getFrameRuntime(frame)
1179
+ return runtime?.topFrame
1180
+ },
1181
+ })
1182
+
1183
+ return renderComponent(
1184
+ next._handle,
1185
+ null,
1186
+ next,
1187
+ domParent,
1188
+ frame,
1189
+ scheduler,
1190
+ styles,
1191
+ rootTarget,
1192
+ vParent,
1193
+ anchor,
1194
+ cursor,
1195
+ )
1196
+ }
1197
+ next._handle = curr._handle
1198
+ let { _content, _handle } = curr
1199
+ return renderComponent(
1200
+ _handle,
1201
+ _content,
1202
+ next,
1203
+ domParent,
1204
+ frame,
1205
+ scheduler,
1206
+ styles,
1207
+ rootTarget,
1208
+ vParent,
1209
+ anchor,
1210
+ cursor,
1211
+ )
1212
+ }
1213
+
1214
+ // Cleanup without DOM removal - used for descendants when parent DOM node is removed
1215
+ function cleanupDescendants(node: VNode, scheduler: Scheduler, styles: StyleManager): void {
1216
+ if (isCommittedTextNode(node)) {
1217
+ return
1218
+ }
1219
+
1220
+ if (isCommittedHostNode(node)) {
1221
+ for (let child of node._children) {
1222
+ cleanupDescendants(child, scheduler, styles)
1223
+ }
1224
+
1225
+ teardownMixins(node._mixState as MixinRuntimeState | undefined)
1226
+ teardownControlledReflection(node)
1227
+ if (node._controller) node._controller.abort()
1228
+ return
1229
+ }
1230
+
1231
+ if (isFragmentNode(node)) {
1232
+ for (let child of node._children) {
1233
+ cleanupDescendants(child, scheduler, styles)
1234
+ }
1235
+ return
1236
+ }
1237
+
1238
+ if (isCommittedComponentNode(node)) {
1239
+ cleanupDescendants(node._content, scheduler, styles)
1240
+ let tasks = node._handle.remove()
1241
+ scheduler.enqueueTasks(tasks)
1242
+ return
1243
+ }
1244
+
1245
+ if (node.type === Frame) {
1246
+ disposeFrameResources(node)
1247
+ return
1248
+ }
1249
+ }
1250
+
1251
+ export function remove(
1252
+ node: VNode,
1253
+ domParent: ParentNode,
1254
+ scheduler: Scheduler,
1255
+ styles: StyleManager,
1256
+ ) {
1257
+ if (isCommittedTextNode(node)) {
1258
+ node._dom.parentNode?.removeChild(node._dom)
1259
+ return
1260
+ }
1261
+
1262
+ if (isCommittedHostNode(node)) {
1263
+ if (node._persistedByMixins) return
1264
+
1265
+ let persistedRemoval = prepareMixinRemoval(node._mixState as MixinRuntimeState | undefined)
1266
+ if (persistedRemoval) {
1267
+ let token = ++persistedRemovalToken
1268
+ markNodePersistedByMixins(node, domParent, token)
1269
+ void persistedRemoval
1270
+ .catch(() => {})
1271
+ .finally(() => {
1272
+ if (!node._persistedByMixins) return
1273
+ if (node._persistedRemovalToken !== token) return
1274
+ unmarkNodePersistedByMixins(node)
1275
+ performHostNodeRemoval(node, domParent, scheduler, styles)
1276
+ })
1277
+ return
1278
+ }
1279
+ performHostNodeRemoval(node, domParent, scheduler, styles)
1280
+ return
1281
+ }
1282
+
1283
+ if (isFragmentNode(node)) {
1284
+ for (let child of node._children) {
1285
+ remove(child, domParent, scheduler, styles)
1286
+ }
1287
+ return
1288
+ }
1289
+
1290
+ if (isCommittedComponentNode(node)) {
1291
+ remove(node._content, domParent, scheduler, styles)
1292
+ let tasks = node._handle.remove()
1293
+ scheduler.enqueueTasks(tasks)
1294
+ return
1295
+ }
1296
+
1297
+ if (node.type === Frame) {
1298
+ disposeFrameResources(node)
1299
+ removeFrameDomRange(node, domParent)
1300
+ return
1301
+ }
1302
+ }
1303
+
1304
+ // Actually remove a host node from DOM and clean up
1305
+ function performHostNodeRemoval(
1306
+ node: CommittedHostNode,
1307
+ domParent: ParentNode,
1308
+ scheduler: Scheduler,
1309
+ styles: StyleManager,
1310
+ ) {
1311
+ if (isHeadHostNode(node)) {
1312
+ for (let child of node._children) {
1313
+ remove(child, node._dom, scheduler, styles)
1314
+ }
1315
+ } else {
1316
+ // Clean up all descendants first (before removing DOM subtree)
1317
+ for (let child of node._children) {
1318
+ cleanupDescendants(child, scheduler, styles)
1319
+ }
1320
+ }
1321
+
1322
+ teardownMixins(node._mixState as MixinRuntimeState | undefined)
1323
+ teardownControlledReflection(node)
1324
+ // Never remove the real document.head node when reconciling a <head> vnode.
1325
+ if (!isHeadHostNode(node)) {
1326
+ node._dom.parentNode?.removeChild(node._dom)
1327
+ }
1328
+ if (node._controller) node._controller.abort()
1329
+ }
1330
+
1331
+ function diffChildren(
1332
+ curr: VNode[] | null,
1333
+ next: VNode[],
1334
+ domParent: ParentNode,
1335
+ frame: FrameHandle,
1336
+ scheduler: Scheduler,
1337
+ styles: StyleManager,
1338
+ vParent: VNode,
1339
+ rootTarget: EventTarget,
1340
+ cursor?: Node | null,
1341
+ anchor?: Node,
1342
+ ) {
1343
+ let nextLength = next.length
1344
+
1345
+ // Warn when duplicate keys are present among siblings. Duplicate keys are
1346
+ // still processed (last one wins), but they make keyed diffing ambiguous.
1347
+ let hasKeys = false
1348
+ let seenKeys: Set<string> | undefined
1349
+ let duplicateKeys: Set<string> | undefined
1350
+ for (let i = 0; i < nextLength; i++) {
1351
+ let node = next[i]
1352
+ if (node && node.key != null) {
1353
+ hasKeys = true
1354
+ if (!seenKeys) {
1355
+ seenKeys = new Set([node.key])
1356
+ continue
1357
+ }
1358
+ if (seenKeys.has(node.key)) {
1359
+ if (!duplicateKeys) {
1360
+ duplicateKeys = new Set()
1361
+ }
1362
+ duplicateKeys.add(node.key)
1363
+ } else {
1364
+ seenKeys.add(node.key)
1365
+ }
1366
+ }
1367
+ }
1368
+
1369
+ if (duplicateKeys?.size) {
1370
+ let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
1371
+ console.warn(
1372
+ `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
1373
+ )
1374
+ }
1375
+
1376
+ // Initial mount / hydration: delegate to insert() for each child so that
1377
+ // hydration cursors and creation logic remain centralized there.
1378
+ if (curr === null) {
1379
+ for (let node of next) {
1380
+ cursor = insert(
1381
+ node,
1382
+ domParent,
1383
+ frame,
1384
+ scheduler,
1385
+ styles,
1386
+ vParent,
1387
+ rootTarget,
1388
+ anchor,
1389
+ cursor,
1390
+ )
1391
+ }
1392
+ vParent._children = next
1393
+ return cursor
1394
+ }
1395
+
1396
+ let currLength = curr.length
1397
+
1398
+ // Detect if any keys are present in the new children. If not, we can fall
1399
+ // back to the simpler index-based diff which is cheaper and matches
1400
+ // pre-existing behavior.
1401
+ if (!hasKeys) {
1402
+ for (let i = 0; i < nextLength; i++) {
1403
+ let currentNode = i < currLength ? curr[i] : null
1404
+ diffVNodes(
1405
+ currentNode,
1406
+ next[i],
1407
+ domParent,
1408
+ frame,
1409
+ scheduler,
1410
+ styles,
1411
+ vParent,
1412
+ rootTarget,
1413
+ anchor,
1414
+ cursor,
1415
+ )
1416
+ }
1417
+
1418
+ if (currLength > nextLength) {
1419
+ for (let i = nextLength; i < currLength; i++) {
1420
+ let node = curr[i]
1421
+ if (node) remove(node, domParent, scheduler, styles)
1422
+ }
1423
+ }
1424
+
1425
+ vParent._children = next
1426
+ return
1427
+ }
1428
+
1429
+ // --- O(n + m) keyed diff with Map-based lookup ------------------------------
1430
+
1431
+ let oldChildren = curr
1432
+ let oldChildrenLength = currLength
1433
+ let remainingOldChildren = oldChildrenLength
1434
+
1435
+ // Build key → index map for O(1) lookup: O(m)
1436
+ let oldKeyMap = new Map<string, number>()
1437
+ for (let i = 0; i < oldChildrenLength; i++) {
1438
+ let c = oldChildren[i]
1439
+ if (c) {
1440
+ c._flags = 0
1441
+ if (c.key != null) {
1442
+ oldKeyMap.set(c.key, i)
1443
+ }
1444
+ }
1445
+ }
1446
+
1447
+ let skew = 0
1448
+ let newChildren: VNode[] = new Array(nextLength)
1449
+
1450
+ // First pass: match new children to old ones using Map lookup: O(n)
1451
+ for (let i = 0; i < nextLength; i++) {
1452
+ let childVNode = next[i]
1453
+ if (!childVNode) {
1454
+ newChildren[i] = childVNode
1455
+ continue
1456
+ }
1457
+
1458
+ newChildren[i] = childVNode
1459
+ childVNode._parent = vParent
1460
+
1461
+ let skewedIndex = i + skew
1462
+ let matchingIndex = -1
1463
+
1464
+ let key = childVNode.key
1465
+ let type = childVNode.type
1466
+
1467
+ if (key != null) {
1468
+ // O(1) Map lookup for keyed children
1469
+ let mapIndex = oldKeyMap.get(key)
1470
+ if (mapIndex !== undefined) {
1471
+ let candidate = oldChildren[mapIndex]
1472
+ let candidateFlags = candidate?._flags ?? 0
1473
+ if (candidate && (candidateFlags & MATCHED) === 0 && candidate.type === type) {
1474
+ matchingIndex = mapIndex
1475
+ }
1476
+ }
1477
+ } else {
1478
+ // Non-keyed children use positional identity only - no searching
1479
+ let searchVNode = oldChildren[skewedIndex]
1480
+ let searchFlags = searchVNode?._flags ?? 0
1481
+ let available = searchVNode != null && (searchFlags & MATCHED) === 0
1482
+ if (available && searchVNode.key == null && type === searchVNode.type) {
1483
+ matchingIndex = skewedIndex
1484
+ }
1485
+ }
1486
+
1487
+ childVNode._index = matchingIndex
1488
+
1489
+ let matchedOldVNode: VNode | null = null
1490
+ if (matchingIndex !== -1) {
1491
+ matchedOldVNode = oldChildren[matchingIndex]
1492
+ remainingOldChildren--
1493
+ if (matchedOldVNode) {
1494
+ matchedOldVNode._flags = (matchedOldVNode._flags ?? 0) | MATCHED
1495
+ }
1496
+ }
1497
+
1498
+ // Determine whether this is a mount vs move and mark INSERT_VNODE
1499
+ let oldDom = matchedOldVNode && findFirstDomAnchor(matchedOldVNode)
1500
+ let isMounting = !matchedOldVNode || !oldDom
1501
+ if (isMounting) {
1502
+ if (matchingIndex === -1) {
1503
+ // Adjust skew similar to Preact when lengths differ
1504
+ if (nextLength > oldChildrenLength) {
1505
+ skew--
1506
+ } else if (nextLength < oldChildrenLength) {
1507
+ skew++
1508
+ }
1509
+ }
1510
+
1511
+ childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
1512
+ } else if (matchingIndex !== i + skew) {
1513
+ if (matchingIndex === i + skew - 1) {
1514
+ skew--
1515
+ } else if (matchingIndex === i + skew + 1) {
1516
+ skew++
1517
+ } else {
1518
+ if (matchingIndex! > i + skew) skew--
1519
+ else skew++
1520
+ childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ // Unmount any old children that weren't matched
1526
+ if (remainingOldChildren) {
1527
+ for (let i = 0; i < oldChildrenLength; i++) {
1528
+ let oldVNode = oldChildren[i]
1529
+ if (oldVNode && ((oldVNode._flags ?? 0) & MATCHED) === 0) {
1530
+ remove(oldVNode, domParent, scheduler, styles)
1531
+ }
1532
+ }
1533
+ }
1534
+
1535
+ // Second pass: diff matched pairs and place/move DOM nodes in the correct
1536
+ // order, similar to Preact's diffChildren + insert.
1537
+ vParent._children = newChildren
1538
+
1539
+ let lastPlaced: Node | null = null
1540
+
1541
+ for (let i = 0; i < nextLength; i++) {
1542
+ let childVNode = newChildren[i]
1543
+ if (!childVNode) continue
1544
+
1545
+ let idx = childVNode._index ?? -1
1546
+ let oldVNode = idx >= 0 ? oldChildren[idx] : null
1547
+
1548
+ diffVNodes(
1549
+ oldVNode,
1550
+ childVNode,
1551
+ domParent,
1552
+ frame,
1553
+ scheduler,
1554
+ styles,
1555
+ vParent,
1556
+ rootTarget,
1557
+ anchor,
1558
+ cursor,
1559
+ )
1560
+
1561
+ let shouldPlace = (childVNode._flags ?? 0) & INSERT_VNODE
1562
+ let firstDom = findFirstDomAnchor(childVNode)
1563
+ let lastDom = firstDom ? findLastDomAnchor(childVNode) : null
1564
+ if (shouldPlace && firstDom && lastDom && firstDom.parentNode === domParent) {
1565
+ let target: Node | null
1566
+ if (lastPlaced === null) {
1567
+ if (vParent._rangeStart && vParent._rangeStart.parentNode === domParent) {
1568
+ target = vParent._rangeStart.nextSibling
1569
+ } else {
1570
+ target = domParent.firstChild
1571
+ }
1572
+ } else {
1573
+ target = lastPlaced.nextSibling
1574
+ }
1575
+
1576
+ if (target === null && anchor) target = anchor
1577
+
1578
+ // If target lies within the range we're moving, skip the move.
1579
+ if (target && domRangeContainsNode(firstDom, lastDom, target)) {
1580
+ // no-op
1581
+ } else if (firstDom !== target) {
1582
+ moveDomRange(domParent, firstDom, lastDom, target)
1583
+ }
1584
+ }
1585
+
1586
+ if (lastDom) lastPlaced = lastDom
1587
+
1588
+ // Clear internal flags for next diff
1589
+ childVNode._flags = 0
1590
+ childVNode._index = undefined
1591
+ }
1592
+
1593
+ return
1594
+ }
1595
+
1596
+ export function findFirstDomAnchor(node: VNode | null | undefined): Node | null {
1597
+ if (!node) return null
1598
+ if (isCommittedTextNode(node)) return node._dom
1599
+ if (isCommittedHostNode(node)) return node._dom
1600
+ if (isCommittedComponentNode(node)) return findFirstDomAnchor(node._content)
1601
+ if (node.type === Frame) return node._rangeStart ?? null
1602
+ if (isFragmentNode(node)) {
1603
+ for (let child of node._children) {
1604
+ let dom = findFirstDomAnchor(child)
1605
+ if (dom) return dom
1606
+ }
1607
+ }
1608
+ return null
1609
+ }
1610
+
1611
+ export function findLastDomAnchor(node: VNode | null | undefined): Node | null {
1612
+ if (!node) return null
1613
+ if (isCommittedTextNode(node)) return node._dom
1614
+ if (isCommittedHostNode(node)) return node._dom
1615
+ if (isCommittedComponentNode(node)) return findLastDomAnchor(node._content)
1616
+ if (node.type === Frame) return node._rangeEnd ?? null
1617
+ if (isFragmentNode(node)) {
1618
+ for (let i = node._children.length - 1; i >= 0; i--) {
1619
+ let dom = findLastDomAnchor(node._children[i])
1620
+ if (dom) return dom
1621
+ }
1622
+ }
1623
+ return null
1624
+ }
1625
+
1626
+ function domRangeContainsNode(first: Node, last: Node, node: Node): boolean {
1627
+ let current: Node | null = first
1628
+ while (current) {
1629
+ if (current === node) return true
1630
+ if (current === last) break
1631
+ current = current.nextSibling
1632
+ }
1633
+ return false
1634
+ }
1635
+
1636
+ function moveDomRange(domParent: ParentNode, first: Node, last: Node, before: Node | null): void {
1637
+ let current: Node | null = first
1638
+ while (current) {
1639
+ let next: Node | null = current === last ? null : current.nextSibling
1640
+ domParent.insertBefore(current, before)
1641
+ if (current === last) break
1642
+ current = next
1643
+ }
1644
+ }
1645
+
1646
+ export function setActiveSchedulerUpdateParents(parents: ParentNode[] | undefined) {
1647
+ activeSchedulerUpdateParents = parents
1648
+ }
1649
+
1650
+ function shouldDispatchInlineMixinLifecycle(node: Node): boolean {
1651
+ let parents = activeSchedulerUpdateParents
1652
+ if (!parents?.length) return true
1653
+ for (let parent of parents) {
1654
+ let parentNode = parent as Node
1655
+ if (parentNode === node) return false
1656
+ if (parentNode.contains(node)) return false
1657
+ }
1658
+ return true
1659
+ }
1660
+
1661
+ export function findNextSiblingDomAnchor(curr: VNode, vParent?: VNode): Node | null {
1662
+ if (!vParent || !Array.isArray(vParent._children)) return null
1663
+ let children = vParent._children
1664
+ let idx = children.indexOf(curr)
1665
+ if (idx === -1) return null
1666
+ for (let i = idx + 1; i < children.length; i++) {
1667
+ let dom = findFirstDomAnchor(children[i])
1668
+ if (dom) return dom
1669
+ }
1670
+ return null
1671
+ }
1672
+
1673
+ function reclaimPersistedMixinNode(
1674
+ persistedNode: CommittedHostNode,
1675
+ newNode: HostNode,
1676
+ frame: FrameHandle,
1677
+ scheduler: Scheduler,
1678
+ styles: StyleManager,
1679
+ vParent: VNode,
1680
+ rootTarget: EventTarget,
1681
+ ): void {
1682
+ cancelPendingMixinRemoval(persistedNode._mixState as MixinRuntimeState | undefined)
1683
+ unmarkNodePersistedByMixins(persistedNode)
1684
+
1685
+ newNode._dom = persistedNode._dom
1686
+ newNode._parent = vParent
1687
+ newNode._controller = persistedNode._controller
1688
+ newNode._mixState = persistedNode._mixState
1689
+ newNode._controlledState = persistedNode._controlledState
1690
+
1691
+ let prevProps = getHostProps(persistedNode)
1692
+ let nextProps = resolveNodeMixProps(
1693
+ newNode,
1694
+ frame,
1695
+ scheduler,
1696
+ newNode._mixState as MixinRuntimeState | undefined,
1697
+ )
1698
+ if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1699
+ dispatchMixinBeforeUpdate(newNode._mixState as MixinRuntimeState | undefined)
1700
+ }
1701
+ diffHostProps(prevProps, nextProps, persistedNode._dom)
1702
+ ensureControlledReflection(newNode as CommittedHostNode, scheduler)
1703
+ syncControlledReflection(newNode as CommittedHostNode, nextProps)
1704
+
1705
+ diffChildren(
1706
+ persistedNode._children,
1707
+ newNode._children,
1708
+ persistedNode._dom,
1709
+ frame,
1710
+ scheduler,
1711
+ styles,
1712
+ newNode,
1713
+ rootTarget,
1714
+ )
1715
+
1716
+ bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
1717
+ if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
1718
+ scheduler.enqueueCommitPhase([
1719
+ () => dispatchMixinCommit(newNode._mixState as MixinRuntimeState | undefined),
1720
+ ])
1721
+ }
1722
+ }