Skip to content
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

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

Merged
merged 9 commits into from Oct 6, 2017
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