diff --git a/packages/react-dev-utils/webpackHotDevClient.js b/packages/react-dev-utils/webpackHotDevClient.js index 611fc6ba042cfeac0f2c78174b8b46a197001232..e375c3fab8d30a06b32ffedffa07c79969a84be3 100644 --- a/packages/react-dev-utils/webpackHotDevClient.js +++ b/packages/react-dev-utils/webpackHotDevClient.js @@ -23,6 +23,16 @@ var launchEditorEndpoint = require('./launchEditorEndpoint'); var formatWebpackMessages = require('./formatWebpackMessages'); var ErrorOverlay = require('react-error-overlay'); +ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) { + // Keep this sync with errorOverlayMiddleware.js + fetch( + `${launchEditorEndpoint}?fileName=` + + window.encodeURIComponent(errorLocation.fileName) + + '&lineNumber=' + + window.encodeURIComponent(errorLocation.lineNumber || 1) + ); +}); + // We need to keep track of if there has been a runtime error. // Essentially, we cannot guarantee application state was not corrupted by the // runtime error. To prevent confusing behavior, we forcibly reload the entire @@ -31,7 +41,6 @@ var ErrorOverlay = require('react-error-overlay'); // See https://github.com/facebookincubator/create-react-app/issues/3096 var hadRuntimeError = false; ErrorOverlay.startReportingRuntimeErrors({ - launchEditorEndpoint: launchEditorEndpoint, onError: function() { hadRuntimeError = true; }, diff --git a/packages/react-error-overlay/src/containers/CompileErrorContainer.js b/packages/react-error-overlay/src/containers/CompileErrorContainer.js index 5d491a9d47f43e0b1831b19fabd1cec0efe38484..9d1e399fd10c36007bbfe10949b539835f965c9b 100644 --- a/packages/react-error-overlay/src/containers/CompileErrorContainer.js +++ b/packages/react-error-overlay/src/containers/CompileErrorContainer.js @@ -12,18 +12,34 @@ import Footer from '../components/Footer'; import Header from '../components/Header'; import CodeBlock from '../components/CodeBlock'; import generateAnsiHTML from '../utils/generateAnsiHTML'; +import parseCompileError from '../utils/parseCompileError'; +import type { ErrorLocation } from '../utils/parseCompileError'; + +const codeAnchorStyle = { + cursor: 'pointer', +}; type Props = {| error: string, + editorHandler: (errorLoc: ErrorLocation) => void, |}; class CompileErrorContainer extends PureComponent<Props, void> { render() { - const { error } = this.props; + const { error, editorHandler } = this.props; + const errLoc: ?ErrorLocation = parseCompileError(error); + const canOpenInEditor = errLoc !== null && editorHandler !== null; return ( <ErrorOverlay> <Header headerText="Failed to compile" /> - <CodeBlock main={true} codeHTML={generateAnsiHTML(error)} /> + <a + onClick={ + canOpenInEditor && errLoc ? () => editorHandler(errLoc) : null + } + style={canOpenInEditor ? codeAnchorStyle : null} + > + <CodeBlock main={true} codeHTML={generateAnsiHTML(error)} /> + </a> <Footer line1="This error occurred during the build time and cannot be dismissed." /> </ErrorOverlay> ); diff --git a/packages/react-error-overlay/src/containers/RuntimeError.js b/packages/react-error-overlay/src/containers/RuntimeError.js index c247ba52a30ac6186f212edc4f0d69ef70423ef1..1db2aba79b9a015f85cff35f8c6f4783d039d377 100644 --- a/packages/react-error-overlay/src/containers/RuntimeError.js +++ b/packages/react-error-overlay/src/containers/RuntimeError.js @@ -11,6 +11,7 @@ import Header from '../components/Header'; import StackTrace from './StackTrace'; import type { StackFrame } from '../utils/stack-frame'; +import type { ErrorLocation } from '../utils/parseCompileError'; const wrapperStyle = { display: 'flex', @@ -26,10 +27,10 @@ export type ErrorRecord = {| type Props = {| errorRecord: ErrorRecord, - launchEditorEndpoint: ?string, + editorHandler: (errorLoc: ErrorLocation) => void, |}; -function RuntimeError({ errorRecord, launchEditorEndpoint }: Props) { +function RuntimeError({ errorRecord, editorHandler }: Props) { const { error, unhandledRejection, contextSize, stackFrames } = errorRecord; const errorName = unhandledRejection ? 'Unhandled Rejection (' + error.name + ')' @@ -58,7 +59,7 @@ function RuntimeError({ errorRecord, launchEditorEndpoint }: Props) { stackFrames={stackFrames} errorName={errorName} contextSize={contextSize} - launchEditorEndpoint={launchEditorEndpoint} + editorHandler={editorHandler} /> </div> ); diff --git a/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js b/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js index 80585f0514226046c0bfc7db2eec4c45a7f448fc..91d4a4f54561729b7ece7bcc954fd46bcdb909a6 100644 --- a/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js +++ b/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js @@ -14,11 +14,12 @@ import RuntimeError from './RuntimeError'; import Footer from '../components/Footer'; import type { ErrorRecord } from './RuntimeError'; +import type { ErrorLocation } from '../utils/parseCompileError'; type Props = {| errorRecords: ErrorRecord[], close: () => void, - launchEditorEndpoint: ?string, + editorHandler: (errorLoc: ErrorLocation) => void, |}; type State = {| @@ -74,7 +75,7 @@ class RuntimeErrorContainer extends PureComponent<Props, State> { )} <RuntimeError errorRecord={errorRecords[this.state.currentIndex]} - launchEditorEndpoint={this.props.launchEditorEndpoint} + editorHandler={this.props.editorHandler} /> <Footer line1="This screen is visible only in development. It will not appear if the app crashes in production." diff --git a/packages/react-error-overlay/src/containers/StackFrame.js b/packages/react-error-overlay/src/containers/StackFrame.js index f16ae6435933f02e8c2e2a139c022c864ef3e971..9ae9104343ef4ae8bbb1e1f32b7d6f33da099325 100644 --- a/packages/react-error-overlay/src/containers/StackFrame.js +++ b/packages/react-error-overlay/src/containers/StackFrame.js @@ -12,6 +12,7 @@ import { getPrettyURL } from '../utils/getPrettyURL'; import { darkGray } from '../styles'; import type { StackFrame as StackFrameType } from '../utils/stack-frame'; +import type { ErrorLocation } from '../utils/parseCompileError'; const linkStyle = { fontSize: '0.9em', @@ -45,10 +46,10 @@ const toggleStyle = { type Props = {| frame: StackFrameType, - launchEditorEndpoint: ?string, contextSize: number, critical: boolean, showCode: boolean, + editorHandler: (errorLoc: ErrorLocation) => void, |}; type State = {| @@ -66,47 +67,35 @@ class StackFrame extends Component<Props, State> { })); }; - getEndpointUrl(): string | null { - if (!this.props.launchEditorEndpoint) { - return null; - } - const { _originalFileName: sourceFileName } = this.props.frame; + getErrorLocation(): ErrorLocation | null { + const { + _originalFileName: fileName, + _originalLineNumber: lineNumber, + } = this.props.frame; // Unknown file - if (!sourceFileName) { + if (!fileName) { return null; } // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1" - const isInternalWebpackBootstrapCode = - sourceFileName.trim().indexOf(' ') !== -1; + const isInternalWebpackBootstrapCode = fileName.trim().indexOf(' ') !== -1; if (isInternalWebpackBootstrapCode) { return null; } // Code is in a real file - return this.props.launchEditorEndpoint || null; + return { fileName, lineNumber: lineNumber || 1 }; } - openInEditor = () => { - const endpointUrl = this.getEndpointUrl(); - if (endpointUrl === null) { + editorHandler = () => { + const errorLoc = this.getErrorLocation(); + if (!errorLoc) { return; } - - const { - _originalFileName: sourceFileName, - _originalLineNumber: sourceLineNumber, - } = this.props.frame; - // Keep this in sync with react-error-overlay/middleware.js - fetch( - `${endpointUrl}?fileName=` + - window.encodeURIComponent(sourceFileName) + - '&lineNumber=' + - window.encodeURIComponent(sourceLineNumber || 1) - ).then(() => {}, () => {}); + this.props.editorHandler(errorLoc); }; onKeyDown = (e: SyntheticKeyboardEvent<>) => { if (e.key === 'Enter') { - this.openInEditor(); + this.editorHandler(); } }; @@ -166,14 +155,15 @@ class StackFrame extends Component<Props, State> { } } - const canOpenInEditor = this.getEndpointUrl() !== null; + const canOpenInEditor = + this.getErrorLocation() !== null && this.props.editorHandler !== null; return ( <div> <div>{functionName}</div> <div style={linkStyle}> <a style={canOpenInEditor ? anchorStyle : null} - onClick={canOpenInEditor ? this.openInEditor : null} + onClick={canOpenInEditor ? this.editorHandler : null} onKeyDown={canOpenInEditor ? this.onKeyDown : null} tabIndex={canOpenInEditor ? '0' : null} > @@ -183,7 +173,7 @@ class StackFrame extends Component<Props, State> { {codeBlockProps && ( <span> <a - onClick={canOpenInEditor ? this.openInEditor : null} + onClick={canOpenInEditor ? this.editorHandler : null} style={canOpenInEditor ? codeAnchorStyle : null} > <CodeBlock {...codeBlockProps} /> diff --git a/packages/react-error-overlay/src/containers/StackTrace.js b/packages/react-error-overlay/src/containers/StackTrace.js index 0be342ddcd32db878759ceb28eb8f195a30cc80b..abec286a30c3bf8318b6a6f9bdc60269b0257d94 100644 --- a/packages/react-error-overlay/src/containers/StackTrace.js +++ b/packages/react-error-overlay/src/containers/StackTrace.js @@ -13,6 +13,7 @@ import { isInternalFile } from '../utils/isInternalFile'; import { isBultinErrorName } from '../utils/isBultinErrorName'; import type { StackFrame as StackFrameType } from '../utils/stack-frame'; +import type { ErrorLocation } from '../utils/parseCompileError'; const traceStyle = { fontSize: '1em', @@ -25,17 +26,12 @@ type Props = {| stackFrames: StackFrameType[], errorName: string, contextSize: number, - launchEditorEndpoint: ?string, + editorHandler: (errorLoc: ErrorLocation) => void, |}; class StackTrace extends Component<Props> { renderFrames() { - const { - stackFrames, - errorName, - contextSize, - launchEditorEndpoint, - } = this.props; + const { stackFrames, errorName, contextSize, editorHandler } = this.props; const renderedFrames = []; let hasReachedAppCode = false, currentBundle = [], @@ -59,7 +55,7 @@ class StackTrace extends Component<Props> { contextSize={contextSize} critical={index === 0} showCode={!shouldCollapse} - launchEditorEndpoint={launchEditorEndpoint} + editorHandler={editorHandler} /> ); const lastElement = index === stackFrames.length - 1; diff --git a/packages/react-error-overlay/src/iframeScript.js b/packages/react-error-overlay/src/iframeScript.js index c95ea36b1a4e2c38ec9efa300631264b35a4c838..3da820264b4b43c922601db07fbc08f6a6e95ed7 100644 --- a/packages/react-error-overlay/src/iframeScript.js +++ b/packages/react-error-overlay/src/iframeScript.js @@ -19,17 +19,22 @@ function render({ currentBuildError, currentRuntimeErrorRecords, dismissRuntimeErrors, - launchEditorEndpoint, + editorHandler, }) { if (currentBuildError) { - return <CompileErrorContainer error={currentBuildError} />; + return ( + <CompileErrorContainer + error={currentBuildError} + editorHandler={editorHandler} + /> + ); } if (currentRuntimeErrorRecords.length > 0) { return ( <RuntimeErrorContainer errorRecords={currentRuntimeErrorRecords} close={dismissRuntimeErrors} - launchEditorEndpoint={launchEditorEndpoint} + editorHandler={editorHandler} /> ); } diff --git a/packages/react-error-overlay/src/index.js b/packages/react-error-overlay/src/index.js index 52ff9199bcba6b131b1db7a2c74e8539f927eba1..a800595cffbc9b03c181ea7888c248c588f0caae 100644 --- a/packages/react-error-overlay/src/index.js +++ b/packages/react-error-overlay/src/index.js @@ -16,22 +16,32 @@ import { applyStyles } from './utils/dom/css'; import iframeScript from 'iframeScript'; import type { ErrorRecord } from './listenToRuntimeErrors'; +import type { ErrorLocation } from './utils/parseCompileError'; type RuntimeReportingOptions = {| onError: () => void, - launchEditorEndpoint: string, filename?: string, |}; +type EditorHandler = (errorLoc: ErrorLocation) => void; + let iframe: null | HTMLIFrameElement = null; let isLoadingIframe: boolean = false; var isIframeReady: boolean = false; +let editorHandler: null | EditorHandler = null; let currentBuildError: null | string = null; let currentRuntimeErrorRecords: Array<ErrorRecord> = []; let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null; let stopListeningToRuntimeErrors: null | (() => void) = null; +export function setEditorHandler(handler: EditorHandler | null) { + editorHandler = handler; + if (iframe) { + update(); + } +} + export function reportBuildError(error: string) { currentBuildError = error; update(); @@ -46,6 +56,13 @@ export function startReportingRuntimeErrors(options: RuntimeReportingOptions) { if (stopListeningToRuntimeErrors !== null) { throw new Error('Already listening'); } + if (options.launchEditorEndpoint) { + console.warn( + 'Warning: `startReportingRuntimeErrors` doesn’t accept ' + + '`launchEditorEndpoint` argument anymore. Use `listenToOpenInEditor` ' + + 'instead with your own implementation to open errors in editor ' + ); + } currentRuntimeErrorOptions = options; listenToRuntimeErrors(errorRecord => { try { @@ -133,7 +150,7 @@ function updateIframeContent() { currentBuildError, currentRuntimeErrorRecords, dismissRuntimeErrors, - launchEditorEndpoint: currentRuntimeErrorOptions.launchEditorEndpoint, + editorHandler, }); if (!isRendered) { diff --git a/packages/react-error-overlay/src/utils/parseCompileError.js b/packages/react-error-overlay/src/utils/parseCompileError.js new file mode 100644 index 0000000000000000000000000000000000000000..2c9b6e60ebb1a93df66b30744ca3b5154b367ac3 --- /dev/null +++ b/packages/react-error-overlay/src/utils/parseCompileError.js @@ -0,0 +1,57 @@ +// @flow +import Anser from 'anser'; + +export type ErrorLocation = {| + fileName: string, + lineNumber: number, +|}; + +const filePathRegex = /^\.(\/[^/\n ]+)+\.[^/\n ]+$/; + +const lineNumberRegexes = [ + // Babel syntax errors + // Based on syntax error formating of babylon parser + // https://github.com/babel/babylon/blob/v7.0.0-beta.22/src/parser/location.js#L19 + /^.*\((\d+):(\d+)\)$/, + + // ESLint errors + // Based on eslintFormatter in react-dev-utils + /^Line (\d+):.+$/, +]; + +// Based on error formatting of webpack +// https://github.com/webpack/webpack/blob/v3.5.5/lib/Stats.js#L183-L217 +function parseCompileError(message: string): ?ErrorLocation { + const lines: Array<string> = message.split('\n'); + let fileName: string = ''; + let lineNumber: number = 0; + + for (let i = 0; i < lines.length; i++) { + const line: string = Anser.ansiToText(lines[i]).trim(); + if (!line) { + continue; + } + + if (!fileName && line.match(filePathRegex)) { + fileName = line; + } + + let k = 0; + while (k < lineNumberRegexes.length) { + const match: ?Array<string> = line.match(lineNumberRegexes[k]); + if (match) { + lineNumber = parseInt(match[1], 10); + break; + } + k++; + } + + if (fileName && lineNumber) { + break; + } + } + + return fileName && lineNumber ? { fileName, lineNumber } : null; +} + +export default parseCompileError;