├── .babelrc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── build ├── backgroundScript.js ├── contentScript.js ├── devtools.html ├── hexagonFAT.png ├── lucid.zip ├── lucidlogo-card-transparent.png ├── manifest.json └── reactTraverser.js ├── devtools.html ├── package-lock.json ├── package.json ├── public ├── ReqResJson.gif ├── StateDiff.gif ├── TreeDisplay.gif ├── TreeFilter.gif ├── assets │ └── lucidlogo-transparent.png ├── demo-gif.gif ├── demo-photo-graphql.png └── demo-photo-react.png ├── src ├── components │ ├── AppNav │ │ ├── AppNav.js │ │ └── app_nav.css │ ├── GraphQLResponse │ │ ├── GraphQLResponse.css │ │ └── GraphQLResponse.js │ ├── GraphQLSchema │ │ ├── GraphQLSchema.css │ │ └── GraphQLSchema.js │ ├── GraphQLTab │ │ ├── GraphQLTab.css │ │ └── GraphQLTab.js │ ├── LogComponent │ │ ├── Log.css │ │ ├── Log.jsx │ │ └── __tests__ │ │ │ ├── __snapshots__ │ │ │ └── log-component.render.js.snap │ │ │ └── log-component.render.js │ ├── LogContainer │ │ ├── LogContainer.css │ │ ├── LogContainer.js │ │ └── __tests__ │ │ │ ├── LogContainer.render.js │ │ │ └── __snapshots__ │ │ │ └── LogContainer.render.js.snap │ ├── ReactTab │ │ ├── ReactTab.css │ │ └── ReactTab.js │ ├── StateContainer │ │ ├── StateContainer.jsx │ │ └── stateContainer.css │ ├── StatePropsBox │ │ ├── StatePropsBox.css │ │ └── StatePropsBox.js │ ├── Tool │ │ └── Tool.js │ └── TreeDiagram │ │ ├── TreeDiagram.css │ │ └── TreeDiagram.jsx ├── devtools.css ├── devtools.js ├── filterComponents.js ├── filterDOM.js └── stateDiff.js ├── stats.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "modules": "commonjs", 9 | "debug": false 10 | } 11 | ], 12 | "@babel/preset-flow", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [ 16 | "@babel/plugin-syntax-dynamic-import", 17 | "@babel/plugin-proposal-class-properties" 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | src/devtools/.DS_Store 63 | .DS_Store 64 | src/devtools/.DS_Store 65 | src/devtools/.DS_Store 66 | 67 | build/webpack-bundle.js 68 | webpack-bundle.js 69 | build/webpack-bundle.js.map 70 | webpack-bundle.js.map -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm test 9 | - npm run build 10 | on: 11 | branch: master -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at gossamer.lucid@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lucid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 |

7 | 8 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Make%20development%20easier%20with%20Lucid.&url=https://github.com/Gossamer-React/Lucid&hashtags=react,graphql,apollographql,javascript,programming,developers,chrome) 9 | ![AppVeyor](https://img.shields.io/badge/build-passing-green.svg) 10 | ![AppVeyor](https://img.shields.io/badge/License-MIT-blue.svg) 11 | 12 | ## **Lucid**: *a React-GraphQL developer tool* 13 | 14 | Lucid is a Chrome Developer Tool designed to help engineers debug their React-GraphQL applications. 15 | - Visualize the component hierarchy, state/props data and state changes of your React application 16 | - See your GraphQL schema, queries, and mutations in real-time 17 | 18 | ## Underlying Technology 19 | ### React Tab 20 | Lucid parses through your React app to generate an interactive tree graph representing your __React component hierarchy__, with node-specific __state and props__ data. The tree updates upon each change to the React app's state, and displays a log of __state diffs__ on the left. This is done by creating a persistent data bridge to the user's React application via the Javascript API for WebExtensions' background and content scripts. Lucid injects scripts utilizing React DevTool's Global Hook to recursively traverse through the React DOM each time setState is called, resulting in a tree and a log that each display real-time feedback. Our app itself uses React internally so as the state of your live app changes, the Lucid tree graph will also provide visual feedback of data flow and state changes through the React components immediately. 21 | 22 | ### GraphQL Tab 23 | Lucid intercepts HTTP requests using Chrome Devtool APIs to display a log of real-time __Apollo client queries and mutations__, along with associated __response__ objects. Lucid also uses GraphQL schema introspection to display __schema__ information from the server. This allows full-stack developers to debug their app from the front-end to the back-end, as requests are generated, responses are returned from the server, and data flows through React components to be rendered in the DOM. 24 | 25 | ## Setup 26 | | Install from Chrome Extension Store | Build your own extension | 27 | | ------------- | ------------- | 28 | | 1. Install React Developer Tools. | 1. Clone the repo and ```npm install``` | 29 | | 2. Install Lucid or __Build your own extension__. | 2. ```npm run build ```| 30 | |3. Restart Chrome Browser. |3. Navigate to chrome://extensions. | 31 | |4. Run your React v16+ application using GraphQL.|4. Click **Load Unpacked** and select your './react-lucid/build' folder.| 32 | |5. Open Chrome Developer Tools (Inspect) and click on the Lucid panel. Trigger a re-render! | 33 | 34 | **IMPORTANT:** Lucid is in *BETA* mode and works best for React v16+ local projects in development environments. 35 | 36 | ## How to Use 37 | ### GraphQL Tab 38 | * In the GraphQL panel, a chronological log of API __requests__ is shown on the left. 39 | * A GraphQL __schema__ of all available types, queries, and mutations is also automatically fetched from the GraphQL server when Lucid is initialized and displayed on the bottom. 40 | * Click each request log to see its associated HTTP __response__. 41 | 42 | **NOTE:** Lucid only listens for HTTP requests while it is open in the Chrome Developer Tool panel. To see any requests that were made upon initial page load, reload your React page after opening Lucid in your Chrome browser. 43 | 44 | ![](public/demo-photo-graphql.png) 45 | 46 | ### React Tab 47 | * In the React panel is a tree graph representing your application's __component hierarchy__. 48 | * Hovering over any React Component in the tree displays the __state and props data__ of that component in the top left. 49 | * The __State Diff__ Log tracks changes in state whenever setState() is triggered. 50 | * Filter out specified higher-order components from your tree graph (e.g. Redux, Apollo-GraphQL, and React Router) by clicking the buttons. 51 | 52 | ![](public/demo-photo-react.png) 53 | 54 | ## Contributing 55 | 56 | Lucid is currently in beta release. Please let us know about bugs and suggestions at gossamer.lucid@gmail.com. Feel free to fork this repo and submit pull requests! 57 | 58 | ## Coming Soon 59 | 60 | 61 | ## Team 62 | 63 | Yong-Nicholas Kim (https://github.com/yongnicholaskim) 64 | 65 | Nian Liu (https://github.com/nianliu18) 66 | 67 | Eterna Tsai (https://github.com/eternalee) 68 | 69 | Neyser Zana (https://github.com/neyser95) 70 | 71 | ## License 72 | MIT 73 | -------------------------------------------------------------------------------- /build/backgroundScript.js: -------------------------------------------------------------------------------- 1 | var _DevtoolPort; 2 | var _ContentscriptPort; 3 | 4 | // * store tab-port connections from multiple open tabs to lucid panel in an array 5 | const connections = {}; 6 | 7 | // * listens to ports being connected 8 | chrome.runtime.onConnect.addListener(port => { 9 | if (port.name === 'devtool-background-port') { 10 | _DevtoolPort = port; 11 | 12 | // * receive message from devtools to trigger reactTraverse 13 | let extensionListener = (message, sender, res) => { 14 | if (message.name === 'connect' && message.tabId) { 15 | chrome.tabs.sendMessage(message.tabId, message); 16 | connections[message.tabId] = port; 17 | return; 18 | } 19 | }; 20 | 21 | port.onMessage.addListener(extensionListener); 22 | } 23 | }); 24 | 25 | // Receives message from content-script and checks for valid connections before posting to devtools 26 | chrome.runtime.onMessage.addListener(function (req, sender, res) { 27 | if (req.type === 'content-script') { 28 | if (sender.tab) { 29 | let tabId = sender.tab.id; 30 | if (tabId in connections) { 31 | //send the request to the specific port in the connections object associated to our tabId 32 | connections[tabId].postMessage({ type: 'appState', msg: req.message }); 33 | } else console.log('ATTENTION:: Tab not found in connection list'); 34 | } else console.log('ATTENTION:: sender.tab not defined'); 35 | return true; 36 | } 37 | }); 38 | 39 | //Remove tabId/port from connection object after tab is closed. 40 | chrome.tabs.onRemoved.addListener(function (tabId) { 41 | delete connections[tabId]; 42 | }); 43 | 44 | //* When react router is invoked a tab change happens and the traverser is lost. This sends a message to the content script so it can check if the traverser needs to be reinjected. 45 | chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { 46 | if (!connections[tabId]) { 47 | return; 48 | } 49 | if (changeInfo.status === 'complete' && _DevtoolPort) { 50 | chrome.tabs.sendMessage(tabId, { type: 'tabChange' }); 51 | } 52 | }); 53 | 54 | //* This will reload extension when it is first installed. 55 | chrome.runtime.onInstalled.addListener(details => { 56 | const currentVersion = chrome.runtime.getManifest().version; 57 | if (details.reason === 'install') { 58 | //* Alert is for debugging purposes, shows install message. 59 | // alert('This is a first install!'); 60 | chrome.storage.local.set({ lastKnownVersion: currentVersion }); 61 | chrome.runtime.reload(); 62 | } 63 | }); 64 | //* This will reload extension when there is a new update. 65 | chrome.runtime.onUpdateAvailable.addListener(details => { 66 | //* Alert is for debugging purposes, shows update message. 67 | // alert('This is an update!'); 68 | const newVersion = details.version; 69 | chrome.storage.local.get('lastKnownVersion', function (result) { 70 | const currentVersion = result.version; 71 | if (newVersion !== currentVersion) { 72 | //* Alert is for debugging purposes, shows update message. 73 | // alert('This is an update!'); 74 | chrome.storage.local.set({ lastKnownVersion: newVersion }); 75 | chrome.runtime.reload(); 76 | } 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /build/contentScript.js: -------------------------------------------------------------------------------- 1 | // * opens new port to connect to our backendscript 2 | let contentScriptPort; 3 | 4 | //To access the DOM & reactDevToolsGlobalHook, inject the script into the document body: 5 | function injectScript(file) { 6 | //this adds to the DOM's body 7 | const body = document.getElementsByTagName('body')[0]; 8 | const scriptFile = document.createElement('script'); 9 | scriptFile.id = 'traverser'; 10 | scriptFile.setAttribute('type', 'text/javascript'); 11 | scriptFile.setAttribute('src', file); 12 | body.appendChild(scriptFile); 13 | } 14 | 15 | chrome.runtime.onMessage.addListener((message, sender, res) => { 16 | //* listen for message.name connect to inject the traverser 17 | if (message.name === 'connect') { 18 | if (!document.getElementById('traverser')) { 19 | injectScript(chrome.extension.getURL('reactTraverser.js')); 20 | } 21 | } 22 | // * receives message about tab update 23 | // TODO: change message.type to message.name 24 | if (message.type && message.type === 'tabChange') { 25 | if (!document.getElementById('traverser')) { 26 | injectScript(chrome.extension.getURL('reactTraverser.js')); 27 | } 28 | } 29 | }); 30 | 31 | //listen for messages from the reactTraverser 32 | window.addEventListener('message', e => { 33 | if (e.data === undefined) return; 34 | if (e.data.type == 'reactTraverser') { 35 | reactDocObj = e.data.data; 36 | 37 | chrome.runtime.sendMessage( 38 | { 39 | type: "content-script", 40 | message: reactDocObj 41 | } 42 | ) 43 | } else { 44 | return; 45 | } 46 | }); 47 | 48 | // * code below is in case we want to purposely invoke the traverser. May or may not be used. 49 | // const newEvent = new Event('run-traverser'); 50 | // window.dispatchEvent(newEvent); -------------------------------------------------------------------------------- /build/devtools.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /build/hexagonFAT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/build/hexagonFAT.png -------------------------------------------------------------------------------- /build/lucid.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/build/lucid.zip -------------------------------------------------------------------------------- /build/lucidlogo-card-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/build/lucidlogo-card-transparent.png -------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "lucid", 4 | "description": "A React-GraphQL developer tool.", 5 | "version": "1.0.3", 6 | "devtools_page": "devtools.html", 7 | "homepage_url": "https://github.com/Gossamer-React/React-Lucid", 8 | "content_scripts": [ 9 | { 10 | "run_at": "document_end", 11 | "matches": [""], 12 | "js": ["contentScript.js"] 13 | } 14 | ], 15 | "background": { 16 | "scripts": ["backgroundScript.js"] 17 | }, 18 | "permissions": ["storage"], 19 | "web_accessible_resources": ["reactTraverser.js"], 20 | "externally_connectable": { 21 | "ids": ["*"] 22 | }, 23 | "icons": { 24 | "128": "hexagonFAT.png" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build/reactTraverser.js: -------------------------------------------------------------------------------- 1 | let timeout; 2 | let reactGlobalHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; 3 | 4 | if (reactGlobalHook) { 5 | const reactInstance = reactGlobalHook.renderers.get(1); 6 | let virtualdom; 7 | var reactDOMArr = []; 8 | 9 | // window.addEventListener('run-traverser', () => { 10 | // console.log('run the traverser!') 11 | // setHook(); 12 | // reactGlobalHook.onCommitFiberRoot(); 13 | // }) 14 | function setHook() { 15 | //React 16+ 16 | if (reactInstance && reactInstance.version) { 17 | reactGlobalHook.onCommitFiberRoot = (function(oCFR) { 18 | return function(...args) { 19 | if (args[1] !== undefined) { 20 | clearTimeout(timeout); 21 | timeout = setTimeout(() => { 22 | virtualdom = args[1]; 23 | let nodeToTraverse = virtualdom.current.stateNode.current; 24 | traverse(nodeToTraverse); 25 | window.postMessage( 26 | JSON.parse( 27 | JSON.stringify({ 28 | type: 'reactTraverser', 29 | data: reactDOMArr 30 | }) 31 | ), 32 | '*' 33 | ); 34 | 35 | reactDOMArr = []; 36 | }, 750); 37 | 38 | return oCFR(...args); 39 | } 40 | }; 41 | })(reactGlobalHook.onCommitFiberRoot); 42 | } else if (reactInstance && reactInstance.Reconciler) { 43 | console.log('React version 16+ (Fiber) is required to use React-Lucid'); 44 | } else { 45 | console.log('React not found- React is required to use React-Lucid'); 46 | } 47 | } 48 | setHook(); 49 | const traverse = (node, childrenarr = reactDOMArr, sib = false) => { 50 | if (node.type) { 51 | if (node.type.name) { 52 | //if desired node, create obj and push into reactDOMArr 53 | obj = { 54 | name: node.type.name, 55 | attributes: { 56 | Id: node._debugID 57 | }, 58 | children: [], 59 | State: stateAndPropParser(node.memoizedState), 60 | Props: stateAndPropParser(node.memoizedProps) 61 | }; 62 | 63 | //Create parent node in reactDOMArr 64 | if (reactDOMArr.length === 0) { 65 | reactDOMArr.push(obj); 66 | childrenarr = reactDOMArr[reactDOMArr.length - 1]['children']; 67 | } else { 68 | childrenarr.push(obj); 69 | if (!sib) { 70 | childrenarr = obj['children']; 71 | } 72 | } 73 | } 74 | } 75 | 76 | if (node.child !== null) { 77 | traverse(node.child, childrenarr, false); 78 | } 79 | if (node.sibling) { 80 | traverse(node.sibling, childrenarr, true); 81 | } 82 | return; 83 | }; 84 | } else { 85 | console.log('React devtool is required to use React-Lucid'); 86 | } 87 | 88 | // * Parsing Functions 89 | 90 | //* This function will try to parser the state and props objects and catch any circular json errors. 91 | /** @param reactObj - this will be the components state or prop object 92 | * @return object/error - this will return a parsered object or a catched error. 93 | */ 94 | const stateAndPropParser = reactObj => { 95 | try { 96 | let result = {}; 97 | if (typeof reactObj === 'object') { 98 | for (let key in reactObj) { 99 | const val = reactObj[key]; 100 | if (typeof val === 'function' || typeof val === 'object') { 101 | result[key] = JSON.parse( 102 | JSON.stringify(val, (key, value) => { 103 | try { 104 | return JSON.parse(JSON.stringify(parserObject(value))); 105 | } catch (error) { 106 | return error; 107 | } 108 | }) 109 | ); 110 | } else { 111 | result[key] = val; 112 | } 113 | } 114 | } else { 115 | result = reactObj; 116 | } 117 | return result; 118 | } catch (e) { 119 | return {}; 120 | } 121 | }; 122 | 123 | // * This function will try to parser an object inside a component state or props objecct and catch any circular json errors. 124 | /** @param propObj - this will be an obj within a component state or prop object 125 | * @return object/error - this will return a parsered object or a catched error. 126 | */ 127 | const parserObject = propObj => { 128 | try { 129 | let result = {}; 130 | if (typeof propObj === 'object') { 131 | for (let key in propObj) { 132 | const val = propObj[key]; 133 | if (typeof val === 'function' || typeof val === 'object') { 134 | result[key] = JSON.parse( 135 | JSON.stringify(val, (key, value) => { 136 | try { 137 | return JSON.parse(JSON.stringify(value)); 138 | } catch (err) { 139 | return 'unable to parser circular reference'; 140 | } 141 | }) 142 | ); 143 | } else { 144 | result[key] = val; 145 | } 146 | } 147 | } else { 148 | result = propObj; 149 | } 150 | return result; 151 | } catch (err) { 152 | return 'unable to parser circular reference'; 153 | } 154 | }; 155 | -------------------------------------------------------------------------------- /devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucid", 3 | "version": "1.0.0", 4 | "description": "A devtool for React and GraphQL developers", 5 | "main": "devtools.js", 6 | "author": "Gossamer", 7 | "license": "ISC", 8 | "scripts": { 9 | "build": "webpack", 10 | "client": "webpack-dev-server --mode development", 11 | "test": "jest" 12 | }, 13 | "jest": { 14 | "transform": { 15 | "^.+\\.(js|jsx)$": "babel-jest", 16 | ".+\\.(css|styl|less|sass|scss)$": "/node_modules/jest-css-modules-transform" 17 | }, 18 | "transformIgnorePatterns": [ 19 | "/node_modules/(?!test-component).+\\.js$" 20 | ] 21 | }, 22 | "dependencies": { 23 | "graphql": "^14.7.0", 24 | "graphql-syntax-highlighter-react": "^0.4.0", 25 | "react": "^16.6.3", 26 | "react-d3-tree": "^1.12.0", 27 | "react-dom": "^16.6.3", 28 | "react-json-view": "^1.19.1" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.15.8", 32 | "@babel/plugin-proposal-class-properties": "^7.14.5", 33 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 34 | "@babel/preset-env": "^7.15.8", 35 | "@babel/preset-flow": "^7.14.5", 36 | "@babel/preset-react": "^7.14.5", 37 | "@babel/register": "^7.15.3", 38 | "autoprefixer": "^9.8.8", 39 | "babel-core": "^7.0.0-bridge.0", 40 | "babel-jest": "^27.2.5", 41 | "babel-loader": "^7.1.5", 42 | "css-loader": "^1.0.1", 43 | "html-webpack-plugin": "^5.3.2", 44 | "jest": "^27.2.5", 45 | "jest-css-modules-transform": "^2.5.0", 46 | "path": "^0.12.7", 47 | "postcss-loader": "^3.0.0", 48 | "react-test-renderer": "^16.14.0", 49 | "style-loader": "^0.23.1", 50 | "terser-webpack-plugin": "^1.2.3", 51 | "webpack": "^5.58.2", 52 | "webpack-cli": "^4.9.0", 53 | "webpack-dev-server": "^4.3.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/ReqResJson.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/ReqResJson.gif -------------------------------------------------------------------------------- /public/StateDiff.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/StateDiff.gif -------------------------------------------------------------------------------- /public/TreeDisplay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/TreeDisplay.gif -------------------------------------------------------------------------------- /public/TreeFilter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/TreeFilter.gif -------------------------------------------------------------------------------- /public/assets/lucidlogo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/assets/lucidlogo-transparent.png -------------------------------------------------------------------------------- /public/demo-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/demo-gif.gif -------------------------------------------------------------------------------- /public/demo-photo-graphql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/demo-photo-graphql.png -------------------------------------------------------------------------------- /public/demo-photo-react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gossamer-React/Lucid/ce6e2d7e57ff5dc19e77d661bf733f7bfa1cfd3d/public/demo-photo-react.png -------------------------------------------------------------------------------- /src/components/AppNav/AppNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './app_nav.css'; 3 | 4 | const appNav = (props) => { 5 | return ( 6 |
7 | 8 | 18 | 28 |
29 | ); 30 | }; 31 | 32 | export default React.memo(appNav); 33 | -------------------------------------------------------------------------------- /src/components/AppNav/app_nav.css: -------------------------------------------------------------------------------- 1 | /* For window */ 2 | #window-nav { 3 | display: flex; 4 | justify-content: space-evenly; 5 | align-items: center; 6 | margin: 5px 0px 20px 0px; 7 | text-align: center; 8 | padding: 0px 15px; 9 | } 10 | 11 | .window-btn{ 12 | width: 25%; 13 | height: 35px; 14 | box-sizing: border-box; 15 | color: #555; 16 | background-color: Transparent; 17 | font-size: 15px; 18 | padding: 5px 0; 19 | } 20 | button:focus{ 21 | outline: none; 22 | } 23 | button:hover{ 24 | background-color: #555; 25 | color: white; 26 | } 27 | .active{ 28 | background-color: #555; 29 | border: none; 30 | color: white; 31 | } 32 | 33 | #logo{ 34 | width: 50px; 35 | height: 50px; 36 | } -------------------------------------------------------------------------------- /src/components/GraphQLResponse/GraphQLResponse.css: -------------------------------------------------------------------------------- 1 | #graphql-res { 2 | box-sizing: border-box; 3 | background-color: white; 4 | box-shadow: 0 15px 30px 0 rgba(0,0,0,0.11), 5 | 0 5px 15px 0 rgba(0,0,0,0.08); 6 | padding: 20px 25px; 7 | margin-bottom: 10px; 8 | } 9 | 10 | .graphql-heading { 11 | margin-bottom: 10px; 12 | } 13 | 14 | .graphql-span { 15 | font-size: 1.3em; 16 | font-weight: 500; 17 | letter-spacing: 0.8px; 18 | line-height: 15px; 19 | } -------------------------------------------------------------------------------- /src/components/GraphQLResponse/GraphQLResponse.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | import styles from './GraphQLResponse.css'; 4 | 5 | const GraphQLResponse = (props) => { 6 | 7 | return ( 8 |
9 |

