@remix-run/ui 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,1211 @@
1
+ import { jsx } from './jsx.ts'
2
+ import { Frame, createFrameHandle, type FrameContent } from './component.ts'
3
+ import { createComponentErrorEvent, getComponentError } from './error-event.ts'
4
+ import { invariant } from './invariant.ts'
5
+ import type { RemixElement, RemixNode } from './jsx.ts'
6
+ import type { FrameHandle } from './component.ts'
7
+ import type { Scheduler, VirtualRoot } from './vdom.ts'
8
+ import { createRangeRoot, createRoot } from './vdom.ts'
9
+ import { diffNodes } from './diff-dom.ts'
10
+ import { createStyleManager, type StyleManager } from '../style/index.ts'
11
+
12
+ type FrameRoot = [Comment, Comment] | Element | Document | DocumentFragment
13
+
14
+ type FrameData = {
15
+ status: 'pending' | 'resolved'
16
+ name?: string
17
+ src: string
18
+ }
19
+
20
+ type HydrationData = {
21
+ moduleUrl: string
22
+ exportName: string
23
+ props: Record<string, unknown>
24
+ }
25
+
26
+ type RmxData = {
27
+ h?: Record<string, HydrationData>
28
+ f?: Record<string, FrameData>
29
+ }
30
+
31
+ export type VirtualRootMarker = Comment & {
32
+ $rmx: ReturnType<typeof createRangeRoot>
33
+ }
34
+
35
+ type FrameMarkerData = FrameData & {
36
+ id: string
37
+ }
38
+
39
+ type PendingClientEntries = Map<Comment, [Comment, RemixElement]>
40
+
41
+ /**
42
+ * Loads a client entry module for hydration.
43
+ */
44
+ export type LoadModule = (moduleUrl: string, exportName: string) => Promise<Function> | Function
45
+
46
+ /**
47
+ * Resolves frame content for the given frame source.
48
+ */
49
+ export type ResolveFrame = (
50
+ src: string,
51
+ signal?: AbortSignal,
52
+ target?: string,
53
+ ) => Promise<FrameContent> | FrameContent
54
+
55
+ type InternalFrameContent = FrameContent | DocumentFragment
56
+
57
+ type FrameTemplateListener = (fragment: DocumentFragment) => void
58
+
59
+ const bufferedFrameTemplates = new Map<string, DocumentFragment[]>()
60
+ const frameTemplateListeners = new Map<string, Set<FrameTemplateListener>>()
61
+
62
+ function syncElementAttributes(target: Element, source: Element) {
63
+ for (let attribute of Array.from(target.attributes)) {
64
+ if (!source.hasAttribute(attribute.name)) {
65
+ target.removeAttribute(attribute.name)
66
+ }
67
+ }
68
+
69
+ for (let attribute of Array.from(source.attributes)) {
70
+ if (target.getAttribute(attribute.name) !== attribute.value) {
71
+ target.setAttribute(attribute.name, attribute.value)
72
+ }
73
+ }
74
+ }
75
+
76
+ export type FrameRuntime = {
77
+ topFrame?: FrameHandle
78
+ errorTarget: EventTarget
79
+ loadModule: LoadModule
80
+ resolveFrame: ResolveFrame
81
+ pendingClientEntries: PendingClientEntries
82
+ scheduler: Scheduler
83
+ styleManager: StyleManager
84
+ data: RmxData
85
+ moduleCache: Map<string, Function>
86
+ moduleLoads: Map<string, Promise<Function | undefined>>
87
+ frameInstances: WeakMap<Comment, Frame>
88
+ namedFrames: Map<string, FrameHandle>
89
+ }
90
+
91
+ export type FrameContext = {
92
+ topFrame?: FrameHandle
93
+ errorTarget: EventTarget
94
+ loadModule: LoadModule
95
+ resolveFrame: ResolveFrame
96
+ pendingClientEntries: PendingClientEntries
97
+ scheduler: Scheduler
98
+ frame: FrameHandle
99
+ styleManager: StyleManager
100
+ data: RmxData
101
+ moduleCache: Map<string, Function>
102
+ moduleLoads: Map<string, Promise<Function | undefined>>
103
+ frameInstances: WeakMap<Comment, Frame>
104
+ namedFrames: Map<string, FrameHandle>
105
+ regionTailRef?: ChildNode | null
106
+ regionParent?: ParentNode | null
107
+ }
108
+
109
+ type FrameInit = {
110
+ name?: string
111
+ topFrame?: FrameHandle
112
+ src: string
113
+ errorTarget: EventTarget
114
+ loadModule: LoadModule
115
+ resolveFrame: ResolveFrame
116
+ pendingClientEntries: PendingClientEntries
117
+ scheduler: Scheduler
118
+ styleManager?: StyleManager
119
+ marker?: FrameMarkerData
120
+ data: RmxData
121
+ moduleCache: Map<string, Function>
122
+ moduleLoads: Map<string, Promise<Function | undefined>>
123
+ frameInstances: WeakMap<Comment, Frame>
124
+ namedFrames: Map<string, FrameHandle>
125
+ }
126
+
127
+ export type Frame = {
128
+ render: (content: InternalFrameContent, options?: RenderOptions) => Promise<void>
129
+ ready: () => Promise<void>
130
+ flush: () => void
131
+ dispose: () => void
132
+ handle: FrameHandle
133
+ }
134
+
135
+ type RenderOptions = {
136
+ initialHydrationTracker?: InitialHydrationTracker
137
+ signal?: AbortSignal
138
+ }
139
+
140
+ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
141
+ let container = createContainer(root)
142
+ let observers: MutationObserver[] = []
143
+ let subscriptions: Array<() => void> = []
144
+ let contentRoot: VirtualRoot | undefined
145
+ let reloadController: AbortController | undefined
146
+ let styleManager = init.styleManager ?? createStyleManager()
147
+
148
+ // Merge any rmx-data found in the current document once at startup.
149
+ mergeRmxDataFromDocument(init.data, container.doc)
150
+
151
+ let runtime = createFrameRuntime({ ...init, styleManager })
152
+
153
+ let frame = createFrameHandle({
154
+ src: init.src,
155
+ $runtime: runtime,
156
+ reload: async () => {
157
+ reloadController?.abort()
158
+ let controller = new AbortController()
159
+ reloadController = controller
160
+ frame.dispatchEvent(new Event('reloadStart'))
161
+ try {
162
+ let content = await init.resolveFrame(frame.src, controller.signal, frameName)
163
+ if (reloadController !== controller || controller.signal.aborted) return controller.signal
164
+ await render(content, { signal: controller.signal })
165
+ return controller.signal
166
+ } catch (error) {
167
+ if (reloadController !== controller || controller.signal.aborted) return controller.signal
168
+ init.errorTarget.dispatchEvent(createComponentErrorEvent(error))
169
+ throw error
170
+ } finally {
171
+ if (reloadController === controller) {
172
+ frame.dispatchEvent(new Event('reloadComplete'))
173
+ }
174
+ }
175
+ },
176
+ replace: async (content: FrameContent) => {
177
+ await render(content)
178
+ },
179
+ })
180
+ runtime.topFrame = runtime.topFrame ?? init.topFrame ?? frame
181
+
182
+ let frameName = init.marker?.name ?? init.name
183
+ if (frameName) {
184
+ init.namedFrames.set(frameName, frame)
185
+ }
186
+
187
+ let context: FrameContext = {
188
+ topFrame: runtime.topFrame,
189
+ errorTarget: init.errorTarget,
190
+ loadModule: init.loadModule,
191
+ resolveFrame: init.resolveFrame,
192
+ pendingClientEntries: init.pendingClientEntries,
193
+ scheduler: init.scheduler,
194
+ frame,
195
+ styleManager,
196
+ data: init.data,
197
+ moduleCache: init.moduleCache,
198
+ moduleLoads: init.moduleLoads,
199
+ frameInstances: init.frameInstances,
200
+ namedFrames: init.namedFrames,
201
+ regionTailRef: container.regionTailRef,
202
+ regionParent: container.regionParent,
203
+ }
204
+
205
+ async function render(content: InternalFrameContent, options?: RenderOptions): Promise<void> {
206
+ if (options?.signal?.aborted) return
207
+
208
+ if (content instanceof ReadableStream) {
209
+ await renderFrameStream(content, container.doc, async (html) => {
210
+ if (options?.signal?.aborted) return
211
+ await render(html, options)
212
+ })
213
+ return
214
+ }
215
+
216
+ if (isRemixNodeFrameContent(content)) {
217
+ if (!contentRoot) {
218
+ let currentNodes = getContentNodes()
219
+ removeVirtualRoots(currentNodes)
220
+ disposeSubFrames(currentNodes, context)
221
+ clearFrameContent()
222
+ contentRoot = createFrameContentRoot()
223
+ }
224
+
225
+ if (options?.signal?.aborted) return
226
+ contentRoot.render(content)
227
+ return
228
+ }
229
+
230
+ if (contentRoot) {
231
+ contentRoot.dispose()
232
+ contentRoot = undefined
233
+ }
234
+
235
+ let isFullDocumentReload =
236
+ container.root instanceof Document &&
237
+ typeof content === 'string' &&
238
+ isFullDocumentHtml(content)
239
+
240
+ if (isFullDocumentReload && typeof content === 'string') {
241
+ // Full-document reload should tear down existing hydrated roots and subframes
242
+ // before diffing fresh HTML, otherwise stale component instances can survive
243
+ // on detached DOM nodes.
244
+ let previousBodyNodes = Array.from(container.doc.body.childNodes)
245
+ removeVirtualRoots(previousBodyNodes)
246
+ disposeSubFrames(previousBodyNodes, context)
247
+ let parsed = new DOMParser().parseFromString(content, 'text/html')
248
+ mergeRmxDataFromDocument(context.data, parsed)
249
+ context.styleManager.reset()
250
+ context.styleManager.adoptServerStyles(
251
+ collectFrameServerStyleTags(createElementContainer(parsed)),
252
+ )
253
+
254
+ syncElementAttributes(container.doc.documentElement, parsed.documentElement)
255
+ syncElementAttributes(container.doc.head, parsed.head)
256
+ syncElementAttributes(container.doc.body, parsed.body)
257
+
258
+ diffNodes(Array.from(container.doc.head.childNodes), Array.from(parsed.head.childNodes), {
259
+ ...context,
260
+ regionParent: container.doc.head,
261
+ regionTailRef: null,
262
+ })
263
+ diffNodes(Array.from(container.doc.body.childNodes), Array.from(parsed.body.childNodes), {
264
+ ...context,
265
+ regionParent: container.doc.body,
266
+ regionTailRef: null,
267
+ })
268
+
269
+ let bodyContainer = createElementContainer(container.doc.body)
270
+ if (options?.signal?.aborted) return
271
+ scheduleHydrationInContainer(bodyContainer, context, options?.initialHydrationTracker)
272
+ createSubFrames(bodyContainer.childNodes, context)
273
+ return
274
+ }
275
+
276
+ let fragment =
277
+ typeof content === 'string' ? createFragmentFromString(container.doc, content) : content
278
+ context.styleManager.reset()
279
+ context.styleManager.adoptServerStyles(
280
+ collectFrameServerStyleTags(createElementContainer(fragment)),
281
+ )
282
+ removeEmptyHeads(fragment)
283
+ mergeRmxDataFromFragment(context.data, fragment)
284
+
285
+ let nextContainer = createContainer(fragment)
286
+
287
+ if (options?.signal?.aborted) return
288
+
289
+ diffNodes(container.childNodes, Array.from(nextContainer.childNodes), {
290
+ ...context,
291
+ regionTailRef: container.regionTailRef,
292
+ regionParent: container.regionParent,
293
+ })
294
+
295
+ if (options?.signal?.aborted) return
296
+ scheduleHydrationInContainer(container, context, options?.initialHydrationTracker)
297
+ createSubFrames(container.childNodes, context)
298
+ }
299
+
300
+ function createFrameContentRoot(): VirtualRoot {
301
+ let virtualRoot: VirtualRoot
302
+ if (container.root instanceof Document) {
303
+ virtualRoot = createRoot(container.doc.body, {
304
+ scheduler: context.scheduler,
305
+ frame,
306
+ styleManager: context.styleManager,
307
+ })
308
+ } else {
309
+ invariant(Array.isArray(root), 'Expected comment-bounded frame root')
310
+ virtualRoot = createRangeRoot(root, {
311
+ scheduler: context.scheduler,
312
+ frame,
313
+ styleManager: context.styleManager,
314
+ })
315
+ }
316
+
317
+ virtualRoot.addEventListener('error', (event: Event) => {
318
+ if (context.errorTarget === virtualRoot) return
319
+ context.errorTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))
320
+ })
321
+
322
+ return virtualRoot
323
+ }
324
+
325
+ function getContentNodes(): Node[] {
326
+ return container.root instanceof Document
327
+ ? Array.from(container.doc.body.childNodes)
328
+ : container.childNodes
329
+ }
330
+
331
+ function clearFrameContent() {
332
+ for (let node of getContentNodes()) {
333
+ node.parentNode?.removeChild(node)
334
+ }
335
+ }
336
+
337
+ async function hydrateInitial(): Promise<void> {
338
+ let initialHydrationTracker = createInitialHydrationTracker()
339
+
340
+ context.styleManager.reset()
341
+ context.styleManager.adoptServerStyles(collectFrameServerStyleTags(container))
342
+ createSubFrames(container.childNodes, context)
343
+ scheduleHydrationInContainer(container, context, initialHydrationTracker)
344
+
345
+ if (init.marker?.status === 'pending') {
346
+ let markerId = init.marker.id
347
+ let early = consumeFrameTemplate(markerId) ?? getEarlyFrameContent(markerId)
348
+ if (early) {
349
+ await render(early, { initialHydrationTracker })
350
+ } else {
351
+ let observer = setupTemplateObserver()
352
+ let unsubscribe = subscribeFrameTemplate(markerId, async (fragment) => {
353
+ unsubscribe()
354
+ await render(fragment)
355
+ observer.disconnect()
356
+ })
357
+ subscriptions.push(unsubscribe)
358
+ let buffered = consumeFrameTemplate(markerId)
359
+ if (buffered) {
360
+ unsubscribe()
361
+ await render(buffered)
362
+ observer.disconnect()
363
+ }
364
+ observers.push(observer)
365
+ }
366
+ }
367
+
368
+ initialHydrationTracker.finalize()
369
+ await initialHydrationTracker.ready()
370
+ }
371
+
372
+ function dispose(): void {
373
+ reloadController?.abort()
374
+ reloadController = undefined
375
+ contentRoot?.dispose()
376
+ contentRoot = undefined
377
+
378
+ // Disconnect any MutationObservers waiting for templates.
379
+ for (let observer of observers) {
380
+ observer.disconnect()
381
+ }
382
+ observers.length = 0
383
+ for (let unsubscribe of subscriptions) {
384
+ unsubscribe()
385
+ }
386
+ subscriptions.length = 0
387
+
388
+ // Remove hydrated virtual roots in this frame's region.
389
+ removeVirtualRoots(container.childNodes)
390
+
391
+ // Dispose sub-frames recursively.
392
+ disposeSubFrames(container.childNodes, context)
393
+ context.styleManager.dispose()
394
+
395
+ if (frameName) {
396
+ if (init.namedFrames.get(frameName) === frame) {
397
+ init.namedFrames.delete(frameName)
398
+ }
399
+ }
400
+ }
401
+
402
+ let readyPromise = hydrateInitial()
403
+
404
+ return {
405
+ render,
406
+ ready: () => readyPromise,
407
+ flush: () => context.scheduler.dequeue(),
408
+ dispose,
409
+ handle: frame,
410
+ }
411
+ }
412
+
413
+ export function createFrameRuntime(init: {
414
+ topFrame?: FrameHandle
415
+ errorTarget: EventTarget
416
+ loadModule: LoadModule
417
+ resolveFrame: ResolveFrame
418
+ pendingClientEntries: PendingClientEntries
419
+ scheduler: Scheduler
420
+ styleManager: StyleManager
421
+ data: RmxData
422
+ moduleCache: Map<string, Function>
423
+ moduleLoads: Map<string, Promise<Function | undefined>>
424
+ frameInstances: WeakMap<Comment, Frame>
425
+ namedFrames: Map<string, FrameHandle>
426
+ }): FrameRuntime {
427
+ return {
428
+ topFrame: init.topFrame,
429
+ errorTarget: init.errorTarget,
430
+ loadModule: init.loadModule,
431
+ resolveFrame: init.resolveFrame,
432
+ pendingClientEntries: init.pendingClientEntries,
433
+ scheduler: init.scheduler,
434
+ styleManager: init.styleManager,
435
+ data: init.data,
436
+ moduleCache: init.moduleCache,
437
+ moduleLoads: init.moduleLoads,
438
+ frameInstances: init.frameInstances,
439
+ namedFrames: init.namedFrames,
440
+ }
441
+ }
442
+
443
+ type InitialHydrationTracker = {
444
+ track: () => () => void
445
+ finalize: () => void
446
+ ready: () => Promise<void>
447
+ }
448
+
449
+ function createInitialHydrationTracker(): InitialHydrationTracker {
450
+ let pending = 0
451
+ let finalized = false
452
+
453
+ let resolveReady: (() => void) | undefined
454
+ let readyPromise = new Promise<void>((resolve) => {
455
+ resolveReady = resolve
456
+ })
457
+
458
+ function maybeResolve() {
459
+ if (finalized && pending === 0) {
460
+ resolveReady?.()
461
+ resolveReady = undefined
462
+ }
463
+ }
464
+
465
+ return {
466
+ track() {
467
+ pending++
468
+ let completed = false
469
+ return () => {
470
+ if (completed) return
471
+ completed = true
472
+ pending--
473
+ maybeResolve()
474
+ }
475
+ },
476
+ finalize() {
477
+ finalized = true
478
+ maybeResolve()
479
+ },
480
+ ready() {
481
+ return readyPromise
482
+ },
483
+ }
484
+ }
485
+
486
+ function mergeRmxDataFromDocument(into: RmxData, doc: Document): void {
487
+ let scripts = Array.from(doc.querySelectorAll('script#rmx-data'))
488
+ for (let script of scripts) {
489
+ if (!(script instanceof HTMLScriptElement)) continue
490
+ mergeRmxData(into, parseRmxDataScript(script))
491
+ script.remove()
492
+ }
493
+ }
494
+
495
+ function mergeRmxDataFromFragment(into: RmxData, fragment: DocumentFragment): void {
496
+ let scripts = Array.from(fragment.querySelectorAll('script#rmx-data'))
497
+ for (let script of scripts) {
498
+ if (!(script instanceof HTMLScriptElement)) continue
499
+ mergeRmxData(into, parseRmxDataScript(script))
500
+ script.remove()
501
+ }
502
+ }
503
+
504
+ function removeEmptyHeads(fragment: DocumentFragment): void {
505
+ let heads = Array.from(fragment.querySelectorAll('head'))
506
+ for (let head of heads) {
507
+ if (!head.childNodes.length) {
508
+ head.remove()
509
+ }
510
+ }
511
+ }
512
+
513
+ function collectFrameServerStyleTags(container: FrameContainer): HTMLStyleElement[] {
514
+ let styles: HTMLStyleElement[] = []
515
+ let nodes =
516
+ container.root instanceof Document
517
+ ? [...Array.from(container.doc.head.childNodes), ...Array.from(container.doc.body.childNodes)]
518
+ : container.childNodes
519
+
520
+ collectOwnedServerStyleTags(nodes, styles)
521
+ return styles
522
+ }
523
+
524
+ function collectOwnedServerStyleTags(nodes: Node[], styles: HTMLStyleElement[]): void {
525
+ for (let i = 0; i < nodes.length; i++) {
526
+ let node = nodes[i]
527
+
528
+ if (isFrameStart(node)) {
529
+ let end = findEndMarker(node, isFrameStart, isFrameEnd)
530
+ i = nodes.indexOf(end)
531
+ continue
532
+ }
533
+
534
+ if (node instanceof HTMLStyleElement && node.matches('style[data-rmx]')) {
535
+ styles.push(node)
536
+ continue
537
+ }
538
+
539
+ if (node instanceof Element || node instanceof Document || node instanceof DocumentFragment) {
540
+ collectOwnedServerStyleTags(Array.from(node.childNodes), styles)
541
+ }
542
+ }
543
+ }
544
+
545
+ function parseRmxDataScript(script: HTMLScriptElement): RmxData {
546
+ try {
547
+ return JSON.parse(script.textContent || '{}')
548
+ } catch {
549
+ console.error('[createFrame] Failed to parse rmx-data script')
550
+ return {}
551
+ }
552
+ }
553
+
554
+ function mergeRmxData(into: RmxData, from: RmxData): void {
555
+ if (from.h) {
556
+ if (!into.h) into.h = {}
557
+ copyOwnRmxEntries(into.h, from.h)
558
+ }
559
+
560
+ if (from.f) {
561
+ if (!into.f) into.f = {}
562
+ copyOwnRmxEntries(into.f, from.f)
563
+ }
564
+ }
565
+
566
+ function copyOwnRmxEntries<T>(target: Record<string, T>, source: Record<string, T>): void {
567
+ for (let key of Object.keys(source)) {
568
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
569
+ if (!Object.hasOwn(source, key)) continue
570
+ target[key] = source[key]!
571
+ }
572
+ }
573
+
574
+ function scheduleHydrationInContainer(
575
+ container: FrameContainer,
576
+ context: FrameContext,
577
+ initialHydrationTracker?: InitialHydrationTracker,
578
+ ): void {
579
+ let hydrationMarkers = findHydrationMarkers(container)
580
+ if (hydrationMarkers.length === 0) return
581
+
582
+ let hydrationData = context.data.h
583
+ if (!hydrationData) return
584
+
585
+ for (let marker of hydrationMarkers) {
586
+ let entry = hydrationData[marker.id]
587
+ if (!entry) continue
588
+ scheduleHydrationMarker(marker, entry, context, initialHydrationTracker)
589
+ }
590
+ }
591
+
592
+ function scheduleHydrationMarker(
593
+ marker: HydrationMarker,
594
+ entry: HydrationData,
595
+ context: FrameContext,
596
+ initialHydrationTracker?: InitialHydrationTracker,
597
+ ): void {
598
+ let done = initialHydrationTracker?.track()
599
+ let key = `${entry.moduleUrl}#${entry.exportName}`
600
+
601
+ let hydrateWithComponent = (component: Function) => {
602
+ if (!isHydrationMarkerLive(marker, context)) return
603
+ let vElement = createElement(component, entry.props)
604
+ context.pendingClientEntries.set(marker.start, [marker.end, vElement])
605
+ hydrateRegion(vElement, marker.start, marker.end, context)
606
+ }
607
+
608
+ let cached = context.moduleCache.get(key)
609
+ if (cached) {
610
+ hydrateWithComponent(cached)
611
+ done?.()
612
+ return
613
+ }
614
+
615
+ getOrStartModuleLoad(key, entry, marker.id, context)
616
+ .then((component) => {
617
+ if (component) {
618
+ hydrateWithComponent(component)
619
+ }
620
+ })
621
+ .finally(() => {
622
+ done?.()
623
+ })
624
+ }
625
+
626
+ function getOrStartModuleLoad(
627
+ key: string,
628
+ entry: HydrationData,
629
+ markerId: string,
630
+ context: FrameContext,
631
+ ): Promise<Function | undefined> {
632
+ let inFlight = context.moduleLoads.get(key)
633
+ if (inFlight) return inFlight
634
+
635
+ let loadPromise = (async () => {
636
+ try {
637
+ let mod = await context.loadModule(entry.moduleUrl, entry.exportName)
638
+ if (typeof mod !== 'function') {
639
+ throw new Error(`Export "${entry.exportName}" from "${entry.moduleUrl}" is not a function`)
640
+ }
641
+ context.moduleCache.set(key, mod)
642
+ return mod
643
+ } catch (error) {
644
+ console.error(`[createFrame] Failed to load module for ${markerId}:`, error)
645
+ return undefined
646
+ } finally {
647
+ context.moduleLoads.delete(key)
648
+ }
649
+ })()
650
+
651
+ context.moduleLoads.set(key, loadPromise)
652
+ return loadPromise
653
+ }
654
+
655
+ function createElement(component: Function, props: Record<string, unknown>): RemixElement {
656
+ let revivedProps = reviveSerializedValue(props)
657
+ return jsx(component, revivedProps as any)
658
+ }
659
+
660
+ function reviveSerializedValue(value: unknown): unknown {
661
+ if (value === null || value === undefined) return value
662
+ if (typeof value !== 'object') return value
663
+
664
+ if (Array.isArray(value)) {
665
+ return value.map((item) => reviveSerializedValue(item))
666
+ }
667
+
668
+ let record = value as Record<string, unknown>
669
+
670
+ if (record.$rmxFrame === true) {
671
+ let props = reviveSerializedObject(record.props)
672
+ let key = reviveSerializedValue(record.key)
673
+ return jsx(Frame as any, props as any, key as any)
674
+ }
675
+
676
+ if (record.$rmx === true && typeof record.type === 'string') {
677
+ let props = reviveSerializedObject(record.props)
678
+ let key = reviveSerializedValue(record.key)
679
+ return jsx(record.type as any, props as any, key as any)
680
+ }
681
+
682
+ let revived: Record<string, unknown> = {}
683
+ for (let key in record) {
684
+ revived[key] = reviveSerializedValue(record[key])
685
+ }
686
+ return revived
687
+ }
688
+
689
+ function reviveSerializedObject(value: unknown): Record<string, unknown> {
690
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
691
+ let revived = reviveSerializedValue(value)
692
+ if (!revived || typeof revived !== 'object' || Array.isArray(revived)) return {}
693
+ return revived as Record<string, unknown>
694
+ }
695
+
696
+ function hydrateRegion(
697
+ vElement: RemixElement,
698
+ start: Comment,
699
+ end: Comment,
700
+ context: FrameContext,
701
+ ): void {
702
+ context.pendingClientEntries.delete(start)
703
+
704
+ // The same marker can be discovered by overlapping hydration passes
705
+ // (for example, document root + nested frame root). Reuse the existing
706
+ // virtual root instead of redefining the marker property.
707
+ if (isHydratedVirtualRootMarker(start)) {
708
+ start.$rmx.render(vElement)
709
+ return
710
+ }
711
+
712
+ let root = createRangeRoot([start, end], {
713
+ scheduler: context.scheduler,
714
+ frame: context.frame,
715
+ styleManager: context.styleManager,
716
+ })
717
+ root.addEventListener('error', (event) => {
718
+ if (context.errorTarget === root) return
719
+ context.errorTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))
720
+ })
721
+
722
+ Object.defineProperty(start, '$rmx', { value: root, enumerable: false })
723
+ root.render(vElement)
724
+ }
725
+
726
+ function createSubFrames(nodes: Node[], context: FrameContext) {
727
+ for (let i = 0; i < nodes.length; i++) {
728
+ let node = nodes[i]
729
+
730
+ if (isFrameStart(node)) {
731
+ let end = findEndMarker(node, isFrameStart, isFrameEnd)
732
+
733
+ if (!context.frameInstances.has(node)) {
734
+ let id = getFrameId(node)
735
+ let marker = context.data.f?.[id]
736
+ if (marker) {
737
+ let frameMarker: FrameMarkerData = { ...marker, id }
738
+ let subFrame = createFrame([node, end], {
739
+ src: frameMarker.src,
740
+ marker: frameMarker,
741
+ topFrame: context.topFrame,
742
+ errorTarget: context.errorTarget,
743
+ loadModule: context.loadModule,
744
+ resolveFrame: context.resolveFrame,
745
+ pendingClientEntries: context.pendingClientEntries,
746
+ scheduler: context.scheduler,
747
+ data: context.data,
748
+ moduleCache: context.moduleCache,
749
+ moduleLoads: context.moduleLoads,
750
+ frameInstances: context.frameInstances,
751
+ namedFrames: context.namedFrames,
752
+ })
753
+ context.frameInstances.set(node, subFrame)
754
+ }
755
+ }
756
+
757
+ i = nodes.indexOf(end)
758
+ continue
759
+ }
760
+
761
+ if (node.childNodes && node.childNodes.length > 0) {
762
+ createSubFrames(Array.from(node.childNodes), context)
763
+ }
764
+ }
765
+ }
766
+
767
+ function isHydrationMarkerLive(marker: HydrationMarker, context: FrameContext): boolean {
768
+ if (!marker.start.isConnected || !marker.end.isConnected) return false
769
+ if (marker.start.parentNode !== marker.end.parentNode) return false
770
+
771
+ let startText = marker.start.data.trim()
772
+ if (startText !== `rmx:h:${marker.id}`) return false
773
+ if (marker.end.data.trim() !== '/rmx:h') return false
774
+
775
+ let parent = marker.start.parentNode
776
+ if (!parent) return false
777
+
778
+ if (context.regionTailRef) {
779
+ let startPosition = marker.start.compareDocumentPosition(context.regionTailRef)
780
+ let endPosition = marker.end.compareDocumentPosition(context.regionTailRef)
781
+ let tailFollowsStart = (startPosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
782
+ let tailFollowsEnd = (endPosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
783
+ if (!tailFollowsStart || !tailFollowsEnd) return false
784
+ }
785
+
786
+ return true
787
+ }
788
+
789
+ function removeVirtualRoots(nodes: Node[]): void {
790
+ for (let i = 0; i < nodes.length; i++) {
791
+ let node = nodes[i]
792
+
793
+ if (isHydratedVirtualRootMarker(node)) {
794
+ node.$rmx.dispose()
795
+ let end = findEndMarker(node, isHydrationStart, isHydrationEnd)
796
+ i = nodes.indexOf(end)
797
+ continue
798
+ }
799
+
800
+ if (node.childNodes && node.childNodes.length > 0) {
801
+ removeVirtualRoots(Array.from(node.childNodes))
802
+ }
803
+ }
804
+ }
805
+
806
+ function disposeSubFrames(nodes: Node[], context: FrameContext): void {
807
+ for (let i = 0; i < nodes.length; i++) {
808
+ let node = nodes[i]
809
+
810
+ if (isFrameStart(node)) {
811
+ let end = findEndMarker(node, isFrameStart, isFrameEnd)
812
+ let subFrame = context.frameInstances.get(node)
813
+ if (subFrame) {
814
+ subFrame.dispose()
815
+ context.frameInstances.delete(node)
816
+ }
817
+ i = nodes.indexOf(end)
818
+ continue
819
+ }
820
+
821
+ if (node.childNodes && node.childNodes.length > 0) {
822
+ disposeSubFrames(Array.from(node.childNodes), context)
823
+ }
824
+ }
825
+ }
826
+
827
+ function getEarlyFrameContent(id: string): DocumentFragment | null {
828
+ let template = document.querySelector(`template#${id}`)
829
+ if (template instanceof HTMLTemplateElement) {
830
+ let fragment = template.content
831
+ template.remove()
832
+ return fragment
833
+ }
834
+ return null
835
+ }
836
+
837
+ function setupTemplateObserver(): MutationObserver {
838
+ let root = document.body ?? document.documentElement ?? document
839
+ let observer = new MutationObserver((mutations) => {
840
+ for (let mutation of mutations) {
841
+ for (let node of mutation.addedNodes) {
842
+ collectAndPublishTemplates(node)
843
+ }
844
+ }
845
+ })
846
+
847
+ observer.observe(root, { childList: true, subtree: true })
848
+ return observer
849
+ }
850
+
851
+ function collectAndPublishTemplates(node: Node): void {
852
+ if (node instanceof HTMLTemplateElement) {
853
+ publishFrameTemplateElement(node)
854
+ return
855
+ }
856
+
857
+ if (!(node instanceof Element)) return
858
+ let templates = Array.from(node.querySelectorAll('template'))
859
+ for (let template of templates) {
860
+ if (!(template instanceof HTMLTemplateElement)) continue
861
+ publishFrameTemplateElement(template)
862
+ }
863
+ }
864
+
865
+ function publishFrameTemplateElement(template: HTMLTemplateElement): void {
866
+ if (!template.id) return
867
+ template.remove()
868
+ publishFrameTemplate(template.id, template.content)
869
+ }
870
+
871
+ export function publishFrameTemplate(id: string, fragment: DocumentFragment): void {
872
+ let listeners = frameTemplateListeners.get(id)
873
+ if (!listeners || listeners.size === 0) {
874
+ let queue = bufferedFrameTemplates.get(id)
875
+ if (!queue) {
876
+ queue = []
877
+ bufferedFrameTemplates.set(id, queue)
878
+ }
879
+ queue.push(fragment)
880
+ return
881
+ }
882
+
883
+ for (let listener of listeners) {
884
+ listener(fragment.cloneNode(true) as DocumentFragment)
885
+ }
886
+ }
887
+
888
+ export function consumeFrameTemplate(id: string): DocumentFragment | null {
889
+ let queue = bufferedFrameTemplates.get(id)
890
+ if (!queue || queue.length === 0) return null
891
+
892
+ let fragment = queue.shift() ?? null
893
+ if (queue.length === 0) {
894
+ bufferedFrameTemplates.delete(id)
895
+ }
896
+
897
+ return fragment
898
+ }
899
+
900
+ function subscribeFrameTemplate(id: string, listener: FrameTemplateListener): () => void {
901
+ let listeners = frameTemplateListeners.get(id)
902
+ if (!listeners) {
903
+ listeners = new Set()
904
+ frameTemplateListeners.set(id, listeners)
905
+ }
906
+ listeners.add(listener)
907
+ return () => {
908
+ let current = frameTemplateListeners.get(id)
909
+ if (!current) return
910
+ current.delete(listener)
911
+ if (current.size === 0) {
912
+ frameTemplateListeners.delete(id)
913
+ }
914
+ }
915
+ }
916
+
917
+ type StreamTemplateParseResult = {
918
+ html: string
919
+ remainder: string
920
+ }
921
+
922
+ const COMPLETE_TEMPLATE_WITH_ID_PATTERN =
923
+ /<template\b[^>]*\bid=(?:"([^"]+)"|'([^']+)')[^>]*>[\s\S]*?<\/template>/gi
924
+
925
+ function extractTemplatesFromBuffer(
926
+ doc: Document,
927
+ buffer: string,
928
+ onTemplate: (id: string, fragment: DocumentFragment) => void,
929
+ ): StreamTemplateParseResult {
930
+ let html = ''
931
+ let cursor = 0
932
+ let hadMatch = false
933
+
934
+ COMPLETE_TEMPLATE_WITH_ID_PATTERN.lastIndex = 0
935
+ let match = COMPLETE_TEMPLATE_WITH_ID_PATTERN.exec(buffer)
936
+
937
+ while (match) {
938
+ hadMatch = true
939
+ let index = match.index
940
+ let fullMatch = match[0]
941
+ let id = match[1] ?? match[2]
942
+ let matchEnd = index + fullMatch.length
943
+
944
+ html += buffer.slice(cursor, index)
945
+
946
+ if (id) {
947
+ let parsed = createFragmentFromString(doc, fullMatch)
948
+ let template = parsed.querySelector('template')
949
+ if (template instanceof HTMLTemplateElement && template.id) {
950
+ onTemplate(template.id, template.content)
951
+ }
952
+ }
953
+
954
+ cursor = matchEnd
955
+ match = COMPLETE_TEMPLATE_WITH_ID_PATTERN.exec(buffer)
956
+ }
957
+
958
+ let tail = buffer.slice(cursor)
959
+ if (tail === '') return { html, remainder: '' }
960
+
961
+ let tailStart = tail.toLowerCase().lastIndexOf('<template')
962
+ if (tailStart === -1) {
963
+ return { html: html + tail, remainder: '' }
964
+ }
965
+
966
+ if (!hadMatch) {
967
+ return {
968
+ html: buffer.slice(0, tailStart),
969
+ remainder: buffer.slice(tailStart),
970
+ }
971
+ }
972
+
973
+ return {
974
+ html: html + tail.slice(0, tailStart),
975
+ remainder: tail.slice(tailStart),
976
+ }
977
+ }
978
+
979
+ async function renderFrameStream(
980
+ stream: ReadableStream<Uint8Array>,
981
+ doc: Document,
982
+ applyHtml: (html: string) => Promise<void>,
983
+ ): Promise<void> {
984
+ let reader = stream.getReader()
985
+ let decoder = new TextDecoder()
986
+ let buffer = ''
987
+ let html = ''
988
+ let appliedLength = 0
989
+ let appliedOnce = false
990
+
991
+ try {
992
+ while (true) {
993
+ let { done, value } = await reader.read()
994
+ if (done) break
995
+
996
+ buffer += decoder.decode(value, { stream: true })
997
+ let parsed = extractTemplatesFromBuffer(doc, buffer, publishFrameTemplate)
998
+ buffer = parsed.remainder
999
+
1000
+ if (parsed.html !== '') {
1001
+ html += parsed.html
1002
+ let htmlMarkers = collectHtmlMarkerSummary(html)
1003
+ if (!hasBalancedMarkerSummary(htmlMarkers)) {
1004
+ continue
1005
+ }
1006
+ await applyHtml(html)
1007
+ appliedLength = html.length
1008
+ appliedOnce = true
1009
+ }
1010
+ }
1011
+
1012
+ buffer += decoder.decode()
1013
+ let parsed = extractTemplatesFromBuffer(doc, buffer, publishFrameTemplate)
1014
+ html += parsed.html
1015
+ buffer = parsed.remainder
1016
+ if (buffer !== '') {
1017
+ html += buffer
1018
+ buffer = ''
1019
+ }
1020
+
1021
+ if (html !== '' && html.length > appliedLength) {
1022
+ await applyHtml(html)
1023
+ appliedOnce = true
1024
+ }
1025
+
1026
+ // A frame stream can legitimately resolve to empty content. Ensure the
1027
+ // existing frame region is cleared instead of treated as a no-op.
1028
+ if (html === '' && !appliedOnce) {
1029
+ await applyHtml('')
1030
+ }
1031
+ } finally {
1032
+ reader.releaseLock()
1033
+ }
1034
+ }
1035
+
1036
+ type FrameContainer = {
1037
+ doc: Document
1038
+ root: ParentNode
1039
+ childNodes: Node[]
1040
+ regionTailRef?: ChildNode | null
1041
+ regionParent?: ParentNode | null
1042
+ }
1043
+
1044
+ function createContainer(root: FrameRoot): FrameContainer {
1045
+ return Array.isArray(root) ? createCommentContainer(root) : createElementContainer(root)
1046
+ }
1047
+
1048
+ function createElementContainer(root: Document | Element | DocumentFragment): FrameContainer {
1049
+ let doc = root instanceof Document ? root : (root.ownerDocument ?? document)
1050
+ return {
1051
+ doc,
1052
+ root,
1053
+ get childNodes() {
1054
+ return Array.from(root.childNodes)
1055
+ },
1056
+ }
1057
+ }
1058
+
1059
+ function createCommentContainer([start, end]: [Comment, Comment]): FrameContainer {
1060
+ let parent = end.parentNode
1061
+ invariant(parent, 'Invalid comment container')
1062
+ invariant(start.parentNode === parent, 'Boundaries must share parent')
1063
+ let doc = parent.ownerDocument ?? document
1064
+
1065
+ let getChildNodesBetween = (): Node[] => {
1066
+ let nodes: Node[] = []
1067
+ let node = start.nextSibling
1068
+ while (node && node !== end) {
1069
+ nodes.push(node)
1070
+ node = node.nextSibling
1071
+ }
1072
+ return nodes
1073
+ }
1074
+
1075
+ return {
1076
+ doc,
1077
+ root: parent,
1078
+ get childNodes() {
1079
+ return getChildNodesBetween()
1080
+ },
1081
+ regionTailRef: end,
1082
+ regionParent: parent,
1083
+ }
1084
+ }
1085
+
1086
+ function createFragmentFromString(doc: Document, content: string): DocumentFragment {
1087
+ let template = doc.createElement('template')
1088
+ template.innerHTML = content.trim()
1089
+ return template.content
1090
+ }
1091
+
1092
+ function isRemixNodeFrameContent(content: InternalFrameContent): content is RemixNode {
1093
+ return !(
1094
+ content instanceof ReadableStream ||
1095
+ content instanceof DocumentFragment ||
1096
+ typeof content === 'string'
1097
+ )
1098
+ }
1099
+
1100
+ function isFullDocumentHtml(content: string): boolean {
1101
+ let trimmed = content.trimStart()
1102
+ return /^<!doctype html\b/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)
1103
+ }
1104
+
1105
+ type HydrationMarker = {
1106
+ id: string
1107
+ start: Comment
1108
+ end: Comment
1109
+ }
1110
+
1111
+ function findHydrationMarkers(container: FrameContainer): HydrationMarker[] {
1112
+ let results: HydrationMarker[] = []
1113
+
1114
+ forEachComment(container, (comment) => {
1115
+ let trimmed = comment.data.trim()
1116
+ if (!trimmed.startsWith('rmx:h:')) return
1117
+
1118
+ let id = trimmed.slice('rmx:h:'.length)
1119
+ let end = findEndMarker(comment, isHydrationStart, isHydrationEnd)
1120
+ results.push({ id, start: comment, end })
1121
+ })
1122
+
1123
+ return results
1124
+ }
1125
+
1126
+ function forEachComment(container: FrameContainer, cb: (comment: Comment) => void): void {
1127
+ walkCommentsInNodes(container.childNodes, cb)
1128
+ }
1129
+
1130
+ function walkCommentsInNodes(nodes: Node[], cb: (comment: Comment) => void): void {
1131
+ for (let i = 0; i < nodes.length; i++) {
1132
+ let node = nodes[i]
1133
+
1134
+ // Frame ownership boundary: hydration markers inside nested frame regions
1135
+ // are discovered and hydrated by the nested frame instance only.
1136
+ if (isFrameStart(node)) {
1137
+ let end = findEndMarker(node, isFrameStart, isFrameEnd)
1138
+ i = nodes.indexOf(end)
1139
+ continue
1140
+ }
1141
+
1142
+ if (node.nodeType === Node.COMMENT_NODE) cb(node as Comment)
1143
+ if (node.childNodes && node.childNodes.length > 0) {
1144
+ walkCommentsInNodes(Array.from(node.childNodes), cb)
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+ function isHydrationStart(node: Comment): boolean {
1150
+ return node.data.trim().startsWith('rmx:h:')
1151
+ }
1152
+
1153
+ function isHydrationEnd(node: Comment): boolean {
1154
+ return node.data.trim() === '/rmx:h'
1155
+ }
1156
+
1157
+ function isHydratedVirtualRootMarker(node: Node): node is VirtualRootMarker {
1158
+ return node instanceof Comment && '$rmx' in node
1159
+ }
1160
+
1161
+ function isFrameStart(node: Node): node is Comment {
1162
+ return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
1163
+ }
1164
+
1165
+ function isFrameEnd(node: Comment): boolean {
1166
+ return node.data.trim() === '/rmx:f'
1167
+ }
1168
+
1169
+ function getFrameId(start: Comment): string {
1170
+ let trimmed = start.data.trim()
1171
+ invariant(trimmed.startsWith('rmx:f:'), 'Invalid frame start marker')
1172
+ return trimmed.slice('rmx:f:'.length)
1173
+ }
1174
+
1175
+ function findEndMarker(
1176
+ start: Comment,
1177
+ isStart: (node: Comment) => boolean,
1178
+ isEnd: (node: Comment) => boolean,
1179
+ ): Comment {
1180
+ let node: Node | null = start.nextSibling
1181
+ let depth = 1
1182
+
1183
+ while (node) {
1184
+ if (node.nodeType === Node.COMMENT_NODE) {
1185
+ let comment = node as Comment
1186
+ if (isStart(comment)) depth++
1187
+ else if (isEnd(comment)) {
1188
+ depth--
1189
+ if (depth === 0) return comment
1190
+ }
1191
+ }
1192
+ node = node.nextSibling
1193
+ }
1194
+
1195
+ throw new Error('End marker not found')
1196
+ }
1197
+
1198
+ function collectHtmlMarkerSummary(html: string): Record<string, number> {
1199
+ return {
1200
+ frameStarts: html.match(/<!--\s*rmx:f:/g)?.length ?? 0,
1201
+ frameEnds: html.match(/<!--\s*\/rmx:f\s*-->/g)?.length ?? 0,
1202
+ hydrationStarts: html.match(/<!--\s*rmx:h:/g)?.length ?? 0,
1203
+ hydrationEnds: html.match(/<!--\s*\/rmx:h\s*-->/g)?.length ?? 0,
1204
+ }
1205
+ }
1206
+
1207
+ function hasBalancedMarkerSummary(summary: Record<string, number>): boolean {
1208
+ return (
1209
+ summary.frameStarts === summary.frameEnds && summary.hydrationStarts === summary.hydrationEnds
1210
+ )
1211
+ }