mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2024-11-22 09:36:43 +03:00
Merge pull request #418 from reduxjs/trace-stack
This commit is contained in:
commit
fb9d826f61
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.0",
|
||||
"lerna": "3.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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' });
|
||||
|
|
13
packages/redux-devtools-trace-monitor/.babelrc
Normal file
13
packages/redux-devtools-trace-monitor/.babelrc
Normal 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"
|
||||
]
|
||||
}
|
5
packages/redux-devtools-trace-monitor/.eslintignore
Normal file
5
packages/redux-devtools-trace-monitor/.eslintignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
build
|
||||
dev
|
||||
dist
|
||||
lib
|
34
packages/redux-devtools-trace-monitor/.eslintrc
Normal file
34
packages/redux-devtools-trace-monitor/.eslintrc
Normal 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"
|
||||
]
|
||||
}
|
19
packages/redux-devtools-trace-monitor/LICENSE
Normal file
19
packages/redux-devtools-trace-monitor/LICENSE
Normal 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.
|
36
packages/redux-devtools-trace-monitor/README.md
Normal file
36
packages/redux-devtools-trace-monitor/README.md
Normal 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
|
63
packages/redux-devtools-trace-monitor/package.json
Normal file
63
packages/redux-devtools-trace-monitor/package.json
Normal 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"
|
||||
}
|
||||
}
|
114
packages/redux-devtools-trace-monitor/src/StackTraceTab.js
Normal file
114
packages/redux-devtools-trace-monitor/src/StackTraceTab.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
92
packages/redux-devtools-trace-monitor/src/openFile.js
Normal file
92
packages/redux-devtools-trace-monitor/src/openFile.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
1
packages/redux-devtools-trace-monitor/src/presets.js
Normal file
1
packages/redux-devtools-trace-monitor/src/presets.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const toExclude = /chrome-extension:\/\/|moz-extension:\/\//;
|
42
packages/redux-devtools-trace-monitor/src/react-error-overlay/components/CodeBlock.js
vendored
Normal file
42
packages/redux-devtools-trace-monitor/src/react-error-overlay/components/CodeBlock.js
vendored
Normal 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;
|
89
packages/redux-devtools-trace-monitor/src/react-error-overlay/components/Collapsible.js
vendored
Normal file
89
packages/redux-devtools-trace-monitor/src/react-error-overlay/components/Collapsible.js
vendored
Normal 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;
|
191
packages/redux-devtools-trace-monitor/src/react-error-overlay/containers/StackFrame.js
vendored
Normal file
191
packages/redux-devtools-trace-monitor/src/react-error-overlay/containers/StackFrame.js
vendored
Normal 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;
|
102
packages/redux-devtools-trace-monitor/src/react-error-overlay/containers/StackFrameCodeBlock.js
vendored
Normal file
102
packages/redux-devtools-trace-monitor/src/react-error-overlay/containers/StackFrameCodeBlock.js
vendored
Normal 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;
|
99
packages/redux-devtools-trace-monitor/src/react-error-overlay/containers/StackTrace.js
vendored
Normal file
99
packages/redux-devtools-trace-monitor/src/react-error-overlay/containers/StackTrace.js
vendored
Normal 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;
|
42
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/dom/absolutifyCaret.js
vendored
Normal file
42
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/dom/absolutifyCaret.js
vendored
Normal 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 };
|
47
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/dom/css.js
vendored
Normal file
47
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/dom/css.js
vendored
Normal 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 };
|
73
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/generateAnsiHTML.js
vendored
Normal file
73
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/generateAnsiHTML.js
vendored
Normal 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;
|
37
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getLinesAround.js
vendored
Normal file
37
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getLinesAround.js
vendored
Normal 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;
|
47
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getPrettyURL.js
vendored
Normal file
47
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getPrettyURL.js
vendored
Normal 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;
|
160
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getSourceMap.js
vendored
Normal file
160
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getSourceMap.js
vendored
Normal 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;
|
52
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getStackFrames.js
vendored
Normal file
52
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/getStackFrames.js
vendored
Normal 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 };
|
25
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/isBultinErrorName.js
vendored
Normal file
25
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/isBultinErrorName.js
vendored
Normal 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;
|
21
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/isInternalFile.js
vendored
Normal file
21
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/isInternalFile.js
vendored
Normal 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 };
|
69
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/mapper.js
vendored
Normal file
69
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/mapper.js
vendored
Normal 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;
|
61
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/parseCompileError.js
vendored
Normal file
61
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/parseCompileError.js
vendored
Normal 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;
|
91
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/parser.js
vendored
Normal file
91
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/parser.js
vendored
Normal 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;
|
18
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/pollyfills.js
vendored
Normal file
18
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/pollyfills.js
vendored
Normal 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');
|
120
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/stack-frame.js
vendored
Normal file
120
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/stack-frame.js
vendored
Normal 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;
|
126
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/unmapper.js
vendored
Normal file
126
packages/redux-devtools-trace-monitor/src/react-error-overlay/utils/unmapper.js
vendored
Normal 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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
Loading…
Reference in New Issue
Block a user