Commit 70768b32 authored by Joe Haddad's avatar Joe Haddad Committed by GitHub
Browse files

Add `react-error-overlay` package (#2111)

* ༼ つ ◕_◕ ༽つ stack-frame-overlay

* Fix linting

* Remove auto overlay

* Fix e2e

* Pull in the rest

* Appease flow

* Correct dep

* Remove old repo references

* Check flow on test

* Test overlay in e2e

* Add cross env

* Rename package

* Make sure it gets built post-install

* Update the README

* Remove extra builds now that there's a postinstall script

* Revert "Remove extra builds now that there's a postinstall script"

This reverts commit 8bf601db.

* Remove broken script

* Fix some dev ergo
parent 1acc3a45
Showing with 808 additions and 1166 deletions
+808 -1166
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"changelog": "lerna-changelog", "changelog": "lerna-changelog",
"create-react-app": "tasks/cra.sh", "create-react-app": "tasks/cra.sh",
"e2e": "tasks/e2e-simple.sh", "e2e": "tasks/e2e-simple.sh",
"postinstall": "lerna bootstrap", "postinstall": "lerna bootstrap && cd packages/react-error-overlay/ && npm run build:prod",
"publish": "tasks/release.sh", "publish": "tasks/release.sh",
"start": "node packages/react-scripts/scripts/start.js", "start": "node packages/react-scripts/scripts/start.js",
"test": "node packages/react-scripts/scripts/test.js --env=jsdom", "test": "node packages/react-scripts/scripts/test.js --env=jsdom",
......
This diff is collapsed.
{
"presets": ["react-app"]
}
{
"extends": "react-app"
}
[ignore]
[include]
src/**/*.js
[libs]
[options]
lib/
coverage/
__tests__
*.test.js
*.spec.js
# `react-error-overlay`
`react-error-overlay` is an overlay which displays when there is a runtime error.
## Development
When developing within this package, make sure you run `npm start` (or `yarn start`) so that the files are compiled as you work.
This is ran in watch mode by default.
If you would like to build this for production, run `npm run build:prod` (or `yarn build:prod`).<br>
If you would like to build this one-off for development, you can run `NODE_ENV=development npm run build` (or `NODE_ENV=development yarn build`).
{
"name": "react-error-overlay",
"version": "0.0.0",
"description": "An overlay for displaying stack frames.",
"main": "lib/index.js",
"scripts": {
"prepublishOnly": "npm run build:prod && npm test",
"start": "cross-env NODE_ENV=development npm run build -- --watch",
"test": "flow && jest",
"build": "babel src/ -d lib/",
"build:prod": "cross-env NODE_ENV=production babel src/ -d lib/"
},
"repository": "facebookincubator/create-react-app",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/facebookincubator/create-react-app/issues"
},
"keywords": [
"overlay",
"syntax",
"error",
"red",
"box",
"redbox",
"crash",
"warning"
],
"author": "Joe Haddad <timer150@gmail.com>",
"files": [
"lib/"
],
"dependencies": {
"anser": "^1.2.5",
"babel-code-frame": "^6.22.0",
"babel-runtime": "^6.23.0",
"react-dev-utils": "^0.5.2",
"settle-promise": "^1.0.0"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-eslint": "7.x",
"babel-preset-react-app": "^2.2.0",
"cross-env": "^4.0.0",
"eslint": "^3.16.1",
"eslint-config-react-app": "^0.6.2",
"eslint-plugin-flowtype": "^2.21.0",
"eslint-plugin-import": "^2.0.1",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^6.4.1",
"flow-bin": "^0.46.0",
"jest": "19.x"
},
"jest": {
"setupFiles": [
"./src/__tests__/setupJest.js"
],
"collectCoverage": true,
"coverageReporters": [
"json"
],
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.js?(x)",
"<rootDir>/src/**/?(*.)(spec|test).js?(x)"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/fixtures/",
"setupJest.js"
]
}
}
/* @flow */
import { applyStyles } from '../utils/dom/css';
import { 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);
}
let text = ' ';
if (totalErrors <= 1) {
additionalReference.appendChild(document.createTextNode(text));
return;
}
text = `Errors ${currentError} of ${totalErrors}`;
const span = document.createElement('span');
span.appendChild(document.createTextNode(text));
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);
span.appendChild(group);
additionalReference.appendChild(span);
}
export type { SwitchCallback };
export { updateAdditional };
/* @flow */
import { applyStyles } from '../utils/dom/css';
import { hintsStyle, hintStyle, closeButtonStyle } from '../styles';
function createHint(document: Document, hint: string) {
const span = document.createElement('span');
span.appendChild(document.createTextNode(hint));
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, '×');
close.addEventListener('click', () => callback());
applyStyles(close, closeButtonStyle);
hints.appendChild(close);
return hints;
}
export type { CloseCallback };
export { createClose };
/* @flow */
import type { ScriptLine } from '../utils/stack-frame';
import { applyStyles } from '../utils/dom/css';
import { absolutifyCaret } from '../utils/dom/absolutifyCaret';
import {
preStyle,
codeStyle,
primaryErrorStyle,
secondaryErrorStyle,
} from '../styles';
import generateAnsiHtml from 'react-dev-utils/ansiHTML';
import codeFrame from 'babel-code-frame';
function createCode(
document: Document,
sourceLines: ScriptLine[],
lineNum: number,
columnNum: number | null,
contextSize: number,
main: boolean = false
) {
const sourceCode = [];
let whiteSpace = Infinity;
sourceLines.forEach(function(e) {
const { content: text } = e;
const m = text.match(/^\s*/);
if (text === '') {
return;
}
if (m && m[0]) {
whiteSpace = Math.min(whiteSpace, m[0].length);
} else {
whiteSpace = 0;
}
});
sourceLines.forEach(function(e) {
let { content: text } = e;
const { lineNumber: line } = e;
if (isFinite(whiteSpace)) {
text = text.substring(whiteSpace);
}
sourceCode[line - 1] = text;
});
const ansiHighlight = codeFrame(
sourceCode.join('\n'),
lineNum,
columnNum == null ? 0 : columnNum - (isFinite(whiteSpace) ? whiteSpace : 0),
{
forceColor: true,
linesAbove: contextSize,
linesBelow: contextSize,
}
);
const htmlHighlight = generateAnsiHtml(ansiHighlight);
const code = document.createElement('code');
code.innerHTML = htmlHighlight;
absolutifyCaret(code);
applyStyles(code, codeStyle);
const ccn = code.childNodes;
// eslint-disable-next-line
oLoop: for (let index = 0; index < ccn.length; ++index) {
const node = ccn[index];
const ccn2 = node.childNodes;
for (let index2 = 0; index2 < ccn2.length; ++index2) {
const lineNode = ccn2[index2];
const text = lineNode.innerText;
if (text == null) {
continue;
}
if (text.indexOf(' ' + lineNum + ' |') === -1) {
continue;
}
// $FlowFixMe
applyStyles(node, main ? primaryErrorStyle : secondaryErrorStyle);
// eslint-disable-next-line
break oLoop;
}
}
const pre = document.createElement('pre');
applyStyles(pre, preStyle);
pre.appendChild(code);
return pre;
}
export { createCode };
/* @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 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;
}
export { createFooter };
/* @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 {
omittedFramesStyle,
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');
} else {
text1.textContent = text1.textContent.replace(/▶/, '');
text1.textContent = text1.textContent.replace(/collapsed/, 'expanded');
}
});
applyStyles(omittedFrames, omittedFramesStyle);
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, omittedFramesStyle);
div.style.display = 'none';
parent.insertBefore(div, first);
}
function frameDiv(document: Document, 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;
}
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);
return frame;
}
function createFrame(
document: Document,
frameSetting: FrameSetting,
frame: StackFrame,
contextSize: number,
critical: boolean,
omits: OmitsObject,
omitBundle: number,
parentContainer: HTMLDivElement,
lastElement: boolean
) {
const { compiled } = frameSetting;
const {
functionName,
fileName,
lineNumber,
columnNumber,
_scriptCode: scriptLines,
_originalFileName: sourceFileName,
_originalLineNumber: sourceLineNumber,
_originalColumnNumber: sourceColumnNumber,
_originalScriptCode: sourceLines,
} = frame;
let url;
if (!compiled && sourceFileName && sourceLineNumber) {
url = sourceFileName + ':' + sourceLineNumber;
if (sourceColumnNumber) {
url += ':' + sourceColumnNumber;
}
} else if (fileName && lineNumber) {
url = fileName + ':' + lineNumber;
if (columnNumber) {
url += ':' + columnNumber;
}
} else {
url = 'unknown';
}
let needsHidden = false;
const internalUrl = isInternalFile(url, sourceFileName);
if (internalUrl) {
++omits.value;
needsHidden = true;
}
let collapseElement = null;
if (!internalUrl || 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 && internalUrl) {
collapseElement = omittedFrames;
} else {
parentContainer.appendChild(omittedFrames);
}
++omits.bundle;
}
omits.value = 0;
}
const elem = frameDiv(document, functionName, url, internalUrl);
if (needsHidden) {
applyStyles(elem, hiddenStyle);
elem.setAttribute('name', 'bundle-' + omitBundle);
}
let hasSource = false;
if (!internalUrl) {
if (
compiled && scriptLines && scriptLines.length !== 0 && lineNumber != null
) {
elem.appendChild(
createCode(
document,
scriptLines,
lineNumber,
columnNumber,
contextSize,
critical
)
);
hasSource = true;
} else if (
!compiled &&
sourceLines &&
sourceLines.length !== 0 &&
sourceLineNumber != null
) {
elem.appendChild(
createCode(
document,
sourceLines,
sourceLineNumber,
sourceColumnNumber,
contextSize,
critical
)
);
hasSource = true;
}
}
return { elem: elem, hasSource: hasSource, collapseElement: collapseElement };
}
export { createFrame };
/* @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 };
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
) {
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 };
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
),
lIndex,
frameSettings,
contextSize
);
if (elem == null) {
return;
}
critical = false;
trace.appendChild(elem);
});
//TODO: fix this
omits.value = 0;
return trace;
}
export { createFrames };
/* @flow */
import { applyStyles } from '../utils/dom/css';
import { overlayStyle, headerStyle, additionalStyle } 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);
overlay.appendChild(createClose(document, closeCallback));
// Create container
const container = document.createElement('div');
container.className = 'cra-container';
overlay.appendChild(container);
// Create additional
const additional = document.createElement('div');
applyStyles(additional, additionalStyle);
container.appendChild(additional);
updateAdditional(
document,
additional,
currentError,
totalErrors,
switchCallback
);
// Create header
const 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(
createFrames(document, frames, frameSettings, contextSize)
);
// Show message
container.appendChild(createFooter(document));
return {
overlay,
additional,
};
}
export { createOverlay };
/* @flow */
type ConsoleProxyCallback = (message: string) => void;
const permanentRegister = function proxyConsole(
type: string,
callback: ConsoleProxyCallback
) {
const orig = console[type];
console[type] = function __stack_frame_overlay_proxy_console__() {
const message = [].slice.call(arguments).join(' ');
callback(message);
return orig.apply(this, arguments);
};
};
export { permanentRegister };
/* @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,
};
/* @flow */
let stackTraceRegistered: boolean = false;
// Default: https://docs.microsoft.com/en-us/scripting/javascript/reference/stacktracelimit-property-error-javascript
let restoreStackTraceValue: number = 10;
const MAX_STACK_LENGTH: number = 50;
function registerStackTraceLimit(limit: number = MAX_STACK_LENGTH) {
if (stackTraceRegistered) {
return;
}
try {
restoreStackTraceValue = Error.stackTraceLimit;
Error.stackTraceLimit = limit;
stackTraceRegistered = true;
} catch (e) {
// Not all browsers support this so we don't care if it errors
}
}
function unregisterStackTraceLimit() {
if (!stackTraceRegistered) {
return;
}
try {
Error.stackTraceLimit = restoreStackTraceValue;
stackTraceRegistered = false;
} catch (e) {
// Not all browsers support this so we don't care if it errors
}
}
export {
registerStackTraceLimit as register,
unregisterStackTraceLimit as unregister,
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment