diff --git a/.eslintrc.json b/.eslintrc.json index d25565b..a4f4c43 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,7 +18,7 @@ "no-this-before-super": "warn", "no-undef": "warn", "no-unreachable": "warn", - "no-unused-vars": "warn", + "no-unused-vars": "off", "constructor-super": "warn", "valid-typeof": "warn" } diff --git a/package-lock.json b/package-lock.json index 5b2c16b..ca3fbe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -455,9 +455,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "bin": { "parser": "bin/babel-parser.js" }, diff --git a/package.json b/package.json index d88ca4d..cb88741 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "scripts": { "lint": "eslint .", "pretest": "npm run lint", - "test": "node ./build/src/test/runTest.js", + "test": "npx tsc ; node ./build/src/test/runTest.js", "dev": "webpack --watch", "webpack": "webpack" }, diff --git a/src/extension.ts b/src/extension.ts index 5788f4a..8b01c00 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,7 +4,7 @@ import { Parser } from './parser'; import { Tree } from './types/tree'; let tree: Parser | undefined = undefined; -let panel: vscode.WebviewPanel | undefined = undefined +let panel: vscode.WebviewPanel | undefined = undefined; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -14,37 +14,31 @@ function activate(context: vscode.ExtensionContext) { let columnToShowIn : vscode.ViewColumn | undefined = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; - // Command that allows for User to select the root file of their React application. const pickFile: vscode.Disposable = vscode.commands.registerCommand('myExtension.pickFile', async () => { - // Check if there is an existing webview panel, if so display it. - if(panel) { - panel.reveal(columnToShowIn) + if (panel) { + panel.reveal(columnToShowIn); } - // Opens window for the User to select the root file of React application const fileArray: vscode.Uri[] = await vscode.window.showOpenDialog({ canSelectFolders: false, canSelectFiles: true, canSelectMany: false }); - // Throw error message if no file was selected if (!fileArray || fileArray.length === 0) { vscode.window.showErrorMessage('No file selected'); return; } - // Create Tree to be inserted into returned HTML tree = new Parser(fileArray[0].path); tree.parse(); const data: Tree = tree.getTree(); - // Check if panel currently has a webview, if it does dispose of it and create another with updated root file selected. // Otherwise create a new webview to display root file selected. - if(!panel) { + if (!panel) { panel = createPanel(context, data, columnToShowIn); } else { panel.dispose() @@ -54,23 +48,19 @@ function activate(context: vscode.ExtensionContext) { // Listens for when webview is closed and disposes of webview resources panel.onDidDispose( () => { - console.log("Before: ", panel) - panel.dispose() + panel.dispose(); panel = undefined; columnToShowIn = undefined; - console.log("After: ", panel) }, null, context.subscriptions - ); + ); }); - // Command to show panel if it is hidden const showPanel: vscode.Disposable = vscode.commands.registerCommand('myExtension.showPanel', () => { - panel.reveal(columnToShowIn) - }); - + panel.reveal(columnToShowIn); + }); context.subscriptions.push(pickFile, showPanel); } diff --git a/src/panel.ts b/src/panel.ts index 8efa72b..8ceda59 100644 --- a/src/panel.ts +++ b/src/panel.ts @@ -1,16 +1,15 @@ import * as vscode from 'vscode'; -import { getNonce } from './getNonce'; +import { getNonce } from './utils/getNonce'; import { Tree } from './types/tree'; -let panel: vscode.WebviewPanel | undefined = undefined +let panel: vscode.WebviewPanel | undefined = undefined; export function createPanel(context: vscode.ExtensionContext, data: Tree, columnToShowIn: vscode.ViewColumn) { - - // utilize method on vscode.window object to create webview + // Utilize method on vscode.window object to create webview panel = vscode.window.createWebviewPanel( 'reactLabyrinth', 'React Labyrinth', - // create one new tab + // Create one tab vscode.ViewColumn.One, { enableScripts: true, @@ -24,10 +23,10 @@ export function createPanel(context: vscode.ExtensionContext, data: Tree, column // Set URI to be the path to bundle const bundlePath: vscode.Uri = vscode.Uri.joinPath(context.extensionUri, 'build', 'bundle.js'); - // set webview URI to pass into html script + // Set webview URI to pass into html script const bundleURI: vscode.Uri = panel.webview.asWebviewUri(bundlePath); - // render html of webview here + // Render html of webview here panel.webview.html = createWebviewHTML(bundleURI, data); // Sends data to Flow.tsx to be displayed after parsed data is received @@ -37,7 +36,6 @@ export function createPanel(context: vscode.ExtensionContext, data: Tree, column case 'onData': if (!msg.value) break; context.workspaceState.update('reactLabyrinth', msg.value); - panel.webview.postMessage( { type: 'parsed-data', @@ -54,10 +52,10 @@ export function createPanel(context: vscode.ExtensionContext, data: Tree, column return panel }; -// getNonce generates a new random string each time ext is used to prevent external injection of foreign code into the html +// getNonce generates a new random string to prevent external injection of foreign code into the HTML const nonce: string = getNonce(); -// function to create the HTML page for webview +// Creates the HTML page for webview function createWebviewHTML(URI: vscode.Uri, initialData: Tree) : string { return ( ` @@ -83,5 +81,5 @@ function createWebviewHTML(URI: vscode.Uri, initialData: Tree) : string { ` - ) + ); } diff --git a/src/parser.ts b/src/parser.ts index eb0bc32..197dc22 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,33 +1,11 @@ import * as fs from 'fs'; import * as path from 'path'; import * as babel from '@babel/parser'; -import { getNonce } from './getNonce'; +import { getNonce } from './utils/getNonce'; import { ImportObj } from './types/ImportObj'; import { Tree } from "./types/tree"; import { File } from '@babel/types'; -// // function to determine server or client component (can look for 'use client' and 'hooks') -// // input: ast node (object) -// // output: boolean -// checkForClientString(node) { -// if (node.type === 'Directive') { -// console.log('node', node); -// // access the value property of the Directive node -// console.log('Directive Value:', node.value); -// // check if the node.value is a 'DirectiveLiteral' node -// if (node.value && node.value.type === 'DirectiveLiteral') { -// // check the value to see if it is 'use client' -// if (typeof node.value.value === 'string' && node.value.value.trim() === 'use client') { -// // access the value property of the 'DirectiveLiteral' node -// console.log('DirectiveLiteral Value:', node.value.value); -// // might need to do something else here to make it known as client type -// return true; -// } -// } -// } -// return false; -// } - export class Parser { entryFile: string; tree: Tree | undefined; @@ -86,7 +64,7 @@ export class Parser { return this.tree; } - // Set Sapling Parser with a specific Data Tree (from workspace state) + // Set entryFile property with the result of Parser (from workspace state) public setTree(tree: Tree) { this.entryFile = tree.filePath; this.tree = tree; @@ -131,7 +109,7 @@ export class Parser { return this.tree!; } - // Traverses the tree and changes expanded property of node whose id matches provided id + // Traverses the tree and changes expanded property of node whose ID matches provided ID public toggleNode(id: string, expanded: boolean): Tree{ const callback = (node: { id: string; expanded: boolean }) => { if (node.id === id) { @@ -204,14 +182,13 @@ export class Parser { // Find imports in the current file, then find child components in the current file const imports = this.getImports(ast.program.body); - if (this.getCallee(ast.program.body)) { + // Set value of isClientComponent property + if (this.getComponentType(ast.program.directives, ast.program.body)) { componentTree.isClientComponent = true; } else { componentTree.isClientComponent = false; } - // console.log('componentTree.isClientComponent', componentTree.isClientComponent); - // console.log('--------------------------------') // Get any JSX Children of current file: if (ast.tokens) { componentTree.children = this.getJSXChildren( @@ -247,10 +224,8 @@ export class Parser { } // Extracts Imports from current file - // const Page1 = lazy(() => import('./page1')); -> is parsed as 'ImportDeclaration' - // import Page2 from './page2'; -> is parsed as 'VariableDeclaration' - // input: array of objects: ast.program.body - // output: object of imoprts + // const App1 = lazy(() => import('./App1')); => is parsed as 'ImportDeclaration' + // import App2 from './App2'; => is parsed as 'VariableDeclaration' private getImports(body: { [key: string]: any }[]): ImportObj { const bodyImports = body.filter((item) => item.type === 'ImportDeclaration' || 'VariableDeclaration'); @@ -278,7 +253,7 @@ export class Parser { } private findVarDecImports(ast: { [key: string]: any }): string | boolean { - // find import path in variable declaration and return it, + // Find import path in variable declaration and return it, if (ast.hasOwnProperty('callee') && ast.callee.type === 'Import') { return ast.arguments[0].value; } @@ -294,20 +269,32 @@ export class Parser { return false; } - // helper function to determine component type (client) - // input: ast.program.body - // output: boolean - private getCallee(body: { [key: string]: any }[]): boolean { - const defaultErr = (err,) => { + // Determines server or client component type (looks for use of 'use client' and react/redux state hooks) + private getComponentType(directive: { [key: string]: any }[], body: { [key: string]: any }[]): boolean { + const defaultErr = (err) => { return { method: 'Error in getCallee method of Parser:', log: err, } }; + console.log('directive: ', directive); + // Initial check for use of directives (ex: 'use client', 'use server', 'use strict') + // Accounts for more than one directive + for (let i = 0; i < directive.length; i++) { + if (directive[i].type === 'Directive') { + if (typeof directive[i].value.value === 'string' && directive[i].value.value.trim() === 'use client') { + return true; + } + } + break; + } + + // Second check for use of React/Redux hooks const bodyCallee = body.filter((item) => item.type === 'VariableDeclaration'); if (bodyCallee.length === 0) return false; + // Helper function const calleeHelper = (item) => { const hooksObj = { useState: 0, @@ -420,7 +407,7 @@ export class Parser { childNodes, ); - // Case for finding components passed in as props e.g. + // Case for finding components passed in as props e.g. } else if ( astTokens[i].type.label === 'jsxName' && (astTokens[i].value === 'Component' || @@ -473,7 +460,6 @@ export class Parser { props: props, children: [], parent: parent.id, - // consider adding the id to the parentList array somehow for D3 integration... parentList: [parent.filePath].concat(parent.parentList), error: '', isClientComponent: false @@ -501,7 +487,7 @@ export class Parser { // Checks if current Node is connected to React-Redux Store private checkForRedux(astTokens: any[], importsObj: ImportObj): boolean { - // Check that react-redux is imported in this file (and we have a connect method or otherwise) + // Check that React-Redux is imported in this file (and we have a connect method or otherwise) let reduxImported = false; let connectAlias; Object.keys(importsObj).forEach((key) => { diff --git a/src/test/runTest.ts b/src/test/runTest.ts index e7c671b..6e7ebf1 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -4,18 +4,15 @@ import { runTests } from '@vscode/test-electron'; async function main() { console.log('made it through the line before try block'); try { - console.log('inside try block'); // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` const extensionDevelopmentPath = path.resolve(__dirname, '../../'); - console.log('inside try block after first var declare'); - // The path to the extension test script // Passed to --extensionTestsPath const extensionTestsPath = path.resolve(__dirname, './suite/index'); - console.log('inside try block after second var declare'); + console.log('inside try block after var declarations'); // Download VS Code, unzip it and run the integration test await runTests({ diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 12bdaa0..170da5a 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -6,9 +6,6 @@ import * as vscode from 'vscode' // we can either use test() or it() -- matter of style for team/project convention describe('Extension Test Suite', () => { - // beforeEach(() => { - // vscode.window.showInformationMessage('Start all tests.'); - // }); it('Sample test', () => { expect([1, 2, 3].indexOf(5)).toBe(-1); diff --git a/src/test/suite/parser.test.ts b/src/test/suite/parser.test.ts index fa6019e..81fee91 100644 --- a/src/test/suite/parser.test.ts +++ b/src/test/suite/parser.test.ts @@ -1,4 +1,3 @@ -// import * as assert from 'assert' -- this one is from node import { Parser } from '../../parser'; import * as path from 'path'; import { beforeEach, expect, test } from '@jest/globals'; @@ -9,53 +8,47 @@ import * as vscode from 'vscode' // const myExtension = require('../extension'); describe('Parser Test Suite', () => { - // beforeEach(() => { - // vscode.window.showInformationMessage('Start all tests.'); - // }); - let parser, tree, file; // UNPARSED TREE TEST describe('It initializes correctly', () => { beforeEach(() => { - // declare var and assign it to a test file and make new instance of Parser - // both of the paths below work - // file = path.join(__dirname, '../test_cases/tc_0/index.js'); - file = path.join(__dirname, '../../../src/test/test_apps/test_0/index.js'); + // Assign the test file and make new instance of Parser + file = path.join(__dirname, '../test_cases/tc_0/index.js'); + // file = path.join(__dirname, '../../../src/test/test_apps/test_0/index.js'); parser = new Parser(file); }); test('It instantiates an object for the parser class', () => { expect((parser)).toBeInstanceOf(Parser); - // assert.typeOf(parser, 'object', 'Value of new instance should be an object'); - // expect(parser).to.be.an('object'); }); test('It begins with a suitable entry file and a tree that is not yet defined', () => { expect(parser.entryFile).toEqual(file); expect(tree).toBeUndefined(); - // below is my code - // assert.strictEqual(parser.entryFile, file, 'These files are strictly equal'); - // assert.isUndefined(tree, 'Tree is defined'); }); }); - // TEST ?: UNPARSED TREE TEST FOR REACT 18(createRoot) - // TEST 0: ONE CHILD - // describe('It works for simple apps', () => { - // before(() => { - // file = path.join(__dirname, ''); - // parser = new Parser(file); - // tree = parser.parse(); - // }); + describe('It works for simple apps', () => { + beforeEach(() => { + file = path.join(__dirname, ''); + parser = new Parser(file); + tree = parser.parse(); + }); - // test('It returns an defined object tree when parsed', () => { - // assert.typeOf(tree, 'object', 'Value of parse() on new instance should be an object'); - // }); - // }); + test('It returns an defined object tree when parsed', () => { + expect(tree).toBeDefined(); + //expect(tree).toMatchObject() + }); + + // test('Parsed tree has a property called name with value index and one child with name App', () => { + + // }); + }); + + // these are the 14 tests we need to test for - // TEST 0.5: CHECK IF COMPONENT IS CLIENT OR SERVER (USING HOOKS) => RENDERS A CERTAIN COLOR // TEST 1: NESTED CHILDREN // TEST 2: THIRD PARTY, REACT ROUTER, DESTRUCTURED IMPORTS // TEST 3: IDENTIFIES REDUX STORE CONNECTION @@ -67,6 +60,9 @@ describe('Parser Test Suite', () => { // TEST 9: FINDING DIFFERENT PROPS ACROSS TWO OR MORE IDENTICAL COMPONENTS // TEST 10: CHECK CHILDREN WORKS AND COMPONENTS WORK // TEST 11: PARSER DOESN'T BREAK UPON RECURSIVE COMPONENTS - // TEST 12: NEXT.JS APPS (pages & app router) - // TEST 13: Variable Declaration Imports and React.lazy Imports -}); \ No newline at end of file + // TEST 12: NEXT.JS APPS (pages version & app router version) + // TEST 13: Variable Declaration Imports and React.lazy Imports + // TEST 14: CHECK IF COMPONENT IS CLIENT OR SERVER (USING HOOKS & DIRECTIVES) => BOOLEAN (priority) + + // LOU is doing EXTENSION TEST in extension.test.ts +}); diff --git a/src/test/test_cases/tc_0/component/App.jsx b/src/test/test_cases/tc_0/component/App.jsx index c8ec938..3608737 100644 --- a/src/test/test_cases/tc_0/component/App.jsx +++ b/src/test/test_cases/tc_0/component/App.jsx @@ -1,6 +1,5 @@ -// import React from "react"; export default function App() { return (
This is the App.
) -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/test/test_cases/tc_0/index.js b/src/test/test_cases/tc_0/index.js index 05603bc..d5da134 100644 --- a/src/test/test_cases/tc_0/index.js +++ b/src/test/test_cases/tc_0/index.js @@ -1,11 +1,4 @@ -// what we are building to test for -// we will point to index.js and call new instance of parser passing in file - -// expecting: -// if the value of new instance is an obj -// tree is undefined with a proper file - -// test case 0 - simple react app with one app component +// Test Case 0 - Simple react app with one app component import React from 'react'; import { createRoot } from 'react-dom/client'; diff --git a/src/test/vscode-environment.js b/src/test/vscode-environment.js index 6e29bdc..21bab9d 100644 --- a/src/test/vscode-environment.js +++ b/src/test/vscode-environment.js @@ -1,6 +1,7 @@ const { TestEnvironment } = require('jest-environment-node'); const vscode = require('vscode'); +// Allows for VSCode Envionrment to be extended to Jest Environment class VsCodeEnvironment extends TestEnvironment { async setup() { await super.setup(); diff --git a/src/test/vscode.js b/src/test/vscode.js index 67e8312..e3ae8ad 100644 --- a/src/test/vscode.js +++ b/src/test/vscode.js @@ -1 +1,2 @@ +// Allows access to vscode object in testing module.exports = global.vscode; \ No newline at end of file diff --git a/src/types/hierarchyData.ts b/src/types/hierarchyData.ts index c5161e4..0554e44 100644 --- a/src/types/hierarchyData.ts +++ b/src/types/hierarchyData.ts @@ -1,5 +1,3 @@ -import { Tree } from "./tree" - export interface hierarchyData { id: string, position: { x: number, y: number }, diff --git a/src/getNonce.ts b/src/utils/getNonce.ts similarity index 98% rename from src/getNonce.ts rename to src/utils/getNonce.ts index dafd778..5cba19f 100644 --- a/src/getNonce.ts +++ b/src/utils/getNonce.ts @@ -6,5 +6,5 @@ export function getNonce() { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; - } +}; diff --git a/src/webview/Flow.tsx b/src/webview/Flow.tsx index d3c9c5b..4b32e8e 100644 --- a/src/webview/Flow.tsx +++ b/src/webview/Flow.tsx @@ -16,6 +16,7 @@ import "./style.css"; const OverviewFlow = () => { + // Required to have different initial states to render through D3 const initialNodes: Node[] = []; const initialEdges: Edge[] = []; @@ -24,20 +25,19 @@ const OverviewFlow = () => { useEffect(() => { window.addEventListener('message', (e: MessageEvent) => { - // Object containing type prop and value prop const msg: MessageEvent = e; - const flowBuilder = new FlowBuilder + const flowBuilder = new FlowBuilder; switch (msg.data.type) { case 'parsed-data': { let data: Tree | undefined = msg.data.value; // Creates our Tree structure - flowBuilder.mappedData(data, initialNodes, initialEdges) + flowBuilder.mappedData(data, initialNodes, initialEdges); setEdges(initialEdges); - setNodes(initialNodes) + setNodes(initialNodes); break; } } diff --git a/src/webview/flowBuilder.tsx b/src/webview/flowBuilder.tsx index 3a08ac9..cac62cc 100644 --- a/src/webview/flowBuilder.tsx +++ b/src/webview/flowBuilder.tsx @@ -1,7 +1,7 @@ import { ConnectionLineType, Edge, Node } from 'reactflow'; import { Tree } from '../types/tree'; -import { getNonce } from '../getNonce'; -import * as d3 from 'd3' +import { getNonce } from '../utils/getNonce'; +import * as d3 from 'd3'; // Contructs our family tree for React application root file that was selected @@ -10,19 +10,17 @@ class FlowBuilder { public mappedData(data: Tree, nodes: Node[], edges: Edge[]): void { // Create a holder for the heirarchical data (msg.value), data comes in an object of all the Trees - const root: d3.HierarchyNode = d3.hierarchy(data) + const root: d3.HierarchyNode = d3.hierarchy(data); // Dynamically adjust height and width of display depending on the amount of nodes const totalNodes: number = root.descendants().length; const width: number = Math.max(totalNodes * 100, 800); const height = Math.max(totalNodes * 20, 500) - - //create tree layout and give nodes their positions and + // Create tree layout and give nodes their positions and const treeLayout: d3.TreeLayout = d3.tree() .size([width, height]) - .separation((a: d3.HierarchyPointNode, b: d3.HierarchyPointNode) => (a.parent == b.parent ? 2 : 2.5)) - + .separation((a: d3.HierarchyPointNode, b: d3.HierarchyPointNode) => (a.parent == b.parent ? 2 : 2.5)); treeLayout(root); // Iterate through each Tree and create a node @@ -55,7 +53,6 @@ class FlowBuilder { animated: true, }; - // Check if the edge already exists before adding const edgeExists: boolean = edges.some( edge => edge.source === newEdge.source && edge.target === newEdge.target @@ -63,11 +60,10 @@ class FlowBuilder { // If edge does not exist, add to our edges array if (!edgeExists) { - edges.push(newEdge) + edges.push(newEdge); } } - } - ) + }); } } diff --git a/src/webview/style.css b/src/webview/style.css index 4a3e57c..f35918d 100644 --- a/src/webview/style.css +++ b/src/webview/style.css @@ -15,7 +15,6 @@ body, text-align: center; } - .react-flow__handle.connectionindicator { pointer-events: none !important; } \ No newline at end of file