Merge pull request #418 from reduxjs/trace-stack

This commit is contained in:
Mihail Diordiev 2018-12-18 22:49:09 +02:00 committed by GitHub
commit fb9d826f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 4677 additions and 124 deletions

View File

@ -1,6 +1,7 @@
{
"private": true,
"devDependencies": {
"babel-eslint": "^10.0.0",
"lerna": "3.4.2"
},
"scripts": {

View File

@ -50,7 +50,8 @@ export default function configureStore(initialState) {
- **pauseActionType** *string* - if specified, whenever `pauseRecording(false)` lifted action is dispatched and there are actions in the history log, will add this action type. If not specified, will commit when paused.
- **shouldStartLocked** *boolean* - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`.
- **shouldHotReload** *boolean* - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`.
- **shouldIncludeCallstack** *boolean* - if set to `true`, will include callstack for every dispatched action. Default to `false`.
- **trace** *boolean* or *function* - if set to `true`, will include stack trace for every dispatched action. You can use a function (with action object as argument) which should return `new Error().stack` string, getting the stack outside of reducers. Default to `false`.
- **traceLimit** *number* - maximum stack trace frames to be stored (in case `trace` option was provided as `true`). By default it's `10`. If `trace` option is a function, `traceLimit` will have no effect, that should be handled there like so: `trace: () => new Error().stack.split('\n').slice(0, limit+1).join('\n')` (`+1` is needed for Chrome where's an extra 1st frame for `Error\n`).
### License

View File

@ -1,6 +1,6 @@
{
"name": "redux-devtools-instrument",
"version": "1.9.3",
"version": "1.9.4",
"description": "Redux DevTools instrumentation",
"main": "lib/instrument.js",
"scripts": {

View File

@ -23,7 +23,7 @@ export const ActionTypes = {
* Action creators to change the History state.
*/
export const ActionCreators = {
performAction(action, shouldIncludeCallstack) {
performAction(action, trace, traceLimit, toExcludeFromTrace) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
@ -38,10 +38,35 @@ export const ActionCreators = {
);
}
return {
type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now(),
stack: shouldIncludeCallstack ? Error().stack : undefined
};
let stack;
if (trace) {
let extraFrames = 0;
if (typeof trace === 'function') {
stack = trace(action);
} else {
const error = Error();
let prevStackTraceLimit;
if (Error.captureStackTrace) {
if (Error.stackTraceLimit < traceLimit) {
prevStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = traceLimit;
}
Error.captureStackTrace(error, toExcludeFromTrace);
} else {
extraFrames = 3;
}
stack = error.stack;
if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit;
if (extraFrames || typeof Error.stackTraceLimit !== 'number' || Error.stackTraceLimit > traceLimit) {
const frames = stack.split('\n');
if (frames.length > traceLimit) {
stack = frames.slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0)).join('\n');
}
}
}
}
return { type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now(), stack };
},
reset() {
@ -188,8 +213,8 @@ function recomputeStates(
/**
* Lifts an app's action into an action on the lifted store.
*/
export function liftAction(action, shouldIncludeCallstack) {
return ActionCreators.performAction(action, shouldIncludeCallstack);
export function liftAction(action, trace, traceLimit, toExcludeFromTrace) {
return ActionCreators.performAction(action, trace, traceLimit, toExcludeFromTrace);
}
/**
@ -502,7 +527,7 @@ export function liftReducerWith(reducer, initialCommittedState, monitorReducer,
minInvalidatedStateIndex = 0;
// iterate through actions
liftedAction.nextLiftedState.forEach(action => {
actionsById[nextActionId] = liftAction(action, options.shouldIncludeCallstack);
actionsById[nextActionId] = liftAction(action, options.trace || options.shouldIncludeCallstack);
stagedActionIds.push(nextActionId);
nextActionId++;
});
@ -595,7 +620,8 @@ export function unliftState(liftedState) {
*/
export function unliftStore(liftedStore, liftReducer, options) {
let lastDefinedState;
const { shouldIncludeCallstack } = options;
const trace = options.trace || options.shouldIncludeCallstack;
const traceLimit = options.traceLimit || 10;
function getState() {
const state = unliftState(liftedStore.getState());
@ -605,15 +631,17 @@ export function unliftStore(liftedStore, liftReducer, options) {
return lastDefinedState;
}
function dispatch(action) {
liftedStore.dispatch(liftAction(action, trace, traceLimit, dispatch));
return action;
}
return {
...liftedStore,
liftedStore,
dispatch(action) {
liftedStore.dispatch(liftAction(action, shouldIncludeCallstack));
return action;
},
dispatch,
getState,

View File

@ -686,6 +686,178 @@ describe('instrument', () => {
});
});
describe('trace option', () => {
let monitoredStore;
let monitoredLiftedStore;
let exportedState;
it('should not include stack trace', () => {
monitoredStore = createStore(counter, instrument());
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBe(undefined);
});
it('should include stack trace', () => {
monitoredStore = createStore(counter, instrument(undefined, { trace: true }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/^Error/);
expect(exportedState.actionsById[1].stack).toNotMatch(/instrument.js/);
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/');
expect(exportedState.actionsById[1].stack.split('\n').length).toBe(10 + 1); // +1 is for `Error\n`
});
it('should include only 3 frames for stack trace', () => {
function fn1() {
monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 3 }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/at fn1 /);
expect(exportedState.actionsById[1].stack).toMatch(/at fn2 /);
expect(exportedState.actionsById[1].stack).toMatch(/at fn3 /);
expect(exportedState.actionsById[1].stack).toNotMatch(/at fn4 /);
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack.split('\n').length).toBe(3 + 1);
}
function fn2() { return fn1(); }
function fn3() { return fn2(); }
function fn4() { return fn3(); }
fn4();
});
it('should force traceLimit value of 3 when Error.stackTraceLimit is 10', () => {
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 10;
function fn1() {
monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 3 }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/at fn1 /);
expect(exportedState.actionsById[1].stack).toMatch(/at fn2 /);
expect(exportedState.actionsById[1].stack).toMatch(/at fn3 /);
expect(exportedState.actionsById[1].stack).toNotMatch(/at fn4 /);
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack.split('\n').length).toBe(3 + 1);
}
function fn2() { return fn1(); }
function fn3() { return fn2(); }
function fn4() { return fn3(); }
fn4();
Error.stackTraceLimit = stackTraceLimit;
});
it('should force traceLimit value of 5 even when Error.stackTraceLimit is 2', () => {
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 2;
monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 5 }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
Error.stackTraceLimit = stackTraceLimit;
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/^Error/);
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/');
expect(exportedState.actionsById[1].stack.split('\n').length).toBe(5 + 1);
});
it('should force default limit of 10 even when Error.stackTraceLimit is 3', () => {
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 3;
function fn1() {
monitoredStore = createStore(counter, instrument(undefined, { trace: true }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
Error.stackTraceLimit = stackTraceLimit;
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/at fn1 /);
expect(exportedState.actionsById[1].stack).toMatch(/at fn2 /);
expect(exportedState.actionsById[1].stack).toMatch(/at fn3 /);
expect(exportedState.actionsById[1].stack).toMatch(/at fn4 /);
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack.split('\n').length).toBe(10 + 1);
}
function fn2() { return fn1(); }
function fn3() { return fn2(); }
function fn4() { return fn3(); }
fn4();
});
it('should include 3 extra frames when Error.captureStackTrace not suported', () => {
const captureStackTrace = Error.captureStackTrace;
Error.captureStackTrace = undefined;
monitoredStore = createStore(counter, instrument(undefined, { trace: true, traceLimit: 5 }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
Error.captureStackTrace = captureStackTrace;
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toMatch(/^Error/);
expect(exportedState.actionsById[1].stack).toContain('instrument.js');
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/');
expect(exportedState.actionsById[1].stack.split('\n').length).toBe(5 + 3 + 1);
});
it('should get stack trace from a function', () => {
const traceFn = () => new Error().stack;
monitoredStore = createStore(counter, instrument(undefined, { trace: traceFn }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toContain('at Object.performAction');
expect(exportedState.actionsById[1].stack).toContain('instrument.js');
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/');
});
it('should get stack trace inside setTimeout using a function', (done) => {
const stack = new Error().stack;
setTimeout(() => {
const traceFn = () => stack + new Error().stack;
monitoredStore = createStore(counter, instrument(undefined, { trace: traceFn }));
monitoredLiftedStore = monitoredStore.liftedStore;
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
expect(exportedState.actionsById[0].stack).toBe(undefined);
expect(exportedState.actionsById[1].stack).toBeA('string');
expect(exportedState.actionsById[1].stack).toContain('at Object.performAction');
expect(exportedState.actionsById[1].stack).toContain('instrument.js');
expect(exportedState.actionsById[1].stack).toContain('instrument.spec.js');
expect(exportedState.actionsById[1].stack).toContain('/mocha/');
done();
});
});
});
describe('Import State', () => {
let monitoredStore;
let monitoredLiftedStore;
@ -736,8 +908,8 @@ describe('instrument', () => {
expect(importMonitoredLiftedStore.getState()).toEqual(expectedImportedState);
});
it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
it('should include stack trace', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { trace: true }));
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
@ -801,8 +973,8 @@ describe('instrument', () => {
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});
it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
it('should include stack trace', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { trace: true }));
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });

View File

@ -0,0 +1,13 @@
{
"presets": [ ["env", { "modules": "commonjs" }], "react", "flow" ],
"plugins": [
["transform-runtime", {
"polyfill": false,
"regenerator": true
}],
"transform-class-properties",
"transform-object-rest-spread",
"add-module-exports",
"transform-decorators-legacy"
]
}

View File

@ -0,0 +1,5 @@
node_modules
build
dev
dist
lib

View File

@ -0,0 +1,34 @@
{
"extends": "plugin:flowtype/recommended",
"globals": {
"chrome": true
},
"env": {
"jest": true,
"browser": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"parser": "babel-eslint",
"rules": {
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"react/sort-comp": 0,
"react/jsx-quotes": 0,
"eol-last": 0,
"no-unused-vars": 0,
"no-console": 1,
"comma-dangle": 0
},
"plugins": [
"react",
"flowtype"
]
}

View File

@ -0,0 +1,19 @@
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,36 @@
Redux DevTools Stack Trace Monitor
==================================
Submonitor for Redux DevTools inspector to show stack traces. Based on [`react-error-overlay`](https://github.com/facebook/create-react-app/tree/master/packages/react-error-overlay) and the contribution of [Mark Erikson](https://github.com/markerikson) in [the PR from `remotedev-app`](https://github.com/zalmoxisus/remotedev-app/pull/43/).
It's integrated in Redux DevTools browser extension. To use it separately with [`redux-devtools`](https://github.com/reduxjs/redux-devtools/packages/redux-devtools) and [`redux-devtools-inspector`](https://github.com/reduxjs/redux-devtools/packages/redux-devtools-inspector) according to [Walkthrough](https://github.com/reduxjs/redux-devtools/blob/master/docs/Walkthrough.md):
##### `containers/DevTools.js`
```js
import React from 'react';
import { createDevTools } from 'redux-devtools';
import Inspector from 'redux-devtools-inspector';
import TraceMonitor from 'redux-devtools-trace-monitor';
export default createDevTools(
<Inspector
tabs: defaultTabs => [...defaultTabs, { name: 'Trace', component: TraceMonitor }]
/>
);
```
##### `store/configureStore.js`
```js
// ...
const enhancer = compose(
// ...
DevTools.instrument({ trace: true })
);
// ...
```
### License
MIT

View File

@ -0,0 +1,63 @@
{
"name": "redux-devtools-trace-monitor",
"version": "0.1.0",
"description": "Submonitor for Redux DevTools inspector to show stack traces.",
"repository": "https://github.com/reduxjs/redux-devtools",
"homepage": "https://github.com/reduxjs/redux-devtools",
"author": "Mark Erikson <mark@isquaredsoftware.com>",
"contributors": [
"Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)"
],
"license": "MIT",
"main": "lib/StackTraceTab.js",
"files": [
"lib"
],
"scripts": {
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"lint": "eslint src test",
"lint:fix": "eslint --fix src test",
"test": "jest --no-cache",
"prepare": "npm run clean && npm run build",
"prepublishOnly": "npm run lint && npm run test"
},
"devDependencies": {
"babel-cli": "^6.10.1",
"babel-eslint": "^10.0.0",
"babel-loader": "^6.2.4",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-react-transform": "^2.0.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.9.0",
"babel-preset-es2015-loose": "^7.0.0",
"babel-preset-react": "^6.5.0",
"babel-preset-react-app": "^3.1.2",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.11.6",
"babel-runtime": "^6.23.0",
"enzyme": "^3.0.0",
"enzyme-adapter-react-15": "1.2.0",
"enzyme-to-json": "^3.3.0",
"eslint": "^5.0.0",
"eslint-plugin-flowtype": "3.2.0",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsx-a11y": "6.1.1",
"eslint-plugin-react": "7.11.1",
"jest": "^23.6.0",
"react-addons-test-utils": "^15.4.0",
"react-dom": "^15.4.0",
"react-test-renderer": "^15.3.2",
"rimraf": "^2.5.2"
},
"dependencies": {
"@babel/code-frame": "^7.0.0",
"anser": "^1.4.7",
"chalk": "^2.4.1",
"html-entities": "^1.2.1",
"react": "^15.4.0",
"redux-devtools-themes": "^1.0.0",
"settle-promise": "^1.0.0"
}
}

View File

@ -0,0 +1,114 @@
import React, { Component, PropTypes } from 'react';
import {getStackFrames} from './react-error-overlay/utils/getStackFrames';
import StackTrace from './react-error-overlay/containers/StackTrace';
import openFile from './openFile';
const rootStyle = {padding: '5px 10px'};
export default class StackTraceTab extends Component {
constructor(props) {
super(props);
this.state = {
stackFrames: []
};
}
componentDidMount() {
// console.log("StackTraceTab mounted");
this.checkForStackTrace();
}
componentDidUpdate(prevProps) {
const {action, actions} = prevProps;
if(action !== this.props.action || actions !== this.props.actions) {
this.checkForStackTrace();
}
}
checkForStackTrace() {
const {action, actions: liftedActionsById} = this.props;
if(!action) {
return;
}
const liftedActions = Object.values(liftedActionsById);
const liftedAction = liftedActions.find(liftedAction => liftedAction.action === action);
if(liftedAction && typeof liftedAction.stack === 'string') {
const deserializedError = Object.assign(new Error(), {stack: liftedAction.stack});
getStackFrames(deserializedError)
.then(stackFrames => {
/* eslint-disable no-console */
if (process.env.NODE_ENV === 'development') console.log('Stack frames: ', stackFrames);
/* eslint-enable no-console */
this.setState({stackFrames, currentError: deserializedError});
});
}
else {
this.setState({
stackFrames: [],
showDocsLink: liftedAction.action && liftedAction.action.type && liftedAction.action.type !== '@@INIT'
});
}
}
onStackLocationClicked = (fileLocation = {}) => {
// console.log("Stack location args: ", ...args);
const {fileName, lineNumber} = fileLocation;
if(fileName && lineNumber) {
const matchingStackFrame = this.state.stackFrames.find(stackFrame => {
const matches = (
(stackFrame._originalFileName === fileName && stackFrame._originalLineNumber === lineNumber) ||
(stackFrame.fileName === fileName && stackFrame.lineNumber === lineNumber)
);
return matches;
});
// console.log("Matching stack frame: ", matchingStackFrame);
if(matchingStackFrame) {
/*
const frameIndex = this.state.stackFrames.indexOf(matchingStackFrame);
const originalStackFrame = parsedFramesNoSourcemaps[frameIndex];
console.log("Original stack frame: ", originalStackFrame);
*/
openFile(fileName, lineNumber, matchingStackFrame);
}
}
}
openDocs = (e) => {
e.stopPropagation();
window.open('https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Features/Trace.md');
}
render() {
const {stackFrames, showDocsLink} = this.state;
if (showDocsLink) {
return (
<div style={rootStyle}>
To enable tracing action calls, you should set `trace` option to `true` for Redux DevTools enhancer.
Refer to <a href="#" onClick={this.openDocs}>this page</a> for more details.
</div>
);
}
return (
<div style={rootStyle}>
<StackTrace
stackFrames={stackFrames}
errorName={"N/A"}
contextSize={3}
editorHandler={this.onStackLocationClicked}
/>
</div>
);
}
}

View File

@ -0,0 +1,92 @@
const isFF = navigator.userAgent.indexOf('Firefox') !== -1;
function openResource(fileName, lineNumber, stackFrame) {
const adjustedLineNumber = Math.max(lineNumber - 1, 0);
chrome.devtools.panels.openResource(fileName, adjustedLineNumber, (result) => {
//console.log("openResource callback args: ", callbackArgs);
if(result.isError) {
const {fileName: finalFileName, lineNumber: finalLineNumber} = stackFrame;
const adjustedLineNumber = Math.max(finalLineNumber - 1, 0);
chrome.devtools.panels.openResource(finalFileName, adjustedLineNumber, (result) => {
// console.log("openResource result: ", result);
});
}
});
}
function openAndCloseTab(url) {
chrome.tabs.create({ url }, tab => {
const removeTab = () => {
chrome.windows.onFocusChanged.removeListener(removeTab);
if (tab && tab.id) {
chrome.tabs.remove(tab.id, () => {
if(chrome.runtime.lastError) console.log(chrome.runtime.lastError); // eslint-disable-line no-console
else if (chrome.devtools && chrome.devtools.inspectedWindow) {
chrome.tabs.update(chrome.devtools.inspectedWindow.tabId, {active: true});
}
});
}
};
if (chrome.windows) chrome.windows.onFocusChanged.addListener(removeTab);
});
}
function openInIframe(url) {
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style = 'display:none';
document.body.appendChild(iframe);
setTimeout(() => iframe.parentNode.removeChild(iframe), 3000);
}
function openInEditor(editor, path, stackFrame) {
const projectPath = path.replace(/\/$/, '');
const file = stackFrame._originalFileName || stackFrame.finalFileName || stackFrame.fileName || '';
let filePath = /^https?:\/\//.test(file) ? file.replace(/^https?:\/\/[^\/]*/, '') : file.replace(/^\w+:\/\//, '');
filePath = filePath.replace(/^\/~\//, '/node_modules/');
const line = stackFrame._originalLineNumber || stackFrame.lineNumber || '0';
const column = stackFrame._originalColumnNumber || stackFrame.columnNumber || '0';
let url;
switch (editor) {
case 'vscode': case 'code':
url = `vscode://file/${projectPath}${filePath}:${line}:${column}`; break;
case 'atom':
url = `atom://core/open/file?filename=${projectPath}${filePath}&line=${line}&column=${column}`; break;
case 'webstorm': case 'phpstorm': case 'idea':
url = `${editor}://open?file=${projectPath}${filePath}&line=${line}&column=${column}`; break;
default: // sublime, emacs, macvim, textmate + custom like https://github.com/eclemens/atom-url-handler
url = `${editor}://open/?url=file://${projectPath}${filePath}&line=${line}&column=${column}`;
}
if (process.env.NODE_ENV === 'development') console.log(url); // eslint-disable-line no-console
if (chrome.devtools && !isFF) {
if (chrome.tabs) openAndCloseTab(url);
else window.open(url);
} else {
openInIframe(url);
}
}
export default function openFile(fileName, lineNumber, stackFrame) {
if (process.env.NODE_ENV === 'development') console.log(fileName, lineNumber, stackFrame); // eslint-disable-line no-console
if (!chrome || !chrome.storage) return; // TODO: Pass editor settings for using outside of browser extension
const storage = isFF ? chrome.storage.local : chrome.storage.sync || chrome.storage.local;
storage.get(['useEditor', 'editor', 'projectPath'], function({ useEditor, editor, projectPath }) {
if (useEditor && projectPath && typeof editor === 'string' && /^\w{1,30}$/.test(editor)) {
openInEditor(editor.toLowerCase(), projectPath, stackFrame);
} else {
if (chrome.devtools && chrome.devtools.panels && chrome.devtools.panels.openResource) {
openResource(fileName, lineNumber, stackFrame);
} else if (chrome.runtime && (chrome.runtime.openOptionsPage || isFF)) {
if (chrome.devtools && isFF) {
chrome.devtools.inspectedWindow.eval('confirm("Set the editor to open the file in?")', result => {
if (!result) return;
chrome.runtime.sendMessage({ type: 'OPEN_OPTIONS' });
});
} else if (confirm('Set the editor to open the file in?')) {
chrome.runtime.openOptionsPage();
}
}
}
});
}

View File

@ -0,0 +1 @@
export const toExclude = /chrome-extension:\/\/|moz-extension:\/\//;

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React from 'react';
const preStyle = {
position: 'relative',
display: 'block',
backgroundColor: '#000',
padding: '0.5em',
marginTop: '0.5em',
marginBottom: '0.5em',
overflowX: 'auto',
whiteSpace: 'pre-wrap',
borderRadius: '0.25rem',
};
const codeStyle = {
fontFamily: 'Consolas, Menlo, monospace',
};
type CodeBlockPropsType = {|
main: boolean,
codeHTML: string,
|};
function CodeBlock(props: CodeBlockPropsType) {
const codeBlock = { __html: props.codeHTML };
return (
<pre style={preStyle}>
<code style={codeStyle} dangerouslySetInnerHTML={codeBlock} />
</pre>
);
}
export default CodeBlock;

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React, { Component } from 'react';
import { nicinabox as theme } from 'redux-devtools-themes';
import type { Element as ReactElement } from 'react';
const _collapsibleStyle = {
color: theme.base06,
backgroundColor: theme.base01,
cursor: 'pointer',
border: 'none',
display: 'block',
width: '100%',
textAlign: 'left',
fontSize: '1em',
padding: '0px 5px',
lineHeight: '1.5',
};
const collapsibleCollapsedStyle = {
..._collapsibleStyle,
marginBottom: '1.5em',
};
const collapsibleExpandedStyle = {
..._collapsibleStyle,
marginBottom: '0.6em',
};
type Props = {|
children: ReactElement<any>[],
|};
type State = {|
collapsed: boolean,
|};
class Collapsible extends Component<Props, State> {
state = {
collapsed: undefined,
};
toggleCollapsed = () => {
this.setState(state => ({
collapsed: !this.isCollapsed(state),
}));
};
isCollapsed = (state) => (
state.collapsed === undefined ? this.props.collapsedByDefault : state.collapsed
);
render() {
const count = this.props.children.length;
const collapsed = this.isCollapsed(this.state);
return (
<div>
<button
onClick={this.toggleCollapsed}
style={
collapsed ? collapsibleCollapsedStyle : collapsibleExpandedStyle
}
>
{(collapsed ? '▶' : '▼') +
` ${count} stack frames were ` +
(collapsed ? 'collapsed.' : 'expanded.')}
</button>
<div style={{ display: collapsed ? 'none' : 'block' }}>
{this.props.children}
<button
onClick={this.toggleCollapsed}
style={collapsibleExpandedStyle}
>
{`${count} stack frames were expanded.`}
</button>
</div>
</div>
);
}
}
export default Collapsible;

View File

@ -0,0 +1,191 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React, { Component } from 'react';
import CodeBlock from './StackFrameCodeBlock';
import { getPrettyURL } from '../utils/getPrettyURL';
import { nicinabox as theme } from 'redux-devtools-themes';
import type { StackFrame as StackFrameType } from '../utils/stack-frame';
import type { ErrorLocation } from '../utils/parseCompileError';
const linkStyle = {
fontSize: '0.9em',
marginBottom: '0.9em',
};
const anchorStyle = {
textDecoration: 'none',
color: theme.base05,
cursor: 'pointer',
};
const codeAnchorStyle = {
cursor: 'pointer',
};
const toggleStyle = {
marginBottom: '1.5em',
color: theme.base05,
cursor: 'pointer',
border: 'none',
display: 'block',
width: '100%',
textAlign: 'left',
background: 'transparent',
fontFamily: 'Consolas, Menlo, monospace',
fontSize: '1em',
padding: '0px',
lineHeight: '1.5',
};
type Props = {|
frame: StackFrameType,
contextSize: number,
critical: boolean,
showCode: boolean,
editorHandler: (errorLoc: ErrorLocation) => void,
|};
type State = {|
compiled: boolean,
|};
class StackFrame extends Component<Props, State> {
state = {
compiled: false,
};
toggleCompiled = () => {
this.setState(state => ({
compiled: !state.compiled,
}));
};
getErrorLocation(): ErrorLocation | null {
const {
_originalFileName: fileName,
_originalLineNumber: lineNumber,
} = this.props.frame;
// Unknown file
if (!fileName) {
return null;
}
// e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1"
const isInternalWebpackBootstrapCode = fileName.trim().indexOf(' ') !== -1;
if (isInternalWebpackBootstrapCode) {
return null;
}
// Code is in a real file
return { fileName, lineNumber: lineNumber || 1 };
}
editorHandler = () => {
const errorLoc = this.getErrorLocation();
if (!errorLoc) {
return;
}
this.props.editorHandler(errorLoc);
};
onKeyDown = (e: SyntheticKeyboardEvent<>) => {
if (e.key === 'Enter') {
this.editorHandler();
}
};
render() {
const { frame, contextSize, critical, showCode } = this.props;
const {
fileName,
lineNumber,
columnNumber,
_scriptCode: scriptLines,
_originalFileName: sourceFileName,
_originalLineNumber: sourceLineNumber,
_originalColumnNumber: sourceColumnNumber,
_originalScriptCode: sourceLines,
} = frame;
const functionName = frame.getFunctionName();
const compiled = this.state.compiled;
const url = getPrettyURL(
sourceFileName,
sourceLineNumber,
sourceColumnNumber,
fileName,
lineNumber,
columnNumber,
compiled
);
let codeBlockProps = null;
if (showCode) {
if (
compiled &&
scriptLines &&
scriptLines.length !== 0 &&
lineNumber != null
) {
codeBlockProps = {
lines: scriptLines,
lineNum: lineNumber,
columnNum: columnNumber,
contextSize,
main: critical,
};
} else if (
!compiled &&
sourceLines &&
sourceLines.length !== 0 &&
sourceLineNumber != null
) {
codeBlockProps = {
lines: sourceLines,
lineNum: sourceLineNumber,
columnNum: sourceColumnNumber,
contextSize,
main: critical,
};
}
}
const canOpenInEditor =
this.getErrorLocation() !== null && this.props.editorHandler !== null;
return (
<div>
<div>{functionName}</div>
<div style={linkStyle}>
<span
style={canOpenInEditor ? anchorStyle : null}
onClick={canOpenInEditor ? this.editorHandler : null}
onKeyDown={canOpenInEditor ? this.onKeyDown : null}
tabIndex={canOpenInEditor ? '0' : null}
>
{url}
</span>
</div>
{codeBlockProps && (
<span>
<span
onClick={canOpenInEditor ? this.editorHandler : null}
style={canOpenInEditor ? codeAnchorStyle : null}
>
<CodeBlock {...codeBlockProps} />
</span>
<button style={toggleStyle} onClick={this.toggleCompiled}>
{'View ' + (compiled ? 'source' : 'compiled')}
</button>
</span>
)}
</div>
);
}
}
export default StackFrame;

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React from 'react';
import CodeBlock from '../components/CodeBlock';
import { applyStyles } from '../utils/dom/css';
import { absolutifyCaret } from '../utils/dom/absolutifyCaret';
import type { ScriptLine } from '../utils/stack-frame';
import generateAnsiHTML from '../utils/generateAnsiHTML';
import { codeFrameColumns } from '@babel/code-frame';
import { nicinabox as theme } from 'redux-devtools-themes';
type StackFrameCodeBlockPropsType = {|
lines: ScriptLine[],
lineNum: number,
columnNum: ?number,
contextSize: number,
main: boolean,
|};
// Exact type workaround for spread operator.
// See: https://github.com/facebook/flow/issues/2405
type Exact<T> = $Shape<T>;
function StackFrameCodeBlock(props: Exact<StackFrameCodeBlockPropsType>) {
const { lines, lineNum, columnNum, contextSize, main } = props;
const sourceCode = [];
let whiteSpace = Infinity;
lines.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;
}
});
lines.forEach(function(e) {
let { content: text } = e;
const { lineNumber: line } = e;
if (isFinite(whiteSpace)) {
text = text.substring(whiteSpace);
}
sourceCode[line - 1] = text;
});
const ansiHighlight = codeFrameColumns(
sourceCode.join('\n'),
{
start: {
line: lineNum,
column:
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);
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, {'background-color': main ? theme.base02 : theme.base01});
// eslint-disable-next-line
break oLoop;
}
}
return <CodeBlock main={main} codeHTML={code.innerHTML} />;
}
export default StackFrameCodeBlock;

View File

@ -0,0 +1,99 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React, { Component } from 'react';
import StackFrame from './StackFrame';
import Collapsible from '../components/Collapsible';
import { isInternalFile } from '../utils/isInternalFile';
import { isBultinErrorName } from '../utils/isBultinErrorName';
import type { StackFrame as StackFrameType } from '../utils/stack-frame';
import type { ErrorLocation } from '../utils/parseCompileError';
const traceStyle = {
fontSize: '1em',
flex: '0 1 auto',
minHeight: '0px',
overflow: 'auto',
};
type Props = {|
stackFrames: StackFrameType[],
errorName: string,
contextSize: number,
editorHandler: (errorLoc: ErrorLocation) => void,
|};
class StackTrace extends Component<Props> {
renderFrames() {
const { stackFrames, errorName, contextSize, editorHandler } = this.props;
const renderedFrames = [];
let hasReachedAppCode = false,
currentBundle = [],
bundleCount = 0,
anyNodeExpanded = false;
stackFrames.forEach((frame, index) => {
const { fileName, _originalFileName: sourceFileName } = frame;
const isInternalUrl = isInternalFile(sourceFileName, fileName);
const isThrownIntentionally = !isBultinErrorName(errorName);
const shouldCollapse =
isInternalUrl && (isThrownIntentionally || hasReachedAppCode);
if (!shouldCollapse) {
anyNodeExpanded = true;
}
if (!isInternalUrl) {
hasReachedAppCode = true;
}
const frameEle = (
<StackFrame
key={'frame-' + index}
frame={frame}
contextSize={contextSize}
critical={index === 0}
showCode={!shouldCollapse}
editorHandler={editorHandler}
/>
);
const lastElement = index === stackFrames.length - 1;
if (shouldCollapse) {
currentBundle.push(frameEle);
}
if (!shouldCollapse || lastElement) {
if (currentBundle.length === 1) {
renderedFrames.push(currentBundle[0]);
} else if (currentBundle.length > 1) {
bundleCount++;
renderedFrames.push(
<Collapsible collapsedByDefault={anyNodeExpanded} key={'bundle-' + bundleCount}>
{currentBundle}
</Collapsible>
);
}
currentBundle = [];
}
if (!shouldCollapse) {
renderedFrames.push(frameEle);
}
});
return renderedFrames;
}
render() {
return <div style={traceStyle}>{this.renderFrames()}</div>;
}
}
export default StackTrace;

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @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';
c.style.marginTop = '-7px';
// $FlowFixMe
removeNextBr(component, c);
}
}
export { absolutifyCaret };

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @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 };

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import Anser from 'anser';
import { nicinabox as theme } from 'redux-devtools-themes';
import { AllHtmlEntities as Entities } from 'html-entities';
var entities = new Entities();
var anserMap = {
'ansi-bright-black': theme.base03,
'ansi-bright-yellow': theme.base0A,
'ansi-yellow': theme.base0B,
'ansi-bright-green': theme.base0B,
'ansi-green': theme.base0F,
'ansi-bright-cyan': theme.base0D,
'ansi-cyan': theme.base0C,
'ansi-bright-red': theme.base09,
'ansi-red': theme.base0E,
'ansi-bright-magenta': theme.base0F,
'ansi-magenta': theme.base0E,
'ansi-white': theme.base00,
};
function generateAnsiHTML(txt: string): string {
var arr = new Anser().ansiToJson(entities.encode(txt), {
use_classes: true,
});
var result = '';
var open = false;
for (var index = 0; index < arr.length; ++index) {
var c = arr[index];
var content = c.content,
fg = c.fg;
var contentParts = content.split('\n');
for (var _index = 0; _index < contentParts.length; ++_index) {
if (!open) {
result += '<span data-ansi-line="true">';
open = true;
}
var part = contentParts[_index].replace('\r', '');
var color = anserMap[fg];
if (color != null) {
result += '<span style="color: ' + color + ';">' + part + '</span>';
} else {
if (fg != null) {
console.log('Missing color mapping:', fg); // eslint-disable-line no-console
}
result += '<span>' + part + '</span>';
}
if (_index < contentParts.length - 1) {
result += '</span>';
open = false;
result += '<br/>';
}
}
}
if (open) {
result += '</span>';
open = false;
}
return result;
}
export default generateAnsiHTML;

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @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;

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
function getPrettyURL(
sourceFileName: ?string,
sourceLineNumber: ?number,
sourceColumnNumber: ?number,
fileName: ?string,
lineNumber: ?number,
columnNumber: ?number,
compiled: boolean
): string {
let prettyURL;
if (!compiled && sourceFileName && typeof sourceLineNumber === 'number') {
// Remove everything up to the first /src/ or /node_modules/
const trimMatch = /^[/|\\].*?[/|\\]((src|node_modules)[/|\\].*)/.exec(
sourceFileName
);
if (trimMatch && trimMatch[1]) {
prettyURL = trimMatch[1];
} else {
prettyURL = sourceFileName;
}
prettyURL += ':' + sourceLineNumber;
// Note: we intentionally skip 0's because they're produced by cheap Webpack maps
if (sourceColumnNumber) {
prettyURL += ':' + sourceColumnNumber;
}
} else if (fileName && typeof lineNumber === 'number') {
prettyURL = fileName + ':' + lineNumber;
// Note: we intentionally skip 0's because they're produced by cheap Webpack maps
if (columnNumber) {
prettyURL += ':' + columnNumber;
}
} else {
prettyURL = 'unknown';
}
return prettyURL.replace('webpack://', '.');
}
export { getPrettyURL };
export default getPrettyURL;

