mirror of
https://github.com/zhigang1992/create-react-app.git
synced 2026-01-12 22:46:30 +08:00
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 8bf601dbd36c1e0da7f785fa9ade70ab08ed8772. * Remove broken script * Fix some dev ergo
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
"changelog": "lerna-changelog",
|
||||
"create-react-app": "tasks/cra.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",
|
||||
"start": "node packages/react-scripts/scripts/start.js",
|
||||
"test": "node packages/react-scripts/scripts/test.js --env=jsdom",
|
||||
|
||||
1165
packages/react-dev-utils/crashOverlay.js
vendored
1165
packages/react-dev-utils/crashOverlay.js
vendored
File diff suppressed because it is too large
Load Diff
3
packages/react-error-overlay/.babelrc
Normal file
3
packages/react-error-overlay/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["react-app"]
|
||||
}
|
||||
3
packages/react-error-overlay/.eslintrc
Normal file
3
packages/react-error-overlay/.eslintrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "react-app"
|
||||
}
|
||||
8
packages/react-error-overlay/.flowconfig
Normal file
8
packages/react-error-overlay/.flowconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[ignore]
|
||||
|
||||
[include]
|
||||
src/**/*.js
|
||||
|
||||
[libs]
|
||||
|
||||
[options]
|
||||
2
packages/react-error-overlay/.gitignore
vendored
Normal file
2
packages/react-error-overlay/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
lib/
|
||||
coverage/
|
||||
3
packages/react-error-overlay/.npmignore
Normal file
3
packages/react-error-overlay/.npmignore
Normal file
@@ -0,0 +1,3 @@
|
||||
__tests__
|
||||
*.test.js
|
||||
*.spec.js
|
||||
11
packages/react-error-overlay/README.md
Normal file
11
packages/react-error-overlay/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# `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`).
|
||||
71
packages/react-error-overlay/package.json
Normal file
71
packages/react-error-overlay/package.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
0
packages/react-error-overlay/src/__tests__/setupJest.js
vendored
Normal file
0
packages/react-error-overlay/src/__tests__/setupJest.js
vendored
Normal file
52
packages/react-error-overlay/src/components/additional.js
vendored
Normal file
52
packages/react-error-overlay/src/components/additional.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
/* @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 };
|
||||
25
packages/react-error-overlay/src/components/close.js
vendored
Normal file
25
packages/react-error-overlay/src/components/close.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/* @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 };
|
||||
89
packages/react-error-overlay/src/components/code.js
vendored
Normal file
89
packages/react-error-overlay/src/components/code.js
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
/* @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 };
|
||||
22
packages/react-error-overlay/src/components/footer.js
vendored
Normal file
22
packages/react-error-overlay/src/components/footer.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/* @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 };
|
||||
232
packages/react-error-overlay/src/components/frame.js
vendored
Normal file
232
packages/react-error-overlay/src/components/frame.js
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
/* @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 };
|
||||
117
packages/react-error-overlay/src/components/frames.js
vendored
Normal file
117
packages/react-error-overlay/src/components/frames.js
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/* @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 };
|
||||
74
packages/react-error-overlay/src/components/overlay.js
vendored
Normal file
74
packages/react-error-overlay/src/components/overlay.js
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
/* @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 };
|
||||
15
packages/react-error-overlay/src/effects/proxyConsole.js
vendored
Normal file
15
packages/react-error-overlay/src/effects/proxyConsole.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/* @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 };
|
||||
44
packages/react-error-overlay/src/effects/shortcuts.js
vendored
Normal file
44
packages/react-error-overlay/src/effects/shortcuts.js
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
/* @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,
|
||||
};
|
||||
36
packages/react-error-overlay/src/effects/stackTraceLimit.js
vendored
Normal file
36
packages/react-error-overlay/src/effects/stackTraceLimit.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/* @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,
|
||||
};
|
||||
40
packages/react-error-overlay/src/effects/unhandledError.js
vendored
Normal file
40
packages/react-error-overlay/src/effects/unhandledError.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
/* @flow */
|
||||
let boundErrorHandler = null;
|
||||
|
||||
type ErrorCallback = (error: Error) => void;
|
||||
|
||||
function errorHandler(callback: ErrorCallback, e: Event): void {
|
||||
if (!e.error) {
|
||||
return;
|
||||
}
|
||||
// $FlowFixMe
|
||||
const { error } = e;
|
||||
if (error instanceof Error) {
|
||||
callback(error);
|
||||
} else {
|
||||
// A non-error was thrown, we don't have a trace. :(
|
||||
// Look in your browser's devtools for more information
|
||||
callback(new Error(error));
|
||||
}
|
||||
}
|
||||
|
||||
function registerUnhandledError(target: EventTarget, callback: ErrorCallback) {
|
||||
if (boundErrorHandler !== null) {
|
||||
return;
|
||||
}
|
||||
boundErrorHandler = errorHandler.bind(undefined, callback);
|
||||
target.addEventListener('error', boundErrorHandler);
|
||||
}
|
||||
|
||||
function unregisterUnhandledError(target: EventTarget) {
|
||||
if (boundErrorHandler === null) {
|
||||
return;
|
||||
}
|
||||
target.removeEventListener('error', boundErrorHandler);
|
||||
boundErrorHandler = null;
|
||||
}
|
||||
|
||||
export {
|
||||
registerUnhandledError as register,
|
||||
unregisterUnhandledError as unregister,
|
||||
};
|
||||
46
packages/react-error-overlay/src/effects/unhandledRejection.js
vendored
Normal file
46
packages/react-error-overlay/src/effects/unhandledRejection.js
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
/* @flow */
|
||||
let boundRejectionHandler = null;
|
||||
|
||||
type ErrorCallback = (error: Error) => void;
|
||||
|
||||
function rejectionHandler(
|
||||
callback: ErrorCallback,
|
||||
e: PromiseRejectionEvent
|
||||
): void {
|
||||
if (e == null || e.reason == null) {
|
||||
return callback(new Error('Unknown'));
|
||||
}
|
||||
let { reason } = e;
|
||||
if (reason instanceof Error) {
|
||||
return callback(reason);
|
||||
}
|
||||
// A non-error was rejected, we don't have a trace :(
|
||||
// Look in your browser's devtools for more information
|
||||
return callback(new Error(reason));
|
||||
}
|
||||
|
||||
function registerUnhandledRejection(
|
||||
target: EventTarget,
|
||||
callback: ErrorCallback
|
||||
) {
|
||||
if (boundRejectionHandler !== null) {
|
||||
return;
|
||||
}
|
||||
boundRejectionHandler = rejectionHandler.bind(undefined, callback);
|
||||
// $FlowFixMe
|
||||
target.addEventListener('unhandledrejection', boundRejectionHandler);
|
||||
}
|
||||
|
||||
function unregisterUnhandledRejection(target: EventTarget) {
|
||||
if (boundRejectionHandler === null) {
|
||||
return;
|
||||
}
|
||||
// $FlowFixMe
|
||||
target.removeEventListener('unhandledrejection', boundRejectionHandler);
|
||||
boundRejectionHandler = null;
|
||||
}
|
||||
|
||||
export {
|
||||
registerUnhandledRejection as register,
|
||||
unregisterUnhandledRejection as unregister,
|
||||
};
|
||||
9
packages/react-error-overlay/src/index.js
vendored
Normal file
9
packages/react-error-overlay/src/index.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/* @flow */
|
||||
import { inject, uninject } from './overlay';
|
||||
|
||||
inject();
|
||||
if (module.hot && typeof module.hot.dispose === 'function') {
|
||||
module.hot.dispose(function() {
|
||||
uninject();
|
||||
});
|
||||
}
|
||||
217
packages/react-error-overlay/src/overlay.js
vendored
Normal file
217
packages/react-error-overlay/src/overlay.js
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
/* @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 {
|
||||
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 { injectCss, 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;
|
||||
|
||||
const 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');
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
injectCss(iframeReference.contentDocument, css);
|
||||
if (document.body != null) {
|
||||
document.body.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) {
|
||||
const nextView = currReferenceIndex + offset;
|
||||
if (nextView < 0 || nextView >= errorReferences.length) {
|
||||
return;
|
||||
}
|
||||
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 => {
|
||||
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();
|
||||
}
|
||||
|
||||
function uninject() {
|
||||
unregisterStackTraceLimit();
|
||||
unregisterShortcuts(window);
|
||||
unregisterPromise(window);
|
||||
unregisterError(window);
|
||||
}
|
||||
|
||||
export { inject, uninject };
|
||||
186
packages/react-error-overlay/src/styles.js
vendored
Normal file
186
packages/react-error-overlay/src/styles.js
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
/* @flow */
|
||||
const black = '#293238',
|
||||
darkGray = '#878e91',
|
||||
lightGray = '#fafafa',
|
||||
red = '#ce1126',
|
||||
lightRed = '#fccfcf',
|
||||
yellow = '#fbf5b4';
|
||||
|
||||
const iframeStyle = {
|
||||
'background-color': lightGray,
|
||||
position: 'fixed',
|
||||
top: '1em',
|
||||
left: '1em',
|
||||
bottom: '1em',
|
||||
right: '1em',
|
||||
width: 'calc(100% - 2em)',
|
||||
height: 'calc(100% - 2em)',
|
||||
border: 'none',
|
||||
'border-radius': '3px',
|
||||
'box-shadow': '0 0 6px 0 rgba(0, 0, 0, 0.5)',
|
||||
'z-index': 1337,
|
||||
};
|
||||
|
||||
const overlayStyle = {
|
||||
'box-sizing': 'border-box',
|
||||
padding: '4rem',
|
||||
'font-family': 'Consolas, Menlo, monospace',
|
||||
color: black,
|
||||
'white-space': 'pre-wrap',
|
||||
overflow: 'auto',
|
||||
'overflow-x': 'hidden',
|
||||
'word-break': 'break-word',
|
||||
'line-height': 1.5,
|
||||
};
|
||||
|
||||
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 = {
|
||||
'background-color': 'inherit',
|
||||
'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',
|
||||
});
|
||||
|
||||
const footerStyle = {
|
||||
'text-align': 'center',
|
||||
color: darkGray,
|
||||
};
|
||||
|
||||
export {
|
||||
iframeStyle,
|
||||
overlayStyle,
|
||||
hintsStyle,
|
||||
hintStyle,
|
||||
closeButtonStyle,
|
||||
additionalStyle,
|
||||
headerStyle,
|
||||
functionNameStyle,
|
||||
linkStyle,
|
||||
anchorStyle,
|
||||
traceStyle,
|
||||
depStyle,
|
||||
primaryErrorStyle,
|
||||
secondaryErrorStyle,
|
||||
omittedFramesStyle,
|
||||
preStyle,
|
||||
toggleStyle,
|
||||
codeStyle,
|
||||
hiddenStyle,
|
||||
groupStyle,
|
||||
groupElemLeft,
|
||||
groupElemRight,
|
||||
footerStyle,
|
||||
};
|
||||
34
packages/react-error-overlay/src/utils/dom/absolutifyCaret.js
vendored
Normal file
34
packages/react-error-overlay/src/utils/dom/absolutifyCaret.js
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
/* @flow */
|
||||
function removeNextBr(parent, component: ?Element) {
|
||||
while (component != null && component.tagName.toLowerCase() !== 'br') {
|
||||
component = component.nextElementSibling;
|
||||
}
|
||||
if (component != null) {
|
||||
parent.removeChild(component);
|
||||
}
|
||||
}
|
||||
|
||||
function absolutifyCaret(component: Node) {
|
||||
const ccn = component.childNodes;
|
||||
for (let index = 0; index < ccn.length; ++index) {
|
||||
const c = ccn[index];
|
||||
// $FlowFixMe
|
||||
if (c.tagName.toLowerCase() !== 'span') {
|
||||
continue;
|
||||
}
|
||||
const _text = c.innerText;
|
||||
if (_text == null) {
|
||||
continue;
|
||||
}
|
||||
const text = _text.replace(/\s/g, '');
|
||||
if (text !== '|^') {
|
||||
continue;
|
||||
}
|
||||
// $FlowFixMe
|
||||
c.style.position = 'absolute';
|
||||
// $FlowFixMe
|
||||
removeNextBr(component, c);
|
||||
}
|
||||
}
|
||||
|
||||
export { absolutifyCaret };
|
||||
9
packages/react-error-overlay/src/utils/dom/consumeEvent.js
vendored
Normal file
9
packages/react-error-overlay/src/utils/dom/consumeEvent.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/* @flow */
|
||||
function consumeEvent(e: Event) {
|
||||
e.preventDefault();
|
||||
if (typeof e.target.blur === 'function') {
|
||||
e.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
export { consumeEvent };
|
||||
40
packages/react-error-overlay/src/utils/dom/css.js
vendored
Normal file
40
packages/react-error-overlay/src/utils/dom/css.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
/* @flow */
|
||||
let injectedCount = 0;
|
||||
const injectedCache = {};
|
||||
|
||||
function getHead(document: Document) {
|
||||
return document.head || document.getElementsByTagName('head')[0];
|
||||
}
|
||||
|
||||
function injectCss(document: Document, css: string): number {
|
||||
const head = getHead(document);
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.appendChild(document.createTextNode(css));
|
||||
head.appendChild(style);
|
||||
|
||||
injectedCache[++injectedCount] = style;
|
||||
return injectedCount;
|
||||
}
|
||||
|
||||
function removeCss(document: Document, ref: number) {
|
||||
if (injectedCache[ref] == null) {
|
||||
return;
|
||||
}
|
||||
const head = getHead(document);
|
||||
head.removeChild(injectedCache[ref]);
|
||||
delete injectedCache[ref];
|
||||
}
|
||||
|
||||
function applyStyles(element: HTMLElement, styles: Object) {
|
||||
element.setAttribute('style', '');
|
||||
for (const key in styles) {
|
||||
if (!styles.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
// $FlowFixMe
|
||||
element.style[key] = styles[key];
|
||||
}
|
||||
}
|
||||
|
||||
export { getHead, injectCss, removeCss, applyStyles };
|
||||
15
packages/react-error-overlay/src/utils/dom/enableTabClick.js
vendored
Normal file
15
packages/react-error-overlay/src/utils/dom/enableTabClick.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/* @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 };
|
||||
64
packages/react-error-overlay/src/utils/errorRegister.js
vendored
Normal file
64
packages/react-error-overlay/src/utils/errorRegister.js
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/* @flow */
|
||||
import type { StackFrame } from './stack-frame';
|
||||
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(
|
||||
error: Error,
|
||||
unhandledRejection: boolean = false,
|
||||
contextSize: number = 3
|
||||
): Promise<ErrorRecordReference> {
|
||||
const parsedFrames = parse(error);
|
||||
let enhancedFramesPromise;
|
||||
if (error.__unmap_source) {
|
||||
enhancedFramesPromise = unmap(
|
||||
// $FlowFixMe
|
||||
error.__unmap_source,
|
||||
parsedFrames,
|
||||
contextSize
|
||||
);
|
||||
} else {
|
||||
enhancedFramesPromise = map(parsedFrames, contextSize);
|
||||
}
|
||||
return enhancedFramesPromise.then(enhancedFrames => {
|
||||
enhancedFrames = 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 };
|
||||
30
packages/react-error-overlay/src/utils/getLinesAround.js
vendored
Normal file
30
packages/react-error-overlay/src/utils/getLinesAround.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import { ScriptLine } from './stack-frame';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} line The line number to provide context around.
|
||||
* @param {number} count The number of lines you'd like for context.
|
||||
* @param {string[] | string} lines The source code.
|
||||
*/
|
||||
function getLinesAround(
|
||||
line: number,
|
||||
count: number,
|
||||
lines: string[] | string
|
||||
): ScriptLine[] {
|
||||
if (typeof lines === 'string') {
|
||||
lines = lines.split('\n');
|
||||
}
|
||||
const result = [];
|
||||
for (
|
||||
let index = Math.max(0, line - 1 - count);
|
||||
index <= Math.min(lines.length - 1, line - 1 + count);
|
||||
++index
|
||||
) {
|
||||
result.push(new ScriptLine(index + 1, lines[index], index === line - 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export { getLinesAround };
|
||||
export default getLinesAround;
|
||||
118
packages/react-error-overlay/src/utils/getSourceMap.js
vendored
Normal file
118
packages/react-error-overlay/src/utils/getSourceMap.js
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
//@flow
|
||||
import { SourceMapConsumer } from 'source-map';
|
||||
|
||||
/**
|
||||
* A wrapped instance of a <code>{@link https://github.com/mozilla/source-map SourceMapConsumer}</code>.
|
||||
*
|
||||
* This exposes methods which will be indifferent to changes made in <code>{@link https://github.com/mozilla/source-map source-map}</code>.
|
||||
*/
|
||||
class SourceMap {
|
||||
__source_map: SourceMapConsumer;
|
||||
|
||||
constructor(sourceMap) {
|
||||
this.__source_map = sourceMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original code position for a generated code position.
|
||||
* @param {number} line The line of the generated code position.
|
||||
* @param {number} column The column of the generated code position.
|
||||
*/
|
||||
getOriginalPosition(
|
||||
line: number,
|
||||
column: number
|
||||
): { source: string, line: number, column: number } {
|
||||
const {
|
||||
line: l,
|
||||
column: c,
|
||||
source: s,
|
||||
} = this.__source_map.originalPositionFor({
|
||||
line,
|
||||
column,
|
||||
});
|
||||
return { line: l, column: c, source: s };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the generated code position for an original position.
|
||||
* @param {string} source The source file of the original code position.
|
||||
* @param {number} line The line of the original code position.
|
||||
* @param {number} column The column of the original code position.
|
||||
*/
|
||||
getGeneratedPosition(
|
||||
source: string,
|
||||
line: number,
|
||||
column: number
|
||||
): { line: number, column: number } {
|
||||
const { line: l, column: c } = this.__source_map.generatedPositionFor({
|
||||
source,
|
||||
line,
|
||||
column,
|
||||
});
|
||||
return {
|
||||
line: l,
|
||||
column: c,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the code for a given source file name.
|
||||
* @param {string} sourceName The name of the source file.
|
||||
*/
|
||||
getSource(sourceName: string): string {
|
||||
return this.__source_map.sourceContentFor(sourceName);
|
||||
}
|
||||
|
||||
getSources(): string[] {
|
||||
return this.__source_map.sources;
|
||||
}
|
||||
}
|
||||
|
||||
function extractSourceMapUrl(fileUri: string, fileContents: string) {
|
||||
const regex = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/gm;
|
||||
let match = null;
|
||||
for (;;) {
|
||||
let next = regex.exec(fileContents);
|
||||
if (next == null) {
|
||||
break;
|
||||
}
|
||||
match = next;
|
||||
}
|
||||
if (!(match && match[1])) {
|
||||
return Promise.reject(`Cannot find a source map directive for ${fileUri}.`);
|
||||
}
|
||||
return Promise.resolve(match[1].toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of <code>{@link SourceMap}</code> for a given fileUri and fileContents.
|
||||
* @param {string} fileUri The URI of the source file.
|
||||
* @param {string} fileContents The contents of the source file.
|
||||
*/
|
||||
async function getSourceMap(
|
||||
fileUri: string,
|
||||
fileContents: string
|
||||
): Promise<SourceMap> {
|
||||
let sm = await extractSourceMapUrl(fileUri, fileContents);
|
||||
if (sm.indexOf('data:') === 0) {
|
||||
const base64 = /^data:application\/json;([\w=:"-]+;)*base64,/;
|
||||
const match2 = sm.match(base64);
|
||||
if (!match2) {
|
||||
throw new Error(
|
||||
'Sorry, non-base64 inline source-map encoding is not supported.'
|
||||
);
|
||||
}
|
||||
sm = sm.substring(match2[0].length);
|
||||
sm = window.atob(sm);
|
||||
sm = JSON.parse(sm);
|
||||
return new SourceMap(new SourceMapConsumer(sm));
|
||||
} else {
|
||||
const index = fileUri.lastIndexOf('/');
|
||||
const url = fileUri.substring(0, index + 1) + sm;
|
||||
const obj = await fetch(url).then(res => res.json());
|
||||
return new SourceMap(new SourceMapConsumer(obj));
|
||||
}
|
||||
}
|
||||
|
||||
export { extractSourceMapUrl, getSourceMap };
|
||||
export default getSourceMap;
|
||||
10
packages/react-error-overlay/src/utils/isInternalFile.js
vendored
Normal file
10
packages/react-error-overlay/src/utils/isInternalFile.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/* @flow */
|
||||
function isInternalFile(url: string, sourceFileName: string | null | void) {
|
||||
return url.indexOf('/~/') !== -1 ||
|
||||
url.indexOf('/node_modules/') !== -1 ||
|
||||
url.trim().indexOf(' ') !== -1 ||
|
||||
sourceFileName == null ||
|
||||
sourceFileName.length === 0;
|
||||
}
|
||||
|
||||
export { isInternalFile };
|
||||
60
packages/react-error-overlay/src/utils/mapper.js
vendored
Normal file
60
packages/react-error-overlay/src/utils/mapper.js
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
import StackFrame from './stack-frame';
|
||||
import { getSourceMap } from './getSourceMap';
|
||||
import { getLinesAround } from './getLinesAround';
|
||||
import { settle } from 'settle-promise';
|
||||
|
||||
/**
|
||||
* Enhances a set of <code>StackFrame</code>s with their original positions and code (when available).
|
||||
* @param {StackFrame[]} frames A set of <code>StackFrame</code>s which contain (generated) code positions.
|
||||
* @param {number} [contextLines=3] The number of lines to provide before and after the line specified in the <code>StackFrame</code>.
|
||||
*/
|
||||
async function map(
|
||||
frames: StackFrame[],
|
||||
contextLines: number = 3
|
||||
): Promise<StackFrame[]> {
|
||||
const cache: any = {};
|
||||
const files: string[] = [];
|
||||
frames.forEach(frame => {
|
||||
const { fileName } = frame;
|
||||
if (fileName == null) return;
|
||||
if (files.indexOf(fileName) !== -1) {
|
||||
return;
|
||||
}
|
||||
files.push(fileName);
|
||||
});
|
||||
await settle(
|
||||
files.map(async fileName => {
|
||||
const fileSource = await fetch(fileName).then(r => r.text());
|
||||
const map = await getSourceMap(fileName, fileSource);
|
||||
cache[fileName] = { fileSource, map };
|
||||
})
|
||||
);
|
||||
return frames.map(frame => {
|
||||
const { functionName, fileName, lineNumber, columnNumber } = frame;
|
||||
let { map, fileSource } = cache[fileName] || {};
|
||||
if (map == null || lineNumber == null) {
|
||||
return frame;
|
||||
}
|
||||
const { source, line, column } = map.getOriginalPosition(
|
||||
lineNumber,
|
||||
columnNumber
|
||||
);
|
||||
const originalSource = source == null ? [] : map.getSource(source);
|
||||
return new StackFrame(
|
||||
functionName,
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
getLinesAround(lineNumber, contextLines, fileSource),
|
||||
functionName,
|
||||
source,
|
||||
line,
|
||||
column,
|
||||
getLinesAround(line, contextLines, originalSource)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export { map };
|
||||
export default map;
|
||||
78
packages/react-error-overlay/src/utils/parser.js
vendored
Normal file
78
packages/react-error-overlay/src/utils/parser.js
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
// @flow
|
||||
import StackFrame from './stack-frame';
|
||||
|
||||
const regexExtractLocation = /\(?(.+?)(?::(\d+))?(?::(\d+))?\)?$/;
|
||||
|
||||
function extractLocation(token: string): [string, number, number] {
|
||||
return regexExtractLocation.exec(token).slice(1).map(v => {
|
||||
const p = Number(v);
|
||||
if (!isNaN(p)) {
|
||||
return p;
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
const regexValidFrame_Chrome = /^\s*(at|in)\s.+(:\d+)/;
|
||||
const regexValidFrame_FireFox = /(^|@)\S+:\d+|.+line\s+\d+\s+>\s+(eval|Function).+/;
|
||||
|
||||
function parseStack(stack: string[]): StackFrame[] {
|
||||
const frames = stack
|
||||
.filter(
|
||||
e => regexValidFrame_Chrome.test(e) || regexValidFrame_FireFox.test(e)
|
||||
)
|
||||
.map(e => {
|
||||
if (regexValidFrame_FireFox.test(e)) {
|
||||
// Strip eval, we don't care about it
|
||||
let isEval = false;
|
||||
if (/ > (eval|Function)/.test(e)) {
|
||||
e = e.replace(
|
||||
/ line (\d+)(?: > eval line \d+)* > (eval|Function):\d+:\d+/g,
|
||||
':$1'
|
||||
);
|
||||
isEval = true;
|
||||
}
|
||||
const data = e.split(/[@]/g);
|
||||
const last = data.pop();
|
||||
return new StackFrame(
|
||||
data.join('@') || (isEval ? 'eval' : null),
|
||||
...extractLocation(last)
|
||||
);
|
||||
} else {
|
||||
// Strip eval, we don't care about it
|
||||
if (e.indexOf('(eval ') !== -1) {
|
||||
e = e.replace(/(\(eval at [^()]*)|(\),.*$)/g, '');
|
||||
}
|
||||
if (e.indexOf('(at ') !== -1) {
|
||||
e = e.replace(/\(at /, '(');
|
||||
}
|
||||
const data = e.trim().split(/\s+/g).slice(1);
|
||||
const last = data.pop();
|
||||
return new StackFrame(data.join(' ') || null, ...extractLocation(last));
|
||||
}
|
||||
});
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an <code>Error</code>, or similar object, into a set of <code>StackFrame</code>s.
|
||||
* @alias parse
|
||||
*/
|
||||
function parseError(error: Error | string | string[]): StackFrame[] {
|
||||
if (error == null) {
|
||||
throw new Error('You cannot pass a null object.');
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return parseStack(error.split('\n'));
|
||||
}
|
||||
if (Array.isArray(error)) {
|
||||
return parseStack(error);
|
||||
}
|
||||
if (typeof error.stack === 'string') {
|
||||
return parseStack(error.stack.split('\n'));
|
||||
}
|
||||
throw new Error('The error you provided does not contain a stack trace.');
|
||||
}
|
||||
|
||||
export { parseError as parse };
|
||||
export default parseError;
|
||||
101
packages/react-error-overlay/src/utils/stack-frame.js
vendored
Normal file
101
packages/react-error-overlay/src/utils/stack-frame.js
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
//@flow
|
||||
|
||||
/** A container holding a script line. */
|
||||
class ScriptLine {
|
||||
/** The line number of this line of source. */
|
||||
lineNumber: number;
|
||||
/** The content (or value) of this line of source. */
|
||||
content: string;
|
||||
/** Whether or not this line should be highlighted. Particularly useful for error reporting with context. */
|
||||
highlight: boolean;
|
||||
|
||||
constructor(lineNumber: number, content: string, highlight: boolean = false) {
|
||||
this.lineNumber = lineNumber;
|
||||
this.content = content;
|
||||
this.highlight = highlight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of a stack frame.
|
||||
*/
|
||||
class StackFrame {
|
||||
functionName: string | null;
|
||||
fileName: string | null;
|
||||
lineNumber: number | null;
|
||||
columnNumber: number | null;
|
||||
|
||||
_originalFunctionName: string | null;
|
||||
_originalFileName: string | null;
|
||||
_originalLineNumber: number | null;
|
||||
_originalColumnNumber: number | null;
|
||||
|
||||
_scriptCode: ScriptLine[] | null;
|
||||
_originalScriptCode: ScriptLine[] | null;
|
||||
|
||||
constructor(
|
||||
functionName: string | null = null,
|
||||
fileName: string | null = null,
|
||||
lineNumber: number | null = null,
|
||||
columnNumber: number | null = null,
|
||||
scriptCode: ScriptLine[] | null = null,
|
||||
sourceFunctionName: string | null = null,
|
||||
sourceFileName: string | null = null,
|
||||
sourceLineNumber: number | null = null,
|
||||
sourceColumnNumber: number | null = null,
|
||||
sourceScriptCode: ScriptLine[] | null = null
|
||||
) {
|
||||
this.functionName = functionName;
|
||||
|
||||
this.fileName = fileName;
|
||||
this.lineNumber = lineNumber;
|
||||
this.columnNumber = columnNumber;
|
||||
|
||||
this._originalFunctionName = sourceFunctionName;
|
||||
this._originalFileName = sourceFileName;
|
||||
this._originalLineNumber = sourceLineNumber;
|
||||
this._originalColumnNumber = sourceColumnNumber;
|
||||
|
||||
this._scriptCode = scriptCode;
|
||||
this._originalScriptCode = sourceScriptCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this function.
|
||||
*/
|
||||
getFunctionName(): string | null {
|
||||
return this.functionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source of the frame.
|
||||
* This contains the file name, line number, and column number when available.
|
||||
*/
|
||||
getSource(): string {
|
||||
let str = '';
|
||||
if (this.fileName != null) {
|
||||
str += this.fileName + ':';
|
||||
}
|
||||
if (this.lineNumber != null) {
|
||||
str += this.lineNumber + ':';
|
||||
}
|
||||
if (this.columnNumber != null) {
|
||||
str += this.columnNumber + ':';
|
||||
}
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pretty version of this stack frame.
|
||||
*/
|
||||
toString(): string {
|
||||
const f = this.getFunctionName();
|
||||
if (f == null) {
|
||||
return this.getSource();
|
||||
}
|
||||
return `${f} (${this.getSource()})`;
|
||||
}
|
||||
}
|
||||
|
||||
export { StackFrame, ScriptLine };
|
||||
export default StackFrame;
|
||||
102
packages/react-error-overlay/src/utils/unmapper.js
vendored
Normal file
102
packages/react-error-overlay/src/utils/unmapper.js
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
// @flow
|
||||
import StackFrame from './stack-frame';
|
||||
import { getSourceMap } from './getSourceMap';
|
||||
import { getLinesAround } from './getLinesAround';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Turns a set of mapped <code>StackFrame</code>s back into their generated code position and enhances them with code.
|
||||
* @param {string} fileUri The URI of the <code>bundle.js</code> file.
|
||||
* @param {StackFrame[]} frames A set of <code>StackFrame</code>s which are already mapped and missing their generated positions.
|
||||
* @param {number} [fileContents=3] The number of lines to provide before and after the line specified in the <code>StackFrame</code>.
|
||||
*/
|
||||
async function unmap(
|
||||
_fileUri: string | { uri: string, contents: string },
|
||||
frames: StackFrame[],
|
||||
contextLines: number = 3
|
||||
): Promise<StackFrame[]> {
|
||||
let fileContents = typeof _fileUri === 'object' ? _fileUri.contents : null;
|
||||
let fileUri = typeof _fileUri === 'object' ? _fileUri.uri : _fileUri;
|
||||
if (fileContents == null) {
|
||||
fileContents = await fetch(fileUri).then(res => res.text());
|
||||
}
|
||||
const map = await getSourceMap(fileUri, fileContents);
|
||||
return frames.map(frame => {
|
||||
const {
|
||||
functionName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
_originalLineNumber,
|
||||
} = frame;
|
||||
if (_originalLineNumber != null) {
|
||||
return frame;
|
||||
}
|
||||
let { fileName } = frame;
|
||||
if (fileName) {
|
||||
fileName = path.normalize(fileName);
|
||||
}
|
||||
if (fileName == null) {
|
||||
return frame;
|
||||
}
|
||||
const fN: string = fileName;
|
||||
const splitCache1: any = {}, splitCache2: any = {}, splitCache3: any = {};
|
||||
const source = map
|
||||
.getSources()
|
||||
.map(s => s.replace(/[\\]+/g, '/'))
|
||||
.filter(s => {
|
||||
s = path.normalize(s);
|
||||
return s.indexOf(fN) === s.length - fN.length;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
let a2 = splitCache1[a] || (splitCache1[a] = a.split(path.sep)),
|
||||
b2 = splitCache1[b] || (splitCache1[b] = b.split(path.sep));
|
||||
return Math.sign(a2.length - b2.length);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
let a2 = splitCache2[a] || (splitCache2[a] = a.split('node_modules')),
|
||||
b2 = splitCache2[b] || (splitCache2[b] = b.split('node_modules'));
|
||||
return Math.sign(a2.length - b2.length);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
let a2 = splitCache3[a] || (splitCache3[a] = a.split('~')),
|
||||
b2 = splitCache3[b] || (splitCache3[b] = b.split('~'));
|
||||
return Math.sign(a2.length - b2.length);
|
||||
});
|
||||
if (source.length < 1 || lineNumber == null) {
|
||||
return new StackFrame(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
functionName,
|
||||
fN,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
null
|
||||
);
|
||||
}
|
||||
const { line, column } = map.getGeneratedPosition(
|
||||
source[0],
|
||||
lineNumber,
|
||||
// $FlowFixMe
|
||||
columnNumber
|
||||
);
|
||||
const originalSource = map.getSource(source[0]);
|
||||
return new StackFrame(
|
||||
functionName,
|
||||
fileUri,
|
||||
line,
|
||||
column || null,
|
||||
getLinesAround(line, contextLines, fileContents || []),
|
||||
functionName,
|
||||
fN,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
getLinesAround(lineNumber, contextLines, originalSource)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export { unmap };
|
||||
export default unmap;
|
||||
@@ -55,7 +55,7 @@ module.exports = {
|
||||
// We ship a few polyfills by default:
|
||||
require.resolve('./polyfills'),
|
||||
// Errors should be considered fatal in development
|
||||
require.resolve('react-dev-utils/crashOverlay'),
|
||||
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
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"postcss-loader": "1.3.3",
|
||||
"promise": "7.1.1",
|
||||
"react-dev-utils": "^0.5.2",
|
||||
"react-error-overlay": "^0.0.0",
|
||||
"style-loader": "0.16.1",
|
||||
"url-loader": "0.5.8",
|
||||
"webpack": "2.4.1",
|
||||
|
||||
@@ -99,6 +99,10 @@ fi
|
||||
# We removed the postinstall, so do it manually
|
||||
./node_modules/.bin/lerna bootstrap --concurrency=1
|
||||
|
||||
cd packages/react-error-overlay/
|
||||
npm run build:prod
|
||||
cd ../..
|
||||
|
||||
# ******************************************************************************
|
||||
# First, pack and install create-react-app.
|
||||
# ******************************************************************************
|
||||
|
||||
@@ -82,6 +82,10 @@ fi
|
||||
# We removed the postinstall, so do it manually
|
||||
./node_modules/.bin/lerna bootstrap --concurrency=1
|
||||
|
||||
cd packages/react-error-overlay/
|
||||
npm run build:prod
|
||||
cd ../..
|
||||
|
||||
# ******************************************************************************
|
||||
# First, pack react-scripts and create-react-app so we can use them.
|
||||
# ******************************************************************************
|
||||
|
||||
@@ -98,7 +98,16 @@ then
|
||||
fi
|
||||
|
||||
# Lint own code
|
||||
./node_modules/.bin/eslint --max-warnings 0 .
|
||||
./node_modules/.bin/eslint --max-warnings 0 packages/babel-preset-react-app/
|
||||
./node_modules/.bin/eslint --max-warnings 0 packages/create-react-app/
|
||||
./node_modules/.bin/eslint --max-warnings 0 packages/eslint-config-react-app/
|
||||
./node_modules/.bin/eslint --max-warnings 0 packages/react-dev-utils/
|
||||
./node_modules/.bin/eslint --max-warnings 0 packages/react-scripts/
|
||||
cd packages/react-error-overlay/
|
||||
./node_modules/.bin/eslint --max-warnings 0 src/
|
||||
npm test
|
||||
npm run build:prod
|
||||
cd ../..
|
||||
|
||||
# ******************************************************************************
|
||||
# First, test the create-react-app development environment.
|
||||
|
||||
Reference in New Issue
Block a user