Response:

10 | 11 | 21 | 22 |
23 | ) 24 | } 25 | 26 | export default GraphQLResponse; 27 | -------------------------------------------------------------------------------- /src/components/GraphQLSchema/GraphQLSchema.css: -------------------------------------------------------------------------------- 1 | #graphql-schema { 2 | box-sizing: border-box; 3 | background-color: white; 4 | box-shadow: 0 15px 30px 0 rgba(0,0,0,0.11), 5 | 0 5px 15px 0 rgba(0,0,0,0.08); 6 | padding: 20px 25px; 7 | overflow-y: scroll; 8 | max-height: 600px; 9 | } 10 | 11 | .graphql-heading { 12 | margin-bottom: 10px; 13 | } -------------------------------------------------------------------------------- /src/components/GraphQLSchema/GraphQLSchema.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { buildClientSchema, printSchema } from 'graphql/utilities'; 3 | import { GraphqlCodeBlock } from 'graphql-syntax-highlighter-react'; 4 | import styles from './GraphQLSchema.css'; 5 | 6 | const GraphQLSchema = ({ schema }) => { 7 | 8 | if (schema !== 'GraphQL schema not available.') { 9 | 10 | let schemaObj = buildClientSchema(JSON.parse(schema)); 11 | let schemaSDL = printSchema(schemaObj); 12 | 13 | return ( 14 |
15 |
16 |

