├── .DS_Store ├── .gitignore ├── .prettierignore ├── README.md ├── build ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png └── notarize.js ├── dev-app-update.yml ├── electron.vite.config.1692742985063.mjs ├── electron.vite.config.ts ├── forge.config.cjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── resources └── icon.png ├── server ├── controllers │ ├── componentController.js │ ├── fsController.js │ └── serverASTController.js └── server.ts ├── src ├── .DS_Store ├── main │ ├── controllers │ │ ├── componentController.js │ │ ├── fsController.js │ │ └── serverASTController.js │ └── index.ts ├── preload │ ├── index.d.ts │ └── index.ts └── renderer │ ├── .DS_Store │ ├── index.html │ └── src │ ├── .DS_Store │ ├── App.tsx │ ├── assets │ ├── .DS_Store │ ├── icons.svg │ ├── images │ │ ├── .DS_Store │ │ ├── ReactRelay-logos │ │ │ ├── ReactRelay-logos_black.png │ │ │ ├── ReactRelay-logos_transparent.png │ │ │ ├── ReactRelay-logos_white.png │ │ │ └── logo_info.txt │ │ ├── ReactRelay.svg │ │ ├── directory-black.svg │ │ ├── directory-white.svg │ │ ├── left-right-solid.svg │ │ └── up-down-solid.svg │ ├── index.css │ └── prism.css │ ├── components │ ├── ComponentCode.tsx │ ├── Details.tsx │ ├── Header.tsx │ ├── MethodButton.tsx │ ├── ModelPreview.tsx │ ├── ProjectPathModal.tsx │ ├── Tree.tsx │ ├── Versions.tsx │ └── custom-nodes │ │ ├── custom-node.tsx │ │ └── custom-node2.tsx │ ├── containers │ └── MethodButtonContainer.tsx │ ├── env.d.ts │ ├── features │ ├── detailSlice.js │ ├── projectSlice.js │ └── searchSlice.js │ ├── global.d.ts │ ├── main.tsx │ └── redux │ └── store.js ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json └── vite.config.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | out 4 | .DS_Storedist/ 5 | dist -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactRelay 2 | 3 | [ReactRelay](https://reactrelay.com) codebase visualization tool with React using Redux for state-management, TypeScript, built on top of [Electron-Vite](https://electron-vite.org/), with data visualization being built off of [React Flow](https://reactflow.dev/) and using [Tailwind](https://tailwindcss.com/) for our styling. This app was designed to create a representation of a project folder's component and file connections, giving a code-preview of individual components, highlighting components wich contain AJAX calls, and any accompanying database schemas. Users are also able to quickly find specific components inside of their project. The application is designed to be used as a tool for developers to visualize the structure of their projects and to help them understand the flow of data through their applications, navigate to specific components, and improve the onboarding experience for both senior and junior devs being introduced to a project. 4 | 5 | ReactRelay is a codebase visualization tool for React-based Javascript projects that allows users to visualize the frontend of a project as a tree of linked nodes (components), and see the AJAX requests associated with each component, as well as their corresponding database schemas. A search feature is available to rapidly identify specific components within a large codebase, and a code-preview tab exists for each component that shows the component's code, formatted exactly as it is in the file. 6 | 7 | 8 | 9 | ## Recommended IDE Setup 10 | 11 | - [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 12 | 13 | ## Install 14 | 15 | ```bash 16 | $ npm install 17 | ``` 18 | # Project Setup 19 | 20 | 21 | ## Development 22 | 23 | For development with hot reloading, run `npm run dev` in the root directory. This will start the electron-vite server and the electron app. The electron app will automatically reload when changes are made. 24 | 25 | ## Production 26 | 27 | In order to create a package for your current operating system, run "npm run package" in the root directory. This will create a distributable for the current operating system. 28 | For production, run `npm run build` in the root directory. This will build the vite server and the electron app. The electron app will then be located in the dist folder. To run the the production build, type `npm start` in the root directory. This will start the electron app from your built project. 29 | 30 | ## Usage 31 | 32 | ### Selecting Your Project 33 | ![Project Selection](https://github.com/oslabs-beta/ReactRelay/assets/103789011/7e429c46-c6b7-4419-b74c-f6bf71375323) 34 | 35 | When opening the application, you will start by opening the file-explorer folder in the top-right corner. You will be shown an option to select two fileds; the component directory an the server directory. Clicking on the button for selecting components will open a file explorer. From there you can select either the src folder of your react project or you can select the components folder, depending on how you've structured your project. With the second button, you will open a file explorer where you can select your project's server folder. Depending on how your project is structured you can either select **only** your **src** folder and pressing continue or to select both your server and your components folder. When you continue, you will be presented with a hierarchical component tree of your project. 36 | 37 | ### Navigating the Tree 38 | 39 | From here, you will see the components of your project laid out with animated lines connecting parent and children components to one another. Components which contain AJAX requests are shown in dark blue whereas other components will be shown in white. By clicking on one of these components, the lines connecting your selected components will be shwon in red and they will allow you to more easily see the immediate relationships of the component you've chosen. From there, you will see a tab appear on the bottom which shows the AJAX routes of the component, if present, as well as a preview for the database schema it's connected, if present. Clicking on the yellow button labelled "code" in the tab will show you a code preview of the component you have selected. 40 | ![Tree Navigation](https://github.com/oslabs-beta/ReactRelay/assets/103789011/fe231a5d-8e2a-4d5e-9f75-9ab625146089) 41 | 42 | ### Other Controls 43 | 44 | Now that you know how to navigate the tree, there are some other components which will help you to explore the tree. The controls in the top right allow you to zoom in and out of the tree, to lock the components so you can no longer move them to avoid changing their layout by accident, as well as a button which centers your viewport on your code. In the bottom-right corner there is a minimap which shows a layout of your tree which you are able to click on to jump to a section of your tree and the ability zoom in on with the mouse wheel to control your window's zoom level while your cursor is over the minimap. In the top-left corner of your window are two buttons which control the layout of tree, whether it is organized vertically or horizontally. In the top right, there is a magnifying glass which, when clicked, shows an input field which allows you to type in the name of a component you're looking for, which will be highlighted in red, allowing you to more quickly locate it inside of the tree. 45 | ![Layout and Search](https://github.com/oslabs-beta/ReactRelay/assets/103789011/c2447079-169f-4afe-9dd8-cfd5c39f8723) 46 | ![Minimap and Controls)](https://github.com/oslabs-beta/ReactRelay/assets/103789011/ce172e6b-d0d7-4e0a-87d7-58f0a499c22b) 47 | 48 | ## Distribution 49 | 50 | In order to create a distributable, run `npm run make` in the root directory. This will create a distributable for the all operating systems you have specified in your configuration. You can specify which operating systems you want to create a distributable for by including different makers inside the forge.config.cjs file. For more information on how to create a distributable for different operating systems, visit https://www.electronforge.io/. It is currently set up to create a distributable for MacOS, Windows, and Linux with .zip for Darwin, .exe for Windows, and .deb or .rpm file types for Linux machines. 51 | 52 | # Technical challenges and solutions 53 | ## Electron and Electron-Vite interacting with Express 54 | 55 | The electron app consists of three diffrent folders: The Main, which contains the window for the Electron application which will be displayed to the user, the Preload, which contains the preload script that will be run in the Electron application, and the Renderer, which contains the React application that will be displayed to the user. The Electron application is created in the main.js file in the main folder. The preload script is created in the preload.js file in the preload folder. You can read more about it here: https://www.electronjs.org . Electron-Vite is a chimera of Electron and the Vite bundler. It allows you to use the Vite bundler to bundle your React application and then use Electron to display it to the user. Whenever you are dealing with chimera, there is the possibility for things not working how you might expect because of the changes which have been made to have both applications working together. 56 | 57 | Electron uses its own version of a server in order to perform operations, so express servers aren't naturally compatible with electron applications and would not normally work, but we have found a way to make them work together so that the front and back ends of the application can communicate. Normally in the root server file in an express server you use the app.listen function to listen to a specified port. Instead, we are exporting that express instance created inside the server.ts fole and importing it into the main folder of the electron application and setting it to listen at a dynamically assigned port. This will allow us to use an express server and its middleware functionality inside of an electron application. 58 | 59 | ## React-Flow 60 | 61 | React-Flow is a data visualization library which allows for the creation of a graph of nodes and edges which we are using to display the relationships between components with each node representing an component inside of a react application and the lines between nodes representing the connections between files. React-Flow is a very helpful library to use, thought there aren't many well-documented examples of its use. React-Flow allows for the creation of custom nodes and node edges and we used this to create custom nodes whos edges reflect the connections between files and whose formatting reflects the content of a file. This is the backbone of the visualization part of our application. The custom nodes are under the components section of our app and can be further customized or to have additional custom nodes added, depending on preference. You can read more about custom nodes in React Flow [here](https://reactflow.dev/docs/api/nodes/custom-nodes/). 62 | 63 | ## AST Traversal and Parsing 64 | 65 | Our application uses the Babel-Parser to expose an uploaded project folder's AST (Abastract Syntax Tree), which normally happens during compile time, to have access to the project's file structure and component hierarchy. This allows us to traverse the AST using Babel Traverse to create a graph of nodes and edges using React Flow's logic which we can then use to create a visualization of the project's file structure and component relationship. Each component is contained within an object which is then destructured indide of Tree.tsx file with the nodes in the graph being populated with information about the clickable node and the edges and connections representing the file connections with the overall graph showing the component interactions and hierarchy with conditional formatting based on whether or not the component contains AJAX calls. You can read more about Babel-Parser [here](https://babeljs.io/docs/en/babel-parser) and Babel-Traverse [here](https://babeljs.io/docs/en/babel-traverse). 66 | 67 | ## Additional Graph Components 68 | 69 | When selecting a component node from our graph, you will be shown a tab with information about the component, including the component's code, the component's AJAX calls, and the component's database schemas. The AJAX calls are displayed next to the database schema of the file, if present. When clicking the "code" tab, you are presented with a code preview of the component. You are also able to search through the graph to quickly locate components. It accmoplishes this by looking through the labels which represent component names and highlights them. Included with the React Flow library are the controls for controlling the view of the graph and navigate through it and having an interactable mini map to navigate the graph. There are also two button components inside of the graph which allow you to change the orientation of the tree. 70 | 71 | # Iteration Ideas 72 | 73 | ### JavaScript Framework Expansion 74 | 75 | The application is currently tested to work on React applications and the components which are used in those libraries. Expanding the converage to include other front-end JavaScript frameworks is an obvious next step for the project starting with Vue and the Angular Framework and possibly expanding the compatibility to include Svelte. There is also preliminary logic for handling the syntax of Next.js files, though it isn't currently far enough along to be implemented. 76 | 77 | ### Integrating Other Languages 78 | 79 | The AST is a data structure which is used by a wide variety of languages and once you understand the logic of how the AST is organized and you are able to read the information from the compiler or parser from you language which exposes the AST. Once you are able to understand the logic and structure of Abstract Syntax Trees, you should be able to add controllers to the server to account for different languages once they are identified. 80 | 81 | ### Adding a SQLite Database 82 | 83 | The ability to add a database to the project would be helpful for filtering out edge cases, persisting user data and projects, and persisting the trees which the users have created to be albe to export it or share it. 84 | 85 | 86 | 87 | 88 | ### SERVER FOLDER 89 | 90 | ### CONTROLLER FILES 91 | 92 | ### fsController 93 | Uses the fs module to analyze a directory and extract the absolute file paths of all the files in that directory, organized in the **allFiles** array in the res.locals object. 94 | 95 | 96 | ### componentController 97 | The ***parseFile*** method in ***componentController*** serves to analyze the frontend structure and synthesize an object containing information about all the components. A ***components*** object is sent to the frontend, and it is structured as follows: 98 | 99 | ***components*** 100 | 101 | { 102 | absolute file path: { 103 | data: { label: component name }, 104 | children: [ 105 | absolute file path of a child component, 106 | absolute file path of another child component, 107 | ], 108 | ajaxRequests: [ 109 | { 110 | route: name of endpoint not including variables, 111 | fullRoute: name of endpoint including variables, 112 | method: method type (GET, POST, etc) 113 | }, 114 | { potentially another ajax request object } 115 | ], 116 | id: absolute file path 117 | }, 118 | another AFP: { another component object } 119 | } 120 | 121 | 122 | 123 | The ***getCode*** method in ***componentController*** takes in the absolute file path of a component as a query parameter, and uses the fs module to extract the code from this file and save it to res.locals.componentCode, to be sent back to the frontend. 124 | 125 | 126 | 127 | 128 | ### serverASTController 129 | The ***parseAll*** method in ***serverASTController*** servers to analyze the server files associated with AJAX requests from the frontend. An array of server-side file paths, ***allFiles***, is iterated through, and the code from each file is converted to an abstract syntax tree using babel parser, then traversed using babel traverse. 130 | 131 | The ***traverseServerAST*** function first organizes the file by file type (mongoose model files, controller files, router files, and root server file). Once a file's type has been determined, it is passed in to another function that is specific for that file type: 132 | 133 | ***traverseServerFile*** analyzes the root server file for incoming endpoints, and organizes the resulting information into 2 objects: 134 | ***linksToRouter*** 135 | 136 | { 137 | endpoint fragment: AFP of corresponding router file, 138 | another endpoint frag: another router file AFP 139 | } 140 | 141 | 142 | allServerRoutesLeadingToController 143 | 144 | { 145 | absolute file path of root: { 146 | endpoint: { 147 | method name: [ 148 | { 149 | path: AFP of file containing middleware method, 150 | middlewareName: name of middleware method 151 | }, 152 | { 153 | potentially another middleware object 154 | }, 155 | ], 156 | another method name: [ 157 | another array of middleware objects 158 | ], 159 | }, 160 | another endpoint: { 161 | another endpoint object 162 | }, 163 | }, 164 | } 165 | 166 | 167 | ***traverseRouterFile*** analyzes router files for ajax method invocations on an express instance, and packages the corresponding route, as well as its associated chain of middleware functions, into an object called: 168 | 169 | ***allRouterRoutesLeadingToController*** 170 | 171 | { 172 | absolute file path of router: { 173 | endpoint fragment: { 174 | method name: [ 175 | { 176 | path: AFP of file containing middleware method, 177 | middlewareName: name of middleware method, 178 | }, 179 | { another middleware object } 180 | ], 181 | another method name: [ 182 | another array of middleware objects 183 | ] 184 | }, 185 | another endpoint fragment: { 186 | another endpoint object 187 | }, 188 | } 189 | } 190 | 191 | 192 | 193 | ***traverseControllerFile*** analyzes controller files for middleware methods, and their corresponding interactions with database schemas, populating this information into the ***controllerSchemas*** object: 194 | 195 | ***controllerSchemas*** 196 | 197 | { 198 | absolute file path of current file: 199 | middleware method name: [ 200 | schema name, 201 | another schema name, 202 | ], 203 | another middleware method name: [ 204 | another array of associated schema names 205 | ] 206 | } 207 | 208 | 209 | ***traverseMongooseFile*** analyzes mongoose model files for new Schema instances and their corresponding labels, then uses these labels to determine the names they are exported as, and creates the ***schemaKey*** object from this information: 210 | ***schemaKey*** 211 | 212 | { 213 | exported schema name: { 214 | schema object 215 | }, 216 | another exported schema name: { another schema object } 217 | } 218 | 219 | 220 | After traversing all files and populating the above referenced objects, the ***allServerRoutesLeadingToController*** and ***allRouterRoutesLeadingToController*** objects are traversed independently to populate the same ***output*** object that is formatted as follows: 221 | 222 | ***output*** 223 | 224 | { 225 | complete endpoint: { 226 | CRUD method name: { 227 | schema name: { schema object }, 228 | another schema name: { another schema object } 229 | }, 230 | another CRUD method name: { another object of schemas } 231 | }, 232 | another complete endpoint: { another object of all method calls to this endpoint } 233 | } 234 | 235 | 236 | 237 | -------------------------------------------------------------------------- 238 | 239 | 240 | ### FRONTEND COMPONENT FILES 241 | 242 | 243 | ### ProjectPathModal.tsx 244 | 245 | 246 | ### Tree.tsx 247 | ***reactflow*** is used to generate and render a tree of 'nodes' connected to each other by 'edges'. ***dagre***, a library for organizing graphs, is used to automatically position the nodes and edges so they are appropriately spaced. 248 | 249 | The ***reactFlowComponents*** object is imported from the Redux Store, and, through a series of steps, is used to: 1. create the appropriate number of nodes for each component, and 2. create an edge for each parent-child relationship that connects each parent component node to a unique child component node. 250 | 251 | ***onNodeClick*** functionality is implemented to set a node to have an active state on-click. (see ***custom-nodes***), and to update the highlighted edges to be the lines surrounding the newly active node. 252 | 253 | **ReactFlow** COMPONENT PROPS 254 | **minZoom** is used to increase zoom-out range. 255 | **onClick** is used to minimize the details panel when the background is clicked. 256 | 257 | 258 | ### custom-nodes 259 | Node position is handled by **dagre**. Node color is conditional: 260 | -red if the current search value is included in the component name, -yellow if the component is the currently active component, 261 | -blue if the component contains AJAX request(s) (**custom-node.tsx**), and -white if the component doesn't contain any AJAX request(s) (**custom-node2.tsx**). 262 | 263 | 264 | ### Header.tsx 265 | Child of ***App.tsx***. Displays header of application. **showSearch** variable is set to the current value in the search input field, and is updated and dispatched the redux store on-change. This variable is used by ***custom-nodes***, which will turn red if their name includes current search value. 266 | 267 | Folder button on the far-right side of the header opens the ***ProjectPathModal.tsx*** component. 268 | 269 | ### ProjectPathModal.tsx 270 | Child of ***App.tsx***. Conditionally displayed on-button-click (see ***Header.tsx*** above). 271 | 272 | **openFileExplorer** function is invoked on click of one of two "CHOOSE PATH" buttons. One for the source code folder and one for the server folder. The user is prompted to select a local folder on-click. The "Continue" button results in the **onContinue** function being invoked, which closes the pop-up modal, and invokes **postPath** for each selected folder. For each invocation, a POST request is made, passing in the selected absolute folder path. The object returned as a response from the server is used to populate either the **components** object or the **server** object in the redux store, depending on which button was used to select the folder. This triggers the **reactFlowComponents** object in ***Tree.tsx*** to be populated, which triggers a useEffect hook that has this object in its dependency array to be invoked, triggering the population of **nodes** and **edges**, via this object, which should result in the rendering of the the component tree. 273 | 274 | 275 | ### Details.tsx 276 | On node-click, the height of the details container is conditional based on the viewport height. Clicking the background of the tree container will minimize the details container. It can also be adjusted manually via click-and-drag. 277 | 278 | The **useNavigate** hook from ***react-router-dom*** API is used to conditionally render either the routes and corresponding schemas of the active component, or the formatted code from the file that the active component is in. The displayed schemas default to those corresponding to the top-most route, and the schemas for each route will be shown when that route is clicked (see ***MethodButton.tsx***). 279 | 280 | 281 | ### MethodButtonContainer.tsx 282 | Child of ***Details.tsx***. Extracts all AJAX requests in the active component via the **nodeInfo** object, and returns a ***MethodButton*** component instance for each request. When a new component is clicked, the active route defaults to the top-most route, as mentioned in the Details.tsx section. 283 | 284 | ### MethodButton.tsx 285 | On-click, the **activeRoute** object is set to the AJAX-request-info that the clicked button represents. 286 | 287 | ### ModelPreview.tsx 288 | Child of ***Details.tsx***. Sets the list of schemas on the right side of the component details container based on the **activeRoute** (imported via the useSelector hook). 289 | 290 | ### ComponentCode.tsx 291 | Child of ***Details.tsx***. Displays the code of the file in which the active component is contained. ***activeComponentCode*** is imported from the redux store via the useSelector hook. This code is fetched in ***Tree.tsx*** **onNodeClick**. 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/build/icon.png -------------------------------------------------------------------------------- /build/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize') 2 | 3 | module.exports = async (context) => { 4 | if (process.platform !== 'darwin') return 5 | 6 | console.log('aftersign hook triggered, start to notarize app.') 7 | 8 | if (!process.env.CI) { 9 | console.log(`skipping notarizing, not in CI.`) 10 | return 11 | } 12 | 13 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 14 | console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.') 15 | return 16 | } 17 | 18 | const appId = 'com.electron.app' 19 | 20 | const { appOutDir } = context 21 | 22 | const appName = context.packager.appInfo.productFilename 23 | 24 | try { 25 | await notarize({ 26 | appBundleId: appId, 27 | appPath: `${appOutDir}/${appName}.app`, 28 | appleId: process.env.APPLE_ID, 29 | appleIdPassword: process.env.APPLEIDPASS 30 | }) 31 | } catch (error) { 32 | console.error(error) 33 | } 34 | 35 | console.log(`done notarizing ${appId}.`) 36 | } 37 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: https://example.com/auto-updates 3 | updaterCacheDirName: react-relay-updater 4 | -------------------------------------------------------------------------------- /electron.vite.config.1692742985063.mjs: -------------------------------------------------------------------------------- 1 | // electron.vite.config.ts 2 | import { resolve } from "path"; 3 | import { defineConfig, externalizeDepsPlugin } from "electron-vite"; 4 | import react from "@vitejs/plugin-react"; 5 | var electron_vite_config_default = defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | "@renderer": resolve("src/renderer/src") 16 | } 17 | }, 18 | plugins: [react()] 19 | } 20 | }); 21 | export { 22 | electron_vite_config_default as default 23 | }; 24 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import react from '@vitejs/plugin-react' 4 | 5 | export default defineConfig({ 6 | main: { 7 | 8 | plugins: [externalizeDepsPlugin()], 9 | build: { 10 | rollupOptions: { 11 | input: 'src/main/index.ts', 12 | output: { 13 | dir: 'dist/main' // Output directory for renderer process 14 | } 15 | } 16 | } 17 | }, 18 | preload: { 19 | plugins: [externalizeDepsPlugin()], 20 | build: { 21 | rollupOptions: { 22 | output: { 23 | dir: 'dist/preload' // Output directory for renderer process 24 | } 25 | } 26 | } 27 | }, 28 | renderer: { 29 | resolve: { 30 | alias: { 31 | '@renderer': resolve('src/renderer/src') 32 | } 33 | }, 34 | plugins: [react()], 35 | build: { 36 | rollupOptions: { 37 | output: { 38 | dir: 'dist/renderer' // Output directory for renderer process 39 | } 40 | } 41 | } 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /forge.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packagerConfig: { 3 | files: ['src/**/*', 'server/**/*'], 4 | ignore: [ 5 | /(.eslintrc.json)|(.gitignore)|(electron.vite.config.ts)|(forge.config.cjs)|(tsconfig.*)/, 6 | ], 7 | }, 8 | rebuildConfig: {}, 9 | makers: [ 10 | { 11 | name: '@electron-forge/maker-squirrel', 12 | config: { 13 | certificateFile: './cert.pfx', 14 | certificatePassword: process.env.CERTIFICATE_PASSWORD, 15 | }, 16 | }, 17 | { 18 | name: '@electron-forge/maker-zip', 19 | platforms: ['darwin', 'linux'], 20 | }, 21 | { 22 | name: '@electron-forge/maker-deb', 23 | config: {}, 24 | }, 25 | { 26 | name: '@electron-forge/maker-rpm', 27 | config: {}, 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-relay", 3 | "version": "1.0.0", 4 | "description": "An Electron application with React and TypeScript", 5 | "main": "./dist/main/index.js", 6 | "author": "example.com", 7 | "homepage": "https://www.electronjs.org", 8 | "scripts": { 9 | "prebuild": "electron-vite build", 10 | "format": "prettier --write .", 11 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 12 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 13 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 14 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 15 | "start": "electron-forge start", 16 | "server": "ts-node server/server.ts", 17 | "dev": "electron-vite dev --watch", 18 | "both": "concurrently --kill-others \"npm run server\" \"npm run dev\"", 19 | "build": "npm run typecheck && electron-vite build --outDir=dist", 20 | "make": "electron-vite build --outDir=dist && electron-forge make", 21 | "package": "electron-vite build --outDir=dist && electron-forge package" 22 | }, 23 | "dependencies": { 24 | "@babel/parser": "^7.22.10", 25 | "@babel/traverse": "^7.22.10", 26 | "@electron-forge/maker-deb": "^6.4.1", 27 | "@electron-forge/maker-dmg": "^6.4.1", 28 | "@electron-forge/maker-rpm": "^6.4.1", 29 | "@electron-toolkit/preload": "^2.0.0", 30 | "@electron-toolkit/utils": "^1.0.2", 31 | "@reduxjs/toolkit": "^1.9.5", 32 | "cors": "^2.8.5", 33 | "dagre": "^0.8.5", 34 | "electron-updater": "^5.3.0", 35 | "express": "^4.18.2", 36 | "fs": "^0.0.1-security", 37 | "path": "^0.12.7", 38 | "prismjs": "^1.29.0", 39 | "react-redux": "^8.1.2", 40 | "react-router-dom": "^6.15.0", 41 | "reactflow": "^11.8.2", 42 | "rollup-plugin-copy": "^3.5.0" 43 | }, 44 | "devDependencies": { 45 | "@electron-forge/cli": "^6.4.1", 46 | "@electron-forge/maker-squirrel": "^6.4.1", 47 | "@electron-forge/maker-zip": "^6.4.1", 48 | "@electron-toolkit/tsconfig": "^1.0.1", 49 | "@electron/notarize": "^1.2.3", 50 | "@types/node": "^18.16.19", 51 | "@types/react": "^18.2.14", 52 | "@types/react-dom": "^18.2.6", 53 | "@typescript-eslint/eslint-plugin": "^5.62.0", 54 | "@typescript-eslint/parser": "^5.62.0", 55 | "@vitejs/plugin-react": "^4.0.3", 56 | "autoprefixer": "^10.4.15", 57 | "concurrently": "^8.2.0", 58 | "daisyui": "^3.5.1", 59 | "electron": "^24.8.3", 60 | "electron-vite": "^1.0.27", 61 | "esbuild": "^0.19.2", 62 | "eslint": "^8.44.0", 63 | "eslint-config-prettier": "^8.8.0", 64 | "eslint-plugin-prettier": "^4.2.1", 65 | "eslint-plugin-react": "^7.32.2", 66 | "postcss": "^8.4.27", 67 | "prettier": "^2.8.8", 68 | "react": "^18.2.0", 69 | "react-dom": "^18.2.0", 70 | "tailwindcss": "^3.3.3", 71 | "ts-node": "^10.9.1", 72 | "typescript": "^5.1.6", 73 | "vite": "^4.4.2" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/resources/icon.png -------------------------------------------------------------------------------- /server/controllers/componentController.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path2 = require('path'); 3 | const parser = require('@babel/parser'); 4 | // const { isIdentifier, isStringLiteral } = require('typescript'); 5 | const traverse = require('@babel/traverse').default; 6 | 7 | const componentController = {} 8 | 9 | //readFileSync method of the fs module is used to grab all the code from 'filePath', 10 | //then the parse method in babel parser is used to create and return an AST version of 11 | //this file. plugins necessary to work with JSX and typescript. 12 | const parseFile = (filePath) => { 13 | const fileCode = fs.readFileSync(filePath, 'utf-8'); 14 | return parser.parse(fileCode, { 15 | sourceType: 'module', 16 | plugins: ['jsx', 'typescript'] 17 | }); 18 | } 19 | 20 | 21 | componentController.parseAll = (req, res, next) => { 22 | // const projectPath = req.body.filePath; 23 | // if (projectPath.length === 0) next(); 24 | let components = {}; 25 | 26 | const templateLiteralRouteParser = (node) => { 27 | let quasis = node.quasis; 28 | let fullRoute = ``; 29 | for (let i = 0; i { 38 | 39 | //variables in closure so they persist as we traverse from node to node (these could probably also be declared between 'traverse' and 'enter') 40 | const potentialChildren = []; 41 | let children = {}; 42 | let current = ''; 43 | let isComponent = false; 44 | let mightBeComponent = false; 45 | const noWayThisWorksCache = {}; 46 | let hardNO = false; 47 | 48 | let fetchPrimed = false; 49 | let ajaxRequests = []; 50 | let xmlHttpReq; 51 | 52 | let axiosLabel; 53 | 54 | let nextJSLinkImported = false; 55 | 56 | //babel traverse used to traverse the passed in ast 57 | traverse(ast, { 58 | 59 | //for each node (i.e. 'path') in the 'ast', the 'enter' method will be invoked 60 | enter(path) { 61 | 62 | //check if we are in the global scope within the file (i.e. not nested). if so, we 63 | //should check if the previously traversed code block was a component 64 | if (path.parent.type === "Program") { 65 | //if the prev code block was a component, add the information from this component to the 'components' object 66 | if (current !== '' && isComponent && !hardNO) { 67 | components[filePath] = { data: { label: current }, children: {...children}, ajaxRequests, id: filePath }; 68 | } 69 | potentialChildren.push(current); 70 | 71 | //reset component information whenever global space is re-entered 72 | children = {}; 73 | current = ''; 74 | isComponent = false; 75 | ajaxRequests = []; 76 | hardNO = false; 77 | 78 | //if the first node in the next code block in the global space of not 1 of the following types, it cannot be a component (possibly not necessary. also, possibly don't need to include "ExpressionStatement") 79 | mightBeComponent = ((path.node.type === "VariableDeclaration" || path.node.type === "ClassDeclaration" || path.node.type === "FunctionDeclaration" || path.node.type === "ExpressionStatement" || path.node.type === "ExportDefaultDeclaration" || path.node.type === "ExportNamedDeclaration")) ? true : false; 80 | } 81 | 82 | //set 'current' to the first Identifier of a new code block if the following conditions are met (the name of the identifier being render is a special case used to itentify the root file. there is probably a better solution) 83 | if (path.isIdentifier() && (path.parent.type === "VariableDeclarator" || path.parent.type === "ClassDeclaration" || path.parent.type === "FunctionDeclaration" || path.node.name === "render") && current === '' && mightBeComponent) { 84 | current = path.node.name !== "render" ? path.node.name : "root"; 85 | } 86 | 87 | //check for imported variables 88 | if (path.isIdentifier() && (path.parent.type === "ImportDefaultSpecifier" || path.parent.type === "ImportSpecifier")) { 89 | 90 | //assign relativePath to the RFP this variable is being imported from 91 | const relativePath = path.parentPath.parent.source.value 92 | 93 | //filter out non-local paths (filters out modules, etc.) 94 | if (relativePath.includes('./') || relativePath.includes('../')) { 95 | 96 | //grab the directory of this file 97 | const currDirectory = path2.dirname(filePath) 98 | 99 | //grab the absolute filepath of the imported file 100 | const afp = path2.resolve(currDirectory, relativePath); 101 | 102 | //grab the file path within the allFiles array that matches "afp" (this is necessary bc 'afp' might not have .js/.jsx/.ts/.tsx at the end of it) 103 | const componentAFP = allFiles.filter(file => file.includes(afp))[0]; 104 | 105 | //add this imported variable as a key in potentialChildren array with its value being its AFP 106 | potentialChildren[path.node.name] = componentAFP; 107 | } 108 | 109 | //prime axios handler if axios is imported 110 | if (relativePath === "axios") axiosLabel = path.node.name; 111 | } 112 | 113 | //if we find a 'callee' with the name 'fetch', we know a fetch is being invoked, so we reassign 'fetchPrimed' to true, which will trigger the program to look for the route (...in the condition directly below this) 114 | if (path.isCallExpression()) { 115 | const callee = path.node.callee; 116 | if (callee.type === "Identifier" && callee.name === "fetch") fetchPrimed = true; 117 | } 118 | 119 | //the first either TemplateLiteral or StringLiteral node after fetch should be the route string 120 | if (fetchPrimed && (path.node.type === "TemplateLiteral" || path.node.type === "StringLiteral")) { 121 | let route = ''; 122 | let fullRoute = ``; 123 | let method = 'GET'; 124 | 125 | //a TemplateLiteral node will have 2 properties: expressions, which store variables, and quasis, which store normal chars in the string. Order of data will always altername between quasi and expressions, and length of quasis will always be length of expressions + 1. This logic can be used to reconstruct this literal string. 126 | if (path.node.type === "TemplateLiteral") { 127 | route = path.node.quasis[0].value.raw; 128 | fullRoute = templateLiteralRouteParser(path.node); 129 | } else if (path.node.type === "StringLiteral") fullRoute = route = path.node.value; 130 | 131 | //arguments prop is sibling of the above literal prop in the AST 132 | const argArrr = path.parentPath.node.arguments; 133 | let objExpIdx = -1; 134 | 135 | //ObjectExpression node will exist in arguments array if fetch contains body as 2nd arg. if none is found, fetch method must be GET 136 | if (argArrr) { 137 | argArrr.forEach((sibling, i) => sibling.type === "ObjectExpression" ? objExpIdx = i : null); 138 | 139 | if (objExpIdx > -1) { 140 | const objProps = argArrr[objExpIdx].properties; 141 | objProps.forEach(prop => { 142 | if (prop.key.name === 'method') method = prop.value.value; 143 | }) 144 | } 145 | } 146 | 147 | //push route and method data into ajaxRequests array, which will be added to component object 148 | ajaxRequests.push({ route, fullRoute, method }); 149 | fetchPrimed = false; 150 | // console.log('this is a route?', ajaxRequests) 151 | } 152 | 153 | //XMLHttpRequest handlers 154 | if (path.isIdentifier() && path.node.name === "XMLHttpRequest" && path.parent.type === "NewExpression") { 155 | const declarationPath = path.findParent((path) => path.isVariableDeclarator()); 156 | if (declarationPath) xmlHttpReq = declarationPath.node.id.name; 157 | // console.log('xmlhttprequest') 158 | } 159 | 160 | if (path.isIdentifier() && path.node.name === xmlHttpReq && path.parent.property.name === "open") { 161 | const callExpressionPath = path.findParent((path) => path.isCallExpression()); 162 | if (callExpressionPath) { 163 | const argsArrr = callExpressionPath.node.arguments; 164 | const method = argsArrr[0].value; 165 | const route = argsArrr[1].value; 166 | ajaxRequests.push({ route, fullRoute: route, method }); 167 | } 168 | } 169 | 170 | // axios handler 171 | if (axiosLabel && path.isIdentifier() && path.node.name === axiosLabel && path.findParent((path) => path.isCallExpression())) { 172 | let route; 173 | let method; 174 | let fullRoute; 175 | if (path.parent.type === "MemberExpression") { 176 | const callExpressionPath = path.findParent((path) => path.isCallExpression()); 177 | method = path.parent.property.name.toUpperCase(); 178 | const callExpArgsArr = callExpressionPath.node.arguments[0]; 179 | if (callExpArgsArr.type === "StringLiteral") { 180 | route = fullRoute = callExpArgsArr.value; 181 | } else if (callExpArgsArr.type === "TemplateLiteral") { 182 | route = callExpArgsArr.quasis[0].value.raw; 183 | fullRoute = templateLiteralRouteParser(callExpArgsArr); 184 | } else if (callExpArgsArr.type === "MemberExpression") { 185 | route = fullroute = `${callExpArgsArr.object.name}.${callExpArgsArr.property.name}`; 186 | } 187 | } else if (path.parent.arguments[0].properties) { 188 | path.parent.arguments[0].properties.forEach(prop => { 189 | if (prop.key.name === "method") method = prop.value.value.toUpperCase(); 190 | if (prop.key.name === "url") { 191 | if (prop.value.type === "TemplateLiteral") { 192 | route = prop.value.quasis[0].value.raw; 193 | fullRoute = templateLiteralRouteParser(prop.value); 194 | } else route = fullRoute = prop.value.value; 195 | } 196 | }) 197 | } 198 | ajaxRequests.push({ route, fullRoute, method }); 199 | } 200 | 201 | 202 | 203 | //this is an attempt to filter out false positives from test files 204 | if (path.isIdentifier() && (path.node.name === "describe" || path.node.name === "xdescribe" || path.node.name === "test" || path.node.name === "xtest") && path.parent.type === "CallExpression" && path.parentPath.parent.type === "ExpressionStatement") hardNO = true; 205 | 206 | //same as below 207 | if (path.isIdentifier() && (path.parent.type === "JSXExpressionContainer" || path.parentPath.parent.type === "JSXExpressionContainer") && Object.keys(potentialChildren).includes(path.node.name)) { 208 | isComponent === true; 209 | const newChildPath = potentialChildren[path.node.name]; 210 | 211 | if (newChildPath) children[newChildPath] = null; 212 | } 213 | 214 | //if the current node has a pattern that is consistently true for child JSX components, and it was deemed to be a potential child via navigating the import statements above, then we can add its file path to the children array for the current component 215 | if (path.isJSXIdentifier()) { 216 | isComponent = true; 217 | if (path.parentPath.parent.type === "JSXElement" && Object.keys(potentialChildren).includes(path.node.name)) { 218 | const newChildPath = potentialChildren[path.node.name]; 219 | 220 | if (newChildPath) children[newChildPath] = null; 221 | } 222 | } 223 | 224 | //preliminary logic for handling next.JS 225 | if (path.isStringLiteral() && path.node.value === "next/link" && path.parent.type === "ImportDeclaration") { 226 | const specifiers = path.parent.specifiers; 227 | specifiers.forEach(obj => { 228 | if (obj.local.name === "Link") nextJSLinkImported = true; 229 | }) 230 | } 231 | 232 | if (nextJSLinkImported && path.isJSXIdentifier() && path.node.name === "Link" && path.parent.type === "JSXOpeningElement") { 233 | const attributes = path.parent.attributes; 234 | attributes.forEach(obj => { 235 | let route; 236 | if (obj.name.name === "href") { 237 | let route = obj.value.type === "StringLiteral" ? obj.value.value : null; 238 | let expression = route ? null : obj.value.expression; 239 | if (!route) { 240 | if (expression.type === "StringLiteral") { 241 | route = expression.value; 242 | } else if (expression.type === "TemplateLiteral") { 243 | route = expression.quasis[0].value.raw; 244 | } 245 | } 246 | const currDirectory = path2.dirname(filePath); 247 | const childFilePath = path2.resolve(currDirectory, route) 248 | const afp = allFiles.filter(file => file.includes(childFilePath))[0]; 249 | children[afp] = null; 250 | } 251 | }) 252 | 253 | //TODO: if we find a Link tag, store this link tag and the filePath it is located in inside of a "links" object. After traversing all files, identify the component for which the link component is a child. add the route as a child of that component.(?) 254 | 255 | } 256 | 257 | 258 | } 259 | }) 260 | 261 | //if the component is exported in this way: "export const ComponentName = () => {...}", it might be the case that there is no code after the component code block, in which case we should check if the previous code block was a component 1 last time. 262 | if (current !== '' && isComponent && !hardNO) components[filePath] = { data: {label: current}, children: {...children}, ajaxRequests, id: filePath }; 263 | } 264 | 265 | 266 | const allFiles = res.locals.allFiles; 267 | console.log(allFiles); 268 | 269 | //iterate through array of files, convert each to AST, and invoke above function to traverse the AST 270 | allFiles.forEach((filePath) => { 271 | const ast = parseFile(filePath); 272 | // console.log('ast'); 273 | traverseAST(ast, filePath); 274 | }) 275 | 276 | //this logic attempts to reassign all children filePath keys to point to their respective filePath Object within the components object 277 | Object.values(components).forEach(component => { 278 | Object.keys(component.children).forEach(filePath => { 279 | 280 | component.children[filePath] = components[filePath]; 281 | }) 282 | }) 283 | 284 | //convert values of children properties to array of Objects, rather than object of filePath keys where the value is the object 285 | Object.values(components).forEach(component => { 286 | const arrayOfChildFilePaths = Object.keys(component.children); 287 | 288 | component.children = arrayOfChildFilePaths; 289 | }) 290 | 291 | console.log('components', components) 292 | res.locals.components = components 293 | next(); 294 | 295 | 296 | 297 | } 298 | 299 | //used to grab code from the file associated with a component, to be displayed in details section on front end 300 | componentController.getCode = (req, res, next) => { 301 | const { id } = req.query; 302 | const decodedId = decodeURIComponent(id); 303 | 304 | const fileCode = fs.readFileSync(decodedId, 'utf-8'); 305 | res.locals.componentCode = fileCode; 306 | next() 307 | } 308 | 309 | module.exports = componentController; -------------------------------------------------------------------------------- /server/controllers/fsController.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const fsController = {}; 5 | 6 | fsController.getArrayOfFilePaths = (req, res, next) => { 7 | const projectPath = req.body.filePath; 8 | if (projectPath.length === 0) next(); 9 | 10 | //dirPath is initially the root directory. This function recursively navigates 11 | //through this directory, passing each file path into arrayOfFiles, and eventually 12 | //returning this array 13 | const getAllFiles = (dirPath, arrayOfFiles = []) => { 14 | const files = fs.readdirSync(dirPath); 15 | 16 | 17 | files.forEach((file) => { 18 | if (fs.statSync(path.join(dirPath, file)).isDirectory()) { 19 | arrayOfFiles = getAllFiles(path.join(dirPath, file), arrayOfFiles); 20 | } else { 21 | arrayOfFiles.push(path.join(dirPath, file)); 22 | } 23 | }) 24 | return arrayOfFiles; 25 | } 26 | 27 | //invoking above 'getAllFiles' function to grab an array of all files in 'projectPath', then filtering for only files that could be react components 28 | const allFiles = getAllFiles(projectPath).filter((file) => file.endsWith('.jsx') || file.endsWith('.js') || file.endsWith('.tsx') || file.endsWith('.ts')); 29 | 30 | console.log('af', allFiles); 31 | res.locals.allFiles = allFiles; 32 | return next(); 33 | 34 | } 35 | 36 | module.exports = fsController; -------------------------------------------------------------------------------- /server/controllers/serverASTController.js: -------------------------------------------------------------------------------- 1 | //BREAKDOWN 2 | //1. IDENTIFY FILE TYPE AS EITHER: 1) SERVER.JS 2) ROUTER 3) CONTROLLER 4) MODEL 3 | //2. PASS FILE INTO ANOTHER TRAVERSAL FUNCTION SPECIFIC FOR THAT FILE TYPE 4 | //3. PASS FILE INFORMATION INTO OBJECT SPECIFIC TO THAT FILE TYPE 5 | //4. ITERATE THROUGH FILE OBJECTS ONE AT A TIME IN THE ORDER SHOWN ABOVE IN STEP 1. USE THIS INFORMATION TO GRADUALLY BUILD AN OUTPUT OBJECT CONTAINING INFORMATION ABOUT EACH ROUTE. 6 | 7 | //SERVER.JS LOGIC 8 | //-LOOK FOR IMPORTED FILES THAT ARE INCLUDED IN ARRAY OF ALL SERVER FILES 9 | //DETERMINE AFP (ABSOLUTE FILES PATH) AND SAVE THIS AFP ALONG WITH THE VARIABLE NAME IT IS IMPORTED AS 10 | //-ALSO IDENTIFY THE LABEL OF EXPRESS INSTANCE 11 | //-WHEN 'USE' METHOD IS INVOKED ON EXPRESS INSTANCE, AND FIRST ARG IS A ROUTE: 12 | //IF 2ND ARG IS THE EXACT MATCH OF ONE OF THE IMPORT LABELS, IT MUST BE A ROUTER (?) 13 | 14 | //-WHEN CRUD METHOD IS INVOKED ON EXPRESS INSTANCE, FIRST ARG SHOULD BE COMPLETE ROUTE, FOLLOWED BY INDEFINITE NUMBER OF CONTROLLER INVOCATIONS. WE CAN CHECK IF THE FOLLOWING ARG SPLIT ON '.' IS INCLUDED IN IMPORT LABELS. IF SO, ADD IT TO OBJECT 15 | //-THERE MAY BE LOGIC TO GET/POST/ETC DATA TO/FROM DB MODEL INSIDE SERVER.JS, EITHER AFTER MIDDLEWARE OR IN PLACE OF IT. 16 | //-FIRST WORK ON TAKING CARE OF LOGIC INVOLVING DB CALLS FROM MIDDLEWARE 17 | 18 | //ROUTER LOGIC 19 | //-DISTINGUISHING FEATURE = IMPORTING EXPRESS, THEN CREATING EXPRESS.ROUTER() INVOCATION (?) 20 | // 21 | 22 | //CONTROLLER LOGIC 23 | //-DISTINGUISHING FEATURE = USING REQ, RES, NEXT, BUT NOT IMPORTING/INVOKING EXPRESS (?) 24 | //ALSO FILE PATH CAN BE MATCHED WITH MIDDLEWARE FOUND IN SERVER.JS/ROUTER FILES 25 | //CHECK IMPORTS FROM LOCAL FILES (SHOULD INCLUDE ANY MODELS) AND SAVE THE VARIABLE NAMES 26 | //IF DB MANIPULATION METHOD IS INVOKED ON ONE OF THESE VARIABLES, SAVE THE RELEVANT INFO IN OBJECT 27 | 28 | //DB LOGIC 29 | //FEATURES: IMPORTING MONGOOSE AND CREATING SCHEMA INSTANCE AND INVOKING CONNECT 30 | //CHALLENGE = GRABBING SCHEMA DATA. POTENTIALLY WILL HAVE TO USE FS MODULE INSTEAD OF AST 31 | 32 | const fs = require('fs'); 33 | const path = require('path'); 34 | const parser = require('@babel/parser'); 35 | // const { isIdentifier, isStringLiteral } = require('typescript'); 36 | const traverse = require('@babel/traverse').default; 37 | 38 | const CRUDMETHODS = ['get', 'post', 'patch', 'put', 'delete']; 39 | 40 | const serverASTController = {}; 41 | 42 | serverASTController.parseAll = (req, res, next) => { 43 | 44 | //readFileSync method of the fs module is used to grab all the code from 'filePath', 45 | //then the parse method in babel parser is used to create and return an AST version of 46 | //this file. plugins necessary to work with JSX and typescript. 47 | const parseFile = (filePath) => { 48 | const fileCode = fs.readFileSync(filePath, 'utf-8'); 49 | return parser.parse(fileCode, { 50 | sourceType: 'module', 51 | plugins: ['jsx', 'typescript'], 52 | }); 53 | }; 54 | 55 | //helper functions 56 | const findVariableName = (curr) => { 57 | const varDecPath = curr.findParent((curr) => curr.isVariableDeclarator()); 58 | if (!varDecPath) return; 59 | if (varDecPath.node.id.name) return varDecPath.node.id.name; 60 | if (varDecPath.node.id.properties) 61 | return varDecPath.node.id.properties.map((prop) => prop.value.name); 62 | }; 63 | 64 | const findImportLabel = (curr) => { 65 | const importDecPath = curr.findParent((curr) => curr.isImportDeclaration()); 66 | return importDecPath.node.specifiers[0].local.name; 67 | }; 68 | 69 | const addES6Import = (curr, filePath, importLabels) => { 70 | const importLabel = findImportLabel(curr); //grab the directory of this file 71 | const currDirectory = path.dirname(filePath); //grab the absolute filepath of the imported file //IRENE: using directory name of current filepath 72 | const afp = path.resolve(currDirectory, curr.node.value); 73 | console.log('afp', afp); 74 | const afpArray = allFiles.filter((file) => file.includes(afp)); //if imported file exists in server directory, add its label name as a key in importLabels obj w value being its afp 75 | if (afpArray.length) importLabels[importLabel] = afpArray[0]; 76 | // console.log(importLabels, 'importLabels'); // importLabels = {starWarsController: /Users/Codesmith/App/Controllers/starWarsControllers.js} this is temporary 77 | return importLabels; 78 | }; 79 | 80 | addES5Import = (curr, filePath, importLabels) => { 81 | const importLabel = findVariableName(curr); 82 | const currDirectory = path.dirname(filePath); 83 | const afp = path.resolve(currDirectory, curr.node.value); 84 | 85 | const afpArray = allFiles.filter((file) => file.includes(afp)); 86 | if (afpArray.length) importLabels[importLabel] = afpArray[0]; 87 | // console.log(importLabels, 'importLabels') 88 | return importLabels; 89 | }; 90 | 91 | //traverse all server files and categorize them as either DB model files, controller files, router files, or the root server file 92 | const traverseServerAST = (ast, filePath) => { 93 | let importExpress = false; 94 | let importMongoose = false; 95 | let expressLabel; 96 | let mongooseLabel; 97 | let expressInstance; 98 | let routerInstance; 99 | let schemaInstance; 100 | let explicitRouter = false; 101 | 102 | traverse(ast, { 103 | enter(curr) { 104 | 105 | //check for specific ES5 syntax imports so we can identify 1) that express, mongoose, and/or express Router is being imported, and 2) what the import is labeled 106 | if ( 107 | curr.isIdentifier() && 108 | curr.node.name === 'require' && 109 | curr.parent.type === 'CallExpression' 110 | ) { 111 | const argument = curr.parent.arguments[0]; 112 | if (argument.type === 'StringLiteral') { 113 | if (argument.value === 'express') importExpress = true; 114 | if (argument.value === 'mongoose') importMongoose = true; 115 | } else if (argument.type === 'TemplateLiteral') { 116 | if (argument.quasis[0].value.raw === 'express') 117 | importExpress = true; 118 | if (argument.quasis[0].value.raw === 'mongoose') 119 | importMongoose = true; 120 | } 121 | if (importExpress || importMongoose) { 122 | const label = findVariableName(curr); 123 | label === 'express' 124 | ? (expressLabel = label) 125 | : importMongoose 126 | ? (mongooseLabel = label) 127 | : null; 128 | // console.log(expressLabel, 'expressLabel'); 129 | importExpress = importLabel = false; 130 | } 131 | } 132 | 133 | //Same logic as above, but for ES6 syntax. TODO: account for possiblity of an import being imported "as" something. i.e.: import { Router as Routeriffic } from 'express' 134 | if ( 135 | curr.isStringLiteral() && 136 | (curr.node.value === 'mongoose' || curr.node.value === 'express') && 137 | curr.parent.type === 'ImportDeclaration' 138 | ) { 139 | if (curr.node.value === 'mongoose') { 140 | importMongoose = true; 141 | mongooseLabel = curr.parent.specifiers[0].local.name; 142 | } 143 | if (curr.node.value === 'express') { 144 | if (curr.parent.specifiers.length >= 1) { 145 | for (let specifier of curr.parent.specifiers) { 146 | if (specifier.local.name === 'Router') { 147 | explicitRouter = true; 148 | importExpress = true; 149 | expressLabel = "express"; 150 | return; 151 | } 152 | } 153 | } 154 | importExpress = true; 155 | expressLabel = curr.parent.specifiers[0].local.name; 156 | } 157 | importExpress = importMongoose = false; 158 | } 159 | 160 | //if it is found that an instance of express has been created, it is assumed this must be the root server file, and so the function for traversing root server files is invoked, and the current traversal is stopped. 161 | if ( 162 | curr.isIdentifier() && 163 | curr.node.name === expressLabel && 164 | curr.parent.type === 'CallExpression' && 165 | curr.parent.arguments.length === 0 166 | ) { 167 | expressInstance = findVariableName(curr); 168 | // console.log('expinst', expressInstance) 169 | traverseServerFile(ast, filePath, expressLabel, expressInstance); 170 | curr.stop(); 171 | } 172 | 173 | //if it is found that a Router instance has been created, it is assumed this must be a router file, and so the func for traversing router files is invoked 174 | if ( 175 | curr.isIdentifier() && 176 | curr.node.name === expressLabel && 177 | curr.parent.type === 'MemberExpression' && 178 | curr.parent.property.name === 'Router' 179 | ) { 180 | routerInstance = findVariableName(curr); 181 | // console.log('routerInstance', routerInstance); 182 | traverseRouterFile(ast, filePath, expressLabel, routerInstance); 183 | curr.stop(); 184 | } 185 | 186 | //extension of above to cover possibility of Router being explicitly imported as itself (ES6) 187 | if ( 188 | curr.isIdentifier() && 189 | explicitRouter && 190 | curr.node.name === 'Router' && 191 | curr.parent.type === 'CallExpression' 192 | ) { 193 | const varDecPath = curr.findParent(curr => curr.isVariableDeclarator()); 194 | if (varDecPath) { 195 | if (varDecPath.node.id) { 196 | routerInstance = varDecPath.node.id.name; 197 | traverseRouterFile(ast, filePath, expressLabel, routerInstance); 198 | curr.stop(); 199 | } 200 | } 201 | } 202 | 203 | //if a mongoose Schema instance is found, assume this file is a models file 204 | if ( 205 | curr.isIdentifier() && 206 | curr.node.name === mongooseLabel && 207 | curr.parent.type === 'MemberExpression' 208 | ) { 209 | if (curr.parent.property.name === 'Schema') { 210 | schemaInstance = findVariableName(curr); 211 | traverseMongooseFile(ast, filePath, mongooseLabel, schemaInstance); 212 | curr.stop(); 213 | } 214 | } 215 | 216 | }, 217 | }); 218 | //if file was not identified as root, router, or model file, assume it is either a controller file, or negligible 219 | if (!expressInstance && !routerInstance && !schemaInstance) 220 | traverseControllerFile(ast, filePath); 221 | }; 222 | 223 | const linksToRouter = {}; 224 | const allServerRoutesLeadingToController = {}; //obj to hold all server routes that lead to a controller 225 | //TODO: STILL NEED TO ACCOUNT FOR MIDDLEWARE FUNCTIONALITY EXPLICITLY FOUND IN THE ROOT SERVER FILE (DB INTERACTIONS THAT OCCUR IN ROOT SERVER FILE) 226 | 227 | //function for traversing the root server file, checking for links to router and controller files 228 | const traverseServerFile = (ast, filePath, expressLabel, expressInstance) => { 229 | //used to store identified imports and their corresponding AFP 230 | let importLabels = {}; 231 | 232 | traverse(ast, { 233 | enter(curr) { 234 | //check for files imported locally using ES6 syntax 235 | if ( 236 | curr.isStringLiteral() && 237 | curr.node.value.includes('./') && 238 | curr.parent.type === 'ImportDeclaration' 239 | ) { 240 | importLabels = addES6Import(curr, filePath, importLabels); 241 | } 242 | 243 | //check for files imported locally using ES5 syntax 244 | if ( 245 | curr.isStringLiteral() && 246 | curr.node.value.includes('./') && 247 | curr.parent.type === 'CallExpression' 248 | ) { 249 | if (curr.parent.callee.name === 'require') { 250 | importLabels = addES5Import(curr, filePath, importLabels); 251 | } 252 | } 253 | 254 | //filter for invocations of the use method on the express instance (app.use(...)), and identify links to router files using this 255 | if ( 256 | curr.isIdentifier() && 257 | curr.node.name === expressInstance && 258 | curr.parent.type === 'MemberExpression' && 259 | curr.parent.property.name === 'use' 260 | ) { 261 | //pinpoint the array of args passed to this method invocation 262 | const callExpPath = curr.findParent((curr) => 263 | curr.isCallExpression() 264 | ); 265 | const arguments = callExpPath.node.arguments; 266 | 267 | //check for links to router files imported above 268 | if ( 269 | arguments.length > 1 && 270 | Object.hasOwn(importLabels, arguments[1].name) 271 | ) { 272 | // linksToRouter[importLabels[arguments[1].name]] = arguments[0].value; 273 | linksToRouter[arguments[0].value] = importLabels[arguments[1].name]; 274 | // '/auth': '/Users/cush572/Codesmith/TEST/ReacType/server/routers/auth.ts', 275 | // '/user-styles': '/Users/cush572/Codesmith/TEST/ReacType/server/routers/stylesRouter.ts' 276 | } 277 | } 278 | 279 | //TODO: refactor below and turn into helper function since this is similar to the condition inside traverseRouterFile function 280 | 281 | //check for server endpoints that bypass router files and link directly to middleware functions in controller files. Populate allServerRoutesLeadingToController obj accordingly. 282 | if ( 283 | curr.isIdentifier() && 284 | curr.node.name === expressInstance && 285 | curr.parent.type === 'MemberExpression' && 286 | CRUDMETHODS.includes(curr.parent.property.name) 287 | ) { 288 | const method = curr.parent.property.name; 289 | const callExpPath = curr.findParent((curr) => 290 | curr.isCallExpression() 291 | ); 292 | const arguments = callExpPath.node.arguments; 293 | const endpoint = arguments[0].value; 294 | arguments.slice(1, -1).forEach((arg) => { 295 | if (!arg.object) return; 296 | if (Object.hasOwn(importLabels, arg.object.name)) { 297 | //check if import label exists (we found import label arg.object.name inside the imports at top of page) 298 | const middlewareMethod = { 299 | path: importLabels[arg.object.name], 300 | middlewareName: arg.property.name, 301 | }; // { path: ${absoluteFilePathOfController}, middlewareName: ${nameOfMiddleware}} 302 | if (allServerRoutesLeadingToController[filePath]) { 303 | if (allServerRoutesLeadingToController[filePath][endpoint]) { 304 | if ( 305 | allServerRoutesLeadingToController[filePath][endpoint][ 306 | method 307 | ] 308 | ) { 309 | allServerRoutesLeadingToController[filePath][endpoint][ 310 | method 311 | ].push(middlewareMethod); 312 | } else { 313 | allServerRoutesLeadingToController[filePath][endpoint][ 314 | method 315 | ] = [middlewareMethod]; 316 | } 317 | } else { 318 | allServerRoutesLeadingToController[filePath][endpoint] = { 319 | [method]: [middlewareMethod], 320 | }; 321 | } 322 | } else { 323 | // console.log('doesn’t exist'); 324 | allServerRoutesLeadingToController[filePath] = { 325 | [endpoint]: { [method]: [middlewareMethod] }, 326 | }; 327 | } 328 | } 329 | }); 330 | } 331 | }, 332 | }); 333 | console.log( 334 | linksToRouter, 335 | 'linksToRouter', 336 | allServerRoutesLeadingToController, 337 | 'allServerRoutesLeadingToController' 338 | ); 339 | }; 340 | 341 | const allRouterRoutesLeadingToController = {}; //all routes in router that lead to controller 342 | 343 | 344 | //function for traversing router files, checking for links controller files and populating these links in the above object 345 | const traverseRouterFile = (ast, filePath, expressLabel, routerInstance) => { 346 | // 347 | let importLabels = {}; 348 | traverse(ast, { 349 | enter(curr) { 350 | //GET LOCALLY IMPORTED FILE LABEL: 351 | if ( 352 | curr.isStringLiteral() && 353 | curr.node.value.includes('./') && 354 | curr.parent.type === 'ImportDeclaration' 355 | ) { 356 | //IRENE: provides access to all the imports at the top of the file 357 | importLabels = addES6Import(curr, filePath, importLabels); 358 | } 359 | 360 | 361 | if ( 362 | curr.isStringLiteral() && 363 | curr.node.value.includes('./') && 364 | curr.parent.type === 'CallExpression' 365 | ) { 366 | if (curr.parent.callee.name === 'require') 367 | importLabels = addES5Import(curr, filePath, importLabels); 368 | } 369 | 370 | //FIND INSTANCE OF ROUTER AND FILTERING FOR THE CRUD METHODS WITHIN IT 371 | //TODO: refactor to helper function (same as function from above) 372 | if ( 373 | curr.isIdentifier() && 374 | curr.node.name === routerInstance && 375 | curr.parent.type === 'MemberExpression' && 376 | CRUDMETHODS.includes(curr.parent.property.name) 377 | ) { 378 | //IRENE: finding instance of router 379 | const method = curr.parent.property.name; //IRENE: trying to understand what specific method is being invoked here 380 | const callExpPath = curr.findParent((curr) => 381 | curr.isCallExpression() 382 | ); //IRENE: 383 | const arguments = callExpPath.node.arguments; //IRENE: trying to find the arguments of router.METHOD() 384 | const endpoint = arguments[0].value; //IRENE: we know first arg will always be the endpoint 385 | arguments.slice(1, -1).forEach((arg) => { 386 | if (!arg.object) return; 387 | //IRENE: we know last arg will be the middleware that handles sending the response back to client. we now need to just look at the arguments between the first and last so we can identify the controller/middleware 388 | if (Object.hasOwn(importLabels, arg.object.name)) { 389 | //IRENE: check if name of the argument in the argument object matches the label in importLabels object 390 | const middlewareMethod = { 391 | path: importLabels[arg.object.name], 392 | middlewareName: arg.property.name, 393 | }; // {path: ${absoluteFilePathOfController}, middlewareName: ${nameOfMiddleware}} 394 | //{{path: ${absoluteFilePathOfController}, middlewareName: ${nameOfMiddleware}}} 395 | if (allRouterRoutesLeadingToController[filePath]) { 396 | if (allRouterRoutesLeadingToController[filePath][endpoint]) { 397 | if ( 398 | allRouterRoutesLeadingToController[filePath][endpoint][ 399 | method 400 | ] 401 | ) { 402 | allRouterRoutesLeadingToController[filePath][endpoint][ 403 | method 404 | ].push(middlewareMethod); 405 | } else { 406 | allRouterRoutesLeadingToController[filePath][endpoint][ 407 | method 408 | ] = [middlewareMethod]; 409 | } 410 | } else { 411 | allRouterRoutesLeadingToController[filePath][endpoint] = { 412 | [method]: [middlewareMethod], 413 | }; 414 | } 415 | } else { 416 | console.log('doesn’t exist'); 417 | allRouterRoutesLeadingToController[filePath] = { 418 | [endpoint]: { [method]: [middlewareMethod] }, 419 | }; 420 | } 421 | //TEMPLATE: allRouterRoutesLeadingToController = { 422 | // [filePath]: { 423 | // [endpoint]: { 424 | // [GET]: [middlewareMethod], 425 | // [POST]: [middlewareMEthod] 426 | // } 427 | // } 428 | // } 429 | 430 | //EXAMPLE END RESULT: allRouterRoutesLeadingToController{ 431 | // '/Users/cush572/Codesmith/Week4/unit-10-databases/server/routes/api.js': { 432 | // '/': { get: Array }, 433 | // '/species': { get: Array }, 434 | // '/homeworld': { get: Array }, 435 | // '/film': { get: Array }, 436 | // '/character': { post: Array, delete: Array } 437 | // } 438 | // } 439 | } 440 | }); 441 | } 442 | }, 443 | }); 444 | console.log('allRouterRoutes....', allRouterRoutesLeadingToController['/Users/cush572/Codesmith/Week4/unit-10-databases/server/routes/api.js']['/character'].post, 'importLabelsssszzz', importLabels); 445 | }; 446 | 447 | 448 | controllerSchemas = {}; 449 | 450 | //function to traverse controller files, identifying middleware methods and their corresponding interactions with database schemas, and populating this information in the above controllerSchemas object 451 | const traverseControllerFile = (ast, filePath, label, instance) => { 452 | const imports = {}; 453 | const schemaInteractions = {}; 454 | let currentControllerMethod; 455 | let expressionStatementPossible = false; 456 | let variableStatementPossible = false; 457 | 458 | console.log('CONTROLLER FILE PATH', filePath); 459 | traverse(ast, { 460 | enter(curr) { 461 | 462 | //account for cases where controller methods are explicitly defined within a controller object 463 | if ( 464 | variableStatementPossible && 465 | currentControllerMethod && 466 | curr.parent.type === 'ObjectExpression' 467 | ) { 468 | const callExpPath = curr.findParent(curr => curr.isCallExpression()) 469 | if (callExpPath) { 470 | if (callExpPath.node.callee) { 471 | 472 | if (callExpPath.node.callee.object) { 473 | if (Object.keys(imports).includes(callExpPath.node.callee.object.name)) return; 474 | } else if (callExpPath.node.callee.name) { 475 | if (callExpPath.node.callee.name === "next") return; 476 | } 477 | } 478 | } 479 | currentControllerMethod = ''; 480 | } 481 | 482 | //if back in the global context of the file (un-nested), and the previous branch was determined to be a controller method, and DB schema interactions were found in this method, populate the controllerSchemas object with relevant info (found in schemaInteractions object) 483 | if (curr.parent.type === 'Program') { 484 | console.log('schemaInt', schemaInteractions) 485 | if (currentControllerMethod && Object.keys(schemaInteractions).length) 486 | controllerSchemas[filePath] = schemaInteractions; 487 | console.log('controllerSchemas', controllerSchemas) 488 | currentControllerMethod = ''; 489 | if (curr.node.type === 'ExpressionStatement') 490 | expressionStatementPossible = true; 491 | if (curr.node.type === 'VariableDeclaration') 492 | variableStatementPossible = true; 493 | } 494 | 495 | //check for files imported locally using ES6 syntax 496 | if ( 497 | curr.isStringLiteral() && 498 | curr.node.value.includes('./') && 499 | curr.parent.type === 'ImportDeclaration' 500 | ) { 501 | const importDecPath = curr.findParent((curr) => 502 | curr.isImportDeclaration() 503 | ); 504 | const importLabels = importDecPath.node.specifiers.map( 505 | (obj) => obj.local.name 506 | ); 507 | const currDirectory = path.dirname(filePath); 508 | const afp = path.resolve(currDirectory, curr.node.value); 509 | const afpArray = allFiles.filter((file) => file.includes(afp)); 510 | if (afpArray.length) 511 | importLabels.forEach((label) => (imports[label] = afpArray[0])); 512 | } 513 | 514 | //check for files imported locally using ES5 syntax 515 | if ( 516 | curr.isStringLiteral() && 517 | curr.node.value.includes('./') && 518 | curr.parent.type === 'CallExpression' 519 | ) { 520 | if (curr.parent.callee.name === 'require') { 521 | let importLabel = findVariableName(curr); 522 | const currDirectory = path.dirname(filePath); 523 | const afp = path.resolve(currDirectory, curr.node.value); 524 | 525 | const afpArray = allFiles.filter((file) => file.includes(afp)); 526 | if (typeof importLabel === 'string') importLabel = [importLabel]; 527 | if (afpArray.length) 528 | importLabel.forEach((label) => (imports[label] = afpArray[0])); 529 | } 530 | console.log('imports', imports) 531 | } 532 | 533 | //filtering for IDENTIFIER: 534 | //that has a parent that is an OBJECT PROPERTY, where the object property has a child VALUE that is of type FUNCTIONEXPRESSION or ARROWFUNCTIONEXPRESSION 535 | if ( 536 | (expressionStatementPossible || variableStatementPossible) && 537 | !currentControllerMethod && 538 | curr.isIdentifier() && 539 | curr.parent.type === 'ObjectProperty' && 540 | curr.parent.value.type === 'ArrowFunctionExpression' 541 | ) { 542 | currentControllerMethod = curr.node.name; 543 | } 544 | 545 | //OR: 546 | //that has a parent that is a MEMBEREXPRESSION, a grandparent that is an ASSIGNMENTEXPRESSION, and an uncle RIGHT property that is an ARROWFUNCTIONEXPRESSION with a params child of length 3; 547 | if ( 548 | variableStatementPossible && 549 | !currentControllerMethod && 550 | curr.isIdentifier() && 551 | curr.parent.property && 552 | curr.parent.type === 'MemberExpression' && 553 | curr.parentPath.parent.type === 'AssignmentExpression' && 554 | curr.parentPath.parent.right 555 | ) { 556 | if (curr.parent.property.name === curr.node.name) 557 | currentControllerMethod = curr.node.name; 558 | console.log('currentControllerMethod', currentControllerMethod) 559 | } 560 | 561 | if ( 562 | currentControllerMethod && 563 | curr.isIdentifier() && 564 | Object.hasOwn(imports, curr.node.name) && 565 | curr.parent.type === 'MemberExpression' && 566 | curr.parent.property 567 | ) { 568 | console.log('currentControllerMethod2', currentControllerMethod) 569 | schemaInteractions[currentControllerMethod] 570 | ? schemaInteractions[currentControllerMethod].push(curr.node.name) 571 | : (schemaInteractions[currentControllerMethod] = [curr.node.name]); 572 | } 573 | }, 574 | }); 575 | console.log('schemaInteractions', schemaInteractions, 'controllerSchemas', controllerSchemas); 576 | }; 577 | 578 | const schemas = {}; 579 | const schemaKey = {}; 580 | 581 | //function for traversing mongoose model files and populating the above schemaKey object with each schema name and its associated schema structure 582 | const traverseMongooseFile = ( 583 | ast, 584 | filePath, 585 | mongooseLabel, 586 | schemaInstance 587 | ) => { 588 | traverse(ast, { 589 | enter(curr) { 590 | //check for new schema instances 591 | if ( 592 | curr.isIdentifier() && 593 | curr.node.name === schemaInstance && 594 | curr.parent.type === 'NewExpression' 595 | ) { 596 | const propsArray = curr.parent.arguments[0].properties; 597 | const schemaName = findVariableName(curr); 598 | 599 | //helper func used to create a clone of the schema as found in the file 600 | const populateSchema = (propsArray) => { 601 | const output = {}; 602 | if (propsArray) { 603 | propsArray.forEach((prop) => { 604 | const key = prop.key.name; 605 | prop.value.type === 'Identifier' 606 | ? (output[key] = prop.value.name) 607 | : prop.value.type === 'StringLiteral' || 608 | prop.value.type === 'BooleanLiteral' 609 | ? (output[key] = prop.value.value) 610 | : prop.value.type === 'ObjectExpression' 611 | ? (output[key] = populateSchema(prop.value.properties)) 612 | : prop.value.type === 'ArrayExpression' 613 | ? (output[key] = prop.value.elements.map((obj) => 614 | populateSchema(obj.properties) 615 | )) 616 | : (output[key] = 'UNKOWN VALUE'); 617 | }); 618 | } 619 | return output; 620 | }; 621 | 622 | //set the schema variable name to the schema 623 | schemas[schemaName] = populateSchema(propsArray); 624 | console.log('schemas', schemas); 625 | } 626 | 627 | //populate schemaKey object with the label each schema is being exported as set to the schema itself 628 | if ( 629 | curr.isIdentifier() && 630 | curr.node.name === mongooseLabel && 631 | curr.parent.type === 'MemberExpression' && 632 | curr.parent.property.name === 'model' 633 | ) { 634 | const callExpPath = curr.findParent((curr) => 635 | curr.isCallExpression() 636 | ); 637 | let schemaExport; 638 | if (Object.hasOwn(schemas, callExpPath.node.arguments[1].name)) { 639 | console.log(callExpPath.node.arguments[1].name, 'zzzzz') 640 | const schemaExport = findVariableName(curr) || callExpPath.node.arguments[0].value[0].toUpperCase() + callExpPath.node.arguments[0].value.slice(1); 641 | schemaKey[schemaExport] = 642 | schemas[callExpPath.node.arguments[1].name]; 643 | } 644 | } 645 | }, 646 | }); 647 | console.log('schemaKey', schemaKey); 648 | return; 649 | }; 650 | 651 | const allFiles = res.locals.allFiles; 652 | 653 | //iterate through array of files, convert each to AST, and invoke function to traverse the AST 654 | allFiles.forEach((filePath) => { 655 | const ast = parseFile(filePath); 656 | // console.log('ast'); 657 | traverseServerAST(ast, filePath); 658 | }); 659 | 660 | const output = {}; 661 | 662 | //populate the above output object with endpoints distributed from the root server file to router files, and their associated methods, schema labels, and schemas 663 | Object.keys(linksToRouter).forEach((route) => { 664 | // console.log('linksToRouter', linksToRouter, 'aRRLTC', allRouterRoutesLeadingToController) 665 | if ( 666 | Object.hasOwn(allRouterRoutesLeadingToController, linksToRouter[route]) 667 | ) { 668 | Object.keys( 669 | allRouterRoutesLeadingToController[linksToRouter[route]] 670 | ).forEach((key) => { 671 | // console.log('route', route, 'key', key) 672 | Object.keys( 673 | allRouterRoutesLeadingToController[linksToRouter[route]][key] 674 | ).forEach((method) => { 675 | // console.log('method', method, route, key) 676 | allRouterRoutesLeadingToController[linksToRouter[route]][key][ 677 | method 678 | ].forEach((controllerMethod) => { 679 | // console.log('controllerMethod', controllerMethod, route, key, 'controllerSchemas', controllerSchemas) 680 | if (Object.hasOwn(controllerSchemas, controllerMethod.path)) { 681 | if ( 682 | Object.hasOwn( 683 | controllerSchemas[controllerMethod.path], 684 | controllerMethod.middlewareName 685 | ) 686 | ) { 687 | controllerSchemas[controllerMethod.path][ 688 | controllerMethod.middlewareName 689 | ].forEach((schema) => { 690 | if (schemaKey[schema]) { 691 | if (output[route + key]) { 692 | if ( 693 | output[route + key][method] && 694 | !output[route + key][method][schema] 695 | ) { 696 | output[route + key][method][schema] = schemaKey[schema]; 697 | } else if (!output[route + key][method]) { 698 | output[route + key][method] = { 699 | [schema]: schemaKey[schema], 700 | }; 701 | } 702 | } else { 703 | output[route + key] = { 704 | [method]: { [schema]: schemaKey[schema] }, 705 | }; 706 | } 707 | } 708 | }); 709 | } 710 | } 711 | }); 712 | }); 713 | }); 714 | } 715 | }); 716 | 717 | // { '/api/': { 718 | // 'GET': { 'Person': { 'name': 'String' } }, 719 | // 'POST': { 'Species': { 'type': 'String' } } 720 | // }, 721 | // { '/api/homeworld': { 722 | // 'GET': { 'Person': 'String' } } 723 | // } 724 | // } 725 | 726 | //populate the output object with endpoints distributed from the root server file directly to controller files, and their associated methods, schema labels, and schemas 727 | Object.keys(allServerRoutesLeadingToController).forEach((file) => { 728 | Object.keys(allServerRoutesLeadingToController[file]).forEach((route) => { 729 | Object.keys(allServerRoutesLeadingToController[file][route]).forEach( 730 | (method) => { 731 | allServerRoutesLeadingToController[file][route][method].forEach( 732 | (middlewareMethod) => { 733 | if (Object.hasOwn(controllerSchemas, middlewareMethod.path)) { 734 | if ( 735 | Object.hasOwn( 736 | controllerSchemas[middlewareMethod.path], 737 | middlewareMethod.middlewareName 738 | ) 739 | ) { 740 | controllerSchemas[middlewareMethod.path][ 741 | middlewareMethod.middlewareName 742 | ].forEach((schema) => { 743 | if (schemaKey[schema]) { 744 | if (output[route]) { 745 | if ( 746 | output[route][method] && 747 | !output[route][method][schema] 748 | ) { 749 | output[route][method][schema] = schemaKey[schema]; 750 | } else if (!output[route][method]) { 751 | output[route][method] = { 752 | [schema]: schemaKey[schema], 753 | }; 754 | } 755 | } else 756 | output[route] = { 757 | [method]: { [schema]: schemaKey[schema] }, 758 | }; 759 | } 760 | }); 761 | } 762 | } 763 | } 764 | ); 765 | } 766 | ); 767 | }); 768 | }); 769 | 770 | console.log('output', output); 771 | res.locals.serverRoutes = output; 772 | 773 | return next(); 774 | }; 775 | 776 | module.exports = serverASTController; 777 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | const app = express(); 4 | // port will be listening on 3000 5 | const port = 3000; 6 | const cors = require('cors'); 7 | 8 | app.use(cors({ 9 | origin: 'http://localhost:5173', 10 | })); 11 | 12 | const fsController = require('./controllers/fsController'); 13 | const componentController = require('./controllers/componentController'); 14 | const serverASTController = require('./controllers/serverASTController'); 15 | 16 | app.use(express.json()); 17 | 18 | app.post('/components', fsController.getArrayOfFilePaths, componentController.parseAll, (req, res) => { 19 | // console.log('hello', res.locals.components); 20 | res.status(201).json(res.locals.components); 21 | }) 22 | 23 | app.post('/server', fsController.getArrayOfFilePaths, serverASTController.parseAll, (req, res) => { 24 | res.status(201).json(res.locals.serverRoutes); 25 | }) 26 | // 'res.locals.serverOutput' Object shape: 27 | // { '/api/': { 28 | // 'GET': { 'Person': { 'name': 'String' } }, 29 | // 'POST': { 'Species': { 'type': 'String' } } 30 | // }, 31 | // { '/api/homeworld': { 32 | // 'GET': { 'Person': 'String' } } 33 | // } 34 | // } 35 | 36 | 37 | app.get('/code', componentController.getCode, (req, res) => { 38 | console.log('res.locals', typeof res.locals.componentCode) 39 | res.status(200).json(res.locals.componentCode); 40 | }) 41 | 42 | app.get('/', (req, res) => { 43 | res.status(404).send('Not Found'); 44 | }); 45 | 46 | 47 | 48 | // if (process.env.NODE_ENV === "development") { 49 | // app.listen(port, () => { 50 | // console.log(`Server listening on port ${port}`); 51 | // }); 52 | // } 53 | 54 | module.exports = app; 55 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/.DS_Store -------------------------------------------------------------------------------- /src/main/controllers/componentController.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path2 = require('path'); 3 | const parser = require('@babel/parser'); 4 | // const { isIdentifier, isStringLiteral } = require('typescript'); 5 | const traverse = require('@babel/traverse').default; 6 | 7 | //readFileSync method of the fs module is used to grab all the code from 'filePath', 8 | //then the parse method in babel parser is used to create and return an AST version of 9 | //this file. plugins necessary to work with JSX and typescript. 10 | const parseFile = (filePath) => { 11 | const fileCode = fs.readFileSync(filePath, 'utf-8'); 12 | return parser.parse(fileCode, { 13 | sourceType: 'module', 14 | plugins: ['jsx', 'typescript'] 15 | }); 16 | } 17 | 18 | 19 | export const parseAllComponents = (event, args, res) => { 20 | // const projectPath = req.body.filePath; 21 | // if (projectPath.length === 0) next(); 22 | let components = {}; 23 | 24 | const templateLiteralRouteParser = (node) => { 25 | let quasis = node.quasis; 26 | let fullRoute = ``; 27 | for (let i = 0; i { 36 | 37 | //variables in closure so they persist as we traverse from node to node (these could probably also be declared between 'traverse' and 'enter') 38 | const potentialChildren = []; 39 | let children = {}; 40 | let current = ''; 41 | let isComponent = false; 42 | let mightBeComponent = false; 43 | const noWayThisWorksCache = {}; 44 | let hardNO = false; 45 | 46 | let fetchPrimed = false; 47 | let ajaxRequests = []; 48 | let xmlHttpReq; 49 | 50 | let axiosLabel; 51 | 52 | let nextJSLinkImported = false; 53 | 54 | //babel traverse used to traverse the passed in ast 55 | traverse(ast, { 56 | 57 | //for each node (i.e. 'path') in the 'ast', the 'enter' method will be invoked 58 | enter(path) { 59 | 60 | //check if we are in the global scope within the file (i.e. not nested). if so, we 61 | //should check if the previously traversed code block was a component 62 | if (path.parent.type === "Program") { 63 | //if the prev code block was a component, add the information from this component to the 'components' object 64 | if (current !== '' && isComponent && !hardNO) { 65 | components[filePath] = { data: { label: current }, children: {...children}, ajaxRequests, id: filePath }; 66 | } 67 | potentialChildren.push(current); 68 | 69 | //reset component information whenever global space is re-entered 70 | children = {}; 71 | current = ''; 72 | isComponent = false; 73 | ajaxRequests = []; 74 | hardNO = false; 75 | 76 | //if the first node in the next code block in the global space of not 1 of the following types, it cannot be a component (possibly not necessary. also, possibly don't need to include "ExpressionStatement") 77 | mightBeComponent = ((path.node.type === "VariableDeclaration" || path.node.type === "ClassDeclaration" || path.node.type === "FunctionDeclaration" || path.node.type === "ExpressionStatement" || path.node.type === "ExportDefaultDeclaration" || path.node.type === "ExportNamedDeclaration")) ? true : false; 78 | } 79 | 80 | //set 'current' to the first Identifier of a new code block if the following conditions are met (the name of the identifier being render is a special case used to itentify the root file. there is probably a better solution) 81 | if (path.isIdentifier() && (path.parent.type === "VariableDeclarator" || path.parent.type === "ClassDeclaration" || path.parent.type === "FunctionDeclaration" || path.node.name === "render") && current === '' && mightBeComponent) { 82 | current = path.node.name !== "render" ? path.node.name : "root"; 83 | } 84 | 85 | //check for imported variables 86 | if (path.isIdentifier() && (path.parent.type === "ImportDefaultSpecifier" || path.parent.type === "ImportSpecifier")) { 87 | 88 | //assign relativePath to the RFP this variable is being imported from 89 | const relativePath = path.parentPath.parent.source.value 90 | 91 | //filter out non-local paths (filters out modules, etc.) 92 | if (relativePath.includes('./') || relativePath.includes('../')) { 93 | 94 | //grab the directory of this file 95 | const currDirectory = path2.dirname(filePath) 96 | 97 | //grab the absolute filepath of the imported file 98 | const afp = path2.resolve(currDirectory, relativePath); 99 | 100 | //grab the file path within the allFiles array that matches "afp" (this is necessary bc 'afp' might not have .js/.jsx/.ts/.tsx at the end of it) 101 | const componentAFP = allFiles.filter(file => file.includes(afp))[0]; 102 | 103 | //add this imported variable as a key in potentialChildren array with its value being its AFP 104 | potentialChildren[path.node.name] = componentAFP; 105 | } 106 | 107 | //prime axios handler if axios is imported 108 | if (relativePath === "axios") axiosLabel = path.node.name; 109 | } 110 | 111 | //if we find a 'callee' with the name 'fetch', we know a fetch is being invoked, so we reassign 'fetchPrimed' to true, which will trigger the program to look for the route (...in the condition directly below this) 112 | if (path.isCallExpression()) { 113 | const callee = path.node.callee; 114 | if (callee.type === "Identifier" && callee.name === "fetch") fetchPrimed = true; 115 | } 116 | 117 | //the first either TemplateLiteral or StringLiteral node after fetch should be the route string 118 | if (fetchPrimed && (path.node.type === "TemplateLiteral" || path.node.type === "StringLiteral")) { 119 | let route = ''; 120 | let fullRoute = ``; 121 | let method = 'GET'; 122 | 123 | //a TemplateLiteral node will have 2 properties: expressions, which store variables, and quasis, which store normal chars in the string. Order of data will always altername between quasi and expressions, and length of quasis will always be length of expressions + 1. This logic can be used to reconstruct this literal string. 124 | if (path.node.type === "TemplateLiteral") { 125 | route = path.node.quasis[0].value.raw; 126 | fullRoute = templateLiteralRouteParser(path.node); 127 | } else if (path.node.type === "StringLiteral") fullRoute = route = path.node.value; 128 | 129 | //arguments prop is sibling of the above literal prop in the AST 130 | const argArrr = path.parentPath.node.arguments; 131 | let objExpIdx = -1; 132 | 133 | //ObjectExpression node will exist in arguments array if fetch contains body as 2nd arg. if none is found, fetch method must be GET 134 | if (argArrr) { 135 | argArrr.forEach((sibling, i) => sibling.type === "ObjectExpression" ? objExpIdx = i : null); 136 | 137 | if (objExpIdx > -1) { 138 | const objProps = argArrr[objExpIdx].properties; 139 | objProps.forEach(prop => { 140 | if (prop.key.name === 'method') method = prop.value.value; 141 | }) 142 | } 143 | } 144 | 145 | //push route and method data into ajaxRequests array, which will be added to component object 146 | ajaxRequests.push({ route, fullRoute, method }); 147 | fetchPrimed = false; 148 | // console.log('this is a route?', ajaxRequests) 149 | } 150 | 151 | //XMLHttpRequest handlers 152 | if (path.isIdentifier() && path.node.name === "XMLHttpRequest" && path.parent.type === "NewExpression") { 153 | const declarationPath = path.findParent((path) => path.isVariableDeclarator()); 154 | if (declarationPath) xmlHttpReq = declarationPath.node.id.name; 155 | // console.log('xmlhttprequest') 156 | } 157 | 158 | if (path.isIdentifier() && path.node.name === xmlHttpReq && path.parent.property.name === "open") { 159 | const callExpressionPath = path.findParent((path) => path.isCallExpression()); 160 | if (callExpressionPath) { 161 | const argsArrr = callExpressionPath.node.arguments; 162 | const method = argsArrr[0].value; 163 | const route = argsArrr[1].value; 164 | ajaxRequests.push({ route, fullRoute: route, method }); 165 | } 166 | } 167 | 168 | // axios handler 169 | if (axiosLabel && path.isIdentifier() && path.node.name === axiosLabel && path.findParent((path) => path.isCallExpression())) { 170 | let route; 171 | let method; 172 | let fullRoute; 173 | if (path.parent.type === "MemberExpression") { 174 | const callExpressionPath = path.findParent((path) => path.isCallExpression()); 175 | method = path.parent.property.name.toUpperCase(); 176 | const callExpArgsArr = callExpressionPath.node.arguments[0]; 177 | if (callExpArgsArr.type === "StringLiteral") { 178 | route = fullRoute = callExpArgsArr.value; 179 | } else if (callExpArgsArr.type === "TemplateLiteral") { 180 | route = callExpArgsArr.quasis[0].value.raw; 181 | fullRoute = templateLiteralRouteParser(callExpArgsArr); 182 | } else if (callExpArgsArr.type === "MemberExpression") { 183 | route = fullroute = `${callExpArgsArr.object.name}.${callExpArgsArr.property.name}`; 184 | } 185 | } else if (path.parent.arguments[0].properties) { 186 | path.parent.arguments[0].properties.forEach(prop => { 187 | if (prop.key.name === "method") method = prop.value.value.toUpperCase(); 188 | if (prop.key.name === "url") { 189 | if (prop.value.type === "TemplateLiteral") { 190 | route = prop.value.quasis[0].value.raw; 191 | fullRoute = templateLiteralRouteParser(prop.value); 192 | } else route = fullRoute = prop.value.value; 193 | } 194 | }) 195 | } 196 | ajaxRequests.push({ route, fullRoute, method }); 197 | } 198 | 199 | 200 | 201 | //this is an attempt to filter out false positives from test files 202 | if (path.isIdentifier() && (path.node.name === "describe" || path.node.name === "xdescribe" || path.node.name === "test" || path.node.name === "xtest") && path.parent.type === "CallExpression" && path.parentPath.parent.type === "ExpressionStatement") hardNO = true; 203 | 204 | //same as below 205 | if (path.isIdentifier() && (path.parent.type === "JSXExpressionContainer" || path.parentPath.parent.type === "JSXExpressionContainer") && Object.keys(potentialChildren).includes(path.node.name)) { 206 | isComponent === true; 207 | const newChildPath = potentialChildren[path.node.name]; 208 | 209 | if (newChildPath) children[newChildPath] = null; 210 | } 211 | 212 | //if the current node has a pattern that is consistently true for child JSX components, and it was deemed to be a potential child via navigating the import statements above, then we can add its file path to the children array for the current component 213 | if (path.isJSXIdentifier()) { 214 | isComponent = true; 215 | if (path.parentPath.parent.type === "JSXElement" && Object.keys(potentialChildren).includes(path.node.name)) { 216 | const newChildPath = potentialChildren[path.node.name]; 217 | 218 | if (newChildPath) children[newChildPath] = null; 219 | } 220 | } 221 | 222 | //preliminary logic for handling next.JS 223 | if (path.isStringLiteral() && path.node.value === "next/link" && path.parent.type === "ImportDeclaration") { 224 | const specifiers = path.parent.specifiers; 225 | specifiers.forEach(obj => { 226 | if (obj.local.name === "Link") nextJSLinkImported = true; 227 | }) 228 | } 229 | 230 | if (nextJSLinkImported && path.isJSXIdentifier() && path.node.name === "Link" && path.parent.type === "JSXOpeningElement") { 231 | const attributes = path.parent.attributes; 232 | attributes.forEach(obj => { 233 | let route; 234 | if (obj.name.name === "href") { 235 | let route = obj.value.type === "StringLiteral" ? obj.value.value : null; 236 | let expression = route ? null : obj.value.expression; 237 | if (!route) { 238 | if (expression.type === "StringLiteral") { 239 | route = expression.value; 240 | } else if (expression.type === "TemplateLiteral") { 241 | route = expression.quasis[0].value.raw; 242 | } 243 | } 244 | const currDirectory = path2.dirname(filePath); 245 | const childFilePath = path2.resolve(currDirectory, route) 246 | const afp = allFiles.filter(file => file.includes(childFilePath))[0]; 247 | children[afp] = null; 248 | } 249 | }) 250 | 251 | //TODO: if we find a Link tag, store this link tag and the filePath it is located in inside of a "links" object. After traversing all files, identify the component for which the link component is a child. add the route as a child of that component.(?) 252 | 253 | } 254 | 255 | 256 | } 257 | }) 258 | 259 | //if the component is exported in this way: "export const ComponentName = () => {...}", it might be the case that there is no code after the component code block, in which case we should check if the previous code block was a component 1 last time. 260 | if (current !== '' && isComponent && !hardNO) components[filePath] = { data: {label: current}, children: {...children}, ajaxRequests, id: filePath }; 261 | } 262 | 263 | 264 | const allFiles = res.locals.allFiles; 265 | console.log(allFiles); 266 | 267 | //iterate through array of files, convert each to AST, and invoke above function to traverse the AST 268 | allFiles.forEach((filePath) => { 269 | const ast = parseFile(filePath); 270 | // console.log('ast'); 271 | traverseAST(ast, filePath); 272 | }) 273 | 274 | //this logic attempts to reassign all children filePath keys to point to their respective filePath Object within the components object 275 | Object.values(components).forEach(component => { 276 | Object.keys(component.children).forEach(filePath => { 277 | 278 | component.children[filePath] = components[filePath]; 279 | }) 280 | }) 281 | 282 | //convert values of children properties to array of Objects, rather than object of filePath keys where the value is the object 283 | Object.values(components).forEach(component => { 284 | const arrayOfChildFilePaths = Object.keys(component.children); 285 | 286 | component.children = arrayOfChildFilePaths; 287 | }) 288 | 289 | console.log('components', components) 290 | res.locals.components = components 291 | 292 | 293 | } 294 | 295 | //used to grab code from the file associated with a component, to be displayed in details section on front end 296 | export const getCode = async (event, args, res) => { 297 | try { 298 | const { id } = args; 299 | const decodedId = decodeURIComponent(id); 300 | 301 | const fileCode = fs.readFileSync(decodedId, 'utf-8'); 302 | res.locals.componentCode = fileCode; 303 | } catch(err) { 304 | console.error(err); 305 | throw new Error('Error in componentController.getCode') 306 | } 307 | } -------------------------------------------------------------------------------- /src/main/controllers/fsController.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | 5 | export const getArrayOfFilePaths = (event, args, res) => { 6 | const projectPath = args.filePath; 7 | if (projectPath.length === 0) return; 8 | 9 | //dirPath is initially the root directory. This function recursively navigates 10 | //through this directory, passing each file path into arrayOfFiles, and eventually 11 | //returning this array 12 | const getAllFiles = (dirPath, arrayOfFiles = []) => { 13 | const files = fs.readdirSync(dirPath); 14 | 15 | 16 | files.forEach((file) => { 17 | if (fs.statSync(path.join(dirPath, file)).isDirectory()) { 18 | arrayOfFiles = getAllFiles(path.join(dirPath, file), arrayOfFiles); 19 | } else { 20 | arrayOfFiles.push(path.join(dirPath, file)); 21 | } 22 | }) 23 | return arrayOfFiles; 24 | } 25 | 26 | //invoking above 'getAllFiles' function to grab an array of all files in 'projectPath', then filtering for only files that could be react components 27 | const allFiles = getAllFiles(projectPath).filter((file) => file.endsWith('.jsx') || file.endsWith('.js') || file.endsWith('.tsx') || file.endsWith('.ts')); 28 | 29 | console.log('af', allFiles); 30 | res.locals.allFiles = allFiles; 31 | 32 | } -------------------------------------------------------------------------------- /src/main/controllers/serverASTController.js: -------------------------------------------------------------------------------- 1 | //BREAKDOWN 2 | //1. IDENTIFY FILE TYPE AS EITHER: 1) SERVER.JS 2) ROUTER 3) CONTROLLER 4) MODEL 3 | //2. PASS FILE INTO ANOTHER TRAVERSAL FUNCTION SPECIFIC FOR THAT FILE TYPE 4 | //3. PASS FILE INFORMATION INTO OBJECT SPECIFIC TO THAT FILE TYPE 5 | //4. ITERATE THROUGH FILE OBJECTS ONE AT A TIME IN THE ORDER SHOWN ABOVE IN STEP 1. USE THIS INFORMATION TO GRADUALLY BUILD AN OUTPUT OBJECT CONTAINING INFORMATION ABOUT EACH ROUTE. 6 | 7 | //SERVER.JS LOGIC 8 | //-LOOK FOR IMPORTED FILES THAT ARE INCLUDED IN ARRAY OF ALL SERVER FILES 9 | //DETERMINE AFP (ABSOLUTE FILES PATH) AND SAVE THIS AFP ALONG WITH THE VARIABLE NAME IT IS IMPORTED AS 10 | //-ALSO IDENTIFY THE LABEL OF EXPRESS INSTANCE 11 | //-WHEN 'USE' METHOD IS INVOKED ON EXPRESS INSTANCE, AND FIRST ARG IS A ROUTE: 12 | //IF 2ND ARG IS THE EXACT MATCH OF ONE OF THE IMPORT LABELS, IT MUST BE A ROUTER (?) 13 | 14 | //-WHEN CRUD METHOD IS INVOKED ON EXPRESS INSTANCE, FIRST ARG SHOULD BE COMPLETE ROUTE, FOLLOWED BY INDEFINITE NUMBER OF CONTROLLER INVOCATIONS. WE CAN CHECK IF THE FOLLOWING ARG SPLIT ON '.' IS INCLUDED IN IMPORT LABELS. IF SO, ADD IT TO OBJECT 15 | //-THERE MAY BE LOGIC TO GET/POST/ETC DATA TO/FROM DB MODEL INSIDE SERVER.JS, EITHER AFTER MIDDLEWARE OR IN PLACE OF IT. 16 | //-FIRST WORK ON TAKING CARE OF LOGIC INVOLVING DB CALLS FROM MIDDLEWARE 17 | 18 | //ROUTER LOGIC 19 | //-DISTINGUISHING FEATURE = IMPORTING EXPRESS, THEN CREATING EXPRESS.ROUTER() INVOCATION (?) 20 | // 21 | 22 | //CONTROLLER LOGIC 23 | //-DISTINGUISHING FEATURE = USING REQ, RES, NEXT, BUT NOT IMPORTING/INVOKING EXPRESS (?) 24 | //ALSO FILE PATH CAN BE MATCHED WITH MIDDLEWARE FOUND IN SERVER.JS/ROUTER FILES 25 | //CHECK IMPORTS FROM LOCAL FILES (SHOULD INCLUDE ANY MODELS) AND SAVE THE VARIABLE NAMES 26 | //IF DB MANIPULATION METHOD IS INVOKED ON ONE OF THESE VARIABLES, SAVE THE RELEVANT INFO IN OBJECT 27 | 28 | //DB LOGIC 29 | //FEATURES: IMPORTING MONGOOSE AND CREATING SCHEMA INSTANCE AND INVOKING CONNECT 30 | //CHALLENGE = GRABBING SCHEMA DATA. POTENTIALLY WILL HAVE TO USE FS MODULE INSTEAD OF AST 31 | 32 | const fs = require('fs'); 33 | const path = require('path'); 34 | const parser = require('@babel/parser'); 35 | // const { isIdentifier, isStringLiteral } = require('typescript'); 36 | const traverse = require('@babel/traverse').default; 37 | 38 | const CRUDMETHODS = ['get', 'post', 'patch', 'put', 'delete']; 39 | 40 | export const parseAllServerFiles = (event, args, res) => { 41 | 42 | //readFileSync method of the fs module is used to grab all the code from 'filePath', 43 | //then the parse method in babel parser is used to create and return an AST version of 44 | //this file. plugins necessary to work with JSX and typescript. 45 | const parseFile = (filePath) => { 46 | const fileCode = fs.readFileSync(filePath, 'utf-8'); 47 | return parser.parse(fileCode, { 48 | sourceType: 'module', 49 | plugins: ['jsx', 'typescript'], 50 | }); 51 | }; 52 | 53 | //helper functions 54 | const findVariableName = (curr) => { 55 | const varDecPath = curr.findParent((curr) => curr.isVariableDeclarator()); 56 | if (!varDecPath) return; 57 | if (varDecPath.node.id.name) return varDecPath.node.id.name; 58 | if (varDecPath.node.id.properties) 59 | return varDecPath.node.id.properties.map((prop) => prop.value.name); 60 | }; 61 | 62 | const findImportLabel = (curr) => { 63 | const importDecPath = curr.findParent((curr) => curr.isImportDeclaration()); 64 | return importDecPath.node.specifiers[0].local.name; 65 | }; 66 | 67 | const addES6Import = (curr, filePath, importLabels) => { 68 | const importLabel = findImportLabel(curr); //grab the directory of this file 69 | const currDirectory = path.dirname(filePath); //grab the absolute filepath of the imported file //IRENE: using directory name of current filepath 70 | const afp = path.resolve(currDirectory, curr.node.value); 71 | console.log('afp', afp); 72 | const afpArray = allFiles.filter((file) => file.includes(afp)); //if imported file exists in server directory, add its label name as a key in importLabels obj w value being its afp 73 | if (afpArray.length) importLabels[importLabel] = afpArray[0]; 74 | // console.log(importLabels, 'importLabels'); // importLabels = {starWarsController: /Users/Codesmith/App/Controllers/starWarsControllers.js} this is temporary 75 | return importLabels; 76 | }; 77 | 78 | const addES5Import = (curr, filePath, importLabels) => { 79 | const importLabel = findVariableName(curr); 80 | const currDirectory = path.dirname(filePath); 81 | const afp = path.resolve(currDirectory, curr.node.value); 82 | 83 | const afpArray = allFiles.filter((file) => file.includes(afp)); 84 | if (afpArray.length) importLabels[importLabel] = afpArray[0]; 85 | // console.log(importLabels, 'importLabels') 86 | return importLabels; 87 | }; 88 | 89 | //traverse all server files and categorize them as either DB model files, controller files, router files, or the root server file 90 | const traverseServerAST = (ast, filePath) => { 91 | let importExpress = false; 92 | let importMongoose = false; 93 | let expressLabel; 94 | let mongooseLabel; 95 | let expressInstance; 96 | let routerInstance; 97 | let schemaInstance; 98 | let explicitRouter = false; 99 | 100 | traverse(ast, { 101 | enter(curr) { 102 | 103 | //check for specific ES5 syntax imports so we can identify 1) that express, mongoose, and/or express Router is being imported, and 2) what the import is labeled 104 | if ( 105 | curr.isIdentifier() && 106 | curr.node.name === 'require' && 107 | curr.parent.type === 'CallExpression' 108 | ) { 109 | const argument = curr.parent.arguments[0]; 110 | if (argument.type === 'StringLiteral') { 111 | if (argument.value === 'express') importExpress = true; 112 | if (argument.value === 'mongoose') importMongoose = true; 113 | } else if (argument.type === 'TemplateLiteral') { 114 | if (argument.quasis[0].value.raw === 'express') 115 | importExpress = true; 116 | if (argument.quasis[0].value.raw === 'mongoose') 117 | importMongoose = true; 118 | } 119 | if (importExpress || importMongoose) { 120 | const label = findVariableName(curr); 121 | label === 'express' 122 | ? (expressLabel = label) 123 | : importMongoose 124 | ? (mongooseLabel = label) 125 | : null; 126 | // console.log(expressLabel, 'expressLabel'); 127 | importExpress = false; 128 | } 129 | } 130 | 131 | //Same logic as above, but for ES6 syntax. TODO: account for possiblity of an import being imported "as" something. i.e.: import { Router as Routeriffic } from 'express' 132 | if ( 133 | curr.isStringLiteral() && 134 | (curr.node.value === 'mongoose' || curr.node.value === 'express') && 135 | curr.parent.type === 'ImportDeclaration' 136 | ) { 137 | if (curr.node.value === 'mongoose') { 138 | importMongoose = true; 139 | mongooseLabel = curr.parent.specifiers[0].local.name; 140 | } 141 | if (curr.node.value === 'express') { 142 | if (curr.parent.specifiers.length >= 1) { 143 | for (let specifier of curr.parent.specifiers) { 144 | if (specifier.local.name === 'Router') { 145 | explicitRouter = true; 146 | importExpress = true; 147 | expressLabel = "express"; 148 | return; 149 | } 150 | } 151 | } 152 | importExpress = true; 153 | expressLabel = curr.parent.specifiers[0].local.name; 154 | } 155 | importExpress = importMongoose = false; 156 | } 157 | 158 | //if it is found that an instance of express has been created, it is assumed this must be the root server file, and so the function for traversing root server files is invoked, and the current traversal is stopped. 159 | if ( 160 | curr.isIdentifier() && 161 | curr.node.name === expressLabel && 162 | curr.parent.type === 'CallExpression' && 163 | curr.parent.arguments.length === 0 164 | ) { 165 | expressInstance = findVariableName(curr); 166 | // console.log('expinst', expressInstance) 167 | traverseServerFile(ast, filePath, expressLabel, expressInstance); 168 | curr.stop(); 169 | } 170 | 171 | //if it is found that a Router instance has been created, it is assumed this must be a router file, and so the func for traversing router files is invoked 172 | if ( 173 | curr.isIdentifier() && 174 | curr.node.name === expressLabel && 175 | curr.parent.type === 'MemberExpression' && 176 | curr.parent.property.name === 'Router' 177 | ) { 178 | routerInstance = findVariableName(curr); 179 | // console.log('routerInstance', routerInstance); 180 | traverseRouterFile(ast, filePath, expressLabel, routerInstance); 181 | curr.stop(); 182 | } 183 | 184 | //extension of above to cover possibility of Router being explicitly imported as itself (ES6) 185 | if ( 186 | curr.isIdentifier() && 187 | explicitRouter && 188 | curr.node.name === 'Router' && 189 | curr.parent.type === 'CallExpression' 190 | ) { 191 | const varDecPath = curr.findParent(curr => curr.isVariableDeclarator()); 192 | if (varDecPath) { 193 | if (varDecPath.node.id) { 194 | routerInstance = varDecPath.node.id.name; 195 | traverseRouterFile(ast, filePath, expressLabel, routerInstance); 196 | curr.stop(); 197 | } 198 | } 199 | } 200 | 201 | //if a mongoose Schema instance is found, assume this file is a models file 202 | if ( 203 | curr.isIdentifier() && 204 | curr.node.name === mongooseLabel && 205 | curr.parent.type === 'MemberExpression' 206 | ) { 207 | if (curr.parent.property.name === 'Schema') { 208 | schemaInstance = findVariableName(curr); 209 | traverseMongooseFile(ast, filePath, mongooseLabel, schemaInstance); 210 | curr.stop(); 211 | } 212 | } 213 | 214 | }, 215 | }); 216 | //if file was not identified as root, router, or model file, assume it is either a controller file, or negligible 217 | if (!expressInstance && !routerInstance && !schemaInstance) 218 | traverseControllerFile(ast, filePath); 219 | }; 220 | 221 | const linksToRouter = {}; 222 | const allServerRoutesLeadingToController = {}; //obj to hold all server routes that lead to a controller 223 | //TODO: STILL NEED TO ACCOUNT FOR MIDDLEWARE FUNCTIONALITY EXPLICITLY FOUND IN THE ROOT SERVER FILE (DB INTERACTIONS THAT OCCUR IN ROOT SERVER FILE) 224 | 225 | //function for traversing the root server file, checking for links to router and controller files 226 | const traverseServerFile = (ast, filePath, expressLabel, expressInstance) => { 227 | //used to store identified imports and their corresponding AFP 228 | let importLabels = {}; 229 | 230 | traverse(ast, { 231 | enter(curr) { 232 | //check for files imported locally using ES6 syntax 233 | if ( 234 | curr.isStringLiteral() && 235 | curr.node.value.includes('./') && 236 | curr.parent.type === 'ImportDeclaration' 237 | ) { 238 | importLabels = addES6Import(curr, filePath, importLabels); 239 | } 240 | 241 | //check for files imported locally using ES5 syntax 242 | if ( 243 | curr.isStringLiteral() && 244 | curr.node.value.includes('./') && 245 | curr.parent.type === 'CallExpression' 246 | ) { 247 | if (curr.parent.callee.name === 'require') { 248 | importLabels = addES5Import(curr, filePath, importLabels); 249 | } 250 | } 251 | 252 | //filter for invocations of the use method on the express instance (app.use(...)), and identify links to router files using this 253 | if ( 254 | curr.isIdentifier() && 255 | curr.node.name === expressInstance && 256 | curr.parent.type === 'MemberExpression' && 257 | curr.parent.property.name === 'use' 258 | ) { 259 | //pinpoint the array of args passed to this method invocation 260 | const callExpPath = curr.findParent((curr) => 261 | curr.isCallExpression() 262 | ); 263 | const args = callExpPath.node.arguments; 264 | 265 | //check for links to router files imported above 266 | if ( 267 | args.length > 1 && 268 | Object.hasOwn(importLabels, args[1].name) 269 | ) { 270 | // linksToRouter[importLabels[arguments[1].name]] = arguments[0].value; 271 | linksToRouter[args[0].value] = importLabels[args[1].name]; 272 | // '/auth': '/Users/cush572/Codesmith/TEST/ReacType/server/routers/auth.ts', 273 | // '/user-styles': '/Users/cush572/Codesmith/TEST/ReacType/server/routers/stylesRouter.ts' 274 | } 275 | } 276 | 277 | //TODO: refactor below and turn into helper function since this is similar to the condition inside traverseRouterFile function 278 | 279 | //check for server endpoints that bypass router files and link directly to middleware functions in controller files. Populate allServerRoutesLeadingToController obj accordingly. 280 | if ( 281 | curr.isIdentifier() && 282 | curr.node.name === expressInstance && 283 | curr.parent.type === 'MemberExpression' && 284 | CRUDMETHODS.includes(curr.parent.property.name) 285 | ) { 286 | const method = curr.parent.property.name; 287 | const callExpPath = curr.findParent((curr) => 288 | curr.isCallExpression() 289 | ); 290 | const args = callExpPath.node.arguments; 291 | const endpoint = args[0].value; 292 | args.slice(1, -1).forEach((arg) => { 293 | if (!arg.object) return; 294 | if (Object.hasOwn(importLabels, arg.object.name)) { 295 | //check if import label exists (we found import label arg.object.name inside the imports at top of page) 296 | const middlewareMethod = { 297 | path: importLabels[arg.object.name], 298 | middlewareName: arg.property.name, 299 | }; // { path: ${absoluteFilePathOfController}, middlewareName: ${nameOfMiddleware}} 300 | if (allServerRoutesLeadingToController[filePath]) { 301 | if (allServerRoutesLeadingToController[filePath][endpoint]) { 302 | if ( 303 | allServerRoutesLeadingToController[filePath][endpoint][ 304 | method 305 | ] 306 | ) { 307 | allServerRoutesLeadingToController[filePath][endpoint][ 308 | method 309 | ].push(middlewareMethod); 310 | } else { 311 | allServerRoutesLeadingToController[filePath][endpoint][ 312 | method 313 | ] = [middlewareMethod]; 314 | } 315 | } else { 316 | allServerRoutesLeadingToController[filePath][endpoint] = { 317 | [method]: [middlewareMethod], 318 | }; 319 | } 320 | } else { 321 | allServerRoutesLeadingToController[filePath] = { 322 | [endpoint]: { [method]: [middlewareMethod] }, 323 | }; 324 | } 325 | } 326 | }); 327 | } 328 | }, 329 | }); 330 | console.log( 331 | linksToRouter, 332 | 'linksToRouter', 333 | allServerRoutesLeadingToController, 334 | 'allServerRoutesLeadingToController' 335 | ); 336 | }; 337 | 338 | const allRouterRoutesLeadingToController = {}; //all routes in router that lead to controller 339 | 340 | 341 | //function for traversing router files, checking for links controller files and populating these links in the above object 342 | const traverseRouterFile = (ast, filePath, expressLabel, routerInstance) => { 343 | // 344 | let importLabels = {}; 345 | traverse(ast, { 346 | enter(curr) { 347 | //GET LOCALLY IMPORTED FILE LABEL: 348 | if ( 349 | curr.isStringLiteral() && 350 | curr.node.value.includes('./') && 351 | curr.parent.type === 'ImportDeclaration' 352 | ) { 353 | //IRENE: provides access to all the imports at the top of the file 354 | importLabels = addES6Import(curr, filePath, importLabels); 355 | } 356 | 357 | 358 | if ( 359 | curr.isStringLiteral() && 360 | curr.node.value.includes('./') && 361 | curr.parent.type === 'CallExpression' 362 | ) { 363 | if (curr.parent.callee.name === 'require') 364 | importLabels = addES5Import(curr, filePath, importLabels); 365 | } 366 | 367 | //FIND INSTANCE OF ROUTER AND FILTERING FOR THE CRUD METHODS WITHIN IT 368 | //TODO: refactor to helper function (same as function from above) 369 | if ( 370 | curr.isIdentifier() && 371 | curr.node.name === routerInstance && 372 | curr.parent.type === 'MemberExpression' && 373 | CRUDMETHODS.includes(curr.parent.property.name) 374 | ) { 375 | //IRENE: finding instance of router 376 | const method = curr.parent.property.name; //IRENE: trying to understand what specific method is being invoked here 377 | const callExpPath = curr.findParent((curr) => 378 | curr.isCallExpression() 379 | ); //IRENE: 380 | const args = callExpPath.node.arguments; //IRENE: trying to find the arguments of router.METHOD() 381 | const endpoint = args[0].value; //IRENE: we know first arg will always be the endpoint 382 | args.slice(1, -1).forEach((arg) => { 383 | if (!arg.object) return; 384 | //IRENE: we know last arg will be the middleware that handles sending the response back to client. we now need to just look at the arguments between the first and last so we can identify the controller/middleware 385 | if (Object.hasOwn(importLabels, arg.object.name)) { 386 | //IRENE: check if name of the argument in the argument object matches the label in importLabels object 387 | const middlewareMethod = { 388 | path: importLabels[arg.object.name], 389 | middlewareName: arg.property.name, 390 | }; // {path: ${absoluteFilePathOfController}, middlewareName: ${nameOfMiddleware}} 391 | //{{path: ${absoluteFilePathOfController}, middlewareName: ${nameOfMiddleware}}} 392 | if (allRouterRoutesLeadingToController[filePath]) { 393 | if (allRouterRoutesLeadingToController[filePath][endpoint]) { 394 | if ( 395 | allRouterRoutesLeadingToController[filePath][endpoint][ 396 | method 397 | ] 398 | ) { 399 | allRouterRoutesLeadingToController[filePath][endpoint][ 400 | method 401 | ].push(middlewareMethod); 402 | } else { 403 | allRouterRoutesLeadingToController[filePath][endpoint][ 404 | method 405 | ] = [middlewareMethod]; 406 | } 407 | } else { 408 | allRouterRoutesLeadingToController[filePath][endpoint] = { 409 | [method]: [middlewareMethod], 410 | }; 411 | } 412 | } else { 413 | console.log('doesn’t exist'); 414 | allRouterRoutesLeadingToController[filePath] = { 415 | [endpoint]: { [method]: [middlewareMethod] }, 416 | }; 417 | } 418 | //TEMPLATE: allRouterRoutesLeadingToController = { 419 | // [filePath]: { 420 | // [endpoint]: { 421 | // [GET]: [middlewareMethod], 422 | // [POST]: [middlewareMEthod] 423 | // } 424 | // } 425 | // } 426 | 427 | //EXAMPLE END RESULT: allRouterRoutesLeadingToController{ 428 | // '/Users/cush572/Codesmith/Week4/unit-10-databases/server/routes/api.js': { 429 | // '/': { get: Array }, 430 | // '/species': { get: Array }, 431 | // '/homeworld': { get: Array }, 432 | // '/film': { get: Array }, 433 | // '/character': { post: Array, delete: Array } 434 | // } 435 | // } 436 | } 437 | }); 438 | } 439 | }, 440 | }); 441 | }; 442 | 443 | 444 | const controllerSchemas = {}; 445 | 446 | //function to traverse controller files, identifying middleware methods and their corresponding interactions with database schemas, and populating this information in the above controllerSchemas object 447 | const traverseControllerFile = (ast, filePath, label, instance) => { 448 | const imports = {}; 449 | const schemaInteractions = {}; 450 | let currentControllerMethod; 451 | let expressionStatementPossible = false; 452 | let variableStatementPossible = false; 453 | 454 | console.log('CONTROLLER FILE PATH', filePath); 455 | traverse(ast, { 456 | enter(curr) { 457 | 458 | //account for cases where controller methods are explicitly defined within a controller object 459 | if ( 460 | variableStatementPossible && 461 | currentControllerMethod && 462 | curr.parent.type === 'ObjectExpression' 463 | ) { 464 | const callExpPath = curr.findParent(curr => curr.isCallExpression()) 465 | if (callExpPath) { 466 | if (callExpPath.node.callee) { 467 | 468 | if (callExpPath.node.callee.object) { 469 | if (Object.keys(imports).includes(callExpPath.node.callee.object.name)) return; 470 | } else if (callExpPath.node.callee.name) { 471 | if (callExpPath.node.callee.name === "next") return; 472 | } 473 | } 474 | } 475 | currentControllerMethod = ''; 476 | } 477 | 478 | //if back in the global context of the file (un-nested), and the previous branch was determined to be a controller method, and DB schema interactions were found in this method, populate the controllerSchemas object with relevant info (found in schemaInteractions object) 479 | if (curr.parent.type === 'Program') { 480 | console.log('schemaInt', schemaInteractions) 481 | if (currentControllerMethod && Object.keys(schemaInteractions).length) 482 | controllerSchemas[filePath] = schemaInteractions; 483 | console.log('controllerSchemas', controllerSchemas) 484 | currentControllerMethod = ''; 485 | if (curr.node.type === 'ExpressionStatement') 486 | expressionStatementPossible = true; 487 | if (curr.node.type === 'VariableDeclaration') 488 | variableStatementPossible = true; 489 | } 490 | 491 | //check for files imported locally using ES6 syntax 492 | if ( 493 | curr.isStringLiteral() && 494 | curr.node.value.includes('./') && 495 | curr.parent.type === 'ImportDeclaration' 496 | ) { 497 | const importDecPath = curr.findParent((curr) => 498 | curr.isImportDeclaration() 499 | ); 500 | const importLabels = importDecPath.node.specifiers.map( 501 | (obj) => obj.local.name 502 | ); 503 | const currDirectory = path.dirname(filePath); 504 | const afp = path.resolve(currDirectory, curr.node.value); 505 | const afpArray = allFiles.filter((file) => file.includes(afp)); 506 | if (afpArray.length) 507 | importLabels.forEach((label) => (imports[label] = afpArray[0])); 508 | } 509 | 510 | //check for files imported locally using ES5 syntax 511 | if ( 512 | curr.isStringLiteral() && 513 | curr.node.value.includes('./') && 514 | curr.parent.type === 'CallExpression' 515 | ) { 516 | if (curr.parent.callee.name === 'require') { 517 | let importLabel = findVariableName(curr); 518 | const currDirectory = path.dirname(filePath); 519 | const afp = path.resolve(currDirectory, curr.node.value); 520 | 521 | const afpArray = allFiles.filter((file) => file.includes(afp)); 522 | if (typeof importLabel === 'string') importLabel = [importLabel]; 523 | if (afpArray.length) 524 | importLabel.forEach((label) => (imports[label] = afpArray[0])); 525 | } 526 | console.log('imports', imports) 527 | } 528 | 529 | //filtering for IDENTIFIER: 530 | //that has a parent that is an OBJECT PROPERTY, where the object property has a child VALUE that is of type FUNCTIONEXPRESSION or ARROWFUNCTIONEXPRESSION 531 | if ( 532 | (expressionStatementPossible || variableStatementPossible) && 533 | !currentControllerMethod && 534 | curr.isIdentifier() && 535 | curr.parent.type === 'ObjectProperty' && 536 | curr.parent.value.type === 'ArrowFunctionExpression' 537 | ) { 538 | currentControllerMethod = curr.node.name; 539 | } 540 | 541 | //OR: 542 | //that has a parent that is a MEMBEREXPRESSION, a grandparent that is an ASSIGNMENTEXPRESSION, and an uncle RIGHT property that is an ARROWFUNCTIONEXPRESSION with a params child of length 3; 543 | if ( 544 | variableStatementPossible && 545 | !currentControllerMethod && 546 | curr.isIdentifier() && 547 | curr.parent.property && 548 | curr.parent.type === 'MemberExpression' && 549 | curr.parentPath.parent.type === 'AssignmentExpression' && 550 | curr.parentPath.parent.right 551 | ) { 552 | if (curr.parent.property.name === curr.node.name) 553 | currentControllerMethod = curr.node.name; 554 | console.log('currentControllerMethod', currentControllerMethod) 555 | } 556 | 557 | if ( 558 | currentControllerMethod && 559 | curr.isIdentifier() && 560 | Object.hasOwn(imports, curr.node.name) && 561 | curr.parent.type === 'MemberExpression' && 562 | curr.parent.property 563 | ) { 564 | console.log('currentControllerMethod2', currentControllerMethod) 565 | schemaInteractions[currentControllerMethod] 566 | ? schemaInteractions[currentControllerMethod].push(curr.node.name) 567 | : (schemaInteractions[currentControllerMethod] = [curr.node.name]); 568 | } 569 | }, 570 | }); 571 | console.log('schemaInteractions', schemaInteractions, 'controllerSchemas', controllerSchemas); 572 | }; 573 | 574 | const schemas = {}; 575 | const schemaKey = {}; 576 | 577 | //function for traversing mongoose model files and populating the above schemaKey object with each schema name and its associated schema structure 578 | const traverseMongooseFile = ( 579 | ast, 580 | filePath, 581 | mongooseLabel, 582 | schemaInstance 583 | ) => { 584 | traverse(ast, { 585 | enter(curr) { 586 | //check for new schema instances 587 | if ( 588 | curr.isIdentifier() && 589 | curr.node.name === schemaInstance && 590 | curr.parent.type === 'NewExpression' 591 | ) { 592 | const propsArray = curr.parent.arguments[0].properties; 593 | const schemaName = findVariableName(curr); 594 | 595 | //helper func used to create a clone of the schema as found in the file 596 | const populateSchema = (propsArray) => { 597 | const output = {}; 598 | if (propsArray) { 599 | propsArray.forEach((prop) => { 600 | const key = prop.key.name; 601 | prop.value.type === 'Identifier' 602 | ? (output[key] = prop.value.name) 603 | : prop.value.type === 'StringLiteral' || 604 | prop.value.type === 'BooleanLiteral' 605 | ? (output[key] = prop.value.value) 606 | : prop.value.type === 'ObjectExpression' 607 | ? (output[key] = populateSchema(prop.value.properties)) 608 | : prop.value.type === 'ArrayExpression' 609 | ? (output[key] = prop.value.elements.map((obj) => 610 | populateSchema(obj.properties) 611 | )) 612 | : (output[key] = 'UNKOWN VALUE'); 613 | }); 614 | } 615 | return output; 616 | }; 617 | 618 | //set the schema variable name to the schema 619 | schemas[schemaName] = populateSchema(propsArray); 620 | console.log('schemas', schemas); 621 | } 622 | 623 | //populate schemaKey object with the label each schema is being exported as set to the schema itself 624 | if ( 625 | curr.isIdentifier() && 626 | curr.node.name === mongooseLabel && 627 | curr.parent.type === 'MemberExpression' && 628 | curr.parent.property.name === 'model' 629 | ) { 630 | const callExpPath = curr.findParent((curr) => 631 | curr.isCallExpression() 632 | ); 633 | let schemaExport; 634 | if (Object.hasOwn(schemas, callExpPath.node.arguments[1].name)) { 635 | console.log(callExpPath.node.arguments[1].name, 'zzzzz') 636 | const schemaExport = findVariableName(curr) || callExpPath.node.arguments[0].value[0].toUpperCase() + callExpPath.node.arguments[0].value.slice(1); 637 | schemaKey[schemaExport] = 638 | schemas[callExpPath.node.arguments[1].name]; 639 | } 640 | } 641 | }, 642 | }); 643 | console.log('schemaKey', schemaKey); 644 | return; 645 | }; 646 | 647 | const allFiles = res.locals.allFiles; 648 | 649 | //iterate through array of files, convert each to AST, and invoke function to traverse the AST 650 | allFiles.forEach((filePath) => { 651 | const ast = parseFile(filePath); 652 | // console.log('ast'); 653 | traverseServerAST(ast, filePath); 654 | }); 655 | 656 | const output = {}; 657 | 658 | //populate the above output object with endpoints distributed from the root server file to router files, and their associated methods, schema labels, and schemas 659 | Object.keys(linksToRouter).forEach((route) => { 660 | // console.log('linksToRouter', linksToRouter, 'aRRLTC', allRouterRoutesLeadingToController) 661 | if ( 662 | Object.hasOwn(allRouterRoutesLeadingToController, linksToRouter[route]) 663 | ) { 664 | Object.keys( 665 | allRouterRoutesLeadingToController[linksToRouter[route]] 666 | ).forEach((key) => { 667 | // console.log('route', route, 'key', key) 668 | Object.keys( 669 | allRouterRoutesLeadingToController[linksToRouter[route]][key] 670 | ).forEach((method) => { 671 | // console.log('method', method, route, key) 672 | allRouterRoutesLeadingToController[linksToRouter[route]][key][ 673 | method 674 | ].forEach((controllerMethod) => { 675 | // console.log('controllerMethod', controllerMethod, route, key, 'controllerSchemas', controllerSchemas) 676 | if (Object.hasOwn(controllerSchemas, controllerMethod.path)) { 677 | if ( 678 | Object.hasOwn( 679 | controllerSchemas[controllerMethod.path], 680 | controllerMethod.middlewareName 681 | ) 682 | ) { 683 | controllerSchemas[controllerMethod.path][ 684 | controllerMethod.middlewareName 685 | ].forEach((schema) => { 686 | if (schemaKey[schema]) { 687 | if (output[route + key]) { 688 | if ( 689 | output[route + key][method] && 690 | !output[route + key][method][schema] 691 | ) { 692 | output[route + key][method][schema] = schemaKey[schema]; 693 | } else if (!output[route + key][method]) { 694 | output[route + key][method] = { 695 | [schema]: schemaKey[schema], 696 | }; 697 | } 698 | } else { 699 | output[route + key] = { 700 | [method]: { [schema]: schemaKey[schema] }, 701 | }; 702 | } 703 | } 704 | }); 705 | } 706 | } 707 | }); 708 | }); 709 | }); 710 | } 711 | }); 712 | 713 | // { '/api/': { 714 | // 'GET': { 'Person': { 'name': 'String' } }, 715 | // 'POST': { 'Species': { 'type': 'String' } } 716 | // }, 717 | // { '/api/homeworld': { 718 | // 'GET': { 'Person': 'String' } } 719 | // } 720 | // } 721 | 722 | //populate the output object with endpoints distributed from the root server file directly to controller files, and their associated methods, schema labels, and schemas 723 | Object.keys(allServerRoutesLeadingToController).forEach((file) => { 724 | Object.keys(allServerRoutesLeadingToController[file]).forEach((route) => { 725 | Object.keys(allServerRoutesLeadingToController[file][route]).forEach( 726 | (method) => { 727 | allServerRoutesLeadingToController[file][route][method].forEach( 728 | (middlewareMethod) => { 729 | if (Object.hasOwn(controllerSchemas, middlewareMethod.path)) { 730 | if ( 731 | Object.hasOwn( 732 | controllerSchemas[middlewareMethod.path], 733 | middlewareMethod.middlewareName 734 | ) 735 | ) { 736 | controllerSchemas[middlewareMethod.path][ 737 | middlewareMethod.middlewareName 738 | ].forEach((schema) => { 739 | if (schemaKey[schema]) { 740 | if (output[route]) { 741 | if ( 742 | output[route][method] && 743 | !output[route][method][schema] 744 | ) { 745 | output[route][method][schema] = schemaKey[schema]; 746 | } else if (!output[route][method]) { 747 | output[route][method] = { 748 | [schema]: schemaKey[schema], 749 | }; 750 | } 751 | } else 752 | output[route] = { 753 | [method]: { [schema]: schemaKey[schema] }, 754 | }; 755 | } 756 | }); 757 | } 758 | } 759 | } 760 | ); 761 | } 762 | ); 763 | }); 764 | }); 765 | 766 | console.log('output', output); 767 | res.locals.serverRoutes = output; 768 | 769 | }; -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, shell, BrowserWindow, ipcMain, dialog } from 'electron' 2 | import { join } from 'path' 3 | import { electronApp, optimizer, is } from '@electron-toolkit/utils' 4 | import icon from '../../resources/icon.png?asset' 5 | 6 | import { parseAllServerFiles } from './controllers/serverASTController.js'; 7 | import { parseAllComponents, getCode } from './controllers/componentController.js'; 8 | import { getArrayOfFilePaths } from './controllers/fsController.js'; 9 | 10 | let server; 11 | 12 | function createWindow(): void { 13 | // Create the browser window. 14 | const mainWindow = new BrowserWindow({ 15 | width: 900, 16 | height: 670, 17 | show: false, 18 | autoHideMenuBar: true, 19 | ...(process.platform === 'linux' ? { icon } : {}), 20 | webPreferences: { 21 | preload: join(__dirname, '../preload/index.js'), 22 | sandbox: false, 23 | nodeIntegration: true, 24 | contextIsolation: true 25 | } 26 | }) 27 | 28 | mainWindow.on('ready-to-show', () => { 29 | mainWindow.show() 30 | }) 31 | 32 | mainWindow.webContents.setWindowOpenHandler((details) => { 33 | shell.openExternal(details.url) 34 | return { action: 'deny' } 35 | }) 36 | 37 | // HMR for renderer base on electron-vite cli. 38 | // Load the remote URL for development or the local html file for production. 39 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 40 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 41 | } else { 42 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')) 43 | } 44 | 45 | 46 | } 47 | 48 | // This method will be called when Electron has finished 49 | // initialization and is ready to create browser windows. 50 | // Some APIs can only be used after this event occurs. 51 | app.whenReady().then(() => { 52 | // Set app user model id for windows 53 | electronApp.setAppUserModelId('com.electron') 54 | 55 | // Default open or close DevTools by F12 in development 56 | // and ignore CommandOrControl + R in production. 57 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 58 | app.on('browser-window-created', (_, window) => { 59 | optimizer.watchWindowShortcuts(window) 60 | }) 61 | 62 | createWindow() 63 | 64 | 65 | 66 | app.on('activate', function () { 67 | // On macOS it's common to re-create a window in the app when the 68 | // dock icon is clicked and there are no other windows open. 69 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 70 | }) 71 | 72 | }) 73 | 74 | // ipcMain creates a communication channel between main and renderer --> looks for events regarding dialog 75 | // async in order to wait for the user to select the filepath 76 | ipcMain.handle('dialog', async (_, method, params) => { 77 | const result = await dialog[method](params) 78 | return result; 79 | }) 80 | 81 | 82 | ipcMain.handle('code', async (e, args) => { 83 | try { 84 | let res = { locals: {componentCode: ''} }; 85 | 86 | await getCode(e, args, res) 87 | return { status: 200, data: res.locals.componentCode } 88 | } catch (err) { 89 | console.error(err) 90 | return { status: 500, error: "Error in ipcMain handler of 'code' route"} 91 | } 92 | }) 93 | 94 | ipcMain.handle('components', async (e, args) => { 95 | try { 96 | let res = { locals: {components: ''} }; 97 | await getArrayOfFilePaths(e, args, res); 98 | await parseAllComponents(e, args, res); 99 | return { status: 201, data: res.locals.components } 100 | } catch (err) { 101 | console.error(err) 102 | return { status: 500, error: "Error in ipcMain handler of 'code' route"} 103 | } 104 | }) 105 | 106 | ipcMain.handle('server', async (e, args) => { 107 | try { 108 | let res = { locals: {serverRoutes: ''} }; 109 | await getArrayOfFilePaths(e, args, res); 110 | await parseAllServerFiles(e, args, res); 111 | return { status: 201, data: res.locals.serverRoutes } 112 | } catch (err) { 113 | console.error(err) 114 | return { status: 500, error: "Error in ipcMain handler of 'code' route"} 115 | } 116 | }) 117 | 118 | // Quit when all windows are closed, except on macOS. There, it's common 119 | // for applications and their menu bar to stay active until the user quits 120 | // explicitly with Cmd + Q. 121 | app.on('window-all-closed', () => { 122 | if (process.platform !== 'darwin') { 123 | app.quit() 124 | server.close(); 125 | } 126 | }) 127 | 128 | // In this file you can include the rest of your app"s specific main process 129 | // code. You can also put them in separate files and require them here. 130 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronAPI 6 | api: unknown 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | // Custom APIs for renderer 5 | const api = { 6 | openDialog: (method, config) => ipcRenderer.invoke("dialog", method, config), 7 | send: (route, data) => { 8 | let validRoutes = ['components', 'server', 'code']; 9 | if (validRoutes.includes(route)) return ipcRenderer.invoke(route, data); 10 | }, 11 | } 12 | 13 | // Use `contextBridge` APIs to expose Electron APIs to 14 | // renderer only if context isolation is enabled, otherwise 15 | // just add to the DOM global. 16 | if (process.contextIsolated) { 17 | try { 18 | contextBridge.exposeInMainWorld('electron', electronAPI) 19 | contextBridge.exposeInMainWorld('api', api) 20 | } catch (error) { 21 | console.error(error) 22 | } 23 | } else { 24 | // @ts-ignore (define in dts) 25 | window.electron = electronAPI 26 | // @ts-ignore (define in dts) 27 | window.api = api 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/renderer/.DS_Store -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactRelay 6 | 7 | 8 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/renderer/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/renderer/src/.DS_Store -------------------------------------------------------------------------------- /src/renderer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import 'tailwindcss/tailwind.css'; 2 | import { useSelector } from 'react-redux' 3 | import Header from './components/Header'; 4 | import Tree from './components/Tree'; 5 | import ProjectPathModal from './components/ProjectPathModal'; 6 | import Details from './components/Details'; 7 | 8 | 9 | function App(): JSX.Element { 10 | const componentName = useSelector((state: any) => state.project.componentName) 11 | 12 | return ( 13 |
14 |
15 | 16 | 17 | {componentName !== '' && 18 |
} 19 |
20 | ) 21 | } 22 | 23 | export default App 24 | -------------------------------------------------------------------------------- /src/renderer/src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/renderer/src/assets/.DS_Store -------------------------------------------------------------------------------- /src/renderer/src/assets/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/renderer/src/assets/images/.DS_Store -------------------------------------------------------------------------------- /src/renderer/src/assets/images/ReactRelay-logos/ReactRelay-logos_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/renderer/src/assets/images/ReactRelay-logos/ReactRelay-logos_black.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/ReactRelay-logos/ReactRelay-logos_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/renderer/src/assets/images/ReactRelay-logos/ReactRelay-logos_transparent.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/ReactRelay-logos/ReactRelay-logos_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ReactRelay/91cbca24544dfc842bd4e01f40bf16bec43c25bf/src/renderer/src/assets/images/ReactRelay-logos/ReactRelay-logos_white.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/ReactRelay-logos/logo_info.txt: -------------------------------------------------------------------------------- 1 | 2 | Fonts used: Lato-Bold 3 | 4 | Colors used: 0E67B4,F7F3E8 5 | 6 | Icon url: https://thenounproject.com/term/relay-race/78045 7 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/ReactRelay.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/directory-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/directory-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/left-right-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/up-down-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/src/assets/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* display: flex; 3 | flex-direction: column; */ 4 | font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 5 | 'Segoe UI', 'Oxygen', 'Ubuntu', 'Cantarell', 'Open Sans', sans-serif; 6 | /* background-color: #2f3241; */ 7 | } 8 | 9 | /* 10 | .container { 11 | flex: 1; 12 | display: flex; 13 | flex-direction: column; 14 | max-width: 840px; 15 | margin: 0 auto; 16 | padding: 15px 30px 0 30px; 17 | } */ 18 | 19 | .edgeClass { 20 | stroke: 'purple' !important; 21 | stroke-width: '3px' !important; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/renderer/src/assets/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 3 | code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} 4 | -------------------------------------------------------------------------------- /src/renderer/src/components/ComponentCode.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | import Prism from 'prismjs'; 3 | import '../assets/prism.css'; 4 | import { useSelector } from 'react-redux' 5 | 6 | 7 | const ComponentCode = () => { 8 | const activeComponentCode = useSelector((state: any) => state.detail.activeComponentCode) 9 | useEffect(() => { 10 | Prism.highlightAll(); 11 | }, [activeComponentCode]) 12 | 13 | 14 | 15 | return ( 16 |
17 |

