@tanstack/redact 0.0.13 → 0.0.15

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 (93) hide show
  1. package/dist/core/internal.d.ts +23 -39
  2. package/dist/core/internal.js +7 -23
  3. package/dist/core/internal.js.map +3 -3
  4. package/dist/dom/dispatcher.d.ts +3 -1
  5. package/dist/dom/dispatcher.js +104 -97
  6. package/dist/dom/dispatcher.js.map +2 -2
  7. package/dist/dom/dom.js +1 -1
  8. package/dist/dom/dom.js.map +2 -2
  9. package/dist/dom/event-replay.js +7 -29
  10. package/dist/dom/event-replay.js.map +2 -2
  11. package/dist/dom/features/class/full.js +16 -16
  12. package/dist/dom/features/class/full.js.map +2 -2
  13. package/dist/dom/features/class/stub.js +7 -7
  14. package/dist/dom/features/class/stub.js.map +2 -2
  15. package/dist/dom/features/context/full.js +7 -7
  16. package/dist/dom/features/context/full.js.map +2 -2
  17. package/dist/dom/features/context/stub.js +3 -3
  18. package/dist/dom/features/context/stub.js.map +2 -2
  19. package/dist/dom/features/forward-ref/full.js +11 -11
  20. package/dist/dom/features/forward-ref/full.js.map +2 -2
  21. package/dist/dom/features/hydration/full.d.ts +26 -14
  22. package/dist/dom/features/hydration/full.js +412 -92
  23. package/dist/dom/features/hydration/full.js.map +2 -2
  24. package/dist/dom/features/hydration/stub.d.ts +13 -12
  25. package/dist/dom/features/hydration/stub.js +23 -8
  26. package/dist/dom/features/hydration/stub.js.map +2 -2
  27. package/dist/dom/features/lazy/full.js +3 -43
  28. package/dist/dom/features/lazy/full.js.map +2 -2
  29. package/dist/dom/features/lazy/stub.js +1 -1
  30. package/dist/dom/features/lazy/stub.js.map +2 -2
  31. package/dist/dom/features/memo/full.js +3 -3
  32. package/dist/dom/features/memo/full.js.map +2 -2
  33. package/dist/dom/features/memo/stub.js +1 -1
  34. package/dist/dom/features/memo/stub.js.map +2 -2
  35. package/dist/dom/features/portal/full.js +2 -2
  36. package/dist/dom/features/portal/full.js.map +2 -2
  37. package/dist/dom/features/suspense/full.js +151 -109
  38. package/dist/dom/features/suspense/full.js.map +2 -2
  39. package/dist/dom/reconcile.d.ts +3 -1
  40. package/dist/dom/reconcile.js +141 -129
  41. package/dist/dom/reconcile.js.map +2 -2
  42. package/dist/dom/root-internal.d.ts +9 -0
  43. package/dist/dom/root-internal.js +31 -0
  44. package/dist/dom/root-internal.js.map +7 -0
  45. package/dist/dom/root.d.ts +1 -1
  46. package/dist/dom/root.js +4 -136
  47. package/dist/dom/root.js.map +2 -2
  48. package/dist/dom/test-utils.js +1 -1
  49. package/dist/dom/test-utils.js.map +2 -2
  50. package/dist/react/children.js +4 -1
  51. package/dist/react/children.js.map +2 -2
  52. package/dist/react/class.js +8 -2
  53. package/dist/react/class.js.map +2 -2
  54. package/dist/react/context.js +4 -1
  55. package/dist/react/context.js.map +2 -2
  56. package/dist/react/index.js +1 -1
  57. package/dist/react/index.js.map +2 -2
  58. package/dist/react/shared-internals.d.ts +4 -7
  59. package/dist/react/shared-internals.js +4 -7
  60. package/dist/react/shared-internals.js.map +2 -2
  61. package/dist/server/stream.d.ts +5 -1
  62. package/dist/server/stream.js.map +2 -2
  63. package/dist/vite/index.js +19 -3
  64. package/dist/vite/index.js.map +2 -2
  65. package/package.json +1 -1
  66. package/src/core/internal.ts +30 -50
  67. package/src/dom/dispatcher.ts +114 -105
  68. package/src/dom/dom.ts +1 -1
  69. package/src/dom/event-replay.ts +14 -30
  70. package/src/dom/features/class/full.ts +16 -16
  71. package/src/dom/features/class/stub.ts +7 -7
  72. package/src/dom/features/context/full.ts +7 -7
  73. package/src/dom/features/context/stub.ts +3 -3
  74. package/src/dom/features/forward-ref/full.ts +11 -11
  75. package/src/dom/features/hydration/full.ts +516 -104
  76. package/src/dom/features/hydration/stub.ts +28 -14
  77. package/src/dom/features/lazy/full.ts +3 -56
  78. package/src/dom/features/lazy/stub.ts +1 -1
  79. package/src/dom/features/memo/full.ts +3 -3
  80. package/src/dom/features/memo/stub.ts +1 -1
  81. package/src/dom/features/portal/full.ts +2 -2
  82. package/src/dom/features/suspense/full.ts +176 -129
  83. package/src/dom/reconcile.ts +164 -170
  84. package/src/dom/root-internal.ts +40 -0
  85. package/src/dom/root.ts +5 -165
  86. package/src/dom/test-utils.ts +1 -1
  87. package/src/react/children.ts +4 -1
  88. package/src/react/class.ts +8 -2
  89. package/src/react/context.ts +4 -1
  90. package/src/react/index.ts +1 -1
  91. package/src/react/shared-internals.ts +8 -14
  92. package/src/server/stream.ts +9 -4
  93. package/src/vite/index.ts +34 -3
