Skip to content

Commit

Permalink
Add click-to-open support for build errors (#3100)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tharakawj authored and Timer committed Oct 6, 2017
1 parent a0030fc commit 00ed100
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 50 deletions.
11 changes: 10 additions & 1 deletion packages/react-dev-utils/webpackHotDevClient.js
Expand Up @@ -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
Expand All @@ -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;
},
Expand Down
Expand Up @@ -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>
);
Expand Down
7 changes: 4 additions & 3 deletions packages/react-error-overlay/src/containers/RuntimeError.js
Expand Up @@ -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',
Expand All @@ -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 + ')'
Expand Down Expand Up @@ -58,7 +59,7 @@ function RuntimeError({ errorRecord, launchEditorEndpoint }: Props) {
stackFrames={stackFrames}
errorName={errorName}
contextSize={contextSize}
launchEditorEndpoint={launchEditorEndpoint}
editorHandler={editorHandler}
/>
</div>
);
Expand Down
Expand Up @@ -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 = {|
Expand Down Expand Up @@ -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."
Expand Down
48 changes: 19 additions & 29 deletions packages/react-error-overlay/src/containers/StackFrame.js
Expand Up @@ -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',
Expand Down Expand Up @@ -45,10 +46,10 @@ const toggleStyle = {

type Props = {|
frame: StackFrameType,
launchEditorEndpoint: ?string,
contextSize: number,
critical: boolean,
showCode: boolean,
editorHandler: (errorLoc: ErrorLocation) => void,
|};

type State = {|
Expand All @@ -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();
}
};
Expand Down Expand Up @@ -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}
>
Expand All @@ -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} />
Expand Down
12 changes: 4 additions & 8 deletions packages/react-error-overlay/src/containers/StackTrace.js
Expand Up @@ -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',
Expand All @@ -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 = [],
Expand All @@ -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;
Expand Down
11 changes: 8 additions & 3 deletions packages/react-error-overlay/src/iframeScript.js
Expand Up @@ -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}
/>
);
}
Expand Down
21 changes: 19 additions & 2 deletions packages/react-error-overlay/src/index.js
Expand Up @@ -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();
Expand All @@ -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 {
Expand Down Expand Up @@ -133,7 +150,7 @@ function updateIframeContent() {
currentBuildError,
currentRuntimeErrorRecords,
dismissRuntimeErrors,
launchEditorEndpoint: currentRuntimeErrorOptions.launchEditorEndpoint,
editorHandler,
});

if (!isRendered) {
Expand Down

0 comments on commit 00ed100

Please sign in to comment.