|
1 | 1 | import {
|
| 2 | + ACTION_BEFORE_REFRESH, |
| 3 | + ACTION_BUILD_ERROR, |
| 4 | + ACTION_BUILD_OK, |
| 5 | + ACTION_DEBUG_INFO, |
| 6 | + ACTION_DEV_INDICATOR, |
| 7 | + ACTION_REFRESH, |
| 8 | + ACTION_ERROR_OVERLAY_CLOSE, |
2 | 9 | ACTION_ERROR_OVERLAY_OPEN,
|
3 |
| - type OverlayDispatch, |
4 |
| - type OverlayState, |
| 10 | + ACTION_ERROR_OVERLAY_TOGGLE, |
| 11 | + ACTION_STATIC_INDICATOR, |
| 12 | + ACTION_UNHANDLED_ERROR, |
| 13 | + ACTION_UNHANDLED_REJECTION, |
| 14 | + ACTION_VERSION_INFO, |
| 15 | + useErrorOverlayReducer, |
5 | 16 | } from '../shared'
|
6 | 17 | import type { GlobalErrorComponent } from '../../global-error'
|
7 | 18 |
|
8 |
| -import { useCallback, useEffect } from 'react' |
9 |
| -import { AppDevOverlayErrorBoundary } from './app-dev-overlay-error-boundary' |
| 19 | +import { useEffect, useInsertionEffect } from 'react' |
| 20 | +import { AppDevOverlayErrorBoundary as AppDevOverlayErrorBoundaryImpl } from './app-dev-overlay-error-boundary' |
10 | 21 | import { FontStyles } from '../font/font-styles'
|
| 22 | +import type { DebugInfo } from '../types' |
11 | 23 | import { DevOverlay } from '../ui/dev-overlay'
|
| 24 | +import { parseStack } from '../utils/parse-stack' |
| 25 | +import { parseComponentStack } from '../utils/parse-component-stack' |
| 26 | +import { getComponentStack, getOwnerStack } from '../../errors/stitched-error' |
12 | 27 | import { handleClientError } from '../../errors/use-error-handler'
|
13 | 28 | import { isNextRouterError } from '../../is-next-router-error'
|
14 | 29 | import { MISSING_ROOT_TAGS_ERROR } from '../../../../shared/lib/errors/constants'
|
| 30 | +import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state' |
| 31 | +import type { VersionInfo } from '../../../../server/dev/parse-version-info' |
15 | 32 |
|
16 | 33 | function readSsrError(): (Error & { digest?: string }) | null {
|
17 | 34 | if (typeof document === 'undefined') {
|
@@ -72,35 +89,157 @@ function ReplaySsrOnlyErrors({
|
72 | 89 | return null
|
73 | 90 | }
|
74 | 91 |
|
75 |
| -export function AppDevOverlay({ |
76 |
| - state, |
77 |
| - dispatch, |
| 92 | +export function AppDevOverlayErrorBoundary({ |
78 | 93 | globalError,
|
79 | 94 | children,
|
80 | 95 | }: {
|
81 |
| - state: OverlayState |
82 |
| - dispatch: OverlayDispatch |
83 | 96 | globalError: [GlobalErrorComponent, React.ReactNode]
|
84 | 97 | children: React.ReactNode
|
85 | 98 | }) {
|
86 |
| - const openOverlay = useCallback(() => { |
| 99 | + function openOverlay() { |
| 100 | + dispatcher.openErrorOverlay() |
| 101 | + } |
| 102 | + |
| 103 | + return ( |
| 104 | + <AppDevOverlayErrorBoundaryImpl |
| 105 | + globalError={globalError} |
| 106 | + onError={openOverlay} |
| 107 | + > |
| 108 | + <ReplaySsrOnlyErrors onBlockingError={openOverlay} /> |
| 109 | + {children} |
| 110 | + </AppDevOverlayErrorBoundaryImpl> |
| 111 | + ) |
| 112 | +} |
| 113 | + |
| 114 | +export interface Dispatcher { |
| 115 | + onBuildOk(): void |
| 116 | + onBuildError(message: string): void |
| 117 | + onVersionInfo(versionInfo: VersionInfo): void |
| 118 | + onDebugInfo(debugInfo: DebugInfo): void |
| 119 | + onBeforeRefresh(): void |
| 120 | + onRefresh(): void |
| 121 | + onStaticIndicator(status: boolean): void |
| 122 | + onDevIndicator(devIndicator: DevIndicatorServerState): void |
| 123 | + onUnhandledError(reason: Error): void |
| 124 | + onUnhandledRejection(reason: Error): void |
| 125 | + openErrorOverlay(): void |
| 126 | + closeErrorOverlay(): void |
| 127 | + toggleErrorOverlay(): void |
| 128 | +} |
| 129 | + |
| 130 | +type Dispatch = ReturnType<typeof useErrorOverlayReducer>[1] |
| 131 | +let maybeDispatch: Dispatch | null = null |
| 132 | +const queue: Array<(dispatch: Dispatch) => void> = [] |
| 133 | + |
| 134 | +// Events might be dispatched before we get a `dispatch` from React (e.g. console.error during module eval). |
| 135 | +// We need to queue them until we have a `dispatch` function available. |
| 136 | +function createQueuable<Args extends any[]>( |
| 137 | + queueableFunction: (dispatch: Dispatch, ...args: Args) => void |
| 138 | +) { |
| 139 | + return (...args: Args) => { |
| 140 | + if (maybeDispatch) { |
| 141 | + queueableFunction(maybeDispatch, ...args) |
| 142 | + } else { |
| 143 | + queue.push((dispatch: Dispatch) => { |
| 144 | + queueableFunction(dispatch, ...args) |
| 145 | + }) |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +// TODO: Extract into separate functions that are imported |
| 151 | +export const dispatcher: Dispatcher = { |
| 152 | + onBuildOk: createQueuable((dispatch: Dispatch) => { |
| 153 | + dispatch({ type: ACTION_BUILD_OK }) |
| 154 | + }), |
| 155 | + onBuildError: createQueuable((dispatch: Dispatch, message: string) => { |
| 156 | + dispatch({ type: ACTION_BUILD_ERROR, message }) |
| 157 | + }), |
| 158 | + onBeforeRefresh: createQueuable((dispatch: Dispatch) => { |
| 159 | + dispatch({ type: ACTION_BEFORE_REFRESH }) |
| 160 | + }), |
| 161 | + onRefresh: createQueuable((dispatch: Dispatch) => { |
| 162 | + dispatch({ type: ACTION_REFRESH }) |
| 163 | + }), |
| 164 | + onVersionInfo: createQueuable( |
| 165 | + (dispatch: Dispatch, versionInfo: VersionInfo) => { |
| 166 | + dispatch({ type: ACTION_VERSION_INFO, versionInfo }) |
| 167 | + } |
| 168 | + ), |
| 169 | + onStaticIndicator: createQueuable((dispatch: Dispatch, status: boolean) => { |
| 170 | + dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status }) |
| 171 | + }), |
| 172 | + onDebugInfo: createQueuable((dispatch: Dispatch, debugInfo: DebugInfo) => { |
| 173 | + dispatch({ type: ACTION_DEBUG_INFO, debugInfo }) |
| 174 | + }), |
| 175 | + onDevIndicator: createQueuable( |
| 176 | + (dispatch: Dispatch, devIndicator: DevIndicatorServerState) => { |
| 177 | + dispatch({ type: ACTION_DEV_INDICATOR, devIndicator }) |
| 178 | + } |
| 179 | + ), |
| 180 | + onUnhandledError: createQueuable((dispatch: Dispatch, error: Error) => { |
| 181 | + // Component stack is added to the error in use-error-handler in case there was a hydration error |
| 182 | + const componentStack = getComponentStack(error) |
| 183 | + const ownerStack = getOwnerStack(error) |
| 184 | + |
| 185 | + dispatch({ |
| 186 | + type: ACTION_UNHANDLED_ERROR, |
| 187 | + reason: error, |
| 188 | + frames: parseStack((error.stack || '') + (ownerStack || '')), |
| 189 | + componentStackFrames: |
| 190 | + typeof componentStack === 'string' |
| 191 | + ? parseComponentStack(componentStack) |
| 192 | + : undefined, |
| 193 | + }) |
| 194 | + }), |
| 195 | + onUnhandledRejection: createQueuable((dispatch: Dispatch, error: Error) => { |
| 196 | + const ownerStack = getOwnerStack(error) |
| 197 | + dispatch({ |
| 198 | + type: ACTION_UNHANDLED_REJECTION, |
| 199 | + reason: error, |
| 200 | + frames: parseStack((error.stack || '') + (ownerStack || '')), |
| 201 | + }) |
| 202 | + }), |
| 203 | + openErrorOverlay: createQueuable((dispatch: Dispatch) => { |
87 | 204 | dispatch({ type: ACTION_ERROR_OVERLAY_OPEN })
|
88 |
| - }, [dispatch]) |
| 205 | + }), |
| 206 | + closeErrorOverlay: createQueuable((dispatch: Dispatch) => { |
| 207 | + dispatch({ type: ACTION_ERROR_OVERLAY_CLOSE }) |
| 208 | + }), |
| 209 | + toggleErrorOverlay: createQueuable((dispatch: Dispatch) => { |
| 210 | + dispatch({ type: ACTION_ERROR_OVERLAY_TOGGLE }) |
| 211 | + }), |
| 212 | +} |
| 213 | + |
| 214 | +function replayQueuedEvents(dispatch: NonNullable<typeof maybeDispatch>) { |
| 215 | + try { |
| 216 | + for (const queuedFunction of queue) { |
| 217 | + queuedFunction(dispatch) |
| 218 | + } |
| 219 | + } finally { |
| 220 | + // TODO: What to do with failed events? |
| 221 | + queue.length = 0 |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +export function AppDevOverlay() { |
| 226 | + const [state, dispatch] = useErrorOverlayReducer('app') |
| 227 | + |
| 228 | + useInsertionEffect(() => { |
| 229 | + maybeDispatch = dispatch |
| 230 | + |
| 231 | + replayQueuedEvents(dispatch) |
| 232 | + |
| 233 | + return () => { |
| 234 | + maybeDispatch = null |
| 235 | + } |
| 236 | + }) |
89 | 237 |
|
90 | 238 | return (
|
91 | 239 | <>
|
92 |
| - <AppDevOverlayErrorBoundary |
93 |
| - globalError={globalError} |
94 |
| - onError={openOverlay} |
95 |
| - > |
96 |
| - <ReplaySsrOnlyErrors onBlockingError={openOverlay} /> |
97 |
| - {children} |
98 |
| - </AppDevOverlayErrorBoundary> |
99 |
| - <> |
100 |
| - {/* Fonts can only be loaded outside the Shadow DOM. */} |
101 |
| - <FontStyles /> |
102 |
| - <DevOverlay state={state} dispatch={dispatch} /> |
103 |
| - </> |
| 240 | + {/* Fonts can only be loaded outside the Shadow DOM. */} |
| 241 | + <FontStyles /> |
| 242 | + <DevOverlay state={state} dispatch={dispatch} /> |
104 | 243 | </>
|
105 | 244 | )
|
106 | 245 | }
|
0 commit comments