Schema:

17 | 18 | 22 | 23 |
24 |
25 | ) 26 | 27 | } else { 28 | 29 | return ( 30 |
31 |
32 |

33 | No GraphQL data available. 34 |

35 |
36 |
37 | ) 38 | } 39 | } 40 | 41 | export default GraphQLSchema; 42 | -------------------------------------------------------------------------------- /src/components/GraphQLTab/GraphQLTab.css: -------------------------------------------------------------------------------- 1 | #graphQLTab{ 2 | box-sizing: border-box; 3 | display: grid; 4 | grid-template-rows: 3fr 7fr; 5 | grid-template-columns: 100%; 6 | grid-template-areas: 7 | 'log-container' 8 | 'graphql'; 9 | padding: 10px; 10 | } 11 | 12 | #graphql-container { 13 | grid-area: graphql; 14 | margin-right: 0px; 15 | min-height: 600px; 16 | } 17 | /* * To hide GraphQLTab */ 18 | .hide{ 19 | display: none!important; 20 | } 21 | 22 | /* *for responsive use */ 23 | @media screen and (min-width: 760px) { 24 | #graphQLTab{ 25 | grid-template-rows: none; 26 | grid-template-columns: 3fr 7fr; 27 | grid-template-areas: 28 | 'log-container graphql'; 29 | max-height: 1200px; 30 | overflow-y: scroll; 31 | } 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/GraphQLTab/GraphQLTab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LogContainer from './../LogContainer/LogContainer'; 3 | import GraphQLResponse from '../../components/GraphQLResponse/GraphQLResponse'; 4 | import GraphQLSchema from '../../components/GraphQLSchema/GraphQLSchema'; 5 | import styles from './GraphQLTab.css'; 6 | 7 | const tabChange = (prevProps, nextProps) => { 8 | if (prevProps.tab === 'React' && nextProps.tab === 'Graphql') { 9 | return false; 10 | } else if (prevProps.tab === 'React' && nextProps.tab === 'React') { 11 | return true; 12 | } else if (prevProps.tab === 'Graphql' && nextProps.tab === 'React') { 13 | return false; 14 | } 15 | }; 16 | 17 | const GraphQLTab = props => { 18 | return ( 19 |
20 | 25 | 26 | {/* Checks to see it there was a request made. */} 27 | {props.log ? ( 28 |
29 | 30 | 31 |
32 | ) : null} 33 |
34 | ); 35 | }; 36 | 37 | export default React.memo(GraphQLTab, tabChange); 38 | -------------------------------------------------------------------------------- /src/components/LogComponent/Log.css: -------------------------------------------------------------------------------- 1 | .log{ 2 | background-color: rgba(255, 255, 255); 3 | box-sizing: border-box; 4 | box-shadow: 0 15px 30px 0 rgba(0,0,0,0.11), 5 | 0 5px 15px 0 rgba(0,0,0,0.08); 6 | /* transition: box-shadow .1s; */ 7 | cursor: pointer; 8 | margin: 15px; 9 | overflow-x: scroll; 10 | padding: 15px; 11 | } 12 | 13 | .log-p{ 14 | margin: 5px 0px; 15 | } 16 | 17 | .log-span{ 18 | font-size: 1.2em; 19 | font-weight: 500; 20 | letter-spacing: 0.50px; 21 | margin: 5px 0px; 22 | } -------------------------------------------------------------------------------- /src/components/LogComponent/Log.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | import { GraphqlCodeBlock } from 'graphql-syntax-highlighter-react'; 4 | import styles from './Log.css'; 5 | 6 | const Log = props => { 7 | return ( 8 |
{ props.logChange(props.logId) }}> 9 |

10 | Request: {props.logId} 11 |

12 |

13 | Operation Name: {props.operationName === null ? 'Null' : props.operationName} 14 | {/* {props.operationName !== null ? (Operation Name:props.operationName) : null} */} 15 |

16 |

17 | Query: 18 | 22 |

23 |

24 | Variables: 25 | 26 | 34 | 35 |

36 |
37 | ); 38 | }; 39 | 40 | export default Log; 41 | -------------------------------------------------------------------------------- /src/components/LogComponent/__tests__/__snapshots__/log-component.render.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` Log component displays proper information from Apollo server. 1`] = ` 4 |
8 |

11 | 12 | Request: 13 | 14 | 15 | 0 16 |

17 |

20 | 21 | Operation Name: 22 | 23 | 24 | Apollo-query 25 |

26 |

29 | 30 | Query: 31 | 32 |

35 |
 36 |         
 39 |           mutation
 40 |         
 41 |         
 44 |            
 45 |         
 46 |         
 49 |           TodoMutation
 50 |         
 51 |         
 54 |           (
 55 |         
 56 |         
 59 |           $
 60 |         
 61 |         
 64 |           title
 65 |         
 66 |         
 69 |           :
 70 |         
 71 |         
 74 |            
 75 |         
 76 |         
 79 |           string
 80 |         
 81 |         
 84 |           !
 85 |         
 86 |         
 89 |           )
 90 |         
 91 |         
 94 |            
 95 |         
 96 |         
 99 |           {
100 |         
101 |       
102 |
103 |         
106 |             
107 |         
108 |         
111 |           addTodo
112 |         
113 |         
116 |           (
117 |         
118 |         
121 |           title
122 |         
123 |         
126 |           :
127 |         
128 |         
131 |            
132 |         
133 |         
136 |           $
137 |         
138 |         
141 |           title
142 |         
143 |         
146 |           )
147 |         
148 |         
151 |            
152 |         
153 |         
156 |           {
157 |         
158 |       
159 |
160 |         
163 |               
164 |         
165 |         
168 |           id
169 |         
170 |       
171 |
172 |         
175 |               
176 |         
177 |         
180 |           title
181 |         
182 |       
183 |
184 |         
187 |               
188 |         
189 |         
192 |           completed
193 |         
194 |       
195 |
196 |         
199 |               
200 |         
201 |         
204 |           __typename
205 |         
206 |       
207 |
208 |         
211 |             
212 |         
213 |         
216 |           }
217 |         
218 |       
219 |
220 |         
223 |           }
224 |         
225 |       
226 |
227 |

228 |

231 | 232 | Variables: 233 | 234 | 237 |

248 |
251 |
254 |
257 | 258 | 267 |
276 | 279 | 292 | 295 | 296 | 297 |
298 | 299 | 309 | { 310 | 311 | 312 |
322 | 334 | 1 335 | item 336 | 337 | 338 |
339 |
340 |
343 |
351 |
367 | 368 | 384 | 391 | " 392 | 393 | 400 | title 401 | 402 | 409 | " 410 | 411 | 412 | 422 | : 423 | 424 | 425 |
437 |
445 | 454 | " 455 | laundry 456 | " 457 | 458 |
459 |
460 |
461 |
462 |
463 | 466 | 477 | } 478 | 479 | 480 |
481 |
482 |
483 |
484 | 485 |

486 |
487 | `; 488 | -------------------------------------------------------------------------------- /src/components/LogComponent/__tests__/log-component.render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Log from './../Log.jsx'; 4 | 5 | // If query being made is from an Apollo server 6 | // Test to make sure that comopnent does not change unexpectdly. 7 | describe('', () => { 8 | it('Log component displays proper information from Apollo server.', () => { 9 | const log = renderer.create( 10 | 24 | ); 25 | 26 | let logComponent = log.toJSON(); 27 | expect(logComponent).toMatchSnapshot(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/LogContainer/LogContainer.css: -------------------------------------------------------------------------------- 1 | /* *for log in the graphql tab */ 2 | #log-container{ 3 | grid-area: log-container; 4 | height: 500px; 5 | margin-bottom: 30px; 6 | } 7 | 8 | #log-header{ 9 | display: flex; 10 | justify-content: space-between; 11 | margin-bottom: 5px; 12 | } 13 | 14 | #logs{ 15 | overflow-y: scroll; 16 | max-height: 95%; 17 | } 18 | 19 | /* *for responsive use */ 20 | @media screen and (min-width: 760px) { 21 | #log-container{ 22 | width: 100%; 23 | margin: 0 auto 30px auto; 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/LogContainer/LogContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Log from './../../components/LogComponent/Log.jsx'; 3 | import styles from './LogContainer.css'; 4 | 5 | const lengthsAreEqual = (prevProps, nextProps) => { 6 | if (prevProps.logs.length === nextProps.logs.length) return true; 7 | 8 | return false; 9 | }; 10 | 11 | const LogContainer = props => { 12 | let logs = props.logs.map((log, i) => { 13 | let text = JSON.parse(log.req.postData.text); 14 | return ( 15 | 23 | ); 24 | }).reverse(); 25 | 26 | return ( 27 | 28 | {logs.length > 0 ? ( 29 |
30 |
31 |

