offbyt 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/README.md +2 -0
- package/cli/index.js +2 -0
- package/cli.js +206 -0
- package/core/detector/detectAxios.js +107 -0
- package/core/detector/detectFetch.js +148 -0
- package/core/detector/detectForms.js +55 -0
- package/core/detector/detectSocket.js +341 -0
- package/core/generator/generateControllers.js +17 -0
- package/core/generator/generateModels.js +25 -0
- package/core/generator/generateRoutes.js +17 -0
- package/core/generator/generateServer.js +18 -0
- package/core/generator/generateSocket.js +160 -0
- package/core/index.js +14 -0
- package/core/ir/IRTypes.js +25 -0
- package/core/ir/buildIR.js +83 -0
- package/core/parser/parseJS.js +26 -0
- package/core/parser/parseTS.js +27 -0
- package/core/rules/relationRules.js +38 -0
- package/core/rules/resourceRules.js +32 -0
- package/core/rules/schemaInference.js +26 -0
- package/core/scanner/scanProject.js +58 -0
- package/deploy/cloudflare.js +41 -0
- package/deploy/cloudflareWorker.js +122 -0
- package/deploy/connect.js +198 -0
- package/deploy/flyio.js +51 -0
- package/deploy/index.js +322 -0
- package/deploy/netlify.js +29 -0
- package/deploy/railway.js +215 -0
- package/deploy/render.js +195 -0
- package/deploy/utils.js +383 -0
- package/deploy/vercel.js +29 -0
- package/index.js +18 -0
- package/lib/generator/advancedCrudGenerator.js +475 -0
- package/lib/generator/crudCodeGenerator.js +486 -0
- package/lib/generator/irBasedGenerator.js +360 -0
- package/lib/ir-builder/index.js +16 -0
- package/lib/ir-builder/irBuilder.js +330 -0
- package/lib/ir-builder/rulesEngine.js +353 -0
- package/lib/ir-builder/templateEngine.js +193 -0
- package/lib/ir-builder/templates/index.js +14 -0
- package/lib/ir-builder/templates/model.template.js +47 -0
- package/lib/ir-builder/templates/routes-generic.template.js +66 -0
- package/lib/ir-builder/templates/routes-user.template.js +105 -0
- package/lib/ir-builder/templates/routes.template.js +102 -0
- package/lib/ir-builder/templates/validation.template.js +15 -0
- package/lib/ir-integration.js +349 -0
- package/lib/modes/benchmark.js +162 -0
- package/lib/modes/configBasedGenerator.js +2258 -0
- package/lib/modes/connect.js +1125 -0
- package/lib/modes/doctorAi.js +172 -0
- package/lib/modes/generateApi.js +435 -0
- package/lib/modes/interactiveSetup.js +548 -0
- package/lib/modes/offline.clean.js +14 -0
- package/lib/modes/offline.enhanced.js +787 -0
- package/lib/modes/offline.js +295 -0
- package/lib/modes/offline.v2.js +13 -0
- package/lib/modes/sync.js +629 -0
- package/lib/scanner/apiEndpointExtractor.js +387 -0
- package/lib/scanner/authPatternDetector.js +54 -0
- package/lib/scanner/frontendScanner.js +642 -0
- package/lib/utils/apiClientGenerator.js +242 -0
- package/lib/utils/apiScanner.js +95 -0
- package/lib/utils/codeInjector.js +350 -0
- package/lib/utils/doctor.js +381 -0
- package/lib/utils/envGenerator.js +36 -0
- package/lib/utils/loadTester.js +61 -0
- package/lib/utils/performanceAnalyzer.js +298 -0
- package/lib/utils/resourceDetector.js +281 -0
- package/package.json +20 -0
- package/templates/.env.template +31 -0
- package/templates/advanced.model.template.js +201 -0
- package/templates/advanced.route.template.js +341 -0
- package/templates/auth.middleware.template.js +87 -0
- package/templates/auth.routes.template.js +238 -0
- package/templates/auth.user.model.template.js +78 -0
- package/templates/cache.middleware.js +34 -0
- package/templates/chat.models.template.js +260 -0
- package/templates/chat.routes.template.js +478 -0
- package/templates/compression.middleware.js +19 -0
- package/templates/database.config.js +74 -0
- package/templates/errorHandler.middleware.js +54 -0
- package/templates/express/controller.ejs +26 -0
- package/templates/express/model.ejs +9 -0
- package/templates/express/route.ejs +18 -0
- package/templates/express/server.ejs +16 -0
- package/templates/frontend.env.template +14 -0
- package/templates/model.template.js +86 -0
- package/templates/package.production.json +51 -0
- package/templates/package.template.json +41 -0
- package/templates/pagination.utility.js +110 -0
- package/templates/production.server.template.js +233 -0
- package/templates/rateLimiter.middleware.js +36 -0
- package/templates/requestLogger.middleware.js +19 -0
- package/templates/response.helper.js +179 -0
- package/templates/route.template.js +130 -0
- package/templates/security.middleware.js +78 -0
- package/templates/server.template.js +91 -0
- package/templates/socket.server.template.js +433 -0
- package/templates/utils.helper.js +157 -0
- package/templates/validation.middleware.js +63 -0
- package/templates/validation.schema.js +128 -0
- package/utils/fileWriter.js +15 -0
- package/utils/logger.js +18 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { globSync } from 'glob';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { scanProject } from '../../core/scanner/scanProject.js';
|
|
6
|
+
import { parseJS } from '../../core/parser/parseJS.js';
|
|
7
|
+
import { parseTS } from '../../core/parser/parseTS.js';
|
|
8
|
+
import { detectFetch } from '../../core/detector/detectFetch.js';
|
|
9
|
+
import { detectAxios } from '../../core/detector/detectAxios.js';
|
|
10
|
+
import { detectForms } from '../../core/detector/detectForms.js';
|
|
11
|
+
import { detectSocket } from '../../core/detector/detectSocket.js';
|
|
12
|
+
import { buildIRFromDetections } from '../../core/ir/buildIR.js';
|
|
13
|
+
import { applyRuleEngine } from '../../core/rules/resourceRules.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Frontend Code Scanner - Enhanced Version
|
|
17
|
+
* Detects ALL API calls including:
|
|
18
|
+
* - Direct fetch/axios calls
|
|
19
|
+
* - Variable-based URLs
|
|
20
|
+
* - Template literals
|
|
21
|
+
* - Query parameters
|
|
22
|
+
* - Nested routing
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export function scanFrontendCode(projectPath) {
|
|
26
|
+
const spinner = ora('🔍 Scanning frontend code...').start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const astResult = runAstDetection(projectPath);
|
|
30
|
+
|
|
31
|
+
// Regex fallback scan (legacy compatibility)
|
|
32
|
+
const patterns = [
|
|
33
|
+
'src/**/*.{html,js,jsx,ts,tsx}',
|
|
34
|
+
'app/**/*.{html,js,jsx,ts,tsx}',
|
|
35
|
+
'pages/**/*.{html,js,jsx,ts,tsx}',
|
|
36
|
+
'components/**/*.{html,js,jsx,ts,tsx}',
|
|
37
|
+
'services/**/*.{html,js,jsx,ts,tsx}',
|
|
38
|
+
'frontend/**/*.{html,js,jsx,ts,tsx}',
|
|
39
|
+
'client/**/*.{html,js,jsx,ts,tsx}',
|
|
40
|
+
'public/**/*.{html,js,jsx,ts,tsx}',
|
|
41
|
+
'*.{html,js,jsx,ts,tsx}'
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const pattern of patterns) {
|
|
46
|
+
files.push(...globSync(pattern, { nodir: true, cwd: projectPath }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const apiCalls = [...astResult.apiCalls];
|
|
50
|
+
const urlVariables = new Map();
|
|
51
|
+
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
try {
|
|
54
|
+
const fullPath = path.join(projectPath, file);
|
|
55
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
56
|
+
|
|
57
|
+
extractUrlVariables(content, urlVariables);
|
|
58
|
+
apiCalls.push(...extractDirectApiCalls(content, file, content));
|
|
59
|
+
apiCalls.push(...extractVariableBasedCalls(content, file, urlVariables, content));
|
|
60
|
+
apiCalls.push(...extractQueryParamCalls(content, file, content));
|
|
61
|
+
} catch {
|
|
62
|
+
// Skip files that can't be read
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const uniqueCalls = deduplicateApiCalls(apiCalls);
|
|
67
|
+
|
|
68
|
+
spinner.succeed(`✅ Found ${uniqueCalls.length} API calls`);
|
|
69
|
+
return uniqueCalls;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
spinner.fail('Failed to scan code');
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractInlineScripts(htmlContent) {
|
|
77
|
+
const scripts = [];
|
|
78
|
+
const scriptPattern = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
|
|
79
|
+
let match;
|
|
80
|
+
|
|
81
|
+
while ((match = scriptPattern.exec(htmlContent)) !== null) {
|
|
82
|
+
const scriptCode = match[1]?.trim();
|
|
83
|
+
if (scriptCode) scripts.push(scriptCode);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return scripts;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runAstDetection(projectPath) {
|
|
90
|
+
const apiCalls = [];
|
|
91
|
+
const forms = [];
|
|
92
|
+
const socketDetections = [];
|
|
93
|
+
|
|
94
|
+
let files = [];
|
|
95
|
+
try {
|
|
96
|
+
files = scanProject(projectPath);
|
|
97
|
+
} catch {
|
|
98
|
+
return { apiCalls, forms, socketDetections };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const parsedTargets = [];
|
|
103
|
+
|
|
104
|
+
if (file.language === 'html') {
|
|
105
|
+
const scripts = extractInlineScripts(file.content);
|
|
106
|
+
for (const script of scripts) {
|
|
107
|
+
const ast = parseJS(script, file.relativePath);
|
|
108
|
+
if (ast) parsedTargets.push({ ast, content: script });
|
|
109
|
+
}
|
|
110
|
+
} else if (file.language === 'js') {
|
|
111
|
+
const ast = parseJS(file.content, file.relativePath);
|
|
112
|
+
if (ast) parsedTargets.push({ ast, content: file.content });
|
|
113
|
+
} else if (file.language === 'ts') {
|
|
114
|
+
const ast = parseTS(file.content, file.relativePath);
|
|
115
|
+
if (ast) parsedTargets.push({ ast, content: file.content });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const { ast, content } of parsedTargets) {
|
|
119
|
+
apiCalls.push(...detectFetch(ast, file.relativePath));
|
|
120
|
+
apiCalls.push(...detectAxios(ast, file.relativePath));
|
|
121
|
+
forms.push(...detectForms(ast, file.relativePath));
|
|
122
|
+
|
|
123
|
+
// Detect Socket.io / WebSocket patterns
|
|
124
|
+
const socketDetection = detectSocket(ast, content);
|
|
125
|
+
if (socketDetection.hasSocket || socketDetection.hasChat) {
|
|
126
|
+
socketDetections.push({
|
|
127
|
+
file: file.relativePath,
|
|
128
|
+
...socketDetection
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { apiCalls, forms, socketDetections };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function buildHybridIR(projectPath) {
|
|
138
|
+
const { apiCalls, forms, socketDetections } = runAstDetection(projectPath);
|
|
139
|
+
const regexCalls = scanFrontendCode(projectPath);
|
|
140
|
+
const merged = deduplicateApiCalls([...apiCalls, ...regexCalls]);
|
|
141
|
+
|
|
142
|
+
const rawIR = buildIRFromDetections(merged, forms);
|
|
143
|
+
const ir = applyRuleEngine(rawIR);
|
|
144
|
+
|
|
145
|
+
// Add socket detection to IR
|
|
146
|
+
ir.socketDetection = mergeSocketDetections(socketDetections);
|
|
147
|
+
|
|
148
|
+
return ir;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Merge multiple socket detections into one
|
|
153
|
+
*/
|
|
154
|
+
function mergeSocketDetections(detections) {
|
|
155
|
+
if (!detections || detections.length === 0) {
|
|
156
|
+
return {
|
|
157
|
+
hasSocket: false,
|
|
158
|
+
hasChat: false,
|
|
159
|
+
socketType: null,
|
|
160
|
+
events: [],
|
|
161
|
+
rooms: false,
|
|
162
|
+
presence: false
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const merged = {
|
|
167
|
+
hasSocket: false,
|
|
168
|
+
hasChat: false,
|
|
169
|
+
socketType: null,
|
|
170
|
+
events: [],
|
|
171
|
+
rooms: false,
|
|
172
|
+
presence: false,
|
|
173
|
+
endpoints: [],
|
|
174
|
+
files: []
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
for (const detection of detections) {
|
|
178
|
+
if (detection.hasSocket) merged.hasSocket = true;
|
|
179
|
+
if (detection.hasChat) merged.hasChat = true;
|
|
180
|
+
if (detection.socketType) merged.socketType = detection.socketType;
|
|
181
|
+
if (detection.rooms) merged.rooms = true;
|
|
182
|
+
if (detection.presence) merged.presence = true;
|
|
183
|
+
|
|
184
|
+
if (detection.events) {
|
|
185
|
+
merged.events.push(...detection.events);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (detection.endpoints) {
|
|
189
|
+
merged.endpoints.push(...detection.endpoints);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (detection.file) {
|
|
193
|
+
merged.files.push(detection.file);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Deduplicate events
|
|
198
|
+
merged.events = Array.from(
|
|
199
|
+
new Map(merged.events.map(e => [e.name, e])).values()
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return merged;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Extract URL variables: const url = `/api/products`;
|
|
207
|
+
*/
|
|
208
|
+
function extractUrlVariables(content, urlVariables) {
|
|
209
|
+
// Pattern: const/let/var variableName = `/api/...`;
|
|
210
|
+
const patterns = [
|
|
211
|
+
/(const|let|var)\s+(\w+)\s*=\s*['"`]([^'"`]+)['"`]/g,
|
|
212
|
+
/(\w+)\s*=\s*['"`](\/api[^'"`]+)['"`]/g,
|
|
213
|
+
/(\w+)\s*=\s*`([^`]*\/api[^`]*)`/g
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const pattern of patterns) {
|
|
217
|
+
let match;
|
|
218
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
219
|
+
const varName = match[2] || match[1];
|
|
220
|
+
const url = match[3] || match[2];
|
|
221
|
+
|
|
222
|
+
if (url && url.includes(`/api`)) {
|
|
223
|
+
urlVariables.set(varName, url);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract direct API calls: fetch(`/api/products`)
|
|
231
|
+
*/
|
|
232
|
+
function extractDirectApiCalls(content, file, fileContent) {
|
|
233
|
+
const calls = [];
|
|
234
|
+
|
|
235
|
+
// Pattern 1: fetch with direct string
|
|
236
|
+
const fetchPattern = /fetch\s*\(\s*['"`]([^'"`\n]+)['"`]\s*,?\s*(\{[^}]*\})?/g;
|
|
237
|
+
let match;
|
|
238
|
+
|
|
239
|
+
while ((match = fetchPattern.exec(content)) !== null) {
|
|
240
|
+
const route = match[1];
|
|
241
|
+
const config = match[2] || '';
|
|
242
|
+
|
|
243
|
+
// Skip non-API calls
|
|
244
|
+
if (!route.includes(`/api`) && !route.startsWith('/')) continue;
|
|
245
|
+
|
|
246
|
+
// Extract method and fields
|
|
247
|
+
const method = extractMethod(config, content, match.index) || 'GET';
|
|
248
|
+
const fields = extractFields(config, content, match.index);
|
|
249
|
+
|
|
250
|
+
calls.push({
|
|
251
|
+
file,
|
|
252
|
+
type: 'fetch',
|
|
253
|
+
method,
|
|
254
|
+
route: cleanRoute(route),
|
|
255
|
+
fields,
|
|
256
|
+
hasQueryParams: route.includes('?'),
|
|
257
|
+
line: content.substring(0, match.index).split('\n').length,
|
|
258
|
+
content: fileContent
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Pattern 2: fetch with template literal
|
|
263
|
+
const fetchTemplatePattern = /fetch\s*\(\s*`([^`]+)`\s*,?\s*(\{[^}]*\})?/g;
|
|
264
|
+
|
|
265
|
+
while ((match = fetchTemplatePattern.exec(content)) !== null) {
|
|
266
|
+
const route = match[1];
|
|
267
|
+
const config = match[2] || '';
|
|
268
|
+
|
|
269
|
+
// Skip if no API path found
|
|
270
|
+
if (!route.includes(`/api`)) continue;
|
|
271
|
+
|
|
272
|
+
const method = extractMethod(config, content, match.index) || 'GET';
|
|
273
|
+
const fields = extractFields(config, content, match.index);
|
|
274
|
+
|
|
275
|
+
calls.push({
|
|
276
|
+
file,
|
|
277
|
+
type: 'fetch',
|
|
278
|
+
method,
|
|
279
|
+
route: cleanRoute(route),
|
|
280
|
+
fields,
|
|
281
|
+
hasQueryParams: route.includes('?'),
|
|
282
|
+
line: content.substring(0, match.index).split('\n').length,
|
|
283
|
+
content: fileContent
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Pattern 3: axios and custom instances (e.g. api, client, request)
|
|
288
|
+
const axiosPattern = /\b(?:axios|api|client|http|request)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`\n]+)['"`]\s*,?\s*(\{[^}]*\})?/g;
|
|
289
|
+
|
|
290
|
+
while ((match = axiosPattern.exec(content)) !== null) {
|
|
291
|
+
const method = match[1].toUpperCase();
|
|
292
|
+
const route = match[2];
|
|
293
|
+
const config = match[3] || '';
|
|
294
|
+
|
|
295
|
+
if (!route.includes(`/api`) && !route.startsWith('/')) continue;
|
|
296
|
+
|
|
297
|
+
const fields = extractFields(config, content, match.index);
|
|
298
|
+
|
|
299
|
+
calls.push({
|
|
300
|
+
file,
|
|
301
|
+
type: 'axios',
|
|
302
|
+
method,
|
|
303
|
+
route: cleanRoute(route),
|
|
304
|
+
fields,
|
|
305
|
+
hasQueryParams: route.includes('?'),
|
|
306
|
+
line: content.substring(0, match.index).split('\n').length,
|
|
307
|
+
content: fileContent
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return calls;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Extract variable-based calls: fetch(url)
|
|
316
|
+
*/
|
|
317
|
+
function extractVariableBasedCalls(content, file, urlVariables, fileContent) {
|
|
318
|
+
const calls = [];
|
|
319
|
+
|
|
320
|
+
// Pattern: fetch(variableName)
|
|
321
|
+
const fetchVarPattern = /fetch\s*\(\s*(\w+)\s*,?\s*(\{[^}]*\})?/g;
|
|
322
|
+
let match;
|
|
323
|
+
|
|
324
|
+
while ((match = fetchVarPattern.exec(content)) !== null) {
|
|
325
|
+
const varName = match[1];
|
|
326
|
+
const config = match[2] || '';
|
|
327
|
+
|
|
328
|
+
// Check if this variable maps to a URL
|
|
329
|
+
let route = null;
|
|
330
|
+
|
|
331
|
+
// Try to find URL from variable map
|
|
332
|
+
if (urlVariables.has(varName)) {
|
|
333
|
+
route = urlVariables.get(varName);
|
|
334
|
+
} else {
|
|
335
|
+
// Try to find in local scope (look backward in content)
|
|
336
|
+
const beforeContext = content.substring(Math.max(0, match.index - 500), match.index);
|
|
337
|
+
const assignPattern = new RegExp(`${varName}\\s*=\\s*['"\`]([^'"\`]+)['"\`]`);
|
|
338
|
+
const assignMatch = beforeContext.match(assignPattern);
|
|
339
|
+
|
|
340
|
+
if (assignMatch) {
|
|
341
|
+
route = assignMatch[1];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (route && (route.includes(`/api`) || route.startsWith('/'))) {
|
|
346
|
+
const method = extractMethod(config, content, match.index) || 'GET';
|
|
347
|
+
const fields = extractFields(config, content, match.index);
|
|
348
|
+
|
|
349
|
+
calls.push({
|
|
350
|
+
file,
|
|
351
|
+
type: 'fetch',
|
|
352
|
+
method,
|
|
353
|
+
route: cleanRoute(route),
|
|
354
|
+
fields,
|
|
355
|
+
hasQueryParams: route.includes('?'),
|
|
356
|
+
line: content.substring(0, match.index).split('\n').length,
|
|
357
|
+
content: fileContent
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return calls;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Extract calls with query parameters
|
|
367
|
+
*/
|
|
368
|
+
function extractQueryParamCalls(content, file, fileContent) {
|
|
369
|
+
const calls = [];
|
|
370
|
+
|
|
371
|
+
// Pattern: URLSearchParams usage
|
|
372
|
+
const searchParamsPattern = /new URLSearchParams\(\)([^;]*params\.append[^;]*)+/g;
|
|
373
|
+
let match;
|
|
374
|
+
|
|
375
|
+
while ((match = searchParamsPattern.exec(content)) !== null) {
|
|
376
|
+
const paramsBlock = match[0];
|
|
377
|
+
|
|
378
|
+
// Extract param names
|
|
379
|
+
const paramNames = [];
|
|
380
|
+
const appendPattern = /params\.append\s*\(\s*['"`](\w+)['"`]/g;
|
|
381
|
+
let paramMatch;
|
|
382
|
+
|
|
383
|
+
while ((paramMatch = appendPattern.exec(paramsBlock)) !== null) {
|
|
384
|
+
paramNames.push(paramMatch[1]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Try to find associated fetch call nearby
|
|
388
|
+
const afterContext = content.substring(match.index, match.index + 300);
|
|
389
|
+
const fetchMatch = afterContext.match(/fetch\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
390
|
+
|
|
391
|
+
if (fetchMatch) {
|
|
392
|
+
const route = fetchMatch[1];
|
|
393
|
+
|
|
394
|
+
calls.push({
|
|
395
|
+
file,
|
|
396
|
+
type: 'fetch',
|
|
397
|
+
method: 'GET',
|
|
398
|
+
route: cleanRoute(route),
|
|
399
|
+
fields: [],
|
|
400
|
+
hasQueryParams: true,
|
|
401
|
+
queryParams: paramNames,
|
|
402
|
+
line: content.substring(0, match.index).split('\n').length,
|
|
403
|
+
content: fileContent
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return calls;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Extract HTTP method from fetch config
|
|
413
|
+
*/
|
|
414
|
+
function extractMethod(config, fullContent, startIndex) {
|
|
415
|
+
// Look in config object
|
|
416
|
+
const methodMatch = config.match(/method\s*:\s*['"`]([A-Z]+)['"`]/i);
|
|
417
|
+
if (methodMatch) return methodMatch[1].toUpperCase();
|
|
418
|
+
|
|
419
|
+
// Look ahead in content
|
|
420
|
+
const snippet = fullContent.substring(startIndex, startIndex + 300);
|
|
421
|
+
const snippetMatch = snippet.match(/method\s*:\s*['"`]([A-Z]+)['"`]/i);
|
|
422
|
+
if (snippetMatch) return snippetMatch[1].toUpperCase();
|
|
423
|
+
|
|
424
|
+
// Check for body presence (implies POST)
|
|
425
|
+
if (config.includes('body:') || snippet.includes('body:')) {
|
|
426
|
+
return 'POST';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return 'GET';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Extract fields from request body
|
|
434
|
+
*/
|
|
435
|
+
function extractFields(config, fullContent, startIndex) {
|
|
436
|
+
const fields = [];
|
|
437
|
+
|
|
438
|
+
// Pattern 1: body: JSON.stringify({ field1, field2 })
|
|
439
|
+
const jsonStringifyPattern = /body\s*:\s*JSON\.stringify\s*\(\s*\{([^}]+)\}/;
|
|
440
|
+
const jsonMatch = config.match(jsonStringifyPattern);
|
|
441
|
+
|
|
442
|
+
if (jsonMatch) {
|
|
443
|
+
const fieldsStr = jsonMatch[1];
|
|
444
|
+
const fieldMatches = fieldsStr.match(/(\w+)\s*[,:]/g);
|
|
445
|
+
if (fieldMatches) {
|
|
446
|
+
fieldMatches.forEach(f => {
|
|
447
|
+
fields.push(f.replace(/[,:]/g, '').trim());
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Pattern 2: Look ahead in content
|
|
453
|
+
const snippet = fullContent.substring(startIndex, startIndex + 500);
|
|
454
|
+
const snippetMatch = snippet.match(/body\s*:\s*JSON\.stringify\s*\(\s*\{([^}]+)\}/);
|
|
455
|
+
|
|
456
|
+
if (snippetMatch) {
|
|
457
|
+
const fieldsStr = snippetMatch[1];
|
|
458
|
+
const fieldMatches = fieldsStr.match(/(\w+)\s*[,:]/g);
|
|
459
|
+
if (fieldMatches) {
|
|
460
|
+
fieldMatches.forEach(f => {
|
|
461
|
+
const fieldName = f.replace(/[,:]/g, '').trim();
|
|
462
|
+
if (!fields.includes(fieldName)) {
|
|
463
|
+
fields.push(fieldName);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Pattern 3: body: JSON.stringify(variableName) - trace the variable
|
|
470
|
+
const varPattern = /body\s*:\s*JSON\.stringify\s*\(\s*(\w+)\s*\)/;
|
|
471
|
+
const varMatch = config.match(varPattern) || snippet.match(varPattern);
|
|
472
|
+
|
|
473
|
+
if (varMatch) {
|
|
474
|
+
const varName = varMatch[1];
|
|
475
|
+
|
|
476
|
+
// Look backward in fullContent to find useState initialization
|
|
477
|
+
const beforeContext = fullContent.substring(0, startIndex);
|
|
478
|
+
|
|
479
|
+
// Pattern: const [formData, setFormData] = useState({ field1: '', field2: '' })
|
|
480
|
+
const statePattern = new RegExp(`const\\s*\\[${varName},\\s*set\\w+\\]\\s*=\\s*useState\\s*\\(\\s*\\{([^}]+)\\}`, 'g');
|
|
481
|
+
const stateMatch = statePattern.exec(beforeContext);
|
|
482
|
+
|
|
483
|
+
if (stateMatch) {
|
|
484
|
+
const stateFields = stateMatch[1];
|
|
485
|
+
const stateFieldMatches = stateFields.match(/(\w+)\s*:/g);
|
|
486
|
+
if (stateFieldMatches) {
|
|
487
|
+
stateFieldMatches.forEach(f => {
|
|
488
|
+
const fieldName = f.replace(':', '').trim();
|
|
489
|
+
if (!fields.includes(fieldName)) {
|
|
490
|
+
fields.push(fieldName);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return fields;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Extract API path from a route that may contain URL variables
|
|
502
|
+
* Examples: "${API_URL}/api/products" -> `/api/products`
|
|
503
|
+
* "${BASE_URL}/api/users/${id}" -> `/api/users/:id`
|
|
504
|
+
*/
|
|
505
|
+
function extractApiPath(route) {
|
|
506
|
+
// Try to extract /api/... path (stop at query params or template literal end)
|
|
507
|
+
const apiMatch = route.match(/\/api\/[^\s?`'"()]*/);
|
|
508
|
+
if (apiMatch) {
|
|
509
|
+
let apiPath = apiMatch[0];
|
|
510
|
+
// Replace remaining ${...} with :id for parameterized routes
|
|
511
|
+
apiPath = apiPath.replace(/\$\{[^}]+\}/g, ':id');
|
|
512
|
+
return apiPath;
|
|
513
|
+
}
|
|
514
|
+
// If no /api/ found, return as-is (might be relative path)
|
|
515
|
+
return route;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Clean and normalize route paths
|
|
520
|
+
*/
|
|
521
|
+
function cleanRoute(route) {
|
|
522
|
+
// First extract API path to remove URL prefixes like ${API_URL}
|
|
523
|
+
let cleaned = extractApiPath(route);
|
|
524
|
+
|
|
525
|
+
// Remove query parameters for base route
|
|
526
|
+
cleaned = cleaned.split('?')[0];
|
|
527
|
+
|
|
528
|
+
// Ensure leading slash
|
|
529
|
+
if (!cleaned.startsWith('/')) {
|
|
530
|
+
cleaned = '/' + cleaned;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return cleaned;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Deduplicate API calls
|
|
538
|
+
*/
|
|
539
|
+
function deduplicateApiCalls(calls) {
|
|
540
|
+
const seen = new Set();
|
|
541
|
+
const unique = [];
|
|
542
|
+
|
|
543
|
+
for (const call of calls) {
|
|
544
|
+
const key = `${call.method}:${call.route}`;
|
|
545
|
+
|
|
546
|
+
if (!seen.has(key)) {
|
|
547
|
+
seen.add(key);
|
|
548
|
+
unique.push(call);
|
|
549
|
+
} else {
|
|
550
|
+
// Merge fields if same endpoint
|
|
551
|
+
const existing = unique.find(c => c.method === call.method && c.route === call.route);
|
|
552
|
+
if (existing && call.fields) {
|
|
553
|
+
call.fields.forEach(f => {
|
|
554
|
+
if (!existing.fields.includes(f)) {
|
|
555
|
+
existing.fields.push(f);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Merge query params
|
|
561
|
+
if (call.queryParams && existing) {
|
|
562
|
+
if (!existing.queryParams) existing.queryParams = [];
|
|
563
|
+
call.queryParams.forEach(p => {
|
|
564
|
+
if (!existing.queryParams.includes(p)) {
|
|
565
|
+
existing.queryParams.push(p);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return unique;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Smart Route Mapping
|
|
577
|
+
* Maps detected API calls to CRUD operations
|
|
578
|
+
*/
|
|
579
|
+
|
|
580
|
+
export function generateRoutesFromAPICalls(apiCalls) {
|
|
581
|
+
const routeMap = new Map();
|
|
582
|
+
|
|
583
|
+
for (const call of apiCalls) {
|
|
584
|
+
// Extract base resource from route (e.g., /auth/signup -> auth)
|
|
585
|
+
const baseResource = getBaseResource(call.route);
|
|
586
|
+
const key = baseResource;
|
|
587
|
+
|
|
588
|
+
if (!routeMap.has(key)) {
|
|
589
|
+
routeMap.set(key, {
|
|
590
|
+
route: `/${baseResource}`,
|
|
591
|
+
model: generateModelName(baseResource),
|
|
592
|
+
operations: [],
|
|
593
|
+
subRoutes: new Set()
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const routeInfo = routeMap.get(key);
|
|
598
|
+
|
|
599
|
+
// Track sub-routes (e.g., /auth/signup, /auth/login)
|
|
600
|
+
routeInfo.subRoutes.add(call.route);
|
|
601
|
+
|
|
602
|
+
routeInfo.operations.push({
|
|
603
|
+
method: call.method,
|
|
604
|
+
route: call.route,
|
|
605
|
+
fields: call.fields,
|
|
606
|
+
description: getOperationDescription(call.method)
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return Array.from(routeMap.values());
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function getBaseResource(route) {
|
|
614
|
+
// /auth/signup -> auth
|
|
615
|
+
// /users -> users
|
|
616
|
+
// /api/products/:id -> products
|
|
617
|
+
return route
|
|
618
|
+
.replace(/^\/+/, '') // Remove leading slashes
|
|
619
|
+
.split('/')[0] // Get first segment
|
|
620
|
+
.replace(/[^\w]/g, ''); // Remove special chars
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function generateModelName(route) {
|
|
624
|
+
// auth -> Auth, users -> User, products -> Product
|
|
625
|
+
const clean = route
|
|
626
|
+
.replace(/^\//, '')
|
|
627
|
+
.split('/')[0]
|
|
628
|
+
.replace(/s$/, ''); // Remove trailing s
|
|
629
|
+
|
|
630
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function getOperationDescription(method) {
|
|
634
|
+
const descriptions = {
|
|
635
|
+
'GET': 'Fetch data',
|
|
636
|
+
'POST': 'Create new',
|
|
637
|
+
'PUT': 'Update data',
|
|
638
|
+
'PATCH': 'Partial update',
|
|
639
|
+
'DELETE': 'Remove data'
|
|
640
|
+
};
|
|
641
|
+
return descriptions[method] || method;
|
|
642
|
+
}
|