Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 6, 2017
11 changes: 10 additions & 1 deletion packages/react-dev-utils/webpackHotDevClient.js
Original file line number Diff line number Diff line change
@@ -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;
},
Original file line number Diff line number Diff line change
@@ -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>
);
7 changes: 4 additions & 3 deletions packages/react-error-overlay/src/containers/RuntimeError.js
Original file line number Diff line number Diff line change
@@ -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>
);
Original file line number Diff line number Diff line change
@@ -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."
48 changes: 19 additions & 29 deletions packages/react-error-overlay/src/containers/StackFrame.js
Original file line number Diff line number Diff line change
@@ -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} />
12 changes: 4 additions & 8 deletions packages/react-error-overlay/src/containers/StackTrace.js
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 8 additions & 3 deletions packages/react-error-overlay/src/iframeScript.js
Original file line number Diff line number Diff line change
@@ -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}
/>
);
}
21 changes: 19 additions & 2 deletions packages/react-error-overlay/src/index.js
Original file line number Diff line number Diff line change
@@ -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) {
57 changes: 57 additions & 0 deletions packages/react-error-overlay/src/utils/parseCompileError.js
Original file line number Diff line number Diff line change
@@ -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;