@tanstack/redact 0.0.21 → 0.1.0
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/attributes.js +2 -1
- package/dist/core/attributes.js.map +2 -2
- package/dist/core/browser.d.ts +9 -0
- package/dist/core/browser.js +35 -0
- package/dist/core/browser.js.map +7 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +3 -3
- package/dist/core/internal.d.ts +31 -8
- package/dist/core/internal.js +3 -1
- package/dist/core/internal.js.map +2 -2
- package/dist/core/resource-hints.d.ts +7 -0
- package/dist/core/resource-hints.js +19 -0
- package/dist/core/resource-hints.js.map +7 -0
- package/dist/core/types.js +1 -1
- package/dist/dom/_all.d.ts +11 -16
- package/dist/dom/_all.js +9 -20
- package/dist/dom/_all.js.map +2 -2
- package/dist/dom/client.d.ts +4 -4
- package/dist/dom/client.js +4 -4
- package/dist/dom/commit.d.ts +23 -0
- package/dist/dom/commit.js +191 -0
- package/dist/dom/commit.js.map +7 -0
- package/dist/dom/dispatcher.d.ts +4 -22
- package/dist/dom/dispatcher.js +184 -126
- package/dist/dom/dispatcher.js.map +3 -3
- package/dist/dom/dom.d.ts +2 -0
- package/dist/dom/dom.js +72 -52
- package/dist/dom/dom.js.map +2 -2
- package/dist/dom/error-info.d.ts +2 -0
- package/dist/dom/error-info.js +16 -0
- package/dist/dom/error-info.js.map +7 -0
- package/dist/dom/event-replay.js +1 -1
- package/dist/dom/features/activity/full.d.ts +1 -0
- package/dist/dom/features/activity/full.js +162 -0
- package/dist/dom/features/activity/full.js.map +7 -0
- package/dist/dom/features/activity/index.d.ts +1 -0
- package/dist/dom/features/activity/index.js +3 -0
- package/dist/dom/features/activity/index.js.map +7 -0
- package/dist/dom/features/activity/stub.d.ts +1 -0
- package/dist/dom/features/activity/stub.js +12 -0
- package/dist/dom/features/activity/stub.js.map +7 -0
- package/dist/dom/features/class/full.js +87 -27
- package/dist/dom/features/class/full.js.map +2 -2
- package/dist/dom/features/class/index.d.ts +1 -1
- package/dist/dom/features/class/index.js +2 -2
- package/dist/dom/features/class/stub.js +3 -3
- package/dist/dom/features/context/full.d.ts +4 -1
- package/dist/dom/features/context/full.js +69 -9
- package/dist/dom/features/context/full.js.map +2 -2
- package/dist/dom/features/context/index.d.ts +1 -1
- package/dist/dom/features/context/index.js +2 -2
- package/dist/dom/features/context/stub.d.ts +4 -1
- package/dist/dom/features/context/stub.js +15 -4
- package/dist/dom/features/context/stub.js.map +2 -2
- package/dist/dom/features/forward-ref/full.js +12 -21
- package/dist/dom/features/forward-ref/full.js.map +2 -2
- package/dist/dom/features/forward-ref/index.d.ts +1 -1
- package/dist/dom/features/forward-ref/index.js +2 -2
- package/dist/dom/features/forward-ref/stub.js +4 -4
- package/dist/dom/features/fragment-refs/index.d.ts +1 -0
- package/dist/dom/features/fragment-refs/index.js +8 -0
- package/dist/dom/features/fragment-refs/index.js.map +7 -0
- package/dist/dom/features/fragment-refs/stub.d.ts +3 -0
- package/dist/dom/features/fragment-refs/stub.js +14 -0
- package/dist/dom/features/fragment-refs/stub.js.map +7 -0
- package/dist/dom/features/hydration/full.d.ts +11 -6
- package/dist/dom/features/hydration/full.js +76 -77
- package/dist/dom/features/hydration/full.js.map +2 -2
- package/dist/dom/features/hydration/index.d.ts +1 -1
- package/dist/dom/features/hydration/index.js +2 -2
- package/dist/dom/features/hydration/stub.d.ts +7 -2
- package/dist/dom/features/hydration/stub.js +1 -1
- package/dist/dom/features/hydration/stub.js.map +1 -1
- package/dist/dom/features/index.d.ts +8 -7
- package/dist/dom/features/index.js +9 -8
- package/dist/dom/features/index.js.map +2 -2
- package/dist/dom/features/lazy/full.js +4 -4
- package/dist/dom/features/lazy/index.d.ts +1 -1
- package/dist/dom/features/lazy/index.js +2 -2
- package/dist/dom/features/lazy/stub.js +4 -4
- package/dist/dom/features/memo/full.js +8 -6
- package/dist/dom/features/memo/full.js.map +2 -2
- package/dist/dom/features/memo/index.d.ts +1 -1
- package/dist/dom/features/memo/index.js +2 -2
- package/dist/dom/features/memo/stub.js +4 -4
- package/dist/dom/features/portal/full.js +4 -4
- package/dist/dom/features/portal/index.d.ts +1 -1
- package/dist/dom/features/portal/index.js +2 -2
- package/dist/dom/features/portal/stub.js +4 -4
- package/dist/dom/features/suspense/full.js +197 -52
- package/dist/dom/features/suspense/full.js.map +2 -2
- package/dist/dom/features/suspense/index.d.ts +1 -1
- package/dist/dom/features/suspense/index.js +2 -2
- package/dist/dom/features/suspense/stub.js +4 -4
- package/dist/dom/features/view-transition/full.d.ts +6 -0
- package/dist/dom/features/view-transition/full.js +280 -0
- package/dist/dom/features/view-transition/full.js.map +7 -0
- package/dist/dom/features/view-transition/index.d.ts +1 -0
- package/dist/dom/features/view-transition/index.js +3 -0
- package/dist/dom/features/view-transition/index.js.map +7 -0
- package/dist/dom/features/view-transition/resources.d.ts +11 -0
- package/dist/dom/features/view-transition/resources.js +108 -0
- package/dist/dom/features/view-transition/resources.js.map +7 -0
- package/dist/dom/features/view-transition/stub.d.ts +6 -0
- package/dist/dom/features/view-transition/stub.js +22 -0
- package/dist/dom/features/view-transition/stub.js.map +7 -0
- package/dist/dom/features/view-transition/visual.d.ts +14 -0
- package/dist/dom/features/view-transition/visual.js +356 -0
- package/dist/dom/features/view-transition/visual.js.map +7 -0
- package/dist/dom/fragment-instance.d.ts +40 -0
- package/dist/dom/fragment-instance.js +324 -0
- package/dist/dom/fragment-instance.js.map +7 -0
- package/dist/dom/index.d.ts +11 -11
- package/dist/dom/index.js +11 -18
- package/dist/dom/index.js.map +3 -3
- package/dist/dom/portal.d.ts +2 -2
- package/dist/dom/portal.js +2 -2
- package/dist/dom/portal.js.map +1 -1
- package/dist/dom/reconcile.d.ts +32 -3
- package/dist/dom/reconcile.js +689 -221
- package/dist/dom/reconcile.js.map +2 -2
- package/dist/dom/resource-hints.d.ts +6 -0
- package/dist/dom/resource-hints.js +115 -0
- package/dist/dom/resource-hints.js.map +7 -0
- package/dist/dom/resource-store.d.ts +13 -0
- package/dist/dom/resource-store.js +43 -0
- package/dist/dom/resource-store.js.map +7 -0
- package/dist/dom/resources.d.ts +1 -0
- package/dist/dom/resources.js +44 -0
- package/dist/dom/resources.js.map +7 -0
- package/dist/dom/retained-effects.d.ts +6 -0
- package/dist/dom/retained-effects.js +49 -0
- package/dist/dom/retained-effects.js.map +7 -0
- package/dist/dom/root-internal.d.ts +4 -4
- package/dist/dom/root-internal.js +2 -2
- package/dist/dom/root-internal.js.map +2 -2
- package/dist/dom/root.d.ts +5 -5
- package/dist/dom/root.js +11 -11
- package/dist/dom/root.js.map +2 -2
- package/dist/dom/test-utils.js +1 -1
- package/dist/react/activity.d.ts +7 -0
- package/dist/react/activity.js +8 -0
- package/dist/react/activity.js.map +7 -0
- package/dist/react/children.d.ts +1 -1
- package/dist/react/children.js +3 -3
- package/dist/react/children.js.map +1 -1
- package/dist/react/class.d.ts +1 -1
- package/dist/react/class.js +3 -3
- package/dist/react/class.js.map +1 -1
- package/dist/react/compiler-runtime.js +2 -2
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js +3 -3
- package/dist/react/context.js.map +1 -1
- package/dist/react/element.d.ts +6 -3
- package/dist/react/element.js +2 -2
- package/dist/react/element.js.map +2 -2
- package/dist/react/hooks.d.ts +26 -7
- package/dist/react/hooks.js +13 -3
- package/dist/react/hooks.js.map +2 -2
- package/dist/react/index.d.ts +39 -24
- package/dist/react/index.js +39 -19
- package/dist/react/index.js.map +3 -3
- package/dist/react/jsx-runtime.d.ts +2 -2
- package/dist/react/jsx-runtime.js +3 -3
- package/dist/react/memo.d.ts +1 -1
- package/dist/react/memo.js +1 -1
- package/dist/react/portal.js +1 -1
- package/dist/react/shared-internals.d.ts +15 -5
- package/dist/react/shared-internals.js +2 -3
- package/dist/react/shared-internals.js.map +2 -2
- package/dist/react/suspense.js +1 -1
- package/dist/react/view-transition.d.ts +32 -0
- package/dist/react/view-transition.js +13 -0
- package/dist/react/view-transition.js.map +7 -0
- package/dist/scheduler/index.js +1 -1
- package/dist/server/bootstrap-script.d.ts +1 -23
- package/dist/server/bootstrap-script.js +7 -6
- package/dist/server/bootstrap-script.js.map +2 -2
- package/dist/server/dispatcher.d.ts +28 -6
- package/dist/server/dispatcher.js +33 -15
- package/dist/server/dispatcher.js.map +2 -2
- package/dist/server/escape.js +16 -15
- package/dist/server/escape.js.map +2 -2
- package/dist/server/index.d.ts +6 -6
- package/dist/server/index.js +7 -7
- package/dist/server/renderToString.d.ts +1 -1
- package/dist/server/renderToString.js +12 -8
- package/dist/server/renderToString.js.map +2 -2
- package/dist/server/resource-hints.d.ts +22 -0
- package/dist/server/resource-hints.js +148 -0
- package/dist/server/resource-hints.js.map +7 -0
- package/dist/server/stream.d.ts +15 -2
- package/dist/server/stream.js +155 -63
- package/dist/server/stream.js.map +2 -2
- package/dist/server/walk.d.ts +15 -2
- package/dist/server/walk.js +143 -96
- package/dist/server/walk.js.map +2 -2
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/index.js +22 -6
- package/dist/vite/index.js.map +2 -2
- package/package.json +1 -1
- package/src/core/attributes.ts +1 -0
- package/src/core/browser.ts +33 -0
- package/src/core/internal.ts +24 -6
- package/src/core/resource-hints.ts +36 -0
- package/src/dom/_all.ts +1 -7
- package/src/dom/commit.ts +186 -0
- package/src/dom/dispatcher.ts +190 -134
- package/src/dom/dom.ts +70 -54
- package/src/dom/error-info.ts +15 -0
- package/src/dom/features/activity/full.ts +166 -0
- package/src/dom/features/activity/index.ts +1 -0
- package/src/dom/features/activity/stub.ts +14 -0
- package/src/dom/features/class/full.ts +72 -28
- package/src/dom/features/context/full.ts +66 -4
- package/src/dom/features/context/stub.ts +6 -0
- package/src/dom/features/forward-ref/full.ts +9 -19
- package/src/dom/features/fragment-refs/index.ts +1 -0
- package/src/dom/features/fragment-refs/stub.ts +3 -0
- package/src/dom/features/hydration/full.ts +84 -85
- package/src/dom/features/hydration/stub.ts +1 -1
- package/src/dom/features/index.ts +1 -0
- package/src/dom/features/memo/full.ts +4 -3
- package/src/dom/features/suspense/full.ts +192 -55
- package/src/dom/features/view-transition/full.ts +288 -0
- package/src/dom/features/view-transition/index.ts +1 -0
- package/src/dom/features/view-transition/resources.ts +107 -0
- package/src/dom/features/view-transition/stub.ts +7 -0
- package/src/dom/features/view-transition/visual.ts +406 -0
- package/src/dom/fragment-instance.ts +337 -0
- package/src/dom/index.ts +6 -7
- package/src/dom/portal.ts +1 -1
- package/src/dom/reconcile.ts +695 -242
- package/src/dom/resource-hints.ts +115 -0
- package/src/dom/resource-store.ts +52 -0
- package/src/dom/resources.ts +39 -0
- package/src/dom/retained-effects.ts +38 -0
- package/src/dom/root-internal.ts +4 -4
- package/src/dom/root.ts +12 -14
- package/src/react/activity.ts +10 -0
- package/src/react/element.ts +6 -2
- package/src/react/hooks.ts +12 -3
- package/src/react/index.ts +18 -0
- package/src/react/shared-internals.ts +3 -3
- package/src/react/view-transition.ts +41 -0
- package/src/server/bootstrap-script.ts +36 -5
- package/src/server/dispatcher.ts +35 -13
- package/src/server/escape.ts +15 -12
- package/src/server/renderToString.ts +7 -5
- package/src/server/resource-hints.ts +165 -0
- package/src/server/stream.ts +154 -62
- package/src/server/walk.ts +72 -11
- package/src/vite/index.ts +30 -13
package/src/server/stream.ts
CHANGED
|
@@ -2,21 +2,25 @@ import type { ReactNode } from '../core'
|
|
|
2
2
|
import {
|
|
3
3
|
beginSSR,
|
|
4
4
|
endSSR,
|
|
5
|
-
installSSRDispatcher,
|
|
6
|
-
uninstallSSRDispatcher,
|
|
7
5
|
applyContextSnapshot,
|
|
6
|
+
currentSSRFrame,
|
|
7
|
+
type SSRFrame,
|
|
8
8
|
} from './dispatcher'
|
|
9
|
-
import { walk, type SuspendedBoundary } from './walk'
|
|
9
|
+
import { walk, type SuspendedBoundary, type BrowserBailoutCallback } from './walk'
|
|
10
|
+
import { browserError, isBrowserToken, isBrowserError } from '../core/browser'
|
|
10
11
|
import { BOUNDARY_REVEAL_RUNTIME, revealScript } from './bootstrap-script'
|
|
11
|
-
import { escapeScript } from './escape'
|
|
12
|
+
import { escapeAttr, escapeScript } from './escape'
|
|
13
|
+
import { resourceURL } from '../core/resource-hints'
|
|
14
|
+
import type { ResourceScope } from './resource-hints'
|
|
12
15
|
|
|
13
16
|
export interface StreamOptions {
|
|
14
17
|
identifierPrefix?: string
|
|
15
|
-
nonce?: string
|
|
18
|
+
nonce?: string | { script?: string; style?: string }
|
|
16
19
|
bootstrapScriptContent?: string | ReadonlyArray<string>
|
|
17
20
|
bootstrapScripts?: ReadonlyArray<string | { src: string; async?: boolean; nonce?: string }>
|
|
18
21
|
bootstrapModules?: ReadonlyArray<string | { src: string; nonce?: string }>
|
|
19
22
|
onError?: (error: unknown) => string | void
|
|
23
|
+
onBrowserBailout?: BrowserBailoutCallback
|
|
20
24
|
signal?: AbortSignal
|
|
21
25
|
progressiveChunkSize?: number
|
|
22
26
|
}
|
|
@@ -28,6 +32,7 @@ export interface ReadableStreamResult extends ReadableStream<Uint8Array> {
|
|
|
28
32
|
export interface PipeableWritable {
|
|
29
33
|
write(chunk: string): unknown
|
|
30
34
|
end(): unknown
|
|
35
|
+
destroy?(error?: unknown): unknown
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface OrchestratorState {
|
|
@@ -35,6 +40,28 @@ export interface OrchestratorState {
|
|
|
35
40
|
pending: Set<Promise<void>>
|
|
36
41
|
closed: boolean
|
|
37
42
|
errored: unknown | null
|
|
43
|
+
aborted: boolean
|
|
44
|
+
reason: unknown
|
|
45
|
+
abort: (reason?: unknown) => void
|
|
46
|
+
cancelled: Promise<void>
|
|
47
|
+
onShellReady?: () => void
|
|
48
|
+
frame?: SSRFrame
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createState(): OrchestratorState {
|
|
52
|
+
let wake!: () => void
|
|
53
|
+
const state: OrchestratorState = {
|
|
54
|
+
nextId: 0, pending: new Set(), closed: false, errored: null,
|
|
55
|
+
aborted: false, reason: undefined,
|
|
56
|
+
cancelled: new Promise<void>((resolve) => { wake = resolve }),
|
|
57
|
+
abort(reason) {
|
|
58
|
+
if (state.closed || state.aborted) return
|
|
59
|
+
state.aborted = true
|
|
60
|
+
state.reason = isBrowserToken(reason) ? browserError(reason) : reason ?? new Error('The render was aborted by the server without a reason.')
|
|
61
|
+
wake()
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
return state
|
|
38
65
|
}
|
|
39
66
|
|
|
40
67
|
type Emit = (chunk: string) => void
|
|
@@ -45,9 +72,7 @@ export async function streamHtml(
|
|
|
45
72
|
options: StreamOptions,
|
|
46
73
|
state: OrchestratorState,
|
|
47
74
|
): Promise<void> {
|
|
48
|
-
|
|
49
|
-
beginSSR(options.identifierPrefix)
|
|
50
|
-
const nonce = options.nonce
|
|
75
|
+
const nonce = scriptNonce(options)
|
|
51
76
|
|
|
52
77
|
try {
|
|
53
78
|
const boundaries: SuspendedBoundary[] = []
|
|
@@ -60,6 +85,7 @@ export async function streamHtml(
|
|
|
60
85
|
// one encode and one enqueue per shell — measured ~2-4% of total SSR
|
|
61
86
|
// time on CPU profiles.
|
|
62
87
|
const shellChunks: string[] = []
|
|
88
|
+
let head = 0
|
|
63
89
|
const bufferedEmit: Emit = (chunk) => {
|
|
64
90
|
shellChunks.push(chunk)
|
|
65
91
|
}
|
|
@@ -70,17 +96,27 @@ export async function streamHtml(
|
|
|
70
96
|
// partial shell chunks or boundary ids from the aborted attempt.
|
|
71
97
|
let shellRendered = false
|
|
72
98
|
let rootRetries = 0
|
|
99
|
+
let resources: SSRFrame['resources']
|
|
73
100
|
while (!shellRendered) {
|
|
74
101
|
const chunkCount = shellChunks.length
|
|
75
102
|
const boundaryCount = boundaries.length
|
|
76
103
|
const nextId = state.nextId
|
|
104
|
+
head = 0
|
|
105
|
+
const previous = beginSSR(options.identifierPrefix)
|
|
106
|
+
currentSSRFrame().styleNonce = typeof options.nonce === 'object' ? options.nonce?.style : undefined
|
|
107
|
+
if (resources) currentSSRFrame().resources = resources
|
|
108
|
+
let suspended: Promise<unknown> | undefined
|
|
77
109
|
try {
|
|
110
|
+
if (state.aborted) throw state.reason
|
|
78
111
|
walk(children, {
|
|
79
112
|
emit: bufferedEmit,
|
|
80
113
|
onSuspend: (b) => boundaries.push(b),
|
|
81
114
|
nextBoundaryId: () => state.nextId++,
|
|
115
|
+
onBrowserBailout: options.onBrowserBailout,
|
|
116
|
+
onHead: () => { head = shellChunks.length },
|
|
82
117
|
})
|
|
83
118
|
shellRendered = true
|
|
119
|
+
state.frame = currentSSRFrame()
|
|
84
120
|
} catch (err) {
|
|
85
121
|
shellChunks.length = chunkCount
|
|
86
122
|
boundaries.length = boundaryCount
|
|
@@ -89,10 +125,18 @@ export async function streamHtml(
|
|
|
89
125
|
if (++rootRetries > 50) {
|
|
90
126
|
throw new Error('renderToReadableStream exceeded 50 root suspension retries.')
|
|
91
127
|
}
|
|
92
|
-
|
|
128
|
+
suspended = err
|
|
129
|
+
} finally {
|
|
130
|
+
resources = currentSSRFrame().resources
|
|
131
|
+
endSSR(previous)
|
|
93
132
|
}
|
|
133
|
+
if (suspended) await Promise.race([suspended, state.cancelled])
|
|
94
134
|
}
|
|
95
135
|
|
|
136
|
+
const hints = state.frame?.resources?.drain()
|
|
137
|
+
if (hints) shellChunks.splice(head, 0, hints)
|
|
138
|
+
if (state.frame) state.frame.shellFlushed = true
|
|
139
|
+
|
|
96
140
|
// 2. Inject runtime + bootstrap scripts (once, after shell). Skip the
|
|
97
141
|
// reveal/event-replay runtime when nothing needs it — no suspensions to
|
|
98
142
|
// reveal and no bootstrap scripts to guard against early user input. That
|
|
@@ -104,7 +148,7 @@ export async function streamHtml(
|
|
|
104
148
|
(options.bootstrapScripts?.length ?? 0) > 0 ||
|
|
105
149
|
(options.bootstrapModules?.length ?? 0) > 0
|
|
106
150
|
if (boundaries.length > 0 || hasBootstrap) {
|
|
107
|
-
shellChunks.push(`<script${nonce ? ` nonce="${nonce}"` : ''}>${BOUNDARY_REVEAL_RUNTIME}</script>`)
|
|
151
|
+
shellChunks.push(`<script${nonce ? ` nonce="${escapeAttr(nonce)}"` : ''}>${BOUNDARY_REVEAL_RUNTIME}</script>`)
|
|
108
152
|
for (const content of bootstrapScriptContentToArray(options.bootstrapScriptContent)) {
|
|
109
153
|
shellChunks.push(inlineBootstrapTag(content, nonce))
|
|
110
154
|
}
|
|
@@ -117,17 +161,20 @@ export async function streamHtml(
|
|
|
117
161
|
}
|
|
118
162
|
|
|
119
163
|
if (shellChunks.length) emit(normalizeDocumentShell(shellChunks.join('')))
|
|
164
|
+
state.onShellReady?.()
|
|
120
165
|
|
|
121
166
|
// 3. Stream suspended boundaries as they resolve
|
|
122
167
|
for (const b of boundaries) streamBoundary(b, emit, options, state)
|
|
123
168
|
await drain(state)
|
|
124
169
|
} catch (err) {
|
|
170
|
+
if (isBrowserError(err)) {
|
|
171
|
+
err = new Error('The server render could not complete because client rendering was requested outside a Suspense boundary.',
|
|
172
|
+
Object.hasOwn(err, 'cause') ? { cause: err.cause } : undefined)
|
|
173
|
+
}
|
|
125
174
|
state.errored = err
|
|
126
175
|
if (options.onError) options.onError(err)
|
|
127
176
|
throw err
|
|
128
177
|
} finally {
|
|
129
|
-
endSSR()
|
|
130
|
-
uninstallSSRDispatcher()
|
|
131
178
|
state.closed = true
|
|
132
179
|
}
|
|
133
180
|
}
|
|
@@ -138,39 +185,59 @@ function streamBoundary(
|
|
|
138
185
|
options: StreamOptions,
|
|
139
186
|
state: OrchestratorState,
|
|
140
187
|
): void {
|
|
188
|
+
const nonce = scriptNonce(options)
|
|
141
189
|
const task = (async () => {
|
|
142
190
|
try {
|
|
143
|
-
await b.thenable
|
|
144
|
-
} catch
|
|
145
|
-
if (options.onError) options.onError(err)
|
|
146
|
-
}
|
|
191
|
+
await Promise.race([b.thenable, state.cancelled])
|
|
192
|
+
} catch {}
|
|
147
193
|
if (state.closed) return
|
|
148
194
|
|
|
195
|
+
const defer = (error: unknown) => {
|
|
196
|
+
if (isBrowserError(error)) options.onBrowserBailout?.(error, { componentStack: (error as any).componentStack || b.componentStack })
|
|
197
|
+
else options.onError?.(error)
|
|
198
|
+
emit(`<script${nonce ? ` nonce="${escapeAttr(nonce)}"` : ''}>$RB(${b.id}${isBrowserError(error) ? '' : ',1'})</script>`)
|
|
199
|
+
}
|
|
200
|
+
if (state.aborted) { defer(state.reason); return }
|
|
201
|
+
|
|
149
202
|
// Re-render the boundary's children into a string, restoring the
|
|
150
203
|
// provider stack from when the boundary first suspended.
|
|
151
204
|
const parts: string[] = []
|
|
152
205
|
const sub: SuspendedBoundary[] = []
|
|
206
|
+
const resourceScope: ResourceScope = {}
|
|
207
|
+
let styles: string[][] | undefined
|
|
208
|
+
const previous = beginSSR(options.identifierPrefix, state.frame)
|
|
209
|
+
state.frame?.resources?.takeStylesheets()
|
|
153
210
|
const restore = applyContextSnapshot(b.contextSnapshot)
|
|
154
211
|
try {
|
|
155
212
|
walk(b.children, {
|
|
156
213
|
emit: (s) => parts.push(s),
|
|
157
214
|
onSuspend: (n) => sub.push(n),
|
|
158
215
|
nextBoundaryId: () => state.nextId++,
|
|
216
|
+
onBrowserBailout: options.onBrowserBailout,
|
|
217
|
+
componentStack: b.componentStack,
|
|
218
|
+
resourceScope,
|
|
159
219
|
})
|
|
220
|
+
state.frame?.resources?.mergeScope(resourceScope)
|
|
160
221
|
} catch (err) {
|
|
161
|
-
if (
|
|
162
|
-
|
|
222
|
+
if (isThenable(err)) {
|
|
223
|
+
streamBoundary({ ...b, thenable: err }, emit, options, state)
|
|
224
|
+
} else defer(err)
|
|
163
225
|
return
|
|
226
|
+
} finally {
|
|
227
|
+
restore()
|
|
228
|
+
endSSR(previous)
|
|
229
|
+
const hints = state.frame?.resources?.drain()
|
|
230
|
+
styles = state.frame?.resources?.takeStylesheets()
|
|
231
|
+
if (hints) emit(hints)
|
|
164
232
|
}
|
|
165
|
-
restore()
|
|
166
233
|
|
|
167
|
-
emit(`<div hidden id="S:${b.id}">${parts.join('')}</div>${revealScript(b.id,
|
|
234
|
+
emit(`<div hidden id="S:${b.id}">${parts.join('')}</div>${revealScript(b.id, nonce, styles)}`)
|
|
168
235
|
|
|
169
236
|
// Recurse: any nested suspensions inside the now-revealed content
|
|
170
237
|
for (const s of sub) streamBoundary(s, emit, options, state)
|
|
171
238
|
})()
|
|
172
239
|
state.pending.add(task)
|
|
173
|
-
task.
|
|
240
|
+
task.then(() => state.pending.delete(task), () => state.pending.delete(task))
|
|
174
241
|
}
|
|
175
242
|
|
|
176
243
|
async function drain(state: OrchestratorState): Promise<void> {
|
|
@@ -183,6 +250,10 @@ function isThenable(value: unknown): value is Promise<unknown> {
|
|
|
183
250
|
return !!value && typeof (value as { then?: unknown }).then === 'function'
|
|
184
251
|
}
|
|
185
252
|
|
|
253
|
+
function scriptNonce(options: StreamOptions): string | undefined {
|
|
254
|
+
return typeof options.nonce === 'string' ? options.nonce : options.nonce?.script
|
|
255
|
+
}
|
|
256
|
+
|
|
186
257
|
function bootstrapScriptContentToArray(
|
|
187
258
|
content: StreamOptions['bootstrapScriptContent'],
|
|
188
259
|
): string[] {
|
|
@@ -191,7 +262,7 @@ function bootstrapScriptContentToArray(
|
|
|
191
262
|
}
|
|
192
263
|
|
|
193
264
|
function inlineBootstrapTag(content: string, defaultNonce: string | undefined): string {
|
|
194
|
-
const nAttr = defaultNonce ? ` nonce="${defaultNonce}"` : ''
|
|
265
|
+
const nAttr = defaultNonce ? ` nonce="${escapeAttr(defaultNonce)}"` : ''
|
|
195
266
|
return `<script${nAttr}>${escapeScript(content)}</script>`
|
|
196
267
|
}
|
|
197
268
|
|
|
@@ -237,9 +308,9 @@ function bootstrapTag(
|
|
|
237
308
|
kind: 'script' | 'module',
|
|
238
309
|
defaultNonce: string | undefined,
|
|
239
310
|
): string {
|
|
240
|
-
const src = typeof entry === 'string' ? entry : entry.src
|
|
311
|
+
const src = escapeAttr(resourceURL(typeof entry === 'string' ? entry : entry.src))
|
|
241
312
|
const nonce = typeof entry === 'string' ? defaultNonce : entry.nonce ?? defaultNonce
|
|
242
|
-
const nAttr = nonce ? ` nonce="${nonce}"` : ''
|
|
313
|
+
const nAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : ''
|
|
243
314
|
if (kind === 'module') return `<script type="module"${nAttr} src="${src}"></script>`
|
|
244
315
|
return `<script async${nAttr} src="${src}"></script>`
|
|
245
316
|
}
|
|
@@ -252,13 +323,15 @@ export function renderToReadableStream(
|
|
|
252
323
|
children: ReactNode,
|
|
253
324
|
options: StreamOptions = {},
|
|
254
325
|
): Promise<ReadableStreamResult> {
|
|
255
|
-
const state
|
|
256
|
-
nextId: 0,
|
|
257
|
-
pending: new Set(),
|
|
258
|
-
closed: false,
|
|
259
|
-
errored: null,
|
|
260
|
-
}
|
|
326
|
+
const state = createState()
|
|
261
327
|
const encoder = new TextEncoder()
|
|
328
|
+
let abortListener: (() => void) | undefined
|
|
329
|
+
const detachAbortListener = () => {
|
|
330
|
+
if (abortListener) {
|
|
331
|
+
options.signal?.removeEventListener('abort', abortListener)
|
|
332
|
+
abortListener = undefined
|
|
333
|
+
}
|
|
334
|
+
}
|
|
262
335
|
|
|
263
336
|
let allReadyResolve!: () => void
|
|
264
337
|
let allReadyReject!: (e: unknown) => void
|
|
@@ -266,6 +339,15 @@ export function renderToReadableStream(
|
|
|
266
339
|
allReadyResolve = r
|
|
267
340
|
allReadyReject = rej
|
|
268
341
|
})
|
|
342
|
+
// Shell failures reject the outer promise before callers receive allReady.
|
|
343
|
+
allReady.catch(() => {})
|
|
344
|
+
let shellResolve!: (stream: ReadableStreamResult) => void
|
|
345
|
+
let shellReject!: (error: unknown) => void
|
|
346
|
+
const shell = new Promise<ReadableStreamResult>((resolve, reject) => {
|
|
347
|
+
shellResolve = resolve
|
|
348
|
+
shellReject = reject
|
|
349
|
+
})
|
|
350
|
+
state.onShellReady = () => queueMicrotask(() => shellResolve(Object.assign(stream, { allReady })))
|
|
269
351
|
|
|
270
352
|
const stream = new ReadableStream<Uint8Array>({
|
|
271
353
|
start(controller) {
|
|
@@ -275,31 +357,41 @@ export function renderToReadableStream(
|
|
|
275
357
|
} catch {}
|
|
276
358
|
}
|
|
277
359
|
|
|
360
|
+
if (options.signal) {
|
|
361
|
+
abortListener = () => {
|
|
362
|
+
detachAbortListener()
|
|
363
|
+
state.abort(options.signal!.reason)
|
|
364
|
+
}
|
|
365
|
+
options.signal.addEventListener('abort', abortListener)
|
|
366
|
+
if (options.signal.aborted) abortListener()
|
|
367
|
+
}
|
|
368
|
+
|
|
278
369
|
streamHtml(children, emit, options, state).then(
|
|
279
370
|
() => {
|
|
371
|
+
detachAbortListener()
|
|
280
372
|
try {
|
|
281
373
|
controller.close()
|
|
282
374
|
} catch {}
|
|
283
375
|
allReadyResolve()
|
|
284
376
|
},
|
|
285
377
|
(err) => {
|
|
378
|
+
detachAbortListener()
|
|
286
379
|
try {
|
|
287
380
|
controller.error(err)
|
|
288
381
|
} catch {}
|
|
289
382
|
allReadyReject(err)
|
|
383
|
+
shellReject(err)
|
|
290
384
|
},
|
|
291
385
|
)
|
|
292
386
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
} catch {}
|
|
298
|
-
})
|
|
387
|
+
},
|
|
388
|
+
cancel(reason) {
|
|
389
|
+
state.abort(reason)
|
|
390
|
+
detachAbortListener()
|
|
299
391
|
},
|
|
300
392
|
})
|
|
301
393
|
|
|
302
|
-
return
|
|
394
|
+
return shell
|
|
303
395
|
}
|
|
304
396
|
|
|
305
397
|
// ---------------------------------------------------------------------------
|
|
@@ -321,18 +413,20 @@ export function renderToPipeableStream(
|
|
|
321
413
|
children: ReactNode,
|
|
322
414
|
options: PipeableOptions = {},
|
|
323
415
|
): PipeableHandle {
|
|
324
|
-
const state
|
|
325
|
-
nextId: 0,
|
|
326
|
-
pending: new Set(),
|
|
327
|
-
closed: false,
|
|
328
|
-
errored: null,
|
|
329
|
-
}
|
|
416
|
+
const state = createState()
|
|
330
417
|
|
|
331
418
|
const buffers: string[] = []
|
|
332
419
|
let dest: PipeableWritable | null = null
|
|
333
420
|
let shellReady = false
|
|
421
|
+
let shellFailed = false
|
|
334
422
|
let finished = false
|
|
335
|
-
let
|
|
423
|
+
let failed = false
|
|
424
|
+
let failure: unknown
|
|
425
|
+
|
|
426
|
+
const finishDestination = (target: PipeableWritable) => {
|
|
427
|
+
if (failed && target.destroy) target.destroy(failure)
|
|
428
|
+
else target.end()
|
|
429
|
+
}
|
|
336
430
|
|
|
337
431
|
const flushTo = (w: PipeableWritable) => {
|
|
338
432
|
if (!buffers.length) return
|
|
@@ -341,11 +435,17 @@ export function renderToPipeableStream(
|
|
|
341
435
|
}
|
|
342
436
|
|
|
343
437
|
const emit: Emit = (chunk) => {
|
|
344
|
-
if (
|
|
438
|
+
if (finished) return
|
|
345
439
|
if (dest) dest.write(chunk)
|
|
346
440
|
else buffers.push(chunk)
|
|
347
441
|
}
|
|
348
442
|
|
|
443
|
+
state.onShellReady = () => queueMicrotask(() => {
|
|
444
|
+
if (shellFailed) return
|
|
445
|
+
shellReady = true
|
|
446
|
+
options.onShellReady?.()
|
|
447
|
+
})
|
|
448
|
+
|
|
349
449
|
// Kick off rendering
|
|
350
450
|
streamHtml(children, emit, options, state).then(
|
|
351
451
|
() => {
|
|
@@ -354,35 +454,27 @@ export function renderToPipeableStream(
|
|
|
354
454
|
options.onAllReady?.()
|
|
355
455
|
},
|
|
356
456
|
(err) => {
|
|
457
|
+
finished = true
|
|
458
|
+
failed = true
|
|
459
|
+
failure = err
|
|
357
460
|
if (!shellReady) {
|
|
461
|
+
shellFailed = true
|
|
358
462
|
options.onShellError?.(err)
|
|
359
463
|
} else {
|
|
360
|
-
|
|
361
|
-
if (dest) dest.end()
|
|
464
|
+
if (dest) finishDestination(dest)
|
|
362
465
|
}
|
|
363
466
|
},
|
|
364
467
|
)
|
|
365
468
|
|
|
366
|
-
// We call onShellReady once the first synchronous emit has landed.
|
|
367
|
-
// streamHtml above runs the shell synchronously before the first await in drain(),
|
|
368
|
-
// so we can schedule onShellReady right after the first microtask.
|
|
369
|
-
queueMicrotask(() => {
|
|
370
|
-
if (aborted || finished) return
|
|
371
|
-
shellReady = true
|
|
372
|
-
options.onShellReady?.()
|
|
373
|
-
})
|
|
374
|
-
|
|
375
469
|
return {
|
|
376
470
|
pipe<T extends PipeableWritable>(target: T): T {
|
|
377
471
|
dest = target
|
|
378
|
-
flushTo(target)
|
|
379
|
-
if (finished) target
|
|
472
|
+
if (!failed) flushTo(target)
|
|
473
|
+
if (finished) finishDestination(target)
|
|
380
474
|
return target
|
|
381
475
|
},
|
|
382
|
-
abort(
|
|
383
|
-
|
|
384
|
-
state.closed = true
|
|
385
|
-
if (dest) dest.end()
|
|
476
|
+
abort(reason?: unknown) {
|
|
477
|
+
state.abort(reason)
|
|
386
478
|
},
|
|
387
479
|
}
|
|
388
480
|
}
|
package/src/server/walk.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
REACT_STRICT_MODE_TYPE,
|
|
16
16
|
REACT_PROFILER_TYPE,
|
|
17
17
|
REACT_PORTAL_TYPE,
|
|
18
|
+
REACT_ACTIVITY_TYPE,
|
|
19
|
+
REACT_VIEW_TRANSITION_TYPE,
|
|
18
20
|
} from '../react'
|
|
19
21
|
import {
|
|
20
22
|
attrToHtml,
|
|
@@ -27,8 +29,19 @@ import {
|
|
|
27
29
|
pushContext,
|
|
28
30
|
popContext,
|
|
29
31
|
snapshotContexts,
|
|
32
|
+
recordHostResource,
|
|
33
|
+
currentSSRFrame,
|
|
30
34
|
type ContextSnapshot,
|
|
31
35
|
} from './dispatcher'
|
|
36
|
+
import { isBrowserError } from '../core/browser'
|
|
37
|
+
import { resourceKind } from '../core/resource-hints'
|
|
38
|
+
import type { ResourceScope } from './resource-hints'
|
|
39
|
+
|
|
40
|
+
export interface ServerErrorInfo {
|
|
41
|
+
componentStack: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type BrowserBailoutCallback = (error: Error, errorInfo: ServerErrorInfo) => void
|
|
32
45
|
|
|
33
46
|
export interface SuspendedBoundary {
|
|
34
47
|
id: number
|
|
@@ -36,15 +49,21 @@ export interface SuspendedBoundary {
|
|
|
36
49
|
children: ReactNode
|
|
37
50
|
thenable: Promise<any>
|
|
38
51
|
contextSnapshot: ContextSnapshot
|
|
52
|
+
componentStack: string
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
export interface WalkOptions {
|
|
42
56
|
emit: (chunk: string) => void
|
|
57
|
+
onHead?: () => void
|
|
43
58
|
onSuspend?: ((boundary: SuspendedBoundary) => void) | undefined
|
|
59
|
+
onBrowserBailout?: BrowserBailoutCallback | undefined
|
|
60
|
+
componentStack?: string | undefined
|
|
61
|
+
thrownInfo?: { componentStack?: string | undefined }
|
|
44
62
|
nextBoundaryId: () => number
|
|
45
63
|
bootstrapped?: boolean | undefined
|
|
46
64
|
isBoundaryResolution?: boolean | undefined
|
|
47
65
|
isSvg?: boolean | undefined
|
|
66
|
+
resourceScope?: ResourceScope | undefined
|
|
48
67
|
/**
|
|
49
68
|
* Tracks whether the most recent emission within the *current text flow*
|
|
50
69
|
* ended with a text node. When the next emission is also text, we emit a
|
|
@@ -160,6 +179,15 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
|
|
|
160
179
|
return
|
|
161
180
|
}
|
|
162
181
|
|
|
182
|
+
if (type === REACT_ACTIVITY_TYPE) {
|
|
183
|
+
if (props.mode !== 'hidden') walkNode(props.children, opts)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
if (type === REACT_VIEW_TRANSITION_TYPE) {
|
|
187
|
+
walkNode(props.children, opts)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
163
191
|
if (typeof type === 'string') {
|
|
164
192
|
walkHost(type, props, opts)
|
|
165
193
|
return
|
|
@@ -196,8 +224,7 @@ function walkElement(el: ReactElement, opts: WalkOptions): void {
|
|
|
196
224
|
const render = (type as any).render
|
|
197
225
|
const ref = (props as any).ref ?? null
|
|
198
226
|
const { ref: _omit, ...rest } = props as any
|
|
199
|
-
|
|
200
|
-
walkNode(rendered, opts)
|
|
227
|
+
walkComponent(render, rest, opts, ref)
|
|
201
228
|
return
|
|
202
229
|
}
|
|
203
230
|
|
|
@@ -234,6 +261,7 @@ function walkHost(
|
|
|
234
261
|
opts: WalkOptions,
|
|
235
262
|
): void {
|
|
236
263
|
const isSvg = opts.isSvg || tag === 'svg'
|
|
264
|
+
if (!isSvg && resourceKind(tag, props) && recordHostResource(tag, props, opts.resourceScope)) return
|
|
237
265
|
// <textarea value="..."> serializes its value as a TEXT CHILD, not an
|
|
238
266
|
// attribute. `defaultValue` is the fallback when `value` is absent. This
|
|
239
267
|
// matches React and the HTML spec — `<textarea value="x">` is not valid
|
|
@@ -270,6 +298,7 @@ function walkHost(
|
|
|
270
298
|
pushSelectContext(val)
|
|
271
299
|
}
|
|
272
300
|
const isOption = tag === 'option'
|
|
301
|
+
try {
|
|
273
302
|
|
|
274
303
|
// Prepend the HTML5 doctype to the stream when rendering an <html> root.
|
|
275
304
|
// Without it the browser parses the document in quirks mode, which breaks
|
|
@@ -312,6 +341,7 @@ function walkHost(
|
|
|
312
341
|
return
|
|
313
342
|
}
|
|
314
343
|
opts.emit('>')
|
|
344
|
+
if (tag === 'head') opts.onHead?.()
|
|
315
345
|
// Opening a host element starts a fresh text flow context for its children.
|
|
316
346
|
// Children's text separator tracking is independent of the outer context.
|
|
317
347
|
const parentTextState = opts.textState
|
|
@@ -322,7 +352,9 @@ function walkHost(
|
|
|
322
352
|
}
|
|
323
353
|
|
|
324
354
|
if (tag === 'html' && !hasHeadChild(props.children)) {
|
|
325
|
-
opts.emit('<head
|
|
355
|
+
opts.emit('<head>')
|
|
356
|
+
opts.onHead?.()
|
|
357
|
+
opts.emit('</head>')
|
|
326
358
|
}
|
|
327
359
|
|
|
328
360
|
const dangerouslyHtml = props.dangerouslySetInnerHTML?.__html
|
|
@@ -358,9 +390,11 @@ function walkHost(
|
|
|
358
390
|
walkNode(props.children, childOpts)
|
|
359
391
|
}
|
|
360
392
|
opts.emit(`</${tag}>`)
|
|
361
|
-
if (isSelect) popSelectContext()
|
|
362
393
|
// Host element closing resets outer flow — next sibling text starts fresh.
|
|
363
394
|
if (parentTextState) parentTextState.lastWasText = false
|
|
395
|
+
} finally {
|
|
396
|
+
if (isSelect) popSelectContext()
|
|
397
|
+
}
|
|
364
398
|
}
|
|
365
399
|
|
|
366
400
|
function hasHeadChild(children: unknown): boolean {
|
|
@@ -393,7 +427,12 @@ function walkComponent(
|
|
|
393
427
|
fn: Function,
|
|
394
428
|
props: Record<string, any>,
|
|
395
429
|
opts: WalkOptions,
|
|
430
|
+
ref?: unknown,
|
|
396
431
|
): void {
|
|
432
|
+
if (opts.onBrowserBailout) {
|
|
433
|
+
opts = { ...opts, componentStack: `\n at ${(fn as any).displayName || fn.name || 'Anonymous'}${opts.componentStack || ''}` }
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
397
436
|
if ((fn as any).prototype?.isReactComponent) {
|
|
398
437
|
const ctxType = (fn as any).contextType
|
|
399
438
|
const ctxValue = ctxType ? ctxType._currentValue : undefined
|
|
@@ -407,8 +446,15 @@ function walkComponent(
|
|
|
407
446
|
walkNode(instance.render(), opts)
|
|
408
447
|
return
|
|
409
448
|
}
|
|
410
|
-
const rendered = (fn as any)(props)
|
|
449
|
+
const rendered = (fn as any)(props, ref)
|
|
411
450
|
walkNode(rendered, opts)
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (opts.thrownInfo && !opts.thrownInfo.componentStack) opts.thrownInfo.componentStack = opts.componentStack
|
|
453
|
+
if (isBrowserError(error) && !(error as any).componentStack) {
|
|
454
|
+
Object.defineProperty(error, 'componentStack', { value: opts.componentStack })
|
|
455
|
+
}
|
|
456
|
+
throw error
|
|
457
|
+
}
|
|
412
458
|
}
|
|
413
459
|
|
|
414
460
|
function walkSuspense(
|
|
@@ -423,26 +469,37 @@ function walkSuspense(
|
|
|
423
469
|
// Try to render the children synchronously. If a thenable is thrown,
|
|
424
470
|
// record the boundary and emit the fallback.
|
|
425
471
|
const childParts: string[] = []
|
|
472
|
+
const nested: SuspendedBoundary[] = []
|
|
473
|
+
const thrownInfo: { componentStack?: string | undefined } = {}
|
|
426
474
|
const childEmit = (s: string) => childParts.push(s)
|
|
475
|
+
const resourceScope: ResourceScope | undefined = currentSSRFrame().shellFlushed ? {} : undefined
|
|
427
476
|
try {
|
|
428
477
|
walkNode(props.children, {
|
|
478
|
+
...opts,
|
|
479
|
+
resourceScope,
|
|
480
|
+
thrownInfo,
|
|
429
481
|
emit: childEmit,
|
|
430
|
-
onSuspend: opts.onSuspend,
|
|
482
|
+
onSuspend: opts.onSuspend ? (boundary) => nested.push(boundary) : undefined,
|
|
431
483
|
nextBoundaryId: opts.nextBoundaryId,
|
|
432
484
|
})
|
|
433
485
|
} catch (thenable: any) {
|
|
434
|
-
|
|
486
|
+
nested.length = 0
|
|
487
|
+
if (isThenable(thenable) || isBrowserError(thenable)) {
|
|
435
488
|
const fallbackParts: string[] = []
|
|
436
|
-
try {
|
|
437
489
|
walkNode(props.fallback, {
|
|
490
|
+
...opts,
|
|
438
491
|
emit: (s) => fallbackParts.push(s),
|
|
439
|
-
onSuspend: opts.onSuspend,
|
|
492
|
+
onSuspend: opts.onSuspend ? (boundary) => nested.push(boundary) : undefined,
|
|
440
493
|
nextBoundaryId: opts.nextBoundaryId,
|
|
441
494
|
})
|
|
442
|
-
|
|
443
|
-
|
|
495
|
+
if (isBrowserError(thenable)) {
|
|
496
|
+
opts.onBrowserBailout?.(thenable, { componentStack: (thenable as any).componentStack || opts.componentStack || '' })
|
|
497
|
+
opts.emit(`<!--$!${id}-->${fallbackParts.join('')}<!--/$-->`)
|
|
498
|
+
for (const boundary of nested) opts.onSuspend?.(boundary)
|
|
499
|
+
return
|
|
444
500
|
}
|
|
445
501
|
emitBoundary(opts, id, fallbackParts.join(''))
|
|
502
|
+
for (const boundary of nested) opts.onSuspend?.(boundary)
|
|
446
503
|
|
|
447
504
|
if (opts.onSuspend) {
|
|
448
505
|
opts.onSuspend({
|
|
@@ -451,6 +508,7 @@ function walkSuspense(
|
|
|
451
508
|
children: props.children,
|
|
452
509
|
thenable,
|
|
453
510
|
contextSnapshot,
|
|
511
|
+
componentStack: thrownInfo.componentStack || opts.componentStack || '',
|
|
454
512
|
})
|
|
455
513
|
}
|
|
456
514
|
return
|
|
@@ -458,6 +516,8 @@ function walkSuspense(
|
|
|
458
516
|
throw thenable
|
|
459
517
|
}
|
|
460
518
|
|
|
519
|
+
if (resourceScope) currentSSRFrame().resources?.mergeScope(resourceScope, opts.resourceScope)
|
|
520
|
+
|
|
461
521
|
// Children rendered fully — emit them wrapped in resolved-boundary markers
|
|
462
522
|
// (`<!--$N-->` / `<!--/$-->`). Without markers, the client hydrator has no
|
|
463
523
|
// way to know this subtree is inside a Suspense, so if the client version
|
|
@@ -468,6 +528,7 @@ function walkSuspense(
|
|
|
468
528
|
opts.emit(`<!--$${id}-->`)
|
|
469
529
|
opts.emit(childParts.join(''))
|
|
470
530
|
opts.emit(`<!--/$-->`)
|
|
531
|
+
for (const boundary of nested) opts.onSuspend?.(boundary)
|
|
471
532
|
}
|
|
472
533
|
|
|
473
534
|
function emitBoundary(opts: WalkOptions, id: number, fallbackHTML: string): void {
|