├── .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 |
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 |
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 |
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 |
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 |
55 |
56 |
57 | ## Tech Stack
58 |
59 |
60 |
61 |
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 |
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 |
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 | , 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 |
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 |
8 |
9 |
15 |
16 |
17 |
18 | Home
19 |
20 |
21 |
22 |
23 | About
24 |
25 |
26 |
27 |
28 |
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 | Click Me ;
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 | Add Item
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 | {isToggled ? 'Disable' : 'Enable'} ;
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 |
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];
--------------------------------------------------------------------------------