@remix-run/ui 0.4.0 → 0.6.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 (195) hide show
  1. package/README.md +74 -1
  2. package/dist/accordion/index.js +2 -2
  3. package/dist/accordion/primitives.d.ts +1 -1
  4. package/dist/accordion/primitives.js.map +1 -1
  5. package/dist/anchor/index.js.map +1 -1
  6. package/dist/animation/animate-layout-mixin.js +1 -1
  7. package/dist/animation/animate-layout-mixin.js.map +1 -1
  8. package/dist/animation/animate-mixins.js +2 -2
  9. package/dist/animation/animate-mixins.js.map +1 -1
  10. package/dist/animation/demos/color-interpolation.js +1 -1
  11. package/dist/animation/demos/color-interpolation.js.map +1 -1
  12. package/dist/animation/demos/drag-release.d.ts +1 -1
  13. package/dist/animation/demos/hold-to-confirm.d.ts +3 -3
  14. package/dist/animation/demos/material-ripple.js +1 -3
  15. package/dist/animation/demos/material-ripple.js.map +1 -1
  16. package/dist/animation/index.js +4 -4
  17. package/dist/animation/spring.js.map +1 -1
  18. package/dist/breadcrumbs/index.js +2 -2
  19. package/dist/button/index.js +25 -25
  20. package/dist/button/index.js.map +1 -1
  21. package/dist/checkbox/index.js +11 -9
  22. package/dist/checkbox/index.js.map +1 -1
  23. package/dist/combobox/index.js +4 -4
  24. package/dist/combobox/index.js.map +1 -1
  25. package/dist/combobox/primitives.d.ts +1 -1
  26. package/dist/combobox/primitives.js +5 -5
  27. package/dist/combobox/primitives.js.map +1 -1
  28. package/dist/dev/refresh.d.ts +1 -0
  29. package/dist/dev/refresh.js +2 -0
  30. package/dist/dev/refresh.js.map +1 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +14 -14
  33. package/dist/input/index.js +9 -9
  34. package/dist/input/index.js.map +1 -1
  35. package/dist/jsx-runtime.js +2 -2
  36. package/dist/listbox/index.js +2 -2
  37. package/dist/menu/hover-aim.js.map +1 -1
  38. package/dist/menu/index.js +3 -3
  39. package/dist/menu/primitives.d.ts +1 -1
  40. package/dist/menu/primitives.js +6 -6
  41. package/dist/menu/primitives.js.map +1 -1
  42. package/dist/popover/index.js +3 -3
  43. package/dist/popover/scroll-lock.js.map +1 -1
  44. package/dist/radio/index.js +7 -7
  45. package/dist/radio/index.js.map +1 -1
  46. package/dist/runtime/client-entry-boundary.d.ts +14 -0
  47. package/dist/runtime/client-entry-boundary.js +29 -0
  48. package/dist/runtime/client-entry-boundary.js.map +1 -0
  49. package/dist/runtime/component.d.ts +19 -4
  50. package/dist/runtime/component.js +39 -9
  51. package/dist/runtime/component.js.map +1 -1
  52. package/dist/runtime/core/attributes.js +36 -3
  53. package/dist/runtime/core/attributes.js.map +1 -1
  54. package/dist/runtime/core/children.d.ts +2 -0
  55. package/dist/runtime/core/children.js +26 -2
  56. package/dist/runtime/core/children.js.map +1 -1
  57. package/dist/runtime/core/props.js +2 -2
  58. package/dist/runtime/core/vnode.d.ts +3 -3
  59. package/dist/runtime/core/vnode.js +22 -1
  60. package/dist/runtime/core/vnode.js.map +1 -1
  61. package/dist/runtime/create-element.d.ts +2 -2
  62. package/dist/runtime/create-element.js +3 -2
  63. package/dist/runtime/create-element.js.map +1 -1
  64. package/dist/runtime/diff-dom.js +307 -220
  65. package/dist/runtime/diff-dom.js.map +1 -1
  66. package/dist/runtime/diff-props.js +3 -3
  67. package/dist/runtime/dom.d.ts +12 -0
  68. package/dist/runtime/element-function.d.ts +3 -0
  69. package/dist/runtime/element-function.js +2 -0
  70. package/dist/runtime/element-function.js.map +1 -0
  71. package/dist/runtime/form-navigation.d.ts +25 -0
  72. package/dist/runtime/form-navigation.js +132 -0
  73. package/dist/runtime/form-navigation.js.map +1 -0
  74. package/dist/runtime/frame-resolution.d.ts +5 -0
  75. package/dist/runtime/frame-resolution.js +10 -0
  76. package/dist/runtime/frame-resolution.js.map +1 -0
  77. package/dist/runtime/frame.d.ts +59 -15
  78. package/dist/runtime/frame.js +383 -114
  79. package/dist/runtime/frame.js.map +1 -1
  80. package/dist/runtime/jsx.d.ts +1 -1
  81. package/dist/runtime/jsx.js +1 -1
  82. package/dist/runtime/jsx.js.map +1 -1
  83. package/dist/runtime/key.d.ts +1 -0
  84. package/dist/runtime/key.js +2 -0
  85. package/dist/runtime/key.js.map +1 -0
  86. package/dist/runtime/mixins/attrs-mixin.js +2 -2
  87. package/dist/runtime/mixins/link-mixin.js +4 -3
  88. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  89. package/dist/runtime/mixins/mixin.d.ts +15 -10
  90. package/dist/runtime/mixins/mixin.js +4 -4
  91. package/dist/runtime/mixins/mixin.js.map +1 -1
  92. package/dist/runtime/mixins/on-mixin.js +1 -1
  93. package/dist/runtime/mixins/ref-mixin.js +1 -1
  94. package/dist/runtime/module-preloader.d.ts +8 -0
  95. package/dist/runtime/module-preloader.js +95 -0
  96. package/dist/runtime/module-preloader.js.map +1 -0
  97. package/dist/runtime/navigation.d.ts +4 -1
  98. package/dist/runtime/navigation.js +157 -32
  99. package/dist/runtime/navigation.js.map +1 -1
  100. package/dist/runtime/reconcile.d.ts +11 -10
  101. package/dist/runtime/reconcile.js +549 -421
  102. package/dist/runtime/reconcile.js.map +1 -1
  103. package/dist/runtime/refresh.d.ts +28 -0
  104. package/dist/runtime/refresh.js +31 -0
  105. package/dist/runtime/refresh.js.map +1 -0
  106. package/dist/runtime/render.js +1 -1
  107. package/dist/runtime/render.js.map +1 -1
  108. package/dist/runtime/run.d.ts +3 -1
  109. package/dist/runtime/run.js +15 -7
  110. package/dist/runtime/run.js.map +1 -1
  111. package/dist/runtime/scheduler.js +8 -16
  112. package/dist/runtime/scheduler.js.map +1 -1
  113. package/dist/runtime/to-vnode.d.ts +2 -2
  114. package/dist/runtime/to-vnode.js +68 -20
  115. package/dist/runtime/to-vnode.js.map +1 -1
  116. package/dist/runtime/vdom.d.ts +4 -2
  117. package/dist/runtime/vdom.js +87 -42
  118. package/dist/runtime/vdom.js.map +1 -1
  119. package/dist/runtime/vnode.d.ts +138 -67
  120. package/dist/runtime/vnode.js +17 -12
  121. package/dist/runtime/vnode.js.map +1 -1
  122. package/dist/select/index.js +11 -11
  123. package/dist/select/index.js.map +1 -1
  124. package/dist/select/primitives.d.ts +1 -1
  125. package/dist/select/primitives.js +6 -6
  126. package/dist/server/stream.d.ts +2 -0
  127. package/dist/server/stream.js +74 -29
  128. package/dist/server/stream.js.map +1 -1
  129. package/dist/shared/focus-styles.d.ts +1 -1
  130. package/dist/shared/focus-styles.js +1 -1
  131. package/dist/shared/focus-styles.js.map +1 -1
  132. package/dist/shared/listbox-popover-styles.js +1 -1
  133. package/dist/shared/style-values.js +26 -26
  134. package/dist/shared/style-values.js.map +1 -1
  135. package/dist/style/css-mixin.js +3 -3
  136. package/dist/style/index.js +2 -2
  137. package/dist/style/style.js +25 -6
  138. package/dist/style/style.js.map +1 -1
  139. package/dist/style/stylesheet.d.ts +0 -1
  140. package/dist/style/stylesheet.js +13 -54
  141. package/dist/style/stylesheet.js.map +1 -1
  142. package/dist/tabs/index.js +11 -11
  143. package/dist/tabs/index.js.map +1 -1
  144. package/dist/tabs/primitives.d.ts +1 -1
  145. package/dist/test.js +1 -1
  146. package/dist/toggle/index.js +9 -9
  147. package/dist/toggle/index.js.map +1 -1
  148. package/dist/toggle/primitives.d.ts +1 -1
  149. package/package.json +10 -7
  150. package/src/animation/animate-mixins.ts +3 -3
  151. package/src/animation/demos/color-interpolation.tsx +1 -1
  152. package/src/button/index.ts +24 -24
  153. package/src/checkbox/index.ts +10 -8
  154. package/src/combobox/index.tsx +1 -1
  155. package/src/dev/refresh.ts +1 -0
  156. package/src/index.ts +1 -1
  157. package/src/input/index.ts +8 -8
  158. package/src/radio/index.ts +6 -5
  159. package/src/runtime/client-entry-boundary.ts +54 -0
  160. package/src/runtime/component.ts +65 -16
  161. package/src/runtime/core/attributes.ts +35 -1
  162. package/src/runtime/core/children.ts +24 -2
  163. package/src/runtime/core/vnode.ts +25 -4
  164. package/src/runtime/create-element.ts +6 -5
  165. package/src/runtime/demos/readme.demo.tsx +4 -4
  166. package/src/runtime/diff-dom.ts +370 -244
  167. package/src/runtime/dom.ts +331 -214
  168. package/src/runtime/element-function.ts +1 -0
  169. package/src/runtime/event-listeners.ts +1 -1
  170. package/src/runtime/form-navigation.ts +184 -0
  171. package/src/runtime/frame-resolution.ts +13 -0
  172. package/src/runtime/frame.ts +500 -124
  173. package/src/runtime/jsx.ts +3 -5
  174. package/src/runtime/key.ts +1 -0
  175. package/src/runtime/mixins/link-mixin.ts +1 -0
  176. package/src/runtime/mixins/mixin.ts +21 -18
  177. package/src/runtime/module-preloader.ts +115 -0
  178. package/src/runtime/navigation.ts +204 -30
  179. package/src/runtime/reconcile.ts +762 -759
  180. package/src/runtime/refresh.ts +46 -0
  181. package/src/runtime/run.ts +12 -2
  182. package/src/runtime/scheduler.ts +9 -29
  183. package/src/runtime/to-vnode.ts +85 -23
  184. package/src/runtime/vdom.ts +89 -70
  185. package/src/runtime/vnode.ts +192 -88
  186. package/src/select/index.tsx +9 -9
  187. package/src/server/README.md +13 -0
  188. package/src/server/stream.ts +93 -30
  189. package/src/shared/focus-styles.ts +1 -1
  190. package/src/shared/style-values.ts +26 -26
  191. package/src/style/style.ts +25 -6
  192. package/src/style/stylesheet.ts +32 -57
  193. package/src/tabs/index.tsx +12 -11
  194. package/src/test/README.md +151 -60
  195. package/src/toggle/index.tsx +9 -7
