├── .DS_Store
├── .eslintrc.json
├── .github
└── pull_request_template.md
├── .gitignore
├── .travis.yml
├── .vscode
└── settings.json
├── FetchTreeChromeExt
├── README.md
├── background.js
├── contentScript.js
├── injectScript.js
├── manifest.json
├── src
│ ├── assets
│ │ └── Logo.png
│ ├── components
│ │ ├── ComponentStorePanel.jsx
│ │ ├── LinkControls.tsx
│ │ ├── Panel.jsx
│ │ ├── getLinkComponent.ts
│ │ ├── index.tsx
│ │ ├── sandbox-styles.css
│ │ ├── treeViz.tsx
│ │ └── useForceUpdate.ts
│ ├── devtools
│ │ ├── devtools.html
│ │ └── devtools.js
│ ├── index.html
│ └── style.css
└── tsconfig.json
├── FetchTreeNPMPkg
├── FetchTreeHook.js
├── README.md
├── componentStore.js
├── package.json
└── parser.ts
├── LICENSE
├── README.md
├── babel.config.json
├── jest.config.js
├── package-lock.json
├── package.json
├── test
├── __tests__
│ ├── fiberwalker.test.js
│ └── parser.test.js
└── testData
│ ├── App.jsx
│ ├── Body.jsx
│ ├── Footer.jsx
│ ├── Nav.jsx
│ ├── index.js
│ ├── mockDataFiberWalker.js
│ ├── mockDataParser.js
│ ├── mockDataReq.js
│ └── style.css
└── webpack.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/react-fetch-tree/caf203accdaff966d7bc08c031a0ab6a3281824f/.DS_Store
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": "**/util",
4 | "env": {
5 | "browser": true,
6 | "es2021": true,
7 | "jquery": true,
8 | "node": true
9 | },
10 | "extends": "eslint:recommended",
11 | "parserOptions": { "sourceType": "module" },
12 | "rules": {
13 | "indent": ["warn", 2],
14 | "no-unused-vars": ["off", { "vars": "local" }],
15 | "prefer-const": "warn",
16 | "quotes": ["warn", "single"],
17 | "semi": ["warn", "always"],
18 | "space-infix-ops": "warn"
19 | }
20 | }
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Checklist
2 |
3 | - [ ] Bugfix
4 | - [ ] New feature
5 | - [ ] Refactor
6 |
7 | # Related Issue
8 |
9 | - the problem you are solving goes here.
10 |
11 | # Solution
12 |
13 | - solution to the problem goes here here. Why did you solve this problem the way you did?
14 |
15 | # Additional Info
16 |
17 | - Any additional information or context
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | coverage/
4 | .env
5 | .DS_Store/
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: "stable"
3 | before_install: npm install request
4 | script:
5 | - npm run test
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | 'git.ignoreLimitWarning': true
3 | }
--------------------------------------------------------------------------------
/FetchTreeChromeExt/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # React Fetch Tree
4 |
5 | React Fetch Tree is a tool for visualizing the location of data requests in a React app. React Fetch Tree can be used to get a bird’s eye view of your React application, show you at a glance where data requests are within your components, and show you a schema of components and their corresponding data requests.
6 |
7 | # Setting up React Fetch Tree
8 |
9 | To use React Fetch Tree there are two steps -
10 |
11 | 1. Download Chrome Extension here - the Chrome extension provides a panel in the Chrome Dev Tools that shows the visualization of your component tree and schema of your components with corresponding data requests.
12 | 2. Download this NPM package in your React application. This allows the parser to access your root application folder and find all data requests in the application.
13 |
14 | In order to obtain the structure of your app for the visualization React Fetch Tree relies on the React Developer Tools. Please install the [React Developer Tools here](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) if you haven't already.
15 |
16 | # Installation
17 |
18 | **To download the package:**
19 |
20 | ```javascript
21 | npm i @rft/reactfetchtree
22 | ```
23 |
24 | **Configure the parser to read from your root file:**
25 |
26 | React Fetch Tree uses an abstract syntax parser to find data requests within your React app and maps these to the component tree visualization in the browser. In order for the parser to run correctly and access all of your components, you will need to pass in the root file path to the parser directly
27 |
28 | To do this:
29 |
30 | 1. Open your node_modules folder, and navigate to the @reactfetchtree/rft folder.
31 | 2. Go to the parser.ts file and enter the path for the entry file. Your entry file is the place where you are hanging your app(The path is already set up with ../../../ which should bring you to your root folder).
32 | The place to add your entry file will look like this:
33 |
34 | ```javascript
35 | const resultObj: string = JSON.stringify(
36 | dependenciesGraph(path.join(__dirname, "../../../ENTER PATH HERE"))
37 | );
38 | ```
39 |
40 | 3. Save this file. Now in your terminal within your app directory, run the following command:
41 |
42 | ```javascript
43 | npm explore @reactfetchtree/rft -- npm run parser
44 | ```
45 |
46 | 4. Your component table has now been generated! If you want to see this data you can find it at node_modules/@reactfetchtree/rft/componentStore.js.
47 |
48 | **Import the Fetch Tree Hook to your React application:**
49 |
50 | 1. Inside your top level component (for example, your App.js file), import the Fetch Tree Hook and add the hook component inside the return statement within the outer enclosing div's or fragment.
51 |
52 | ```javascript
53 | import FetchTreeHook from "@reactfetchtree/rft/FetchTreeHook";
54 | ```
55 |
56 | 2. You can now start your local server and run the Fetch Tree Chrome Extension in your browser.
57 |
58 |
59 | ## Project contributors
60 |
61 | Trevor Carr
62 | - [linkedin](https://www.linkedin.com/in/trevor-carr-220481203/)
63 | - [Github](https://github.com/trevcarr95)
64 |
65 | Cara Dibdin
66 | - [Linkedin](https://www.linkedin.com/in/cara-dibdin/)
67 | - [Github](https://github.com/caradubdub)
68 |
69 | James Ferrell
70 | - [Linkedin](https://www.linkedin.com/in/james-d-ferrell/)
71 | - [Github](https://github.com/jdferrell009)
72 |
73 | Chris Lung
74 | - [Linkedin](https://www.linkedin.com/in/chris-lung-cpa-5b69b2ba/)
75 | - [Github](https://github.com/chrisl-13)
76 |
77 | Anika Mustafiz
78 | - [Linkedin](https://www.linkedin.com/in/anikamustafiz-lillies/)
79 | - [Github](https://github.com/amustafiz)
--------------------------------------------------------------------------------
/FetchTreeChromeExt/background.js:
--------------------------------------------------------------------------------
1 | const connections = {};
2 |
3 | //Handle port logic for connections between contentScript and devtools panel
4 | chrome.runtime.onConnect.addListener((port) => {
5 | //Assign the listener function to a variable so we can remove it later
6 | const devToolsListener = (message, sender, sendResponse) => {
7 | //Create a new key/value pair of current window & devtools tab when a new devtools tab is opened
8 | if (message.name === 'connect' && message.tabId) {
9 | if (!connections[message.tabId]) {
10 | connections[message.tabId] = port;
11 | }
12 | return;
13 | }
14 | if (message.name === 'orgChart') {
15 | const portID = sender.sender.tab.id;
16 | //Check to see if port is part of current connections object, if yes pass message through port
17 | if (connections[portID]) {
18 | connections[portID].postMessage({
19 | name: message.name,
20 | payload: message.payload,
21 | });
22 | }
23 | }
24 |
25 | if (message.name === 'componentObj') {
26 | const portID = sender.sender.tab.id;
27 | //Check to see if port is part of current connections object, if yes pass message through port
28 | if (connections[portID]) {
29 | connections[portID].postMessage({
30 | name: message.name,
31 | payload: message.payload,
32 | });
33 | }
34 | }
35 | return true;
36 | };
37 |
38 | //Establish port with content script
39 | if (port.name === 'contentScript') port.onMessage.addListener(devToolsListener);
40 |
41 | //Establish port with devtools panel
42 | if (port.name === 'Panel') port.onMessage.addListener(devToolsListener);
43 |
44 | port.onDisconnect.addListener((portObj) => {
45 | //Remove listener
46 | portObj.onMessage.removeListener(devToolsListener);
47 |
48 | //Remove this connection instance from list of connections
49 | const tabIds = Object.keys(connections);
50 | tabIds.forEach((id) => {
51 | if (connections[id] === portObj) delete connections[id];
52 | });
53 | });
54 | });
55 |
56 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/contentScript.js:
--------------------------------------------------------------------------------
1 | //Declare function used to injectScript to dom
2 | function injectScript(file_path, tag) {
3 | const node = document.getElementsByTagName(tag)[0];
4 | const script = document.createElement('script');
5 | script.setAttribute('type', 'text/javascript');
6 | script.setAttribute('src', file_path);
7 | node.appendChild(script);
8 | }
9 |
10 | //Call function with injectScript.js as argument
11 | injectScript(chrome.runtime.getURL('injectScript.js'), 'body');
12 |
13 | //Set up port for communication between background.js and contentScript
14 | const port = chrome.runtime.connect('pfcfmbpfgfnlhfbccgddiilfdmdlhdom', {
15 | name: 'contentScript',
16 | });
17 | //Send test message
18 | port.postMessage({
19 | name: 'contentScript test',
20 | payload: 'this is coming from contentScript',
21 | });
22 |
23 | let componentObj = {};
24 |
25 | //Set up listener for messages coming from client side
26 | window.addEventListener(
27 | 'message',
28 | function (event) {
29 | // Only accept messages from the current tab
30 | if (event.source != window) return;
31 |
32 | // If componentObj is received through window from injectScript, pass to panel through port
33 | if (event.data.type === 'componentObj') {
34 | componentObj = event.data.payload;
35 | port.postMessage({ name: 'componentObj', payload: componentObj })
36 | }
37 |
38 | // If orgChart is received through window from injectScript, pass to panel through port
39 | if (event.data.type && event.data.type === 'orgChart') {
40 | port.postMessage({
41 | name: 'orgChart',
42 | payload: event.data.payload,
43 | });
44 | }
45 | },
46 | false
47 | );
48 |
49 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/injectScript.js:
--------------------------------------------------------------------------------
1 | //Declare object to be consumed by fiberwalker
2 | let componentObj = {};
3 |
4 | //Set up listener for messages coming from client side
5 | window.addEventListener(
6 | 'message',
7 | function (event) {
8 | // Only accept messages from the current tab
9 | if (event.source != window) return;
10 |
11 | // Conditional check to see if componentObj has been received from client side FetchTreeHook
12 | if (event.data.type && event.data.type === 'componentObj') componentObj = event.data.payload;
13 | },
14 | false
15 | );
16 |
17 |
18 | //Fiberwalker function
19 | const fiberwalker = (
20 | node,
21 | componentStore,
22 | treedata = { name: 'Fiber Root', children: [] }
23 | ) => {
24 | const dataReqArr = [
25 | 'fetch',
26 | 'axios',
27 | 'http',
28 | 'https',
29 | 'qwest',
30 | 'superagent',
31 | 'XMLHttpRequest',
32 | ];
33 |
34 | function Node(name) {
35 | this.name = name;
36 | this.children = [];
37 | }
38 |
39 | if (!node) return;
40 |
41 | while (node) {
42 | let name;
43 | if (node.elementType) {
44 | if (typeof node.elementType == 'string') {
45 | name = node.elementType;
46 | } else if (node.elementType.name !== undefined) {
47 | name = node.elementType.name;
48 | } else {
49 | name = 'anon.';
50 | }
51 | } else {
52 | name = 'anon.';
53 | }
54 | const currentNode = { name, children: [] };
55 | if (componentStore !== undefined) {
56 | let requests = [];
57 | let str = '';
58 | if (componentStore[name]) {
59 | //Iterate through every entry and check request type
60 | const dataRequest = Object.values(componentStore[name]);
61 | dataRequest.forEach((el) => {
62 | if (dataReqArr.includes(el.reqType)) {
63 | requests.push(`${el.reqType}`);
64 | }
65 | });
66 |
67 | while (requests.length) {
68 | const temp = requests.splice(0, 1);
69 | const number = requests.reduce((acc, cur) => {
70 | if (cur == temp) acc += 1;
71 | return acc;
72 | }, 1);
73 | requests = requests.filter((el) => el != temp);
74 | str += !str.length
75 | ? `${number} ${temp} request${number > 1 ? 's' : ''}`
76 | : `, ${number} ${temp} request${number > 1 ? 's' : ''}`;
77 | }
78 | }
79 | currentNode.dataRequest = str;
80 | }
81 | treedata.children.push(currentNode);
82 |
83 | if (node.child) {
84 | fiberwalker(
85 | node.child,
86 | componentStore,
87 | treedata.children[treedata.children.length - 1]
88 | );
89 | }
90 |
91 | node = node.sibling;
92 | }
93 | return treedata;
94 | };
95 |
96 | //Declare variables needed for onCommitFiberRoot function
97 | let __ReactFiberDOM;
98 | const devTools = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
99 | let orgChart;
100 |
101 | //Add custom functionality to devTools onCommitFiberRoot function
102 | devTools.onCommitFiberRoot = (function (original) {
103 | return function (...args) {
104 | __ReactFiberDOM = args[1];
105 | orgChart = fiberwalker(__ReactFiberDOM.current, componentObj);
106 | // Pass orgChart through window to contentScript
107 | window.postMessage({
108 | type: 'orgChart',
109 | payload: orgChart,
110 | });
111 | return original(...args);
112 | };
113 | })(devTools.onCommitFiberRoot);
114 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "React Fetch Tree",
4 | "description": "Interface to present data compiled from client codebase in development mode, using react devtool functions and babel parser",
5 | "version": "1.0",
6 | "browser_action": {
7 | "default_icon": "Logo.png"
8 | },
9 | "icons": {
10 | "128": "Logo.png",
11 | "16": "Logo.png"
12 | },
13 | "background": {
14 | "scripts": [
15 | "background.js"
16 | ],
17 | "persistent": false
18 | },
19 | "content_scripts": [
20 | {
21 | "matches": [
22 | "http://localhost/*"
23 | ],
24 | "js": [
25 | "contentScript.js"
26 | ]
27 | }
28 | ],
29 | "web_accessible_resources": [
30 | "injectScript.js"
31 | ],
32 | "devtools_page": "devtools.html"
33 | }
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/assets/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/react-fetch-tree/caf203accdaff966d7bc08c031a0ab6a3281824f/FetchTreeChromeExt/src/assets/Logo.png
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/ComponentStorePanel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | //Display all components and their corresponding data requests
4 | const ComponentStorePanel = (props) => {
5 | return (
6 |
7 | {props.componentArr.map((key, i) =>
8 |
9 |
{key[0] !== 'null' && key[0]}
10 |
11 | {key[0] !== 'null' && Object.entries(key[1]).length === 0 && 'No data requests'}
12 |
13 |
14 | {Object.values(key[1]).map(key =>
15 |
16 | Data Request Type: '{key.reqType}' Parent: '{key.parentName}'
17 |
18 | )}
19 |
20 | )}
21 |
22 | )
23 | }
24 |
25 | export default ComponentStorePanel;
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/LinkControls.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | //Orientation settings for visualization
4 | const controlStyles = { fontSize: 14, fontWeight: 500, color: '#282828' };
5 |
6 | type Props = {
7 | orientation: string;
8 | setOrientation: (orientation: string) => void;
9 | };
10 |
11 |
12 | export default function LinkControls({
13 | orientation,
14 | setOrientation,
15 | }: Props) {
16 |
17 | return (
18 |
19 |
20 |
21 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/Panel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import ParentSize from '@visx/responsive/lib/components/ParentSize';
3 | import Viz from './treeViz';
4 | import ComponentStorePanel from './ComponentStorePanel';
5 |
6 | const Panel = () => {
7 | //Set default state for panel
8 | const [displayStore, setDisplayStore] = useState(true);
9 | const [componentArr, setComponentArr] = useState([['App', {}]]);
10 | const [orgChart, setOrgChart] = useState({ name: 'Fiber Root' });
11 | //Set flag for receiving componentObj from port
12 | let componentObjReceived = false;
13 |
14 | //Create a port and establish a connection between panel and background.js
15 | const port = chrome.runtime.connect({ name: 'Panel' });
16 | port.postMessage({
17 | name: 'connect',
18 | tabId: chrome.devtools.inspectedWindow.tabId,
19 | });
20 |
21 | //Listen for messages sent in the port and set state for components
22 | port.onMessage.addListener((message) => {
23 | if (message.name === 'componentObj') {
24 | if (!componentObjReceived) {
25 | setComponentArr(Object.entries(message.payload));
26 | componentObjReceived = true;
27 | }
28 | }
29 | if (message.name === 'orgChart') {
30 | setOrgChart(message.payload);
31 | }
32 | });
33 |
34 | //Toggle function to change views in panel
35 | const toggle = (e) => {
36 | e.target.value === 'Component Store'
37 | ? setDisplayStore(true)
38 | : setDisplayStore(false);
39 | };
40 |
41 | return (
42 |
43 |
44 |
45 |
62 |
79 |
80 |
81 |
82 | {displayStore === false ? (
83 |
84 | {({ width, height }) => (
85 |
86 | )}
87 |
88 | ) : (
89 |
90 | )}
91 |
92 |
93 | );
94 | };
95 |
96 | export default Panel;
97 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/getLinkComponent.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LinkHorizontal,
3 | LinkVertical,
4 | } from '@visx/shape';
5 |
6 | export default function getLinkComponent({
7 | orientation,
8 | }: {
9 | layout: string;
10 | linkType: string;
11 | orientation: string;
12 | }): React.ComponentType {
13 | let LinkComponent: React.ComponentType;
14 |
15 | if (orientation === 'vertical') {
16 | LinkComponent = LinkVertical;
17 | } else {
18 | LinkComponent = LinkHorizontal;
19 | }
20 | return LinkComponent;
21 | }
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import './sandbox-styles.css';
4 | import Panel from './Panel';
5 |
6 | render(
7 | ,
10 | document.getElementById('app')
11 | );
12 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/sandbox-styles.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | #root {
4 | height: 100%;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
6 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
7 | line-height: 2em;
8 | }
9 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/treeViz.tsx:
--------------------------------------------------------------------------------
1 | import React, { useDebugValue, useState, useMemo } from 'react';
2 | import { Group } from '@visx/group';
3 | import { hierarchy, Tree } from '@visx/hierarchy';
4 | import { LinearGradient } from '@visx/gradient';
5 | import useForceUpdate from './useForceUpdate';
6 | import LinkControls from './LinkControls';
7 | import getLinkComponent from './getLinkComponent';
8 | import { Zoom } from '@visx/zoom';
9 | import { localPoint } from '@visx/event';
10 | interface TreeNode {
11 | name: string;
12 | isExpanded?: boolean;
13 | children?: TreeNode[];
14 | dataRequest?: string;
15 | }
16 | interface DataRequest {
17 | name: string;
18 | dataRequest: string;
19 | }
20 |
21 | const defaultMargin = { top: 30, left: 30, right: 30, bottom: 30 };
22 |
23 | export type LinkTypesProps = {
24 | width: number;
25 | height: number;
26 | margin?: { top: number; right: number; bottom: number; left: number };
27 | orgChart: TreeNode;
28 | };
29 |
30 | export default function Viz({
31 | width: totalWidth,
32 | height: totalHeight,
33 | margin = defaultMargin,
34 | orgChart: orgChart,
35 | }: LinkTypesProps) {
36 | const [layout, setLayout] = useState('cartesian');
37 | const [orientation, setOrientation] = useState('vertical');
38 | const [linkType, setLinkType] = useState('diagonal');
39 | const [stepPercent, setStepPercent] = useState(0.5);
40 | let [spread, setSpread] = useState(1);
41 | const forceUpdate = useForceUpdate();
42 | const [displayFetch, setDisplayFetch] = useState(false);
43 | const [fetchComponent, setFetchComponent] = useState({ name: '', dataRequest: '' })
44 |
45 | const innerWidth = totalWidth - margin.left - margin.right;
46 | const innerHeight = totalHeight - margin.top - margin.bottom;
47 |
48 | const data: TreeNode = useMemo(() => {
49 | return orgChart;
50 | }, [orgChart]);
51 |
52 | let origin: { x: number; y: number };
53 | let sizeWidth: number;
54 | let sizeHeight: number;
55 |
56 | origin = { x: 0, y: 0 };
57 | if (orientation === 'vertical') {
58 | sizeWidth = innerWidth;
59 | sizeHeight = innerHeight;
60 | } else {
61 | sizeWidth = innerHeight;
62 | sizeHeight = innerWidth;
63 | }
64 |
65 | const LinkComponent = getLinkComponent({ layout, linkType, orientation });
66 |
67 | const initialTransform = {
68 | scaleX: 1,
69 | scaleY: 1,
70 | translateX: 0,
71 | translateY: 0,
72 | skewX: 0,
73 | skewY: 0,
74 | };
75 |
76 | const changeSpread = (e) => {
77 | if (e.target.id === 'buttonMinus') {
78 | if (spread > 0 && spread !== 1) setSpread(spread -= 1)
79 | } else {
80 | if (spread < 10 && spread !== 10) setSpread(spread += 1)
81 | }
82 | }
83 |
84 | return totalWidth < 10 ? null : (
85 |
86 |
87 | {displayFetch ?
{`Name: ${fetchComponent.name}, Data Request: ${fetchComponent.dataRequest}`}
:
}
88 |
92 |
93 |
102 | {(zoom) => (
103 |
104 |
251 |
252 |
259 |
266 |
276 |
286 |
287 |
288 | Node Spread:
289 |
290 |
291 |
{spread}
292 |
293 |
294 |
295 |
296 |
297 |
298 | )}
299 |
300 |
301 | );
302 | }
303 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/components/useForceUpdate.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export default function useForceUpdate() {
4 | const [, setValue] = useState(0);
5 | return () => setValue(value => value + 1); // update state to force render
6 | }
7 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/devtools/devtools.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/devtools/devtools.js:
--------------------------------------------------------------------------------
1 | //Create panel
2 | chrome.devtools.panels.create(
3 | 'React Fetch Tree', // Title for the panel tab
4 | null, // Can specify path to an icon
5 | 'index.html', // Html page for injecting into the tab's content
6 | () => { }
7 | );
8 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |

22 |
React Fetch Tree
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/src/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | font-family: Roboto, system-ui, sans-serif;
6 | }
7 |
8 | :root {
9 | --main-bg: #f3f3f3;
10 | --text-color: #282828;
11 | }
12 |
13 | body {
14 | height: 100%;
15 | background-color: var(--main-bg);
16 | color: var(--text-color);
17 | }
18 |
19 | .btn {
20 | margin: 0;
21 | text-align: center;
22 | border: none;
23 | background: #2f2f2f;
24 | color: #888;
25 | padding: 0 4px;
26 | border: 1px solid grey;
27 | }
28 | .btn-lg {
29 | font-size: 12px;
30 | width: 50px;
31 | line-height: 1;
32 | padding: 4px;
33 | }
34 | .btn-zoom {
35 | width: 26px;
36 | font-size: 22px;
37 | }
38 | .btn-bottom {
39 | margin-bottom: 1rem;
40 | }
41 | .controls {
42 | position: absolute;
43 | top: 15px;
44 | right: 15px;
45 | display: flex;
46 | flex-direction: column;
47 | align-items: flex-end;
48 | }
49 | .relative {
50 | position: relative;
51 | }
52 | .fetchBox {
53 | display: flex;
54 | align-items: center;
55 | justify-content: flex-end;
56 | text-align: center;
57 | padding-left: 5px;
58 | padding-bottom: 5px;
59 | background-color: var(--main-bg);
60 | }
61 |
62 | #requestDisplay {
63 | margin-right: auto;
64 | }
65 |
66 | .panelNav {
67 | background-color: var(--main-bg);
68 | color: var(--text-color);
69 | height: 40px;
70 | display: flex;
71 | align-items: center;
72 | }
73 |
74 | .allOptions {
75 | padding-left: 15px;
76 | display: flex;
77 | margin-left: -8px;
78 | margin-top: 15px;
79 | margin-bottom: 10px;
80 | }
81 |
82 | .allOptions button {
83 | align-self: center;
84 | display: inline-block;
85 | padding: 0.35em 1.2em;
86 | border: 0.1em solid var(--text-color);
87 | margin: 0 0.3em 0.3em 0;
88 | border-radius: 0.12em;
89 | box-sizing: border-box;
90 | text-align: center;
91 | transition: all 0.2s;
92 | color: #fdfdfd;
93 | font-weight: 500;
94 | text-decoration: none;
95 | }
96 |
97 | .allOptions button:hover {
98 | color: var(--text-color) !important;
99 | background-color: #b998f4 !important;
100 | border: 1px solid var(--text-color) !important;
101 | }
102 |
103 | .allOptions button:focus {
104 | outline: none;
105 | }
106 |
107 | #componentStore {
108 | height: 92vh;
109 | max-height: 100%;
110 | padding: 10px;
111 | background-color: #272b4d;
112 | }
113 |
114 | .componentName {
115 | font-size: 1.5em;
116 | color: #68d1f5;
117 | }
118 |
119 | .componentDetails {
120 | font-size: 1em;
121 | }
122 |
123 | .detailsLabel {
124 | color: #b998f4;
125 | }
126 |
127 | .details {
128 | color: #26deb0;
129 | }
130 |
--------------------------------------------------------------------------------
/FetchTreeChromeExt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "noImplicitAny": false,
5 | "module": "es6",
6 | "target": "es5",
7 | "jsx": "react",
8 | "allowJs": true,
9 | "allowSyntheticDefaultImports": true,
10 | "typeRoots": [
11 | "../node_modules/@types"
12 | ],
13 | "types": [
14 | "chrome",
15 | "jest",
16 | "node"
17 | ],
18 | },
19 | "include": [
20 | "../node_modules/@visx"
21 | ]
22 | }
--------------------------------------------------------------------------------
/FetchTreeNPMPkg/FetchTreeHook.js:
--------------------------------------------------------------------------------
1 | import componentObj from './componentStore';
2 | import { Component } from 'react';
3 |
4 | //Declare FetchTreeHook that user will import into their codebase
5 | class FetchTreeHook extends Component {
6 | constructor(props) {
7 | super(props)
8 | this.state = {
9 | dummy: true,
10 | };
11 | }
12 |
13 | render() {
14 | window.postMessage({ type: 'componentObj', payload: componentObj }, "*");
15 | //Trigger state change to populate data in panel
16 | setTimeout(() => {
17 | this.setState({
18 | dummy: true,
19 | });
20 | }, 2000);
21 | return null;
22 | }
23 | }
24 |
25 | export default FetchTreeHook;
--------------------------------------------------------------------------------
/FetchTreeNPMPkg/README.md:
--------------------------------------------------------------------------------
1 | # React Fetch Tree - reactfetchtree.com
2 |
3 | React Fetch Tree is an open source developer tool for visualizing the location of data requests in a React app. React Fetch Tree can be used to get a bird’s eye view of your React application, show you at a glance where data requests are within your components, and show you a schema of components and their corresponding data requests.
4 |
5 | # Setting up React Fetch Tree
6 |
7 | To use React Fetch Tree there are two steps -
8 |
9 | 1. Download Chrome Extension here - the Chrome extension provides a panel in the Chrome Dev Tools that shows the visualization of your component tree and schema of your components with corresponding data requests.
10 | 2. Download this NPM package in your React application. This allows the parser to access your root application folder and find all data requests in the application.
11 |
12 | In order to obtain the structure of your app for the visualization React Fetch Tree relies on the React Developer Tools. Please install the [React Developer Tools here](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) if you haven't already.
13 |
14 | # Installation
15 |
16 | **To download the package:**
17 |
18 | ```javascript
19 | npm i @reactfetchtree/rft
20 | ```
21 |
22 | **Configure the parser to read from your root file:**
23 |
24 | React Fetch Tree uses an abstract syntax parser to find data requests within your React app and maps these to the component tree visualization in the browser. In order for the parser to run correctly and access all of your components, you will need to pass in the root file path to the parser directly
25 |
26 | To do this:
27 |
28 | 1. Open your node_modules folder, and navigate to the @reactfetchtree/rft folder.
29 | 2. Go to the parser.ts file and enter the path for the entry file. Your entry file is the place where you are hanging your app(The path is already set up with ../../../ which should bring you to your root folder).
30 | The place to add your entry file will look like this:
31 |
32 | ```javascript
33 | const resultObj: string = JSON.stringify(
34 | dependenciesGraph(path.join(__dirname, "../../../ENTER PATH HERE"))
35 | );
36 | ```
37 |
38 | 3. Save this file. Now in your terminal within your app directory, run the following command:
39 |
40 | ```javascript
41 | npm explore @reactfetchtree/rft -- npm run parser
42 | ```
43 | Upon success you will see the message:
44 | `parser completed successfully`
45 |
46 | 4. Your component table has now been generated! If you want to see this data you can find it at node_modules/@reactfetchtree/rft/componentStore.js.
47 |
48 | **Import the Fetch Tree Hook to your React application:**
49 |
50 | 1. Inside your top level component (for example, your App.js file), import the Fetch Tree Hook and add the hook component inside the return statement within the outer enclosing div's or fragment.
51 |
52 | ```javascript
53 | import FetchTreeHook from "@reactfetchtree/rft/FetchTreeHook";
54 |
55 | const App = () => {
56 | return (
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default App;
66 | ```
67 |
68 | 2. You can now start your local server and run the Fetch Tree Chrome Extension in your browser.
69 |
70 |
71 | ## Project contributors
72 |
73 | Trevor Carr
74 | - [Linkedin](https://www.linkedin.com/in/carr-trevor/)
75 | - [Github](https://github.com/trevcarr95)
76 |
77 | Cara Dibdin
78 | - [Linkedin](https://www.linkedin.com/in/cara-dibdin/)
79 | - [Github](https://github.com/caradubdub)
80 |
81 | James Ferrell
82 | - [Linkedin](https://www.linkedin.com/in/james-d-ferrell/)
83 | - [Github](https://github.com/jdferrell009)
84 |
85 | Chris Lung
86 | - [Linkedin](https://www.linkedin.com/in/chris-lung-cpa-5b69b2ba/)
87 | - [Github](https://github.com/chrisl-13)
88 |
89 | Anika Mustafiz
90 | - [Linkedin](https://www.linkedin.com/in/anikamustafiz-lillies/)
91 | - [Github](https://github.com/amustafiz)
--------------------------------------------------------------------------------
/FetchTreeNPMPkg/componentStore.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/react-fetch-tree/caf203accdaff966d7bc08c031a0ab6a3281824f/FetchTreeNPMPkg/componentStore.js
--------------------------------------------------------------------------------
/FetchTreeNPMPkg/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@reactfetchtree/rft",
3 | "version": "3.4.0",
4 | "scripts": {
5 | "parser": "ts-node parser.ts"
6 | },
7 | "dependencies": {
8 | "@babel/parser": "^7.13.4",
9 | "@babel/traverse": "^7.13.0",
10 | "react": "^17.0.1",
11 | "ts-node": "^9.1.1",
12 | "typescript": "^4.2.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/FetchTreeNPMPkg/parser.ts:
--------------------------------------------------------------------------------
1 | const babelParser = require('@babel/parser');
2 | const traverse = require('@babel/traverse').default;
3 | const fs = require('fs');
4 | const path = require('path');
5 |
6 | let ID: number = 0;
7 |
8 | type CacheStore = { [key: string]: number } | {};
9 | type InvocationStore = { [key: string]: [] | string[] } | {};
10 | type NodeStore = { [key: string]: { reqType: string, parentName: string, filename: string } } | {};
11 | type ComponentStore = { [key: string]: { reqType: string, parentName: string } } | {};
12 |
13 | const [cache, invocationStore, nodeStore, componentStore]: [CacheStore, InvocationStore, NodeStore, ComponentStore] = [{}, {}, {}, {}];
14 |
15 | //Helper function to check node existence
16 | const nodeExistence = (
17 | nodePosition: string,
18 | reqType: string,
19 | parentName: string,
20 | filename: string,
21 | exists: boolean = false
22 | ) => {
23 | let nodePos: string = `line: ${nodePosition['line']}, column: ${nodePosition['column']}`;
24 | if (parentName === null) parentName = 'Anonymous';
25 | if (nodeStore[nodePos]) exists = true;
26 | if (!exists) {
27 | let nodeFileName: string[] | string = filename.split('/');
28 | nodeFileName = nodeFileName[nodeFileName.length - 1].split('.')[0];
29 | nodeStore[nodePos] = {
30 | reqType,
31 | parentName,
32 | fileName: nodeFileName,
33 | };
34 | }
35 | return;
36 | };
37 |
38 | //Obtain target file's dependencies
39 | const getDependencies = (filename: string) => {
40 | const dependencies: string[] = [];
41 | let [reqType, parentName]: [string | null, string | null] = [null, null];
42 | let parserConfig: { sourceType: string, plugins: string[] } = {
43 | sourceType: 'module',
44 | plugins: ['jsx'],
45 | }
46 |
47 | const content: string = fs.readFileSync(filename, 'utf8');
48 | const raw_ast: {} = babelParser.parse(content, parserConfig);
49 |
50 | //Node types and conditionals
51 | const IdentifierPath = {
52 | CallExpression: ({ node }) => {
53 | reqType = node.callee.name;
54 | if (node.callee.name) {
55 | nodeExistence(node.loc.start, reqType, parentName, filename);
56 | } []
57 | if (invocationStore[parentName]) {
58 | invocationStore[parentName].push(reqType);
59 | }
60 | },
61 | MemberExpression: ({ node }) => {
62 | reqType = node.object.name;
63 | if (
64 | reqType === 'axios' ||
65 | reqType === 'http' ||
66 | reqType === 'https' ||
67 | reqType === 'qwest' ||
68 | reqType === 'superagent'
69 | ) {
70 | nodeExistence(node.loc.start, reqType, parentName, filename);
71 | }
72 | if (node.property.name === 'ajax') {
73 | reqType = node.property.name;
74 | nodeExistence(node.loc.start, reqType, parentName, filename);
75 | }
76 | },
77 | NewExpression: ({ node }) => {
78 | reqType = node.callee.name;
79 | if (reqType === 'XMLHttpRequest') {
80 | nodeExistence(node.loc.start, reqType, parentName, filename);
81 | }
82 | },
83 | ReturnStatement: ({ node }) => {
84 | if (node.argument) {
85 | if (
86 | node.argument.type === 'JSXElement' ||
87 | (node.argument.type === 'JSXFragment' &&
88 | parentName &&
89 | !componentStore.hasOwnProperty(parentName))
90 | ) {
91 | componentStore[parentName] = {};
92 | }
93 | }
94 | },
95 | JSXExpressionContainer: ({ node }) => {
96 | reqType = node.expression.name;
97 | if (node.expression.name) {
98 | if (invocationStore[parentName]) {
99 | invocationStore[parentName].push(reqType);
100 | }
101 | }
102 | },
103 | };
104 |
105 | //Traverse AST using babeltraverse to identify imported nodes
106 | traverse(raw_ast, {
107 | ImportDeclaration: ({ node }) => {
108 | if (node.source.value.indexOf('./') !== -1) {
109 | if (node.specifiers.length !== 0) {
110 | dependencies.push(node.source.value);
111 | }
112 | }
113 | },
114 | Function(path) {
115 | if (path.node.id) {
116 | parentName = path.node.id.name;
117 | if (!invocationStore[parentName]) {
118 | invocationStore[parentName] = [];
119 | }
120 | }
121 | path.traverse(IdentifierPath);
122 | parentName = null;
123 | },
124 | VariableDeclarator(path) {
125 | if (path.parent.declarations[0].id.name) {
126 | parentName = path.parent.declarations[0].id.name;
127 | if (!invocationStore[parentName]) {
128 | invocationStore[parentName] = [];
129 | }
130 | }
131 | path.traverse(IdentifierPath);
132 | parentName = null;
133 | },
134 | ExpressionStatement(path) {
135 | path.traverse(IdentifierPath);
136 | },
137 | ClassDeclaration(path) {
138 | if (path.node.id) {
139 | parentName = path.node.id.name;
140 | if (!invocationStore[parentName]) {
141 | invocationStore[parentName] = [];
142 | }
143 | }
144 | path.traverse(IdentifierPath);
145 | parentName = null;
146 | },
147 | });
148 |
149 | const id: number = ID++;
150 | cache[filename] = id;
151 |
152 | return {
153 | id,
154 | filename,
155 | dependencies,
156 | };
157 | };
158 |
159 | //Helper function to complete componentStore
160 | const componentGraph = (invocationStore: {}, nodeStore: {}, componentStore: {}) => {
161 | const dataTypeCheck: {}[] = [invocationStore, nodeStore, componentStore];
162 | if (dataTypeCheck.some(arg => Array.isArray(arg) || !arg || typeof arg !== 'object')) {
163 | throw new TypeError('Arguments passed in must be of an object data type');
164 | };
165 |
166 | for (let node in nodeStore) {
167 | let { parentName, reqType, fileName }: { parentName: string, reqType: string, fileName: string } = nodeStore[node];
168 | if (
169 | reqType === 'fetch' ||
170 | reqType === 'axios' ||
171 | reqType === 'http' ||
172 | reqType === 'https' ||
173 | reqType === 'qwest' ||
174 | reqType === 'superagent' ||
175 | reqType === 'ajax' ||
176 | reqType === 'XMLHttpRequest'
177 | ) {
178 | if (componentStore[parentName]) {
179 | componentStore[parentName][node] = { reqType, parentName };
180 | }
181 | if (componentStore[fileName]) {
182 | componentStore[fileName][node] = { reqType, parentName: fileName };
183 | }
184 | for (let component in invocationStore) {
185 | invocationStore[component].forEach((dataReq) => {
186 | if (
187 | componentStore[component] &&
188 | invocationStore[component].includes(parentName)
189 | ) {
190 | componentStore[component][node] = { reqType, parentName };
191 | }
192 | });
193 | }
194 | }
195 | }
196 | return componentStore;
197 | };
198 |
199 | const dependenciesGraph = (entryFile: string) => {
200 | const extension: string = entryFile.match(/\.[0-9a-z]+$/i)[0];
201 |
202 | if (extension === '.js' || extension === '.jsx') {
203 | const entry: { id: number; filename: any; dependencies: any[]; } = getDependencies(entryFile);
204 | const queue: {
205 | id: number;
206 | filename: any;
207 | dependencies: any[];
208 | }[] = [entry];
209 |
210 | for (const asset of queue) {
211 | const dirname = path.dirname(asset.filename);
212 |
213 | asset.dependencies.forEach((relativePath) => {
214 | let absolutePath = path.resolve(dirname, relativePath);
215 | let fileCheck = fs.existsSync(absolutePath);
216 | let child;
217 |
218 | if (!fileCheck) {
219 | absolutePath = path.resolve(dirname, relativePath + '.js');
220 | fileCheck = fs.existsSync(absolutePath);
221 | if (!fileCheck) absolutePath = absolutePath + 'x';
222 | }
223 |
224 | if (!cache[absolutePath]) {
225 | child = getDependencies(absolutePath);
226 | queue.push(child);
227 | }
228 | });
229 | }
230 | return componentGraph(invocationStore, nodeStore, componentStore);
231 | } else {
232 | throw new Error('Entry file must be .js or .jsx')
233 | }
234 | };
235 |
236 | /*
237 | Please enter the path for entry file as the argument in dependenciesGraph.
238 | Must be a .js/.jsx file or parser will not run.
239 | */
240 |
241 | if (process.env.NODE_ENV !== 'test') {
242 | //Enter path to entry file
243 | const resultObj: string = JSON.stringify(
244 | dependenciesGraph(path.join(__dirname, '../../../ENTER PATH HERE'))
245 | );
246 |
247 | const componentObj: string = `const componentObj = ${resultObj}
248 | module.exports = componentObj;`;
249 |
250 | try {
251 | fs.writeFileSync(
252 | path.join(__dirname, './componentStore.js'),
253 | componentObj,
254 | (err) => {
255 | if (err) throw err;
256 | }
257 | );
258 | console.log('parser completed successfully');
259 | } catch (err) {
260 | console.log(err);
261 | };
262 | }
263 |
264 | module.exports = {
265 | dependenciesGraph,
266 | componentGraph,
267 | getDependencies,
268 | nodeExistence,
269 | };
270 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # React Fetch Tree - reactfetchtree.com
4 |
5 | React Fetch Tree is an open source developer tool for visualizing the location of data requests in a React app. React Fetch Tree can be used to get a bird’s eye view of your React application, show you at a glance where data requests are within your components, and show you a schema of components and their corresponding data requests.
6 |
7 | # Setting up React Fetch Tree
8 |
9 | To use React Fetch Tree there are two steps -
10 |
11 | 1. Download Chrome Extension here - the Chrome extension provides a panel in the Chrome Dev Tools that shows the visualization of your component tree and schema of your components with corresponding data requests.
12 | 2. Download the React Fetch Tree NPM package in your React application. This allows the parser to access your root application folder and find all data requests in the application.
13 |
14 | In order to obtain the structure of your app for the visualization React Fetch Tree relies on the React Developer Tools. Please install the [React Developer Tools here](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) if you haven't already.
15 |
16 | # Installing the NPM Package
17 |
18 | **To download the package:**
19 |
20 | ```javascript
21 | npm i @reactfetchtree/rft
22 | ```
23 |
24 | **Configure the parser to read from your root file:**
25 |
26 | React Fetch Tree uses an abstract syntax parser to find data requests within your React app and maps these to the component tree visualization in the browser. In order for the parser to run correctly and access all of your components, you will need to pass in the root file path to the parser directly
27 |
28 | To do this:
29 |
30 | 1. Open your node_modules folder, and navigate to the @reactfetchtree/rft folder.
31 | 2. Go to the parser.ts file and enter the path for the entry file. Your entry file is the place where you are hanging your app(The path is already set up with ../../../ which should bring you to your root folder).
32 | The place to add your entry file will look like this:
33 |
34 | ```javascript
35 | const resultObj: string = JSON.stringify(
36 | dependenciesGraph(path.join(__dirname, "../../../ENTER PATH HERE"))
37 | );
38 | ```
39 |
40 | 3. Save this file. Now in your terminal within your app directory, run the following command:
41 |
42 | ```javascript
43 | npm explore @reactfetchtree/rft -- npm run parser
44 | ```
45 | Upon success you will see the message:
46 | `parser completed successfully`
47 |
48 | 4. Your component table has now been generated! If you want to see this data you can find it at node_modules/@reactfetchtree/rft/componentStore.js.
49 |
50 | **Import the Fetch Tree Hook to your React application:**
51 |
52 | 1. Inside your top level component (for example, your App.js file), import the Fetch Tree Hook and add the hook component inside the return statement within the outer enclosing div's or fragment.
53 |
54 | ```javascript
55 | import FetchTreeHook from "@reactfetchtree/rft/FetchTreeHook";
56 | const App = () => {
57 | return (
58 |
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default App;
67 | ```
68 |
69 | 2. You can now start your local server and run the Fetch Tree Chrome Extension in your browser.
70 |
71 | # Installing the React Fetch Tree Chrome Extension
72 |
73 | Installing the React Fetch Tree Chrome Extension should be fairly simple. Firstly, you will need to be using the Chrome browser. Then access the Chrome Web Store location for React Fetch Tree and download.
74 |
75 | Now when you inspect your browser window to open the console, React Fetch Tree will be available as a tab in the panel.
76 |
77 | # Troubleshooting
78 |
79 | ## My component tree isn't rendering
80 |
81 | If you can't see your component tree at all, there could be a couple of things that need to change. First, try triggering a state change in your app as this will call the function on the FiberDOM that generates the visualization object.
82 |
83 | ## Project contributors
84 |
85 | Trevor Carr
86 | - [Linkedin](https://www.linkedin.com/in/carr-trevor/)
87 | - [Github](https://github.com/trevcarr95)
88 |
89 | Cara Dibdin
90 | - [Linkedin](https://www.linkedin.com/in/cara-dibdin/)
91 | - [Github](https://github.com/caradubdub)
92 |
93 | James Ferrell
94 | - [Linkedin](https://www.linkedin.com/in/james-d-ferrell/)
95 | - [Github](https://github.com/jdferrell009)
96 |
97 | Chris Lung
98 | - [Linkedin](https://www.linkedin.com/in/chris-lung-cpa-5b69b2ba/)
99 | - [Github](https://github.com/chrisl-13)
100 |
101 | Anika Mustafiz
102 | - [Linkedin](https://www.linkedin.com/in/anikamustafiz-lillies/)
103 | - [Github](https://github.com/amustafiz)
104 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ]
6 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactfetchtree",
3 | "version": "1.0.0",
4 | "description": "React Fetch Tree Google Chrome DevTools Extension",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/oslabs-beta/react-fetch-tree"
8 | },
9 | "author": "React Fetch Tree",
10 | "scripts": {
11 | "test": "NODE_ENV=test && jest",
12 | "build": "webpack"
13 | },
14 | "dependencies": {
15 | "@babel/parser": "^7.13.4",
16 | "@babel/traverse": "^7.13.0",
17 | "react": "^17.0.1",
18 | "ts-node": "^9.1.1",
19 | "typescript": "^4.2.3",
20 | "@types/react": "^17.0.3",
21 | "@types/react-dom": "^17.0.3",
22 | "@visx/gradient": "^1.7.0",
23 | "@visx/group": "^1.7.0",
24 | "@visx/hierarchy": "^1.7.0",
25 | "@visx/responsive": "^1.7.0",
26 | "@visx/shape": "^1.7.0",
27 | "@visx/zoom": "^1.7.0",
28 | "d3-shape": "^2.1.0",
29 | "react-dom": "^16.14.0",
30 | "style-loader": "^2.0.0",
31 | "ts-loader": "^8.1.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.13.10",
35 | "@babel/preset-env": "^7.13.10",
36 | "@babel/preset-react": "^7.12.13",
37 | "@types/chrome": "^0.0.133",
38 | "@types/jest": "^26.0.22",
39 | "@types/node": "^14.14.37",
40 | "babel-loader": "^8.2.2",
41 | "clean-webpack-plugin": "^3.0.0",
42 | "copy-webpack-plugin": "^8.0.0",
43 | "css-loader": "^5.1.3",
44 | "eslint": "^7.22.0",
45 | "eslint-plugin-react": "^7.22.0",
46 | "file-loader": "^6.2.0",
47 | "html-webpack-plugin": "^5.3.1",
48 | "jest": "^26.6.3",
49 | "mini-css-extract-plugin": "^1.4.0",
50 | "ts-jest": "^26.5.4",
51 | "webpack": "^5.26.2",
52 | "webpack-cli": "^4.5.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/test/__tests__/fiberwalker.test.js:
--------------------------------------------------------------------------------
1 | const {
2 | fiberwalker,
3 | fiberTree,
4 | componentStore,
5 | } = require('../testData/mockDataFiberWalker.js');
6 |
7 | describe('fiberwalker', () => {
8 |
9 | it('Output hould return an object', () => {
10 | const result = fiberwalker(fiberTree, componentStore);
11 | expect(typeof result).toBe('object');
12 | });
13 |
14 | it('Output should return an empty object when empty objects are passed in', () => {
15 | expect(() => (fiberwalker({}, {}, {})).toEqual({}));
16 | });
17 |
18 | it('Output should return an error when any argument passed in is not an object', () => {
19 | expect(() => (fiberwalker(123, {}, {})).toThrow(TypeError));
20 | expect(() => (fiberwalker({}, [], {})).toThrow(TypeError));
21 | expect(() => (fiberwalker([], 'abc', {})).toThrow(TypeError));
22 | expect(() => (fiberwalker([], {}, null)).toThrow(TypeError));
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/__tests__/parser.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const testPath = path.join(__dirname, '../testData/index.js');
3 | //THIS NEEDS TO BE REFACTORED, we need mock parser file, not grabbing from npm directly
4 | const {
5 | dependenciesGraph,
6 | componentGraph,
7 | getDependencies,
8 | } = require('../../FetchTreeNPMPkg/parser.ts');
9 | const {
10 | invocationStore,
11 | nodeStore,
12 | componentStore,
13 | } = require('../testData/mockDataParser.js');
14 | describe('dependenciesGraph', () => {
15 | const result = dependenciesGraph(testPath);
16 |
17 | it('Output should return an error if empty string', () => {
18 | expect(() => { dependenciesGraph('') }).toThrow(TypeError);
19 | });
20 |
21 | it('Output should return an object', () => {
22 | expect(typeof result).toBe('object');
23 | });
24 |
25 | it('Output should return an error if file extension is not .jsx or .jsx', () => {
26 | expect(() => { dependenciesGraph(__dirname, '../testData/style.css') }).toThrow();
27 | });
28 | });
29 |
30 | describe('getDependencies', () => {
31 | const result = getDependencies(testPath);
32 |
33 | it('Output should return an object', () => {
34 | expect(typeof result).toBe('object');
35 | });
36 |
37 | it('Output should return an object with properties', () => {
38 | expect(Object.keys(result).length).not.toEqual(0);
39 | });
40 |
41 | it('Output should return an error if empty string', () => {
42 | expect(() => { getDependencies('') }).toThrow();
43 | });
44 | });
45 |
46 | describe('componentGraph', () => {
47 | const result = componentGraph(invocationStore, nodeStore, componentStore);
48 | it('Output should return an object', () => {
49 | expect(typeof result).toBe('object');
50 | });
51 |
52 | it('Output should return an empty object when empty objects are passed in', () => {
53 | expect(() => (componentGraph({}, {}, {})).toEqual({}));
54 | });
55 |
56 | it('Output should return an error when any argument passed in is not an object', () => {
57 | expect(() => (componentGraph(123, {}, {})).toThrow(TypeError));
58 | expect(() => (componentGraph({}, [], {})).toThrow(TypeError));
59 | expect(() => (componentGraph([], 'abc', {})).toThrow(TypeError));
60 | expect(() => (componentGraph([], {}, null)).toThrow(TypeError));
61 | });
62 |
63 | });
--------------------------------------------------------------------------------
/test/testData/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Nav from './Nav';
3 | import Body from './Body.jsx';
4 | import Footer from './Footer.jsx';
5 |
6 | const App = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/test/testData/Body.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { testVarExp } from './mockDataReq.js';
3 |
4 | const Body = () => {
5 | useEffect(() => {
6 | fetch(`https://swapi.dev/api/people/${id}/`)
7 | .then((response) => response.json())
8 | .then((data) => {
9 | const { starships } = data;
10 | return starships;
11 | });
12 |
13 | const secondResult = testVarExp();
14 | });
15 |
16 | return (
17 |
18 |
Hello world, this is the body
19 |
20 | );
21 | };
22 |
23 | export default Body;
24 |
--------------------------------------------------------------------------------
/test/testData/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { testFuncExp, testArrowExp } from './mockDataReq.js';
3 |
4 | const Footer = () => {
5 | let fetchResult;
6 | useEffect(() => {
7 | fetchResult = testArrowExp();
8 | });
9 |
10 | fetch('/');
11 |
12 | return (
13 |
14 |
I am the footer {fetchResult}
15 |
16 | );
17 | };
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/test/testData/Nav.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | const Nav = () => {
5 | useEffect(() => {
6 | axios.get('/name').then((res) => console.log(res));
7 | });
8 |
9 | return (
10 |
11 |
The navbar
12 |
13 |
14 | );
15 | };
16 |
17 | export default Nav;
18 |
--------------------------------------------------------------------------------
/test/testData/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM, { render } from 'react-dom';
3 | import App from './App';
4 |
5 | render(
6 | ,
7 | document.getElementById('root')
8 | );
--------------------------------------------------------------------------------
/test/testData/mockDataFiberWalker.js:
--------------------------------------------------------------------------------
1 | const fiberTree = {
2 | child: {
3 | child: {
4 | child: {
5 | child: {
6 | child: null,
7 | sibling: {
8 | child: {
9 | child: null,
10 | sibling: {
11 | child: null,
12 | sibling: null,
13 | elementType: { name: 'Recommendations' },
14 | },
15 | elementType: { name: 'Favorites' },
16 | },
17 | sibling: {
18 | child: null,
19 | sibling: null,
20 | elementType: { name: 'Footer' },
21 | },
22 | elementType: { name: 'Body' },
23 | },
24 | elementType: { name: 'NavBar' },
25 | },
26 | sibling: null,
27 | elementType: { name: 'App' },
28 | },
29 | sibling: null,
30 | elementType: { $$typeof: 'Symbol(react.provider)' },
31 | },
32 | sibling: null,
33 | elementType: { name: 'Provider' },
34 | },
35 | };
36 |
37 | const componentStore = {
38 | NavBar: {
39 | 'line: 27, column: 2': { reqType: 'fetch', parentName: null },
40 | 'line: 52, column: 2': { reqType: 'axios', parentName: 'Profile' },
41 | },
42 | Body: {
43 | 'line: 27, column: 2': { reqType: 'fetch', parentName: 'null' },
44 | 'line: 52, column: 2': { reqType: 'axios', parentName: 'testVarExp' },
45 | },
46 | Footer: {
47 | 'line: 27, column: 2': { reqType: 'fetch', parentName: 'testFuncExp' },
48 | },
49 | };
50 |
51 | const fiberwalker = (
52 | node,
53 | componentStore,
54 | treedata = { name: 'Fiber Root', children: [] }
55 | ) => {
56 |
57 | const dataTypeCheck = [node, componentStore, treedata];
58 | if (dataTypeCheck.some(arg => Array.isArray(arg) || !arg || typeof arg !== 'object')) {
59 | throw new TypeError('Arguments passed in must be of an object data type');
60 | };
61 |
62 | const dataReqArr = [
63 | 'fetch',
64 | 'axios',
65 | 'http',
66 | 'https',
67 | 'qwest',
68 | 'superagent',
69 | 'XMLHttpRequest',
70 | ];
71 |
72 | function Node(name) {
73 | this.name = name;
74 | this.children = [];
75 | }
76 |
77 | if (!node) return;
78 |
79 | while (node) {
80 | let name;
81 | if (node.elementType) {
82 | if (typeof node.elementType == 'string') {
83 | name = node.elementType;
84 | } else if (node.elementType.name !== undefined) {
85 | name = node.elementType.name;
86 | } else {
87 | name = 'anon.';
88 | }
89 | } else {
90 | name = 'anon.';
91 | }
92 | const currentNode = { name, children: [] };
93 | if (componentStore !== undefined) {
94 | if (componentStore[name]) {
95 | //iterate through every entry and check request type
96 | const dataRequest = componentStore[name];
97 | for (let key in dataRequest) {
98 | if (dataReqArr.includes(dataRequest[key].reqType)) {
99 | currentNode.attributes = {
100 | containsFetch: `${dataRequest[key].reqType}`,
101 | };
102 | }
103 | }
104 | }
105 | }
106 | treedata.children.push(currentNode);
107 |
108 | if (node.child) {
109 | fiberwalker(
110 | node.child,
111 | componentStore,
112 | treedata.children[treedata.children.length - 1]
113 | );
114 | }
115 |
116 | node = node.sibling;
117 | }
118 | return treedata;
119 | };
120 |
121 | module.exports = { fiberTree, fiberwalker, componentStore };
--------------------------------------------------------------------------------
/test/testData/mockDataParser.js:
--------------------------------------------------------------------------------
1 | const queue = [
2 | {
3 | id: 0,
4 | filename:
5 | '/Users/chrislung/CodesmithPTRI/react-fetch-tree/FetchTreeNPMPkg/testData/index.js',
6 | dependencies: ['./App'],
7 | },
8 | {
9 | id: 1,
10 | filename:
11 | '/Users/chrislung/CodesmithPTRI/react-fetch-tree/FetchTreeNPMPkg/testData/App.jsx',
12 | dependencies: ['./Nav', './Body.jsx', './Footer.jsx'],
13 | },
14 | {
15 | id: 2,
16 | filename:
17 | '/Users/chrislung/CodesmithPTRI/react-fetch-tree/FetchTreeNPMPkg/testData/Nav.jsx',
18 | dependencies: [],
19 | },
20 | {
21 | id: 3,
22 | filename:
23 | '/Users/chrislung/CodesmithPTRI/react-fetch-tree/FetchTreeNPMPkg/testData/Body.jsx',
24 | dependencies: ['./mockDataReq.js'],
25 | },
26 | {
27 | id: 4,
28 | filename:
29 | '/Users/chrislung/CodesmithPTRI/react-fetch-tree/FetchTreeNPMPkg/testData/Footer.jsx',
30 | dependencies: ['./mockDataReq.js'],
31 | },
32 | {
33 | id: 5,
34 | filename:
35 | '/Users/chrislung/CodesmithPTRI/react-fetch-tree/FetchTreeNPMPkg/testData/mockDataReq.js',
36 | dependencies: [],
37 | },
38 | ];
39 |
40 | const invocationStore = {
41 | App: [],
42 | Nav: ['useEffect', undefined, undefined, undefined],
43 | Body: ['useEffect', undefined, undefined, 'fetch', undefined, 'testVarExp'],
44 | secondResult: ['testVarExp'],
45 | Footer: ['useEffect', 'testArrowExp', 'fetch', 'fetchResult'],
46 | fetchResult: [],
47 | testVarExp: [undefined, undefined, 'fetch', undefined],
48 | testFuncExp: [undefined, undefined, 'fetch', undefined],
49 | testArrowExp: [undefined, undefined, 'fetch', undefined],
50 | };
51 |
52 | const nodeStore = {
53 | 'line: 5, column: 0': {
54 | reqType: 'render',
55 | parentName: 'Anonymous',
56 | fileName: 'index',
57 | },
58 | 'line: 5, column: 2': {
59 | reqType: 'useEffect',
60 | parentName: 'Nav',
61 | fileName: 'Nav',
62 | },
63 | 'line: 6, column: 4': {
64 | reqType: 'axios',
65 | parentName: 'Nav',
66 | fileName: 'Nav',
67 | },
68 | 'line: 6, column: 2': {
69 | reqType: 'useEffect',
70 | parentName: 'Body',
71 | fileName: 'Body',
72 | },
73 | 'line: 7, column: 4': {
74 | reqType: 'fetch',
75 | parentName: 'Body',
76 | fileName: 'Body',
77 | },
78 | 'line: 14, column: 25': {
79 | reqType: 'testVarExp',
80 | parentName: 'Body',
81 | fileName: 'Body',
82 | },
83 | 'line: 7, column: 2': {
84 | reqType: 'useEffect',
85 | parentName: 'Footer',
86 | fileName: 'Footer',
87 | },
88 | 'line: 8, column: 18': {
89 | reqType: 'testArrowExp',
90 | parentName: 'Footer',
91 | fileName: 'Footer',
92 | },
93 | 'line: 11, column: 2': {
94 | reqType: 'fetch',
95 | parentName: 'Footer',
96 | fileName: 'Footer',
97 | },
98 | 'line: 1, column: 26': {
99 | reqType: 'fetch',
100 | parentName: 'testVarExp',
101 | fileName: 'mockDataReq',
102 | },
103 | 'line: 10, column: 8': {
104 | reqType: 'fetch',
105 | parentName: 'testFuncExp',
106 | fileName: 'mockDataReq',
107 | },
108 | 'line: 21, column: 9': {
109 | reqType: 'fetch',
110 | parentName: 'testArrowExp',
111 | fileName: 'mockDataReq',
112 | },
113 | };
114 |
115 | const componentStore = { App: {}, null: {}, Nav: {}, Body: {}, Footer: {} };
116 |
117 | module.exports = { queue, invocationStore, componentStore, nodeStore };
118 |
--------------------------------------------------------------------------------
/test/testData/mockDataReq.js:
--------------------------------------------------------------------------------
1 | export const testVarExp = fetch(`https://swapi.dev/api/people/${id}/`)
2 | .then((response) => response.json())
3 | .then((data) => {
4 | const { starships } = data;
5 | return starships;
6 | });
7 |
8 | export async function testFuncExp(id) {
9 |
10 | await fetch(`https://swapi.dev/api/people/${id}/`)
11 | .then((response) => response.json())
12 | .then((data) => {
13 | const { name } = data;
14 | return name;
15 | });
16 | return;
17 | }
18 |
19 | export const testArrowExp = (id) => {
20 |
21 | return fetch(`https://swapi.dev/api/people/${id}/`)
22 | .then((response) => response.json())
23 | .then((data) => {
24 | const { starships } = data;
25 | return starships;
26 | });
27 | }
--------------------------------------------------------------------------------
/test/testData/style.css:
--------------------------------------------------------------------------------
1 | /* dummy CSS File */
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const destination = path.resolve(__dirname, 'build');
7 | const chromeExt = path.resolve(__dirname, 'FetchTreeChromeExt');
8 |
9 | module.exports = {
10 | mode: 'development',
11 | entry: {
12 | app: `${chromeExt}/src/components/index.tsx`,
13 | injectScript: `${chromeExt}/injectScript.js`,
14 | contentScript: `${chromeExt}/contentScript.js`,
15 | },
16 | output: {
17 | path: path.resolve('./build/'),
18 | filename: '[name].js',
19 | publicPath: '.',
20 | },
21 | devtool: 'cheap-module-source-map',
22 | module: {
23 | rules: [
24 | {
25 | test: /\.(svg|png|jpg|gif|jpeg)$/,
26 | use: {
27 | loader: 'file-loader',
28 | options: {
29 | name: '[name].[hash].[ext]',
30 | outputPath: 'imgs',
31 | },
32 | },
33 | },
34 | {
35 | test: /\.jsx?$/,
36 | exclude: /(node_modules)/,
37 | use: {
38 | loader: 'babel-loader',
39 | options: { presets: ['@babel/preset-env', '@babel/preset-react'] },
40 | },
41 | },
42 | {
43 | test: /\.tsx?$/,
44 | use: 'ts-loader',
45 | exclude: /node_modules/,
46 | },
47 | { test: /\.(css)$/, use: ['style-loader', 'css-loader'] },
48 | {
49 | test: /\.s[ac]ss$/i,
50 | use: [
51 | // Creates `style` nodes from JS strings
52 | MiniCssExtractPlugin.loader,
53 | // Translates CSS into CommonJS
54 | 'css-loader',
55 | // Compiles Sass to CSS
56 | 'sass-loader',
57 | ],
58 | },
59 | ],
60 | },
61 | resolve: {
62 | extensions: ['.js', '.jsx', '.scss', '.css', '.ts', '.tsx', '.jpg', '.png'],
63 | },
64 | plugins: [
65 | new MiniCssExtractPlugin(),
66 | new CleanWebpackPlugin(),
67 | new CopyWebpackPlugin({
68 | patterns: [
69 | { from: `${chromeExt}/manifest.json`, to: destination },
70 | { from: `${chromeExt}/src/devtools/devtools.html`, to: destination },
71 | { from: `${chromeExt}/src/devtools/devtools.js`, to: destination },
72 | { from: `${chromeExt}/src/index.html`, to: destination },
73 | { from: `${chromeExt}/background.js`, to: destination },
74 | { from: `${chromeExt}/src/style.css`, to: destination },
75 | { from: `${chromeExt}/src/assets/Logo.png`, to: destination },
76 | ],
77 | }),
78 | ],
79 | };
--------------------------------------------------------------------------------