@@ -7,29 +7,24 @@ import type { Fiber, FiberRoot } from '../../../core'
7
7
  // element matching, and the WeakMap of cursors per fiber are all stripped.
8
8
 
9
9
  export class HydrationCursor {
10
- next: ChildNode | null = null
11
- parent: Node
12
- endBefore: ChildNode | null = null
10
+ n: ChildNode | null = null
11
+ p: Node
12
+ e: ChildNode | null = null
13
13
  constructor(parent?: Node, _s?: ChildNode | null, _e?: ChildNode | null) {
14
- this.parent = parent as Node
14
+ this.p = parent as Node
15
15
  }
16
- takeHostNode(): ChildNode | null {
16
+ take(): ChildNode | null {
17
17
  return null
18
18
  }
19
- takeMatchingHeadElement(): ChildNode | null {
19
+ head(): ChildNode | null {
20
20
  return null
21
21
  }
22
- remaining(): ChildNode[] {
23
- return []
22
+ has(): boolean {
23
+ return false
24
24
  }
25
25
  }
26
26
 
27
- export interface BoundaryInfo {
28
- kind: 'pending' | 'resolved'
29
- id: number
30
- startMark: Comment
31
- endMark: Comment
32
- }
27
+ export type BoundaryInfo = [0 | 1, number, Comment, Comment]
33
28
 
34
29
  export function beginHydration(_root: FiberRoot): void {
35
30
  throw new Error(
@@ -70,3 +65,22 @@ export function findHostParent(fiber: Fiber): Fiber {
70
65
  export function installHydrationScrollGuard(): void {}
71
66
 
72
67
  export function drainReplayQueue(): void {}
68
+
69
+ export interface HydrationBailoutError extends Error {
70
+ f: Fiber | null
71
+ }
72
+
73
+ export function isHydrationBailout(_error: unknown): _error is HydrationBailoutError {
74
+ return false
75
+ }
76
+
77
+ export function abortHydration(cause: unknown, fiber: Fiber | null = null): never {
78
+ const error = (cause instanceof Error ? cause : new Error('Hydration mismatch.')) as HydrationBailoutError
79
+ ;(error as any).f = fiber
80
+ throw error
81
+ }
82
+
83
+ export function hydrateRootImpl(): never {
84
+ beginHydration(null as any)
85
+ throw new Error('`hydrateRoot` requires the `hydration` feature.')
86
+ }
@@ -4,16 +4,10 @@ import {
4
4
  registerRenderer,
5
5
  registerTypeMatcher,
6
6
  renderFiber,
7
- scheduleUpdate,
8
7
  isThenable,
9
8
  handleSuspended,
10
- getCurrentRoot,
9
+ deferHydration,
11
10
  } from '../../reconcile'
12
- import {
13
- getHydrationCursor,
14
- setHydrationCursor,
15
- findHostParent as findHydrationHost,
16
- } from '../hydration'
17
11
 
18
12
  function renderLazy(fiber: Fiber, domParent: Node, anchor: Node | null): void {
19
13
  const { _payload, _init } = fiber.type as any
@@ -22,54 +16,7 @@ function renderLazy(fiber: Fiber, domParent: Node, anchor: Node | null): void {
22
16
  resolved = _init(_payload)
23
17
  } catch (thenable: any) {
24
18
  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
- }
19
+ if (deferHydration(fiber, thenable)) return
73
20
  handleSuspended(fiber, thenable)
74
21
  // reconcileChildren would be called here if we were rendering children,
75
22
  // but Lazy delegates and has no children of its own.
@@ -81,7 +28,7 @@ function renderLazy(fiber: Fiber, domParent: Node, anchor: Node | null): void {
81
28
  const savedType = fiber.type
82
29
  fiber.type = resolved
83
30
  fiber.tag =
84
- typeof resolved === 'function'
31
+ typeof resolved == 'function'
85
32
  ? resolved.prototype?.isReactComponent
86
33
  ? FiberTag.Class
87
34
  : FiberTag.Function
@@ -28,7 +28,7 @@ function renderLazyStub(fiber: Fiber, domParent: Node, anchor: Node | null): voi
28
28
  const savedType = fiber.type
29
29
  fiber.type = resolved
30
30
  fiber.tag =
31
- typeof resolved === 'function'
31
+ typeof resolved == 'function'
32
32
  ? resolved.prototype?.isReactComponent
33
33
  ? FiberTag.Class
34
34
  : FiberTag.Function
@@ -25,8 +25,8 @@ function shallowEqual(a: any, b: any): boolean {
25
25
 
26
26
  function renderMemo(fiber: Fiber, domParent: Node, anchor: Node | null): void {
27
27
  const { type, compare } = fiber.type as any
28
- const props = fiber.pendingProps ?? {}
29
- const prev = fiber.memoizedProps
28
+ const props = fiber.pp ?? {}
29
+ const prev = fiber.mp
30
30
 
31
31
  // Memo's prop-equality gate guards PARENT-triggered rerenders. If this
32
32
  // render is a STATE-triggered rerender of this exact fiber (hook update,
@@ -45,7 +45,7 @@ function renderMemo(fiber: Fiber, domParent: Node, anchor: Node | null): void {
45
45
  // `forwardRef`. Without the marker-based branch we'd mistreat
46
46
  // `memo(forwardRef(...))` as a Fragment and render nothing.
47
47
  let innerTag: FiberTag
48
- if (typeof type === 'function') {
48
+ if (typeof type == 'function') {
49
49
  innerTag = type.prototype?.isReactComponent ? FiberTag.Class : FiberTag.Function
50
50
  } else if (type && typeof type === 'object') {
51
51
  const m = (type as any).$$typeof
@@ -14,7 +14,7 @@ function renderMemoStub(fiber: Fiber, domParent: Node, anchor: Node | null): voi
14
14
  const { type } = fiber.type as any
15
15
 
16
16
  let innerTag: FiberTag
17
- if (typeof type === 'function') {
17
+ if (typeof type == 'function') {
18
18
  innerTag = type.prototype?.isReactComponent ? FiberTag.Class : FiberTag.Function
19
19
  } else if (type && typeof type === 'object') {
20
20
  const m = (type as any).$$typeof
@@ -9,12 +9,12 @@ import {
9
9
  } from '../../reconcile'
10
10
 
11
11
  function renderPortal(fiber: Fiber, _domParent: Node, _anchor: Node | null): void {
12
- const { children, container } = fiber.pendingProps as {
12
+ const { children, container } = fiber.pp as {
13
13
  children: ReactNode
14
14
  container: Element
15
15
  }
16
16
  reconcileChildren(fiber, childrenToArray(children), container, null)
17
- fiber.memoizedProps = fiber.pendingProps
17
+ fiber.mp = fiber.pp
18
18
  }
19
19
 
20
20
  registerElementMarker(REACT_PORTAL_TYPE)
@@ -14,6 +14,7 @@ import {
14
14
  runEffects,
15
15
  getCurrentRoot,
16
16
  withCurrentRoot,
17
+ discardPendingWork,
17
18
  } from '../../reconcile'
18
19
  import {
19
20
  HydrationCursor,
@@ -21,16 +22,13 @@ import {
21
22
  clearHydrationCursor,
22
23
  advanceCursorPast,
23
24
  tryConsumeBoundary,
25
+ isHydrationBailout,
24
26
  } from '../hydration'
25
27
 
26
- const suspendHandlerStack: Array<(t: Promise<any>) => void> = []
28
+ let suspendHandler: ((t: Promise<any>) => void) | null = null
27
29
 
28
30
  function realHandleSuspended(fiber: Fiber, thenable: Promise<any>): void {
29
- const handler = suspendHandlerStack[suspendHandlerStack.length - 1]
30
- if (handler) {
31
- handler(thenable)
32
- return
33
- }
31
+ if (suspendHandler) return suspendHandler(thenable)
34
32
  // Fallback: schedule re-render when promise settles
35
33
  thenable.then(
36
34
  () => scheduleUpdate(fiber),
@@ -44,43 +42,34 @@ function realHandleSuspended(fiber: Fiber, thenable: Promise<any>): void {
44
42
  // and component state survive across the suspension. The fallback is mounted
45
43
  // alongside the hidden primary until the pending promise resolves.
46
44
  //
47
- // We track the hidden subtree DOM in `state.hiddenDoms` (root host nodes +
45
+ // We track the hidden subtree DOM in `state.d` (root host nodes +
48
46
  // their original `display` so we can restore it) and the fallback as a
49
- // detached Fragment fiber in `state.fallbackFiber` (deliberately kept OUT of
47
+ // detached Fragment fiber in `state.f` (deliberately kept OUT of
50
48
  // `fiber.child` so reconciles against `props.children` don't trip on it).
51
49
  // First-mount suspensions have no committed DOM worth preserving, so they
52
50
  // keep the original unmount-and-render-fallback behavior.
53
51
  interface SuspenseState {
54
- suspended: boolean
55
- pending: Promise<any> | null
56
- hydrated?: boolean
57
- boundaryId?: number
58
- startMark?: Comment
59
- endMark?: Comment
60
- realChildren?: any
61
- _awaitingLazyHydration?: boolean
52
+ p?: Promise<any> | null
53
+ b?: Comment
54
+ e?: Comment
55
+ r?: any
56
+ a?: boolean
62
57
  // Re-suspend preservation:
63
- hiddenDoms: Array<[HTMLElement, string]> | null
64
- fallbackFiber: Fiber | null
58
+ d?: Array<[HTMLElement, string]> | null
59
+ f?: Fiber | null
65
60
  }
66
61
 
67
62
  function renderSuspense(fiber: Fiber, domParent: Node, anchor: Node | null): void {
68
- const props = fiber.pendingProps ?? {}
69
- const state = (fiber.memoizedState ??= {
70
- suspended: false,
71
- pending: null as Promise<any> | null,
72
- hiddenDoms: null,
73
- fallbackFiber: null,
74
- }) as SuspenseState
63
+ const props = fiber.pp ?? {}
64
+ const state = (fiber.ms ??= {}) as SuspenseState
75
65
 
76
66
  // Streaming hydration: if the next DOM node is a server-emitted boundary
77
67
  // marker, route through the boundary-aware hydration path.
78
68
  const root = getCurrentRoot()
79
- if (root?.hydrating && fiber.parent && !state.hydrated) {
80
- const boundary = tryConsumeBoundary(fiber.parent)
69
+ if (root?.h && !state.b) {
70
+ const boundary = tryConsumeBoundary(fiber.parent!)
81
71
  if (boundary) {
82
72
  hydrateSuspenseBoundary(fiber, props, boundary, domParent, anchor)
83
- state.hydrated = true
84
73
  return
85
74
  }
86
75
  }
@@ -91,8 +80,8 @@ function renderSuspense(fiber: Fiber, domParent: Node, anchor: Node | null): voi
91
80
  // yet. Until the Lazy's resume fires, skip our own tryChildren pass so
92
81
  // an unrelated re-render can't accidentally flip us into the suspended
93
82
  // path and mount a duplicate fallback on top of the SSR content.
94
- if (state._awaitingLazyHydration) {
95
- fiber.memoizedProps = props
83
+ if (state.a) {
84
+ fiber.mp = props
96
85
  return
97
86
  }
98
87
 
@@ -100,78 +89,62 @@ function renderSuspense(fiber: Fiber, domParent: Node, anchor: Node | null): voi
100
89
  // re-attempt primary children (would re-throw and churn the tree). Just
101
90
  // refresh the fallback in case its JSX changed, and wait for the pending
102
91
  // promise to fire scheduleUpdate.
103
- if (state.suspended && state.pending && state.fallbackFiber) {
104
- state.fallbackFiber.pendingProps = { children: props.fallback }
105
- renderFiber(state.fallbackFiber, domParent, anchor)
106
- fiber.memoizedProps = props
107
- return
108
- }
109
-
110
- // Initial-mount suspended path: no committed primary to preserve. Old
111
- // behavior — render fallback into fiber.child directly.
112
- if (state.suspended && state.pending && !state.fallbackFiber) {
113
- reconcileChildren(fiber, childrenToArray(props.fallback), domParent, anchor)
114
- fiber.memoizedProps = props
92
+ if (state.p) {
93
+ if (state.f) {
94
+ state.f.pp = { children: props.fallback }
95
+ renderFiber(state.f, domParent, anchor)
96
+ } else {
97
+ // Initial-mount suspended path: no committed primary to preserve.
98
+ reconcileChildren(fiber, childrenToArray(props.fallback), domParent, anchor)
99
+ }
100
+ fiber.mp = props
115
101
  return
116
102
  }
117
103
 
118
104
  // Snapshot whether we have an existing committed primary tree before
119
105
  // attempting the new render. If the new attempt suspends and we did have a
120
106
  // committed primary, we keep it (hidden) rather than destroying it.
121
- const hadCommittedPrimary = fiber.memoizedProps !== undefined && fiber.child !== null
107
+ const hadCommittedPrimary = fiber.mp !== undefined && fiber.child !== null
122
108
 
123
- const savedHandler = suspendHandlerStack[suspendHandlerStack.length - 1]
124
- let suspendedThisRender = false
125
- let suspendedThenable: Promise<any> | null = null
126
- suspendHandlerStack.push((thenable) => {
127
- suspendedThisRender = true
128
- suspendedThenable = thenable
129
- })
109
+ const prevHandler = suspendHandler
110
+ let pendingThenable: any
111
+ suspendHandler = (thenable) => {
112
+ pendingThenable = thenable
113
+ }
130
114
  try {
131
115
  reconcileChildren(fiber, childrenToArray(props.children), domParent, anchor)
132
116
  } finally {
133
- suspendHandlerStack.pop()
134
- void savedHandler
117
+ suspendHandler = prevHandler
135
118
  }
136
119
 
137
- if (suspendedThisRender && suspendedThenable) {
138
- state.suspended = true
139
- state.pending = suspendedThenable
120
+ if (pendingThenable) {
121
+ state.p = pendingThenable
140
122
  const onSettle = () => {
141
- state.suspended = false
142
- state.pending = null
123
+ state.p = null
143
124
  scheduleUpdate(fiber)
144
125
  }
145
- suspendedThenable.then(onSettle, onSettle)
126
+ pendingThenable.then(onSettle, onSettle)
146
127
 
147
128
  if (hadCommittedPrimary && fiber.child) {
148
129
  // Hide the primary subtree's root host doms so the fallback is the only
149
130
  // thing visible, but the underlying nodes (and their scroll/state/focus)
150
131
  // survive. Save original `display` for the resume path.
151
- const hostDoms: Node[] = []
132
+ const hidden: Array<[HTMLElement, string]> = []
152
133
  let c: Fiber | null = fiber.child
153
134
  while (c) {
154
- collectRootHostDoms(c, hostDoms)
135
+ hideRootHostDoms(c, hidden)
155
136
  c = c.sibling
156
137
  }
157
- const hidden: Array<[HTMLElement, string]> = []
158
- for (const d of hostDoms) {
159
- if (d.nodeType === 1) {
160
- const el = d as HTMLElement
161
- hidden.push([el, el.style.display])
162
- el.style.display = 'none'
163
- }
164
- }
165
- state.hiddenDoms = hidden
138
+ state.d = hidden
166
139
 
167
140
  // Mount fallback in a detached Fragment fiber. Kept off `fiber.child`
168
141
  // so reconciles of primary don't see it as a stale match candidate.
169
- if (!state.fallbackFiber) {
170
- state.fallbackFiber = createFiber(FiberTag.Fragment, null, null)
171
- state.fallbackFiber.parent = fiber
142
+ if (!state.f) {
143
+ state.f = createFiber(FiberTag.Fragment, null, null)
144
+ state.f.parent = fiber
172
145
  }
173
- state.fallbackFiber.pendingProps = { children: props.fallback }
174
- renderFiber(state.fallbackFiber, domParent, anchor)
146
+ state.f.pp = { children: props.fallback }
147
+ renderFiber(state.f, domParent, anchor)
175
148
  } else {
176
149
  // First-mount suspension — nothing to preserve.
177
150
  unmountAllChildren(fiber, domParent)
@@ -180,32 +153,36 @@ function renderSuspense(fiber: Fiber, domParent: Node, anchor: Node | null): voi
180
153
  } else {
181
154
  // Render succeeded. Clean up any preserved-suspend state from a prior
182
155
  // suspension cycle: unhide primary, unmount the orphan fallback fiber.
183
- if (state.hiddenDoms) {
184
- for (const [el, origDisplay] of state.hiddenDoms) {
156
+ if (state.d) {
157
+ for (const [el, origDisplay] of state.d) {
185
158
  el.style.display = origDisplay
186
159
  }
187
- state.hiddenDoms = null
160
+ state.d = null
188
161
  }
189
- if (state.fallbackFiber) {
190
- unmountFiber(state.fallbackFiber, domParent)
191
- state.fallbackFiber = null
162
+ if (state.f) {
163
+ unmountFiber(state.f, domParent)
164
+ state.f = null
192
165
  }
193
166
  }
194
- fiber.memoizedProps = props
167
+ fiber.mp = props
195
168
  }
196
169
 
197
170
  // Walk a fiber subtree collecting host/text DOM nodes that sit at the root
198
171
  // of the subtree (do not descend through their children — display:none on
199
172
  // the root hides the whole element). Used by the hide-on-suspend path.
200
- function collectRootHostDoms(fiber: Fiber, out: Node[]): void {
201
- if (fiber.tag === FiberTag.Host || fiber.tag === FiberTag.Text) {
202
- if (fiber.dom) out.push(fiber.dom)
173
+ function hideRootHostDoms(fiber: Fiber, out: Array<[HTMLElement, string]>): void {
174
+ if (fiber.tag === FiberTag.Host) {
175
+ const el = fiber.dom as HTMLElement | null
176
+ if (el) {
177
+ out.push([el, el.style.display])
178
+ el.style.display = 'none'
179
+ }
203
180
  return
204
181
  }
205
182
  if (fiber.tag === FiberTag.Portal) return
206
183
  let c = fiber.child
207
184
  while (c) {
208
- collectRootHostDoms(c, out)
185
+ hideRootHostDoms(c, out)
209
186
  c = c.sibling
210
187
  }
211
188
  }
@@ -213,82 +190,152 @@ function collectRootHostDoms(fiber: Fiber, out: Node[]): void {
213
190
  function hydrateSuspenseBoundary(
214
191
  fiber: Fiber,
215
192
  props: any,
216
- boundary: { kind: 'pending' | 'resolved'; id: number; startMark: Comment; endMark: Comment },
193
+ boundary: [0 | 1, number, Comment, Comment],
217
194
  domParent: Node,
218
195
  anchor: Node | null,
219
196
  ): void {
220
- const { kind, id, startMark, endMark } = boundary
197
+ const [pendingBoundary, id, startMark, endMark] = boundary
221
198
  // Record the boundary shape so we can re-hydrate on reveal.
222
- fiber.memoizedState = {
223
- suspended: false,
224
- pending: null,
225
- hydrated: true,
226
- boundaryId: id,
227
- startMark,
228
- endMark,
229
- realChildren: props.children,
199
+ fiber.ms = {
200
+ b: startMark,
201
+ e: endMark,
202
+ r: props.children,
230
203
  }
231
204
 
232
- if (kind === 'resolved') {
205
+ if (!pendingBoundary) {
233
206
  // Real DOM is inline between startMark and endMark. Hydrate into it.
234
- const cursor = new HydrationCursor(startMark.parentNode!, startMark.nextSibling, endMark)
235
- setHydrationCursor(fiber, cursor)
236
- reconcileChildren(fiber, childrenToArray(props.children), domParent, anchor)
237
- clearHydrationCursor(fiber)
238
- advanceCursorPast(fiber.parent!, endMark)
239
- fiber.memoizedProps = props
207
+ const parent = startMark.parentNode!
208
+ try {
209
+ setHydrationCursor(fiber, new HydrationCursor(parent, startMark.nextSibling, endMark))
210
+ reconcileChildren(fiber, childrenToArray(props.children), domParent, anchor)
211
+ advanceCursorPast(fiber.parent!, endMark)
212
+ } catch (e) {
213
+ if (!isHydrationBailout(e)) {
214
+ clearBoundaryRange(startMark, endMark)
215
+ unmountAllChildren(fiber, parent)
216
+ throw e
217
+ }
218
+ recoverBoundaryHydration(fiber, props.children, parent, startMark, endMark)
219
+ } finally {
220
+ clearHydrationCursor(fiber)
221
+ }
222
+ fiber.mp = props
240
223
  return
241
224
  }
242
225
 
243
226
  // Pending: fallback DOM lives inside <div id="B:ID">. Hydrate the fallback
244
227
  // React subtree against that div's children.
245
- const bDiv = (document as Document).getElementById(`B:${id}`)
246
- if (bDiv) {
247
- const cursor = new HydrationCursor(bDiv)
248
- setHydrationCursor(fiber, cursor)
249
- reconcileChildren(fiber, childrenToArray(props.fallback), domParent, anchor)
228
+ const bDiv = document.getElementById(`B:${id}`)
229
+ try {
230
+ if (bDiv) {
231
+ setHydrationCursor(fiber, new HydrationCursor(bDiv))
232
+ reconcileChildren(fiber, childrenToArray(props.fallback), domParent, anchor)
233
+ } else {
234
+ // Couldn't find fallback container — render fresh (non-adopting)
235
+ reconcileChildren(fiber, childrenToArray(props.fallback), domParent, anchor)
236
+ }
237
+ } catch (e) {
238
+ if (!isHydrationBailout(e) || !bDiv) throw e
239
+ recoverFallbackHydration(fiber, props.fallback, bDiv)
240
+ } finally {
250
241
  clearHydrationCursor(fiber)
251
- } else {
252
- // Couldn't find fallback container — render fresh (non-adopting)
253
- reconcileChildren(fiber, childrenToArray(props.fallback), domParent, anchor)
254
242
  }
255
243
  advanceCursorPast(fiber.parent!, endMark)
256
244
 
257
245
  // Register for server-streamed reveal (HTML chunks + $RC calls).
258
- const win = globalThis as any
259
- if (typeof win.$RH === 'function') {
260
- win.$RH(id, () => rehydrateBoundary(fiber))
246
+ ;(globalThis as any).$RH?.(id, () => rehydrateBoundary(fiber))
247
+ // If the inline runtime isn't present, nothing external will mark us dy.
248
+
249
+ fiber.mp = props
250
+ }
251
+
252
+ function recoverBoundaryHydration(
253
+ fiber: Fiber,
254
+ children: any,
255
+ parent: Node,
256
+ startMark: Comment,
257
+ endMark: Comment,
258
+ ): void {
259
+ const root = findRoot(fiber)!
260
+ const prevHydrating = root.h
261
+ discardPendingWork(root)
262
+ clearBoundaryRange(startMark, endMark)
263
+ unmountAllChildren(fiber, parent)
264
+ root.h = false
265
+ try {
266
+ reconcileChildren(fiber, childrenToArray(children), parent, endMark)
267
+ advanceCursorPast(fiber.parent!, endMark)
268
+ } catch (clientError) {
269
+ clearBoundaryRange(startMark, endMark)
270
+ unmountAllChildren(fiber, parent)
271
+ throw clientError
272
+ } finally {
273
+ root.h = prevHydrating
261
274
  }
262
- // If the inline runtime isn't present, nothing external will mark us dirty.
275
+ }
263
276
 
264
- fiber.memoizedProps = props
277
+ function recoverFallbackHydration(fiber: Fiber, fallback: any, parent: HTMLElement): void {
278
+ const root = findRoot(fiber)!
279
+ const prevHydrating = root.h
280
+ discardPendingWork(root)
281
+ parent.textContent = ''
282
+ unmountAllChildren(fiber, parent)
283
+ root.h = false
284
+ try {
285
+ reconcileChildren(fiber, childrenToArray(fallback), parent, null)
286
+ } catch (clientError) {
287
+ parent.textContent = ''
288
+ unmountAllChildren(fiber, parent)
289
+ throw clientError
290
+ } finally {
291
+ root.h = prevHydrating
292
+ }
265
293
  }
266
294
 
267
295
  function rehydrateBoundary(fiber: Fiber): void {
268
- const state = fiber.memoizedState
269
- if (!state || !state.startMark || !state.endMark) return
296
+ const state = fiber.ms
297
+ if (!state || !state.b || !state.e) return
270
298
 
271
299
  const root = findRoot(fiber)
272
- if (!root) return
273
- const parent = state.startMark.parentNode as Node
274
- if (!parent) return
300
+ const parent = state.b.parentNode as Node
301
+ if (!root || !parent) return
275
302
 
276
303
  // Unmount existing fallback subtree. Its DOM has already been removed by $RC
277
- // (or at least its container); unmounting here cleans up fibers + effects.
304
+ // (or at least its container); unmounting here cleans up fibers + fx.
278
305
  withCurrentRoot(root, () => {
279
- unmountAllChildren(fiber, parent)
306
+ const prevHydrating = root.h
307
+ try {
308
+ unmountAllChildren(fiber, parent)
280
309
 
281
- // Re-hydrate with real children against the now-real DOM range.
282
- root.hydrating = true
283
- const cursor = new HydrationCursor(parent, state.startMark.nextSibling, state.endMark)
284
- setHydrationCursor(fiber, cursor)
285
- reconcileChildren(fiber, childrenToArray(state.realChildren), parent, null)
286
- clearHydrationCursor(fiber)
287
- root.hydrating = false
310
+ // Re-hydrate with real children against the now-real DOM range.
311
+ root.h = true
312
+ setHydrationCursor(fiber, new HydrationCursor(parent, state.b.nextSibling, state.e))
313
+ reconcileChildren(fiber, childrenToArray(state.r), parent, null)
314
+ } catch (e) {
315
+ if (!isHydrationBailout(e)) {
316
+ clearBoundaryRange(state.b, state.e)
317
+ unmountAllChildren(fiber, parent)
318
+ throw e
319
+ }
320
+
321
+ recoverBoundaryHydration(fiber, state.r, parent, state.b, state.e)
322
+ } finally {
323
+ root.h = prevHydrating
324
+ clearHydrationCursor(fiber)
325
+ }
288
326
  runEffects(root)
289
327
  })
290
328
  }
291
329
 
330
+ function clearBoundaryRange(startMark: Comment, endMark: Comment): void {
331
+ let node = startMark.nextSibling
332
+ while (node && node !== endMark) {
333
+ const next = node.nextSibling
334
+ node.parentNode?.removeChild(node)
335
+ node = next
336
+ }
337
+ }
338
+
292
339
  registerTypeMatcher((type) => (type === REACT_SUSPENSE_TYPE ? FiberTag.Suspense : null))
293
340
  registerRenderer(FiberTag.Suspense, renderSuspense)
294
341
  installCapability('handleSuspended', realHandleSuspended)