@tanstack/redact 0.0.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 (233) hide show
  1. package/dist/core/index.d.ts +2 -0
  2. package/dist/core/index.js +4 -0
  3. package/dist/core/index.js.map +7 -0
  4. package/dist/core/internal.d.ts +76 -0
  5. package/dist/core/internal.js +59 -0
  6. package/dist/core/internal.js.map +7 -0
  7. package/dist/core/types.d.ts +21 -0
  8. package/dist/core/types.js +10 -0
  9. package/dist/core/types.js.map +7 -0
  10. package/dist/dom/_all.d.ts +17 -0
  11. package/dist/dom/_all.js +51 -0
  12. package/dist/dom/_all.js.map +7 -0
  13. package/dist/dom/client.d.ts +3 -0
  14. package/dist/dom/client.js +8 -0
  15. package/dist/dom/client.js.map +7 -0
  16. package/dist/dom/dispatcher.d.ts +20 -0
  17. package/dist/dom/dispatcher.js +267 -0
  18. package/dist/dom/dispatcher.js.map +7 -0
  19. package/dist/dom/dom.d.ts +10 -0
  20. package/dist/dom/dom.js +262 -0
  21. package/dist/dom/dom.js.map +7 -0
  22. package/dist/dom/event-replay.d.ts +13 -0
  23. package/dist/dom/event-replay.js +46 -0
  24. package/dist/dom/event-replay.js.map +7 -0
  25. package/dist/dom/features/class/full.d.ts +1 -0
  26. package/dist/dom/features/class/full.js +91 -0
  27. package/dist/dom/features/class/full.js.map +7 -0
  28. package/dist/dom/features/class/index.d.ts +1 -0
  29. package/dist/dom/features/class/index.js +3 -0
  30. package/dist/dom/features/class/index.js.map +7 -0
  31. package/dist/dom/features/class/stub.d.ts +1 -0
  32. package/dist/dom/features/class/stub.js +50 -0
  33. package/dist/dom/features/class/stub.js.map +7 -0
  34. package/dist/dom/features/context/full.d.ts +1 -0
  35. package/dist/dom/features/context/full.js +49 -0
  36. package/dist/dom/features/context/full.js.map +7 -0
  37. package/dist/dom/features/context/index.d.ts +1 -0
  38. package/dist/dom/features/context/index.js +3 -0
  39. package/dist/dom/features/context/index.js.map +7 -0
  40. package/dist/dom/features/context/stub.d.ts +1 -0
  41. package/dist/dom/features/context/stub.js +22 -0
  42. package/dist/dom/features/context/stub.js.map +7 -0
  43. package/dist/dom/features/forward-ref/full.d.ts +1 -0
  44. package/dist/dom/features/forward-ref/full.js +51 -0
  45. package/dist/dom/features/forward-ref/full.js.map +7 -0
  46. package/dist/dom/features/forward-ref/index.d.ts +1 -0
  47. package/dist/dom/features/forward-ref/index.js +3 -0
  48. package/dist/dom/features/forward-ref/index.js.map +7 -0
  49. package/dist/dom/features/forward-ref/stub.d.ts +1 -0
  50. package/dist/dom/features/forward-ref/stub.js +22 -0
  51. package/dist/dom/features/forward-ref/stub.js.map +7 -0
  52. package/dist/dom/features/hydration/full.d.ts +61 -0
  53. package/dist/dom/features/hydration/full.js +264 -0
  54. package/dist/dom/features/hydration/full.js.map +7 -0
  55. package/dist/dom/features/hydration/index.d.ts +1 -0
  56. package/dist/dom/features/hydration/index.js +3 -0
  57. package/dist/dom/features/hydration/index.js.map +7 -0
  58. package/dist/dom/features/hydration/stub.d.ts +28 -0
  59. package/dist/dom/features/hydration/stub.js +66 -0
  60. package/dist/dom/features/hydration/stub.js.map +7 -0
  61. package/dist/dom/features/index.d.ts +7 -0
  62. package/dist/dom/features/index.js +9 -0
  63. package/dist/dom/features/index.js.map +7 -0
  64. package/dist/dom/features/lazy/full.d.ts +1 -0
  65. package/dist/dom/features/lazy/full.js +78 -0
  66. package/dist/dom/features/lazy/full.js.map +7 -0
  67. package/dist/dom/features/lazy/index.d.ts +1 -0
  68. package/dist/dom/features/lazy/index.js +3 -0
  69. package/dist/dom/features/lazy/index.js.map +7 -0
  70. package/dist/dom/features/lazy/stub.d.ts +1 -0
  71. package/dist/dom/features/lazy/stub.js +36 -0
  72. package/dist/dom/features/lazy/stub.js.map +7 -0
  73. package/dist/dom/features/memo/full.d.ts +1 -0
  74. package/dist/dom/features/memo/full.js +59 -0
  75. package/dist/dom/features/memo/full.js.map +7 -0
  76. package/dist/dom/features/memo/index.d.ts +1 -0
  77. package/dist/dom/features/memo/index.js +3 -0
  78. package/dist/dom/features/memo/index.js.map +7 -0
  79. package/dist/dom/features/memo/stub.d.ts +1 -0
  80. package/dist/dom/features/memo/stub.js +36 -0
  81. package/dist/dom/features/memo/stub.js.map +7 -0
  82. package/dist/dom/features/portal/full.d.ts +1 -0
  83. package/dist/dom/features/portal/full.js +19 -0
  84. package/dist/dom/features/portal/full.js.map +7 -0
  85. package/dist/dom/features/portal/index.d.ts +1 -0
  86. package/dist/dom/features/portal/index.js +3 -0
  87. package/dist/dom/features/portal/index.js.map +7 -0
  88. package/dist/dom/features/portal/stub.d.ts +1 -0
  89. package/dist/dom/features/portal/stub.js +7 -0
  90. package/dist/dom/features/portal/stub.js.map +7 -0
  91. package/dist/dom/features/suspense/full.d.ts +1 -0
  92. package/dist/dom/features/suspense/full.js +146 -0
  93. package/dist/dom/features/suspense/full.js.map +7 -0
  94. package/dist/dom/features/suspense/index.d.ts +1 -0
  95. package/dist/dom/features/suspense/index.js +3 -0
  96. package/dist/dom/features/suspense/index.js.map +7 -0
  97. package/dist/dom/features/suspense/stub.d.ts +1 -0
  98. package/dist/dom/features/suspense/stub.js +6 -0
  99. package/dist/dom/features/suspense/stub.js.map +7 -0
  100. package/dist/dom/index.d.ts +25 -0
  101. package/dist/dom/index.js +45 -0
  102. package/dist/dom/index.js.map +7 -0
  103. package/dist/dom/portal.d.ts +2 -0
  104. package/dist/dom/portal.js +15 -0
  105. package/dist/dom/portal.js.map +7 -0
  106. package/dist/dom/reconcile.d.ts +40 -0
  107. package/dist/dom/reconcile.js +795 -0
  108. package/dist/dom/reconcile.js.map +7 -0
  109. package/dist/dom/root.d.ts +15 -0
  110. package/dist/dom/root.js +85 -0
  111. package/dist/dom/root.js.map +7 -0
  112. package/dist/dom/test-utils.d.ts +1 -0
  113. package/dist/dom/test-utils.js +10 -0
  114. package/dist/dom/test-utils.js.map +7 -0
  115. package/dist/react/children.d.ts +8 -0
  116. package/dist/react/children.js +75 -0
  117. package/dist/react/children.js.map +7 -0
  118. package/dist/react/class.d.ts +31 -0
  119. package/dist/react/class.js +40 -0
  120. package/dist/react/class.js.map +7 -0
  121. package/dist/react/context.d.ts +27 -0
  122. package/dist/react/context.js +31 -0
  123. package/dist/react/context.js.map +7 -0
  124. package/dist/react/element.d.ts +10 -0
  125. package/dist/react/element.js +64 -0
  126. package/dist/react/element.js.map +7 -0
  127. package/dist/react/hooks.d.ts +37 -0
  128. package/dist/react/hooks.js +146 -0
  129. package/dist/react/hooks.js.map +7 -0
  130. package/dist/react/index.d.ts +76 -0
  131. package/dist/react/index.js +187 -0
  132. package/dist/react/index.js.map +7 -0
  133. package/dist/react/jsx-runtime.d.ts +5 -0
  134. package/dist/react/jsx-runtime.js +21 -0
  135. package/dist/react/jsx-runtime.js.map +7 -0
  136. package/dist/react/memo.d.ts +29 -0
  137. package/dist/react/memo.js +47 -0
  138. package/dist/react/memo.js.map +7 -0
  139. package/dist/react/portal.d.ts +1 -0
  140. package/dist/react/portal.js +6 -0
  141. package/dist/react/portal.js.map +7 -0
  142. package/dist/react/shared-internals.d.ts +33 -0
  143. package/dist/react/shared-internals.js +29 -0
  144. package/dist/react/shared-internals.js.map +7 -0
  145. package/dist/react/suspense.d.ts +15 -0
  146. package/dist/react/suspense.js +16 -0
  147. package/dist/react/suspense.js.map +7 -0
  148. package/dist/scheduler/index.d.ts +44 -0
  149. package/dist/scheduler/index.js +109 -0
  150. package/dist/scheduler/index.js.map +7 -0
  151. package/dist/server/bootstrap-script.d.ts +22 -0
  152. package/dist/server/bootstrap-script.js +16 -0
  153. package/dist/server/bootstrap-script.js.map +7 -0
  154. package/dist/server/dispatcher.d.ts +37 -0
  155. package/dist/server/dispatcher.js +140 -0
  156. package/dist/server/dispatcher.js.map +7 -0
  157. package/dist/server/escape.d.ts +7 -0
  158. package/dist/server/escape.js +159 -0
  159. package/dist/server/escape.js.map +7 -0
  160. package/dist/server/index.d.ts +16 -0
  161. package/dist/server/index.js +26 -0
  162. package/dist/server/index.js.map +7 -0
  163. package/dist/server/renderToString.d.ts +7 -0
  164. package/dist/server/renderToString.js +29 -0
  165. package/dist/server/renderToString.js.map +7 -0
  166. package/dist/server/stream.d.ts +40 -0
  167. package/dist/server/stream.js +200 -0
  168. package/dist/server/stream.js.map +7 -0
  169. package/dist/server/walk.d.ts +33 -0
  170. package/dist/server/walk.js +298 -0
  171. package/dist/server/walk.js.map +7 -0
  172. package/dist/vite/index.d.ts +96 -0
  173. package/dist/vite/index.js +217 -0
  174. package/dist/vite/index.js.map +7 -0
  175. package/package.json +83 -0
  176. package/src/core/index.ts +2 -0
  177. package/src/core/internal.ts +107 -0
  178. package/src/core/types.ts +36 -0
  179. package/src/dom/_all.ts +54 -0
  180. package/src/dom/client.ts +4 -0
  181. package/src/dom/dispatcher.ts +318 -0
  182. package/src/dom/dom.ts +316 -0
  183. package/src/dom/event-replay.ts +55 -0
  184. package/src/dom/features/class/full.ts +104 -0
  185. package/src/dom/features/class/index.ts +1 -0
  186. package/src/dom/features/class/stub.ts +65 -0
  187. package/src/dom/features/context/full.ts +57 -0
  188. package/src/dom/features/context/index.ts +1 -0
  189. package/src/dom/features/context/stub.ts +32 -0
  190. package/src/dom/features/forward-ref/full.ts +54 -0
  191. package/src/dom/features/forward-ref/index.ts +1 -0
  192. package/src/dom/features/forward-ref/stub.ts +28 -0
  193. package/src/dom/features/hydration/full.ts +345 -0
  194. package/src/dom/features/hydration/index.ts +1 -0
  195. package/src/dom/features/hydration/stub.ts +72 -0
  196. package/src/dom/features/index.ts +11 -0
  197. package/src/dom/features/lazy/full.ts +98 -0
  198. package/src/dom/features/lazy/index.ts +1 -0
  199. package/src/dom/features/lazy/stub.ts +45 -0
  200. package/src/dom/features/memo/full.ts +74 -0
  201. package/src/dom/features/memo/index.ts +1 -0
  202. package/src/dom/features/memo/stub.ts +42 -0
  203. package/src/dom/features/portal/full.ts +22 -0
  204. package/src/dom/features/portal/index.ts +1 -0
  205. package/src/dom/features/portal/stub.ts +11 -0
  206. package/src/dom/features/suspense/full.ts +191 -0
  207. package/src/dom/features/suspense/index.ts +1 -0
  208. package/src/dom/features/suspense/stub.ts +12 -0
  209. package/src/dom/index.ts +33 -0
  210. package/src/dom/portal.ts +16 -0
  211. package/src/dom/reconcile.ts +1169 -0
  212. package/src/dom/root.ts +107 -0
  213. package/src/dom/test-utils.ts +6 -0
  214. package/src/react/children.ts +85 -0
  215. package/src/react/class.ts +56 -0
  216. package/src/react/context.ts +50 -0
  217. package/src/react/element.ts +80 -0
  218. package/src/react/hooks.ts +190 -0
  219. package/src/react/index.ts +132 -0
  220. package/src/react/jsx-runtime.ts +16 -0
  221. package/src/react/memo.ts +45 -0
  222. package/src/react/portal.ts +1 -0
  223. package/src/react/shared-internals.ts +79 -0
  224. package/src/react/suspense.ts +18 -0
  225. package/src/scheduler/index.ts +124 -0
  226. package/src/server/bootstrap-script.ts +52 -0
  227. package/src/server/dispatcher.ts +158 -0
  228. package/src/server/escape.ts +187 -0
  229. package/src/server/index.ts +26 -0
  230. package/src/server/renderToString.ts +33 -0
  231. package/src/server/stream.ts +302 -0
  232. package/src/server/walk.ts +448 -0
  233. package/src/vite/index.ts +401 -0
