@merlean/analyzer 2.0.0 → 2.2.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/lib/analyzer.js +431 -106
- package/package.json +1 -1
package/lib/analyzer.js
CHANGED
|
@@ -1,66 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Framework-Agnostic Codebase Scanner
|
|
3
3
|
*
|
|
4
|
-
* Scans
|
|
5
|
-
*
|
|
4
|
+
* Scans ANY codebase (frontend or backend) to extract API patterns:
|
|
5
|
+
* - Express/Fastify/Koa/Hapi route definitions
|
|
6
|
+
* - NestJS decorators (@Get, @Post, @Body, @Query, etc.)
|
|
7
|
+
* - Frontend HTTP calls (fetch, axios, HttpClient, etc.)
|
|
8
|
+
* - Swagger/OpenAPI annotations
|
|
9
|
+
* - Request body schemas and query parameters
|
|
10
|
+
* - TypeScript interfaces/DTOs
|
|
11
|
+
* - Validation schemas (Zod, Joi, class-validator)
|
|
6
12
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
13
|
+
* The goal is to understand what APIs exist, their parameters,
|
|
14
|
+
* and body structures - regardless of framework or code style.
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
const fs = require('fs');
|
|
12
18
|
const path = require('path');
|
|
13
19
|
const { glob } = require('glob');
|
|
14
20
|
|
|
15
|
-
//
|
|
16
|
-
const
|
|
21
|
+
// File patterns to scan (be inclusive)
|
|
22
|
+
const FILE_PATTERNS = [
|
|
17
23
|
'**/*.js',
|
|
18
24
|
'**/*.jsx',
|
|
19
25
|
'**/*.ts',
|
|
20
26
|
'**/*.tsx',
|
|
21
27
|
'**/*.vue',
|
|
22
28
|
'**/*.svelte',
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'**/index.js',
|
|
26
|
-
'**/*api*.js',
|
|
27
|
-
'**/*service*.js',
|
|
28
|
-
'**/*fetch*.js',
|
|
29
|
-
'**/*http*.js'
|
|
29
|
+
'**/*.mjs',
|
|
30
|
+
'**/*.cjs'
|
|
30
31
|
];
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Only ignore truly irrelevant directories
|
|
33
34
|
const IGNORE_PATTERNS = [
|
|
34
35
|
'**/node_modules/**',
|
|
35
36
|
'**/vendor/**',
|
|
36
37
|
'**/.git/**',
|
|
37
38
|
'**/dist/**',
|
|
38
39
|
'**/build/**',
|
|
40
|
+
'**/coverage/**',
|
|
39
41
|
'**/__pycache__/**',
|
|
40
42
|
'**/venv/**',
|
|
41
43
|
'**/*.min.js',
|
|
42
44
|
'**/*.map',
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
45
|
+
'**/*.d.ts', // TypeScript declaration files
|
|
46
|
+
'**/*.spec.ts', // Test files
|
|
47
|
+
'**/*.spec.js',
|
|
48
|
+
'**/*.test.ts',
|
|
49
|
+
'**/*.test.js',
|
|
50
|
+
'**/__tests__/**',
|
|
51
|
+
'**/__mocks__/**'
|
|
48
52
|
];
|
|
49
53
|
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
// Files that are highly likely to contain API definitions
|
|
55
|
+
const HIGH_PRIORITY_PATTERNS = [
|
|
56
|
+
/routes?\.ts$/i,
|
|
57
|
+
/routes?\.js$/i,
|
|
58
|
+
/router\.ts$/i,
|
|
59
|
+
/router\.js$/i,
|
|
60
|
+
/controller\.ts$/i,
|
|
61
|
+
/controller\.js$/i,
|
|
62
|
+
/\.controller\.ts$/i,
|
|
63
|
+
/\.controller\.js$/i,
|
|
64
|
+
/service\.ts$/i,
|
|
65
|
+
/service\.js$/i,
|
|
66
|
+
/\.service\.ts$/i,
|
|
67
|
+
/\.service\.js$/i,
|
|
68
|
+
/api\.ts$/i,
|
|
69
|
+
/api\.js$/i,
|
|
70
|
+
/endpoints?\.ts$/i,
|
|
71
|
+
/endpoints?\.js$/i,
|
|
72
|
+
/http\.ts$/i,
|
|
73
|
+
/http\.js$/i,
|
|
74
|
+
/client\.ts$/i,
|
|
75
|
+
/client\.js$/i,
|
|
76
|
+
/dto\.ts$/i,
|
|
77
|
+
/\.dto\.ts$/i,
|
|
78
|
+
/interfaces?\.ts$/i,
|
|
79
|
+
/types?\.ts$/i,
|
|
80
|
+
/schema\.ts$/i,
|
|
81
|
+
/schemas?\.ts$/i,
|
|
82
|
+
/validation\.ts$/i
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Medium priority - might contain API patterns
|
|
86
|
+
const MEDIUM_PRIORITY_PATTERNS = [
|
|
87
|
+
/index\.ts$/i,
|
|
88
|
+
/index\.js$/i,
|
|
89
|
+
/app\.ts$/i,
|
|
90
|
+
/app\.js$/i,
|
|
91
|
+
/main\.ts$/i,
|
|
92
|
+
/main\.js$/i,
|
|
93
|
+
/server\.ts$/i,
|
|
94
|
+
/server\.js$/i
|
|
54
95
|
];
|
|
55
96
|
|
|
56
97
|
/**
|
|
57
|
-
* Scan codebase and collect
|
|
98
|
+
* Scan codebase and collect API patterns from any framework
|
|
58
99
|
*/
|
|
59
100
|
async function scanCodebase(codebasePath) {
|
|
60
|
-
console.log(' Scanning
|
|
101
|
+
console.log(' Scanning files...');
|
|
61
102
|
|
|
62
|
-
// Get
|
|
63
|
-
const files = await glob(
|
|
103
|
+
// Get all matching files
|
|
104
|
+
const files = await glob(FILE_PATTERNS, {
|
|
64
105
|
cwd: codebasePath,
|
|
65
106
|
ignore: IGNORE_PATTERNS,
|
|
66
107
|
absolute: true
|
|
@@ -68,23 +109,34 @@ async function scanCodebase(codebasePath) {
|
|
|
68
109
|
|
|
69
110
|
console.log(` Found ${files.length} files`);
|
|
70
111
|
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
|
|
112
|
+
// Categorize and prioritize files
|
|
113
|
+
const { highPriority, mediumPriority, other } = categorizeFiles(files, codebasePath);
|
|
114
|
+
|
|
115
|
+
console.log(` High priority: ${highPriority.length}, Medium: ${mediumPriority.length}, Other: ${other.length}`);
|
|
116
|
+
|
|
117
|
+
// Analyze high priority files first (routes, controllers, services, DTOs)
|
|
118
|
+
// Then medium priority, then scan others for API patterns
|
|
119
|
+
const filesToAnalyze = [
|
|
120
|
+
...highPriority,
|
|
121
|
+
...mediumPriority.slice(0, 20),
|
|
122
|
+
...other.slice(0, 50)
|
|
123
|
+
];
|
|
74
124
|
|
|
75
|
-
console.log(` Analyzing ${filesToAnalyze.length}
|
|
125
|
+
console.log(` Analyzing ${filesToAnalyze.length} files for API patterns...`);
|
|
76
126
|
|
|
77
|
-
// Read and extract API patterns from files
|
|
78
127
|
const fileContents = [];
|
|
128
|
+
let filesWithPatterns = 0;
|
|
129
|
+
|
|
79
130
|
for (const file of filesToAnalyze) {
|
|
80
131
|
try {
|
|
81
132
|
const content = fs.readFileSync(file, 'utf-8');
|
|
82
133
|
const relativePath = path.relative(codebasePath, file);
|
|
83
134
|
|
|
84
|
-
// Extract API
|
|
85
|
-
const extracted = extractApiPatterns(content, relativePath);
|
|
135
|
+
// Extract API patterns from the file
|
|
136
|
+
const extracted = extractApiPatterns(content, relativePath, file);
|
|
86
137
|
|
|
87
|
-
if (extracted.
|
|
138
|
+
if (extracted.hasApiPatterns) {
|
|
139
|
+
filesWithPatterns++;
|
|
88
140
|
fileContents.push({
|
|
89
141
|
path: relativePath,
|
|
90
142
|
content: extracted.content
|
|
@@ -95,61 +147,367 @@ async function scanCodebase(codebasePath) {
|
|
|
95
147
|
}
|
|
96
148
|
}
|
|
97
149
|
|
|
98
|
-
console.log(` Found API patterns in ${
|
|
150
|
+
console.log(` Found API patterns in ${filesWithPatterns} files`);
|
|
99
151
|
|
|
100
152
|
return fileContents;
|
|
101
153
|
}
|
|
102
154
|
|
|
103
155
|
/**
|
|
104
|
-
*
|
|
156
|
+
* Categorize files by priority
|
|
157
|
+
*/
|
|
158
|
+
function categorizeFiles(files, basePath) {
|
|
159
|
+
const highPriority = [];
|
|
160
|
+
const mediumPriority = [];
|
|
161
|
+
const other = [];
|
|
162
|
+
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
const relativePath = path.relative(basePath, file);
|
|
165
|
+
|
|
166
|
+
if (HIGH_PRIORITY_PATTERNS.some(p => p.test(relativePath))) {
|
|
167
|
+
highPriority.push(file);
|
|
168
|
+
} else if (MEDIUM_PRIORITY_PATTERNS.some(p => p.test(relativePath))) {
|
|
169
|
+
mediumPriority.push(file);
|
|
170
|
+
} else {
|
|
171
|
+
other.push(file);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { highPriority, mediumPriority, other };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract Swagger/OpenAPI documentation blocks
|
|
180
|
+
*/
|
|
181
|
+
function extractSwaggerBlocks(content) {
|
|
182
|
+
const swaggerBlocks = [];
|
|
183
|
+
|
|
184
|
+
// Match JSDoc blocks with @swagger
|
|
185
|
+
const swaggerRegex = /\/\*\*[\s\S]*?@swagger[\s\S]*?\*\//g;
|
|
186
|
+
let match;
|
|
187
|
+
|
|
188
|
+
while ((match = swaggerRegex.exec(content)) !== null) {
|
|
189
|
+
swaggerBlocks.push(match[0]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return swaggerBlocks;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract TypeScript interfaces and types that look like DTOs/schemas
|
|
105
197
|
*/
|
|
106
|
-
function
|
|
107
|
-
const
|
|
198
|
+
function extractTypeDefinitions(content, filePath) {
|
|
199
|
+
const typeBlocks = [];
|
|
108
200
|
const lines = content.split('\n');
|
|
109
201
|
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
202
|
+
// Look for interfaces and types that might be request/response schemas
|
|
203
|
+
const typeKeywords = [
|
|
204
|
+
/interface\s+\w*(Request|Response|Dto|Body|Query|Params|Payload|Input|Output)\w*\s*\{/i,
|
|
205
|
+
/type\s+\w*(Request|Response|Dto|Body|Query|Params|Payload|Input|Output)\w*\s*=/i,
|
|
206
|
+
/interface\s+\w+\s*\{/, // Any interface in DTO/schema files
|
|
207
|
+
/type\s+\w+\s*=/ // Any type in DTO/schema files
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
// Only extract type definitions from files that look like DTOs/types
|
|
211
|
+
const isDtoFile = /dto|interface|type|schema|model/i.test(filePath);
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const line = lines[i];
|
|
116
215
|
|
|
117
|
-
|
|
118
|
-
{ regex: /axios\.(get|post|put|patch|delete)\s*\(\s*[`'"](.*?)[`'"]/g, type: 'axios' },
|
|
119
|
-
{ regex: /axios\s*\(\s*\{[^}]*url\s*:\s*[`'"](.*?)[`'"]/g, type: 'axios-config' },
|
|
216
|
+
const isTypeDefinition = typeKeywords.some(regex => regex.test(line));
|
|
120
217
|
|
|
121
|
-
|
|
122
|
-
|
|
218
|
+
if (isTypeDefinition || (isDtoFile && /^(export\s+)?(interface|type)\s+/.test(line))) {
|
|
219
|
+
// Find the complete type block (until closing brace at same indent level)
|
|
220
|
+
let braceCount = 0;
|
|
221
|
+
let started = false;
|
|
222
|
+
let endLine = i;
|
|
223
|
+
|
|
224
|
+
for (let j = i; j < lines.length && j < i + 50; j++) {
|
|
225
|
+
const l = lines[j];
|
|
226
|
+
for (const char of l) {
|
|
227
|
+
if (char === '{') {
|
|
228
|
+
braceCount++;
|
|
229
|
+
started = true;
|
|
230
|
+
} else if (char === '}') {
|
|
231
|
+
braceCount--;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
endLine = j;
|
|
235
|
+
if (started && braceCount === 0) break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const block = lines.slice(i, endLine + 1)
|
|
239
|
+
.map((l, idx) => `${i + idx + 1}: ${l}`)
|
|
240
|
+
.join('\n');
|
|
241
|
+
|
|
242
|
+
typeBlocks.push(block);
|
|
243
|
+
i = endLine; // Skip processed lines
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return typeBlocks;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Extract request body and query parameter patterns
|
|
252
|
+
*/
|
|
253
|
+
function extractRequestPatterns(content) {
|
|
254
|
+
const patterns = [];
|
|
255
|
+
const lines = content.split('\n');
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < lines.length; i++) {
|
|
258
|
+
const line = lines[i];
|
|
123
259
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
260
|
+
// Destructuring patterns for req.body, req.query, req.params
|
|
261
|
+
if (/(?:const|let|var)\s*\{[^}]+\}\s*=\s*req\.(body|query|params)/i.test(line) ||
|
|
262
|
+
/req\.(body|query|params)\s*[;.]/.test(line)) {
|
|
263
|
+
|
|
264
|
+
// Get context around it
|
|
265
|
+
const startLine = Math.max(0, i - 2);
|
|
266
|
+
const endLine = Math.min(lines.length - 1, i + 5);
|
|
267
|
+
|
|
268
|
+
const block = lines.slice(startLine, endLine + 1)
|
|
269
|
+
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
270
|
+
.join('\n');
|
|
271
|
+
|
|
272
|
+
patterns.push(`// Request parameter extraction:\n${block}`);
|
|
273
|
+
i = endLine;
|
|
274
|
+
}
|
|
127
275
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
276
|
+
// NestJS decorators: @Body(), @Query(), @Param()
|
|
277
|
+
if (/@(Body|Query|Param|Headers)\s*\(/i.test(line)) {
|
|
278
|
+
const startLine = Math.max(0, i - 1);
|
|
279
|
+
const endLine = Math.min(lines.length - 1, i + 3);
|
|
280
|
+
|
|
281
|
+
const block = lines.slice(startLine, endLine + 1)
|
|
282
|
+
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
283
|
+
.join('\n');
|
|
284
|
+
|
|
285
|
+
patterns.push(`// NestJS parameter decorator:\n${block}`);
|
|
286
|
+
i = endLine;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Validation schemas: Joi, Zod, class-validator
|
|
290
|
+
if (/Joi\.(object|string|number|array|boolean)\s*\(/i.test(line) ||
|
|
291
|
+
/z\.(object|string|number|array|boolean)\s*\(/i.test(line) ||
|
|
292
|
+
/@(IsString|IsNumber|IsArray|IsBoolean|IsOptional|ValidateNested)/i.test(line)) {
|
|
293
|
+
|
|
294
|
+
const startLine = Math.max(0, i - 2);
|
|
295
|
+
const endLine = Math.min(lines.length - 1, i + 10);
|
|
296
|
+
|
|
297
|
+
const block = lines.slice(startLine, endLine + 1)
|
|
298
|
+
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
299
|
+
.join('\n');
|
|
300
|
+
|
|
301
|
+
patterns.push(`// Validation schema:\n${block}`);
|
|
302
|
+
i = endLine;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return patterns;
|
|
307
|
+
}
|
|
131
308
|
|
|
132
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Extract API patterns from file content - framework agnostic
|
|
311
|
+
*/
|
|
312
|
+
function extractApiPatterns(content, filePath, absolutePath) {
|
|
313
|
+
const lines = content.split('\n');
|
|
314
|
+
let hasApiPatterns = false;
|
|
133
315
|
const extractedBlocks = [];
|
|
134
316
|
|
|
317
|
+
// Check if this is a route/controller file - if so, include more context
|
|
318
|
+
const isRouteFile = HIGH_PRIORITY_PATTERNS.some(p => p.test(filePath));
|
|
319
|
+
const isDtoFile = /dto|interface|types?|schema/i.test(filePath);
|
|
320
|
+
|
|
321
|
+
// ============================================
|
|
322
|
+
// PATTERN 0: Swagger/OpenAPI documentation
|
|
323
|
+
// ============================================
|
|
324
|
+
const swaggerBlocks = extractSwaggerBlocks(content);
|
|
325
|
+
if (swaggerBlocks.length > 0) {
|
|
326
|
+
hasApiPatterns = true;
|
|
327
|
+
extractedBlocks.push(`// Swagger/OpenAPI documentation found:\n${swaggerBlocks.join('\n\n')}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================
|
|
331
|
+
// PATTERN 1: TypeScript interfaces/types (DTOs, schemas)
|
|
332
|
+
// ============================================
|
|
333
|
+
const typeBlocks = extractTypeDefinitions(content, filePath);
|
|
334
|
+
if (typeBlocks.length > 0) {
|
|
335
|
+
hasApiPatterns = true;
|
|
336
|
+
extractedBlocks.push(`// TypeScript type definitions:\n${typeBlocks.join('\n\n')}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ============================================
|
|
340
|
+
// PATTERN 2: Request body/query extraction
|
|
341
|
+
// ============================================
|
|
342
|
+
const requestPatterns = extractRequestPatterns(content);
|
|
343
|
+
if (requestPatterns.length > 0) {
|
|
344
|
+
hasApiPatterns = true;
|
|
345
|
+
extractedBlocks.push(requestPatterns.join('\n\n'));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ============================================
|
|
349
|
+
// PATTERN 3: Express/Koa/Fastify Router definitions
|
|
350
|
+
// ============================================
|
|
351
|
+
const routerPatterns = [
|
|
352
|
+
// Express Router: router.get('/path', handler)
|
|
353
|
+
/\.(get|post|put|patch|delete|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
354
|
+
// Express app: app.get('/path', handler)
|
|
355
|
+
/app\.(get|post|put|patch|delete|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
356
|
+
// Fastify: fastify.get('/path', handler)
|
|
357
|
+
/fastify\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
// ============================================
|
|
361
|
+
// PATTERN 4: NestJS/Decorators
|
|
362
|
+
// ============================================
|
|
363
|
+
const decoratorPatterns = [
|
|
364
|
+
/@(Get|Post|Put|Patch|Delete|All)\s*\(\s*['"`]?([^'"`\)]*)/gi,
|
|
365
|
+
/@Controller\s*\(\s*['"`]([^'"`]+)/gi,
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
// ============================================
|
|
369
|
+
// PATTERN 5: Frontend HTTP calls
|
|
370
|
+
// ============================================
|
|
371
|
+
const httpCallPatterns = [
|
|
372
|
+
// fetch() calls
|
|
373
|
+
/fetch\s*\(\s*[`'"](.*?)[`'"]/g,
|
|
374
|
+
/fetch\s*\(\s*`([^`]*)`/g,
|
|
375
|
+
// axios calls
|
|
376
|
+
/axios\.(get|post|put|patch|delete)\s*\(\s*[`'"](.*?)[`'"]/g,
|
|
377
|
+
/axios\s*\(\s*\{[^}]*url\s*:\s*[`'"](.*?)[`'"]/g,
|
|
378
|
+
// Angular HttpClient
|
|
379
|
+
/this\.http\.(get|post|put|patch|delete)\s*[<(]/g,
|
|
380
|
+
/httpClient\.(get|post|put|patch|delete)\s*[<(]/g,
|
|
381
|
+
// jQuery ajax
|
|
382
|
+
/\$\.(ajax|get|post)\s*\(\s*[`'"](.*?)[`'"]/g,
|
|
383
|
+
// Generic request libraries
|
|
384
|
+
/request\.(get|post|put|patch|delete)\s*\(/g,
|
|
385
|
+
/got\.(get|post|put|patch|delete)\s*\(/g,
|
|
386
|
+
/superagent\.(get|post|put|patch|delete)\s*\(/g,
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
// ============================================
|
|
390
|
+
// PATTERN 6: API URL definitions
|
|
391
|
+
// ============================================
|
|
392
|
+
const urlPatterns = [
|
|
393
|
+
// API endpoints in strings
|
|
394
|
+
/['"`](\/api\/[^'"`\s]+)['"`]/g,
|
|
395
|
+
/['"`](\/v\d+\/[^'"`\s]+)['"`]/g,
|
|
396
|
+
/['"`](https?:\/\/[^'"`\s]*\/api[^'"`\s]*)['"`]/g,
|
|
397
|
+
// Base URL definitions
|
|
398
|
+
/(?:API_BASE|API_URL|BASE_URL|baseURL|apiUrl|apiBase|API_ENDPOINT|BACKEND_URL)\s*[:=]\s*['"`]([^'"`]+)['"`]/gi,
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
// ============================================
|
|
402
|
+
// PATTERN 7: Method + URL combinations
|
|
403
|
+
// ============================================
|
|
404
|
+
const methodUrlPatterns = [
|
|
405
|
+
/(GET|POST|PUT|PATCH|DELETE)\s*[,:]?\s*['"`](\/[^'"`]+)['"`]/gi,
|
|
406
|
+
/method:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/gi,
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
// Combine all patterns for line scanning
|
|
410
|
+
const allPatterns = [
|
|
411
|
+
...routerPatterns,
|
|
412
|
+
...decoratorPatterns,
|
|
413
|
+
...httpCallPatterns,
|
|
414
|
+
...urlPatterns,
|
|
415
|
+
...methodUrlPatterns
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
// If this is a route/controller file with swagger docs, include the whole file
|
|
419
|
+
if (isRouteFile && swaggerBlocks.length > 0) {
|
|
420
|
+
hasApiPatterns = true;
|
|
421
|
+
// Include entire file content (truncated if too long)
|
|
422
|
+
const maxLines = 300;
|
|
423
|
+
const truncatedContent = lines.length > maxLines
|
|
424
|
+
? lines.slice(0, maxLines).join('\n') + `\n// ... ${lines.length - maxLines} more lines ...`
|
|
425
|
+
: content;
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
hasApiPatterns: true,
|
|
429
|
+
content: `// File: ${filePath}\n// Route/Controller file with Swagger docs - full content:\n\n${truncatedContent}`
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// If this is a route file without swagger, still include more content
|
|
434
|
+
if (isRouteFile) {
|
|
435
|
+
const hasRouteContent = allPatterns.some(p => {
|
|
436
|
+
p.lastIndex = 0;
|
|
437
|
+
return p.test(content);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (hasRouteContent) {
|
|
441
|
+
hasApiPatterns = true;
|
|
442
|
+
const maxLines = 200;
|
|
443
|
+
const truncatedContent = lines.length > maxLines
|
|
444
|
+
? lines.slice(0, maxLines).join('\n') + `\n// ... ${lines.length - maxLines} more lines ...`
|
|
445
|
+
: content;
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
hasApiPatterns: true,
|
|
449
|
+
content: `// File: ${filePath}\n// Route/Controller file - full content:\n\n${truncatedContent}`
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// If this is a DTO/types file, include full content
|
|
455
|
+
if (isDtoFile && typeBlocks.length > 0) {
|
|
456
|
+
hasApiPatterns = true;
|
|
457
|
+
const maxLines = 150;
|
|
458
|
+
const truncatedContent = lines.length > maxLines
|
|
459
|
+
? lines.slice(0, maxLines).join('\n') + `\n// ... ${lines.length - maxLines} more lines ...`
|
|
460
|
+
: content;
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
hasApiPatterns: true,
|
|
464
|
+
content: `// File: ${filePath}\n// DTO/Types file - full content:\n\n${truncatedContent}`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// For non-route files, extract relevant sections
|
|
469
|
+
// First, extract imports and base URL definitions
|
|
470
|
+
const baseUrlLines = [];
|
|
471
|
+
|
|
472
|
+
for (let i = 0; i < Math.min(lines.length, 50); i++) {
|
|
473
|
+
const line = lines[i];
|
|
474
|
+
if (/(?:API_BASE|API_URL|BASE_URL|baseURL|apiUrl|BACKEND)/i.test(line)) {
|
|
475
|
+
baseUrlLines.push(`${i + 1}: ${line}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (baseUrlLines.length > 0) {
|
|
480
|
+
extractedBlocks.push('// Base URL definitions:\n' + baseUrlLines.join('\n'));
|
|
481
|
+
hasApiPatterns = true;
|
|
482
|
+
}
|
|
483
|
+
|
|
135
484
|
// Line-by-line extraction with context
|
|
136
485
|
for (let i = 0; i < lines.length; i++) {
|
|
137
486
|
const line = lines[i];
|
|
138
487
|
|
|
139
|
-
// Check
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
488
|
+
// Check all patterns
|
|
489
|
+
let hasPattern = false;
|
|
490
|
+
for (const pattern of allPatterns) {
|
|
491
|
+
pattern.lastIndex = 0;
|
|
492
|
+
if (pattern.test(line)) {
|
|
493
|
+
hasPattern = true;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
143
497
|
|
|
144
498
|
// Also check for common API keywords
|
|
145
|
-
const hasKeyword =
|
|
499
|
+
const hasKeyword = /\.get\(|\.post\(|\.put\(|\.patch\(|\.delete\(|fetch\(|axios|\/api\/|endpoint|@Get|@Post|@Put|@Delete/i.test(line);
|
|
146
500
|
|
|
147
501
|
if (hasPattern || hasKeyword) {
|
|
148
|
-
|
|
502
|
+
hasApiPatterns = true;
|
|
149
503
|
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
const
|
|
504
|
+
// Determine context needed
|
|
505
|
+
const isPostOrPut = /post|put|patch/i.test(line);
|
|
506
|
+
const contextBefore = isPostOrPut ? 20 : 5; // More context for mutations
|
|
507
|
+
const contextAfter = isPostOrPut ? 10 : 5;
|
|
508
|
+
|
|
509
|
+
const startLine = Math.max(0, i - contextBefore);
|
|
510
|
+
const endLine = Math.min(lines.length - 1, i + contextAfter);
|
|
153
511
|
|
|
154
512
|
const block = lines.slice(startLine, endLine + 1)
|
|
155
513
|
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
@@ -162,49 +520,16 @@ function extractApiPatterns(content, filePath) {
|
|
|
162
520
|
}
|
|
163
521
|
}
|
|
164
522
|
|
|
165
|
-
|
|
166
|
-
|
|
523
|
+
if (hasApiPatterns && extractedBlocks.length > 0) {
|
|
524
|
+
// Deduplicate blocks
|
|
525
|
+
const uniqueBlocks = [...new Set(extractedBlocks)];
|
|
167
526
|
return {
|
|
168
|
-
|
|
169
|
-
content: `// File: ${filePath}\n// API patterns
|
|
527
|
+
hasApiPatterns: true,
|
|
528
|
+
content: `// File: ${filePath}\n// API patterns extracted:\n\n${uniqueBlocks.join('\n\n// ---\n\n')}`
|
|
170
529
|
};
|
|
171
530
|
}
|
|
172
531
|
|
|
173
|
-
|
|
174
|
-
if (/api|fetch|axios|service|http/i.test(filePath)) {
|
|
175
|
-
return {
|
|
176
|
-
hasApiCalls: true,
|
|
177
|
-
content: `// File: ${filePath}\n${content.slice(0, 5000)}`
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return { hasApiCalls: false, content: '' };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Prioritize frontend files based on keywords
|
|
186
|
-
*/
|
|
187
|
-
function prioritizeFiles(files, basePath) {
|
|
188
|
-
return files.sort((a, b) => {
|
|
189
|
-
const aPath = path.relative(basePath, a).toLowerCase();
|
|
190
|
-
const bPath = path.relative(basePath, b).toLowerCase();
|
|
191
|
-
|
|
192
|
-
// Deprioritize test files
|
|
193
|
-
if (aPath.includes('test') || aPath.includes('spec')) return 1;
|
|
194
|
-
if (bPath.includes('test') || bPath.includes('spec')) return -1;
|
|
195
|
-
|
|
196
|
-
// Prioritize src/frontend folders
|
|
197
|
-
if (aPath.includes('src/') || aPath.includes('frontend/')) {
|
|
198
|
-
if (!bPath.includes('src/') && !bPath.includes('frontend/')) return -1;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const aScore = PRIORITY_KEYWORDS.reduce((score, kw) =>
|
|
202
|
-
aPath.includes(kw) ? score + 1 : score, 0);
|
|
203
|
-
const bScore = PRIORITY_KEYWORDS.reduce((score, kw) =>
|
|
204
|
-
bPath.includes(kw) ? score + 1 : score, 0);
|
|
205
|
-
|
|
206
|
-
return bScore - aScore;
|
|
207
|
-
});
|
|
532
|
+
return { hasApiPatterns: false, content: '' };
|
|
208
533
|
}
|
|
209
534
|
|
|
210
535
|
module.exports = { scanCodebase };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merlean/analyzer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "AI Bot codebase analyzer - generates site maps for AI assistant integration",
|
|
5
5
|
"keywords": ["ai", "bot", "analyzer", "claude", "anthropic", "widget"],
|
|
6
6
|
"author": "zmaren",
|