From 0827aaff7bc8c921ba7565a014cbbc07a5211c26 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Mon, 6 Mar 2017 21:59:57 -0500 Subject: [PATCH] Add runtime error overlay (#1101) * Add red box prototype * Unmount fail node when hot-reloaded (future proofing) * Slightly improve error reason * Add Chrome unhandledrejection listener * Close red box when Escape is pressed * Construct our own source if not available * Resolve sourcemaps for friendlier traces * Allow error to be scrolled * Only utilize sourcemaps, not the magic. :( * Make view similar to React Native * Fix an uncaught syntax error * Add workaround for Firefox 50 * Add a hint for closing the prompt * Multiple error support When there's multiple errors, simply state that there are more instead of re-rendering * Log any renderer error * Dim node_modules in display * Override chrome stack trace limit * Magic: show me some source! * Add ability to toggle internal calls * Switch text from show -> hide * Change color scheme to something easier on the eyes * Change UI to be friendlier (thanks @nicinabox) https://github.com/facebookincubator/create-react-app/pull/1101#issuecomment-263621057 https://github.com/facebookincubator/create-react-app/pull/1101#issuecomment-263636171 * Update styles * Add container inside overlay * Apply pre style * Add line highlight * Add omitted frames ui * Move yellow to var * Do all function names in black * Adapt container based on screen size * Extract ansiHTML Use base16-github theme * Linting * Add syntax highlighting of errors * Linting * Remove white background * Append new files to package.json * Target exact version * White is a little harsh, use same color as red box. * Fix a bug where omitted frames would never be rendered (under certain circumstances) * Show local file path instead of confusing webpack:// * Do not require the entire file * Remove css file * Use context size, not a magic variable. * Fix title regression * Update dependency * Do not center text for internal calls * Temporarily disable links * Switch internal toggle to 'i' * Remove unnecessary rules from container style * Reduce omitted frames font size * Fix font family in pre > code * Re-introduce line highlighting * Object. -> (anonymous function) * Add ability to see script source * Add missing ansi mappings * Remove SIAF * Skip native functions * Extract hints div function * Extract renderers * Refacor var names * If source is disabled, don't use the line. * Allow toggle between source * Allow bundles to be expanded * Wow, how did I let this one slip? * Toggle text for UX/DX * Make it so clicking Close unmounts the error component * Do not allow hot reloading once an error is thrown * Do not wrap lines for small displays * Fix toggle when additional errors happen * Make the close a button * Capture and store errors * Dispose on render; move additional logic * Only make code not wrap * Switch to a view-by-index method * Allow user to switch between errors with arrow keys * Fix text while switching between errors * Update close button style * Render additional errors at top * Add left and right arrows * Make frames pointy * UTF-8 arrows * Don't mount unneeded DOM node * Switch to single changing text for compiled <-> source * Don't display arrows with only one error. * Collapsed and expanded * Make sure the last collapse toggle is appended * Do not show the stack trace if it doesn't resolve to a file we wrote * Style container with media queries * Do not allow x overflow; break words by default. * Trim off whitespace of lines * Remove padding since it's not outer-most * Add footer message * Extract css file to JS * Only inject the css when the overlay is up * Extract red variable * Remove env * Update babel-code-frame * Set force color to true * Extract out collapse div logic * Remove arrow field * Insert a top collapse * Make browser friendlier * Absolutify ^ * Make arrows buttons * Accessify * Let there be ES5 * Pretty css * Use forEach where we can ... * Remove extracted loop * Fix IE compatibility * Capture React warnings * Add source override via React output parsing * Whoops, fix arguments to crash. * es5-ify * Re-enable e2e-install directory test * Only rewrite line number if it was resolved and leaves a line at bottom * Rename failFast to crashOverlay * Disable console proxy * Appease linter * Appease more --- packages/react-dev-utils/ansiHTML.js | 102 +++ packages/react-dev-utils/crashOverlay.js | 831 ++++++++++++++++++ packages/react-dev-utils/package.json | 6 +- .../react-dev-utils/webpackHotDevClient.js | 27 +- .../config/webpack.config.dev.js | 2 + 5 files changed, 946 insertions(+), 22 deletions(-) create mode 100644 packages/react-dev-utils/ansiHTML.js create mode 100644 packages/react-dev-utils/crashOverlay.js diff --git a/packages/react-dev-utils/ansiHTML.js b/packages/react-dev-utils/ansiHTML.js new file mode 100644 index 00000000..27a514e0 --- /dev/null +++ b/packages/react-dev-utils/ansiHTML.js @@ -0,0 +1,102 @@ +/** + * 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. + */ + +'use strict'; + +var Anser = require('anser'); + +// Color scheme inspired by https://chriskempson.github.io/base16/css/base16-github.css +// var base00 = 'ffffff'; // Default Background +var base01 = 'f5f5f5'; // Lighter Background (Used for status bars) +// var base02 = 'c8c8fa'; // Selection Background +var base03 = '969896'; // Comments, Invisibles, Line Highlighting +// var base04 = 'e8e8e8'; // Dark Foreground (Used for status bars) +var base05 = '333333'; // Default Foreground, Caret, Delimiters, Operators +// var base06 = 'ffffff'; // Light Foreground (Not often used) +// var base07 = 'ffffff'; // Light Background (Not often used) +var base08 = 'ed6a43'; // Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted +// var base09 = '0086b3'; // Integers, Boolean, Constants, XML Attributes, Markup Link Url +// var base0A = '795da3'; // Classes, Markup Bold, Search Text Background +var base0B = '183691'; // Strings, Inherited Class, Markup Code, Diff Inserted +var base0C = '183691'; // Support, Regular Expressions, Escape Characters, Markup Quotes +// var base0D = '795da3'; // Functions, Methods, Attribute IDs, Headings +var base0E = 'a71d5d'; // Keywords, Storage, Selector, Markup Italic, Diff Changed +// var base0F = '333333'; // Deprecated, Opening/Closing Embedded Language Tags e.g. + +// Map ANSI colors from what babel-code-frame uses to base16-github +// See: https://github.com/babel/babel/blob/e86f62b304d280d0bab52c38d61842b853848ba6/packages/babel-code-frame/src/index.js#L9-L22 +var colors = { + reset: [base05, 'transparent'], + black: base05, + red: base08, /* marker, bg-invalid */ + green: base0B, /* string */ + yellow: base08, /* capitalized, jsx_tag, punctuator */ + blue: base0C, + magenta: base0C, /* regex */ + cyan: base0E, /* keyword */ + gray: base03, /* comment, gutter */ + lightgrey: base01, + darkgrey: base03 +}; + +var anserMap = { + 'ansi-bright-black': 'black', + 'ansi-bright-yellow': 'yellow', + 'ansi-yellow': 'yellow', + 'ansi-bright-green': 'green', + 'ansi-green': 'green', + 'ansi-bright-cyan': 'cyan', + 'ansi-cyan': 'cyan', + 'ansi-bright-red': 'red', + 'ansi-red': 'red', + 'ansi-bright-magenta': 'magenta', + 'ansi-magenta': 'magenta' +}; + +function ansiHTML(txt) { + var arr = new Anser().ansiToJson(txt, { + use_classes: true + }); + + var result = ''; + var open = false; + for (var index = 0; index < arr.length; ++index) { + var c = arr[index]; + var content = c.content, + fg = c.fg; + + var contentParts = content.split('\n'); + for (var _index = 0; _index < contentParts.length; ++_index) { + if (!open) { + result += ''; + open = true; + } + var part = contentParts[_index].replace('\r', ''); + var color = colors[anserMap[fg]]; + if (color != null) { + result += '' + part + ''; + } else { + if (fg != null) console.log('Missing color mapping: ', fg); + result += '' + part + ''; + } + if (_index < contentParts.length - 1) { + result += ''; + open = false; + result += '
'; + } + } + } + if (open) { + result += ''; + open = false; + } + return result; +} + +module.exports = ansiHTML; diff --git a/packages/react-dev-utils/crashOverlay.js b/packages/react-dev-utils/crashOverlay.js new file mode 100644 index 00000000..d92e793a --- /dev/null +++ b/packages/react-dev-utils/crashOverlay.js @@ -0,0 +1,831 @@ +/** + * 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. + */ + +'use strict'; + +var codeFrame = require('babel-code-frame'); +var ansiHTML = require('./ansiHTML'); +var StackTraceResolve = require('stack-frame-resolver').default; + +var CONTEXT_SIZE = 3; + +var black = '#293238'; +var darkGray = '#878e91'; +var lightGray = '#fafafa'; +var red = '#ce1126'; +var lightRed = '#fccfcf'; +var yellow = '#fbf5b4'; + +function getHead() { + return document.head || document.getElementsByTagName('head')[0]; +} + +var injectedCss = []; + +// From: http://stackoverflow.com/a/524721/127629 +function injectCss(css) { + var head = getHead(); + var style = document.createElement('style'); + + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); + injectedCss.push(style); +} + +var css = [ + '.cra-container {', + ' padding-right: 15px;', + ' padding-left: 15px;', + ' margin-right: auto;', + ' margin-left: auto;', + '}', + '', + '@media (min-width: 768px) {', + ' .cra-container {', + ' width: calc(750px - 6em);', + ' }', + '}', + '', + '@media (min-width: 992px) {', + ' .cra-container {', + ' width: calc(970px - 6em);', + ' }', + '}', + '', + '@media (min-width: 1200px) {', + ' .cra-container {', + ' width: calc(1170px - 6em);', + ' }', + '}' +].join('\n'); + +var overlayStyle = { + position: 'fixed', + 'box-sizing': 'border-box', + top: '1em', left: '1em', + bottom: '1em', right: '1em', + width: 'calc(100% - 2em)', height: 'calc(100% - 2em)', + 'border-radius': '3px', + 'background-color': lightGray, + padding: '4rem', + 'z-index': 1337, + 'font-family': 'Consolas, Menlo, monospace', + color: black, + 'white-space': 'pre-wrap', + overflow: 'auto', + 'overflow-x': 'hidden', + 'word-break': 'break-all', + 'box-shadow': '0 0 6px 0 rgba(0, 0, 0, 0.5)', + 'line-height': 1.5 +}; + +var hintsStyle = { + 'font-size': '0.8em', + 'margin-top': '-3em', + 'margin-bottom': '3em', + 'text-align': 'right', + color: darkGray +}; + +var hintStyle = { + padding: '0.5em 1em', + cursor: 'pointer' +}; + +var closeButtonStyle = { + 'font-size': '26px', + color: black, + padding: '0.5em 1em', + cursor: 'pointer', + position: 'absolute', + right: 0, + top: 0 +}; + +var additionalStyle = { + 'margin-bottom': '1.5em', + 'margin-top': '-4em' +}; + +var headerStyle = { + 'font-size': '1.7em', + 'font-weight': 'bold', + color: red +}; + +var functionNameStyle = { + 'margin-top': '1em', + 'font-size': '1.2em' +}; + +var linkStyle = { + 'font-size': '0.9em' +}; + +var anchorStyle = { + 'text-decoration': 'none', + color: darkGray +}; + +var traceStyle = { + 'font-size': '1em' +}; + +var depStyle = { + 'font-size': '1.2em' +}; + +var primaryErrorStyle = { + 'background-color': lightRed +}; + +var secondaryErrorStyle = { + 'background-color': yellow +}; + +var omittedFramesStyle = { + color: black, + 'font-size': '0.9em', + 'margin': '1.5em 0', + cursor: 'pointer' +}; + +var preStyle = { + display: 'block', + padding: '0.5em', + 'margin-top': '1.5em', + 'margin-bottom': '0px', + 'overflow-x': 'auto', + 'font-size': '1.1em', + 'white-space': 'pre' +}; + +var toggleStyle = { + 'margin-bottom': '1.5em', + color: darkGray, + cursor: 'pointer' +}; + +var codeStyle = { + 'font-family': 'Consolas, Menlo, monospace' +}; + +var hiddenStyle = { + display: 'none' +}; + +var groupStyle = { + 'margin-left': '1em' +}; + +var _groupElemStyle = { + 'background-color': 'inherit', + 'border-color': '#ddd', + 'border-width': '1px', + 'border-radius': '4px', + 'border-style': 'solid', + padding: '3px 6px', + cursor: 'pointer' +}; + +var groupElemLeft = Object.assign({}, _groupElemStyle, { + 'border-top-right-radius': '0px', + 'border-bottom-right-radius': '0px', + 'margin-right': '0px' +}); + +var groupElemRight = Object.assign({}, _groupElemStyle, { + 'border-top-left-radius': '0px', + 'border-bottom-left-radius': '0px', + 'margin-left': '-1px' +}); + +var footerStyle = { + 'text-align': 'center', + color: darkGray +}; + +function applyStyles(element, styles) { + element.setAttribute('style', ''); + // Firefox can't handle const due to non-compliant implementation + // Revisit Jan 2016 + // https://developer.mozilla.org/en-US/Firefox/Releases/51#JavaScript + // https://bugzilla.mozilla.org/show_bug.cgi?id=1101653 + for (var key in styles) { + if (!styles.hasOwnProperty(key)) continue; + var val = styles[key]; + if (typeof val === 'function') val = val(); + element.style[key] = val.toString(); + } +} + +var overlayReference = null; +var additionalReference = null; +var capturedErrors = []; +var viewIndex = -1; +var frameSettings = []; + +function consumeEvent(e) { + e.preventDefault(); + e.target.blur(); +} + +function accessify(node) { + node.setAttribute('tabindex', 0); + node.addEventListener('keydown', function (e) { + var key = e.key, + which = e.which, + keyCode = e.keyCode; + if (key === 'Enter' || which === 13 || keyCode === 13) { + e.preventDefault(); + e.target.click(); + } + }); +} + +function renderAdditional() { + if (additionalReference.lastChild) { + additionalReference.removeChild(additionalReference.lastChild); + } + + var text = ' '; + if (capturedErrors.length <= 1) { + additionalReference.appendChild(document.createTextNode(text)); + return; + } + text = 'Errors ' + (viewIndex + 1) + ' of ' + capturedErrors.length; + var span = document.createElement('span'); + span.appendChild(document.createTextNode(text)); + var group = document.createElement('span'); + applyStyles(group, groupStyle); + var left = document.createElement('button'); + applyStyles(left, groupElemLeft); + left.addEventListener('click', function (e) { + consumeEvent(e); + switchError(-1); + }); + left.appendChild(document.createTextNode('←')); + accessify(left); + var right = document.createElement('button'); + applyStyles(right, groupElemRight); + right.addEventListener('click', function (e) { + consumeEvent(e); + switchError(1); + }); + right.appendChild(document.createTextNode('→')); + accessify(right); + group.appendChild(left); + group.appendChild(right); + span.appendChild(group); + additionalReference.appendChild(span); +} + +function removeNextBr(parent, component) { + while (component != null && component.tagName.toLowerCase() !== 'br') { + component = component.nextSibling; + } + if (component != null) { + parent.removeChild(component); + } +} + +function absolutifyCode(component) { + var ccn = component.childNodes; + for (var index = 0; index < ccn.length; ++index) { + var c = ccn[index]; + if (c.tagName.toLowerCase() !== 'span') continue; + var text = c.innerText.replace(/\s/g, ''); + if (text !== '|^') continue; + c.style.position = 'absolute'; + removeNextBr(component, c); + } +} + +function sourceCodePre(sourceLines, lineNum, columnNum) { + var main = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; + + var sourceCode = []; + var whiteSpace = Infinity; + sourceLines.forEach(function (_ref2) { + var text = _ref2.text; + + var m = text.match(/^\s*/); + if (text === '') return; + if (m && m[0]) { + whiteSpace = Math.min(whiteSpace, m[0].length); + } else { + whiteSpace = 0; + } + }); + sourceLines.forEach(function (_ref3) { + var text = _ref3.text, + line = _ref3.line; + + if (isFinite(whiteSpace)) text = text.substring(whiteSpace); + sourceCode[line - 1] = text; + }); + sourceCode = sourceCode.join('\n'); + var ansiHighlight = codeFrame(sourceCode, lineNum, columnNum - (isFinite(whiteSpace) ? whiteSpace : 0), { + forceColor: true, + linesAbove: CONTEXT_SIZE, + linesBelow: CONTEXT_SIZE + }); + var htmlHighlight = ansiHTML(ansiHighlight); + var code = document.createElement('code'); + code.innerHTML = htmlHighlight; + absolutifyCode(code); + applyStyles(code, codeStyle); + + var ccn = code.childNodes; + for (var index = 0; index < ccn.length; ++index) { + var node = ccn[index]; + var breakOut = false; + var ccn2 = node.childNodes; + for (var index2 = 0; index2 < ccn2.length; ++index2) { + var lineNode = ccn2[index2]; + if (lineNode.innerText.indexOf(' ' + lineNum + ' |') === -1) continue; + applyStyles(node, main ? primaryErrorStyle : secondaryErrorStyle); + breakOut = true; + } + if (breakOut) break; + } + var pre = document.createElement('pre'); + applyStyles(pre, preStyle); + pre.appendChild(code); + return pre; +} + +function createHint(hint) { + var span = document.createElement('span'); + span.appendChild(document.createTextNode(hint)); + applyStyles(span, hintStyle); + return span; +} + +function hintsDiv() { + var hints = document.createElement('div'); + applyStyles(hints, hintsStyle); + + var close = createHint('×'); + close.addEventListener('click', function () { + unmount(); + }); + applyStyles(close, closeButtonStyle); + hints.appendChild(close); + return hints; +} + +function frameDiv(functionName, url, internalUrl) { + var frame = document.createElement('div'); + var frameFunctionName = document.createElement('div'); + + var cleanedFunctionName = void 0; + if (!functionName || functionName === 'Object.') { + cleanedFunctionName = '(anonymous function)'; + } else { + cleanedFunctionName = functionName; + } + + var cleanedUrl = url.replace('webpack://', '.'); + + if (internalUrl) { + applyStyles(frameFunctionName, Object.assign({}, functionNameStyle, depStyle)); + } else { + applyStyles(frameFunctionName, functionNameStyle); + } + + frameFunctionName.appendChild(document.createTextNode(cleanedFunctionName)); + frame.appendChild(frameFunctionName); + + var frameLink = document.createElement('div'); + applyStyles(frameLink, linkStyle); + var frameAnchor = document.createElement('a'); + applyStyles(frameAnchor, anchorStyle); + //frameAnchor.href = url + frameAnchor.appendChild(document.createTextNode(cleanedUrl)); + frameLink.appendChild(frameAnchor); + frame.appendChild(frameLink); + + return frame; +} + +function getGroupToggle(omitsCount, omitBundle) { + var omittedFrames = document.createElement('div'); + accessify(omittedFrames); + var text1 = document.createTextNode('\u25B6 ' + omitsCount + ' stack frames were collapsed.'); + omittedFrames.appendChild(text1); + omittedFrames.addEventListener('click', function () { + var hide = text1.textContent.match(/▲/); + var list = document.getElementsByName('bundle-' + omitBundle); + for (var index = 0; index < list.length; ++index) { + var 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'); + } else { + text1.textContent = text1.textContent.replace(/▶/, '▲'); + text1.textContent = text1.textContent.replace(/collapsed/, 'expanded'); + } + }); + applyStyles(omittedFrames, omittedFramesStyle); + return omittedFrames; +} + +function insertBeforeBundle(parent, omitsCount, omitBundle, actionElement) { + var children = document.getElementsByName('bundle-' + omitBundle); + if (children.length < 1) return; + var first = children[0]; + while (first.parentNode !== parent) { + first = first.parentNode; + } + var div = document.createElement('div'); + accessify(div); + div.setAttribute('name', 'bundle-' + omitBundle); + var text = document.createTextNode('\u25BC ' + omitsCount + ' stack frames were expanded.'); + div.appendChild(text); + div.addEventListener('click', function () { + return actionElement.click(); + }); + applyStyles(div, omittedFramesStyle); + div.style.display = 'none'; + + parent.insertBefore(div, first); +} + +function traceFrame(frameSetting, frame, critical, omits, omitBundle, parentContainer, lastElement) { + var compiled = frameSetting.compiled; + var functionName = frame.functionName, + fileName = frame.fileName, + lineNumber = frame.lineNumber, + columnNumber = frame.columnNumber, + scriptLines = frame.scriptLines, + sourceFileName = frame.sourceFileName, + sourceLineNumber = frame.sourceLineNumber, + sourceColumnNumber = frame.sourceColumnNumber, + sourceLines = frame.sourceLines; + + var url = void 0; + if (!compiled && sourceFileName) { + url = sourceFileName + ':' + sourceLineNumber; + if (sourceColumnNumber) url += ':' + sourceColumnNumber; + } else { + url = fileName + ':' + lineNumber; + if (columnNumber) url += ':' + columnNumber; + } + + var needsHidden = false; + var internalUrl = isInternalFile(url, sourceFileName); + if (internalUrl) { + ++omits.value; + needsHidden = true; + } + var collapseElement = null; + if (!internalUrl || lastElement) { + if (omits.value > 0) { + var omittedFrames = getGroupToggle(omits.value, omitBundle); + setTimeout(function () { + insertBeforeBundle.apply(undefined, arguments); + }.bind(undefined, parentContainer, omits.value, omitBundle, omittedFrames), 1); + if (lastElement && internalUrl) { + collapseElement = omittedFrames; + } else { + parentContainer.appendChild(omittedFrames); + } + ++omits.bundle; + } + omits.value = 0; + } + + var elem = frameDiv(functionName, url, internalUrl); + if (needsHidden) { + applyStyles(elem, hiddenStyle); + elem.setAttribute('name', 'bundle-' + omitBundle); + } + + var hasSource = false; + if (!internalUrl) { + if (compiled && scriptLines.length !== 0) { + elem.appendChild(sourceCodePre(scriptLines, lineNumber, columnNumber, critical)); + hasSource = true; + } else if (!compiled && sourceLines.length !== 0) { + elem.appendChild(sourceCodePre(sourceLines, sourceLineNumber, sourceColumnNumber, critical)); + hasSource = true; + } + } + + return { elem: elem, hasSource: hasSource, collapseElement: collapseElement }; +} + +function lazyFrame(parent, factory, lIndex) { + var fac = factory(); + if (fac == null) return; + var hasSource = fac.hasSource, + elem = fac.elem, + collapseElement = fac.collapseElement; + + var elemWrapper = document.createElement('div'); + elemWrapper.appendChild(elem); + + if (hasSource) { + (function () { + var compiledDiv = document.createElement('div'); + accessify(compiledDiv); + applyStyles(compiledDiv, toggleStyle); + + var o = frameSettings[lIndex]; + var compiledText = document.createTextNode('View ' + (o && o.compiled ? 'source' : 'compiled')); + compiledDiv.addEventListener('click', function () { + if (o) o.compiled = !o.compiled; + + var next = lazyFrame(parent, factory, lIndex); + 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 traceDiv(resolvedFrames) { + var trace = document.createElement('div'); + applyStyles(trace, traceStyle); + + var index = 0; + var critical = true; + var omits = { value: 0, bundle: 1 }; + resolvedFrames.forEach(function (frame) { + var lIndex = index++; + var elem = lazyFrame(trace, traceFrame.bind(undefined, frameSettings[lIndex], frame, critical, omits, omits.bundle, trace, index === resolvedFrames.length), lIndex); + if (elem == null) return; + critical = false; + trace.appendChild(elem); + }); + //TODO: fix this + omits.value = 0; + + return trace; +} + +function footer() { + var div = document.createElement('div'); + applyStyles(div, footerStyle); + div.appendChild(document.createTextNode('This screen is visible only in development. It will not appear when 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; +} + +function render(error, name, message, resolvedFrames) { + dispose(); + + frameSettings = resolvedFrames.map(function () { + return { compiled: false }; + }); + + injectCss(css); + + // Create overlay + var overlay = document.createElement('div'); + applyStyles(overlay, overlayStyle); + overlay.appendChild(hintsDiv()); + + // Create container + var container = document.createElement('div'); + container.className = 'cra-container'; + overlay.appendChild(container); + + // Create additional + additionalReference = document.createElement('div'); + applyStyles(additionalReference, additionalStyle); + container.appendChild(additionalReference); + renderAdditional(); + + // Create header + var header = document.createElement('div'); + applyStyles(header, headerStyle); + if (message.match(/^\w*:/)) { + header.appendChild(document.createTextNode(message)); + } else { + header.appendChild(document.createTextNode(name + ': ' + message)); + } + container.appendChild(header); + + // Create trace + container.appendChild(traceDiv(resolvedFrames)); + + // Show message + container.appendChild(footer()); + + // Mount + document.body.appendChild(overlayReference = overlay); +} + +function dispose() { + if (overlayReference === null) return; + document.body.removeChild(overlayReference); + overlayReference = null; + var head = getHead(); + injectedCss.forEach(function (node) { + head.removeChild(node); + }); + injectedCss = []; +} + +function unmount() { + dispose(); + capturedErrors = []; + viewIndex = -1; +} + +function isInternalFile(url, sourceFileName) { + return url.indexOf('/~/') !== -1 || url.trim().indexOf(' ') !== -1 || !sourceFileName; +} + +function renderError(index) { + viewIndex = index; + var _capturedErrors$index = capturedErrors[index], + error = _capturedErrors$index.error, + unhandledRejection = _capturedErrors$index.unhandledRejection, + resolvedFrames = _capturedErrors$index.resolvedFrames; + + if (unhandledRejection) { + render(error, 'Unhandled Rejection (' + error.name + ')', error.message, resolvedFrames); + } else { + render(error, error.name, error.message, resolvedFrames); + } +} + +function crash(error) { + var unhandledRejection = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var sourceOverrides = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + + if (module.hot) module.hot.decline(); + + StackTraceResolve(error, CONTEXT_SIZE).then(function (resolvedFrames) { + resolvedFrames = resolvedFrames.filter(function (_ref) { + var functionName = _ref.functionName; + return functionName.indexOf('__cra_proxy_console__') === -1; + }); + var overrideCount = sourceOverrides.length, + frameCount = resolvedFrames.length; + var frameIndex = 0; + for (var overrideIndex = 0; overrideIndex < overrideCount; ++overrideIndex) { + var tag = sourceOverrides[overrideIndex]; + var shouldContinue = false; + for (; frameIndex < frameCount; ++frameIndex) { + var sourceFileName = resolvedFrames[frameIndex].sourceFileName; + + if (sourceFileName == null) continue; + if (sourceFileName.indexOf('/' + tag.file) !== -1) { + var prevLineNumber = resolvedFrames[frameIndex].sourceLineNumber; + if (Math.abs(prevLineNumber - tag.lineNum) < CONTEXT_SIZE) { + resolvedFrames[frameIndex].sourceLineNumber = tag.lineNum; + } + shouldContinue = true; + break; + } + } + if (shouldContinue) continue; + break; + } + capturedErrors.push({ error: error, unhandledRejection: unhandledRejection, resolvedFrames: resolvedFrames }); + if (overlayReference !== null) renderAdditional(); + else { + renderError(viewIndex = 0); + } + }).catch(function (e) { + // This is another fail case (unlikely to happen) + // e.g. render(...) throws an error with provided arguments + console.log('Red box renderer error:', e); + unmount(); + render(null, 'Error', 'There is an error with red box. *Please* report this (see console).', []); + }); +} + +function switchError(offset) { + try { + var nextView = viewIndex + offset; + if (nextView < 0 || nextView >= capturedErrors.length) return; + renderError(nextView); + } catch (e) { + console.log('Red box renderer error:', e); + unmount(); + render(null, 'Error', 'There is an error with red box. *Please* report this (see console).', []); + } +} + +window.onerror = function (messageOrEvent, source, lineno, colno, error) { + if (error == null || !(error instanceof Error) || messageOrEvent.indexOf('Script error') !== -1) { + crash(new Error(error || messageOrEvent)); // TODO: more helpful message + } else { + crash(error); + } +}; + +var promiseHandler = function promiseHandler(event) { + if (event != null && event.reason != null) { + var reason = event.reason; + + if (reason == null || !(reason instanceof Error)) { + crash(new Error(reason), true); + } else { + crash(reason, true); + } + } else { + crash(new Error('Unknown event'), true); + } +}; + +window.addEventListener('unhandledrejection', promiseHandler); + +var escapeHandler = function escapeHandler(event) { + var key = event.key, + keyCode = event.keyCode, + which = event.which; + + if (key === 'Escape' || keyCode === 27 || which === 27) unmount(); + else if (key === 'ArrowLeft' || keyCode === 37 || which === 37) switchError(-1); + else if (key === 'ArrowRight' || keyCode === 39 || which === 39) switchError(1); +}; + +window.addEventListener('keydown', escapeHandler); + +try { + Error.stackTraceLimit = 50; +} catch (e) { + // Browser may not support this, we don't care. +} + +// eslint-disable-next-line +var proxyConsole = function proxyConsole(type) { + var orig = console[type]; + console[type] = function __cra_proxy_console__() { + var warning = [].slice.call(arguments).join(' '); + var nIndex = warning.indexOf('\n'); + var message = warning; + if (nIndex !== -1) message = message.substring(0, nIndex); + var stack = warning.substring(nIndex + 1).split('\n').filter(function (line) { + return line.indexOf('(at ') !== -1; + }).map(function (line) { + var prefix = '(at '; + var suffix = ')'; + line = line.substring(line.indexOf(prefix) + prefix.length); + line = line.substring(0, line.indexOf(suffix)); + var parts = line.split(/[:]/g); + if (parts.length !== 2) return null; + var file = parts[0]; + var lineNum = Number(parts[1]); + if (isNaN(lineNum)) return null; + return { file: file, lineNum: lineNum }; + }).filter(function (obj) { + return obj !== null; + }); + var error = void 0; + try { + throw new Error(message); + } catch (e) { + error = e; + } + setTimeout(function () { + return crash(error, false, stack); + }); + return orig.apply(this, arguments); + }; +}; + +// proxyConsole('error'); + +if (module.hot) { + module.hot.dispose(function () { + unmount(); + window.removeEventListener('unhandledrejection', promiseHandler); + window.removeEventListener('keydown', escapeHandler); + }); +} diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 4e1b350c..64037886 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -11,8 +11,10 @@ "node": ">=4" }, "files": [ + "ansiHTML.js", "checkRequiredFiles.js", "clearConsole.js", + "crashOverlay.js", "FileSizeReporter.js", "formatWebpackMessages.js", "getProcessForPort.js", @@ -24,7 +26,8 @@ "webpackHotDevClient.js" ], "dependencies": { - "ansi-html": "0.0.5", + "anser": "1.1.0", + "babel-code-frame": "6.20.0", "chalk": "1.1.3", "escape-string-regexp": "1.0.5", "filesize": "3.3.0", @@ -33,6 +36,7 @@ "opn": "4.0.2", "recursive-readdir": "2.1.1", "sockjs-client": "1.1.2", + "stack-frame-resolver": "0.1.3", "strip-ansi": "3.0.1" } } diff --git a/packages/react-dev-utils/webpackHotDevClient.js b/packages/react-dev-utils/webpackHotDevClient.js index f4b88fd8..b060ddea 100644 --- a/packages/react-dev-utils/webpackHotDevClient.js +++ b/packages/react-dev-utils/webpackHotDevClient.js @@ -18,28 +18,15 @@ // that looks similar to our console output. The error overlay is inspired by: // https://github.com/glenjamin/webpack-hot-middleware -var ansiHTML = require('ansi-html'); var SockJS = require('sockjs-client'); var stripAnsi = require('strip-ansi'); var url = require('url'); var formatWebpackMessages = require('./formatWebpackMessages'); var Entities = require('html-entities').AllHtmlEntities; +var ansiHTML = require('./ansiHTML'); var entities = new Entities(); -// Color scheme inspired by https://github.com/glenjamin/webpack-hot-middleware -var colors = { - reset: ['transparent', 'transparent'], - black: '181818', - red: 'E36049', - green: 'B3CB74', - yellow: 'FFD080', - blue: '7CAFC2', - magenta: '7FACCA', - cyan: 'C3C2EF', - lightgrey: 'EBE7E3', - darkgrey: '6D7891' -}; -ansiHTML.setColors(colors); +var red = '#E36049'; function createOverlayIframe(onIframeLoad) { var iframe = document.createElement('iframe'); @@ -69,8 +56,8 @@ function addOverlayDivTo(iframe) { div.style.bottom = 0; div.style.width = '100vw'; div.style.height = '100vh'; - div.style.backgroundColor = 'black'; - div.style.color = '#E8E8E8'; + div.style.backgroundColor = '#fafafa'; + div.style.color = '#333'; div.style.fontFamily = 'Menlo, Consolas, monospace'; div.style.fontSize = 'large'; div.style.padding = '2rem'; @@ -118,14 +105,12 @@ function showErrorOverlay(message) { ensureOverlayDivExists(function onOverlayDivReady(overlayDiv) { // Make it look similar to our terminal. overlayDiv.innerHTML = - 'Failed to compile.

' + + 'Failed to compile.

' + ansiHTML(entities.encode(message)); }); } -function destroyErrorOverlay() { +function destroyErrorOverlay() { if (!overlayDiv) { // It is not there in the first place. return; diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index a279a60a..a952969e 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -58,6 +58,8 @@ 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-dev-utils/crashOverlay'), // Finally, this is your app's code: paths.appIndexJs // We include the app code last so that if there is a runtime error during