-
-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathonRenderClient.tsx
More file actions
140 lines (124 loc) · 5.99 KB
/
onRenderClient.tsx
File metadata and controls
140 lines (124 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// https://vike.dev/onRenderClient
export { onRenderClient }
import ReactDOM, { type RootOptions } from 'react-dom/client'
import { getHeadSetting } from './getHeadSetting.js'
import type { PageContextClient } from 'vike/types'
import { getPageElement } from './getPageElement.js'
import type { PageContextInternal } from '../types/PageContext.js'
import { callCumulativeHooks } from '../utils/callCumulativeHooks.js'
import { applyHeadSettings } from './applyHeadSettings.js'
import { resolveReactOptions } from './resolveReactOptions.js'
import { getGlobalObject } from '../utils/getGlobalObject.js'
import { isObject } from '../utils/isObject.js'
import { getBetterErrorLight } from '../utils/getBetterErrorLight.js'
const globalObject = getGlobalObject<{
root?: ReactDOM.Root
onUncaughtErrorLocal?: (err: unknown) => void
}>('onRenderClient.tsx', {})
async function onRenderClient(pageContext: PageContextClient & PageContextInternal) {
pageContext._headAlreadySet = pageContext.isHydration
// Use case:
// - Store hydration https://github.com/vikejs/vike-react/issues/110
await callCumulativeHooks(pageContext.config.onBeforeRenderClient, pageContext)
const { page, renderPromise, renderPromiseReject } = getPageElement(pageContext)
pageContext.page = page
// Local callback for current page
globalObject.onUncaughtErrorLocal = (err: unknown) => {
renderPromiseReject(err)
}
const container = document.getElementById('root')!
const { hydrateRootOptions, createRootOptions } = resolveReactOptions(pageContext)
if (
pageContext.isHydration &&
// Whether the page was [Server-Side Rendered](https://vike.dev/ssr).
container.innerHTML !== ''
) {
// First render while using SSR, i.e. [hydration](https://vike.dev/hydration)
globalObject.root = ReactDOM.hydrateRoot(container, page, {
...hydrateRootOptions,
// onUncaughtError is the right callback: https://gist.github.com/brillout/b9516e83a7a4517f4dbd0ef50e9dd716
onUncaughtError(...args) {
onUncaughtErrorGlobal.call(this, args, hydrateRootOptions)
},
})
} else {
if (!globalObject.root) {
// First render without SSR
globalObject.root = ReactDOM.createRoot(container, {
...createRootOptions,
onUncaughtError(...args) {
onUncaughtErrorGlobal.call(this, args, createRootOptions)
},
})
}
globalObject.root.render(page)
}
pageContext.root = globalObject.root
try {
await renderPromise
} finally {
delete globalObject.onUncaughtErrorLocal
}
if (!pageContext.isHydration) {
pageContext._headAlreadySet = true
applyHead(pageContext)
}
// Use cases:
// - Custom user settings: https://vike.dev/head-tags#custom-settings
// - Testing tools: https://github.com/vikejs/vike-react/issues/95
await callCumulativeHooks(pageContext.config.onAfterRenderClient, pageContext)
}
function applyHead(pageContext: PageContextClient) {
const title = getHeadSetting<string | null>('title', pageContext)
const lang = getHeadSetting<string | null>('lang', pageContext)
applyHeadSettings(title, lang)
}
// Global callback, attached once upon hydration.
function onUncaughtErrorGlobal(
this: unknown,
args: OnUncaughtErrorArgs,
userOptions: { onUncaughtError?: OnUncaughtError } | undefined,
) {
const [errorOriginal, errorInfo] = args
const errorEnhanced = getErrorWithComponentStack(errorOriginal, errorInfo)
console.error(errorEnhanced)
// Used by Vike:
// https://github.com/vikejs/vike/blob/8ce2cbda756892f0ff083256291515b5a45fe319/packages/vike/client/runtime-client-routing/renderPageClientSide.ts#L838-L844
if (isObject(errorEnhanced)) errorEnhanced.isAlreadyLogged = true
globalObject.onUncaughtErrorLocal?.(errorEnhanced)
userOptions?.onUncaughtError?.call(this, errorEnhanced, errorInfo)
}
type OnUncaughtError = RootOptions['onUncaughtError']
type OnUncaughtErrorArgs = Parameters<NonNullable<RootOptions['onUncaughtError']>>
type OnUncaughtErrorInfo = OnUncaughtErrorArgs[1]
// Inject componentStack to the error's stack trace
// - Server counterpart: https://github.com/brillout/react-streaming/blob/e0a6210957e65dad2c92877ad075ebac4713d8fa/src/server/renderToStream/common.ts#L93
function getErrorWithComponentStack(errorOriginal: unknown, errorInfo?: OnUncaughtErrorInfo) {
if (!errorInfo?.componentStack || !isObject(errorOriginal)) return errorOriginal
const errorStackLines = String(errorOriginal.stack).split('\n')
// Inject the component stack right before the React stack trace (potentially *after* some vike-react or react-streaming strack trace, e.g. if react-streaming's useAsync() throws an error).
// Perfect cutoff (as of react@19.2.0), but can easily break upon React internal refactoring
let cutoff = errorStackLines.findIndex((l) => l.includes('react_stack_bottom_frame'))
if (cutoff === -1) {
// Ideally, we should inject the component stack right before the React stack trace, and *after* any vike-react or react-streaming strack trace.
// But we cannot (easily) do that on the client-side, because Vite pre-bundles React, vike-react, and react-streaming inside a single bundle:
// ```console
// # This is React code, but it's included inside the vike-react pre-optimized bundle
// Object.react_stack_bottom_frame (http://localhost:3000/node_modules/.vite/deps/vike-react___internal_integration_onRenderClient.js)
// ```
cutoff = errorStackLines.findIndex((l) => l.includes('node_modules') && l.includes('react'))
}
if (cutoff === -1) return errorOriginal
const errorStackLinesBegin = errorStackLines.slice(0, cutoff)
const errorStackLinesEnd = errorStackLines.slice(cutoff)
const componentStackLines = errorInfo.componentStack.split('\n').filter(Boolean)
if (componentStackLines[0] === errorStackLinesBegin.at(-1)) componentStackLines.shift()
const stackEnhanced = [
//
...errorStackLinesBegin,
...componentStackLines,
...errorStackLinesEnd,
].join('\n')
const errorBetter = getBetterErrorLight(errorOriginal, { stack: stackEnhanced })
return errorBetter
}