@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,192 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { AstParser } from '../parser/astParser';
|
|
6
|
+
import { ApiDoc, ApiEndpoint, ParsedRoute } from '../shared/types';
|
|
7
|
+
|
|
8
|
+
export class DocsGenerator {
|
|
9
|
+
private parser: AstParser;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.parser = new AstParser();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async generate(options: {
|
|
16
|
+
sourceDir: string;
|
|
17
|
+
outputDir: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
}): Promise<ApiDoc> {
|
|
21
|
+
const spinner = ora('Scanning source files for API routes...').start();
|
|
22
|
+
|
|
23
|
+
const parsedFiles = await this.parser.parseDirectory(options.sourceDir);
|
|
24
|
+
|
|
25
|
+
const endpoints: ApiEndpoint[] = [];
|
|
26
|
+
|
|
27
|
+
for (const parsedFile of parsedFiles) {
|
|
28
|
+
for (const route of parsedFile.routes) {
|
|
29
|
+
endpoints.push(this.buildEndpoint(route));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
spinner.text = `Generating documentation for ${endpoints.length} endpoints...`;
|
|
34
|
+
|
|
35
|
+
const doc: ApiDoc = {
|
|
36
|
+
title: options.title || 'API Documentation',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
baseUrl: options.baseUrl || 'http://localhost:3000',
|
|
39
|
+
endpoints,
|
|
40
|
+
generatedAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
fs.mkdirSync(options.outputDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(options.outputDir, 'api-docs.json'),
|
|
47
|
+
JSON.stringify(doc, null, 2)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
path.join(options.outputDir, 'api-docs.md'),
|
|
52
|
+
this.generateMarkdown(doc)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(options.outputDir, 'openapi.json'),
|
|
57
|
+
JSON.stringify(this.generateOpenApiSpec(doc), null, 2)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
spinner.succeed(chalk.green(`Generated API docs for ${endpoints.length} endpoints`));
|
|
61
|
+
return doc;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private buildEndpoint(route: ParsedRoute): ApiEndpoint {
|
|
65
|
+
const pathParams = route.params.map(p => ({
|
|
66
|
+
name: p,
|
|
67
|
+
type: 'string',
|
|
68
|
+
required: true,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const exampleRequest: Record<string, unknown> = {};
|
|
72
|
+
const exampleResponse: Record<string, unknown> = {
|
|
73
|
+
success: true,
|
|
74
|
+
data: {},
|
|
75
|
+
message: 'Operation completed successfully',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (['POST', 'PUT', 'PATCH'].includes(route.method)) {
|
|
79
|
+
exampleRequest.body = { key: 'value' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
method: route.method,
|
|
84
|
+
path: route.path,
|
|
85
|
+
params: pathParams,
|
|
86
|
+
queryParams: [],
|
|
87
|
+
description: `${route.method} ${route.path}`,
|
|
88
|
+
exampleRequest,
|
|
89
|
+
exampleResponse,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private generateMarkdown(doc: ApiDoc): string {
|
|
94
|
+
const lines: string[] = [
|
|
95
|
+
`# ${doc.title}`,
|
|
96
|
+
'',
|
|
97
|
+
`**Version:** ${doc.version}`,
|
|
98
|
+
`**Base URL:** ${doc.baseUrl}`,
|
|
99
|
+
`**Generated:** ${doc.generatedAt}`,
|
|
100
|
+
'',
|
|
101
|
+
'## Endpoints',
|
|
102
|
+
'',
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const endpoint of doc.endpoints) {
|
|
106
|
+
lines.push(`### ${endpoint.method} \`${endpoint.path}\``);
|
|
107
|
+
lines.push('');
|
|
108
|
+
|
|
109
|
+
if (endpoint.description) {
|
|
110
|
+
lines.push(endpoint.description);
|
|
111
|
+
lines.push('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (endpoint.params.length > 0) {
|
|
115
|
+
lines.push('**Path Parameters:**');
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push('| Name | Type | Required |');
|
|
118
|
+
lines.push('|------|------|----------|');
|
|
119
|
+
for (const p of endpoint.params) {
|
|
120
|
+
lines.push(`| ${p.name} | ${p.type} | ${p.required ? 'Yes' : 'No'} |`);
|
|
121
|
+
}
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (endpoint.exampleRequest && Object.keys(endpoint.exampleRequest).length > 0) {
|
|
126
|
+
lines.push('**Example Request:**');
|
|
127
|
+
lines.push('```json');
|
|
128
|
+
lines.push(JSON.stringify(endpoint.exampleRequest, null, 2));
|
|
129
|
+
lines.push('```');
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (endpoint.exampleResponse) {
|
|
134
|
+
lines.push('**Example Response:**');
|
|
135
|
+
lines.push('```json');
|
|
136
|
+
lines.push(JSON.stringify(endpoint.exampleResponse, null, 2));
|
|
137
|
+
lines.push('```');
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push('---');
|
|
142
|
+
lines.push('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private generateOpenApiSpec(doc: ApiDoc): Record<string, unknown> {
|
|
149
|
+
const paths: Record<string, unknown> = {};
|
|
150
|
+
|
|
151
|
+
for (const endpoint of doc.endpoints) {
|
|
152
|
+
const openApiPath = endpoint.path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
|
|
153
|
+
|
|
154
|
+
if (!paths[openApiPath]) {
|
|
155
|
+
paths[openApiPath] = {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const pathItem = paths[openApiPath] as Record<string, unknown>;
|
|
159
|
+
const method = endpoint.method.toLowerCase();
|
|
160
|
+
|
|
161
|
+
pathItem[method] = {
|
|
162
|
+
summary: endpoint.description,
|
|
163
|
+
parameters: endpoint.params.map(p => ({
|
|
164
|
+
name: p.name,
|
|
165
|
+
in: 'path',
|
|
166
|
+
required: p.required,
|
|
167
|
+
schema: { type: p.type },
|
|
168
|
+
})),
|
|
169
|
+
responses: {
|
|
170
|
+
'200': {
|
|
171
|
+
description: 'Success',
|
|
172
|
+
content: {
|
|
173
|
+
'application/json': {
|
|
174
|
+
example: endpoint.exampleResponse,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
openapi: '3.0.0',
|
|
184
|
+
info: {
|
|
185
|
+
title: doc.title,
|
|
186
|
+
version: doc.version,
|
|
187
|
+
},
|
|
188
|
+
servers: [{ url: doc.baseUrl }],
|
|
189
|
+
paths,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { AstParser } from '../parser/astParser';
|
|
6
|
+
import { GeneratedTest, ParsedFunction, ParsedRoute } from '../shared/types';
|
|
7
|
+
|
|
8
|
+
export class TestGenerator {
|
|
9
|
+
private parser: AstParser;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.parser = new AstParser();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async generate(options: {
|
|
16
|
+
sourceDir: string;
|
|
17
|
+
outputDir: string;
|
|
18
|
+
framework: 'jest' | 'vitest';
|
|
19
|
+
}): Promise<GeneratedTest[]> {
|
|
20
|
+
const spinner = ora('Scanning source files...').start();
|
|
21
|
+
|
|
22
|
+
const parsedFiles = await this.parser.parseDirectory(options.sourceDir);
|
|
23
|
+
spinner.text = `Generating tests for ${parsedFiles.length} files...`;
|
|
24
|
+
|
|
25
|
+
const generated: GeneratedTest[] = [];
|
|
26
|
+
|
|
27
|
+
for (const parsedFile of parsedFiles) {
|
|
28
|
+
if (parsedFile.functions.length === 0 && parsedFile.routes.length === 0) continue;
|
|
29
|
+
|
|
30
|
+
const testContent = this.generateTestContent(parsedFile.filePath, {
|
|
31
|
+
functions: parsedFile.functions,
|
|
32
|
+
routes: parsedFile.routes,
|
|
33
|
+
framework: options.framework,
|
|
34
|
+
sourceDir: options.sourceDir,
|
|
35
|
+
outputDir: options.outputDir,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const relativePath = path.relative(options.sourceDir, parsedFile.filePath);
|
|
39
|
+
const testFileName = relativePath.replace(/\.(js|ts|jsx|tsx)$/, `.test.${parsedFile.filePath.endsWith('.ts') || parsedFile.filePath.endsWith('.tsx') ? 'ts' : 'js'}`);
|
|
40
|
+
const outputPath = path.join(options.outputDir, testFileName);
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
43
|
+
fs.writeFileSync(outputPath, testContent);
|
|
44
|
+
|
|
45
|
+
generated.push({
|
|
46
|
+
filePath: outputPath,
|
|
47
|
+
sourceFile: parsedFile.filePath,
|
|
48
|
+
framework: options.framework,
|
|
49
|
+
content: testContent,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
spinner.succeed(chalk.green(`Generated ${generated.length} test files`));
|
|
54
|
+
return generated;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private generateTestContent(
|
|
58
|
+
filePath: string,
|
|
59
|
+
options: {
|
|
60
|
+
functions: ParsedFunction[];
|
|
61
|
+
routes: ParsedRoute[];
|
|
62
|
+
framework: 'jest' | 'vitest';
|
|
63
|
+
sourceDir: string;
|
|
64
|
+
outputDir: string;
|
|
65
|
+
}
|
|
66
|
+
): string {
|
|
67
|
+
const { functions, routes, framework } = options;
|
|
68
|
+
const relativeImport = path.relative(
|
|
69
|
+
path.dirname(path.join(options.outputDir, path.relative(options.sourceDir, filePath))),
|
|
70
|
+
filePath
|
|
71
|
+
).replace(/\.(ts|tsx)$/, '');
|
|
72
|
+
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
|
|
75
|
+
if (framework === 'vitest') {
|
|
76
|
+
lines.push(`import { describe, it, expect, vi, beforeEach } from 'vitest';`);
|
|
77
|
+
} else {
|
|
78
|
+
lines.push(`import { describe, it, expect, jest, beforeEach } from '@jest/globals';`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lines.push(`import * as module from '${relativeImport.startsWith('.') ? relativeImport : './' + relativeImport}';`);
|
|
82
|
+
lines.push('');
|
|
83
|
+
|
|
84
|
+
for (const fn of functions) {
|
|
85
|
+
lines.push(`describe('${fn.name}', () => {`);
|
|
86
|
+
|
|
87
|
+
lines.push(` it('should be defined', () => {`);
|
|
88
|
+
lines.push(` expect(module.${fn.name}).toBeDefined();`);
|
|
89
|
+
lines.push(` });`);
|
|
90
|
+
lines.push('');
|
|
91
|
+
|
|
92
|
+
if (fn.isAsync) {
|
|
93
|
+
lines.push(` it('should return a promise', async () => {`);
|
|
94
|
+
const mockArgs = fn.params.map(() => 'undefined').join(', ');
|
|
95
|
+
lines.push(` const result = module.${fn.name}(${mockArgs});`);
|
|
96
|
+
lines.push(` expect(result).toBeInstanceOf(Promise);`);
|
|
97
|
+
lines.push(` });`);
|
|
98
|
+
} else {
|
|
99
|
+
lines.push(` it('should execute without throwing', () => {`);
|
|
100
|
+
const mockArgs = generateMockArgs(fn.params);
|
|
101
|
+
lines.push(` expect(() => module.${fn.name}(${mockArgs})).not.toThrow();`);
|
|
102
|
+
lines.push(` });`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(` it('should handle edge case: null/undefined inputs', ${fn.isAsync ? 'async ' : ''}() => {`);
|
|
107
|
+
lines.push(` // Edge case: test with null/undefined inputs`);
|
|
108
|
+
const nullArgs = fn.params.map(() => 'null').join(', ');
|
|
109
|
+
if (fn.isAsync) {
|
|
110
|
+
lines.push(` try {`);
|
|
111
|
+
lines.push(` await module.${fn.name}(${nullArgs});`);
|
|
112
|
+
lines.push(` } catch (e) {`);
|
|
113
|
+
lines.push(` expect(e).toBeDefined();`);
|
|
114
|
+
lines.push(` }`);
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(` try {`);
|
|
117
|
+
lines.push(` module.${fn.name}(${nullArgs});`);
|
|
118
|
+
lines.push(` } catch (e) {`);
|
|
119
|
+
lines.push(` expect(e).toBeDefined();`);
|
|
120
|
+
lines.push(` }`);
|
|
121
|
+
}
|
|
122
|
+
lines.push(` });`);
|
|
123
|
+
|
|
124
|
+
lines.push(`});`);
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (routes.length > 0) {
|
|
129
|
+
lines.push(`// Express Route Tests`);
|
|
130
|
+
lines.push(`describe('Express Routes', () => {`);
|
|
131
|
+
|
|
132
|
+
for (const route of routes) {
|
|
133
|
+
lines.push(` describe('${route.method} ${route.path}', () => {`);
|
|
134
|
+
lines.push(` it('should handle request correctly', () => {`);
|
|
135
|
+
lines.push(` const mockReq = {`);
|
|
136
|
+
lines.push(` params: { ${route.params.map(p => `${p}: 'test'`).join(', ')} },`);
|
|
137
|
+
lines.push(` query: {},`);
|
|
138
|
+
lines.push(` body: {},`);
|
|
139
|
+
lines.push(` headers: {},`);
|
|
140
|
+
lines.push(` };`);
|
|
141
|
+
lines.push(` const mockRes = {`);
|
|
142
|
+
lines.push(` status: ${framework === 'jest' ? 'jest' : 'vi'}.fn().mockReturnThis(),`);
|
|
143
|
+
lines.push(` json: ${framework === 'jest' ? 'jest' : 'vi'}.fn().mockReturnThis(),`);
|
|
144
|
+
lines.push(` send: ${framework === 'jest' ? 'jest' : 'vi'}.fn().mockReturnThis(),`);
|
|
145
|
+
lines.push(` };`);
|
|
146
|
+
lines.push(` expect(mockReq).toBeDefined();`);
|
|
147
|
+
lines.push(` expect(mockRes).toBeDefined();`);
|
|
148
|
+
lines.push(` });`);
|
|
149
|
+
lines.push(` });`);
|
|
150
|
+
lines.push('');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push(`});`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function generateMockArgs(params: string[]): string {
|
|
161
|
+
return params.map(param => {
|
|
162
|
+
if (param.includes('...')) return '[]';
|
|
163
|
+
const lower = param.toLowerCase();
|
|
164
|
+
if (lower.includes('id')) return "'test-id'";
|
|
165
|
+
if (lower.includes('name')) return "'test-name'";
|
|
166
|
+
if (lower.includes('email')) return "'test@example.com'";
|
|
167
|
+
if (lower.includes('num') || lower.includes('count') || lower.includes('age')) return '0';
|
|
168
|
+
if (lower.includes('flag') || lower.includes('bool') || lower.includes('is') || lower.includes('has')) return 'false';
|
|
169
|
+
if (lower.includes('arr') || lower.includes('list') || lower.includes('items')) return '[]';
|
|
170
|
+
if (lower.includes('obj') || lower.includes('data') || lower.includes('config')) return '{}';
|
|
171
|
+
if (lower.includes('fn') || lower.includes('callback') || lower.includes('cb')) return '() => {}';
|
|
172
|
+
return "''";
|
|
173
|
+
}).join(', ');
|
|
174
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as parser from '@babel/parser';
|
|
2
|
+
import traverse from '@babel/traverse';
|
|
3
|
+
import * as t from '@babel/types';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { ParsedFile, ParsedFunction, ParsedRoute } from '../shared/types';
|
|
7
|
+
|
|
8
|
+
export class AstParser {
|
|
9
|
+
async parseFile(filePath: string): Promise<ParsedFile> {
|
|
10
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
11
|
+
const ext = path.extname(filePath);
|
|
12
|
+
|
|
13
|
+
const plugins: parser.ParserPlugin[] = ['jsx'];
|
|
14
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
15
|
+
plugins.push('typescript');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let ast: t.File;
|
|
19
|
+
try {
|
|
20
|
+
ast = parser.parse(content, {
|
|
21
|
+
sourceType: 'module',
|
|
22
|
+
plugins,
|
|
23
|
+
errorRecovery: true,
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
return { filePath, functions: [], routes: [], imports: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const functions: ParsedFunction[] = [];
|
|
30
|
+
const routes: ParsedRoute[] = [];
|
|
31
|
+
const imports: string[] = [];
|
|
32
|
+
|
|
33
|
+
traverse(ast, {
|
|
34
|
+
ImportDeclaration(nodePath) {
|
|
35
|
+
imports.push(nodePath.node.source.value);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
ExportNamedDeclaration(nodePath) {
|
|
39
|
+
const declaration = nodePath.node.declaration;
|
|
40
|
+
if (!declaration) return;
|
|
41
|
+
|
|
42
|
+
if (t.isFunctionDeclaration(declaration) && declaration.id) {
|
|
43
|
+
functions.push(extractFunction(declaration));
|
|
44
|
+
} else if (t.isVariableDeclaration(declaration)) {
|
|
45
|
+
declaration.declarations.forEach(decl => {
|
|
46
|
+
if (
|
|
47
|
+
t.isVariableDeclarator(decl) &&
|
|
48
|
+
t.isIdentifier(decl.id) &&
|
|
49
|
+
(t.isArrowFunctionExpression(decl.init) || t.isFunctionExpression(decl.init))
|
|
50
|
+
) {
|
|
51
|
+
functions.push(extractVariableFunction(decl.id.name, decl.init));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
ExportDefaultDeclaration(nodePath) {
|
|
58
|
+
const declaration = nodePath.node.declaration;
|
|
59
|
+
if (t.isFunctionDeclaration(declaration)) {
|
|
60
|
+
const fn = extractFunction(declaration);
|
|
61
|
+
fn.name = fn.name || 'default';
|
|
62
|
+
functions.push(fn);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
CallExpression(nodePath) {
|
|
67
|
+
const node = nodePath.node;
|
|
68
|
+
if (
|
|
69
|
+
t.isMemberExpression(node.callee) &&
|
|
70
|
+
t.isIdentifier(node.callee.property)
|
|
71
|
+
) {
|
|
72
|
+
const method = node.callee.property.name.toUpperCase();
|
|
73
|
+
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
|
|
74
|
+
|
|
75
|
+
if (httpMethods.includes(method) && node.arguments.length >= 2) {
|
|
76
|
+
const firstArg = node.arguments[0];
|
|
77
|
+
if (t.isStringLiteral(firstArg)) {
|
|
78
|
+
const routePath = firstArg.value;
|
|
79
|
+
const params = extractPathParams(routePath);
|
|
80
|
+
routes.push({
|
|
81
|
+
method,
|
|
82
|
+
path: routePath,
|
|
83
|
+
handler: `handler_${method.toLowerCase()}_${routePath.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
84
|
+
params,
|
|
85
|
+
middleware: [],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { filePath, functions, routes, imports };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async parseDirectory(dirPath: string): Promise<ParsedFile[]> {
|
|
97
|
+
const results: ParsedFile[] = [];
|
|
98
|
+
const files = this.getFiles(dirPath);
|
|
99
|
+
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = await this.parseFile(file);
|
|
103
|
+
results.push(parsed);
|
|
104
|
+
} catch {
|
|
105
|
+
// Skip unparseable files
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getFiles(dirPath: string): string[] {
|
|
113
|
+
const files: string[] = [];
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(dirPath)) return files;
|
|
116
|
+
|
|
117
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
121
|
+
|
|
122
|
+
if (entry.isDirectory()) {
|
|
123
|
+
if (!['node_modules', '.git', 'dist', 'build', 'coverage'].includes(entry.name)) {
|
|
124
|
+
files.push(...this.getFiles(fullPath));
|
|
125
|
+
}
|
|
126
|
+
} else if (entry.isFile()) {
|
|
127
|
+
const ext = path.extname(entry.name);
|
|
128
|
+
if (['.js', '.ts', '.jsx', '.tsx'].includes(ext)) {
|
|
129
|
+
files.push(fullPath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return files;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractFunction(node: t.FunctionDeclaration): ParsedFunction {
|
|
139
|
+
return {
|
|
140
|
+
name: node.id?.name || 'anonymous',
|
|
141
|
+
params: node.params.map(p => {
|
|
142
|
+
if (t.isIdentifier(p)) return p.name;
|
|
143
|
+
if (t.isAssignmentPattern(p) && t.isIdentifier(p.left)) return p.left.name;
|
|
144
|
+
if (t.isRestElement(p) && t.isIdentifier(p.argument)) return `...${p.argument.name}`;
|
|
145
|
+
return 'param';
|
|
146
|
+
}),
|
|
147
|
+
isAsync: node.async,
|
|
148
|
+
isExported: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractVariableFunction(
|
|
153
|
+
name: string,
|
|
154
|
+
node: t.ArrowFunctionExpression | t.FunctionExpression
|
|
155
|
+
): ParsedFunction {
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
params: node.params.map(p => {
|
|
159
|
+
if (t.isIdentifier(p)) return p.name;
|
|
160
|
+
if (t.isAssignmentPattern(p) && t.isIdentifier(p.left)) return p.left.name;
|
|
161
|
+
if (t.isRestElement(p) && t.isIdentifier(p.argument)) return `...${p.argument.name}`;
|
|
162
|
+
return 'param';
|
|
163
|
+
}),
|
|
164
|
+
isAsync: node.async,
|
|
165
|
+
isExported: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function extractPathParams(routePath: string): string[] {
|
|
170
|
+
const matches = routePath.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
171
|
+
return matches ? matches.map(m => m.slice(1)) : [];
|
|
172
|
+
}
|