diff --git a/packages/react-error-overlay/middleware.js b/packages/react-dev-utils/errorOverlayMiddleware.js similarity index 76% rename from packages/react-error-overlay/middleware.js rename to packages/react-dev-utils/errorOverlayMiddleware.js index d4fd0d399f1..b2a857d6e37 100644 --- a/packages/react-error-overlay/middleware.js +++ b/packages/react-dev-utils/errorOverlayMiddleware.js @@ -8,12 +8,12 @@ */ 'use strict'; -const launchEditor = require('react-dev-utils/launchEditor'); +const launchEditor = require('./launchEditor'); +const launchEditorEndpoint = require('./launchEditorEndpoint'); module.exports = function createLaunchEditorMiddleware() { return function launchEditorMiddleware(req, res, next) { - // Keep this in sync with react-error-overlay - if (req.url.startsWith('/__open-stack-frame-in-editor')) { + if (req.url.startsWith(launchEditorEndpoint)) { launchEditor(req.query.fileName, req.query.lineNumber); res.end(); } else { diff --git a/packages/react-error-overlay/src/utils/dom/consumeEvent.js b/packages/react-dev-utils/launchEditorEndpoint.js similarity index 65% rename from packages/react-error-overlay/src/utils/dom/consumeEvent.js rename to packages/react-dev-utils/launchEditorEndpoint.js index 90bb9d1e815..e21870be9c4 100644 --- a/packages/react-error-overlay/src/utils/dom/consumeEvent.js +++ b/packages/react-dev-utils/launchEditorEndpoint.js @@ -6,13 +6,7 @@ * 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. */ +'use strict'; -/* @flow */ -function consumeEvent(e: Event) { - e.preventDefault(); - if (typeof e.target.blur === 'function') { - e.target.blur(); - } -} - -export { consumeEvent }; +// TODO: we might want to make this injectable to support DEV-time non-root URLs. +module.exports = '/__open-stack-frame-in-editor'; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 8816e4009a6..ccc22bf6332 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -11,12 +11,12 @@ "node": ">=6" }, "files": [ - "ansiHTML.js", "checkRequiredFiles.js", "clearConsole.js", "crashOverlay.js", "crossSpawn.js", "eslintFormatter.js", + "errorOverlayMiddleware.js", "FileSizeReporter.js", "printBuildError.js", "formatWebpackMessages.js", @@ -24,6 +24,7 @@ "inquirer.js", "InterpolateHtmlPlugin.js", "launchEditor.js", + "launchEditorEndpoint.js", "ModuleScopePlugin.js", "noopServiceWorkerMiddleware.js", "openBrowser.js", @@ -35,7 +36,6 @@ ], "dependencies": { "address": "1.0.2", - "anser": "1.4.1", "babel-code-frame": "6.22.0", "chalk": "1.1.3", "cross-spawn": "5.1.0", @@ -44,10 +44,10 @@ "filesize": "3.5.10", "global-modules": "1.0.0", "gzip-size": "3.0.0", - "html-entities": "1.2.1", "inquirer": "3.2.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 f2f206a5cd2..b6effe718e5 100644 --- a/packages/react-dev-utils/webpackHotDevClient.js +++ b/packages/react-dev-utils/webpackHotDevClient.js @@ -21,143 +21,27 @@ var SockJS = require('sockjs-client'); var stripAnsi = require('strip-ansi'); var url = require('url'); +var launchEditorEndpoint = require('./launchEditorEndpoint'); var formatWebpackMessages = require('./formatWebpackMessages'); -var ansiHTML = require('./ansiHTML'); - -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; - } - - // 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); -} +var ErrorOverlay = require('react-error-overlay'); + +ErrorOverlay.startReportingRuntimeErrors({ + launchEditorEndpoint: launchEditorEndpoint, + onError: function() { + // TODO: why do we need this? + if (module.hot && typeof module.hot.decline === 'function') { + module.hot.decline(); + } + }, +}); -function showErrorOverlay(message) { - ensureOverlayDivExists(function onOverlayDivReady(overlayDiv) { - // TODO: unify this with our runtime overlay - overlayDiv.innerHTML = - '<div style="' + - overlayHeaderStyle() + - '">Failed to compile</div>' + - '<pre style="' + - 'display: block; padding: 0.5em; margin-top: 0; ' + - 'margin-bottom: 0.5em; overflow-x: auto; white-space: pre-wrap; ' + - 'border-radius: 0.25rem; background-color: rgba(206, 17, 38, 0.05)">' + - '<code style="font-family: Consolas, Menlo, monospace;">' + - ansiHTML(message) + - '</code></pre>' + - '<div style="' + - 'font-family: sans-serif; color: rgb(135, 142, 145); margin-top: 0.5rem; ' + - 'flex: 0 0 auto">' + - 'This error occurred during the build time and cannot be dismissed.</div>'; +if (module.hot && typeof module.hot.dispose === 'function') { + module.hot.dispose(function() { + // TODO: why do we need this? + ErrorOverlay.stopReportingRuntimeErrors(); }); } -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; -} - // Connect to WebpackDevServer via a socket. var connection = new SockJS( url.format({ @@ -205,9 +89,9 @@ function handleSuccess() { // Attempt to apply hot updates or reload. if (isHotUpdate) { tryApplyUpdates(function onHotUpdateSuccess() { - // Only destroy it when we're sure it's a hot update. + // Only dismiss it when we're sure it's a hot update. // Otherwise it would flicker right before the reload. - destroyErrorOverlay(); + ErrorOverlay.dismissBuildError(); }); } } @@ -247,9 +131,9 @@ function handleWarnings(warnings) { // Only print warnings if we aren't refreshing the page. // Otherwise they'll disappear right away anyway. printWarnings(); - // Only destroy it when we're sure it's a hot update. + // Only dismiss it when we're sure it's a hot update. // Otherwise it would flicker right before the reload. - destroyErrorOverlay(); + ErrorOverlay.dismissBuildError(); }); } else { // Print initial warnings immediately. @@ -271,7 +155,7 @@ function handleErrors(errors) { }); // Only show the first error. - showErrorOverlay(formatted.errors[0]); + ErrorOverlay.reportBuildError(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/fixtures/bundle.json b/packages/react-error-overlay/fixtures/bundle.json index 7dfd31f5863..16670f6231f 100644 --- a/packages/react-error-overlay/fixtures/bundle.json +++ b/packages/react-error-overlay/fixtures/bundle.json @@ -240,11 +240,11 @@ ] }, { - "functionName": "Object.batchedUpdates", + "functionName": "batchedUpdates", "fileName": "http://localhost:3000/static/js/bundle.js", "lineNumber": 33900, "columnNumber": 26, - "_originalFunctionName": "Object.batchedUpdates", + "_originalFunctionName": "batchedUpdates", "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactDefaultBatchingStrategy.js", "_originalLineNumber": 62, "_originalColumnNumber": 0, @@ -264,11 +264,11 @@ ] }, { - "functionName": "Object.batchedUpdates", + "functionName": "batchedUpdates", "fileName": "http://localhost:3000/static/js/bundle.js", "lineNumber": 2181, "columnNumber": 27, - "_originalFunctionName": "Object.batchedUpdates", + "_originalFunctionName": "batchedUpdates", "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactUpdates.js", "_originalLineNumber": 97, "_originalColumnNumber": 0, @@ -288,11 +288,11 @@ ] }, { - "functionName": "Object._renderNewRootComponent", + "functionName": "_renderNewRootComponent", "fileName": "http://localhost:3000/static/js/bundle.js", "lineNumber": 14398, "columnNumber": 18, - "_originalFunctionName": "Object._renderNewRootComponent", + "_originalFunctionName": "_renderNewRootComponent", "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactMount.js", "_originalLineNumber": 320, "_originalColumnNumber": 0, @@ -312,11 +312,11 @@ ] }, { - "functionName": "Object._renderSubtreeIntoContainer", + "functionName": "_renderSubtreeIntoContainer", "fileName": "http://localhost:3000/static/js/bundle.js", "lineNumber": 14479, "columnNumber": 32, - "_originalFunctionName": "Object._renderSubtreeIntoContainer", + "_originalFunctionName": "_renderSubtreeIntoContainer", "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactMount.js", "_originalLineNumber": 401, "_originalColumnNumber": 0, @@ -336,11 +336,11 @@ ] }, { - "functionName": "Object.render", + "functionName": "render", "fileName": "http://localhost:3000/static/js/bundle.js", "lineNumber": 14500, "columnNumber": 23, - "_originalFunctionName": "Object.render", + "_originalFunctionName": "render", "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactMount.js", "_originalLineNumber": 422, "_originalColumnNumber": 0, @@ -360,11 +360,11 @@ ] }, { - "functionName": "Object.friendlySyntaxErrorLabel", + "functionName": null, "fileName": "http://localhost:3000/static/js/bundle.js", "lineNumber": 17287, "columnNumber": 20, - "_originalFunctionName": "Object.friendlySyntaxErrorLabel", + "_originalFunctionName": null, "_originalFileName": "webpack:///packages/react-scripts/template/src/index.js", "_originalLineNumber": 6, "_originalColumnNumber": 0, @@ -432,11 +432,11 @@ ] }, { - "functionName": "Object.<anonymous>", + "functionName": null, "fileName": "http://localhost:3000/static/js/bundle.js", "lineNumber": 41219, "columnNumber": 18, - "_originalFunctionName": "Object.<anonymous>", + "_originalFunctionName": null, "_originalFileName": null, "_originalLineNumber": null, "_originalColumnNumber": null, diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json index 5a9e864c50f..65c9b5efa0a 100644 --- a/packages/react-error-overlay/package.json +++ b/packages/react-error-overlay/package.json @@ -34,7 +34,9 @@ "anser": "1.4.1", "babel-code-frame": "6.22.0", "babel-runtime": "6.23.0", - "react-dev-utils": "^3.1.0", + "html-entities": "1.2.1", + "react": "^15.5.4", + "react-dom": "^15.5.4", "settle-promise": "1.0.0", "source-map": "0.5.6" }, diff --git a/packages/react-error-overlay/src/__tests__/stack-frame.js b/packages/react-error-overlay/src/__tests__/stack-frame.js index dc6a01b4a06..5a015260ab2 100644 --- a/packages/react-error-overlay/src/__tests__/stack-frame.js +++ b/packages/react-error-overlay/src/__tests__/stack-frame.js @@ -13,9 +13,9 @@ test('proper empty shape', () => { const empty = new StackFrame(); expect(empty).toMatchSnapshot(); - expect(empty.getFunctionName()).toBe(null); + expect(empty.getFunctionName()).toBe('(anonymous function)'); expect(empty.getSource()).toBe(''); - expect(empty.toString()).toBe(''); + expect(empty.toString()).toBe('(anonymous function)'); }); test('proper full shape', () => { 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..503b1198c3f --- /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 ( + <span + title="Click or press Escape to dismiss." + onClick={close} + style={closeButtonStyle} + > + × + </span> + ); +} + +export default CloseButton; diff --git a/packages/react-error-overlay/src/components/CodeBlock.js b/packages/react-error-overlay/src/components/CodeBlock.js new file mode 100644 index 00000000000..478f0111b9b --- /dev/null +++ b/packages/react-error-overlay/src/components/CodeBlock.js @@ -0,0 +1,54 @@ +/** + * 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 { redTransparent, yellowTransparent } from '../styles'; + +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 = {| + main: boolean, + codeHTML: string, +|}; + +function CodeBlock(props: CodeBlockPropsType) { + const preStyle = props.main ? primaryPreStyle : secondaryPreStyle; + const codeBlock = { __html: props.codeHTML }; + + return ( + <pre style={preStyle}> + <code style={codeStyle} dangerouslySetInnerHTML={codeBlock} /> + </pre> + ); +} + +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..92f1de4295c --- /dev/null +++ b/packages/react-error-overlay/src/components/Collapsible.js @@ -0,0 +1,78 @@ +/** + * 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 ( + <div> + <button + onClick={this.toggleCollaped} + style={ + collapsed ? collapsibleCollapsedStyle : collapsibleExpandedStyle + } + > + {(collapsed ? '▶' : '▼') + + ` ${count} stack frames were ` + + (collapsed ? 'collapsed.' : 'expanded.')} + </button> + <div style={{ display: collapsed ? 'none' : 'block' }}> + {this.props.children} + <button + onClick={this.toggleCollaped} + style={collapsibleExpandedStyle} + > + {`▲ ${count} stack frames were expanded.`} + </button> + </div> + </div> + ); + } +} + +export default Collapsible; 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..68eb8465674 --- /dev/null +++ b/packages/react-error-overlay/src/components/Footer.js @@ -0,0 +1,36 @@ +/** + * 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', +}; + +type FooterPropsType = {| + line1: string, + line2?: string, +|}; + +function Footer(props: FooterPropsType) { + return ( + <div style={footerStyle}> + {props.line1} + <br /> + {props.line2} + </div> + ); +} + +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..a2f40973d00 --- /dev/null +++ b/packages/react-error-overlay/src/components/Header.js @@ -0,0 +1,39 @@ +/** + * 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', +}; + +type HeaderPropType = {| + headerText: string, +|}; + +function Header(props: HeaderPropType) { + return ( + <div style={headerStyle}> + {props.headerText} + </div> + ); +} + +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..4eba743cef7 --- /dev/null +++ b/packages/react-error-overlay/src/components/NavigationBar.js @@ -0,0 +1,70 @@ +/** + * 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 ( + <div style={navigationBarStyle}> + <span style={buttonContainerStyle}> + <button onClick={previous} style={leftButtonStyle}> + ← + </button> + <button onClick={next} style={rightButtonStyle}> + → + </button> + </span> + {`${currentError} of ${totalErrors} errors on the page`} + </div> + ); +} + +export default NavigationBar; 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 ( + <div style={overlayStyle} ref={this.getIframeWindow}> + {this.props.children} + </div> + ); + } +} + +export default Overlay; 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 4087f4d9c9d..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'); - - if (functionName && functionName.indexOf('Object.') === 0) { - functionName = functionName.slice('Object.'.length); - } - if (functionName === '<anonymous>') { - functionName = null; - } - const cleanedFunctionName = functionName || '(anonymous function)'; - 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/containers/CompileErrorContainer.js b/packages/react-error-overlay/src/containers/CompileErrorContainer.js new file mode 100644 index 00000000000..bd193eb50b4 --- /dev/null +++ b/packages/react-error-overlay/src/containers/CompileErrorContainer.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, { PureComponent } from 'react'; +import Overlay from '../components/Overlay'; +import Footer from '../components/Footer'; +import Header from '../components/Header'; +import CodeBlock from '../components/CodeBlock'; +import generateAnsiHTML from '../utils/generateAnsiHTML'; + +class CompileErrorContainer extends PureComponent { + render() { + const { error } = this.props; + return ( + <Overlay> + <Header headerText="Failed to compile" /> + <CodeBlock main={true} codeHTML={generateAnsiHTML(error)} /> + <Footer line1="This error occurred during the build time and cannot be dismissed." /> + </Overlay> + ); + } +} + +export default CompileErrorContainer; diff --git a/packages/react-error-overlay/src/containers/RuntimeError.js b/packages/react-error-overlay/src/containers/RuntimeError.js new file mode 100644 index 00000000000..c64824137d2 --- /dev/null +++ b/packages/react-error-overlay/src/containers/RuntimeError.js @@ -0,0 +1,68 @@ +/** + * 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 '../components/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[], +|}; + +type Props = {| + errorRecord: ErrorRecord, + launchEditorEndpoint: ?string, +|}; + +function RuntimeError({ errorRecord, launchEditorEndpoint }: Props) { + 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 ( + <div style={wrapperStyle}> + <Header headerText={headerText} /> + <StackTrace + stackFrames={stackFrames} + errorName={errorName} + contextSize={contextSize} + launchEditorEndpoint={launchEditorEndpoint} + /> + </div> + ); +} + +export default RuntimeError; diff --git a/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js b/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js new file mode 100644 index 00000000000..c84adb19492 --- /dev/null +++ b/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js @@ -0,0 +1,77 @@ +/** + * 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 CloseButton from '../components/CloseButton'; +import NavigationBar from '../components/NavigationBar'; +import RuntimeError from './RuntimeError'; +import Footer from '../components/Footer'; + +class RuntimeErrorContainer extends PureComponent { + state = { + currentIndex: 0, + }; + + 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, + })); + }; + + 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 ( + <Overlay shortcutHandler={this.shortcutHandler}> + <CloseButton close={close} /> + {totalErrors > 1 && + <NavigationBar + currentError={this.state.currentIndex + 1} + totalErrors={totalErrors} + previous={this.previous} + next={this.next} + />} + <RuntimeError + errorRecord={errorRecords[this.state.currentIndex]} + launchEditorEndpoint={this.props.launchEditorEndpoint} + /> + <Footer + line1="This screen is visible only in development. It will not appear if the app crashes in production." + line2="Open your browser’s developer console to further inspect this error." + /> + </Overlay> + ); + } +} + +export default RuntimeErrorContainer; diff --git a/packages/react-error-overlay/src/containers/StackFrame.js b/packages/react-error-overlay/src/containers/StackFrame.js new file mode 100644 index 00000000000..c95ce003f49 --- /dev/null +++ b/packages/react-error-overlay/src/containers/StackFrame.js @@ -0,0 +1,188 @@ +/** + * 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 './StackFrameCodeBlock'; +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 codeAnchorStyle = { + 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, + })); + }; + + canOpenInEditor() { + if (!this.props.launchEditorEndpoint) { + return; + } + const { _originalFileName: sourceFileName } = this.props.frame; + // Unknown file + if (!sourceFileName) { + return false; + } + // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1" + const isInternalWebpackBootstrapCode = + sourceFileName.trim().indexOf(' ') !== -1; + if (isInternalWebpackBootstrapCode) { + return false; + } + // Code is in a real file + return true; + } + + openInEditor = () => { + if (!this.canOpenInEditor()) { + return; + } + const { + _originalFileName: sourceFileName, + _originalLineNumber: sourceLineNumber, + } = this.props.frame; + // Keep this in sync with react-error-overlay/middleware.js + fetch( + `${this.props.launchEditorEndpoint}?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; + const { + fileName, + lineNumber, + columnNumber, + _scriptCode: scriptLines, + _originalFileName: sourceFileName, + _originalLineNumber: sourceLineNumber, + _originalColumnNumber: sourceColumnNumber, + _originalScriptCode: sourceLines, + } = frame; + const functionName = frame.getFunctionName(); + + 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, + }; + } + } + + const canOpenInEditor = this.canOpenInEditor(); + return ( + <div> + <div> + {functionName} + </div> + <div style={linkStyle}> + <a + style={canOpenInEditor ? anchorStyle : null} + onClick={canOpenInEditor ? this.openInEditor : null} + onKeyDown={canOpenInEditor ? this.onKeyDown : null} + tabIndex={canOpenInEditor ? '0' : null} + > + {url} + </a> + </div> + {codeBlockProps && + <span> + <a + onClick={canOpenInEditor ? this.openInEditor : null} + style={canOpenInEditor ? codeAnchorStyle : null} + > + <CodeBlock {...codeBlockProps} /> + </a> + <button style={toggleStyle} onClick={this.toggleCompiled}> + {'View ' + (compiled ? 'source' : 'compiled')} + </button> + </span>} + </div> + ); + } +} + +export default StackFrame; diff --git a/packages/react-error-overlay/src/components/code.js b/packages/react-error-overlay/src/containers/StackFrameCodeBlock.js similarity index 71% rename from packages/react-error-overlay/src/components/code.js rename to packages/react-error-overlay/src/containers/StackFrameCodeBlock.js index 580fe3b1be5..2ed685cff49 100644 --- a/packages/react-error-overlay/src/components/code.js +++ b/packages/react-error-overlay/src/containers/StackFrameCodeBlock.js @@ -8,33 +8,29 @@ */ /* @flow */ -import type { ScriptLine } from '../utils/stack-frame'; +import React from 'react'; +import CodeBlock from '../components/CodeBlock'; import { applyStyles } from '../utils/dom/css'; import { absolutifyCaret } from '../utils/dom/absolutifyCaret'; -import { - codeStyle, - primaryErrorStyle, - primaryPreStyle, - secondaryErrorStyle, - secondaryPreStyle, -} from '../styles'; - -import generateAnsiHtml from 'react-dev-utils/ansiHTML'; +import type { ScriptLine } from '../utils/stack-frame'; +import { primaryErrorStyle, secondaryErrorStyle } from '../styles'; +import generateAnsiHTML from '../utils/generateAnsiHTML'; import codeFrame from 'babel-code-frame'; -function createCode( - document: Document, - sourceLines: ScriptLine[], +type StackFrameCodeBlockPropsType = {| + lines: ScriptLine[], lineNum: number, - columnNum: number | null, + columnNum: number, contextSize: number, main: boolean, - onSourceClick: ?Function -) { +|}; + +function StackFrameCodeBlock(props: StackFrameCodeBlockPropsType) { + 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 +42,7 @@ function createCode( whiteSpace = 0; } }); - sourceLines.forEach(function(e) { + lines.forEach(function(e) { let { content: text } = e; const { lineNumber: line } = e; @@ -65,11 +61,10 @@ function createCode( linesBelow: contextSize, } ); - const htmlHighlight = generateAnsiHtml(ansiHighlight); + const htmlHighlight = generateAnsiHTML(ansiHighlight); const code = document.createElement('code'); code.innerHTML = htmlHighlight; absolutifyCaret(code); - applyStyles(code, codeStyle); const ccn = code.childNodes; // eslint-disable-next-line @@ -91,19 +86,8 @@ 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; + return <CodeBlock main={main} codeHTML={code.innerHTML} />; } -export { createCode }; +export default StackFrameCodeBlock; diff --git a/packages/react-error-overlay/src/containers/StackTrace.js b/packages/react-error-overlay/src/containers/StackTrace.js new file mode 100644 index 00000000000..4cb20bce128 --- /dev/null +++ b/packages/react-error-overlay/src/containers/StackTrace.js @@ -0,0 +1,95 @@ +/** + * 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 '../components/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, + launchEditorEndpoint, + } = 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 = ( + <StackFrame + key={'frame-' + index} + frame={frame} + contextSize={contextSize} + critical={index === 0} + showCode={!shouldCollapse} + launchEditorEndpoint={launchEditorEndpoint} + /> + ); + 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( + <Collapsible key={'bundle-' + bundleCount}> + {currentBundle} + </Collapsible> + ); + } + currentBundle = []; + } + + if (!shouldCollapse) { + renderedFrames.push(frameEle); + } + }); + + return renderedFrames; + } + + render() { + return ( + <div style={traceStyle}> + {this.renderFrames()} + </div> + ); + } +} + +export default StackTrace; 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/index.js b/packages/react-error-overlay/src/index.js index 4f3b2316727..ff4f1c71404 100644 --- a/packages/react-error-overlay/src/index.js +++ b/packages/react-error-overlay/src/index.js @@ -8,11 +8,158 @@ */ /* @flow */ -import { inject, uninject } from './overlay'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import CompileErrorContainer from './containers/CompileErrorContainer'; +import RuntimeErrorContainer from './containers/RuntimeErrorContainer'; +import { listenToRuntimeErrors } from './listenToRuntimeErrors'; +import { iframeStyle, overlayStyle } from './styles'; +import { applyStyles } from './utils/dom/css'; -inject(); -if (module.hot && typeof module.hot.dispose === 'function') { - module.hot.dispose(function() { - uninject(); +import type { ErrorRecord } from './listenToRuntimeErrors'; + +type RuntimeReportingOptions = {| + onError: () => void, + launchEditorEndpoint: string, +|}; + +let iframe: null | HTMLIFrameElement = null; +let isLoadingIframe: boolean = false; + +let renderedElement: null | React.Element<any> = null; +let currentBuildError: null | string = null; +let currentRuntimeErrorRecords: Array<ErrorRecord> = []; +let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null; +let stopListeningToRuntimeErrors: null | (() => void) = null; + +export function reportBuildError(error: string) { + currentBuildError = error; + update(); +} + +export function dismissBuildError() { + currentBuildError = null; + update(); +} + +export function startReportingRuntimeErrors(options: RuntimeReportingOptions) { + if (stopListeningToRuntimeErrors !== null) { + throw new Error('Already listening'); + } + currentRuntimeErrorOptions = options; + listenToRuntimeErrors(errorRecord => { + try { + if (typeof options.onError === 'function') { + options.onError.call(null); + } + } finally { + handleRuntimeError(errorRecord); + } }); } + +function handleRuntimeError(errorRecord) { + if ( + currentRuntimeErrorRecords.some(({ error }) => error === errorRecord.error) + ) { + // Deduplicate identical errors. + // This fixes https://github.com/facebookincubator/create-react-app/issues/3011. + return; + } + currentRuntimeErrorRecords = currentRuntimeErrorRecords.concat([errorRecord]); + update(); +} + +function dismissRuntimeErrors() { + currentRuntimeErrorRecords = []; + update(); +} + +export function stopReportingRuntimeErrors() { + if (stopListeningToRuntimeErrors === null) { + throw new Error('Not currently listening'); + } + currentRuntimeErrorOptions = null; + try { + stopListeningToRuntimeErrors(); + } finally { + stopListeningToRuntimeErrors = null; + } +} + +function update() { + renderedElement = render(); + // Loading iframe can be either sync or async depending on the browser. + if (isLoadingIframe) { + // Iframe is loading. + // First render will happen soon--don't need to do anything. + return; + } + if (iframe) { + // Iframe has already loaded. + // Just update it. + updateIframeContent(); + return; + } + // We need to schedule the first render. + isLoadingIframe = true; + const loadingIframe = window.document.createElement('iframe'); + applyStyles(loadingIframe, iframeStyle); + loadingIframe.onload = function() { + const iframeDocument = loadingIframe.contentDocument; + if (iframeDocument != null && iframeDocument.body != null) { + iframeDocument.body.style.margin = '0'; + // Keep popup within body boundaries for iOS Safari + iframeDocument.body.style['max-width'] = '100vw'; + const iframeRoot = iframeDocument.createElement('div'); + applyStyles(iframeRoot, overlayStyle); + iframeDocument.body.appendChild(iframeRoot); + + // Ready! Now we can update the UI. + iframe = loadingIframe; + isLoadingIframe = false; + updateIframeContent(); + } + }; + const appDocument = window.document; + appDocument.body.appendChild(loadingIframe); +} + +function render() { + if (currentBuildError) { + return <CompileErrorContainer error={currentBuildError} />; + } + if (currentRuntimeErrorRecords.length > 0) { + if (!currentRuntimeErrorOptions) { + throw new Error('Expected options to be injected.'); + } + return ( + <RuntimeErrorContainer + errorRecords={currentRuntimeErrorRecords} + close={dismissRuntimeErrors} + launchEditorEndpoint={currentRuntimeErrorOptions.launchEditorEndpoint} + /> + ); + } + return null; +} + +function updateIframeContent() { + if (iframe === null) { + throw new Error('Iframe has not been created yet.'); + } + const iframeBody = iframe.contentDocument.body; + if (!iframeBody) { + throw new Error('Expected iframe to have a body.'); + } + const iframeRoot = iframeBody.firstChild; + if (renderedElement === null) { + // Destroy iframe and force it to be recreated on next error + window.document.body.removeChild(iframe); + ReactDOM.unmountComponentAtNode(iframeRoot); + iframe = null; + return; + } + // Update the overlay + ReactDOM.render(renderedElement, iframeRoot); +} diff --git a/packages/react-error-overlay/src/listenToRuntimeErrors.js b/packages/react-error-overlay/src/listenToRuntimeErrors.js new file mode 100644 index 00000000000..45c43fa5d40 --- /dev/null +++ b/packages/react-error-overlay/src/listenToRuntimeErrors.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 { + register as registerError, + unregister as unregisterError, +} from './effects/unhandledError'; +import { + register as registerPromise, + unregister as unregisterPromise, +} from './effects/unhandledRejection'; +import { + register as registerStackTraceLimit, + unregister as unregisterStackTraceLimit, +} from './effects/stackTraceLimit'; +import { + permanentRegister as permanentRegisterConsole, + registerReactStack, + unregisterReactStack, +} from './effects/proxyConsole'; +import { massage as massageWarning } from './utils/warnings'; +import getStackFrames from './utils/getStackFrames'; + +import type { StackFrame } from './utils/stack-frame'; + +const CONTEXT_SIZE: number = 3; + +export type ErrorRecord = {| + error: Error, + unhandledRejection: boolean, + contextSize: number, + stackFrames: StackFrame[], +|}; + +export function listenToRuntimeErrors(crash: ErrorRecord => void) { + function crashWithFrames(error: Error, unhandledRejection = false) { + getStackFrames(error, unhandledRejection, CONTEXT_SIZE) + .then(stackFrames => { + if (stackFrames == null) { + return; + } + crash({ + error, + unhandledRejection, + contextSize: CONTEXT_SIZE, + stackFrames, + }); + }) + .catch(e => { + console.log('Could not get the stack frames of error:', e); + }); + } + registerError(window, error => crashWithFrames(error, false)); + registerPromise(window, error => crashWithFrames(error, true)); + registerStackTraceLimit(); + registerReactStack(); + permanentRegisterConsole('error', (warning, stack) => { + const data = massageWarning(warning, stack); + crashWithFrames( + // $FlowFixMe + { + message: data.message, + stack: data.stack, + __unmap_source: '/static/js/bundle.js', + }, + false + ); + }); + + return function stopListening() { + unregisterStackTraceLimit(); + unregisterPromise(window); + unregisterError(window); + unregisterReactStack(); + }; +} diff --git a/packages/react-error-overlay/src/overlay.js b/packages/react-error-overlay/src/overlay.js deleted file mode 100644 index 181cb02716d..00000000000 --- a/packages/react-error-overlay/src/overlay.js +++ /dev/null @@ -1,235 +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 { - register as registerError, - unregister as unregisterError, -} from './effects/unhandledError'; -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, -} from './effects/stackTraceLimit'; -import { - permanentRegister as permanentRegisterConsole, - registerReactStack, - 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 { applyStyles } from './utils/dom/css'; -import { createOverlay } from './components/overlay'; -import { updateAdditional } from './components/additional'; - -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(); - - const iframe = window.document.createElement('iframe'); - applyStyles(iframe, iframeStyle); - iframeReference = iframe; - iframe.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) { - 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); - } - 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); - } -} - -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() { - if (iframeReference === null) { - return; - } - window.document.body.removeChild(iframeReference); - iframeReference = null; - additionalReference = null; -} - -function unmount() { - disposeCurrentView(); - drainErrors(); - errorReferences = []; - currReferenceIndex = -1; -} - -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) { - 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.'); - } - renderErrorByIndex((currReferenceIndex = 0)); - } - }) - .catch(e => { - console.log('Could not consume 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(); - permanentRegisterConsole('error', (warning, stack) => { - const data = massageWarning(warning, stack); - crash( - // $FlowFixMe - { - message: data.message, - stack: data.stack, - __unmap_source: '/static/js/bundle.js', - }, - false - ); - }); -} - -function uninject() { - unregisterStackTraceLimit(); - unregisterShortcuts(window); - unregisterPromise(window); - unregisterError(window); - unregisterReactStack(); -} - -export { inject, uninject }; diff --git a/packages/react-error-overlay/src/styles.js b/packages/react-error-overlay/src/styles.js index bf17561d721..d6557c5d95f 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 = { @@ -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/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-dev-utils/ansiHTML.js b/packages/react-error-overlay/src/utils/generateAnsiHTML.js similarity index 95% rename from packages/react-dev-utils/ansiHTML.js rename to packages/react-error-overlay/src/utils/generateAnsiHTML.js index 90bf70374ee..509daddc23c 100644 --- a/packages/react-dev-utils/ansiHTML.js +++ b/packages/react-error-overlay/src/utils/generateAnsiHTML.js @@ -7,10 +7,11 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -'use strict'; +/* @flow */ + +import Anser from 'anser'; +import { AllHtmlEntities as Entities } from 'html-entities'; -var Anser = require('anser'); -var Entities = require('html-entities').AllHtmlEntities; var entities = new Entities(); // Color scheme inspired by https://chriskempson.github.io/base16/css/base16-github.css @@ -62,7 +63,7 @@ var anserMap = { 'ansi-white': 'darkgrey', }; -function ansiHTML(txt) { +function generateAnsiHTML(txt: string): string { var arr = new Anser().ansiToJson(entities.encode(txt), { use_classes: true, }); @@ -104,4 +105,4 @@ function ansiHTML(txt) { return result; } -module.exports = ansiHTML; +export default generateAnsiHTML; diff --git a/packages/react-error-overlay/src/utils/getLinesAround.js b/packages/react-error-overlay/src/utils/getLinesAround.js index a03e09a5817..7cb2ea5c54d 100644 --- a/packages/react-error-overlay/src/utils/getLinesAround.js +++ b/packages/react-error-overlay/src/utils/getLinesAround.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -//@flow +/* @flow */ import { ScriptLine } from './stack-frame'; /** 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..47b834d08d0 --- /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/getSourceMap.js b/packages/react-error-overlay/src/utils/getSourceMap.js index 80dd6002e0e..1d8405519bd 100644 --- a/packages/react-error-overlay/src/utils/getSourceMap.js +++ b/packages/react-error-overlay/src/utils/getSourceMap.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -//@flow +/* @flow */ import { SourceMapConsumer } from 'source-map'; /** 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<ErrorRecordReference | null> { +): Promise<StackFrame[] | null> { 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..cf732b838f4 --- /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; diff --git a/packages/react-error-overlay/src/utils/mapper.js b/packages/react-error-overlay/src/utils/mapper.js index fc3eb2266e6..656c216c144 100644 --- a/packages/react-error-overlay/src/utils/mapper.js +++ b/packages/react-error-overlay/src/utils/mapper.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @flow +/* @flow */ import StackFrame from './stack-frame'; import { getSourceMap } from './getSourceMap'; import { getLinesAround } from './getLinesAround'; diff --git a/packages/react-error-overlay/src/utils/parser.js b/packages/react-error-overlay/src/utils/parser.js index bfbb85f7969..1cbee1d1ccc 100644 --- a/packages/react-error-overlay/src/utils/parser.js +++ b/packages/react-error-overlay/src/utils/parser.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @flow +/* @flow */ import StackFrame from './stack-frame'; const regexExtractLocation = /\(?(.+?)(?::(\d+))?(?::(\d+))?\)?$/; diff --git a/packages/react-error-overlay/src/utils/stack-frame.js b/packages/react-error-overlay/src/utils/stack-frame.js index ae28cb53585..49f9da7fa11 100644 --- a/packages/react-error-overlay/src/utils/stack-frame.js +++ b/packages/react-error-overlay/src/utils/stack-frame.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -//@flow +/* @flow */ /** A container holding a script line. */ class ScriptLine { @@ -54,6 +54,20 @@ class StackFrame { sourceColumnNumber: number | null = null, sourceScriptCode: ScriptLine[] | null = null ) { + if (functionName && functionName.indexOf('Object.') === 0) { + functionName = functionName.slice('Object.'.length); + } + if ( + // 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. + functionName === 'friendlySyntaxErrorLabel' || + functionName === 'exports.__esModule' || + functionName === '<anonymous>' || + !functionName + ) { + functionName = null; + } this.functionName = functionName; this.fileName = fileName; @@ -72,8 +86,8 @@ class StackFrame { /** * Returns the name of this function. */ - getFunctionName(): string | null { - return this.functionName; + getFunctionName(): string { + return this.functionName || '(anonymous function)'; } /** @@ -98,11 +112,9 @@ class StackFrame { * Returns a pretty version of this stack frame. */ toString(): string { - const f = this.getFunctionName(); - if (f == null) { - return this.getSource(); - } - return `${f} (${this.getSource()})`; + const functionName = this.getFunctionName(); + const source = this.getSource(); + return `${functionName}${source ? ` (${source})` : ``}`; } } diff --git a/packages/react-error-overlay/src/utils/unmapper.js b/packages/react-error-overlay/src/utils/unmapper.js index b01736d74aa..60b2bee432b 100644 --- a/packages/react-error-overlay/src/utils/unmapper.js +++ b/packages/react-error-overlay/src/utils/unmapper.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @flow +/* @flow */ import StackFrame from './stack-frame'; import { getSourceMap } from './getSourceMap'; import { getLinesAround } from './getLinesAround'; diff --git a/packages/react-error-overlay/src/utils/warnings.js b/packages/react-error-overlay/src/utils/warnings.js index 8dcd7e1ee30..bcc54ceb733 100644 --- a/packages/react-error-overlay/src/utils/warnings.js +++ b/packages/react-error-overlay/src/utils/warnings.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @flow +/* @flow */ import type { ReactFrame } from '../effects/proxyConsole'; function stripInlineStacktrace(message: string): string { diff --git a/packages/react-scripts/bin/react-scripts.js b/packages/react-scripts/bin/react-scripts.js index 1261de6be19..a8f37e813ed 100755 --- a/packages/react-scripts/bin/react-scripts.js +++ b/packages/react-scripts/bin/react-scripts.js @@ -13,8 +13,9 @@ const spawn = require('react-dev-utils/crossSpawn'); const args = process.argv.slice(2); -const scriptIndex = args.findIndex(x => - x === 'build' || x === 'eject' || x === 'start' || x === 'test'); +const scriptIndex = args.findIndex( + x => x === 'build' || x === 'eject' || x === 'start' || x === 'test' +); const script = scriptIndex === -1 ? args[0] : args[scriptIndex]; const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []; diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index e9f985909da..3ef1104a177 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -56,8 +56,6 @@ module.exports = { require.resolve('react-dev-utils/webpackHotDevClient'), // We ship a few polyfills by default: require.resolve('./polyfills'), - // Errors should be considered fatal in development - require.resolve('react-error-overlay'), // Finally, this is your app's code: paths.appIndexJs, // We include the app code last so that if there is a runtime error during diff --git a/packages/react-scripts/config/webpackDevServer.config.js b/packages/react-scripts/config/webpackDevServer.config.js index 2a351e668bb..9c3889abae8 100644 --- a/packages/react-scripts/config/webpackDevServer.config.js +++ b/packages/react-scripts/config/webpackDevServer.config.js @@ -10,7 +10,7 @@ // @remove-on-eject-end 'use strict'; -const errorOverlayMiddleware = require('react-error-overlay/middleware'); +const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware'); const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware'); const config = require('./webpack.config.dev'); const paths = require('./paths'); diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index bf6c6be48f2..fa74ffa5815 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -49,7 +49,6 @@ "postcss-loader": "2.0.6", "promise": "8.0.1", "react-dev-utils": "^3.1.0", - "react-error-overlay": "^1.0.10", "style-loader": "0.18.2", "sw-precache-webpack-plugin": "0.11.4", "url-loader": "0.5.9",