refactor(error-overlay): unify Pages/App router error overlay source (#62939)

This commit is contained in:
Balázs Orbán 2024-03-10 22:34:30 +01:00 committed by GitHub
parent fff9ddc204
commit 1e26cceff4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 417 additions and 583 deletions

View file

@ -2,7 +2,7 @@ import { bold, red, yellow } from '../../lib/picocolors'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import textTable from 'next/dist/compiled/text-table'
import createStore from 'next/dist/compiled/unistore'
import formatWebpackMessages from '../../client/dev/error-overlay/format-webpack-messages'
import formatWebpackMessages from '../../client/components/react-dev-overlay/internal/helpers/format-webpack-messages'
import { store as consoleStore } from './store'
import type { OutputState } from './store'
import type { webpack } from 'next/dist/compiled/webpack/webpack'

View file

@ -1,6 +1,6 @@
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { red } from '../../lib/picocolors'
import formatWebpackMessages from '../../client/dev/error-overlay/format-webpack-messages'
import formatWebpackMessages from '../../client/components/react-dev-overlay/internal/helpers/format-webpack-messages'
import { nonNullable } from '../../lib/non-nullable'
import type { COMPILER_INDEXES } from '../../shared/lib/constants'
import {

View file

@ -165,7 +165,7 @@ export function hydrate() {
</StrictModeIfEnabled>
)
const rootLayoutMissingTags = window.__next_root_layout_missing_tags || null
const rootLayoutMissingTags = window.__next_root_layout_missing_tags
const hasMissingTags = !!rootLayoutMissingTags?.length
const options = { onRecoverableError } satisfies ReactDOMClient.RootOptions
@ -190,8 +190,8 @@ export function hydrate() {
require('./components/react-dev-overlay/app/ReactDevOverlay')
.default as typeof import('./components/react-dev-overlay/app/ReactDevOverlay').default
const INITIAL_OVERLAY_STATE: typeof import('./components/react-dev-overlay/app/error-overlay-reducer').INITIAL_OVERLAY_STATE =
require('./components/react-dev-overlay/app/error-overlay-reducer').INITIAL_OVERLAY_STATE
const INITIAL_OVERLAY_STATE: typeof import('./components/react-dev-overlay/shared').INITIAL_OVERLAY_STATE =
require('./components/react-dev-overlay/shared').INITIAL_OVERLAY_STATE
const getSocketUrl: typeof import('./components/react-dev-overlay/internal/helpers/get-socket-url').getSocketUrl =
require('./components/react-dev-overlay/internal/helpers/get-socket-url')
@ -207,10 +207,7 @@ export function hydrate() {
const errorTree = (
<FallbackLayout>
<ReactDevOverlay
state={{
...INITIAL_OVERLAY_STATE,
rootLayoutMissingTags,
}}
state={{ ...INITIAL_OVERLAY_STATE, rootLayoutMissingTags }}
onReactError={() => {}}
>
{reactEl}

View file

@ -1,9 +1,5 @@
import * as React from 'react'
import { ACTION_UNHANDLED_ERROR } from './error-overlay-reducer'
import type {
OverlayState,
UnhandledErrorAction,
} from './error-overlay-reducer'
import { ACTION_UNHANDLED_ERROR, type OverlayState } from '../shared'
import { ShadowPortal } from '../internal/components/ShadowPortal'
import { BuildError } from '../internal/container/BuildError'
@ -18,7 +14,7 @@ import { RootLayoutMissingTagsError } from '../internal/container/root-layout-mi
interface ReactDevOverlayState {
reactError: SupportedErrorEvent | null
}
class ReactDevOverlay extends React.PureComponent<
export default class ReactDevOverlay extends React.PureComponent<
{
state: OverlayState
children: React.ReactNode
@ -29,17 +25,17 @@ class ReactDevOverlay extends React.PureComponent<
state = { reactError: null }
static getDerivedStateFromError(error: Error): ReactDevOverlayState {
const e = error
const event: UnhandledErrorAction = {
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(e.stack!),
if (!error.stack) return { reactError: null }
return {
reactError: {
id: 0,
event: {
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack),
},
},
}
const errorEvent: SupportedErrorEvent = {
id: 0,
event,
}
return { reactError: errorEvent }
}
componentDidCatch(componentErr: Error) {
@ -52,7 +48,7 @@ class ReactDevOverlay extends React.PureComponent<
const hasBuildError = state.buildError != null
const hasRuntimeErrors = Boolean(state.errors.length)
const hasMissingTags = Boolean(state.rootLayoutMissingTags)
const hasMissingTags = Boolean(state.rootLayoutMissingTags?.length)
const isMounted =
hasBuildError || hasRuntimeErrors || reactError || hasMissingTags
@ -71,7 +67,7 @@ class ReactDevOverlay extends React.PureComponent<
<CssReset />
<Base />
<ComponentStyles />
{state.rootLayoutMissingTags ? (
{state.rootLayoutMissingTags?.length ? (
<RootLayoutMissingTagsError
missingTags={state.rootLayoutMissingTags}
/>
@ -101,5 +97,3 @@ class ReactDevOverlay extends React.PureComponent<
)
}
}
export default ReactDevOverlay

View file

@ -1,165 +0,0 @@
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
import type { SupportedErrorEvent } from '../internal/container/Errors'
import type { ComponentStackFrame } from '../internal/helpers/parse-component-stack'
export const ACTION_BUILD_OK = 'build-ok'
export const ACTION_BUILD_ERROR = 'build-error'
export const ACTION_BEFORE_REFRESH = 'before-fast-refresh'
export const ACTION_REFRESH = 'fast-refresh'
export const ACTION_UNHANDLED_ERROR = 'unhandled-error'
export const ACTION_UNHANDLED_REJECTION = 'unhandled-rejection'
export const ACTION_VERSION_INFO = 'version-info'
export const INITIAL_OVERLAY_STATE: OverlayState = {
nextId: 1,
buildError: null,
errors: [],
notFound: false,
refreshState: { type: 'idle' },
versionInfo: { installed: '0.0.0', staleness: 'unknown' },
rootLayoutMissingTags: null,
}
interface BuildOkAction {
type: typeof ACTION_BUILD_OK
}
interface BuildErrorAction {
type: typeof ACTION_BUILD_ERROR
message: string
}
interface BeforeFastRefreshAction {
type: typeof ACTION_BEFORE_REFRESH
}
interface FastRefreshAction {
type: typeof ACTION_REFRESH
}
export interface UnhandledErrorAction {
type: typeof ACTION_UNHANDLED_ERROR
reason: Error
frames: StackFrame[]
componentStackFrames?: ComponentStackFrame[]
warning?: [string, string, string]
}
export interface UnhandledRejectionAction {
type: typeof ACTION_UNHANDLED_REJECTION
reason: Error
frames: StackFrame[]
}
interface VersionInfoAction {
type: typeof ACTION_VERSION_INFO
versionInfo: VersionInfo
}
export type FastRefreshState =
| {
type: 'idle'
}
| {
type: 'pending'
errors: SupportedErrorEvent[]
}
export interface OverlayState {
nextId: number
buildError: string | null
errors: SupportedErrorEvent[]
rootLayoutMissingTags: string[] | null
refreshState: FastRefreshState
versionInfo: VersionInfo
notFound: boolean
}
function pushErrorFilterDuplicates(
errors: SupportedErrorEvent[],
err: SupportedErrorEvent
): SupportedErrorEvent[] {
return [
...errors.filter((e) => {
// Filter out duplicate errors
return e.event.reason !== err.event.reason
}),
err,
]
}
export const errorOverlayReducer: React.Reducer<
Readonly<OverlayState>,
Readonly<
| BuildOkAction
| BuildErrorAction
| BeforeFastRefreshAction
| FastRefreshAction
| UnhandledErrorAction
| UnhandledRejectionAction
| VersionInfoAction
>
> = (state, action) => {
switch (action.type) {
case ACTION_BUILD_OK: {
return { ...state, buildError: null }
}
case ACTION_BUILD_ERROR: {
return { ...state, buildError: action.message }
}
case ACTION_BEFORE_REFRESH: {
return { ...state, refreshState: { type: 'pending', errors: [] } }
}
case ACTION_REFRESH: {
return {
...state,
buildError: null,
errors:
// Errors can come in during updates. In this case, UNHANDLED_ERROR
// and UNHANDLED_REJECTION events might be dispatched between the
// BEFORE_REFRESH and the REFRESH event. We want to keep those errors
// around until the next refresh. Otherwise we run into a race
// condition where those errors would be cleared on refresh completion
// before they can be displayed.
state.refreshState.type === 'pending'
? state.refreshState.errors
: [],
refreshState: { type: 'idle' },
}
}
case ACTION_UNHANDLED_ERROR:
case ACTION_UNHANDLED_REJECTION: {
switch (state.refreshState.type) {
case 'idle': {
return {
...state,
nextId: state.nextId + 1,
errors: pushErrorFilterDuplicates(state.errors, {
id: state.nextId,
event: action,
}),
}
}
case 'pending': {
return {
...state,
nextId: state.nextId + 1,
refreshState: {
...state.refreshState,
errors: pushErrorFilterDuplicates(state.refreshState.errors, {
id: state.nextId,
event: action,
}),
},
}
}
default:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = state.refreshState
return state
}
}
case ACTION_VERSION_INFO: {
return { ...state, versionInfo: action.versionInfo }
}
default: {
return state
}
}
}

View file

@ -1,27 +1,18 @@
import type { ReactNode } from 'react'
import React, {
useCallback,
useEffect,
useReducer,
useMemo,
startTransition,
} from 'react'
import { useCallback, useEffect, startTransition, useMemo } from 'react'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import formatWebpackMessages from '../../../dev/error-overlay/format-webpack-messages'
import formatWebpackMessages from '../internal/helpers/format-webpack-messages'
import { useRouter } from '../../navigation'
import {
ACTION_VERSION_INFO,
INITIAL_OVERLAY_STATE,
errorOverlayReducer,
} from './error-overlay-reducer'
import {
ACTION_BUILD_OK,
ACTION_BUILD_ERROR,
ACTION_BEFORE_REFRESH,
ACTION_BUILD_ERROR,
ACTION_BUILD_OK,
ACTION_REFRESH,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
} from './error-overlay-reducer'
ACTION_VERSION_INFO,
useErrorOverlayReducer,
} from '../shared'
import { parseStack } from '../internal/helpers/parseStack'
import ReactDevOverlay from './ReactDevOverlay'
import { useErrorHandler } from '../internal/helpers/use-error-handler'
@ -40,9 +31,8 @@ import type {
TurbopackMsgToBrowser,
} from '../../../../server/dev/hot-reloader-types'
import { extractModulesFromTurbopackMessage } from '../../../../server/dev/extract-modules-from-turbopack-message'
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from '../../../dev/error-overlay/messages'
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from '../shared'
import type { HydrationErrorState } from '../internal/helpers/hydration-error-info'
interface Dispatcher {
onBuildOk(): void
onBuildError(message: string): void
@ -244,6 +234,7 @@ function tryApplyUpdates(
)
}
/** Handles messages from the sevrer for the App Router. */
function processMessage(
obj: HMR_ACTION_TYPES,
sendMessage: (message: string) => void,
@ -314,9 +305,8 @@ function processMessage(
const { errors, warnings } = obj
// Is undefined when it's a 'built' event
if ('versionInfo' in obj) {
dispatcher.onVersionInfo(obj.versionInfo)
}
if ('versionInfo' in obj) dispatcher.onVersionInfo(obj.versionInfo)
const hasErrors = Boolean(errors && errors.length)
// Compilation with errors (e.g. syntax error or missing modules).
if (hasErrors) {
@ -464,10 +454,8 @@ export default function HotReload({
assetPrefix: string
children?: ReactNode
}) {
const [state, dispatch] = useReducer(
errorOverlayReducer,
INITIAL_OVERLAY_STATE
)
const [state, dispatch] = useErrorOverlayReducer()
const dispatcher = useMemo<Dispatcher>(() => {
return {
onBuildOk() {
@ -486,32 +474,38 @@ export default function HotReload({
dispatch({ type: ACTION_VERSION_INFO, versionInfo })
},
}
}, [])
}, [dispatch])
const handleOnUnhandledError = useCallback((error: Error): void => {
const errorDetails = (error as any).details as
| HydrationErrorState
| undefined
// Component stack is added to the error in use-error-handler in case there was a hydration errror
const componentStack = errorDetails?.componentStack
const warning = errorDetails?.warning
dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack!),
componentStackFrames: componentStack
? parseComponentStack(componentStack)
: undefined,
warning,
})
}, [])
const handleOnUnhandledRejection = useCallback((reason: Error): void => {
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(reason.stack!),
})
}, [])
const handleOnUnhandledError = useCallback(
(error: Error): void => {
const errorDetails = (error as any).details as
| HydrationErrorState
| undefined
// Component stack is added to the error in use-error-handler in case there was a hydration errror
const componentStack = errorDetails?.componentStack
const warning = errorDetails?.warning
dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack!),
componentStackFrames: componentStack
? parseComponentStack(componentStack)
: undefined,
warning,
})
},
[dispatch]
)
const handleOnUnhandledRejection = useCallback(
(reason: Error): void => {
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(reason.stack!),
})
},
[dispatch]
)
const handleOnReactError = useCallback(() => {
RuntimeErrorHandler.hadRuntimeError = true
}, [])

View file

@ -2,11 +2,9 @@ import { useState, useEffect, useMemo, useCallback } from 'react'
import {
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
} from '../../app/error-overlay-reducer'
import type {
UnhandledErrorAction,
UnhandledRejectionAction,
} from '../../app/error-overlay-reducer'
type UnhandledErrorAction,
type UnhandledRejectionAction,
} from '../../shared'
import {
Dialog,
DialogBody,

View file

@ -5,7 +5,7 @@ import { Overlay } from '../components/Overlay'
import { VersionStalenessInfo } from '../components/VersionStalenessInfo'
import { HotlinkedText } from '../components/hot-linked-text'
export type RootLayoutMissingTagsErrorProps = {
type RootLayoutMissingTagsErrorProps = {
missingTags: string[]
versionInfo?: VersionInfo
}

View file

@ -1,7 +1,7 @@
import {
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
} from '../../app/error-overlay-reducer'
} from '../../shared'
import type { SupportedErrorEvent } from '../container/Errors'
import { getOriginalStackFrames } from './stack-frame'
import type { OriginalStackFrame } from './stack-frame'

View file

@ -32,9 +32,7 @@ function getOriginalStackFrame(
`${
process.env.__NEXT_ROUTER_BASEPATH || ''
}/__nextjs_original-stack-frame?${params.toString()}`,
{
signal: controller.signal,
}
{ signal: controller.signal }
)
.finally(() => {
clearTimeout(tm)

View file

@ -1,4 +1,4 @@
import React from 'react'
import * as React from 'react'
type ErrorBoundaryProps = {
children?: React.ReactNode
@ -8,7 +8,7 @@ type ErrorBoundaryProps = {
}
type ErrorBoundaryState = { error: Error | null }
class ErrorBoundary extends React.PureComponent<
export class ErrorBoundary extends React.PureComponent<
ErrorBoundaryProps,
ErrorBoundaryState
> {
@ -47,5 +47,3 @@ class ErrorBoundary extends React.PureComponent<
)
}
}
export { ErrorBoundary }

View file

@ -3,117 +3,12 @@ import * as React from 'react'
import * as Bus from './bus'
import { ShadowPortal } from '../internal/components/ShadowPortal'
import { BuildError } from '../internal/container/BuildError'
import type { SupportedErrorEvent } from '../internal/container/Errors'
import { Errors } from '../internal/container/Errors'
import { ErrorBoundary } from './ErrorBoundary'
import { Base } from '../internal/styles/Base'
import { ComponentStyles } from '../internal/styles/ComponentStyles'
import { CssReset } from '../internal/styles/CssReset'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
type RefreshState =
| {
// No refresh in progress.
type: 'idle'
}
| {
// The refresh process has been triggered, but the new code has not been
// executed yet.
type: 'pending'
errors: SupportedErrorEvent[]
}
type OverlayState = {
nextId: number
buildError: string | null
errors: SupportedErrorEvent[]
refreshState: RefreshState
versionInfo: VersionInfo
}
function pushErrorFilterDuplicates(
errors: SupportedErrorEvent[],
err: SupportedErrorEvent
): SupportedErrorEvent[] {
return [
...errors.filter((e) => {
// Filter out duplicate errors
return e.event.reason !== err.event.reason
}),
err,
]
}
function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
switch (ev.type) {
case Bus.TYPE_BUILD_OK: {
return { ...state, buildError: null }
}
case Bus.TYPE_BUILD_ERROR: {
return { ...state, buildError: ev.message }
}
case Bus.TYPE_BEFORE_REFRESH: {
return { ...state, refreshState: { type: 'pending', errors: [] } }
}
case Bus.TYPE_REFRESH: {
return {
...state,
buildError: null,
errors:
// Errors can come in during updates. In this case, UNHANDLED_ERROR
// and UNHANDLED_REJECTION events might be dispatched between the
// BEFORE_REFRESH and the REFRESH event. We want to keep those errors
// around until the next refresh. Otherwise we run into a race
// condition where those errors would be cleared on refresh completion
// before they can be displayed.
state.refreshState.type === 'pending'
? state.refreshState.errors
: [],
refreshState: { type: 'idle' },
}
}
case Bus.TYPE_UNHANDLED_ERROR:
case Bus.TYPE_UNHANDLED_REJECTION: {
switch (state.refreshState.type) {
case 'idle': {
return {
...state,
nextId: state.nextId + 1,
errors: pushErrorFilterDuplicates(state.errors, {
id: state.nextId,
event: ev,
}),
}
}
case 'pending': {
return {
...state,
nextId: state.nextId + 1,
refreshState: {
...state.refreshState,
errors: pushErrorFilterDuplicates(state.refreshState.errors, {
id: state.nextId,
event: ev,
}),
},
}
}
default:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = state.refreshState
return state
}
}
case Bus.TYPE_VERSION_INFO: {
return { ...state, versionInfo: ev.versionInfo }
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = ev
return state
}
}
}
import { useErrorOverlayReducer } from '../shared'
type ErrorType = 'runtime' | 'build'
@ -127,83 +22,74 @@ const shouldPreventDisplay = (
return preventType.includes(errorType)
}
type ReactDevOverlayProps = {
interface ReactDevOverlayProps {
children?: React.ReactNode
preventDisplay?: ErrorType[]
globalOverlay?: boolean
}
const ReactDevOverlay: React.FunctionComponent<ReactDevOverlayProps> =
function ReactDevOverlay({ children, preventDisplay, globalOverlay }) {
const [state, dispatch] = React.useReducer<
React.Reducer<OverlayState, Bus.BusEvent>
>(reducer, {
nextId: 1,
buildError: null,
errors: [],
refreshState: {
type: 'idle',
},
versionInfo: { installed: '0.0.0', staleness: 'unknown' },
})
export default function ReactDevOverlay({
children,
preventDisplay,
globalOverlay,
}: ReactDevOverlayProps) {
const [state, dispatch] = useErrorOverlayReducer()
React.useEffect(() => {
Bus.on(dispatch)
return function () {
Bus.off(dispatch)
}
}, [dispatch])
React.useEffect(() => {
Bus.on(dispatch)
return function () {
Bus.off(dispatch)
}
}, [dispatch])
const onComponentError = React.useCallback(
(_error: Error, _componentStack: string | null) => {
// TODO: special handling
},
[]
)
const onComponentError = React.useCallback(
(_error: Error, _componentStack: string | null) => {
// TODO: special handling
},
[]
)
const hasBuildError = state.buildError != null
const hasRuntimeErrors = Boolean(state.errors.length)
const errorType = hasBuildError
? 'build'
: hasRuntimeErrors
? 'runtime'
: null
const isMounted = errorType !== null
const hasBuildError = state.buildError != null
const hasRuntimeErrors = Boolean(state.errors.length)
const errorType = hasBuildError
? 'build'
: hasRuntimeErrors
? 'runtime'
: null
const isMounted = errorType !== null
const displayPrevented = shouldPreventDisplay(errorType, preventDisplay)
const displayPrevented = shouldPreventDisplay(errorType, preventDisplay)
return (
<React.Fragment>
<ErrorBoundary
globalOverlay={globalOverlay}
isMounted={isMounted}
onError={onComponentError}
>
{children ?? null}
</ErrorBoundary>
{isMounted ? (
<ShadowPortal>
<CssReset />
<Base />
<ComponentStyles />
return (
<>
<ErrorBoundary
globalOverlay={globalOverlay}
isMounted={isMounted}
onError={onComponentError}
>
{children ?? null}
</ErrorBoundary>
{isMounted ? (
<ShadowPortal>
<CssReset />
<Base />
<ComponentStyles />
{displayPrevented ? null : hasBuildError ? (
<BuildError
message={state.buildError!}
versionInfo={state.versionInfo}
/>
) : hasRuntimeErrors ? (
<Errors
isAppDir={false}
errors={state.errors}
initialDisplayState={'fullscreen'}
versionInfo={state.versionInfo}
/>
) : undefined}
</ShadowPortal>
) : undefined}
</React.Fragment>
)
}
export default ReactDevOverlay
{displayPrevented ? null : hasBuildError ? (
<BuildError
message={state.buildError!}
versionInfo={state.versionInfo}
/>
) : hasRuntimeErrors ? (
<Errors
isAppDir={false}
errors={state.errors}
versionInfo={state.versionInfo}
initialDisplayState={'fullscreen'}
/>
) : undefined}
</ShadowPortal>
) : undefined}
</>
)
}

View file

@ -1,47 +1,4 @@
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import type { ComponentStackFrame } from '../internal/helpers/parse-component-stack'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
export const TYPE_BUILD_OK = 'build-ok'
export const TYPE_BUILD_ERROR = 'build-error'
export const TYPE_REFRESH = 'fast-refresh'
export const TYPE_BEFORE_REFRESH = 'before-fast-refresh'
export const TYPE_UNHANDLED_ERROR = 'unhandled-error'
export const TYPE_UNHANDLED_REJECTION = 'unhandled-rejection'
export const TYPE_VERSION_INFO = 'version-info'
export type BuildOk = { type: typeof TYPE_BUILD_OK }
export type BuildError = {
type: typeof TYPE_BUILD_ERROR
message: string
}
export type BeforeFastRefresh = { type: typeof TYPE_BEFORE_REFRESH }
export type FastRefresh = { type: typeof TYPE_REFRESH }
export type UnhandledError = {
type: typeof TYPE_UNHANDLED_ERROR
reason: Error
frames: StackFrame[]
componentStackFrames?: ComponentStackFrame[]
}
export type UnhandledRejection = {
type: typeof TYPE_UNHANDLED_REJECTION
reason: Error
frames: StackFrame[]
}
export type VersionInfoEvent = {
type: typeof TYPE_VERSION_INFO
versionInfo: VersionInfo
}
export type BusEvent =
| BuildOk
| BuildError
| FastRefresh
| BeforeFastRefresh
| UnhandledError
| UnhandledRejection
| VersionInfoEvent
import type { BusEvent } from '../shared'
export type BusEventHandler = (ev: BusEvent) => void

View file

@ -5,6 +5,15 @@ import {
hydrationErrorState,
patchConsoleError,
} from '../internal/helpers/hydration-error-info'
import {
ACTION_BEFORE_REFRESH,
ACTION_BUILD_ERROR,
ACTION_BUILD_OK,
ACTION_REFRESH,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
ACTION_VERSION_INFO,
} from '../shared'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
// Patch console.error to collect information about hydration errors
@ -46,7 +55,7 @@ function onUnhandledError(ev: ErrorEvent) {
// This is to avoid same error as different type showing up on client to cause flashing.
if (e.name !== 'ModuleBuildError' && e.name !== 'ModuleNotFoundError') {
Bus.emit({
type: Bus.TYPE_UNHANDLED_ERROR,
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(e.stack!),
componentStackFrames,
@ -67,13 +76,13 @@ function onUnhandledRejection(ev: PromiseRejectionEvent) {
const e = reason
Bus.emit({
type: Bus.TYPE_UNHANDLED_REJECTION,
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(e.stack!),
})
}
function register() {
export function register() {
if (isRegistered) {
return
}
@ -89,7 +98,7 @@ function register() {
window.addEventListener('unhandledrejection', onUnhandledRejection)
}
function unregister() {
export function unregister() {
if (!isRegistered) {
return
}
@ -106,35 +115,26 @@ function unregister() {
window.removeEventListener('unhandledrejection', onUnhandledRejection)
}
function onBuildOk() {
Bus.emit({ type: Bus.TYPE_BUILD_OK })
export function onBuildOk() {
Bus.emit({ type: ACTION_BUILD_OK })
}
function onBuildError(message: string) {
Bus.emit({ type: Bus.TYPE_BUILD_ERROR, message })
export function onBuildError(message: string) {
Bus.emit({ type: ACTION_BUILD_ERROR, message })
}
function onRefresh() {
Bus.emit({ type: Bus.TYPE_REFRESH })
export function onRefresh() {
Bus.emit({ type: ACTION_REFRESH })
}
function onBeforeRefresh() {
Bus.emit({ type: Bus.TYPE_BEFORE_REFRESH })
export function onBeforeRefresh() {
Bus.emit({ type: ACTION_BEFORE_REFRESH })
}
function onVersionInfo(versionInfo: VersionInfo) {
Bus.emit({ type: Bus.TYPE_VERSION_INFO, versionInfo })
export function onVersionInfo(versionInfo: VersionInfo) {
Bus.emit({ type: ACTION_VERSION_INFO, versionInfo })
}
export { getErrorByType } from '../internal/helpers/getErrorByType'
export { getServerError } from '../internal/helpers/nodeStackFrames'
export { default as ReactDevOverlay } from './ReactDevOverlay'
export {
onBuildOk,
onBuildError,
register,
unregister,
onBeforeRefresh,
onRefresh,
onVersionInfo,
}

View file

@ -35,17 +35,17 @@ import {
onBeforeRefresh,
onRefresh,
onVersionInfo,
} from '../../components/react-dev-overlay/pages/client'
} from './client'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import { addMessageListener, sendMessage } from './websocket'
import formatWebpackMessages from './format-webpack-messages'
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../server/dev/hot-reloader-types'
import formatWebpackMessages from '../internal/helpers/format-webpack-messages'
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../../server/dev/hot-reloader-types'
import type {
HMR_ACTION_TYPES,
TurbopackMsgToBrowser,
} from '../../../server/dev/hot-reloader-types'
import { extractModulesFromTurbopackMessage } from '../../../server/dev/extract-modules-from-turbopack-message'
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from './messages'
} from '../../../../server/dev/hot-reloader-types'
import { extractModulesFromTurbopackMessage } from '../../../../server/dev/extract-modules-from-turbopack-message'
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from '../shared'
// This alternative WebpackDevServer combines the functionality of:
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js
@ -258,7 +258,7 @@ function handleAvailableHash(hash: string) {
mostRecentCompilationHash = hash
}
// Handle messages from the server.
/** Handles messages from the sevrer for the Pages Router. */
function processMessage(obj: HMR_ACTION_TYPES) {
if (!('action' in obj)) {
return
@ -273,9 +273,7 @@ function processMessage(obj: HMR_ACTION_TYPES) {
}
case HMR_ACTIONS_SENT_TO_BROWSER.BUILT:
case HMR_ACTIONS_SENT_TO_BROWSER.SYNC: {
if (obj.hash) {
handleAvailableHash(obj.hash)
}
if (obj.hash) handleAvailableHash(obj.hash)
const { errors, warnings } = obj

View file

@ -1,4 +1,4 @@
import type { HMR_ACTION_TYPES } from '../../../server/dev/hot-reloader-types'
import type { HMR_ACTION_TYPES } from '../../../../server/dev/hot-reloader-types'
let source: WebSocket

View file

@ -0,0 +1,168 @@
import { useReducer } from 'react'
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import type { VersionInfo } from '../../../server/dev/parse-version-info'
import type { SupportedErrorEvent } from './internal/container/Errors'
import type { ComponentStackFrame } from './internal/helpers/parse-component-stack'
type FastRefreshState =
/** No refresh in progress. */
| { type: 'idle' }
/** The refresh process has been triggered, but the new code has not been executed yet. */
| { type: 'pending'; errors: SupportedErrorEvent[] }
export interface OverlayState {
nextId: number
buildError: string | null
errors: SupportedErrorEvent[]
refreshState: FastRefreshState
rootLayoutMissingTags: typeof window.__next_root_layout_missing_tags
versionInfo: VersionInfo
notFound: boolean
}
export const ACTION_BUILD_OK = 'build-ok'
export const ACTION_BUILD_ERROR = 'build-error'
export const ACTION_BEFORE_REFRESH = 'before-fast-refresh'
export const ACTION_REFRESH = 'fast-refresh'
export const ACTION_VERSION_INFO = 'version-info'
export const ACTION_UNHANDLED_ERROR = 'unhandled-error'
export const ACTION_UNHANDLED_REJECTION = 'unhandled-rejection'
interface BuildOkAction {
type: typeof ACTION_BUILD_OK
}
interface BuildErrorAction {
type: typeof ACTION_BUILD_ERROR
message: string
}
interface BeforeFastRefreshAction {
type: typeof ACTION_BEFORE_REFRESH
}
interface FastRefreshAction {
type: typeof ACTION_REFRESH
}
export interface UnhandledErrorAction {
type: typeof ACTION_UNHANDLED_ERROR
reason: Error
frames: StackFrame[]
componentStackFrames?: ComponentStackFrame[]
warning?: [string, string, string]
}
export interface UnhandledRejectionAction {
type: typeof ACTION_UNHANDLED_REJECTION
reason: Error
frames: StackFrame[]
}
interface VersionInfoAction {
type: typeof ACTION_VERSION_INFO
versionInfo: VersionInfo
}
export type BusEvent =
| BuildOkAction
| BuildErrorAction
| BeforeFastRefreshAction
| FastRefreshAction
| UnhandledErrorAction
| UnhandledRejectionAction
| VersionInfoAction
function pushErrorFilterDuplicates(
errors: SupportedErrorEvent[],
err: SupportedErrorEvent
): SupportedErrorEvent[] {
return [
...errors.filter((e) => {
// Filter out duplicate errors
return e.event.reason !== err.event.reason
}),
err,
]
}
export const INITIAL_OVERLAY_STATE: OverlayState = {
nextId: 1,
buildError: null,
errors: [],
notFound: false,
refreshState: { type: 'idle' },
rootLayoutMissingTags: [],
versionInfo: { installed: '0.0.0', staleness: 'unknown' },
}
export function useErrorOverlayReducer() {
return useReducer<React.Reducer<OverlayState, BusEvent>>((_state, action) => {
switch (action.type) {
case ACTION_BUILD_OK: {
return { ..._state, buildError: null }
}
case ACTION_BUILD_ERROR: {
return { ..._state, buildError: action.message }
}
case ACTION_BEFORE_REFRESH: {
return { ..._state, refreshState: { type: 'pending', errors: [] } }
}
case ACTION_REFRESH: {
return {
..._state,
buildError: null,
errors:
// Errors can come in during updates. In this case, UNHANDLED_ERROR
// and UNHANDLED_REJECTION events might be dispatched between the
// BEFORE_REFRESH and the REFRESH event. We want to keep those errors
// around until the next refresh. Otherwise we run into a race
// condition where those errors would be cleared on refresh completion
// before they can be displayed.
_state.refreshState.type === 'pending'
? _state.refreshState.errors
: [],
refreshState: { type: 'idle' },
}
}
case ACTION_UNHANDLED_ERROR:
case ACTION_UNHANDLED_REJECTION: {
switch (_state.refreshState.type) {
case 'idle': {
return {
..._state,
nextId: _state.nextId + 1,
errors: pushErrorFilterDuplicates(_state.errors, {
id: _state.nextId,
event: action,
}),
}
}
case 'pending': {
return {
..._state,
nextId: _state.nextId + 1,
refreshState: {
..._state.refreshState,
errors: pushErrorFilterDuplicates(_state.refreshState.errors, {
id: _state.nextId,
event: action,
}),
},
}
}
default:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = _state.refreshState
return _state
}
}
case ACTION_VERSION_INFO: {
return { ..._state, versionInfo: action.versionInfo }
}
default: {
return _state
}
}
}, INITIAL_OVERLAY_STATE)
}
export const REACT_REFRESH_FULL_RELOAD_FROM_ERROR =
'[Fast Refresh] performing full reload because your application had an unrecoverable error'

View file

@ -1,7 +1,10 @@
/* globals __webpack_hash__ */
import { displayContent } from './fouc'
import initOnDemandEntries from './on-demand-entries-client'
import { addMessageListener, connectHMR } from './error-overlay/websocket'
import {
addMessageListener,
connectHMR,
} from '../components/react-dev-overlay/pages/websocket'
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../server/dev/hot-reloader-types'
declare global {

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../server/dev/hot-reloader-types'
import type { HMR_ACTION_TYPES } from '../../server/dev/hot-reloader-types'
import { addMessageListener } from './error-overlay/websocket'
import { addMessageListener } from '../components/react-dev-overlay/pages/websocket'
type VerticalPosition = 'top' | 'bottom'
type HorizonalPosition = 'left' | 'right'

View file

@ -1,2 +0,0 @@
export const REACT_REFRESH_FULL_RELOAD_FROM_ERROR =
'[Fast Refresh] performing full reload because your application had an unrecoverable error'

View file

@ -1,5 +1,5 @@
import connect from './error-overlay/hot-dev-client'
import { sendMessage } from './error-overlay/websocket'
import connect from '../components/react-dev-overlay/pages/hot-reloader-client'
import { sendMessage } from '../components/react-dev-overlay/pages/websocket'
let reloading = false

View file

@ -1,5 +1,5 @@
import Router from '../router'
import { sendMessage } from './error-overlay/websocket'
import { sendMessage } from '../components/react-dev-overlay/pages/websocket'
export default async (page?: string) => {
if (page) {

View file

@ -3,7 +3,10 @@ import initOnDemandEntries from './dev/on-demand-entries-client'
import initializeBuildWatcher from './dev/dev-build-watcher'
import type { ShowHideHandler } from './dev/dev-build-watcher'
import { displayContent } from './dev/fouc'
import { connectHMR, addMessageListener } from './dev/error-overlay/websocket'
import {
connectHMR,
addMessageListener,
} from './components/react-dev-overlay/pages/websocket'
import {
assign,
urlQueryToSearchParams,
@ -29,79 +32,86 @@ export function pageBootrap(assetPrefix: string) {
addMessageListener((payload) => {
if (reloading) return
if ('action' in payload) {
if (payload.action === HMR_ACTIONS_SENT_TO_BROWSER.SERVER_ERROR) {
const { stack, message } = JSON.parse(payload.errorJSON)
const error = new Error(message)
error.stack = stack
throw error
} else if (payload.action === HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE) {
reloading = true
window.location.reload()
} else if (
payload.action ===
HMR_ACTIONS_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE
) {
fetch(
`${assetPrefix}/_next/static/development/_devPagesManifest.json`
)
.then((res) => res.json())
.then((manifest) => {
window.__DEV_PAGES_MANIFEST = manifest
})
.catch((err) => {
console.log(`Failed to fetch devPagesManifest`, err)
})
switch (payload.action) {
case HMR_ACTIONS_SENT_TO_BROWSER.SERVER_ERROR: {
const { stack, message } = JSON.parse(payload.errorJSON)
const error = new Error(message)
error.stack = stack
throw error
}
case HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE: {
reloading = true
window.location.reload()
break
}
case HMR_ACTIONS_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE: {
fetch(
`${assetPrefix}/_next/static/development/_devPagesManifest.json`
)
.then((res) => res.json())
.then((manifest) => {
window.__DEV_PAGES_MANIFEST = manifest
})
.catch((err) => {
console.log(`Failed to fetch devPagesManifest`, err)
})
break
}
default:
break
}
} else if ('event' in payload) {
if (payload.event === HMR_ACTIONS_SENT_TO_BROWSER.MIDDLEWARE_CHANGES) {
return window.location.reload()
} else if (
payload.event === HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES
) {
const isOnErrorPage = window.next.router.pathname === '/_error'
// On the error page we want to reload the page when a page was changed
if (isOnErrorPage) {
switch (payload.event) {
case HMR_ACTIONS_SENT_TO_BROWSER.MIDDLEWARE_CHANGES: {
return window.location.reload()
}
} else if (
payload.event === HMR_ACTIONS_SENT_TO_BROWSER.SERVER_ONLY_CHANGES
) {
const { pages } = payload
// Make sure to reload when the dev-overlay is showing for an
// API route
// TODO: Fix `__NEXT_PAGE` type
if (pages.includes(router.query.__NEXT_PAGE as string)) {
return window.location.reload()
case HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES: {
const isOnErrorPage = window.next.router.pathname === '/_error'
// On the error page we want to reload the page when a page was changed
if (isOnErrorPage) return window.location.reload()
break
}
case HMR_ACTIONS_SENT_TO_BROWSER.SERVER_ONLY_CHANGES: {
const { pages } = payload
if (!router.clc && pages.includes(router.pathname)) {
console.log('Refreshing page data due to server-side change')
// Make sure to reload when the dev-overlay is showing for an
// API route
// TODO: Fix `__NEXT_PAGE` type
if (pages.includes(router.query.__NEXT_PAGE as string)) {
return window.location.reload()
}
buildIndicatorHandler?.show()
if (!router.clc && pages.includes(router.pathname)) {
console.log('Refreshing page data due to server-side change')
const clearIndicator = () => buildIndicatorHandler?.hide()
buildIndicatorHandler?.show()
router
.replace(
router.pathname +
'?' +
String(
assign(
urlQueryToSearchParams(router.query),
new URLSearchParams(location.search)
)
),
router.asPath,
{ scroll: false }
)
.catch(() => {
// trigger hard reload when failing to refresh data
// to show error overlay properly
location.reload()
})
.finally(clearIndicator)
const clearIndicator = () => buildIndicatorHandler?.hide()
router
.replace(
router.pathname +
'?' +
String(
assign(
urlQueryToSearchParams(router.query),
new URLSearchParams(location.search)
)
),
router.asPath,
{ scroll: false }
)
.catch(() => {
// trigger hard reload when failing to refresh data
// to show error overlay properly
location.reload()
})
.finally(clearIndicator)
}
break
}
default:
break
}
}
})

View file

@ -1,4 +1,4 @@
import { sendMessage } from '../dev/error-overlay/websocket'
import { sendMessage } from '../components/react-dev-overlay/pages/websocket'
import type { Span } from './tracer'
export default function reportToSocket(span: Span) {