Files
create-react-app/packages/react-dev-utils/failFast.js
2016-12-05 19:20:53 -05:00

562 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const codeFrame = require('babel-code-frame')
const ansiHTML = require('./ansiHTML')
const StackTraceResolve = require('stacktrace-resolve').default
const CONTEXT_SIZE = 3
const black = '#293238'
const darkGray = '#878e91'
const lightGray = '#fafafa'
const red = '#ce1126'
const lightRed = '#fccfcf'
const yellow = '#fbf5b4'
const 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',
'box-shadow': '0 0 6px 0 rgba(0, 0, 0, 0.5)',
'line-height': 1.5,
}
// TODO: reapply containerStyle on resize or externalize to css and ensure
// e2e checks & tests pass
// ref commit: 46db1ae54c0449b737f82fb1cf8a47b7457d5b9b
const containerStyle = {
'margin': '0 auto',
width: () => calcWidth(window.innerWidth)
}
const hintsStyle = {
'font-size': '0.8em',
'margin-top': '-3em',
'margin-bottom': '3em',
'text-align': 'right',
color: darkGray
}
const hintStyle = {
padding: '0.5em 1em',
cursor: 'pointer'
}
const closeButtonStyle = {
'font-size': '26px',
color: black,
padding: '0.5em 1em',
cursor: 'pointer',
position: 'absolute',
right: 0,
top: 0,
}
const additionalStyle = {
'margin-bottom': '1.5em',
'margin-top': '-4em',
}
const headerStyle = {
'font-size': '1.7em',
'font-weight': 'bold',
color: red
}
const functionNameStyle = {
'margin-top': '1em',
'font-size': '1.2em'
}
const linkStyle = {
'font-size': '0.9em',
}
const anchorStyle = {
'text-decoration': 'none',
color: darkGray
}
const traceStyle = {
'font-size': '1em'
}
const depStyle = {
'font-size': '1.2em',
}
const primaryErrorStyle = {
'background-color': lightRed
}
const secondaryErrorStyle = {
'background-color': yellow
}
const omittedFramesStyle = {
color: black,
'font-size': '0.9em',
'margin': '1.5em 0',
cursor: 'pointer'
}
const preStyle = {
display: 'block',
padding: '0.5em',
'margin-top': '1.5em',
'margin-bottom': '0px',
'overflow-x': 'auto',
'font-size': '1.1em',
'white-space': 'pre',
}
const toggleStyle = {
'margin-bottom': '1.5em',
color: darkGray,
cursor: 'pointer'
}
const codeStyle = {
'font-family': 'Consolas, Menlo, monospace',
}
const hiddenStyle = {
display: 'none'
}
const groupStyle = {
'margin-left': '1em'
}
const _groupElemStyle = {
'border-color': '#ddd',
'border-width': '1px',
'border-radius': '4px',
'border-style': 'solid',
padding: '3px 6px',
cursor: 'pointer'
}
const groupElemLeft = Object.assign({}, _groupElemStyle, {
'border-top-right-radius': '0px',
'border-bottom-right-radius': '0px',
'margin-right': '0px'
})
const groupElemRight = Object.assign({}, _groupElemStyle, {
'border-top-left-radius': '0px',
'border-bottom-left-radius': '0px',
'margin-left': '-1px'
})
function calcWidth(width) {
if (width >= 1200) return '1170px'
if (width >= 992) return '970px'
if (width >= 768) return '750px'
return 'auto'
}
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 (let key in styles) {
if (!styles.hasOwnProperty(key)) continue
let val = styles[key]
if (typeof val === 'function') val = val()
element.style[key] = val.toString()
}
}
let overlayReference = null
let additionalReference = null
let capturedErrors = []
let viewIndex = -1
let frameSettings = []
function renderAdditional() {
if (additionalReference.lastChild) {
additionalReference.removeChild(additionalReference.lastChild)
}
let text = ' '
if (capturedErrors.length <= 1) {
additionalReference.appendChild(document.createTextNode(text))
return
}
text = `Errors ${viewIndex + 1} of ${capturedErrors.length}`
const span = document.createElement('span')
span.appendChild(document.createTextNode(text))
const group = document.createElement('span')
applyStyles(group, groupStyle)
const left = document.createElement('span')
applyStyles(left, groupElemLeft)
left.addEventListener('click', () => switchError(-1))
left.appendChild(document.createTextNode('←'))
const right = document.createElement('span')
applyStyles(right, groupElemRight)
right.addEventListener('click', () => switchError(1))
right.appendChild(document.createTextNode('→'))
group.appendChild(left)
group.appendChild(right)
span.appendChild(group)
additionalReference.appendChild(span)
}
function sourceCodePre(sourceLines, lineNum, columnNum, main = false) {
let sourceCode = []
sourceLines.forEach(({ text, line }) => {
sourceCode[line - 1] = text
})
sourceCode = sourceCode.join('\n')
const ansiHighlight = codeFrame(sourceCode, lineNum, columnNum, {
highlightCode: true,
linesAbove: CONTEXT_SIZE,
linesBelow: CONTEXT_SIZE
})
const htmlHighlight = ansiHTML(ansiHighlight)
const code = document.createElement('code')
code.innerHTML = htmlHighlight
applyStyles(code, codeStyle)
for (let node of code.childNodes) {
let breakOut = false
for (let lineNode of node.childNodes) {
if (lineNode.innerText.indexOf(` ${lineNum} |`) === -1) continue
applyStyles(node, main ? primaryErrorStyle : secondaryErrorStyle)
breakOut = true
}
if (breakOut) break
}
const pre = document.createElement('pre')
applyStyles(pre, preStyle)
pre.appendChild(code)
return pre
}
function createHint(hint) {
const span = document.createElement('span')
span.appendChild(document.createTextNode(hint))
applyStyles(span, hintStyle)
return span
}
function hintsDiv() {
const hints = document.createElement('div')
applyStyles(hints, hintsStyle)
const close = createHint('×')
close.addEventListener('click', e => {
unmount()
})
applyStyles(close, closeButtonStyle)
hints.appendChild(close)
return hints
}
function frameDiv(functionName, url, internalUrl) {
const frame = document.createElement('div')
const frameFunctionName = document.createElement('div')
let cleanedFunctionName
if (!functionName || functionName === 'Object.<anonymous>') {
cleanedFunctionName = '(anonymous function)'
} else {
cleanedFunctionName = functionName
}
let 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.href = url
frameAnchor.appendChild(document.createTextNode(cleanedUrl))
frameLink.appendChild(frameAnchor)
frame.appendChild(frameLink)
return frame
}
function traceFrame(frameSetting, frame, critical, omits, omitBundle, parentContainer) {
const { compiled } = frameSetting
const {
functionName,
fileName, lineNumber, columnNumber,
scriptLines,
sourceFileName, sourceLineNumber, sourceColumnNumber,
sourceLines
} = frame
// Skip native functions like Array.forEach
if (fileName === '(native)') return
let url
if (!compiled && sourceFileName) {
url = sourceFileName + ':' + sourceLineNumber
if (sourceColumnNumber) url += ':' + sourceColumnNumber
} else {
url = fileName + ':' + lineNumber
if (columnNumber) url += ':' + columnNumber
}
let needsHidden = false
const internalUrl = isInternalFile(url)
if (internalUrl) {
++omits.value
needsHidden = true
} else {
if (omits.value > 0) {
const omittedFrames = document.createElement('div')
const text1 = document.createTextNode(`${omits.value} stack frames were omitted.`)
omittedFrames.appendChild(text1)
omittedFrames.addEventListener('click', e => {
const hide = text1.textContent.match(/▲/)
document.getElementsByName(`bundle-${omitBundle}`).forEach(n => {
if (hide) {
n.style.display = 'none'
} else {
n.style.display = ''
}
})
if (hide) {
text1.textContent = text1.textContent.replace(/▲/, '▶')
} else {
text1.textContent = text1.textContent.replace(/▶/, '▲')
}
})
applyStyles(omittedFrames, omittedFramesStyle)
parentContainer.appendChild(omittedFrames)
++omits.bundle
}
omits.value = 0
}
const elem = frameDiv(functionName, url, internalUrl)
if (needsHidden) {
applyStyles(elem, hiddenStyle)
elem.setAttribute('name', `bundle-${omitBundle}`)
}
let 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, hasSource }
}
function lazyFrame(parent, factory, lIndex) {
const fac = factory()
if (fac == null) return
const { hasSource, elem } = fac
const elemWrapper = document.createElement('div')
elemWrapper.appendChild(elem)
if (hasSource) {
const compiledDiv = document.createElement('div')
applyStyles(compiledDiv, toggleStyle)
const o = frameSettings[lIndex]
const compiledText = document.createTextNode(`View ${o && o.compiled ? 'source' : 'compiled'}`)
compiledDiv.addEventListener('click', () => {
if (o) o.compiled = !o.compiled
const next = lazyFrame(parent, factory, lIndex)
if (next != null) {
parent.insertBefore(next, elemWrapper)
parent.removeChild(elemWrapper)
}
})
compiledDiv.appendChild(compiledText)
elemWrapper.appendChild(compiledDiv)
}
return elemWrapper
}
function traceDiv(resolvedFrames) {
const trace = document.createElement('div')
applyStyles(trace, traceStyle)
let index = 0
let critical = true
const omits = { value: 0, bundle: 1 }
for (let frame of resolvedFrames) {
const lIndex = index++
const elem = lazyFrame(
trace,
traceFrame.bind(undefined, frameSettings[lIndex], frame, critical, omits, omits.bundle, trace),
lIndex
)
if (elem == null) continue
critical = false
trace.appendChild(elem)
}
//TODO: fix this
omits.value = 0
return trace
}
function render(error, name, message, resolvedFrames) {
dispose()
frameSettings = resolvedFrames.map(() => { return { compiled: false } })
// Create overlay
const overlay = document.createElement('div')
applyStyles(overlay, overlayStyle)
overlay.appendChild(hintsDiv())
// Create container
const container = document.createElement('div')
applyStyles(container, containerStyle)
overlay.appendChild(container)
// Create additional
additionalReference = document.createElement('div')
applyStyles(additionalReference, additionalStyle)
container.appendChild(additionalReference)
renderAdditional()
// Create header
const header = document.createElement('div')
applyStyles(header, headerStyle)
header.appendChild(document.createTextNode(`${name}: ${message}`))
container.appendChild(header)
// Create trace
container.appendChild(traceDiv(resolvedFrames))
// Mount
document.body.appendChild(overlayReference = overlay)
}
function dispose() {
if (overlayReference === null) return
document.body.removeChild(overlayReference)
overlayReference = null
}
function unmount() {
dispose()
capturedErrors = []
viewIndex = -1
}
function isInternalFile(url) {
return url.indexOf('/~/') !== -1 || url.trim().indexOf(' ') !== -1
}
function renderError(index) {
viewIndex = index
const { error, unhandledRejection, resolvedFrames } = capturedErrors[index]
if (unhandledRejection) {
render(error, `Unhandled Rejection (${error.name})`, error.message, resolvedFrames)
} else {
render(error, error.name, error.message, resolvedFrames)
}
}
function crash(error, unhandledRejection = false) {
if (module.hot) module.hot.decline()
StackTraceResolve(error, CONTEXT_SIZE).then(function(resolvedFrames) {
capturedErrors.push({ error, unhandledRejection, 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 {
const 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)
}
}
let promiseHandler = function(event) {
if (event != null && event.reason != null) {
const { reason } = event
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)
let escapeHandler = function(event) {
const { key, keyCode, which } = event
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) { }
if (module.hot) {
module.hot.dispose(function() {
unmount()
window.removeEventListener('unhandledrejection', promiseHandler)
window.removeEventListener('keydown', escapeHandler)
})
}