Commit 00ed100b authored by Tharaka Wijebandara's avatar Tharaka Wijebandara Committed by Joe Haddad
Browse files

Add click-to-open support for build errors (#3100)

* Implement click-to-open for babel syntax errors in build error overlay

* Add click-to-open support for lint errors and refactor parser

* Reactor code to reuse open-in-editor functionality in both build and runtime error overlays

* Fix some eslint warnings

* Add a comment about keeping middleware and dev client in sync

* Remove es6 features from webpack dev client

* Make open-in-editor functionality to work with new iframe script

* Rename `openInEditor` to `editorHandler`
 -  Remove indirection of openInEditorListener
 -  Check editorHandler for null before styling error clickable

* Fix flow errors
parent a0030fcf
Showing with 142 additions and 50 deletions
+142 -50
...@@ -23,6 +23,16 @@ var launchEditorEndpoint = require('./launchEditorEndpoint'); ...@@ -23,6 +23,16 @@ var launchEditorEndpoint = require('./launchEditorEndpoint');
var formatWebpackMessages = require('./formatWebpackMessages'); var formatWebpackMessages = require('./formatWebpackMessages');
var ErrorOverlay = require('react-error-overlay'); 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. // We need to keep track of if there has been a runtime error.
// Essentially, we cannot guarantee application state was not corrupted by the // Essentially, we cannot guarantee application state was not corrupted by the
// runtime error. To prevent confusing behavior, we forcibly reload the entire // runtime error. To prevent confusing behavior, we forcibly reload the entire
...@@ -31,7 +41,6 @@ var ErrorOverlay = require('react-error-overlay'); ...@@ -31,7 +41,6 @@ var ErrorOverlay = require('react-error-overlay');
// See https://github.com/facebookincubator/create-react-app/issues/3096 // See https://github.com/facebookincubator/create-react-app/issues/3096
var hadRuntimeError = false; var hadRuntimeError = false;
ErrorOverlay.startReportingRuntimeErrors({ ErrorOverlay.startReportingRuntimeErrors({
launchEditorEndpoint: launchEditorEndpoint,
onError: function() { onError: function() {
hadRuntimeError = true; hadRuntimeError = true;
}, },
......
...@@ -12,18 +12,34 @@ import Footer from '../components/Footer'; ...@@ -12,18 +12,34 @@ import Footer from '../components/Footer';
import Header from '../components/Header'; import Header from '../components/Header';
import CodeBlock from '../components/CodeBlock'; import CodeBlock from '../components/CodeBlock';
import generateAnsiHTML from '../utils/generateAnsiHTML'; import generateAnsiHTML from '../utils/generateAnsiHTML';
import parseCompileError from '../utils/parseCompileError';
import type { ErrorLocation } from '../utils/parseCompileError';
const codeAnchorStyle = {
cursor: 'pointer',
};
type Props = {| type Props = {|
error: string, error: string,
editorHandler: (errorLoc: ErrorLocation) => void,
|}; |};
class CompileErrorContainer extends PureComponent<Props, void> { class CompileErrorContainer extends PureComponent<Props, void> {
render() { render() {
const { error } = this.props; const { error, editorHandler } = this.props;
const errLoc: ?ErrorLocation = parseCompileError(error);
const canOpenInEditor = errLoc !== null && editorHandler !== null;
return ( return (
<ErrorOverlay> <ErrorOverlay>
<Header headerText="Failed to compile" /> <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." /> <Footer line1="This error occurred during the build time and cannot be dismissed." />
</ErrorOverlay> </ErrorOverlay>
); );
......
...@@ -11,6 +11,7 @@ import Header from '../components/Header'; ...@@ -11,6 +11,7 @@ import Header from '../components/Header';
import StackTrace from './StackTrace'; import StackTrace from './StackTrace';
import type { StackFrame } from '../utils/stack-frame'; import type { StackFrame } from '../utils/stack-frame';
import type { ErrorLocation } from '../utils/parseCompileError';
const wrapperStyle = { const wrapperStyle = {
display: 'flex', display: 'flex',
...@@ -26,10 +27,10 @@ export type ErrorRecord = {| ...@@ -26,10 +27,10 @@ export type ErrorRecord = {|
type Props = {| type Props = {|
errorRecord: ErrorRecord, 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 { error, unhandledRejection, contextSize, stackFrames } = errorRecord;
const errorName = unhandledRejection const errorName = unhandledRejection
? 'Unhandled Rejection (' + error.name + ')' ? 'Unhandled Rejection (' + error.name + ')'
...@@ -58,7 +59,7 @@ function RuntimeError({ errorRecord, launchEditorEndpoint }: Props) { ...@@ -58,7 +59,7 @@ function RuntimeError({ errorRecord, launchEditorEndpoint }: Props) {
stackFrames={stackFrames} stackFrames={stackFrames}
errorName={errorName} errorName={errorName}
contextSize={contextSize} contextSize={contextSize}
launchEditorEndpoint={launchEditorEndpoint} editorHandler={editorHandler}
/> />
</div> </div>
); );
......
...@@ -14,11 +14,12 @@ import RuntimeError from './RuntimeError'; ...@@ -14,11 +14,12 @@ import RuntimeError from './RuntimeError';
import Footer from '../components/Footer'; import Footer from '../components/Footer';
import type { ErrorRecord } from './RuntimeError'; import type { ErrorRecord } from './RuntimeError';
import type { ErrorLocation } from '../utils/parseCompileError';
type Props = {| type Props = {|
errorRecords: ErrorRecord[], errorRecords: ErrorRecord[],
close: () => void, close: () => void,
launchEditorEndpoint: ?string, editorHandler: (errorLoc: ErrorLocation) => void,
|}; |};
type State = {| type State = {|
...@@ -74,7 +75,7 @@ class RuntimeErrorContainer extends PureComponent<Props, State> { ...@@ -74,7 +75,7 @@ class RuntimeErrorContainer extends PureComponent<Props, State> {
)} )}
<RuntimeError <RuntimeError
errorRecord={errorRecords[this.state.currentIndex]} errorRecord={errorRecords[this.state.currentIndex]}
launchEditorEndpoint={this.props.launchEditorEndpoint} editorHandler={this.props.editorHandler}
/> />
<Footer <Footer
line1="This screen is visible only in development. It will not appear if the app crashes in production." line1="This screen is visible only in development. It will not appear if the app crashes in production."
......
...@@ -12,6 +12,7 @@ import { getPrettyURL } from '../utils/getPrettyURL'; ...@@ -12,6 +12,7 @@ import { getPrettyURL } from '../utils/getPrettyURL';
import { darkGray } from '../styles'; import { darkGray } from '../styles';
import type { StackFrame as StackFrameType } from '../utils/stack-frame'; import type { StackFrame as StackFrameType } from '../utils/stack-frame';
import type { ErrorLocation } from '../utils/parseCompileError';
const linkStyle = { const linkStyle = {
fontSize: '0.9em', fontSize: '0.9em',
...@@ -45,10 +46,10 @@ const toggleStyle = { ...@@ -45,10 +46,10 @@ const toggleStyle = {
type Props = {| type Props = {|
frame: StackFrameType, frame: StackFrameType,
launchEditorEndpoint: ?string,
contextSize: number, contextSize: number,
critical: boolean, critical: boolean,
showCode: boolean, showCode: boolean,
editorHandler: (errorLoc: ErrorLocation) => void,
|}; |};
type State = {| type State = {|
...@@ -66,47 +67,35 @@ class StackFrame extends Component<Props, State> { ...@@ -66,47 +67,35 @@ class StackFrame extends Component<Props, State> {
})); }));
}; };
getEndpointUrl(): string | null { getErrorLocation(): ErrorLocation | null {
if (!this.props.launchEditorEndpoint) { const {
return null; _originalFileName: fileName,
} _originalLineNumber: lineNumber,
const { _originalFileName: sourceFileName } = this.props.frame; } = this.props.frame;
// Unknown file // Unknown file
if (!sourceFileName) { if (!fileName) {
return null; return null;
} }
// e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1" // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1"
const isInternalWebpackBootstrapCode = const isInternalWebpackBootstrapCode = fileName.trim().indexOf(' ') !== -1;
sourceFileName.trim().indexOf(' ') !== -1;
if (isInternalWebpackBootstrapCode) { if (isInternalWebpackBootstrapCode) {
return null; return null;
} }
// Code is in a real file // Code is in a real file
return this.props.launchEditorEndpoint || null; return { fileName, lineNumber: lineNumber || 1 };
} }
openInEditor = () => { editorHandler = () => {
const endpointUrl = this.getEndpointUrl(); const errorLoc = this.getErrorLocation();
if (endpointUrl === null) { if (!errorLoc) {
return; return;
} }
this.props.editorHandler(errorLoc);
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(() => {}, () => {});
}; };
onKeyDown = (e: SyntheticKeyboardEvent<>) => { onKeyDown = (e: SyntheticKeyboardEvent<>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
this.openInEditor(); this.editorHandler();
} }
}; };
...@@ -166,14 +155,15 @@ class StackFrame extends Component<Props, State> { ...@@ -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 ( return (
<div> <div>
<div>{functionName}</div> <div>{functionName}</div>
<div style={linkStyle}> <div style={linkStyle}>
<a <a
style={canOpenInEditor ? anchorStyle : null} style={canOpenInEditor ? anchorStyle : null}
onClick={canOpenInEditor ? this.openInEditor : null} onClick={canOpenInEditor ? this.editorHandler : null}
onKeyDown={canOpenInEditor ? this.onKeyDown : null} onKeyDown={canOpenInEditor ? this.onKeyDown : null}
tabIndex={canOpenInEditor ? '0' : null} tabIndex={canOpenInEditor ? '0' : null}
> >
...@@ -183,7 +173,7 @@ class StackFrame extends Component<Props, State> { ...@@ -183,7 +173,7 @@ class StackFrame extends Component<Props, State> {
{codeBlockProps && ( {codeBlockProps && (
<span> <span>
<a <a
onClick={canOpenInEditor ? this.openInEditor : null} onClick={canOpenInEditor ? this.editorHandler : null}
style={canOpenInEditor ? codeAnchorStyle : null} style={canOpenInEditor ? codeAnchorStyle : null}
> >
<CodeBlock {...codeBlockProps} /> <CodeBlock {...codeBlockProps} />
......
...@@ -13,6 +13,7 @@ import { isInternalFile } from '../utils/isInternalFile'; ...@@ -13,6 +13,7 @@ import { isInternalFile } from '../utils/isInternalFile';
import { isBultinErrorName } from '../utils/isBultinErrorName'; import { isBultinErrorName } from '../utils/isBultinErrorName';
import type { StackFrame as StackFrameType } from '../utils/stack-frame'; import type { StackFrame as StackFrameType } from '../utils/stack-frame';
import type { ErrorLocation } from '../utils/parseCompileError';
const traceStyle = { const traceStyle = {
fontSize: '1em', fontSize: '1em',
...@@ -25,17 +26,12 @@ type Props = {| ...@@ -25,17 +26,12 @@ type Props = {|
stackFrames: StackFrameType[], stackFrames: StackFrameType[],
errorName: string, errorName: string,
contextSize: number, contextSize: number,
launchEditorEndpoint: ?string, editorHandler: (errorLoc: ErrorLocation) => void,
|}; |};
class StackTrace extends Component<Props> { class StackTrace extends Component<Props> {
renderFrames() { renderFrames() {
const { const { stackFrames, errorName, contextSize, editorHandler } = this.props;
stackFrames,
errorName,
contextSize,
launchEditorEndpoint,
} = this.props;
const renderedFrames = []; const renderedFrames = [];
let hasReachedAppCode = false, let hasReachedAppCode = false,
currentBundle = [], currentBundle = [],
...@@ -59,7 +55,7 @@ class StackTrace extends Component<Props> { ...@@ -59,7 +55,7 @@ class StackTrace extends Component<Props> {
contextSize={contextSize} contextSize={contextSize}
critical={index === 0} critical={index === 0}
showCode={!shouldCollapse} showCode={!shouldCollapse}
launchEditorEndpoint={launchEditorEndpoint} editorHandler={editorHandler}
/> />
); );
const lastElement = index === stackFrames.length - 1; const lastElement = index === stackFrames.length - 1;
......
...@@ -19,17 +19,22 @@ function render({ ...@@ -19,17 +19,22 @@ function render({
currentBuildError, currentBuildError,
currentRuntimeErrorRecords, currentRuntimeErrorRecords,
dismissRuntimeErrors, dismissRuntimeErrors,
launchEditorEndpoint, editorHandler,
}) { }) {
if (currentBuildError) { if (currentBuildError) {
return <CompileErrorContainer error={currentBuildError} />; return (
<CompileErrorContainer
error={currentBuildError}
editorHandler={editorHandler}
/>
);
} }
if (currentRuntimeErrorRecords.length > 0) { if (currentRuntimeErrorRecords.length > 0) {
return ( return (
<RuntimeErrorContainer <RuntimeErrorContainer
errorRecords={currentRuntimeErrorRecords} errorRecords={currentRuntimeErrorRecords}
close={dismissRuntimeErrors} close={dismissRuntimeErrors}
launchEditorEndpoint={launchEditorEndpoint} editorHandler={editorHandler}
/> />
); );
} }
......
...@@ -16,22 +16,32 @@ import { applyStyles } from './utils/dom/css'; ...@@ -16,22 +16,32 @@ import { applyStyles } from './utils/dom/css';
import iframeScript from 'iframeScript'; import iframeScript from 'iframeScript';
import type { ErrorRecord } from './listenToRuntimeErrors'; import type { ErrorRecord } from './listenToRuntimeErrors';
import type { ErrorLocation } from './utils/parseCompileError';
type RuntimeReportingOptions = {| type RuntimeReportingOptions = {|
onError: () => void, onError: () => void,
launchEditorEndpoint: string,
filename?: string, filename?: string,
|}; |};
type EditorHandler = (errorLoc: ErrorLocation) => void;
let iframe: null | HTMLIFrameElement = null; let iframe: null | HTMLIFrameElement = null;
let isLoadingIframe: boolean = false; let isLoadingIframe: boolean = false;
var isIframeReady: boolean = false; var isIframeReady: boolean = false;
let editorHandler: null | EditorHandler = null;
let currentBuildError: null | string = null; let currentBuildError: null | string = null;
let currentRuntimeErrorRecords: Array<ErrorRecord> = []; let currentRuntimeErrorRecords: Array<ErrorRecord> = [];
let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null; let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null;
let stopListeningToRuntimeErrors: null | (() => void) = null; let stopListeningToRuntimeErrors: null | (() => void) = null;
export function setEditorHandler(handler: EditorHandler | null) {
editorHandler = handler;
if (iframe) {
update();
}
}
export function reportBuildError(error: string) { export function reportBuildError(error: string) {
currentBuildError = error; currentBuildError = error;
update(); update();
...@@ -46,6 +56,13 @@ export function startReportingRuntimeErrors(options: RuntimeReportingOptions) { ...@@ -46,6 +56,13 @@ export function startReportingRuntimeErrors(options: RuntimeReportingOptions) {
if (stopListeningToRuntimeErrors !== null) { if (stopListeningToRuntimeErrors !== null) {
throw new Error('Already listening'); 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; currentRuntimeErrorOptions = options;
listenToRuntimeErrors(errorRecord => { listenToRuntimeErrors(errorRecord => {
try { try {
...@@ -133,7 +150,7 @@ function updateIframeContent() { ...@@ -133,7 +150,7 @@ function updateIframeContent() {
currentBuildError, currentBuildError,
currentRuntimeErrorRecords, currentRuntimeErrorRecords,
dismissRuntimeErrors, dismissRuntimeErrors,
launchEditorEndpoint: currentRuntimeErrorOptions.launchEditorEndpoint, editorHandler,
}); });
if (!isRendered) { if (!isRendered) {
......
// @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;
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment