refactor(error-overlay): unify Pages/App router error overlay source (#62939)
This commit is contained in:
parent
fff9ddc204
commit
1e26cceff4
25 changed files with 417 additions and 583 deletions
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}, [])
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
168
packages/next/src/client/components/react-dev-overlay/shared.ts
Normal file
168
packages/next/src/client/components/react-dev-overlay/shared.ts
Normal 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'
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export const REACT_REFRESH_FULL_RELOAD_FROM_ERROR =
|
||||
'[Fast Refresh] performing full reload because your application had an unrecoverable error'
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue