@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,1347 @@
1
+ import type { ComponentHandle, FrameHandle, Key, RemixNode } from '../runtime/component.ts'
2
+ import type { ElementType, ElementProps, RemixElement } from '../runtime/jsx.ts'
3
+ import { Fragment, createComponent, createFrameHandle, Frame } from '../runtime/component.ts'
4
+ import { isEntry, type EntryComponent } from '../runtime/client-entries.ts'
5
+ import { normalizeSvgAttribute } from '../runtime/svg-attributes.ts'
6
+
7
+ interface VNode {
8
+ type: ElementType
9
+ props: ElementProps
10
+ key?: Key
11
+ _handle?: ComponentHandle
12
+ _parent?: VNode
13
+ }
14
+
15
+ export function createVNode(type: ElementType, props: ElementProps, key?: Key): VNode {
16
+ return { type, props, key }
17
+ }
18
+
19
+ /**
20
+ * Options for server-side rendering to a byte stream.
21
+ */
22
+ export interface RenderToStreamOptions {
23
+ /** Source URL to associate with the current frame render. */
24
+ frameSrc?: string | URL
25
+ /** Source URL for the top-level frame in nested frame renders. */
26
+ topFrameSrc?: string | URL
27
+ /** Error hook invoked when rendering work throws. */
28
+ onError?: (error: unknown) => void
29
+ /** Callback used to resolve nested frame content during streaming SSR. */
30
+ resolveFrame?: (
31
+ src: string,
32
+ target?: string,
33
+ context?: ResolveFrameContext,
34
+ ) => Promise<string | ReadableStream<Uint8Array>> | string | ReadableStream<Uint8Array>
35
+ /**
36
+ * Callback used to resolve runtime module metadata for client entry modules during SSR.
37
+ */
38
+ resolveClientEntry?: (
39
+ entryId: string,
40
+ component: EntryComponent,
41
+ ) => Promise<ResolvedClientEntry> | ResolvedClientEntry
42
+ }
43
+
44
+ /**
45
+ * Context passed to `resolveFrame` during server rendering.
46
+ */
47
+ export interface ResolveFrameContext {
48
+ /** Source URL for the frame currently being resolved. */
49
+ currentFrameSrc: string
50
+ /** Source URL for the top-level frame in the current render. */
51
+ topFrameSrc: string
52
+ }
53
+
54
+ interface HydrationData {
55
+ moduleUrl: string
56
+ exportName: string
57
+ props: Record<string, unknown>
58
+ }
59
+
60
+ interface UnresolvedHydrationData {
61
+ entryId: string
62
+ component: EntryComponent
63
+ props: Record<string, unknown>
64
+ }
65
+
66
+ interface ResolvedClientEntry {
67
+ href: string
68
+ exportName: string
69
+ }
70
+
71
+ interface FrameData {
72
+ status: 'pending' | 'resolved'
73
+ name?: string
74
+ src: string
75
+ }
76
+
77
+ interface RenderContext {
78
+ insideSvg: boolean
79
+ onError: (error: unknown) => void
80
+ parentVNode?: VNode
81
+ styleCache: Map<string, { selector: string; css: string }>
82
+ emittedStyles: Set<string>
83
+ resolveFrame: (
84
+ src: string,
85
+ target?: string,
86
+ context?: ResolveFrameContext,
87
+ ) => Promise<string | ReadableStream<Uint8Array>> | string | ReadableStream<Uint8Array>
88
+ pendingFrames: Array<{ frameId: string; promise: Promise<ResolvedFrameHtml> }>
89
+ hydrationData: Map<string, HydrationData>
90
+ unresolvedHydrationData: Map<string, UnresolvedHydrationData>
91
+ frameData: Map<string, FrameData>
92
+ blockingFrameTails: ReadableStream<Uint8Array>[]
93
+ serverIdScope: string
94
+ serverIdCounter: number
95
+ }
96
+
97
+ interface ResolvedFrameHtml {
98
+ html: string
99
+ tail?: ReadableStream<Uint8Array>
100
+ }
101
+
102
+ interface SsrFrameState {
103
+ frame: FrameHandle
104
+ topFrame: FrameHandle
105
+ }
106
+
107
+ type Segment =
108
+ | { kind: 'static'; html: string }
109
+ | { kind: 'composite'; parts: Segment[] }
110
+ | {
111
+ kind: 'frame'
112
+ frameId: string
113
+ content: Segment | null
114
+ pending?: Promise<void>
115
+ }
116
+
117
+ const SELF_CLOSING_TAGS = new Set([
118
+ 'area',
119
+ 'base',
120
+ 'br',
121
+ 'col',
122
+ 'embed',
123
+ 'hr',
124
+ 'img',
125
+ 'input',
126
+ 'link',
127
+ 'meta',
128
+ 'param',
129
+ 'source',
130
+ 'track',
131
+ 'wbr',
132
+ ])
133
+
134
+ const DEFAULT_STYLE_LAYER = 'rmx'
135
+ const DOCTYPE_PATTERN = /<!doctype(?:\s[^>]*)?>/gi
136
+
137
+ function stripDoctypeMarkup(html: string): string {
138
+ return html.replace(DOCTYPE_PATTERN, '')
139
+ }
140
+
141
+ function getStyleLayerName(selector: string, layer: string = DEFAULT_STYLE_LAYER): string {
142
+ return `${layer}.${selector}`
143
+ }
144
+
145
+ const NUMERIC_CSS_PROPS = new Set([
146
+ 'z-index',
147
+ 'opacity',
148
+ 'flex-grow',
149
+ 'flex-shrink',
150
+ 'flex-order',
151
+ 'grid-area',
152
+ 'grid-row',
153
+ 'grid-column',
154
+ 'font-weight',
155
+ 'line-height',
156
+ 'order',
157
+ 'orphans',
158
+ 'widows',
159
+ 'zoom',
160
+ 'columns',
161
+ 'column-count',
162
+ ])
163
+
164
+ const FRAMEWORK_PROPS = new Set(['children', 'innerHTML', 'on', 'key', 'mix'])
165
+
166
+ const ssrSignal = Object.freeze({
167
+ get aborted() {
168
+ return false
169
+ },
170
+ get reason() {
171
+ return undefined
172
+ },
173
+ get onabort() {
174
+ return null
175
+ },
176
+ set onabort(_: AbortSignal['onabort']) {},
177
+ addEventListener(
178
+ _type: string,
179
+ _listener: EventListenerOrEventListenerObject | null,
180
+ _options?: AddEventListenerOptions | boolean,
181
+ ) {},
182
+ removeEventListener(
183
+ _type: string,
184
+ _listener: EventListenerOrEventListenerObject | null,
185
+ _options?: EventListenerOptions | boolean,
186
+ ) {},
187
+ dispatchEvent(_event: Event) {
188
+ return true
189
+ },
190
+ throwIfAborted() {},
191
+ }) as AbortSignal
192
+
193
+ /**
194
+ * Renders a node tree to a streaming HTML response body.
195
+ *
196
+ * @param node Node tree to render.
197
+ * @param options Stream rendering options.
198
+ * @returns A readable byte stream of HTML.
199
+ */
200
+ export function renderToStream(
201
+ node: RemixNode,
202
+ options?: RenderToStreamOptions,
203
+ ): ReadableStream<Uint8Array> {
204
+ let encoder = new TextEncoder()
205
+ let onError = options?.onError ?? ((error) => console.error(error))
206
+ let currentFrameSrc = normalizeFrameSrc(options?.frameSrc ?? options?.topFrameSrc)
207
+ let topFrameSrc = normalizeFrameSrc(options?.topFrameSrc ?? currentFrameSrc)
208
+ let rootFrameState = createSsrFrameState(currentFrameSrc, topFrameSrc)
209
+
210
+ let context: RenderContext = {
211
+ insideSvg: false,
212
+ onError,
213
+ resolveFrame: options?.resolveFrame ?? defaultResolveFrame,
214
+ styleCache: new Map(),
215
+ emittedStyles: new Set(),
216
+ pendingFrames: [],
217
+ hydrationData: new Map(),
218
+ unresolvedHydrationData: new Map(),
219
+ frameData: new Map(),
220
+ blockingFrameTails: [],
221
+ serverIdScope: crypto.randomUUID().slice(0, 8),
222
+ serverIdCounter: 0,
223
+ }
224
+
225
+ return new ReadableStream({
226
+ async start(controller) {
227
+ try {
228
+ let root = buildSegment(node, context, rootFrameState)
229
+ await resolveBlocking(root)
230
+ await resolveClientEntries(context, options?.resolveClientEntry)
231
+ validateClientEntriesForHydration(context)
232
+ let html = serializeSegment(root)
233
+ let finalHtml = finalizeHtml(html, context)
234
+ let bytes = encoder.encode(finalHtml)
235
+ controller.enqueue(bytes)
236
+
237
+ // If we have any tails from blocking frame streams, stream them now.
238
+ // These contain nested non-blocking frame templates (or other follow-up chunks)
239
+ // that must come after the initial document chunk.
240
+ let tailPromise =
241
+ context.blockingFrameTails.length > 0
242
+ ? streamByteStreams(context.blockingFrameTails, controller, context.onError)
243
+ : Promise.resolve()
244
+
245
+ // If we have pending non-blocking frames, stream them as they resolve
246
+ let pendingPromise =
247
+ context.pendingFrames.length > 0
248
+ ? streamPendingFrames(context, controller, encoder)
249
+ : Promise.resolve()
250
+
251
+ await Promise.all([tailPromise, pendingPromise])
252
+
253
+ controller.close()
254
+ } catch (error) {
255
+ onError(error)
256
+ controller.error(error)
257
+ }
258
+ },
259
+ })
260
+ }
261
+
262
+ function defaultResolveFrame(): never {
263
+ throw new Error('No resolveFrame provided')
264
+ }
265
+
266
+ function normalizeFrameSrc(value?: string | URL): string {
267
+ return value == null ? '' : String(value)
268
+ }
269
+
270
+ function createSsrFrameState(frameSrc: string, topFrameSrc = frameSrc): SsrFrameState {
271
+ let topFrame = createFrameHandle({ src: topFrameSrc })
272
+ let frame = frameSrc === topFrameSrc ? topFrame : createFrameHandle({ src: frameSrc })
273
+ return { frame, topFrame }
274
+ }
275
+
276
+ function getResolveFrameContext(frameState: SsrFrameState): ResolveFrameContext {
277
+ return {
278
+ currentFrameSrc: frameState.frame.src,
279
+ topFrameSrc: frameState.topFrame.src,
280
+ }
281
+ }
282
+
283
+ function randomId(prefix: string): string {
284
+ return prefix + crypto.randomUUID().slice(0, 8)
285
+ }
286
+
287
+ function createServerComponentId(context: RenderContext): string {
288
+ context.serverIdCounter++
289
+ return `s${context.serverIdScope}-${context.serverIdCounter}`
290
+ }
291
+
292
+ async function splitFirstChunk(
293
+ stream: ReadableStream<Uint8Array>,
294
+ ): Promise<{ first: Uint8Array; tail: ReadableStream<Uint8Array> }> {
295
+ let reader = stream.getReader()
296
+
297
+ let { value, done } = await reader.read()
298
+ if (done || !value) {
299
+ reader.releaseLock()
300
+ return {
301
+ first: new Uint8Array(),
302
+ tail: new ReadableStream({
303
+ start(controller) {
304
+ controller.close()
305
+ },
306
+ }),
307
+ }
308
+ }
309
+
310
+ let released = false
311
+ function release() {
312
+ if (released) return
313
+ released = true
314
+ try {
315
+ reader.releaseLock()
316
+ } catch {
317
+ // ignore
318
+ }
319
+ }
320
+
321
+ let tail = new ReadableStream<Uint8Array>({
322
+ async pull(controller) {
323
+ let next = await reader.read()
324
+ if (next.done) {
325
+ controller.close()
326
+ release()
327
+ return
328
+ }
329
+ controller.enqueue(next.value)
330
+ },
331
+ cancel(reason) {
332
+ release()
333
+ return reader.cancel(reason)
334
+ },
335
+ })
336
+
337
+ return { first: value, tail }
338
+ }
339
+
340
+ async function resolveFrameHtml(
341
+ input: string | ReadableStream<Uint8Array>,
342
+ ): Promise<ResolvedFrameHtml> {
343
+ if (typeof input === 'string') return { html: stripDoctypeMarkup(input) }
344
+
345
+ let decoder = new TextDecoder()
346
+ let { first, tail } = await splitFirstChunk(input)
347
+ return { html: stripDoctypeMarkup(decoder.decode(first)), tail }
348
+ }
349
+
350
+ function isRemixElement(node: unknown): node is RemixElement {
351
+ return typeof node === 'object' && node !== null && '$rmx' in node
352
+ }
353
+
354
+ function staticSeg(html: string): Segment {
355
+ return { kind: 'static', html }
356
+ }
357
+
358
+ function compositeSeg(parts: Segment[]): Segment {
359
+ return { kind: 'composite', parts }
360
+ }
361
+
362
+ function buildSegment(node: RemixNode, context: RenderContext, frameState: SsrFrameState): Segment {
363
+ if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
364
+ return staticSeg(escapeTextContent(String(node)))
365
+ }
366
+
367
+ if (node === null || node === undefined || typeof node === 'boolean') {
368
+ return staticSeg('')
369
+ }
370
+
371
+ if (Array.isArray(node)) {
372
+ return compositeSeg(node.map((child) => buildSegment(child, context, frameState)))
373
+ }
374
+
375
+ if (isRemixElement(node)) {
376
+ let type = node.type
377
+ let props = node.props
378
+
379
+ if (type === Fragment) {
380
+ let children = props.children
381
+ return children != null ? buildSegment(children, context, frameState) : staticSeg('')
382
+ }
383
+
384
+ if (typeof type === 'string') {
385
+ let tag = type
386
+
387
+ if (tag === 'html') {
388
+ return buildElementSegment(tag, props, context, frameState)
389
+ }
390
+
391
+ if (tag === 'head') {
392
+ return buildHeadElementSegment(tag, props, context, frameState)
393
+ }
394
+
395
+ return buildElementSegment(tag, props, context, frameState)
396
+ }
397
+
398
+ if (typeof type === 'function') {
399
+ if (type === Frame) {
400
+ return buildFrameSegment(props, context, frameState)
401
+ }
402
+ if (isEntry(type)) {
403
+ return buildEntrySegment(type, props, context, frameState)
404
+ }
405
+ return buildComponentSegment(
406
+ type,
407
+ props,
408
+ context,
409
+ createServerComponentId(context),
410
+ frameState,
411
+ )
412
+ }
413
+ }
414
+
415
+ return staticSeg('')
416
+ }
417
+
418
+ function buildFrameSegment(props: any, context: RenderContext, frameState: SsrFrameState): Segment {
419
+ let frameId = randomId('f')
420
+
421
+ // Store frame data in context for aggregation
422
+ context.frameData.set(frameId, {
423
+ status: props.fallback ? 'pending' : 'resolved',
424
+ name: props.name,
425
+ src: props.src,
426
+ })
427
+
428
+ let seg: Segment = {
429
+ kind: 'frame',
430
+ frameId,
431
+ content: null,
432
+ }
433
+
434
+ let resolveFrameContext = getResolveFrameContext(frameState)
435
+ let nonBlocking = !!props.fallback
436
+ if (nonBlocking) {
437
+ seg.content = buildSegment(props.fallback, context, frameState)
438
+ let framePromise = Promise.resolve(
439
+ context.resolveFrame(props.src, props.name, resolveFrameContext),
440
+ ).then(async (resolved) => resolveFrameHtml(resolved))
441
+ context.pendingFrames.push({ frameId, promise: framePromise })
442
+ } else {
443
+ seg.pending = Promise.resolve(
444
+ context.resolveFrame(props.src, props.name, resolveFrameContext),
445
+ ).then(async (resolved) => {
446
+ let { html, tail } = await resolveFrameHtml(resolved)
447
+ seg.content = staticSeg(html)
448
+ if (tail) {
449
+ context.blockingFrameTails.push(tail)
450
+ }
451
+ })
452
+ }
453
+
454
+ return seg
455
+ }
456
+
457
+ function buildElementSegment(
458
+ tag: string,
459
+ props: any,
460
+ context: RenderContext,
461
+ frameState: SsrFrameState,
462
+ ): Segment {
463
+ let mixedProps = resolveSsrMixedProps(tag, props, context, frameState)
464
+ let processedProps = processStyleProps(mixedProps)
465
+ // Determine namespace context for the current element and its children
466
+ let currentIsSvg = context.insideSvg || tag === 'svg'
467
+ let attrs = renderAttributes(processedProps, currentIsSvg)
468
+
469
+ if (SELF_CLOSING_TAGS.has(tag)) {
470
+ return staticSeg(`<${tag}${attrs} />`)
471
+ }
472
+
473
+ if (props.innerHTML) {
474
+ return staticSeg(`<${tag}${attrs}>${props.innerHTML}</${tag}>`)
475
+ }
476
+
477
+ let open = staticSeg(`<${tag}${attrs}>`)
478
+ // Adjust svg context for children: foreignObject switches back to HTML
479
+ let previousInsideSvg = context.insideSvg
480
+ context.insideSvg = tag === 'foreignObject' ? false : currentIsSvg
481
+ let children =
482
+ props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('')
483
+ context.insideSvg = previousInsideSvg
484
+ let close = staticSeg(`</${tag}>`)
485
+ return compositeSeg([open, children, close])
486
+ }
487
+
488
+ function buildHeadElementSegment(
489
+ tag: string,
490
+ props: any,
491
+ context: RenderContext,
492
+ frameState: SsrFrameState,
493
+ ): Segment {
494
+ let processedProps = processStyleProps(props)
495
+ let attrs = renderAttributes(processedProps, false)
496
+
497
+ let open = staticSeg(`<${tag}${attrs}>`)
498
+ let children =
499
+ props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('')
500
+ let close = staticSeg(`</${tag}>`)
501
+
502
+ return compositeSeg([open, children, close])
503
+ }
504
+
505
+ function renderAttributes(props: any, isSvg: boolean): string {
506
+ let attrs = ''
507
+
508
+ for (let key in props) {
509
+ if (FRAMEWORK_PROPS.has(key)) continue
510
+
511
+ let value = props[key]
512
+ if (value === undefined || value === null || value === false) continue
513
+
514
+ let attrName = transformAttributeName(key, isSvg)
515
+
516
+ if (value === true) {
517
+ attrs += ` ${attrName}`
518
+ } else {
519
+ attrs += ` ${attrName}="${escapeHtml(String(value))}"`
520
+ }
521
+ }
522
+
523
+ return attrs
524
+ }
525
+
526
+ function resolveSsrMixedProps(
527
+ hostType: string,
528
+ initialProps: ElementProps,
529
+ context: RenderContext,
530
+ frameState: SsrFrameState,
531
+ ): ElementProps {
532
+ let descriptors = resolveSsrMixDescriptors(initialProps)
533
+ if (descriptors.length === 0) return initialProps
534
+
535
+ let composedProps = withoutSsrMix(initialProps)
536
+ let mixinProps = withoutSsrMixinTreeProps(composedProps)
537
+ let maxDescriptors = 1024
538
+
539
+ for (let index = 0; index < descriptors.length && index < maxDescriptors; index++) {
540
+ let descriptor = descriptors[index]
541
+ let runner = resolveSsrMixinRunner(hostType, descriptor, context, frameState)
542
+ if (!runner) continue
543
+
544
+ let result: unknown
545
+ try {
546
+ result = runner(...descriptor.args, mixinProps)
547
+ } catch (error) {
548
+ console.error(error)
549
+ continue
550
+ }
551
+
552
+ if (!result) continue
553
+ if (isSsrMixinElement(result)) continue
554
+
555
+ let returnedDescriptors = resolveReturnedSsrMixDescriptors(result)
556
+ if (returnedDescriptors) {
557
+ for (let returned of returnedDescriptors) descriptors.push(returned)
558
+ continue
559
+ }
560
+
561
+ if (!isRemixElement(result)) {
562
+ console.error(new Error('mixins must return a remix element'))
563
+ continue
564
+ }
565
+ let remixResult = result
566
+
567
+ let resultType =
568
+ typeof remixResult.type === 'string'
569
+ ? remixResult.type
570
+ : isSsrMixinElement(remixResult.type)
571
+ ? remixResult.type.__rmxMixinElementType
572
+ : null
573
+
574
+ if (resultType !== hostType) {
575
+ console.error(new Error('mixins must return an element with the same host type'))
576
+ continue
577
+ }
578
+
579
+ if (remixResult.type !== resultType) {
580
+ remixResult = { ...remixResult, type: resultType }
581
+ }
582
+
583
+ let nextProps = sanitizeReturnedSsrMixinProps(remixResult.props as ElementProps)
584
+ let nestedDescriptors = resolveSsrMixDescriptors(nextProps)
585
+ for (let nested of nestedDescriptors) descriptors.push(nested)
586
+ composedProps = { ...composedProps, ...withoutSsrMix(nextProps) }
587
+ mixinProps = withoutSsrMixinTreeProps(composedProps)
588
+ }
589
+
590
+ let nextMix = initialProps.mix
591
+ return {
592
+ ...composedProps,
593
+ ...(nextMix === undefined ? {} : { mix: nextMix }),
594
+ }
595
+ }
596
+
597
+ function resolveSsrMixinRunner(
598
+ hostType: string,
599
+ descriptor: { type?: unknown; args?: unknown[] },
600
+ context: RenderContext,
601
+ frameState: SsrFrameState,
602
+ ): ((...args: unknown[]) => unknown) | null {
603
+ if (typeof descriptor.type !== 'function') return null
604
+ try {
605
+ let handle = createSsrMixinHandle(hostType, descriptor, context, frameState)
606
+ let runner = descriptor.type(handle, hostType)
607
+ if (typeof runner !== 'function') return null
608
+ return runner
609
+ } catch (error) {
610
+ console.error(error)
611
+ return null
612
+ }
613
+ }
614
+
615
+ function createSsrMixinHandle(
616
+ hostType: string,
617
+ _descriptor: { type?: unknown },
618
+ context: RenderContext,
619
+ frameState: SsrFrameState,
620
+ ) {
621
+ let element = ((handle: { props: ElementProps; update(): Promise<AbortSignal> }) => () => ({
622
+ $rmx: true as const,
623
+ type: hostType,
624
+ key: null,
625
+ props: handle.props,
626
+ })) as ((handle: {
627
+ props: ElementProps
628
+ update(): Promise<AbortSignal>
629
+ }) => () => RemixElement) & {
630
+ __rmxMixinElementType: string
631
+ }
632
+ element.__rmxMixinElementType = hostType
633
+
634
+ return {
635
+ id: 'ssr-mixin',
636
+ context: {
637
+ get(providerType: ElementType | symbol) {
638
+ if (typeof providerType !== 'function') {
639
+ return undefined
640
+ }
641
+
642
+ let current = context.parentVNode
643
+ while (current) {
644
+ if (current.type === providerType) {
645
+ let providerHandle = current._handle
646
+ if (providerHandle) {
647
+ return providerHandle.getContextValue()
648
+ }
649
+ }
650
+ current = current._parent
651
+ }
652
+
653
+ return undefined
654
+ },
655
+ },
656
+ frame: createFrameHandle({
657
+ src: frameState.frame.src,
658
+ $runtime: {
659
+ styleCache: context.styleCache,
660
+ },
661
+ }),
662
+ element,
663
+ signal: ssrSignal,
664
+ update: () => {
665
+ throw new Error('handle.update() is not available during SSR.')
666
+ },
667
+ queueTask: () => {},
668
+ on: () => {},
669
+ addEventListener: () => {},
670
+ removeEventListener: () => {},
671
+ dispatchEvent: () => true,
672
+ }
673
+ }
674
+
675
+ function resolveSsrMixDescriptors(props: ElementProps): Array<{ type: any; args: unknown[] }> {
676
+ let mix = props.mix
677
+ if (!mix) return []
678
+ if (Array.isArray(mix)) {
679
+ if (mix.length === 0) return []
680
+ return mix.filter(Boolean) as Array<{ type: any; args: unknown[] }>
681
+ }
682
+ return [mix] as Array<{ type: any; args: unknown[] }>
683
+ }
684
+
685
+ function withoutSsrMix(props: ElementProps): ElementProps {
686
+ if (!('mix' in props)) return props
687
+ let output = { ...props }
688
+ delete output.mix
689
+ return output
690
+ }
691
+
692
+ function withoutSsrMixinTreeProps(props: ElementProps): ElementProps {
693
+ if (!('children' in props) && !('innerHTML' in props)) return props
694
+ let output = { ...props }
695
+ delete output.children
696
+ delete output.innerHTML
697
+ return output
698
+ }
699
+
700
+ function sanitizeReturnedSsrMixinProps(props: ElementProps): ElementProps {
701
+ if (!('children' in props) && !('innerHTML' in props)) return props
702
+ console.error(new Error('mixins must not return children or innerHTML'))
703
+ return withoutSsrMixinTreeProps(props)
704
+ }
705
+
706
+ function resolveReturnedSsrMixDescriptors(
707
+ value: unknown,
708
+ ): Array<{ type: Function; args: unknown[] }> | null {
709
+ let descriptors: Array<{ type: Function; args: unknown[] }> = []
710
+ if (!collectReturnedSsrMixDescriptors(value, descriptors)) {
711
+ return null
712
+ }
713
+
714
+ return descriptors
715
+ }
716
+
717
+ function collectReturnedSsrMixDescriptors(
718
+ value: unknown,
719
+ output: Array<{ type: Function; args: unknown[] }>,
720
+ ): boolean {
721
+ if (!value) {
722
+ return true
723
+ }
724
+
725
+ if (Array.isArray(value)) {
726
+ for (let item of value) {
727
+ if (!collectReturnedSsrMixDescriptors(item, output)) {
728
+ return false
729
+ }
730
+ }
731
+ return true
732
+ }
733
+
734
+ if (!isSsrMixinDescriptor(value)) {
735
+ return false
736
+ }
737
+
738
+ output.push(value)
739
+ return true
740
+ }
741
+
742
+ function isSsrMixinElement(
743
+ value: unknown,
744
+ ): value is ((...args: unknown[]) => unknown) & { __rmxMixinElementType: string } {
745
+ if (typeof value !== 'function') return false
746
+ return '__rmxMixinElementType' in value
747
+ }
748
+
749
+ function isSsrMixinDescriptor(value: unknown): value is { type: Function; args: unknown[] } {
750
+ if (!value || typeof value !== 'object' || isRemixElement(value)) {
751
+ return false
752
+ }
753
+
754
+ let descriptor = value as { type?: unknown; args?: unknown }
755
+ return typeof descriptor.type === 'function' && Array.isArray(descriptor.args)
756
+ }
757
+
758
+ function buildComponentSegment(
759
+ type: Function,
760
+ props: any,
761
+ context: RenderContext,
762
+ componentId: string,
763
+ frameState: SsrFrameState,
764
+ ): Segment {
765
+ let vnode = createVNode(type, props)
766
+ if (context.parentVNode) {
767
+ vnode._parent = context.parentVNode
768
+ }
769
+
770
+ let handle = createComponent({
771
+ id: componentId,
772
+ type: type,
773
+ frame: frameState.frame,
774
+ signal: ssrSignal,
775
+ getContext(providerType) {
776
+ let current = vnode._parent
777
+ while (current) {
778
+ if (current.type === providerType) {
779
+ let providerHandle = current._handle
780
+ // TODO: need better vnode types to avoid defensive checks
781
+ if (providerHandle) {
782
+ return providerHandle.getContextValue()
783
+ }
784
+ }
785
+ current = current._parent
786
+ }
787
+ return undefined
788
+ },
789
+ getFrameByName() {
790
+ return undefined
791
+ },
792
+ getTopFrame() {
793
+ return frameState.topFrame
794
+ },
795
+ })
796
+
797
+ vnode._handle = handle
798
+ let [renderedNode] = handle.render(props)
799
+ let childContext = { ...context, parentVNode: vnode }
800
+
801
+ return buildSegment(renderedNode, childContext, frameState)
802
+ }
803
+
804
+ function createHydrationPropsReplacer(context: RenderContext, frameState: SsrFrameState) {
805
+ function unwrapNode(node: RemixNode): unknown {
806
+ if (node === null || node === undefined || typeof node === 'boolean') return node
807
+ if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
808
+ return node
809
+ }
810
+ if (Array.isArray(node)) {
811
+ return node.map((child) => unwrapNode(child))
812
+ }
813
+ if (isRemixElement(node)) {
814
+ return unwrapElement(node)
815
+ }
816
+ return node
817
+ }
818
+
819
+ function unwrapElement(element: RemixElement): unknown {
820
+ let type = element.type
821
+ let props = element.props
822
+
823
+ // Preserve Frame semantics through serialized props by emitting
824
+ // a dedicated descriptor that can be revived on the client.
825
+ if (type === Frame) {
826
+ return {
827
+ $rmxFrame: true,
828
+ props: transformProps(props),
829
+ key: element.key,
830
+ }
831
+ }
832
+
833
+ // If it's a DOM tag, return a serializable shape with transformed props
834
+ if (typeof type === 'string') {
835
+ return { $rmx: true, type, props: transformProps(props) }
836
+ }
837
+
838
+ // Component function: render synchronously, then unwrap its result
839
+ if (typeof type === 'function') {
840
+ let vnode = createVNode(type, props)
841
+ if (context.parentVNode) {
842
+ vnode._parent = context.parentVNode
843
+ }
844
+
845
+ let handle = createComponent({
846
+ id: 'SERIALIZED',
847
+ type: type,
848
+ frame: frameState.frame,
849
+ signal: ssrSignal,
850
+ getContext(providerType) {
851
+ let current = vnode._parent
852
+ while (current) {
853
+ if (current.type === providerType) {
854
+ let providerHandle = current._handle
855
+ if (providerHandle) {
856
+ return providerHandle.getContextValue()
857
+ }
858
+ }
859
+ current = current._parent
860
+ }
861
+ return undefined
862
+ },
863
+ getFrameByName() {
864
+ return undefined
865
+ },
866
+ getTopFrame() {
867
+ return frameState.topFrame
868
+ },
869
+ })
870
+
871
+ vnode._handle = handle
872
+ let [renderedNode] = handle.render(props)
873
+ return unwrapNode(renderedNode)
874
+ }
875
+
876
+ return null
877
+ }
878
+
879
+ function transformProps(input: ElementProps): Record<string, unknown> {
880
+ let out: Record<string, unknown> = {}
881
+ for (let key in input) {
882
+ let value = input[key]
883
+ if (key === 'children') {
884
+ out[key] = unwrapNode(value)
885
+ } else {
886
+ if (isRemixElement(value)) {
887
+ out[key] = unwrapNode(value)
888
+ } else if (Array.isArray(value)) {
889
+ out[key] = value.map((v) => unwrapNode(v))
890
+ } else {
891
+ out[key] = value
892
+ }
893
+ }
894
+ }
895
+ return out
896
+ }
897
+
898
+ return function replacer(_key: string, value: unknown) {
899
+ if (isRemixElement(value)) {
900
+ return unwrapElement(value)
901
+ }
902
+ if (Array.isArray(value)) {
903
+ return value.map((v) => unwrapNode(v))
904
+ }
905
+ return value
906
+ }
907
+ }
908
+
909
+ function buildEntrySegment(
910
+ type: EntryComponent,
911
+ props: any,
912
+ context: RenderContext,
913
+ frameState: SsrFrameState,
914
+ ): Segment {
915
+ let instanceId = randomId('h')
916
+ let rendered = buildComponentSegment(type, props, context, instanceId, frameState)
917
+
918
+ // Store hydration data in context for aggregation
919
+ let replacer = createHydrationPropsReplacer(context, frameState)
920
+ context.unresolvedHydrationData.set(instanceId, {
921
+ entryId: type.$entryId,
922
+ component: type,
923
+ props: JSON.parse(JSON.stringify(props, replacer)),
924
+ })
925
+
926
+ let start = staticSeg(`<!-- rmx:h:${instanceId} -->`)
927
+ let end = staticSeg('<!-- /rmx:h -->')
928
+ return compositeSeg([start, rendered, end])
929
+ }
930
+
931
+ function resolveDefaultClientEntry(
932
+ entryId: string,
933
+ component: EntryComponent,
934
+ ): ResolvedClientEntry {
935
+ let fallbackExportName = component.name || ''
936
+ let hashIndex = entryId.lastIndexOf('#')
937
+ if (hashIndex === -1 && fallbackExportName) {
938
+ return {
939
+ exportName: fallbackExportName,
940
+ href: entryId,
941
+ }
942
+ }
943
+
944
+ if (hashIndex !== -1) {
945
+ let exportName = entryId.slice(hashIndex + 1) || fallbackExportName
946
+ if (exportName) {
947
+ return {
948
+ exportName,
949
+ href: entryId.slice(0, hashIndex),
950
+ }
951
+ }
952
+ }
953
+
954
+ throw new Error(
955
+ `clientEntry() requires either an export name in the entry ID (e.g., "/js/module.js#ComponentName"), a named component function, or a resolveClientEntry hook that resolves one. Received "${entryId}".`,
956
+ )
957
+ }
958
+
959
+ async function resolveClientEntries(
960
+ context: RenderContext,
961
+ resolveClientEntry?: (
962
+ entryId: string,
963
+ component: EntryComponent,
964
+ ) => Promise<ResolvedClientEntry> | ResolvedClientEntry,
965
+ ): Promise<void> {
966
+ if (context.unresolvedHydrationData.size === 0) return
967
+
968
+ let resolvedEntries = new Map<string, ResolvedClientEntry>()
969
+
970
+ for (let [hydrationId, unresolvedHydrationData] of context.unresolvedHydrationData) {
971
+ let { entryId, component, props } = unresolvedHydrationData
972
+ let resolvedEntry = resolvedEntries.get(entryId)
973
+ if (!resolvedEntry) {
974
+ resolvedEntry = resolveClientEntry
975
+ ? await Promise.resolve(resolveClientEntry(entryId, component))
976
+ : resolveDefaultClientEntry(entryId, component)
977
+ validateResolvedClientEntry(entryId, resolvedEntry)
978
+ resolvedEntries.set(entryId, resolvedEntry)
979
+ }
980
+
981
+ context.hydrationData.set(hydrationId, {
982
+ exportName: resolvedEntry.exportName,
983
+ moduleUrl: resolvedEntry.href,
984
+ props,
985
+ })
986
+ }
987
+
988
+ context.unresolvedHydrationData.clear()
989
+ }
990
+
991
+ function validateResolvedClientEntry(
992
+ entryId: string,
993
+ resolvedEntry: ResolvedClientEntry,
994
+ ): asserts resolvedEntry is ResolvedClientEntry {
995
+ if (!resolvedEntry || typeof resolvedEntry !== 'object') {
996
+ throw new Error(
997
+ `resolveClientEntry must return an object with href and exportName. Received "${entryId}".`,
998
+ )
999
+ }
1000
+
1001
+ if (!resolvedEntry.href) {
1002
+ throw new Error(`resolveClientEntry must return a non-empty href. Received "${entryId}".`)
1003
+ }
1004
+
1005
+ if (!resolvedEntry.exportName) {
1006
+ throw new Error(`resolveClientEntry must return a non-empty exportName. Received "${entryId}".`)
1007
+ }
1008
+ }
1009
+
1010
+ function validateClientEntriesForHydration(context: RenderContext): void {
1011
+ if (context.unresolvedHydrationData.size > 0) {
1012
+ let [hydrationId, unresolvedHydrationData] = context.unresolvedHydrationData.entries().next()
1013
+ .value as [string, UnresolvedHydrationData]
1014
+ throw new Error(
1015
+ `Client entry was not resolved for hydration. Received "${unresolvedHydrationData.entryId}" (${hydrationId}).`,
1016
+ )
1017
+ }
1018
+ }
1019
+
1020
+ // Resolve all blocking frame content once
1021
+ async function resolveBlocking(segment: Segment): Promise<void> {
1022
+ if (segment.kind === 'frame') {
1023
+ if (segment.pending) {
1024
+ await segment.pending
1025
+ segment.pending = undefined
1026
+ }
1027
+ if (segment.content) await resolveBlocking(segment.content)
1028
+ return
1029
+ }
1030
+ if (segment.kind === 'composite') {
1031
+ for (let part of segment.parts) {
1032
+ await resolveBlocking(part)
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ // Serialize the segment tree to HTML
1038
+ function serializeSegment(seg: Segment): string {
1039
+ if (seg.kind === 'static') return seg.html
1040
+ if (seg.kind === 'composite') return seg.parts.map(serializeSegment).join('')
1041
+ // frame
1042
+ let inner = seg.content ? serializeSegment(seg.content) : ''
1043
+ let start = `<!-- rmx:f:${seg.frameId} -->`
1044
+ let end = `<!-- /rmx:f -->`
1045
+ return start + inner + end
1046
+ }
1047
+
1048
+ function escapeHtml(str: string): string {
1049
+ return str
1050
+ .replace(/&/g, '&amp;')
1051
+ .replace(/</g, '&lt;')
1052
+ .replace(/>/g, '&gt;')
1053
+ .replace(/"/g, '&quot;')
1054
+ .replace(/'/g, '&#39;')
1055
+ }
1056
+
1057
+ function escapeTextContent(str: string): string {
1058
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1059
+ }
1060
+
1061
+ function escapeTemplateContent(html: string): string {
1062
+ return html.replace(/<\/template/gi, '<\\/template')
1063
+ }
1064
+
1065
+ function transformAttributeName(name: string, isSvg: boolean): string {
1066
+ // aria-/data- pass through
1067
+ if (name.startsWith('aria-') || name.startsWith('data-')) return name
1068
+
1069
+ // HTML mappings
1070
+ if (name === 'className') return 'class'
1071
+ if (!isSvg) {
1072
+ if (name === 'htmlFor') return 'for'
1073
+ if (name === 'tabIndex') return 'tabindex'
1074
+ if (name === 'acceptCharset') return 'accept-charset'
1075
+ if (name === 'httpEquiv') return 'http-equiv'
1076
+ return name.toLowerCase()
1077
+ }
1078
+
1079
+ return normalizeSvgAttribute(name).attr
1080
+ }
1081
+
1082
+ function finalizeHtml(html: string, context: RenderContext): string {
1083
+ let hasHtmlRoot = html.trimStart().toLowerCase().startsWith('<html')
1084
+
1085
+ let styles = collectStyleTags(context)
1086
+ if (styles) {
1087
+ let headContent = styles
1088
+ if (hasHtmlRoot) {
1089
+ // For HTML root, inject into existing head or create one
1090
+ let headCloseIndex = html.indexOf('</head>')
1091
+ if (headCloseIndex !== -1) {
1092
+ // Inject before existing </head>
1093
+ html = html.slice(0, headCloseIndex) + headContent + html.slice(headCloseIndex)
1094
+ } else {
1095
+ // No existing head, inject after <html>
1096
+ let htmlOpenMatch = html.match(/<html[^>]*>/)
1097
+ if (htmlOpenMatch) {
1098
+ let insertIndex = htmlOpenMatch.index! + htmlOpenMatch[0].length
1099
+ html =
1100
+ html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex)
1101
+ }
1102
+ }
1103
+ } else {
1104
+ // No HTML root, prepend head
1105
+ html = `<head>${headContent}</head>${html}`
1106
+ }
1107
+ }
1108
+
1109
+ html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
1110
+
1111
+ // Append aggregated hydration/frame data script at the end
1112
+ let rmxData = buildRmxDataScript(context)
1113
+ if (rmxData) {
1114
+ if (hasHtmlRoot) {
1115
+ // Insert before </body> if present, otherwise before </html>
1116
+ let bodyCloseIndex = html.indexOf('</body>')
1117
+ if (bodyCloseIndex !== -1) {
1118
+ html = html.slice(0, bodyCloseIndex) + rmxData + html.slice(bodyCloseIndex)
1119
+ } else {
1120
+ let htmlCloseIndex = html.indexOf('</html>')
1121
+ if (htmlCloseIndex !== -1) {
1122
+ html = html.slice(0, htmlCloseIndex) + rmxData + html.slice(htmlCloseIndex)
1123
+ } else {
1124
+ html += rmxData
1125
+ }
1126
+ }
1127
+ } else {
1128
+ html += rmxData
1129
+ }
1130
+ }
1131
+
1132
+ return html
1133
+ }
1134
+
1135
+ function processStyleProps(props: any): any {
1136
+ let processedProps = { ...props }
1137
+ let classAttr = typeof props.class === 'string' ? props.class : ''
1138
+ let className = typeof props.className === 'string' ? props.className : ''
1139
+ let mergedClassName = [classAttr, className].filter(Boolean).join(' ')
1140
+
1141
+ if (mergedClassName) {
1142
+ processedProps.className = mergedClassName
1143
+ delete processedProps.class
1144
+ }
1145
+
1146
+ if (typeof props.style === 'object') {
1147
+ processedProps.style = serializeStyleObject(props.style)
1148
+ }
1149
+
1150
+ return processedProps
1151
+ }
1152
+
1153
+ function collectStyleTags(context: RenderContext): string {
1154
+ if (context.styleCache.size === 0) return ''
1155
+
1156
+ let tags: string[] = []
1157
+ for (let { selector, css } of context.styleCache.values()) {
1158
+ let tag = renderStyleTag(selector, css)
1159
+ if (tag) tags.push(tag)
1160
+ }
1161
+ return tags.join('')
1162
+ }
1163
+
1164
+ function wrapStyleForLayer(
1165
+ selector: string,
1166
+ css: string,
1167
+ layer: string = DEFAULT_STYLE_LAYER,
1168
+ ): string {
1169
+ let trimmed = css.trim()
1170
+ if (!trimmed) return ''
1171
+ return `@layer ${getStyleLayerName(selector, layer)} { ${trimmed} }`
1172
+ }
1173
+
1174
+ function renderStyleTag(
1175
+ selector: string,
1176
+ css: string,
1177
+ layer: string = DEFAULT_STYLE_LAYER,
1178
+ ): string {
1179
+ let wrappedCss = wrapStyleForLayer(selector, css, layer)
1180
+ if (!wrappedCss) return ''
1181
+ return `<style data-rmx="${escapeHtml(selector)}">${wrappedCss}</style>`
1182
+ }
1183
+
1184
+ function readStyleTagAttribute(attrs: string, name: string): string | null {
1185
+ let match = attrs.match(new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`))
1186
+ if (!match) return null
1187
+ return match[1] ?? match[2] ?? null
1188
+ }
1189
+
1190
+ function dedupeServerStyleTagsInHtml(html: string, seenStyles: Set<string>): string {
1191
+ return html.replace(/<style\b([^>]*)>[\s\S]*?<\/style>/gi, (match, attrs) => {
1192
+ let selector = readStyleTagAttribute(attrs, 'data-rmx')
1193
+ if (!selector) return match
1194
+
1195
+ if (seenStyles.has(selector)) return ''
1196
+ seenStyles.add(selector)
1197
+ return match
1198
+ })
1199
+ }
1200
+
1201
+ function buildRmxDataScript(context: RenderContext): string {
1202
+ if (context.hydrationData.size === 0 && context.frameData.size === 0) {
1203
+ return ''
1204
+ }
1205
+
1206
+ let data: {
1207
+ h?: Record<string, HydrationData>
1208
+ f?: Record<string, FrameData>
1209
+ } = {}
1210
+
1211
+ if (context.hydrationData.size > 0) {
1212
+ data.h = Object.fromEntries(context.hydrationData)
1213
+ }
1214
+
1215
+ if (context.frameData.size > 0) {
1216
+ data.f = Object.fromEntries(context.frameData)
1217
+ }
1218
+
1219
+ let serializedData = escapeScriptJson(JSON.stringify(data))
1220
+ return `<script type="application/json" id="rmx-data">${serializedData}</script>`
1221
+ }
1222
+
1223
+ function escapeScriptJson(json: string): string {
1224
+ // Avoid prematurely closing the script tag when serialized data contains "</script>".
1225
+ return json.replace(/</g, '\\u003c')
1226
+ }
1227
+
1228
+ function serializeStyleObject(style: Record<string, any>): string {
1229
+ let parts: string[] = []
1230
+
1231
+ for (let [key, value] of Object.entries(style)) {
1232
+ if (value == null) continue
1233
+ if (typeof value === 'boolean') continue
1234
+ if (typeof value === 'number' && !Number.isFinite(value)) continue
1235
+
1236
+ // Convert camelCase to kebab-case
1237
+ let cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
1238
+
1239
+ // Add px to numeric values where appropriate
1240
+ let shouldAppendPx =
1241
+ typeof value === 'number' &&
1242
+ value !== 0 &&
1243
+ !NUMERIC_CSS_PROPS.has(cssKey) &&
1244
+ !cssKey.startsWith('--')
1245
+
1246
+ let cssValue = shouldAppendPx
1247
+ ? `${value}px`
1248
+ : Array.isArray(value)
1249
+ ? value.join(', ')
1250
+ : String(value)
1251
+
1252
+ parts.push(`${cssKey}: ${cssValue};`)
1253
+ }
1254
+
1255
+ return parts.join(' ')
1256
+ }
1257
+
1258
+ // Frame styles work end-to-end when frame handlers use their own `renderToStream`:
1259
+ // the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
1260
+ // the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
1261
+ // document and adopts the CSS into an adopted stylesheet.
1262
+ async function streamPendingFrames(
1263
+ context: RenderContext,
1264
+ controller: ReadableStreamDefaultController,
1265
+ encoder: TextEncoder,
1266
+ ): Promise<void> {
1267
+ let processedFrames = new Set<string>()
1268
+
1269
+ while (true) {
1270
+ let batch = context.pendingFrames.filter(({ frameId }) => !processedFrames.has(frameId))
1271
+ if (batch.length === 0) break
1272
+
1273
+ await Promise.all(
1274
+ batch.map(async ({ frameId, promise }) => {
1275
+ processedFrames.add(frameId)
1276
+ try {
1277
+ let { html, tail } = await promise
1278
+ html = dedupeServerStyleTagsInHtml(html, context.emittedStyles)
1279
+
1280
+ // Stream as a template element (first chunk only)
1281
+ let templateHtml = `<template id="${frameId}">${escapeTemplateContent(html)}</template>`
1282
+ controller.enqueue(encoder.encode(templateHtml))
1283
+
1284
+ // Forward any additional chunks from a stream-valued resolveFrame result.
1285
+ if (tail) {
1286
+ await streamByteStreams([tail], controller, context.onError)
1287
+ }
1288
+ } catch (error) {
1289
+ context.onError(error)
1290
+ }
1291
+ }),
1292
+ )
1293
+ }
1294
+ }
1295
+
1296
+ async function streamByteStreams(
1297
+ streams: ReadableStream<Uint8Array>[],
1298
+ controller: ReadableStreamDefaultController,
1299
+ onError: (error: unknown) => void,
1300
+ ): Promise<void> {
1301
+ await Promise.all(
1302
+ streams.map(async (stream) => {
1303
+ let reader = stream.getReader()
1304
+ try {
1305
+ while (true) {
1306
+ let { done, value } = await reader.read()
1307
+ if (done) break
1308
+ controller.enqueue(value)
1309
+ }
1310
+ } catch (error) {
1311
+ onError(error)
1312
+ } finally {
1313
+ reader.releaseLock()
1314
+ }
1315
+ }),
1316
+ )
1317
+ }
1318
+
1319
+ async function drain(stream: ReadableStream<Uint8Array>): Promise<string> {
1320
+ let reader = stream.getReader()
1321
+ let decoder = new TextDecoder()
1322
+ let html = ''
1323
+
1324
+ while (true) {
1325
+ let { done, value } = await reader.read()
1326
+ if (done) break
1327
+ html += decoder.decode(value)
1328
+ }
1329
+
1330
+ return html
1331
+ }
1332
+
1333
+ /**
1334
+ * Renders a node tree to a complete HTML string.
1335
+ *
1336
+ * @param node Node tree to render.
1337
+ * @returns Rendered HTML.
1338
+ */
1339
+ export async function renderToString(node: RemixNode): Promise<string> {
1340
+ return drain(
1341
+ renderToStream(node, {
1342
+ onError(error) {
1343
+ throw error
1344
+ },
1345
+ }),
1346
+ )
1347
+ }