@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.
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +7 -0
- package/dist/core/internal.d.ts +76 -0
- package/dist/core/internal.js +59 -0
- package/dist/core/internal.js.map +7 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +7 -0
- package/dist/dom/_all.d.ts +17 -0
- package/dist/dom/_all.js +51 -0
- package/dist/dom/_all.js.map +7 -0
- package/dist/dom/client.d.ts +3 -0
- package/dist/dom/client.js +8 -0
- package/dist/dom/client.js.map +7 -0
- package/dist/dom/dispatcher.d.ts +20 -0
- package/dist/dom/dispatcher.js +267 -0
- package/dist/dom/dispatcher.js.map +7 -0
- package/dist/dom/dom.d.ts +10 -0
- package/dist/dom/dom.js +262 -0
- package/dist/dom/dom.js.map +7 -0
- package/dist/dom/event-replay.d.ts +13 -0
- package/dist/dom/event-replay.js +46 -0
- package/dist/dom/event-replay.js.map +7 -0
- package/dist/dom/features/class/full.d.ts +1 -0
- package/dist/dom/features/class/full.js +91 -0
- package/dist/dom/features/class/full.js.map +7 -0
- package/dist/dom/features/class/index.d.ts +1 -0
- package/dist/dom/features/class/index.js +3 -0
- package/dist/dom/features/class/index.js.map +7 -0
- package/dist/dom/features/class/stub.d.ts +1 -0
- package/dist/dom/features/class/stub.js +50 -0
- package/dist/dom/features/class/stub.js.map +7 -0
- package/dist/dom/features/context/full.d.ts +1 -0
- package/dist/dom/features/context/full.js +49 -0
- package/dist/dom/features/context/full.js.map +7 -0
- package/dist/dom/features/context/index.d.ts +1 -0
- package/dist/dom/features/context/index.js +3 -0
- package/dist/dom/features/context/index.js.map +7 -0
- package/dist/dom/features/context/stub.d.ts +1 -0
- package/dist/dom/features/context/stub.js +22 -0
- package/dist/dom/features/context/stub.js.map +7 -0
- package/dist/dom/features/forward-ref/full.d.ts +1 -0
- package/dist/dom/features/forward-ref/full.js +51 -0
- package/dist/dom/features/forward-ref/full.js.map +7 -0
- package/dist/dom/features/forward-ref/index.d.ts +1 -0
- package/dist/dom/features/forward-ref/index.js +3 -0
- package/dist/dom/features/forward-ref/index.js.map +7 -0
- package/dist/dom/features/forward-ref/stub.d.ts +1 -0
- package/dist/dom/features/forward-ref/stub.js +22 -0
- package/dist/dom/features/forward-ref/stub.js.map +7 -0
- package/dist/dom/features/hydration/full.d.ts +61 -0
- package/dist/dom/features/hydration/full.js +264 -0
- package/dist/dom/features/hydration/full.js.map +7 -0
- package/dist/dom/features/hydration/index.d.ts +1 -0
- package/dist/dom/features/hydration/index.js +3 -0
- package/dist/dom/features/hydration/index.js.map +7 -0
- package/dist/dom/features/hydration/stub.d.ts +28 -0
- package/dist/dom/features/hydration/stub.js +66 -0
- package/dist/dom/features/hydration/stub.js.map +7 -0
- package/dist/dom/features/index.d.ts +7 -0
- package/dist/dom/features/index.js +9 -0
- package/dist/dom/features/index.js.map +7 -0
- package/dist/dom/features/lazy/full.d.ts +1 -0
- package/dist/dom/features/lazy/full.js +78 -0
- package/dist/dom/features/lazy/full.js.map +7 -0
- package/dist/dom/features/lazy/index.d.ts +1 -0
- package/dist/dom/features/lazy/index.js +3 -0
- package/dist/dom/features/lazy/index.js.map +7 -0
- package/dist/dom/features/lazy/stub.d.ts +1 -0
- package/dist/dom/features/lazy/stub.js +36 -0
- package/dist/dom/features/lazy/stub.js.map +7 -0
- package/dist/dom/features/memo/full.d.ts +1 -0
- package/dist/dom/features/memo/full.js +59 -0
- package/dist/dom/features/memo/full.js.map +7 -0
- package/dist/dom/features/memo/index.d.ts +1 -0
- package/dist/dom/features/memo/index.js +3 -0
- package/dist/dom/features/memo/index.js.map +7 -0
- package/dist/dom/features/memo/stub.d.ts +1 -0
- package/dist/dom/features/memo/stub.js +36 -0
- package/dist/dom/features/memo/stub.js.map +7 -0
- package/dist/dom/features/portal/full.d.ts +1 -0
- package/dist/dom/features/portal/full.js +19 -0
- package/dist/dom/features/portal/full.js.map +7 -0
- package/dist/dom/features/portal/index.d.ts +1 -0
- package/dist/dom/features/portal/index.js +3 -0
- package/dist/dom/features/portal/index.js.map +7 -0
- package/dist/dom/features/portal/stub.d.ts +1 -0
- package/dist/dom/features/portal/stub.js +7 -0
- package/dist/dom/features/portal/stub.js.map +7 -0
- package/dist/dom/features/suspense/full.d.ts +1 -0
- package/dist/dom/features/suspense/full.js +146 -0
- package/dist/dom/features/suspense/full.js.map +7 -0
- package/dist/dom/features/suspense/index.d.ts +1 -0
- package/dist/dom/features/suspense/index.js +3 -0
- package/dist/dom/features/suspense/index.js.map +7 -0
- package/dist/dom/features/suspense/stub.d.ts +1 -0
- package/dist/dom/features/suspense/stub.js +6 -0
- package/dist/dom/features/suspense/stub.js.map +7 -0
- package/dist/dom/index.d.ts +25 -0
- package/dist/dom/index.js +45 -0
- package/dist/dom/index.js.map +7 -0
- package/dist/dom/portal.d.ts +2 -0
- package/dist/dom/portal.js +15 -0
- package/dist/dom/portal.js.map +7 -0
- package/dist/dom/reconcile.d.ts +40 -0
- package/dist/dom/reconcile.js +795 -0
- package/dist/dom/reconcile.js.map +7 -0
- package/dist/dom/root.d.ts +15 -0
- package/dist/dom/root.js +85 -0
- package/dist/dom/root.js.map +7 -0
- package/dist/dom/test-utils.d.ts +1 -0
- package/dist/dom/test-utils.js +10 -0
- package/dist/dom/test-utils.js.map +7 -0
- package/dist/react/children.d.ts +8 -0
- package/dist/react/children.js +75 -0
- package/dist/react/children.js.map +7 -0
- package/dist/react/class.d.ts +31 -0
- package/dist/react/class.js +40 -0
- package/dist/react/class.js.map +7 -0
- package/dist/react/context.d.ts +27 -0
- package/dist/react/context.js +31 -0
- package/dist/react/context.js.map +7 -0
- package/dist/react/element.d.ts +10 -0
- package/dist/react/element.js +64 -0
- package/dist/react/element.js.map +7 -0
- package/dist/react/hooks.d.ts +37 -0
- package/dist/react/hooks.js +146 -0
- package/dist/react/hooks.js.map +7 -0
- package/dist/react/index.d.ts +76 -0
- package/dist/react/index.js +187 -0
- package/dist/react/index.js.map +7 -0
- package/dist/react/jsx-runtime.d.ts +5 -0
- package/dist/react/jsx-runtime.js +21 -0
- package/dist/react/jsx-runtime.js.map +7 -0
- package/dist/react/memo.d.ts +29 -0
- package/dist/react/memo.js +47 -0
- package/dist/react/memo.js.map +7 -0
- package/dist/react/portal.d.ts +1 -0
- package/dist/react/portal.js +6 -0
- package/dist/react/portal.js.map +7 -0
- package/dist/react/shared-internals.d.ts +33 -0
- package/dist/react/shared-internals.js +29 -0
- package/dist/react/shared-internals.js.map +7 -0
- package/dist/react/suspense.d.ts +15 -0
- package/dist/react/suspense.js +16 -0
- package/dist/react/suspense.js.map +7 -0
- package/dist/scheduler/index.d.ts +44 -0
- package/dist/scheduler/index.js +109 -0
- package/dist/scheduler/index.js.map +7 -0
- package/dist/server/bootstrap-script.d.ts +22 -0
- package/dist/server/bootstrap-script.js +16 -0
- package/dist/server/bootstrap-script.js.map +7 -0
- package/dist/server/dispatcher.d.ts +37 -0
- package/dist/server/dispatcher.js +140 -0
- package/dist/server/dispatcher.js.map +7 -0
- package/dist/server/escape.d.ts +7 -0
- package/dist/server/escape.js +159 -0
- package/dist/server/escape.js.map +7 -0
- package/dist/server/index.d.ts +16 -0
- package/dist/server/index.js +26 -0
- package/dist/server/index.js.map +7 -0
- package/dist/server/renderToString.d.ts +7 -0
- package/dist/server/renderToString.js +29 -0
- package/dist/server/renderToString.js.map +7 -0
- package/dist/server/stream.d.ts +40 -0
- package/dist/server/stream.js +200 -0
- package/dist/server/stream.js.map +7 -0
- package/dist/server/walk.d.ts +33 -0
- package/dist/server/walk.js +298 -0
- package/dist/server/walk.js.map +7 -0
- package/dist/vite/index.d.ts +96 -0
- package/dist/vite/index.js +217 -0
- package/dist/vite/index.js.map +7 -0
- package/package.json +83 -0
- package/src/core/index.ts +2 -0
- package/src/core/internal.ts +107 -0
- package/src/core/types.ts +36 -0
- package/src/dom/_all.ts +54 -0
- package/src/dom/client.ts +4 -0
- package/src/dom/dispatcher.ts +318 -0
- package/src/dom/dom.ts +316 -0
- package/src/dom/event-replay.ts +55 -0
- package/src/dom/features/class/full.ts +104 -0
- package/src/dom/features/class/index.ts +1 -0
- package/src/dom/features/class/stub.ts +65 -0
- package/src/dom/features/context/full.ts +57 -0
- package/src/dom/features/context/index.ts +1 -0
- package/src/dom/features/context/stub.ts +32 -0
- package/src/dom/features/forward-ref/full.ts +54 -0
- package/src/dom/features/forward-ref/index.ts +1 -0
- package/src/dom/features/forward-ref/stub.ts +28 -0
- package/src/dom/features/hydration/full.ts +345 -0
- package/src/dom/features/hydration/index.ts +1 -0
- package/src/dom/features/hydration/stub.ts +72 -0
- package/src/dom/features/index.ts +11 -0
- package/src/dom/features/lazy/full.ts +98 -0
- package/src/dom/features/lazy/index.ts +1 -0
- package/src/dom/features/lazy/stub.ts +45 -0
- package/src/dom/features/memo/full.ts +74 -0
- package/src/dom/features/memo/index.ts +1 -0
- package/src/dom/features/memo/stub.ts +42 -0
- package/src/dom/features/portal/full.ts +22 -0
- package/src/dom/features/portal/index.ts +1 -0
- package/src/dom/features/portal/stub.ts +11 -0
- package/src/dom/features/suspense/full.ts +191 -0
- package/src/dom/features/suspense/index.ts +1 -0
- package/src/dom/features/suspense/stub.ts +12 -0
- package/src/dom/index.ts +33 -0
- package/src/dom/portal.ts +16 -0
- package/src/dom/reconcile.ts +1169 -0
- package/src/dom/root.ts +107 -0
- package/src/dom/test-utils.ts +6 -0
- package/src/react/children.ts +85 -0
- package/src/react/class.ts +56 -0
- package/src/react/context.ts +50 -0
- package/src/react/element.ts +80 -0
- package/src/react/hooks.ts +190 -0
- package/src/react/index.ts +132 -0
- package/src/react/jsx-runtime.ts +16 -0
- package/src/react/memo.ts +45 -0
- package/src/react/portal.ts +1 -0
- package/src/react/shared-internals.ts +79 -0
- package/src/react/suspense.ts +18 -0
- package/src/scheduler/index.ts +124 -0
- package/src/server/bootstrap-script.ts +52 -0
- package/src/server/dispatcher.ts +158 -0
- package/src/server/escape.ts +187 -0
- package/src/server/index.ts +26 -0
- package/src/server/renderToString.ts +33 -0
- package/src/server/stream.ts +302 -0
- package/src/server/walk.ts +448 -0
- package/src/vite/index.ts +401 -0
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FiberTag,
|
|
3
|
+
FiberFlag,
|
|
4
|
+
createFiber,
|
|
5
|
+
REACT_ELEMENT_TYPE,
|
|
6
|
+
REACT_LEGACY_ELEMENT_TYPE,
|
|
7
|
+
REACT_FRAGMENT_TYPE,
|
|
8
|
+
type Fiber,
|
|
9
|
+
type FiberRoot,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
type Hook,
|
|
13
|
+
type Effect,
|
|
14
|
+
} from '../core'
|
|
15
|
+
import {
|
|
16
|
+
ReactSharedInternals,
|
|
17
|
+
REACT_LAZY_TYPE,
|
|
18
|
+
REACT_STRICT_MODE_TYPE,
|
|
19
|
+
REACT_PROFILER_TYPE,
|
|
20
|
+
} from '../react'
|
|
21
|
+
import { createHostNode, setProp } from './dom'
|
|
22
|
+
import { makeDispatcher } from './dispatcher'
|
|
23
|
+
import {
|
|
24
|
+
adoptHostDom,
|
|
25
|
+
adoptTextDom,
|
|
26
|
+
tryConsumeBoundary,
|
|
27
|
+
advanceCursorPast,
|
|
28
|
+
setHydrationCursor,
|
|
29
|
+
getHydrationCursor,
|
|
30
|
+
clearHydrationCursor,
|
|
31
|
+
HydrationCursor,
|
|
32
|
+
findHostParent as findHydrationHost,
|
|
33
|
+
} from './features/hydration'
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Render scheduling
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
let currentRoot: FiberRoot | null = null
|
|
40
|
+
let flushing = false
|
|
41
|
+
let isBatching = false
|
|
42
|
+
const pendingRoots = new Set<FiberRoot>()
|
|
43
|
+
|
|
44
|
+
// Set by rerenderFiber to identify the exact memo-tagged fiber whose INTERNAL
|
|
45
|
+
// state (hook update, useSyncExternalStore notification) triggered this render
|
|
46
|
+
// pass. renderMemo checks this to bypass its prop-equality gate for that fiber.
|
|
47
|
+
// Without the bypass, a memo bail would swallow state changes: React's memo is
|
|
48
|
+
// only a parent-triggered gate — state-driven rerenders must always run the
|
|
49
|
+
// inner function. Router-adjacent components (Outlet, Match, MatchInner) are
|
|
50
|
+
// all memo-wrapped and subscribe to stores; missing this bypass breaks nav
|
|
51
|
+
// content updates even though the URL changes.
|
|
52
|
+
let forceRerenderingFiber: Fiber | null = null
|
|
53
|
+
|
|
54
|
+
export function scheduleUpdate(fiber: Fiber): void {
|
|
55
|
+
// Drop updates scheduled on already-unmounted fibers. Subscribers (router,
|
|
56
|
+
// query, any external store) can fire after unmount if their cleanup was
|
|
57
|
+
// missed, and letting those reach rerenderFiber mounts zombie DOM into the
|
|
58
|
+
// old .parent's DOM (which stays reachable via the stale pointer).
|
|
59
|
+
if (fiber.unmounted) return
|
|
60
|
+
const root = findRoot(fiber)
|
|
61
|
+
if (!root) return
|
|
62
|
+
root.pending.add(fiber)
|
|
63
|
+
fiber.dirty = true
|
|
64
|
+
pendingRoots.add(root)
|
|
65
|
+
if (isBatching) return
|
|
66
|
+
if (!root.scheduled) {
|
|
67
|
+
root.scheduled = true
|
|
68
|
+
queueMicrotask(flushPending)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function flushSyncWork(fn: () => void): void {
|
|
73
|
+
const wasBatching = isBatching
|
|
74
|
+
isBatching = true
|
|
75
|
+
try {
|
|
76
|
+
fn()
|
|
77
|
+
} finally {
|
|
78
|
+
isBatching = wasBatching
|
|
79
|
+
}
|
|
80
|
+
flushPending()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function batchedUpdates<T>(fn: () => T): T {
|
|
84
|
+
const wasBatching = isBatching
|
|
85
|
+
isBatching = true
|
|
86
|
+
try {
|
|
87
|
+
return fn()
|
|
88
|
+
} finally {
|
|
89
|
+
isBatching = wasBatching
|
|
90
|
+
if (!wasBatching) flushPending()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function flushPending(): void {
|
|
95
|
+
if (flushing) return
|
|
96
|
+
flushing = true
|
|
97
|
+
try {
|
|
98
|
+
let guard = 0
|
|
99
|
+
while (pendingRoots.size > 0) {
|
|
100
|
+
if (++guard > 50) {
|
|
101
|
+
throw new Error('flushPending exceeded 50 iterations — suspected infinite update loop.')
|
|
102
|
+
}
|
|
103
|
+
const roots = [...pendingRoots]
|
|
104
|
+
pendingRoots.clear()
|
|
105
|
+
for (const root of roots) {
|
|
106
|
+
root.scheduled = false
|
|
107
|
+
// Render each pending fiber from shallowest first so an ancestor's
|
|
108
|
+
// cascade reaches descendants before we try to render them directly.
|
|
109
|
+
// Descendants rendered via cascade still have `dirty=true` (only
|
|
110
|
+
// rerenderFiber clears it); when we later reach them in this loop,
|
|
111
|
+
// rerenderFiber's own `if (!dirty) return` is our short-circuit. We
|
|
112
|
+
// previously filtered descendants of dirty ancestors here, but that
|
|
113
|
+
// loses updates whenever an ancestor's render doesn't actually reach
|
|
114
|
+
// the descendant — e.g. React.memo bailing on equal props. Keep all
|
|
115
|
+
// dirty fibers and let rerenderFiber de-dupe via its dirty check.
|
|
116
|
+
const pending = [...root.pending]
|
|
117
|
+
root.pending.clear()
|
|
118
|
+
pending.sort((a, b) => fiberDepth(a) - fiberDepth(b))
|
|
119
|
+
for (const fiber of pending) {
|
|
120
|
+
rerenderFiber(fiber, root)
|
|
121
|
+
}
|
|
122
|
+
runEffects(root)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
flushing = false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fiberDepth(fiber: Fiber): number {
|
|
131
|
+
let d = 0
|
|
132
|
+
let p: Fiber | null = fiber.parent
|
|
133
|
+
while (p) {
|
|
134
|
+
d++
|
|
135
|
+
p = p.parent
|
|
136
|
+
}
|
|
137
|
+
return d
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function findRoot(fiber: Fiber): FiberRoot | null {
|
|
141
|
+
let f: Fiber | null = fiber
|
|
142
|
+
while (f) {
|
|
143
|
+
if (f.root) return f.root
|
|
144
|
+
f = f.parent
|
|
145
|
+
}
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Entry points (called by createRoot)
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
export function renderRoot(root: FiberRoot, children: ReactNode): void {
|
|
154
|
+
const rootFiber = root.current
|
|
155
|
+
rootFiber.pendingProps = { children }
|
|
156
|
+
currentRoot = root
|
|
157
|
+
try {
|
|
158
|
+
reconcileChildren(rootFiber, childrenToArray(children), root.container as Node, null)
|
|
159
|
+
rootFiber.memoizedProps = rootFiber.pendingProps
|
|
160
|
+
rootFiber.dirty = false
|
|
161
|
+
} finally {
|
|
162
|
+
currentRoot = null
|
|
163
|
+
}
|
|
164
|
+
runEffects(root)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function rerenderFiber(fiber: Fiber, root: FiberRoot): void {
|
|
168
|
+
if (!fiber.dirty) return
|
|
169
|
+
// Skip fibers that were unmounted between scheduling and flush. Without this,
|
|
170
|
+
// the flush loop re-enters a zombie fiber whose .parent is still set; its
|
|
171
|
+
// render mounts fresh DOM into the old parent's still-attached DOM (since
|
|
172
|
+
// unmountFiber only clears fiber.child, not fiber.parent). Visible as route
|
|
173
|
+
// content from a previous location staying on screen after nav, because a
|
|
174
|
+
// pending rerender on the old route's LibraryLandingPage (unmounted during
|
|
175
|
+
// Outlet's shallow-first render) still fires from root.pending.
|
|
176
|
+
if (fiber.unmounted) return
|
|
177
|
+
// Clear BEFORE rendering so a scheduleUpdate() triggered mid-render (e.g.
|
|
178
|
+
// error boundary catching a descendant throw) marks us dirty for the next
|
|
179
|
+
// flush iteration instead of being wiped out when render() completes.
|
|
180
|
+
fiber.dirty = false
|
|
181
|
+
currentRoot = root
|
|
182
|
+
// If this rerender is resuming a hydration that was deferred by a suspension,
|
|
183
|
+
// re-activate hydration mode for its duration so descendants adopt DOM
|
|
184
|
+
// instead of re-creating it.
|
|
185
|
+
const resumeHydration =
|
|
186
|
+
fiber.memoizedState && (fiber.memoizedState as any)._pendingHydration === true
|
|
187
|
+
const prevHydrating = root.hydrating
|
|
188
|
+
if (resumeHydration) {
|
|
189
|
+
delete (fiber.memoizedState as any)._pendingHydration
|
|
190
|
+
root.hydrating = true
|
|
191
|
+
}
|
|
192
|
+
const prevForcing = forceRerenderingFiber
|
|
193
|
+
forceRerenderingFiber = fiber
|
|
194
|
+
try {
|
|
195
|
+
renderFiber(fiber, getHostParent(fiber), getAnchor(fiber))
|
|
196
|
+
} finally {
|
|
197
|
+
forceRerenderingFiber = prevForcing
|
|
198
|
+
if (resumeHydration) {
|
|
199
|
+
root.hydrating = prevHydrating
|
|
200
|
+
// Deferred hydration completed — detach the preserved cursor so future
|
|
201
|
+
// updates (post-hydration state changes) don't try to adopt stale DOM.
|
|
202
|
+
clearHydrationCursor(fiber)
|
|
203
|
+
}
|
|
204
|
+
currentRoot = null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Element → children normalization
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
type TextChild = { _text: string }
|
|
213
|
+
type NormalizedChild = ReactElement | TextChild | null
|
|
214
|
+
|
|
215
|
+
// NEVER use `'_text' in child` to distinguish text wrappers from elements.
|
|
216
|
+
// TanStack's RSC renderable proxies (createRscProxy with renderable: true) are
|
|
217
|
+
// Proxy wrappers around real React elements whose `has` trap returns `true`
|
|
218
|
+
// for ANY string key — so `'_text' in rscProxy` is TRUE even though the proxy
|
|
219
|
+
// is an element. That misidentification set a Text fiber's `pendingProps` to
|
|
220
|
+
// `child._text` (another chained RSC proxy), which then rendered as
|
|
221
|
+
// `[object Object]` when createTextNode stringified the element. React
|
|
222
|
+
// elements always carry `$$typeof`; our text wrapper never does — so the
|
|
223
|
+
// presence of `$$typeof` is the invariant we rely on.
|
|
224
|
+
function isTextChild(child: Exclude<NormalizedChild, null>): child is TextChild {
|
|
225
|
+
return (child as any).$$typeof === undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function childrenToArray(children: ReactNode): NormalizedChild[] {
|
|
229
|
+
const out: NormalizedChild[] = []
|
|
230
|
+
pushChildren(children, out)
|
|
231
|
+
return out
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function pushChildren(node: ReactNode, out: NormalizedChild[]): void {
|
|
235
|
+
if (node == null || typeof node === 'boolean') return
|
|
236
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
237
|
+
// Empty strings render no text node (matches React + the `<!-- -->`
|
|
238
|
+
// separator elision on the SSR side so server/client agree).
|
|
239
|
+
if (node === '') return
|
|
240
|
+
out.push({ _text: '' + node })
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (Array.isArray(node)) {
|
|
244
|
+
for (let i = 0; i < node.length; i++) pushChildren(node[i], out)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (isIterable(node)) {
|
|
248
|
+
for (const item of node as Iterable<ReactNode>) pushChildren(item, out)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (typeof node === 'object') {
|
|
252
|
+
const t = (node as any).$$typeof
|
|
253
|
+
if (ACCEPTED_ELEMENT_MARKERS.has(t)) {
|
|
254
|
+
out.push(node as ReactElement)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
// Raw React.lazy as a child. RSC Flight encodes 'use client' components
|
|
258
|
+
// (CodeBlock, CodeExplorer, etc.) as bare Lazy objects in the tree, not
|
|
259
|
+
// wrapped in REACT_ELEMENT_TYPE. Dropping them made code snippets
|
|
260
|
+
// disappear from docs pages. The RSC decoder pre-awaits payloads via
|
|
261
|
+
// `awaitLazyElements`, so by render time the status is 'fulfilled' and
|
|
262
|
+
// `_init()` returns the resolved element synchronously.
|
|
263
|
+
if (t === REACT_LAZY_TYPE) {
|
|
264
|
+
const lazy = node as any
|
|
265
|
+
const resolved = lazy._init(lazy._payload)
|
|
266
|
+
pushChildren(resolved, out)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isIterable(obj: any): boolean {
|
|
273
|
+
return obj != null && typeof obj !== 'string' && typeof obj[Symbol.iterator] === 'function'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getKeyOf(child: NormalizedChild, index: number): string {
|
|
277
|
+
if (!child) return 'n' + index
|
|
278
|
+
if (isTextChild(child)) return '$t' + index
|
|
279
|
+
if (child.key != null) return 'k' + child.key
|
|
280
|
+
return 'i' + index
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function sameType(fiber: Fiber, child: NormalizedChild): boolean {
|
|
284
|
+
if (!child) return false
|
|
285
|
+
if (isTextChild(child)) return fiber.tag === FiberTag.Text
|
|
286
|
+
return fiber.type === child.type && sameKey(fiber.key, child.key)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sameKey(a: string | null, b: string | null | undefined): boolean {
|
|
290
|
+
return (a ?? null) === (b ?? null)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Fiber creation
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
function fiberFromChild(child: NormalizedChild, parent: Fiber): Fiber {
|
|
298
|
+
if (!child) return createFiber(FiberTag.Fragment, null, null)
|
|
299
|
+
if (isTextChild(child)) {
|
|
300
|
+
const f = createFiber(FiberTag.Text, null, null)
|
|
301
|
+
f.pendingProps = child._text
|
|
302
|
+
f.parent = parent
|
|
303
|
+
return f
|
|
304
|
+
}
|
|
305
|
+
const type = child.type
|
|
306
|
+
let tag: FiberTag = FiberTag.Host
|
|
307
|
+
const marker = type && (type as any).$$typeof
|
|
308
|
+
if (typeof type === 'string') tag = FiberTag.Host
|
|
309
|
+
else if (type === REACT_FRAGMENT_TYPE) tag = FiberTag.Fragment
|
|
310
|
+
else if (type === REACT_STRICT_MODE_TYPE || type === REACT_PROFILER_TYPE) tag = FiberTag.Fragment
|
|
311
|
+
else {
|
|
312
|
+
// Feature-registered type matchers (Portal, future extractions). Features
|
|
313
|
+
// that carry the symbol as element.type directly (rather than wrapping in
|
|
314
|
+
// REACT_ELEMENT_TYPE) match here by type identity.
|
|
315
|
+
let matched: FiberTag | null = null
|
|
316
|
+
for (const m of TYPE_MATCHERS) {
|
|
317
|
+
matched = m(type, marker)
|
|
318
|
+
if (matched !== null) break
|
|
319
|
+
}
|
|
320
|
+
if (matched !== null) tag = matched
|
|
321
|
+
else if (typeof type === 'function') {
|
|
322
|
+
tag = type.prototype && type.prototype.isReactComponent ? FiberTag.Class : FiberTag.Function
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const f = createFiber(tag, type, child.key ?? null)
|
|
326
|
+
f.ref = (child as any).ref ?? null
|
|
327
|
+
f.pendingProps = child.props
|
|
328
|
+
f.parent = parent
|
|
329
|
+
return f
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Reconciliation
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Reconcile a parent fiber's child list against new normalized children.
|
|
338
|
+
* Mutates parent.child and the sibling chain.
|
|
339
|
+
* Mounts new host DOM into `domParent` before `anchor` (or appends if anchor === null).
|
|
340
|
+
*/
|
|
341
|
+
export function reconcileChildren(
|
|
342
|
+
parent: Fiber,
|
|
343
|
+
newChildren: NormalizedChild[],
|
|
344
|
+
domParent: Node,
|
|
345
|
+
anchor: Node | null,
|
|
346
|
+
): void {
|
|
347
|
+
const existing = collectChildren(parent)
|
|
348
|
+
const keyed = new Map<string, Fiber>()
|
|
349
|
+
for (const f of existing) {
|
|
350
|
+
if (f.key != null) keyed.set('k' + f.key, f)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let prevNewFiber: Fiber | null = null
|
|
354
|
+
const claimed = new Set<Fiber>()
|
|
355
|
+
let structurallyChanged = false
|
|
356
|
+
// Budget-guided positional matching. We walk `existing` (unkeyed only) with a
|
|
357
|
+
// single cursor `existingIdx` and, on a type mismatch, choose insert vs delete
|
|
358
|
+
// based on the remaining length delta (`budget`):
|
|
359
|
+
// budget > 0: more new than old remain → treat slot as an INSERTION: keep
|
|
360
|
+
// the old cursor and create a fresh fiber for new[i].
|
|
361
|
+
// budget < 0: more old than new remain → treat slot as a DELETION: advance
|
|
362
|
+
// the old cursor past the mismatched fiber (it'll be unmounted
|
|
363
|
+
// in the unclaimed pass) and retry.
|
|
364
|
+
// budget == 0: equal remaining → treat as REPLACE by preferring delete
|
|
365
|
+
// until budget flips positive or we hit a match.
|
|
366
|
+
// This avoids greedy forward scans that steal a later same-type fiber for a
|
|
367
|
+
// newly inserted leading sibling (e.g. smallMenu flipping null → <div>
|
|
368
|
+
// stealing the content <div>'s fiber and tearing down the drawer fragment).
|
|
369
|
+
let existingIdx = 0
|
|
370
|
+
let unkeyedOld = 0
|
|
371
|
+
for (const f of existing) if (f.key == null) unkeyedOld++
|
|
372
|
+
let unkeyedNew = 0
|
|
373
|
+
for (const c of newChildren) if (c != null) unkeyedNew++
|
|
374
|
+
let budget = unkeyedNew - unkeyedOld
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < newChildren.length; i++) {
|
|
377
|
+
const child = newChildren[i]
|
|
378
|
+
if (child == null) continue
|
|
379
|
+
|
|
380
|
+
let match: Fiber | null = null
|
|
381
|
+
|
|
382
|
+
// key-based match
|
|
383
|
+
if (child && typeof child === 'object' && !isTextChild(child) && (child as ReactElement).key != null) {
|
|
384
|
+
const k = 'k' + (child as ReactElement).key
|
|
385
|
+
const m = keyed.get(k)
|
|
386
|
+
if (m && m.type === (child as ReactElement).type) {
|
|
387
|
+
match = m
|
|
388
|
+
keyed.delete(k)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!match) {
|
|
393
|
+
while (existingIdx < existing.length) {
|
|
394
|
+
const cand = existing[existingIdx]!
|
|
395
|
+
if (claimed.has(cand) || cand.key != null) {
|
|
396
|
+
existingIdx++
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
if (sameType(cand, child)) {
|
|
400
|
+
match = cand
|
|
401
|
+
existingIdx++
|
|
402
|
+
break
|
|
403
|
+
}
|
|
404
|
+
// Type mismatch at the cursor. Resolve via budget.
|
|
405
|
+
if (budget > 0) {
|
|
406
|
+
// Insertion: leave cand in place, create new for child.
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
// Deletion (or replace-as-delete-first): advance past cand. It remains
|
|
410
|
+
// unclaimed and will be unmounted at the end.
|
|
411
|
+
existingIdx++
|
|
412
|
+
budget++
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Detect reorder: matched fiber is not at its original position
|
|
417
|
+
if (match && existing[i] !== match) structurallyChanged = true
|
|
418
|
+
|
|
419
|
+
let fiber: Fiber
|
|
420
|
+
if (match) {
|
|
421
|
+
claimed.add(match)
|
|
422
|
+
fiber = match
|
|
423
|
+
if (isTextChild(child!)) {
|
|
424
|
+
fiber.pendingProps = child._text
|
|
425
|
+
} else {
|
|
426
|
+
fiber.type = (child as ReactElement).type
|
|
427
|
+
fiber.pendingProps = (child as ReactElement).props
|
|
428
|
+
fiber.ref = (child as any).ref ?? null
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
fiber = fiberFromChild(child, parent)
|
|
432
|
+
structurallyChanged = true
|
|
433
|
+
if (budget > 0) budget--
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fiber.parent = parent
|
|
437
|
+
fiber.sibling = null
|
|
438
|
+
if (prevNewFiber) prevNewFiber.sibling = fiber
|
|
439
|
+
else parent.child = fiber
|
|
440
|
+
prevNewFiber = fiber
|
|
441
|
+
|
|
442
|
+
// Render this fiber (mount or update)
|
|
443
|
+
renderFiber(fiber, domParent, anchor)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!prevNewFiber) parent.child = null
|
|
447
|
+
else prevNewFiber.sibling = null
|
|
448
|
+
|
|
449
|
+
// Head content is additive — server may inject metadata/stylesheets (Vite
|
|
450
|
+
// dev styles, Sentry, analytics) that aren't in the React tree. Unmounting
|
|
451
|
+
// them on every reconcile thrashes styles and causes flash of unstyled
|
|
452
|
+
// content. Keep existing head children that weren't matched this pass.
|
|
453
|
+
const parentIsHeadHost =
|
|
454
|
+
parent.tag === FiberTag.Host &&
|
|
455
|
+
typeof parent.type === 'string' &&
|
|
456
|
+
(parent.type as string).toLowerCase() === 'head'
|
|
457
|
+
|
|
458
|
+
if (!parentIsHeadHost) {
|
|
459
|
+
// Unmount unclaimed
|
|
460
|
+
for (const f of existing) {
|
|
461
|
+
if (!claimed.has(f)) {
|
|
462
|
+
unmountFiber(f, domParent)
|
|
463
|
+
structurallyChanged = true
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Leftover keyed
|
|
467
|
+
for (const f of keyed.values()) {
|
|
468
|
+
if (!claimed.has(f)) {
|
|
469
|
+
unmountFiber(f, domParent)
|
|
470
|
+
structurallyChanged = true
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// During hydration, DOM is already in document order from the cursor-driven
|
|
476
|
+
// adoption walk. Running placeChildrenInOrder here would reappend nodes to
|
|
477
|
+
// the end of domParent when the true anchor (often an end marker comment)
|
|
478
|
+
// isn't reflected in `anchor`. Skip it in hydration mode.
|
|
479
|
+
//
|
|
480
|
+
// For <head>, skip always — HeadContent re-renders routinely (route match
|
|
481
|
+
// changes, providers updating), and reordering every <link>/<style>/<meta>
|
|
482
|
+
// on each re-render causes stylesheet flash and re-download. Head element
|
|
483
|
+
// ordering is semantically fluid; the browser doesn't care about exact
|
|
484
|
+
// order within <head>.
|
|
485
|
+
const parentIsHead =
|
|
486
|
+
(domParent as Element).nodeType === 1 &&
|
|
487
|
+
(domParent as Element).tagName.toLowerCase() === 'head'
|
|
488
|
+
if (structurallyChanged && !currentRoot?.hydrating && !parentIsHead) {
|
|
489
|
+
placeChildrenInOrder(parent, domParent, anchor)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function placeChildrenInOrder(parent: Fiber, domParent: Node, anchor: Node | null): void {
|
|
494
|
+
const doms: Node[] = []
|
|
495
|
+
let c = parent.child
|
|
496
|
+
while (c) {
|
|
497
|
+
collectHostDoms(c, doms)
|
|
498
|
+
c = c.sibling
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Pre-check: if our fiber-owned DOM is already in document order within
|
|
502
|
+
// domParent AND the end anchor matches, no reorder is needed. This is the
|
|
503
|
+
// common case on stable re-renders, and avoids detaching/re-attaching
|
|
504
|
+
// subtrees (which cancels CSS animations and triggers layout).
|
|
505
|
+
if (doms.length > 0) {
|
|
506
|
+
let current: Node | null = doms[0]!
|
|
507
|
+
let inOrder = current.parentNode === domParent
|
|
508
|
+
for (let i = 1; inOrder && i < doms.length; i++) {
|
|
509
|
+
current = current!.nextSibling
|
|
510
|
+
// Skip foreign nodes (SSR-injected scripts, dev-styles) between owned
|
|
511
|
+
// fiber DOMs — they should stay where they are.
|
|
512
|
+
while (current && !doms.includes(current as Node)) {
|
|
513
|
+
current = current.nextSibling
|
|
514
|
+
}
|
|
515
|
+
if (current !== doms[i]) inOrder = false
|
|
516
|
+
}
|
|
517
|
+
if (inOrder) return
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Reverse-iterate, anchoring each node before the one that should follow it.
|
|
521
|
+
// This works because by the time we're placing doms[i], doms[i+1] is already
|
|
522
|
+
// in its final slot. Forward iteration is buggy: insertBefore(doms[i],
|
|
523
|
+
// doms[i+1]) pulls doms[i] forward past any nodes that SHOULD move behind
|
|
524
|
+
// it, leaving those nodes mis-anchored (app-starter Analyze/Lucky swap, npm
|
|
525
|
+
// stats library dropdown reorder — both reported by users).
|
|
526
|
+
//
|
|
527
|
+
// Concrete example: start=[A, R, L], target=[A, L, R]. Forward pass gives
|
|
528
|
+
// [L, A, R] (wrong). Reverse pass moves R to end, then L and A are already
|
|
529
|
+
// correct — 1 move, matches target.
|
|
530
|
+
//
|
|
531
|
+
// Skip nodes already in their target position so CSS transitions on stable
|
|
532
|
+
// siblings aren't cancelled (e.g. drawer slide animation).
|
|
533
|
+
for (let i = doms.length - 1; i >= 0; i--) {
|
|
534
|
+
const d = doms[i]!
|
|
535
|
+
const targetNext: Node | null = i + 1 < doms.length ? doms[i + 1]! : anchor
|
|
536
|
+
if (d.parentNode !== domParent || d.nextSibling !== targetNext) {
|
|
537
|
+
domParent.insertBefore(d, targetNext)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function collectHostDoms(fiber: Fiber, out: Node[]): void {
|
|
543
|
+
if (fiber.tag === FiberTag.Host || fiber.tag === FiberTag.Text) {
|
|
544
|
+
if (fiber.dom) out.push(fiber.dom)
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
if (fiber.tag === FiberTag.Portal) return
|
|
548
|
+
let c = fiber.child
|
|
549
|
+
while (c) {
|
|
550
|
+
collectHostDoms(c, out)
|
|
551
|
+
c = c.sibling
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function collectChildren(parent: Fiber): Fiber[] {
|
|
556
|
+
const out: Fiber[] = []
|
|
557
|
+
let c = parent.child
|
|
558
|
+
while (c) {
|
|
559
|
+
out.push(c)
|
|
560
|
+
c = c.sibling
|
|
561
|
+
}
|
|
562
|
+
return out
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// Rendering per fiber tag
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
export type RenderFn = (fiber: Fiber, domParent: Node, anchor: Node | null) => void
|
|
570
|
+
export type TypeMatcher = (type: any, marker: any) => FiberTag | null
|
|
571
|
+
|
|
572
|
+
// Mutable renderer registry indexed by FiberTag. Feature modules install their
|
|
573
|
+
// renderer via registerRenderer(); unregistered features render as no-ops. The
|
|
574
|
+
// initial registrations below rely on function-declaration hoisting — every
|
|
575
|
+
// render* function is declared with `function` later in this file.
|
|
576
|
+
const RENDERERS: Array<RenderFn | undefined> = new Array(13)
|
|
577
|
+
|
|
578
|
+
// Element-marker allowlist for child normalization (pushChildren). Core-always
|
|
579
|
+
// markers are seeded here; features add their own via registerElementMarker.
|
|
580
|
+
const ACCEPTED_ELEMENT_MARKERS = new Set<symbol>([
|
|
581
|
+
REACT_ELEMENT_TYPE as symbol,
|
|
582
|
+
REACT_LEGACY_ELEMENT_TYPE as symbol,
|
|
583
|
+
])
|
|
584
|
+
|
|
585
|
+
// Type-to-tag matchers tried in registration order from fiberFromChild's
|
|
586
|
+
// fallback branch. Features register here for element types that aren't
|
|
587
|
+
// marker-based (e.g. Portal, where element.type IS the symbol).
|
|
588
|
+
const TYPE_MATCHERS: TypeMatcher[] = []
|
|
589
|
+
|
|
590
|
+
export function registerRenderer(tag: FiberTag, fn: RenderFn): void {
|
|
591
|
+
RENDERERS[tag] = fn
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function registerTypeMatcher(m: TypeMatcher): void {
|
|
595
|
+
TYPE_MATCHERS.push(m)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function registerElementMarker(sym: symbol): void {
|
|
599
|
+
ACCEPTED_ELEMENT_MARKERS.add(sym)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Accessor + scoped setter for the module-level `currentRoot`. Feature modules
|
|
603
|
+
// need these to participate in the render loop (e.g. Suspense re-hydration
|
|
604
|
+
// must temporarily set the root while rebuilding a boundary subtree).
|
|
605
|
+
export function getCurrentRoot(): FiberRoot | null {
|
|
606
|
+
return currentRoot
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function withCurrentRoot<T>(root: FiberRoot | null, fn: () => T): T {
|
|
610
|
+
const prev = currentRoot
|
|
611
|
+
currentRoot = root
|
|
612
|
+
try {
|
|
613
|
+
return fn()
|
|
614
|
+
} finally {
|
|
615
|
+
currentRoot = prev
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// The memo feature uses this to bypass its prop-equality gate on state-driven
|
|
620
|
+
// rerenders of the memoized fiber itself (hook update / subscribed store),
|
|
621
|
+
// where props haven't changed by definition.
|
|
622
|
+
export function getForceRerenderingFiber(): Fiber | null {
|
|
623
|
+
return forceRerenderingFiber
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
registerRenderer(FiberTag.Text, renderText)
|
|
627
|
+
registerRenderer(FiberTag.Host, renderHost)
|
|
628
|
+
registerRenderer(FiberTag.Function, renderFunction)
|
|
629
|
+
registerRenderer(FiberTag.Fragment, renderFragment)
|
|
630
|
+
|
|
631
|
+
export function renderFiber(fiber: Fiber, domParent: Node, anchor: Node | null): void {
|
|
632
|
+
const fn = RENDERERS[fiber.tag]
|
|
633
|
+
if (fn) fn(fiber, domParent, anchor)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function renderText(fiber: Fiber, domParent: Node, anchor: Node | null): void {
|
|
637
|
+
const text = fiber.pendingProps as string
|
|
638
|
+
if (!fiber.dom) {
|
|
639
|
+
const hydrated = currentRoot?.hydrating ? adoptTextDom(fiber, fiber.parent!, text) : false
|
|
640
|
+
if (!hydrated) {
|
|
641
|
+
fiber.dom = document.createTextNode(text)
|
|
642
|
+
insertInto(domParent, fiber.dom, anchor)
|
|
643
|
+
}
|
|
644
|
+
} else if ((fiber.dom as Text).data !== text) {
|
|
645
|
+
;(fiber.dom as Text).data = text
|
|
646
|
+
}
|
|
647
|
+
fiber.memoizedProps = text
|
|
648
|
+
// dirty cleared at rerender start; leaving true lets mid-render schedule persist
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
|
|
652
|
+
const props = fiber.pendingProps ?? {}
|
|
653
|
+
const prev = fiber.memoizedProps ?? {}
|
|
654
|
+
const type = fiber.type as string
|
|
655
|
+
const isSvg = type === 'svg' || (domParent as Element).namespaceURI === 'http://www.w3.org/2000/svg'
|
|
656
|
+
|
|
657
|
+
// <select value> must be applied AFTER children mount — setting `.value`
|
|
658
|
+
// on a `<select>` with no matching `<option>` yet resets it to empty. Same
|
|
659
|
+
// for `defaultValue` on first mount. Stash and replay.
|
|
660
|
+
const isSelect = type === 'select'
|
|
661
|
+
const deferredSelectValue =
|
|
662
|
+
isSelect && (props.value !== undefined || props.defaultValue !== undefined)
|
|
663
|
+
? props.value !== undefined ? props.value : props.defaultValue
|
|
664
|
+
: undefined
|
|
665
|
+
|
|
666
|
+
if (!fiber.dom) {
|
|
667
|
+
const hydrated = currentRoot?.hydrating ? adoptHostDom(fiber, fiber.parent!) : false
|
|
668
|
+
if (!hydrated) {
|
|
669
|
+
fiber.dom = createHostNode(type, isSvg)
|
|
670
|
+
// Two passes so form-control attributes (notably <input type>) are in
|
|
671
|
+
// place before event handlers attach. setEventHandler reads the
|
|
672
|
+
// element's runtime state to decide the DOM event name (e.g. onChange
|
|
673
|
+
// → `input` vs `change`); binding before `type` is applied would
|
|
674
|
+
// attach to the wrong event for checkbox/radio/file inputs.
|
|
675
|
+
for (const k in props) {
|
|
676
|
+
if (isSelect && (k === 'value' || k === 'defaultValue')) continue
|
|
677
|
+
if (isEventProp(k)) continue
|
|
678
|
+
setProp(fiber.dom as Element, k, props[k], undefined, isSvg)
|
|
679
|
+
}
|
|
680
|
+
for (const k in props) {
|
|
681
|
+
if (!isEventProp(k)) continue
|
|
682
|
+
setProp(fiber.dom as Element, k, props[k], undefined, isSvg)
|
|
683
|
+
}
|
|
684
|
+
insertInto(domParent, fiber.dom, anchor)
|
|
685
|
+
}
|
|
686
|
+
attachRef(fiber, fiber.dom)
|
|
687
|
+
} else {
|
|
688
|
+
const el = fiber.dom as Element
|
|
689
|
+
for (const k in prev) {
|
|
690
|
+
if (!(k in props)) setProp(el, k, undefined, prev[k], isSvg)
|
|
691
|
+
}
|
|
692
|
+
// Non-event props first for the same reason as above: a `type` change
|
|
693
|
+
// must land before we ask setEventHandler to resolve the DOM event for
|
|
694
|
+
// `onChange`.
|
|
695
|
+
for (const k in props) {
|
|
696
|
+
if (isSelect && (k === 'value' || k === 'defaultValue')) continue
|
|
697
|
+
if (isEventProp(k)) continue
|
|
698
|
+
if (prev[k] !== props[k]) setProp(el, k, props[k], prev[k], isSvg)
|
|
699
|
+
}
|
|
700
|
+
for (const k in props) {
|
|
701
|
+
if (!isEventProp(k)) continue
|
|
702
|
+
if (prev[k] !== props[k]) setProp(el, k, props[k], prev[k], isSvg)
|
|
703
|
+
}
|
|
704
|
+
if (prev !== props) syncRefIfChanged(fiber, fiber.dom)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Children go into this DOM node
|
|
708
|
+
reconcileChildren(fiber, childrenToArray(props.children), fiber.dom!, null)
|
|
709
|
+
|
|
710
|
+
// During hydration, if after reconciling all client-expected children we
|
|
711
|
+
// still have server DOM left in the cursor for this host, that's a
|
|
712
|
+
// structural mismatch (server produced more than client wants). Report.
|
|
713
|
+
// <head>/<html> are position-insensitive — leftover here is normal
|
|
714
|
+
// (Vite dev-style injections, SSR-only scripts, etc.).
|
|
715
|
+
if (currentRoot?.hydrating) {
|
|
716
|
+
const parentTag = (fiber.type as string).toLowerCase()
|
|
717
|
+
if (parentTag !== 'head' && parentTag !== 'html') {
|
|
718
|
+
const cursor = getHydrationCursor(fiber)
|
|
719
|
+
if (cursor) {
|
|
720
|
+
const leftover = cursor.remaining().filter(
|
|
721
|
+
(n) => n.nodeType === 1 || n.nodeType === 3,
|
|
722
|
+
)
|
|
723
|
+
if (leftover.length > 0 && currentRoot.onRecoverableError) {
|
|
724
|
+
currentRoot.onRecoverableError(
|
|
725
|
+
new Error(
|
|
726
|
+
`Hydration mismatch: server rendered ${leftover.length} extra ` +
|
|
727
|
+
`${leftover.length === 1 ? 'node' : 'nodes'} inside <${parentTag}> ` +
|
|
728
|
+
`that the client tree did not.`,
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
for (const n of leftover) n.parentNode?.removeChild(n)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Apply <select> value after options are mounted.
|
|
738
|
+
if (isSelect && deferredSelectValue !== undefined) {
|
|
739
|
+
const select = fiber.dom as HTMLSelectElement
|
|
740
|
+
if (Array.isArray(deferredSelectValue)) {
|
|
741
|
+
const asStrings = deferredSelectValue.map((v) => '' + v)
|
|
742
|
+
for (const opt of Array.from(select.options)) {
|
|
743
|
+
opt.selected = asStrings.includes(opt.value)
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
select.value = '' + deferredSelectValue
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
fiber.memoizedProps = props
|
|
751
|
+
// dirty cleared at rerender start; leaving true lets mid-render schedule persist
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function renderFunction(fiber: Fiber, domParent: Node, anchor: Node | null): void {
|
|
755
|
+
const prevDispatcher = ReactSharedInternals.H
|
|
756
|
+
const prevFiber = ReactSharedInternals.currentFiber
|
|
757
|
+
const prevHook = ReactSharedInternals.currentHook
|
|
758
|
+
const prevIndex = ReactSharedInternals.hookIndex
|
|
759
|
+
|
|
760
|
+
ReactSharedInternals.H = makeDispatcher()
|
|
761
|
+
ReactSharedInternals.currentFiber = fiber
|
|
762
|
+
ReactSharedInternals.currentHook = null
|
|
763
|
+
ReactSharedInternals.hookIndex = 0
|
|
764
|
+
|
|
765
|
+
let rendered: ReactNode
|
|
766
|
+
let deferredForHydration = false
|
|
767
|
+
try {
|
|
768
|
+
rendered = (fiber.type as Function)(fiber.pendingProps ?? {})
|
|
769
|
+
} catch (e: any) {
|
|
770
|
+
if (isThenable(e)) {
|
|
771
|
+
if (currentRoot?.hydrating) {
|
|
772
|
+
// Suspension during initial hydration. Leave the existing DOM alone
|
|
773
|
+
// and preserve the in-scope hydration cursor on THIS fiber so it
|
|
774
|
+
// survives the synchronous endHydration() that fires when the initial
|
|
775
|
+
// hydrateRoot() call returns. When the promise settles, the fiber
|
|
776
|
+
// re-renders (see rerenderFiber) with hydration re-activated and its
|
|
777
|
+
// descendants adopt DOM instead of creating new nodes.
|
|
778
|
+
const hostParent = findHydrationHost(fiber)
|
|
779
|
+
const inheritedCursor = getHydrationCursor(hostParent)
|
|
780
|
+
if (inheritedCursor) {
|
|
781
|
+
setHydrationCursor(fiber, inheritedCursor)
|
|
782
|
+
}
|
|
783
|
+
fiber.memoizedState = {
|
|
784
|
+
...(fiber.memoizedState ?? {}),
|
|
785
|
+
_pendingHydration: true,
|
|
786
|
+
}
|
|
787
|
+
// Mirror renderLazy's guard: mark the nearest Suspense ancestor as
|
|
788
|
+
// awaiting hydration-resume, so any re-render of that Suspense (e.g.
|
|
789
|
+
// rehydrateBoundary fired by $RC, or an unrelated state update from a
|
|
790
|
+
// sibling) doesn't re-enter `tryChildren`, re-throw, and flip Suspense
|
|
791
|
+
// into its suspended+pending path — which would unmount our deferred
|
|
792
|
+
// subtree and remount a fallback on top of the SSR content. By
|
|
793
|
+
// pinning the Suspense to a "hydration-suspended" no-op until our
|
|
794
|
+
// resume fires, the deferred re-render owns the adoption pass.
|
|
795
|
+
let sus: Fiber | null = fiber.parent
|
|
796
|
+
while (sus && sus.tag !== FiberTag.Suspense) sus = sus.parent
|
|
797
|
+
if (sus && sus.memoizedState) {
|
|
798
|
+
;(sus.memoizedState as any)._awaitingLazyHydration = true
|
|
799
|
+
}
|
|
800
|
+
const clearAwait = () => {
|
|
801
|
+
if (sus && sus.memoizedState) {
|
|
802
|
+
;(sus.memoizedState as any)._awaitingLazyHydration = false
|
|
803
|
+
}
|
|
804
|
+
scheduleUpdate(fiber)
|
|
805
|
+
}
|
|
806
|
+
e.then(clearAwait, clearAwait)
|
|
807
|
+
deferredForHydration = true
|
|
808
|
+
} else {
|
|
809
|
+
CAPABILITIES.handleSuspended(fiber, e)
|
|
810
|
+
rendered = null
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
handleErrorInRender(fiber, e)
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
} finally {
|
|
817
|
+
ReactSharedInternals.H = prevDispatcher
|
|
818
|
+
ReactSharedInternals.currentFiber = prevFiber
|
|
819
|
+
ReactSharedInternals.currentHook = prevHook
|
|
820
|
+
ReactSharedInternals.hookIndex = prevIndex
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (deferredForHydration) return
|
|
824
|
+
|
|
825
|
+
reconcileChildren(fiber, childrenToArray(rendered), domParent, anchor)
|
|
826
|
+
fiber.memoizedProps = fiber.pendingProps
|
|
827
|
+
// dirty cleared at rerender start; leaving true lets mid-render schedule persist
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function hasAncestorHydrationCursor(_fiber: Fiber): boolean {
|
|
831
|
+
// Reserved for future per-Suspense-boundary hydration deferral. For now the
|
|
832
|
+
// top-level hydration path is all we need to special-case.
|
|
833
|
+
return false
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function renderFragment(fiber: Fiber, domParent: Node, anchor: Node | null): void {
|
|
837
|
+
const props = fiber.pendingProps ?? {}
|
|
838
|
+
reconcileChildren(fiber, childrenToArray(props.children), domParent, anchor)
|
|
839
|
+
fiber.memoizedProps = props
|
|
840
|
+
// dirty cleared at rerender start; leaving true lets mid-render schedule persist
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Error handling + default Suspense capability
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
|
|
847
|
+
// Default handler when the Suspense feature isn't installed: just schedule
|
|
848
|
+
// a re-render when the thrown thenable settles. No boundary walk, no
|
|
849
|
+
// fallback swap — children render empty during the pending window.
|
|
850
|
+
function defaultHandleSuspended(fiber: Fiber, thenable: Promise<any>): void {
|
|
851
|
+
thenable.then(
|
|
852
|
+
() => scheduleUpdate(fiber),
|
|
853
|
+
() => scheduleUpdate(fiber),
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
// Capability hooks — cross-cutting behaviors that features override.
|
|
859
|
+
// Defaults here preserve today's behavior so the indirection is transparent
|
|
860
|
+
// when all features are loaded. A feature's full-module can install its own
|
|
861
|
+
// implementation via installCapability(); stubs leave the default in place,
|
|
862
|
+
// where the default may intentionally degrade (e.g. a no-Context build's
|
|
863
|
+
// readContext never walks the tree because no Provider fibers exist).
|
|
864
|
+
// ---------------------------------------------------------------------------
|
|
865
|
+
|
|
866
|
+
export interface Capabilities {
|
|
867
|
+
handleSuspended: (fiber: Fiber, thenable: Promise<any>) => void
|
|
868
|
+
readContext: (fiber: Fiber, ctx: any) => any
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const CAPABILITIES: Capabilities = {
|
|
872
|
+
handleSuspended: defaultHandleSuspended,
|
|
873
|
+
readContext: defaultReadContext,
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function installCapability<K extends keyof Capabilities>(
|
|
877
|
+
name: K,
|
|
878
|
+
fn: Capabilities[K],
|
|
879
|
+
): void {
|
|
880
|
+
CAPABILITIES[name] = fn
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Wrapper for features that catch thrown thenables inside their render
|
|
884
|
+
// functions. Delegates to the installed Suspense capability.
|
|
885
|
+
export function handleSuspended(fiber: Fiber, thenable: Promise<any>): void {
|
|
886
|
+
CAPABILITIES.handleSuspended(fiber, thenable)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export function handleErrorInRender(fiber: Fiber, err: any): void {
|
|
890
|
+
// Bubble to nearest class boundary with getDerivedStateFromError / componentDidCatch
|
|
891
|
+
let f: Fiber | null = fiber.parent
|
|
892
|
+
while (f) {
|
|
893
|
+
if (f.tag === FiberTag.Class) {
|
|
894
|
+
const Ctor = f.type as any
|
|
895
|
+
const instance = f.stateNode
|
|
896
|
+
if (Ctor.getDerivedStateFromError) {
|
|
897
|
+
const update = Ctor.getDerivedStateFromError(err)
|
|
898
|
+
instance.state = { ...instance.state, ...update }
|
|
899
|
+
}
|
|
900
|
+
if (instance.componentDidCatch) {
|
|
901
|
+
try {
|
|
902
|
+
instance.componentDidCatch(err, { componentStack: '' })
|
|
903
|
+
} catch {}
|
|
904
|
+
}
|
|
905
|
+
scheduleUpdate(f)
|
|
906
|
+
return
|
|
907
|
+
}
|
|
908
|
+
f = f.parent
|
|
909
|
+
}
|
|
910
|
+
// No boundary — report to root
|
|
911
|
+
if (currentRoot?.onUncaughtError) currentRoot.onUncaughtError(err)
|
|
912
|
+
else throw err
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export function isThenable(x: any): x is Promise<any> {
|
|
916
|
+
return x != null && typeof x.then === 'function'
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// ---------------------------------------------------------------------------
|
|
920
|
+
// Unmount
|
|
921
|
+
// ---------------------------------------------------------------------------
|
|
922
|
+
|
|
923
|
+
function unmountFiber(fiber: Fiber, domParent: Node): void {
|
|
924
|
+
fiber.unmounted = true
|
|
925
|
+
// Recurse first
|
|
926
|
+
let c = fiber.child
|
|
927
|
+
while (c) {
|
|
928
|
+
const next = c.sibling
|
|
929
|
+
unmountFiber(c, fiber.tag === FiberTag.Host ? fiber.dom! : domParent)
|
|
930
|
+
c = next
|
|
931
|
+
}
|
|
932
|
+
fiber.child = null
|
|
933
|
+
|
|
934
|
+
// Run cleanups (effects + layout effects)
|
|
935
|
+
if (fiber.cleanups) {
|
|
936
|
+
for (const cleanup of fiber.cleanups) {
|
|
937
|
+
try {
|
|
938
|
+
cleanup()
|
|
939
|
+
} catch (e) {
|
|
940
|
+
if (currentRoot?.onRecoverableError) currentRoot.onRecoverableError(e)
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
fiber.cleanups = null
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (fiber.tag === FiberTag.Class && fiber.stateNode?.componentWillUnmount) {
|
|
947
|
+
try {
|
|
948
|
+
fiber.stateNode.componentWillUnmount()
|
|
949
|
+
} catch (e) {
|
|
950
|
+
if (currentRoot?.onRecoverableError) currentRoot.onRecoverableError(e)
|
|
951
|
+
}
|
|
952
|
+
fiber.stateNode._fiber = null
|
|
953
|
+
fiber.stateNode._enqueueUpdate = null
|
|
954
|
+
fiber.stateNode._forceUpdate = null
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Detach ref
|
|
958
|
+
if (fiber.ref) detachRef(fiber.ref)
|
|
959
|
+
|
|
960
|
+
// Remove DOM if host
|
|
961
|
+
if (fiber.tag === FiberTag.Host && fiber.dom && fiber.dom.parentNode) {
|
|
962
|
+
fiber.dom.parentNode.removeChild(fiber.dom)
|
|
963
|
+
} else if (fiber.tag === FiberTag.Text && fiber.dom && fiber.dom.parentNode) {
|
|
964
|
+
fiber.dom.parentNode.removeChild(fiber.dom)
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
export function unmountAllChildren(parent: Fiber, domParent: Node): void {
|
|
969
|
+
let c = parent.child
|
|
970
|
+
while (c) {
|
|
971
|
+
const next = c.sibling
|
|
972
|
+
unmountFiber(c, domParent)
|
|
973
|
+
c = next
|
|
974
|
+
}
|
|
975
|
+
parent.child = null
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ---------------------------------------------------------------------------
|
|
979
|
+
// DOM navigation helpers
|
|
980
|
+
// ---------------------------------------------------------------------------
|
|
981
|
+
|
|
982
|
+
function insertInto(parent: Node, node: Node, anchor: Node | null): void {
|
|
983
|
+
// Anchor may have been removed or moved since it was computed (mutations
|
|
984
|
+
// from unmount, boundary reveal, user code, HMR). If it's no longer a child
|
|
985
|
+
// of `parent`, fall back to append — trying to insertBefore a non-child
|
|
986
|
+
// throws NotFoundError and dev-loops the reconciler.
|
|
987
|
+
if (anchor && anchor.parentNode === parent) {
|
|
988
|
+
parent.insertBefore(node, anchor)
|
|
989
|
+
} else {
|
|
990
|
+
parent.appendChild(node)
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function getHostParent(fiber: Fiber): Node {
|
|
995
|
+
let p = fiber.parent
|
|
996
|
+
while (p) {
|
|
997
|
+
if (p.tag === FiberTag.Host) return p.dom!
|
|
998
|
+
if (p.tag === FiberTag.Root)
|
|
999
|
+
return (p.stateNode as Node) || (p.dom as Node) || (p.root?.container as Node)
|
|
1000
|
+
if (p.tag === FiberTag.Portal) {
|
|
1001
|
+
// Portal renders its children into the `container` prop, not into any
|
|
1002
|
+
// DOM element the portal fiber "owns". Read the container from the
|
|
1003
|
+
// portal's own props so a rerenderFiber triggered on a descendant
|
|
1004
|
+
// (e.g. a Floating-UI-positioned popper in a Radix Portal) finds its
|
|
1005
|
+
// host parent — otherwise getHostParent returns undefined and the
|
|
1006
|
+
// next renderHost crashes reading `.namespaceURI` on undefined.
|
|
1007
|
+
const props = (p.pendingProps ?? p.memoizedProps) as { container?: Element } | null
|
|
1008
|
+
return (props?.container as Node) || (p.stateNode as Node) || (p.dom as Node) || (p.root?.container as Node)
|
|
1009
|
+
}
|
|
1010
|
+
p = p.parent
|
|
1011
|
+
}
|
|
1012
|
+
throw new Error('No host parent found.')
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function getAnchor(fiber: Fiber): Node | null {
|
|
1016
|
+
// Return the first DOM node that comes after this fiber within the host parent
|
|
1017
|
+
let f: Fiber | null = fiber.sibling
|
|
1018
|
+
while (f) {
|
|
1019
|
+
const d = firstDomNode(f)
|
|
1020
|
+
if (d) return d
|
|
1021
|
+
f = f.sibling
|
|
1022
|
+
}
|
|
1023
|
+
// Ascend
|
|
1024
|
+
let p = fiber.parent
|
|
1025
|
+
while (p && p.tag !== FiberTag.Host && p.tag !== FiberTag.Root && p.tag !== FiberTag.Portal) {
|
|
1026
|
+
if (p.sibling) {
|
|
1027
|
+
const d = firstDomNode(p.sibling)
|
|
1028
|
+
if (d) return d
|
|
1029
|
+
}
|
|
1030
|
+
p = p.parent
|
|
1031
|
+
}
|
|
1032
|
+
return null
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function firstDomNode(fiber: Fiber): Node | null {
|
|
1036
|
+
if (fiber.tag === FiberTag.Host || fiber.tag === FiberTag.Text) return fiber.dom
|
|
1037
|
+
let c = fiber.child
|
|
1038
|
+
while (c) {
|
|
1039
|
+
const d = firstDomNode(c)
|
|
1040
|
+
if (d) return d
|
|
1041
|
+
c = c.sibling
|
|
1042
|
+
}
|
|
1043
|
+
return null
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ---------------------------------------------------------------------------
|
|
1047
|
+
// Context read — exported for dispatcher.ts (useContext, use()). Delegates to
|
|
1048
|
+
// the installed capability so the Context feature can override with a walking
|
|
1049
|
+
// implementation that finds the nearest Provider fiber. When the feature is
|
|
1050
|
+
// stubbed, the default here returns ctx._currentValue — correct because no
|
|
1051
|
+
// Provider fibers exist in the tree (Provider element → Fragment via the
|
|
1052
|
+
// stub's type matcher).
|
|
1053
|
+
// ---------------------------------------------------------------------------
|
|
1054
|
+
|
|
1055
|
+
export function readContext(fiber: Fiber, ctx: any): any {
|
|
1056
|
+
return CAPABILITIES.readContext(fiber, ctx)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function defaultReadContext(_fiber: Fiber, ctx: any): any {
|
|
1060
|
+
return ctx._currentValue
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// ---------------------------------------------------------------------------
|
|
1064
|
+
// Refs
|
|
1065
|
+
// ---------------------------------------------------------------------------
|
|
1066
|
+
|
|
1067
|
+
function attachRef(fiber: Fiber, value: any): void {
|
|
1068
|
+
const ref = fiber.ref ?? (fiber.pendingProps?.ref ?? null)
|
|
1069
|
+
if (!ref) return
|
|
1070
|
+
if (typeof ref === 'function') {
|
|
1071
|
+
const cleanup = ref(value)
|
|
1072
|
+
if (typeof cleanup === 'function') {
|
|
1073
|
+
fiber.cleanups ||= []
|
|
1074
|
+
fiber.cleanups.push(cleanup)
|
|
1075
|
+
} else {
|
|
1076
|
+
fiber.cleanups ||= []
|
|
1077
|
+
fiber.cleanups.push(() => ref(null))
|
|
1078
|
+
}
|
|
1079
|
+
} else {
|
|
1080
|
+
ref.current = value
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function syncRefIfChanged(fiber: Fiber, value: any): void {
|
|
1085
|
+
const ref = fiber.ref ?? (fiber.pendingProps?.ref ?? null)
|
|
1086
|
+
if (!ref) return
|
|
1087
|
+
if (typeof ref === 'object' && ref.current !== value) ref.current = value
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function detachRef(ref: any): void {
|
|
1091
|
+
if (typeof ref === 'function') {
|
|
1092
|
+
try {
|
|
1093
|
+
ref(null)
|
|
1094
|
+
} catch {}
|
|
1095
|
+
} else if (ref && typeof ref === 'object') {
|
|
1096
|
+
ref.current = null
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ---------------------------------------------------------------------------
|
|
1101
|
+
// Effects
|
|
1102
|
+
// ---------------------------------------------------------------------------
|
|
1103
|
+
|
|
1104
|
+
const pendingEffects: Array<{ fiber: Fiber; effect: Effect }> = []
|
|
1105
|
+
const pendingLayoutEffects: Array<{ fiber: Fiber; effect: Effect }> = []
|
|
1106
|
+
const pendingLifecycles: Array<{ fiber: Fiber; fn: () => void }> = []
|
|
1107
|
+
|
|
1108
|
+
export function enqueueEffect(fiber: Fiber, effect: Effect): void {
|
|
1109
|
+
if (effect.tag === 'layout' || effect.tag === 'insertion') {
|
|
1110
|
+
pendingLayoutEffects.push({ fiber, effect })
|
|
1111
|
+
} else {
|
|
1112
|
+
pendingEffects.push({ fiber, effect })
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function scheduleLifecycle(fiber: Fiber, fn: () => void): void {
|
|
1117
|
+
pendingLifecycles.push({ fiber, fn })
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
export function runEffects(root: FiberRoot): void {
|
|
1121
|
+
// Layout effects synchronously
|
|
1122
|
+
while (pendingLayoutEffects.length) {
|
|
1123
|
+
const { fiber, effect } = pendingLayoutEffects.shift()!
|
|
1124
|
+
runEffect(fiber, effect, root)
|
|
1125
|
+
}
|
|
1126
|
+
// Then lifecycles
|
|
1127
|
+
while (pendingLifecycles.length) {
|
|
1128
|
+
const { fn } = pendingLifecycles.shift()!
|
|
1129
|
+
try {
|
|
1130
|
+
fn()
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
if (root.onCaughtError) root.onCaughtError(e)
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// Passive effects on microtask
|
|
1136
|
+
if (pendingEffects.length) {
|
|
1137
|
+
const batch = pendingEffects.splice(0)
|
|
1138
|
+
queueMicrotask(() => {
|
|
1139
|
+
for (const { fiber, effect } of batch) runEffect(fiber, effect, root)
|
|
1140
|
+
})
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function runEffect(fiber: Fiber, effect: Effect, root: FiberRoot): void {
|
|
1145
|
+
try {
|
|
1146
|
+
const cleanup = effect.create()
|
|
1147
|
+
effect.destroy = typeof cleanup === 'function' ? cleanup : undefined
|
|
1148
|
+
if (effect.destroy) {
|
|
1149
|
+
fiber.cleanups ||= []
|
|
1150
|
+
fiber.cleanups.push(effect.destroy)
|
|
1151
|
+
}
|
|
1152
|
+
} catch (e) {
|
|
1153
|
+
if (root.onCaughtError) root.onCaughtError(e)
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// ---------------------------------------------------------------------------
|
|
1158
|
+
// Utilities
|
|
1159
|
+
// ---------------------------------------------------------------------------
|
|
1160
|
+
|
|
1161
|
+
function isEventProp(name: string): boolean {
|
|
1162
|
+
return (
|
|
1163
|
+
name.length > 2 &&
|
|
1164
|
+
name.charCodeAt(0) === 111 /* o */ &&
|
|
1165
|
+
name.charCodeAt(1) === 110 /* n */ &&
|
|
1166
|
+
name.charCodeAt(2) >= 65 /* 'A'-ish: any uppercase start (onClick, onChange, …) */
|
|
1167
|
+
)
|
|
1168
|
+
}
|
|
1169
|
+
|