Request Log

32 | 41 |
42 |
{logs}
43 |
44 | ) :

No requests have been made yet.

} 45 |
46 | ); 47 | }; 48 | 49 | export default React.memo(LogContainer, lengthsAreEqual); 50 | -------------------------------------------------------------------------------- /src/components/LogContainer/__tests__/LogContainer.render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import LogContainer from './../LogContainer'; 4 | import Log from './../../LogComponent/Log.jsx'; 5 | 6 | //* Tests to make sure that comopnent does not change unexpectdly. 7 | describe('', () => { 8 | it('Will render the LogContainer with an emapy array of logs and match the snapshot', ()=> { 9 | const logContainer = renderer.create( 10 | 13 | ).toJSON(); 14 | 15 | expect(logContainer).toMatchSnapshot(); 16 | }); 17 | 18 | it('Will render the LogContainer with one log and match the snapshot', ()=> { 19 | const logContainer = renderer.create( 20 | 33 | ).toJSON(); 34 | 35 | expect(logContainer).toMatchSnapshot(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/LogContainer/__tests__/__snapshots__/LogContainer.render.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` Will render the LogContainer with an emapy array of logs and match the snapshot 1`] = ` 4 |

5 | No requests have been made yet. 6 |

7 | `; 8 | 9 | exports[` Will render the LogContainer with one log and match the snapshot 1`] = ` 10 |
13 |
16 |

