feat(trace): convert to TypeScript (#647)

* get started

* progress

* finish test

* Revert that file

* Move types to dev

* Add enzyme types

* Bump that version

* prettier
This commit is contained in:
Nathan Bierema 2020-09-29 09:37:13 -04:00 committed by GitHub
parent d37e7d93e1
commit 03217001df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 434 additions and 321 deletions

View File

@ -84,6 +84,7 @@
"peerDependencies": {
"@types/react": "^16.3.18",
"react": "^16.3.0",
"redux": "^3.4.0 || ^4.0.0",
"redux-devtools-inspector-monitor": "^0.14.0"
}
}

View File

@ -1,13 +1,8 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
],
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties"
]
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@ -0,0 +1 @@
lib

View File

@ -0,0 +1,21 @@
module.exports = {
extends: '../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
},
{
files: ['test/*.ts', 'test/*.tsx'],
extends: '../../eslintrc.ts.react.jest.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./test/tsconfig.json'],
},
},
],
};

View File

@ -0,0 +1,3 @@
module.exports = {
preset: 'ts-jest',
};

View File

@ -2,47 +2,61 @@
"name": "redux-devtools-trace-monitor",
"version": "0.1.3",
"description": "Submonitor for Redux DevTools inspector to show stack traces.",
"repository": "https://github.com/reduxjs/redux-devtools",
"homepage": "https://github.com/reduxjs/redux-devtools",
"homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-trace-monitor",
"license": "MIT",
"author": "Mark Erikson <mark@isquaredsoftware.com>",
"contributors": [
"Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)"
],
"license": "MIT",
"main": "lib/StackTraceTab.js",
"files": [
"lib"
],
"main": "lib/StackTraceTab.js",
"types": "lib/StackTraceTab.d.ts",
"repository": "https://github.com/reduxjs/redux-devtools",
"scripts": {
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"test": "jest --no-cache",
"prepare": "npm run clean && npm run build",
"prepublishOnly": "npm run test && npm run clean && npm run build"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"enzyme-to-json": "^3.5.0",
"jest": "^26.2.2",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rimraf": "^3.0.2"
"test": "jest",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"preversion": "npm run type-check && npm run lint && npm run test",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@types/chrome": "^0.0.124",
"anser": "^1.4.9",
"html-entities": "^1.3.1",
"react": "^16.13.1",
"redux-devtools-themes": "^1.0.0",
"settle-promise": "^1.0.0"
},
"devDependencies": {
"@types/babel__code-frame": "^7.0.2",
"@types/enzyme": "^3.10.5",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/html-entities": "^1.2.16",
"@types/react": "^16.9.46",
"@types/redux-devtools-themes": "^1.0.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"enzyme-to-json": "^3.5.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1",
"redux": "^4.0.5",
"redux-devtools": "^3.7.0",
"redux-devtools-inspector-monitor": "^0.14.0"
},
"peerDependencies": {
"@types/react": "^16.3.18",
"react": "^16.3.0",
"redux": "^3.4.0 || ^4.0.0",
"redux-devtools": "^3.4.0",
"redux-devtools-inspector-monitor": "^0.14.0"
}
}

View File

