diff --git a/packages/react-error-overlay/src/effects/proxyConsole.js b/packages/react-error-overlay/src/effects/proxyConsole.js index a0a771f0e71..d66bb8fb063 100644 --- a/packages/react-error-overlay/src/effects/proxyConsole.js +++ b/packages/react-error-overlay/src/effects/proxyConsole.js @@ -1,5 +1,32 @@ /* @flow */ -type ConsoleProxyCallback = (message: string) => void; + +type ReactFrame = { + fileName: string | null, + lineNumber: number | null, + functionName: string | null, +}; +const ReactFrameStack: Array = []; + +export type { ReactFrame }; + +const registerReactStack = () => { + // $FlowFixMe + console.stack = frames => ReactFrameStack.push(frames); + // $FlowFixMe + console.stackEnd = frames => ReactFrameStack.pop(); +}; + +const unregisterReactStack = () => { + // $FlowFixMe + console.stack = undefined; + // $FlowFixMe + console.stackEnd = undefined; +}; + +type ConsoleProxyCallback = ( + message: string, + frames: ReactFrame[] | void +) => void; const permanentRegister = function proxyConsole( type: string, callback: ConsoleProxyCallback @@ -7,9 +34,9 @@ const permanentRegister = function proxyConsole( const orig = console[type]; console[type] = function __stack_frame_overlay_proxy_console__() { const message = [].slice.call(arguments).join(' '); - callback(message); + callback(message, ReactFrameStack[ReactFrameStack.length - 1]); return orig.apply(this, arguments); }; }; -export { permanentRegister }; +export { permanentRegister, registerReactStack, unregisterReactStack }; diff --git a/packages/react-error-overlay/src/overlay.js b/packages/react-error-overlay/src/overlay.js index 7c3ebb5363c..273c9ecb00f 100644 --- a/packages/react-error-overlay/src/overlay.js +++ b/packages/react-error-overlay/src/overlay.js @@ -21,6 +21,8 @@ import { } from './effects/stackTraceLimit'; import { permanentRegister as permanentRegisterConsole, + registerReactStack, + unregisterReactStack, } from './effects/proxyConsole'; import { massage as massageWarning } from './utils/warnings'; @@ -213,8 +215,9 @@ function inject() { registerShortcuts(window, shortcutHandler); registerStackTraceLimit(); - permanentRegisterConsole('error', warning => { - const data = massageWarning(warning); + registerReactStack(); + permanentRegisterConsole('error', (warning, stack) => { + const data = massageWarning(warning, stack); if (data == null) return; crash( // $FlowFixMe @@ -233,6 +236,7 @@ function uninject() { unregisterShortcuts(window); unregisterPromise(window); unregisterError(window); + unregisterReactStack(); } export { inject, uninject }; diff --git a/packages/react-error-overlay/src/utils/warnings.js b/packages/react-error-overlay/src/utils/warnings.js index 28d66e994b6..8a0a660c648 100644 --- a/packages/react-error-overlay/src/utils/warnings.js +++ b/packages/react-error-overlay/src/utils/warnings.js @@ -1,42 +1,38 @@ // @flow +import type { ReactFrame } from '../effects/proxyConsole'; -// This is a list of React warnings to display -// There must be zero or one capture group; and the capture group is assumed to be a missing stack frame. -const warnings = [ - /^.*React.createElement: type is invalid.+Check your code at (.*?:.*)[.]$/, -]; -// This is a list of information to remove from React warnings, it's not particularly useful to show +// This is a list of information to remove from React warnings, it's not particularly useful to show. const removals = [/Check your code at (.*?:.*)[.]/]; -function massage(warning: string): { message: string, stack: string } | null { - const nIndex = warning.indexOf('\n'); - let message = warning; - if (nIndex !== -1) { - message = message.substring(0, nIndex); - } - let stack = warning.substring(nIndex + 1); - - let found = false; - for (const warning of warnings) { - const m = message.match(warning); - if (!m) { - continue; - } - found = true; - if (!m[1]) { - break; - } - stack = `in (render function) (at ${m[1]})\n${stack}`; - break; - } - if (!found) { +function massage( + warning: string, + frames: ReactFrame[] | void +): { message: string, stack: string } | null { + if (!frames) { return null; } + let message = warning; + const nIndex = message.indexOf('\n'); + if (nIndex !== -1) message = message.substring(0, nIndex); + for (const trim of removals) { message = message.replace(trim, ''); } + let stack = ''; + for (let index = 0; index < frames.length; ++index) { + const { fileName, lineNumber } = frames[index]; + if (fileName == null || lineNumber == null) continue; + let { functionName } = frames[index]; + if (functionName == null && index === 0 && index + 1 < frames.length) { + functionName = frames[index + 1].functionName; + if (functionName !== null) functionName = `(${functionName})`; + } + functionName = functionName || '(unknown function)'; + + stack += `in ${functionName} (at ${fileName}:${lineNumber})\n`; + } return { message, stack }; }