From 1e43349583bb3582b34f6e36d77275116082d42a Mon Sep 17 00:00:00 2001 From: Tharaka Wijebandara Date: Sun, 11 Jun 2017 12:36:45 +0530 Subject: [PATCH 01/13] Convert react-error-overlay to React --- packages/react-error-overlay/package.json | 2 + .../src/components/CloseButton.js | 38 ++ .../src/components/{code.js => CodeBlock.js} | 71 ++-- .../src/components/Collapsible.js | 80 ++++ .../src/components/ErrorOverlay.js | 112 ++++++ .../src/components/ErrorView.js | 46 +++ .../src/components/Footer.js | 31 ++ .../src/components/Header.js | 51 +++ .../src/components/NavigationBar.js | 66 ++++ .../src/components/StackFrame.js | 176 +++++++++ .../src/components/StackTrace.js | 83 ++++ .../src/components/additional.js | 72 ---- .../src/components/close.js | 35 -- .../src/components/footer.js | 31 -- .../src/components/frame.js | 355 ------------------ .../src/components/frames.js | 132 ------- .../src/components/overlay.js | 95 ----- .../src/effects/shortcuts.js | 53 --- packages/react-error-overlay/src/overlay.js | 190 +++------- packages/react-error-overlay/src/styles.js | 178 +-------- .../src/utils/dom/consumeEvent.js | 18 - .../src/utils/dom/enableTabClick.js | 24 -- .../src/utils/getPrettyURL.js | 49 +++ .../{errorRegister.js => getStackFrames.js} | 40 +- .../src/utils/isBultinErrorName.js | 27 ++ 25 files changed, 866 insertions(+), 1189 deletions(-) create mode 100644 packages/react-error-overlay/src/components/CloseButton.js rename packages/react-error-overlay/src/components/{code.js => CodeBlock.js} (69%) create mode 100644 packages/react-error-overlay/src/components/Collapsible.js create mode 100644 packages/react-error-overlay/src/components/ErrorOverlay.js create mode 100644 packages/react-error-overlay/src/components/ErrorView.js create mode 100644 packages/react-error-overlay/src/components/Footer.js create mode 100644 packages/react-error-overlay/src/components/Header.js create mode 100644 packages/react-error-overlay/src/components/NavigationBar.js create mode 100644 packages/react-error-overlay/src/components/StackFrame.js create mode 100644 packages/react-error-overlay/src/components/StackTrace.js delete mode 100644 packages/react-error-overlay/src/components/additional.js delete mode 100644 packages/react-error-overlay/src/components/close.js delete mode 100644 packages/react-error-overlay/src/components/footer.js delete mode 100644 packages/react-error-overlay/src/components/frame.js delete mode 100644 packages/react-error-overlay/src/components/frames.js delete mode 100644 packages/react-error-overlay/src/components/overlay.js delete mode 100644 packages/react-error-overlay/src/effects/shortcuts.js delete mode 100644 packages/react-error-overlay/src/utils/dom/consumeEvent.js delete mode 100644 packages/react-error-overlay/src/utils/dom/enableTabClick.js create mode 100644 packages/react-error-overlay/src/utils/getPrettyURL.js rename packages/react-error-overlay/src/utils/{errorRegister.js => getStackFrames.js} (59%) create mode 100644 packages/react-error-overlay/src/utils/isBultinErrorName.js diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json index 15bf9e484b4..c9a7508a903 100644 --- a/packages/react-error-overlay/package.json +++ b/packages/react-error-overlay/package.json @@ -34,7 +34,9 @@ "anser": "1.2.5", "babel-code-frame": "6.22.0", "babel-runtime": "6.23.0", + "react": "^15.5.4", "react-dev-utils": "^3.0.2", + "react-dom": "^15.5.4", "settle-promise": "1.0.0", "source-map": "0.5.6" }, diff --git a/packages/react-error-overlay/src/components/CloseButton.js b/packages/react-error-overlay/src/components/CloseButton.js new file mode 100644 index 00000000000..51a1a0c7b78 --- /dev/null +++ b/packages/react-error-overlay/src/components/CloseButton.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React from 'react'; +import { black } from '../styles'; + +const closeButtonStyle = { + color: black, + lineHeight: '1rem', + fontSize: '1.5rem', + padding: '1rem', + cursor: 'pointer', + position: 'absolute', + right: 0, + top: 0, +}; + +type CloseCallback = () => void; +function CloseButton({ close }: { close: CloseCallback }) { + return ( + + × + + ); +} + +export default CloseButton; diff --git a/packages/react-error-overlay/src/components/code.js b/packages/react-error-overlay/src/components/CodeBlock.js similarity index 69% rename from packages/react-error-overlay/src/components/code.js rename to packages/react-error-overlay/src/components/CodeBlock.js index 580fe3b1be5..ce5c11dd212 100644 --- a/packages/react-error-overlay/src/components/code.js +++ b/packages/react-error-overlay/src/components/CodeBlock.js @@ -8,33 +8,58 @@ */ /* @flow */ -import type { ScriptLine } from '../utils/stack-frame'; +import React from 'react'; import { applyStyles } from '../utils/dom/css'; import { absolutifyCaret } from '../utils/dom/absolutifyCaret'; +import type { ScriptLine } from '../utils/stack-frame'; import { - codeStyle, primaryErrorStyle, - primaryPreStyle, secondaryErrorStyle, - secondaryPreStyle, + redTransparent, + yellowTransparent, } from '../styles'; import generateAnsiHtml from 'react-dev-utils/ansiHTML'; import codeFrame from 'babel-code-frame'; -function createCode( - document: Document, - sourceLines: ScriptLine[], +const _preStyle = { + display: 'block', + padding: '0.5em', + marginTop: '0.5em', + marginBottom: '0.5em', + overflowX: 'auto', + whiteSpace: 'pre-wrap', + borderRadius: '0.25rem', +}; + +const primaryPreStyle = { + ..._preStyle, + backgroundColor: redTransparent, +}; + +const secondaryPreStyle = { + ..._preStyle, + backgroundColor: yellowTransparent, +}; + +const codeStyle = { + fontFamily: 'Consolas, Menlo, monospace', +}; + +type CodeBlockPropsType = { + lines: ScriptLine[], lineNum: number, - columnNum: number | null, + columnNum: number, contextSize: number, main: boolean, - onSourceClick: ?Function -) { +}; + +function CodeBlock(props: CodeBlockPropsType) { + const { lines, lineNum, columnNum, contextSize, main } = props; const sourceCode = []; let whiteSpace = Infinity; - sourceLines.forEach(function(e) { + lines.forEach(function(e) { const { content: text } = e; const m = text.match(/^\s*/); if (text === '') { @@ -46,7 +71,7 @@ function createCode( whiteSpace = 0; } }); - sourceLines.forEach(function(e) { + lines.forEach(function(e) { let { content: text } = e; const { lineNumber: line } = e; @@ -69,7 +94,6 @@ function createCode( const code = document.createElement('code'); code.innerHTML = htmlHighlight; absolutifyCaret(code); - applyStyles(code, codeStyle); const ccn = code.childNodes; // eslint-disable-next-line @@ -91,19 +115,14 @@ function createCode( break oLoop; } } - const pre = document.createElement('pre'); - applyStyles(pre, main ? primaryPreStyle : secondaryPreStyle); - pre.appendChild(code); - if (typeof onSourceClick === 'function') { - let handler = onSourceClick; - pre.style.cursor = 'pointer'; - pre.addEventListener('click', function() { - handler(); - }); - } - - return pre; + const preStyle = main ? primaryPreStyle : secondaryPreStyle; + const codeBlock = { __html: code.innerHTML }; + return ( +
+      
+    
+ ); } -export { createCode }; +export default CodeBlock; diff --git a/packages/react-error-overlay/src/components/Collapsible.js b/packages/react-error-overlay/src/components/Collapsible.js new file mode 100644 index 00000000000..3d474d4e204 --- /dev/null +++ b/packages/react-error-overlay/src/components/Collapsible.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React, { Component } from 'react'; +import { black } from '../styles'; + +const _collapsibleStyle = { + color: black, + cursor: 'pointer', + border: 'none', + display: 'block', + width: '100%', + textAlign: 'left', + background: '#fff', + fontFamily: 'Consolas, Menlo, monospace', + fontSize: '1em', + padding: '0px', + lineHeight: '1.5', +}; + +const collapsibleCollapsedStyle = { + ..._collapsibleStyle, + marginBottom: '1.5em', +}; + +const collapsibleExpandedStyle = { + ..._collapsibleStyle, + marginBottom: '0.6em', +}; + +class Collapsible extends Component { + state = { + collapsed: true, + }; + + toggleCollaped = () => { + this.setState(state => ({ + collapsed: !state.collapsed, + })); + }; + + render() { + const count = this.props.children.length; + const collapsed = this.state.collapsed; + return ( +
+ +
+ {this.props.children} + +
+
+ ); + } +} + +export default Collapsible; diff --git a/packages/react-error-overlay/src/components/ErrorOverlay.js b/packages/react-error-overlay/src/components/ErrorOverlay.js new file mode 100644 index 00000000000..670a4b10989 --- /dev/null +++ b/packages/react-error-overlay/src/components/ErrorOverlay.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React, { PureComponent } from 'react'; +import CloseButton from './CloseButton'; +import NavigationBar from './NavigationBar'; +import ErrorView from './ErrorView'; +import Footer from './Footer'; +import { black } from '../styles'; + +const overlayStyle = { + position: 'relative', + display: 'inline-flex', + flexDirection: 'column', + height: '100%', + width: '1024px', + maxWidth: '100%', + overflowX: 'hidden', + overflowY: 'auto', + padding: '0.5rem', + boxSizing: 'border-box', + textAlign: 'left', + fontFamily: 'Consolas, Menlo, monospace', + fontSize: '11px', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + lineHeight: 1.5, + color: black, +}; + +class ErrorOverlay extends PureComponent { + state = { + currentIndex: 0, + }; + iframeWindow: window = null; + + previous = () => { + this.setState((state, props) => ({ + currentIndex: state.currentIndex > 0 + ? state.currentIndex - 1 + : props.errorRecords.length - 1, + })); + }; + + next = () => { + this.setState((state, props) => ({ + currentIndex: state.currentIndex < props.errorRecords.length - 1 + ? state.currentIndex + 1 + : 0, + })); + }; + + handleSortcuts = (e: KeyboardEvent) => { + const { key } = e; + if (key === 'Escape') { + this.props.close(); + } else if (key === 'ArrowLeft') { + this.previous(); + } else if (key === 'ArrowRight') { + this.next(); + } + }; + + getIframeWindow = (element: HTMLDivElement) => { + if (element) { + const document = element.ownerDocument; + this.iframeWindow = document.defaultView; + } + }; + + componentDidMount() { + window.addEventListener('keydown', this.handleSortcuts); + if (this.iframeWindow) { + this.iframeWindow.addEventListener('keydown', this.handleSortcuts); + } + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.handleSortcuts); + if (this.iframeWindow) { + this.iframeWindow.removeEventListener('keydown', this.handleSortcuts); + } + } + + render() { + const { errorRecords, close } = this.props; + const totalErrors = errorRecords.length; + return ( +
+ + {totalErrors > 1 && + } + +
+
+ ); + } +} + +export default ErrorOverlay; diff --git a/packages/react-error-overlay/src/components/ErrorView.js b/packages/react-error-overlay/src/components/ErrorView.js new file mode 100644 index 00000000000..84726b2816e --- /dev/null +++ b/packages/react-error-overlay/src/components/ErrorView.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React from 'react'; +import Header from './Header'; +import StackTrace from './StackTrace'; +import type { StackFrame } from '../utils/stack-frame'; + +const wrapperStyle = { + display: 'flex', + flexDirection: 'column', +}; + +type ErrorRecord = { + error: Error, + unhandledRejection: boolean, + contextSize: number, + stackFrames: StackFrame[], +}; + +function ErrorView({ errorRecord }: { errorRecord: ErrorRecord }) { + const { error, unhandledRejection, contextSize, stackFrames } = errorRecord; + const errorName = unhandledRejection + ? 'Unhandled Rejection (' + error.name + ')' + : error.name; + + return ( +
+
+ +
+ ); +} + +export default ErrorView; diff --git a/packages/react-error-overlay/src/components/Footer.js b/packages/react-error-overlay/src/components/Footer.js new file mode 100644 index 00000000000..132d8feb7e7 --- /dev/null +++ b/packages/react-error-overlay/src/components/Footer.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React from 'react'; +import { darkGray } from '../styles'; + +const footerStyle = { + fontFamily: 'sans-serif', + color: darkGray, + marginTop: '0.5rem', + flex: '0 0 auto', +}; + +function Footer() { + return ( +
+ This screen is visible only in development. It will not appear if the app crashes in production. +
+ Open your browser’s developer console to further inspect this error. +
+ ); +} + +export default Footer; diff --git a/packages/react-error-overlay/src/components/Header.js b/packages/react-error-overlay/src/components/Header.js new file mode 100644 index 00000000000..59999d4e451 --- /dev/null +++ b/packages/react-error-overlay/src/components/Header.js @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React from 'react'; +import { red } from '../styles'; + +const headerStyle = { + fontSize: '2em', + fontFamily: 'sans-serif', + color: red, + whiteSpace: 'pre-wrap', + // Top bottom margin spaces header + // Right margin revents overlap with close button + margin: '0 2rem 0.75rem 0', + flex: '0 0 auto', + maxHeight: '50%', + overflow: 'auto', +}; + +function Header({ name, message }: { name: string, message: string }) { + // Make message prettier + let finalMessage = message.match(/^\w*:/) || !name + ? message + : name + ': ' + message; + + finalMessage = finalMessage + // TODO: maybe remove this prefix from fbjs? + // It's just scaring people + .replace(/^Invariant Violation:\s*/, '') + // This is not helpful either: + .replace(/^Warning:\s*/, '') + // Break the actionable part to the next line. + // AFAIK React 16+ should already do this. + .replace(' Check the render method', '\n\nCheck the render method') + .replace(' Check your code at', '\n\nCheck your code at'); + + return ( +
+ {finalMessage} +
+ ); +} + +export default Header; diff --git a/packages/react-error-overlay/src/components/NavigationBar.js b/packages/react-error-overlay/src/components/NavigationBar.js new file mode 100644 index 00000000000..6c8e559b2fa --- /dev/null +++ b/packages/react-error-overlay/src/components/NavigationBar.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React from 'react'; +import { red, redTransparent } from '../styles'; + +const navigationBarStyle = { + marginBottom: '0.5rem', +}; + +const buttonContainerStyle = { + marginRight: '1em', +}; + +const _navButtonStyle = { + backgroundColor: redTransparent, + color: red, + border: 'none', + borderRadius: '4px', + padding: '3px 6px', + cursor: 'pointer', +}; + +const leftButtonStyle = { + ..._navButtonStyle, + borderTopRightRadius: '0px', + borderBottomRightRadius: '0px', + marginRight: '1px', +}; + +const rightButtonStyle = { + ..._navButtonStyle, + borderTopLeftRadius: '0px', + borderBottomLeftRadius: '0px', +}; + +type Callback = () => void; + +type NavigationBarPropsType = { + currentError: number, + totalErrors: number, + previous: Callback, + next: Callback, +}; + +function NavigationBar(props: NavigationBarPropsType) { + const { currentError, totalErrors, previous, next } = props; + return ( +
+ + + + + {`${currentError} of ${totalErrors} errors on the page`} +
+ ); +} + +export default NavigationBar; diff --git a/packages/react-error-overlay/src/components/StackFrame.js b/packages/react-error-overlay/src/components/StackFrame.js new file mode 100644 index 00000000000..3b45b5a9b23 --- /dev/null +++ b/packages/react-error-overlay/src/components/StackFrame.js @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React, { Component } from 'react'; +import CodeBlock from './CodeBlock'; +import { getPrettyURL } from '../utils/getPrettyURL'; +import { darkGray } from '../styles'; + +const linkStyle = { + fontSize: '0.9em', + marginBottom: '0.9em', +}; + +const anchorStyle = { + textDecoration: 'none', + color: darkGray, + cursor: 'pointer', +}; + +const toggleStyle = { + marginBottom: '1.5em', + color: darkGray, + cursor: 'pointer', + border: 'none', + display: 'block', + width: '100%', + textAlign: 'left', + background: '#fff', + fontFamily: 'Consolas, Menlo, monospace', + fontSize: '1em', + padding: '0px', + lineHeight: '1.5', +}; + +class StackFrame extends Component { + state = { + compiled: false, + }; + + toggleCompiled = () => { + this.setState(state => ({ + compiled: !state.compiled, + })); + }; + + openInEditor = () => { + const { + _originalFileName: sourceFileName, + _originalLineNumber: sourceLineNumber, + } = this.props.frame; + if (sourceFileName) { + // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1" + const isInternalWebpackBootstrapCode = sourceFileName + .trim() + .indexOf(' ') !== -1; + if (!isInternalWebpackBootstrapCode) { + // Keep this in sync with react-error-overlay/middleware.js + fetch( + '/__open-stack-frame-in-editor?fileName=' + + window.encodeURIComponent(sourceFileName) + + '&lineNumber=' + + window.encodeURIComponent(sourceLineNumber || 1) + ).then(() => {}, () => {}); + } + } + }; + + onKeyDown = (e: SyntheticKeyboardEvent) => { + if (e.key === 'Enter') { + this.openInEditor(); + } + }; + + render() { + const { frame, contextSize, critical, showCode } = this.props; + let { functionName, _originalFileName: sourceFileName } = frame; + const { + fileName, + lineNumber, + columnNumber, + _scriptCode: scriptLines, + _originalLineNumber: sourceLineNumber, + _originalColumnNumber: sourceColumnNumber, + _originalScriptCode: sourceLines, + } = frame; + + // TODO: find a better place for this. + // Chrome has a bug with inferring function.name: + // https://github.com/facebookincubator/create-react-app/issues/2097 + // Let's ignore a meaningless name we get for top-level modules. + if ( + functionName === 'Object.friendlySyntaxErrorLabel' || + functionName === 'Object.exports.__esModule' || + functionName === 'Object.' || + !functionName + ) { + functionName = '(anonymous function)'; + } + + const compiled = this.state.compiled; + + const url = getPrettyURL( + sourceFileName, + sourceLineNumber, + sourceColumnNumber, + fileName, + lineNumber, + columnNumber, + compiled + ); + + let codeBlockProps = null; + + if (showCode) { + if ( + compiled && + scriptLines && + scriptLines.length !== 0 && + lineNumber != null + ) { + codeBlockProps = { + lines: scriptLines, + lineNum: lineNumber, + columnNum: columnNumber, + contextSize, + main: critical, + }; + } else if ( + !compiled && + sourceLines && + sourceLines.length !== 0 && + sourceLineNumber != null + ) { + codeBlockProps = { + lines: sourceLines, + lineNum: sourceLineNumber, + columnNum: sourceColumnNumber, + contextSize, + main: critical, + }; + } + } + + return ( +
+
{functionName}
+
+ + {url} + +
+ {codeBlockProps && + + + + } +
+ ); + } +} + +export default StackFrame; diff --git a/packages/react-error-overlay/src/components/StackTrace.js b/packages/react-error-overlay/src/components/StackTrace.js new file mode 100644 index 00000000000..0a7b5b7e183 --- /dev/null +++ b/packages/react-error-overlay/src/components/StackTrace.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React, { Component } from 'react'; +import StackFrame from './StackFrame'; +import Collapsible from './Collapsible'; +import { isInternalFile } from '../utils/isInternalFile'; +import { isBultinErrorName } from '../utils/isBultinErrorName'; + +const traceStyle = { + fontSize: '1em', + flex: '0 1 auto', + minHeight: '0px', + overflow: 'auto', +}; + +class StackTrace extends Component { + renderFrames() { + const { stackFrames, errorName, contextSize } = this.props; + const renderedFrames = []; + let hasReachedAppCode = false, currentBundle = [], bundleCount = 0; + + stackFrames.forEach((frame, index) => { + const { fileName, _originalFileName: sourceFileName } = frame; + const isInternalUrl = isInternalFile(sourceFileName, fileName); + const isThrownIntentionally = !isBultinErrorName(errorName); + const shouldCollapse = isInternalUrl && + (isThrownIntentionally || hasReachedAppCode); + + if (!isInternalUrl) { + hasReachedAppCode = true; + } + + const frameEle = ( + + ); + const lastElement = index === stackFrames.length - 1; + + if (shouldCollapse) { + currentBundle.push(frameEle); + } + + if (!shouldCollapse || lastElement) { + if (currentBundle.length === 1) { + renderedFrames.push(currentBundle[0]); + } else if (currentBundle.length > 1) { + bundleCount++; + renderedFrames.push( + + {currentBundle} + + ); + } + currentBundle = []; + } + + if (!shouldCollapse) { + renderedFrames.push(frameEle); + } + }); + + return renderedFrames; + } + + render() { + return
{this.renderFrames()}
; + } +} + +export default StackTrace; diff --git a/packages/react-error-overlay/src/components/additional.js b/packages/react-error-overlay/src/components/additional.js deleted file mode 100644 index b573c740634..00000000000 --- a/packages/react-error-overlay/src/components/additional.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -import { applyStyles } from '../utils/dom/css'; -import { - additionalChildStyle, - groupStyle, - groupElemLeft, - groupElemRight, -} from '../styles'; -import { consumeEvent } from '../utils/dom/consumeEvent'; -import { enableTabClick } from '../utils/dom/enableTabClick'; - -type SwitchCallback = (offset: number) => void; -function updateAdditional( - document: Document, - additionalReference: HTMLDivElement, - currentError: number, - totalErrors: number, - switchCallback: SwitchCallback -) { - if (additionalReference.lastChild) { - additionalReference.removeChild(additionalReference.lastChild); - } - - if (totalErrors <= 1) { - return; - } - - const div = document.createElement('div'); - applyStyles(div, additionalChildStyle); - - const group = document.createElement('span'); - applyStyles(group, groupStyle); - - const left = document.createElement('button'); - applyStyles(left, groupElemLeft); - left.addEventListener('click', function(e: MouseEvent) { - consumeEvent(e); - switchCallback(-1); - }); - left.appendChild(document.createTextNode('←')); - enableTabClick(left); - - const right = document.createElement('button'); - applyStyles(right, groupElemRight); - right.addEventListener('click', function(e: MouseEvent) { - consumeEvent(e); - switchCallback(1); - }); - right.appendChild(document.createTextNode('→')); - enableTabClick(right); - - group.appendChild(left); - group.appendChild(right); - div.appendChild(group); - - const text = `${currentError} of ${totalErrors} errors on the page`; - div.appendChild(document.createTextNode(text)); - - additionalReference.appendChild(div); -} - -export type { SwitchCallback }; -export { updateAdditional }; diff --git a/packages/react-error-overlay/src/components/close.js b/packages/react-error-overlay/src/components/close.js deleted file mode 100644 index 2ced8d0ce92..00000000000 --- a/packages/react-error-overlay/src/components/close.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -import { applyStyles } from '../utils/dom/css'; -import { hintsStyle, hintStyle, closeButtonStyle } from '../styles'; - -function createHint(document: Document, hint: string, title: string) { - const span = document.createElement('span'); - span.appendChild(document.createTextNode(hint)); - span.setAttribute('title', title); - applyStyles(span, hintStyle); - return span; -} - -type CloseCallback = () => void; -function createClose(document: Document, callback: CloseCallback) { - const hints = document.createElement('div'); - applyStyles(hints, hintsStyle); - - const close = createHint(document, '×', 'Click or press Escape to dismiss.'); - close.addEventListener('click', () => callback()); - applyStyles(close, closeButtonStyle); - hints.appendChild(close); - return hints; -} - -export type { CloseCallback }; -export { createClose }; diff --git a/packages/react-error-overlay/src/components/footer.js b/packages/react-error-overlay/src/components/footer.js deleted file mode 100644 index 4586a04ff2b..00000000000 --- a/packages/react-error-overlay/src/components/footer.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -import { applyStyles } from '../utils/dom/css'; -import { footerStyle } from '../styles'; - -function createFooter(document: Document) { - const div = document.createElement('div'); - applyStyles(div, footerStyle); - div.appendChild( - document.createTextNode( - 'This screen is visible only in development. It will not appear if the app crashes in production.' - ) - ); - div.appendChild(document.createElement('br')); - div.appendChild( - document.createTextNode( - 'Open your browser’s developer console to further inspect this error.' - ) - ); - return div; -} - -export { createFooter }; diff --git a/packages/react-error-overlay/src/components/frame.js b/packages/react-error-overlay/src/components/frame.js deleted file mode 100644 index 43d0d4043a3..00000000000 --- a/packages/react-error-overlay/src/components/frame.js +++ /dev/null @@ -1,355 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -import { enableTabClick } from '../utils/dom/enableTabClick'; -import { createCode } from './code'; -import { isInternalFile } from '../utils/isInternalFile'; -import type { StackFrame } from '../utils/stack-frame'; -import type { FrameSetting, OmitsObject } from './frames'; -import { applyStyles } from '../utils/dom/css'; -import { - omittedFramesExpandedStyle, - omittedFramesCollapsedStyle, - functionNameStyle, - depStyle, - linkStyle, - anchorStyle, - hiddenStyle, -} from '../styles'; - -function getGroupToggle( - document: Document, - omitsCount: number, - omitBundle: number -) { - const omittedFrames = document.createElement('div'); - enableTabClick(omittedFrames); - const text1 = document.createTextNode( - '\u25B6 ' + omitsCount + ' stack frames were collapsed.' - ); - omittedFrames.appendChild(text1); - omittedFrames.addEventListener('click', function() { - const hide = text1.textContent.match(/▲/); - const list = document.getElementsByName('bundle-' + omitBundle); - for (let index = 0; index < list.length; ++index) { - const n = list[index]; - if (hide) { - n.style.display = 'none'; - } else { - n.style.display = ''; - } - } - if (hide) { - text1.textContent = text1.textContent.replace(/▲/, '▶'); - text1.textContent = text1.textContent.replace(/expanded/, 'collapsed'); - applyStyles(omittedFrames, omittedFramesCollapsedStyle); - } else { - text1.textContent = text1.textContent.replace(/▶/, '▲'); - text1.textContent = text1.textContent.replace(/collapsed/, 'expanded'); - applyStyles(omittedFrames, omittedFramesExpandedStyle); - } - }); - applyStyles(omittedFrames, omittedFramesCollapsedStyle); - return omittedFrames; -} - -function insertBeforeBundle( - document: Document, - parent: Node, - omitsCount: number, - omitBundle: number, - actionElement -) { - const children = document.getElementsByName('bundle-' + omitBundle); - if (children.length < 1) { - return; - } - let first: ?Node = children[0]; - while (first != null && first.parentNode !== parent) { - first = first.parentNode; - } - const div = document.createElement('div'); - enableTabClick(div); - div.setAttribute('name', 'bundle-' + omitBundle); - const text = document.createTextNode( - '\u25BC ' + omitsCount + ' stack frames were expanded.' - ); - div.appendChild(text); - div.addEventListener('click', function() { - return actionElement.click(); - }); - applyStyles(div, omittedFramesExpandedStyle); - div.style.display = 'none'; - - parent.insertBefore(div, first); -} - -function frameDiv( - document: Document, - functionName, - url, - internalUrl, - onSourceClick: ?Function -) { - const frame = document.createElement('div'); - const frameFunctionName = document.createElement('div'); - - let cleanedFunctionName; - if (!functionName || functionName === 'Object.') { - cleanedFunctionName = '(anonymous function)'; - } else { - cleanedFunctionName = functionName; - } - - const cleanedUrl = url.replace('webpack://', '.'); - - if (internalUrl) { - applyStyles( - frameFunctionName, - Object.assign({}, functionNameStyle, depStyle) - ); - } else { - applyStyles(frameFunctionName, functionNameStyle); - } - - frameFunctionName.appendChild(document.createTextNode(cleanedFunctionName)); - frame.appendChild(frameFunctionName); - - const frameLink = document.createElement('div'); - applyStyles(frameLink, linkStyle); - const frameAnchor = document.createElement('a'); - applyStyles(frameAnchor, anchorStyle); - frameAnchor.appendChild(document.createTextNode(cleanedUrl)); - frameLink.appendChild(frameAnchor); - frame.appendChild(frameLink); - - if (typeof onSourceClick === 'function') { - let handler = onSourceClick; - enableTabClick(frameAnchor); - frameAnchor.style.cursor = 'pointer'; - frameAnchor.addEventListener('click', function() { - handler(); - }); - } - - return frame; -} - -function isBultinErrorName(errorName: ?string) { - switch (errorName) { - case 'EvalError': - case 'InternalError': - case 'RangeError': - case 'ReferenceError': - case 'SyntaxError': - case 'TypeError': - case 'URIError': - return true; - default: - return false; - } -} - -function getPrettyURL( - sourceFileName: ?string, - sourceLineNumber: ?number, - sourceColumnNumber: ?number, - fileName: ?string, - lineNumber: ?number, - columnNumber: ?number, - compiled: boolean -): string { - let prettyURL; - if (!compiled && sourceFileName && typeof sourceLineNumber === 'number') { - // Remove everything up to the first /src/ or /node_modules/ - const trimMatch = /^[/|\\].*?[/|\\]((src|node_modules)[/|\\].*)/.exec( - sourceFileName - ); - if (trimMatch && trimMatch[1]) { - prettyURL = trimMatch[1]; - } else { - prettyURL = sourceFileName; - } - prettyURL += ':' + sourceLineNumber; - // Note: we intentionally skip 0's because they're produced by cheap Webpack maps - if (sourceColumnNumber) { - prettyURL += ':' + sourceColumnNumber; - } - } else if (fileName && typeof lineNumber === 'number') { - prettyURL = fileName + ':' + lineNumber; - // Note: we intentionally skip 0's because they're produced by cheap Webpack maps - if (columnNumber) { - prettyURL += ':' + columnNumber; - } - } else { - prettyURL = 'unknown'; - } - return prettyURL; -} - -function createFrame( - document: Document, - frameSetting: FrameSetting, - frame: StackFrame, - contextSize: number, - critical: boolean, - omits: OmitsObject, - omitBundle: number, - parentContainer: HTMLDivElement, - lastElement: boolean, - errorName: ?string -) { - const { compiled } = frameSetting; - let { functionName, _originalFileName: sourceFileName } = frame; - const { - fileName, - lineNumber, - columnNumber, - _scriptCode: scriptLines, - _originalLineNumber: sourceLineNumber, - _originalColumnNumber: sourceColumnNumber, - _originalScriptCode: sourceLines, - } = frame; - - // TODO: find a better place for this. - // Chrome has a bug with inferring function.name: - // https://github.com/facebookincubator/create-react-app/issues/2097 - // Let's ignore a meaningless name we get for top-level modules. - if ( - functionName === 'Object.friendlySyntaxErrorLabel' || - functionName === 'Object.exports.__esModule' - ) { - functionName = '(anonymous function)'; - } - - const prettyURL = getPrettyURL( - sourceFileName, - sourceLineNumber, - sourceColumnNumber, - fileName, - lineNumber, - columnNumber, - compiled - ); - - let needsHidden = false; - const isInternalUrl = isInternalFile(sourceFileName, fileName); - const isThrownIntentionally = !isBultinErrorName(errorName); - const shouldCollapse = - isInternalUrl && (isThrownIntentionally || omits.hasReachedAppCode); - - if (!isInternalUrl) { - omits.hasReachedAppCode = true; - } - - if (shouldCollapse) { - ++omits.value; - needsHidden = true; - } - - let collapseElement = null; - if (!shouldCollapse || lastElement) { - if (omits.value > 0) { - const capV = omits.value; - const omittedFrames = getGroupToggle(document, capV, omitBundle); - window.requestAnimationFrame(() => { - insertBeforeBundle( - document, - parentContainer, - capV, - omitBundle, - omittedFrames - ); - }); - if (lastElement && shouldCollapse) { - collapseElement = omittedFrames; - } else { - parentContainer.appendChild(omittedFrames); - } - ++omits.bundle; - } - omits.value = 0; - } - - let onSourceClick = null; - if (sourceFileName) { - // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1" - const isInternalWebpackBootstrapCode = - sourceFileName.trim().indexOf(' ') !== -1; - if (!isInternalWebpackBootstrapCode) { - onSourceClick = () => { - // Keep this in sync with react-error-overlay/middleware.js - fetch( - '/__open-stack-frame-in-editor?fileName=' + - window.encodeURIComponent(sourceFileName) + - '&lineNumber=' + - window.encodeURIComponent(sourceLineNumber || 1) - ).then(() => {}, () => {}); - }; - } - } - - const elem = frameDiv( - document, - functionName, - prettyURL, - shouldCollapse, - onSourceClick - ); - if (needsHidden) { - applyStyles(elem, hiddenStyle); - elem.setAttribute('name', 'bundle-' + omitBundle); - } - - let hasSource = false; - if (!shouldCollapse) { - if ( - compiled && - scriptLines && - scriptLines.length !== 0 && - lineNumber != null - ) { - elem.appendChild( - createCode( - document, - scriptLines, - lineNumber, - columnNumber, - contextSize, - critical, - onSourceClick - ) - ); - hasSource = true; - } else if ( - !compiled && - sourceLines && - sourceLines.length !== 0 && - sourceLineNumber != null - ) { - elem.appendChild( - createCode( - document, - sourceLines, - sourceLineNumber, - sourceColumnNumber, - contextSize, - critical, - onSourceClick - ) - ); - hasSource = true; - } - } - - return { elem: elem, hasSource: hasSource, collapseElement: collapseElement }; -} - -export { createFrame }; diff --git a/packages/react-error-overlay/src/components/frames.js b/packages/react-error-overlay/src/components/frames.js deleted file mode 100644 index 8bd50509295..00000000000 --- a/packages/react-error-overlay/src/components/frames.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -import type { StackFrame } from '../utils/stack-frame'; -import { applyStyles } from '../utils/dom/css'; -import { traceStyle, toggleStyle } from '../styles'; -import { enableTabClick } from '../utils/dom/enableTabClick'; -import { createFrame } from './frame'; - -type OmitsObject = { - value: number, - bundle: number, - hasReachedAppCode: boolean, -}; -type FrameSetting = { compiled: boolean }; -export type { OmitsObject, FrameSetting }; - -function createFrameWrapper( - document: Document, - parent: HTMLDivElement, - factory, - lIndex: number, - frameSettings: FrameSetting[], - contextSize: number -) { - const fac = factory(); - if (fac == null) { - return; - } - const { hasSource, elem, collapseElement } = fac; - - const elemWrapper = document.createElement('div'); - elemWrapper.appendChild(elem); - - if (hasSource) { - const compiledDiv = document.createElement('div'); - enableTabClick(compiledDiv); - applyStyles(compiledDiv, toggleStyle); - - const o = frameSettings[lIndex]; - const compiledText = document.createTextNode( - 'View ' + (o && o.compiled ? 'source' : 'compiled') - ); - compiledDiv.addEventListener('click', function() { - if (o) { - o.compiled = !o.compiled; - } - - const next = createFrameWrapper( - document, - parent, - factory, - lIndex, - frameSettings, - contextSize - ); - if (next != null) { - parent.insertBefore(next, elemWrapper); - parent.removeChild(elemWrapper); - } - }); - compiledDiv.appendChild(compiledText); - elemWrapper.appendChild(compiledDiv); - } - - if (collapseElement != null) { - elemWrapper.appendChild(collapseElement); - } - - return elemWrapper; -} - -function createFrames( - document: Document, - resolvedFrames: StackFrame[], - frameSettings: FrameSetting[], - contextSize: number, - errorName: ?string -) { - if (resolvedFrames.length !== frameSettings.length) { - throw new Error( - 'You must give a frame settings array of identical length to resolved frames.' - ); - } - const trace = document.createElement('div'); - applyStyles(trace, traceStyle); - - let index = 0; - let critical = true; - const omits: OmitsObject = { value: 0, bundle: 1, hasReachedAppCode: false }; - resolvedFrames.forEach(function(frame) { - const lIndex = index++; - const elem = createFrameWrapper( - document, - trace, - createFrame.bind( - undefined, - document, - frameSettings[lIndex], - frame, - contextSize, - critical, - omits, - omits.bundle, - trace, - index === resolvedFrames.length, - errorName - ), - lIndex, - frameSettings, - contextSize - ); - if (elem == null) { - return; - } - critical = false; - trace.appendChild(elem); - }); - //TODO: fix this - omits.value = 0; - - return trace; -} - -export { createFrames }; diff --git a/packages/react-error-overlay/src/components/overlay.js b/packages/react-error-overlay/src/components/overlay.js deleted file mode 100644 index 69acf9ad43f..00000000000 --- a/packages/react-error-overlay/src/components/overlay.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -import { applyStyles } from '../utils/dom/css'; -import { containerStyle, overlayStyle, headerStyle } from '../styles'; -import { createClose } from './close'; -import { createFrames } from './frames'; -import { createFooter } from './footer'; -import type { CloseCallback } from './close'; -import type { StackFrame } from '../utils/stack-frame'; -import { updateAdditional } from './additional'; -import type { FrameSetting } from './frames'; -import type { SwitchCallback } from './additional'; - -function createOverlay( - document: Document, - name: ?string, - message: string, - frames: StackFrame[], - contextSize: number, - currentError: number, - totalErrors: number, - switchCallback: SwitchCallback, - closeCallback: CloseCallback -): { - overlay: HTMLDivElement, - additional: HTMLDivElement, -} { - const frameSettings: FrameSetting[] = frames.map(() => ({ compiled: false })); - // Create overlay - const overlay = document.createElement('div'); - applyStyles(overlay, overlayStyle); - - // Create container - const container = document.createElement('div'); - applyStyles(container, containerStyle); - overlay.appendChild(container); - container.appendChild(createClose(document, closeCallback)); - - // Create "Errors X of Y" in case of multiple errors - const additional = document.createElement('div'); - updateAdditional( - document, - additional, - currentError, - totalErrors, - switchCallback - ); - container.appendChild(additional); - - // Create header - const header = document.createElement('div'); - applyStyles(header, headerStyle); - - // Make message prettier - let finalMessage = - message.match(/^\w*:/) || !name ? message : name + ': ' + message; - - finalMessage = finalMessage - // TODO: maybe remove this prefix from fbjs? - // It's just scaring people - .replace(/^Invariant Violation:\s*/, '') - // This is not helpful either: - .replace(/^Warning:\s*/, '') - // Break the actionable part to the next line. - // AFAIK React 16+ should already do this. - .replace(' Check the render method', '\n\nCheck the render method') - .replace(' Check your code at', '\n\nCheck your code at'); - - // Put it in the DOM - header.appendChild(document.createTextNode(finalMessage)); - container.appendChild(header); - - // Create trace - container.appendChild( - createFrames(document, frames, frameSettings, contextSize, name) - ); - - // Show message - container.appendChild(createFooter(document)); - - return { - overlay, - additional, - }; -} - -export { createOverlay }; diff --git a/packages/react-error-overlay/src/effects/shortcuts.js b/packages/react-error-overlay/src/effects/shortcuts.js deleted file mode 100644 index bf8fd6d5eb7..00000000000 --- a/packages/react-error-overlay/src/effects/shortcuts.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -const SHORTCUT_ESCAPE = 'SHORTCUT_ESCAPE', - SHORTCUT_LEFT = 'SHORTCUT_LEFT', - SHORTCUT_RIGHT = 'SHORTCUT_RIGHT'; - -let boundKeyHandler = null; - -type ShortcutCallback = (type: string) => void; - -function keyHandler(callback: ShortcutCallback, e: KeyboardEvent) { - const { key, keyCode, which } = e; - if (key === 'Escape' || keyCode === 27 || which === 27) { - callback(SHORTCUT_ESCAPE); - } else if (key === 'ArrowLeft' || keyCode === 37 || which === 37) { - callback(SHORTCUT_LEFT); - } else if (key === 'ArrowRight' || keyCode === 39 || which === 39) { - callback(SHORTCUT_RIGHT); - } -} - -function registerShortcuts(target: EventTarget, callback: ShortcutCallback) { - if (boundKeyHandler !== null) { - return; - } - boundKeyHandler = keyHandler.bind(undefined, callback); - target.addEventListener('keydown', boundKeyHandler); -} - -function unregisterShortcuts(target: EventTarget) { - if (boundKeyHandler === null) { - return; - } - target.removeEventListener('keydown', boundKeyHandler); - boundKeyHandler = null; -} - -export { - SHORTCUT_ESCAPE, - SHORTCUT_LEFT, - SHORTCUT_RIGHT, - registerShortcuts as register, - unregisterShortcuts as unregister, - keyHandler as handler, -}; diff --git a/packages/react-error-overlay/src/overlay.js b/packages/react-error-overlay/src/overlay.js index 181cb02716d..883d5604bab 100644 --- a/packages/react-error-overlay/src/overlay.js +++ b/packages/react-error-overlay/src/overlay.js @@ -8,6 +8,8 @@ */ /* @flow */ +import React from 'react'; +import ReactDOM from 'react-dom'; import { register as registerError, unregister as unregisterError, @@ -16,14 +18,6 @@ import { register as registerPromise, unregister as unregisterPromise, } from './effects/unhandledRejection'; -import { - register as registerShortcuts, - unregister as unregisterShortcuts, - handler as keyEventHandler, - SHORTCUT_ESCAPE, - SHORTCUT_LEFT, - SHORTCUT_RIGHT, -} from './effects/shortcuts'; import { register as registerStackTraceLimit, unregister as unregisterStackTraceLimit, @@ -34,179 +28,96 @@ import { unregisterReactStack, } from './effects/proxyConsole'; import { massage as massageWarning } from './utils/warnings'; - -import { - consume as consumeError, - getErrorRecord, - drain as drainErrors, -} from './utils/errorRegister'; -import type { ErrorRecordReference } from './utils/errorRegister'; - -import type { StackFrame } from './utils/stack-frame'; -import { iframeStyle } from './styles'; +import { iframeStyle, overlayStyle } from './styles'; import { applyStyles } from './utils/dom/css'; -import { createOverlay } from './components/overlay'; -import { updateAdditional } from './components/additional'; +import getStackFrames from './utils/getStackFrames'; +import ErrorOverlay from './components/ErrorOverlay'; const CONTEXT_SIZE: number = 3; -let iframeReference: HTMLIFrameElement | null = null; -let additionalReference = null; -let errorReferences: ErrorRecordReference[] = []; -let currReferenceIndex: number = -1; -function render(name: ?string, message: string, resolvedFrames: StackFrame[]) { - disposeCurrentView(); +let errorRecords = []; - const iframe = window.document.createElement('iframe'); - applyStyles(iframe, iframeStyle); - iframeReference = iframe; - iframe.onload = () => { +let container: HTMLDivElement | null = null; +let iframeReference: HTMLIFrameElement | null = null; + +// Mount overlay container +function mount(callback) { + iframeReference = window.document.createElement('iframe'); + applyStyles(iframeReference, iframeStyle); + iframeReference.onload = () => { if (iframeReference == null) { return; } - const w = iframeReference.contentWindow; const document = iframeReference.contentDocument; - - const { overlay, additional } = createOverlay( - document, - name, - message, - resolvedFrames, - CONTEXT_SIZE, - currReferenceIndex + 1, - errorReferences.length, - offset => { - switchError(offset); - }, - () => { - unmount(); - } - ); - if (w != null) { - w.onkeydown = event => { - keyEventHandler(type => shortcutHandler(type), event); - }; - } - if (document.body != null) { + if (document != null && document.body != null) { document.body.style.margin = '0'; // Keep popup within body boundaries for iOS Safari // $FlowFixMe document.body.style['max-width'] = '100vw'; - - (document.body: any).appendChild(overlay); + container = document.createElement('div'); + applyStyles(container, overlayStyle); + (document.body: any).appendChild(container); + callback(); } - additionalReference = additional; }; - window.document.body.appendChild(iframe); -} - -function renderErrorByIndex(index: number) { - currReferenceIndex = index; - - const { error, unhandledRejection, enhancedFrames } = getErrorRecord( - errorReferences[index] - ); - - if (unhandledRejection) { - render( - 'Unhandled Rejection (' + error.name + ')', - error.message, - enhancedFrames - ); - } else { - render(error.name, error.message, enhancedFrames); - } + window.document.body.appendChild(iframeReference); } -function switchError(offset) { - if (errorReferences.length === 0) { - return; - } - - let nextView = currReferenceIndex + offset; - - if (nextView < 0) { - nextView = errorReferences.length - 1; - } else if (nextView >= errorReferences.length) { - nextView = 0; - } - - renderErrorByIndex(nextView); -} - -function disposeCurrentView() { +// Unmount overlay container +function unmount() { + ReactDOM.unmountComponentAtNode(container); if (iframeReference === null) { return; } window.document.body.removeChild(iframeReference); iframeReference = null; - additionalReference = null; + container = null; + errorRecords = []; } -function unmount() { - disposeCurrentView(); - drainErrors(); - errorReferences = []; - currReferenceIndex = -1; +function render() { + ReactDOM.render( + , + container + ); } function crash(error: Error, unhandledRejection = false) { if (module.hot && typeof module.hot.decline === 'function') { module.hot.decline(); } - consumeError(error, unhandledRejection, CONTEXT_SIZE) - .then(ref => { - if (ref == null) { + + getStackFrames(error, unhandledRejection, CONTEXT_SIZE) + .then(stackFrames => { + if (stackFrames == null) { return; } - errorReferences.push(ref); - if (iframeReference !== null && additionalReference !== null) { - updateAdditional( - iframeReference.contentDocument, - additionalReference, - currReferenceIndex + 1, - errorReferences.length, - offset => { - switchError(offset); - } - ); - } else { - if (errorReferences.length !== 1) { - throw new Error('Something is *really* wrong.'); + errorRecords = [ + ...errorRecords, + { + error, + unhandledRejection, + contextSize: CONTEXT_SIZE, + stackFrames, + }, + ]; + + if (errorRecords.length > 0) { + if (container == null) { + mount(() => render()); + } else { + render(); } - renderErrorByIndex((currReferenceIndex = 0)); } }) .catch(e => { - console.log('Could not consume error:', e); + console.log('Could not get the stack frames of error:', e); }); } -function shortcutHandler(type: string) { - switch (type) { - case SHORTCUT_ESCAPE: { - unmount(); - break; - } - case SHORTCUT_LEFT: { - switchError(-1); - break; - } - case SHORTCUT_RIGHT: { - switchError(1); - break; - } - default: { - //TODO: this - break; - } - } -} - function inject() { registerError(window, error => crash(error)); registerPromise(window, error => crash(error, true)); - registerShortcuts(window, shortcutHandler); registerStackTraceLimit(); registerReactStack(); @@ -226,7 +137,6 @@ function inject() { function uninject() { unregisterStackTraceLimit(); - unregisterShortcuts(window); unregisterPromise(window); unregisterError(window); unregisterReactStack(); diff --git a/packages/react-error-overlay/src/styles.js b/packages/react-error-overlay/src/styles.js index bf17561d721..3b16a90bcbe 100644 --- a/packages/react-error-overlay/src/styles.js +++ b/packages/react-error-overlay/src/styles.js @@ -35,84 +35,6 @@ const overlayStyle = { 'background-color': white, }; -const containerStyle = { - position: 'relative', - display: 'inline-flex', - 'flex-direction': 'column', - height: '100%', - width: '1024px', - 'max-width': '100%', - 'overflow-x': 'hidden', - 'overflow-y': 'auto', - padding: '0.5rem', - 'box-sizing': 'border-box', - 'text-align': 'left', - 'font-family': 'Consolas, Menlo, monospace', - 'font-size': '11px', - 'white-space': 'pre-wrap', - 'word-break': 'break-word', - 'line-height': 1.5, - color: black, -}; - -const hintsStyle = { - color: darkGray, -}; - -const hintStyle = { - padding: '0.5em 1em', - cursor: 'pointer', -}; - -const closeButtonStyle = { - color: black, - 'line-height': '1rem', - 'font-size': '1.5rem', - padding: '1rem', - cursor: 'pointer', - position: 'absolute', - right: 0, - top: 0, -}; - -const additionalChildStyle = { - 'margin-bottom': '0.5rem', -}; - -const headerStyle = { - 'font-size': '2em', - 'font-family': 'sans-serif', - color: red, - 'white-space': 'pre-wrap', - // Top bottom margin spaces header - // Right margin revents overlap with close button - margin: '0 2rem 0.75rem 0', - flex: '0 0 auto', - 'max-height': '50%', - overflow: 'auto', -}; - -const functionNameStyle = {}; - -const linkStyle = { - 'font-size': '0.9em', - 'margin-bottom': '0.9em', -}; - -const anchorStyle = { - 'text-decoration': 'none', - color: darkGray, -}; - -const traceStyle = { - 'font-size': '1em', - flex: '0 1 auto', - 'min-height': '0px', - overflow: 'auto', -}; - -const depStyle = {}; - const primaryErrorStyle = { 'background-color': lightRed, }; @@ -121,104 +43,14 @@ const secondaryErrorStyle = { 'background-color': yellow, }; -const omittedFramesCollapsedStyle = { - color: black, - cursor: 'pointer', - 'margin-bottom': '1.5em', -}; - -const omittedFramesExpandedStyle = { - color: black, - cursor: 'pointer', - 'margin-bottom': '0.6em', -}; - -const _preStyle = { - display: 'block', - padding: '0.5em', - 'margin-top': '0.5em', - 'margin-bottom': '0.5em', - 'overflow-x': 'auto', - 'white-space': 'pre-wrap', - 'border-radius': '0.25rem', -}; -const primaryPreStyle = Object.assign({}, _preStyle, { - 'background-color': redTransparent, -}); -const secondaryPreStyle = Object.assign({}, _preStyle, { - 'background-color': yellowTransparent, -}); - -const toggleStyle = { - 'margin-bottom': '1.5em', - color: darkGray, - cursor: 'pointer', -}; - -const codeStyle = { - 'font-family': 'Consolas, Menlo, monospace', -}; - -const hiddenStyle = { - display: 'none', -}; - -const groupStyle = { - 'margin-right': '1em', -}; - -const _groupElemStyle = { - 'background-color': redTransparent, - color: red, - border: 'none', - 'border-radius': '4px', - padding: '3px 6px', - cursor: 'pointer', -}; - -const groupElemLeft = Object.assign({}, _groupElemStyle, { - 'border-top-right-radius': '0px', - 'border-bottom-right-radius': '0px', - 'margin-right': '1px', -}); - -const groupElemRight = Object.assign({}, _groupElemStyle, { - 'border-top-left-radius': '0px', - 'border-bottom-left-radius': '0px', -}); - -const footerStyle = { - 'font-family': 'sans-serif', - color: darkGray, - 'margin-top': '0.5rem', - flex: '0 0 auto', -}; - export { - containerStyle, iframeStyle, overlayStyle, - hintsStyle, - hintStyle, - closeButtonStyle, - additionalChildStyle, - headerStyle, - functionNameStyle, - linkStyle, - anchorStyle, - traceStyle, - depStyle, primaryErrorStyle, - primaryPreStyle, secondaryErrorStyle, - secondaryPreStyle, - omittedFramesCollapsedStyle, - omittedFramesExpandedStyle, - toggleStyle, - codeStyle, - hiddenStyle, - groupStyle, - groupElemLeft, - groupElemRight, - footerStyle, + black, + darkGray, + red, + redTransparent, + yellowTransparent, }; diff --git a/packages/react-error-overlay/src/utils/dom/consumeEvent.js b/packages/react-error-overlay/src/utils/dom/consumeEvent.js deleted file mode 100644 index 90bb9d1e815..00000000000 --- a/packages/react-error-overlay/src/utils/dom/consumeEvent.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -function consumeEvent(e: Event) { - e.preventDefault(); - if (typeof e.target.blur === 'function') { - e.target.blur(); - } -} - -export { consumeEvent }; diff --git a/packages/react-error-overlay/src/utils/dom/enableTabClick.js b/packages/react-error-overlay/src/utils/dom/enableTabClick.js deleted file mode 100644 index b663c055f8c..00000000000 --- a/packages/react-error-overlay/src/utils/dom/enableTabClick.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -function enableTabClick(node: Element) { - node.setAttribute('tabindex', '0'); - node.addEventListener('keydown', function(e: KeyboardEvent) { - const { key, which, keyCode } = e; - if (key === 'Enter' || which === 13 || keyCode === 13) { - e.preventDefault(); - if (typeof e.target.click === 'function') { - e.target.click(); - } - } - }); -} - -export { enableTabClick }; diff --git a/packages/react-error-overlay/src/utils/getPrettyURL.js b/packages/react-error-overlay/src/utils/getPrettyURL.js new file mode 100644 index 00000000000..85a5a3f9efc --- /dev/null +++ b/packages/react-error-overlay/src/utils/getPrettyURL.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +//@flow +function getPrettyURL( + sourceFileName: ?string, + sourceLineNumber: ?number, + sourceColumnNumber: ?number, + fileName: ?string, + lineNumber: ?number, + columnNumber: ?number, + compiled: boolean +): string { + let prettyURL; + if (!compiled && sourceFileName && typeof sourceLineNumber === 'number') { + // Remove everything up to the first /src/ or /node_modules/ + const trimMatch = /^[/|\\].*?[/|\\]((src|node_modules)[/|\\].*)/.exec( + sourceFileName + ); + if (trimMatch && trimMatch[1]) { + prettyURL = trimMatch[1]; + } else { + prettyURL = sourceFileName; + } + prettyURL += ':' + sourceLineNumber; + // Note: we intentionally skip 0's because they're produced by cheap Webpack maps + if (sourceColumnNumber) { + prettyURL += ':' + sourceColumnNumber; + } + } else if (fileName && typeof lineNumber === 'number') { + prettyURL = fileName + ':' + lineNumber; + // Note: we intentionally skip 0's because they're produced by cheap Webpack maps + if (columnNumber) { + prettyURL += ':' + columnNumber; + } + } else { + prettyURL = 'unknown'; + } + return prettyURL.replace('webpack://', '.'); +} + +export { getPrettyURL }; +export default getPrettyURL; diff --git a/packages/react-error-overlay/src/utils/errorRegister.js b/packages/react-error-overlay/src/utils/getStackFrames.js similarity index 59% rename from packages/react-error-overlay/src/utils/errorRegister.js rename to packages/react-error-overlay/src/utils/getStackFrames.js index 0bd3379cef1..e5a4073106b 100644 --- a/packages/react-error-overlay/src/utils/errorRegister.js +++ b/packages/react-error-overlay/src/utils/getStackFrames.js @@ -13,22 +13,11 @@ import { parse } from './parser'; import { map } from './mapper'; import { unmap } from './unmapper'; -type ErrorRecord = { - error: Error, - unhandledRejection: boolean, - contextSize: number, - enhancedFrames: StackFrame[], -}; -type ErrorRecordReference = number; -const recorded: ErrorRecord[] = []; - -let errorsConsumed: ErrorRecordReference = 0; - -function consume( +function getStackFrames( error: Error, unhandledRejection: boolean = false, contextSize: number = 3 -): Promise { +): Promise { const parsedFrames = parse(error); let enhancedFramesPromise; if (error.__unmap_source) { @@ -49,32 +38,13 @@ function consume( ) { return null; } - enhancedFrames = enhancedFrames.filter( + return enhancedFrames.filter( ({ functionName }) => functionName == null || functionName.indexOf('__stack_frame_overlay_proxy_console__') === -1 ); - recorded[++errorsConsumed] = { - error, - unhandledRejection, - contextSize, - enhancedFrames, - }; - return errorsConsumed; }); } -function getErrorRecord(ref: ErrorRecordReference): ErrorRecord { - return recorded[ref]; -} - -function drain() { - // $FlowFixMe - const keys = Object.keys(recorded); - for (let index = 0; index < keys.length; ++index) { - delete recorded[keys[index]]; - } -} - -export { consume, getErrorRecord, drain }; -export type { ErrorRecordReference }; +export default getStackFrames; +export { getStackFrames }; diff --git a/packages/react-error-overlay/src/utils/isBultinErrorName.js b/packages/react-error-overlay/src/utils/isBultinErrorName.js new file mode 100644 index 00000000000..ee36709cd2b --- /dev/null +++ b/packages/react-error-overlay/src/utils/isBultinErrorName.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +//@flow +function isBultinErrorName(errorName: ?string) { + switch (errorName) { + case 'EvalError': + case 'InternalError': + case 'RangeError': + case 'ReferenceError': + case 'SyntaxError': + case 'TypeError': + case 'URIError': + return true; + default: + return false; + } +} + +export { isBultinErrorName }; +export default isBultinErrorName; From 822a5f68dd90cfdb0104dca10f40082efd9c8141 Mon Sep 17 00:00:00 2001 From: Tharaka Wijebandara Date: Sun, 9 Jul 2017 11:42:48 +0530 Subject: [PATCH 02/13] Update compile-time error overlay to use react-error-overlay components * Refactor react-error-overlay components to container and presentational components. * Make the compile-time error overlay a part of react-error-overlay package. * Use react-error-overlay as dependency in react-dev-utils to show compile-time errors. --- packages/react-dev-utils/package.json | 1 + .../react-dev-utils/webpackHotDevClient.js | 149 ++---------------- packages/react-error-overlay/.flowconfig | 1 + packages/react-error-overlay/package.json | 1 + .../src/compileErrorOverlay.js | 49 ++++++ .../src/components/CodeBlock.js | 82 +--------- .../src/components/Collapsible.js | 8 +- .../src/components/ErrorOverlay.js | 112 ------------- .../src/components/Footer.js | 11 +- .../src/components/Header.js | 22 +-- .../src/components/Overlay.js | 74 +++++++++ .../src/containers/CompileErrorContainer.js | 34 ++++ .../RuntimeError.js} | 24 ++- .../src/containers/RuntimeErrorContainer.js | 74 +++++++++ .../{components => containers}/StackFrame.js | 11 +- .../src/containers/StackFrameCodeBlock.js | 94 +++++++++++ .../{components => containers}/StackTrace.js | 16 +- packages/react-error-overlay/src/index.js | 4 +- .../{overlay.js => runtimeErrorOverlay.js} | 34 ++-- packages/react-error-overlay/src/styles.js | 2 +- .../src/utils/dom/mountOverlayIframe.js | 35 ++++ 21 files changed, 444 insertions(+), 394 deletions(-) create mode 100644 packages/react-error-overlay/src/compileErrorOverlay.js delete mode 100644 packages/react-error-overlay/src/components/ErrorOverlay.js create mode 100644 packages/react-error-overlay/src/components/Overlay.js create mode 100644 packages/react-error-overlay/src/containers/CompileErrorContainer.js rename packages/react-error-overlay/src/{components/ErrorView.js => containers/RuntimeError.js} (55%) create mode 100644 packages/react-error-overlay/src/containers/RuntimeErrorContainer.js rename packages/react-error-overlay/src/{components => containers}/StackFrame.js (95%) create mode 100644 packages/react-error-overlay/src/containers/StackFrameCodeBlock.js rename packages/react-error-overlay/src/{components => containers}/StackTrace.js (86%) rename packages/react-error-overlay/src/{overlay.js => runtimeErrorOverlay.js} (75%) create mode 100644 packages/react-error-overlay/src/utils/dom/mountOverlayIframe.js diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index e09e990d0b7..e15f9ec553b 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -47,6 +47,7 @@ "inquirer": "3.1.1", "is-root": "1.0.0", "opn": "5.1.0", + "react-error-overlay": "^1.0.9", "recursive-readdir": "2.2.1", "shell-quote": "1.6.1", "sockjs-client": "1.1.4", diff --git a/packages/react-dev-utils/webpackHotDevClient.js b/packages/react-dev-utils/webpackHotDevClient.js index 78002b28efb..5256f71912d 100644 --- a/packages/react-dev-utils/webpackHotDevClient.js +++ b/packages/react-dev-utils/webpackHotDevClient.js @@ -22,143 +22,10 @@ var SockJS = require('sockjs-client'); var stripAnsi = require('strip-ansi'); var url = require('url'); var formatWebpackMessages = require('./formatWebpackMessages'); -var Entities = require('html-entities').AllHtmlEntities; -var ansiHTML = require('./ansiHTML'); -var entities = new Entities(); - -function createOverlayIframe(onIframeLoad) { - var iframe = document.createElement('iframe'); - iframe.id = 'react-dev-utils-webpack-hot-dev-client-overlay'; - iframe.src = 'about:blank'; - iframe.style.position = 'fixed'; - iframe.style.left = 0; - iframe.style.top = 0; - iframe.style.right = 0; - iframe.style.bottom = 0; - iframe.style.width = '100vw'; - iframe.style.height = '100vh'; - iframe.style.border = 'none'; - iframe.style.zIndex = 2147483647; - iframe.onload = onIframeLoad; - return iframe; -} - -function addOverlayDivTo(iframe) { - // TODO: unify these styles with react-error-overlay - iframe.contentDocument.body.style.margin = 0; - iframe.contentDocument.body.style.maxWidth = '100vw'; - - var outerDiv = iframe.contentDocument.createElement('div'); - outerDiv.id = 'react-dev-utils-webpack-hot-dev-client-overlay-div'; - outerDiv.style.width = '100%'; - outerDiv.style.height = '100%'; - outerDiv.style.boxSizing = 'border-box'; - outerDiv.style.textAlign = 'center'; - outerDiv.style.backgroundColor = 'rgb(255, 255, 255)'; - - var div = iframe.contentDocument.createElement('div'); - div.style.position = 'relative'; - div.style.display = 'inline-flex'; - div.style.flexDirection = 'column'; - div.style.height = '100%'; - div.style.width = '1024px'; - div.style.maxWidth = '100%'; - div.style.overflowX = 'hidden'; - div.style.overflowY = 'auto'; - div.style.padding = '0.5rem'; - div.style.boxSizing = 'border-box'; - div.style.textAlign = 'left'; - div.style.fontFamily = 'Consolas, Menlo, monospace'; - div.style.fontSize = '11px'; - div.style.whiteSpace = 'pre-wrap'; - div.style.wordBreak = 'break-word'; - div.style.lineHeight = '1.5'; - div.style.color = 'rgb(41, 50, 56)'; - - outerDiv.appendChild(div); - iframe.contentDocument.body.appendChild(outerDiv); - return div; -} - -function overlayHeaderStyle() { - return ( - 'font-size: 2em;' + - 'font-family: sans-serif;' + - 'color: rgb(206, 17, 38);' + - 'white-space: pre-wrap;' + - 'margin: 0 2rem 0.75rem 0px;' + - 'flex: 0 0 auto;' + - 'max-height: 35%;' + - 'overflow: auto;' - ); -} - -var overlayIframe = null; -var overlayDiv = null; -var lastOnOverlayDivReady = null; - -function ensureOverlayDivExists(onOverlayDivReady) { - if (overlayDiv) { - // Everything is ready, call the callback right away. - onOverlayDivReady(overlayDiv); - return; - } - - // Creating an iframe may be asynchronous so we'll schedule the callback. - // In case of multiple calls, last callback wins. - lastOnOverlayDivReady = onOverlayDivReady; - - if (overlayIframe) { - // We're already creating it. - return; - } +var showCompileErrorOverlay = require('react-error-overlay') + .showCompileErrorOverlay; - // Create iframe and, when it is ready, a div inside it. - overlayIframe = createOverlayIframe(function onIframeLoad() { - overlayDiv = addOverlayDivTo(overlayIframe); - // Now we can talk! - lastOnOverlayDivReady(overlayDiv); - }); - - // Zalgo alert: onIframeLoad() will be called either synchronously - // or asynchronously depending on the browser. - // We delay adding it so `overlayIframe` is set when `onIframeLoad` fires. - document.body.appendChild(overlayIframe); -} - -function showErrorOverlay(message) { - ensureOverlayDivExists(function onOverlayDivReady(overlayDiv) { - // TODO: unify this with our runtime overlay - overlayDiv.innerHTML = - '
Failed to compile
' + - '
' +
-      '' +
-      ansiHTML(entities.encode(message)) +
-      '
' + - '
' + - 'This error occurred during the build time and cannot be dismissed.
'; - }); -} - -function destroyErrorOverlay() { - if (!overlayDiv) { - // It is not there in the first place. - return; - } - - // Clean up and reset internal state. - document.body.removeChild(overlayIframe); - overlayDiv = null; - overlayIframe = null; - lastOnOverlayDivReady = null; -} +var destroyOverlay = null; // Connect to WebpackDevServer via a socket. var connection = new SockJS( @@ -209,7 +76,9 @@ function handleSuccess() { tryApplyUpdates(function onHotUpdateSuccess() { // Only destroy it when we're sure it's a hot update. // Otherwise it would flicker right before the reload. - destroyErrorOverlay(); + if (destroyOverlay) { + destroyOverlay(); + } }); } } @@ -251,7 +120,9 @@ function handleWarnings(warnings) { printWarnings(); // Only destroy it when we're sure it's a hot update. // Otherwise it would flicker right before the reload. - destroyErrorOverlay(); + if (destroyOverlay) { + destroyOverlay(); + } }); } else { // Print initial warnings immediately. @@ -273,7 +144,7 @@ function handleErrors(errors) { }); // Only show the first error. - showErrorOverlay(formatted.errors[0]); + destroyOverlay = showCompileErrorOverlay(formatted.errors[0]); // Also log them to the console. if (typeof console !== 'undefined' && typeof console.error === 'function') { diff --git a/packages/react-error-overlay/.flowconfig b/packages/react-error-overlay/.flowconfig index 261b8646fc3..8d7de784e29 100644 --- a/packages/react-error-overlay/.flowconfig +++ b/packages/react-error-overlay/.flowconfig @@ -1,4 +1,5 @@ [ignore] +.*/node_modules/eslint-plugin-jsx-a11y/.* [include] src/**/*.js diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json index c9a7508a903..f9e3ed20ffd 100644 --- a/packages/react-error-overlay/package.json +++ b/packages/react-error-overlay/package.json @@ -34,6 +34,7 @@ "anser": "1.2.5", "babel-code-frame": "6.22.0", "babel-runtime": "6.23.0", + "html-entities": "^1.2.1", "react": "^15.5.4", "react-dev-utils": "^3.0.2", "react-dom": "^15.5.4", diff --git a/packages/react-error-overlay/src/compileErrorOverlay.js b/packages/react-error-overlay/src/compileErrorOverlay.js new file mode 100644 index 00000000000..a89b1dd9cfe --- /dev/null +++ b/packages/react-error-overlay/src/compileErrorOverlay.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import CompileErrorContainer from './containers/CompileErrorContainer'; +import { mountOverlayIframe } from './utils/dom/mountOverlayIframe'; + +let container: HTMLDivElement | null = null; +let iframeReference: HTMLIFrameElement | null = null; + +function mount(callback) { + iframeReference = mountOverlayIframe(containerDiv => { + container = containerDiv; + callback(); + }); +} + +function unmount() { + if (iframeReference === null) { + return; + } + ReactDOM.unmountComponentAtNode(container); + window.document.body.removeChild(iframeReference); + iframeReference = null; + container = null; +} + +function render(error: string) { + ReactDOM.render(, container); +} + +function showCompileErrorOverlay(error: string) { + if (container == null) { + mount(() => render(error)); + } else { + render(error); + } + return unmount; +} + +export { showCompileErrorOverlay }; diff --git a/packages/react-error-overlay/src/components/CodeBlock.js b/packages/react-error-overlay/src/components/CodeBlock.js index ce5c11dd212..2d50dc692ba 100644 --- a/packages/react-error-overlay/src/components/CodeBlock.js +++ b/packages/react-error-overlay/src/components/CodeBlock.js @@ -9,19 +9,7 @@ /* @flow */ import React from 'react'; -import { applyStyles } from '../utils/dom/css'; -import { absolutifyCaret } from '../utils/dom/absolutifyCaret'; -import type { ScriptLine } from '../utils/stack-frame'; -import { - primaryErrorStyle, - secondaryErrorStyle, - redTransparent, - yellowTransparent, -} from '../styles'; - -import generateAnsiHtml from 'react-dev-utils/ansiHTML'; - -import codeFrame from 'babel-code-frame'; +import { redTransparent, yellowTransparent } from '../styles'; const _preStyle = { display: 'block', @@ -48,76 +36,14 @@ const codeStyle = { }; type CodeBlockPropsType = { - lines: ScriptLine[], - lineNum: number, - columnNum: number, - contextSize: number, main: boolean, + codeHTML: string, }; function CodeBlock(props: CodeBlockPropsType) { - const { lines, lineNum, columnNum, contextSize, main } = props; - const sourceCode = []; - let whiteSpace = Infinity; - lines.forEach(function(e) { - const { content: text } = e; - const m = text.match(/^\s*/); - if (text === '') { - return; - } - if (m && m[0]) { - whiteSpace = Math.min(whiteSpace, m[0].length); - } else { - whiteSpace = 0; - } - }); - lines.forEach(function(e) { - let { content: text } = e; - const { lineNumber: line } = e; - - if (isFinite(whiteSpace)) { - text = text.substring(whiteSpace); - } - sourceCode[line - 1] = text; - }); - const ansiHighlight = codeFrame( - sourceCode.join('\n'), - lineNum, - columnNum == null ? 0 : columnNum - (isFinite(whiteSpace) ? whiteSpace : 0), - { - forceColor: true, - linesAbove: contextSize, - linesBelow: contextSize, - } - ); - const htmlHighlight = generateAnsiHtml(ansiHighlight); - const code = document.createElement('code'); - code.innerHTML = htmlHighlight; - absolutifyCaret(code); - - const ccn = code.childNodes; - // eslint-disable-next-line - oLoop: for (let index = 0; index < ccn.length; ++index) { - const node = ccn[index]; - const ccn2 = node.childNodes; - for (let index2 = 0; index2 < ccn2.length; ++index2) { - const lineNode = ccn2[index2]; - const text = lineNode.innerText; - if (text == null) { - continue; - } - if (text.indexOf(' ' + lineNum + ' |') === -1) { - continue; - } - // $FlowFixMe - applyStyles(node, main ? primaryErrorStyle : secondaryErrorStyle); - // eslint-disable-next-line - break oLoop; - } - } + const preStyle = props.main ? primaryPreStyle : secondaryPreStyle; + const codeBlock = { __html: props.codeHTML }; - const preStyle = main ? primaryPreStyle : secondaryPreStyle; - const codeBlock = { __html: code.innerHTML }; return (
       
diff --git a/packages/react-error-overlay/src/components/Collapsible.js b/packages/react-error-overlay/src/components/Collapsible.js
index 3d474d4e204..92f1de4295c 100644
--- a/packages/react-error-overlay/src/components/Collapsible.js
+++ b/packages/react-error-overlay/src/components/Collapsible.js
@@ -57,11 +57,9 @@ class Collapsible extends Component {
             collapsed ? collapsibleCollapsedStyle : collapsibleExpandedStyle
           }
         >
-          {
-            (collapsed ? '▶' : '▼') +
-              ` ${count} stack frames were ` +
-              (collapsed ? 'collapsed.' : 'expanded.')
-          }
+          {(collapsed ? '▶' : '▼') +
+            ` ${count} stack frames were ` +
+            (collapsed ? 'collapsed.' : 'expanded.')}
         
         
{this.props.children} diff --git a/packages/react-error-overlay/src/components/ErrorOverlay.js b/packages/react-error-overlay/src/components/ErrorOverlay.js deleted file mode 100644 index 670a4b10989..00000000000 --- a/packages/react-error-overlay/src/components/ErrorOverlay.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -/* @flow */ -import React, { PureComponent } from 'react'; -import CloseButton from './CloseButton'; -import NavigationBar from './NavigationBar'; -import ErrorView from './ErrorView'; -import Footer from './Footer'; -import { black } from '../styles'; - -const overlayStyle = { - position: 'relative', - display: 'inline-flex', - flexDirection: 'column', - height: '100%', - width: '1024px', - maxWidth: '100%', - overflowX: 'hidden', - overflowY: 'auto', - padding: '0.5rem', - boxSizing: 'border-box', - textAlign: 'left', - fontFamily: 'Consolas, Menlo, monospace', - fontSize: '11px', - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', - lineHeight: 1.5, - color: black, -}; - -class ErrorOverlay extends PureComponent { - state = { - currentIndex: 0, - }; - iframeWindow: window = null; - - previous = () => { - this.setState((state, props) => ({ - currentIndex: state.currentIndex > 0 - ? state.currentIndex - 1 - : props.errorRecords.length - 1, - })); - }; - - next = () => { - this.setState((state, props) => ({ - currentIndex: state.currentIndex < props.errorRecords.length - 1 - ? state.currentIndex + 1 - : 0, - })); - }; - - handleSortcuts = (e: KeyboardEvent) => { - const { key } = e; - if (key === 'Escape') { - this.props.close(); - } else if (key === 'ArrowLeft') { - this.previous(); - } else if (key === 'ArrowRight') { - this.next(); - } - }; - - getIframeWindow = (element: HTMLDivElement) => { - if (element) { - const document = element.ownerDocument; - this.iframeWindow = document.defaultView; - } - }; - - componentDidMount() { - window.addEventListener('keydown', this.handleSortcuts); - if (this.iframeWindow) { - this.iframeWindow.addEventListener('keydown', this.handleSortcuts); - } - } - - componentWillUnmount() { - window.removeEventListener('keydown', this.handleSortcuts); - if (this.iframeWindow) { - this.iframeWindow.removeEventListener('keydown', this.handleSortcuts); - } - } - - render() { - const { errorRecords, close } = this.props; - const totalErrors = errorRecords.length; - return ( -
- - {totalErrors > 1 && - } - -
-
- ); - } -} - -export default ErrorOverlay; diff --git a/packages/react-error-overlay/src/components/Footer.js b/packages/react-error-overlay/src/components/Footer.js index 132d8feb7e7..ab751c97d96 100644 --- a/packages/react-error-overlay/src/components/Footer.js +++ b/packages/react-error-overlay/src/components/Footer.js @@ -18,12 +18,17 @@ const footerStyle = { flex: '0 0 auto', }; -function Footer() { +type FooterPropsType = { + line1: string, + line2?: string, +}; + +function Footer(props: FooterPropsType) { return (
- This screen is visible only in development. It will not appear if the app crashes in production. + {props.line1}
- Open your browser’s developer console to further inspect this error. + {props.line2}
); } diff --git a/packages/react-error-overlay/src/components/Header.js b/packages/react-error-overlay/src/components/Header.js index 59999d4e451..dc0d686e873 100644 --- a/packages/react-error-overlay/src/components/Header.js +++ b/packages/react-error-overlay/src/components/Header.js @@ -24,26 +24,14 @@ const headerStyle = { overflow: 'auto', }; -function Header({ name, message }: { name: string, message: string }) { - // Make message prettier - let finalMessage = message.match(/^\w*:/) || !name - ? message - : name + ': ' + message; - - finalMessage = finalMessage - // TODO: maybe remove this prefix from fbjs? - // It's just scaring people - .replace(/^Invariant Violation:\s*/, '') - // This is not helpful either: - .replace(/^Warning:\s*/, '') - // Break the actionable part to the next line. - // AFAIK React 16+ should already do this. - .replace(' Check the render method', '\n\nCheck the render method') - .replace(' Check your code at', '\n\nCheck your code at'); +type HeaderPropType = { + headerText: string, +}; +function Header(props: HeaderPropType) { return (
- {finalMessage} + {props.headerText}
); } diff --git a/packages/react-error-overlay/src/components/Overlay.js b/packages/react-error-overlay/src/components/Overlay.js new file mode 100644 index 00000000000..4fe530b6fee --- /dev/null +++ b/packages/react-error-overlay/src/components/Overlay.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React, { Component } from 'react'; +import { black } from '../styles'; + +const overlayStyle = { + position: 'relative', + display: 'inline-flex', + flexDirection: 'column', + height: '100%', + width: '1024px', + maxWidth: '100%', + overflowX: 'hidden', + overflowY: 'auto', + padding: '0.5rem', + boxSizing: 'border-box', + textAlign: 'left', + fontFamily: 'Consolas, Menlo, monospace', + fontSize: '11px', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + lineHeight: 1.5, + color: black, +}; + +class Overlay extends Component { + iframeWindow: window = null; + + getIframeWindow = (element: HTMLDivElement) => { + if (element) { + const document = element.ownerDocument; + this.iframeWindow = document.defaultView; + } + }; + + onKeyDown = (e: KeyboardEvent) => { + const { shortcutHandler } = this.props; + if (shortcutHandler) { + shortcutHandler(e.key); + } + }; + + componentDidMount() { + window.addEventListener('keydown', this.onKeyDown); + if (this.iframeWindow) { + this.iframeWindow.addEventListener('keydown', this.onKeyDown); + } + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.onKeyDown); + if (this.iframeWindow) { + this.iframeWindow.removeEventListener('keydown', this.onKeyDown); + } + } + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +export default Overlay; diff --git a/packages/react-error-overlay/src/containers/CompileErrorContainer.js b/packages/react-error-overlay/src/containers/CompileErrorContainer.js new file mode 100644 index 00000000000..78303df3a0c --- /dev/null +++ b/packages/react-error-overlay/src/containers/CompileErrorContainer.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* @flow */ +import React, { PureComponent } from 'react'; +import Overlay from '../components/Overlay'; +import Footer from '../components/Footer'; +import Header from '../components/Header'; +import CodeBlock from '../components/CodeBlock'; +import { AllHtmlEntities as Entities } from 'html-entities'; +import ansiHTML from 'react-dev-utils/ansiHTML'; + +const entities = new Entities(); + +class CompileErrorContainer extends PureComponent { + render() { + const { error } = this.props; + return ( + +
+ +