@@ -1,11 +1,8 @@
1
1
  import { invariant } from './invariant.ts'
2
2
  import type { FrameContext } from './frame.ts'
3
+ import { disposeClientEntryBoundary, getClientEntryBoundaryOwner } from './client-entry-boundary.ts'
3
4
 
4
- type HydratedVirtualRootStartMarker = Comment & {
5
- $rmx: {
6
- dispose(): void
7
- }
8
- }
5
+ type MarkerKind = 'frame-start' | 'frame-end' | 'virtual-root-start' | 'virtual-root-end'
9
6
 
10
7
  type CommentMarkerRangeReplacement = {
11
8
  currentStart: Comment
@@ -14,6 +11,25 @@ type CommentMarkerRangeReplacement = {
14
11
  nextEndIndex: number
15
12
  }
16
13
 
14
+ type NodeSiblingUnit = {
15
+ kind: 'node'
16
+ node: Node
17
+ startIndex: number
18
+ endIndex: number
19
+ }
20
+
21
+ type BoundarySiblingUnit = {
22
+ kind: 'frame' | 'hydration'
23
+ start: Comment
24
+ end: Comment
25
+ startIndex: number
26
+ endIndex: number
27
+ }
28
+
29
+ type SiblingUnit = NodeSiblingUnit | BoundarySiblingUnit
30
+
31
+ const REMIX_PRESERVE_DOM_ATTRIBUTE = 'rmx-preserve-dom'
32
+
17
33
  export function diffNodes(curr: Node[], next: Node[], context: FrameContext) {
18
34
  let parent = curr[0]?.parentNode ?? context.regionParent ?? null
19
35
  invariant(parent, 'Parent node not found')
@@ -24,70 +40,7 @@ export function diffNodes(curr: Node[], next: Node[], context: FrameContext) {
24
40
  context.regionTailRef ??
25
41
  (curr.length > 0 ? (curr[curr.length - 1].nextSibling as ChildNode | null) : null)
26
42
 
27
- let currentIndex = 0
28
- let nextIndex = 0
29
- while (currentIndex < curr.length || nextIndex < next.length) {
30
- let c = curr[currentIndex]
31
- let n = next[nextIndex]
32
-
33
- if (!c && n) {
34
- if (regionTailRef) {
35
- parent.insertBefore(n, regionTailRef)
36
- } else {
37
- parent.appendChild(n)
38
- }
39
- nextIndex++
40
- } else if (c && !n) {
41
- removeNode(c, parent, context)
42
- currentIndex++
43
- } else if (c && n) {
44
- let replacement = getCommentMarkerRangeReplacement(
45
- c,
46
- n,
47
- curr,
48
- next,
49
- currentIndex,
50
- nextIndex,
51
- context,
52
- )
53
- if (replacement) {
54
- replaceCommentMarkerRange(replacement, parent, context)
55
- currentIndex = replacement.currentEndIndex + 1
56
- nextIndex = replacement.nextEndIndex + 1
57
- continue
58
- }
59
-
60
- // Skip hydrated client-entry marker ranges; hydration pass re-renders
61
- // roots with new props from incoming payload
62
- if (isVirtualRootStartMarker(c) && isVirtualRootStartMarker(n)) {
63
- let currentEnd = findHydrationEndMarker(c)
64
- let nextEnd = findHydrationEndMarker(n)
65
- let nextData = n.data
66
- if (c.data !== nextData) c.data = nextData
67
-
68
- let currentEndIndex = curr.indexOf(currentEnd)
69
- let nextEndIndex = next.indexOf(nextEnd)
70
- currentIndex = currentEndIndex + 1
71
- nextIndex = nextEndIndex + 1
72
- continue
73
- }
74
-
75
- let cursor = diffNode(c, n, context)
76
- if (cursor) {
77
- let nextEndIndex = next.indexOf(cursor)
78
- let currentEndIndex =
79
- isFrameStartMarker(c) && isFrameEndMarker(cursor)
80
- ? findFrameEndIndex(curr, currentIndex)
81
- : currentIndex
82
- currentIndex = currentEndIndex + 1
83
- nextIndex = nextEndIndex + 1
84
- continue
85
- }
86
-
87
- currentIndex++
88
- nextIndex++
89
- }
90
- }
43
+ diffSiblingUnits(curr, next, parent, regionTailRef, context)
91
44
  }
92
45
 
93
46
  function diffNode(current: Node, next: Node, context: FrameContext): ChildNode | undefined {
@@ -111,45 +64,46 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
111
64
  }
112
65
 
113
66
  // Comment -> Comment
114
- if (isCommentNode(current) && isCommentNode(next)) {
67
+ if (isCommentNode(current) && isCommentNode(next) && markerKindsMatch(current, next)) {
115
68
  let newData = next.data
116
- if (current.data !== newData) {
117
- let updated = false
118
- if (isFrameStartMarker(current)) {
119
- if (shouldPreserveFrameStartMarker(current, next, context)) {
69
+ let updated = false
70
+ if (isFrameStartMarker(current)) {
71
+ if (shouldPreserveFrameStartMarker(current, next, context)) {
72
+ if (current.data !== newData) {
120
73
  current.data = newData
121
- updated = true
122
-
123
- let frame = context.frameInstances.get(current)
124
- let nextMarkerData = getFrameMarkerData(next, context)
125
- if (frame && nextMarkerData) {
126
- if (nextMarkerData.status === 'resolved') {
127
- let nextEnd = findFrameEndMarker(next)
128
- let nextContent = collectFrameContentFragment(current.ownerDocument, next, nextEnd)
129
- void frame.renderMarkerContent(
130
- { ...nextMarkerData, id: getFrameId(next) },
131
- nextContent,
132
- {
133
- signal: context.signal,
134
- },
135
- )
136
- return nextEnd
137
- }
138
-
139
- if (frame.isDisplayingResolvedContent()) {
140
- return findFrameEndMarker(next)
141
- }
74
+ }
75
+ updated = true
76
+
77
+ let frame = context.frameInstances.get(current)
78
+ let nextMarkerData = getFrameMarkerData(next, context)
79
+ if (frame && nextMarkerData) {
80
+ if (nextMarkerData.status === 'resolved') {
81
+ let nextEnd = findFrameEndMarker(next)
82
+ let nextContent = collectFrameContentFragment(current.ownerDocument, next, nextEnd)
83
+ void frame.renderMarkerContent(
84
+ { ...nextMarkerData, id: getFrameId(next) },
85
+ nextContent,
86
+ {
87
+ data: context.data,
88
+ signal: context.signal,
89
+ },
90
+ )
91
+ return nextEnd
92
+ }
93
+
94
+ if (frame.isDisplayingResolvedContent()) {
95
+ return findFrameEndMarker(next)
142
96
  }
143
- } else {
144
- disposeFrameStartMarker(current, context)
145
- current.data = newData
146
- updated = true
147
97
  }
148
- }
149
- if (!updated) {
98
+ } else if (current.data !== newData) {
99
+ disposeFrameStartMarker(current, context)
150
100
  current.data = newData
101
+ updated = true
151
102
  }
152
103
  }
104
+ if (!updated && current.data !== newData) {
105
+ current.data = newData
106
+ }
153
107
  return
154
108
  }
155
109
 
@@ -166,6 +120,7 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
166
120
  }
167
121
 
168
122
  // Same tag: update attributes then children
123
+ if (shouldPreserveDomElement(current, next)) return
169
124
  diffElementAttributes(current, next)
170
125
  if (shouldPreserveElementChildren(current, next)) return
171
126
  diffElementChildren(current, next, context)
@@ -205,6 +160,14 @@ function diffElementAttributes(current: Element, next: Element): void {
205
160
  }
206
161
  }
207
162
 
163
+ function shouldPreserveDomElement(current: Element, next: Element): boolean {
164
+ if (!next.hasAttribute(REMIX_PRESERVE_DOM_ATTRIBUTE)) return false
165
+ if (!current.hasAttribute(REMIX_PRESERVE_DOM_ATTRIBUTE)) {
166
+ current.setAttribute(REMIX_PRESERVE_DOM_ATTRIBUTE, '')
167
+ }
168
+ return true
169
+ }
170
+
208
171
  function shouldPreserveLiveAttribute(current: Element, next: Element, name: string): boolean {
209
172
  if (name === 'open') {
210
173
  if (current instanceof HTMLDetailsElement && next instanceof HTMLDetailsElement) {
@@ -274,49 +237,70 @@ function isPopoverOpen(element: Element): boolean {
274
237
  }
275
238
 
276
239
  function diffElementChildren(current: Element, next: Element, context: FrameContext): void {
277
- let currentChildren = Array.from(current.childNodes)
240
+ let currentChildren: Node[]
241
+
242
+ // Allow actively managed preload link tags in the head to stay in the document
243
+ // during diffing rather than removing them which aborts the preload in Safari
244
+ if (context.isActiveModulePreload && current === current.ownerDocument.head) {
245
+ currentChildren = []
246
+ for (let node of current.childNodes) {
247
+ if (!context.isActiveModulePreload(node)) currentChildren.push(node)
248
+ }
249
+ } else {
250
+ currentChildren = Array.from(current.childNodes)
251
+ }
278
252
  let nextChildren = Array.from(next.childNodes)
253
+ diffSiblingUnits(currentChildren, nextChildren, current, null, context)
254
+ }
279
255
 
280
- // Keyed map by data-key for current children
256
+ function diffSiblingUnits(
257
+ currentNodes: Node[],
258
+ nextNodes: Node[],
259
+ parent: ParentNode,
260
+ regionTailRef: ChildNode | null,
261
+ context: FrameContext,
262
+ ): void {
263
+ let currentUnits = parseSiblingUnits(currentNodes)
264
+ let nextUnits = parseSiblingUnits(nextNodes)
281
265
  let keyToIndex = new Map<string, number>()
282
- for (let i = 0; i < currentChildren.length; i++) {
283
- let node = currentChildren[i]
284
- if (isElement(node)) {
285
- let key = node.getAttribute('data-key')
286
- if (key != null) keyToIndex.set(key, i)
287
- }
266
+
267
+ for (let i = 0; i < currentUnits.length; i++) {
268
+ let key = getSiblingUnitKey(currentUnits[i])
269
+ if (key !== undefined) keyToIndex.set(key, i)
288
270
  }
289
271
 
290
- let used = new Array<boolean>(currentChildren.length).fill(false)
291
- let matchIndexForNext = new Array<number>(nextChildren.length).fill(-1)
272
+ let used = new Array<boolean>(currentUnits.length).fill(false)
273
+ let matchIndexForNext = new Array<number>(nextUnits.length).fill(-1)
292
274
 
293
- for (let i = 0; i < nextChildren.length; i++) {
294
- let nextChild = nextChildren[i]
275
+ // Reserve globally matched keyed elements and semantic boundaries before
276
+ // positionally pairing the remaining ordinary siblings.
277
+ for (let i = 0; i < nextUnits.length; i++) {
278
+ let nextUnit = nextUnits[i]
295
279
  let matchIndex = -1
280
+ let key = getSiblingUnitKey(nextUnit)
296
281
 
297
- if (isFrameEndMarker(nextChild)) {
298
- for (let j = 0; j < currentChildren.length; j++) {
299
- if (!used[j] && isFrameEndMarker(currentChildren[j])) {
300
- matchIndex = j
301
- break
302
- }
303
- }
304
- } else if (isElement(nextChild)) {
305
- let key = nextChild.getAttribute('data-key')
306
- if (key != null && keyToIndex.has(key)) {
307
- let idx = keyToIndex.get(key)!
308
- if (!used[idx]) matchIndex = idx
282
+ if (key !== undefined) {
283
+ let keyedIndex = keyToIndex.get(key)
284
+ if (
285
+ keyedIndex !== undefined &&
286
+ !used[keyedIndex] &&
287
+ siblingUnitsComparable(currentUnits[keyedIndex], nextUnit)
288
+ ) {
289
+ matchIndex = keyedIndex
309
290
  }
310
291
  }
311
292
 
312
- if (matchIndex === -1) {
313
- let candidateIndex = i
314
- if (
315
- candidateIndex < currentChildren.length &&
316
- !used[candidateIndex] &&
317
- nodeTypesComparable(currentChildren[candidateIndex], nextChild)
318
- ) {
319
- matchIndex = candidateIndex
293
+ // Boundary identities behave like implicit keys. Repeated boundaries with
294
+ // the same identity are paired in source order because server markers do
295
+ // not carry a separate application key.
296
+ if (matchIndex === -1 && nextUnit.kind !== 'node') {
297
+ for (let j = 0; j < currentUnits.length; j++) {
298
+ let currentUnit = currentUnits[j]
299
+ if (used[j] || currentUnit.kind !== nextUnit.kind) continue
300
+ if (shouldPreserveBoundaryUnit(currentUnit, nextUnit, context)) {
301
+ matchIndex = j
302
+ break
303
+ }
320
304
  }
321
305
  }
322
306
 
@@ -324,136 +308,229 @@ function diffElementChildren(current: Element, next: Element, context: FrameCont
324
308
  matchIndexForNext[i] = matchIndex
325
309
  }
326
310
 
327
- // Forward pass: update matched, collect committed
328
- let committed: Array<Node | undefined> = new Array(nextChildren.length)
329
- for (let i = 0; i < nextChildren.length; i++) {
330
- let mi = matchIndexForNext[i]
331
- if (mi !== -1) {
332
- let curChild = currentChildren[mi]
333
- let nextChild = nextChildren[i]
334
-
335
- let replacement = getCommentMarkerRangeReplacement(
336
- curChild,
337
- nextChild,
338
- currentChildren,
339
- nextChildren,
340
- mi,
341
- i,
342
- context,
343
- )
344
- if (replacement) {
345
- replaceCommentMarkerRange(replacement, current, context)
311
+ let remainingCurrentIndexes: number[] = []
312
+ for (let i = 0; i < currentUnits.length; i++) {
313
+ if (!used[i]) remainingCurrentIndexes.push(i)
314
+ }
346
315
 
347
- for (let k = mi; k <= replacement.currentEndIndex; k++) used[k] = true
316
+ let remainingNextIndexes: number[] = []
317
+ for (let i = 0; i < nextUnits.length; i++) {
318
+ if (matchIndexForNext[i] === -1) remainingNextIndexes.push(i)
319
+ }
348
320
 
349
- committed[i] = replacement.nextStart
350
- committed[replacement.nextEndIndex] = nextChildren[replacement.nextEndIndex]
351
- for (let j = i + 1; j < replacement.nextEndIndex; j++) committed[j] = undefined
321
+ let remainingLength = Math.min(remainingCurrentIndexes.length, remainingNextIndexes.length)
322
+ for (let i = 0; i < remainingLength; i++) {
323
+ let currentIndex = remainingCurrentIndexes[i]
324
+ let nextIndex = remainingNextIndexes[i]
325
+ let currentUnit = currentUnits[currentIndex]
326
+ let nextUnit = nextUnits[nextIndex]
352
327
 
353
- i = replacement.nextEndIndex
354
- continue
355
- }
328
+ if (
329
+ currentUnit.kind !== 'node' ||
330
+ nextUnit.kind !== 'node' ||
331
+ getSiblingUnitKey(currentUnit) !== undefined ||
332
+ getSiblingUnitKey(nextUnit) !== undefined ||
333
+ !siblingUnitsComparable(currentUnit, nextUnit)
334
+ ) {
335
+ continue
336
+ }
356
337
 
357
- let cursor = diffNode(curChild, nextChild, context)
358
- if (cursor) {
359
- // `diffNode` can preserve hydration and frame marker ranges by returning
360
- // the next end marker, so skip the owned interior nodes here.
361
- let nextEndIdx = nextChildren.indexOf(cursor)
362
- let currEndIdx =
363
- isFrameStartMarker(curChild) && isFrameEndMarker(cursor)
364
- ? findFrameEndIndex(currentChildren, mi)
365
- : findHydrationEndIndex(currentChildren, mi)
366
-
367
- // Adjacent marker ranges can pre-match into the next range and leave an
368
- // orphaned end marker behind. Clear those stale matches first.
369
- for (let j = i + 1; j <= nextEndIdx; j++) {
370
- let matchedIndex = matchIndexForNext[j]
371
- if (matchedIndex > currEndIdx) {
372
- used[matchedIndex] = false
373
- }
374
- matchIndexForNext[j] = -1
375
- }
338
+ used[currentIndex] = true
339
+ matchIndexForNext[nextIndex] = currentIndex
340
+ }
376
341
 
377
- // Mark the entire current region as used to avoid removals.
378
- for (let k = mi; k <= currEndIdx; k++) used[k] = true
342
+ let committed = new Array<SiblingUnit>(nextUnits.length)
343
+ for (let i = 0; i < nextUnits.length; i++) {
344
+ let matchIndex = matchIndexForNext[i]
345
+ let nextUnit = nextUnits[i]
346
+ if (matchIndex === -1) {
347
+ committed[i] = nextUnit
348
+ continue
349
+ }
379
350
 
380
- // Preserve both comment markers in committed; skip interior in reorder pass.
381
- committed[i] = curChild // start marker
382
- committed[nextEndIdx] = currentChildren[currEndIdx] // end marker
383
- for (let j = i + 1; j < nextEndIdx; j++) committed[j] = undefined
351
+ let currentUnit = currentUnits[matchIndex]
352
+ if (currentUnit.kind === 'node' && nextUnit.kind === 'node') {
353
+ diffNode(currentUnit.node, nextUnit.node, context)
354
+ committed[i] = currentUnit
355
+ continue
356
+ }
384
357
 
385
- // Jump to end of region.
386
- i = nextEndIdx
387
- continue
388
- }
389
- committed[i] = curChild
390
- } else {
391
- committed[i] = nextChildren[i]
358
+ invariant(currentUnit.kind !== 'node' && nextUnit.kind !== 'node', 'Expected boundaries')
359
+
360
+ let replacement = getCommentMarkerRangeReplacement(
361
+ currentUnit.start,
362
+ nextUnit.start,
363
+ currentNodes,
364
+ nextNodes,
365
+ currentUnit.startIndex,
366
+ nextUnit.startIndex,
367
+ context,
368
+ )
369
+ if (replacement) {
370
+ replaceCommentMarkerRange(replacement, parent, context)
371
+ committed[i] = nextUnit
372
+ continue
392
373
  }
374
+
375
+ let cursor = diffNode(currentUnit.start, nextUnit.start, context)
376
+ if (!cursor && currentUnit.kind === 'frame' && nextUnit.kind === 'frame') {
377
+ diffNodes(
378
+ collectNodesBetween(currentUnit.start, currentUnit.end),
379
+ nextNodes.slice(nextUnit.startIndex + 1, nextUnit.endIndex),
380
+ {
381
+ ...context,
382
+ regionParent: parent,
383
+ regionTailRef: currentUnit.end,
384
+ },
385
+ )
386
+ }
387
+ committed[i] = currentUnit
393
388
  }
394
389
 
395
- // Backward pass: reorder via inserts while avoiding redundant moves
396
- let anchor: Node | undefined = undefined
390
+ let anchor: Node | null = regionTailRef
397
391
  for (let i = committed.length - 1; i >= 0; i--) {
398
- let node = committed[i]
399
- if (!node) continue
400
-
401
- // Use only an anchor that is actually a child of the current parent
402
- let ref = anchor && anchor.parentNode === current ? anchor : null
392
+ let unit = committed[i]
393
+ let first = getSiblingUnitFirstNode(unit)
394
+ let ref = anchor?.parentNode === parent ? anchor : null
403
395
 
404
- // Existing comment markers delimit live ranges, so do not reorder them
405
- // independently from their contents. New markers still need to be inserted
406
- // before they can be used as anchors.
407
396
  if (
408
- isVirtualRootStartMarker(node) ||
409
- isVirtualRootEndMarker(node) ||
410
- isFrameStartMarker(node) ||
411
- isFrameEndMarker(node)
397
+ unit.kind === 'node' &&
398
+ isPreservedDomElement(unit.node) &&
399
+ unit.node.parentNode === parent
412
400
  ) {
413
- if (node.parentNode !== current) {
414
- current.insertBefore(node, ref)
415
- }
416
- anchor = node
401
+ anchor = unit.node
417
402
  continue
418
403
  }
419
404
 
420
- if (node.parentNode === current) {
421
- // Node already in parent: move only if its nextSibling is not the desired ref.
422
- let targetNext = ref
423
- let alreadyInPlace =
424
- (targetNext === null && node.nextSibling === null) || node.nextSibling === targetNext
425
- if (!alreadyInPlace) {
426
- current.insertBefore(node, targetNext)
427
- }
428
- } else {
429
- // New node: insert relative to a valid ref or append
430
- current.insertBefore(node, ref)
405
+ placeSiblingUnitBefore(unit, parent, ref)
406
+ if (first.parentNode === parent) anchor = first
407
+ }
408
+
409
+ for (let i = 0; i < currentUnits.length; i++) {
410
+ if (!used[i]) removeSiblingUnit(currentUnits[i], parent, context)
411
+ }
412
+ }
413
+
414
+ function parseSiblingUnits(nodes: Node[]): SiblingUnit[] {
415
+ let units: SiblingUnit[] = []
416
+
417
+ for (let i = 0; i < nodes.length; i++) {
418
+ let node = nodes[i]
419
+ if (isVirtualRootStartMarker(node)) {
420
+ let endIndex = findHydrationEndIndex(nodes, i)
421
+ invariant(endIndex > i, 'Hydration end marker not found')
422
+ let end = nodes[endIndex]
423
+ invariant(isVirtualRootEndMarker(end), 'Expected hydration end marker')
424
+ units.push({ kind: 'hydration', start: node, end, startIndex: i, endIndex })
425
+ i = endIndex
426
+ continue
431
427
  }
432
428
 
433
- // Advance anchor only after the node is placed in the correct parent
434
- if (node.parentNode === current) {
435
- anchor = node
429
+ if (isFrameStartMarker(node)) {
430
+ let endIndex = findFrameEndIndex(nodes, i)
431
+ invariant(endIndex > i, 'Frame end marker not found')
432
+ let end = nodes[endIndex]
433
+ invariant(isFrameEndMarker(end), 'Expected frame end marker')
434
+ units.push({ kind: 'frame', start: node, end, startIndex: i, endIndex })
435
+ i = endIndex
436
+ continue
436
437
  }
438
+
439
+ invariant(!isVirtualRootEndMarker(node), 'Unexpected hydration end marker')
440
+ invariant(!isFrameEndMarker(node), 'Unexpected frame end marker')
441
+ units.push({ kind: 'node', node, startIndex: i, endIndex: i })
437
442
  }
438
443
 
439
- // Remove any current children not used
440
- for (let i = 0; i < currentChildren.length; i++) {
441
- if (!used[i]) {
442
- let nodeToRemove = currentChildren[i]
443
- removeNode(nodeToRemove, current, context)
444
+ return units
445
+ }
446
+
447
+ function getSiblingUnitKey(unit: SiblingUnit): string | undefined {
448
+ if (unit.kind !== 'node' || !isElement(unit.node)) return
449
+ return unit.node.getAttribute('data-key') ?? undefined
450
+ }
451
+
452
+ function siblingUnitsComparable(current: SiblingUnit, next: SiblingUnit): boolean {
453
+ if (current.kind !== next.kind) return false
454
+ if (current.kind !== 'node' || next.kind !== 'node') return true
455
+ return nodeTypesComparable(current.node, next.node)
456
+ }
457
+
458
+ function shouldPreserveBoundaryUnit(
459
+ current: BoundarySiblingUnit,
460
+ next: BoundarySiblingUnit,
461
+ context: FrameContext,
462
+ ): boolean {
463
+ if (current.kind !== next.kind) return false
464
+ return current.kind === 'frame'
465
+ ? shouldPreserveFrameStartMarker(current.start, next.start, context)
466
+ : shouldPreserveHydrationStartMarker(current.start, next.start, context)
467
+ }
468
+
469
+ function getSiblingUnitFirstNode(unit: SiblingUnit): Node {
470
+ return unit.kind === 'node' ? unit.node : unit.start
471
+ }
472
+
473
+ function placeSiblingUnitBefore(unit: SiblingUnit, parent: ParentNode, ref: Node | null): void {
474
+ if (unit.kind === 'node') {
475
+ if (unit.node.parentNode !== parent || unit.node.nextSibling !== ref) {
476
+ parent.insertBefore(unit.node, ref)
444
477
  }
478
+ return
479
+ }
480
+
481
+ let nodes = collectNodeRange(unit.start, unit.end)
482
+ if (unit.start.parentNode === parent && unit.end.nextSibling === ref) return
483
+
484
+ let fragment = unit.start.ownerDocument.createDocumentFragment()
485
+ for (let node of nodes) fragment.appendChild(node)
486
+ parent.insertBefore(fragment, ref)
487
+ }
488
+
489
+ function removeSiblingUnit(unit: SiblingUnit, parent: ParentNode, context: FrameContext): void {
490
+ if (unit.kind === 'node') {
491
+ removeNode(unit.node, parent, context)
492
+ return
445
493
  }
494
+
495
+ for (let node of collectNodeRange(unit.start, unit.end)) {
496
+ removeNode(node, parent, context)
497
+ }
498
+ }
499
+
500
+ function collectNodesBetween(start: Comment, end: Comment): Node[] {
501
+ let nodes: Node[] = []
502
+ let node = start.nextSibling
503
+ while (node && node !== end) {
504
+ nodes.push(node)
505
+ node = node.nextSibling
506
+ }
507
+ return nodes
508
+ }
509
+
510
+ function isPreservedDomElement(node: Node): node is Element {
511
+ return isElement(node) && node.hasAttribute(REMIX_PRESERVE_DOM_ATTRIBUTE)
446
512
  }
447
513
 
448
514
  function nodeTypesComparable(a: Node, b: Node): boolean {
449
515
  if (isTextNode(a) && isTextNode(b)) return true
450
516
  if (isElement(a) && isElement(b)) return a.tagName === b.tagName
451
- if (isVirtualRootStartMarker(a) && isVirtualRootStartMarker(b)) return true
452
- if (isVirtualRootEndMarker(a) && isVirtualRootEndMarker(b)) return true
453
- if (isCommentNode(a) && isCommentNode(b)) return true
517
+ if (isCommentNode(a) && isCommentNode(b)) return markerKindsMatch(a, b)
454
518
  return false
455
519
  }
456
520
 
521
+ function getMarkerKind(node: Node): MarkerKind | undefined {
522
+ if (!isCommentNode(node)) return undefined
523
+ if (isFrameStartMarker(node)) return 'frame-start'
524
+ if (isFrameEndMarker(node)) return 'frame-end'
525
+ if (isVirtualRootStartMarker(node)) return 'virtual-root-start'
526
+ if (isVirtualRootEndMarker(node)) return 'virtual-root-end'
527
+ return undefined
528
+ }
529
+
530
+ function markerKindsMatch(a: Node, b: Node): boolean {
531
+ return getMarkerKind(a) === getMarkerKind(b)
532
+ }
533
+
457
534
  function isHydrationEndComment(node: Node): node is Comment {
458
535
  return isCommentNode(node) && node.data.trim() === '/rmx:h'
459
536
  }
@@ -477,8 +554,15 @@ function findHydrationEndMarker(start: Comment): Comment {
477
554
  }
478
555
 
479
556
  function findHydrationEndIndex(nodes: Node[], startIdx: number): number {
557
+ let depth = 1
558
+
480
559
  for (let j = startIdx + 1; j < nodes.length; j++) {
481
- if (isHydrationEndComment(nodes[j])) return j
560
+ let node = nodes[j]
561
+ if (isVirtualRootStartMarker(node)) depth++
562
+ if (isHydrationEndComment(node)) {
563
+ depth--
564
+ if (depth === 0) return j
565
+ }
482
566
  }
483
567
  return startIdx
484
568
  }
@@ -541,14 +625,31 @@ function shouldPreserveFrameStartMarker(
541
625
  ): boolean {
542
626
  if (!isFrameStartMarker(next)) return false
543
627
 
544
- let currentData = getFrameMarkerData(current, context)
545
628
  let nextData = getFrameMarkerData(next, context)
629
+ let currentFrame = context.frameInstances.get(current)
630
+
631
+ return (
632
+ currentFrame !== undefined &&
633
+ nextData !== undefined &&
634
+ currentFrame.matchesIdentity(nextData.src, nextData.name)
635
+ )
636
+ }
637
+
638
+ function shouldPreserveHydrationStartMarker(
639
+ current: Comment,
640
+ next: Comment,
641
+ context: FrameContext,
642
+ ): boolean {
643
+ if (!isVirtualRootStartMarker(next)) return false
644
+
645
+ let currentOwner = getClientEntryBoundaryOwner(current)
646
+ let nextData = getHydrationMarkerData(next, context)
546
647
 
547
648
  return (
548
- currentData !== undefined &&
649
+ currentOwner !== undefined &&
549
650
  nextData !== undefined &&
550
- currentData.src === nextData.src &&
551
- currentData.name === nextData.name
651
+ currentOwner.identity.moduleUrl === nextData.moduleUrl &&
652
+ currentOwner.identity.exportName === nextData.exportName
552
653
  )
553
654
  }
554
655
 
@@ -576,6 +677,30 @@ function getCommentMarkerRangeReplacement(
576
677
  nextEndIndex: findFrameEndIndex(nextNodes, nextIndex),
577
678
  }
578
679
  }
680
+
681
+ if (
682
+ isVirtualRootStartMarker(current) &&
683
+ isVirtualRootStartMarker(next) &&
684
+ !shouldPreserveHydrationStartMarker(current, next, context)
685
+ ) {
686
+ return {
687
+ currentStart: current,
688
+ nextStart: next,
689
+ currentEndIndex: findHydrationEndIndex(currentNodes, currentIndex),
690
+ nextEndIndex: findHydrationEndIndex(nextNodes, nextIndex),
691
+ }
692
+ }
693
+ }
694
+
695
+ function getHydrationMarkerData(marker: Comment, context: FrameContext) {
696
+ let id = getHydrationId(marker)
697
+ return context.data.h?.[id]
698
+ }
699
+
700
+ function getHydrationId(marker: Comment): string {
701
+ let trimmed = marker.data.trim()
702
+ invariant(trimmed.startsWith('rmx:h:'), 'Invalid hydration start marker')
703
+ return trimmed.slice('rmx:h:'.length)
579
704
  }
580
705
 
581
706
  function getFrameMarkerData(marker: Comment, context: FrameContext) {
@@ -594,8 +719,8 @@ function replaceCommentMarkerRange(
594
719
  parent: ParentNode,
595
720
  context: FrameContext,
596
721
  ): void {
597
- let currentEnd = findFrameEndMarker(replacement.currentStart)
598
- let nextEnd = findFrameEndMarker(replacement.nextStart)
722
+ let currentEnd = findCommentMarkerRangeEnd(replacement.currentStart)
723
+ let nextEnd = findCommentMarkerRangeEnd(replacement.nextStart)
599
724
  let nextNodes = collectNodeRange(replacement.nextStart, nextEnd)
600
725
  let currentNodes = collectNodeRange(replacement.currentStart, currentEnd)
601
726
 
@@ -608,6 +733,12 @@ function replaceCommentMarkerRange(
608
733
  }
609
734
  }
610
735
 
736
+ function findCommentMarkerRangeEnd(start: Comment): Comment {
737
+ if (isFrameStartMarker(start)) return findFrameEndMarker(start)
738
+ if (isVirtualRootStartMarker(start)) return findHydrationEndMarker(start)
739
+ throw new Error('Comment marker range start not found')
740
+ }
741
+
611
742
  function collectNodeRange(start: Node, end: Node): Node[] {
612
743
  let nodes: Node[] = []
613
744
  let node: Node | null = start
@@ -653,8 +784,7 @@ function disposeRemovedVirtualRoots(node: Node): void {
653
784
  let next = stack.pop()
654
785
  if (!next) continue
655
786
 
656
- if (isHydratedVirtualRootStartMarker(next)) {
657
- next.$rmx.dispose()
787
+ if (isVirtualRootStartMarker(next) && disposeClientEntryBoundary(next)) {
658
788
  continue
659
789
  }
660
790
 
@@ -692,10 +822,6 @@ function isVirtualRootStartMarker(node: Node): node is Comment {
692
822
  return isCommentNode(node) && node.data.trim().startsWith('rmx:h:')
693
823
  }
694
824
 
695
- function isHydratedVirtualRootStartMarker(node: Node): node is HydratedVirtualRootStartMarker {
696
- return isVirtualRootStartMarker(node) && '$rmx' in node
697
- }
698
-
699
825
  function isVirtualRootEndMarker(node: Node): node is Comment {
700
826
  return isCommentNode(node) && node.data.trim() === '/rmx:h'
701
827
  }