From 822a5f68dd90cfdb0104dca10f40082efd9c8141 Mon Sep 17 00:00:00 2001 From: Tharaka Wijebandara Date: Sun, 9 Jul 2017 11:42:48 +0530 Subject: [PATCH] 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 ( + +
+ +