From 3f47c94e81d92e85ba73b524cbf1fd02caab896b Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 10 Jul 2024 19:48:16 +0200 Subject: [PATCH] extract the helper --- packages/next/src/client/app-index.tsx | 30 +++-- .../internal/helpers/hydration-error-info.ts | 28 ++--- .../internal/helpers/use-error-handler.ts | 111 ++++++++---------- .../react-dev-overlay/pages/client.ts | 6 +- 4 files changed, 85 insertions(+), 90 deletions(-) diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx index 0316701c441d1..dcf2b49f276bf 100644 --- a/packages/next/src/client/app-index.tsx +++ b/packages/next/src/client/app-index.tsx @@ -14,6 +14,28 @@ import { createMutableActionQueue, } from '../shared/lib/router/action-queue' import { HMR_ACTIONS_SENT_TO_BROWSER } from '../server/dev/hot-reloader-types' +import { isNextRouterError } from './components/is-next-router-error' +import { handleClientError } from './components/react-dev-overlay/internal/helpers/use-error-handler' + +// Patch console.error to collect information about hydration errors +const origConsoleError = window.console.error +window.console.error = (...args) => { + // See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78 + const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0] + if (!isNextRouterError(error)) { + if (process.env.NODE_ENV !== 'production') { + const storeHydrationErrorStateFromConsoleArgs = + require('./components/react-dev-overlay/internal/helpers/hydration-error-info') + .storeHydrationErrorStateFromConsoleArgs as typeof import('./components/react-dev-overlay/internal/helpers/hydration-error-info').storeHydrationErrorStateFromConsoleArgs + storeHydrationErrorStateFromConsoleArgs() + + storeHydrationErrorStateFromConsoleArgs(...args) + handleClientError(error) + } + + origConsoleError.apply(window.console, args) + } +} /// @@ -181,14 +203,6 @@ export function hydrate() { const isError = document.documentElement.id === '__next_error__' || hasMissingTags - if (process.env.NODE_ENV !== 'production') { - // Patch console.error to collect information about hydration errors - const patchConsoleError = - require('./components/react-dev-overlay/internal/helpers/hydration-error-info') - .patchConsoleError as typeof import('./components/react-dev-overlay/internal/helpers/hydration-error-info').patchConsoleError - patchConsoleError() - } - if (isError) { if (process.env.NODE_ENV !== 'production') { // if an error is thrown while rendering an RSC stream, this will catch it in dev diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts index 932cda3dfb4e1..5596f7dde95e1 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts @@ -65,22 +65,18 @@ export const getReactHydrationDiffSegments = (msg: NullableText) => { * When the hydration runtime error is thrown, the message and component stack are added to the error. * This results in a more helpful error message in the error overlay. */ -export function patchConsoleError() { - const prev = console.error - console.error = function (msg, serverContent, clientContent, componentStack) { - if (isKnownHydrationWarning(msg)) { - hydrationErrorState.warning = [ - // remove the last %s from the message - msg, - serverContent, - clientContent, - ] - hydrationErrorState.componentStack = componentStack - hydrationErrorState.serverContent = serverContent - hydrationErrorState.clientContent = clientContent - } - // @ts-expect-error argument is defined - prev.apply(console, arguments) +export function storeHydrationErrorStateFromConsoleArgs(...args: any[]) { + const [msg, serverContent, clientContent, componentStack] = args + if (isKnownHydrationWarning(msg)) { + hydrationErrorState.warning = [ + // remove the last %s from the message + msg, + serverContent, + clientContent, + ] + hydrationErrorState.componentStack = componentStack + hydrationErrorState.serverContent = serverContent + hydrationErrorState.clientContent = clientContent } } diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts index 21bd3062edf6c..84bc800417aa8 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts @@ -24,67 +24,63 @@ const rejectionQueue: Array = [] const errorHandlers: Array = [] const rejectionHandlers: Array = [] -if (typeof window !== 'undefined') { - function handleError(error: unknown) { - if ( - !error || - !(error instanceof Error) || - typeof error.stack !== 'string' - ) { - // A non-error was thrown, we don't have anything to show. :-( - return - } +export function handleClientError(error: unknown) { + if (!error || !(error instanceof Error) || typeof error.stack !== 'string') { + // A non-error was thrown, we don't have anything to show. :-( + return + } - const isCausedByHydrationFailure = isHydrationError(error) - if ( - isHydrationError(error) && - !error.message.includes( - 'https://nextjs.org/docs/messages/react-hydration-error' - ) - ) { - const reactHydrationDiffSegments = getReactHydrationDiffSegments( - error.message - ) - let parsedHydrationErrorState: typeof hydrationErrorState = {} - if (reactHydrationDiffSegments) { + const isCausedByHydrationFailure = isHydrationError(error) + if ( + isHydrationError(error) && + !error.message.includes( + 'https://nextjs.org/docs/messages/react-hydration-error' + ) + ) { + const reactHydrationDiffSegments = getReactHydrationDiffSegments( + error.message + ) + let parsedHydrationErrorState: typeof hydrationErrorState = {} + if (reactHydrationDiffSegments) { + parsedHydrationErrorState = { + ...(error as any).details, + ...hydrationErrorState, + warning: hydrationErrorState.warning || [ + getDefaultHydrationErrorMessage(), + ], + notes: reactHydrationDiffSegments[0], + reactOutputComponentDiff: reactHydrationDiffSegments[1], + } + } else { + // If there's any extra information in the error message to display, + // append it to the error message details property + if (hydrationErrorState.warning) { + // The patched console.error found hydration errors logged by React + // Append the logged warning to the error message parsedHydrationErrorState = { ...(error as any).details, + // It contains the warning, component stack, server and client tag names ...hydrationErrorState, - warning: hydrationErrorState.warning || [ - getDefaultHydrationErrorMessage(), - ], - notes: reactHydrationDiffSegments[0], - reactOutputComponentDiff: reactHydrationDiffSegments[1], } - } else { - // If there's any extra information in the error message to display, - // append it to the error message details property - if (hydrationErrorState.warning) { - // The patched console.error found hydration errors logged by React - // Append the logged warning to the error message - parsedHydrationErrorState = { - ...(error as any).details, - // It contains the warning, component stack, server and client tag names - ...hydrationErrorState, - } - } - error.message += - '\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error' } - ;(error as any).details = parsedHydrationErrorState + error.message += + '\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error' } + ;(error as any).details = parsedHydrationErrorState + } - // Only queue one hydration every time - if (isCausedByHydrationFailure) { - if (!hasHydrationError) { - errorQueue.push(error) - } - hasHydrationError = true - } - for (const handler of errorHandlers) { - handler(error) + // Only queue one hydration every time + if (isCausedByHydrationFailure) { + if (!hasHydrationError) { + errorQueue.push(error) } + hasHydrationError = true } + for (const handler of errorHandlers) { + handler(error) + } +} +if (typeof window !== 'undefined') { // These event handlers must be added outside of the hook because there is no // guarantee that the hook will be alive in a mounted component in time to // when the errors occur. @@ -96,19 +92,10 @@ if (typeof window !== 'undefined') { event.preventDefault() return false } - handleError(event.error) + handleClientError(event.error) } ) - // caught errors go through console.error - const origConsoleError = window.console.error - window.console.error = (...args) => { - // See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78 - const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0] - if (!isNextRouterError(error)) { - handleError(error) - origConsoleError.apply(window.console, args) - } - } + window.addEventListener( 'unhandledrejection', (ev: WindowEventMap['unhandledrejection']): void => { diff --git a/packages/next/src/client/components/react-dev-overlay/pages/client.ts b/packages/next/src/client/components/react-dev-overlay/pages/client.ts index 0b167708d1967..ad6293c2bd761 100644 --- a/packages/next/src/client/components/react-dev-overlay/pages/client.ts +++ b/packages/next/src/client/components/react-dev-overlay/pages/client.ts @@ -4,7 +4,7 @@ import { parseComponentStack } from '../internal/helpers/parse-component-stack' import { getReactHydrationDiffSegments, hydrationErrorState, - patchConsoleError, + storeHydrationErrorStateFromConsoleArgs, } from '../internal/helpers/hydration-error-info' import { ACTION_BEFORE_REFRESH, @@ -21,9 +21,6 @@ import { isHydrationError, } from '../../is-hydration-error' -// Patch console.error to collect information about hydration errors -patchConsoleError() - let isRegistered = false let stackTraceLimit: number | undefined = undefined @@ -93,6 +90,7 @@ let origConsoleError = console.error function nextJsHandleConsoleError(...args: any[]) { // See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78 const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0] + storeHydrationErrorStateFromConsoleArgs(...args) handleError(error) origConsoleError.apply(window.console, args) }