@remix-run/ui 0.0.0 → 0.1.0

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