diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index e09e990d0b..e15f9ec553 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 78002b28ef..5256f71912 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 = - '
' +
- '' +
- ansiHTML(entities.encode(message)) +
- '
' +
- 'diff --git a/packages/react-error-overlay/src/components/Collapsible.js b/packages/react-error-overlay/src/components/Collapsible.js index 3d474d4e20..92f1de4295 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 670a4b1098..0000000000 --- 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 ( --- ); - } -} - -export default ErrorOverlay; diff --git a/packages/react-error-overlay/src/components/Footer.js b/packages/react-error-overlay/src/components/Footer.js index 132d8feb7e..ab751c97d9 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 (- {totalErrors > 1 && - } - - - - This screen is visible only in development. It will not appear if the app crashes in production. + {props.line1}); } diff --git a/packages/react-error-overlay/src/components/Header.js b/packages/react-error-overlay/src/components/Header.js index 59999d4e45..dc0d686e87 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 (
- Open your browser’s developer console to further inspect this error. + {props.line2}- {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 0000000000..4fe530b6fe --- /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 0000000000..78303df3a0 --- /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 ( ++ + ); + } +} + +export default CompileErrorContainer; diff --git a/packages/react-error-overlay/src/components/ErrorView.js b/packages/react-error-overlay/src/containers/RuntimeError.js similarity index 55% rename from packages/react-error-overlay/src/components/ErrorView.js rename to packages/react-error-overlay/src/containers/RuntimeError.js index 84726b2816..5cbc77a2ad 100644 --- a/packages/react-error-overlay/src/components/ErrorView.js +++ b/packages/react-error-overlay/src/containers/RuntimeError.js @@ -9,7 +9,7 @@ /* @flow */ import React from 'react'; -import Header from './Header'; +import Header from '../components/Header'; import StackTrace from './StackTrace'; import type { StackFrame } from '../utils/stack-frame'; @@ -25,15 +25,31 @@ type ErrorRecord = { stackFrames: StackFrame[], }; -function ErrorView({ errorRecord }: { errorRecord: ErrorRecord }) { +function RuntimeError({ errorRecord }: { errorRecord: ErrorRecord }) { const { error, unhandledRejection, contextSize, stackFrames } = errorRecord; const errorName = unhandledRejection ? 'Unhandled Rejection (' + error.name + ')' : error.name; + // Make header prettier + const message = error.message; + let headerText = + message.match(/^\w*:/) || !errorName ? message : errorName + ': ' + message; + + headerText = headerText + // 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 (+ + + -+ { + 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, + })); + }; + + shortcutHandler = (key: string) => { + if (key === 'Escape') { + this.props.close(); + } else if (key === 'ArrowLeft') { + this.previous(); + } else if (key === 'ArrowRight') { + this.next(); + } + }; + + render() { + const { errorRecords, close } = this.props; + const totalErrors = errorRecords.length; + return ( + + + ); + } +} + +export default RuntimeErrorContainer; diff --git a/packages/react-error-overlay/src/components/StackFrame.js b/packages/react-error-overlay/src/containers/StackFrame.js similarity index 95% rename from packages/react-error-overlay/src/components/StackFrame.js rename to packages/react-error-overlay/src/containers/StackFrame.js index 3b45b5a9b2..6c32c596e6 100644 --- a/packages/react-error-overlay/src/components/StackFrame.js +++ b/packages/react-error-overlay/src/containers/StackFrame.js @@ -9,7 +9,7 @@ /* @flow */ import React, { Component } from 'react'; -import CodeBlock from './CodeBlock'; +import CodeBlock from './StackFrameCodeBlock'; import { getPrettyURL } from '../utils/getPrettyURL'; import { darkGray } from '../styles'; @@ -57,9 +57,8 @@ class StackFrame extends Component { } = this.props.frame; if (sourceFileName) { // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1" - const isInternalWebpackBootstrapCode = sourceFileName - .trim() - .indexOf(' ') !== -1; + const isInternalWebpackBootstrapCode = + sourceFileName.trim().indexOf(' ') !== -1; if (!isInternalWebpackBootstrapCode) { // Keep this in sync with react-error-overlay/middleware.js fetch( @@ -150,7 +149,9 @@ class StackFrame extends Component { return (+ {totalErrors > 1 && + } + + + -{functionName}++ {functionName} +; +} + +export default StackFrameCodeBlock; diff --git a/packages/react-error-overlay/src/components/StackTrace.js b/packages/react-error-overlay/src/containers/StackTrace.js similarity index 86% rename from packages/react-error-overlay/src/components/StackTrace.js rename to packages/react-error-overlay/src/containers/StackTrace.js index 0a7b5b7e18..c5f80ea40f 100644 --- a/packages/react-error-overlay/src/components/StackTrace.js +++ b/packages/react-error-overlay/src/containers/StackTrace.js @@ -10,7 +10,7 @@ /* @flow */ import React, { Component } from 'react'; import StackFrame from './StackFrame'; -import Collapsible from './Collapsible'; +import Collapsible from '../components/Collapsible'; import { isInternalFile } from '../utils/isInternalFile'; import { isBultinErrorName } from '../utils/isBultinErrorName'; @@ -25,14 +25,16 @@ class StackTrace extends Component { renderFrames() { const { stackFrames, errorName, contextSize } = this.props; const renderedFrames = []; - let hasReachedAppCode = false, currentBundle = [], bundleCount = 0; + 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); + const shouldCollapse = + isInternalUrl && (isThrownIntentionally || hasReachedAppCode); if (!isInternalUrl) { hasReachedAppCode = true; @@ -76,7 +78,11 @@ class StackTrace extends Component { } render() { - return{this.renderFrames()}; + return ( ++ {this.renderFrames()} ++ ); } } diff --git a/packages/react-error-overlay/src/index.js b/packages/react-error-overlay/src/index.js index 4f3b231672..ccbaed27bf 100644 --- a/packages/react-error-overlay/src/index.js +++ b/packages/react-error-overlay/src/index.js @@ -8,7 +8,7 @@ */ /* @flow */ -import { inject, uninject } from './overlay'; +import { inject, uninject } from './runtimeErrorOverlay'; inject(); if (module.hot && typeof module.hot.dispose === 'function') { @@ -16,3 +16,5 @@ if (module.hot && typeof module.hot.dispose === 'function') { uninject(); }); } + +export { showCompileErrorOverlay } from './compileErrorOverlay'; diff --git a/packages/react-error-overlay/src/overlay.js b/packages/react-error-overlay/src/runtimeErrorOverlay.js similarity index 75% rename from packages/react-error-overlay/src/overlay.js rename to packages/react-error-overlay/src/runtimeErrorOverlay.js index 883d5604ba..74b7f33072 100644 --- a/packages/react-error-overlay/src/overlay.js +++ b/packages/react-error-overlay/src/runtimeErrorOverlay.js @@ -28,10 +28,9 @@ import { unregisterReactStack, } from './effects/proxyConsole'; import { massage as massageWarning } from './utils/warnings'; -import { iframeStyle, overlayStyle } from './styles'; -import { applyStyles } from './utils/dom/css'; +import { mountOverlayIframe } from './utils/dom/mountOverlayIframe'; import getStackFrames from './utils/getStackFrames'; -import ErrorOverlay from './components/ErrorOverlay'; +import RuntimeErrorContainer from './containers/RuntimeErrorContainer'; const CONTEXT_SIZE: number = 3; @@ -42,33 +41,20 @@ 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 document = iframeReference.contentDocument; - 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'; - container = document.createElement('div'); - applyStyles(container, overlayStyle); - (document.body: any).appendChild(container); - callback(); - } - }; - window.document.body.appendChild(iframeReference); + iframeReference = mountOverlayIframe(containerDiv => { + container = containerDiv; + callback(); + }); + // below the compile error overlay + iframeReference.style.zIndex = String(2147483647 - 1); } // Unmount overlay container function unmount() { - ReactDOM.unmountComponentAtNode(container); if (iframeReference === null) { return; } + ReactDOM.unmountComponentAtNode(container); window.document.body.removeChild(iframeReference); iframeReference = null; container = null; @@ -77,7 +63,7 @@ function unmount() { function render() { ReactDOM.render( -, + , container ); } diff --git a/packages/react-error-overlay/src/styles.js b/packages/react-error-overlay/src/styles.js index 3b16a90bcb..d6557c5d95 100644 --- a/packages/react-error-overlay/src/styles.js +++ b/packages/react-error-overlay/src/styles.js @@ -24,7 +24,7 @@ const iframeStyle = { width: '100%', height: '100%', border: 'none', - 'z-index': 2147483647 - 1, // below the compile error overlay + 'z-index': 2147483647, }; const overlayStyle = { diff --git a/packages/react-error-overlay/src/utils/dom/mountOverlayIframe.js b/packages/react-error-overlay/src/utils/dom/mountOverlayIframe.js new file mode 100644 index 0000000000..22338bac0e --- /dev/null +++ b/packages/react-error-overlay/src/utils/dom/mountOverlayIframe.js @@ -0,0 +1,35 @@ +/** + * 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 { iframeStyle, overlayStyle } from '../../styles'; +import { applyStyles } from './css'; + +function mountOverlayIframe( + callback: (containerDiv: HTMLDivElement) => void +): HTMLIFrameElement { + const iframe = window.document.createElement('iframe'); + applyStyles(iframe, iframeStyle); + iframe.onload = () => { + const document = iframe.contentDocument; + if (document != null && document.body != null) { + document.body.style.margin = '0'; + // Keep popup within body boundaries for iOS Safari + document.body.style['max-width'] = '100vw'; + const container = document.createElement('div'); + applyStyles(container, overlayStyle); + (document.body: any).appendChild(container); + callback(container); + } + }; + window.document.body.appendChild(iframe); + return iframe; +} + +export { mountOverlayIframe };