View File

@ -0,0 +1,160 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @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
): Promise<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(
//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));
}
/*
return extractSourceMapUrl(fileUri, fileContents)
.then(sm => {
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;
return fetch(url).then(res => res.json())
.then(obj => {
return new SourceMap(new SourceMapConsumer(obj))
})
//const obj = await fetch(url).then(res => res.json());
//return new SourceMap(new SourceMapConsumer(obj));
}
});
*/
}
export { extractSourceMapUrl, getSourceMap };
export default getSourceMap;

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import type { StackFrame } from './stack-frame';
import { parse } from './parser';
import { map } from './mapper';
import { unmap } from './unmapper';
import { toExclude } from '../../presets';
function getStackFrames(
error: Error,
unhandledRejection: boolean = false,
contextSize: number = 3
): Promise<StackFrame[] | null> {
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 => {
/*
if (
enhancedFrames
.map(f => f._originalFileName)
.filter(f => f != null && f.indexOf('node_modules') === -1).length === 0
) {
return null;
}
*/
return enhancedFrames.filter(
({ functionName, fileName }) =>
(functionName == null ||
functionName.indexOf('__stack_frame_overlay_proxy_console__') === -1) &&
!toExclude.test(fileName)
);
});
}
export default getStackFrames;
export { getStackFrames };

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
function isBultinErrorName(errorName: ?string) {
switch (errorName) {
case 'EvalError':
case 'InternalError':
case 'RangeError':
case 'ReferenceError':
case 'SyntaxError':
case 'TypeError':
case 'URIError':
return true;
default:
return false;
}
}
export { isBultinErrorName };
export default isBultinErrorName;

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
function isInternalFile(sourceFileName: ?string, fileName: ?string) {
return (
sourceFileName == null ||
sourceFileName === '' ||
sourceFileName.indexOf('/~/') !== -1 ||
sourceFileName.indexOf('/node_modules/') !== -1 ||
sourceFileName.trim().indexOf(' ') !== -1 ||
fileName == null ||
fileName === ''
);
}
export { isInternalFile };

