├── .gitignore ├── README.md ├── babel.config.json ├── client └── src │ ├── App.js │ ├── App.tsx │ ├── components │ ├── CodeEditor.tsx │ ├── OldSidebar.tsx │ ├── Sidebar.tsx │ ├── Topbar.tsx │ └── sidebar.css │ ├── context │ ├── ProjectsProvider.tsx │ └── contextStore.js │ ├── index.js │ ├── ipcRenderer.ts │ ├── pages │ ├── Dashboard │ │ └── Dashboard.tsx │ ├── Diagram │ │ ├── DiagramPage.tsx │ │ ├── components │ │ │ ├── DeleteEdge.tsx │ │ │ ├── Diagram.tsx │ │ │ ├── EndpointFileNode.tsx │ │ │ ├── EndpointNode.tsx │ │ │ ├── FetchFileNode.tsx │ │ │ ├── GetEdge.tsx │ │ │ ├── PatchEdge.tsx │ │ │ ├── PostEdge.tsx │ │ │ ├── PutEdge.tsx │ │ │ └── diagram.css │ │ ├── diagram.css │ │ └── utils │ │ │ └── generateNodes.ts │ ├── Endpoints │ │ └── EndpointsPage.tsx │ ├── Home │ │ ├── HomePage.tsx │ │ └── components │ │ │ └── Home.tsx │ ├── LandingPage │ │ └── LandingPage.tsx │ ├── List │ │ ├── List.tsx │ │ ├── components │ │ │ ├── BackEndList.tsx │ │ │ ├── FetchFileCard.tsx │ │ │ ├── FetchFileDetails.tsx │ │ │ └── FrontEndList.tsx │ │ └── list.css │ ├── Projects │ │ ├── ProjectsPage.tsx │ │ ├── components │ │ │ ├── AddProject.tsx │ │ │ ├── ApprovedExtensions.tsx │ │ │ ├── ListProjects.tsx │ │ │ ├── ProjectDirectories.tsx │ │ │ └── ProjectListCard.tsx │ │ └── projects.css │ └── Settings │ │ ├── SettingsPage.tsx │ │ └── components │ │ ├── Settings.tsx │ │ └── settings.css │ ├── styles.css │ ├── types.ts │ └── variables.css ├── forge.config.js ├── package-lock.json ├── package.json ├── server ├── ast │ ├── clientParser.js │ ├── importsFinder.js │ ├── routerParser.js │ └── serverParser.js ├── ast2 │ ├── Breadcrumb.js │ ├── importsFinder.js │ ├── routerParser.js │ └── serverParser.js ├── harmonode_logo_fullname.png ├── icon.png ├── index.d.ts ├── main.ts ├── types.ts └── utils │ ├── createComponentObject.ts │ ├── getBackEndObj.js │ ├── getFileDirectories.ts │ ├── monitorFileChanges.ts │ ├── pathUtils.ts │ └── stringifyCode.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/*.js 3 | npm-debug.log 4 | .DS_Store 5 | .env 6 | tsCompiled 7 | 8 | out/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | Logo 9 |
As modern web applications continue to evolve, so does the complexity of managing and visualizing endpoints, fetch requests, and data flow between the client-side, routes, and backend components. As your application scales, keeping track of these connections can quickly turn into a nightmare. 10 | Enter Harmonode — an Electron-powered development tool that lightens the challenges of endpoint management and visualization. By harnessing the power of ReactFlow, Harmonode empowers users to seamlessly navigate through the web of connections, offering a clear and concise visualization of the trail each route follows through the component tree of an app, from it's initial request(s) in any frontend components, through the server, any middleware, and back again. 11 |
12 |
13 | 14 | 15 | 16 | 17 | --- 18 | 19 | ## Contents 20 | 21 | - [Usage](#usage) 22 | - [Getting started](#gettingstarted) 23 | - [Contributors](#contributors) 24 | 25 | 26 | 27 | ## Usage 28 |
29 | From the home screen, navigate to "Projects", and click "Add New Project". Follow the prompts to load your project code base, and be sure to designate the name of your backend server file. Fetch requests that are made directly within React components will be readable by Harmonode, which will then render a visual using React Flow of the paths of these requests through the file structure. 30 |
31 | 32 | 33 | --- 34 | 35 | 36 | ![homePageScreenShot](https://github.com/oslabs-beta/Harmonode/assets/68034977/3087b770-6a33-4c80-b60a-c274ef4235f2) 37 | 38 | 39 | 40 | --- 41 | ![ezgif com-optimize (1)](https://github.com/oslabs-beta/Harmonode/assets/126123010/97a61e91-15d1-4c42-9714-dba63dbe0dba) 42 | 43 | 44 | ![ezgif com-optimize (1)](https://github.com/oslabs-beta/Harmonode/assets/126123010/87061dab-96d9-4c4e-96a0-d26463b34db1) 45 | 46 | 47 | --- 48 | 49 | 50 | 51 | ## Getting Started 52 | 53 | Either clone straight from this repo, or download the app directly from [harmonode.com](https://harmonode.com/) and follow the installation directions. 54 | 55 | If cloning from here, run ```npm install```, and make sure that typescript is installed globally. ```npm run dev``` will start the electron app on your local system. Click on "Projects" in the sidebar, then "Add New Project", and load any standard project built with a React frontend and a Node.js backend, preferably with Express. Click on Diagram to view a React Flow visual of fetch paths, or List to view a more detailed view. Various viewing options are available in Settings. 56 | 57 | Currently, simple fetch requests which are passed either strings or complete variables as the url, e.g...
58 |
59 | fetch(someUrl, {method: 'POST', etc...}), or
60 | fetch('localhost:3000/users') 61 | 62 | ...will suffice, although we are building additional functionality for more real-world usability. So, in the future, requests which are being passed concatenated variables will work, as will requests made from util methods being called in frontend components. 63 | 64 | 65 | 66 | ## Contributors 67 | 68 | The founding fathers of Harmonode: 69 | 70 | - Hamza Chaudhry | [@hmz44](https://github.com/hmz44) 71 | - Eric Dalio | [@EricDalio](https://github.com/EricDalio) 72 | - Ken Johnston | [@kfiddle](https://github.com/kfiddle) 73 | - Sebastian Sarmiento | [@sebastiansarm](https://github.com/sebastiansarm/) 74 | - Tim Weidinger | [@timweidinger](https://github.com/timweidinger) 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | 4 | 5 | "presets": ["@babel/preset-env", 6 | "@babel/preset-react" 7 | ] 8 | // "plugins": [ 9 | // ["babel-plugin-react-css-modules", 10 | // { 11 | // "webpackHotModuleReloading": true, 12 | // "autoResolveMultipleImports": true 13 | // }] 14 | // ] 15 | } -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import all components here 3 | import {Sidebar} from 'react-pro-sidebar'; 4 | import LandingPage from './pages/LandingPage/LandingPage'; 5 | import NavBar from './components/Sidebar'; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useReducer } from 'react'; 2 | import { Route, Routes, useNavigate, Navigate } from 'react-router'; 3 | import Home from './pages/Home/components/Home'; 4 | import ProjectsPage from './pages/Projects/ProjectsPage'; 5 | // import Sidebar from './components/newSidebar'; 6 | // import Sidebar from './components/Sidebar'; 7 | import Dashboard from './pages/Dashboard/Dashboard'; 8 | import List from './pages/List/List'; 9 | import Settings from './pages/Settings/SettingsPage'; 10 | import { ProjectsContext } from './context/contextStore'; 11 | import Topbar from './components/Topbar'; 12 | import DiagramPage from './pages/Diagram/DiagramPage'; 13 | import HomePage from './pages/Home/HomePage'; 14 | import Sidebar from './components/Sidebar'; 15 | const { ipcRenderer } = window.require('electron'); 16 | 17 | const App = () => { 18 | const { setActiveProject, dispatchProjects, activeProject } = 19 | useContext(ProjectsContext); 20 | // console.log(activeProject, 'ACTIVE PROJECT FROM APP'); 21 | // mount our event listener on file changes 22 | useEffect(() => { 23 | const handleFileChanged = (e, newAst) => { 24 | dispatchProjects({ 25 | type: 'update', 26 | payload: { ...activeProject, ast: newAst }, 27 | }); 28 | setActiveProject({ ...activeProject, ast: newAst }); 29 | }; 30 | 31 | ipcRenderer.on('fileChanged', handleFileChanged); 32 | 33 | return () => { 34 | ipcRenderer.removeListener('fileChanged', handleFileChanged); 35 | }; 36 | }, [activeProject]); 37 | 38 | return ( 39 | // 40 |
41 | 42 |
43 | 44 | 45 | 46 | } /> 47 | {/* } /> */} 48 | } /> 49 | } /> 50 | } /> 51 | } /> 52 | } /> 53 | 54 |
55 |
56 | //
57 | ); 58 | }; 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /client/src/components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import Editor from 'react-simple-code-editor'; 3 | import {highlight, languages} from 'prismjs/components/prism-core'; 4 | import 'prismjs/components/prism-clike'; 5 | import 'prismjs/components/prism-javascript'; 6 | import 'prismjs/themes/prism.min.css'; //Example style, you can use another 7 | import '../styles.css'; 8 | import {saveStringCode, stringCode} from '../ipcRenderer'; 9 | 10 | function CodeEditor({file, close}) { 11 | const [code, setCode] = useState(''); 12 | const [position, setPosition] = useState({x: 0, y: 0}); 13 | const [initialPosition, setInitialPosition] = useState({x: 0, y: 0}); 14 | const [isDragging, setIsDragging] = useState(false); 15 | const [showSave, setShowSave] = useState(false); 16 | 17 | useEffect(() => { 18 | async function getStringCode() { 19 | const codeString = await stringCode(file.fullPath); 20 | setCode(codeString); 21 | } 22 | getStringCode(); 23 | }, []); 24 | 25 | useEffect(() => { 26 | if (showSave) { 27 | setTimeout(() => { 28 | setShowSave(false); 29 | }, 1500); 30 | } 31 | }, [showSave]); 32 | 33 | function handleClose() { 34 | close(); 35 | setCode(''); 36 | } 37 | 38 | function handleSave() { 39 | saveStringCode(file.fullPath, code); 40 | setShowSave(true); 41 | } 42 | 43 | const handleMouseDown = (e) => { 44 | setIsDragging(true); 45 | setInitialPosition({x: e.clientX, y: e.clientY}); 46 | }; 47 | 48 | const handleMouseUp = (e) => { 49 | setIsDragging(false); 50 | setInitialPosition({x: position.x, y: position.y}); 51 | }; 52 | 53 | const handleMouseMove = (e) => { 54 | if (!isDragging) return; 55 | 56 | const dx = e.clientX - initialPosition.x; 57 | const dy = e.clientY - initialPosition.y; 58 | 59 | setPosition(() => ({ 60 | x: position.x + dx, 61 | y: position.y + dy, 62 | })); 63 | }; 64 | 65 | useEffect(() => { 66 | document.addEventListener('mousemove', handleMouseMove); 67 | document.addEventListener('mouseup', handleMouseUp); 68 | 69 | return () => { 70 | document.removeEventListener('mousemove', handleMouseMove); 71 | document.removeEventListener('mouseup', handleMouseUp); 72 | }; 73 | }, [isDragging]); 74 | 75 | return ( 76 |
85 |
97 |

{file.fileName}

98 |

(last updated: {new Date(file.lastUpdated).toLocaleString()} )

99 |
100 |
101 | setCode(code)} 104 | highlight={(code) => highlight(code, languages.js)} 105 | padding={10} 106 | style={{ 107 | fontFamily: '"Fira code", "Fira Mono", monospace', 108 | fontSize: 12, 109 | }} 110 | /> 111 | {showSave && ( 112 |
113 |

Code is saved!

114 |
115 | )} 116 |
117 |
118 | 121 | 124 |
125 |
126 | ); 127 | } 128 | 129 | export default CodeEditor; 130 | -------------------------------------------------------------------------------- /client/src/components/OldSidebar.tsx: -------------------------------------------------------------------------------- 1 | // import React, { useState, useContext } from 'react'; 2 | 3 | // //import react pro sidebar 4 | // import { 5 | // Sidebar as ReactSidebar, 6 | // Menu, 7 | // MenuItem, 8 | // SubMenu, 9 | // } from 'react-pro-sidebar'; 10 | // import { useNavigate } from 'react-router'; 11 | // //import mui icons for sidebar 12 | // import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; 13 | // import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined'; 14 | // import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; 15 | // import ListAltOutlinedIcon from '@mui/icons-material/ListAltOutlined'; 16 | // import PolylineOutlinedIcon from '@mui/icons-material/PolylineOutlined'; 17 | // import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; 18 | // import KeyboardDoubleArrowRightOutlinedIcon from '@mui/icons-material/KeyboardDoubleArrowRightOutlined'; 19 | // import KeyboardDoubleArrowLeftOutlinedIcon from '@mui/icons-material/KeyboardDoubleArrowLeftOutlined'; 20 | 21 | // function Sidebar() { 22 | // const [sidebarCollapse, setSidebarCollapse] = useState(false); 23 | 24 | // //func that will change sidebarCollapse state from false to true and true to false 25 | // const sidebarIconClick = () => { 26 | // //condition checking to change state from true to false and vice versa 27 | // sidebarCollapse ? setSidebarCollapse(false) : setSidebarCollapse(true); 28 | // }; 29 | 30 | // const navigate = useNavigate(); 31 | 32 | // const handleClick = (path) => { 33 | // console.log('HERE'); 34 | // navigate(path); 35 | // // console.log(window.location.href, 'url'); 36 | // }; 37 | 38 | // // const testClick = () => { 39 | // // dirDispatcher({type: 'setDirTree', payload: 'leaf'}) 40 | // // } 41 | 42 | // return ( 43 | //
44 | // 55 | // {/* */} 56 | //
57 | // {/* small and big changes using sidebarCollapse state */} 58 | //

{sidebarCollapse ? 'Hn' : 'Harmonode'}

