├── .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 |
8 | 9 |
, 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 | 108 | 109 | 115 | 120 | { 132 | if (zoom.isDragging) zoom.dragEnd(); 133 | }} 134 | onDoubleClick={(event) => { 135 | const point = localPoint(event) || { x: 0, y: 0 }; 136 | zoom.scale({ scaleX: 1.1, scaleY: 1.1, point }); 137 | }} 138 | /> 139 | 140 | 142 | d.isExpanded ? null : d.children 143 | )} 144 | size={[sizeWidth, sizeHeight]} 145 | separation={(a, b) => 146 | (a.parent === b.parent ? spread : 1) 147 | } 148 | > 149 | {(tree) => ( 150 | 151 | {tree.links().map((link, i) => ( 152 | 160 | ))} 161 | 162 | {tree.descendants().map((node, key) => { 163 | const width = 60; 164 | const height = 30; 165 | 166 | let top: number; 167 | let left: number; 168 | if (orientation === 'vertical') { 169 | top = node.y; 170 | left = node.x; 171 | } else { 172 | top = node.x; 173 | left = node.y; 174 | } 175 | 176 | return ( 177 | 178 | {node.depth === 0 && ( 179 | { 183 | node.data.isExpanded = !node.data.isExpanded; 184 | forceUpdate(); 185 | }} 186 | /> 187 | )} 188 | {node.depth !== 0 && ( 189 | 190 | 15 ? node.data.name.length * 4.5 : node.data.name.length * 6} 193 | 194 | y={-height / 2} 195 | x={node.data.name.length < 4 ? -15 : node.data.name.length > 15 ? -node.data.name.length * 2.25 : -node.data.name.length * 3} 196 | fill={ 197 | node.data.dataRequest ? '#e8e8e8' : '#272b4d' 198 | } 199 | stroke={ 200 | node.data.children ? '#b998f4' : '#26deb0' 201 | } 202 | strokeWidth={1} 203 | strokeDasharray={ 204 | node.data.children ? '0' : '2,2' 205 | } 206 | strokeOpacity={node.data.children ? 1 : 0.6} 207 | rx={node.data.children ? 0 : 10} 208 | 209 | onClick={() => { 210 | if (node.data.dataRequest) { 211 | setDisplayFetch(true); 212 | setFetchComponent({ name: node.data.name, dataRequest: node.data.dataRequest }) 213 | } else { 214 | setDisplayFetch(false) 215 | } 216 | // node.data.isExpanded = !node.data.isExpanded; 217 | forceUpdate(); 218 | }} 219 | /> 220 | )} 221 | 240 | {node.data.name} 241 | 242 | 243 | ); 244 | })} 245 | 246 | )} 247 | 248 | 249 | 250 | 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 | logo for react fetch tree 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 |
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 | }; --------------------------------------------------------------------------------