View File

@ -0,0 +1,69 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @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;

View File

@ -0,0 +1,61 @@
// @flow
import Anser from 'anser';
export type ErrorLocation = {|
fileName: string,
lineNumber: number,
colNumber?: number,
|};
const filePathRegex = /^\.(\/[^/\n ]+)+\.[^/\n ]+$/;
const lineNumberRegexes = [
// Babel syntax errors
// Based on syntax error formating of babylon parser
// https://github.com/babel/babylon/blob/v7.0.0-beta.22/src/parser/location.js#L19
/^.*\((\d+):(\d+)\)$/,
// ESLint errors
// Based on eslintFormatter in react-dev-utils
/^Line (\d+):.+$/,
];
// Based on error formatting of webpack
// https://github.com/webpack/webpack/blob/v3.5.5/lib/Stats.js#L183-L217
function parseCompileError(message: string): ?ErrorLocation {
const lines: Array<string> = message.split('\n');
let fileName: string = '';
let lineNumber: number = 0;
let colNumber: number = 0;
for (let i = 0; i < lines.length; i++) {
const line: string = Anser.ansiToText(lines[i]).trim();
if (!line) {
continue;
}
if (!fileName && line.match(filePathRegex)) {
fileName = line;
}
let k = 0;
while (k < lineNumberRegexes.length) {
const match: ?Array<string> = line.match(lineNumberRegexes[k]);
if (match) {
lineNumber = parseInt(match[1], 10);
// colNumber starts with 0 and hence add 1
colNumber = parseInt(match[2], 10) + 1 || 1;
break;
}
k++;
}
if (fileName && lineNumber) {
break;
}
}
return fileName && lineNumber ? { fileName, lineNumber, colNumber } : null;
}
export default parseCompileError;