59 | //
60 | //
61 | // {/* changing sidebar collapse icon on click */} 62 | // {sidebarCollapse ? ( 63 | // 64 | // ) : ( 65 | // 66 | // )} 67 | //
68 | // {/*
*/} 69 | // {/* 72 | // */} 73 | // 78 | // } 80 | // onClick={() => handleClick('/home')} 81 | // > 82 | // Home 83 | // 84 | // } 86 | // onClick={() => handleClick('/projects')} 87 | // > 88 | // Projects 89 | // 90 | // } 92 | // onClick={() => handleClick('/dashboard')} 93 | // > 94 | // Dashboard 95 | // 96 | // } 98 | // onClick={() => handleClick('/list')} 99 | // > 100 | // List 101 | // 102 | // } 104 | // onClick={() => handleClick('/diagram')} 105 | // > 106 | // Diagram 107 | // 108 | // } 110 | // onClick={() => handleClick('/settings')} 111 | // > 112 | // Settings 113 | // 114 | // 115 | //
116 | //
117 | // ); 118 | // } 119 | 120 | // export default Sidebar; 121 | -------------------------------------------------------------------------------- /client/src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router'; 3 | import { MenuItem, Menu } from 'react-pro-sidebar'; 4 | import './sidebar.css'; 5 | import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; 6 | import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined'; 7 | import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; 8 | import ListAltOutlinedIcon from '@mui/icons-material/ListAltOutlined'; 9 | import PolylineOutlinedIcon from '@mui/icons-material/PolylineOutlined'; 10 | import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; 11 | // import KeyboardDoubleArrowRightOutlinedIcon from '@mui/icons-material/KeyboardDoubleArrowRightOutlined'; 12 | // import KeyboardDoubleArrowLeftOutlinedIcon from '@mui/icons-material/KeyboardDoubleArrowLeftOutlined'; 13 | // import { IconButton } from '@mui/material'; 14 | 15 | const menuItems = [ 16 | { 17 | label: 'Home', 18 | icon: , 19 | path: '/home', 20 | }, 21 | { 22 | label: 'Projects', 23 | icon: , 24 | path: '/projects', 25 | }, 26 | // { 27 | // label: 'Dashboard', 28 | // icon: , 29 | // path: '/dashboard', 30 | // }, 31 | { 32 | label: 'Diagram', 33 | icon: , 34 | path: '/diagram', 35 | }, 36 | { 37 | label: 'List', 38 | icon: , 39 | path: '/list', 40 | }, 41 | { 42 | label: 'Settings', 43 | icon: , 44 | path: '/settings', 45 | }, 46 | ]; 47 | 48 | const Sidebar = () => { 49 | const navigate = useNavigate(); 50 | 51 | const [selectedMenuItem, setSelectedMenuItem] = useState(null); 52 | const [collapsed, setCollapsed] = useState(false); 53 | 54 | const toggleSidebar = () => { 55 | setCollapsed(!collapsed); 56 | }; 57 | 58 | const handleClick = (path) => { 59 | navigate(path); 60 | }; 61 | 62 | const handleMenuItemClick = (idx) => { 63 | setSelectedMenuItem(idx); 64 | }; 65 | 66 | return ( 67 |
68 | {/*
69 | 70 | {collapsed ? ( 71 | 72 | ) : ( 73 | 74 | )} 75 | */} 76 | 77 | {menuItems.map((item, idx) => ( 78 | { 82 | handleMenuItemClick(idx); 83 | handleClick(item.path); 84 | }} 85 | style={ 86 | selectedMenuItem === idx ? { backgroundColor: '#143542' } : {} 87 | } 88 | > 89 | 90 | {item.label} 91 | 92 | 93 | ))} 94 | 95 |
96 | ); 97 | }; 98 | 99 | export default Sidebar; 100 | -------------------------------------------------------------------------------- /client/src/components/Topbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { ProjectsContext } from '../context/contextStore'; 3 | 4 | function Topbar() { 5 | const { activeProject } = useContext(ProjectsContext); 6 | console.log(activeProject); 7 | return ( 8 |
9 | {activeProject.name ? ( 10 |

11 | Active Project: {activeProject.name} ({activeProject.folder}) 12 |

13 | ) : ( 14 |

No Active Project

15 | )} 16 |
17 | ); 18 | } 19 | 20 | export default Topbar; 21 | -------------------------------------------------------------------------------- /client/src/components/sidebar.css: -------------------------------------------------------------------------------- 1 | .main-menu:hover, 2 | nav.main-menu.expanded { 3 | width: 250px; 4 | overflow: visible; 5 | } 6 | 7 | .main-menu { 8 | background-color: var(--secondary-color); 9 | border-right: 1px solid #e5e5e5; 10 | 11 | top: 0; 12 | bottom: 0; 13 | height: 100%; 14 | left: 0; 15 | width: 66px; 16 | overflow: hidden; 17 | -webkit-transition: width 0.05s linear; 18 | transition: width 0.2s linear; 19 | -webkit-transform: translateZ(0) scale(1, 1); 20 | z-index: 1000; 21 | } 22 | 23 | /* .main-menu > ul { 24 | margin: 10px 10px 10px 10px; 25 | } */ 26 | 27 | /* icons */ 28 | .main-menu li { 29 | position: relative; 30 | display: block; 31 | width: 250px; 32 | } 33 | 34 | .main-menu li > a { 35 | position: relative; 36 | display: table; 37 | border-collapse: collapse; 38 | border-spacing: 0; 39 | font-family: 'Roboto' sans-serif; 40 | font-size: 14px; 41 | text-decoration: none; 42 | -webkit-transform: translateZ(0) scale(1, 1); 43 | -webkit-transition: all 0.1s linear; 44 | transition: all 0.1s linear; 45 | } 46 | 47 | .main-menu .nav-icon { 48 | position: relative; 49 | display: table-cell; 50 | width: 60px; 51 | height: 36px; 52 | text-align: center; 53 | vertical-align: middle; 54 | font-size: 18px; 55 | color: #999; 56 | } 57 | .sidebar-header { 58 | display: flex; 59 | justify-content: flex-start; 60 | align-items: center; 61 | padding: 10px; 62 | background-color: #143542; 63 | color: #fff; 64 | } 65 | 66 | .main-menu .nav-text { 67 | position: relative; 68 | vertical-align: middle; 69 | width: 190px; 70 | font-family: 'Roboto', sans-serif; 71 | color: var(--font-main-color); 72 | font-size: large; 73 | } 74 | 75 | /* .main-menu > ul.logout { 76 | position: absolute; 77 | left: 0; 78 | bottom: 0; 79 | } */ 80 | 81 | .no-touch .scrollable.hover { 82 | overflow-y: hidden; 83 | } 84 | 85 | .no-touch .scrollable.hover:hover { 86 | overflow-y: auto; 87 | overflow: visible; 88 | } 89 | 90 | a:hover a:focus { 91 | text-decoration: none; 92 | } 93 | 94 | nav { 95 | -webkit-user-select: none; 96 | -moz-user-select: none; 97 | -ms-user-select: none; 98 | -o-user-select: none; 99 | user-select: none; 100 | } 101 | 102 | nav ul, 103 | nav li { 104 | outline: 0; 105 | margin: 0; 106 | padding: 0; 107 | } 108 | 109 | .main-menu li:hover > a, 110 | nav.main-menu li.active > a, 111 | .dropdown-menu > li > a:hover .dropdown-menu > li > a:focus, 112 | .dropdown-menu > .active > a, 113 | .dropdown-menu > .active > a:hover, 114 | .dropdown-menu > .active > a:focus, 115 | .no-touch .dashboard-page nav.dashboard-menu ul li:hover a, 116 | .dashboard-page nav.dashboard-menu ul li.active a { 117 | background-color: #143542 !important; 118 | } 119 | .area { 120 | float: left; 121 | background: #e2e2e2; 122 | width: 100%; 123 | height: 100%; 124 | } 125 | @font-face { 126 | font-family: 'Roboto'; 127 | font-style: normal; 128 | font-weight: 700; 129 | src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lbBP.ttf) 130 | format('truetype'); 131 | } 132 | -------------------------------------------------------------------------------- /client/src/context/ProjectsProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useEffect, useReducer, useState} from 'react'; 3 | import {ProjectsContext} from './contextStore.js'; 4 | import {getProjects, storeProjects} from '../ipcRenderer'; 5 | 6 | function projectsReducer(state, {type, payload}) { 7 | switch (type) { 8 | case 'add': 9 | const newStateAdd = [...state, payload]; 10 | storeProjects(newStateAdd); 11 | return newStateAdd; 12 | case 'delete': 13 | const newStateDelete = state.filter( 14 | (project) => project.name !== payload.name 15 | ); 16 | storeProjects(newStateDelete); 17 | return newStateDelete; 18 | case 'update': 19 | const newStateUpdate = state.map((project) => 20 | project.id === payload.id ? payload : project 21 | ); 22 | storeProjects(newStateUpdate); 23 | return newStateUpdate; 24 | case 'load': 25 | return payload; 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | async function initProjects() { 32 | const projects = await getProjects(); 33 | 34 | return projects ? projects : []; 35 | } 36 | 37 | export default function ProjectsProvider({children}) { 38 | const [projects, dispatchProjects] = useReducer(projectsReducer, []); 39 | // fill out empty stuff for active project so it doesn't throw errors elsewhere when we try 40 | // to iterate over stuff 41 | const [activeProject, setActiveProject] = useState({ 42 | ast: { 43 | fetchFiles: [], 44 | endPointFiles: [], 45 | fetches: [], 46 | endpoints: [], 47 | }, 48 | fileName: '', 49 | extensions: [], 50 | id: '', 51 | ignore: [], 52 | name: '', 53 | server: '', 54 | }); 55 | 56 | useEffect(() => { 57 | async function dispatchStoredProjects() { 58 | dispatchProjects({type: 'load', payload: await initProjects()}); 59 | } 60 | dispatchStoredProjects(); 61 | }, []); 62 | 63 | return ( 64 | 67 | {children} 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /client/src/context/contextStore.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const DirTreeHolder = React.createContext(); 4 | 5 | export const ProjectsContext = React.createContext({ 6 | projects: [], 7 | dispatchProjects: (projects) => {}, 8 | activeProject: {}, 9 | setActiveProject: (project) => {}, 10 | }); 11 | 12 | // we will want to kind of mimic what we did above 13 | // have an active theme, a function to set the active theme 14 | // a list of themes (to choose from) 15 | 16 | export const SettingsContext = React.createContext({ 17 | activeTheme: {}, 18 | }); 19 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './variables.css'; 4 | import './styles.css'; 5 | import {BrowserRouter as Router} from 'react-router-dom'; 6 | 7 | import App from './App'; 8 | import ProjectsProvider from './context/ProjectsProvider'; 9 | 10 | const root = document.createElement('div'); 11 | root.id = 'root'; 12 | document.body.appendChild(root); 13 | 14 | const htmlRoot = ReactDOM.createRoot(document.getElementById('root')); 15 | 16 | htmlRoot.render( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /client/src/ipcRenderer.ts: -------------------------------------------------------------------------------- 1 | const {ipcRenderer} = window.require('electron'); 2 | 3 | export async function storeProjects(projects) { 4 | ipcRenderer.invoke('storeProjects', projects); 5 | } 6 | 7 | export async function getProjects() { 8 | const projects = await ipcRenderer.invoke('getProjects'); 9 | return Array.isArray(projects) ? projects : []; 10 | } 11 | 12 | export async function loadProject(project) { 13 | const updatedProject = await ipcRenderer.invoke('loadProject', project); 14 | return updatedProject; 15 | } 16 | 17 | export async function stringCode(filePath) { 18 | const stringCode = await ipcRenderer.invoke('stringCode', filePath); 19 | return stringCode; 20 | } 21 | 22 | export async function saveStringCode(filePath, code) { 23 | await ipcRenderer.invoke('saveCode', filePath, code); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/pages/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // dashboard will hold number of endpoints, number of fetches 3 | 4 | function Dashboard() { 5 | function handleSubmit(e, variable) { 6 | e.preventDefault(); 7 | document.documentElement.style.setProperty( 8 | variable, 9 | e.target.inputField.value 10 | ); 11 | } 12 | 13 | return ( 14 |
15 |
Dashboard Page!!!!!
16 | {/*
handleSubmit(e, '--primary-color')} 18 | style={{ display: 'flex' }} 19 | > 20 | 21 | 22 |
23 |
handleSubmit(e, '--secondary-color')} 25 | style={{ display: 'flex' }} 26 | > 27 | 28 | 29 |
30 |
handleSubmit(e, '--tertiary-color')} 32 | style={{ display: 'flex' }} 33 | > 34 | 35 | 36 |
37 |
handleSubmit(e, '--quaternary-color')} 39 | style={{ display: 'flex' }} 40 | > 41 | 42 | 43 |
44 |
handleSubmit(e, '--font-main-color')} 46 | style={{ display: 'flex' }} 47 | > 48 | 49 | 50 |
*/} 51 |
52 | ); 53 | } 54 | //--font-main-color 55 | export default Dashboard; 56 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/DiagramPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ReactFlowProvider} from 'reactflow'; 3 | import Diagram from './components/Diagram'; 4 | import './diagram.css'; 5 | 6 | function DiagramPage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default DiagramPage; 15 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/DeleteEdge.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import {BaseEdge, EdgeProps, getBezierPath, EdgeLabelRenderer} from 'reactflow'; 3 | // import './buttonedge.css'; 4 | 5 | export default function DeleteEdge({ 6 | id, 7 | sourceX, 8 | sourceY, 9 | targetX, 10 | targetY, 11 | sourcePosition, 12 | targetPosition, 13 | style = {}, 14 | markerEnd, 15 | }: EdgeProps) { 16 | const [edgePath, labelX, labelY] = getBezierPath({ 17 | sourceX, 18 | sourceY, 19 | sourcePosition, 20 | targetX, 21 | targetY, 22 | targetPosition, 23 | }); 24 | 25 | const edgeStyle = { 26 | ...style, 27 | stroke: 'red', 28 | strokeWidth: 3, 29 | }; 30 | 31 | return ( 32 | <> 33 | 34 | 35 | 36 |
51 |
52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/Diagram.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useState, useContext, useEffect} from 'react'; 2 | import {v4 as uuid} from 'uuid'; 3 | import './diagram.css'; 4 | import {ProjectsContext} from '../../../context/contextStore'; 5 | import ReactFlow, { 6 | useNodesState, 7 | useEdgesState, 8 | addEdge, 9 | MiniMap, 10 | Controls, 11 | Background, 12 | BackgroundVariant, 13 | Panel, 14 | useReactFlow, 15 | useNodesInitialized, 16 | } from 'reactflow'; 17 | import 'reactflow/dist/style.css'; 18 | import FetchFileNode from './FetchFileNode'; 19 | import GetEdge from './GetEdge'; 20 | import PostEdge from './PostEdge'; 21 | import PutEdge from './PutEdge'; 22 | import PatchEdge from './PatchEdge'; 23 | import DeleteEdge from './DeleteEdge'; 24 | import CodeEditor from '../../../components/CodeEditor'; 25 | import EndpointFileNode from './EndpointFileNode'; 26 | import EndpointNode from './EndpointNode'; 27 | 28 | const nodeTypes = { 29 | fetchFileNode: FetchFileNode, 30 | endpointFileNode: EndpointFileNode, 31 | endpointNode: EndpointNode, 32 | }; 33 | const edgeTypes = { 34 | getEdge: GetEdge, 35 | postEdge: PostEdge, 36 | putEdge: PutEdge, 37 | patchEdge: PatchEdge, 38 | deleteEdge: DeleteEdge, 39 | }; 40 | 41 | function Diagram() { 42 | const {fitView} = useReactFlow(); 43 | const {activeProject} = useContext(ProjectsContext); 44 | const [showEditor, setShowEditor] = useState(false); 45 | const [editorFile, setEditorFile] = useState({}); 46 | 47 | if (activeProject.ast.fetches.length === 0) return

No project loaded

; 48 | // eventually going to use this paths to generate all of the path nodes and all of the edges 49 | 50 | function clickEdit(file) { 51 | setEditorFile(file); 52 | setShowEditor(true); 53 | } 54 | 55 | // codMinimap colors 56 | 57 | const nodeColor = (node) => { 58 | switch (node.type) { 59 | case 'fetchFileNode': 60 | return '#19A7CE'; 61 | case 'endpointNode': 62 | return '#98DFD6'; 63 | case '3': 64 | return '#FCC8D1'; 65 | case '4': 66 | return '#FFF89C'; 67 | case 'errorNode': 68 | return '#ff0072'; 69 | default: 70 | return '#F1F1F1'; 71 | } 72 | }; 73 | //diagram colors 74 | const fetchFileNode = { 75 | border: '3px solid #146C94', 76 | background: '#19A7CE', 77 | borderRadius: 15, 78 | }; 79 | 80 | const endpointNode = { 81 | border: '3px solid #db7d3e', 82 | background: '#ccb022', 83 | color: 'black', 84 | fontSize: '.75em', 85 | width: '12.5em', 86 | display: 'flex', 87 | justifyContent: 'center', 88 | borderRadius: 15, 89 | }; 90 | 91 | const endpointFileNode = { 92 | border: '3px solid #32c371', 93 | background: '#1a934e', 94 | fontSize: '.85em', 95 | width: '11.25em', 96 | display: 'flex', 97 | justifyContent: 'center', 98 | borderRadius: 15, 99 | }; 100 | 101 | const errorNode = { 102 | border: '3px solid #ff0071', 103 | background: '#FCC8D1', 104 | borderRadius: 15, 105 | }; 106 | // State for switching between vertical and horizontal view 107 | const [orientation, setOrientation] = useState('horizontal'); 108 | const uniquePaths = getUniquePaths(activeProject); 109 | const allPaths = [ 110 | ...activeProject.ast.endpoints, 111 | ...activeProject.ast.fetches, 112 | ]; 113 | 114 | function getUniquePaths(project) { 115 | const tempCache = {}; 116 | // get all of the paths and remove duplicates 117 | return [...project.ast.fetches, ...project.ast.endpoints] 118 | .map((path) => { 119 | if (tempCache[path.id]) return null; 120 | tempCache[path.id] = true; 121 | return {path: path.path, id: path.id}; 122 | }) 123 | .filter((path) => path !== null); 124 | } 125 | 126 | function generateEndpointFiles(paths) { 127 | const pathCache: any = {}; 128 | const pathsArray: any = []; 129 | for (const path of paths) { 130 | pathCache[path].hasOwnProperty('method') 131 | ? pathCache[path].method.push(path.method) 132 | : (pathCache[path] = {method: [path.method]}); 133 | } 134 | for (const key of Object.keys(pathCache)) { 135 | pathsArray.push(pathCache[key]); 136 | } 137 | return pathsArray; 138 | } 139 | function generateNodes(project = activeProject, orientation) { 140 | // Common spacing for horizontal/vertical stacking 141 | 142 | // Generate the nodes 143 | const initEndpointNodes: any = uniquePaths.map((path: any) => { 144 | return { 145 | id: path.id, 146 | animated: true, 147 | data: {label: path.path}, 148 | style: endpointNode, 149 | type: 'endpointNode', 150 | }; 151 | }); 152 | 153 | const initFetchFileNodes = project.ast.fetchFiles.map((file) => { 154 | return { 155 | id: file.id, // This is fetchFiles.id 156 | animated: true, 157 | data: {label: file.fileName, file: file, showEditor: clickEdit}, //each file needs an id and we'll use the id to connect the nodes 158 | style: fetchFileNode, 159 | type: 'fetchFileNode', 160 | }; 161 | }); 162 | 163 | const initEndpointFileNodes = project.ast.endpointFiles.map((file) => { 164 | return { 165 | id: file.id, 166 | animated: true, 167 | data: {label: file.fileName, file: file, showEditor: clickEdit}, 168 | style: endpointFileNode, 169 | type: 'endpointFileNode', 170 | }; 171 | }); 172 | // Apply spacing based on the longest node length 173 | 174 | // get the count of all the files 175 | const endpointCount = initEndpointNodes.length - 1; 176 | const fetchFileCount = initFetchFileNodes.length - 1; 177 | const endpointFileCount = initEndpointFileNodes.length - 1; 178 | // get the max length to control spacing 179 | const diagSpacing = Math.max(endpointCount, fetchFileCount) * 160; 180 | 181 | // endpoint spacing 182 | initEndpointNodes.forEach((node, i) => { 183 | const position = 184 | orientation === 'horizontal' 185 | ? { 186 | x: i * (diagSpacing / endpointCount), 187 | y: 200, 188 | } 189 | : { 190 | x: diagSpacing / endpointCount, 191 | y: i * (diagSpacing / 2 / endpointCount), 192 | }; 193 | node.position = position; 194 | }); 195 | 196 | // fetchFile spacing 197 | initFetchFileNodes.forEach((node, i) => { 198 | const position = 199 | orientation === 'horizontal' 200 | ? {x: i * (diagSpacing / fetchFileCount), y: 0} 201 | : {x: 0, y: i * (diagSpacing / fetchFileCount / 2)}; 202 | 203 | node.position = position; 204 | }); 205 | 206 | // endPointFile spacing 207 | initEndpointFileNodes.forEach((node, i) => { 208 | const position = 209 | orientation === 'horizontal' 210 | ? {x: i * (diagSpacing / endpointFileCount), y: 400} 211 | : {x: 0, y: i * (diagSpacing / fetchFileCount / 2)}; 212 | node.position = position; 213 | }); 214 | 215 | return [ 216 | ...initFetchFileNodes, 217 | ...initEndpointNodes, 218 | ...initEndpointFileNodes, 219 | ]; 220 | } 221 | 222 | function returnEdgeType(method) { 223 | switch (method) { 224 | case 'GET': 225 | return ['getEdge', 'a', 'f']; 226 | case 'POST': 227 | return ['postEdge', 'b', 'g']; 228 | case 'PUT': 229 | return ['putEdge', 'c', 'h']; 230 | case 'PATCH': 231 | return ['patchEdge', 'd', 'i']; 232 | case 'DELETE': 233 | return ['deleteEdge', 'e', 'j']; 234 | default: 235 | return ['getEdge', 'a', 'f']; 236 | } 237 | } 238 | 239 | // Need to add functionality so that for each proj. load.. 240 | // it will create nodes based on what is necesscary 241 | // We determine how many nodes are necesscary based on what user selected and on fileLoad for count? 242 | function generateEdges(project = activeProject) { 243 | const fetchFileEdges = project.ast.fetchFiles.flatMap((file, idx) => { 244 | return file.fetches 245 | .map((fetch) => { 246 | let endpoint; 247 | if (!endpoint) { 248 | endpoint = allPaths.find( 249 | (endpoint) => endpoint.path === fetch.path 250 | ); 251 | } 252 | if (endpoint) { 253 | const edgeTypeArray = returnEdgeType(fetch.method); // 'GET' 254 | return { 255 | id: uuid(), 256 | animated: true, 257 | target: endpoint.id, 258 | source: file.id, 259 | type: edgeTypeArray[0], 260 | sourceHandle: edgeTypeArray[1], 261 | targetHandle: edgeTypeArray[1], 262 | data: {file: fetch}, 263 | }; 264 | } 265 | endpoint = null; 266 | return null; 267 | }) 268 | .filter((edge) => edge !== null); 269 | }); 270 | 271 | const endpointFileEdges = project.ast.endpointFiles.flatMap((file) => { 272 | return file.endpoints 273 | .map((endpoint) => { 274 | let endpt; 275 | if (!endpt) { 276 | endpt = allPaths.find((endpnt) => endpnt.path === endpoint.path); 277 | } 278 | if (endpt) { 279 | const edgeTypeArray = returnEdgeType(endpoint.method); 280 | return { 281 | id: uuid(), 282 | animated: true, 283 | target: endpt.id, 284 | source: file.id, 285 | type: edgeTypeArray[0], 286 | sourceHandle: edgeTypeArray[1], 287 | targetHandle: edgeTypeArray[2], 288 | data: {file: endpoint}, 289 | }; 290 | } 291 | endpt = null; 292 | return null; 293 | }) 294 | .filter((edge) => edge !== null); 295 | }); 296 | 297 | return [...endpointFileEdges, ...fetchFileEdges]; 298 | } 299 | 300 | // State Management for nodes and edges (connectors) 301 | 302 | const [nodes, setNodes, onNodesChange] = useNodesState( 303 | generateNodes(activeProject, orientation) 304 | ); 305 | const [edges, setEdges, onEdgesChange] = useEdgesState( 306 | generateEdges(activeProject) 307 | ); 308 | const nodesInitialized = useNodesInitialized(); 309 | 310 | useEffect(() => { 311 | setNodes(generateNodes(activeProject, orientation)); 312 | setEdges(generateEdges(activeProject)); 313 | }, [activeProject, orientation]); 314 | 315 | useEffect(() => { 316 | fitView(); 317 | }, [nodesInitialized]); 318 | 319 | const onConnect = useCallback( 320 | (params) => setEdges((eds) => addEdge(params, eds)), // Not doing anything for now 321 | [setEdges] 322 | ); 323 | 324 | const handleHorizontalClick = () => { 325 | setOrientation('horizontal'); 326 | }; 327 | const handleVerticalClick = () => { 328 | setOrientation('vertical'); 329 | }; 330 | 331 | // const onNodeClick = useCallback((event, node) => { 332 | // console.log(node, 'node clicked'); 333 | // }, []); 334 | 335 | return ( 336 |
344 | {showEditor && ( 345 |
346 | setShowEditor(false)} /> 347 |
348 | )} 349 | 359 |
    366 |
    370 | GET 371 |
  • 372 |
    376 | POST 377 |
  • 378 |
  • 379 |
    383 | PUT 384 |
  • 385 |
  • 386 |
    390 | PATCH 391 |
  • 392 |
  • 393 |
    397 | DELETE 398 |
  • 399 |
