├── .gitignore
├── assets
├── Demo.gif
├── logo.png
├── InstallVisiVue.gif
└── CommandPalleteDemo.gif
├── src
├── webview
│ ├── main.js
│ └── components
│ │ ├── ColorKey.vue
│ │ ├── initial-elements.js
│ │ ├── NodeTemplate.vue
│ │ ├── FileImport.vue
│ │ ├── elements.js
│ │ └── App.vue
├── types
│ └── Tree.ts
├── extension.ts
├── panel.ts
└── parser.ts
├── .vscode
├── extensions.json
├── settings.json
├── tasks.json
└── launch.json
├── .vscodeignore
├── CHANGELOG.md
├── .eslintrc.json
├── vueCompilerTypes.txt
├── tsconfig.json
├── LICENSE
├── webpack.config.ts
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | out
--------------------------------------------------------------------------------
/assets/Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/VisiVue/HEAD/assets/Demo.gif
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/VisiVue/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/InstallVisiVue.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/VisiVue/HEAD/assets/InstallVisiVue.gif
--------------------------------------------------------------------------------
/assets/CommandPalleteDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/VisiVue/HEAD/assets/CommandPalleteDemo.gif
--------------------------------------------------------------------------------
/src/webview/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import App from "./components/App";
4 | createApp(App).mount("#app");
5 |
6 | // import FileImport from "./components/FileImport";
7 | // createApp(FileImport).mount("#app");
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | out/**
4 | node_modules/**
5 | src/**
6 | .gitignore
7 | .yarnrc
8 | webpack.config.js
9 | vsc-extension-quickstart.md
10 | **/tsconfig.json
11 | **/.eslintrc.json
12 | **/*.map
13 | **/*.ts
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "extension" 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 | ## [Unreleased]
8 |
9 | - Initial release
--------------------------------------------------------------------------------
/src/types/Tree.ts:
--------------------------------------------------------------------------------
1 | export type Tree = {
2 | id: string;
3 | name: string;
4 | fileName: string;
5 | filePath: string;
6 | fileDirname: string;
7 | importPath: string;
8 | children: {}[];
9 | parentList: string[];
10 | props: {
11 | oneWay: string[];
12 | twoWay: string[];
13 | };
14 | error: string;
15 | };
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/naming-convention": "warn",
13 | "@typescript-eslint/semi": "warn",
14 | "curly": "warn",
15 | "eqeqeq": "warn",
16 | "no-throw-literal": "warn",
17 | "semi": "off"
18 | },
19 | "ignorePatterns": [
20 | "out",
21 | "dist",
22 | "**/*.d.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files
5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files
6 | },
7 | "search.exclude": {
8 | "out": true, // set this to false to include "out" folder in search results
9 | "dist": true // set this to false to include "dist" folder in search results
10 | },
11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
12 | "typescript.tsc.autoDetect": "off"
13 | }
--------------------------------------------------------------------------------
/vueCompilerTypes.txt:
--------------------------------------------------------------------------------
1 | type === 1: Represents an element node. These nodes correspond to HTML elements in the template.
2 | type === 2: Represents a text node. These nodes contain static text or interpolated expressions within double curly braces ({{ }}).
3 | type === 3: Represents a text node. These nodes contain static text content.
4 | type === 4: Represents a comment node. These nodes represent HTML comments in the template.
5 | type === 5: Represents an interpolation node. These nodes contain dynamic expressions within double curly braces ({{ }}).
6 | type === 6: Represents an attribute node. These nodes contain static attributes like class, id, or other HTML attributes.
7 | type === 7: Represents a directive node. These nodes correspond to directives like v-bind, v-on, or v-model.
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "ES2020",
5 | "outDir": "out",
6 | "lib": [
7 | "ES2020"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "allowJs": true,
12 | "strict": false, /* enable all strict type-checking options */
13 | /* Additional Checks */
14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
17 | },
18 | "include": ["src"],
19 | "exclude" : [
20 | "node_modules",
21 | ".vscode-test",
22 | "src/webviews",
23 | "test"
24 |
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import Panel from "./panel";
3 |
4 | export function activate(context: vscode.ExtensionContext) {
5 | let disposable = vscode.commands.registerCommand(
6 | "extension.showPanels",
7 | () => {
8 | // @ts-ignore
9 | Panel.createOrShow(context);
10 | }
11 | );
12 |
13 | context.subscriptions.push(disposable);
14 |
15 | // Create VisiVue status bar button in the bottom right of vscode
16 | const item = vscode.window.createStatusBarItem(
17 | vscode.StatusBarAlignment.Right
18 | );
19 |
20 | item.command = "extension.showPanels";
21 | item.tooltip = "Activate VisiVue Extension";
22 | item.text = "$(type-hierarchy) Launch VisiVue";
23 | item.show();
24 | }
25 | export function deactivate() {}
26 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$ts-webpack-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never",
13 | "group": "watchers"
14 | },
15 | "group": {
16 | "kind": "build",
17 | "isDefault": true
18 | }
19 | },
20 | {
21 | "type": "npm",
22 | "script": "watch-tests",
23 | "problemMatcher": "$tsc-watch",
24 | "isBackground": true,
25 | "presentation": {
26 | "reveal": "never",
27 | "group": "watchers"
28 | },
29 | "group": "build"
30 | },
31 | {
32 | "label": "tasks: watch-tests",
33 | "dependsOn": [
34 | "npm: watch",
35 | "npm: watch-tests"
36 | ],
37 | "problemMatcher": []
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it 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 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/dist/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | },
20 | {
21 | "name": "Extension Tests",
22 | "type": "extensionHost",
23 | "request": "launch",
24 | "args": [
25 | "--extensionDevelopmentPath=${workspaceFolder}",
26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
27 | ],
28 | "outFiles": [
29 | "${workspaceFolder}/out/**/*.js",
30 | "${workspaceFolder}/dist/**/*.js"
31 | ],
32 | "preLaunchTask": "tasks: watch-tests"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/webpack.config.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as path from "path";
3 | import * as webpack from "webpack";
4 | import { VueLoaderPlugin } from "vue-loader";
5 |
6 | const extConfig: webpack.Configuration = {
7 | mode: "development",
8 | target: "node",
9 | entry: "./src/extension.ts",
10 | output: {
11 | filename: "extension.js",
12 | libraryTarget: "commonjs2",
13 | path: path.resolve(__dirname, "out"),
14 | },
15 | resolve: { extensions: [".ts", ".js", ".vue"] },
16 | module: {
17 | rules: [
18 | { test: /\.ts$/, loader: "ts-loader" },
19 | // { test: /\.vue$/, loader: "vue-loader" },
20 | ],
21 | },
22 | externals: { vscode: "vscode" },
23 | // plugins: [new VueLoaderPlugin()],
24 | };
25 |
26 | const webviewConfig: webpack.Configuration = {
27 | mode: "development",
28 | target: "web",
29 | entry: "./src/webview/main.js", // ENTRY FILE: CHANGE FOR VUE
30 | output: {
31 | filename: "[name].wv.js",
32 | path: path.resolve(__dirname, "out"),
33 | },
34 | resolve: {
35 | extensions: [".js", ".ts", ".tsx", ".scss", ".vue"],
36 | },
37 | module: {
38 | rules: [
39 | { test: /\.tsx?$/, use: ["babel-loader", "ts-loader"] },
40 | { test: /\.vue$/, loader: "vue-loader" },
41 | {
42 | test: /\.css$/,
43 | use: ["style-loader", "css-loader"],
44 | },
45 | ],
46 | },
47 | plugins: [new VueLoaderPlugin()],
48 | };
49 |
50 | export default [webviewConfig, extConfig];
--------------------------------------------------------------------------------
/src/webview/components/ColorKey.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Color Key
4 |
One-way
5 |
Two-way
6 |
Both
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/webview/components/initial-elements.js:
--------------------------------------------------------------------------------
1 | // import { MarkerType } from "@vue-flow/core";
2 |
3 |
4 | /**
5 | * You can pass elements together as a v-model value
6 | * or split them up into nodes and edges and pass them to the `nodes` and `edges` props of Vue Flow (or useVueFlow composable)
7 | */
8 |
9 | // need to figure out how to dynamically create the 'x' and 'y' positions based on the node's position in the tree
10 |
11 | export const initialElements = [
12 | {
13 | id: "1",
14 | type: "input",
15 | label: "monday.js",
16 | position: { x: 250, y: 0 },
17 | class: "light",
18 | parentId: 0,
19 | },
20 | {
21 | id: "2",
22 | // type: "output", // specifies handles. 'default' or none, is two handles (top and bottom). 'input' has a bottom handle. 'output' has a top handle
23 | label: "App.vue",
24 | position: { x: 250, y: 100 },
25 | class: "light",
26 | parentId: "1",
27 | },
28 | {
29 | id: "3",
30 | label: "MainContainer.vue",
31 | position: { x: 250, y: 200 },
32 | class: "light",
33 | parentId: "2",
34 | },
35 | {
36 | id: "4",
37 | label: "MainDisplay.vue",
38 | position: { x: 150, y: 300 },
39 | class: "light",
40 | parentId: "3",
41 | },
42 | {
43 | id: "5",
44 | type: "output",
45 | label: "Content.vue",
46 | position: { x: 350, y: 300 },
47 | class: "light",
48 | parentId: "3",
49 | },
50 | {
51 | id: "e1-2",
52 | source: "1",
53 | target: "2",
54 | // animated: false
55 | },
56 | {
57 | id: "e2-3",
58 | type: "smoothstep",
59 | label: "one-way",
60 | source: "2",
61 | target: "3",
62 | animated: false,
63 | // markerEnd: MarkerType.ArrowClosed,
64 | },
65 | {
66 | id: "e3-4",
67 | type: "smoothstep",
68 | label: "smoothstep-edge",
69 | source: "3",
70 | target: "4",
71 | },
72 | {
73 | id: "e3-5",
74 | type: "step",
75 | label: "step-edge",
76 | source: "3",
77 | target: "5",
78 | style: { stroke: "orange" },
79 | labelBgStyle: { fill: "orange" },
80 | },
81 | ];
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "extension",
3 | "displayName": "extension",
4 | "description": "",
5 | "version": "0.0.1",
6 | "engines": {
7 | "vscode": "^1.79.0"
8 | },
9 | "categories": [
10 | "Other"
11 | ],
12 | "activationEvents": [],
13 | "main": "./out/extension.js",
14 | "contributes": {
15 | "commands": [
16 | {
17 | "command": "extension.showPanels",
18 | "title": "Show VisiVue Panel"
19 | },
20 | {
21 | "command": "extension.testPanel",
22 | "title": "Test Panel"
23 | }
24 | ]
25 | },
26 | "scripts": {
27 | "vscode:prepublish": "npm run compile",
28 | "compile": "webpack --mode production --config webpack.config.ts",
29 | "watch": "webpack --watch",
30 | "package": "webpack --mode production --devtool hidden-source-map",
31 | "compile-tests": "tsc -p . --outDir out",
32 | "watch-tests": "tsc -p . -w --outDir out",
33 | "pretest": "npm run compile-tests && npm run compile && npm run lint",
34 | "lint": "eslint src --ext ts",
35 | "test": "node ./out/test/runTest.js"
36 | },
37 | "devDependencies": {
38 | "@babel/core": "^7.22.6",
39 | "@types/glob": "^8.1.0",
40 | "@types/mocha": "^10.0.1",
41 | "@types/node": "20.2.5",
42 | "@types/vscode": "^1.79.0",
43 | "@typescript-eslint/eslint-plugin": "^5.59.8",
44 | "@typescript-eslint/parser": "^5.59.8",
45 | "@vscode/test-electron": "^2.3.2",
46 | "eslint": "^8.41.0",
47 | "glob": "^8.1.0",
48 | "mocha": "^10.2.0",
49 | "ts-loader": "^9.4.3",
50 | "typescript": "^5.1.5",
51 | "vue-template-babel-compiler": "^2.0.0",
52 | "webpack": "^5.85.0",
53 | "webpack-cli": "^5.1.1"
54 | },
55 | "dependencies": {
56 | "@babel/preset-env": "^7.22.5",
57 | "@vue-flow/background": "^1.2.0",
58 | "@vue-flow/controls": "^1.1.0",
59 | "@vue-flow/core": "^1.20.2",
60 | "@vue-flow/minimap": "^1.1.1",
61 | "@vue/compiler-sfc": "^3.3.4",
62 | "babel": "^6.23.0",
63 | "babel-loader": "^9.1.2",
64 | "css-loader": "^6.8.1",
65 | "os-dns-native": "^1.2.0",
66 | "sass-loader": "^13.3.2",
67 | "style-loader": "^3.3.3",
68 | "ts-node": "^10.9.1",
69 | "vscode": "^1.1.37",
70 | "vue": "3.3.4",
71 | "vue-flow": "^0.3.0",
72 | "vue-loader": "^17.2.2",
73 | "vue-template-compiler": "2.7.14"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/webview/components/NodeTemplate.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 | {{ label }}
31 |
34 |
35 |
36 |
37 | {{ vars }}
38 |
39 |
40 |
41 |
42 | {{ vars }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/webview/components/FileImport.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
23 |
24 |
--------------------------------------------------------------------------------
/src/panel.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { Parser } from './parser';
3 |
4 | export default class Panel {
5 | public static currentPanel: Panel | undefined;
6 |
7 | private static readonly viewType = 'VisiVue';
8 |
9 | private readonly _panel: vscode.WebviewPanel;
10 | private readonly _extensionUri: vscode.Uri;
11 | private readonly _context: vscode.ExtensionContext;
12 | private _disposables: vscode.Disposable[] = [];
13 | private parser: Parser | undefined;
14 |
15 | // Method to see if a panel already exists. If not, create a new panel with the 'context' passed into it in the extension.ts file
16 | public static createOrShow(context: vscode.ExtensionContext) {
17 | const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
18 | if (Panel.currentPanel){
19 | Panel.currentPanel._panel.reveal(column);
20 | } else {
21 | Panel.currentPanel = new Panel(context, vscode.ViewColumn.Two);
22 | }
23 | }
24 |
25 | private constructor(_context: vscode.ExtensionContext, column: vscode.ViewColumn) {
26 | this._context = _context;
27 | this._extensionUri = _context.extensionUri;
28 |
29 | // Create and show a new webview panel
30 | this._panel = vscode.window.createWebviewPanel(Panel.viewType, "VisiVueTree", column, {
31 | enableScripts: true,
32 | retainContextWhenHidden: true,
33 | localResourceRoots: [this._extensionUri],
34 | });
35 |
36 | // Set the webview's initial html content
37 | this._panel.webview.html = this._getHtmlForWebview(this._panel.webview);
38 |
39 | //Listen for when the panel gets disposed
40 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
41 | this._panel.webview.onDidReceiveMessage(
42 | async (msg: any) => {
43 | switch (msg.type) {
44 | case 'onFile':
45 | if (msg.value){
46 | // connects us to parser file
47 | this.parser = new Parser(msg.value);
48 | this.parser.entryFileParse();
49 | this.updateView();
50 | }
51 | break;
52 | // For the implementation of a button on each individual node that will redirect the user to the source code
53 | // of the given component that is clicked on. For this to work, must post a message with the type being 'onViewFile'
54 | // so that this case gets hit.
55 | case 'onViewFile':
56 | if (!msg.value) {
57 | return;
58 | };
59 | const doc = await vscode.workspace.openTextDocument(msg.value);
60 | const editor = await vscode.window.showTextDocument(doc, {
61 | preserveFocus: false,
62 | preview: false
63 | });
64 | break;
65 | }
66 | },
67 | null,
68 | this._disposables
69 | );
70 | }
71 |
72 | // Whenever a new file is uploaded, it is parsed. We retrieve the tree and post it to the front-end
73 | private async updateView() {
74 | const tree = this.parser!.getTree();
75 | this._context.workspaceState.update('VisiVueTree', tree);
76 | this._panel.webview.postMessage({
77 | type: 'parsed-data',
78 | value: tree,
79 | settings: await vscode.workspace.getConfiguration('VisiVueTree')
80 | });
81 | }
82 |
83 | // Method to clear disposables array and 'clean' the data
84 | public dispose() {
85 | Panel.currentPanel = undefined;
86 | this._panel.dispose();
87 | while (this._disposables.length) {
88 | const node = this._disposables.pop();
89 | if (node) {
90 | node.dispose();
91 | }
92 | }
93 | }
94 | private _getHtmlForWebview(webview: vscode.Webview) {
95 | // @ts-ignore
96 | const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'out', 'main.wv.js'));
97 | return `
98 |
99 |
100 |
101 |
102 |
103 | Panel Title
104 |
105 |
106 |
107 |
114 |
115 |
116 |
117 | `;
118 | }
119 | }
--------------------------------------------------------------------------------
/src/webview/components/elements.js:
--------------------------------------------------------------------------------
1 |
2 | export const tree = {
3 | children: [{
4 | children: [
5 | {
6 | children: [
7 | {
8 | children: [],
9 | count: 1,
10 | depth: 0,
11 | error: "",
12 | expanded: false,
13 | fileName: "GrandBaby One-A",
14 | filePath:
15 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
16 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
17 | importPath: "/",
18 | name: "GrandBaby One-A",
19 | parentList: [
20 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
21 | ],
22 | props: {
23 | oneWay: [],
24 | twoWay: [],
25 | },
26 | reactRouter: false,
27 | reduxConnect: false,
28 | thirdParty: false,
29 | // [[Prototype]]: Object,
30 | },
31 | {
32 | children: [
33 | {
34 | children: [],
35 | count: 1,
36 | depth: 0,
37 | error: "",
38 | expanded: false,
39 | fileName: "G-G-Baby 1",
40 | filePath:
41 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
42 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
43 | importPath: "/",
44 | name: "G-G-Baby 1",
45 | parentList: [
46 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
47 | ],
48 | props: {
49 | oneWay: ['p-3'],
50 | twoWay: [],
51 | },
52 | reactRouter: false,
53 | reduxConnect: false,
54 | thirdParty: false,
55 | // [[Prototype]]: Object,
56 | },
57 | ],
58 | count: 1,
59 | depth: 0,
60 | error: "",
61 | expanded: false,
62 | fileName: "GrandBaby One-B",
63 | filePath:
64 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
65 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
66 | importPath: "/",
67 | name: "GrandBaby One-B",
68 | parentList: [
69 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
70 | ],
71 | props: {
72 | oneWay: ['p-3'],
73 | twoWay: ['p-2'],
74 | },
75 | reactRouter: false,
76 | reduxConnect: false,
77 | thirdParty: false,
78 | // [[Prototype]]: Object,
79 | },
80 | ],
81 | count: 1,
82 | depth: 0,
83 | error: "",
84 | expanded: false,
85 | fileName: "Child One",
86 | filePath:
87 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
88 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
89 | importPath: "/",
90 | name: "Child One",
91 | parentList: [
92 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
93 | ],
94 | props: {
95 | oneWay: ['p-1'],
96 | twoWay: ['p-2'],
97 | },
98 | reactRouter: false,
99 | reduxConnect: false,
100 | thirdParty: false,
101 | // [[Prototype]]: Object,
102 | },
103 | {
104 | children: [{
105 | children: [],
106 | count: 1,
107 | depth: 0,
108 | error: "",
109 | expanded: false,
110 | fileName: "GrandBaby One-C",
111 | filePath:
112 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
113 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
114 | importPath: "/",
115 | name: "GrandBaby One-C",
116 | parentList: [
117 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
118 | ],
119 | props: {
120 | oneWay: [],
121 | twoWay: [],
122 | },
123 | reactRouter: false,
124 | reduxConnect: false,
125 | thirdParty: false,
126 | // [[Prototype]]: Object,
127 | },
128 | {
129 | children: [
130 | {
131 | children: [],
132 | count: 1,
133 | depth: 0,
134 | error: "",
135 | expanded: false,
136 | fileName: "G-G-Baby 2",
137 | filePath:
138 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
139 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
140 | importPath: "/",
141 | name: "G-G-Baby 2",
142 | parentList: [
143 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
144 | ],
145 | props: {
146 | oneWay: ['p-3'],
147 | twoWay: [],
148 | },
149 | reactRouter: false,
150 | reduxConnect: false,
151 | thirdParty: false,
152 | // [[Prototype]]: Object,
153 | },
154 | ],
155 | count: 1,
156 | depth: 0,
157 | error: "",
158 | expanded: false,
159 | fileName: "GrandBaby One-D",
160 | filePath:
161 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
162 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
163 | importPath: "/",
164 | name: "GrandBaby One-D",
165 | parentList: [
166 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
167 | ],
168 | props: {
169 | oneWay: ['p-3'],
170 | twoWay: ['p-2'],
171 | },
172 | reactRouter: false,
173 | reduxConnect: false,
174 | thirdParty: false,
175 | // [[Prototype]]: Object,
176 | },
177 | ],
178 | count: 1,
179 | depth: 0,
180 | error: "",
181 | expanded: false,
182 | fileName: "Child Two",
183 | filePath:
184 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
185 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
186 | importPath: "/",
187 | name: "Child Two",
188 | parentList: [
189 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
190 | ],
191 | props: {
192 | oneWay: [],
193 | twoWay: ['p-4'],
194 | },
195 | reactRouter: false,
196 | reduxConnect: false,
197 | thirdParty: false,
198 | // [[Prototype]]: Object,
199 | },
200 | ],
201 | count: 1,
202 | depth: 0,
203 | error: "",
204 | expanded: false,
205 | fileName: "Parent",
206 | filePath:
207 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
208 | id: "twJWgoTMrb7UkXQI8tC1iGPX9VpfK50I",
209 | importPath: "/",
210 | name: "Parent",
211 | parentList: [],
212 | props: {
213 | oneWay: [],
214 | twoWay: [],
215 | },
216 | reactRouter: false,
217 | reduxConnect: false,
218 | thirdParty: false,
219 | // [[Prototype]]: Object,
220 | }],
221 | count: 1,
222 | depth: 0,
223 | error: "",
224 | expanded: false,
225 | fileName: "Index.tsx",
226 | filePath:
227 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/components/Sidebar.tsx",
228 | id: "FA6sF8JbqMloijRT6QOxHh3i5p1v29bJ",
229 | importPath: "/",
230 | name: "Index.tsx",
231 | parentList: [
232 | "/Users/abehenderson/Documents/Codesmith/ReacTree/src/webview/index.tsx",
233 | ],
234 | props: {
235 | oneWay: [],
236 | twoWay: [],
237 | },
238 | reactRouter: false,
239 | reduxConnect: false,
240 | thirdParty: false,
241 | // [[Prototype]]: Object,
242 | };
243 |
244 |
245 |
--------------------------------------------------------------------------------
/src/parser.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { Tree } from './types/Tree';
3 | import { readFileSync } from 'fs';
4 | import * as vueCompiler from '@vue/compiler-sfc';
5 | import { parse, transform } from '@vue/compiler-dom';
6 |
7 | export class Parser {
8 | entryFile: string;
9 | tree: Tree | undefined;
10 | constructor(filePath: string) {
11 | // default for mac users
12 | this.entryFile = filePath;
13 | // conditionals checking if OS is windows
14 | // check to see if it is a windows platform
15 | if (process.platform === 'linux' && this.entryFile.includes('wsl$')){
16 | // string manipulation to make sure the entryFile matches what we need for when we reference it in the
17 | // root definition in the parse method
18 | this.entryFile = path.resolve(filePath.split(path.win32.sep).join(path.posix.sep));
19 | this.entryFile = '/' + this.entryFile.split('/').slice(3).join('/');
20 | } else if (process.platform === 'linux' && /[a-zA-Z]/.test(this.entryFile[0])) {
21 | const root = `/mnt/${this.entryFile[0].toLowerCase()}`;
22 | this.entryFile = path.join(root, filePath.split(path.win32.sep).slice(1).join(path.posix.sep));
23 | }
24 | // cleaning this.tree and re-setting it to undefined
25 | this.tree = undefined;
26 | }
27 | // This is called in panel.ts to initiate the building of the AST. parser() is then called to initiate iteration over
28 | // the user's component hierarchy to build out the AST that we will eventually send to App.vue where they will modify
29 | // such that the tree is compatible with Vue Flow.
30 | public entryFileParse() {
31 | const root = {
32 | id: '1',
33 | name: path.basename(this.entryFile).replace(/\.vue?$/, ''),
34 | fileName: path.basename(this.entryFile),
35 | filePath: this.entryFile,
36 | fileDirname: path.dirname(this.entryFile),
37 | importPath: '/',
38 | parentList: [],
39 | children: [],
40 | props: {
41 | oneWay: [],
42 | twoWay: []
43 | },
44 | error: ''
45 | };
46 | this.tree = root;
47 | // store AST that parser function creates one large AST (One object with nested children) to send to Vue Flow
48 | this.parser(this.tree);
49 | return this.tree;
50 | }
51 |
52 | // DON'T FORGET TO CHANGE TYPES LATER AFTER TESTING IS DONE
53 | private parser(root: Tree): void {
54 | let queue: (Tree | string)[] = [root];
55 | let id = root.id;
56 | // iterate through tree
57 | while(queue.length !== 0) {
58 | let curr: any = queue.shift();
59 | // console.log(curr.name)
60 | // A check to see if the current node shifted out of the queue is one we want to iterate over or not.
61 | if (curr === 'dead') {continue;}
62 | let sourceCode: string = readFileSync(path.resolve(curr.filePath)).toString();
63 | // getChildren() is a helper function. Check below in the code for more details.
64 | const arrOfChildren = this.getChildren(sourceCode, curr.fileName, id);
65 | // getImports() is a helper function. Check below in the code for more details.
66 | const imports = this.getImports(sourceCode, curr.fileName, id);
67 | // Iterate through array of child components and instantiate a new ChildNode object.
68 | // Because App.vue is expecting to receive a nested object,
69 | // we are building a single object with children components nested
70 | for (let i = 0; i < arrOfChildren.length; i++) {
71 | let goodToCreateNode = false;
72 | // extractVariabes is a helper function. Check below in the code for more details.
73 | const objOfVariables = this.extractVariables(sourceCode, arrOfChildren[i]);
74 | id = `${+id + 1}`;
75 | let filePath = curr.fileDirname;
76 | for (let j = 0; j < imports.length; j++) {
77 | if (imports[j].local === arrOfChildren[i]) {
78 | // Check to see if the source of the import for the given file is from a user created file or from Vue.
79 | if (imports[j].source[0] !== '@') {
80 | if (imports[j].source.includes('.vue')) {
81 | filePath += imports[j].source.slice(1);
82 | } else {
83 | filePath += imports[j].source.slice(1) + '.vue';
84 | }
85 | goodToCreateNode = true;
86 | break;
87 | }
88 | }
89 | }
90 |
91 | if (goodToCreateNode) {
92 | const newFileDirname = path.dirname(filePath);
93 | const childSourceCode = readFileSync(path.resolve(filePath)).toString();
94 | // Handles edge cases for components that have no script tag, but have template tags (icons, etc.)
95 | if (!childSourceCode.includes('script' || 'script setup')) {break;};
96 | const childNode = {
97 | id: id,
98 | name: arrOfChildren[i],
99 | fileName: `${arrOfChildren[i]}.vue`,
100 | filePath: filePath,
101 | fileDirname: newFileDirname,
102 | importPath: '/',
103 | parentList: [],
104 | children: [],
105 | props: {
106 | oneWay: [],
107 | twoWay: []
108 | },
109 | error: ''
110 | };
111 | // Access object that stores oneway and twoway data bound variables and push to proper arrays of the newly created node.
112 | objOfVariables.twoway.forEach(el => {
113 | childNode.props.twoWay.push(el);
114 | });
115 | objOfVariables.oneway.forEach(el => {
116 | childNode.props.oneWay.push(el);
117 | });
118 | // Here is where the tree is being built out by populating the 'children' array.
119 | curr.children.push(childNode);
120 | queue.push(childNode);
121 | } else {
122 | queue.push('dead');
123 | }
124 | }
125 | }
126 | };
127 |
128 | public getTree(): Tree{
129 | return this.tree!;
130 | }
131 |
132 | // Helper function to grab child elements. * Consider console logging vueCompiler.compileTemplate({source: sourceCode, filename, id}) to see what properties it has. *
133 | public getChildren(sourceCode: string, filename: string, id: string): any {
134 | const arrOfChildren = vueCompiler.compileTemplate({ source: sourceCode, filename, id }).ast.components;
135 | return arrOfChildren;
136 | }
137 | // Helper function to extract variables when iterating through the components.
138 | // 'parse' is imported from @vue/compiler-dom on line 5.
139 | public extractVariables(template: string, component: string): any {
140 | const variables = {
141 | oneway: [],
142 | twoway: []
143 | };
144 | const ast = parse(template);
145 | transform(ast, {
146 | nodeTransforms: [
147 | (node) => {
148 | // console.log('NODE: ', node)
149 |
150 | // Refer to vueCompilerTypes.txt at the root level to understand what each type refers to.
151 | // Dont need to iterate through props because there can only be one v-model tag per component
152 | if (node.hasOwnProperty('tag') && node['tag'] === component) {
153 | if (node.type === 1 && node.props.some((prop) => prop.type === 7 && prop.name === 'model')) {
154 | const twoWayDirective = node.props.find((prop) => prop.type === 7 && prop.name === 'model');
155 | try {
156 | if (twoWayDirective['arg'] !== undefined) {
157 | variables.twoway.push(twoWayDirective['arg'].content);
158 | } else {
159 | variables.twoway.push(twoWayDirective['exp'].content)
160 | }
161 | } catch(error){
162 | }
163 | }
164 | // iterate through props because there can be multiple one way props passed down into each component
165 | if (node.type === 1 && node.props.some((prop) => (prop.type === 7 || prop.type === 6 ) && prop.name !== 'model')){
166 | node.props.forEach(currnode => {
167 | if (currnode.type === 7 && currnode.name !== 'model') {
168 | if (currnode['arg'] !== undefined) {
169 | variables.oneway.push(currnode.arg['content']);
170 | } else {
171 | variables.oneway.push(currnode.exp['content']);
172 | }
173 | }
174 | });
175 | }
176 | }
177 | }
178 | ]
179 | });
180 | return variables;
181 | }
182 | // Call parse method from vueCompiler on current component. It will return an object of type SFCParseResult
183 | // Deconstruct result in 'descriptor'. 'descriptor' is of type SFCDescriptor, which is important because that is the only type that
184 | // vueCompiler.compileScript takes as its first argument.
185 | // Second argument is an options object with the only required property being 'id'.
186 | // This will return an object of type SFCScriptBlock that has an imports property.
187 | // Store the values array of the object and return it.
188 | // Here, the values will be objects that have a 'source' property that we will be able to grab the import path for later use.
189 | // (Will be used for dynamically creating the correct file path)
190 | public getImports(template: string, filename: string, id: string): any {
191 | if (!template.includes('script' || 'script setup')) {return [];}
192 | const { descriptor } = vueCompiler.parse(template, {filename}); // return object type SFCParseResult with descriptor property
193 | const { imports } = vueCompiler.compileScript(descriptor, {id}); // return object type SFCScriptBlock with imports property
194 | const result = Object.values(imports);
195 | return result;
196 | }
197 | }
198 |
199 |
--------------------------------------------------------------------------------
/src/webview/components/App.vue:
--------------------------------------------------------------------------------
1 |
229 |
230 |
231 |
236 |
237 |
245 |
246 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
379 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
VisiVue (Beta)
5 | As applications built in Vue JS scale in size and complexity, developers tasked with their maintenance and management often face challenges, particularly when dealing with unanticipated side-effects caused by two-way data binding. This is where VisiVue comes in. VisiVue is a VS Code extension that aims to fulfill our mission of providing Vue developers with deeper insight into their application's structure, data flow, and state management as it scales.
6 |
7 |
8 |
9 |
10 | With VisiVue, developers are able to:
11 | Visualize the component hierarchy of an application
12 | Keep track of stateful variables as they flow from parent to children
13 | Understand what variables are one-way data bound and two-way data bound
14 | Keep track of which components these stateful variables are drilled through
15 |
16 |
17 |
18 |
19 | Get Started
20 |
21 | 1. Head to the VS Code store and download VisiVue
22 |
23 |
24 |
25 |
26 | 2. Once installed, you may need to reload your VS Code, but next open up the command pallette by pressing "cmd + shift + p" on MacOS or "ctrl + shift + p" and click the command "Show VisiVue Panel".
27 |
28 |
29 |
30 |
31 | 3. Once you click the command in the command pallette, it will open up VisiVue! All you have to do is import the Vue file you want to visualize and you are set!
32 |
33 |
34 |
35 |
36 | Open Source
37 |
38 | How to Contribute
39 |
40 | ---------------------
41 | We are looking for developers who believe in VisiVue and want to take it to the next level! Here are some features that we are currently working on implementing. Feel free to fork this repo and take a look at the source code!
42 |
43 |
44 |
45 | Before submitting a pull request, please open an issue on our 'Issues' section.
46 |
47 |
48 |
49 | | Further Features to Implement | Status |
50 | | ------- | ------ |
51 | | Vue Router Compatiblity | ⏳ |
52 | | Pinia Compatibility | ⏳ |
53 | | Testing Suites | ⏳ |
54 | | Make the tree centered and zoomed properly on mount | ⏳ |
55 | | Icon on each node that pulls up source code for that specific node | ⏳ |
56 | | Compatibility with Vue applications that utilize provide/inject functions | ⏳ |
57 |
58 |
59 |
60 | How to Run in Development Mode
61 |
62 | ---------------------
63 |
64 | 1. Clone the dev repo.
65 | ```
66 | git clone https://github.com/oslabs-beta/VisiVue.git
67 | ```
68 | 2. Go into the VisiVue directory on your local machine and install dependencies
69 | ```
70 | npm install
71 | ```
72 | 3. Once you have successfully installed dependencies, to open the developer environment for the VS Code extension, press 'ctrl + f5' if you are on Windows, or 'fn + f5' if you are on MacOS. This will open a new VS Code text editor and allow you to see a development version of the extension.
73 |
74 | - Note that may be difficult to get console logs when working on the Vue files specifically, so use the browser version by first checking out to the dev branch on your local machine:
75 | ```
76 | git checkout dev
77 | ```
78 | -Then navigate into the 'visivue-browser' directory:
79 | ```
80 | cd visivue-browser
81 | ```
82 | -And then run:
83 | ```
84 | npm run dev
85 | ```
86 | -there we have a browser version of our extension set up with nodemon
87 |
88 | 4. Once the VS Code development version is open, open the command pallete by pressing 'cmd + shift + p' and look for the command 'Show VisiVue Panel' and now you can see the extension!
89 |
90 | - If you are working in the VS Code developer environment, make sure to press 'cmd + r' to refresh the extension so that it compiles and runs your most recent changes.
91 |
92 |
93 | Here are some links to documentation that you may find useful:
94 |
95 | * Vue.js
96 | * Vue Flow
97 | * VS Code Extension API
98 | * Webview API
99 | * Developer's Guide to Building VSCode Webview Panel with React and Messages by Michael Mike Benliyan
100 | * TypeScript
101 |
102 |
103 | Known Bugs
104 |
105 | If you select a file and a tree is rendered: subsequently selecting another file should clear the current tree and render a new tree for that new file. However, what happens is that the newly rendered tree will not have nodes, but will still persist the edges. What we suspect is happening is that Vue Flow's internal processing of our data is causing this issue. We are currently working on a fix.
106 |
107 |
108 |
109 |
110 |
111 | Change Log
112 |
113 | Tech Stack
114 |
115 | 
116 | 
117 | 
118 | 
119 | 
120 | 
121 | 
122 | 
123 |
124 |
125 |
126 |
127 | VisiVue Contributors
128 | Accelerated by OS Labs and inspired by ReacTree
129 |
130 |
131 |
132 | | Name |
133 | GitHub |
134 | LinkedIn |
135 |
136 |
137 |
138 |
139 | | Abe Henderson |
140 |  |
141 |  |
142 |
143 |
144 | | Christopher Park |
145 |  |
146 |  |
147 |
148 | Kasey Nguyen |
149 |  |
150 |  |
151 |
152 |
153 | | Ulf Wong |
154 |  |
155 |  |
156 |
157 |
158 | | Yosuke Tomita |
159 |  |
160 |  |
161 |
162 |
163 |
164 |
165 |
166 | ---
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------