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