400 | 401 | 404 | 407 | 408 | 409 | 410 | {/* 416 | */} 423 |
424 |
425 | ); 426 | } 427 | 428 | export default Diagram; 429 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/EndpointFileNode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Handle, Position} from 'reactflow'; 3 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; 4 | import {faPenToSquare} from '@fortawesome/free-solid-svg-icons'; 5 | 6 | const handleStyle = {left: 11}; 7 | const handleStyle2 = {left: 42}; 8 | const handleStyle3 = {left: 73}; 9 | const handleStyle4 = {left: 104}; 10 | const handleStyle5 = {left: 135}; 11 | 12 | const editIcon = ; 13 | 14 | function EndpointFileNode({data, isConnectable}) { 15 | function handleClick() { 16 | data.showEditor(data.file); 17 | } 18 | 19 | return ( 20 |
21 | 26 |
27 | {data.label} 28 | 31 |
32 | 33 | 40 | 47 | 54 | 61 | 68 |
69 | ); 70 | } 71 | 72 | export default EndpointFileNode; 73 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/EndpointNode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Handle, Position} from 'reactflow'; 3 | import {v4 as uuid} from 'uuid'; 4 | 5 | const handleStyles: any = [ 6 | {left: 11}, 7 | {left: 42}, 8 | {left: 73}, 9 | {left: 104}, 10 | {left: 135}, 11 | ]; 12 | const idListTop = ['a', 'b', 'c', 'd', 'e']; 13 | const idListBottom = ['f', 'g', 'h', 'i', 'j']; 14 | 15 | function EndpointNode({data, isConnectable}) { 16 | return ( 17 |
18 | {handleStyles.map((style, i) => { 19 | return ( 20 | 28 | ); 29 | })} 30 |
{data.label}
31 | {handleStyles.map((style, i) => { 32 | return ( 33 | 41 | ); 42 | })} 43 |
44 | ); 45 | } 46 | 47 | export default EndpointNode; 48 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/FetchFileNode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Handle, Position} from 'reactflow'; 3 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; 4 | import {faPenToSquare} from '@fortawesome/free-solid-svg-icons'; 5 | 6 | const handleStyle = {left: 11}; 7 | const handleStyle2 = {left: 42}; 8 | const handleStyle3 = {left: 73}; 9 | const handleStyle4 = {left: 104}; 10 | const handleStyle5 = {left: 135}; 11 | 12 | const editIcon = ; 13 | 14 | function FetchFileNode({data, isConnectable}) { 15 | function handleClick() { 16 | data.showEditor(data.file); 17 | } 18 | 19 | return ( 20 |
21 | 26 |
27 | {data.label} 28 | 31 |
32 | 33 | 40 | 47 | 54 | 61 | 68 |
69 | ); 70 | } 71 | 72 | export default FetchFileNode; 73 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/GetEdge.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import {BaseEdge, EdgeProps, getBezierPath, EdgeLabelRenderer} from 'reactflow'; 3 | // import './buttonedge.css'; 4 | 5 | export default function GetEdge({ 6 | id, 7 | sourceX, 8 | sourceY, 9 | targetX, 10 | targetY, 11 | sourcePosition, 12 | targetPosition, 13 | style = {}, 14 | markerEnd, 15 | data, 16 | }: EdgeProps) { 17 | const [edgePath, labelX, labelY] = getBezierPath({ 18 | sourceX, 19 | sourceY, 20 | sourcePosition, 21 | targetX, 22 | targetY, 23 | targetPosition, 24 | }); 25 | 26 | const edgeStyle = { 27 | ...style, 28 | stroke: 'limegreen', 29 | strokeWidth: 3, 30 | }; 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 |
52 |
53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/PatchEdge.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import {BaseEdge, EdgeProps, getBezierPath, EdgeLabelRenderer} from 'reactflow'; 3 | // import './buttonedge.css'; 4 | 5 | export default function PatchEdge({ 6 | id, 7 | sourceX, 8 | sourceY, 9 | targetX, 10 | targetY, 11 | sourcePosition, 12 | targetPosition, 13 | style = {}, 14 | markerEnd, 15 | }: EdgeProps) { 16 | const [edgePath, labelX, labelY] = getBezierPath({ 17 | sourceX, 18 | sourceY, 19 | sourcePosition, 20 | targetX, 21 | targetY, 22 | targetPosition, 23 | }); 24 | 25 | const edgeStyle = { 26 | ...style, 27 | stroke: 'orange', 28 | strokeWidth: 3, 29 | }; 30 | 31 | return ( 32 | <> 33 | 34 | 35 | 36 |
51 |
52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/PostEdge.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import {BaseEdge, EdgeProps, getBezierPath, EdgeLabelRenderer} from 'reactflow'; 3 | // import './buttonedge.css'; 4 | 5 | export default function PostEdge({ 6 | id, 7 | sourceX, 8 | sourceY, 9 | targetX, 10 | targetY, 11 | sourcePosition, 12 | targetPosition, 13 | style = {}, 14 | markerEnd, 15 | data, 16 | }: EdgeProps) { 17 | const [edgePath, labelX, labelY] = getBezierPath({ 18 | sourceX, 19 | sourceY, 20 | sourcePosition, 21 | targetX, 22 | targetY, 23 | targetPosition, 24 | }); 25 | 26 | const [showData, setShowData] = useState(false); 27 | const edgeStyle = { 28 | ...style, 29 | stroke: 'blue', 30 | strokeWidth: 3, 31 | }; 32 | 33 | const bodyKeys = data.file.body ? data.file.body.keys : []; 34 | 35 | return ( 36 | <> 37 | 38 | 39 | 40 |
setShowData(true)} 57 | onMouseLeave={() => setShowData(false)} 58 | > 59 | {showData && ( 60 |
67 |

Data

68 | 69 |
70 |

Body:

71 |
    72 | {bodyKeys.length > 0 ? ( 73 | bodyKeys.map((key) => ( 74 |
  • {key}
  • 75 | )) 76 | ) : ( 77 |

    'No data'

    78 | )} 79 |
80 |
81 |
82 | )} 83 |
84 |
85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/PutEdge.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import {BaseEdge, EdgeProps, getBezierPath, EdgeLabelRenderer} from 'reactflow'; 3 | // import './buttonedge.css'; 4 | 5 | export default function PutEdge({ 6 | id, 7 | sourceX, 8 | sourceY, 9 | targetX, 10 | targetY, 11 | sourcePosition, 12 | targetPosition, 13 | style = {}, 14 | markerEnd, 15 | }: EdgeProps) { 16 | const [edgePath, labelX, labelY] = getBezierPath({ 17 | sourceX, 18 | sourceY, 19 | sourcePosition, 20 | targetX, 21 | targetY, 22 | targetPosition, 23 | }); 24 | 25 | const edgeStyle = { 26 | ...style, 27 | stroke: 'violet', 28 | strokeWidth: 3, 29 | }; 30 | 31 | return ( 32 | <> 33 | 34 | 35 | 36 |
51 |
52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/components/diagram.css: -------------------------------------------------------------------------------- 1 | .verticalButton { 2 | width: 50%; 3 | margin-left: 50%; 4 | height: 50px; 5 | } 6 | .horizontalButton { 7 | width: 50%; 8 | margin-left: 50%; 9 | height: 50px; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/diagram.css: -------------------------------------------------------------------------------- 1 | .diagram-node { 2 | height: 2.125rem; 3 | width: 9em; 4 | justify-content: center; 5 | display: flex; 6 | align-items: center; 7 | } 8 | 9 | .diagram-button { 10 | background: none; 11 | padding: 0; 12 | margin: 0.1em 0 0 0.25em; 13 | width: 1em; 14 | align-self: first baseline; 15 | justify-self: end; 16 | cursor: pointer; 17 | translate: 5px; 18 | } 19 | 20 | .diagram-code-editor { 21 | position: fixed; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | z-index: 3; 27 | background-color: rgba(0, 0, 0, 0.5); 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | } 32 | 33 | .diagram-legend-item { 34 | height: 1em; 35 | width: 2em; 36 | display: inline-block; 37 | margin-right: 0.5em; 38 | } 39 | -------------------------------------------------------------------------------- /client/src/pages/Diagram/utils/generateNodes.ts: -------------------------------------------------------------------------------- 1 | const fetchFileNode = { 2 | border: '3px solid #146C94', 3 | background: '#19A7CE', 4 | borderRadius: 15, 5 | }; 6 | 7 | const endpointNode = { 8 | border: '3px solid #1B9C85', 9 | background: '#98DFD6', 10 | borderRadius: 15, 11 | }; 12 | 13 | const defaultNodeStyle3 = { 14 | border: '3px solid #FFEA11', 15 | background: '#FFF89C', 16 | borderRadius: 15, 17 | }; 18 | 19 | const errorNode = { 20 | border: '3px solid #ff0071', 21 | background: '#FCC8D1', 22 | borderRadius: 15, 23 | }; 24 | -------------------------------------------------------------------------------- /client/src/pages/Endpoints/EndpointsPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function EndpointsPage() { 4 | return ( 5 |
EndpointsPage
6 | ) 7 | } 8 | 9 | export default EndpointsPage -------------------------------------------------------------------------------- /client/src/pages/Home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from './components/Home'; 3 | 4 | function HomePage() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default HomePage; 13 | -------------------------------------------------------------------------------- /client/src/pages/Home/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import icon from '../../../../../server/icon.png'; 3 | import fullnameIcon from '../../../../../server/harmonode_logo_fullname.png'; 4 | 5 | 6 | function Home() { 7 | const ImageCenter: React.CSSProperties = { 8 | textAlign: 'center', 9 | }; 10 | const image: React.CSSProperties = { 11 | width: '450px', 12 | marginTop: 25, 13 | marginBottom: 0, 14 | marginRight: '8rem' 15 | }; 16 | const titleText: React.CSSProperties = { 17 | fontStyle: 'bold', 18 | fontSize: 75, 19 | color: '#42A186', 20 | marginTop: 0, 21 | marginBottom: 3, 22 | textAlign: 'center', 23 | }; 24 | const italicText: React.CSSProperties = { 25 | fontStyle: 'italic', 26 | fontSize: 35, 27 | margin: 0, 28 | textAlign: 'center', 29 | // color: '#868484', 30 | }; 31 | const subTitleText: React.CSSProperties = { 32 | fontSize: 25, 33 | margin: 0, 34 | textAlign: 'center', 35 | marginTop: 10, 36 | color: '#42A186', 37 | }; 38 | const headingText: React.CSSProperties = { 39 | fontStyle: 'bold', 40 | fontSize: 25, 41 | margin: 0, 42 | paddingLeft: 20, 43 | color: '#42A186', 44 | }; 45 | const pText: React.CSSProperties = { 46 | fontSize: 18, 47 | margin: "1rem", 48 | paddingLeft: 20, 49 | }; 50 | const bannerContainer: React.CSSProperties = { 51 | display: 'flex', 52 | marginBottom: '5rem', 53 | // justifyContent: 'space-around' 54 | }; 55 | 56 | const bannerTextContainer: React.CSSProperties = { 57 | display: "flex", 58 | // alignItems: "flex-start", 59 | flexDirection: "column", 60 | justifyContent: "center", 61 | paddingTop: "2.5rem" 62 | }; 63 | 64 | const contentContainer: React.CSSProperties = { 65 | marginLeft: "2.7rem", 66 | }; 67 | 68 | 69 | 70 | return ( 71 |
72 |
73 | Image 74 |
75 |

"Harmonize your code with Harmonode!"

76 |

77 | A tool to visualize the flow between front end fetch requests
and 78 | backend routes. 79 |

80 |
81 |
82 |
83 |

Currently:

84 |
    85 |
  • Load any project from your file system
  • 86 |
      87 |
    • 88 | Ignore directories you don’t want to visualize (like test folders, 89 | bundled folders, etc.) 90 |
    • 91 |
    • 92 | Choose what extensions you want to approve for visualizing (you can 93 | ignore jsx files for example) 94 |
    • 95 |
    96 |
  • 97 | Tracks live-updates to any changes made to your project in real time 98 |
  • 99 |
      100 |
    • 101 | When the user hits save on their working project, those changes are 102 | immediately reflected in Harmonode. 103 |
    • 104 |
    105 |
  • Can set the theme of your project
  • 106 |
  • Displays a list of all of the fetch requests
  • 107 |
  • 108 | Displays a ReactFlow diagram that shows the connections between files 109 | that have fetch calls and the endpoints that they are fetching 110 |
  • 111 |
112 |

Next in Development:

113 |
    114 |
  • 115 | Show the data structure that the frontend is sending to the backend 116 |
  • 117 |
  • 118 | Show the data structure that the backend is sending to the frontend 119 |
  • 120 |
  • 121 | Show the middleware (controllers) and potentially what they are doing 122 | with the data 123 |
  • 124 |
  • 125 | Handle more complicated fetch requests (like template literals, urls 126 | by variable) 127 |
  • 128 |
  • Write unit tests
  • 129 |
  • Include other fetch utilities like axios.
  • 130 |
131 |
132 |
133 | ); 134 | } 135 | 136 | export default Home; 137 | -------------------------------------------------------------------------------- /client/src/pages/LandingPage/LandingPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function LandingPage() { 4 | return ( 5 | // render sidebar here 6 |
7 |

Harmonode

8 |
9 | ); 10 | } 11 | 12 | export default LandingPage; 13 | -------------------------------------------------------------------------------- /client/src/pages/List/List.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react' 2 | import { useNavigate } from 'react-router' 3 | import { ProjectsContext } from '../../context/contextStore' 4 | import FrontEndList from './components/FrontEndList' 5 | 6 | 7 | import './list.css'; 8 | 9 | // container for all the list components 10 | 11 | function ListPage() { 12 | 13 | return ( 14 |
15 |

List

16 | 17 | 18 |
19 | ) 20 | } 21 | 22 | export default ListPage -------------------------------------------------------------------------------- /client/src/pages/List/components/BackEndList.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Harmonode/d1b2102389bfb37bc080e77d3510425048f1c96d/client/src/pages/List/components/BackEndList.tsx -------------------------------------------------------------------------------- /client/src/pages/List/components/FetchFileCard.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {ProjectsContext} from '../../../context/contextStore'; 3 | import FetchFileDetails from './FetchFileDetails'; 4 | 5 | 6 | function FetchFileCard({ file }) { 7 | 8 | const lastUpdated = new Date(file.lastUpdated).toLocaleString(); 9 | 10 | 11 | 12 | 13 | return ( 14 |
15 |
16 |

{file.fileName}

17 |
Last updated: {lastUpdated}
18 | 19 |
20 |
21 | ) 22 | 23 | } 24 | 25 | export default FetchFileCard 26 | -------------------------------------------------------------------------------- /client/src/pages/List/components/FetchFileDetails.tsx: -------------------------------------------------------------------------------- 1 | import {ProjectsContext} from '../../../context/contextStore'; 2 | import React from 'react'; 3 | import {v4 as uuid} from 'uuid'; 4 | 5 | function FetchFileDetails({file}) { 6 | const fetchesComponents = file.fetches.map((fetch) => { 7 | return ( 8 |
9 |
Method: {fetch.method}
10 |
Path: {fetch.path}
11 |
12 | ); 13 | }); 14 | return ( 15 |
16 |
17 |
Fetches:
18 |
{fetchesComponents}
19 |
20 |
21 | ); 22 | } 23 | 24 | export default FetchFileDetails; 25 | -------------------------------------------------------------------------------- /client/src/pages/List/components/FrontEndList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, } from 'react'; 2 | import { ProjectsContext } from '../../../context/contextStore'; 3 | import {v4 as uuid} from 'uuid'; 4 | import FetchFileCard from './FetchFileCard'; 5 | 6 | 7 | 8 | 9 | function FrontEndList({file}) { 10 | const { activeProject } = useContext(ProjectsContext); 11 | 12 | 13 | if (!activeProject) { 14 | return

No active project selected.

15 | } 16 | const fetchFiles = activeProject.ast.fetchFiles 17 | 18 | const fetchFilesComponents = fetchFiles.map((file) => { 19 | return 20 | }) 21 | if (fetchFilesComponents.length === 0) { 22 | return

No fetch files found for the active project.

23 | } 24 | 25 | 26 | 27 | return ( 28 |
    29 |

    Fetch Files

    30 | {fetchFilesComponents} 31 |
32 | ) 33 | } 34 | 35 | 36 | export default FrontEndList; -------------------------------------------------------------------------------- /client/src/pages/List/list.css: -------------------------------------------------------------------------------- 1 | .list-page { 2 | color: var(--font-main-color); 3 | background-color: var(--secondary-color); 4 | border: 15px solid var(--secondary-color); 5 | } 6 | 7 | /* 8 | ================= 9 | FrontEndList Styles 10 | ================= 11 | */ 12 | .project-form { 13 | border: 15px solid var(--secondary-color); 14 | background-color: var(--secondary-color); 15 | } 16 | 17 | .project-name-header { 18 | background-color: (--tertiary-color); 19 | color: var(--font-main-color); 20 | } 21 | 22 | /* 23 | ================= 24 | Fetch File List Card 25 | ================= 26 | */ 27 | .listHeader { 28 | text-align: center; 29 | } 30 | 31 | .fetchFilesHeader { 32 | text-align: center; 33 | } 34 | 35 | .fetch-file-card { 36 | background-color: var(--primary-color); 37 | min-width: 25em; 38 | padding: .5em; 39 | margin: 0.5em; 40 | border-radius: 0.95em; 41 | 42 | } 43 | 44 | .fetch-file-card-header { 45 | display: flex; 46 | justify-content: space-between; 47 | width: 100%; 48 | flex-direction:column; 49 | margin-left: 0em; 50 | } 51 | 52 | .fetch-file-card-header h2 { 53 | display: flex; 54 | flex-direction: row; 55 | justify-content: center; 56 | padding-right: .5em; 57 | margin: 0; 58 | 59 | } 60 | 61 | .fetch-file-card-header h5 { 62 | display: flex; 63 | flex-direction: row; 64 | justify-content: center; 65 | padding-right: 1em; 66 | margin: 0; 67 | } 68 | 69 | .filesContainer { 70 | align-items: center; 71 | padding-right: 3em; 72 | } 73 | .fetch-file-card-body { 74 | display: flex; 75 | } 76 | 77 | .fetch-file-card-body-row { 78 | display: flex; 79 | justify-content: space-between; 80 | } 81 | 82 | .fetch-file-card-path-label { 83 | font-weight: bold; 84 | 85 | } 86 | 87 | .fetch-file-card-path { 88 | margin-left: 2em; 89 | } 90 | 91 | .fetch-file-method { 92 | width: 100%; 93 | margin: .3em; 94 | padding-top: .7em; 95 | } 96 | 97 | .fetch-file-path { 98 | width: 100%; 99 | margin: .3em; 100 | margin-bottom: .4em 101 | } 102 | 103 | .fetches-header { 104 | font-weight: bold; 105 | padding-left: 0.3em; 106 | } 107 | 108 | .file-name { 109 | width: 100%; 110 | } -------------------------------------------------------------------------------- /client/src/pages/Projects/ProjectsPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import AddProject from './components/AddProject'; 3 | import ListProjects from './components/ListProjects'; 4 | 5 | import './projects.css'; 6 | import ProjectsProvider from '../../context/ProjectsProvider'; 7 | 8 | //this is like our container 9 | 10 | 11 | function ProjectsPage() { 12 | const [showNew, setShowNew] = useState(false); 13 | return ( 14 |
15 | {!showNew && ( 16 | <> 17 | 18 | 21 | 22 | )} 23 | {showNew && setShowNew(false)} />} 24 |
25 | ); 26 | } 27 | 28 | export default ProjectsPage; 29 | -------------------------------------------------------------------------------- /client/src/pages/Projects/components/AddProject.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import ProjectDirectories from './ProjectDirectories'; 3 | import {DirectoryTree, Directory} from '../../../types'; 4 | import ApprovedExtensions from './ApprovedExtensions'; 5 | import {v4 as uuid} from 'uuid'; 6 | import {ProjectsContext} from '../../../context/contextStore'; 7 | const {ipcRenderer} = window.require('electron'); 8 | 9 | interface projectObj { 10 | folder: string; 11 | server: string; 12 | ignore: string[]; 13 | extensions: string[]; 14 | id: string; 15 | name: string; 16 | ast: object[]; 17 | } 18 | 19 | // Component to add a new project 20 | function AddProject({hideNew}) { 21 | const { 22 | projects, 23 | dispatchProjects, 24 | setActiveProject, 25 | activeProject, 26 | }: { 27 | projects: projectObj[]; 28 | dispatchProjects: Function; 29 | setActiveProject: Function; 30 | activeProject: object; 31 | } = useContext(ProjectsContext); 32 | const [projectFolder, setProjectFolder] = useState(''); 33 | const [projectName, setProjectName] = useState(''); 34 | const [serverPath, setServerPath] = useState(''); 35 | const [ignoredDirs, setIgnoredDirs] = useState([]); 36 | const [dirDetails, setDirDetails] = useState( 37 | {} as DirectoryTree 38 | ); 39 | const [approvedExts, setApprovedExts] = useState([]); 40 | const [fileCount, setFileCount] = useState(0); 41 | 42 | // function that finds all the files that will be loaded so we can display 43 | // the file count on the project load page 44 | async function fileLoad() { 45 | const fileCount = await ipcRenderer.invoke( 46 | 'countCodeFiles', 47 | projectFolder, 48 | ignoredDirs, 49 | approvedExts, 50 | serverPath 51 | ); 52 | setFileCount(fileCount); 53 | } 54 | 55 | // monitoring when extensions and ignoredDirs state change so we can invoke 56 | // fileLoad .. needs to be useEffect and conditionally executed after serverPath 57 | // is set so we don't have file read errors in backend with missing arguments 58 | useEffect(() => { 59 | if (serverPath) { 60 | fileLoad(); 61 | } 62 | }, [approvedExts, ignoredDirs]); 63 | 64 | // function to get the directory path of the project folder 65 | async function getDir(e) { 66 | e.preventDefault(); 67 | const folderPath = await ipcRenderer.invoke('openFolderDialog'); 68 | if (!folderPath) return; 69 | const folderPathSplit = folderPath.split('/'); 70 | setProjectName(folderPathSplit[folderPathSplit.length - 1]); 71 | setProjectFolder(folderPath); 72 | } 73 | 74 | // function to get the file path of the server 75 | async function getFile(e) { 76 | e.preventDefault(); 77 | const filePath = await ipcRenderer.invoke('openFileDialog', projectFolder); 78 | setServerPath(filePath); 79 | } 80 | 81 | // we automatically infer the project name based on the folder it sits in 82 | // func to capitalize the first letter of the file just to make it look nice 83 | function projectNameFormat(name: string) { 84 | const capitalizeFirst = name[0].toUpperCase(); 85 | return capitalizeFirst + name.slice(1); 86 | } 87 | 88 | // what to do when the user saves the project and loads it 89 | async function formSubmit(e) { 90 | e.preventDefault(); 91 | // grab the files from electron backend 92 | const files = await ipcRenderer.invoke( 93 | 'readCodeFiles', 94 | projectFolder, 95 | ignoredDirs, 96 | approvedExts, 97 | serverPath 98 | ); 99 | /* 100 | _________ 101 | projectObj = {} 102 | 1. Project Folder 103 | 2. Server File 104 | 3. Ignore diectories 105 | 4. Extensions to include 106 | 5. Project Name 107 | 6. AST Object 108 | 109 | */ 110 | 111 | const projectName = e.target.projectName.value; 112 | const projectObj = { 113 | folder: projectFolder, 114 | server: serverPath, 115 | ignore: ignoredDirs, 116 | extensions: approvedExts, 117 | id: uuid(), 118 | name: projectName, 119 | ast: files, 120 | }; 121 | 122 | for (const project of projects) { 123 | if (project.name === projectName) { 124 | console.log('duplicate'); 125 | return; 126 | } 127 | } 128 | setActiveProject(projectObj); 129 | dispatchProjects({type: 'add', payload: projectObj}); 130 | hideNew(); 131 | } 132 | 133 | // callback passed down to ProjectDirectories component 134 | function setIgnore(ignoreList: string[], dirs: DirectoryTree) { 135 | setIgnoredDirs(ignoreList); 136 | setDirDetails(dirs); 137 | } 138 | 139 | // callback passed down to ApprovedExtensions component 140 | function setApproved(approvedArray: string[]) { 141 | setApprovedExts(approvedArray); 142 | } 143 | 144 | return ( 145 | <> 146 |

