├── .DS_Store ├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── jest.config.js ├── media ├── RL(Final).png └── reactLabyrinthFinal.png ├── package.json ├── postcss.config.cjs ├── pull_request_template.md ├── src ├── extension.ts ├── panel.ts ├── parser.ts ├── test │ ├── runTest.ts │ ├── suite │ │ ├── index.ts │ │ └── parser.test.ts │ ├── test_cases │ │ ├── tc_0 │ │ │ ├── component │ │ │ │ └── App.jsx │ │ │ └── index.js │ │ ├── tc_1 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ └── Main.jsx │ │ │ └── index.js │ │ ├── tc_11 │ │ │ ├── components │ │ │ │ ├── App1.jsx │ │ │ │ └── App2.jsx │ │ │ └── index.js │ │ ├── tc_12a │ │ │ ├── components │ │ │ │ └── Navbar.jsx │ │ │ └── pages │ │ │ │ └── index.js │ │ ├── tc_12b │ │ │ └── app │ │ │ │ ├── homepage.jsx │ │ │ │ ├── layout.jsx │ │ │ │ └── page.jsx │ │ ├── tc_13 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ ├── Component1.jsx │ │ │ │ ├── Component2.jsx │ │ │ │ └── Component3.jsx │ │ │ └── index.js │ │ ├── tc_14 │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ ├── Component1.jsx │ │ │ │ ├── Component2.jsx │ │ │ │ ├── Component3.jsx │ │ │ │ ├── Component4.jsx │ │ │ │ ├── Component5.jsx │ │ │ │ ├── Component6.jsx │ │ │ │ └── Component7.jsx │ │ │ └── index.js │ │ ├── tc_2 │ │ │ └── index.js │ │ ├── tc_6 │ │ │ ├── component │ │ │ │ └── App.jsx │ │ │ ├── index.js │ │ │ └── otherComponent │ │ │ │ └── anotherApp.jsx │ │ └── tc_7 │ │ │ ├── components │ │ │ ├── App.jsx │ │ │ └── ChildApp.jsx │ │ │ └── index.js │ ├── vscode-environment.js │ └── vscode.js ├── types │ ├── ImportObj.ts │ ├── builder.ts │ ├── connection.ts │ ├── hierarchyData.ts │ ├── index.d.ts │ └── tree.ts ├── utils │ ├── getNonce.ts │ └── modal.ts └── webview │ ├── App.tsx │ ├── Flow.tsx │ ├── flowBuilder.tsx │ ├── index.tsx │ └── style.css ├── tailwind.config.js ├── tsconfig.json └── webpack.config.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/React-Labyrinth/574acc007ebbf12fb7860eb696bd5ab4e1bb4792/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "off", 20 | "no-unreachable": "warn", 21 | "no-unused-vars": "off", 22 | "constructor-super": "warn", 23 | "valid-typeof": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /package-lock.json 3 | /.vscode-test/ 4 | /build/ 5 | /.DS_Store 6 | /coverage/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | 9 | { 10 | "name": "Run Extension", 11 | "type": "extensionHost", 12 | "request": "launch", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "args": [ 22 | "--extensionDevelopmentPath=${workspaceFolder}", 23 | "--extensionTestsPath=${workspaceFolder}/test/suite/index" 24 | ], 25 | "outFiles": ["${workspaceFolder}/build/**.js"], 26 | "preLaunchTask": "npm: compile" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureOnOpen": false 3 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/* 2 | 3 | !build/ 4 | !CHANGELOG.md 5 | !packages.json 6 | !media/ 7 | !README.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "react-labyrinth" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [1.0.0] 8 | 9 | - Initial release 10 | 11 | ## [1.0.3] 12 | - Updated README.md in VS Code Marketplace Details section. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 React Labyrinth 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 |

React Labyrinth

2 |

3 | react labyrinth logo 4 |

5 | 6 |

7 | A VS Code Extension that generates a hierarchical tree of React components
8 | and identifies the component type with a single file upload. 9 |
10 |

11 | 12 |
13 | 14 | Install React Labyrinth 15 | 16 |

17 |
18 | 19 | ## __Table of Contents__ 20 | 1. [Overview](#overview) 21 | 2. [Installation](#installation) 22 | 3. [Getting Started](#getting-started) 23 | 4. [Tech Stack](#tech-stack) 24 | 5. [Publications](#publications) 25 | 6. [Contributing](#contributing) 26 | 7. [Meet Our Team](#meet-our-team) 27 | 8. [License](#license) 28 | 29 | ## Overview 30 | React Server Components operate exclusively on the server, enabling tasks such as executing database queries within the component itself, rather than relying on backend requests. This paradigm distinguishes them from traditional React components, known as Client Components. However, identifying which Client Components could be optimized as Server Components isn't always straightforward, potentially leading to inefficient bundle sizes and longer time to interactive (TTI) for clients. 31 | 32 | To address this challenge, we aim to develop a visualization tool to help developers determine their application's component types. By enhancing component-type visibility and aiding in the transition to server components, our tool empowers developers to optimize their applications effectively. 33 | 34 | ## Installation 35 | 36 | React Labyrinth extension can be installed via the VS Code Marketplace. Start by clicking the Extensions icon in the Activity Bar on the side of VS Code or by using the View: Extensions command (Ctrl/Cmd+Shift+X). Search for 'react labyrinth' and click the "install" button. Upon completion, VS Code will have installed the extension and React Labyrinth is ready for use. 37 | 38 |

39 | screenshot of react labyrinth in VS Code extension store 40 |

41 | 42 | ## Getting Started 43 | 44 | Once React Labyrinth is installed in your VS Code, you'll notice its logo added to the Activity Bar on the left-hand side. Simply click on the React Labyrinth logo to launch the extension. 45 |

46 | how to activate react labyrinth extension 47 |

48 | 49 | Upon activation, a sidebar will appear, featuring a 'View Tree' button. Clicking this button will prompt the file explorer to open, allowing you to select the root file of your React App to load the tree structure. 50 | 51 | Client Components will be distinguished by an orange background, while Server Components will feature a blue background for easy identification. 52 | 53 |

54 | gif of using react labyrinth extension 55 |

56 | 57 | ## Tech Stack 58 | 59 |

60 | 61 | list of tech stack icons 62 | 63 |

64 | 65 | ## Publications 66 | 67 | Check out our Medium articles: [Part 1](https://medium.com/@franciscoglopez49/react-labyrinth-a-helping-hand-with-react-server-components-84406d2ebf2c) and [Part 2](https://medium.com/@ashleyluu87/data-flow-from-vs-code-extension-webview-panel-react-components-2f94b881467e) for more information about React Labyrinth! 68 | 69 | 70 | ## Contributing 71 | 72 | Contributions are the cornerstone of the open-source community, fostering an environment of learning, inspiration, and innovation. Your contributions are invaluable and greatly appreciated. 73 | 74 | For more details and to begin exploring React Labyrinth, visit its [LinkedIn page](https://www.linkedin.com/company/react-labyrinth). These resources offer comprehensive insights into the project, its functionality, key features, and how to get started. 75 | 76 | Furthermore, you can access the project's source code, documentation, and issue tracker on GitHub. Feel free to fork the project, implement changes, and submit pull requests to enhance its development. 77 | 78 | If you find React Labyrinth beneficial, consider starring it on GitHub to boost its visibility and attract more contributors and users. Your support is crucial in advancing the project's growth and impact. 79 | 80 | [Request Feature / Report Bug](https://github.com/oslabs-beta/React-Labyrinth/issues) 81 | 82 | ## Meet Our Team 83 | 84 | * Ashley Luu — [Github](https://github.com/ash-t-luu) & [LinkedIn](https://www.linkedin.com/in/ashley-t-luu/) 85 | * Christina Raether — [Github](https://github.com/ChristinaRaether) & [LinkedIn](https://www.linkedin.com/in/christinaraether/) 86 | * Francisco Lopez — [Github](https://github.com/Ponch49) & [LinkedIn](https://www.linkedin.com/in/francisco-g-lopez/) 87 | * Johnny Arroyo — [Github](https://github.com/Johnny-Arroyo) & [LinkedIn](https://www.linkedin.com/in/johnny-arroyo/) 88 | * Louis Kuczykowski — [Github](https://github.com/Louka3) & [LinkedIn](https://www.linkedin.com/in/louiskuczykowski/) 89 | 90 | 91 | ## License 92 | 93 | React Labyrinth is developed under the [MIT license](https://en.wikipedia.org/wiki/MIT_License) 94 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - main 5 | tags: 6 | include: 7 | - v* 8 | - refs/tags/v* 9 | 10 | pr: 11 | branches: 12 | include: 13 | - main 14 | 15 | strategy: 16 | matrix: 17 | linux: 18 | imageName: 'ubuntu-latest' 19 | mac: 20 | imageName: 'macos-latest' 21 | 22 | pool: 23 | vmImage: $(imageName) 24 | 25 | steps: 26 | 27 | - task: NodeTool@0 28 | inputs: 29 | versionSpec: '16.x' 30 | displayName: 'Install Node.js' 31 | 32 | - bash: | 33 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 34 | echo ">>> Started xvfb" 35 | displayName: Start xvfb 36 | condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) 37 | 38 | - bash: | 39 | echo ">>> Run npm install and Compile vscode-test" 40 | npm install && npm run compile 41 | echo ">>> Run sample integration test" 42 | npm install && npm run compile && npm run tests 43 | displayName: Run Tests 44 | env: 45 | DISPLAY: ':99.0' 46 | 47 | - bash: | 48 | echo ">>> Publish" 49 | npm run deploy 50 | displayName: Publish 51 | condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/tags/')), or(eq(variables['Agent.OS'], 'Linux'), eq(variables['Agent.OS'], 'MacOS'))) 52 | env: 53 | VSCE_PAT: $(VSCE_PAT) -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | preset: "ts-jest", 5 | testEnvironment: "./src/test/vscode-environment", 6 | preset: "ts-jest/presets/default-esm", 7 | globals: { 8 | "ts-jest": { 9 | useESM: true, 10 | }, 11 | }, 12 | moduleNameMapper: { 13 | vscode: path.join(__dirname, 'src', 'test', 'vscode.js') 14 | }, 15 | testMatch: ['**/test/**/*.js', '**/?(*.)+(spec|test).js'], 16 | modulePathIgnorePatterns: ["node_modules"], 17 | collectCoverage: true, 18 | coverageReporters: [ 'lcov', 'text', 'html'], 19 | collectCoverageFrom: ['./src/**'], 20 | coverageDirectory: 'coverage', 21 | transform: { 22 | "^.+\\.[jt]sx?$": "babel-jest" 23 | }, 24 | }; -------------------------------------------------------------------------------- /media/RL(Final).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/React-Labyrinth/574acc007ebbf12fb7860eb696bd5ab4e1bb4792/media/RL(Final).png -------------------------------------------------------------------------------- /media/reactLabyrinthFinal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/React-Labyrinth/574acc007ebbf12fb7860eb696bd5ab4e1bb4792/media/reactLabyrinthFinal.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-labyrinth", 3 | "displayName": "React Labyrinth", 4 | "description": "React Component Type hierarchy tree visualization tool", 5 | "version": "1.0.2", 6 | "icon": "./media/reactLabyrinthFinal.png", 7 | "publisher": "react-labyrinth", 8 | "preview": false, 9 | "engines": { 10 | "vscode": "^1.85.1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/oslabs-beta/React-Labyrinth/tree/main" 15 | }, 16 | "categories": [ 17 | "Other" 18 | ], 19 | "keywords": [ 20 | "react", 21 | "rsc", 22 | "react components", 23 | "hierarchy tree", 24 | "parent-child", 25 | "visualizer", 26 | "server component", 27 | "client component", 28 | "react server components" 29 | ], 30 | "license": "MIT", 31 | "pricing": "Free", 32 | "activationEvents": [ 33 | "onStartupFinished" 34 | ], 35 | "main": "./build/extension.js", 36 | "contributes": { 37 | "commands": [ 38 | { 39 | "command": "myExtension.showPanel", 40 | "title": "Show Panel", 41 | "category": "React Labyrinth" 42 | }, 43 | { 44 | "command": "myExtension.pickFile", 45 | "title": "Pick File", 46 | "category": "React Labyrinth" 47 | } 48 | ], 49 | "viewsContainers": { 50 | "activitybar": [ 51 | { 52 | "id": "react-labyrinth", 53 | "title": "React Labyrinth", 54 | "icon": "/media/RL(Final).png" 55 | } 56 | ] 57 | }, 58 | "views": { 59 | "react-labyrinth": [ 60 | { 61 | "id": "tree-render", 62 | "name": "Tree" 63 | } 64 | ] 65 | }, 66 | "viewsWelcome": [ 67 | { 68 | "view": "tree-render", 69 | "contents": "View tree to see component types!\n[View Tree](command:myExtension.pickFile)\n" 70 | } 71 | ] 72 | }, 73 | "scripts": { 74 | "vscode:prepublish": "npm run prod", 75 | "lint": "eslint .", 76 | "pretest": "npm run lint", 77 | "test": "npx tsc ; node ./build/src/test/runTest.js", 78 | "tests": "node ./build/src/test/runTest.js", 79 | "dev": "webpack --mode development --config webpack.config.ts --watch", 80 | "prod": "webpack --mode production --config webpack.config.ts", 81 | "compile": "tsc -p ./", 82 | "coverage": "jest --coverage", 83 | "deploy": "vsce publish", 84 | "package": "vsce package" 85 | }, 86 | "devDependencies": { 87 | "@babel/preset-typescript": "^7.23.3", 88 | "@types/glob": "^8.1.0", 89 | "@types/jest": "^29.5.11", 90 | "@types/node": "18.x", 91 | "@types/react": "^18.2.45", 92 | "@types/react-dom": "^18.2.18", 93 | "@types/vscode": "^1.84.0", 94 | "@vscode/test-cli": "^0.0.4", 95 | "@vscode/test-electron": "^2.3.8", 96 | "@vscode/vsce": "^2.23.0", 97 | "eslint": "^8.54.0", 98 | "glob": "^10.3.10", 99 | "jest": "^29.7.0", 100 | "jest-environment-jsdom": "^29.7.0", 101 | "jest-environment-node": "^29.7.0", 102 | "postcss-loader": "^7.3.3", 103 | "postcss-preset-env": "^9.3.0", 104 | "tailwindcss": "^3.3.6", 105 | "ts-jest": "^29.1.2", 106 | "typescript": "^5.3.3", 107 | "webpack-cli": "^5.1.4" 108 | }, 109 | "dependencies": { 110 | "@babel/core": "^7.23.3", 111 | "@babel/parser": "^7.23.9", 112 | "@babel/preset-env": "^7.23.3", 113 | "@babel/preset-react": "^7.23.3", 114 | "babel": "^6.23.0", 115 | "babel-loader": "^9.1.3", 116 | "css-loader": "^6.8.1", 117 | "d3": "^7.8.5", 118 | "react": "^18.2.0", 119 | "react-dom": "^18.2.0", 120 | "reactflow": "^11.10.1", 121 | "sass-loader": "^13.3.2", 122 | "style-loader": "^3.3.3", 123 | "ts-loader": "^9.5.1", 124 | "webpack": "^5.89.0" 125 | }, 126 | "babel": { 127 | "presets": [ 128 | [ 129 | "@babel/preset-react", 130 | { 131 | "runtime": "automatic" 132 | } 133 | ], 134 | [ 135 | "@babel/preset-env", 136 | { 137 | "targets": { 138 | "node": "current" 139 | } 140 | } 141 | ], 142 | "@babel/preset-typescript" 143 | ] 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | 'postcss-preset-env', 6 | tailwindcss 7 | ], 8 | }; -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | **Issue Type** 4 | 5 | - [x] Bug 6 | - [ ] Feature 7 | - [ ] Tech Debt 8 | 9 | **Description** 10 | A clear and concise description of the issue. 11 | 12 | **Ticket Item** 13 | 14 | **Figma link - optional** 15 | **Spec link - optional** 16 | 17 | **Steps to Reproduce Bug / Validate Feature / Confirm Tech Debt Fix** 18 | 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Previous behavior** 25 | A clear and concise description of what was originally happening. 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots & Videos** 31 | If applicable, add screenshots and videos to help explain your issue type. 32 | 33 | **Additional context** 34 | Add any other context about the problem he 35 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import {createPanel} from './panel'; 3 | import { Parser } from './parser'; 4 | import { Tree } from './types/tree'; 5 | import { showNotification } from './utils/modal'; 6 | 7 | let tree: Parser | undefined = undefined; 8 | let panel: vscode.WebviewPanel | undefined = undefined; 9 | 10 | // This method is called when your extension is activated 11 | // Your extension is activated the very first time the command is executed 12 | function activate(context: vscode.ExtensionContext) { 13 | 14 | // This is the column where Webview will be revealed to 15 | let columnToShowIn : vscode.ViewColumn | undefined = vscode.window.activeTextEditor 16 | ? vscode.window.activeTextEditor.viewColumn 17 | : undefined; 18 | 19 | // Command that allows for User to select the root file of their React application. 20 | const pickFile: vscode.Disposable = vscode.commands.registerCommand('myExtension.pickFile', async () => { 21 | // Check if there is an existing webview panel, if so display it. 22 | if (panel) { 23 | panel.reveal(columnToShowIn); 24 | } 25 | 26 | // Opens window for the User to select the root file of React application 27 | const fileArray: vscode.Uri[] = await vscode.window.showOpenDialog({ canSelectFolders: false, canSelectFiles: true, canSelectMany: false }); 28 | 29 | // Throw error message if no file was selected 30 | if (!fileArray || fileArray.length === 0) { 31 | showNotification({message: 'No file selected'}); 32 | return; 33 | } 34 | 35 | // Create Tree to be inserted into returned HTML 36 | tree = new Parser(fileArray[0].path); 37 | tree.parse(); 38 | const data: Tree = tree.getTree(); 39 | 40 | // Check if panel currently has a webview, if it does dispose of it and create another with updated root file selected. 41 | // Otherwise create a new webview to display root file selected. 42 | if (!panel) { 43 | panel = createPanel(context, data, columnToShowIn); 44 | } else { 45 | panel.dispose() 46 | panel = createPanel(context, data, columnToShowIn); 47 | } 48 | 49 | // Listens for when webview is closed and disposes of webview resources 50 | panel.onDidDispose( 51 | () => { 52 | panel.dispose(); 53 | panel = undefined; 54 | columnToShowIn = undefined; 55 | }, 56 | null, 57 | context.subscriptions 58 | ); 59 | }); 60 | 61 | // Command to show panel if it is hidden 62 | const showPanel: vscode.Disposable = vscode.commands.registerCommand('myExtension.showPanel', () => { 63 | if (!panel) { 64 | 65 | showNotification({message: 'Please select root file of app', timeout: 3000}); 66 | return; 67 | 68 | } else { 69 | 70 | panel.reveal(columnToShowIn); 71 | return; 72 | 73 | } 74 | 75 | }); 76 | 77 | context.subscriptions.push(pickFile, showPanel); 78 | } 79 | 80 | // This method is called when your extension is deactivated 81 | function deactivate() {} 82 | 83 | module.exports = { 84 | activate, 85 | deactivate 86 | } 87 | -------------------------------------------------------------------------------- /src/panel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { getNonce } from './utils/getNonce'; 3 | import { Tree } from './types/tree'; 4 | 5 | let panel: vscode.WebviewPanel | undefined = undefined; 6 | 7 | export function createPanel(context: vscode.ExtensionContext, data: Tree, columnToShowIn: vscode.ViewColumn) { 8 | // Utilize method on vscode.window object to create webview 9 | panel = vscode.window.createWebviewPanel( 10 | 'reactLabyrinth', 11 | 'React Labyrinth', 12 | // Create one tab 13 | vscode.ViewColumn.One, 14 | { 15 | enableScripts: true, 16 | retainContextWhenHidden: true 17 | } 18 | ); 19 | 20 | // Set the icon logo of extension webview 21 | panel.iconPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'RL(Final).png'); 22 | 23 | // Set URI to be the path to bundle 24 | const bundlePath: vscode.Uri = vscode.Uri.joinPath(context.extensionUri, 'build', 'bundle.js'); 25 | 26 | // Set webview URI to pass into html script 27 | const bundleURI: vscode.Uri = panel.webview.asWebviewUri(bundlePath); 28 | 29 | // Render html of webview here 30 | panel.webview.html = createWebviewHTML(bundleURI, data); 31 | 32 | // Sends data to Flow.tsx to be displayed after parsed data is received 33 | panel.webview.onDidReceiveMessage( 34 | async (msg: any) => { 35 | switch (msg.type) { 36 | case 'onData': 37 | if (!msg.value) break; 38 | context.workspaceState.update('reactLabyrinth', msg.value); 39 | panel.webview.postMessage( 40 | { 41 | type: 'parsed-data', 42 | value: msg.value, // tree object 43 | settings: vscode.workspace.getConfiguration('reactLabyrinth') 44 | }); 45 | break; 46 | } 47 | }, 48 | undefined, 49 | context.subscriptions 50 | ); 51 | 52 | return panel 53 | }; 54 | 55 | // getNonce generates a new random string to prevent external injection of foreign code into the HTML 56 | const nonce: string = getNonce(); 57 | 58 | // Creates the HTML page for webview 59 | function createWebviewHTML(URI: vscode.Uri, initialData: Tree) : string { 60 | return ( 61 | ` 62 | 63 | 64 | 65 | 66 | 67 | React Labyrinth 68 | 69 | 70 |
71 | 80 | 81 | 82 | 83 | ` 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as babel from '@babel/parser'; 4 | import { getNonce } from './utils/getNonce'; 5 | import { ImportObj } from './types/ImportObj'; 6 | import { Tree } from "./types/tree"; 7 | import { File } from '@babel/types'; 8 | 9 | export class Parser { 10 | entryFile: string; 11 | tree: Tree | undefined; 12 | 13 | constructor(filePath: string) { 14 | // Fix when selecting files in wsl file system 15 | this.entryFile = filePath; 16 | if (process.platform === 'linux' && this.entryFile.includes('wsl$')) { 17 | this.entryFile = path.resolve( 18 | filePath.split(path.win32.sep).join(path.posix.sep) 19 | ); 20 | this.entryFile = '/' + this.entryFile.split('/').slice(3).join('/'); 21 | // Fix for when running wsl but selecting files held on windows file system 22 | } else if ( 23 | process.platform === 'linux' && 24 | /[a-zA-Z]/.test(this.entryFile[0]) 25 | ) { 26 | const root = `/mnt/${this.entryFile[0].toLowerCase()}`; 27 | this.entryFile = path.join( 28 | root, 29 | filePath.split(path.win32.sep).slice(1).join(path.posix.sep) 30 | ); 31 | } 32 | this.tree = undefined; 33 | // Break down and reasemble given filePath safely for any OS using path? 34 | } 35 | 36 | // method to generate component tree based on current entryFile 37 | public parse(): Tree { 38 | // Create root Tree node 39 | const root = { 40 | id: getNonce(), 41 | name: path.basename(this.entryFile).replace(/\.(t|j)sx?$/, ''), 42 | fileName: path.basename(this.entryFile), 43 | filePath: this.entryFile, 44 | importPath: '/', // this.entryFile here breaks windows file path on root e.g. C:\\ is detected as third party 45 | expanded: false, 46 | depth: 0, 47 | count: 1, 48 | thirdParty: false, 49 | reactRouter: false, 50 | reduxConnect: false, 51 | children: [], 52 | parent: '', 53 | parentList: [], 54 | props: {}, 55 | error: '', 56 | isClientComponent: false, 57 | }; 58 | this.tree = root; 59 | this.parser(root); 60 | // clean up nodes with error: 'File not found' 61 | this.removeTreesWithError(this.tree); 62 | return this.tree; 63 | } 64 | 65 | private removeTreesWithError(tree: Tree): void { 66 | // base case 67 | if(tree.children.length === 0) return; 68 | // iterate over tree.children array to check for error. 69 | for(let i = 0; i < tree.children.length; i++){ 70 | // call removeTreesWithError on every tree in the children array 71 | if(tree.children[i].children.length !== 0){ 72 | this.removeTreesWithError(tree.children[i]); 73 | } 74 | if(tree.children[i].error && (tree.children[i].error === 'File not found' || tree.children[i].error === 'Error while processing this file/node')){ 75 | // when an error is found, splice the tree out of the children array 76 | tree.children.splice(i,1); 77 | i--; // decrement to account for change in children array length 78 | } 79 | } 80 | }; 81 | 82 | public getTree(): Tree { 83 | return this.tree!; 84 | } 85 | 86 | // Set entryFile property with the result of Parser (from workspace state) 87 | public setTree(tree: Tree) { 88 | this.entryFile = tree.filePath; 89 | this.tree = tree; 90 | } 91 | 92 | public updateTree(filePath: string): Tree { 93 | let children: any[] = []; 94 | 95 | const getChildNodes = (node: Tree): void => { 96 | const { depth, filePath, expanded } = node; 97 | children.push({ depth, filePath, expanded }); 98 | }; 99 | 100 | const matchExpand = (node: Tree): void => { 101 | for (let i = 0; i < children.length; i += 1) { 102 | const oldNode = children[i]; 103 | if ( 104 | oldNode.depth === node.depth && 105 | oldNode.filePath === node.filePath && 106 | oldNode.expanded 107 | ) { 108 | node.expanded = true; 109 | } 110 | } 111 | }; 112 | 113 | const callback = (node: Tree): void => { 114 | if (node.filePath === filePath) { 115 | node.children.forEach((child) => { 116 | this.traverseTree(getChildNodes, child); 117 | }); 118 | 119 | const newNode = this.parser(node); 120 | 121 | this.traverseTree(matchExpand, newNode); 122 | 123 | children = []; 124 | } 125 | }; 126 | 127 | this.traverseTree(callback, this.tree); 128 | return this.tree!; 129 | } 130 | 131 | // Traverses the tree and changes expanded property of node whose ID matches provided ID 132 | public toggleNode(id: string, expanded: boolean): Tree{ 133 | const callback = (node: { id: string; expanded: boolean }) => { 134 | if (node.id === id) { 135 | node.expanded = expanded; 136 | } 137 | }; 138 | 139 | this.traverseTree(callback, this.tree); 140 | return this.tree!; 141 | } 142 | 143 | // Traverses all nodes of current component tree and applies callback to each node 144 | private traverseTree(callback: Function, node: Tree | undefined = this.tree): void { 145 | if (!node) { 146 | return; 147 | } 148 | 149 | callback(node); 150 | 151 | node.children.forEach((childNode) => { 152 | this.traverseTree(callback, childNode); 153 | }); 154 | } 155 | 156 | // Recursively builds the React component tree structure starting from root node 157 | private parser(componentTree: Tree): Tree | undefined { 158 | // If import is a node module, do not parse any deeper 159 | if (!['\\', '/', '.'].includes(componentTree.importPath[0])) { 160 | componentTree.thirdParty = true; 161 | if ( 162 | componentTree.fileName === 'react-router-dom' || 163 | componentTree.fileName === 'react-router' 164 | ) { 165 | componentTree.reactRouter = true; 166 | } 167 | return; 168 | } 169 | 170 | // Check that file has valid fileName/Path, if not found, add error to node and halt 171 | const fileName = this.getFileName(componentTree); 172 | if (!fileName) { 173 | componentTree.error = 'File not found'; 174 | return; 175 | } 176 | 177 | // If current node recursively calls itself, do not parse any deeper: 178 | if (componentTree.parentList.includes(componentTree.filePath)) { 179 | return; 180 | } 181 | 182 | // Create abstract syntax tree of current component tree file 183 | let ast: babel.ParseResult; 184 | try { 185 | ast = babel.parse( 186 | fs.readFileSync(path.resolve(componentTree.filePath), 'utf-8'), 187 | { 188 | sourceType: 'module', 189 | tokens: true, 190 | plugins: ['jsx', 'typescript'], 191 | } 192 | ); 193 | } catch (err) { 194 | componentTree.error = 'Error while processing this file/node'; 195 | return componentTree; 196 | } 197 | 198 | // Find imports in the current file, then find child components in the current file 199 | const imports = this.getImports(ast.program.body); 200 | 201 | // Set value of isClientComponent property 202 | if (this.getComponentType(ast.program.directives, ast.program.body)) { 203 | componentTree.isClientComponent = true; 204 | } else { 205 | componentTree.isClientComponent = false; 206 | } 207 | 208 | // Get any JSX Children of current file: 209 | if (ast.tokens) { 210 | componentTree.children = this.getJSXChildren( 211 | ast.tokens, 212 | imports, 213 | componentTree, 214 | ); 215 | } 216 | 217 | // Check if current node is connected to the Redux store 218 | if (ast.tokens) { 219 | componentTree.reduxConnect = this.checkForRedux(ast.tokens, imports); 220 | } 221 | 222 | // Recursively parse all child components 223 | componentTree.children.forEach((child) => this.parser(child)); 224 | return componentTree; 225 | } 226 | 227 | // Finds files where import string does not include a file extension 228 | private getFileName(componentTree: Tree): string | undefined { 229 | const ext = path.extname(componentTree.filePath); 230 | let fileName: string | undefined = componentTree.fileName; 231 | 232 | if (!ext) { 233 | // Try and find file extension that exists in directory: 234 | const fileArray = fs.readdirSync(path.dirname(componentTree.filePath)); 235 | const regEx = new RegExp(`${componentTree.fileName}.(j|t)sx?$`); 236 | fileName = fileArray.find((fileStr) => fileStr.match(regEx)); 237 | fileName ? (componentTree.filePath += path.extname(fileName)) : null; 238 | } 239 | return fileName; 240 | } 241 | 242 | // Extracts Imports from current file 243 | // const App1 = lazy(() => import('./App1')); => is parsed as 'ImportDeclaration' 244 | // import App2 from './App2'; => is parsed as 'VariableDeclaration' 245 | private getImports(body: { [key: string]: any }[]): ImportObj { 246 | const bodyImports = body.filter((item) => item.type === 'ImportDeclaration' || 'VariableDeclaration'); 247 | 248 | return bodyImports.reduce((accum, curr) => { 249 | if (curr.type === 'ImportDeclaration') { 250 | curr.specifiers.forEach(({ local, imported }) => { 251 | accum[local.name] = { 252 | importPath: curr.source.value, 253 | importName: imported ? imported.name : local.name, 254 | }; 255 | }); 256 | } 257 | if (curr.type === 'VariableDeclaration' && curr.declarations) { 258 | const importPath = this.findVarDecImports(curr.declarations[0]); 259 | if (importPath) { 260 | const importName = curr.declarations[0].id.name; 261 | accum[importName] = { 262 | importPath, 263 | importName 264 | }; 265 | } 266 | } 267 | return accum; 268 | }, {}); 269 | } 270 | 271 | private findVarDecImports(ast: { [key: string]: any }): string | boolean { 272 | // Find import path in variable declaration and return it, 273 | if (ast.hasOwnProperty('callee') && ast.callee.type === 'Import') { 274 | return ast.arguments[0].value; 275 | } 276 | // Otherwise look for imports in any other non null/undefined objects in the tree: 277 | for (let key in ast) { 278 | if (ast.hasOwnProperty(key) && typeof ast[key] === 'object' && ast[key]) { 279 | const importPath = this.findVarDecImports(ast[key]); 280 | if (importPath) { 281 | return importPath; 282 | } 283 | } 284 | } 285 | return false; 286 | } 287 | 288 | // Determines server or client component type (looks for use of 'use client' and react/redux state hooks) 289 | private getComponentType(directive: { [key: string]: any }[], body: { [key: string]: any }[]) { 290 | const defaultErr = (err) => { 291 | return { 292 | method: 'Error in getCallee method of Parser:', 293 | log: err, 294 | } 295 | }; 296 | 297 | // Initial check for use of directives (ex: 'use client', 'use server', 'use strict') 298 | // Accounts for more than one directive 299 | for (let i = 0; i < directive.length; i++) { 300 | if (directive[i].type === 'Directive') { 301 | if (typeof directive[i].value.value === 'string' && directive[i].value.value.trim() === 'use client') { 302 | return true; 303 | } 304 | } 305 | break; 306 | } 307 | 308 | // Second check for use of React/Redux hooks 309 | // Checks for components declared using 'const' 310 | const bodyCallee = body.filter((item) => item.type === 'VariableDeclaration'); 311 | 312 | // Checks for components declared using 'export default function' 313 | const exportCallee = body.filter((item) => item.type === 'ExportDefaultDeclaration'); 314 | 315 | // Checks for components declared using 'function' 316 | const functionCallee = body.filter((item) => item.type === 'FunctionDeclaration'); 317 | 318 | // Helper function 319 | const calleeHelper = (item) => { 320 | const hooksObj = { 321 | useState: 0, 322 | useContext: 0, 323 | useRef: 0, 324 | useImperativeHandle: 0, 325 | useNavigate: 0, 326 | useLocation: 0, 327 | useLayoutEffect: 0, 328 | useInsertionEffect: 0, 329 | useMemo: 0, 330 | useCallback: 0, 331 | useTransition: 0, 332 | useDeferredValue: 0, 333 | useEffect: 0, 334 | useReducer: 0, 335 | useDispatch: 0, 336 | useActions: 0, 337 | useSelector: 0, 338 | useShallowEqualSelector: 0, 339 | useStore: 0, 340 | bindActionCreators: 0, 341 | } 342 | if (item.type === 'VariableDeclaration') { 343 | try { 344 | let calleeName = item.declarations[0]?.init?.callee?.name; 345 | if (hooksObj.hasOwnProperty(calleeName) || (typeof calleeName === 'string' && calleeName.startsWith('use'))) { 346 | return true; 347 | } 348 | } 349 | catch (err) { 350 | const error = defaultErr(err); 351 | console.error(error.method, '\n', error.log); 352 | } 353 | } 354 | else if (item.type === 'ExpressionStatement') { 355 | try { 356 | const calleeName = item.expression?.callee?.name; 357 | if (calleeName === undefined) return false; 358 | if (hooksObj.hasOwnProperty(calleeName) || (typeof calleeName === 'string' && calleeName.startsWith('use'))) { 359 | return true; 360 | } 361 | } 362 | catch (err) { 363 | const error = defaultErr(err); 364 | console.error(error.method, '\n', error.log); 365 | } 366 | } 367 | return false; 368 | } 369 | 370 | // Process Function Declarations 371 | for (const func of functionCallee) { 372 | const calleeArr = func.body?.body; 373 | if (!calleeArr) continue; // Skip if no body 374 | 375 | for (const callee of calleeArr) { 376 | if (calleeHelper(callee)) { 377 | return true; 378 | } 379 | } 380 | } 381 | 382 | // Process Export Declarations 383 | for (const exportDecl of exportCallee) { 384 | const calleeArr = exportDecl.declaration.body?.body; 385 | if (!calleeArr) continue; // Skip if no body 386 | 387 | for (const callee of calleeArr) { 388 | if (calleeHelper(callee)) { 389 | return true; 390 | } 391 | } 392 | } 393 | 394 | // Process Body Declarations 395 | for (const bodyDecl of bodyCallee) { 396 | const calleeArr = bodyDecl.declarations[0]?.init?.body?.body; 397 | if (!calleeArr) continue; // Skip if no body 398 | 399 | for (const callee of calleeArr) { 400 | if (calleeHelper(callee)) { 401 | return true; 402 | } 403 | } 404 | } 405 | 406 | return false; 407 | } 408 | 409 | // Finds JSX React Components in current file 410 | private getJSXChildren( 411 | astTokens: any[], 412 | importsObj: ImportObj, 413 | parentNode: Tree 414 | ): Tree[] { 415 | 416 | let childNodes: { [key: string]: Tree } = {}; 417 | let props: { [key: string]: boolean } = {}; 418 | let token: { [key: string]: any }; 419 | 420 | for (let i = 0; i < astTokens.length; i++) { 421 | // Case for finding JSX tags eg 422 | if ( 423 | astTokens[i].type.label === 'jsxTagStart' && 424 | astTokens[i + 1].type.label === 'jsxName' && 425 | importsObj[astTokens[i + 1].value] 426 | ) { 427 | token = astTokens[i + 1]; 428 | props = this.getJSXProps(astTokens, i + 2); 429 | childNodes = this.getChildNodes( 430 | importsObj, 431 | token, 432 | props, 433 | parentNode, 434 | childNodes, 435 | ); 436 | 437 | // Case for finding components passed in as props e.g. 438 | } else if ( 439 | astTokens[i].type.label === 'jsxName' && 440 | (astTokens[i].value === 'Component' || 441 | astTokens[i].value === 'children') && 442 | importsObj[astTokens[i + 3].value] 443 | ) { 444 | token = astTokens[i + 3]; 445 | childNodes = this.getChildNodes( 446 | importsObj, 447 | token, 448 | props, 449 | parentNode, 450 | childNodes, 451 | ); 452 | } 453 | } 454 | return Object.values(childNodes); 455 | } 456 | 457 | private getChildNodes( 458 | imports: ImportObj, 459 | astToken: { [key: string]: any }, 460 | props: { [key: string]: boolean }, 461 | parent: Tree, 462 | children: { [key: string]: Tree } 463 | ): { [key: string]: Tree } { 464 | if (children[astToken.value]) { 465 | children[astToken.value].count += 1; 466 | children[astToken.value].props = { 467 | ...children[astToken.value].props, 468 | ...props, 469 | }; 470 | } else { 471 | // Add tree node to childNodes if one does not exist 472 | children[astToken.value] = { 473 | id: getNonce(), 474 | name: imports[astToken.value]['importName'], 475 | fileName: path.basename(imports[astToken.value]['importPath']), 476 | filePath: path.resolve( 477 | path.dirname(parent.filePath), 478 | imports[astToken.value]['importPath'] 479 | ), 480 | importPath: imports[astToken.value]['importPath'], 481 | expanded: false, 482 | depth: parent.depth + 1, 483 | thirdParty: false, 484 | reactRouter: false, 485 | reduxConnect: false, 486 | count: 1, 487 | props: props, 488 | children: [], 489 | parent: parent.id, 490 | parentList: [parent.filePath].concat(parent.parentList), 491 | error: '', 492 | isClientComponent: false 493 | }; 494 | } 495 | return children; 496 | } 497 | 498 | // Extracts prop names from a JSX element 499 | private getJSXProps(astTokens: { [key: string]: any }[], 500 | j: number 501 | ): { [key: string]: boolean } { 502 | const props: any = {}; 503 | while (astTokens[j].type.label !== 'jsxTagEnd') { 504 | if ( 505 | astTokens[j].type.label === 'jsxName' && 506 | astTokens[j + 1].value === '=' 507 | ) { 508 | props[astTokens[j].value] = true; 509 | } 510 | j += 1; 511 | } 512 | return props; 513 | } 514 | 515 | // Checks if current Node is connected to React-Redux Store 516 | private checkForRedux(astTokens: any[], importsObj: ImportObj): boolean { 517 | // Check that React-Redux is imported in this file (and we have a connect method or otherwise) 518 | let reduxImported = false; 519 | let connectAlias; 520 | Object.keys(importsObj).forEach((key) => { 521 | if ( 522 | importsObj[key].importPath === 'react-redux' && 523 | importsObj[key].importName === 'connect' 524 | ) { 525 | reduxImported = true; 526 | connectAlias = key; 527 | } 528 | }); 529 | 530 | if (!reduxImported) { 531 | return false; 532 | } 533 | 534 | // Check that connect method is invoked and exported in the file 535 | for (let i = 0; i < astTokens.length; i += 1) { 536 | if ( 537 | astTokens[i].type.label === 'export' && 538 | astTokens[i + 1].type.label === 'default' && 539 | astTokens[i + 2].value === connectAlias 540 | ) { 541 | return true; 542 | } 543 | } 544 | return false; 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { runTests } from '@vscode/test-electron'; 3 | 4 | async function main() { 5 | try { 6 | // The folder containing the Extension Manifest package.json 7 | // Passed to `--extensionDevelopmentPath` 8 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 9 | 10 | // The path to the extension test script 11 | // Passed to --extensionTestsPath 12 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 13 | 14 | // Download VS Code, unzip it and run the integration test 15 | await runTests({ 16 | version: "1.85.1", 17 | extensionDevelopmentPath, 18 | extensionTestsPath 19 | }); 20 | } catch (err) { 21 | console.error('Failed to run tests', err); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | main(); 27 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { glob } from 'glob'; 3 | import * as jest from 'jest'; 4 | 5 | export async function run(): Promise { 6 | try { 7 | const testsRoot = path.resolve(__dirname, '..'); 8 | const files = await glob('**/**.test.js', { cwd: testsRoot }); 9 | 10 | if (files.length === 0) { 11 | console.warn('No test files found'); 12 | return; 13 | } 14 | 15 | return new Promise(async (c, e) => { 16 | try { 17 | await jest.run([...files]); 18 | c(); 19 | } catch (err) { 20 | console.error(err); 21 | e(err); 22 | } 23 | }); 24 | } catch (err) { 25 | console.error(err); 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/suite/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../../parser'; 2 | import * as path from 'path'; 3 | import { beforeAll, beforeEach, expect, test } from '@jest/globals'; 4 | 5 | describe('Parser Test Suite', () => { 6 | let parser, tree, file; 7 | const fs = require('fs'); 8 | 9 | // TEST 0: ONE CHILD 10 | describe('It works for simple apps', () => { 11 | beforeAll(() => { 12 | // console.log('-----test 0----------') 13 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_0/index.js'); 14 | parser = new Parser(file); 15 | tree = parser.parse(); 16 | }); 17 | 18 | test('Tree should not be undefined', () => { 19 | expect(tree).toBeDefined(); 20 | expect(typeof(tree)).toBe('object'); 21 | }); 22 | 23 | test('Parsed tree has a property called name with value index, and a child with the name App', () => { 24 | expect(tree).toHaveProperty('name', 'index'); 25 | expect(tree.children[0]).toHaveProperty('name', 'App'); 26 | }); 27 | }); 28 | 29 | // these are the 14 tests we need to test for 30 | 31 | // TEST 1: NESTED CHILDREN 32 | 33 | describe('It checks for nested Children', () => { 34 | beforeEach(() => { 35 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_1/index.js'); 36 | parser = new Parser(file); 37 | tree = parser.parse(); 38 | }) 39 | 40 | test('Parsed tree should have a property called name with the value index, and one child with name App, which has its own child, Main', () => { 41 | expect(tree).toHaveProperty('name', 'index'); 42 | expect(tree.children[0]).toHaveProperty('name', 'App'); 43 | // console.log(tree.children[0].children); 44 | expect(tree.children[0].children[0]).toHaveProperty('name', 'Main'); 45 | }) 46 | 47 | test('Parsed tree has correct amount of child components', () => { 48 | expect(tree.children).toHaveLength(1); 49 | expect(tree.children[0].children).toHaveLength(1); 50 | }) 51 | 52 | test('Parsed tree depth is accurate', () => { 53 | expect(tree).toHaveProperty('depth', 0); 54 | expect(tree.children[0]).toHaveProperty('depth', 1); 55 | expect(tree.children[0].children[0]).toHaveProperty('depth', 2); 56 | }) 57 | }) 58 | 59 | // TEST 2: THIRD PARTY, REACT ROUTER, DESTRUCTURED IMPORTS 60 | describe('It works for third party, React Router, and destructured imports', () => { 61 | beforeAll(() => { 62 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_2/index.js'); 63 | parser = new Parser(file); 64 | tree = parser.parse(); 65 | }) 66 | 67 | test('Should parse destructured and third party imports', () => { 68 | expect(tree).toHaveProperty('thirdParty', false); 69 | expect(tree.children[0]).toHaveProperty('thirdParty', true); 70 | expect(tree.children[1]).toHaveProperty('thirdParty', true); 71 | 72 | try { 73 | expect(tree.children[0].name).toContain('Switch') 74 | } catch { 75 | expect(tree.children[0].name).toContain('Route') 76 | 77 | } 78 | try { 79 | expect(tree.children[1].name).toContain('Switch') 80 | } catch { 81 | expect(tree.children[1].name).toContain('Route') 82 | 83 | } 84 | }) 85 | 86 | test('third party should be reactRouter', () => { 87 | expect(tree.children[0]).toHaveProperty('reactRouter', true); 88 | expect(tree.children[1]).toHaveProperty('reactRouter', true); 89 | }) 90 | 91 | }) 92 | 93 | 94 | // TEST 6: BAD IMPORT OF APP2 FROM APP1 COMPONENT 95 | describe('Catches bad imports', () => { 96 | beforeEach(() => { 97 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_6/component/App.jsx'); 98 | parser = new Parser(file); 99 | tree = parser.parse(); 100 | }); 101 | 102 | test("Child component with bad file path does not show up on the node tree", () => { 103 | expect(tree.children.length).toBe(0); 104 | }); 105 | }); 106 | 107 | // TEST 7: SYNTAX ERROR IN APP FILE CAUSES PARSER ERROR 108 | xdescribe('Parser should not work for components with syntax errors in the code', () => { 109 | beforeEach(() => { 110 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_7/index.js'); 111 | parser = new Parser(file); 112 | tree = parser.parse(); 113 | }); 114 | 115 | test("Parser stops parsing when there is a syntax error in a component", () => { 116 | expect(tree.children.length).toBe(0); 117 | }); 118 | }); 119 | 120 | // TEST 11: PARSER DOESN'T BREAK UPON RECURSIVE COMPONENTS 121 | describe('It should render the second call of mutually recursive components, but no further', () => { 122 | beforeAll(() => { 123 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_11/index.js'); 124 | parser = new Parser(file); 125 | tree = parser.parse(); 126 | // console.log('tree11', tree); 127 | }); 128 | 129 | test('Tree should not be undefined', () => { 130 | expect(tree).toBeDefined(); 131 | }); 132 | 133 | test('Tree should have an index component while child App1, grandchild App2, great-grandchild App1', () => { 134 | expect(tree).toHaveProperty('name', 'index'); 135 | expect(tree.children).toHaveLength(1); 136 | expect(tree.children[0]).toHaveProperty('name', 'App1'); 137 | expect(tree.children[0].children).toHaveLength(1); 138 | expect(tree.children[0].children[0]).toHaveProperty('name', 'App2'); 139 | expect(tree.children[0].children[0].children).toHaveLength(1); 140 | expect(tree.children[0].children[0].children[0]).toHaveProperty('name', 'App1'); 141 | expect(tree.children[0].children[0].children[0].children).toHaveLength(0); 142 | }); 143 | }); 144 | 145 | // TEST 12A: NEXT.JS APPS (PAGES ROUTER) 146 | describe('It should parse Next.js applications using Pages Router', () => { 147 | beforeAll(() => { 148 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_12a/pages/index.js'); 149 | parser = new Parser(file); 150 | tree = parser.parse(); 151 | }); 152 | 153 | test('Root should be named index, children should be named Head and Navbar, children of Navbar should be named Link and Image', () => { 154 | expect(tree).toHaveProperty('name', 'index'); 155 | expect(tree.children).toHaveLength(2); 156 | expect(tree.children[0]).toHaveProperty('name', 'Head'); 157 | expect(tree.children[1]).toHaveProperty('name', 'Navbar'); 158 | 159 | expect(tree.children[1].children).toHaveLength(2); 160 | expect(tree.children[1].children[0]).toHaveProperty('name', 'Link'); 161 | expect(tree.children[1].children[1]).toHaveProperty('name', 'Image'); 162 | }); 163 | }); 164 | 165 | // TEST 12B: NEXT.JS APPS (APP ROUTER) 166 | describe('It should parser Next.js applications using Apps Router', () => { 167 | beforeAll(() => { 168 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_12b/app/page.jsx'); 169 | parser = new Parser(file); 170 | tree = parser.parse(); 171 | }); 172 | 173 | test('Root should be named page, it should have one child named Homepage', () => { 174 | expect(tree).toHaveProperty('name', 'page'); 175 | expect(tree.children).toHaveLength(1); 176 | expect(tree.children[0]).toHaveProperty('name', 'HomePage'); 177 | }); 178 | }); 179 | 180 | // TEST 13: VARIABLE DECLARATION IMPORTS AND REACT.LAZY IMPORTS 181 | describe('It should parse VariableDeclaration imports including React.lazy imports', () => { 182 | beforeAll(() => { 183 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_13/index.js'); 184 | parser = new Parser(file); 185 | tree = parser.parse(); 186 | }); 187 | 188 | test('Root should be named index, it should have one child named App', () => { 189 | expect(tree).toHaveProperty('name', 'index'); 190 | expect(tree.children).toHaveLength(1); 191 | expect(tree.children[0]).toHaveProperty('name', 'App'); 192 | }); 193 | 194 | test('App should have three children, Component1, Component2 and Component3, all found successfully', () => { 195 | expect(tree.children[0].children[0]).toHaveProperty('name', 'Component1'); 196 | expect(tree.children[0].children[0]).toHaveProperty('thirdParty', false); 197 | 198 | expect(tree.children[0].children[1]).toHaveProperty('name', 'Component2'); 199 | expect(tree.children[0].children[1]).toHaveProperty('thirdParty', false); 200 | 201 | expect(tree.children[0].children[2]).toHaveProperty('name', 'Component3'); 202 | expect(tree.children[0].children[2]).toHaveProperty('thirdParty', false); 203 | }); 204 | }); 205 | 206 | // TEST 14: CHECK IF COMPONENT IS A CLIENT COMPONENT USING HOOKS AND DIRECTIVES 207 | describe('It should parse components and determine if the component type', () => { 208 | beforeAll(() => { 209 | file = path.join(__dirname, '../../../../src/test/test_cases/tc_14/index.js'); 210 | parser = new Parser(file); 211 | tree = parser.parse(); 212 | }); 213 | 214 | test('Root should be named index, it should have one children named App', () => { 215 | expect(tree).toHaveProperty('name', 'index'); 216 | expect(tree.children).toHaveLength(1); 217 | expect(tree.children[0]).toHaveProperty('name', 'App'); 218 | }); 219 | 220 | test('App should have three children, Component1, Component4, Component5 is a client component using hooks, Component2 is a client component using directives, and Component3, Component6, Component7 is not a client component', () => { 221 | expect(tree.children[0].children[0]).toHaveProperty('name', 'Component1'); 222 | expect(tree.children[0].children[0]).toHaveProperty('isClientComponent', true); 223 | 224 | expect(tree.children[0].children[1]).toHaveProperty('name', 'Component2'); 225 | expect(tree.children[0].children[1]).toHaveProperty('isClientComponent', true); 226 | 227 | expect(tree.children[0].children[2]).toHaveProperty('name', 'Component3'); 228 | expect(tree.children[0].children[2]).toHaveProperty('isClientComponent', false); 229 | 230 | expect(tree.children[0].children[3]).toHaveProperty('name', 'Component4'); 231 | expect(tree.children[0].children[3]).toHaveProperty('isClientComponent', true); 232 | 233 | expect(tree.children[0].children[4]).toHaveProperty('name', 'Component5'); 234 | expect(tree.children[0].children[4]).toHaveProperty('isClientComponent', true); 235 | 236 | expect(tree.children[0].children[5]).toHaveProperty('name', 'Component6'); 237 | expect(tree.children[0].children[5]).toHaveProperty('isClientComponent', false); 238 | 239 | expect(tree.children[0].children[6]).toHaveProperty('name', 'Component7'); 240 | expect(tree.children[0].children[6]).toHaveProperty('isClientComponent', false); 241 | }); 242 | }); 243 | 244 | 245 | 246 | 247 | // TEST 3: IDENTIFIES REDUX STORE CONNECTION 248 | // TEST 4: ALIASED IMPORTS 249 | // TEST 5: MISSING EXTENSIONS AND UNUSED IMPORTS 250 | 251 | // TEST 8: MULTIPLE PROPS ON ONE COMPONENT 252 | // TEST 9: FINDING DIFFERENT PROPS ACROSS TWO OR MORE IDENTICAL COMPONENTS 253 | 254 | // LOU is doing EXTENSION TEST in extension.test.ts 255 | 256 | }); 257 | -------------------------------------------------------------------------------- /src/test/test_cases/tc_0/component/App.jsx: -------------------------------------------------------------------------------- 1 | export default function App() { 2 | return ( 3 |
4 |
This is the App.
5 |
6 | ) 7 | }; -------------------------------------------------------------------------------- /src/test/test_cases/tc_0/index.js: -------------------------------------------------------------------------------- 1 | // Test Case 0 - Simple react app with one app component 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import App from './component/App.jsx'; 6 | 7 | const root = createRoot(document.getElementById('root')); 8 | root.render(); -------------------------------------------------------------------------------- /src/test/test_cases/tc_1/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Main from './Main.jsx'; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |
App
8 |
9 |
10 | ) 11 | } 12 | 13 | export default App; -------------------------------------------------------------------------------- /src/test/test_cases/tc_1/components/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Main = () => { 4 | return ( 5 |
Main App
6 | ) 7 | } 8 | 9 | export default Main; -------------------------------------------------------------------------------- /src/test/test_cases/tc_1/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import App from "./components/App.jsx"; 4 | 5 | //TEST 1 - Simple App with 2 components, App and Main 6 | //App renders Main 7 | 8 | render( 9 |
10 | 11 |
, document.getElementById('root') 12 | ); -------------------------------------------------------------------------------- /src/test/test_cases/tc_11/components/App1.jsx: -------------------------------------------------------------------------------- 1 | import App2 from './App2.jsx'; 2 | 3 | export default function App1() { 4 | return ( 5 |
6 |
I am App 1
7 | 8 |
9 | ); 10 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_11/components/App2.jsx: -------------------------------------------------------------------------------- 1 | import App1 from './App1.jsx'; 2 | 3 | export default function App2() { 4 | return ( 5 |
6 |
This is App 2
7 | 8 |
9 | ); 10 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_11/index.js: -------------------------------------------------------------------------------- 1 | // Test Case 11 - Recursive import of App1 and App2 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import App1 from './components/App1.jsx'; 6 | 7 | const root = createRoot(document.getElementById('root')); 8 | root.render(); -------------------------------------------------------------------------------- /src/test/test_cases/tc_12a/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Image from 'next/image'; 3 | import logo from '../public/nextjs_logo.png'; 4 | 5 | export const Navbar = () => { 6 | return ( 7 | 29 | ); 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /src/test/test_cases/tc_12a/pages/index.js: -------------------------------------------------------------------------------- 1 | // Test Case 12a - Parse for Next.js using Pages Router 2 | 3 | import Head from 'next/head'; 4 | import Navbar from '../components/Navbar.jsx'; 5 | 6 | export default function Home() { 7 | return ( 8 |
9 | 10 | Home Page 11 | 12 | 13 | 14 | 15 |
16 |

Welcome to Next.js!

17 |

This is a sample index page created with Next.js.

18 |
19 |
20 |

Footer content here

21 |
22 |
23 | ); 24 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_12b/app/homepage.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export default function HomePage({ recentPosts }) { 4 | return ( 5 |
6 | {recentPosts.map((post) => ( 7 |
{post.title}
8 | ))} 9 |
10 | ); 11 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_12b/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import './styles/globals.css'; 2 | import React from 'react'; 3 | 4 | export const metadata = { 5 | title: 'Home', 6 | description: 'Welcome to Next.js', 7 | } 8 | 9 | export default function RootLayout({ children }) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_12b/app/page.jsx: -------------------------------------------------------------------------------- 1 | // Import your Client Component 2 | import HomePage from './homepage.jsx'; 3 | 4 | async function getPosts() { 5 | const res = await fetch('https://...'); 6 | const posts = await res.json(); 7 | return posts; 8 | } 9 | 10 | export default async function Page() { 11 | // Fetch data directly in a Server Component 12 | const recentPosts = await getPosts(); 13 | // Forward fetched data to your Client Component 14 | return ( 15 | 16 | ); 17 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_13/components/App.jsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | const Component1 = lazy(() => import('./Component1')); 3 | import Component2 from "./Component2"; 4 | import Component3 from "./Component3"; 5 | 6 | export default function Pages() { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_13/components/Component1.jsx: -------------------------------------------------------------------------------- 1 | export default function Component1() { 2 | return ( 3 |
4 |
This is Component 1.
5 |
6 | ); 7 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_13/components/Component2.jsx: -------------------------------------------------------------------------------- 1 | export default function Component2() { 2 | return ( 3 |
4 |
This is Component 2.
5 |
6 | ); 7 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_13/components/Component3.jsx: -------------------------------------------------------------------------------- 1 | export default function Component3() { 2 | return ( 3 |
4 |
This is Component 3.
5 |
6 | ); 7 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_13/index.js: -------------------------------------------------------------------------------- 1 | // Test Case 13 - Recursive import of App1 and App2 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import App from './components/App'; 6 | 7 | const root = createRoot(document.getElementById('root')); 8 | root.render(); -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Component1 from "./Component1"; 3 | import Component2 from "./Component2"; 4 | import Component3 from "./Component3"; 5 | import Component4 from "./Component4"; 6 | import Component5 from "./Component5"; 7 | import Component6 from "./Component6"; 8 | import Component7 from "./Component7"; 9 | 10 | export default function Pages() { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/Component1.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const Component1 = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | const handleClick = () => { 7 | setCount(count + 1); 8 | }; 9 | 10 | return ( 11 |
12 |

This is Component 1.

13 |

Count: {count}

14 | ; 15 |
16 | ); 17 | } 18 | 19 | export default Component1; -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/Component2.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState, useEffect } from 'react'; 3 | 4 | export default function Component2() { 5 | const [seconds, setSeconds] = useState(0); 6 | 7 | useEffect(() => { 8 | const interval = setInterval(() => { 9 | setSeconds(prevSeconds => prevSeconds + 1); 10 | }, 1000); 11 | 12 | return () => clearInterval(interval); 13 | }); 14 | 15 | return ( 16 |
17 |

Timer Component

18 |

Seconds: {seconds}

19 |
20 | ); 21 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/Component3.jsx: -------------------------------------------------------------------------------- 1 | export default function Component3() { 2 | return ( 3 |
4 |

Static Component

5 |

This is a static component without interactivity or state.

6 |
7 | ); 8 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/Component4.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function Component4() { 4 | const [items, setItems] = useState([]); 5 | 6 | const addItem = () => { 7 | const newItem = `Item ${items.length + 1}`; 8 | setItems([...items, newItem]); 9 | }; 10 | 11 | return ( 12 |
13 |

List Component

14 | 15 |
    16 | {items.map((item, index) => ( 17 |
  • {item}
  • 18 | ))} 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/Component5.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | function Component5() { 4 | const [isToggled, setIsToggled] = useState(false); 5 | 6 | const handleToggle = () => { 7 | setIsToggled(!isToggled); 8 | }; 9 | 10 | return ( 11 |
12 |

Toggle Component

13 |

Status: {isToggled ? 'Enabled' : 'Disabled'}

14 | ; 15 |
16 | ); 17 | } 18 | 19 | export default Component5; -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/Component6.jsx: -------------------------------------------------------------------------------- 1 | function Component6() { 2 | return ( 3 |
4 |

Static Component

5 |

This is a static component without interactivity or state.

6 |
7 | ); 8 | } 9 | 10 | export default Component6; -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/components/Component7.jsx: -------------------------------------------------------------------------------- 1 | const Component7 = () => { 2 | return ( 3 |
4 |

Static Component

5 |

This is a static component without interactivity or state.

6 |
7 | ); 8 | } 9 | 10 | export default Component7; -------------------------------------------------------------------------------- /src/test/test_cases/tc_14/index.js: -------------------------------------------------------------------------------- 1 | // Test Case 14 - Check Component Type 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import App from './components/App'; 6 | 7 | const root = createRoot(document.getElementById('root')); 8 | root.render(); -------------------------------------------------------------------------------- /src/test/test_cases/tc_2/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import { Switch, Route} from 'react-router-dom'; 4 | 5 | 6 | // TEST 2: THIRD PARTY, REACT ROUTER, DESTRUCTURED IMPORTS 7 | 8 | render( 9 |
10 | 11 | 12 | 13 | 14 |
, document.getElementById('root') 15 | ); -------------------------------------------------------------------------------- /src/test/test_cases/tc_6/component/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import anotherApp from "./anotherApp"; // this is purposefully the wrong file path for anotherApp 3 | 4 | const App = () => { 5 | return ( 6 |
7 |

Hello from App.jsx

8 | 9 |
10 | ) 11 | }; 12 | 13 | export default App; -------------------------------------------------------------------------------- /src/test/test_cases/tc_6/index.js: -------------------------------------------------------------------------------- 1 | // !TEST 6: BAD IMPORT OF APP2 FROM APP1 COMPONENT 2 | import React from 'react'; 3 | import { createRoot } from 'react-dom/client'; 4 | import App from './components/App.jsx'; 5 | 6 | // tests whether the parser still works when a component is given the wrong File path 7 | 8 | const root = createRoot(document.getElementById('root')); 9 | root.render(); -------------------------------------------------------------------------------- /src/test/test_cases/tc_6/otherComponent/anotherApp.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const anotherApp = () => { 4 | return ( 5 |
6 |

Greetings from inside anotherApp

7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_7/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ChildApp from './ChildApp'; 3 | 4 | export const App = () => { 5 | // this should not work when given to the parser 6 | return ( 7 |
8 |

Syntax Error

9 | 10 |
11 | ) 12 | 13 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_7/components/ChildApp.jsx: -------------------------------------------------------------------------------- 1 | // this component will not show up in the children of App due to App's syntax error 2 | import React, { Component } from 'react'; 3 | 4 | export const ChildApp = () => { 5 | return ( 6 |
7 |

Child of App with Syntax Error

8 |
9 | ) 10 | 11 | } -------------------------------------------------------------------------------- /src/test/test_cases/tc_7/index.js: -------------------------------------------------------------------------------- 1 | //! TEST 7: SYNTAX ERROR IN APP FILE CAUSES PARSER ERROR 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import App from './components/App.jsx'; 6 | 7 | const root = createRoot(document.getElementById('root')); 8 | root.render(); -------------------------------------------------------------------------------- /src/test/vscode-environment.js: -------------------------------------------------------------------------------- 1 | const { TestEnvironment } = require('jest-environment-jsdom'); 2 | const vscode = require('@vscode/test-electron'); 3 | 4 | // Allows for VSCode Envionrment to be extended to Jest Environment 5 | class VsCodeEnvironment extends TestEnvironment { 6 | async setup() { 7 | await super.setup(); 8 | this.global.vscode = vscode; 9 | } 10 | 11 | async teardown() { 12 | this.global.vscode = {}; 13 | await super.teardown(); 14 | } 15 | } 16 | 17 | module.exports = VsCodeEnvironment; -------------------------------------------------------------------------------- /src/test/vscode.js: -------------------------------------------------------------------------------- 1 | // Allows access to vscode object in testing 2 | module.exports = global.vscode; -------------------------------------------------------------------------------- /src/types/ImportObj.ts: -------------------------------------------------------------------------------- 1 | export type ImportObj = { 2 | [key: string]: { importPath: string; importName: string; }; 3 | }; -------------------------------------------------------------------------------- /src/types/builder.ts: -------------------------------------------------------------------------------- 1 | export type Builder = { 2 | parsedData: [object]; 3 | id: number; 4 | x: number; 5 | y: number; 6 | initialNodes: []; 7 | viewData: any; 8 | edgeId: number; 9 | initialEdges: []; 10 | }; -------------------------------------------------------------------------------- /src/types/connection.ts: -------------------------------------------------------------------------------- 1 | export enum ConnectionLineType { 2 | Bezier = 'default', 3 | Straight = 'straight', 4 | Step = 'step', 5 | SmoothStep = 'smoothstep', 6 | SimpleBezier = 'simplebezier', 7 | }; -------------------------------------------------------------------------------- /src/types/hierarchyData.ts: -------------------------------------------------------------------------------- 1 | export interface hierarchyData { 2 | id: string, 3 | position: { x: number, y: number }, 4 | type: string, 5 | data: { label: string }, 6 | style: { 7 | borderRadius: string, 8 | borderWidth: string, 9 | borderColor: string, 10 | display: string, 11 | justifyContent: string, 12 | placeItems: string, 13 | backgroundColor: string, 14 | } 15 | }; -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | declare module '*.jpeg'; -------------------------------------------------------------------------------- /src/types/tree.ts: -------------------------------------------------------------------------------- 1 | export type Tree = { 2 | id: string; 3 | name: string; 4 | fileName: string; 5 | filePath: string; 6 | importPath: string; 7 | expanded: boolean; 8 | depth: number; 9 | count: number; 10 | thirdParty: boolean; 11 | reactRouter: boolean; 12 | reduxConnect: boolean; 13 | children: Tree[]; 14 | parent: string; 15 | parentList: string[]; 16 | props: { [key: string]: boolean; }; 17 | error: string; 18 | isClientComponent: boolean; 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/getNonce.ts: -------------------------------------------------------------------------------- 1 | export function getNonce() { 2 | let text: string = ""; 3 | const possible: string = 4 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 5 | for (let i = 0; i < 32; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | return text; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/utils/modal.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function showNotification({message, timeout = 5000 }: { message: string, timeout?: number }) { 4 | await vscode.window.withProgress( 5 | { 6 | location: vscode.ProgressLocation.Notification, 7 | cancellable: false 8 | }, 9 | async (progress) => { 10 | progress.report({ increment: 100, message: `${message}` }); 11 | await new Promise((resolve) => setTimeout(resolve, timeout)); 12 | } 13 | ); 14 | } -------------------------------------------------------------------------------- /src/webview/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Flow from "./Flow"; 3 | import "./style.css"; 4 | 5 | export default function App() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } -------------------------------------------------------------------------------- /src/webview/Flow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import ReactFlow, { 3 | MiniMap, 4 | Panel, 5 | Controls, 6 | Background, 7 | useNodesState, 8 | useEdgesState, 9 | Node, 10 | Edge 11 | } from "reactflow"; 12 | import FlowBuilder from "./flowBuilder"; 13 | import { Tree } from "../types/tree"; 14 | import "reactflow/dist/style.css"; 15 | import "./style.css"; 16 | 17 | const OverviewFlow = () => { 18 | 19 | // Required to have different initial states to render through D3 20 | const initialNodes: Node[] = []; 21 | const initialEdges: Edge[] = []; 22 | 23 | const [nodes, setNodes, onNodesChange] = useNodesState([]); 24 | const [edges, setEdges, onEdgesChange] = useEdgesState([]); 25 | 26 | useEffect(() => { 27 | window.addEventListener('message', (e: MessageEvent) => { 28 | // Object containing type prop and value prop 29 | const msg: MessageEvent = e; 30 | const flowBuilder = new FlowBuilder; 31 | 32 | switch (msg.data.type) { 33 | case 'parsed-data': { 34 | let data: Tree | undefined = msg.data.value; 35 | 36 | // Creates our Tree structure 37 | flowBuilder.mappedData(data, initialNodes, initialEdges); 38 | 39 | setEdges(initialEdges); 40 | setNodes(initialNodes); 41 | break; 42 | } 43 | } 44 | }); 45 | }, []); 46 | 47 | return ( 48 |
49 | 58 | { 60 | if (n.style?.backgroundColor) return n.style.backgroundColor; 61 | if (n.type === "default") return "#1a192b"; 62 | return "#eee"; 63 | }} 64 | nodeColor={(n): string => { 65 | if (n.style?.backgroundColor) return n.style.backgroundColor; 66 | return "#fff"; 67 | }} 68 | nodeBorderRadius={2} 69 | /> 70 | 71 |
72 |
73 |

Client: 00

74 |
75 |
76 |

Server: 00

77 |
78 |
79 |
80 | 81 | 82 |
83 |
84 | ); 85 | }; 86 | 87 | export default OverviewFlow; 88 | -------------------------------------------------------------------------------- /src/webview/flowBuilder.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectionLineType, Edge, Node } from 'reactflow'; 2 | import { Tree } from '../types/tree'; 3 | import { getNonce } from '../utils/getNonce'; 4 | import * as d3 from 'd3'; 5 | 6 | // Contructs our family tree for React application root file that was selected 7 | 8 | class FlowBuilder { 9 | 10 | public mappedData(data: Tree, nodes: Node[], edges: Edge[]): void { 11 | 12 | // Create a holder for the heirarchical data (msg.value), data comes in an object of all the Trees 13 | const root: d3.HierarchyNode = d3.hierarchy(data); 14 | 15 | // Dynamically adjust height and width of display depending on the amount of nodes 16 | const totalNodes: number = root.descendants().length; 17 | const width: number = Math.max(totalNodes * 100, 800); 18 | const height = Math.max(totalNodes * 20, 500) 19 | 20 | // Create tree layout and give nodes their positions and 21 | const treeLayout: d3.TreeLayout = d3.tree() 22 | .size([width, height]) 23 | .separation((a: d3.HierarchyPointNode, b: d3.HierarchyPointNode) => (a.parent == b.parent ? 2 : 2.5)); 24 | 25 | treeLayout(root); 26 | // Iterate through each Tree and create a node 27 | root.each((node: any): void => { 28 | 29 | // Create a Node from the current Root and add it to our nodes array 30 | nodes.push({ 31 | id: node.data.id, 32 | position: { x: node.x ? node.x : 0, y: node.y ? node.y : 0 }, 33 | type: node.depth === 0 ? 'input' : !node.children ? 'output' : 'default', 34 | data: { label: node.data.name }, 35 | style: { 36 | borderRadius: '6px', 37 | borderWidth: '2px', 38 | borderColor: '#6b7280', 39 | display: 'flex', 40 | justifyContent: 'center', 41 | placeItems: 'center', 42 | backgroundColor: `${(node.data.isClientComponent) ? '#fdba74' : '#93C5FD'}`, 43 | } 44 | }); 45 | 46 | // If the current node has a parent, create an edge to show relationship 47 | if (node.data.parent) { 48 | const newEdge: Edge = { 49 | id: `${getNonce()}`, 50 | source: node.data.parent, 51 | target: node.data.id, 52 | type: ConnectionLineType.Bezier, 53 | animated: true, 54 | }; 55 | 56 | // Check if the edge already exists before adding 57 | const edgeExists: boolean = edges.some( 58 | edge => edge.source === newEdge.source && edge.target === newEdge.target 59 | ); 60 | 61 | // If edge does not exist, add to our edges array 62 | if (!edgeExists) { 63 | edges.push(newEdge); 64 | } 65 | } 66 | }); 67 | } 68 | } 69 | 70 | export default FlowBuilder; -------------------------------------------------------------------------------- /src/webview/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import './style.css'; 4 | import App from "./App"; 5 | 6 | const rootElement = document.getElementById("root"); 7 | const root = createRoot(rootElement); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ); -------------------------------------------------------------------------------- /src/webview/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | #root, 8 | 9 | .App { 10 | height: 100%; 11 | } 12 | 13 | .App { 14 | font-family: sans-serif; 15 | text-align: center; 16 | } 17 | 18 | .react-flow__handle.connectionindicator { 19 | pointer-events: none !important; 20 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./build/bundle.js'], 3 | theme: { 4 | extend: { 5 | backgroundColor: { 6 | orange: '#fdba74', 7 | blue: '#93C5FD', 8 | }, 9 | }, 10 | }, 11 | variants: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | important: true, 16 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./build/src", 6 | "esModuleInterop": true, 7 | "lib": ["es6", "dom"], 8 | "jsx": "react", 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "rootDir": "./src", 12 | "allowJs": true, 13 | "strict": false, 14 | }, 15 | "jest": { 16 | "tsconfig": "tsconfig.json" 17 | }, 18 | "include": ["src"], 19 | "exclude": [ 20 | "node_modules", 21 | ".vscode-test", 22 | "src/webviews", 23 | "src/test/test_cases" 24 | ] 25 | } -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as webpack from 'webpack'; 3 | 4 | const extConfig: webpack.Configuration = { 5 | target: 'node', 6 | entry: './src/extension.ts', 7 | output: { 8 | filename: 'extension.js', 9 | libraryTarget: 'commonjs2', 10 | path: path.resolve(__dirname, 'build'), 11 | }, 12 | resolve: { extensions: ['.ts', '.js'] }, 13 | module: { rules: [{ test: /\.ts$/, loader: 'ts-loader' }] }, 14 | externals: { vscode: 'vscode' }, 15 | }; 16 | 17 | const webviewConfig: webpack.Configuration = { 18 | target: 'web', 19 | entry: './src/webview/index.tsx', 20 | output: { 21 | filename: 'bundle.js', 22 | path: path.resolve(__dirname, 'build'), 23 | }, 24 | resolve: { 25 | extensions: ['.js', '.ts', '.tsx', 'scss'], 26 | }, 27 | module: { 28 | rules: [ 29 | { test: /\.tsx?$/, use: ['babel-loader', 'ts-loader'] }, 30 | { 31 | test: /\.css$/, 32 | use: ['style-loader', 'css-loader', 'postcss-loader'], 33 | }, 34 | ], 35 | }, 36 | }; 37 | 38 | export default [webviewConfig, extConfig]; --------------------------------------------------------------------------------