mirror of
https://github.com/zhigang1992/create-react-app.git
synced 2026-03-29 22:38:26 +08:00
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
This commit is contained in:
committed by
Joe Haddad
parent
a0030fcf2d
commit
00ed100b26
11
packages/react-dev-utils/webpackHotDevClient.js
vendored
11
packages/react-dev-utils/webpackHotDevClient.js
vendored
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
packages/react-error-overlay/src/iframeScript.js
vendored
11
packages/react-error-overlay/src/iframeScript.js
vendored
@@ -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
packages/react-error-overlay/src/index.js
vendored
21
packages/react-error-overlay/src/index.js
vendored
@@ -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
packages/react-error-overlay/src/utils/parseCompileError.js
vendored
Normal file
57
packages/react-error-overlay/src/utils/parseCompileError.js
vendored
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user