Choose Project Folder

147 |
148 | 151 | {projectFolder && ( 152 | <> 153 |

154 | Project Folder: {projectFolder} 155 |

156 | 159 | {serverPath && ( 160 | <> 161 |
162 |

Server File: {serverPath}

163 |
166 | 170 | 171 |
172 |

173 | Number of Files to be Monitored: {fileCount} 174 |

175 |
176 |

Project Name:

177 | 183 |
184 | 187 |
188 | 189 | )} 190 | 191 | )} 192 |
193 | 194 | ); 195 | } 196 | 197 | export default AddProject; 198 | -------------------------------------------------------------------------------- /client/src/pages/Projects/components/ApprovedExtensions.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {v4 as uuid} from 'uuid'; 3 | 4 | function ApprovedExtensions({setApproved}) { 5 | const [extensionList, setExtensionList] = useState([ 6 | '.jsx', 7 | '.js', 8 | '.tsx', 9 | '.ts', 10 | ]); 11 | const [approvedExt, setApprovedExt] = useState(extensionList); 12 | 13 | // function to handle the logic of checking and unchecking the checkboxes 14 | function handleCheck(e) { 15 | // deconstruct the value and the checked boolean from the checkbox 16 | const {value, checked} = e.target; 17 | 18 | // updated the approvedExt array in state based on the value 19 | if (approvedExt.includes(value) && !checked) 20 | setApprovedExt(approvedExt.filter((ext) => ext != value)); 21 | else if (!approvedExt.includes(value) && checked) 22 | setApprovedExt([...approvedExt, value]); 23 | } 24 | 25 | // whenever approvedExt state changes, invoke the callback to pass array to parent 26 | useEffect(() => { 27 | setApproved(approvedExt); 28 | }, [approvedExt]); 29 | 30 | // make our options jsx array so we can display it in the component render 31 | const options = extensionList.map((extension) => { 32 | return ( 33 |
34 | 40 | 43 |
44 | ); 45 | }); 46 | 47 | return ( 48 |
49 |

Extensions to Include:

50 | {options} 51 |
52 | ); 53 | } 54 | 55 | export default ApprovedExtensions; 56 | -------------------------------------------------------------------------------- /client/src/pages/Projects/components/ListProjects.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router'; 3 | import ProjectListCard from './ProjectListCard'; 4 | import { ProjectsContext } from '../../../context/contextStore'; 5 | import { v4 as uuid } from 'uuid'; 6 | 7 | interface projectObject { 8 | name: string; 9 | server: string; 10 | folder: string; 11 | ignore: string[]; 12 | extensions: string[]; 13 | ast: object[]; 14 | } 15 | // Component to list all of the projects that have been saved previously 16 | function ListProjects() { 17 | const { projects } = useContext(ProjectsContext); 18 | const navigate = useNavigate(); 19 | 20 | const projectComponents = projects.map((project) => { 21 | return ; 22 | }); 23 | 24 | function navigateElsewhere() { 25 | navigate('/diagram'); 26 | } 27 | 28 | return ( 29 |
30 |

Projects