Component Code

18 |
19 |         
20 |         {activeComponentCode}
21 |         
22 |       
23 |
24 | ) 25 | } 26 | 27 | export default ComponentCode; -------------------------------------------------------------------------------- /src/renderer/src/components/Details.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { BrowserRouter as Router, Routes, Route, useLocation, useNavigate } from 'react-router-dom'; 3 | import ModelPreview from './ModelPreview'; 4 | import MethodButtonContainer from '@renderer/containers/MethodButtonContainer'; 5 | import ComponentCode from './ComponentCode' 6 | import { useSelector } from 'react-redux' 7 | 8 | function Details(): JSX.Element { 9 | const [height, setHeight] = useState(0); 10 | const navigate = useNavigate(); 11 | const location = useLocation(); 12 | const nodeInfo = useSelector((state: any) => state.project.nodeInfo); 13 | const componentName = useSelector((state: any) => state.project.componentName) 14 | const treeContainerClick = useSelector((state: any) => state.detail.treeContainerClick) 15 | 16 | useEffect(() => { 17 | window.innerHeight > 800 ? setHeight('40vh') : setHeight('30vh'); 18 | },[nodeInfo]) 19 | 20 | useEffect(() => { 21 | const newHeight = height > '30vh' ? '20vh' : 0; 22 | setHeight(newHeight) 23 | }, [treeContainerClick]) 24 | 25 | 26 | const handler = (mouseDownEvent) => { 27 | // const startHeight = height; 28 | // const startPosition = mouseDownEvent.pageY; 29 | function onMouseMove(mouseMoveEvent) { 30 | const newHeight = window.innerHeight - mouseMoveEvent.pageY; //startHeight = height of div // startPosition = where the mouse is positioned // mouseMoveEvenet.pageY = detects where mouse is on the screen //pageY is property of mouse event (on y axis unit is in pixels) 31 | // console.log('start:', startHeight, ' position:', startPosition, 'mouse: ', mouseMoveEvent.pageY) 32 | setHeight(newHeight) 33 | } 34 | 35 | function onMouseUp() { 36 | window.document.body.removeEventListener('mousemove', onMouseMove) 37 | window.document.body.removeEventListener('mouseup', onMouseUp) 38 | } 39 | 40 | window.document.body.addEventListener("mousemove", onMouseMove); 41 | window.document.body.addEventListener("mouseup", onMouseUp); 42 | } 43 | 44 | console.log('location.pathname', location.pathname) 45 | if (location.pathname.length > 6) navigate('/'); 46 | 47 | 48 | return ( 49 | <> 50 |
51 |
52 |
53 |
54 | 55 |
56 |

57 |

58 | {componentName} 59 |
60 |

location.pathname === '/' ? navigate('/code') : navigate('/')}> 61 | {location.pathname === '/' ? 'ROUTES' : 'CODE'} 62 |

63 | 64 |
65 |
66 |

67 |
68 | 69 | 70 | 71 |
72 | 73 | 75 | 76 | 77 | 78 | }/> 79 | } /> 80 | 81 |
82 |
83 | 84 | ); 85 | } 86 | 87 | export default Details; 88 | -------------------------------------------------------------------------------- /src/renderer/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import dirUpload from '../assets/images/directory-black.svg'; 3 | // import appLogo from '../assets/images/ReactRelay.svg' 4 | import appLogo from '../assets/images/ReactRelay-logos/ReactRelay-logos_white.png'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { setSearchValue, setShowSearch } from '../features/searchSlice'; 7 | 8 | 9 | function Header({}): JSX.Element { 10 | const dispatch = useDispatch(); 11 | const searchBar = useSelector((state: any) => state.search.searchValue); 12 | const showSearch = useSelector((state: any) => state.search.showSearch); 13 | 14 | const searchInputRef = useRef(null); 15 | 16 | 17 | const handleSearchClick = () => { 18 | 19 | dispatch(setShowSearch()); 20 | 21 | if (searchInputRef.current) { 22 | searchInputRef.current.focus(); 23 | } 24 | } 25 | 26 | return ( 27 |
28 |
29 | 30 |
31 |
32 | {showSearch && ( 33 | dispatch(setSearchValue(e.target.value))} 40 | value={searchBar} 41 | placeholder="Search components" /> 42 | )} 43 | 44 | 47 | 48 |
49 | 50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | export default Header -------------------------------------------------------------------------------- /src/renderer/src/components/MethodButton.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { setActiveRoute } from '../features/detailSlice'; 3 | 4 | const MethodButton = ({methodName, endPointName}) => { 5 | const dispatch = useDispatch(); 6 | 7 | const infoObj = { 8 | methodName, 9 | endPointName 10 | } 11 | 12 | //FIXME: colors are a bit hard to read 13 | const colorCode = { 14 | 'GET': 'bg-[#579972]', 15 | 'POST': 'bg-[#f2cc44]', 16 | 'PUT': 'bg-[#6ea2e6]', 17 | 'PATCH': 'bg-[#b49ed3]', 18 | 'DELETE': 'bg-[#f79a8d]' 19 | } 20 | 21 | 22 | const testClick = () => { 23 | dispatch(setActiveRoute(infoObj)); 24 | console.log('Method: ', methodName, ' Endpoint: ', endPointName); 25 | } 26 | 27 | return ( 28 |
29 |
testClick()}> 30 |
31 | {methodName} 32 |
33 |
34 |
{endPointName}
35 |
36 |
37 | ) 38 | } 39 | 40 | export default MethodButton -------------------------------------------------------------------------------- /src/renderer/src/components/ModelPreview.tsx: -------------------------------------------------------------------------------- 1 | import Prism from 'prismjs'; 2 | import '../assets/prism.css'; 3 | import { useEffect } from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | 6 | function ModelPreview() { 7 | const serverSchemas = useSelector((state: any) => state.project.server); 8 | const activeRoute = useSelector((state: any) => state.detail.activeRoute); 9 | 10 | //use Prism API to highlight the displayed schemas 11 | useEffect(() => { 12 | Prism.highlightAll(); 13 | }, [activeRoute]) 14 | 15 | let display = ''; 16 | let output; 17 | let activeEndpoint = activeRoute.endPointName; 18 | 19 | //handle edge-case of the AJAX call on the front-end explicitly including the local host address. 20 | if (/^http:\/\/localhost:\d+\//.test(activeEndpoint)) { 21 | activeEndpoint = "/" + activeEndpoint.split(/^http:\/\/localhost:\d+\//)[1]; 22 | } 23 | console.log('activeRoute', activeRoute) 24 | const activeMethod = activeRoute.methodName ? activeRoute.methodName.toLowerCase() : ''; 25 | 26 | //check if the selected AJAX endpoint is conditional (i.e. contains a variable) 27 | let hasVariable = /\$\{[^ ]+\}/.test(activeEndpoint); 28 | 29 | const serverRoutes = Object.keys(serverSchemas).map(route => route); 30 | 31 | console.log(serverRoutes, 'serverRoutes11111', serverSchemas) 32 | 33 | //if an endpoint exists and contains no variable, check if there are corresponding server-side interactions with DB schemas. if so, reassign display var to "found". 34 | if (hasVariable) { 35 | display = "variable" 36 | } else if (activeEndpoint) { 37 | if (serverRoutes.includes(activeEndpoint)) { 38 | display = "found"; 39 | } else if (serverRoutes.includes(activeEndpoint + "/")) { 40 | display = "found"; 41 | activeEndpoint += "/"; 42 | } else if (activeEndpoint[activeEndpoint.length-1] === "/" && serverRoutes.includes(activeEndpoint.slice(0,-1))) { 43 | display = "found"; 44 | activeEndpoint = activeEndpoint.slice(0,-1); 45 | } 46 | } 47 | 48 | //if the AJAX request was found server-side, and contains a schema(s), iterate through the schemas for that AJAX request in the 'serverSchemas' object and add them to the 'output' variable as an array of strings to be displayed on right side of the details container. 49 | if (display === 'found') { 50 | output = Object.keys(serverSchemas[activeEndpoint][activeMethod]).map(schema => `${schema} ` + JSON.stringify(serverSchemas[activeEndpoint][activeMethod][schema], null, 2)).join('\n'); 51 | } 52 | 53 | console.log('aep', activeEndpoint, 'serverRoutes', serverRoutes, 'activeRoute', activeRoute) 54 | 55 | console.log('display', display) 56 | return ( 57 |
58 |

Expected Data Structure

59 |
60 |         
61 |         {
62 |           display === '' ? "SCHEMA NOT FOUND"
63 |           : display === "variable" ? "ROUTE IS CONDITIONAL"
64 |           : display === "found" ? output
65 |           : ''
66 |         }
67 |         
68 |       
69 |
70 | ) 71 | } 72 | 73 | export default ModelPreview -------------------------------------------------------------------------------- /src/renderer/src/components/ProjectPathModal.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector, useDispatch } from 'react-redux'; 2 | import { addPath, setComponents, setServer } from '../features/projectSlice' 3 | 4 | function ProjectPathModal() { 5 | 6 | const dispatch = useDispatch(); 7 | const componentPath = useSelector((state: any) => state.project.componentPath) 8 | const serverPath = useSelector((state: any) => state.project.serverPath) 9 | 10 | const dialogConfig = { 11 | title: 'Select a project', 12 | buttonLabel: 'Select', 13 | properties: ['openDirectory'] 14 | } 15 | // window.api.openDialog returns the filepath when the filepath is chosen from the dialog 16 | // should i add a case where user doesn't actually select a filepath 17 | const openFileExplorer = async (pathType): Promise => { //FIXME: add to type 18 | const {filePaths} = await (window as any).api.openDialog('showOpenDialog', dialogConfig) //TODO: add to type 19 | // if user chooses cancel then don't do anything 20 | if (filePaths[0] === '' || !filePaths[0]) return null; 21 | dispatch(addPath([pathType, filePaths[0]])); 22 | } 23 | 24 | const postPath = async (pathType, path): Promise => { 25 | if (path === '' || !path) return null; 26 | console.log('componentPath', pathType, path) 27 | const endpoint = { 28 | component: `components`, 29 | server: `server` 30 | } 31 | const response = await (window as any).api.send(endpoint[pathType], { filePath: path }) // sends to the componentController or serverASTController the filepath 32 | console.log('response', response) 33 | if (response.status >=200 && response.status < 300) { 34 | console.log('pathtype:', pathType, 'path:', path, 'res:', response.data) 35 | if (pathType === 'component') dispatch(setComponents(response.data)); 36 | else if (pathType === 'server') dispatch(setServer(response.data)); 37 | } 38 | } 39 | 40 | const onContinue = () => { 41 | console.log('component', componentPath) 42 | postPath('component', componentPath); 43 | postPath('server', serverPath); 44 | (window as any).openExplorerModal.close(); 45 | } 46 | 47 | return ( 48 | 49 |
50 |

Open Project

51 |
52 |
53 | 56 |
57 | 58 |
59 |

60 | {componentPath !== '' ? componentPath : 'No folder chosen'} 61 |

62 |
63 |
64 |
65 |
66 |
67 | 70 |
71 | 72 |

73 | {serverPath !== '' ? serverPath : 'No folder chosen'} 74 |

75 |
76 |
77 |
78 |
79 |
80 |
81 | {/* if there is a button in form, it will close the modal */} 82 | 83 |
84 |
85 | 86 |
87 |
88 | 89 |
90 |
91 | 92 | ) 93 | } 94 | 95 | export default ProjectPathModal -------------------------------------------------------------------------------- /src/renderer/src/components/Tree.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import ReactFlow, { 3 | addEdge, 4 | ConnectionLineType, 5 | Panel, 6 | useNodesState, 7 | useEdgesState, 8 | Controls, 9 | MiniMap, 10 | } from 'reactflow'; 11 | 12 | import dagre from 'dagre'; 13 | import horizontal from '../assets/images/left-right-solid.svg'; 14 | import vertical from '../assets/images/up-down-solid.svg'; 15 | import CustomNode from './custom-nodes/custom-node'; 16 | import CustomNode2 from './custom-nodes/custom-node2'; 17 | import '../assets/index.css'; 18 | import 'reactflow/dist/style.css'; 19 | import { useSelector, useDispatch } from 'react-redux'; 20 | import { 21 | setNodeInfo, 22 | setComponentName, 23 | } from '../features/projectSlice'; 24 | import { setTreeContainerClick, 25 | setActive, 26 | setActiveComponentCode } from '../features/detailSlice' 27 | const nodeTypes = { 28 | CustomNode, 29 | CustomNode2, 30 | }; 31 | 32 | 33 | const edgeType = 'smoothstep'; 34 | // declaring both edge styles which we will be using 35 | // these use svg styling in case one wants to update them. 36 | // you can look up options here: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute 37 | const edgeStyle = { 38 | stroke: 'black', 39 | 'strokeWidth': 4, 40 | }; 41 | const edgeStyle2 = { 42 | stroke: 'red', 43 | 'strokeWidth': 8, 44 | }; 45 | 46 | const dagreGraph = new dagre.graphlib.Graph(); 47 | dagreGraph.setDefaultEdgeLabel(() => ({})); 48 | 49 | // controls spacing between nodes 50 | const nodeWidth = 300; 51 | const nodeHeight = 50; 52 | 53 | // All you have to do to change the default layout is change 'LR' to 'TB' or 'RL' 54 | const getLayoutedElements = (nodes, edges, direction = 'LR') => { 55 | const isHorizontal = direction === 'LR'; 56 | dagreGraph.setGraph({ rankdir: direction }); 57 | 58 | nodes.forEach((node) => { 59 | dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); 60 | }); 61 | 62 | edges.forEach((edge) => { 63 | dagreGraph.setEdge(edge.source, edge.target); 64 | edge.animated = true; 65 | }); 66 | 67 | dagre.layout(dagreGraph); 68 | 69 | nodes.forEach((node) => { 70 | const nodeWithPosition = dagreGraph.node(node.id); 71 | node.targetPosition = isHorizontal ? 'left' : 'top'; 72 | node.sourcePosition = isHorizontal ? 'right' : 'bottom'; 73 | // We are shifting the dagre node position (anchor=center center) to the top left 74 | // so it matches the React Flow node anchor point (top left). 75 | node.position = { 76 | x: nodeWithPosition.x - nodeWidth / 2, 77 | y: nodeWithPosition.y - nodeHeight / 2, 78 | }; 79 | return node; 80 | }); 81 | return { nodes, edges }; 82 | }; 83 | 84 | // setting the types for different components 85 | 86 | type Component = { 87 | id: string; 88 | data: any; 89 | children: string[]; 90 | ajaxRequests: string[]; 91 | }; 92 | 93 | type Node = { 94 | id: string; 95 | data: any; 96 | position: { x: number; y: number }; 97 | type: string; 98 | }; 99 | 100 | type Edge = { 101 | id: string; 102 | source: string; 103 | target: string; 104 | type: string; 105 | animated: boolean; 106 | className: string; 107 | style: object; 108 | }; 109 | 110 | function Tree({}): JSX.Element { 111 | const [nodes, setNodes, onNodesChange] = useNodesState([]); 112 | const [edges, setEdges, onEdgesChange] = useEdgesState([]); 113 | const reactFlowComponents = useSelector((state: any) => state.project.components); 114 | const active = useSelector((state: any) => state.detail.active); 115 | const dispatch = useDispatch(); 116 | //components that are re-used are given unique id's by adding a number to the end of the AFP. this function converts that id back to the AFP (i.e. as it appears in reactFlowComponents), then return the object associated with this AFP key in reactFlowComponents. 117 | const getComponentFromNodeId = (id: string): Component => { 118 | let i = id.length - 1; 119 | while (/[0-9]/.test(id[i]) && i > 10) i--; 120 | return reactFlowComponents[id.slice(0, i + 1)]; 121 | }; 122 | 123 | const handleTreeContainerClick = (e) => { 124 | if (!e.target.closest('.react-flow__node')) { 125 | dispatch(setTreeContainerClick()); 126 | } 127 | }; 128 | 129 | useEffect(() => { 130 | if (!reactFlowComponents) return; 131 | const newNodes: Node[] = []; 132 | const newEdges: Edge[] = []; 133 | const childCount = {}; 134 | const listOfChildIds = new Set(); 135 | 136 | //create a Set containing all components that are children of other components (used to isolate 'roots' array below) 137 | 138 | (Object.values(reactFlowComponents) as Component[]).forEach( 139 | (obj: Component) => { 140 | obj.children.forEach((childId) => listOfChildIds.add(childId)); 141 | } 142 | ); 143 | 144 | //recursive func that increments the value of component id in "childCount" array by 1 for each instance of that child, then invokes gatherChildren passing in the obj in reactFlowComponents that represents that child component 145 | const gatherChildren = (root: Component, ripCord: string[] = []): void => { 146 | // console.log('component', root) 147 | root.children.forEach((childId) => { 148 | if ( 149 | Object.hasOwn(reactFlowComponents, childId) && 150 | !ripCord.includes(childId) 151 | ) { 152 | // Used to prevent infinite loop of when a child may create its 153 | // parent conditionally. If that occurs, the recursive function ends. 154 | childCount[childId] 155 | ? childCount[childId]++ 156 | : (childCount[childId] = 1); 157 | ripCord.push(childId); 158 | gatherChildren(reactFlowComponents[childId], ripCord); 159 | ripCord.pop(); 160 | } 161 | }); 162 | }; 163 | 164 | //filter for components that have no parent, then invoke 'gatherChildren' on each of them 165 | // console.log('list of children and their id ---> ', listOfChildIds); 166 | const roots = Object.values(reactFlowComponents).filter( 167 | (obj: any): obj is Component => !listOfChildIds.has(obj.id) 168 | ); 169 | // console.log('ROOTS ----> ', roots); 170 | if (roots.length) roots.forEach((root) => gatherChildren(root)); //iterate thru each root and gather it's children 171 | 172 | // console.log(childCount, '<---- childCount'); 173 | 174 | //iterate through all components in reactFlowComponents. Whatever the value of that componentId is in childCount, create that many new nodes for this component. (create just 1 node if it doesn't appear in childCount); 175 | (Object.values(reactFlowComponents) as Component[]).forEach( 176 | (obj: Component) => { 177 | // obj.ajaxRequests --> check if this is empty or has values 178 | // to change the node's styling using custom nodes 179 | let i = childCount[obj.id] || 1; 180 | // adds the number of components which are present, as there could be multiple copies 181 | //TODO: determine whether or not we need multiple copies. 182 | // takes care of a component that is used in more than just 1 component 183 | while (i >= 1) { 184 | newNodes.push({ 185 | id: obj.id + i, 186 | data: { ...obj.data, active: false }, 187 | position: { x: 0, y: 0 }, 188 | type: obj.ajaxRequests.length ? 'CustomNode' : 'CustomNode2', 189 | }); 190 | i--; 191 | } 192 | } 193 | ); 194 | 195 | //for each node, for each of its children, create a connection (edge) between that node and one of the nodes that represents the child. Pick the child node whose id ends with the value of the child node in the 'childCount' object. Then decrement this value in 'childCount' so that no child has multiple parents. 196 | newNodes.forEach((obj) => { 197 | const component = getComponentFromNodeId(obj.id); 198 | component.children.forEach((childId) => { 199 | let child = childCount[childId] || 1; 200 | newEdges.push({ 201 | id: obj.id.concat(childId + child), 202 | source: obj.id, 203 | target: childId + child, 204 | type: edgeType, 205 | animated: true, 206 | className: 'edgeClass', 207 | // setting the default edge style to be the modified one to make 208 | // the edges more visible when zoomed out. 209 | style: edgeStyle, 210 | }); 211 | childCount[childId]--; 212 | }); 213 | }); 214 | 215 | const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( 216 | newNodes, 217 | newEdges 218 | ); 219 | setNodes(layoutedNodes); 220 | setEdges(layoutedEdges); 221 | }, [reactFlowComponents]); 222 | 223 | const onConnect = useCallback( 224 | (params) => 225 | setEdges( 226 | ( 227 | eds //eds is previous value of the edge's variable 228 | ) => 229 | addEdge( 230 | { ...params, type: ConnectionLineType.SmoothStep, animated: true }, 231 | eds 232 | ) 233 | ), 234 | [] 235 | ); 236 | const onLayout = useCallback( 237 | (direction) => { 238 | const { nodes: layoutedNodes, edges: layoutedEdges } = 239 | getLayoutedElements(nodes, edges, direction); 240 | setNodes([...layoutedNodes]); 241 | setEdges([...layoutedEdges]); 242 | }, 243 | [nodes, edges] 244 | ); 245 | 246 | // on nodeClick we will want to set the state of the node info 247 | const onNodeClick = async (_, element) => { 248 | const component = getComponentFromNodeId(element.id); 249 | const compName = getComponentName(element.id); 250 | dispatch(setComponentName(compName)); 251 | dispatch(setNodeInfo(reactFlowComponents[component.id].ajaxRequests)); 252 | const updatedNodes = nodes.map((node) => { 253 | // console.log(active); 254 | return node.id === element.id 255 | ? { ...node, data: { ...node.data, active: true } } 256 | : node.id === active 257 | ? { ...node, data: { ...node.data, active: false } } 258 | : node; 259 | }); 260 | dispatch(setActive(element.id)); 261 | const encodedId = encodeURIComponent(component.id); 262 | const componentCode = await (window as any).api.send('code', { id: encodedId }); 263 | // console.log(componentCode, 'componentCode'); 264 | // console.log('data', data) 265 | dispatch(setActiveComponentCode(componentCode.data)); 266 | // when clicked, the active node's edges will be highlighted in red, 267 | // otherwise they will go back to the default black color. 268 | // the edge.source and edge.target are both selected here to highlight 269 | // the connection line on both ends of the node. 270 | const updatedEdges = edges.map((edge) => { 271 | return edge.source === element.id || edge.target === element.id 272 | ? { ...edge, style: edgeStyle2 } 273 | : edge.source === active || edge.target === active 274 | ? { ...edge, style: edgeStyle } 275 | : edge; 276 | }); 277 | setNodes(updatedNodes); 278 | setEdges(updatedEdges); 279 | }; 280 | 281 | // TODO: REFACTOR THIS 282 | const getComponentName = (string) => { 283 | const splitString = string.split('/'); // splitting the file path by / characters 284 | const componentExtension = splitString[splitString.length - 1]; // getting the final file of the directory 285 | const splitFileType = componentExtension.split('.'); // splitting the file path from its file extension 286 | splitFileType[splitFileType.length - 1] = splitFileType[ 287 | // selecting whatever comes as the final extension 288 | // replace any numbers in the file extension with an empty string 289 | splitFileType.length - 1 290 | ].replaceAll(/[0-9]/g, ''); 291 | return splitFileType.join('.'); // re-join the file extension with a '.' to properly re-form it 292 | }; 293 | 294 | //TODO: add fragment so that you can return without a div 295 | return ( 296 | handleTreeContainerClick(e)} 311 | > 312 | 313 |
314 | 320 | 330 |
331 |
332 | 333 | 334 |
335 | ); 336 | } 337 | 338 | export default Tree; 339 | -------------------------------------------------------------------------------- /src/renderer/src/components/Versions.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | function Versions(): JSX.Element { 4 | const [versions] = useState(window.electron.process.versions) 5 | 6 | return ( 7 |
    8 |
  • Electron v{versions.electron}
  • 9 |
  • Chromium v{versions.chrome}
  • 10 |
  • Node v{versions.node}
  • 11 |
  • V8 v{versions.v8}
  • 12 |
13 | ) 14 | } 15 | 16 | export default Versions 17 | -------------------------------------------------------------------------------- /src/renderer/src/components/custom-nodes/custom-node.tsx: -------------------------------------------------------------------------------- 1 | // Path: src/renderer/src/components/CustomNodes.jsx 2 | import React, { useMemo, useEffect, useState } from 'react'; 3 | import { Handle, Position } from 'reactflow'; 4 | import 'tailwindcss/tailwind.css'; 5 | import { useSelector } from 'react-redux'; 6 | 7 | interface NodeProps { 8 | data: { 9 | label: string, 10 | active: boolean, 11 | }; 12 | sourcePosition: { 13 | x: number, 14 | y: number, 15 | }; 16 | targetPosition: number, 17 | } 18 | 19 | const handleStyle = { 20 | //why is this decalred but never read 21 | left: 10, 22 | style: 'bg-primary', 23 | }; 24 | 25 | const checkForSearchMatch = (label: string) => { 26 | return useSelector((state: any) => { 27 | const searchValue = state.search.searchValue.toLowerCase(); 28 | return label.toLowerCase().includes(searchValue) ? searchValue : null; 29 | }, (a,b) => a === b) 30 | } 31 | 32 | const CustomNode = React.memo(({ data, sourcePosition, targetPosition }) => { 33 | const { label } = data; 34 | const searchValue = checkForSearchMatch(label.toLowerCase()); 35 | 36 | 37 | return ( 38 |
43 | {/* Handle are the dotes on the edge of the node where the lines connect */} 44 | 45 |