@@ -0,0 +1,448 @@
1
+ import {
2
+ REACT_ELEMENT_TYPE,
3
+ REACT_LEGACY_ELEMENT_TYPE,
4
+ REACT_FRAGMENT_TYPE,
5
+ type ReactNode,
6
+ type ReactElement,
7
+ } from '../core'
8
+ import {
9
+ REACT_SUSPENSE_TYPE,
10
+ REACT_PROVIDER_TYPE,
11
+ REACT_CONSUMER_TYPE,
12
+ REACT_FORWARD_REF_TYPE,
13
+ REACT_MEMO_TYPE,
14
+ REACT_LAZY_TYPE,
15
+ REACT_STRICT_MODE_TYPE,
16
+ REACT_PROFILER_TYPE,
17
+ REACT_PORTAL_TYPE,
18
+ } from '../react'
19
+ import {
20
+ attrToHtml,
21
+ escapeText,
22
+ escapeScript,
23
+ VOID_ELEMENTS,
24
+ RAW_TEXT_ELEMENTS,
25
+ } from './escape'
26
+ import {
27
+ pushContext,
28
+ popContext,
29
+ snapshotContexts,
30
+ type ContextSnapshot,
31
+ } from './dispatcher'
32
+
33
+ export interface SuspendedBoundary {
34
+ id: number
35
+ fallbackHTML: string
36
+ children: ReactNode
37
+ thenable: Promise<any>
38
+ contextSnapshot: ContextSnapshot
39
+ }
40
+
41
+ export interface WalkOptions {
42
+ emit: (chunk: string) => void
43
+ onSuspend?: (boundary: SuspendedBoundary) => void
44
+ nextBoundaryId: () => number
45
+ bootstrapped?: boolean
46
+ isBoundaryResolution?: boolean
47
+ /**
48
+ * Tracks whether the most recent emission within the *current text flow*
49
+ * ended with a text node. When the next emission is also text, we emit a
50
+ * `<!-- -->` separator so the browser's HTML parser doesn't merge them
51
+ * into a single text node — required for hydration to line up text
52
+ * boundaries with the React tree. Reset to `false` whenever we enter a
53
+ * new host element (`<tag>` opens a fresh text flow).
54
+ */
55
+ textState?: { lastWasText: boolean }
56
+ }
57
+
58
+ /**
59
+ * Synchronously walk a React node and emit HTML string pieces to `opts.emit`.
60
+ * Suspended boundaries are emitted as fallbacks with marker IDs; if `opts.onSuspend`
61
+ * is provided, the suspension is recorded for later streaming.
62
+ */
63
+ export function walk(node: ReactNode, opts: WalkOptions): void {
64
+ // Seed a text state if the caller didn't provide one so the separator logic
65
+ // is active for the whole tree.
66
+ if (!opts.textState) opts = { ...opts, textState: { lastWasText: false } }
67
+ walkNode(node, opts)
68
+ }
69
+
70
+ // --- <select> selection context ----------------------------------------------
71
+ // Stack of active select values (or `undefined` when the current select has
72
+ // no controlled value). `<option>` walks read the top of stack to decide
73
+ // whether to emit `selected=""`.
74
+ const selectValueStack: unknown[] = []
75
+ function pushSelectContext(value: unknown): void {
76
+ selectValueStack.push(value)
77
+ }
78
+ function popSelectContext(): void {
79
+ selectValueStack.pop()
80
+ }
81
+ function currentSelectValue(): unknown {
82
+ return selectValueStack.length ? selectValueStack[selectValueStack.length - 1] : undefined
83
+ }
84
+
85
+ function optionChildText(children: unknown): string {
86
+ // `<option>Text</option>` — if no `value` prop, the option's value is its
87
+ // flat string/number child content. Matches DOM semantics (`option.value`
88
+ // defaults to `textContent` when no attribute is set).
89
+ if (children == null) return ''
90
+ if (typeof children === 'string' || typeof children === 'number') return '' + children
91
+ if (Array.isArray(children)) return children.map(optionChildText).join('')
92
+ return ''
93
+ }
94
+
95
+ function emitText(text: string, opts: WalkOptions): void {
96
+ // Empty string renders no text node and doesn't start/extend a text flow —
97
+ // skip entirely so sibling text isn't separated by a stray `<!-- -->`.
98
+ if (text === '') return
99
+ if (opts.textState?.lastWasText) opts.emit('<!-- -->')
100
+ opts.emit(escapeText(text))
101
+ if (opts.textState) opts.textState.lastWasText = true
102
+ }
103
+
104
+ function walkNode(node: ReactNode, opts: WalkOptions): void {
105
+ if (node == null || node === false || node === true) return
106
+
107
+ if (typeof node === 'string') {
108
+ emitText(node, opts)
109
+ return
110
+ }
111
+ if (typeof node === 'number') {
112
+ emitText(String(node), opts)
113
+ return
114
+ }
115
+ if (Array.isArray(node)) {
116
+ for (const c of node) walkNode(c, opts)
117
+ return
118
+ }
119
+ if (typeof (node as any)[Symbol.iterator] === 'function') {
120
+ for (const item of node as Iterable<ReactNode>) walkNode(item, opts)
121
+ return
122
+ }
123
+
124
+ if (typeof node !== 'object') return
125
+ const t = (node as any).$$typeof
126
+
127
+ // Raw React.lazy in the tree (RSC Flight encodes 'use client' components —
128
+ // CodeBlock, CodeExplorer, etc. — as bare Lazy objects directly in the
129
+ // tree, not wrapped in REACT_ELEMENT_TYPE). SSR previously dropped these
130
+ // here, so code snippets never made it into the server HTML. The RSC
131
+ // decoder server-side awaits payloads before rendering, so status is
132
+ // 'fulfilled' and `_init()` returns the resolved element synchronously.
133
+ // If still pending (shouldn't happen post-awaitLazyElements), throw the
134
+ // thenable so streaming SSR suspends the current boundary and retries.
135
+ if (t === REACT_LAZY_TYPE) {
136
+ const lazy = node as any
137
+ const resolved = lazy._init(lazy._payload)
138
+ walkNode(resolved, opts)
139
+ return
140
+ }
141
+
142
+ if (t !== REACT_ELEMENT_TYPE && t !== REACT_LEGACY_ELEMENT_TYPE) return
143
+
144
+ const el = node as ReactElement
145
+ walkElement(el, opts)
146
+ }
147
+
148
+ function walkElement(el: ReactElement, opts: WalkOptions): void {
149
+ const type = el.type
150
+ const props = el.props ?? {}
151
+
152
+ if (type === REACT_FRAGMENT_TYPE || type === REACT_STRICT_MODE_TYPE || type === REACT_PROFILER_TYPE) {
153
+ walkNode(props.children, opts)
154
+ return
155
+ }
156
+
157
+ if (type === REACT_SUSPENSE_TYPE) {
158
+ walkSuspense(props, opts)
159
+ return
160
+ }
161
+
162
+ if (typeof type === 'string') {
163
+ walkHost(type, props, opts)
164
+ return
165
+ }
166
+
167
+ const marker = (type as any)?.$$typeof
168
+
169
+ if (marker === REACT_PORTAL_TYPE) {
170
+ // Portals don't render to the main HTML output on the server.
171
+ return
172
+ }
173
+
174
+ if (marker === REACT_PROVIDER_TYPE) {
175
+ const ctx = (type as any)._context
176
+ pushContext(ctx, props.value)
177
+ try {
178
+ walkNode(props.children, opts)
179
+ } finally {
180
+ popContext(ctx)
181
+ }
182
+ return
183
+ }
184
+
185
+ if (marker === REACT_CONSUMER_TYPE) {
186
+ const ctx = (type as any)._context
187
+ const render = props.children
188
+ if (typeof render === 'function') {
189
+ walkNode(render(ctx._currentValue), opts)
190
+ }
191
+ return
192
+ }
193
+
194
+ if (marker === REACT_FORWARD_REF_TYPE) {
195
+ const render = (type as any).render
196
+ const ref = (props as any).ref ?? null
197
+ const { ref: _omit, ...rest } = props as any
198
+ const rendered = render(rest, ref)
199
+ walkNode(rendered, opts)
200
+ return
201
+ }
202
+
203
+ if (marker === REACT_MEMO_TYPE) {
204
+ const inner = (type as any).type
205
+ walkElement({ ...el, type: inner } as ReactElement, opts)
206
+ return
207
+ }
208
+
209
+ if (marker === REACT_LAZY_TYPE) {
210
+ const { _payload, _init } = type as any
211
+ try {
212
+ const resolved = _init(_payload)
213
+ walkElement({ ...el, type: resolved } as ReactElement, opts)
214
+ } catch (thenable: any) {
215
+ if (isThenable(thenable)) {
216
+ // Suspend this point
217
+ throw thenable
218
+ }
219
+ throw thenable
220
+ }
221
+ return
222
+ }
223
+
224
+ if (typeof type === 'function') {
225
+ walkComponent(type, props, opts)
226
+ return
227
+ }
228
+ }
229
+
230
+ function walkHost(
231
+ tag: string,
232
+ props: Record<string, any>,
233
+ opts: WalkOptions,
234
+ ): void {
235
+ // <textarea value="..."> serializes its value as a TEXT CHILD, not an
236
+ // attribute. `defaultValue` is the fallback when `value` is absent. This
237
+ // matches React and the HTML spec — `<textarea value="x">` is not valid
238
+ // HTML; the value is the element's textContent.
239
+ const isTextarea = tag === 'textarea'
240
+ const textareaValue = isTextarea
241
+ ? props.value != null
242
+ ? props.value
243
+ : props.defaultValue
244
+ : undefined
245
+
246
+ // <input defaultValue="..."> should parse with that value — emit it as a
247
+ // `value` attribute. Similarly `defaultChecked` becomes `checked`. This
248
+ // keeps hydration consistent: the browser parser sees the initial value,
249
+ // and on client commit our setProp seeds `.defaultValue`/`.defaultChecked`
250
+ // without stomping the user-typed value.
251
+ const isInput = tag === 'input'
252
+ const inputValueAttr =
253
+ isInput && props.value == null && props.defaultValue != null
254
+ ? props.defaultValue
255
+ : undefined
256
+ const inputCheckedAttr =
257
+ isInput && props.checked == null && props.defaultChecked != null
258
+ ? props.defaultChecked
259
+ : undefined
260
+
261
+ // <select value="..."> does NOT become an attribute on `<select>` — the
262
+ // HTML spec has no such attribute. React resolves the selection by stamping
263
+ // `selected` on the matching `<option>` children during render. Stash the
264
+ // target value(s) on the walk state and the child `<option>` walk reads it.
265
+ const isSelect = tag === 'select'
266
+ if (isSelect) {
267
+ const val = props.value != null ? props.value : props.defaultValue
268
+ pushSelectContext(val)
269
+ }
270
+ const isOption = tag === 'option'
271
+
272
+ // Prepend the HTML5 doctype to the stream when rendering an <html> root.
273
+ // Without it the browser parses the document in quirks mode, which breaks
274
+ // CSS sizing (documentElement.clientHeight returns the content height, not
275
+ // the viewport) — and Floating-UI-based libraries (Radix dropdowns etc.)
276
+ // then compute off-screen positions for overlays.
277
+ if (tag === 'html') opts.emit('<!DOCTYPE html>')
278
+
279
+ opts.emit('<' + tag)
280
+ for (const k in props) {
281
+ if (isTextarea && (k === 'value' || k === 'defaultValue')) continue
282
+ if (isInput && (k === 'defaultValue' || k === 'defaultChecked')) continue
283
+ if (isSelect && (k === 'value' || k === 'defaultValue')) continue
284
+ if (isOption && k === 'selected') continue
285
+ opts.emit(attrToHtml(k, props[k]))
286
+ }
287
+ if (inputValueAttr !== undefined) {
288
+ opts.emit(attrToHtml('value', inputValueAttr))
289
+ }
290
+ if (inputCheckedAttr !== undefined) {
291
+ opts.emit(attrToHtml('checked', inputCheckedAttr))
292
+ }
293
+ if (isOption) {
294
+ const selectVal = currentSelectValue()
295
+ if (selectVal !== undefined) {
296
+ const optionValue =
297
+ props.value != null ? props.value : optionChildText(props.children)
298
+ const matches = Array.isArray(selectVal)
299
+ ? selectVal.some((v) => '' + v === '' + optionValue)
300
+ : '' + selectVal === '' + optionValue
301
+ if (matches) opts.emit(' selected=""')
302
+ } else if (props.selected) {
303
+ opts.emit(' selected=""')
304
+ }
305
+ }
306
+
307
+ if (VOID_ELEMENTS.has(tag)) {
308
+ opts.emit('/>')
309
+ if (opts.textState) opts.textState.lastWasText = false
310
+ return
311
+ }
312
+ opts.emit('>')
313
+ // Opening a host element starts a fresh text flow context for its children.
314
+ // Children's text separator tracking is independent of the outer context.
315
+ const parentTextState = opts.textState
316
+ const childOpts: WalkOptions = { ...opts, textState: { lastWasText: false } }
317
+
318
+ const dangerouslyHtml = props.dangerouslySetInnerHTML?.__html
319
+
320
+ if (isTextarea && textareaValue != null) {
321
+ opts.emit(escapeText(String(textareaValue)))
322
+ opts.emit(`</${tag}>`)
323
+ if (parentTextState) parentTextState.lastWasText = false
324
+ return
325
+ }
326
+
327
+ if (RAW_TEXT_ELEMENTS.has(tag)) {
328
+ // script/style: raw-text. React allows either a string/number child or
329
+ // dangerouslySetInnerHTML — some libs (Start's Scripts) use the latter.
330
+ if (dangerouslyHtml != null) {
331
+ opts.emit(escapeScript(String(dangerouslyHtml)))
332
+ } else {
333
+ const children = props.children
334
+ if (typeof children === 'string' || typeof children === 'number') {
335
+ opts.emit(escapeScript(String(children)))
336
+ } else if (Array.isArray(children)) {
337
+ opts.emit(escapeScript(children.filter((c) => c != null).join('')))
338
+ }
339
+ }
340
+ opts.emit(`</${tag}>`)
341
+ if (parentTextState) parentTextState.lastWasText = false
342
+ return
343
+ }
344
+
345
+ if (dangerouslyHtml != null) {
346
+ opts.emit(String(dangerouslyHtml))
347
+ } else {
348
+ walkNode(props.children, childOpts)
349
+ }
350
+ opts.emit(`</${tag}>`)
351
+ if (isSelect) popSelectContext()
352
+ // Host element closing resets outer flow — next sibling text starts fresh.
353
+ if (parentTextState) parentTextState.lastWasText = false
354
+ }
355
+
356
+ function walkComponent(
357
+ fn: Function,
358
+ props: Record<string, any>,
359
+ opts: WalkOptions,
360
+ ): void {
361
+ if ((fn as any).prototype?.isReactComponent) {
362
+ const ctxType = (fn as any).contextType
363
+ const ctxValue = ctxType ? ctxType._currentValue : undefined
364
+ const instance = new (fn as any)(props, ctxValue)
365
+ instance.props = props
366
+ instance.context = ctxValue
367
+ if ((fn as any).getDerivedStateFromProps) {
368
+ const d = (fn as any).getDerivedStateFromProps(props, instance.state)
369
+ if (d) instance.state = { ...instance.state, ...d }
370
+ }
371
+ walkNode(instance.render(), opts)
372
+ return
373
+ }
374
+ const rendered = (fn as any)(props)
375
+ walkNode(rendered, opts)
376
+ }
377
+
378
+ function walkSuspense(
379
+ props: Record<string, any>,
380
+ opts: WalkOptions,
381
+ ): void {
382
+ const id = opts.nextBoundaryId()
383
+ // Snapshot contexts BEFORE attempting children, so if a descendant suspends
384
+ // we can replay the same provider stack when re-rendering the boundary.
385
+ const contextSnapshot = snapshotContexts()
386
+
387
+ // Try to render the children synchronously. If a thenable is thrown,
388
+ // record the boundary and emit the fallback.
389
+ const childParts: string[] = []
390
+ const childEmit = (s: string) => childParts.push(s)
391
+ try {
392
+ walkNode(props.children, {
393
+ emit: childEmit,
394
+ onSuspend: opts.onSuspend,
395
+ nextBoundaryId: opts.nextBoundaryId,
396
+ })
397
+ } catch (thenable: any) {
398
+ if (isThenable(thenable)) {
399
+ const fallbackParts: string[] = []
400
+ try {
401
+ walkNode(props.fallback, {
402
+ emit: (s) => fallbackParts.push(s),
403
+ onSuspend: opts.onSuspend,
404
+ nextBoundaryId: opts.nextBoundaryId,
405
+ })
406
+ } catch {
407
+ // Fallback suspending is unsupported; emit nothing
408
+ }
409
+ emitBoundary(opts, id, fallbackParts.join(''))
410
+
411
+ if (opts.onSuspend) {
412
+ opts.onSuspend({
413
+ id,
414
+ fallbackHTML: fallbackParts.join(''),
415
+ children: props.children,
416
+ thenable,
417
+ contextSnapshot,
418
+ })
419
+ }
420
+ return
421
+ }
422
+ throw thenable
423
+ }
424
+
425
+ // Children rendered fully — emit them wrapped in resolved-boundary markers
426
+ // (`<!--$N-->` / `<!--/$-->`). Without markers, the client hydrator has no
427
+ // way to know this subtree is inside a Suspense, so if the client version
428
+ // of a descendant (e.g. `React.lazy`) suspends it can't pinpoint which DOM
429
+ // range to adopt on resolve — it creates fresh DOM next to the SSR content,
430
+ // producing visible duplicates (e.g. double navbar logos). Markers let the
431
+ // client treat this as a resolved boundary and hydrate in-place.
432
+ opts.emit(`<!--$${id}-->`)
433
+ opts.emit(childParts.join(''))
434
+ opts.emit(`<!--/$-->`)
435
+ }
436
+
437
+ function emitBoundary(opts: WalkOptions, id: number, fallbackHTML: string): void {
438
+ // Visible div wrapper so the fallback UI shows; B: id lets $RC locate it on
439
+ // reveal. The leading/trailing comments let hydration detect a pending
440
+ // boundary and register a reveal callback.
441
+ opts.emit(`<!--$?${id}--><div id="B:${id}">`)
442
+ opts.emit(fallbackHTML)
443
+ opts.emit(`</div><!--/$-->`)
444
+ }
445
+
446
+ function isThenable(x: any): x is Promise<any> {
447
+ return x != null && typeof x.then === 'function'
448
+ }