31 | {projectComponents} 32 | 35 |
36 | ); 37 | } 38 | 39 | export default ListProjects; 40 | -------------------------------------------------------------------------------- /client/src/pages/Projects/components/ProjectDirectories.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {v4 as uuid} from 'uuid'; 3 | import {DirObj} from '../../../../../server/types'; 4 | import {DirectoryTree, Directory} from '../../../types'; 5 | const {ipcRenderer} = window.require('electron'); 6 | 7 | // optional ignoreList passed in so we can use this elsewhere 8 | function ProjectDirectories({ 9 | dirPath, 10 | setIgnore, 11 | ignoreList = {} as DirectoryTree, 12 | }) { 13 | const [dirs, setDirs] = useState({ 14 | directories: [], 15 | } as DirectoryTree); 16 | 17 | // checkBox toggle handling, recursively checking/unchecking nested dir paths 18 | function handleChange(e) { 19 | const {value, checked} = e.target; 20 | 21 | function recurseCheck(dirs) { 22 | for (const dir of dirs) { 23 | if (dir.fullPath === value) { 24 | dir.checked = checked; 25 | if (dir.children && dir.children.length > 0) 26 | recurseCheckSubs(dir.children); 27 | } else { 28 | recurseCheck(dir.children); 29 | } 30 | } 31 | } 32 | function recurseCheckSubs(children) { 33 | for (const subChild of children) { 34 | subChild.checked = checked; 35 | if (subChild.children && subChild.children.length > 0) 36 | recurseCheckSubs(subChild.children); 37 | } 38 | } 39 | recurseCheck(dirs.directories); 40 | 41 | setDirs({...dirs}); 42 | } 43 | 44 | // useEffect to track changes to dirs so we can send the state back to the parent 45 | useEffect(() => { 46 | // have an array that we push the paths to 47 | const ignoreArray: string[] = []; 48 | // recurse through the directories to push the parent most directory to the ignore array 49 | function recurseCheck(dir) { 50 | for (const path of dir) { 51 | if (path.checked) ignoreArray.push(path.fullPath); 52 | else { 53 | if (path.children && path.children.length > 0) { 54 | recurseCheck(path.children); 55 | } 56 | } 57 | } 58 | } 59 | recurseCheck(dirs.directories); 60 | 61 | // send the ignore array back to the parent 62 | setIgnore(ignoreArray, dirs); 63 | }, [dirs]); 64 | 65 | // call the ipcRenderer to get the directories of the filepath 66 | useEffect(() => { 67 | // if an optional ignoreList was passed in, we can just load this instead 68 | if (ignoreList.hasOwnProperty('directories')) { 69 | return setDirs(ignoreList); 70 | } 71 | // grab the directories from the recursive file search 72 | async function getDirectories() { 73 | const directories = await ipcRenderer.invoke('getDirectories', dirPath); 74 | 75 | // build DirectoryTree structure 76 | const rootDirectory: DirectoryTree = {directories: []}; 77 | directories.forEach((directory) => { 78 | // remove parts of the filepath we don't care about 79 | const path = (directory as DirObj).filePath.replace( 80 | new RegExp(`^${dirPath}`), 81 | '' 82 | ); 83 | // set the filepath as the path without the part we don't care about 84 | directory.filePath = path; 85 | 86 | // setting the base of the rootDirectory tree 87 | let currentLevel = rootDirectory.directories; 88 | // creating a full path string so that we can include that as a property in dirs 89 | let currentFullPath = ''; 90 | const pathParts = directory.filePath.split('/').filter((part) => part); 91 | 92 | pathParts.forEach((part, index) => { 93 | // add the '/' back in to the full path so we can display it 94 | currentFullPath += `/${part}`; 95 | // see if the path exists already 96 | let existingPath = currentLevel.find((dir) => dir.name === part); 97 | // if it doesn't exist, let's push the new path 98 | if (!existingPath) { 99 | const newPath: Directory = { 100 | name: part, 101 | fullPath: currentFullPath, 102 | checked: false, 103 | children: [], 104 | }; 105 | 106 | currentLevel.push(newPath); 107 | existingPath = newPath; 108 | } 109 | 110 | if (index < pathParts.length - 1) { 111 | existingPath.children = existingPath.children || []; 112 | currentLevel = existingPath.children; 113 | } 114 | }); 115 | }); 116 | 117 | setDirs(rootDirectory); 118 | } 119 | getDirectories(); 120 | }, []); 121 | 122 | // create the checkbox options components 123 | const options: JSX.Element[] = []; 124 | 125 | function createOptions(dir) { 126 | if (dir.length === 0) return; 127 | dir.map((directory) => { 128 | const fileName = directory.name; 129 | const path = directory.fullPath; 130 | 131 | const nestedCount = (path.match(/\//g) || []).length - 1; 132 | 133 | const isChecked = directory.checked; 134 | const optionElement = ( 135 |
140 | 146 | 149 |
150 | ); 151 | options.push(optionElement); 152 | if (directory.children.length > 0) createOptions(directory.children); 153 | }); 154 | } 155 | createOptions(dirs.directories); 156 | 157 | return ( 158 |
159 |

Ignore Directories:

160 | {options} 161 |
162 | ); 163 | } 164 | 165 | export default ProjectDirectories; 166 | -------------------------------------------------------------------------------- /client/src/pages/Projects/components/ProjectListCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { ProjectsContext } from '../../../context/contextStore'; 3 | import { loadProject } from '../../../ipcRenderer'; 4 | import '../../Projects/projects.css'; 5 | 6 | function ProjectListCard({ project }) { 7 | const { dispatchProjects, activeProject, setActiveProject } = 8 | useContext(ProjectsContext); 9 | 10 | function handleDelete(e) { 11 | dispatchProjects({ type: 'delete', payload: project }); 12 | if (project.id === activeProject.id) setActiveProject({}); 13 | } 14 | 15 | async function handleLoad(e) { 16 | const files = await loadProject(project); 17 | const newProject = { 18 | folder: project.folder, 19 | server: project.server, 20 | ignore: project.ignore, 21 | extensions: project.extensions, 22 | id: project.id, 23 | name: project.name, 24 | ast: files, 25 | }; 26 | 27 | dispatchProjects({ type: 'update', payload: newProject }); 28 | setActiveProject(newProject); 29 | } 30 | 31 | function formatPath(path) { 32 | const pathCutoff = 50; 33 | if (path.length > pathCutoff) { 34 | let newPath = 35 | path.slice(0, pathCutoff / 2) + 36 | '...' + 37 | path.slice(path.length - pathCutoff / 2); 38 | return newPath; 39 | } 40 | return path; 41 | } 42 | 43 | const formattedPath = formatPath(project.folder); 44 | 45 | return ( 46 |
47 |
48 |

{project.name}

49 |
50 | 53 | 56 |
57 |
58 |
59 |
60 |

File Path:

61 |

{formattedPath}

62 |
63 |
64 |
65 |
66 | ); 67 | } 68 | 69 | export default ProjectListCard; 70 | -------------------------------------------------------------------------------- /client/src/pages/Projects/projects.css: -------------------------------------------------------------------------------- 1 | .projects-page { 2 | color: var(--font-main-color); 3 | background-color: var(--secondary-color); 4 | border: 15px solid var(--secondary-color); 5 | width: 35%; 6 | } 7 | /* 8 | ================= 9 | AddProject Styles 10 | ================= 11 | */ 12 | .project-form { 13 | border: 15px solid var(--secondary-color); 14 | background-color: var(--secondary-color); 15 | } 16 | 17 | .project-name-header { 18 | background-color: (--tertiary-color); 19 | color: var(--font-main-color); 20 | } 21 | 22 | .project-header { 23 | font: 'Roboto' sans-serif; 24 | color: var(--font-main-color); 25 | text-align: center; 26 | padding-right: 1.8em; 27 | } 28 | 29 | /* 30 | ================= 31 | Project List Card 32 | ================= 33 | */ 34 | 35 | .project-card { 36 | background-color: var(--primary-color); 37 | min-width: 25em; 38 | padding: 0.5em; 39 | margin: 0.5em; 40 | border-radius: 0.95em; 41 | } 42 | 43 | .project-card-header { 44 | display: flex; 45 | justify-content: space-between; 46 | align-items: center; 47 | } 48 | 49 | .project-card-header h2 { 50 | margin: 0; 51 | } 52 | 53 | .project-header-btns { 54 | display: flex; 55 | } 56 | 57 | .project-header-btns > * { 58 | border: 1px solid var(--secondary-color); 59 | margin-left: 0.5em; 60 | border-radius: 5px; 61 | } 62 | 63 | .project-card-body { 64 | display: flex; 65 | } 66 | 67 | .project-card-body-row { 68 | display: flex; 69 | justify-content: space-between; 70 | } 71 | 72 | .project-card-path-label { 73 | font-weight: bold; 74 | } 75 | 76 | .project-card-path { 77 | margin-left: 2em; 78 | } 79 | 80 | .chooseProjHeader { 81 | text-align: center; 82 | padding-right: 1em; 83 | } 84 | .serverHeader { 85 | text-align: center; 86 | } 87 | 88 | .project-name-container { 89 | /* align-items: center; */ 90 | padding-right: 3.5em; 91 | padding-bottom: 0.5em; 92 | text-align: center; 93 | } 94 | 95 | .inputBox { 96 | /* align-items: center; */ 97 | border: 0; 98 | outline: 0; 99 | font: 'Roboto' sans-serif; 100 | width: 12em; 101 | height: 1.9em; 102 | border-radius: 0.25em; 103 | box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.2); 104 | cursor: pointer; 105 | } 106 | 107 | .numOfFilesHeader { 108 | text-align: center; 109 | } 110 | 111 | .projFolderHeader { 112 | text-align: center; 113 | } 114 | .projPageButtons { 115 | align-items: center; 116 | margin-left: 25%; 117 | width: 40%; 118 | } 119 | 120 | .loadButton { 121 | border-radius: 50px; 122 | width: 85px; 123 | } 124 | 125 | .deleteButton { 126 | border-radius: 50px; 127 | width: 85px; 128 | } 129 | -------------------------------------------------------------------------------- /client/src/pages/Settings/SettingsPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Settings from './components/Settings'; 3 | // Settings will let user configure what files to ignore in projects 4 | // How often to check for updates 5 | // Stretch (kinda): changing color theme 6 | function SettingsPage() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | export default SettingsPage; 15 | -------------------------------------------------------------------------------- /client/src/pages/Settings/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { flexbox } from '@mui/system'; 2 | import React, { useState } from 'react'; 3 | import { SketchPicker } from 'react-color'; 4 | import ColorPicker from 'react-color-picker'; 5 | import { HexColorPicker } from 'react-colorful'; 6 | import './settings.css'; 7 | 8 | function Settings() { 9 | const defaultTheme = { 10 | '--primary-color': '#164b60', 11 | '--secondary-color': '#1b6b93', 12 | '--tertiary-color': '#42A186', 13 | '--quaternary-color': '#42A186', 14 | '--inactive-color': '#868484', 15 | '--font-main-color': '#f6fff5', 16 | }; 17 | const themeOption1 = { 18 | '--primary-color': '#27374D', 19 | '--secondary-color': '#526D82', 20 | '--tertiary-color': '#9DB2BF', 21 | '--quaternary-color': '#9DB2BF', 22 | '--inactive-color': '#868484', 23 | '--font-main-color': '#DDE6ED', 24 | }; 25 | 26 | const themeOption2 = { 27 | '--primary-color': '#B0DAFF', 28 | '--secondary-color': '#146C94', 29 | '--tertiary-color': '#19A7CE', 30 | '--quaternary-color': '#19A7CE', 31 | '--inactive-color': '#868484', 32 | '--font-main-color': '#FEFF86', 33 | }; 34 | 35 | const themeOption3 = { 36 | '--primary-color': '#6527BE', 37 | '--secondary-color': '#9681EB', 38 | '--tertiary-color': '#868484', 39 | '--quaternary-color': '#868484', 40 | '--inactive-color': '#A7EDE7', 41 | '--font-main-color': '#45CFDD', 42 | }; 43 | 44 | const themeOption4 = { 45 | '--primary-color': '#61764B', 46 | '--secondary-color': '#9BA17B', 47 | '--tertiary-color': '#CFB997', 48 | '--quaternary-color': '#CFB997', 49 | '--inactive-color': '#FAD6A5', 50 | '--font-main-color': '#A7EDE7', 51 | }; 52 | //set customize theme button state 53 | const [themeChoice, setChoice] = useState(false); 54 | 55 | //set default color picker state 56 | const [primaryColorChoice, setPimaryColorChoice] = useState('#164b60'); 57 | const [secondaryColorChoice, setSecondaryColorChoice] = useState('#1b6b93'); 58 | const [tertiaryColorChoice, setTertiaryColorChoice] = useState('#42A186'); 59 | const [quatenaryColorChoice, setQuatenaryColorChoice] = useState('#04D6B2'); 60 | const [inactiveColorChoice, setInactiveColorChoice] = useState('#868484'); 61 | const [fontManColorChoice, setFontMaiColorChoice] = useState('#f6fff5'); 62 | 63 | //color pickker - handle primary color 64 | function handlePrimaryColor(color) { 65 | document.documentElement.style.setProperty('--primary-color', color); 66 | } 67 | //color pickker - handle secondary color 68 | function handleSecondaryColor(color) { 69 | document.documentElement.style.setProperty('--secondary-color', color); 70 | } 71 | //color pickker - handle tertiary color 72 | function handleTertiaryColor(color) { 73 | console.log('color ', color); 74 | document.documentElement.style.setProperty('--tertiary-color', color); 75 | } 76 | //color pickker - handle quaternary color 77 | function handleQuaternaryColor(color) { 78 | document.documentElement.style.setProperty('--quaternary-color', color); 79 | } 80 | //color pickker - handle inactive color 81 | function handleInactiveColor(color) { 82 | document.documentElement.style.setProperty('--inactive-color', color); 83 | } 84 | //color pickker - handle font-main color 85 | function handleFontMainColor(color) { 86 | document.documentElement.style.setProperty('--font-main-color', color); 87 | } 88 | 89 | //handle theme botton choices 90 | document.documentElement.style.setProperty('--secondary-color', '#1b6b93'); 91 | const [showThemeChoices, setThemeButtonStatus] = useState(false); 92 | function themeChoiceClick(e) { 93 | console.log('CLICK'); 94 | console.log('showThemeChoices1 ', showThemeChoices); 95 | if (!showThemeChoices) { 96 | console.log('Change to true'); 97 | setThemeButtonStatus(true); 98 | } else { 99 | setThemeButtonStatus(false); 100 | console.log('Change to false'); 101 | } 102 | console.log('showThemeChoices2 ', showThemeChoices); 103 | } 104 | 105 | //handle color theme selection 106 | function handleClick(theme, e) { 107 | if (theme !== 'themeChoose') { 108 | for (let key in theme) { 109 | document.documentElement.style.setProperty(key, theme[key]); 110 | } 111 | } else { 112 | if (themeChoice === false) { 113 | setChoice(true); 114 | } else { 115 | setChoice(false); 116 | } 117 | } 118 | } 119 | 120 | return ( 121 |
122 |

Settings