View File

@ -0,0 +1,91 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @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;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
if (typeof Promise === 'undefined') {
// Rejection tracking prevents a common issue where React gets into an
// inconsistent state due to an error, but it gets swallowed by a Promise,
// and the user has no idea what causes React's erratic future behavior.
require('promise/lib/rejection-tracking').enable();
window.Promise = require('promise/lib/es6-extensions.js');
}
// Object.assign() is commonly used with React.
// It will use the native implementation if it's present and isn't buggy.
Object.assign = require('object-assign');

View File

@ -0,0 +1,120 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @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
) {
if (functionName && functionName.indexOf('Object.') === 0) {
functionName = functionName.slice('Object.'.length);
}
if (
// Chrome has a bug with inferring function.name:
// https://github.com/facebook/create-react-app/issues/2097
// Let's ignore a meaningless name we get for top-level modules.
functionName === 'friendlySyntaxErrorLabel' ||
functionName === 'exports.__esModule' ||
functionName === '<anonymous>' ||
!functionName
) {
functionName = null;
}
this.functionName = functionName;
this.fileName = fileName;
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 {
return this.functionName || '(anonymous function)';
}
/**
* 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 functionName = this.getFunctionName();
const source = this.getSource();
return `${functionName}${source ? ` (${source})` : ''}`;
}
}
export { StackFrame, ScriptLine };
export default StackFrame;

View File

@ -0,0 +1,126 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import StackFrame from './stack-frame';
import { getSourceMap } from './getSourceMap';
import { getLinesAround } from './getLinesAround';
import path from 'path';
function count(search: string, string: string): number {
// Count starts at -1 becuse a do-while loop always runs at least once
let count = -1,
index = -1;
do {
// First call or the while case evaluated true, meaning we have to make
// count 0 or we found a character
++count;
// Find the index of our search string, starting after the previous index
index = string.indexOf(search, index + 1);
} while (index !== -1);
return count;
}
/**
* 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) {
// The web version of this module only provides POSIX support, so Windows
// paths like C:\foo\\baz\..\\bar\ cannot be normalized.
// A simple solution to this is to replace all `\` with `/`, then
// normalize afterwards.
fileName = path.normalize(fileName.replace(/[\\]+/g, '/'));
}
if (fileName == null) {
return frame;
}
const fN: string = fileName;
const source = map
.getSources()
// Prepare path for normalization; see comment above for reasoning.
.map(s => s.replace(/[\\]+/g, '/'))
.filter(p => {
p = path.normalize(p);
const i = p.lastIndexOf(fN);
return i !== -1 && i === p.length - fN.length;
})
.map(p => ({
token: p,
seps: count(path.sep, path.normalize(p)),
penalties: count('node_modules', p) + count('~', p),
}))
.sort((a, b) => {
const s = Math.sign(a.seps - b.seps);
if (s !== 0) {
return s;
}
return Math.sign(a.penalties - b.penalties);
});
if (source.length < 1 || lineNumber == null) {
return new StackFrame(
null,
null,
null,
null,
null,
functionName,
fN,
lineNumber,
columnNumber,
null
);
}
const sourceT = source[0].token;
const { line, column } = map.getGeneratedPosition(
sourceT,
lineNumber,
// $FlowFixMe
columnNumber
);
const originalSource = map.getSource(sourceT);
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;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { configure, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import StackTraceTab from '../src/StackTraceTab';
import Adapter from 'enzyme-adapter-react-15';
configure({ adapter: new Adapter() });
function genAsyncSnapshot(component, done) {
setTimeout(() => {
component.update();
expect(toJson(component)).toMatchSnapshot();
done();
});
}
const actions = {
0: { type: 'PERFORM_ACTION', action: { type: '@@INIT' } },
1: { type: 'PERFORM_ACTION', action: { type: 'INCREMENT_COUNTER' } },
2: {
type: 'PERFORM_ACTION', action: { type: 'INCREMENT_COUNTER' },
stack: 'Error\n at fn1 (app.js:72:24)\n at fn2 (app.js:84:31)\n at fn3 (chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/page.bundle.js:1269:80)'
}
};
describe('StackTraceTab component', () => {
it('should render with no props', (done) => {
const component = mount(<StackTraceTab />);
genAsyncSnapshot(component, done);
});
it('should render with props, but without stack', (done) => {
const component = mount(
<StackTraceTab
actions={actions} action={actions[0].action}
/>
);
genAsyncSnapshot(component, done);
});
it('should render the link to docs', (done) => {
const component = mount(
<StackTraceTab
actions={actions} action={actions[1].action}
/>
);
genAsyncSnapshot(component, done);
});
it('should render with trace stack', (done) => {
const component = mount(
<StackTraceTab
actions={actions} action={actions[2].action}
/>
);
genAsyncSnapshot(component, done);
});
});

View File

@ -0,0 +1,379 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StackTraceTab component should render the link to docs 1`] = `
<StackTraceTab
action={
Object {
"type": "INCREMENT_COUNTER",
}
}
actions={
Object {
"0": Object {
"action": Object {
"type": "@@INIT",
},
"type": "PERFORM_ACTION",
},
"1": Object {
"action": Object {
"type": "INCREMENT_COUNTER",
},
"type": "PERFORM_ACTION",
},
"2": Object {
"action": Object {
"type": "INCREMENT_COUNTER",
},
"stack": "Error
at fn1 (app.js:72:24)
at fn2 (app.js:84:31)
at fn3 (chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/page.bundle.js:1269:80)",
"type": "PERFORM_ACTION",
},
}
}
>
<div
style={
Object {
"padding": "5px 10px",
}
}
>
To enable tracing action calls, you should set \`trace\` option to \`true\` for Redux DevTools enhancer. Refer to
<a
href="#"
onClick={[Function]}
>
this page
</a>
for more details.
</div>
</StackTraceTab>
`;
exports[`StackTraceTab component should render with no props 1`] = `
<StackTraceTab>
<div
style={
Object {
"padding": "5px 10px",
}
}
>
<StackTrace
contextSize={3}
editorHandler={[Function]}
errorName="N/A"
stackFrames={Array []}
>
<div
style={
Object {
"flex": "0 1 auto",
"fontSize": "1em",
"minHeight": "0px",
"overflow": "auto",
}
}
/>
</StackTrace>
</div>
</StackTraceTab>
`;
exports[`StackTraceTab component should render with props, but without stack 1`] = `
<StackTraceTab
action={
Object {
"type": "@@INIT",
}
}
actions={
Object {
"0": Object {
"action": Object {
"type": "@@INIT",
},
"type": "PERFORM_ACTION",
},
"1": Object {
"action": Object {
"type": "INCREMENT_COUNTER",
},
"type": "PERFORM_ACTION",
},
"2": Object {
"action": Object {
"type": "INCREMENT_COUNTER",
},
"stack": "Error
at fn1 (app.js:72:24)
at fn2 (app.js:84:31)
at fn3 (chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/page.bundle.js:1269:80)",
"type": "PERFORM_ACTION",
},
}
}
>
<div
style={
Object {
"padding": "5px 10px",
}
}
>
<StackTrace
contextSize={3}
editorHandler={[Function]}
errorName="N/A"
stackFrames={Array []}
>
<div
style={
Object {
"flex": "0 1 auto",
"fontSize": "1em",
"minHeight": "0px",
"overflow": "auto",
}
}
/>
</StackTrace>
</div>
</StackTraceTab>
`;
exports[`StackTraceTab component should render with trace stack 1`] = `
<StackTraceTab
action={
Object {
"type": "INCREMENT_COUNTER",
}
}
actions={
Object {
"0": Object {
"action": Object {
"type": "@@INIT",
},
"type": "PERFORM_ACTION",
},
"1": Object {
"action": Object {
"type": "INCREMENT_COUNTER",
},
"type": "PERFORM_ACTION",
},
"2": Object {
"action": Object {
"type": "INCREMENT_COUNTER",
},
"stack": "Error
at fn1 (app.js:72:24)
at fn2 (app.js:84:31)
at fn3 (chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/page.bundle.js:1269:80)",
"type": "PERFORM_ACTION",
},
}
}
>
<div
style={
Object {
"padding": "5px 10px",
}
}
>
<StackTrace
contextSize={3}
editorHandler={[Function]}
errorName="N/A"
stackFrames={
Array [
StackFrame {
"_originalColumnNumber": null,
"_originalFileName": null,
"_originalFunctionName": null,
"_originalLineNumber": null,
"_originalScriptCode": null,
"_scriptCode": null,
"columnNumber": 24,
"fileName": "app.js",
"functionName": "fn1",
"lineNumber": 72,
},
StackFrame {
"_originalColumnNumber": null,
"_originalFileName": null,
"_originalFunctionName": null,
"_originalLineNumber": null,
"_originalScriptCode": null,
"_scriptCode": null,
"columnNumber": 31,
"fileName": "app.js",
"functionName": "fn2",
"lineNumber": 84,
},
]
}
>
<div
style={
Object {
"flex": "0 1 auto",
"fontSize": "1em",
"minHeight": "0px",
"overflow": "auto",
}
}
>
<Collapsible
collapsedByDefault={false}
key="bundle-1"
>
<div>
<button
onClick={[Function]}
style={
Object {
"backgroundColor": "#3C444F",
"border": "none",
"color": "#FFFFFF",
"cursor": "pointer",
"display": "block",
"fontSize": "1em",
"lineHeight": "1.5",
"marginBottom": "0.6em",
"padding": "0px 5px",
"textAlign": "left",
"width": "100%",
}
}
>
▼ 2 stack frames were expanded.
</button>
<div
style={
Object {
"display": "block",
}
}
>
<StackFrame
contextSize={3}
critical={true}
editorHandler={[Function]}
frame={
StackFrame {
"_originalColumnNumber": null,
"_originalFileName": null,
"_originalFunctionName": null,
"_originalLineNumber": null,
"_originalScriptCode": null,
"_scriptCode": null,
"columnNumber": 24,
"fileName": "app.js",
"functionName": "fn1",
"lineNumber": 72,
}
}
key="frame-0"
showCode={false}
>
<div>
<div>
fn1
</div>
<div
style={
Object {
"fontSize": "0.9em",
"marginBottom": "0.9em",
}
}
>
<span
onClick={null}
onKeyDown={null}
style={null}
tabIndex={null}
>
app.js:72:24
</span>
</div>
</div>
</StackFrame>
<StackFrame
contextSize={3}
critical={false}
editorHandler={[Function]}
frame={
StackFrame {
"_originalColumnNumber": null,
"_originalFileName": null,
"_originalFunctionName": null,
"_originalLineNumber": null,
"_originalScriptCode": null,
"_scriptCode": null,
"columnNumber": 31,
"fileName": "app.js",
"functionName": "fn2",
"lineNumber": 84,
}
}
key="frame-1"
showCode={false}
>
<div>
<div>
fn2
</div>
<div
style={
Object {
"fontSize": "0.9em",
"marginBottom": "0.9em",
}
}
>
<span
onClick={null}
onKeyDown={null}
style={null}
tabIndex={null}
>
app.js:84:31
</span>
</div>
</div>
</StackFrame>
<button
onClick={[Function]}
style={
Object {
"backgroundColor": "#3C444F",
"border": "none",
"color": "#FFFFFF",
"cursor": "pointer",
"display": "block",
"fontSize": "1em",
"lineHeight": "1.5",
"marginBottom": "0.6em",
"padding": "0px 5px",
"textAlign": "left",
"width": "100%",
}
}
>
▲ 2 stack frames were expanded.
</button>
</div>
</div>
</Collapsible>
</div>
</StackTrace>
</div>
</StackTraceTab>
`;

2235
yarn.lock

File diff suppressed because it is too large Load Diff