@ -3,26 +3,45 @@ import React, { Component } from 'react';
import { getStackFrames } from './react-error-overlay/utils/getStackFrames';
import StackTrace from './react-error-overlay/containers/StackTrace';
import openFile from './openFile';
import { Action } from 'redux';
import { TabComponentProps } from 'redux-devtools-inspector-monitor';
import StackFrame from './react-error-overlay/utils/stack-frame';
import { ErrorLocation } from './react-error-overlay/utils/parseCompileError';
const rootStyle = { padding: '5px 10px' };
export default class StackTraceTab extends Component {
interface Props<S, A extends Action<unknown>> extends TabComponentProps<S, A> {
openFile: (
fileName: string,
lineNumber: number,
stackFrame: StackFrame
) => void;
}
interface State {
stackFrames: StackFrame[];
currentError?: Error;
showDocsLink?: boolean;
}
export default class StackTraceTab<
S,
A extends Action<unknown>
> extends Component<Props<S, A>, State> {
static defaultProps = {
openFile,
};
constructor(props) {
super(props);
this.state = {
stackFrames: [],
};
}
state: State = {
stackFrames: [],
};
componentDidMount() {
// console.log("StackTraceTab mounted");
this.checkForStackTrace();
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: Props<S, A>) {
const { action, actions } = prevProps;
if (action !== this.props.action || actions !== this.props.actions) {
@ -47,25 +66,32 @@ export default class StackTraceTab extends Component {
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 });
});
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: stackFrames!,
currentError: deserializedError,
});
})
.catch(() => {
// noop
});
} else {
this.setState({
stackFrames: [],
showDocsLink:
liftedAction.action &&
liftedAction.action.type &&
liftedAction.action.type !== '@@INIT',
liftedAction!.action &&
liftedAction!.action.type &&
liftedAction!.action.type !== '@@INIT',
});
}
}
onStackLocationClicked = (fileLocation = {}) => {
onStackLocationClicked = (fileLocation: Partial<ErrorLocation> = {}) => {
// console.log("Stack location args: ", ...args);
const { fileName, lineNumber } = fileLocation;
@ -93,7 +119,7 @@ export default class StackTraceTab extends Component {
}
};
openDocs = (e) => {
openDocs: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.stopPropagation();
window.open(
'https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Features/Trace.md'

View File

@ -1,31 +1,35 @@
import StackFrame from './react-error-overlay/utils/stack-frame';
const isFF = navigator.userAgent.indexOf('Firefox') !== -1;
function openResource(fileName, lineNumber, stackFrame) {
function openResource(
fileName: string,
lineNumber: number,
stackFrame: 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);
}
);
}
chrome.devtools.panels.openResource(fileName, adjustedLineNumber, ((result: {
isError?: boolean;
}) => {
//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);
}
);
}
);
}) as () => void);
}
function openAndCloseTab(url) {
function openAndCloseTab(url: string) {
chrome.tabs.create({ url }, (tab) => {
const removeTab = () => {
chrome.windows.onFocusChanged.removeListener(removeTab);
@ -45,19 +49,19 @@ function openAndCloseTab(url) {
});
}
function openInIframe(url) {
function openInIframe(url: string) {
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style = 'display:none';
iframe.style.display = 'none';
document.body.appendChild(iframe);
setTimeout(() => iframe.parentNode.removeChild(iframe), 3000);
setTimeout(() => iframe.parentNode!.removeChild(iframe), 3000);
}
function openInEditor(editor, path, stackFrame) {
function openInEditor(editor: string, path: string, stackFrame: StackFrame) {
const projectPath = path.replace(/\/$/, '');
const file =
stackFrame._originalFileName ||
stackFrame.finalFileName ||
((stackFrame as unknown) as { finalFileName: string }).finalFileName ||
stackFrame.fileName ||
'';
let filePath = /^https?:\/\//.test(file)
@ -95,7 +99,11 @@ function openInEditor(editor, path, stackFrame) {
}
}
export default function openFile(fileName, lineNumber, stackFrame) {
export default function openFile(
fileName: string,
lineNumber: number,
stackFrame: StackFrame
) {
if (process.env.NODE_ENV === 'development')
// eslint-disable-next-line no-console
console.log(fileName, lineNumber, stackFrame);

View File

@ -5,10 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React from 'react';
import React, { CSSProperties } from 'react';
const preStyle = {
const preStyle: CSSProperties = {
position: 'relative',
display: 'block',
backgroundColor: '#000',
@ -24,10 +23,10 @@ const codeStyle = {
fontFamily: 'Consolas, Menlo, monospace',
};
type CodeBlockPropsType = {|
main: boolean,
codeHTML: string,
|};
interface CodeBlockPropsType {
main: boolean;
codeHTML: string;
}
function CodeBlock(props: CodeBlockPropsType) {
const codeBlock = { __html: props.codeHTML };

View File

@ -5,13 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React, { Component } from 'react';
import React, { Component, CSSProperties, ReactNode } from 'react';
import { nicinabox as theme } from 'redux-devtools-themes';
import type { Element as ReactElement } from 'react';
const _collapsibleStyle = {
const _collapsibleStyle: CSSProperties = {
color: theme.base06,
backgroundColor: theme.base01,
cursor: 'pointer',
@ -24,26 +21,27 @@ const _collapsibleStyle = {
lineHeight: '1.5',
};
const collapsibleCollapsedStyle = {
const collapsibleCollapsedStyle: CSSProperties = {
..._collapsibleStyle,
marginBottom: '1.5em',
};
const collapsibleExpandedStyle = {
const collapsibleExpandedStyle: CSSProperties = {
..._collapsibleStyle,
marginBottom: '0.6em',
};
type Props = {|
children: ReactElement<any>[],
|};
interface Props {
collapsedByDefault?: boolean;
children: ReactNode[];
}
type State = {|
collapsed: boolean,
|};
interface State {
collapsed: boolean | undefined;
}
class Collapsible extends Component<Props, State> {
state = {
state: State = {
collapsed: undefined,
};
@ -53,7 +51,7 @@ class Collapsible extends Component<Props, State> {
}));
};
isCollapsed = (state) =>
isCollapsed = (state: State) =>
state.collapsed === undefined
? this.props.collapsedByDefault
: state.collapsed;

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React, { Component } from 'react';
import React, { Component, CSSProperties } from 'react';
import CodeBlock from './StackFrameCodeBlock';
import { getPrettyURL } from '../utils/getPrettyURL';
import { nicinabox as theme } from 'redux-devtools-themes';
@ -14,22 +13,22 @@ 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 = {
const linkStyle: CSSProperties = {
fontSize: '0.9em',
marginBottom: '0.9em',
};
const anchorStyle = {
const anchorStyle: CSSProperties = {
textDecoration: 'none',
color: theme.base05,
cursor: 'pointer',
};
const codeAnchorStyle = {
const codeAnchorStyle: CSSProperties = {
cursor: 'pointer',
};
const toggleStyle = {
const toggleStyle: CSSProperties = {
marginBottom: '1.5em',
color: theme.base05,
cursor: 'pointer',
@ -44,20 +43,20 @@ const toggleStyle = {
lineHeight: '1.5',
};
type Props = {|
frame: StackFrameType,
contextSize: number,
critical: boolean,
showCode: boolean,
editorHandler: (errorLoc: ErrorLocation) => void,
|};
interface Props {
frame: StackFrameType;
contextSize: number;
critical: boolean;
showCode: boolean;
editorHandler: (errorLoc: ErrorLocation) => void;
}
type State = {|
compiled: boolean,
|};
interface State {
compiled: boolean;
}
class StackFrame extends Component<Props, State> {
state = {
state: State = {
compiled: false,
};
@ -93,7 +92,9 @@ class StackFrame extends Component<Props, State> {
this.props.editorHandler(errorLoc);
};
onKeyDown = (e /* : SyntheticKeyboardEvent<> */) => {
onKeyDown: React.KeyboardEventHandler<HTMLSpanElement> = (
e /* : SyntheticKeyboardEvent<> */
) => {
if (e.key === 'Enter') {
this.editorHandler();
}
@ -162,10 +163,10 @@ class StackFrame extends Component<Props, State> {
<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}
style={canOpenInEditor ? anchorStyle : undefined}
onClick={canOpenInEditor ? this.editorHandler : undefined}
onKeyDown={canOpenInEditor ? this.onKeyDown : undefined}
tabIndex={canOpenInEditor ? 0 : undefined}
>
{url}
</span>
@ -173,8 +174,8 @@ class StackFrame extends Component<Props, State> {
{codeBlockProps && (
<span>
<span
onClick={canOpenInEditor ? this.editorHandler : null}
style={canOpenInEditor ? codeAnchorStyle : null}
onClick={canOpenInEditor ? this.editorHandler : undefined}
style={canOpenInEditor ? codeAnchorStyle : undefined}
>
<CodeBlock {...codeBlockProps} />
</span>

View File

@ -5,40 +5,31 @@
* 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 { 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,
|};
interface StackFrameCodeBlockPropsType {
lines: ScriptLine[];
lineNum: number;
columnNum: number | null | undefined;
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> */
) {
function StackFrameCodeBlock(props: StackFrameCodeBlockPropsType) {
const { lines, lineNum, columnNum, contextSize, main } = props;
const sourceCode = [];
const sourceCode: string[] = [];
let whiteSpace = Infinity;
lines.forEach(function (e) {
const { content: text } = e;
const m = text.match(/^\s*/);
const m = /^\s*/.exec(text);
if (text === '') {
return;
}
@ -86,16 +77,16 @@ function StackFrameCodeBlock(
const ccn2 = node.childNodes;
for (let index2 = 0; index2 < ccn2.length; ++index2) {
const lineNode = ccn2[index2];
const text = lineNode.innerText;
const text = (lineNode as HTMLElement).innerText;
if (text == null) {
continue;
}
if (text.indexOf(' ' + lineNum + ' |') === -1) {
if (text.indexOf(` ${lineNum} |`) === -1) {
continue;
}
// $FlowFixMe
applyStyles(node, {
'background-color': main ? theme.base02 : theme.base01,
applyStyles(node as HTMLElement, {
backgroundColor: main ? theme.base02 : theme.base01,
});
// eslint-disable-next-line
break oLoop;

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import React, { Component } from 'react';
import React, { Component, ReactElement } from 'react';
import StackFrame from './StackFrame';
import Collapsible from '../components/Collapsible';
import { isInternalFile } from '../utils/isInternalFile';
@ -22,19 +21,19 @@ const traceStyle = {
overflow: 'auto',
};
type Props = {|
stackFrames: StackFrameType[],
errorName: string,
contextSize: number,
editorHandler: (errorLoc: ErrorLocation) => void,
|};
interface 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 = [];
const renderedFrames: ReactElement[] = [];
let hasReachedAppCode = false,
currentBundle = [],
currentBundle: ReactElement[] = [],
bundleCount = 0,
anyNodeExpanded = false;
@ -55,7 +54,7 @@ class StackTrace extends Component<Props> {
const frameEle = (
<StackFrame
key={'frame-' + index}
key={`frame-${index}`}
frame={frame}
contextSize={contextSize}
critical={index === 0}
@ -77,7 +76,7 @@ class StackTrace extends Component<Props> {
renderedFrames.push(
<Collapsible
collapsedByDefault={anyNodeExpanded}
key={'bundle-' + bundleCount}
key={`bundle-${bundleCount}`}
>
{currentBundle}
</Collapsible>

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
function removeNextBr(parent, component: ?Element) {
function removeNextBr(parent: Node, component: Element | null | undefined) {
while (component != null && component.tagName.toLowerCase() !== 'br') {
component = component.nextElementSibling;
}
@ -18,7 +17,7 @@ function removeNextBr(parent, component: ?Element) {
function absolutifyCaret(component: Node) {
const ccn = component.childNodes;
for (let index = 0; index < ccn.length; ++index) {
const c = ccn[index];
const c = ccn[index] as HTMLElement;
// $FlowFixMe
if (c.tagName.toLowerCase() !== 'span') {
continue;

View File

@ -5,9 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
let injectedCount = 0;
const injectedCache = {};
const injectedCache: { [key: number]: HTMLStyleElement } = {};
function getHead(document: Document) {
return document.head || document.getElementsByTagName('head')[0];
@ -33,14 +32,17 @@ function removeCss(document: Document, ref: number) {
delete injectedCache[ref];
}
function applyStyles(element: HTMLElement, styles: Object) {
function applyStyles(
element: HTMLElement,
styles: Partial<CSSStyleDeclaration>
) {
element.setAttribute('style', '');
for (const key in styles) {
if (!Object.prototype.hasOwnProperty.call(styles, key)) {
continue;
}
// $FlowFixMe
element.style[key] = styles[key];
element.style[key] = styles[key]!;
}
}

View File

@ -5,15 +5,13 @@
* 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();
const entities = new Entities();
var anserMap = {
const anserMap = {
'ansi-bright-black': theme.base03,
'ansi-bright-yellow': theme.base0A,
'ansi-yellow': theme.base0B,
@ -29,25 +27,25 @@ var anserMap = {
};
function generateAnsiHTML(txt: string): string {
var arr = new Anser().ansiToJson(entities.encode(txt), {
const 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,
let result = '';
let open = false;
for (let index = 0; index < arr.length; ++index) {
const c = arr[index];
const content = c.content,
fg = c.fg;
var contentParts = content.split('\n');
for (var _index = 0; _index < contentParts.length; ++_index) {
const contentParts = content.split('\n');
for (let _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];
const part = contentParts[_index].replace('\r', '');
const color = anserMap[fg as keyof typeof anserMap];
if (color != null) {
result += '<span style="color: ' + color + ';">' + part + '</span>';
} else {

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import { ScriptLine } from './stack-frame';
/**

View File

@ -5,14 +5,13 @@
* 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,
sourceFileName: string | null | undefined,
sourceLineNumber: number | null | undefined,
sourceColumnNumber: number | null | undefined,
fileName: string | null | undefined,
lineNumber: number | null | undefined,
columnNumber: number | null | undefined,
compiled: boolean
): string {
let prettyURL;
@ -26,16 +25,16 @@ function getPrettyURL(
} else {
prettyURL = sourceFileName;
}
prettyURL += ':' + sourceLineNumber;
prettyURL += `:${sourceLineNumber}`;
// Note: we intentionally skip 0's because they're produced by cheap Webpack maps
if (sourceColumnNumber) {
prettyURL += ':' + sourceColumnNumber;
prettyURL += `:${sourceColumnNumber}`;
}
} else if (fileName && typeof lineNumber === 'number') {
prettyURL = fileName + ':' + lineNumber;
prettyURL = `${fileName}:${lineNumber}`;
// Note: we intentionally skip 0's because they're produced by cheap Webpack maps
if (columnNumber) {
prettyURL += ':' + columnNumber;
prettyURL += `:${columnNumber}`;
}
} else {
prettyURL = 'unknown';

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import { SourceMapConsumer } from 'source-map';
import { RawSourceMap, SourceMapConsumer } from 'source-map';
/**
* A wrapped instance of a <code>{@link https://github.com/mozilla/source-map SourceMapConsumer}</code>.
@ -16,7 +15,7 @@ import { SourceMapConsumer } from 'source-map';
class SourceMap {
__source_map: SourceMapConsumer;
constructor(sourceMap) {
constructor(sourceMap: SourceMapConsumer) {
this.__source_map = sourceMap;
}
@ -28,7 +27,7 @@ class SourceMap {
getOriginalPosition(
line: number,
column: number
): { source: string, line: number, column: number } {
): { source: string; line: number; column: number } {
const {
line: l,
column: c,
@ -50,7 +49,7 @@ class SourceMap {
source: string,
line: number,
column: number
): { line: number, column: number } {
): { line: number; column: number } {
const { line: l, column: c } = this.__source_map.generatedPositionFor({
source,
line,
@ -71,7 +70,7 @@ class SourceMap {
}
getSources(): string[] {
return this.__source_map.sources;
return ((this.__source_map as unknown) as { sources: string[] }).sources;
}
}
@ -82,7 +81,7 @@ function extractSourceMapUrl(
const regex = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/gm;
let match = null;
for (;;) {
let next = regex.exec(fileContents);
const next = regex.exec(fileContents);
if (next == null) {
break;
}
@ -107,7 +106,7 @@ async function getSourceMap(
let sm = await extractSourceMapUrl(fileUri, fileContents);
if (sm.indexOf('data:') === 0) {
const base64 = /^data:application\/json;([\w=:"-]+;)*base64,/;
const match2 = sm.match(base64);
const match2 = base64.exec(sm);
if (!match2) {
throw new Error(
'Sorry, non-base64 inline source-map encoding is not supported.'
@ -116,7 +115,9 @@ async function getSourceMap(
sm = sm.substring(match2[0].length);
sm = window.atob(sm);
sm = JSON.parse(sm);
return new SourceMap(new SourceMapConsumer(sm));
return new SourceMap(
new SourceMapConsumer((sm as unknown) as RawSourceMap)
);
} else {
const index = fileUri.lastIndexOf('/');
const url = fileUri.substring(0, index + 1) + sm;

View File

@ -5,7 +5,6 @@
* 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';
@ -14,15 +13,21 @@ import { toExclude } from '../../presets';
function getStackFrames(
error: Error,
unhandledRejection: boolean = false, // eslint-disable-line no-unused-vars
contextSize: number = 3
unhandledRejection = false, // eslint-disable-line no-unused-vars
contextSize = 3
): Promise<StackFrame[] | null> {
const parsedFrames = parse(error);
let enhancedFramesPromise;
if (error.__unmap_source) {
if (
((error as unknown) as {
__unmap_source: string | { uri: string; contents: string };
}).__unmap_source
) {
enhancedFramesPromise = unmap(
// $FlowFixMe
error.__unmap_source,
((error as unknown) as {
__unmap_source: string | { uri: string; contents: string };
}).__unmap_source,
parsedFrames,
contextSize
);
@ -44,7 +49,7 @@ function getStackFrames(
(functionName == null ||
functionName.indexOf('__stack_frame_overlay_proxy_console__') ===
-1) &&
!toExclude.test(fileName)
!toExclude.test(fileName!)
);
});
}

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
function isBultinErrorName(errorName: ?string) {
function isBultinErrorName(errorName: string | null | undefined) {
switch (errorName) {
case 'EvalError':
case 'InternalError':

View File

@ -5,8 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
function isInternalFile(sourceFileName: ?string, fileName: ?string) {
function isInternalFile(
sourceFileName: string | null | undefined,
fileName: string | null | undefined
) {
return (
sourceFileName == null ||
sourceFileName === '' ||

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import StackFrame from './stack-frame';
import { getSourceMap } from './getSourceMap';
import { getLinesAround } from './getLinesAround';
@ -18,7 +17,7 @@ import { settle } from 'settle-promise';
*/
async function map(
frames: StackFrame[],
contextLines: number = 3
contextLines = 3
): Promise<StackFrame[]> {
const cache: any = {};
const files: string[] = [];
@ -41,7 +40,7 @@ async function map(
);
return frames.map((frame) => {
const { functionName, fileName, lineNumber, columnNumber } = frame;
let { map, fileSource } = cache[fileName] || {};
const { map, fileSource } = cache[fileName!] || {};
if (map == null || lineNumber == null) {
return frame;
}

View File

@ -1,11 +1,10 @@
// @flow
import Anser from 'anser';
export type ErrorLocation = {|
fileName: string,
lineNumber: number,
colNumber?: number,
|};
export interface ErrorLocation {
fileName: string;
lineNumber: number;
colNumber?: number;
}
const filePathRegex = /^\.(\/[^/\n ]+)+\.[^/\n ]+$/;
@ -22,11 +21,11 @@ const lineNumberRegexes = [
// 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;
function parseCompileError(message: string): ErrorLocation | null | undefined {
const lines: string[] = message.split('\n');
let fileName = '';
let lineNumber = 0;
let colNumber = 0;
for (let i = 0; i < lines.length; i++) {
const line: string = Anser.ansiToText(lines[i]).trim();
@ -34,13 +33,15 @@ function parseCompileError(message: string): ?ErrorLocation {
continue;
}
if (!fileName && line.match(filePathRegex)) {
if (!fileName && filePathRegex.exec(line)) {
fileName = line;
}
let k = 0;
while (k < lineNumberRegexes.length) {
const match: ?Array<string> = line.match(lineNumberRegexes[k]);
const match: string[] | null | undefined = lineNumberRegexes[k].exec(
line
);
if (match) {
lineNumber = parseInt(match[1], 10);
// colNumber starts with 0 and hence add 1

View File

@ -5,14 +5,13 @@
* 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)
.exec(token)!
.slice(1)
.map((v) => {
const p = Number(v);
@ -20,7 +19,7 @@ function extractLocation(token: string): [string, number, number] {
return p;
}
return v;
});
}) as [string, number, number];
}
const regexValidFrame_Chrome = /^\s*(at|in)\s.+(:\d+)/;
@ -46,7 +45,7 @@ function parseStack(stack: string[]): StackFrame[] {
const last = data.pop();
return new StackFrame(
data.join('@') || (isEval ? 'eval' : null),
...extractLocation(last)
...extractLocation(last!)
);
} else {
// Strip eval, we don't care about it
@ -58,7 +57,10 @@ function parseStack(stack: string[]): StackFrame[] {
}
const data = e.trim().split(/\s+/g).slice(1);
const last = data.pop();
return new StackFrame(data.join(' ') || null, ...extractLocation(last));
return new StackFrame(
data.join(' ') || null,
...extractLocation(last!)
);
}
});
return frames;

View File

@ -9,6 +9,7 @@ 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.
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('promise/lib/rejection-tracking').enable();
window.Promise = require('promise/lib/es6-extensions.js');
}

View File

@ -5,8 +5,6 @@
* 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. */
@ -16,7 +14,7 @@ class ScriptLine {
/** 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) {
constructor(lineNumber: number, content: string, highlight = false) {
this.lineNumber = lineNumber;
this.content = content;
this.highlight = highlight;
@ -98,10 +96,10 @@ class StackFrame {
str += this.fileName + ':';
}
if (this.lineNumber != null) {
str += this.lineNumber + ':';
str += `${this.lineNumber}:`;
}
if (this.columnNumber != null) {
str += this.columnNumber + ':';
str += `${this.columnNumber}:`;
}
return str.slice(0, -1);
}

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
/* @flow */
import StackFrame from './stack-frame';
import { getSourceMap } from './getSourceMap';
import { getLinesAround } from './getLinesAround';
@ -32,16 +31,16 @@ function count(search: string, string: string): number {
* @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 },
_fileUri: string | { uri: string; contents: string },
frames: StackFrame[],
contextLines: number = 3
contextLines = 3
): Promise<StackFrame[]> {
let fileContents = typeof _fileUri === 'object' ? _fileUri.contents : null;
let fileUri = typeof _fileUri === 'object' ? _fileUri.uri : _fileUri;
const fileUri = typeof _fileUri === 'object' ? _fileUri.uri : _fileUri;
if (fileContents == null) {
fileContents = await fetch(fileUri).then((res) => res.text());
}
const map = await getSourceMap(fileUri, fileContents);
const map = await getSourceMap(fileUri, fileContents!);
return frames.map((frame) => {
const {
functionName,
@ -104,7 +103,7 @@ async function unmap(
sourceT,
lineNumber,
// $FlowFixMe
columnNumber
columnNumber!
);
const originalSource = map.getSource(sourceT);
return new StackFrame(

View File

@ -0,0 +1,3 @@
declare module 'settle-promise' {
export function settle(promises: Promise<void>[]): Promise<void>;
}

View File

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

View File

@ -0,0 +1,68 @@
import React, { ReactComponentElement } from 'react';
import { configure, mount, ReactWrapper } from 'enzyme';
import toJson from 'enzyme-to-json';
import StackTraceTab from '../src/StackTraceTab';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
function genAsyncSnapshot(
component: ReactWrapper<any, any, any>,
done: () => void
) {
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)',
},
};
const StackTraceTabAsAny = StackTraceTab as any;
describe('StackTraceTab component', () => {
it('should render with no props', () => {
return new Promise((done) => {
const component = mount(<StackTraceTabAsAny />);
genAsyncSnapshot(component, done);
});
});
it('should render with props, but without stack', () => {
return new Promise((done) => {
const component = mount(
<StackTraceTabAsAny actions={actions} action={actions[0].action} />
);
genAsyncSnapshot(component, done);
});
});
it('should render the link to docs', () => {
return new Promise((done) => {
const component = mount(
<StackTraceTabAsAny actions={actions} action={actions[1].action} />
);
genAsyncSnapshot(component, done);
});
});
it('should render with trace stack', () => {
return new Promise((done) => {
const component = mount(
<StackTraceTabAsAny actions={actions} action={actions[2].action} />
);
genAsyncSnapshot(component, done);
});
});
});

View File

@ -301,12 +301,7 @@ exports[`StackTraceTab component should render with trace stack 1`] = `
}
}
>
<span
onClick={null}
onKeyDown={null}
style={null}
tabIndex={null}
>
<span>
app.js:72:24
</span>
</div>
@ -345,12 +340,7 @@ exports[`StackTraceTab component should render with trace stack 1`] = `
}
}
>
<span
onClick={null}
onKeyDown={null}
style={null}
tabIndex={null}
>
<span>
app.js:84:31
</span>
</div>

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.react.base.json",
"include": ["../src", "."]
}

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.react.base.json",
"compilerOptions": {
"outDir": "lib"
},
"include": ["src"]
}

View File

@ -8,6 +8,6 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33311
"types": ["node", "jest", "webpack-env"]
"types": ["node", "jest", "webpack-env", "chrome"]
}
}

View File

@ -378,7 +378,7 @@
"@babel/helper-create-class-features-plugin" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-proposal-decorators@^7.10.5", "@babel/plugin-proposal-decorators@^7.8.3":
"@babel/plugin-proposal-decorators@^7.8.3":
version "7.10.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4"
integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ==
@ -1110,7 +1110,7 @@
levenary "^1.1.1"
semver "^5.5.0"
"@babel/preset-flow@^7.0.0", "@babel/preset-flow@^7.10.4":
"@babel/preset-flow@^7.0.0":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.10.4.tgz#e0d9c72f8cb02d1633f6a5b7b16763aa2edf659f"
integrity sha512-XI6l1CptQCOBv+ZKYwynyswhtOKwpZZp5n0LG1QKCo8erRhqjoQV6nvx61Eg30JHpysWQSBwA2AWRU3pBbSY5g==
@ -3251,6 +3251,11 @@
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
"@types/babel__code-frame@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.2.tgz#e0c0f1648cbc09a9d4e5b4ed2ae9a6f7c8f5aeb0"
integrity sha512-imO+jT/yjOKOAS5GQZ8SDtwiIloAGGr6OaZDKB0V5JVaSfGZLat5K5/ZRtyKW6R60XHV3RHYPTFfhYb+wDKyKg==
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
@ -3309,6 +3314,14 @@
dependencies:
"@types/node" "*"
"@types/chrome@^0.0.124":
version "0.0.124"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.124.tgz#1cdb8e1c1ddb04b15844f5a71b9907f73bbb84a2"
integrity sha512-0UmDQ6A9gaahvztKryIonSTyUMEhuhKNyyJAnBB7ZJN/YXP7YRkL4onPFSTxnIbXcMnYsQFo8TxsGP8jY2mdEw==
dependencies:
"@types/filesystem" "*"
"@types/har-format" "*"
"@types/classnames@^2.2.10":
version "2.2.10"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
@ -3419,6 +3432,18 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/filesystem@*":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.29.tgz#ee3748eb5be140dcf980c3bd35f11aec5f7a3748"
integrity sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==
dependencies:
"@types/filewriter" "*"
"@types/filewriter@*":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3"
integrity sha1-wFTor02d11205jq8dviFFocU1LM=
"@types/glob-base@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@types/glob-base/-/glob-base-0.3.0.tgz#a581d688347e10e50dd7c17d6f2880a10354319d"
@ -3439,6 +3464,11 @@
dependencies:
"@types/node" "*"
"@types/har-format@*":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.4.tgz#3275842095abb60d14b47fa798cc9ff708dab6d4"
integrity sha512-iUxzm1meBm3stxUMzRqgOVHjj4Kgpgu5w9fm4X7kPRfSgVRzythsucEN7/jtOo8SQzm+HfcxWWzJS0mJDH/3DQ==
"@types/hast@^2.0.0":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.1.tgz#b16872f2a6144c7025f296fb9636a667ebb79cd9"
@ -3464,6 +3494,11 @@
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/html-entities@^1.2.16":
version "1.2.16"
resolved "https://registry.yarnpkg.com/@types/html-entities/-/html-entities-1.2.16.tgz#4d1fe208c4c33727ac4657e6f5d92bfe52427023"
integrity sha512-CI6fHfFvkTtX2Nlr4JBA6yIFTfA4p9E6w9ky64X6PrfXiTALhUh/SOa+Sxvv2p87m+y5AH71lAUrx0lSYx4hKQ==
"@types/html-minifier-terser@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"