@remix-run/ui 0.0.0 → 0.1.1

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