├── .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 |
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 | 
37 |
38 |
39 |
40 | ---
41 | 
42 |
43 |
44 | 
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 |
119 | Close / Discard Unsaved Changes
120 |
121 |
122 | Save
123 |
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 | // {/* {
70 | // console.log('clicked', globalDir.dirTree)
71 | // }} type='button'>clickMe2
72 | // clickMe */}
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 | {/*
23 |
30 |
37 |
44 | */}
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 |
402 | Vertical View
403 |
404 |
405 | Horizontal View
406 |
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 |
29 | {editIcon}
30 |
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 |
29 | {editIcon}
30 |
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 |
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 | setShowNew(true)}>
19 | Add New Project
20 |
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 |
149 | Choose Project Directory
150 |
151 | {projectFolder && (
152 | <>
153 |
154 | Project Folder: {projectFolder}
155 |
156 |
157 | Choose Server File
158 |
159 | {serverPath && (
160 | <>
161 |
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 |
41 | {extension}
42 |
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 |
33 | Clear Project List
34 |
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 |
147 | {fileName}
148 |
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 |
51 | Load
52 |
53 |
54 | Delete
55 |
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 |
129 | {!showThemeChoices ? 'Change Themes' : 'Hide Change Themes'}
130 |
131 | {showThemeChoices ? (
132 |
133 | handleClick(defaultTheme, e)}
136 | >
137 | Blue Mode (Default)
138 |
139 | handleClick(themeOption1, e)}
142 | >
143 | Dark Mode
144 |
145 | handleClick(themeOption2, e)}
148 | >
149 | Beach Mode
150 |
151 | handleClick(themeOption3, e)}
154 | >
155 | Purple Mode
156 |
157 | handleClick(themeOption4, e)}
160 | >
161 | Forest Mode
162 |
163 |
164 |
165 | handleClick('themeChoose', e)}
170 | >
171 | {' '}
172 | {themeChoice ? 'Hide Customize Colors' : 'Customize Colors'}
173 |
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 |
--------------------------------------------------------------------------------