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