├── deno_cli ├── .gitignore ├── package-lock.json ├── edact.ts ├── init.ts ├── types.ts ├── parse │ ├── find_files.ts │ ├── parse.ts │ └── detect_endpoints.ts ├── README.md └── test │ ├── test.ts │ ├── class.ts │ └── error.ts ├── vscode_extension ├── .gitignore ├── .vscode │ ├── settings.json │ ├── extensions.json │ ├── tasks.json │ └── launch.json ├── media │ ├── edact-vscode.png │ └── Thumbs-Up-Silhouette.png ├── src │ ├── data │ │ └── api.ts │ ├── index.js │ ├── utils │ │ ├── utils.ts │ │ ├── localStorage.ts │ │ ├── constant.ts │ │ └── vsCodeApi.ts │ ├── components │ │ ├── Response.tsx │ │ ├── Request.tsx │ │ ├── RequestComponents.tsx │ │ ├── RunButtons.tsx │ │ ├── ParseButton.tsx │ │ └── Endpoint.tsx │ ├── containers │ │ ├── ConfigPanel.tsx │ │ ├── Initalize.tsx │ │ ├── Routes.tsx │ │ └── Sidebar.tsx │ ├── providers │ │ ├── lib │ │ │ ├── filefinder.ts │ │ │ ├── parse.ts │ │ │ ├── detect_endpoints.ts │ │ │ └── test.ts │ │ └── sidebar-provider.tsx │ └── extension.ts ├── README.md ├── tsconfig.json ├── webpack.config.js ├── types.ts ├── package.json └── script │ └── sidebar-webview.css ├── .DS_Store └── README.md /deno_cli/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.vscode -------------------------------------------------------------------------------- /vscode_extension/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/errordactyl/HEAD/.DS_Store -------------------------------------------------------------------------------- /vscode_extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false 3 | } -------------------------------------------------------------------------------- /vscode_extension/media/edact-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/errordactyl/HEAD/vscode_extension/media/edact-vscode.png -------------------------------------------------------------------------------- /deno_cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "errordactyl", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /vscode_extension/media/Thumbs-Up-Silhouette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/errordactyl/HEAD/vscode_extension/media/Thumbs-Up-Silhouette.png -------------------------------------------------------------------------------- /vscode_extension/src/data/api.ts: -------------------------------------------------------------------------------- 1 | import { endpoint } from '../../types'; 2 | 3 | export const MOCK_ROUTES: endpoint[] = [{method: "GET", path: '/books'}, {method: "POST", path: '/books/all'}] -------------------------------------------------------------------------------- /vscode_extension/README.md: -------------------------------------------------------------------------------- 1 | # VS Code Extension 2 | The VS Code extension is currently in early stages of development. To test the extension, you need to fork and clone this repository and then debug the project in VS Code with F5. 3 | 4 | Make sure to compile and build the project before you debug! 5 | -------------------------------------------------------------------------------- /vscode_extension/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import Sidebar from './containers/Sidebar'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); // createRoot(container!) if you use TypeScript 8 | root.render( 9 | 10 | ); -------------------------------------------------------------------------------- /vscode_extension/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "dbaeumer.vscode-eslint" 8 | ] 9 | } -------------------------------------------------------------------------------- /vscode_extension/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /vscode_extension/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | function getNonce() { 3 | let text = ''; 4 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | for (let i = 0; i < 32; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | return text; 9 | } 10 | 11 | export const Utils = { 12 | getNonce, 13 | }; -------------------------------------------------------------------------------- /vscode_extension/src/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { Memento } from "vscode"; 2 | 3 | export class LocalStorageWrapper { 4 | constructor(private storage: Memento) {} 5 | 6 | public getValue(key: string) : T|undefined { 7 | return this.storage.get(key); 8 | } 9 | 10 | public setValue(key: string, value: T){ 11 | this.storage.update(key, value); 12 | } 13 | } -------------------------------------------------------------------------------- /vscode_extension/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | const SIDEBAR_WEBVIEW_ID = 'edact.mainView'; 2 | 3 | const ELEMENT_IDS = { 4 | PARSE_BUTTON: 'parse-files' 5 | }; 6 | 7 | const DEFAULT_PATHS = { 8 | SERVER: "/server/server.js", 9 | ROUTES: "server/routes", 10 | PORT: 3000, 11 | } 12 | 13 | export const EXTENSION_CONSTANT = { 14 | SIDEBAR_WEBVIEW_ID, 15 | ELEMENT_IDS, 16 | DEFAULT_PATHS 17 | }; -------------------------------------------------------------------------------- /vscode_extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es6", "dom"], 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "strict": true, 9 | "jsx": "react-jsx", 10 | "baseUrl": "./src", 11 | "importHelpers": true 12 | }, 13 | "files": ["src/extension.ts", "src/utils/constant.ts", "src/providers/sidebar-provider.tsx"] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /vscode_extension/src/components/Response.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" 2 | 3 | interface ResponseProps { 4 | response?: string 5 | } 6 | 7 | // component to show response data for a particular endpoint 8 | export default function Response({ response }: ResponseProps) { 9 | // needs to display data returned from run routes action 10 | return ( 11 |
12 | 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /vscode_extension/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 15 | "preLaunchTask": "npm: watch" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /vscode_extension/src/containers/ConfigPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"; 3 | import Request from '../components/Request'; 4 | import Response from '../components/Response'; 5 | import { endpoint } from '../../types'; 6 | 7 | interface PanelProps { 8 | endpoint: endpoint 9 | } 10 | 11 | export default function Panel({ endpoint }: PanelProps) { 12 | 13 | 14 | return( 15 | 16 | Request 17 | Response 18 | 19 | 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /vscode_extension/src/providers/lib/filefinder.ts: -------------------------------------------------------------------------------- 1 | import { workspace, WorkspaceFolder, Uri, RelativePattern } from 'vscode'; 2 | const path = require('path'); 3 | 4 | // convert path for server folder into uri 5 | 6 | export default async function fileFinder(folder: WorkspaceFolder, routesPath: string) { 7 | // create pattern 8 | const pattern: RelativePattern = new RelativePattern(folder, routesPath + '/*.{js,ts}'); 9 | console.log('relativePattern', pattern); 10 | 11 | const foundFiles = await workspace.findFiles(pattern, '**/node_modules/**', 10); 12 | console.log('foundFiles', foundFiles); 13 | 14 | const relativePaths: Uri[] = []; 15 | 16 | foundFiles.forEach((uri: Uri) => { 17 | relativePaths.push(uri); 18 | }) 19 | 20 | // return array of file paths 21 | console.log('relativePaths', relativePaths) 22 | return relativePaths; 23 | } -------------------------------------------------------------------------------- /vscode_extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { EXTENSION_CONSTANT } from "./utils/constant"; 3 | import { SidebarWebview } from "./providers/sidebar-provider"; 4 | import { LocalStorageWrapper } from './utils/localStorage' 5 | 6 | export function activate(context: vscode.ExtensionContext) { 7 | 8 | let extStorage = new LocalStorageWrapper(context.globalState); 9 | let workspaceStorage = new LocalStorageWrapper(context.workspaceState); 10 | 11 | // Register view 12 | const SidebarWebViewProvider = new SidebarWebview(context?.extensionUri, extStorage, workspaceStorage); 13 | let view = vscode.window.registerWebviewViewProvider( 14 | EXTENSION_CONSTANT.SIDEBAR_WEBVIEW_ID, 15 | SidebarWebViewProvider 16 | ); 17 | context.subscriptions.push(view); 18 | }; 19 | 20 | // this method is called when your extension is deactivated 21 | export function deactivate() {} -------------------------------------------------------------------------------- /deno_cli/edact.ts: -------------------------------------------------------------------------------- 1 | import { parse as parser } from 'https://deno.land/std@0.166.0/flags/mod.ts'; 2 | import { parse } from './parse/parse.ts'; 3 | import { init } from './init.ts'; 4 | import Test from './test/class.ts' 5 | 6 | const args = parser(Deno.args); 7 | let body = ''; 8 | 9 | const test = new Test(); 10 | 11 | //handle arguments 12 | switch (args._[0]) { 13 | case 'init': 14 | init(); 15 | break; 16 | case undefined: 17 | if (args.b||args.body) body += (args.b||args.body); 18 | if (args.p||args.post) test.testOne('POST', args.p||args.post, body); 19 | else if (args.u||args.patch||args.put) test.testOne(args.patch?'PATCH':'PUT', args.u||args.patch||args.put, body); 20 | else if (args.g||args.get) test.testOne('GET', args.g||args.get); 21 | else if (args.d||args.delete) test.testOne('DELETE', args.d||args.delete); 22 | else if (args.f) parse(); 23 | else test.testAll(); 24 | break; 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /vscode_extension/src/components/Request.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" 2 | import { useState } from "react"; 3 | import { Body, Parameters } from "./RequestComponents"; 4 | 5 | export default function Request() { 6 | 7 | const [current, setCurrent] = useState("Body") 8 | 9 | const handleSelect = (event:any) => { 10 | console.log(event.target.value) 11 | setCurrent(event.target.value) 12 | } 13 | 14 | const renderCurrent = (current:string) => { 15 | switch (current) { 16 | case 'Body': 17 | return 18 | case 'Parameters': 19 | return 20 | break; 21 | } 22 | } 23 | 24 | return ( 25 |
26 | handleSelect(event)}> 27 | Body 28 | Parameters 29 | Authentication 30 | Headers 31 | 32 | {renderCurrent(current)} 33 |
34 | ) 35 | } -------------------------------------------------------------------------------- /vscode_extension/src/components/RequestComponents.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | import { VSCodeTextArea, VSCodeTextField, VSCodeButton } from '@vscode/webview-ui-toolkit/react' 3 | 4 | interface ConfigProps { 5 | path: string, 6 | body?: Record 7 | } 8 | 9 | // config box for endpoint components 10 | export const Body = ({ path, body }: ConfigProps ) => { 11 | // editable text box for options: body, endpoint url 12 | // pass data back to parent Endpoint component via props 13 | 14 | // format body 15 | let bodyData = ''; 16 | for (const key in body) { 17 | bodyData += `${key}: ${body[key]}\n`; 18 | } 19 | 20 | return ( 21 |
22 | Request Body 23 | Save 24 |
25 | ) 26 | } 27 | 28 | export const Parameters = () => { 29 | return ( 30 |
31 | Parameters 32 | Save 33 |
34 | ) 35 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Errordactyl](https://errordactyl.com/) 2 | 3 | Errordactyl is a tool that automates HTTP endpoint testing and error handling for web servers in the Node.js and Deno runtime environment. 4 | 5 | * **Simple:** Errordactyl utilizes a configuration file to store all route endpoints and their associated HTTP methods, making it easy to access or modify custom routes. 6 | * **Easy-to-run:** Errordactyl encapsulates its functionality into minimalist command line interface commands, allowing for a painless setup and execution. 7 | * **Readable:** Compiled error data gets returned from the error stream as a JSON object with elegant formatting to ensure error readability. 8 | 9 | # Installation 10 | * [Deno CLI Tool](https://github.com/oslabs-beta/errordactyl/tree/main/deno_cli/) 11 | * [VS Code Extension](https://github.com/oslabs-beta/errordactyl/tree/main/vscode_extension/) 12 | 13 | 14 | ## Contributing 15 | 16 | The main purpose of this repository is to provide a general overview of the architecture of the application. Development of the tool is ongoing and we are open to any contributions that may be provided from curious onlookers and users. 17 | -------------------------------------------------------------------------------- /vscode_extension/src/components/RunButtons.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; 2 | import { MdArrowForwardIos, MdDoubleArrow } from 'react-icons/md'; 3 | import { endpoint } from '../../types'; 4 | import { VSCodeAPI } from '../utils/vsCodeApi'; 5 | 6 | interface RunBtnProps { 7 | selectedRoutes: endpoint[] 8 | } 9 | 10 | // buttons to run either batch endpoints or selected 11 | export default function RunButtons({selectedRoutes}:RunBtnProps) { 12 | 13 | const handleSelectClick = () => { 14 | console.log("selected Routes", selectedRoutes); 15 | } 16 | 17 | const handleRunAllClick = () => { 18 | VSCodeAPI.postMessage({action: 'test-routes'}); 19 | } 20 | 21 | return ( 22 |
23 | handleSelectClick()}> 24 | 25 | Test selected routes 26 | 27 | handleRunAllClick()}> 28 | 29 | Test all routes 30 | 31 |
32 | 33 | ) 34 | } -------------------------------------------------------------------------------- /vscode_extension/src/utils/vsCodeApi.ts: -------------------------------------------------------------------------------- 1 | // interface for vscode class instance 2 | interface vscode { 3 | postMessage(message: any): void; 4 | getState(): any; 5 | setState(newState: any): any 6 | } 7 | 8 | declare const vscode: vscode; 9 | 10 | class VSCodeWrapper { 11 | private readonly vscodeApi: vscode = vscode; 12 | 13 | /** 14 | * Send message to the extension framework. 15 | * @param message 16 | */ 17 | public postMessage(message: any): void { 18 | this.vscodeApi.postMessage(message); 19 | } 20 | 21 | /** 22 | * Add listener for messages from extension framework. 23 | * @param callback called when the extension sends a message 24 | * @returns function to clean up the message eventListener. 25 | */ 26 | public onMessage(callback: (message: any) => void): () => void { 27 | window.addEventListener('message', callback); 28 | return () => window.removeEventListener('message', callback); 29 | } 30 | 31 | public getState(): any { 32 | return this.vscodeApi.getState(); 33 | } 34 | 35 | public setState(newState: any): void { 36 | this.vscodeApi.setState(newState); 37 | } 38 | } 39 | 40 | export const VSCodeAPI: VSCodeWrapper = new VSCodeWrapper(); 41 | 42 | -------------------------------------------------------------------------------- /vscode_extension/src/components/ParseButton.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from 'react'; 2 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; 3 | import { VSCodeAPI } from '../utils/vsCodeApi'; 4 | import { endpoint } from '../../types'; 5 | import { VscSearch } from 'react-icons/vsc'; 6 | import { RxReset } from 'react-icons/rx'; 7 | 8 | interface ParseButtonProps { 9 | setConfigInit: Dispatch>, 10 | setRoutes: Dispatch>> 11 | } 12 | 13 | export default function ParseButton({ setConfigInit, setRoutes }: ParseButtonProps) { 14 | 15 | const testHandler = () => { 16 | VSCodeAPI.postMessage({ action: "parse" }); 17 | } 18 | 19 | const resetHandler = () => { 20 | setConfigInit(false); 21 | setRoutes([]); 22 | VSCodeAPI.postMessage({action: "reset"}); 23 | } 24 | 25 | return ( 26 |
27 | 28 | 29 | Search for routes 30 | 31 | 32 | 33 | Reset Extension 34 | 35 |
36 | ) 37 | } -------------------------------------------------------------------------------- /vscode_extension/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: "development", 5 | target: 'node', 6 | entry: './src/index.js', 7 | output: { 8 | path: path.resolve(__dirname, "dist"), 9 | filename: 'bundle.js', 10 | }, 11 | externals: { 12 | vscode: 'commonjs vscode' 13 | }, 14 | resolve: { 15 | extensions: ['.ts', '.js', '.tsx'], 16 | alias: { 17 | utils: path.resolve(__dirname, './src/utils.ts'), 18 | constant: path.resolve(__dirname, './src/constant.ts'), 19 | providers: path.resolve(__dirname, './src/providers/'), 20 | components: path.resolve(__dirname, './src/components/'), 21 | containers: path.resolve(__dirname, './src/containers/'), 22 | }, 23 | }, 24 | plugins: [], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(jsx|js)$/, 29 | exclude: /node_modules/, 30 | use: { 31 | loader: 'babel-loader', 32 | options: { 33 | presets: ['@babel/preset-env', '@babel/preset-react'], 34 | }, 35 | }, 36 | }, 37 | { 38 | test: /\.(ts|tsx)$/, 39 | exclude: /node_modules/, 40 | use: [{loader: 'ts-loader'}] 41 | }, 42 | { 43 | test: /\.css$/, 44 | exclude: /node_modules/, 45 | use: ['style-loader', 'css-loader'] 46 | } 47 | ] 48 | } 49 | 50 | }; -------------------------------------------------------------------------------- /vscode_extension/src/providers/lib/parse.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises'); 2 | import { window, Uri, WorkspaceFolder } from "vscode"; 3 | 4 | import fileFinder from './filefinder'; 5 | import { detectEndpoints } from './detect_endpoints'; 6 | import { endpoint, config } from '../../../types'; 7 | 8 | export async function parse(config: config, folder: WorkspaceFolder) { 9 | 10 | console.log("config", config); 11 | // pull down any file paths from config object retrieved from state 12 | let files: Uri[] | undefined = await fileFinder(folder, config.routesPath); 13 | // console.log("files", files); 14 | 15 | if (files) config.filePaths = files; 16 | 17 | // new approach 18 | const routes: endpoint[] | undefined = await detectEndpoints(files) 19 | 20 | // const routes: endpoint[] = []; 21 | // use filePaths array to grab routes 22 | // iterate over config.filePaths 23 | // files?.forEach((file: Uri) => { 24 | // adds to array of endpoints as each file is read 25 | // detectEndpoints(file, routes); 26 | // }) 27 | // TODO: need to iterate over files array and execute detect SYNCHRONOUSLY 28 | 29 | window.showInformationMessage('Successfully detected server routes\nPlease input request data for applicable endpoints'); // fs.writeFile 30 | 31 | return routes; 32 | // could ask user to either manually enter in config file, or enter in command line? 33 | } -------------------------------------------------------------------------------- /vscode_extension/src/containers/Initalize.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"; 3 | import { VSCodeAPI } from "../utils/vsCodeApi"; 4 | import { EXTENSION_CONSTANT } from "../utils/constant"; 5 | import { config } from "../../types"; 6 | 7 | export default function Initalize() { 8 | const [serverPath, setServerPath] = useState(EXTENSION_CONSTANT.DEFAULT_PATHS.SERVER); 9 | const [routesPath, setRoutesPath] = useState(EXTENSION_CONSTANT.DEFAULT_PATHS.ROUTES); 10 | const [port, setPort] = useState(EXTENSION_CONSTANT.DEFAULT_PATHS.PORT) 11 | 12 | 13 | const initialize = (): void => { 14 | let config: config = { 15 | serverPath: serverPath, 16 | routesPath: routesPath, 17 | PORT: port 18 | } 19 | 20 | VSCodeAPI.postMessage({action: 'set-config', data: config}) 21 | } 22 | 23 | return ( 24 |
25 | setServerPath(e.target.value)}>Server File Path: 26 | setRoutesPath(e.target.value)}>Routes Directory Path: 27 | setPort(e.target.value)}>Port: 28 | initialize()}>Initialize Workspace 29 |
30 | ) 31 | }; -------------------------------------------------------------------------------- /deno_cli/init.ts: -------------------------------------------------------------------------------- 1 | export async function init() { 2 | // store user input for config file 3 | let path: string | null = './server/routes'; 4 | let files: string[] | undefined = []; 5 | let pathToServer: string | null = './server/server.ts'; 6 | 7 | const useDefaultServerPath = confirm('Do you want to use the default server file location of ./server/server.ts?'); 8 | 9 | if (!useDefaultServerPath) { 10 | pathToServer = prompt('Please enter the path to your server file:'); 11 | } 12 | 13 | const useDefaultRouteLocation = confirm('Do you want to use the default route location of ./server/routes?'); 14 | // if y then continue to next questions 15 | 16 | // else ask user to input routes 17 | if (!useDefaultRouteLocation) { 18 | path = prompt('Please enter the directory path where your routes are stored:'); 19 | // const confirmation = confirm('Route directory path: ' + path); 20 | // if (!confirmation) 21 | } 22 | 23 | const testAllFiles = confirm('Do you want to test all routes found in this directory?'); 24 | 25 | if (!testAllFiles) { 26 | const userInput = prompt('Please enter all relative file paths that you would like to test (separated by spaces):'); 27 | files = userInput?.split(' '); 28 | } 29 | 30 | const config = { 31 | serverPath: pathToServer, 32 | routesPath: path, 33 | filePaths: files, 34 | } 35 | 36 | await Deno.mkdir('./_errordactyl'); 37 | 38 | await Deno.writeTextFile('./_errordactyl/config.json', JSON.stringify(config)); 39 | console.log('Successfully generated initial config in ./_errordactyl/config.json') 40 | } -------------------------------------------------------------------------------- /deno_cli/types.ts: -------------------------------------------------------------------------------- 1 | export interface endpoint { 2 | path: string, 3 | // params?: RegExpMatchArray | null, 4 | body?: Record, 5 | headers?: Record, 6 | response?: string 7 | } 8 | 9 | export interface methodCacheExtended { 10 | GET: endpoint[], 11 | POST: endpoint[], 12 | PUT: endpoint[], 13 | PATCH: endpoint[], 14 | DELETE: endpoint[] 15 | } 16 | 17 | export interface methodCache { 18 | get: Array; 19 | post: Array; 20 | put: Array; 21 | delete: Array; 22 | patch: Array; 23 | } 24 | 25 | interface reqObjType { 26 | url: string, 27 | method: string, 28 | hasBody: boolean, 29 | } 30 | 31 | interface resObjType { 32 | status: string, 33 | type: string | undefined, 34 | hasBody: boolean, 35 | writable: boolean, 36 | } 37 | 38 | export interface strObjType { 39 | message: string, 40 | request: string, 41 | response: string, 42 | location: string | undefined, 43 | lineNo: string | undefined, 44 | colNo: string | undefined, 45 | } 46 | 47 | export interface finalObjType { 48 | message: string, 49 | request: reqObjType, 50 | response: resObjType, 51 | location: string | undefined, 52 | lineNo: string | undefined, 53 | colNo: string | undefined, 54 | } 55 | export interface testMethods { 56 | testAll: () => void, 57 | testOne: (method:string, endpoint:string, body?:string) => void 58 | } 59 | 60 | export interface config { 61 | serverPath: string, 62 | routesPath: string, 63 | filePaths: string[], 64 | endpoints: methodCacheExtended 65 | PORT?: number 66 | } 67 | -------------------------------------------------------------------------------- /vscode_extension/src/containers/Routes.tsx: -------------------------------------------------------------------------------- 1 | // container to hold all endpoint components 2 | // have to pass in array of endpoint objects 3 | import { Dispatch, SetStateAction } from 'react'; 4 | import { endpoint } from '../../types'; 5 | import Endpoint from '../components/Endpoint'; 6 | import ParseButton from '../components/ParseButton'; 7 | import RunButtons from '../components/RunButtons'; 8 | import { VSCodeDivider } from '@vscode/webview-ui-toolkit/react'; 9 | import { VscSearch } from 'react-icons/vsc'; 10 | 11 | interface RoutesProps { 12 | setConfigInit: Dispatch>, 13 | setSelected: Dispatch>>, 14 | selectedRoutes: endpoint[], 15 | setRoutes: Dispatch>>, 16 | endpoints: endpoint[] 17 | } 18 | 19 | export default function Routes({ setConfigInit, setSelected, selectedRoutes, setRoutes, endpoints}: RoutesProps) { 20 | // render array of endpoint components 21 | const routes = endpoints.map((endpoint, i) => { 22 | return ( 23 | 24 | ) 25 | }) 26 | return ( 27 |
28 |
29 |

Routes

30 | 31 |
32 | 33 |
34 | {routes} 35 | {routes.length? :
Click to search for routes!
} 36 |
37 |
38 | ) 39 | } -------------------------------------------------------------------------------- /deno_cli/parse/find_files.ts: -------------------------------------------------------------------------------- 1 | // read config file and store any user overrides 2 | // const ROUTES = config.routes || './routes' 3 | 4 | // default behavior of app, traverse routes folder looking for endpoints 5 | // Deno.readDir 6 | // need to check each DirEntry to see if it is itself a directory 7 | // error handling for this step: check if provided path is a STRING => is a folder or a file in the directory (Deno.errors.NotFound) => does the folder contain routes 8 | 9 | const findFiles = async (path: string) => { 10 | try { 11 | // check if path is a folder 12 | const fileInfo = await Deno.stat(path); 13 | if (fileInfo.isDirectory) { 14 | // console.log('this is a directory'); 15 | } else { 16 | console.log('must provide a valid directory'); 17 | return; 18 | } 19 | 20 | const files: string[] = []; 21 | 22 | const readDirs = async (folder: string) => { 23 | for await (const dirEntry of Deno.readDir(folder)) { 24 | const entryPath = `${folder}/${dirEntry.name}`; 25 | // if we get to a directory call readDirs on it 26 | if (dirEntry.isDirectory) { 27 | await readDirs(entryPath); 28 | 29 | } else { 30 | files.push(entryPath); 31 | } 32 | } 33 | return; 34 | } 35 | 36 | await readDirs(path); 37 | // console.log('findFiles', files) 38 | return files; 39 | 40 | } catch (e) { 41 | // handle different errors here 42 | if (e instanceof Deno.errors.NotFound) console.log('threw NotFound'); 43 | } 44 | }; 45 | 46 | 47 | export default findFiles; 48 | -------------------------------------------------------------------------------- /vscode_extension/src/containers/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Routes from './Routes'; 2 | import Initialize from './Initalize'; 3 | import { endpoint } from '../../types'; 4 | import { useState, useEffect } from 'react'; 5 | import { VSCodeAPI } from '../utils/vsCodeApi'; 6 | 7 | 8 | // main app to display in the sidebar provider html 9 | export default function SideBar() { 10 | const [routes, setRoutes] = useState([]); 11 | const [selected, setSelected] = useState([]); 12 | const [configInit, setConfigInit] = useState(false); 13 | 14 | // function to retrieve endpoints from extension storage 15 | const getRoutes = () => { 16 | VSCodeAPI.postMessage({action: 'get-initial-state'}); 17 | }; 18 | // function to send requests 19 | const runRoutes = () => { 20 | 21 | } 22 | 23 | //useEffects to listener 24 | useEffect(() => { 25 | VSCodeAPI.onMessage((event) => { 26 | console.log('message heard'); 27 | const message = event.data; 28 | 29 | switch (message.action) { 30 | case 'parse': 31 | console.log('message received by the sidebar'); 32 | // message should come back with routes array 33 | setRoutes(message.data); 34 | break; 35 | case 'config': 36 | message.data? setRoutes(message.data) : null; 37 | setConfigInit(true); 38 | } 39 | }); 40 | // retrieve initial state 41 | getRoutes(); 42 | }, []) 43 | 44 | return ( 45 |
46 | {configInit? routes && : } 47 |
48 | ) 49 | } -------------------------------------------------------------------------------- /vscode_extension/types.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | 3 | export type methodType = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" 4 | 5 | export interface endpoint { 6 | method: methodType, 7 | path: string, 8 | // params?: RegExpMatchArray | null, 9 | body?: Record, 10 | headers?: Record, 11 | response?: string 12 | } 13 | 14 | export interface methodCacheExtended { 15 | "GET": endpoint[], 16 | "POST": endpoint[], 17 | "PUT": endpoint[], 18 | "PATCH": endpoint[], 19 | "DELETE": endpoint[] 20 | } 21 | 22 | export interface methodCache { 23 | get: Array; 24 | post: Array; 25 | put: Array; 26 | delete: Array; 27 | patch: Array; 28 | } 29 | 30 | interface reqObjType { 31 | url: string, 32 | method: string, 33 | hasBody: boolean, 34 | } 35 | 36 | interface resObjType { 37 | status: string, 38 | type: string | undefined, 39 | hasBody: boolean, 40 | writable: boolean, 41 | } 42 | 43 | export interface strObjType { 44 | message: string, 45 | request: string, 46 | response: string, 47 | location: string | undefined, 48 | lineNo: string | undefined, 49 | colNo: string | undefined, 50 | } 51 | 52 | export interface finalObjType { 53 | message: string, 54 | request: reqObjType, 55 | response: resObjType, 56 | location: string | undefined, 57 | lineNo: string | undefined, 58 | colNo: string | undefined, 59 | } 60 | export interface testMethods { 61 | testAll: () => void, 62 | testOne: (method:string, endpoint:string, body?:string) => void 63 | } 64 | 65 | export interface config { 66 | serverPath: string, 67 | routesPath: string, 68 | filePaths?: Uri[], 69 | endpoints?: endpoint[], 70 | PORT?: number 71 | } 72 | -------------------------------------------------------------------------------- /vscode_extension/src/components/Endpoint.tsx: -------------------------------------------------------------------------------- 1 | // component containing information and config for a specific endpoint 2 | import { endpoint } from '../../types'; 3 | import { VSCodeButton, VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; 4 | import { Dispatch, SetStateAction, useState } from 'react'; 5 | import Panel from '../containers/ConfigPanel'; 6 | 7 | interface EndpointProps { 8 | setSelected: Dispatch>>, 9 | selectedRoutes: endpoint[], 10 | endpoint: endpoint 11 | }; 12 | 13 | export default function Endpoint({ setSelected, selectedRoutes, endpoint }: EndpointProps) { 14 | const [panel, showPanel] = useState(false); 15 | 16 | //handle checked endpoints 17 | const handleChange = (event:any) => { 18 | //if checked, add to selected array 19 | if (event.target.checked) { 20 | setSelected(state => [...state, endpoint]); 21 | } 22 | //if unchecked, remove endpoint from selected array 23 | else { 24 | selectedRoutes.forEach((route, i) => { 25 | if (route === endpoint) { 26 | const firstHalf = selectedRoutes.slice(0,i+1); 27 | const secondHalf = selectedRoutes.slice(i+1); 28 | firstHalf.pop(); 29 | const newSelected = firstHalf.concat(secondHalf); 30 | setSelected(newSelected); 31 | } 32 | }); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 |
39 | handleChange(event)}>{endpoint.method}: {endpoint.path} 40 | (showPanel(!panel))}>+ 41 |
42 | {panel? : null} 43 |
44 | ); 45 | } -------------------------------------------------------------------------------- /vscode_extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode_extension", 3 | "version": "1.0.0", 4 | "engines": { 5 | "vscode": "^1.74.0" 6 | }, 7 | "description": "vscode extension version of errordactyl", 8 | "activationEvents": [ 9 | "onView:edact.mainView" 10 | ], 11 | "main": "./dist/src/extension.js", 12 | "contributes": { 13 | "viewsContainers": { 14 | "activitybar": [ 15 | { 16 | "id": "edact-sidebar", 17 | "title": "Errordactyl", 18 | "icon": "./media/edact-vscode.png" 19 | } 20 | ] 21 | }, 22 | "views": { 23 | "edact-sidebar": [ 24 | { 25 | "type": "webview", 26 | "id": "edact.mainView", 27 | "name": "Errordactyl" 28 | } 29 | ] 30 | } 31 | }, 32 | "scripts": { 33 | "test": "echo \"Error: no test specified\" && exit 1", 34 | "compile": "tsc -p ./", 35 | "build": "webpack", 36 | "watch": "tsc -w -p ./" 37 | }, 38 | "author": "danger noodles", 39 | "license": "ISC", 40 | "devDependencies": { 41 | "@babel/core": "^7.20.12", 42 | "@babel/preset-env": "^7.20.2", 43 | "@babel/preset-react": "^7.18.6", 44 | "@types/node": "^18.13.0", 45 | "@types/vscode": "^1.74.0", 46 | "@types/vscode-webview": "^1.57.1", 47 | "@vscode/webview-ui-toolkit": "^1.2.1", 48 | "babel-loader": "^9.1.2", 49 | "ts-loader": "^9.4.2", 50 | "typescript": "^4.9.4", 51 | "webpack": "^5.75.0", 52 | "webpack-cli": "^5.0.1" 53 | }, 54 | "dependencies": { 55 | "@types/react-dom": "^18.0.10", 56 | "module-alias": "^2.2.2", 57 | "react": "^18.2.0", 58 | "react-dom": "^18.2.0", 59 | "react-icons": "^4.7.1" 60 | }, 61 | "_moduleAliases": { 62 | "utils": "dist/utils", 63 | "providers": "dist/providers", 64 | "components": "dist/components", 65 | "constant": "dist/constant.js" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /deno_cli/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Deno Installation 3 | 4 | Fork and clone the repository and install the errordactyl (edact) script into Deno's global bin. 5 | * **Note:** application permissions are still being scoped and optimized. 6 | 7 | ```sh 8 | deno install -A 'path To edact.ts' 9 | ``` 10 | we are working on publishing to Deno Land so that installation can be more streamlined! 11 | 12 | ## Configuration 13 | 14 | Suppose we have following example of a route built using Oak.js listening for requests from port 3000. An exception is thrown on purpose in order to test the error handling functionality of the tool. 15 | 16 | ```ts 17 | import { Router } from 'https://deno.land/x/oak@v11.1.0/mod.ts'; 18 | 19 | const router = new Router(); 20 | router.get('/', (ctx) => { 21 | ctx.response.body = "Get Request"; 22 | throw new EvalError; 23 | }); 24 | ``` 25 | 26 | Initialize errordactyl in the project that you are currently testing with `edact init` and complete the inital prompts to create a config file in `projectRoot/_errordactyl/` 27 | 28 | ```sh 29 | edact init 30 | ``` 31 | 32 | After the initial setup is complete, compile your executable bash script by running `edact -f` to populate the file with all of the endpoint routes. 33 | 34 | ```sh 35 | edact -f 36 | ``` 37 | 38 | Edit the request body, parameters, and headers in `/_errordactyl/config.json` and simply run `edact` to invoke the generated shell script, testing all of the detected endpoints from the configuration file. 39 | 40 | ```sh 41 | edact 42 | ``` 43 | 44 | The generated output from the CLI would be: 45 | 46 | ```javascript 47 | [ 48 | { 49 | message: "[uncaught application error]: Error - ", 50 | request: { url: "http://localhost:3000/", method: "GET", hasBody: false }, 51 | response: { status: 200, type: undefined, hasBody: true, writable: true }, 52 | location: "/Users/Ernietheerrordactyl/Documents/test/server/routes/router.ts", 53 | lineNo: 7, 54 | colNo: 11 55 | } 56 | ] 57 | ``` 58 | 59 | The output error data is returned as a JSON object, writing all of the error stack trace data into a readable format. 60 | -------------------------------------------------------------------------------- /vscode_extension/script/sidebar-webview.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100vh; 3 | width: 100vw; 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | /* background-color: green; */ 8 | } 9 | 10 | #root, .sidebar { 11 | /* background-color: blue; */ 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | .sidebar { 17 | } 18 | 19 | .routes-header { 20 | display:flex; 21 | flex-direction: row; 22 | align-items: center; 23 | justify-content: space-between; 24 | padding-left: 1em; 25 | } 26 | 27 | #routes-wrapper { 28 | display:flex; 29 | flex-direction: column; 30 | } 31 | 32 | #parse-button { 33 | height: 50%; 34 | background-color: transparent; 35 | } 36 | 37 | #parse-button:hover #parse-tooltip { 38 | visibility: visible; 39 | } 40 | 41 | .endpoint-label { 42 | display:flex; 43 | flex-direction: row; 44 | align-items: center; 45 | padding-left: 1em; 46 | padding-bottom: 0.5em; 47 | } 48 | 49 | #show-panel-button { 50 | background-color: transparent; 51 | } 52 | 53 | .panel { 54 | height: fit-content; 55 | padding-left: 1em; 56 | } 57 | 58 | #view-1 { 59 | display:flex; 60 | flex-direction: column; 61 | visibility: visible; 62 | } 63 | 64 | #run-button { 65 | width: 3em; 66 | 67 | } 68 | 69 | #run-button:hover #tooltip { 70 | visibility: visible; 71 | } 72 | 73 | #run-btn-wrapper { 74 | display:flex; 75 | column-gap: 5%; 76 | padding-left: 1em; 77 | padding-top: 0.5em; 78 | } 79 | 80 | #tooltip { 81 | visibility: hidden; 82 | width: 120px; 83 | background-color: transparent; 84 | color: #fff; 85 | text-align: left; 86 | border-radius: 6px; 87 | padding-left: 1em; 88 | 89 | /* Position the tooltip */ 90 | position: absolute; 91 | z-index: 1; 92 | top: 5%; 93 | left: 0; 94 | } 95 | 96 | #parse-tooltip { 97 | visibility: hidden; 98 | width: fit-content; 99 | background-color: transparent; 100 | color: #fff; 101 | text-align: left; 102 | border-radius: 6px; 103 | padding-left: 1em; 104 | 105 | /* Position the tooltip */ 106 | position: absolute; 107 | z-index: 1; 108 | left: 0; 109 | top: 5%; 110 | } 111 | 112 | #search-message { 113 | position: absolute; 114 | left: 5%; 115 | top: 10%; 116 | } -------------------------------------------------------------------------------- /deno_cli/parse/parse.ts: -------------------------------------------------------------------------------- 1 | import findFiles from './find_files.ts'; 2 | import { detectEndpoints, detectEndpointsWithParams } from './detect_endpoints.ts'; 3 | import { methodCache, methodCacheExtended } from '../types.ts'; 4 | // check if user wants to overwrite file paths in json file 5 | 6 | const methods: methodCache = { 7 | get: [], 8 | post: [], 9 | put: [], 10 | delete: [], 11 | patch: [] 12 | } 13 | 14 | const methodsExtended: methodCacheExtended = { 15 | GET: [], 16 | POST: [], 17 | PUT: [], 18 | DELETE: [], 19 | PATCH: [] 20 | } 21 | 22 | export async function parse() { 23 | let config = await Deno.readTextFile('./_errordactyl/config.json').then(response => JSON.parse(response)); 24 | let files: string[] | undefined = config.filePaths; 25 | 26 | // check if filePaths is empty, if not ask user if they want to overwrite the paths listed there 27 | 28 | if (files && files.length > 0) { 29 | const overwritePaths = confirm('Do you want to overwrite file paths in config.json?'); 30 | 31 | if (overwritePaths) { 32 | // find files in routes folder declared in config file 33 | const path = config.routesPath; 34 | files = await findFiles(path); 35 | // console.log(files); 36 | // updates array of file names in config 37 | config.filePaths = files; 38 | await Deno.writeTextFile('./_errordactyl/config.json', JSON.stringify(config)); 39 | } 40 | 41 | } else { 42 | // find files in routes folder declared in config file 43 | const path = config.routesPath; 44 | files = await findFiles(path); 45 | console.log('files',files); 46 | // updates array of file names in config 47 | config.filePaths = files; 48 | await Deno.writeTextFile('./_errordactyl/config.json', JSON.stringify(config)); 49 | } 50 | 51 | // use filePaths array to grab routes 52 | // iterate over config.filePaths 53 | files?.map((file: string) => { 54 | // adds to single methodCache as each file is read 55 | detectEndpointsWithParams(file, methodsExtended); 56 | }) 57 | 58 | // print methods to config 59 | config = await Deno.readTextFile('./_errordactyl/config.json').then(response => JSON.parse(response)); 60 | config.endpoints = methodsExtended; 61 | await Deno.writeTextFile('./_errordactyl/config.json', JSON.stringify(config)); 62 | console.log('Successfully detected server routes; wrote to errordactyl config\nPlease input request data for applicable endpoints'); 63 | 64 | // could ask user to either manually enter in config file, or enter in command line? 65 | } -------------------------------------------------------------------------------- /deno_cli/parse/detect_endpoints.ts: -------------------------------------------------------------------------------- 1 | import { methodCache, methodCacheExtended, endpoint } from '../types.ts'; 2 | 3 | 4 | // // better time complexity option 5 | export const detectEndpoints = async (path: string, methods: methodCache) => { 6 | let file = await Deno.readTextFile(path); 7 | // strip comments from file 8 | file = file.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); 9 | 10 | for (const method in methods) { 11 | const regex = new RegExp(`\\.${method}\\("(\\S)*"`, 'g'); 12 | const endpoints: RegExpMatchArray|null = file.match(regex); 13 | if (endpoints) { 14 | for (const match of endpoints) { 15 | methods[method as keyof typeof methods].push(match.replace(`.${method}(`, '').replaceAll('"', '')); 16 | } 17 | } 18 | } 19 | 20 | } 21 | 22 | export const detectEndpointsWithParams = async (path: string, methods: methodCacheExtended) => { 23 | let file = await Deno.readTextFile(path); 24 | // strip comments from file 25 | file = file.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); 26 | // iterate over methods objects and match method in file 27 | for (const method in methods) { 28 | const regex = new RegExp(`\\.${method}\\("(\\S)*"`, 'gi'); 29 | const endpoints: RegExpMatchArray|null = file.match(regex); 30 | if (endpoints) { 31 | for (const match of endpoints) { 32 | const path: string = match.replace(`.${method.toLowerCase()}(`, '').replaceAll('"', ''); 33 | // // check for params and create params array 34 | // const params: RegExpMatchArray|null = path.match(/:(\S)*/g); 35 | // const endpoint = params ? {path, params} : {path}; 36 | let endpoint: endpoint = {path}; 37 | if (method != 'GET' && method != 'DELETE') { 38 | endpoint = {path, body: {}} 39 | } 40 | methods[method as keyof typeof methods].push(endpoint); 41 | } 42 | } 43 | } 44 | } 45 | 46 | // { 47 | // get: ['/endpoint'], 48 | // get: [{ 49 | // endpoint: '/endpoint', 50 | // params: [] <-- only create if params are detected 51 | // }], 52 | // post: [ 53 | // { 54 | // endpoint: '/endpoint', 55 | // params: [], 56 | // body: {} 57 | // } 58 | // ] 59 | // } 60 | 61 | // better space complexity option 62 | // import { readLines } from 'https://deno.land/std@0.78.0/io/mod.ts'; 63 | 64 | // const spaceDetect = async (file: string) => { 65 | // const stream = await Deno.open(file); 66 | // for await (const line of readLines(stream)) { 67 | // for (const method in methods) { 68 | // const regex = new RegExp(`(?<=\\.${method}\\()\\'(.*)\\'`, 'g'); 69 | // const found = line.match(regex); 70 | // if (found) { 71 | // const path = found[0].substring(1, found[0].length - 1); 72 | // methods[method as keyof typeof methods].push(path); 73 | // } 74 | // } 75 | // } 76 | // } 77 | 78 | -------------------------------------------------------------------------------- /vscode_extension/src/providers/lib/detect_endpoints.ts: -------------------------------------------------------------------------------- 1 | import { endpoint, methodType } from '../../../types'; 2 | import { workspace, Uri } from 'vscode'; 3 | 4 | export const detectEndpoints = async (files: Uri[] | undefined) => { 5 | if (files !== undefined) { 6 | let routes: endpoint[] = []; 7 | for (const uri of files) { 8 | // read file from passed in route 9 | let temp = await workspace.fs.readFile(uri); // replaced with Node's fs.readFile 10 | let file = temp.toString(); 11 | // console.log('read file', file); 12 | // strip comments from file 13 | file = file.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); 14 | // iterate over methods dictionary and match method in file 15 | const methods: methodType[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; 16 | 17 | for (const method of methods) { 18 | const regex = new RegExp(`\\.${method}\\(('|")(\\S)*('|")`, 'gi'); 19 | const endpoints: RegExpMatchArray|null = file.match(regex); 20 | 21 | if (endpoints) { 22 | for (const match of endpoints) { 23 | // format discovered route 24 | const path: string = match.replace(`.${method.toLowerCase()}(`, '').replace(/"|'/g, ''); 25 | // create endpoint object 26 | let endpoint: endpoint = {method, path}; 27 | // add body to appropriate route types 28 | if (method != 'GET' && method != 'DELETE') { 29 | endpoint = {method, path, body: {}} 30 | } 31 | // add endpoint object to routes array 32 | routes.push(endpoint); 33 | } 34 | } 35 | } 36 | } 37 | return routes; 38 | } 39 | } 40 | 41 | // export const detectEndpoints = async (path: Uri, routes: endpoint[]) => { 42 | // // read file from passed in route 43 | // let temp = await workspace.fs.readFile(path); // replaced with Node's fs.readFile 44 | // let file = temp.toString(); 45 | // // console.log('read file', file); 46 | // // strip comments from file 47 | // file = file.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); 48 | // // iterate over methods dictionary and match method in file 49 | // const methods: methodType[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; 50 | 51 | // for (const method of methods) { 52 | // const regex = new RegExp(`\\.${method}\\(('|")(\\S)*('|")`, 'gi'); 53 | // const endpoints: RegExpMatchArray|null = file.match(regex); 54 | 55 | // if (endpoints) { 56 | // for (const match of endpoints) { 57 | // // format discovered route 58 | // const path: string = match.replace(`.${method.toLowerCase()}(`, '').replace(/"|'/g, ''); 59 | // // create endpoint object 60 | // let endpoint: endpoint = {method, path}; 61 | // // add body to appropriate route types 62 | // if (method != 'GET' && method != 'DELETE') { 63 | // endpoint = {method, path, body: {}} 64 | // } 65 | // // add endpoint object to routes array 66 | // routes.push(endpoint); 67 | // } 68 | // } 69 | // } 70 | // console.log("routes", routes); 71 | // } -------------------------------------------------------------------------------- /vscode_extension/src/providers/lib/test.ts: -------------------------------------------------------------------------------- 1 | import { endpoint, methodType } from '../../../types'; 2 | import { window, workspace, Uri } from 'vscode'; 3 | const { spawn, spawnSync, execSync } = require('node:child_process'); 4 | const util = require('node:util'); 5 | const exec = util.promisify(require('node:child_process').exec); 6 | 7 | export const test = async (endpoints: endpoint[], port: string, directory: string) => { 8 | return new Promise((resolve, reject) => { 9 | let pathToWorkspace: string = ''; 10 | if (workspace.workspaceFolders) { 11 | pathToWorkspace = workspace.workspaceFolders[0].uri.fsPath; 12 | } 13 | console.log(pathToWorkspace); 14 | const subprocess = spawn('npm', ['start'], { 15 | cwd: pathToWorkspace, 16 | detached: true 17 | }); 18 | subprocess.on('spawn', async () => { 19 | console.log('server started'); 20 | 21 | await exec('sleep 2'); // might not need this 22 | 23 | for (const endpoint of endpoints) { 24 | try { 25 | const curlCommand = `curl -s ${endpoint.method} localhost:${port}/books${endpoint.path}`; // added api here b/c my routes are at /api 26 | console.log(curlCommand); 27 | const { stdout, stderr } = await exec(curlCommand); 28 | if (stdout) { 29 | endpoint.response = stdout; 30 | } else if (stderr) { 31 | endpoint.response = stderr; 32 | } 33 | } catch (e) { 34 | endpoint.response = e as string; 35 | } 36 | } 37 | console.log("almost dead"); 38 | // subprocess.kill(); 39 | 40 | resolve(endpoints); 41 | 42 | }) 43 | subprocess.on('error', (err: Error) => { 44 | console.error('Failed to start subprocess.'); 45 | reject(err) 46 | }); 47 | subprocess.stdout.on('data', (data: ArrayBuffer) => { 48 | console.log(data.toString()); 49 | }); 50 | subprocess.on('close', () => { 51 | console.log("subprocess closed"); 52 | }) 53 | 54 | }) 55 | } 56 | 57 | // sync attempt 58 | export const runRoutes = async (endpoints: endpoint[], port: string) => { 59 | let directory = ''; 60 | if (workspace.workspaceFolders) { 61 | directory = workspace.workspaceFolders[0].uri.path; 62 | } 63 | // generate script string 64 | const startServer = 'npm start &'; 65 | const sleep = 1; 66 | // start server and listen to stream 67 | const serverProcess = spawnSync(`cd ${directory} &&` + startServer + `sleep ${sleep}`); 68 | // when server process is closed, log endpoints 69 | serverProcess.on('close', () => { 70 | console.log('server closed:', endpoints); 71 | }) 72 | 73 | // iterate over endpoints and store responses from stdout 74 | for (const endpoint of endpoints) { 75 | // send request 76 | const response = execSync(`curl -s ${endpoint.method} localhost:${port}${endpoint.path}`); 77 | endpoint.response = response; 78 | } 79 | 80 | // access pid with port and close server 81 | execSync(`lsof i :${port} | xargs kill -9`, () => { 82 | console.log('kill server') 83 | }); 84 | return endpoints; 85 | } -------------------------------------------------------------------------------- /deno_cli/test/test.ts: -------------------------------------------------------------------------------- 1 | import { endpoint, config } from '../types.ts'; 2 | import errors from './error.ts'; 3 | 4 | const td = (d: Uint8Array) => new TextDecoder().decode(d); 5 | // declare config and script globally but do not assign values outside of functions 6 | let config: config; 7 | let script: string; 8 | 9 | const startScript = async ():Promise => { 10 | config = await Deno.readTextFile('./_errordactyl/config.json').then(res => JSON.parse(res)); 11 | const pathToServer:string = config.serverPath; 12 | 13 | let script = `#!/bin/bash\ndeno run -A ${pathToServer} &\nDENO_PID=$!\nsleep 2\n`; 14 | 15 | const colorVars = 16 | `NC='\\0033[0m' 17 | BPURPLE='\\033[1;35m' 18 | BGREEN='\\033[1;32m'`; 19 | 20 | script += colorVars; 21 | return; 22 | } 23 | 24 | const routeScripter = (method:string, data:Array | string, body?:string) => { 25 | 26 | Array.isArray(data)? 27 | method === ('GET'||'DELETE')? 28 | (data.forEach((endpoint, index) => { 29 | const getScript = ` 30 | \n${method}${index}=$(curl -s localhost:3000${endpoint.path}) 31 | echo -e "\${BPURPLE}${method} to '${endpoint.path}': \${NC}\$GET${index}" 32 | ` 33 | script += getScript; 34 | })): 35 | ( 36 | data.forEach((endpoint, index) => { 37 | const postScript = ` 38 | \n${method}${index}=$(curl -s -X ${method} -d '${JSON.stringify(endpoint.body)}' localhost:3000${endpoint.path}) 39 | echo -e "\${BPURPLE}${method} to '${endpoint.path}': \${NC}\$${method}${index}" 40 | ` 41 | script += postScript; 42 | })): 43 | method === ('GET'||'DELETE')? 44 | (script +=` 45 | \n${method}=$(curl -s localhost:3000${data}) 46 | echo -e "\${BPURPLE}${method} to '${data}': \${NC}\$${method}" 47 | ` 48 | ): 49 | script += ` 50 | \n${method}=$(curl -s -X ${method} -d ${body} localhost:3000${data}) 51 | echo -e "\${BPURPLE}${method} to '${data}': \${NC}\$${method}" 52 | ` 53 | return; 54 | } 55 | 56 | const writeAndRun = async () => { 57 | script += `\nkill $DENO_PID`; 58 | 59 | await Deno.writeTextFile('./_errordactyl/test.sh', script); 60 | 61 | await Deno.run({cmd: ['chmod', '+x', './_errordactyl/test.sh']}).status(); 62 | 63 | const p = await Deno.run({cmd: ['./_errordactyl/test.sh'], stdout:'piped', stderr:'piped'}); 64 | await p.status(); 65 | 66 | console.log('%cYour server responded:%c\n', 'background-color: white', 'background-color: transparent'); 67 | console.log(td(await p.output()).trim()) 68 | 69 | const STDERR = (td(await p.stderrOutput()).trim()) 70 | console.log(errors(STDERR)); 71 | } 72 | 73 | 74 | 75 | export const testAll = async () => { 76 | await startScript(); 77 | const endpoints = config.endpoints; 78 | 79 | for (const method in endpoints) { 80 | routeScripter(method, endpoints[method as keyof typeof endpoints]) 81 | } 82 | 83 | writeAndRun(); 84 | } 85 | 86 | export const testOne = async (method:string, endpoint:string, body?:string) => { 87 | await startScript(); 88 | routeScripter(method, endpoint, body); 89 | 90 | writeAndRun(); 91 | } 92 | 93 | // export const testMethods: testMethods = { 94 | // testAll: testAll, 95 | // testOne: testOne 96 | // } 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /deno_cli/test/class.ts: -------------------------------------------------------------------------------- 1 | // class encapsulation of test logic 2 | import { config as dotenv } from "https://deno.land/x/dotenv/mod.ts"; 3 | const env = dotenv(); 4 | 5 | import { endpoint, config } from '../types.ts'; 6 | // import errors from './error.ts'; 7 | 8 | export default class Test { 9 | config: config; 10 | script: string; 11 | 12 | constructor() { 13 | this.config = {} as config; 14 | this.script = ''; 15 | } 16 | 17 | td = (d: Uint8Array) => new TextDecoder().decode(d); 18 | 19 | startScript = async ():Promise => { 20 | this.config = await Deno.readTextFile('./_errordactyl/config.json').then(res => JSON.parse(res)); 21 | const pathToServer:string = this.config.serverPath; 22 | this.script = `#!/bin/bash\ndeno run --allow-net --allow-read ${pathToServer} &\nDENO_PID=$!\nsleep .5\n`; 23 | const colorVars = 24 | `NC='\\0033[0m' 25 | BPURPLE='\\033[1;35m' 26 | BGREEN='\\033[1;32m'`; 27 | 28 | this.script += colorVars; 29 | return; 30 | } 31 | 32 | routeScripter = (method:string, data:Array | string, body?:string) => { 33 | // retrieve PORT in order of priority 34 | const PORT: number = this.config.PORT || Number(env.PORT) || 3000; 35 | // maybe refactor this giant ternary lol 36 | Array.isArray(data)? 37 | method === ('GET'||'DELETE')? 38 | (data.forEach((endpoint, index) => { 39 | const getScript = ` 40 | \n${method}${index}=$(curl -s localhost:${PORT}${endpoint.path}) 41 | echo -e "\${BPURPLE}${method} to '${endpoint.path}': \${NC}\$GET${index}" 42 | ` 43 | this.script += getScript; 44 | })): 45 | ( 46 | data.forEach((endpoint, index) => { 47 | const postScript = ` 48 | \n${method}${index}=$(curl -s -X ${method} -d '${JSON.stringify(endpoint.body)}' localhost:3000${endpoint.path}) 49 | echo -e "\${BPURPLE}${method} to '${endpoint.path}': \${NC}\$${method}${index}" 50 | ` 51 | this.script += postScript; 52 | })): 53 | method === ('GET'||'DELETE')? 54 | (this.script +=` 55 | \n${method}=$(curl -s localhost:3000${data}) 56 | echo -e "\${BPURPLE}${method} to '${data}': \${NC}\$${method}" 57 | ` 58 | ): 59 | this.script += ` 60 | \n${method}=$(curl -s -X ${method} -d ${body} localhost:3000${data}) 61 | echo -e "\${BPURPLE}${method} to '${data}': \${NC}\$${method}" 62 | ` 63 | return; 64 | } 65 | 66 | writeAndRun = async () => { 67 | this.script += `\nkill $DENO_PID`; 68 | 69 | await Deno.writeTextFile('./_errordactyl/test.sh', this.script); 70 | 71 | await Deno.run({cmd: ['chmod', '+x', './_errordactyl/test.sh']}).status(); 72 | 73 | const p = await Deno.run({cmd: ['./_errordactyl/test.sh'], stdout:'piped', stderr:'piped'}); 74 | await p.status(); 75 | 76 | console.log('%cYour server responded:%c\n', 'background-color: white', 'background-color: transparent'); 77 | console.log(this.td(await p.output()).trim()) 78 | 79 | const STDERR = (this.td(await p.stderrOutput()).trim()) 80 | // console.log(errors(STDERR)); 81 | } 82 | 83 | testAll = async () => { 84 | await this.startScript(); 85 | const endpoints = this.config.endpoints; 86 | 87 | for (const method in endpoints) { 88 | this.routeScripter(method, endpoints[method as keyof typeof endpoints]) 89 | } 90 | 91 | this.writeAndRun(); 92 | } 93 | 94 | testOne = async (method:string, endpoint:string, body?:string) => { 95 | await this.startScript(); 96 | this.routeScripter(method, endpoint, body); 97 | 98 | this.writeAndRun(); 99 | } 100 | } -------------------------------------------------------------------------------- /vscode_extension/src/providers/sidebar-provider.tsx: -------------------------------------------------------------------------------- 1 | // class implementation of sidebar view provider 2 | import { WebviewViewProvider, WebviewView, Webview, Uri, EventEmitter, workspace, window } from "vscode"; 3 | // import * as ReactDOMServer from "react-dom/server"; 4 | import { Utils } from "../utils/utils"; 5 | import { parse } from "./lib/parse"; 6 | import { config } from "../../types"; 7 | import { test, runRoutes } from "./lib/test"; 8 | 9 | //@ts-ignore 10 | 11 | export class SidebarWebview implements WebviewViewProvider { 12 | 13 | constructor( 14 | private readonly extensionPath: Uri, 15 | private extStorage: any, 16 | private workspaceStorage: any, 17 | private _view: any = null 18 | ){} 19 | 20 | private onDidChangeTreeData: EventEmitter = new EventEmitter(); 21 | 22 | 23 | refresh(context: any): void { 24 | this.onDidChangeTreeData.fire(null); 25 | this._view.webview.html = this._getHtmlForWebview(this._view?.webview); 26 | }; 27 | 28 | resolveWebviewView(webviewView: WebviewView): void | Thenable { 29 | webviewView.webview.options = { 30 | enableScripts: true, 31 | localResourceRoots: [this.extensionPath], 32 | }; 33 | webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); 34 | this._view = webviewView; 35 | this.activateMessageListener(); 36 | } 37 | 38 | private activateMessageListener() { 39 | this._view.webview.onDidReceiveMessage(async (message: any) => { 40 | let config = this.workspaceStorage.getValue("config"); 41 | let routes = this.workspaceStorage.getValue("routes"); 42 | 43 | switch (message.action) { 44 | case 'parse': 45 | if (workspace.workspaceFolders !== undefined) { 46 | 47 | const folder = workspace.workspaceFolders[0]; 48 | console.log('folder', folder); 49 | // returns array of endpoint objects 50 | const routes = await parse(config, folder); 51 | console.log('parsed routes', routes); 52 | console.log('test'); 53 | // set state 54 | this.workspaceStorage.setValue("routes", routes); 55 | // post message back to webview 56 | this._view.webview.postMessage({action: 'parse', data: routes}); 57 | } else { 58 | window.showInformationMessage('No directory currently opened'); 59 | } 60 | break; 61 | case 'get-initial-state': 62 | // retrieve any data already in state (check for config) 63 | if (config) { 64 | console.log("config data in workspaceStorage (initial state)", config); 65 | console.log("routes data in workspaceStorage (initial state)", routes); 66 | this._view.webview.postMessage({action: "config", data: routes}); 67 | } 68 | break; 69 | case 'set-config': 70 | // store config in workspace storage 71 | this.workspaceStorage.setValue("config", message.data); 72 | console.log("config data in workspaceStorage (setting state)", this.workspaceStorage.getValue("config")) 73 | this._view.webview.postMessage({action: "config"}) 74 | break; 75 | 76 | case 'test-routes': 77 | console.log('test-routes message received') 78 | // generate script and return responses from server 79 | const endpointsWithResponse = await test(routes, config.PORT, config.serverPath); 80 | // const endpointsWithResponse = await runRoutes(routes, config.PORT); 81 | console.log('final endpoints ->', endpointsWithResponse); 82 | // send back updated config object 83 | config.endpoints = endpointsWithResponse; 84 | this._view.webview.postMessage({action: 'config', data: endpointsWithResponse}); 85 | break; 86 | case 'reset' : 87 | this.workspaceStorage.setValue("config", undefined); 88 | this.workspaceStorage.setValue("routes", undefined); 89 | console.log("storage reset, config is now:", this.workspaceStorage.getValue("config")); 90 | console.log("storage reset, routes is now:", this.workspaceStorage.getValue("routes")); 91 | } 92 | }) 93 | } 94 | 95 | private _getHtmlForWebview(webview: Webview) { 96 | // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. 97 | // Script to handle user action 98 | const scriptUri = webview.asWebviewUri( 99 | Uri.joinPath(this.extensionPath, "dist", "bundle.js") 100 | ); 101 | 102 | // CSS file to handle styling 103 | const styleUri = webview.asWebviewUri( 104 | Uri.joinPath(this.extensionPath, "script", "sidebar-webview.css") 105 | ); 106 | 107 | // Use a nonce to only allow a specific script to be run. 108 | const nonce = Utils.getNonce(); 109 | 110 | return ` 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
119 | 120 | 121 |
122 | 123 | 124 | `; 125 | } 126 | } 127 | 128 | {/* */} -------------------------------------------------------------------------------- /deno_cli/test/error.ts: -------------------------------------------------------------------------------- 1 | // import * as path from "https://deno.land/std/path/mod.ts" 2 | 3 | // console.log(path.dirname(path.fromFileUrl(Deno.mainModule))); 4 | 5 | import { strObjType, finalObjType } from "../types.ts"; 6 | 7 | export default function errors(errorMessage: string): finalObjType[] | string { 8 | if (!errorMessage) return "..."; 9 | 10 | const arrayOfRegex = [ 11 | /(?<=file:\/\/)\S*/, // looking for strings after file://... 12 | /(?<=\.(j|t)s:)\S*/, // looking for .js or .ts suffixes 13 | / (.*): /gi, // looking for object keys in the request and response string object 14 | /\"(.*)\"/, // looking for a word between quotation marks 15 | /:(\d+):(\d+)/, // looking for a colon between two numbers 16 | ] 17 | 18 | const arrayErrorStack : (string[] | null) = []; 19 | const arrayOfUsefulErrorInformation : finalObjType[] = []; 20 | 21 | //Remove all of the new lines and spaces and extract the error stack string into an array of strings for each element in the stack 22 | { 23 | 24 | JSON.stringify(errorMessage).split("\\n\\n").forEach( 25 | (stringElement) => 26 | { 27 | stringElement.split('\\n').forEach((element) => arrayErrorStack.push(element.replaceAll(' at ',''))) 28 | } 29 | ) 30 | 31 | } 32 | 33 | // Loop through the array of error stack strings and parse each string into readable, usable information 34 | for (let i = 0; i < arrayErrorStack.length; i++) { 35 | 36 | const finalObj : finalObjType = { // declare a finalObj variable 37 | message: '', 38 | request: { 39 | url: '', 40 | method: '', 41 | hasBody: Boolean(), 42 | }, 43 | response: { 44 | status: '', 45 | type: '', 46 | hasBody: Boolean(), 47 | writable: Boolean(), 48 | }, 49 | location: '', 50 | lineNo: '', 51 | colNo: '', 52 | }; 53 | 54 | if (arrayErrorStack[i].includes('[uncaught application error]:')) { 55 | 56 | const extractFileDirectoryFromErrorStack : (RegExpMatchArray | null) = arrayErrorStack[i+3].match(arrayOfRegex[0]); 57 | 58 | const decodedURIArrayElement = ((extractFileDirectoryFromErrorStack?.[0] !== undefined) ? decodeURI(extractFileDirectoryFromErrorStack?.[0]) : undefined);// memoize the file path to the error 59 | const lineAndColNumbers = decodedURIArrayElement?.match(arrayOfRegex[1])?.[0].split(':'); //extract line and column numbers 60 | 61 | const strobj : strObjType = { 62 | message: arrayErrorStack[i].replaceAll(/\"/g,''), 63 | request: arrayErrorStack[i+1].replace(/request: /, '').replaceAll(/\\/g, ''), 64 | response: arrayErrorStack[i+2].replace(/response: /, ''), 65 | location: decodedURIArrayElement, 66 | lineNo: lineAndColNumbers?.[0], 67 | colNo: lineAndColNumbers?.[1], 68 | }; 69 | 70 | // Assign finalObj.message to strobj.message 71 | { 72 | ({message: finalObj.message, lineNo: finalObj.lineNo, colNo: finalObj.colNo} = strobj) 73 | } 74 | 75 | // Turn the request message into an object in the block below: 76 | { 77 | const JSONstringArr : string[] = []; 78 | strobj.request.split(',').forEach((ele) => { 79 | const requestObjectKeys = ele.match(arrayOfRegex[2])?.[0].replace(' ','"').replace(':','":') 80 | if (requestObjectKeys !== undefined) { 81 | JSONstringArr.push(requestObjectKeys); 82 | } 83 | }) 84 | 85 | JSONstringArr.forEach((ele) => { 86 | const removeExtraQuotationMarksFromReqObjKeys : (RegExpMatchArray | null) = ele.match(arrayOfRegex[3]) 87 | if (removeExtraQuotationMarksFromReqObjKeys !== null) { 88 | {strobj.request = strobj.request.replace(removeExtraQuotationMarksFromReqObjKeys[1], removeExtraQuotationMarksFromReqObjKeys[0])} 89 | } 90 | }) 91 | 92 | finalObj.request = JSON.parse(strobj.request); 93 | } 94 | 95 | 96 | // Turn the response message into an object in the block below: 97 | { 98 | const JSONstringArr : string[] = []; 99 | strobj.response = strobj.response.replace('type: undefined', 'type: "undefined"') // JSON.parse does not parse undefined 100 | strobj.response.split(',').forEach((ele) => { 101 | const testForObjKeys : (RegExpMatchArray | null) = ele.match(arrayOfRegex[2]) 102 | if (testForObjKeys !== null) { 103 | JSONstringArr.push(testForObjKeys[0].replace(' ','"').replace(':','":')) 104 | } 105 | }) 106 | 107 | JSONstringArr.forEach((ele) => { 108 | const stringifyKeyNamesOfResponseObject : (RegExpMatchArray | null) = ele.match(arrayOfRegex[3]) 109 | if (stringifyKeyNamesOfResponseObject !== null) { 110 | strobj.response = strobj.response.replace(stringifyKeyNamesOfResponseObject[1], stringifyKeyNamesOfResponseObject[0]) 111 | } 112 | }) 113 | 114 | finalObj.response = JSON.parse(strobj.response); 115 | 116 | if (finalObj.response.type === 'undefined') finalObj.response.type = undefined // turn 'undefined' into undefined 117 | } 118 | 119 | 120 | // Remove the line and column number from the location object: 121 | { 122 | const removeLineAndColumnNumber = strobj.location?.match(arrayOfRegex[4]); 123 | if (removeLineAndColumnNumber !== null && removeLineAndColumnNumber !== undefined) { 124 | finalObj.location = strobj.location?.replace(removeLineAndColumnNumber[0], '') 125 | } 126 | } 127 | arrayOfUsefulErrorInformation.push(finalObj); 128 | } 129 | } 130 | console.log('it still works after break week!') 131 | return arrayOfUsefulErrorInformation; 132 | } --------------------------------------------------------------------------------