52 | {label} 53 |

54 | 60 |
61 | ); 62 | }); 63 | 64 | export default CustomNode; 65 | -------------------------------------------------------------------------------- /src/renderer/src/components/custom-nodes/custom-node2.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Handle, Position } from 'reactflow'; 3 | const handleStyle = { 4 | left: 10, 5 | }; 6 | import 'tailwindcss/tailwind.css'; 7 | import { useSelector } from 'react-redux'; 8 | 9 | const checkForSearchMatch = (label: string) => { 10 | return useSelector((state: any) => { 11 | const searchValue = state.search.searchValue.toLowerCase(); 12 | return label.toLowerCase().includes(searchValue) ? searchValue : null; 13 | }, (a,b) => a === b) 14 | } 15 | 16 | const CustomNode2 = ({ data, sourcePosition, targetPosition }) => { 17 | const { label } = data; 18 | const searchValue = checkForSearchMatch(label); 19 | 20 | return ( 21 |
26 | 27 |

{label}

28 | 34 |
35 | ); 36 | }; 37 | 38 | export default memo(CustomNode2); 39 | -------------------------------------------------------------------------------- /src/renderer/src/containers/MethodButtonContainer.tsx: -------------------------------------------------------------------------------- 1 | import MethodButton from "@renderer/components/MethodButton"; 2 | import { useState, useEffect } from "react"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { setActiveRoute } from '../features/detailSlice'; 5 | 6 | function MethodButtonContainer() { 7 | const [methodButtons, setMethodButtons] = useState([]); 8 | const nodeInfo = useSelector((state: any) => state.project.nodeInfo); 9 | const dispatch = useDispatch(); 10 | 11 | console.log('nodeinfo', nodeInfo) 12 | 13 | useEffect(() => { 14 | const buttons = nodeInfo.map((nodeObj) => ( 15 | 19 | )); 20 | setMethodButtons(buttons); 21 | dispatch(setActiveRoute(Object.keys(nodeInfo).length ? { methodName: nodeInfo[0].method, endPointName: nodeInfo[0].fullRoute } : { methodName: '', endPointName: '' })) 22 | }, [nodeInfo]); 23 | 24 | return ( 25 |
26 | {nodeInfo.length !== 0 && methodButtons} 27 |
28 | ) 29 | } 30 | 31 | export default MethodButtonContainer; -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/renderer/src/features/detailSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | active: null, 5 | activeComponentCode: '', 6 | treeContainerClick: true, 7 | activeRoute: {endPointName: '', methodName: ''} 8 | }; 9 | 10 | export const detailSlice = createSlice({ 11 | name: 'detail', 12 | initialState, 13 | reducers: { 14 | setTreeContainerClick: (state) => { 15 | state.treeContainerClick = !state.treeContainerClick; 16 | }, 17 | setActive: (state, action) => { 18 | state.active = action.payload; 19 | }, 20 | setActiveComponentCode: (state, action) => { 21 | state.activeComponentCode = action.payload; 22 | }, 23 | setActiveRoute: (state, action) => { 24 | state.activeRoute = action.payload; 25 | } 26 | }, 27 | }); 28 | 29 | export const { setTreeContainerClick, setActive, setActiveComponentCode, setActiveRoute } = 30 | detailSlice.actions; 31 | export default detailSlice.reducer; 32 | -------------------------------------------------------------------------------- /src/renderer/src/features/projectSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | nodeInfo: [], 5 | server: {}, 6 | serverPath: '', 7 | components: {}, 8 | componentName: '', 9 | componentPath: '', 10 | }; 11 | 12 | export const projectSlice = createSlice({ 13 | name: 'project', 14 | initialState, 15 | reducers: { 16 | addPath: (state, action) => { 17 | const pathType = action.payload[0]; 18 | const pathName = action.payload[1]; 19 | state[pathType] = pathName; 20 | }, 21 | setServer: (state, action) => { 22 | state.server = action.payload; 23 | }, 24 | setComponents: (state, action) => { 25 | state.components = action.payload; 26 | }, 27 | setNodeInfo: (state, action) => { 28 | state.nodeInfo = action.payload; 29 | }, 30 | setComponentName: (state, action) => { 31 | state.componentName = action.payload; 32 | }, 33 | }, 34 | }); 35 | 36 | export const { 37 | addPath, 38 | setServer, 39 | setComponents, 40 | setComponentName, 41 | setNodeInfo, 42 | } = projectSlice.actions; 43 | export default projectSlice.reducer; 44 | -------------------------------------------------------------------------------- /src/renderer/src/features/searchSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | searchValue: '', 5 | showSearch: false 6 | }; 7 | 8 | export const searchSlice = createSlice({ 9 | name: 'search', 10 | initialState, 11 | reducers: { 12 | setSearchValue: (state, action) => { 13 | state.searchValue = action.payload; 14 | }, 15 | setShowSearch: (state) => { 16 | state.showSearch = !state.showSearch 17 | } 18 | }, 19 | }); 20 | 21 | export const { 22 | setSearchValue, 23 | setShowSearch 24 | } = searchSlice.actions; 25 | export default searchSlice.reducer; 26 | -------------------------------------------------------------------------------- /src/renderer/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | api: { 4 | send: (channel: string, data: any) => Promise; 5 | }; 6 | } 7 | } 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import './assets/index.css' 5 | import App from './App' 6 | import { Provider } from 'react-redux' 7 | import { store } from './redux/store.js' 8 | 9 | 10 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /src/renderer/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import projectReducer from '../features/projectSlice'; 3 | import detailReducer from '../features/detailSlice'; 4 | import searchReducer from '../features/searchSlice'; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | project: projectReducer, 9 | detail: detailReducer, 10 | search: searchReducer, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/**/*.html', 5 | './', 6 | './src/**/*.tsx', 7 | './src/**/*.jsx', 8 | './src/**/*.ts', 9 | './src/**/*.js', 10 | ], 11 | theme: { 12 | extend: {}, 13 | }, 14 | plugins: [require('daisyui')], 15 | daisyui: { 16 | themes: [ 17 | { 18 | mytheme: { 19 | primary: '#4DA5CB', 20 | secondary: '#FFD059', 21 | accent: '#FF7959', 22 | neutral: '#A6D9EF', 23 | 'base-100': '#A6D9EF', 24 | // 'tree-background': '#22303C', 25 | info: '#73BEDE', 26 | success: '#36d399', 27 | warning: '#FFB909', 28 | error: '#f87272', 29 | }, 30 | borderWidth: { 31 | DEFAULT: '1px', 32 | 0: '0', 33 | 2: '2px', 34 | 3: '3px', 35 | 4: '4px', 36 | 6: '6px', 37 | 8: '8px', 38 | }, 39 | }, 40 | ], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", "src/main/*", "src/preload/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "module": "ESNext", 7 | "target": "ES2020", 8 | "types": ["electron-vite/node"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/**/*", 6 | "src/renderer/src/**/*.tsx", 7 | "src/preload/*.d.ts", 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "jsx": "react-jsx", 12 | "baseUrl": ".", 13 | "paths": { 14 | "@renderer/*": [ 15 | "src/renderer/src/*" 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // export default { 2 | // server: { 3 | // proxy: { 4 | // '/components': 'http://localhost:3000' 5 | // } 6 | // } 7 | // } --------------------------------------------------------------------------------