├── .travis.yml ├── demo.gif ├── src ├── devtools │ ├── devtools.html │ └── devtools.ts ├── styles │ ├── styles.scss │ └── __mocks__ │ │ └── styleMock.js ├── assets │ ├── ChronoScope.png │ └── ChronoScopeTitle.png ├── components │ ├── App.tsx │ ├── LineGraph.tsx │ ├── TreeGraph.tsx │ └── MainContainer.tsx ├── index.html ├── index.tsx ├── contentscript │ └── contentscript.ts ├── interfaces.ts └── backgroundScript │ └── backgroundScript.ts ├── package ├── demo.gif ├── package.json ├── README.md └── index.js ├── .gitignore ├── __tests__ ├── __snapshots__ │ └── App.test.tsx.snap ├── LineGraph.test.tsx ├── setupEnzyme.tsx ├── App.test.tsx ├── TreeGraph.test.tsx └── notes ├── babel.config.js ├── tsconfig.json ├── manifest.json ├── jest.config.js ├── .eslintrc.js ├── LICENSE ├── webpack.config.js ├── package.json └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/react-chronoscope/HEAD/demo.gif -------------------------------------------------------------------------------- /src/devtools/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /package/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/react-chronoscope/HEAD/package/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | npm-debug.log 5 | .DS_Store 6 | package-lock.json 7 | false -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding:1px; 3 | background-color: red($color: #7e1212); 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/ChronoScope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/react-chronoscope/HEAD/src/assets/ChronoScope.png -------------------------------------------------------------------------------- /src/assets/ChronoScopeTitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/react-chronoscope/HEAD/src/assets/ChronoScopeTitle.png -------------------------------------------------------------------------------- /src/styles/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // required so jest doesn't throw error while importing min.css from timeline 2 | module.exports = {}; 3 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { MainContainer } from './MainContainer'; 3 | 4 | export const App: React.SFC = () => ; 5 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/App.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`React-ChronoScope Component Tests Component: App should render correctly with no props 1`] = ``; 4 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React-ChronoScope 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { App } from './components/App'; 4 | 5 | const container = document.getElementById('root'); 6 | 7 | ReactDOM.render( 8 | , 9 | container, 10 | ); 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | plugins: [ 8 | ['@babel/plugin-proposal-class-properties', { loose: true }], 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /src/devtools/devtools.ts: -------------------------------------------------------------------------------- 1 | 2 | chrome.devtools.panels.create( 3 | 'React-ChronoScope', // title for the panel tab 4 | './assets/ChronoScope32.png', // you can specify here path to an icon 5 | 'index.html', // html page for injecting into the tab's content 6 | null, // you can pass here a callback function 7 | ); 8 | -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-chronoscope", 3 | "version": "3.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": ["react", "hierarchy", "components", "fiber"], 10 | "author": "ChronoScope", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /src/components/LineGraph.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Timeline from 'react-visjs-timeline'; 3 | import { ITimelineProps } from '../interfaces'; 4 | 5 | const LineGraph: React.SFC = ({ data, options }) => ( 6 | 10 | ); 11 | 12 | export default LineGraph; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "noImplicitAny": false, 5 | "module": "commonjs", 6 | "target": "es6", 7 | "jsx": "react", 8 | "types": ["@types/chrome", "@types/react", "@types/react-dom", "@types/node", "@types/jest", "@types/enzyme"], 9 | "esModuleInterop": true, 10 | } 11 | } -------------------------------------------------------------------------------- /__tests__/LineGraph.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | 4 | // needed to mock chrome dev tools api 5 | import chrome from "sinon-chrome"; 6 | 7 | import LineGraph from '../src/components/LineGraph'; 8 | 9 | describe('React-ChronoScope Component Tests', () => { 10 | describe('Component: LineGraph', () => { 11 | test.skip('skip', () => {}); 12 | }); 13 | }); -------------------------------------------------------------------------------- /__tests__/setupEnzyme.tsx: -------------------------------------------------------------------------------- 1 | import Adapter from "enzyme-adapter-react-16"; 2 | import { configure} from 'enzyme'; 3 | 4 | // Enzyme is a wrapper around React test utilities which makes it easier to 5 | // shallow render and traverse the shallow rendered tree. 6 | 7 | // Newer Enzyme versions require an adapter to a particular version of React 8 | configure({ adapter: new Adapter() }); 9 | 10 | test.skip('skip', () => {}); -------------------------------------------------------------------------------- /src/contentscript/contentscript.ts: -------------------------------------------------------------------------------- 1 | import { IMessage } from '../interfaces'; 2 | 3 | // listen for message from npm package 4 | window.addEventListener('message', (msg: IMessage) => { 5 | // filter the incoming msg.data 6 | if (msg.data.action === 'npmToContent') { 7 | // send the message to the chrome - backgroundScript 8 | chrome.runtime.sendMessage({ 9 | action: 'ContentToBackground', 10 | payload: msg.data, 11 | }); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { App } from '../src/components/App'; 4 | import { MainContainer } from '../src/components/MainContainer'; 5 | 6 | describe('React-ChronoScope Component Tests', () => { 7 | describe('Component: App', () => { 8 | let wrapper; 9 | 10 | beforeAll(() => { 11 | wrapper = shallow(); 12 | }); 13 | 14 | it('should render correctly with no props', () => { 15 | expect(wrapper).toMatchSnapshot(); 16 | }); 17 | 18 | it('Renders Main Container', () => { 19 | expect(wrapper.type()).toEqual(MainContainer); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "manifest_version": 2, 4 | "name": "React ChronoScope", 5 | "version": "1.0.0", 6 | "devtools_page": "devtools.html", 7 | "permissions": ["activeTab"], 8 | "content_scripts": [ 9 | { 10 | "matches": [""], 11 | "js": ["contentscript.js"] 12 | } 13 | ], 14 | "background": { 15 | "scripts": ["backgroundscript.js"], 16 | "persistant": false 17 | }, 18 | "externally_connectable": { 19 | "ids": ["*"] 20 | }, 21 | "icons": { 22 | "16": "./assets/ChronoScope.png", 23 | "48": "./assets/ChronoScope.png", 24 | "128": "./assets/ChronoScope.png" 25 | }, 26 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'" 27 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '\\.(ts|tsx)$': 'babel-jest', 4 | }, 5 | moduleNameMapper: { 6 | // required so jest doesn't throw error while importing min.css from timeline 7 | '\\.(css|less)$': '/src/styles/__mocks__/styleMock.js', 8 | }, 9 | testPathIgnorePatterns: [ 10 | '/(build|docs|node_modules)/', 11 | ], 12 | testEnvironment: 'jsdom', 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 14 | setupFiles: [ 15 | 'raf/polyfill', 16 | ], 17 | testRegex: '/__tests__/.*\\.(ts|tsx|js)$', 18 | snapshotSerializers: ['enzyme-to-json/serializer'], 19 | setupFilesAfterEnv: ['/__tests__/setupEnzyme.tsx'], 20 | globals: { 21 | 'ts-jest': { 22 | diagnostics: false, 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IMessage { 2 | data: IMessageData; 3 | } 4 | 5 | export interface IMessageData { 6 | action: string; 7 | payload: ITree; 8 | } 9 | 10 | export interface ITree { 11 | name?: string; 12 | children?: ITree[]; 13 | stats?: any; 14 | nodeSvgShape?: IShape; 15 | } 16 | 17 | export interface ITreeProps { 18 | data: ITree[]; 19 | } 20 | 21 | export interface ITimelineProps { 22 | data: any[]; 23 | options: object; 24 | } 25 | 26 | export interface IShape { 27 | shape: string; 28 | shapeProps: IShapeProps; 29 | } 30 | 31 | interface IShapeProps { 32 | rx: number; 33 | ry: number; 34 | fill: string; 35 | } 36 | 37 | export interface IStateAndProps { 38 | name?: any; 39 | state?: any; 40 | props?: any; 41 | renderTotal?: any; 42 | } 43 | -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # React ChronoScope 4 | 5 | A package for traversing React Fiber Tree and extracting information. 6 | 7 | # Installing 8 | 9 | Install the package in the React application. 10 | ``` 11 | npm i react-chronoscope 12 | ``` 13 | Import the npm library into root container file of React Application and invoke the library with the root container. 14 | ``` 15 | import chronoscope from 'react-chronoscope'; 16 | const container = document.querySelector('#root'); 17 | render( 18 | , 19 | container, 20 | () => chronoscope(container) 21 | ); 22 | ``` 23 | 24 | # Authors 25 | [Jason Huang](https://github.com/jhmoon999)
26 | [Jimmy Mei](https://github.com/Jimmei27)
27 | [Matt Peters](https://github.com/mgpeters)
28 | [Sergiy Alariki](https://github.com/Serrzhik)
29 | [Vinh Chau](https://github.com/Vchau511) 30 | 31 | 32 |

33 | -------------------------------------------------------------------------------- /src/backgroundScript/backgroundScript.ts: -------------------------------------------------------------------------------- 1 | import { IMessageData } from '../interfaces'; 2 | 3 | // define a variable to store tree data structure from content script; 4 | let treeGraph; 5 | // connected port will be saved here 6 | let currentPort; 7 | 8 | // listen for connection from the chrome dev tool; 9 | chrome.runtime.onConnect.addListener((port) => { 10 | // save the port 11 | currentPort = port; 12 | // send message to Chrome Dev Tool on initial connect 13 | port.postMessage({ 14 | payload: treeGraph, 15 | }); 16 | }); 17 | 18 | // listen for message from contentScript 19 | chrome.runtime.onMessage.addListener((msg: IMessageData) => { 20 | // reassign the treeGraph 21 | treeGraph = msg.payload; 22 | // once the message is accepted from content script, send it to dev tool 23 | if (currentPort) { 24 | currentPort.postMessage({ 25 | payload: treeGraph, 26 | }); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/TreeGraph.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, render } from 'enzyme'; 3 | 4 | // needed to mock chrome dev tools api 5 | import * as chrome from "sinon-chrome"; 6 | 7 | import TreeGraph from '../src/components/TreeGraph'; 8 | 9 | describe('React-ChronoScope Component Tests', () => { 10 | describe('Component: TreeGraph', () => { 11 | test.skip('skip', () => {}); 12 | // let wrapper; 13 | 14 | // beforeAll(() => { 15 | // global.chrome = chrome; 16 | // wrapper = shallow(); 17 | // }); 18 | 19 | // it('should render correctly with no props', () => { 20 | // expect(wrapper).toMatchSnapshot(); 21 | // }); 22 | 23 | // it('Renders a
tag', () => { 24 | // expect(wrapper.type()).toEqual('div'); 25 | // }); 26 | 27 | // it('should have called a webextension API', () => { 28 | // expect(chrome.runtime.connect()).toHaveBeenCalled(); 29 | // }); 30 | 31 | // afterAll(() => { 32 | // chrome.flush() 33 | // }) 34 | }); 35 | }); -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb', 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly', 13 | }, 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaFeatures: { 17 | jsx: true, 18 | }, 19 | ecmaVersion: 2018, 20 | sourceType: 'module', 21 | }, 22 | plugins: [ 23 | 'react', 24 | '@typescript-eslint', 25 | ], 26 | rules: { 27 | "import/prefer-default-export": "off", 28 | "react/jsx-filename-extension": "off", 29 | 'import/no-unresolved': 0, 30 | "import/extensions": "off", 31 | "no-unused-vars": "off", 32 | "no-undef": "off", 33 | "class-methods-use-this": "off", 34 | "no-use-before-define": "off", 35 | "consistent-return": "off", 36 | "no-shadow": "off", 37 | "max-len": "off", 38 | "no-param-reassign": "off", 39 | "no-underscore-dangle": "off", 40 | "no-new-wrappers": "off", 41 | "react/prop-types": "off", 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/notes: -------------------------------------------------------------------------------- 1 | To debug TypeScript tests, the json specified under “VS Code debug” 2 | section below in the story need to be added under configurations in 3 | launch.json which can be created by going to Debug Menu and then Add 4 | Configuration in VS Code. 5 | 6 | <- Hit Debug Tab in VSCode 7 | < - select node.js 8 | <- edit your launch.json file with the code below 9 | 10 | { 11 | "type": "node", 12 | "request": "launch", 13 | "name": "Jest Current File", 14 | "program": "${workspaceFolder}/node_modules/.bin/jest", 15 | "args": ["${relativeFile}"], 16 | "console": "integratedTerminal", 17 | "internalConsoleOptions": "neverOpen", 18 | "windows": { 19 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 20 | } 21 | } 22 | 23 | Links: 24 | Jest - https://jestjs.io/docs/en/getting-started 25 | Jest & TS - https://medium.com/@RupaniChirag/writing-unit-tests-in-typescript-d4719b8a0a40 26 | Testing React with Jest & Enzyme - https://medium.com/codeclan/testing-react-with-jest-and-enzyme-20505fec4675 27 | (this one has the real juice -m ) Enzyme & Jest & TS - https://medium.com/@tejasupmanyu/setting-up-unit-tests-in-react-typescipt-with-jest-and-enzyme-56634e54703 28 | What and How - https://djangostars.com/blog/what-and-how-to-test-with-enzyme-and-jest-full-instruction-on-react-component-testing/ 29 | -------------------------------------------------------------------------------- /src/components/TreeGraph.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as React from "react"; 3 | import { useState } from 'react'; 4 | import Tree from 'react-d3-tree'; 5 | import { ITreeProps, IShape, IStateAndProps } from '../interfaces'; 6 | 7 | const TreeGraph: React.SFC = ({ data }) => { 8 | const [stateAndProps, setStateAndProps] = useState({}); 9 | const [shape, setShape] = useState(null); 10 | const [name, setName] = useState(''); 11 | 12 | const handleHover = (e) => { 13 | const { stats, nodeSvgShape, name } = e; 14 | setStateAndProps(stats); 15 | setShape(nodeSvgShape); 16 | setName(name); 17 | } 18 | 19 | const handleUnHover = () => { 20 | setStateAndProps({}); 21 | setShape(null); 22 | setName(''); 23 | } 24 | 25 | return ( 26 |
27 |
28 | { 29 | shape && 30 | shape.shapeProps.fill === 'red' && 31 |

Optimize Performance: Use shouldComponentUpdate, or React.PureComponent, or React.memo

32 | } 33 |

Component: {name}

34 |
State: {stateAndProps.state}
35 |
Props: {stateAndProps.props}
36 |
Render Time: {stateAndProps.renderTotal}
37 |
38 |
39 | 48 |
49 |
50 | ); 51 | } 52 | 53 | export default TreeGraph; 54 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: { 7 | contentScript: path.join(__dirname, './src/contentScript/contentScript.ts'), // path.resolve(__dirname, './src/index.tsx'), 8 | backgroundScript: path.join(__dirname, './src/backgroundScript/backgroundScript.ts'), 9 | devtools: path.join(__dirname, './src/devtools/devtools.ts'), 10 | bundle: path.join(__dirname, './src/index.tsx'), 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: '[name].js', 15 | }, 16 | devServer: { 17 | publicPath: '/build/', 18 | }, 19 | mode: process.env.NODE_ENV, 20 | resolve: { 21 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(t|j)sx?$/, 27 | exclude: /node_modules/, 28 | use: { loader: 'ts-loader' }, 29 | }, 30 | { 31 | enforce: 'pre', 32 | test: /\.js$/, 33 | exclude: /node_modules/, 34 | loader: 'source-map-loader', 35 | }, 36 | { 37 | test: /\.html$/i, 38 | use: { 39 | loader: 'html-loader', 40 | }, 41 | }, 42 | { 43 | test: /\.(sa|sc|c)ss$/, 44 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 45 | }, 46 | { 47 | test: /\.(png|jpe?g|gif)$/i, 48 | use: [ 49 | { 50 | loader: 'file-loader', 51 | }, 52 | ], 53 | }, 54 | ], 55 | }, 56 | plugins: [ 57 | new MiniCssExtractPlugin({ 58 | filename: '[name].css', 59 | chunkFilename: '[id].css', 60 | }), 61 | new HtmlWebpackPlugin({ 62 | title: 'React-ChronoScope', 63 | filename: 'index.html', 64 | template: 'src/index.html', 65 | chunks: ['bundle'], 66 | }), 67 | new HtmlWebpackPlugin({ 68 | filename: 'devtools.html', 69 | template: 'src/devtools/devtools.html', 70 | chunks: ['devtools'], 71 | }), 72 | ], 73 | devtool: 'source-map', 74 | }; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-chronoscope", 3 | "version": "1.0.0", 4 | "description": "React ChronoScope", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "clean": "rm -rf dist && cpy manifest.json dist && cpy src/assets/* dist/assets", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "NODE_DEV=development webpack-dev-server", 10 | "test": "jest --silent", 11 | "test:watch": "jest --silent --watch", 12 | "coverage": "jest --silent --coverage", 13 | "lint": "eslint .", 14 | "lint:fix": "eslint . --fix" 15 | }, 16 | "author": "ChronoScope", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@babel/core": "^7.8.3", 20 | "@babel/plugin-proposal-class-properties": "^7.8.3", 21 | "@babel/preset-env": "^7.8.3", 22 | "@babel/preset-react": "^7.8.3", 23 | "@babel/preset-typescript": "^7.8.3", 24 | "@types/chrome": "0.0.95", 25 | "@types/enzyme": "^3.10.5", 26 | "@types/jest": "^25.1.3", 27 | "@types/node": "^13.7.6", 28 | "@types/react": "^16.9.20", 29 | "@types/react-dom": "^16.9.5", 30 | "@typescript-eslint/eslint-plugin": "^2.21.0", 31 | "@typescript-eslint/parser": "^2.21.0", 32 | "babel-core": "^7.0.0-bridge.0", 33 | "babel-jest": "^25.1.0", 34 | "babel-loader": "^8.0.6", 35 | "cpy-cli": "^3.0.0", 36 | "css-loader": "^3.4.2", 37 | "enzyme": "^3.11.0", 38 | "enzyme-adapter-react-16": "^1.15.2", 39 | "enzyme-to-json": "^3.4.4", 40 | "eslint": "^6.8.0", 41 | "eslint-config-airbnb": "^18.0.1", 42 | "eslint-plugin-import": "^2.20.1", 43 | "eslint-plugin-jsx-a11y": "^6.2.3", 44 | "eslint-plugin-react": "^7.18.3", 45 | "eslint-plugin-react-hooks": "^1.7.0", 46 | "html-loader": "^0.5.5", 47 | "html-webpack-plugin": "^3.2.0", 48 | "jest": "^25.1.0", 49 | "mini-css-extract-plugin": "^0.9.0", 50 | "node-sass": "^4.13.1", 51 | "sass": "^1.25.0", 52 | "sass-loader": "^8.0.2", 53 | "sinon-chrome": "^3.0.1", 54 | "style-loader": "^1.1.3", 55 | "ts-jest": "^25.2.1", 56 | "ts-loader": "^6.2.1", 57 | "typescript": "^3.8.3", 58 | "webpack": "^4.41.5", 59 | "webpack-cli": "^3.3.10" 60 | }, 61 | "dependencies": { 62 | "react": "^16.5.2", 63 | "react-d3-tree": "^1.16.1", 64 | "react-dom": "^16.5.2", 65 | "react-visjs-timeline": "^1.6.0", 66 | "vis-timeline": "^7.1.3" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState, useEffect } from 'react'; 3 | import TreeGraph from './TreeGraph'; 4 | import LineGraph from './LineGraph'; 5 | import { ITree } from '../interfaces'; 6 | 7 | let treeGraphData: ITree[] = [{ 8 | name: '', 9 | children: [], 10 | }]; 11 | 12 | // initialize port that will be upon when component Mounts 13 | let port; 14 | 15 | const timeLineArray = []; 16 | 17 | let items = []; 18 | 19 | const options = { 20 | width: '100%', 21 | height: '500px', 22 | stack: true, 23 | showCurrentTime: false, 24 | showMajorLabels: false, 25 | zoomable: false, 26 | start: new Number(0), 27 | end: new Number(10), 28 | min: new Number(0), 29 | max: new Number(40), 30 | type: 'range', 31 | selectable: true, 32 | horizontalScroll: false, 33 | verticalScroll: true, 34 | }; 35 | 36 | let event; 37 | 38 | function getData(Node, baseTime) { 39 | event = {}; 40 | event.start = new Number((Number(Node.stats.renderStart) - Number(baseTime)).toFixed(2)); 41 | event.end = new Number((Number(Node.stats.renderStart) + Number(Node.stats.renderTotal) - Number(baseTime)).toFixed(2)); 42 | event.content = Node.name; 43 | event.title = Node.name; 44 | items.push(event); 45 | if (Node.children.length !== 0) { 46 | Node.children.forEach((child) => { 47 | getData(child, baseTime); 48 | }); 49 | } 50 | } 51 | 52 | export const MainContainer: React.FC = () => { 53 | const [tree, setTree] = useState(treeGraphData); 54 | 55 | useEffect(() => { 56 | // open connection with background script 57 | // make sure to open only one port 58 | if (!port) port = chrome.runtime.connect(); 59 | // listen for a message from the background script 60 | port.onMessage.addListener((message) => { 61 | if (JSON.stringify([message.payload.payload]) !== JSON.stringify(treeGraphData)) { 62 | // save new tree 63 | treeGraphData = [message.payload.payload]; 64 | getData(treeGraphData[0], treeGraphData[0].stats.renderStart); 65 | setTree(treeGraphData); 66 | timeLineArray.shift(); 67 | timeLineArray.push(items); 68 | items = []; 69 | } 70 | }); 71 | }); 72 | 73 | return ( 74 | <> 75 |

React ChronoScope

76 |
77 |

Tree Diagram

78 |
79 | 80 |
81 |
82 |
83 |

TimeLine

84 | { 85 | timeLineArray.map((items) => ) 86 | } 87 |
88 | 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 |

6 | 7 | # 8 |

Developers' tool to monitor React performance and pinpoint areas
that require further optimization

9 | 10 | # 11 | 12 | ### What Is React ChronoScope? 13 | 14 |

15 | React ChronoScope is a performance monitoring tool for React developers. It visualizes React application's components displaying components that require further optimization. 16 | 17 | React ChronoScope parses through the React application to construct an interactive tree diagram of the component hierarchy. 18 | 19 |

20 | ReactChronoscope Demo 21 |

22 | 23 | ### How To Install 24 | 25 | 1. Download the [extension](https://chrome.google.com/webstore/detail/react-chronoscope/haeiefchakokoggcngggkfbgklaifbbm) from the Chrome Web Store. 26 | 27 | 2. Install the [npm package](https://www.npmjs.com/package/react-chronoscope) in the React application. 28 | 29 | ``` 30 | npm i react-chronoscope 31 | ``` 32 | 33 | 3. Import the npm library into root container file of React Application and invoke the library with the root container. 34 | 35 | ``` 36 | import chronoscope from 'react-chronoscope'; 37 | const container = document.querySelector('#root'); 38 | render( 39 | , 40 | container, 41 | () => chronoscope(container) 42 | ); 43 | ``` 44 | 45 | ### How To Use 46 | After installing both the Chrome Extension and the npm package, run the react application in the browser. Then open Chrome Developer Tools (Inspect) on the React Application and click on ``` React ChronoScope ``` at the top of the Developer Tools panel. 47 | 48 | ### Features 49 | - Node-collapsible tree diagram that displays all hierarchy tree components of a React application. 50 | - Each Node has information vital for debugging and development such state, props and how optimized is the rendering process. 51 | - Color legend:
52 | - ![#FF0000](https://placehold.it/15/FF0000/000000?text=+) `- component was unnecessarily re-rendered.` 53 | - ![#90EE90](https://placehold.it/15/90EE90/000000?text=+) `- component was re-rendered` 54 | - ![#808080](https://placehold.it/15/808080/000000?text=+) `- component was not re-rendered` 55 | 56 | - Timeline that illustrates when each component renders. 57 | 58 |

59 | 60 | ## Team 61 | 62 | - **Jason Huang** - [https://github.com/jhmoon999] 63 | - **Jimmy Mei** - [https://github.com/Jimmei27] 64 | - **Matt Peters** - [https://github.com/mgpeters] 65 | - **Sergiy Alariki** - [https://github.com/Serrzhik] 66 | - **Vinh Chau** - [https://github.com/Vchau511] 67 | 68 | ## License 69 | 70 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details -------------------------------------------------------------------------------- /package/index.js: -------------------------------------------------------------------------------- 1 | let wasMounted = false; 2 | 3 | class Node { 4 | constructor(name, parent, children, fiber) { 5 | this.name = name; 6 | this.parent = parent; 7 | this.children = children; 8 | this.stats = { 9 | state: JSON.stringify(fiber.memoizedState), 10 | props: JSONStringify(fiber.memoizedProps), 11 | effectTag: fiber.effectTag, 12 | type: typeof fiber.type, 13 | renderStart: fiber.actualStartTime.toFixed(2), 14 | renderTotal: fiber.actualDuration.toFixed(2), 15 | }; 16 | this.nodeSvgShape = { 17 | shape: 'ellipse', 18 | shapeProps: { 19 | rx: 10, 20 | ry: 10, 21 | fill: 'lightgreen', 22 | }, 23 | }; 24 | } 25 | 26 | initializeProps(fiber) { 27 | let props = ''; 28 | if (fiber.memoizedProps.children) { 29 | if (typeof fiber.memoizedProps.children[0] === 'object') { 30 | fiber.memoizedProps.children.forEach((object) => { 31 | props += JSON.stringify(object.props); 32 | }); 33 | } else props = JSON.stringify(fiber.memoizedProps.children); 34 | } else { 35 | props = JSON.stringify(fiber.memoizedProps); 36 | } 37 | 38 | return props; 39 | } 40 | } 41 | 42 | function JSONStringify(object) { 43 | let cache = []; 44 | const string = JSON.stringify(object, 45 | // custom replacer - gets around "TypeError: Converting circular structure to JSON" 46 | (key, value) => { 47 | if (typeof value === 'object' && value !== null) { 48 | if (cache.indexOf(value) !== -1) { 49 | // Circular reference found, discard key 50 | return; 51 | } 52 | // Store value in collection 53 | cache.push(value); 54 | } 55 | return value; 56 | }, 4); 57 | cache = null; // garbage collection 58 | return string; 59 | } 60 | 61 | let prevTreeGraph = null; 62 | 63 | function treeCreator(hostRoot) { 64 | // helper function - that accepts the node - Host Root 65 | function treeGraphFromHostRootCreator(fiber) { 66 | // create a treeGraph 67 | const treeGraph = new Node(fiber.type.name, null, [], fiber); // Represent the top most Element (like App); 68 | const helper = (fiber, treeGraph) => { 69 | // check if fiber.child !== null - traverse 70 | if (fiber.child) { 71 | // push the new Node to the treeGraph.children array 72 | // the parent will the tree graph we are currently working with (do the type check for elements that are functions or html elements) 73 | let newGraphNode = treeGraph; 74 | if (typeof fiber.child.type !== 'object' && (fiber.child.child ? typeof fiber.child.child.type !== 'object' : true)) { 75 | newGraphNode = new Node(fiber.child.key || (fiber.child.type ? fiber.child.type.name : fiber.child.type) || fiber.child.type, treeGraph, [], fiber.child); 76 | treeGraph.children.push(newGraphNode); 77 | } 78 | // recursively invoke the helper on child 79 | helper(fiber.child, newGraphNode); 80 | } 81 | // check if fiber.sibling !== null - traverse 82 | if (fiber.sibling) { 83 | let newGraphNode = treeGraph; 84 | if (typeof fiber.sibling.type !== 'object' && (fiber.sibling.child ? typeof fiber.sibling.child.type !== 'object' : true)) { 85 | // create new GraphNode based on it with parent being a treeGraph.parent 86 | newGraphNode = new Node(fiber.sibling.key || (fiber.sibling.type ? fiber.sibling.type.name : fiber.sibling.type) || fiber.sibling.type, treeGraph.parent, [], fiber.sibling); 87 | // push the node on to the treeGraph.parent.children array 88 | treeGraph.parent.children.push(newGraphNode); 89 | } 90 | helper(fiber.sibling, newGraphNode); 91 | } 92 | // name of the element can be found in child.type.name 93 | }; 94 | // invoke the helper function 95 | helper(fiber, treeGraph); // fiber is an App Fiber 96 | return treeGraph; 97 | } 98 | let treeGraph; 99 | // check if the hostRoot has a child 100 | if (hostRoot.child) { 101 | // yes? invoke the search function on the child - App Fiber 102 | // assign the returned result to tree 103 | treeGraph = treeGraphFromHostRootCreator(hostRoot.child); 104 | } 105 | 106 | function recursivelyDeleteParent(root) { 107 | if (root.parent) { 108 | delete root.parent; 109 | } 110 | if (root.children) { 111 | root.children.forEach((child) => recursivelyDeleteParent(child)); 112 | } 113 | } 114 | recursivelyDeleteParent(treeGraph); 115 | delete treeGraph.parent; 116 | 117 | const tempTreeGraph = JSON.parse(JSON.stringify(treeGraph)); 118 | // recursively compare state and props in prevTreeGraph and treeGraph 119 | const compareStateAndProps = (node, prevNode, parentShapeProps) => { 120 | // compare state and props properties on stats properties for both nodes 121 | // if same - treeGraph.stats.stateOrPropsChanged - false 122 | if (node && prevNode) { 123 | // check if the node's type is a string 124 | // yes? give it a color of the parent - because Composite Component renders(or not) Host Component 125 | if (node.stats.type === 'string') { 126 | node.nodeSvgShape.shapeProps = parentShapeProps; 127 | delete node.stats.state; 128 | delete node.stats.props; 129 | } else if (prevNode.stats.state === node.stats.state && prevNode.stats.props === node.stats.props) { 130 | if ((node.stats.effectTag === 0 || node.stats.effectTag === 4) && wasMounted) { 131 | node.nodeSvgShape.shapeProps.fill = 'gray'; 132 | } else { 133 | node.nodeSvgShape.shapeProps.fill = 'red'; 134 | node.nodeSvgShape.shapeProps.rx = 12; 135 | node.nodeSvgShape.shapeProps.ry = 12; 136 | } 137 | } 138 | 139 | // delete node.stats; 140 | 141 | // recursively invoke the function for each children 142 | if (node.children.length) { 143 | for (let i = 0; i < node.children.length; i += 1) { 144 | compareStateAndProps(node.children[i], prevNode.children[i], node.nodeSvgShape.shapeProps); 145 | } 146 | } 147 | } else if (node) { 148 | // delete node.stats; 149 | if (node.stats.type === 'string') { 150 | delete node.stats.state; 151 | delete node.stats.props; 152 | } 153 | 154 | // recursively invoke the function for each children 155 | if (node.children.length) { 156 | for (let i = 0; i < node.children.length; i += 1) { 157 | compareStateAndProps(node.children[i], null, node.nodeSvgShape.shapeProps); 158 | } 159 | } 160 | } 161 | 162 | if (!wasMounted) { 163 | // delete node.stats; 164 | if (node.stats.type === 'string') { 165 | delete node.stats.state; 166 | delete node.stats.props; 167 | } 168 | if (node.children.length) { 169 | for (let i = 0; i < node.children.length; i += 1) { 170 | compareStateAndProps(node.children[i]); 171 | } 172 | } 173 | } 174 | }; 175 | 176 | compareStateAndProps(treeGraph, prevTreeGraph, null); 177 | prevTreeGraph = tempTreeGraph; 178 | wasMounted = true; 179 | window.postMessage({ action: 'npmToContent', payload: treeGraph }); 180 | } 181 | 182 | module.exports = function (container) { 183 | const fiberRoot = container._reactRootContainer._internalRoot; 184 | const hostRoot = fiberRoot.current; 185 | 186 | setTimeout(() => treeCreator(hostRoot), 500); // needs to wait for the page load 187 | window.addEventListener('click', () => { 188 | // check if the hostRoot is new - only then invoke 189 | setTimeout(() => { 190 | if (hostRoot !== fiberRoot.current) { 191 | treeCreator(fiberRoot.current); 192 | } 193 | }, 200); 194 | }); 195 | window.addEventListener('keyup', () => { 196 | setTimeout(() => { 197 | // check if the hostRoot is new - only then invoke 198 | if (hostRoot !== fiberRoot.current) { 199 | treeCreator(fiberRoot.current); 200 | } 201 | }, 200); 202 | }); 203 | }; 204 | --------------------------------------------------------------------------------