17 | Request Log 18 |

19 | 26 |
27 |
30 |
34 |

37 | 38 | Request: 39 | 40 | 41 | 0 42 |

43 |

46 | 47 | Operation Name: 48 | 49 | 50 | Request One 51 |

52 |

55 | 56 | Query: 57 | 58 |

59 | [PARSE ERROR] 60 | This is the query. 61 |
62 |

63 |

66 | 67 | Variables: 68 | 69 | 72 |

83 |
86 |
89 |
92 | 93 | 102 |
111 | 114 | 127 | 130 | 131 | 132 |
133 | 134 | 144 | { 145 | 146 | 147 |
157 | 169 | 1 170 | item 171 | 172 | 173 |
174 |
175 |
178 |
186 |
202 | 203 | 219 | 226 | " 227 | 228 | 235 | title 236 | 237 | 244 | " 245 | 246 | 247 | 257 | : 258 | 259 | 260 |
272 |
280 | 289 | " 290 | laundry 291 | " 292 | 293 |
294 |
295 |
296 |
297 |
298 | 301 | 312 | } 313 | 314 | 315 |
316 |
317 |
318 |
319 | 320 |

321 |
322 |
323 |
324 | `; 325 | -------------------------------------------------------------------------------- /src/components/ReactTab/ReactTab.css: -------------------------------------------------------------------------------- 1 | #reactTab { 2 | padding: 10px; 3 | display: grid; 4 | grid-template-rows: 1fr 3fr; 5 | grid-template-columns: 3fr 7fr; 6 | grid-template-areas: 7 | 'sp-box tree' 8 | 'diff tree'; 9 | } 10 | 11 | #reactLoader { 12 | margin: 30px; 13 | text-align: center 14 | } 15 | 16 | /* for different screen sizes */ 17 | @media screen and (max-width: 600px) { 18 | #reactTab { 19 | display: grid; 20 | grid-template-rows: 4fr 3fr 4fr; 21 | grid-template-columns: 1fr; 22 | grid-template-areas: 23 | 'diff' 24 | 'sp-box' 25 | 'tree'; 26 | max-height: 1200px; 27 | } 28 | } -------------------------------------------------------------------------------- /src/components/ReactTab/ReactTab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './ReactTab.css'; 3 | // * Components 4 | import StatePropsBox from './../../components/StatePropsBox/StatePropsBox'; 5 | import StateContainer from './../../components/StateContainer/StateContainer.jsx'; 6 | import TreeDiagram from './../../components/TreeDiagram/TreeDiagram.jsx'; 7 | 8 | const tabChange = (prevProps, nextProps) => { 9 | if (prevProps.tab === 'Graphql' && nextProps.tab === 'React'){ 10 | return false; 11 | }else if(prevProps.tab === 'Graphql' && nextProps.tab === 'Graphql'){ 12 | return true; 13 | }else if (prevProps.tab === 'React' && nextProps.tab === 'Graphql'){ 14 | return false; 15 | } 16 | }; 17 | 18 | const reactTab = props => { 19 | return ( 20 | //* If this.state.appState has not been populated by reactTraverser.js, show a message asking users to setState(), else render Tree 21 | 22 | {props.appStateLength === 0 ? ( 23 |
24 | devtool logo 25 |

26 | Please trigger a setState() to see the React Component Tree/State 27 | Log. 28 |

29 |

30 | Note: Lucid requires React Devtools to run and works best on local 31 | apps using React v16+ in dev mode 32 |

33 |
34 | ) : ( 35 |
36 | 40 | 45 | 46 |
47 | )} 48 |
49 | ); 50 | }; 51 | 52 | export default React.memo(reactTab, tabChange); 53 | -------------------------------------------------------------------------------- /src/components/StateContainer/StateContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | import styles from './stateContainer.css'; 4 | 5 | const lengthsAreEqual = (prevProps, nextProps) => { 6 | if (prevProps.stateDiffs.length === nextProps.stateDiffs.length) return true; 7 | 8 | return false; 9 | }; 10 | 11 | const StateContainer = ({ stateDiffs, clearLog }) => { 12 | return ( 13 |
14 |
15 |

State Log

16 | 25 |
26 | 27 | {stateDiffs.map((el, i) => { 28 | return ( 29 |
30 | 31 | 32 | 43 | 44 | 45 |
46 | ); 47 | }).reverse()} 48 |
49 | ); 50 | }; 51 | 52 | export default React.memo(StateContainer, lengthsAreEqual); 53 | -------------------------------------------------------------------------------- /src/components/StateContainer/stateContainer.css: -------------------------------------------------------------------------------- 1 | #stateDiff-container { 2 | grid-area: diff; 3 | max-height: 500px; 4 | overflow-y: scroll; 5 | padding: 10px; 6 | margin-bottom: 15px; 7 | } 8 | 9 | #state-header{ 10 | display: flex; 11 | justify-content: space-between; 12 | margin-bottom: 5px; 13 | } 14 | 15 | .stateDiff-div { 16 | overflow-x: scroll; 17 | font-size: .9em; 18 | line-height: .8em; 19 | letter-spacing: 0.50px; 20 | background-color: rgba(255, 255, 255); 21 | box-shadow: 0 15px 30px 0 rgba(0,0,0,0.11), 22 | 0 5px 15px 0 rgba(0,0,0,0.08); 23 | margin-bottom: 15px; 24 | padding: 0px 10px; 25 | } 26 | 27 | .state-span{ 28 | font-family: 'Roboto'; 29 | margin: 6px; 30 | font-size: 1.2em; 31 | font-weight: 500; 32 | letter-spacing: 0.8px; 33 | line-height: 15px; 34 | } -------------------------------------------------------------------------------- /src/components/StatePropsBox/StatePropsBox.css: -------------------------------------------------------------------------------- 1 | .state-prop-display { 2 | grid-area: sp-box; 3 | background-color: #333; 4 | color: white; 5 | max-height: 150px; 6 | overflow-y: scroll; 7 | border: 1px solid black; 8 | padding: 5px 5px; 9 | margin-right: 10px; 10 | } 11 | .pre-tag { 12 | white-space: pre-wrap; 13 | } -------------------------------------------------------------------------------- /src/components/StatePropsBox/StatePropsBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from './StatePropsBox.css'; 3 | 4 | const StatePropsBox = (props) => { 5 | const stateObj = props.nodeData.State; 6 | const propObj = props.nodeData.Props 7 | 8 | return ( 9 |
10 | {(props.nodeData.Props !== null || props.nodeData.State !== null) ? 11 |
12 |

{props.nodeData.name}

13 |
State:{JSON.stringify(stateObj, null, 2)} 
14 |
Props:{JSON.stringify(propObj, undefined, 2)}
15 |
16 | : 17 |

Empty, hover nodes to get data

18 | } 19 |
20 | ) 21 | } 22 | 23 | export default StatePropsBox; -------------------------------------------------------------------------------- /src/components/Tool/Tool.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Tool = props => { 4 | const { nodeData } = props; 5 | 6 | return ( 7 |
props.handleMouseOver(nodeData)} 10 | > 11 |

{nodeData.name}

12 |
13 | ); 14 | }; 15 | 16 | export default Tool; 17 | -------------------------------------------------------------------------------- /src/components/TreeDiagram/TreeDiagram.css: -------------------------------------------------------------------------------- 1 | #treeWrapper{ 2 | grid-area: tree; 3 | height: 700px; 4 | padding: 10px; 5 | margin-left: 10px; 6 | font-family: 'Roboto'; 7 | font-size: .9em; 8 | letter-spacing: 1.5px; 9 | } 10 | #treeButtons{ 11 | text-align: center; 12 | } 13 | .rd3t-tree-container{ 14 | margin-top: 15px; 15 | height: 90%!important; 16 | } -------------------------------------------------------------------------------- /src/components/TreeDiagram/TreeDiagram.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tree from 'react-d3-tree'; 3 | import Tool from './../Tool/Tool'; 4 | import filterComponents from '../../filterComponents'; 5 | import styles from './TreeDiagram.css'; 6 | 7 | class TreeDiagram extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | transition: null, 12 | orientation: 'vertical', 13 | foreignObjectWrapper: { x: 8, y: 4 }, 14 | nodeSize: { x: 95, y: 85 }, 15 | }; 16 | } 17 | 18 | 19 | componentDidMount() { 20 | //from reactD3 library *centering 21 | const dimensions = this.treeContainer.getBoundingClientRect(); 22 | 23 | this.setState({ 24 | translate: { 25 | x: dimensions.width / 2, 26 | y: dimensions.height / 8 27 | }, 28 | }); 29 | } 30 | 31 | handleFlip = () => { 32 | const dimensions = this.treeContainer.getBoundingClientRect(); 33 | if (this.state.orientation === 'vertical') { 34 | this.setState({ 35 | orientation: 'horizontal', 36 | foreignObjectWrapper: { x: 5, y: 8 }, 37 | nodeSize: { x: 110, y: 110 }, 38 | translate: { 39 | x: dimensions.width / 8, 40 | y: dimensions.height / 2 41 | } 42 | }) 43 | } else { 44 | this.setState({ 45 | orientation: 'vertical', 46 | foreignObjectWrapper: { x: 8, y: 4 }, 47 | nodeSize: { x: 85, y: 85 }, 48 | translate: { 49 | x: dimensions.width / 2, 50 | y: dimensions.height / 8 51 | } 52 | }); 53 | } 54 | } 55 | 56 | render() { 57 | const styles = { 58 | nodes: { 59 | node: { 60 | circle: { 61 | fill: "black", 62 | fontSize: "0.1", 63 | strokeWidth: 0.5 64 | } 65 | }, 66 | attributes: { 67 | fill: "white", 68 | fontSize: "10", 69 | strokeWidth: 0.5 70 | }, 71 | leafNode: { 72 | circle: { 73 | fill: "none", 74 | fontSize: "0.1", 75 | strokeWidth: 0.5 76 | }, 77 | attributes: { 78 | fill: "white", 79 | fontSize: "10", 80 | strokeWidth: 0.5 81 | } 82 | } 83 | } 84 | }; 85 | 86 | 87 | return ( 88 |
(this.treeContainer = tc)}> 89 |
90 | 91 | 92 | 93 | 94 |
95 | 96 | , 106 | foreignObjectWrapper: this.state.foreignObjectWrapper 107 | }} 108 | /> 109 |
110 | ); 111 | } 112 | } 113 | 114 | export default TreeDiagram; 115 | -------------------------------------------------------------------------------- /src/devtools.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300'); 2 | *{ 3 | margin: 0px; 4 | } 5 | 6 | body{ 7 | background: linear-gradient(to right, #ffffff, #e2ebf0); 8 | } 9 | 10 | h2{ 11 | font-family: 'Avenir'; 12 | letter-spacing: 1px; 13 | font-weight: 100; 14 | font-size: 1.5em; 15 | padding: 5px; 16 | text-transform: uppercase; 17 | } 18 | 19 | #app-container{ 20 | display: flex; 21 | flex-flow: column; 22 | padding: 10px; 23 | max-height: 95%; 24 | } 25 | 26 | /* *Buttons */ 27 | button{ 28 | font-size: 10px; 29 | text-transform: uppercase; 30 | letter-spacing: 1.15px; 31 | border: solid 1px rgba(214, 214, 214); 32 | border-radius: 4px; 33 | padding: 0px 6px; 34 | height: 20px; 35 | cursor: pointer; 36 | font-family: 'Avenir'; 37 | transition: all .4s ease; 38 | margin: 5px; 39 | } 40 | 41 | .toggleOn { 42 | background-color: rgba(214, 214, 214, 0.8); 43 | color: white; 44 | } 45 | 46 | /* * for window */ 47 | .hide{ 48 | display: none!important; 49 | } -------------------------------------------------------------------------------- /src/devtools.js: -------------------------------------------------------------------------------- 1 | // * Dependencies 2 | import React, { Component } from 'react'; 3 | import { render } from 'react-dom'; 4 | import { getIntrospectionQuery } from 'graphql/utilities'; 5 | import getStateDiffs from './stateDiff'; 6 | import filter from './filterDOM'; 7 | // * Components 8 | import AppNav from './components/AppNav/AppNav'; 9 | import GraphQLTab from './components/GraphQLTab/GraphQLTab'; 10 | import ReactTab from './components/ReactTab/ReactTab'; 11 | // * CSS 12 | import styles from './devtools.css'; 13 | 14 | class App extends Component { 15 | constructor() { 16 | super(); 17 | this.state = { 18 | window: 'Graphql', 19 | logs: [], 20 | appReactDOM: [], 21 | appFilteredDOM: [], 22 | appState: [], 23 | nodeData: [], 24 | schema: 'GraphQL schema not available.', 25 | stateDiff: [], 26 | logView: null, 27 | componentsToFilter: [] 28 | }; 29 | 30 | chrome.devtools.panels.create('Lucid', null, 'devtools.html'); 31 | } 32 | 33 | componentDidMount() { 34 | let chromePort = chrome.runtime.connect({ 35 | name: 'devtool-background-port' 36 | }); 37 | 38 | chromePort.postMessage({ 39 | name: 'connect', 40 | tabId: chrome.devtools.inspectedWindow.tabId 41 | }); 42 | 43 | let timeout; 44 | chromePort.onMessage.addListener(req => { 45 | // * checks if the message it's receiving is about a change in the DOM 46 | if (req.type === 'appState') { 47 | let oldstate = this.state.appReactDOM; 48 | this.setState({ appReactDOM: req.msg }); 49 | 50 | if (oldstate.length > 0) { 51 | let newStateDiffs = getStateDiffs(oldstate, this.state.appReactDOM); 52 | let stateDiff = this.state.stateDiff.slice(); 53 | stateDiff.push(...newStateDiffs); 54 | this.setState({ stateDiff }); 55 | } 56 | 57 | //if there is an active setTimeout, clear it 58 | clearTimeout(timeout); 59 | timeout = setTimeout(() => { 60 | this.setState({ appState: req.msg }); 61 | }, 1000); 62 | } 63 | }); 64 | 65 | //* Leverage Javascript API for WebExtensions to access browser network tab and log req/res objects in state log array 66 | chrome.devtools.network.onRequestFinished.addListener(httpReq => { 67 | if (httpReq.request.postData !== undefined) { 68 | let reqBody = JSON.parse(httpReq.request.postData.text); 69 | 70 | if (reqBody.variables && reqBody.query) { 71 | let log = {}; 72 | log.req = httpReq.request; 73 | 74 | if (httpReq.response.content) { 75 | httpReq.getContent(responseBody => { 76 | if (responseBody === '') { 77 | log.res = 'No response received'; 78 | } else { 79 | const parsedResponseBody = JSON.parse(responseBody); 80 | log.res = parsedResponseBody; 81 | this.setState({ 82 | logs: [...this.state.logs, log], 83 | logView: log 84 | }); 85 | } 86 | }); 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | 93 | //* function to dynamically grab url for backend, and fetch GraphQL schema from Apollo Server and add to state 94 | fetchSchemaFromGraphQLServer() { 95 | if (this.state.logs.length !== 0) { 96 | let url = this.state.logs[this.state.logs.length - 1].req.url; 97 | 98 | fetch(url, { 99 | method: 'POST', 100 | headers: { 'Content-Type': 'application/json' }, 101 | body: JSON.stringify({ query: getIntrospectionQuery() }) 102 | }) 103 | .then(res => res.json()) 104 | .then(json => 105 | this.setState({ 106 | schema: JSON.stringify(json.data) 107 | }) 108 | ); 109 | } 110 | } 111 | 112 | //* invoke schema fetch only after a log object from a previous response is available 113 | componentDidUpdate(prevProps, prevState) { 114 | if (this.state.schema === 'GraphQL schema not available.') { 115 | this.fetchSchemaFromGraphQLServer(); 116 | } 117 | 118 | if (prevState.appState !== this.state.appState) { 119 | if (this.state.componentsToFilter.length) { 120 | let result = []; 121 | filter(this.state.appState, this.state.componentsToFilter, result); 122 | this.setState({ appFilteredDOM: result }); 123 | } 124 | } 125 | } 126 | 127 | // * Handles the tab click for tree and req/res window 128 | handleWindowChange = target => { 129 | if (target.dataset.btn === 'React') { 130 | this.setState({ window: 'React' }); 131 | } else { 132 | this.setState({ window: 'Graphql' }); 133 | } 134 | }; 135 | 136 | // * Handles the filter for the component tree 137 | handleFilter = (e, arr) => { 138 | if (e.target.classList.contains('toggleOn')) { 139 | e.target.classList.remove('toggleOn'); 140 | } else { 141 | e.target.classList.add('toggleOn'); 142 | } 143 | let result = []; 144 | if (!this.state.componentsToFilter.includes(arr[0])) { 145 | let componentsArr = this.state.componentsToFilter.concat(arr); 146 | filter(this.state.appState, componentsArr, result); 147 | this.setState({ 148 | componentsToFilter: componentsArr, 149 | appFilteredDOM: result 150 | }); 151 | } else { 152 | let list = this.state.componentsToFilter; 153 | for (let i = 0; i < list.length; i++) { 154 | if (arr.includes(list[i])) { 155 | list.splice(i--, 1); 156 | } 157 | } 158 | filter(this.state.appState, list, result); 159 | this.setState({ componentsToFilter: list, appFilteredDOM: result }); 160 | } 161 | }; 162 | 163 | //* handle data coming back from mouse hover in tree diagram 164 | handleMouseOver = data => { 165 | this.setState({ 166 | nodeData: data 167 | }); 168 | }; 169 | 170 | // * handles the clearing of both the request log and diff log 171 | handleClearLog = e => { 172 | const data = e.target.dataset.log; 173 | if (data === 'req-log') { 174 | this.setState({ logs: [] }); 175 | } else { 176 | this.setState({ stateDiff: [] }); 177 | } 178 | }; 179 | 180 | // * handles the change of a log 181 | handleLogChange = reqId => { 182 | let req = this.state.logs[reqId]; 183 | req.id = reqId; 184 | this.setState({ logView: req }); 185 | }; 186 | 187 | render() { 188 | return ( 189 |
190 | 194 | 202 | 216 |
217 | ); 218 | } 219 | } 220 | 221 | render(, document.getElementById('root')); -------------------------------------------------------------------------------- /src/filterComponents.js: -------------------------------------------------------------------------------- 1 | let filterComponents = { 2 | //filter react-router 3 | reactRouterComponents: [ 4 | 'BrowserRouter', 5 | 'Router', 6 | 'Switch', 7 | 'Route', 8 | 'Link', 9 | 'StaticRouter', 10 | 'NavLink', 11 | 'Redirect', 12 | 'MemoryRouter', 13 | 'Prompt', 14 | 'NavLink', 15 | ], 16 | 17 | //filter react-redux 18 | reduxComponents: ['Provider', 'Connect'], 19 | 20 | //filter apollo-react 21 | apolloComponents: [ 22 | 'ApolloProvider', 23 | 'ApolloConsumer', 24 | 'Query', 25 | 'Mutation', 26 | 'Subscription', 27 | 'MockedProvider', 28 | 'graphql', 29 | 'compose', 30 | 'withApollo' 31 | ] 32 | } 33 | 34 | export default filterComponents; -------------------------------------------------------------------------------- /src/filterDOM.js: -------------------------------------------------------------------------------- 1 | const filter = (node, componentsArr, childrenArr) => { 2 | if (node.name === undefined) { 3 | filter(node[0], componentsArr, childrenArr) 4 | } else { 5 | if (componentsArr.includes(node.name)) { 6 | if (node.children && node.children.length) { 7 | node.children.forEach(node => { 8 | filter(node, componentsArr, childrenArr) 9 | }); 10 | } 11 | } else { 12 | let copy = JSON.parse(JSON.stringify(node)); 13 | delete copy['children']; 14 | copy['children'] = []; 15 | childrenArr.push(copy); 16 | 17 | if (node.children && node.children.length) { 18 | node.children.forEach(node => { 19 | filter(node, componentsArr, copy.children); 20 | }) 21 | } 22 | } 23 | } 24 | return; 25 | } 26 | export default filter; -------------------------------------------------------------------------------- /src/stateDiff.js: -------------------------------------------------------------------------------- 1 | function recurseDiff() { 2 | const intialDiff = []; 3 | 4 | return function traverseDiff(olds, news, path = '') { 5 | let diff = intialDiff.slice(); 6 | 7 | //if both old and new node are (real) objects/arrays with items to iterate over 8 | if ( 9 | (typeof olds === 'object' && olds !== null) && 10 | (typeof news === 'object' && news !== null) && 11 | (Object.keys(olds).length > 0 || olds.length > 0) && 12 | (Object.keys(news).length > 0 || news.length > 0) 13 | ) { 14 | 15 | for (let key in olds) { 16 | // if two sub-items aren't the same and they are both objects with stuff to iterate over 17 | 18 | if (JSON.stringify(olds[key]) !== JSON.stringify(news[key])) { 19 | if ( 20 | (typeof olds[key] === 'object' && olds[key] !== null) && 21 | (typeof news[key] === 'object' && news[key] !== null) && 22 | (Object.keys(olds).length > 0 || olds.length > 0) && 23 | (Object.keys(news).length > 0 || news.length > 0) 24 | ) { 25 | // // keep track of which component we're in 26 | // let breadcrumb; 27 | // (olds.name === undefined && news.name === undefined) ? 28 | // breadcrumb = '' : 29 | // breadcrumb = olds.name + '>>'; 30 | // recurse on the item 31 | // recurseDiff(olds[key], news[key], path += breadcrumb) 32 | 33 | diff.push(...traverseDiff(olds[key], news[key], olds.name)); 34 | } else { 35 | // push the old vs new items into the diff array 36 | let obj = { 37 | component: path, 38 | oldState: olds, 39 | newState: news 40 | }; 41 | 42 | diff.push(obj); 43 | } 44 | 45 | } 46 | } 47 | } 48 | return diff; 49 | } 50 | } 51 | 52 | const getStateDiffs = recurseDiff(); 53 | 54 | export default getStateDiffs; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | 6 | module.exports = { 7 | mode: 'production', 8 | entry: './src/devtools.js', 9 | output: { 10 | path: path.resolve(__dirname, 'build'), 11 | filename: 'webpack-bundle.js', 12 | publicPath: '' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js|jsx)$/, 18 | exclude: /(node_modules|__tests__)/, 19 | use: [ 20 | { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['@babel/react', ['@babel/env', { modules: false }]], 24 | plugins: ["@babel/plugin-proposal-class-properties"] 25 | }, 26 | } 27 | ] 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | 'style-loader', 33 | 'css-loader' 34 | ] 35 | } 36 | ] 37 | }, 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | filename: 'devtools.html', 41 | template: 'devtools.html', 42 | inject: 'head' 43 | }) 44 | ], 45 | optimization: { 46 | minimizer: [new TerserPlugin()], 47 | nodeEnv: 'production' 48 | } 49 | } 50 | --------------------------------------------------------------------------------