123 | 131 | {showThemeChoices ? ( 132 |
133 | 139 | 145 | 151 | 157 | 163 | 164 |
165 | 174 |
175 |
176 | {themeChoice ? ( 177 |
178 |

Primary Color

179 |

Secondary Color

180 |

Tertiary Color

181 |

Quatenary Color

182 |

Inactive Color

183 |

Main Font Color

184 | 185 | 189 | 193 | 197 | 201 | 205 | 209 |
210 | ) : ( 211 |
212 | )} 213 |
214 |
215 | ) : ( 216 |
217 | )} 218 |
219 | ); 220 | } 221 | 222 | export default Settings; 223 | -------------------------------------------------------------------------------- /client/src/pages/Settings/components/settings.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | } 4 | 5 | .primaryButtonInactive { 6 | width: 350px; 7 | text-align: center; 8 | font-size: 14pt; 9 | background-color: var(--inactive-color); 10 | } 11 | 12 | .primaryButtonActive { 13 | width: 350px; 14 | text-align: center; 15 | font-size: 14pt; 16 | background-color: var(--secondary-color); 17 | } 18 | 19 | .button { 20 | display: flex; 21 | align-items: center; 22 | height: 10px; 23 | margin-left: 20px; 24 | font-size: 14pt; 25 | width: 300px; 26 | justify-items: center; 27 | row-gap: 10px; 28 | column-gap: 10px; 29 | } 30 | 31 | .container { 32 | margin-left: 40px; 33 | display: grid; 34 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; 35 | align-items: start; 36 | justify-items: center; 37 | row-gap: 10px; 38 | column-gap: 10px; 39 | font-size: 14pt; 40 | } 41 | -------------------------------------------------------------------------------- /client/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); 2 | @font-face { 3 | font-family: 'Robot'; 4 | font-style: 'normal'; 5 | font-weight: 400; 6 | src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu4mxP.ttf) 7 | format('truetype'); 8 | } 9 | 10 | @font-face { 11 | font-family: 'Roboto'; 12 | font-style: normal; 13 | font-weight: 700; 14 | src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lbBP.ttf) 15 | format('truetype'); 16 | } 17 | 18 | body { 19 | background-color: var(--primary-color); 20 | margin: 0; 21 | font-family: 'Roboto', sans-serif; 22 | } 23 | 24 | h1, 25 | h2, 26 | h3, 27 | h4, 28 | h5, 29 | h6 { 30 | font-family: 'Roboto', sans-serif; 31 | font-weight: 700; 32 | text-decoration: none; 33 | } 34 | 35 | .app { 36 | display: flex; 37 | min-height: calc(100vh - 3em); 38 | color: var(--font-main-color); 39 | } 40 | 41 | .content { 42 | margin: 0; 43 | padding: 4em 10vw; 44 | } 45 | 46 | .topbar { 47 | background-color: var(--quaternary-color); 48 | margin: 0; 49 | height: 3em; 50 | display: flex; 51 | justify-content: center; 52 | } 53 | 54 | .topbar-inactive { 55 | background-color: var(--inactive-color); 56 | } 57 | 58 | button { 59 | background-color: var(--tertiary-color); 60 | color: var(--font-main-color); 61 | border-radius: 15px; 62 | padding: 14px 20px; 63 | margin: 8px 0; 64 | border: none; 65 | cursor: pointer; 66 | width: 100%; 67 | } 68 | 69 | button:hover { 70 | background-color: #75768e; 71 | box-shadow: 0 12px 16px 0 rgba(0, 0, 0, 0.24), 72 | 0 17px 50px 0 rgba(0, 0, 0, 0.19); 73 | transition-duration: 0.4s; 74 | } 75 | 76 | /* 77 | ======================= 78 | Code Editor Styles 79 | ======================= 80 | */ 81 | .code-editor { 82 | display: flex; 83 | flex-direction: column; 84 | background: rgb(29, 28, 28); 85 | border-radius: 15px; 86 | min-width: 35rem; 87 | } 88 | 89 | .editor-container { 90 | max-height: 60vh; 91 | overflow-y: auto; 92 | cursor: default; 93 | } 94 | 95 | .editor-name { 96 | padding: 0.4em; 97 | margin: 0; 98 | user-select: none; 99 | } 100 | 101 | .token.operator { 102 | background-color: transparent !important; 103 | } 104 | 105 | .token.number { 106 | /* Your styles for numbers here */ 107 | color: blue; 108 | } 109 | 110 | .token.property { 111 | /* Your styles for keys in objects here */ 112 | color: green; 113 | } 114 | 115 | .editor-close-btn { 116 | background-color: rgb(156, 10, 10); 117 | } 118 | 119 | .editor-save-btn, 120 | .editor-close-btn { 121 | margin: 0.5em 1em 0.5em 1em; 122 | } 123 | 124 | .code-editor-save-modal { 125 | position: fixed; 126 | top: 50%; 127 | left: 50%; 128 | transform: translate(-50%, -50%); 129 | background-color: var(--primary-color); 130 | color: var(--font-main-color); 131 | padding: 1em; 132 | } 133 | -------------------------------------------------------------------------------- /client/src/types.ts: -------------------------------------------------------------------------------- 1 | // Directory stuff 2 | export interface DirectoryTree { 3 | directories: Directory[]; 4 | } 5 | 6 | export interface Directory { 7 | name: string; 8 | fullPath: string; 9 | checked: boolean; 10 | children?: Directory[]; 11 | } 12 | -------------------------------------------------------------------------------- /client/src/variables.css: -------------------------------------------------------------------------------- 1 | /* 2 | Original Color Palette 3 | --primary-color: #164B60; 4 | --secondary-color: #1B6B93; 5 | --tertiary-color: #4FC0D0; 6 | --quaternary-color: #A2FF86; 7 | 8 | */ 9 | 10 | :root { 11 | --primary-color: #164b60; 12 | --secondary-color: #1b6b93; 13 | --tertiary-color: #42a186; 14 | --quaternary-color: #42a186; 15 | --inactive-color: #868484; 16 | --font-main-color: #f6fff5; 17 | --diagram-bg-color: #071014; 18 | } 19 | .something { 20 | color: #173e4d; 21 | color: #4caf50; 22 | } 23 | -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packagerConfig: { 3 | asar: true, 4 | }, 5 | rebuildConfig: {}, 6 | makers: [ 7 | { 8 | name: '@electron-forge/maker-zip', 9 | platforms: ['darwin', 'win32'], 10 | arch: ['arm64', 'x64'] 11 | }, 12 | { 13 | name: '@electron-forge/maker-deb', 14 | config: {}, 15 | }, 16 | { 17 | name: '@electron-forge/maker-rpm', 18 | config: {}, 19 | }, 20 | ], 21 | plugins: [ 22 | { 23 | name: '@electron-forge/plugin-auto-unpack-natives', 24 | config: {}, 25 | }, 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "harmonode", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./tsCompiled/server/main.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "npm run dev & wait-on http://localhost:8080/ && electron-forge start", 9 | "dev": "run-p tsc-dev copy-images electron-dev webpack-dev", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "webpack-dev": "cross-env NODE_ENV=development webpack serve --no-open", 12 | "electron-dev": "nodemon --watch tsCompiled/backend --exec \"npx cross-env NODE_ENV=development electron .\"", 13 | "tsc-dev": "tsc --watch", 14 | "copy-images": "cp server/*.png tsCompiled/server", 15 | "package": "electron-forge package", 16 | "make": "electron-forge make" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@babel/parser": "^7.22.7", 22 | "@babel/traverse": "^7.22.8", 23 | "@emotion/react": "^11.11.1", 24 | "@emotion/styled": "^11.11.0", 25 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 26 | "@fortawesome/react-fontawesome": "^0.2.0", 27 | "@mui/icons-material": "^5.14.0", 28 | "@mui/material": "^5.14.0", 29 | "axios": "^1.4.0", 30 | "body-parser": "^1.20.2", 31 | "concurrently": "^8.2.0", 32 | "config": "^3.3.9", 33 | "copyfiles": "^2.4.1", 34 | "cors": "^2.8.5", 35 | "cross-env": "^7.0.3", 36 | "css-loader": "^6.8.1", 37 | "dotenv": "^16.3.1", 38 | "electron-squirrel-startup": "^1.0.0", 39 | "electron-store": "^8.1.0", 40 | "express": "^4.18.2", 41 | "nodemon": "^2.0.22", 42 | "npm": "^9.7.2", 43 | "npm-run-all": "^4.1.5", 44 | "path-browserify": "^1.0.1", 45 | "prismjs": "^1.29.0", 46 | "react": "^18.2.0", 47 | "react-color": "^2.19.3", 48 | "react-color-picker": "^4.0.2", 49 | "react-colorful": "^5.6.1", 50 | "react-dom": "^18.2.0", 51 | "react-pro-sidebar": "^1.1.0-alpha.1", 52 | "react-router": "^6.14.1", 53 | "react-router-dom": "^6.14.1", 54 | "react-simple-code-editor": "^0.13.1", 55 | "reactflow": "^11.7.4", 56 | "run": "^1.4.0", 57 | "sass": "^1.63.6", 58 | "sass-loader": "^13.3.2", 59 | "serve": "^14.2.0", 60 | "style-loader": "^3.3.3", 61 | "ts-loader": "^9.4.4", 62 | "typescript": "^5.1.6", 63 | "uuid": "^9.0.0" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.22.8", 67 | "@babel/preset-env": "^7.22.7", 68 | "@babel/preset-react": "^7.22.5", 69 | "@electron-forge/cli": "^6.2.1", 70 | "@electron-forge/maker-deb": "^6.2.1", 71 | "@electron-forge/maker-rpm": "^6.2.1", 72 | "@electron-forge/maker-squirrel": "^6.2.1", 73 | "@electron-forge/maker-zip": "^6.2.1", 74 | "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", 75 | "@testing-library/jest-dom": "^5.16.5", 76 | "@testing-library/react": "^14.0.0", 77 | "babel-jest": "^29.6.1", 78 | "babel-loader": "^9.1.2", 79 | "concurrently": "^8.2.0", 80 | "copy-webpack-plugin": "^11.0.0", 81 | "electron": "^25.3.0", 82 | "electron-reloader": "^1.2.3", 83 | "file-loader": "^6.2.0", 84 | "html-webpack-plugin": "^5.5.3", 85 | "jest": "^29.6.0", 86 | "jest-environment-jsdom": "^29.6.1", 87 | "url-loader": "^4.1.1", 88 | "webpack": "^5.88.1", 89 | "webpack-cli": "^5.1.4", 90 | "webpack-dev-server": "^4.15.1" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server/ast/clientParser.js: -------------------------------------------------------------------------------- 1 | const {parse} = require('@babel/parser'); 2 | const generate = require('@babel/generator').default; 3 | const trav = require('@babel/traverse').default; 4 | 5 | const input = ` 6 | const express = require('express'); 7 | const fetch = require('node-fetch'); 8 | const app = express(); 9 | let data = {user: 'WRONG USER', role: 'WRONG ROLE'} 10 | app.listen(3000, (data) => { 11 | //data = {user: 'RIGHT USER', role: 'RIGHT ROLE'} 12 | fetch('http://localhost:3000/harmodevs', { 13 | method: 'POST', 14 | headers: { contentType: 'application/json' }, 15 | body: {user: 'RIGHT USER', role: 'RIGHT ROLE'} 16 | }); 17 | fetch('http://localhost:3000/cool_stuff'); 18 | fetch('http://localhost:3000/harmodevious'); 19 | }); 20 | `; 21 | 22 | function fetchParser(input) { 23 | // Parse the input to an Abstract Syntax Tree (AST) 24 | const ast = parse(input, { 25 | sourceType: 'module', 26 | plugins: ['jsx', 'flow'], 27 | }); 28 | 29 | // Create an object to hold all variables in scope 30 | const allScopedVars = {}; 31 | 32 | // Container for parsed fetch calls 33 | const fetchCalls = []; 34 | 35 | // Traverse the AST 36 | trav(ast, { 37 | enter(path) { 38 | // Check for assignment expressions and store the variable to allScopedVars 39 | if ( 40 | path.node.type === 'AssignmentExpression' && 41 | path.node.left && 42 | path.node.right 43 | ) { 44 | if (path.node.left.type === 'Identifier') { 45 | allScopedVars[path.node.left.name] = path.node.right; 46 | } 47 | } 48 | 49 | // Check for variable declarations and store the variable to allScopedVars 50 | if ( 51 | path.node.type === 'VariableDeclarator' && 52 | path.node.id && 53 | path.node.init 54 | ) { 55 | allScopedVars[path.node.id.name] = path.node.init; 56 | } 57 | }, 58 | }); 59 | 60 | // Function to retrieve the body of a fetch request 61 | function getFetchBody(body, path) { 62 | const bodyDetails = {stringified: false, keys: []}; 63 | if (body.type === 'CallExpression') { 64 | if ( 65 | body.callee.object.name === 'JSON' && 66 | body.callee.property.name === 'stringify' 67 | ) { 68 | const argument = body.arguments[0]; 69 | if (argument.type === 'Identifier') { 70 | const originalValue = allScopedVars[argument.name]; 71 | if (typeof originalValue === 'object' && originalValue.properties) { 72 | const properties = originalValue.properties; 73 | properties.forEach((prop) => bodyDetails.keys.push(prop.key.name)); 74 | bodyDetails.stringified = true; 75 | } 76 | } 77 | } 78 | } else if (body.type === 'Identifier') { 79 | const originalValue = allScopedVars[body.name]; 80 | if (typeof originalValue === 'object' && originalValue.properties) { 81 | const properties = originalValue.properties; 82 | properties.forEach((prop) => bodyDetails.keys.push(prop.key.name)); 83 | bodyDetails.stringified = false; 84 | } 85 | } else if (body.type === 'ObjectExpression') { 86 | body.properties.forEach((prop) => { 87 | bodyDetails.keys.push(prop.key.name); 88 | }); 89 | } 90 | return bodyDetails; 91 | } 92 | 93 | // Second pass through the AST 94 | trav(ast, { 95 | enter(path) { 96 | if ( 97 | path.node.type === 'CallExpression' && 98 | path.node.callee.name === 'fetch' 99 | ) { 100 | // Get the URL of the fetch call 101 | const fetchUrl = generate(path.node.arguments[0]).code.replace( 102 | /['"]+/g, 103 | '' 104 | ); 105 | const fetchOptions = path.node.arguments[1]; 106 | 107 | if (fetchOptions) { 108 | // Get the method of the fetch call 109 | const methodProp = fetchOptions.properties.find( 110 | (p) => p.key.name === 'method' 111 | ); 112 | const method = methodProp 113 | ? generate(methodProp.value).code.replace(/['"]+/g, '') 114 | : 'GET'; 115 | // Get the body of the fetch call 116 | const bodyProp = fetchOptions.properties.find( 117 | (p) => p.key.name === 'body' 118 | ); 119 | const body = bodyProp ? getFetchBody(bodyProp.value) : null; 120 | 121 | // Push the method, URL, and body of the fetch call to the fetchCalls array 122 | fetchCalls.push({ 123 | method, 124 | path: fetchUrl, 125 | body, 126 | }); 127 | } else { 128 | // If the fetch call has no options, push the method and URL to the fetchCalls array 129 | fetchCalls.push({ 130 | method: 'GET', 131 | path: fetchUrl, 132 | }); 133 | } 134 | } 135 | }, 136 | }); 137 | 138 | // Return the array of parsed fetch calls 139 | return fetchCalls; 140 | } 141 | 142 | export default fetchParser; 143 | -------------------------------------------------------------------------------- /server/ast/importsFinder.js: -------------------------------------------------------------------------------- 1 | const babelParser = require("@babel/parser"); 2 | const trav = require("@babel/traverse").default; 3 | 4 | const find = (ast) => { 5 | const importedRoutes = {}; 6 | 7 | trav(ast, { 8 | enter(path) { 9 | if (path.node.type === "VariableDeclarator") { 10 | if ( 11 | path.node.init.type === "CallExpression" && 12 | path.node.init.callee.name === "require" 13 | ) { 14 | importedRoutes[path.node.id.name] = path.node.init.arguments[0].value; 15 | } 16 | } 17 | }, 18 | }); 19 | 20 | return importedRoutes; 21 | }; 22 | 23 | export default find; 24 | -------------------------------------------------------------------------------- /server/ast/routerParser.js: -------------------------------------------------------------------------------- 1 | const babelParser = require("@babel/parser"); 2 | const trav = require("@babel/traverse").default; 3 | 4 | import find from "./importsFinder"; 5 | 6 | const routerParser = (codeString) => { 7 | const ast = babelParser.parse(codeString, { 8 | sourceType: "module", 9 | plugins: ["jsx"], 10 | }); 11 | 12 | const routerObj = { routerEndPoints: [] }; 13 | const routerEnds = []; 14 | 15 | const importedRoutes = find(ast); 16 | 17 | 18 | const findOriginalVal = (variable) => { 19 | if ( 20 | variable in importedRoutes && 21 | typeof importedRoutes[variable] === "string" 22 | ) 23 | return importedRoutes[variable]; 24 | return "deadEnd"; 25 | }; 26 | 27 | trav(ast, { 28 | enter(path) { 29 | let current = path.node; 30 | let isMethodCall = 31 | current.type === "CallExpression" && 32 | current.callee.type === "MemberExpression"; 33 | let isGet = isMethodCall && current.callee.property.name === "get"; 34 | let isPost = isMethodCall && current.callee.property.name === "post"; 35 | if (isGet) { 36 | if (current.arguments[0].value) { 37 | routerObj.routerEndPoints.push(current.arguments[0].value); 38 | if ( 39 | current.arguments[1] && 40 | current.arguments[1].type === "Identifier" 41 | ) { 42 | const nextInLine = current.arguments[1]; 43 | routerObj[nextInLine.name] = findOriginalVal(nextInLine.name); 44 | } 45 | } 46 | } 47 | }, 48 | }); 49 | 50 | return routerObj; 51 | }; 52 | 53 | export default routerParser; 54 | -------------------------------------------------------------------------------- /server/ast/serverParser.js: -------------------------------------------------------------------------------- 1 | const babelParser = require('@babel/parser'); 2 | const trav = require('@babel/traverse').default; 3 | 4 | import find from './importsFinder'; 5 | 6 | const endpointParse = (codeString) => { 7 | const ast = babelParser.parse(codeString, { 8 | sourceType: 'module', 9 | plugins: ['jsx'], 10 | }); 11 | 12 | const routesObj = {serverEndPoints: []}; 13 | const importedRoutes = find(ast); 14 | 15 | trav(ast, { 16 | enter(path) { 17 | if (path.node.type === 'VariableDeclarator') { 18 | if ( 19 | path.node.init.type === 'CallExpression' && 20 | path.node.init.callee.name === 'require' 21 | ) { 22 | importedRoutes[path.node.id.name] = path.node.init.arguments[0].value; 23 | } 24 | } 25 | }, 26 | }); 27 | 28 | const findOriginalVal = (variable) => { 29 | if ( 30 | variable in importedRoutes && 31 | typeof importedRoutes[variable] === 'string' 32 | ) 33 | return importedRoutes[variable]; 34 | return 'deadEnd'; 35 | }; 36 | 37 | trav(ast, { 38 | enter(path) { 39 | let current = path.node; 40 | if ( 41 | current.type === 'CallExpression' && 42 | current.callee.type === 'MemberExpression' && 43 | current.callee.object.name === 'app' && 44 | current.callee.property.name === 'use' 45 | ) { 46 | if (current.arguments[0].value) { 47 | routesObj.serverEndPoints.push(current.arguments[0].value); 48 | if ( 49 | current.arguments[1] && 50 | current.arguments[1].type === 'Identifier' 51 | ) { 52 | const nextInLine = current.arguments[1]; 53 | routesObj.serverEndPoints.push( 54 | findOriginalVal(current.arguments[1].name) 55 | ); 56 | routesObj[nextInLine.name] = findOriginalVal(nextInLine.name); 57 | } 58 | } 59 | } 60 | }, 61 | }); 62 | 63 | return routesObj; 64 | }; 65 | 66 | export default endpointParse; 67 | -------------------------------------------------------------------------------- /server/ast2/Breadcrumb.js: -------------------------------------------------------------------------------- 1 | export class Breadcrumb { 2 | constructor() { 3 | this.nextFile; 4 | } 5 | 6 | fileName(fileName) { 7 | this.fileName = fileName; 8 | return this; 9 | } 10 | 11 | path(path) { 12 | this.path = path; 13 | return this; 14 | } 15 | 16 | method(method) { 17 | this.method = method; 18 | return this; 19 | } 20 | 21 | fullPath(path) { 22 | this.fullPath = path; 23 | return this; 24 | } 25 | 26 | lastUpdated(time) { 27 | this.lastUpdated = time; 28 | return this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/ast2/importsFinder.js: -------------------------------------------------------------------------------- 1 | const babelParser = require('@babel/parser'); 2 | const trav = require('@babel/traverse').default; 3 | 4 | const find = (ast) => { 5 | const importedRoutes = {}; 6 | 7 | trav(ast, { 8 | enter(path) { 9 | if (path.node.type === 'VariableDeclarator') { 10 | if ( 11 | path.node.init.type === 'CallExpression' && 12 | path.node.init.callee.name === 'require' 13 | ) { 14 | importedRoutes[path.node.id.name] = path.node.init.arguments[0].value; 15 | } 16 | } 17 | }, 18 | }); 19 | 20 | return importedRoutes; 21 | }; 22 | 23 | export default find; 24 | -------------------------------------------------------------------------------- /server/ast2/routerParser.js: -------------------------------------------------------------------------------- 1 | const babelParser = require('@babel/parser'); 2 | const trav = require('@babel/traverse').default; 3 | 4 | import find from '..ast/importsFinder'; 5 | import {Breadcrumb} from './Breadcrumb'; 6 | 7 | const routerParser = (codeString, fileName) => { 8 | const ast = babelParser.parse(codeString, { 9 | sourceType: 'module', 10 | plugins: ['jsx'], 11 | }); 12 | 13 | const routerObj = {routerEndPoints: []}; 14 | const paths = []; 15 | const importedRoutes = find(ast); 16 | 17 | const findOriginalVal = (variable) => { 18 | if ( 19 | variable in importedRoutes && 20 | typeof importedRoutes[variable] === 'string' 21 | ) 22 | return importedRoutes[variable]; 23 | return 'deadEnd'; 24 | }; 25 | 26 | trav(ast, { 27 | enter(path) { 28 | let current = path.node; 29 | let isMethodCall = 30 | current.type === 'CallExpression' && 31 | current.callee.type === 'MemberExpression'; 32 | let isGet = isMethodCall && current.callee.property.name === 'get'; 33 | let isPost = isMethodCall && current.callee.property.name === 'post'; 34 | if (isGet) { 35 | if (current.arguments[0].value) { 36 | let breadcrumb = new Breadcrumb() 37 | .fileName(fileName) 38 | .path(current.arguments[0].value) 39 | .method('get'); 40 | routerObj.routerEndPoints.push(current.arguments[0].value); 41 | if ( 42 | current.arguments[1] && 43 | current.arguments[1].type === 'Identifier' 44 | ) { 45 | const nextInLine = current.arguments[1]; 46 | breadcrumb.nextFile(findOriginalVal(nextInLine.name)); 47 | } 48 | paths.push(); 49 | } 50 | } else if (isPost) { 51 | if (current.arguments[0].value) { 52 | routerObj.routerEndPoints.push(); 53 | } 54 | } 55 | }, 56 | }); 57 | 58 | return routerObj; 59 | }; 60 | 61 | export default routerParser; 62 | -------------------------------------------------------------------------------- /server/ast2/serverParser.js: -------------------------------------------------------------------------------- 1 | const babelParser = require('@babel/parser'); 2 | const trav = require('@babel/traverse').default; 3 | 4 | import {Breadcrumb} from './Breadcrumb'; 5 | import find from './importsFinder'; 6 | 7 | const endpointParse = (codeString, fileName, fullPath, lastUpdated) => { 8 | const ast = babelParser.parse(codeString, { 9 | sourceType: 'module', 10 | plugins: ['jsx'], 11 | }); 12 | 13 | // array of arrays. Each path is an array with one beginning element 14 | const paths = []; 15 | 16 | const importedRoutes = find(ast); 17 | 18 | const findOriginalVal = (variable) => { 19 | if ( 20 | variable in importedRoutes && 21 | typeof importedRoutes[variable] === 'string' 22 | ) 23 | return importedRoutes[variable]; 24 | return 'deadEnd'; 25 | }; 26 | 27 | trav(ast, { 28 | enter(path) { 29 | let current = path.node; 30 | if ( 31 | current.type !== 'CallExpression' || 32 | current.callee.type !== 'MemberExpression' 33 | ) 34 | return; 35 | 36 | const methodArray = ['get', 'post', 'put', 'delete', 'patch', 'use']; 37 | const method = current.callee.property.name; 38 | if (methodArray.includes(method)) paths.push(routeParse(current, method)); 39 | }, 40 | }); 41 | 42 | function routeParse(current, method) { 43 | if (current.arguments[0].value) { 44 | let breadcrumb = new Breadcrumb() 45 | .fileName(fileName) 46 | .path(current.arguments[0].value) 47 | .method(method.toUpperCase()) 48 | .fullPath(fullPath) 49 | .lastUpdated(lastUpdated); 50 | if (current.arguments[1] && current.arguments[1].type === 'Identifier') { 51 | const nextNodeInLine = current.arguments[1]; 52 | breadcrumb.nextFile = findOriginalVal(nextNodeInLine.name); 53 | } 54 | return breadcrumb; 55 | } 56 | } 57 | 58 | return paths; 59 | }; 60 | 61 | export default endpointParse; 62 | -------------------------------------------------------------------------------- /server/harmonode_logo_fullname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Harmonode/d1b2102389bfb37bc080e77d3510425048f1c96d/server/harmonode_logo_fullname.png -------------------------------------------------------------------------------- /server/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Harmonode/d1b2102389bfb37bc080e77d3510425048f1c96d/server/icon.png -------------------------------------------------------------------------------- /server/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module 'react-color'; 3 | declare module 'react-color-picker'; 4 | -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import {BrowserWindow, app, ipcMain, dialog, nativeImage} from 'electron'; 2 | import {stringCodeBase} from './utils/stringifyCode'; 3 | import {getDirectories} from './utils/getFileDirectories'; 4 | import {DirObj, FileObj} from './types'; 5 | import monitorFiles from './utils/monitorFileChanges'; 6 | import * as path from 'path'; 7 | import Store from 'electron-store'; 8 | import createComponentObject from './utils/createComponentObject'; 9 | import * as fs from 'fs'; 10 | 11 | const dev: boolean = process.env.NODE_ENV === 'development'; 12 | const url = require('url'); 13 | const store = new Store(); 14 | 15 | // need to have all our file watchers in global so we can clear them 16 | let watchers: fs.FSWatcher[] = []; 17 | 18 | let mainWindow: BrowserWindow | null; 19 | 20 | process.on('uncaughtException', (error) => { 21 | // Hiding the error on the terminal as well 22 | console.error('Uncaught Exception:', error); 23 | }); 24 | app.setName('Harmonode'); 25 | const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png')); 26 | app.dock.setIcon(icon); 27 | function createWindow() { 28 | mainWindow = new BrowserWindow({ 29 | width: 1800, 30 | height: 1400, 31 | minWidth: 900, 32 | minHeight: 720, 33 | title: 'Harmonode', 34 | icon: nativeImage.createFromPath(path.join(__dirname, 'icon.png')), 35 | show: false, 36 | webPreferences: {nodeIntegration: true, contextIsolation: false}, 37 | }); 38 | 39 | let indexPath: string; 40 | if (dev) { 41 | indexPath = url.format({ 42 | protocol: 'http:', 43 | host: 'localhost:8080', 44 | pathname: 'index.html', 45 | slashes: true, 46 | }); 47 | } else { 48 | // need to eventually change for when this isn't the dev 49 | indexPath = url.format({ 50 | protocol: 'http:', 51 | host: 'localhost:8080', 52 | pathname: 'index.html', 53 | slashes: true, 54 | }); 55 | } 56 | 57 | mainWindow.loadURL(indexPath); 58 | 59 | mainWindow.once('ready-to-show', () => { 60 | if (mainWindow) mainWindow.show(); 61 | }); 62 | } 63 | 64 | app.whenReady().then(() => { 65 | createWindow(); 66 | }); 67 | 68 | app.on('window-all-closed', () => { 69 | if (process.platform !== 'darwin') { 70 | app.quit(); 71 | } else { 72 | mainWindow = null; 73 | } 74 | }); 75 | 76 | app.on('activate', () => { 77 | // On macOS it's common to re-create a window in the app when the dock icon is clicked and there are no other windows open. 78 | if (mainWindow === null) { 79 | createWindow(); 80 | } 81 | }); 82 | 83 | /* 84 | ================= 85 | IPC Handlers 86 | ================= 87 | */ 88 | 89 | ipcMain.handle('openFolderDialog', async () => { 90 | const result = await dialog.showOpenDialog({ 91 | properties: ['openDirectory'], 92 | }); 93 | return result.filePaths[0]; 94 | }); 95 | 96 | ipcMain.handle('openFileDialog', async (_, dirPath) => { 97 | const result = await dialog.showOpenDialog({ 98 | properties: ['openFile'], 99 | defaultPath: dirPath, 100 | }); 101 | return result.filePaths[0]; 102 | }); 103 | 104 | ipcMain.handle( 105 | 'countCodeFiles', 106 | async (_, dirPath, ignoreList, approvedExt, serverPath) => { 107 | const codeFiles: FileObj[] = await stringCodeBase( 108 | dirPath, 109 | ignoreList, 110 | approvedExt, 111 | serverPath 112 | ); 113 | return codeFiles.length; 114 | } 115 | ); 116 | 117 | ipcMain.handle( 118 | 'readCodeFiles', 119 | async (_, folder, ignore, extensions, server) => { 120 | const codeFiles: FileObj[] = await stringCodeBase( 121 | folder, 122 | ignore, 123 | extensions, 124 | server 125 | ); 126 | 127 | const codeFileObj = { 128 | folder, 129 | ignore, 130 | extensions, 131 | server, 132 | }; 133 | // create the component object 134 | const componentObj = createComponentObject(codeFiles, server); 135 | 136 | // clear all the watchers before adding new ones 137 | for (const watcher of watchers) watcher.close(); 138 | 139 | // add the new file watchers 140 | watchers = monitorFiles(componentObj, codeFileObj); 141 | 142 | // return the component object to front end 143 | return componentObj; 144 | } 145 | ); 146 | 147 | // handler to load a folder 148 | // -rebuilds the AST for it to get most up to date info 149 | // -clears the watchers on the previous project 150 | // -adds new watchers on the loaded project 151 | ipcMain.handle('loadProject', async (_, project) => { 152 | // need to restring the code base 153 | const {folder, ignore, extensions, server} = project; 154 | const codeFiles: FileObj[] = await stringCodeBase( 155 | folder, 156 | ignore, 157 | extensions, 158 | server 159 | ); 160 | 161 | const codeFileObj = {folder, ignore, extensions, server}; 162 | // create the component object 163 | const componentObj = createComponentObject(codeFiles, server); 164 | 165 | // clear any existing watchers 166 | for (const watcher of watchers) watcher.close(); 167 | // set new watchers 168 | watchers = monitorFiles(componentObj, codeFileObj); 169 | // return the component object 170 | return componentObj; 171 | }); 172 | 173 | ipcMain.handle('getDirectories', async (_, dirPath) => { 174 | const directories: DirObj[] = await getDirectories(dirPath); 175 | return directories; 176 | }); 177 | 178 | ipcMain.handle('stringCode', async (_, filePath) => { 179 | return await fs.readFileSync(filePath, 'utf-8'); 180 | }); 181 | 182 | ipcMain.handle('saveCode', async (_, filePath, code) => { 183 | return await fs.writeFileSync(filePath, code); 184 | }); 185 | 186 | // ==== Electron Store Stuff ==== 187 | 188 | ipcMain.handle('storeProjects', (event, projects) => { 189 | store.set('projects', projects); 190 | }); 191 | 192 | ipcMain.handle('getProjects', (event) => { 193 | const storedProjects = store.get('projects'); 194 | return storedProjects; 195 | }); 196 | 197 | ipcMain.handle('deleteProjects', (event) => { 198 | store.delete('projects'); 199 | }); 200 | 201 | export {mainWindow}; 202 | -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | // file object for information about files read with fs 2 | export interface FileObj { 3 | fileName: string; 4 | filePath: string; 5 | fullPath: string; 6 | contents: string; 7 | mDate?: Date; // <--- last modified date, used for knowing when ast will update the file 8 | } 9 | 10 | // directory object for information about directories read with fs 11 | export interface DirObj { 12 | fileName: string; 13 | filePath: string; 14 | } 15 | 16 | // the root ast structure which has all the ast data collected in it 17 | export interface astRoot { 18 | fetches: astFetch[]; 19 | endpoints: astEndpoint[]; 20 | fetchFiles: astFetchFile[]; 21 | endpointFiles: astEndpointFile[]; 22 | } 23 | 24 | 25 | // produces information from ast parser with information on a per file basis for fetch requests 26 | export interface astFetchFile { 27 | fileName: string; 28 | filePath: string; 29 | fullPath: string; 30 | id: string; 31 | lastUpdated: Date | undefined; 32 | fetches: object[]; 33 | } 34 | 35 | // produces information from ast parser with information on a per file basis for endpoint details 36 | export interface astEndpointFile { 37 | fileName: string; 38 | filePath: string; 39 | fullPath: string; 40 | id: string; 41 | isServer: boolean; 42 | lastUpdated: Date | undefined; 43 | endpoints: string[]; 44 | } 45 | 46 | // produces information derived from ast parser with information on a per fetch request basis 47 | export interface astFetch {} 48 | 49 | // produces information derived from ast parser with information on a per endpoint basis 50 | export interface astEndpoint {} 51 | export interface pathFileObj { 52 | path: string[]; 53 | file: FileObj; 54 | } -------------------------------------------------------------------------------- /server/utils/createComponentObject.ts: -------------------------------------------------------------------------------- 1 | import fetchParser from '../ast/clientParser'; 2 | import endpointParse from '../ast/serverParser'; 3 | import { 4 | astEndpoint, 5 | astEndpointFile, 6 | astFetch, 7 | astFetchFile, 8 | astRoot, 9 | } from '../types'; 10 | import {v4 as uuid} from 'uuid'; 11 | import {getPathArray} from './pathUtils'; 12 | import fullBackEndCreator from './getBackEndObj'; 13 | 14 | // module that creates the component object that will be sent to the front end 15 | 16 | export default function createComponentObject(codeFiles, serverPath) { 17 | // the basic structure of the data object we use to drive everything in harmonode 18 | const componentObj: astRoot = { 19 | fetches: [] as astFetch[], 20 | endpoints: [] as astEndpoint[], 21 | fetchFiles: [] as astFetchFile[], 22 | endpointFiles: [] as astEndpointFile[], 23 | }; 24 | 25 | // call the helper function to push the data we need to into our component object 26 | pushFilesToCompObj(codeFiles, componentObj, serverPath); 27 | createFetchArray(componentObj); 28 | // this will be the object we eventually return to the front end 29 | return componentObj; 30 | } 31 | 32 | function pushFilesToCompObj(codeFiles, componentObj, serverPath) { 33 | const paths = {}; 34 | const endpoints: any = []; 35 | const allPathArrays: Array> = []; 36 | 37 | for (const file of codeFiles) { 38 | // getting the AST for fetches 39 | allPathArrays.push(getPathArray(file.fullPath, serverPath)); 40 | // if it's the server path, let's load the server stuff into an ast 41 | if (file.fullPath === serverPath) { 42 | // get the AST for the server 43 | const serverObj = endpointParse(file.contents).serverEndPoints.filter( 44 | (path) => path[0] !== '.' 45 | ); 46 | const parsedEndpointsArray = serverObj; 47 | const endpointsArray = parsedEndpointsArray.map((endpoint) => { 48 | return { 49 | method: 'GLOBAL', 50 | path: endpoint, 51 | id: uuid(), 52 | }; 53 | }); 54 | // if (parsedEndpointsArray.length > 0) { 55 | // componentObj.endpointFiles.push({ 56 | // fileName: file.fileName, 57 | // fullPath: file.fullPath, 58 | // filePath: file.filePath, 59 | // id: uuid(), 60 | // lastUpdated: file.mDate, 61 | // isServer: true, 62 | // endpoints: endpointsArray, 63 | // }); 64 | // // endpoints.push(...endpointsArray); 65 | // componentObj.fromImports = serverObj; 66 | // } 67 | 68 | continue; // skip the rest since we have what we need 69 | } 70 | // getting the AST for fetches 71 | const parsedFetchesArray = fetchParser(file.contents); 72 | const fetchesArray = parsedFetchesArray.map((fetch) => { 73 | const endpoint = getEndpoint(fetch.path); 74 | const fetchStore = `${endpoint}-${fetch.method}`; 75 | const idProp = `${endpoint}-id`; 76 | if (!paths.hasOwnProperty(idProp)) paths[idProp] = uuid(); 77 | if (!paths.hasOwnProperty(fetchStore)) { 78 | paths[fetchStore] = { 79 | ...fetch, 80 | path: getEndpoint(fetch.path), 81 | id: paths[idProp], 82 | }; 83 | } 84 | // endpoints.push(paths[fetchStore]); 85 | return paths[fetchStore]; 86 | }); 87 | if (parsedFetchesArray.length > 0) { 88 | componentObj.fetchFiles.push({ 89 | fileName: file.fileName, 90 | fullPath: file.fullPath, 91 | filePath: file.filePath, 92 | id: uuid(), 93 | lastUpdated: file.mDate, 94 | fetches: fetchesArray, 95 | }); 96 | } 97 | } 98 | const backendCreation = fullBackEndCreator(codeFiles, serverPath); 99 | const backendRoutes = backendAdd(backendCreation, paths); 100 | 101 | for (const route of backendRoutes) { 102 | const pathsProp = `${route.path}-${route.method}`; 103 | const idProp = `${route.path}-id`; 104 | if (!paths.hasOwnProperty(idProp)) { 105 | paths[idProp] = uuid(); 106 | } 107 | if (paths.hasOwnProperty(pathsProp)) { 108 | paths[pathsProp] = { 109 | ...paths[pathsProp], 110 | fullPath: route.fullPath, 111 | fileName: route.fileName, 112 | lastUpdated: route.lastUpdated, 113 | }; 114 | } else { 115 | paths[pathsProp] = { 116 | method: route.method, 117 | path: route.path, 118 | fileName: route.fileName, 119 | fullPath: route.fullPath, 120 | lastUpdated: route.lastUpdated, 121 | id: paths[idProp], 122 | }; 123 | } 124 | endpoints.push(paths[pathsProp]); 125 | } 126 | // console.log(endpoints, '!!!!!ENDPOINTS!!!!!!!!'); 127 | componentObj.endpoints.push(...endpoints); 128 | const endpointFiles: any = []; 129 | const serverAndEndpoints = [...endpoints]; 130 | const endpointCache: any = {}; 131 | for (const endpoint of serverAndEndpoints) { 132 | if (endpoint.method !== 'GLOBAL') { 133 | const idProp = `${endpoint.path}-id`; 134 | if (!endpointCache.hasOwnProperty(endpoint.fileName)) { 135 | endpointCache[endpoint.fileName] = { 136 | fileName: endpoint.fileName, 137 | fullPath: endpoint.fullPath, 138 | lastUpdated: endpoint.lastUpdated, 139 | id: uuid(), 140 | endpoints: [ 141 | {method: endpoint.method, path: endpoint.path, id: paths[idProp]}, 142 | ], 143 | }; 144 | } else { 145 | endpointCache[endpoint.fileName].endpoints.push({ 146 | method: endpoint.method, 147 | path: endpoint.path, 148 | id: paths[idProp], 149 | }); 150 | } 151 | } 152 | } 153 | for (const key of Object.keys(endpointCache)) { 154 | // console.log(endpointCache[key], '!!!ENDPOINT CACHE!!!!!!'); 155 | endpointFiles.push(endpointCache[key]); 156 | } 157 | componentObj.endpointFiles.push(...endpointFiles); 158 | // console.log(endpoints, '!!!!!ENDPOINTS!!!!!!'); 159 | } 160 | 161 | function createFetchArray(componentObj) { 162 | const fetches = {}; 163 | for (const fetchFile of componentObj.fetchFiles) { 164 | for (const fetch of fetchFile.fetches) { 165 | const fetchStore = `${fetch.path}-${fetch.method}`; 166 | if (!fetches.hasOwnProperty(fetchStore)) { 167 | fetches[fetchStore] = { 168 | ...fetch, 169 | files: [{name: fetchFile.fileName, id: fetchFile.id}], 170 | }; 171 | } else { 172 | fetches[fetchStore] = { 173 | ...fetches[fetchStore], 174 | files: fetches[fetchStore].files.concat({ 175 | name: fetchFile.fileName, 176 | id: fetchFile.id, 177 | }), 178 | }; 179 | } 180 | } 181 | } 182 | for (const key of Object.keys(fetches)) { 183 | componentObj.fetches.push(fetches[key]); 184 | } 185 | } 186 | 187 | // URL Sanitizing functions for fetch request URL's to extract the endpoints 188 | function isLocalHost(url) { 189 | const localHostPatterns = [ 190 | /^localhost(:\d+)?$/, 191 | /^127\.0\.0\.1(:\d+)?$/, 192 | /^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/, // Match IP addresses like 192.168.1.1:3000 193 | ]; 194 | 195 | return localHostPatterns.some((pattern) => pattern.test(url)); 196 | } 197 | 198 | function getEndpoint(url) { 199 | if (typeof url !== 'string') return 'unknownurl'; 200 | // Check if the URL starts with 'http' or '/' to determine if it's a non-local URL or just a path 201 | url = url.replaceAll('`', '').split('?')[0]; 202 | // if creating a url triggers an error, then we know it's missing a '/' at the beginning 203 | try { 204 | new URL(url).hostname; 205 | } catch (error) { 206 | if (!url.startsWith('/')) url = '/' + url; 207 | } 208 | if (!url.startsWith('http') && url.startsWith('/')) { 209 | if (isLocalHost(url.split('/')[0])) { 210 | // It's a local URL without the protocol 211 | // Extract the endpoint by removing the domain and protocol from the URL 212 | const urlParts = url.split('/'); 213 | return `/${urlParts.slice(1).join('/')}`; 214 | } 215 | // It's just a path, return it as is 216 | return url; 217 | } 218 | 219 | // Check if the URL is a local URL 220 | if (isLocalHost(new URL(url).hostname)) { 221 | // Extract the endpoint by removing the domain and protocol from the URL 222 | const urlParts = url.split('/'); 223 | return `/${urlParts.slice(3).join('/')}`; 224 | } 225 | 226 | // It's a non-local URL, return it as is 227 | return url; 228 | } 229 | 230 | function backendAdd(backendCreation, paths) { 231 | let pathCache: any = []; 232 | 233 | for (const creation of backendCreation) { 234 | let method = creation.method === 'USE' ? '' : creation.method; 235 | const pathArray = creation.fileName.split('.'); 236 | const pathCacheFind = pathCache.find((path) => { 237 | let nextFile = path.nextFile; 238 | if (!path.nextFile) return false; 239 | nextFile = nextFile.split('/'); 240 | nextFile = nextFile[nextFile.length - 1]; 241 | return nextFile === pathArray[0]; 242 | }); 243 | 244 | if (pathCacheFind && method) { 245 | const creationPath = creation.path.endsWith('/') 246 | ? creation.path.slice(0, creation.path.length - 1) 247 | : creation.path; 248 | 249 | pathCache.push({...pathCacheFind}); 250 | 251 | delete pathCacheFind.nextFile; 252 | 253 | pathCacheFind.path = `${pathCacheFind.path}${creationPath}`; 254 | pathCacheFind.method = method; 255 | pathCacheFind.fileName = creation.fileName; 256 | pathCacheFind.fullPath = creation.fullPath; 257 | pathCacheFind.lastUpdated = creation.lastUpdated; 258 | } else { 259 | const newPathObj: any = {}; 260 | 261 | if (creation.nextFile) newPathObj.nextFile = creation.nextFile; 262 | pathCache.push({ 263 | ...newPathObj, 264 | path: creation.path, 265 | method: method, 266 | }); 267 | } 268 | } 269 | return pathCache.filter((path) => path.method != ''); 270 | } 271 | -------------------------------------------------------------------------------- /server/utils/getBackEndObj.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | // import endpointParse from "../ast/serverParser"; 3 | import endpointParse from '../ast2/serverParser'; 4 | import routerParser from '../ast/routerParser'; 5 | import {FileObj, pathFileObj} from '../types'; 6 | 7 | // helper function to extract array of strings for paths in each file 8 | const getPathArray = (routeString) => { 9 | const pathParts = routeString.split(path.sep); 10 | const last = pathParts[pathParts.length - 1]; 11 | if (last.includes('.')) 12 | pathParts[pathParts.length - 1] = last.slice(0, last.indexOf('.')); 13 | return pathParts; 14 | }; 15 | 16 | // return a new file in the file collection. Must receive an origin file. If a valid destination file exists, return it 17 | // if NOT, return 'not a valid path or file doesn't exist' 18 | 19 | const fullBackEndCreator = (codefiles, serverPath) => { 20 | // create the full array of all file paths, so we can navigate through this... 21 | const allPathArrays = []; 22 | const pathFileObjs = []; 23 | 24 | // this object will have all needed data from the server file, to be retrieved in loop below... 25 | let serverFilePaths = {}; 26 | const breadCrumbs = []; 27 | 28 | for (const file of codefiles) { 29 | allPathArrays.push(getPathArray(file.fullPath)); 30 | pathFileObjs.push({path: getPathArray(file.fullPath), file}); 31 | 32 | // parse the server file first and get each endpoint and where it will go next 33 | if (file.fullPath === serverPath) { 34 | serverFilePaths = endpointParse( 35 | file.contents, 36 | file.fileName, 37 | file.fullPath, 38 | file.mDate 39 | ).filter((path) => path !== undefined); 40 | breadCrumbs.push(...serverFilePaths); 41 | } 42 | } 43 | 44 | const navToOtherFile = (originString, destinationString) => { 45 | if (typeof destinationString !== 'string') return 'no router file'; 46 | const originPaths = getPathArray(originString); 47 | const destPathArray = destinationString.split('/'); 48 | let pathToNewFile = []; 49 | let dots = 0; 50 | 51 | for (let el of destPathArray) { 52 | if (el === '..') { 53 | dots === 0 ? (dots -= 2) : (dots -= 1); 54 | } else if (el === '.') dots -= 1; 55 | } 56 | 57 | pathToNewFile = [ 58 | ...originPaths.slice(0, dots), 59 | ...destPathArray.filter((el) => !el.includes('.')), 60 | ]; 61 | for (let pathFile of pathFileObjs) { 62 | if (JSON.stringify(pathFile.path) === JSON.stringify(pathToNewFile)) { 63 | return pathFile.file; 64 | } 65 | } 66 | 67 | for (let pathArray of allPathArrays) { 68 | if (path.join(...pathArray) === path.join(...pathToNewFile)) { 69 | return path.join(...pathArray); 70 | } 71 | } 72 | 73 | return 'no such file exists'; 74 | }; 75 | 76 | for (let breadcrumb of serverFilePaths) { 77 | // for getting stuff out of routes 78 | if (breadcrumb.nextFile) { 79 | const routerFile = navToOtherFile(serverPath, breadcrumb.nextFile); 80 | if (typeof routerFile === 'string') continue; 81 | const newCrumbs = endpointParse( 82 | routerFile.contents, 83 | routerFile.fileName, 84 | routerFile.fullPath, 85 | breadcrumb.lastUpdated 86 | ); 87 | // console.log(newCrumbs, '!!!! NEW CRUMBS !!!!'); 88 | breadCrumbs.push(...newCrumbs); 89 | } 90 | } 91 | return breadCrumbs; 92 | }; 93 | 94 | export default fullBackEndCreator; 95 | -------------------------------------------------------------------------------- /server/utils/getFileDirectories.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | const path = require('path'); 3 | import {DirObj} from '../types'; 4 | 5 | // module that will display all the file directories so the user can choose which ones to ignore 6 | export async function getDirectories(dirPath: string) { 7 | const directoryArray: DirObj[] = []; 8 | // ignore git files and node_modules automatically 9 | 10 | const dirIgnoreList: string[] = ['node_modules', '.git']; 11 | function recurseDirs(directoryPath: string = dirPath) { 12 | const files = fs.readdirSync(directoryPath) as string[]; 13 | 14 | files.forEach((file: string) => { 15 | if (dirIgnoreList.includes(file)) return; 16 | const filePath: string = path.join(directoryPath, file); 17 | const fsStats: fs.Stats = fs.statSync(filePath); 18 | 19 | if (fsStats.isDirectory()) { 20 | const dirObj: DirObj = {} as DirObj; 21 | dirObj.fileName = file; 22 | dirObj.filePath = filePath; 23 | directoryArray.push(dirObj); 24 | recurseDirs(filePath); 25 | } else return; 26 | }); 27 | } 28 | 29 | recurseDirs(dirPath); 30 | return directoryArray; 31 | } 32 | -------------------------------------------------------------------------------- /server/utils/monitorFileChanges.ts: -------------------------------------------------------------------------------- 1 | import {ipcMain} from 'electron'; 2 | import * as fs from 'fs'; 3 | import {mainWindow} from '../main'; 4 | import createComponentObject from './createComponentObject'; 5 | import {stringCodeBase} from './stringifyCode'; 6 | // module to monitor a project file for changes 7 | // Will need to be on a setTimeout, cleared only when another project is loaded 8 | // factory function for monitoring the files 9 | export default function monitorFiles(astRootObj, codeFileObj) { 10 | const fetchFiles = astRootObj.fetchFiles; 11 | const fetches = astRootObj.fetches; 12 | const endpoints = astRootObj.endpoints; 13 | const endpointFiles = astRootObj.endpointFiles; 14 | 15 | const watchers: fs.FSWatcher[] = []; // watchers array so we can clear them all 16 | /* 17 | ===================== 18 | FetchFiles Monitoring 19 | ===================== 20 | */ 21 | function setWatchers() { 22 | for (const file of fetchFiles) { 23 | // some editors sometimes trigger two change events, so we can do this based on modified time 24 | const watcher = fs.watch(file.fullPath, async (eventType, filename) => { 25 | let fsStats: fs.Stats; 26 | 27 | // ===File Rename Event=== 28 | if (eventType === 'rename') { 29 | // update the file name 30 | console.log(`${file.fileName} was renamed to ${filename}`); 31 | file.fullPath = `${file.filePath}/${filename}`; 32 | file.fileName = filename; 33 | mainWindow?.webContents.send('fileChanged', astRootObj); 34 | } 35 | 36 | // ===File Change Event=== 37 | 38 | // renaming triggers a change event first, which causes an error - 39 | // because the filename on the change is the previous one - so it doesn't exist 40 | // this try catch will just return since we don't care about that change 41 | try { 42 | fsStats = fs.statSync(file.fullPath); 43 | } catch (error) { 44 | return; 45 | } 46 | // get the last modified time 47 | const mTime = fsStats.mtime; 48 | 49 | // we have to do this check to avoid two change events firing 50 | // which VSCode and other code editors emit sometimes 51 | 52 | // if the last modified time is different than what we saved, do stuff... 53 | if (mTime.getTime() != file.lastUpdated.getTime()) { 54 | file.lastUpdated = mTime; // update the last modified time 55 | console.log(`The file ${filename} was ${eventType}d`); 56 | const {folder, ignore, extensions, server} = codeFileObj; 57 | const codeFiles = await stringCodeBase( 58 | folder, 59 | ignore, 60 | extensions, 61 | server 62 | ); 63 | const newAst = createComponentObject(codeFiles, server); 64 | /* 65 | const stringifiedFile = stringFileContents(file.fullPath) 66 | const parsedFetchArray = fetchParser(stringifiedFile) 67 | const newFileFetches = [] 68 | for (const fetch of file.fetches) { 69 | } 70 | 71 | */ 72 | mainWindow?.webContents.send('fileChanged', newAst); 73 | } 74 | }); 75 | watchers.push(watcher); 76 | } 77 | 78 | /* 79 | ====================== 80 | Server File Monitoring 81 | ====================== 82 | */ 83 | 84 | for (const file of endpointFiles) { 85 | const watcher = fs.watch(file.fullPath, async (eventType, filename) => { 86 | let fsStats: fs.Stats; 87 | 88 | // ===File Rename Event=== 89 | if (eventType === 'rename') { 90 | // update the file name 91 | console.log(`${file.fileName} was renamed to ${filename}`); 92 | file.fullPath = `${file.filePath}/${filename}`; 93 | file.fileName = filename; 94 | mainWindow?.webContents.send('fileChanged', astRootObj); 95 | } 96 | 97 | // ===File Change Event=== 98 | 99 | // renaming triggers a change event first, which causes an error - 100 | // because the filename on the change is the previous one - so it doesn't exist 101 | // this try catch will just return since we don't care about that change 102 | try { 103 | fsStats = fs.statSync(file.fullPath); 104 | } catch (error) { 105 | return; 106 | } 107 | // get the last modified time 108 | const mTime = fsStats.mtime; 109 | // we have to do this check to avoid two change events firing 110 | // which VSCode and other code editors emit sometimes 111 | 112 | // if the last modified time is different than what we saved, do stuff... 113 | if (mTime.getTime() != file.lastUpdated.getTime()) { 114 | file.lastUpdated = mTime; // update the last modified time 115 | console.log(`The file ${filename} was ${eventType}d`); 116 | const {folder, ignore, extensions, server} = codeFileObj; 117 | const codeFiles = await stringCodeBase( 118 | folder, 119 | ignore, 120 | extensions, 121 | server 122 | ); 123 | const newAst = createComponentObject(codeFiles, server); 124 | mainWindow?.webContents.send('fileChanged', newAst); 125 | } 126 | }); 127 | watchers.push(watcher); 128 | } 129 | } 130 | setWatchers(); 131 | 132 | return watchers; 133 | } 134 | -------------------------------------------------------------------------------- /server/utils/pathUtils.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | export const getPathArray = (routeString : string, serverPath : string) : string[] => { 4 | const pathParts : string[] = routeString.split(path.sep); 5 | const last : string = pathParts[pathParts.length - 1] 6 | if (last.includes(".")) 7 | pathParts[pathParts.length - 1] = last.slice(0, last.indexOf(".")); 8 | return pathParts; 9 | }; 10 | -------------------------------------------------------------------------------- /server/utils/stringifyCode.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | const path = require('path'); 3 | import {FileObj} from '../types'; 4 | 5 | // function to grab all the white listed files and convert contents to string for AST handling 6 | // projectDir = root directory to grab the code files 7 | // dirIgnoreList is an array of the directory names that we want to ignore 8 | // extensionApproveList is an array of the file extensions we will want to include to reduce AST parsing overhead 9 | export async function getCodeFiles( 10 | projectDir: string, 11 | dirIgnoreList: string[], 12 | extensionApproveList: string[] 13 | ) { 14 | // always ignore node_modules and .git 15 | dirIgnoreList = [ 16 | ...dirIgnoreList, 17 | '/node_modules', 18 | '/.git', 19 | '/webpack.config.js', 20 | ]; 21 | // create a fileArray to put the path of each file 22 | const fileArray: FileObj[] = []; 23 | 24 | // recursive function to collect all the file paths into the array 25 | function recurseFiles(directoryPath: string = projectDir) { 26 | // get all of the file paths into an array so we can iterate and recurse through them 27 | const files: string[] = fs.readdirSync(directoryPath); 28 | 29 | // iterate over all the files 30 | files.forEach((file: string) => { 31 | // skip if the file path is in the ignore list 32 | if (dirIgnoreList.includes(file)) return; 33 | 34 | // skip if the extension is in the ignore list 35 | const fileSplit: string[] = file.split('.'); 36 | 37 | // get the full file path 38 | const filePath: string = path.join(directoryPath, file); 39 | if (dirIgnoreList.includes(filePath.replace(projectDir, ''))) return; 40 | 41 | // get the files stats - tells us meta details of the file 42 | const fsStats: fs.Stats = fs.statSync(filePath); 43 | if ( 44 | fsStats.isFile() && 45 | !extensionApproveList.includes(`.${fileSplit[fileSplit.length - 1]}`) 46 | ) { 47 | return; 48 | } 49 | 50 | // if it's a file, let's push the information to our filePath array 51 | if (fsStats.isFile()) { 52 | const fileObj: FileObj = {} as FileObj; 53 | fileObj.fileName = file; 54 | fileObj.filePath = directoryPath; 55 | fileObj.fullPath = filePath; 56 | fileObj.contents = ''; 57 | fileObj.mDate = new Date(fsStats.mtime); 58 | fileArray.push(fileObj); 59 | } 60 | // if it's a directory, let's recurse again with the directory as the new path 61 | else if (fsStats.isDirectory()) recurseFiles(filePath); 62 | }); 63 | } 64 | // invoke the recursive function 65 | recurseFiles(projectDir); 66 | 67 | // return the fileArray of all the filepaths 68 | return fileArray; 69 | } 70 | 71 | export async function stringFileContents(filePath: string) { 72 | // stringify the file path in utf-8 encoding using the filesystem 73 | const stringedCode: string = fs.readFileSync(filePath, 'utf-8') as string; 74 | // return the stringified code 75 | return stringedCode; 76 | } 77 | 78 | export async function stringCodeBase( 79 | projectDir: string, 80 | dirIgnoreList: string[], 81 | extensionApproveList: string[], 82 | serverPath: string 83 | ) { 84 | // grab all of the file paths of the code base 85 | const fileArray: object[] = await getCodeFiles( 86 | projectDir, 87 | dirIgnoreList, 88 | extensionApproveList 89 | ); 90 | 91 | // set an empty array to put all of our stringified code objects inside 92 | const stringifiedCodeObjectArray: FileObj[] = []; 93 | 94 | // for each file in the fileArray, stringify the code with the filesystem and set it as a value to an object with the key as the value of the filepath 95 | for (const fileObj of fileArray) { 96 | const stringifiedCodeObject: FileObj = {...fileObj} as FileObj; 97 | stringifiedCodeObject.contents = await stringFileContents( 98 | (fileObj as FileObj).fullPath 99 | ); 100 | stringifiedCodeObjectArray.push(stringifiedCodeObject); 101 | } 102 | 103 | // return our object 104 | return stringifiedCodeObjectArray; 105 | } 106 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | 8 | /** SEEQR: Must target ES6 at max because html-webpack-plugin explodes with esnext features the babel-loader configuration is being a brat, so we'll do the transpiling here*/ 9 | "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 10 | 11 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 12 | "lib": [ 13 | "es2020", 14 | "dom", 15 | "esnext" 16 | ] /* Specify library files to be included in the compilation. */, 17 | "allowJs": true /* Allow javascript files to be compiled. */, 18 | // "checkJs": true, /* Report errors in .js files. */ 19 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 20 | 21 | "declaration": true /* Generates corresponding '.d.ts' file. */, 22 | 23 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 24 | "sourceMap": true /* Generates corresponding '.map' file. */, 25 | // "outFile": "./", /* Concatenate and emit output to single file. */ 26 | "outDir": "./tsCompiled" /* Redirect output structure to the directory. */, 27 | // "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 28 | // "composite": true, /* Enable project compilation */ 29 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 30 | // "removeComments": true, /* Do not emit comments to output. */ 31 | // "noEmit": true, /* Do not emit outputs. */ 32 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 33 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 34 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 35 | 36 | /* Strict Type-Checking Options */ 37 | "strict": true /* Enable all strict type-checking options. */, 38 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 39 | "strictNullChecks": true /* Enable strict null checks. */, 40 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 41 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 42 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 43 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 44 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 45 | 46 | /* Additional Checks */ 47 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 48 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 49 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 50 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 51 | 52 | /* Module Resolution Options */ 53 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 54 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 55 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 56 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 57 | "typeRoots": [ 58 | "./node_modules/@types" 59 | ] /* List of folders to include type definitions from. */, // Added ./node_modeules/@types to fix red squigglies for react.node types 60 | // "types": ["jest"] /* Type declaration files to be included in compilation. */, 61 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 62 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 63 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 64 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 65 | 66 | /* Source Map Options */ 67 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 68 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 69 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 70 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 71 | 72 | /* Experimental Options */ 73 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 74 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 75 | /* Advanced Options */ 76 | "skipLibCheck": true /* Skip type checking of declaration files. */, 77 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 78 | "resolveJsonModule": true /* Include modules imported with '.json' extension. Requires TypeScript version 2.9 or later. */ 79 | } 80 | // "include": [ 81 | // "backend", "frontend", "__tests__/frontend/lib/testClickNodeForDetails.ts", "__tests__/frontend/lib/checkClickNode.ts" 82 | // ] 83 | } 84 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | process.env.NODE_ENV === 'development'; 5 | 6 | module.exports = { 7 | entry: './client/src/index.js', 8 | mode: process.env.NODE_ENV, 9 | output: { 10 | path: path.resolve(__dirname, './build'), 11 | filename: 'bundle.js', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?/, 17 | exclude: /(node_modules)/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-react', '@babel/preset-env'], 22 | }, 23 | }, 24 | }, 25 | { 26 | test: /\.tsx?$/, 27 | exclude: /(node_modules)/, 28 | use: 'ts-loader', 29 | }, 30 | { 31 | test: /\.s?[ac]ss$/i, 32 | use: ['style-loader', 'css-loader', 'sass-loader'], 33 | }, 34 | { 35 | test: /\.(png|jp(e*)g|svg|gif)$/, 36 | include: path.resolve(__dirname, 'server'), 37 | use: [ 38 | { 39 | loader: 'file-loader', 40 | options: { 41 | outputPath: '', 42 | name: '[name].[ext]', 43 | }, 44 | }, 45 | ], 46 | }, 47 | { test: /\\.(png|jp(e*)g|svg|gif)$/, use: ['file-loader'] }, 48 | ], 49 | }, 50 | 51 | resolve: { 52 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 53 | }, 54 | 55 | plugins: [ 56 | new HtmlWebpackPlugin({ 57 | filename: 'index.html', 58 | title: 'Harmonode', 59 | }), 60 | ], 61 | devServer: { 62 | historyApiFallback: { 63 | index: '/index.html', // Specify the entry point HTML file 64 | }, 65 | client: { 66 | overlay: { 67 | runtimeErrors: (error) => { 68 | if (error.message === 'ResizeObserver loop limit exceeded') { 69 | return false; 70 | } 71 | return true; 72 | }, 73 | }, 74 | }, 75 | }, 76 | }; 77 | --------------------------------------------------------------------------------