@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,28 @@
1
+ import { FiberTag, type Fiber } from '../../../core'
2
+ import { REACT_FORWARD_REF_TYPE } from '../../../react'
3
+ import { registerRenderer, registerTypeMatcher, renderFiber } from '../../reconcile'
4
+
5
+ // Stub: ForwardRef feature disabled. `forwardRef(fn)` elements still render,
6
+ // but the ref prop is NOT forwarded — the component is called as a plain
7
+ // function with just props. React 19+ supports refs as normal props on
8
+ // function components, so most apps can drop forwardRef entirely; this stub
9
+ // preserves JSX compatibility while stripping the dispatcher save/restore
10
+ // machinery.
11
+ function renderForwardRefStub(fiber: Fiber, domParent: Node, anchor: Node | null): void {
12
+ const render = (fiber.type as any).render
13
+ const savedTag = fiber.tag
14
+ const savedType = fiber.type
15
+ fiber.type = render
16
+ fiber.tag = FiberTag.Function
17
+ try {
18
+ renderFiber(fiber, domParent, anchor)
19
+ } finally {
20
+ fiber.tag = savedTag
21
+ fiber.type = savedType
22
+ }
23
+ }
24
+
25
+ registerTypeMatcher((_type, marker) =>
26
+ marker === REACT_FORWARD_REF_TYPE ? FiberTag.ForwardRef : null,
27
+ )
28
+ registerRenderer(FiberTag.ForwardRef, renderForwardRefStub)
@@ -0,0 +1,345 @@
1
+ import { FiberTag, type Fiber, type FiberRoot } from '../../../core'
2
+ import { setProp } from '../../dom'
3
+ import { findRoot } from '../../reconcile'
4
+
5
+ // Re-export from event-replay so all hydration concerns live behind one
6
+ // feature boundary — the plugin's stub swap strips drainReplayQueue too.
7
+ export { drainReplayQueue } from '../../event-replay'
8
+
9
+ const GUARD_WINDOW_MS = 3000
10
+
11
+ /**
12
+ * Preserve the user's scroll position across hydration. If the user scrolled
13
+ * between SSR paint and hydrate (common in dev where JS takes seconds to
14
+ * load), libraries that wire scroll-restoration into a `useLayoutEffect`
15
+ * near the root (e.g. TanStack Router) will run during our synchronous
16
+ * hydrate and call `window.scrollTo(savedFromLastVisit)` — overwriting the
17
+ * user's fresh scroll. We install a short-lived wrapper around scrollTo that
18
+ * suppresses programmatic calls when a user-initiated scroll happened
19
+ * recently. Only runs in the hydration feature — the stub skips it.
20
+ */
21
+ export function installHydrationScrollGuard(): void {
22
+ if (typeof window === 'undefined') return
23
+ const w = window as any
24
+ if (w.__tdomScrollGuardInstalled) return
25
+ w.__tdomScrollGuardInstalled = true
26
+ const guardStartedAt = performance.now()
27
+ let lastUserScrollAt = 0
28
+ let programmatic = 0
29
+ w.__tdomScrollLog = []
30
+ window.addEventListener(
31
+ 'scroll',
32
+ () => {
33
+ if (programmatic === 0) {
34
+ lastUserScrollAt = performance.now()
35
+ w.__tdomScrollLog.push({ t: Math.round(lastUserScrollAt), ev: 'user-scroll', y: window.scrollY })
36
+ }
37
+ },
38
+ { capture: true, passive: true },
39
+ )
40
+ const origScrollTo = window.scrollTo.bind(window)
41
+ window.scrollTo = function (this: any, ...args: any[]) {
42
+ const now = performance.now()
43
+ const inGuardWindow = now - guardStartedAt < GUARD_WINDOW_MS
44
+ const userScrolledRecently = lastUserScrollAt > 0 && now - lastUserScrollAt < 1500
45
+ if (inGuardWindow && userScrolledRecently) {
46
+ w.__tdomScrollLog.push({
47
+ t: Math.round(now),
48
+ ev: 'suppressed',
49
+ args: JSON.stringify(args).slice(0, 80),
50
+ tSinceHydrate: Math.round(now - guardStartedAt),
51
+ tSinceUserScroll: Math.round(now - lastUserScrollAt),
52
+ })
53
+ return
54
+ }
55
+ w.__tdomScrollLog.push({
56
+ t: Math.round(now),
57
+ ev: 'allowed',
58
+ args: JSON.stringify(args).slice(0, 80),
59
+ tSinceHydrate: Math.round(now - guardStartedAt),
60
+ inGuard: inGuardWindow,
61
+ userScrolled: userScrolledRecently,
62
+ })
63
+ programmatic++
64
+ try {
65
+ return (origScrollTo as any).apply(this, args)
66
+ } finally {
67
+ queueMicrotask(() => {
68
+ programmatic = Math.max(0, programmatic - 1)
69
+ })
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Hydration cursor: walks existing DOM children in document order so we can
76
+ * adopt them during fiber tree construction. One cursor per host parent.
77
+ *
78
+ * `endBefore` scopes the cursor to a subrange — used by rehydrateBoundary()
79
+ * so we only adopt DOM up to the closing `/$` marker for that boundary.
80
+ */
81
+ export class HydrationCursor {
82
+ next: ChildNode | null
83
+ parent: Node
84
+ endBefore: ChildNode | null
85
+ constructor(parent: Node, start: ChildNode | null = null, endBefore: ChildNode | null = null) {
86
+ this.parent = parent
87
+ this.next = start ?? parent.firstChild
88
+ this.endBefore = endBefore
89
+ }
90
+ takeHostNode(): ChildNode | null {
91
+ while (this.next && this.next !== this.endBefore) {
92
+ const n = this.next
93
+ // Skip anything that isn't an element (1) or text (3):
94
+ // comments (8), doctype (10), processing instructions (7), cdata (4).
95
+ if (n.nodeType !== 1 && n.nodeType !== 3) {
96
+ this.next = n.nextSibling
97
+ continue
98
+ }
99
+ this.next = n.nextSibling
100
+ return n
101
+ }
102
+ return null
103
+ }
104
+ /**
105
+ * Position-insensitive lookup for head/html adoption. Scans forward past
106
+ * non-matching nodes without removing them, matching by tag AND the key
107
+ * attributes that identify head elements uniquely (rel/href for links,
108
+ * name/property for meta, src for script). Non-matching nodes stay in
109
+ * place so the SSR'd stylesheet/script order is preserved.
110
+ */
111
+ takeMatchingHeadElement(tag: string, props: Record<string, any>): ChildNode | null {
112
+ const target = tag.toLowerCase()
113
+ const keyAttrs = HEAD_KEY_ATTRS[target] ?? []
114
+ let scan = this.parent.firstChild
115
+ while (scan) {
116
+ if (
117
+ scan.nodeType === 1 &&
118
+ (scan as Element).tagName.toLowerCase() === target &&
119
+ headAttrsMatch(scan as Element, props, keyAttrs)
120
+ ) {
121
+ CLAIMED.add(scan)
122
+ return scan
123
+ }
124
+ scan = scan.nextSibling
125
+ }
126
+ return null
127
+ }
128
+ remaining(): ChildNode[] {
129
+ const out: ChildNode[] = []
130
+ let n = this.next
131
+ while (n && n !== this.endBefore) {
132
+ out.push(n)
133
+ n = n.nextSibling
134
+ }
135
+ return out
136
+ }
137
+ }
138
+
139
+ const hydrationCursors = new WeakMap<Fiber, HydrationCursor>()
140
+
141
+ // Head elements that we match against server DOM by attribute signature.
142
+ const HEAD_KEY_ATTRS: Record<string, ReadonlyArray<string>> = {
143
+ link: ['rel', 'href', 'sizes', 'type'],
144
+ meta: ['name', 'property', 'charset', 'http-equiv'],
145
+ script: ['src', 'type'],
146
+ style: [],
147
+ title: [],
148
+ }
149
+
150
+ // DOM elements already claimed by some fiber during this hydration pass.
151
+ const CLAIMED = new WeakSet<Node>()
152
+
153
+ function headAttrsMatch(
154
+ el: Element,
155
+ props: Record<string, any>,
156
+ keys: ReadonlyArray<string>,
157
+ ): boolean {
158
+ if (CLAIMED.has(el)) return false
159
+ if (keys.length === 0) return true
160
+ for (const k of keys) {
161
+ const propVal = props[k] ?? (k === 'http-equiv' ? props.httpEquiv : undefined)
162
+ const elVal = el.getAttribute(k)
163
+ // If neither defines it, skip this key; if one defines it, they must match.
164
+ if (propVal == null && elVal == null) continue
165
+ if (propVal == null || elVal == null) continue // tolerate missing on either side
166
+ if (String(propVal) !== elVal) return false
167
+ }
168
+ // At least one matching signal must be present.
169
+ return keys.some((k) => props[k] != null || el.hasAttribute(k))
170
+ }
171
+
172
+ export function beginHydration(root: FiberRoot): void {
173
+ root.hydrating = true
174
+ hydrationCursors.set(root.current, new HydrationCursor(root.container))
175
+ }
176
+
177
+ export function endHydration(root: FiberRoot): void {
178
+ root.hydrating = false
179
+ hydrationCursors.delete(root.current)
180
+ }
181
+
182
+ /**
183
+ * Inspect the current cursor position for a streaming-suspense boundary
184
+ * marker emitted by the server. Returns info + advances the cursor past the
185
+ * marker pair (start comment + fallback/real content + end comment).
186
+ */
187
+ export interface BoundaryInfo {
188
+ kind: 'pending' | 'resolved'
189
+ id: number
190
+ startMark: Comment
191
+ endMark: Comment
192
+ }
193
+
194
+ export function tryConsumeBoundary(parent: Fiber): BoundaryInfo | null {
195
+ const cursor = hydrationCursors.get(findHostParent(parent))
196
+ if (!cursor) return null
197
+ const peek = cursor.next
198
+ if (!peek || peek.nodeType !== 8) return null
199
+ const data = (peek as Comment).data
200
+ const m = /^(\$\??)(\d+)$/.exec(data)
201
+ if (!m) return null
202
+ const kind = m[1] === '$?' ? 'pending' : 'resolved'
203
+ const id = Number(m[2])
204
+ const startMark = peek as Comment
205
+ // Advance past the start comment
206
+ cursor.next = startMark.nextSibling
207
+ // Locate end comment: closest <!--/$-->
208
+ let endMark: Comment | null = null
209
+ let scan = startMark.nextSibling
210
+ while (scan) {
211
+ if (scan.nodeType === 8 && (scan as Comment).data === '/$') {
212
+ endMark = scan as Comment
213
+ break
214
+ }
215
+ scan = scan.nextSibling
216
+ }
217
+ if (!endMark) return null
218
+ return { kind, id, startMark, endMark }
219
+ }
220
+
221
+ export function advanceCursorPast(parent: Fiber, node: Node): void {
222
+ const cursor = hydrationCursors.get(findHostParent(parent))
223
+ if (!cursor) return
224
+ cursor.next = node.nextSibling
225
+ }
226
+
227
+ export function getHydrationCursor(hostFiber: Fiber): HydrationCursor | undefined {
228
+ return hydrationCursors.get(hostFiber)
229
+ }
230
+
231
+ export function setHydrationCursor(hostFiber: Fiber, cursor: HydrationCursor): void {
232
+ hydrationCursors.set(hostFiber, cursor)
233
+ }
234
+
235
+ export function clearHydrationCursor(hostFiber: Fiber): void {
236
+ hydrationCursors.delete(hostFiber)
237
+ }
238
+
239
+ /**
240
+ * Try to adopt a DOM node for this host fiber. Returns true if adopted.
241
+ * Attaches existing attrs/children via separate hydrate pass.
242
+ */
243
+ export function adoptHostDom(fiber: Fiber, parent: Fiber): boolean {
244
+ const hostParent = findHostParent(parent)
245
+ const cursor = hydrationCursors.get(hostParent)
246
+ if (!cursor) return false
247
+
248
+ const tag = (fiber.type as string).toLowerCase()
249
+ const parentEl = cursor.parent as Element
250
+ const parentTag =
251
+ parentEl.nodeType === 1 ? (parentEl as Element).tagName.toLowerCase() : ''
252
+ const isHeadish = parentTag === 'head' || parentTag === 'html'
253
+
254
+ let candidate: ChildNode | null
255
+ if (isHeadish) {
256
+ // Head/html children are position-insensitive — server may emit them in
257
+ // a different order than the React tree (React 19 head hoisting, etc.).
258
+ // Scan forward without removing non-matching nodes; match on attribute
259
+ // signature so we don't adopt the wrong <link> and clobber its props.
260
+ candidate = cursor.takeMatchingHeadElement(tag, fiber.pendingProps ?? {})
261
+ } else {
262
+ candidate = cursor.takeHostNode()
263
+ }
264
+
265
+ if (!candidate) {
266
+ // Client expected a host here but the cursor is exhausted — server gave
267
+ // fewer children than the client tree. Report the structural gap (React
268
+ // fires `onRecoverableError` for this exact case) and let the reconciler
269
+ // mount a fresh DOM for this fiber below.
270
+ // Exception: <head> children are position-insensitive; a missing match
271
+ // there means "server didn't hoist this one yet", which we silently mount.
272
+ if (!isHeadish) onMismatch(fiber, null)
273
+ return false
274
+ }
275
+
276
+ if (candidate.nodeType !== 1 || (candidate as Element).tagName.toLowerCase() !== tag) {
277
+ // mismatch — log and re-render fresh from this point
278
+ onMismatch(fiber, candidate)
279
+ return false
280
+ }
281
+ fiber.dom = candidate
282
+ // Apply props (attach events, sync IDL props). Don't re-set existing attrs.
283
+ const props = fiber.pendingProps ?? {}
284
+ const isSvg =
285
+ tag === 'svg' ||
286
+ ((candidate as Element).namespaceURI === 'http://www.w3.org/2000/svg' &&
287
+ tag !== 'foreignobject')
288
+ for (const k in props) {
289
+ if (k === 'children') continue
290
+ if (k[0] === 'o' && k[1] === 'n' && typeof props[k] === 'function') {
291
+ setProp(candidate as Element, k, props[k], undefined, isSvg)
292
+ }
293
+ // Non-event props: trust the server HTML, skip
294
+ }
295
+ // Set up child cursor for this host's children
296
+ hydrationCursors.set(fiber, new HydrationCursor(candidate))
297
+ return true
298
+ }
299
+
300
+ export function adoptTextDom(fiber: Fiber, parent: Fiber, text: string): boolean {
301
+ const cursor = hydrationCursors.get(findHostParent(parent))
302
+ if (!cursor) return false
303
+ const candidate = cursor.takeHostNode()
304
+ if (!candidate) return false
305
+ if (candidate.nodeType === 3) {
306
+ if ((candidate as Text).data !== text) {
307
+ ;(candidate as Text).data = text
308
+ }
309
+ fiber.dom = candidate
310
+ return true
311
+ }
312
+ onMismatch(fiber, candidate)
313
+ return false
314
+ }
315
+
316
+ export function findHostParent(fiber: Fiber): Fiber {
317
+ let f: Fiber | null = fiber
318
+ while (f) {
319
+ // A fiber explicitly holding a cursor acts as a boundary for hydration
320
+ // (e.g. Suspense with a scoped cursor during fallback/boundary hydration).
321
+ if (hydrationCursors.has(f)) return f
322
+ if (f.tag === FiberTag.Host || f.tag === FiberTag.Root || f.tag === FiberTag.Portal) {
323
+ return f
324
+ }
325
+ f = f.parent
326
+ }
327
+ throw new Error('No host parent found')
328
+ }
329
+
330
+ function onMismatch(fiber: Fiber, actualNode: ChildNode | null): void {
331
+ // For v1: log and exit hydration for this subtree. The normal reconciler
332
+ // will create a fresh DOM node below.
333
+ const root = findRoot(fiber)
334
+ if (root?.onRecoverableError) {
335
+ root.onRecoverableError(
336
+ new Error(
337
+ `Hydration mismatch: expected <${(fiber.type as string) ?? 'text'}> but found ${
338
+ actualNode ? (actualNode.nodeType === 1 ? (actualNode as Element).tagName : 'text') : 'nothing'
339
+ }.`,
340
+ ),
341
+ )
342
+ }
343
+ // Remove stale DOM if still there
344
+ if (actualNode && actualNode.parentNode) actualNode.parentNode.removeChild(actualNode)
345
+ }
@@ -0,0 +1 @@
1
+ export * from './full'
@@ -0,0 +1,72 @@
1
+ import type { Fiber, FiberRoot } from '../../../core'
2
+
3
+ // Stub: Hydration feature disabled. Every adoption attempt returns "no match",
4
+ // all cursor operations no-op, and `beginHydration` throws so `hydrateRoot`
5
+ // fails loudly — apps that opt out of hydration should use `createRoot`.
6
+ // The SSR walk-the-DOM machinery, streaming-boundary coordination, head
7
+ // element matching, and the WeakMap of cursors per fiber are all stripped.
8
+
9
+ export class HydrationCursor {
10
+ next: ChildNode | null = null
11
+ parent: Node
12
+ endBefore: ChildNode | null = null
13
+ constructor(parent?: Node, _s?: ChildNode | null, _e?: ChildNode | null) {
14
+ this.parent = parent as Node
15
+ }
16
+ takeHostNode(): ChildNode | null {
17
+ return null
18
+ }
19
+ takeMatchingHeadElement(): ChildNode | null {
20
+ return null
21
+ }
22
+ remaining(): ChildNode[] {
23
+ return []
24
+ }
25
+ }
26
+
27
+ export interface BoundaryInfo {
28
+ kind: 'pending' | 'resolved'
29
+ id: number
30
+ startMark: Comment
31
+ endMark: Comment
32
+ }
33
+
34
+ export function beginHydration(_root: FiberRoot): void {
35
+ throw new Error(
36
+ '`hydrateRoot` requires the `hydration` feature. ' +
37
+ 'Enable it via @tanstack/redact/vite `features.hydration = true`, ' +
38
+ 'or use `createRoot` for a SPA (no SSR hydration).',
39
+ )
40
+ }
41
+
42
+ export function endHydration(_root: FiberRoot): void {}
43
+
44
+ export function tryConsumeBoundary(_parent: Fiber): BoundaryInfo | null {
45
+ return null
46
+ }
47
+
48
+ export function advanceCursorPast(_parent: Fiber, _node: Node): void {}
49
+
50
+ export function getHydrationCursor(_hostFiber: Fiber): HydrationCursor | undefined {
51
+ return undefined
52
+ }
53
+
54
+ export function setHydrationCursor(_hostFiber: Fiber, _cursor: HydrationCursor): void {}
55
+
56
+ export function clearHydrationCursor(_hostFiber: Fiber): void {}
57
+
58
+ export function adoptHostDom(_fiber: Fiber, _parent: Fiber): boolean {
59
+ return false
60
+ }
61
+
62
+ export function adoptTextDom(_fiber: Fiber, _parent: Fiber, _text: string): boolean {
63
+ return false
64
+ }
65
+
66
+ export function findHostParent(fiber: Fiber): Fiber {
67
+ return fiber
68
+ }
69
+
70
+ export function installHydrationScrollGuard(): void {}
71
+
72
+ export function drainReplayQueue(): void {}
@@ -0,0 +1,11 @@
1
+ // Feature wiring. Each import is a side-effect import that triggers the
2
+ // feature module's self-registration with the reconciler (renderer, type
3
+ // matcher, element marker). A vite plugin can swap any entry to './stub'
4
+ // to disable that feature in the emitted bundle.
5
+ import './portal'
6
+ import './context'
7
+ import './suspense'
8
+ import './memo'
9
+ import './forward-ref'
10
+ import './lazy'
11
+ import './class'
@@ -0,0 +1,98 @@
1
+ import { FiberTag, type Fiber } from '../../../core'
2
+ import { REACT_LAZY_TYPE } from '../../../react'
3
+ import {
4
+ registerRenderer,
5
+ registerTypeMatcher,
6
+ renderFiber,
7
+ scheduleUpdate,
8
+ isThenable,
9
+ handleSuspended,
10
+ getCurrentRoot,
11
+ } from '../../reconcile'
12
+ import {
13
+ getHydrationCursor,
14
+ setHydrationCursor,
15
+ findHostParent as findHydrationHost,
16
+ } from '../hydration'
17
+
18
+ function renderLazy(fiber: Fiber, domParent: Node, anchor: Node | null): void {
19
+ const { _payload, _init } = fiber.type as any
20
+ let resolved: any
21
+ try {
22
+ resolved = _init(_payload)
23
+ } catch (thenable: any) {
24
+ if (isThenable(thenable)) {
25
+ // During initial hydration, a lazy component inside an SSR-resolved
26
+ // Suspense boundary needs special handling: the SSR DOM for its
27
+ // resolved content is already in the page and our cursor is pointing
28
+ // at it. A normal `handleSuspended` would schedule the lazy's later
29
+ // re-render WITHOUT the hydration cursor — so when it eventually
30
+ // resolves, we'd create a fresh DOM copy next to the SSR one (visible
31
+ // as duplicate logos / buttons / sections inside the Suspense). Mirror
32
+ // the pattern in renderFunction: preserve the in-scope cursor on this
33
+ // fiber and flag it for deferred re-hydration, so rerenderFiber
34
+ // restores `root.hydrating = true` and the resolved render adopts the
35
+ // existing DOM instead of mounting a duplicate.
36
+ const root = getCurrentRoot()
37
+ if (root?.hydrating) {
38
+ const hostParent = findHydrationHost(fiber)
39
+ const inheritedCursor = getHydrationCursor(hostParent)
40
+ if (inheritedCursor) {
41
+ setHydrationCursor(fiber, inheritedCursor)
42
+ }
43
+ fiber.memoizedState = {
44
+ ...(fiber.memoizedState ?? {}),
45
+ _pendingHydration: true,
46
+ }
47
+ // Mark the nearest ancestor Suspense as "awaiting hydration-resume"
48
+ // so a post-hydration re-render of that Suspense doesn't accidentally
49
+ // flip into suspended+pending and mount a fallback atop the SSR
50
+ // content. Match-by-tag-name works whether Suspense is the full
51
+ // feature or stubbed to Fragment (the walk just never finds one).
52
+ let sus: Fiber | null = fiber.parent
53
+ while (sus && sus.tag !== FiberTag.Suspense) sus = sus.parent
54
+ if (sus && sus.memoizedState) {
55
+ ;(sus.memoizedState as any)._awaitingLazyHydration = true
56
+ }
57
+ thenable.then(
58
+ () => {
59
+ if (sus && sus.memoizedState) {
60
+ ;(sus.memoizedState as any)._awaitingLazyHydration = false
61
+ }
62
+ scheduleUpdate(fiber)
63
+ },
64
+ () => {
65
+ if (sus && sus.memoizedState) {
66
+ ;(sus.memoizedState as any)._awaitingLazyHydration = false
67
+ }
68
+ scheduleUpdate(fiber)
69
+ },
70
+ )
71
+ return
72
+ }
73
+ handleSuspended(fiber, thenable)
74
+ // reconcileChildren would be called here if we were rendering children,
75
+ // but Lazy delegates and has no children of its own.
76
+ return
77
+ }
78
+ throw thenable
79
+ }
80
+ const savedTag = fiber.tag
81
+ const savedType = fiber.type
82
+ fiber.type = resolved
83
+ fiber.tag =
84
+ typeof resolved === 'function'
85
+ ? resolved.prototype?.isReactComponent
86
+ ? FiberTag.Class
87
+ : FiberTag.Function
88
+ : FiberTag.Fragment
89
+ try {
90
+ renderFiber(fiber, domParent, anchor)
91
+ } finally {
92
+ fiber.tag = savedTag
93
+ fiber.type = savedType
94
+ }
95
+ }
96
+
97
+ registerTypeMatcher((_type, marker) => (marker === REACT_LAZY_TYPE ? FiberTag.Lazy : null))
98
+ registerRenderer(FiberTag.Lazy, renderLazy)
@@ -0,0 +1 @@
1
+ export * from './full'
@@ -0,0 +1,45 @@
1
+ import { FiberTag, type Fiber } from '../../../core'
2
+ import { REACT_LAZY_TYPE } from '../../../react'
3
+ import {
4
+ registerRenderer,
5
+ registerTypeMatcher,
6
+ renderFiber,
7
+ isThenable,
8
+ handleSuspended,
9
+ } from '../../reconcile'
10
+
11
+ // Stub: Lazy feature disabled. Lazy elements still resolve — sync if the
12
+ // payload is ready, otherwise the thrown thenable goes through the default
13
+ // `handleSuspended` capability (retry-on-settle). What's stripped: the
14
+ // hydration-deferred-reveal pathway and the Suspense-awaiting coordination.
15
+ function renderLazyStub(fiber: Fiber, domParent: Node, anchor: Node | null): void {
16
+ const { _payload, _init } = fiber.type as any
17
+ let resolved: any
18
+ try {
19
+ resolved = _init(_payload)
20
+ } catch (thrown: any) {
21
+ if (isThenable(thrown)) {
22
+ handleSuspended(fiber, thrown)
23
+ return
24
+ }
25
+ throw thrown
26
+ }
27
+ const savedTag = fiber.tag
28
+ const savedType = fiber.type
29
+ fiber.type = resolved
30
+ fiber.tag =
31
+ typeof resolved === 'function'
32
+ ? resolved.prototype?.isReactComponent
33
+ ? FiberTag.Class
34
+ : FiberTag.Function
35
+ : FiberTag.Fragment
36
+ try {
37
+ renderFiber(fiber, domParent, anchor)
38
+ } finally {
39
+ fiber.tag = savedTag
40
+ fiber.type = savedType
41
+ }
42
+ }
43
+
44
+ registerTypeMatcher((_type, marker) => (marker === REACT_LAZY_TYPE ? FiberTag.Lazy : null))
45
+ registerRenderer(FiberTag.Lazy, renderLazyStub)
@@ -0,0 +1,74 @@
1
+ import { FiberTag, type Fiber } from '../../../core'
2
+ import {
3
+ REACT_FORWARD_REF_TYPE,
4
+ REACT_MEMO_TYPE,
5
+ REACT_LAZY_TYPE,
6
+ } from '../../../react'
7
+ import {
8
+ registerRenderer,
9
+ registerTypeMatcher,
10
+ renderFiber,
11
+ getForceRerenderingFiber,
12
+ } from '../../reconcile'
13
+
14
+ function shallowEqual(a: any, b: any): boolean {
15
+ if (a === b) return true
16
+ if (!a || !b) return false
17
+ const ak = Object.keys(a)
18
+ const bk = Object.keys(b)
19
+ if (ak.length !== bk.length) return false
20
+ for (const k of ak) {
21
+ if (a[k] !== b[k]) return false
22
+ }
23
+ return true
24
+ }
25
+
26
+ function renderMemo(fiber: Fiber, domParent: Node, anchor: Node | null): void {
27
+ const { type, compare } = fiber.type as any
28
+ const props = fiber.pendingProps ?? {}
29
+ const prev = fiber.memoizedProps
30
+
31
+ // Memo's prop-equality gate guards PARENT-triggered rerenders. If this
32
+ // render is a STATE-triggered rerender of this exact fiber (hook update,
33
+ // useSyncExternalStore notification), props haven't changed by definition —
34
+ // bailing would swallow the state change and the subscriber never re-runs.
35
+ // rerenderFiber tags the fiber so we skip the gate here.
36
+ const bypassMemo = fiber === getForceRerenderingFiber()
37
+ const eq = !bypassMemo && prev && (compare ? compare(prev, props) : shallowEqual(prev, props))
38
+ if (eq) {
39
+ // Re-render children with previous output (already in tree)
40
+ return
41
+ }
42
+
43
+ // Determine the delegated tag based on the memoized type. `React.memo` can
44
+ // wrap plain functions, class components, OR other special types like
45
+ // `forwardRef`. Without the marker-based branch we'd mistreat
46
+ // `memo(forwardRef(...))` as a Fragment and render nothing.
47
+ let innerTag: FiberTag
48
+ if (typeof type === 'function') {
49
+ innerTag = type.prototype?.isReactComponent ? FiberTag.Class : FiberTag.Function
50
+ } else if (type && typeof type === 'object') {
51
+ const m = (type as any).$$typeof
52
+ if (m === REACT_FORWARD_REF_TYPE) innerTag = FiberTag.ForwardRef
53
+ else if (m === REACT_MEMO_TYPE) innerTag = FiberTag.Memo
54
+ else if (m === REACT_LAZY_TYPE) innerTag = FiberTag.Lazy
55
+ else innerTag = FiberTag.Fragment
56
+ } else {
57
+ innerTag = FiberTag.Fragment
58
+ }
59
+
60
+ // Swap tag and type for this render pass; this is a "delegating" render
61
+ const savedTag = fiber.tag
62
+ const savedType = fiber.type
63
+ fiber.tag = innerTag
64
+ fiber.type = type
65
+ try {
66
+ renderFiber(fiber, domParent, anchor)
67
+ } finally {
68
+ fiber.tag = savedTag
69
+ fiber.type = savedType
70
+ }
71
+ }
72
+
73
+ registerTypeMatcher((_type, marker) => (marker === REACT_MEMO_TYPE ? FiberTag.Memo : null))
74
+ registerRenderer(FiberTag.Memo, renderMemo)
@@ -0,0 +1 @@
1
+ export * from './full'