@mg21st/dev-assist 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +17 -0
- package/.github/workflows/ci.yml +42 -0
- package/.github/workflows/docs.yml +49 -0
- package/.github/workflows/publish.yml +49 -0
- package/README.md +117 -0
- package/bin/dev-assist.js +4 -0
- package/dev-assist.config.js +10 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +133 -0
- package/dist/cli/wizard.d.ts +5 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +66 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +62 -0
- package/dist/generators/docsGenerator.d.ts +15 -0
- package/dist/generators/docsGenerator.d.ts.map +1 -0
- package/dist/generators/docsGenerator.js +186 -0
- package/dist/generators/testGenerator.d.ts +12 -0
- package/dist/generators/testGenerator.d.ts.map +1 -0
- package/dist/generators/testGenerator.js +185 -0
- package/dist/parser/astParser.d.ts +7 -0
- package/dist/parser/astParser.d.ts.map +1 -0
- package/dist/parser/astParser.js +194 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +247 -0
- package/dist/shared/types.d.ts +77 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +3 -0
- package/docs/_config.yml +22 -0
- package/docs/api-reference.md +173 -0
- package/docs/architecture.md +90 -0
- package/docs/configuration.md +52 -0
- package/docs/contributing.md +101 -0
- package/docs/index.md +50 -0
- package/docs/installation.md +95 -0
- package/docs/usage.md +107 -0
- package/package.json +58 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/wizard.ts +63 -0
- package/src/config.ts +29 -0
- package/src/generators/docsGenerator.ts +192 -0
- package/src/generators/testGenerator.ts +174 -0
- package/src/parser/astParser.ts +172 -0
- package/src/server/index.ts +238 -0
- package/src/shared/types.ts +83 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +19 -0
- package/ui/index.html +13 -0
- package/ui/package-lock.json +3086 -0
- package/ui/package.json +31 -0
- package/ui/postcss.config.js +6 -0
- package/ui/src/App.tsx +36 -0
- package/ui/src/components/ApiDocsTab.tsx +184 -0
- package/ui/src/components/ApiTestingTab.tsx +363 -0
- package/ui/src/components/Dashboard.tsx +128 -0
- package/ui/src/components/Layout.tsx +76 -0
- package/ui/src/components/TestsTab.tsx +149 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/styles/index.css +41 -0
- package/ui/tailwind.config.js +20 -0
- package/ui/tsconfig.json +19 -0
- package/ui/vite.config.ts +19 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { createServer } from 'http';
|
|
6
|
+
import { Server as SocketIOServer } from 'socket.io';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import chokidar from 'chokidar';
|
|
10
|
+
import rateLimit from 'express-rate-limit';
|
|
11
|
+
import { AstParser } from '../parser/astParser';
|
|
12
|
+
import { TestGenerator } from '../generators/testGenerator';
|
|
13
|
+
import { DocsGenerator } from '../generators/docsGenerator';
|
|
14
|
+
import { loadConfig } from '../config';
|
|
15
|
+
|
|
16
|
+
export async function startServer(options: { port: number; openBrowser: boolean }): Promise<void> {
|
|
17
|
+
const app = express();
|
|
18
|
+
const httpServer = createServer(app);
|
|
19
|
+
const io = new SocketIOServer(httpServer, { cors: { origin: '*' } });
|
|
20
|
+
const config = await loadConfig();
|
|
21
|
+
|
|
22
|
+
app.use(cors());
|
|
23
|
+
app.use(express.json());
|
|
24
|
+
|
|
25
|
+
// Rate limiter: max 60 requests per minute per IP for API routes
|
|
26
|
+
const apiLimiter = rateLimit({
|
|
27
|
+
windowMs: 60_000,
|
|
28
|
+
max: 60,
|
|
29
|
+
standardHeaders: true,
|
|
30
|
+
legacyHeaders: false,
|
|
31
|
+
message: { error: 'Too many requests, please try again later.' },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const uiDistPath = path.join(__dirname, '../../ui/dist');
|
|
35
|
+
if (fs.existsSync(uiDistPath)) {
|
|
36
|
+
app.use(express.static(uiDistPath));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const parser = new AstParser();
|
|
40
|
+
const testGenerator = new TestGenerator();
|
|
41
|
+
const docsGenerator = new DocsGenerator();
|
|
42
|
+
|
|
43
|
+
app.get('/api/summary', apiLimiter, async (_req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const sourceDir = config.sourceDir || './src';
|
|
46
|
+
const parsedFiles = await parser.parseDirectory(path.resolve(process.cwd(), sourceDir));
|
|
47
|
+
|
|
48
|
+
let totalFunctions = 0;
|
|
49
|
+
let totalRoutes = 0;
|
|
50
|
+
|
|
51
|
+
for (const file of parsedFiles) {
|
|
52
|
+
totalFunctions += file.functions.length;
|
|
53
|
+
totalRoutes += file.routes.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
res.json({
|
|
57
|
+
totalFiles: parsedFiles.length,
|
|
58
|
+
totalFunctions,
|
|
59
|
+
totalRoutes,
|
|
60
|
+
totalTests: 0,
|
|
61
|
+
generatedAt: new Date().toISOString(),
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
res.status(500).json({ error: String(error) });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
app.get('/api/files', apiLimiter, async (_req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const sourceDir = config.sourceDir || './src';
|
|
71
|
+
const parsedFiles = await parser.parseDirectory(path.resolve(process.cwd(), sourceDir));
|
|
72
|
+
res.json(parsedFiles);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
res.status(500).json({ error: String(error) });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
app.post('/api/generate/tests', apiLimiter, async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const { sourceDir, outputDir, framework } = req.body as {
|
|
81
|
+
sourceDir?: string;
|
|
82
|
+
outputDir?: string;
|
|
83
|
+
framework?: string;
|
|
84
|
+
};
|
|
85
|
+
const generated = await testGenerator.generate({
|
|
86
|
+
sourceDir: sourceDir || config.sourceDir || './src',
|
|
87
|
+
outputDir: outputDir || config.testOutputDir || './__tests__',
|
|
88
|
+
framework: (framework || config.testFramework || 'jest') as 'jest' | 'vitest',
|
|
89
|
+
});
|
|
90
|
+
res.json({ success: true, files: generated });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
res.status(500).json({ error: String(error) });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.post('/api/generate/docs', apiLimiter, async (req, res) => {
|
|
97
|
+
try {
|
|
98
|
+
const { sourceDir, outputDir } = req.body as {
|
|
99
|
+
sourceDir?: string;
|
|
100
|
+
outputDir?: string;
|
|
101
|
+
};
|
|
102
|
+
const doc = await docsGenerator.generate({
|
|
103
|
+
sourceDir: sourceDir || config.sourceDir || './src',
|
|
104
|
+
outputDir: outputDir || config.docsOutputDir || './docs',
|
|
105
|
+
baseUrl: config.baseUrl,
|
|
106
|
+
});
|
|
107
|
+
res.json({ success: true, doc });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
res.status(500).json({ error: String(error) });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
app.get('/api/docs', apiLimiter, async (_req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const docsPath = path.join(process.cwd(), config.docsOutputDir || './docs', 'api-docs.json');
|
|
116
|
+
if (fs.existsSync(docsPath)) {
|
|
117
|
+
const docs = JSON.parse(fs.readFileSync(docsPath, 'utf-8'));
|
|
118
|
+
res.json(docs);
|
|
119
|
+
} else {
|
|
120
|
+
res.json({ endpoints: [], title: 'API Documentation', version: '1.0.0', baseUrl: config.baseUrl });
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
res.status(500).json({ error: String(error) });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
app.get('/api/tests', apiLimiter, async (_req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const testDir = path.join(process.cwd(), config.testOutputDir || './__tests__');
|
|
130
|
+
const tests: { filePath: string; content: string }[] = [];
|
|
131
|
+
|
|
132
|
+
if (fs.existsSync(testDir)) {
|
|
133
|
+
const getTestFiles = (dir: string): string[] => {
|
|
134
|
+
const files: string[] = [];
|
|
135
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const fullPath = path.join(dir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) files.push(...getTestFiles(fullPath));
|
|
139
|
+
else if (entry.isFile() && entry.name.includes('.test.')) files.push(fullPath);
|
|
140
|
+
}
|
|
141
|
+
return files;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const testFiles = getTestFiles(testDir);
|
|
145
|
+
for (const file of testFiles) {
|
|
146
|
+
tests.push({
|
|
147
|
+
filePath: path.relative(process.cwd(), file),
|
|
148
|
+
content: fs.readFileSync(file, 'utf-8'),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
res.json(tests);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
res.status(500).json({ error: String(error) });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.post('/api/proxy', async (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const { url, method, headers, body } = req.body as {
|
|
162
|
+
url: string;
|
|
163
|
+
method: string;
|
|
164
|
+
headers?: Record<string, string>;
|
|
165
|
+
body?: unknown;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const startTime = Date.now();
|
|
169
|
+
|
|
170
|
+
const fetchOptions: RequestInit = {
|
|
171
|
+
method: method || 'GET',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
...(headers || {}),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (body && ['POST', 'PUT', 'PATCH'].includes(method?.toUpperCase() || '')) {
|
|
179
|
+
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const response = await fetch(url, fetchOptions);
|
|
183
|
+
const responseTime = Date.now() - startTime;
|
|
184
|
+
|
|
185
|
+
let responseData: unknown;
|
|
186
|
+
const contentType = response.headers.get('content-type') || '';
|
|
187
|
+
|
|
188
|
+
if (contentType.includes('application/json')) {
|
|
189
|
+
responseData = await response.json();
|
|
190
|
+
} else {
|
|
191
|
+
responseData = await response.text();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
res.json({
|
|
195
|
+
status: response.status,
|
|
196
|
+
statusText: response.statusText,
|
|
197
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
198
|
+
data: responseData,
|
|
199
|
+
responseTime,
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
res.status(500).json({ error: String(error) });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
app.get('*', apiLimiter, (_req, res) => {
|
|
207
|
+
const indexPath = path.join(__dirname, '../../ui/dist/index.html');
|
|
208
|
+
if (fs.existsSync(indexPath)) {
|
|
209
|
+
res.sendFile(indexPath);
|
|
210
|
+
} else {
|
|
211
|
+
res.json({ message: 'DevAssist API Server', status: 'running' });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (config.watchMode) {
|
|
216
|
+
const watcher = chokidar.watch(config.sourceDir || './src', {
|
|
217
|
+
ignored: /node_modules/,
|
|
218
|
+
persistent: true,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
watcher.on('change', (filePath: string) => {
|
|
222
|
+
io.emit('file-changed', { filePath });
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
io.on('connection', (socket) => {
|
|
227
|
+
socket.emit('connected', { message: 'DevAssist server connected' });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
httpServer.listen(options.port, () => {
|
|
231
|
+
console.log(chalk.green.bold(`\n✅ DevAssist UI running at http://localhost:${options.port}\n`));
|
|
232
|
+
console.log(chalk.gray('Press Ctrl+C to stop the server\n'));
|
|
233
|
+
|
|
234
|
+
if (options.openBrowser) {
|
|
235
|
+
open(`http://localhost:${options.port}`);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface ParsedFunction {
|
|
2
|
+
name: string;
|
|
3
|
+
params: string[];
|
|
4
|
+
isAsync: boolean;
|
|
5
|
+
isExported: boolean;
|
|
6
|
+
returnType?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ParsedRoute {
|
|
10
|
+
method: string;
|
|
11
|
+
path: string;
|
|
12
|
+
handler: string;
|
|
13
|
+
params: string[];
|
|
14
|
+
middleware: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ParsedFile {
|
|
18
|
+
filePath: string;
|
|
19
|
+
functions: ParsedFunction[];
|
|
20
|
+
routes: ParsedRoute[];
|
|
21
|
+
imports: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GeneratedTest {
|
|
25
|
+
filePath: string;
|
|
26
|
+
sourceFile: string;
|
|
27
|
+
framework: 'jest' | 'vitest';
|
|
28
|
+
content: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ApiEndpoint {
|
|
32
|
+
method: string;
|
|
33
|
+
path: string;
|
|
34
|
+
params: PathParam[];
|
|
35
|
+
queryParams: QueryParam[];
|
|
36
|
+
bodySchema?: BodySchema;
|
|
37
|
+
description?: string;
|
|
38
|
+
exampleRequest?: Record<string, unknown>;
|
|
39
|
+
exampleResponse?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PathParam {
|
|
43
|
+
name: string;
|
|
44
|
+
type: string;
|
|
45
|
+
required: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface QueryParam {
|
|
49
|
+
name: string;
|
|
50
|
+
type: string;
|
|
51
|
+
required: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BodySchema {
|
|
55
|
+
type: string;
|
|
56
|
+
properties: Record<string, { type: string; required?: boolean }>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ApiDoc {
|
|
60
|
+
title: string;
|
|
61
|
+
version: string;
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
endpoints: ApiEndpoint[];
|
|
64
|
+
generatedAt: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DevAssistConfig {
|
|
68
|
+
testFramework: 'jest' | 'vitest';
|
|
69
|
+
sourceDir: string;
|
|
70
|
+
testOutputDir: string;
|
|
71
|
+
docsOutputDir: string;
|
|
72
|
+
port: number;
|
|
73
|
+
watchMode: boolean;
|
|
74
|
+
baseUrl: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ProjectSummary {
|
|
78
|
+
totalFiles: number;
|
|
79
|
+
totalFunctions: number;
|
|
80
|
+
totalRoutes: number;
|
|
81
|
+
totalTests: number;
|
|
82
|
+
generatedAt: string;
|
|
83
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "ui"]
|
|
19
|
+
}
|
package/ui/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>DevAssist - Developer Toolkit</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|