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,1125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Connection Module
|
|
3
|
+
* Automatically connects generated backend with React frontend
|
|
4
|
+
* - Scans React components for API calls and field names
|
|
5
|
+
* - Scans backend routes
|
|
6
|
+
* - Detects mismatches (URLs, field names, response structure)
|
|
7
|
+
* - Auto-fixes components and creates .env
|
|
8
|
+
* - 100% Offline mode - No AI, No Network
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import ora from 'ora';
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// MAIN CONNECT FUNCTION
|
|
18
|
+
// ============================================================
|
|
19
|
+
export async function connectFrontendBackend(projectPath) {
|
|
20
|
+
try {
|
|
21
|
+
console.log(chalk.cyan('\n🔗 offbyt Auto-Connection Engine\n'));
|
|
22
|
+
console.log(chalk.gray('Scanner: Frontend → Backend Connection Analysis\n'));
|
|
23
|
+
|
|
24
|
+
const backendPath = path.join(projectPath, 'backend');
|
|
25
|
+
|
|
26
|
+
// Verify project structure
|
|
27
|
+
if (!fs.existsSync(backendPath)) {
|
|
28
|
+
console.error(chalk.red('❌ Backend not found. Run "offbyt generate" first.\n'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Step 1: Scan React components
|
|
33
|
+
const step1 = ora('Step 1/5: Scanning React components...').start();
|
|
34
|
+
const reactAnalysis = scanReactComponents(projectPath);
|
|
35
|
+
step1.succeed(`✅ Found ${reactAnalysis.components.length} API components`);
|
|
36
|
+
|
|
37
|
+
// Step 2: Scan backend structure
|
|
38
|
+
const step2 = ora('Step 2/5: Analyzing backend routes...').start();
|
|
39
|
+
const backendAnalysis = scanBackendRoutes(backendPath);
|
|
40
|
+
step2.succeed(`✅ Found ${backendAnalysis.routes.length} backend routes`);
|
|
41
|
+
|
|
42
|
+
// Step 3: Detect mismatches
|
|
43
|
+
const step3 = ora('Step 3/5: Analyzing mismatches...').start();
|
|
44
|
+
const mismatches = detectMismatches(reactAnalysis, backendAnalysis);
|
|
45
|
+
step3.succeed(`✅ Detected ${mismatches.issues.length} issues`);
|
|
46
|
+
|
|
47
|
+
// Step 4: Auto-fix issues
|
|
48
|
+
const step4 = ora('Step 4/5: Auto-fixing components...').start();
|
|
49
|
+
const fixResults = applyAutoFixes(projectPath, backendPath, mismatches, reactAnalysis, backendAnalysis);
|
|
50
|
+
step4.succeed(`✅ Fixed ${fixResults.fixed} issues`);
|
|
51
|
+
|
|
52
|
+
// Detect Vite
|
|
53
|
+
let isVite = false;
|
|
54
|
+
try {
|
|
55
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
56
|
+
if (fs.existsSync(pkgPath)) {
|
|
57
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
58
|
+
if (pkg.dependencies?.vite || pkg.devDependencies?.vite) {
|
|
59
|
+
isVite = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch(e) {}
|
|
63
|
+
|
|
64
|
+
// Step 5: Create/Update .env
|
|
65
|
+
const step5 = ora('Step 5/5: Configuring environment...').start();
|
|
66
|
+
createFrontendEnv(projectPath, backendAnalysis.serverPort, isVite);
|
|
67
|
+
step5.succeed('✅ .env file configured');
|
|
68
|
+
|
|
69
|
+
// Summary
|
|
70
|
+
console.log(chalk.green('\n✅ Auto-Connection Complete!\n'));
|
|
71
|
+
console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
72
|
+
console.log(chalk.white(' Components Updated:'), reactAnalysis.components.length);
|
|
73
|
+
console.log(chalk.white(' Issues Fixed:'), fixResults.fixed);
|
|
74
|
+
console.log(chalk.white(' API URL Configured:'), `http://localhost:${backendAnalysis.serverPort}`);
|
|
75
|
+
console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
76
|
+
|
|
77
|
+
if (mismatches.issues.length > 0) {
|
|
78
|
+
console.log(chalk.yellow('⚠️ Issues Found (Auto-Fixed):\n'));
|
|
79
|
+
mismatches.issues.forEach((issue, i) => {
|
|
80
|
+
console.log(chalk.gray(` ${i+1}. ${issue.type}`));
|
|
81
|
+
console.log(chalk.gray(` File: ${issue.file}`));
|
|
82
|
+
console.log(chalk.gray(` Details: ${issue.detail}\n`));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(chalk.cyan('🚀 Ready to start:\n'));
|
|
87
|
+
console.log(chalk.white(' Frontend:', chalk.bold(`npm start`)));
|
|
88
|
+
console.log(chalk.white(' Backend:', chalk.bold(`npm start`)));
|
|
89
|
+
console.log(chalk.cyan('\n'));
|
|
90
|
+
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(chalk.red('❌ Connection Error:', error.message));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================
|
|
98
|
+
// SCANNERS
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
function scanReactComponents(projectPath) {
|
|
102
|
+
const components = [];
|
|
103
|
+
const candidateDirs = [
|
|
104
|
+
path.join(projectPath, 'src'),
|
|
105
|
+
path.join(projectPath, 'public'),
|
|
106
|
+
path.join(projectPath, 'frontend'),
|
|
107
|
+
path.join(projectPath, 'client')
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
let files = [];
|
|
111
|
+
|
|
112
|
+
for (const dir of candidateDirs) {
|
|
113
|
+
if (fs.existsSync(dir)) {
|
|
114
|
+
files.push(...getAllFilesRecursive(dir));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
files.push(...getTopLevelFiles(projectPath));
|
|
119
|
+
|
|
120
|
+
files = Array.from(new Set(files)).filter(file =>
|
|
121
|
+
file.endsWith('.jsx') ||
|
|
122
|
+
file.endsWith('.js') ||
|
|
123
|
+
file.endsWith('.ts') ||
|
|
124
|
+
file.endsWith('.tsx') ||
|
|
125
|
+
file.endsWith('.html')
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (files.length === 0) {
|
|
129
|
+
return { components: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
files.forEach(file => {
|
|
133
|
+
try {
|
|
134
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
135
|
+
|
|
136
|
+
// Extract API calls
|
|
137
|
+
const apiCalls = extractApiCalls(content);
|
|
138
|
+
|
|
139
|
+
// Extract form field names (inputs, setState, etc)
|
|
140
|
+
const fieldNames = extractFormFields(content);
|
|
141
|
+
|
|
142
|
+
// Extract response parsing patterns
|
|
143
|
+
const responsePatterns = extractResponsePatterns(content);
|
|
144
|
+
|
|
145
|
+
if (apiCalls.length > 0 || fieldNames.length > 0) {
|
|
146
|
+
components.push({
|
|
147
|
+
file: path.relative(projectPath, file),
|
|
148
|
+
apiCalls,
|
|
149
|
+
fieldNames,
|
|
150
|
+
responsePatterns,
|
|
151
|
+
content
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.warn(chalk.gray(` ⚠️ Skipped: ${path.relative(projectPath, file)}`));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return { components };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function scanBackendRoutes(backendPath) {
|
|
163
|
+
const routes = [];
|
|
164
|
+
let serverPort = 5000;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Get port from server.js or main.ts (NestJS)
|
|
168
|
+
const serverFile = path.join(backendPath, 'server.js');
|
|
169
|
+
const mainFile = path.join(backendPath, 'main.ts');
|
|
170
|
+
|
|
171
|
+
if (fs.existsSync(serverFile)) {
|
|
172
|
+
const serverContent = fs.readFileSync(serverFile, 'utf8');
|
|
173
|
+
const portMatch = serverContent.match(/PORT["\s]*=\s*['"]?(\d+)['"]?/);
|
|
174
|
+
if (portMatch) serverPort = parseInt(portMatch[1]);
|
|
175
|
+
|
|
176
|
+
const portEnvMatch = serverContent.match(/process\.env\.PORT\s*\|\|\s*(\d+)/);
|
|
177
|
+
if (portEnvMatch) serverPort = parseInt(portEnvMatch[1]);
|
|
178
|
+
}
|
|
179
|
+
else if (fs.existsSync(mainFile)) {
|
|
180
|
+
const mainContent = fs.readFileSync(mainFile, 'utf8');
|
|
181
|
+
const portMatch = mainContent.match(/PORT["\s]*=\s*['"]?(\d+)['"]?/);
|
|
182
|
+
if (portMatch) serverPort = parseInt(portMatch[1]);
|
|
183
|
+
|
|
184
|
+
const portEnvMatch = mainContent.match(/process\.env\.PORT\s*\|\|\s*(\d+)/);
|
|
185
|
+
if (portEnvMatch) serverPort = parseInt(portEnvMatch[1]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Scan routes
|
|
189
|
+
const routesDir = path.join(backendPath, 'routes');
|
|
190
|
+
if (fs.existsSync(routesDir)) {
|
|
191
|
+
const routeFiles = fs.readdirSync(routesDir).filter(f => f.endsWith('.js') || f.endsWith('.ts'));
|
|
192
|
+
|
|
193
|
+
routeFiles.forEach(file => {
|
|
194
|
+
const filePath = path.join(routesDir, file);
|
|
195
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
196
|
+
const resourceName = file.replace(/\.routes\.(js|ts)$/i, '').replace(/\.(js|ts)$/i, '');
|
|
197
|
+
|
|
198
|
+
// Extract route definitions (supports multiline route declarations)
|
|
199
|
+
const routeRegex = /(?:router|app|fastify)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
200
|
+
let routeMatch;
|
|
201
|
+
while ((routeMatch = routeRegex.exec(content)) !== null) {
|
|
202
|
+
const method = routeMatch[1].toUpperCase();
|
|
203
|
+
const routePath = routeMatch[2];
|
|
204
|
+
|
|
205
|
+
routes.push({
|
|
206
|
+
method,
|
|
207
|
+
path: routePath,
|
|
208
|
+
file,
|
|
209
|
+
fullPath: buildApiRoutePath(resourceName, routePath)
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Scan NestJS controllers (TypeScript with decorators)
|
|
216
|
+
const controllersDir = path.join(backendPath, 'controllers');
|
|
217
|
+
if (fs.existsSync(controllersDir)) {
|
|
218
|
+
const controllerFiles = fs.readdirSync(controllersDir).filter(f => f.endsWith('.controller.ts'));
|
|
219
|
+
|
|
220
|
+
controllerFiles.forEach(file => {
|
|
221
|
+
const filePath = path.join(controllersDir, file);
|
|
222
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
223
|
+
|
|
224
|
+
// Extract @Controller('resource') decorator
|
|
225
|
+
const controllerMatch = content.match(/@Controller\(['"`]([^'"`]+)['"`]\)/);
|
|
226
|
+
const baseRoute = controllerMatch ? controllerMatch[1] : '';
|
|
227
|
+
|
|
228
|
+
// Extract @Get(), @Post(), @Put(), @Delete(), @Patch() decorators
|
|
229
|
+
const methodRegex = /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]?([^'"`\)]*?)['"`]?\s*\)/gi;
|
|
230
|
+
let methodMatch;
|
|
231
|
+
|
|
232
|
+
while ((methodMatch = methodRegex.exec(content)) !== null) {
|
|
233
|
+
const method = methodMatch[1].toUpperCase();
|
|
234
|
+
const routePath = methodMatch[2] || '';
|
|
235
|
+
|
|
236
|
+
// Build full path: /api/{baseRoute}/{routePath}
|
|
237
|
+
let fullPath = `/api/` + baseRoute;
|
|
238
|
+
if (routePath) {
|
|
239
|
+
// Handle :id parameters
|
|
240
|
+
const cleanPath = routePath.replace(/^\//, '');
|
|
241
|
+
fullPath += '/' + cleanPath;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
routes.push({
|
|
245
|
+
method,
|
|
246
|
+
path: routePath || '/',
|
|
247
|
+
file,
|
|
248
|
+
fullPath: fullPath
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Scan models for field names
|
|
255
|
+
const models = [];
|
|
256
|
+
const modelsDir = path.join(backendPath, 'models');
|
|
257
|
+
if (fs.existsSync(modelsDir)) {
|
|
258
|
+
const modelFiles = fs.readdirSync(modelsDir).filter(f => f.endsWith('.js') || f.endsWith('.ts'));
|
|
259
|
+
|
|
260
|
+
modelFiles.forEach(file => {
|
|
261
|
+
const filePath = path.join(modelsDir, file);
|
|
262
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
263
|
+
const fields = extractMongooseFields(content);
|
|
264
|
+
|
|
265
|
+
models.push({
|
|
266
|
+
name: file.replace(/\.(js|ts)$/, ''),
|
|
267
|
+
file: file,
|
|
268
|
+
fields: fields
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Scan NestJS entities (TypeORM)
|
|
274
|
+
const entitiesDir = path.join(backendPath, 'entities');
|
|
275
|
+
if (fs.existsSync(entitiesDir)) {
|
|
276
|
+
const entityFiles = fs.readdirSync(entitiesDir).filter(f => f.endsWith('.entity.ts'));
|
|
277
|
+
|
|
278
|
+
entityFiles.forEach(file => {
|
|
279
|
+
const filePath = path.join(entitiesDir, file);
|
|
280
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
281
|
+
|
|
282
|
+
// Extract fields from @Column() decorators
|
|
283
|
+
const fieldRegex = /@Column\([^)]*\)\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g;
|
|
284
|
+
const fields = [];
|
|
285
|
+
let fieldMatch;
|
|
286
|
+
|
|
287
|
+
while ((fieldMatch = fieldRegex.exec(content)) !== null) {
|
|
288
|
+
fields.push(fieldMatch[1]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Also extract @PrimaryGeneratedColumn
|
|
292
|
+
if (content.includes('@PrimaryGeneratedColumn')) {
|
|
293
|
+
const idMatch = content.match(/@PrimaryGeneratedColumn\([^)]*\)\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/);
|
|
294
|
+
if (idMatch && !fields.includes(idMatch[1])) {
|
|
295
|
+
fields.unshift(idMatch[1]); // Add id at the beginning
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
models.push({
|
|
300
|
+
name: file.replace('.entity.ts', ''),
|
|
301
|
+
file: file,
|
|
302
|
+
fields: fields
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { routes, models, serverPort };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.warn(chalk.yellow(`⚠️ Backend scan error: ${error.message}`));
|
|
310
|
+
return { routes: [], models: [], serverPort: 5000 };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================
|
|
315
|
+
// MISMATCH DETECTION
|
|
316
|
+
// ============================================================
|
|
317
|
+
|
|
318
|
+
function detectMismatches(reactAnalysis, backendAnalysis) {
|
|
319
|
+
const issues = [];
|
|
320
|
+
|
|
321
|
+
reactAnalysis.components.forEach(component => {
|
|
322
|
+
// Check 1: API URLs (should use environment variable)
|
|
323
|
+
component.apiCalls.forEach(call => {
|
|
324
|
+
// If URL is hardcoded /api/... without env variable, flag it
|
|
325
|
+
if (call.url.startsWith(`/api`) && !call.usesEnvVar) {
|
|
326
|
+
issues.push({
|
|
327
|
+
type: 'Hardcoded API URL',
|
|
328
|
+
file: component.file,
|
|
329
|
+
detail: `URL "${call.url}" should use environment variable`,
|
|
330
|
+
fix: 'url_format',
|
|
331
|
+
component: component.file,
|
|
332
|
+
apiCall: call
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// If URL doesn't have env variable and not http/https
|
|
336
|
+
else if (!call.usesEnvVar && !call.url.includes('http')) {
|
|
337
|
+
if (!call.url.startsWith('/')) {
|
|
338
|
+
issues.push({
|
|
339
|
+
type: 'Invalid API URL Format',
|
|
340
|
+
file: component.file,
|
|
341
|
+
detail: `URL should start with / or use env variable: ${call.url}`,
|
|
342
|
+
fix: 'url_format',
|
|
343
|
+
component: component.file,
|
|
344
|
+
apiCall: call
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Check 2: Route exists in backend
|
|
350
|
+
const cleanUrl = call.url
|
|
351
|
+
.replace(/`\$\{process\.env\.REACT_APP_API_URL\}/g, '')
|
|
352
|
+
.replace(/`\$\{import\.meta\.env\.VITE_API_URL\}/g, '')
|
|
353
|
+
.replace(/`/g, '');
|
|
354
|
+
|
|
355
|
+
const apiPath = cleanUrl.startsWith(`/api`)
|
|
356
|
+
? cleanUrl
|
|
357
|
+
: `/api${cleanUrl.startsWith('/') ? '' : '/'}${cleanUrl}`;
|
|
358
|
+
|
|
359
|
+
const normalizedTarget = canonicalRouteMatchPath(apiPath);
|
|
360
|
+
const routeExists = backendAnalysis.routes.some(route =>
|
|
361
|
+
canonicalRouteMatchPath(route.fullPath) === normalizedTarget
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (!routeExists && !isAuthUrl(apiPath)) {
|
|
365
|
+
issues.push({
|
|
366
|
+
type: 'Missing Backend Route',
|
|
367
|
+
file: component.file,
|
|
368
|
+
detail: `API endpoint "${apiPath}" not found in backend routes`,
|
|
369
|
+
fix: 'missing_route',
|
|
370
|
+
component: component.file,
|
|
371
|
+
apiPath: apiPath,
|
|
372
|
+
method: call.method || 'GET'
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Check 3: Field name mismatches
|
|
378
|
+
component.fieldNames.forEach(field => {
|
|
379
|
+
const backendHasField = backendAnalysis.models.some(model =>
|
|
380
|
+
model.fields.some(f => f.name === field.name)
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (!backendHasField && field.name !== 'password' && field.name !== 'email') {
|
|
384
|
+
// Try fuzzy matching (e.g., fullname vs name)
|
|
385
|
+
const similarField = backendAnalysis.models.flatMap(m => m.fields)
|
|
386
|
+
.find(f => isSimilarField(f.name, field.name));
|
|
387
|
+
|
|
388
|
+
if (similarField) {
|
|
389
|
+
issues.push({
|
|
390
|
+
type: 'Field Name Mismatch',
|
|
391
|
+
file: component.file,
|
|
392
|
+
detail: `"${field.name}" should be "${similarField.name}"`,
|
|
393
|
+
fix: 'field_name',
|
|
394
|
+
component: component.file,
|
|
395
|
+
oldField: field.name,
|
|
396
|
+
newField: similarField.name
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Check 4: Response parsing patterns
|
|
403
|
+
if (component.responsePatterns.length > 0) {
|
|
404
|
+
// Infer expected structure from response patterns
|
|
405
|
+
component.responsePatterns.forEach(pattern => {
|
|
406
|
+
if (pattern.accessPath.includes('[0]') || pattern.accessPath.includes('.data.data')) {
|
|
407
|
+
issues.push({
|
|
408
|
+
type: 'Response Structure Issue',
|
|
409
|
+
file: component.file,
|
|
410
|
+
detail: `Possible nested response structure: ${pattern.accessPath}`,
|
|
411
|
+
fix: 'response_structure',
|
|
412
|
+
component: component.file,
|
|
413
|
+
pattern
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return { issues };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Helper functions for route matching
|
|
425
|
+
*/
|
|
426
|
+
function normalizeRoutePath(path) {
|
|
427
|
+
if (!path) return '/';
|
|
428
|
+
|
|
429
|
+
let normalized = String(path)
|
|
430
|
+
.trim()
|
|
431
|
+
.replace(/["'`]/g, '')
|
|
432
|
+
.replace(/\$\{[^}]+\}/g, ':id')
|
|
433
|
+
.replace(/\/+/g, '/');
|
|
434
|
+
|
|
435
|
+
if (!normalized.startsWith('/')) normalized = `/${normalized}`;
|
|
436
|
+
normalized = normalized.toLowerCase();
|
|
437
|
+
|
|
438
|
+
if (normalized.length > 1) {
|
|
439
|
+
normalized = normalized.replace(/\/+$/g, '');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return normalized || '/';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function isAuthUrl(path) {
|
|
446
|
+
return normalizeRoutePath(path).includes('/auth/');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function canonicalRouteMatchPath(routePath) {
|
|
450
|
+
return normalizeRoutePath(routePath)
|
|
451
|
+
.replace(/\/:([^/]+)/g, '/:id')
|
|
452
|
+
.replace(/\/(\d+)(?=\/|$)/g, '/:id')
|
|
453
|
+
.replace(/\/([0-9a-f]{24})(?=\/|$)/gi, '/:id');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function buildApiRoutePath(resourceName, routePath) {
|
|
457
|
+
const cleanResource = resourceName.replace(/\.routes$/i, '');
|
|
458
|
+
const normalizedRoute = normalizeRoutePath(routePath);
|
|
459
|
+
|
|
460
|
+
if (normalizedRoute.startsWith(`/api/`)) {
|
|
461
|
+
return normalizedRoute;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (normalizedRoute === '/') {
|
|
465
|
+
return normalizeRoutePath(`/api/${cleanResource}`);
|
|
466
|
+
}
|
|
467
|
+
return normalizeRoutePath(`/api/${cleanResource}${normalizedRoute}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
// ============================================================
|
|
473
|
+
// AUTO-FIXES
|
|
474
|
+
// ============================================================
|
|
475
|
+
|
|
476
|
+
function applyAutoFixes(projectPath, backendPath, mismatches, reactAnalysis, backendAnalysis) {
|
|
477
|
+
let fixedCount = 0;
|
|
478
|
+
|
|
479
|
+
// Detect Vite
|
|
480
|
+
let isVite = false;
|
|
481
|
+
try {
|
|
482
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
483
|
+
if (fs.existsSync(pkgPath)) {
|
|
484
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
485
|
+
if (pkg.dependencies?.vite || pkg.devDependencies?.vite) {
|
|
486
|
+
isVite = true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch(e) {}
|
|
490
|
+
|
|
491
|
+
mismatches.issues.forEach(issue => {
|
|
492
|
+
try {
|
|
493
|
+
if (issue.fix === 'missing_route') {
|
|
494
|
+
// Auto-generate missing routes
|
|
495
|
+
const { resourceName, routePath, method } = parseApiPath(issue.apiPath);
|
|
496
|
+
const routeFile = path.join(backendPath, 'routes', `${resourceName}.routes.js`);
|
|
497
|
+
|
|
498
|
+
if (resourceName && fs.existsSync(routeFile) && !isUnsafeAutoRoute(routePath)) {
|
|
499
|
+
let content = fs.readFileSync(routeFile, 'utf8');
|
|
500
|
+
|
|
501
|
+
const isExpressStyleRouteFile =
|
|
502
|
+
/express\.Router\(/i.test(content) ||
|
|
503
|
+
/const\s+router\s*=\s*express\.Router\(/i.test(content) ||
|
|
504
|
+
/export\s+default\s+router\s*;/i.test(content);
|
|
505
|
+
|
|
506
|
+
// Only inject auto-routes into Express router files.
|
|
507
|
+
if (!isExpressStyleRouteFile) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const methodLower = (method || 'GET').toLowerCase();
|
|
512
|
+
const alreadyExistsRegex = new RegExp(
|
|
513
|
+
`router\\.${methodLower}\\s*\\(\\s*['"]${escapeRegex(routePath)}['"]`,
|
|
514
|
+
'i'
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (!alreadyExistsRegex.test(content)) {
|
|
518
|
+
const newRoute = generateMissingRoute(routePath, method, resourceName);
|
|
519
|
+
|
|
520
|
+
if (newRoute) {
|
|
521
|
+
// Insert new route before the export statement
|
|
522
|
+
if (/export\s+default\s+router\s*;/.test(content)) {
|
|
523
|
+
content = content.replace(/export\s+default\s+router\s*;/, `${newRoute}\n\nexport default router;`);
|
|
524
|
+
} else {
|
|
525
|
+
content = `${content}\n${newRoute}\n`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fs.writeFileSync(routeFile, content, 'utf8');
|
|
529
|
+
fixedCount++;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (issue.fix === 'field_name') {
|
|
536
|
+
// Fix field name in component
|
|
537
|
+
const filePath = path.join(projectPath, issue.component);
|
|
538
|
+
if (fs.existsSync(filePath)) {
|
|
539
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
540
|
+
const regex = new RegExp(`\\b${escapeRegex(issue.oldField)}\\b`, 'g');
|
|
541
|
+
|
|
542
|
+
// Only replace in API payloads and form context, not in comments
|
|
543
|
+
content = content.replace(regex, (match) => {
|
|
544
|
+
// Simple heuristic: if surrounded by quotes or brackets, it's likely a data field
|
|
545
|
+
return match;
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// More precise: only replace in specific contexts (object literals, formData)
|
|
549
|
+
content = replaceFieldNameInContext(content, issue.oldField, issue.newField);
|
|
550
|
+
|
|
551
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
552
|
+
fixedCount++;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (issue.fix === 'response_structure') {
|
|
557
|
+
// Fix response parsing
|
|
558
|
+
const filePath = path.join(projectPath, issue.component);
|
|
559
|
+
if (fs.existsSync(filePath)) {
|
|
560
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
561
|
+
|
|
562
|
+
// Auto-detect and fix common response structure issues
|
|
563
|
+
content = fixPartialUpdateMethods(content);
|
|
564
|
+
content = fixResponseParsing(content);
|
|
565
|
+
|
|
566
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
567
|
+
fixedCount++;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (issue.fix === 'url_format') {
|
|
572
|
+
// Fix API URL format
|
|
573
|
+
const filePath = path.join(projectPath, issue.component);
|
|
574
|
+
if (fs.existsSync(filePath)) {
|
|
575
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
576
|
+
content = fixApiUrls(content, isVite);
|
|
577
|
+
content = fixPartialUpdateMethods(content);
|
|
578
|
+
content = fixResponseParsing(content);
|
|
579
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
580
|
+
fixedCount++;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.warn(chalk.gray(` ⚠️ Could not fix: ${issue.file} - ${error.message}`));
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Safety pass: normalize partial status updates from PUT -> PATCH
|
|
589
|
+
// This protects generated frontends that toggle completed/status fields.
|
|
590
|
+
const componentFiles = new Set(
|
|
591
|
+
(reactAnalysis.components || [])
|
|
592
|
+
.map((component) => path.join(projectPath, component.file))
|
|
593
|
+
.filter(Boolean)
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
componentFiles.forEach((filePath) => {
|
|
597
|
+
try {
|
|
598
|
+
if (!fs.existsSync(filePath)) return;
|
|
599
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
600
|
+
const updated = fixPartialUpdateMethods(original);
|
|
601
|
+
if (updated !== original) {
|
|
602
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
603
|
+
fixedCount++;
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
// Ignore file-level rewrite failures
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
return { fixed: fixedCount };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function fixResponseParsing(content) {
|
|
614
|
+
// Fix common response parsing patterns
|
|
615
|
+
|
|
616
|
+
// Pattern 1: res.data.data.token → res.data.token (if response is {data: {...}})
|
|
617
|
+
content = content.replace(/data\.data\.(\w+)/g, 'data.$1');
|
|
618
|
+
|
|
619
|
+
// Pattern 2: response.data.data.user → response.data.user
|
|
620
|
+
content = content.replace(/response\.data\.data\.(\w+)/g, 'response.data.$1');
|
|
621
|
+
|
|
622
|
+
// Pattern 3: const x = await response.json();
|
|
623
|
+
// Works for both raw API arrays and wrapped payloads like { success, data }
|
|
624
|
+
content = content.replace(
|
|
625
|
+
/const\s+(\w+)\s*=\s*await\s+(\w+)\.json\(\);/g,
|
|
626
|
+
(fullMatch, variableName, responseName) => {
|
|
627
|
+
if (variableName.endsWith('Response')) {
|
|
628
|
+
return fullMatch;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return `const ${variableName}Response = await ${responseName}.json();\n const ${variableName} = Array.isArray(${variableName}Response) ? ${variableName}Response : (${variableName}Response.data ?? ${variableName}Response);`;
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
return content;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function fixApiUrls(content, isVite = false) {
|
|
639
|
+
// Ensure API calls use environment variables for base URL
|
|
640
|
+
const API_URL = isVite ? 'import.meta.env.VITE_API_URL' : 'process.env.REACT_APP_API_URL';
|
|
641
|
+
|
|
642
|
+
// Pattern 1: fetch('/api/... → fetch(`/api/...
|
|
643
|
+
content = content.replace(
|
|
644
|
+
/fetch\s*\(\s*['"`](\/api\/[^'"`]+)['"`]/g,
|
|
645
|
+
`fetch(\`\${${API_URL}}$1\``
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
// Pattern 2: Template literals with /api at start: fetch(`/api/products/${id}`)
|
|
649
|
+
content = content.replace(
|
|
650
|
+
/fetch\s*\(\s*`(\/api\/[^`]+)`/g,
|
|
651
|
+
(match, url) => {
|
|
652
|
+
// If already has env variable, skip
|
|
653
|
+
if (url.includes('$') || match.includes(API_URL)) return match;
|
|
654
|
+
return `fetch(\`\${${API_URL}}${url}\``;
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
// Pattern 2b: fetch(`${API_BASE}/api/...`) -> fetch(`/api/...`)
|
|
659
|
+
content = content.replace(
|
|
660
|
+
/fetch\s*\(\s*`\$\{[^}]+\}(\/api\/[^`]+)`/g,
|
|
661
|
+
`fetch(\`\${${API_URL}}$1\``
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Pattern 3: const url = '/api/... → const url = `/api/...
|
|
665
|
+
content = content.replace(
|
|
666
|
+
/(const|let|var)\s+(\w+)\s*=\s*['"`](\/api\/[^'"`]+)['"`]/g,
|
|
667
|
+
`$1 $2 = \`\${${API_URL}}$3\``
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
// Pattern 3b: const url = `${API_BASE}/api/...` -> const url = `/api/...`
|
|
671
|
+
content = content.replace(
|
|
672
|
+
/(const|let|var)\s+(\w+)\s*=\s*`\$\{[^}]+\}(\/api\/[^`]+)`/g,
|
|
673
|
+
`$1 $2 = \`\${${API_URL}}$3\``
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// Pattern 4: axios calls and custom instances
|
|
677
|
+
content = content.replace(
|
|
678
|
+
/\b(axios|api|client|http|request)\.(get|post|put|delete|patch)\s*\(\s*['"`](\/api\/[^'"`]+)['"`]/g,
|
|
679
|
+
`$1.$2(\`\${${API_URL}}$3\``
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// Pattern 4b: axios.get(`${API_BASE}/api/...`) -> axios.get(`/api/...`)
|
|
683
|
+
content = content.replace(
|
|
684
|
+
/\b(axios|api|client|http|request)\.(get|post|put|delete|patch)\s*\(\s*`\$\{[^}]+\}(\/api\/[^`]+)`/g,
|
|
685
|
+
`$1.$2(\`\${${API_URL}}$3\``
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
return content;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function fixPartialUpdateMethods(content) {
|
|
692
|
+
// Convert fetch PUT calls to PATCH when payload appears to be status-toggle only
|
|
693
|
+
content = content.replace(/fetch\s*\(\s*[\s\S]*?\{[\s\S]*?\}\s*\)/g, (callExpression) => {
|
|
694
|
+
if (!/method\s*:\s*['"`]PUT['"`]/i.test(callExpression)) return callExpression;
|
|
695
|
+
|
|
696
|
+
const payloadMatches = [...callExpression.matchAll(/body\s*:\s*JSON\.stringify\s*\(\s*(\{[\s\S]*?\})\s*\)/gi)];
|
|
697
|
+
if (payloadMatches.length === 0) return callExpression;
|
|
698
|
+
|
|
699
|
+
const shouldPatch = payloadMatches.some((match) => isPartialStatusPayload(match[1]));
|
|
700
|
+
if (!shouldPatch) return callExpression;
|
|
701
|
+
|
|
702
|
+
return callExpression.replace(/method\s*:\s*['"`]PUT['"`]/i, "method: 'PATCH'");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// Convert axios-like .put(url, payload) to .patch(url, payload) for partial status payloads
|
|
706
|
+
content = content.replace(
|
|
707
|
+
/\b(axios|api|client|http|request)\.put\s*\(\s*([^,]+)\s*,\s*(\{[\s\S]*?\})\s*(,\s*[\s\S]*?)?\)/g,
|
|
708
|
+
(match, clientName, urlArg, payload, trailing = '') => {
|
|
709
|
+
if (!isPartialStatusPayload(payload)) return match;
|
|
710
|
+
return `${clientName}.patch(${urlArg}, ${payload}${trailing})`;
|
|
711
|
+
}
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
return content;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isPartialStatusPayload(payloadText = '') {
|
|
718
|
+
const statusLikeKeys = ['completed', 'isCompleted', 'done', 'isDone', 'status', 'state', 'isActive', 'active', 'archived'];
|
|
719
|
+
const fullUpdateKeys = ['title', 'name', 'description', 'email', 'password', 'phone', 'address', 'price', 'amount', 'content'];
|
|
720
|
+
|
|
721
|
+
const hasStatusLikeKey = statusLikeKeys.some((key) =>
|
|
722
|
+
new RegExp(`\\b${escapeRegex(key)}\\b\\s*:`, 'i').test(payloadText)
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const hasFullUpdateKey = fullUpdateKeys.some((key) =>
|
|
726
|
+
new RegExp(`\\b${escapeRegex(key)}\\b\\s*:`, 'i').test(payloadText)
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
return hasStatusLikeKey && !hasFullUpdateKey;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function replaceFieldNameInContext(content, oldField, newField) {
|
|
733
|
+
// Replace field names only in appropriate contexts
|
|
734
|
+
|
|
735
|
+
// In object literals: {"oldField": value}
|
|
736
|
+
content = content.replace(
|
|
737
|
+
new RegExp(`['"](${escapeRegex(oldField)})['"]\\s*:`, 'g'),
|
|
738
|
+
`"${newField}":`
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// In FormData: formData.append('oldField', ...)
|
|
742
|
+
content = content.replace(
|
|
743
|
+
new RegExp(`\\bappend\\s*\\(['"](${escapeRegex(oldField)})['"]`, 'g'),
|
|
744
|
+
`append('${newField}'`
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
// In object shorthand: {oldField}
|
|
748
|
+
content = content.replace(
|
|
749
|
+
new RegExp(`\\b${escapeRegex(oldField)}\\b(?=\\s*[,}])`, 'g'),
|
|
750
|
+
newField
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
return content;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function createFrontendEnv(projectPath, serverPort, isVite = false) {
|
|
757
|
+
const envPath = path.join(projectPath, '.env');
|
|
758
|
+
const envVar = isVite ? 'VITE_API_URL' : 'REACT_APP_API_URL';
|
|
759
|
+
const envContent = `${envVar}=http://localhost:${serverPort}\n`;
|
|
760
|
+
|
|
761
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ============================================================
|
|
765
|
+
// HELPER FUNCTIONS
|
|
766
|
+
// ============================================================
|
|
767
|
+
|
|
768
|
+
function getAllFilesRecursive(dirPath) {
|
|
769
|
+
let files = [];
|
|
770
|
+
|
|
771
|
+
try {
|
|
772
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
773
|
+
|
|
774
|
+
entries.forEach(entry => {
|
|
775
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
776
|
+
|
|
777
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
778
|
+
files = [...files, ...getAllFilesRecursive(fullPath)];
|
|
779
|
+
} else if (entry.isFile()) {
|
|
780
|
+
files.push(fullPath);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
// Silently ignore permission errors
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return files;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function getTopLevelFiles(dirPath) {
|
|
791
|
+
try {
|
|
792
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
793
|
+
return entries
|
|
794
|
+
.filter(entry => entry.isFile())
|
|
795
|
+
.map(entry => path.join(dirPath, entry.name));
|
|
796
|
+
} catch {
|
|
797
|
+
return [];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function extractApiCalls(content) {
|
|
802
|
+
const calls = [];
|
|
803
|
+
const isProcessEnvUrl = (raw) => /process\.env\.REACT_APP_API_URL|import\.meta\.env\.VITE_API_URL/.test(raw);
|
|
804
|
+
|
|
805
|
+
// Pattern 1: fetch with string literal (relative or absolute URL)
|
|
806
|
+
const fetchRegex = /fetch\s*\(\s*['"]([^'"\n]+)['"]/g;
|
|
807
|
+
let match;
|
|
808
|
+
|
|
809
|
+
while ((match = fetchRegex.exec(content)) !== null) {
|
|
810
|
+
const rawUrl = match[1];
|
|
811
|
+
const normalizedUrl = normalizeApiPath(rawUrl);
|
|
812
|
+
if (!normalizedUrl) continue;
|
|
813
|
+
|
|
814
|
+
calls.push({
|
|
815
|
+
type: 'fetch',
|
|
816
|
+
url: normalizedUrl,
|
|
817
|
+
usesEnvVar: isProcessEnvUrl(rawUrl)
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Pattern 2: fetch with template literal: fetch(`${API_BASE}/api/products/${id}`)
|
|
822
|
+
const fetchTemplateRegex = /fetch\s*\(\s*`([^`]+)`/g;
|
|
823
|
+
|
|
824
|
+
while ((match = fetchTemplateRegex.exec(content)) !== null) {
|
|
825
|
+
const rawUrl = match[1];
|
|
826
|
+
// Clean ${...} variables to :id
|
|
827
|
+
const cleanUrl = rawUrl.replace(/\$\{[^}]+\}/g, ':id');
|
|
828
|
+
const normalizedUrl = normalizeApiPath(cleanUrl);
|
|
829
|
+
if (!normalizedUrl) continue;
|
|
830
|
+
|
|
831
|
+
calls.push({
|
|
832
|
+
type: 'fetch',
|
|
833
|
+
url: normalizedUrl,
|
|
834
|
+
usesEnvVar: isProcessEnvUrl(rawUrl),
|
|
835
|
+
hasTemplate: true
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Pattern 3: const url = `/api/...` OR `/api/...`
|
|
840
|
+
const urlVarRegex = /(?:const|let|var)\s+(\w+)\s*=\s*['"`]([^'"`\n]+)['"`]/g;
|
|
841
|
+
const urlVars = new Map();
|
|
842
|
+
|
|
843
|
+
while ((match = urlVarRegex.exec(content)) !== null) {
|
|
844
|
+
const rawUrl = match[2];
|
|
845
|
+
const normalizedUrl = normalizeApiPath(rawUrl);
|
|
846
|
+
if (!normalizedUrl) continue;
|
|
847
|
+
|
|
848
|
+
urlVars.set(match[1], {
|
|
849
|
+
url: normalizedUrl,
|
|
850
|
+
usesEnvVar: isProcessEnvUrl(rawUrl)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Find fetch(variableName)
|
|
855
|
+
const fetchVarRegex = /fetch\s*\(\s*(\w+)\s*[,)]/g;
|
|
856
|
+
|
|
857
|
+
while ((match = fetchVarRegex.exec(content)) !== null) {
|
|
858
|
+
const varName = match[1];
|
|
859
|
+
const urlMeta = urlVars.get(varName);
|
|
860
|
+
|
|
861
|
+
if (urlMeta) {
|
|
862
|
+
calls.push({
|
|
863
|
+
type: 'fetch',
|
|
864
|
+
url: urlMeta.url,
|
|
865
|
+
usesEnvVar: urlMeta.usesEnvVar
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Pattern 4: axios calls and custom instances (relative or absolute URL)
|
|
871
|
+
const axiosRegex = /\b(?:axios|api|client|http|request)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`\n]+)['"`]/g;
|
|
872
|
+
|
|
873
|
+
while ((match = axiosRegex.exec(content)) !== null) {
|
|
874
|
+
const rawUrl = match[2];
|
|
875
|
+
const normalizedUrl = normalizeApiPath(rawUrl);
|
|
876
|
+
if (!normalizedUrl) continue;
|
|
877
|
+
|
|
878
|
+
calls.push({
|
|
879
|
+
type: 'axios',
|
|
880
|
+
method: match[1],
|
|
881
|
+
url: normalizedUrl,
|
|
882
|
+
usesEnvVar: isProcessEnvUrl(rawUrl)
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Pattern 4.5: axios calls with template literals: axios.get(`/api/products/${id}`)
|
|
887
|
+
const axiosTemplateRegex = /\b(?:axios|api|client|http|request)\.(get|post|put|delete|patch)\s*\(\s*`([^`]+)`/g;
|
|
888
|
+
|
|
889
|
+
while ((match = axiosTemplateRegex.exec(content)) !== null) {
|
|
890
|
+
const rawUrl = match[2];
|
|
891
|
+
// Clean ${...} variables to :id
|
|
892
|
+
const cleanUrl = rawUrl.replace(/\$\{[^}]+\}/g, ':id');
|
|
893
|
+
const normalizedUrl = normalizeApiPath(cleanUrl);
|
|
894
|
+
if (!normalizedUrl) continue;
|
|
895
|
+
|
|
896
|
+
calls.push({
|
|
897
|
+
type: 'axios',
|
|
898
|
+
method: match[1],
|
|
899
|
+
url: normalizedUrl,
|
|
900
|
+
usesEnvVar: isProcessEnvUrl(rawUrl),
|
|
901
|
+
hasTemplate: true
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return calls;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function normalizeApiPath(rawUrl) {
|
|
909
|
+
if (!rawUrl) return null;
|
|
910
|
+
|
|
911
|
+
const clean = rawUrl
|
|
912
|
+
.trim()
|
|
913
|
+
.replace(/\$\{process\.env\.REACT_APP_API_URL\}|\$\{import\.meta\.env\.VITE_API_URL\}/g, '')
|
|
914
|
+
.replace(/\$\{[^}]+\}/g, ':id');
|
|
915
|
+
|
|
916
|
+
if (!clean) return null;
|
|
917
|
+
|
|
918
|
+
// Relative API paths
|
|
919
|
+
if (clean.startsWith(`/api/`)) return normalizeRoutePath(clean);
|
|
920
|
+
|
|
921
|
+
// Template/absolute paths that include /api/
|
|
922
|
+
const apiIndex = clean.indexOf(`/api/`);
|
|
923
|
+
if (apiIndex >= 0) {
|
|
924
|
+
return normalizeRoutePath(clean.slice(apiIndex));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function extractFormFields(content) {
|
|
931
|
+
const fields = [];
|
|
932
|
+
|
|
933
|
+
// Pattern 1: setState({field: ...})
|
|
934
|
+
const setStateRegex = /\{(\w+):\s*[^}]*\}/g;
|
|
935
|
+
let match;
|
|
936
|
+
|
|
937
|
+
while ((match = setStateRegex.exec(content)) !== null) {
|
|
938
|
+
const fieldName = match[1];
|
|
939
|
+
if (!['data', 'error', 'loading', 'response'].includes(fieldName)) {
|
|
940
|
+
fields.push({ name: fieldName });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Pattern 2: formData.append('field', ...)
|
|
945
|
+
const formDataRegex = /append\s*\(\s*['"`](\w+)['"`]/g;
|
|
946
|
+
|
|
947
|
+
while ((match = formDataRegex.exec(content)) !== null) {
|
|
948
|
+
fields.push({ name: match[1] });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Pattern 3: Object destructuring {name, email, ...}
|
|
952
|
+
const destructRegex = /const\s*\{([^}]+)\}\s*=/g;
|
|
953
|
+
|
|
954
|
+
while ((match = destructRegex.exec(content)) !== null) {
|
|
955
|
+
const vars = match[1].split(',').map(v => v.trim().split(':')[0]);
|
|
956
|
+
vars.forEach(v => {
|
|
957
|
+
if (v && !['useState', 'useEffect'].includes(v)) {
|
|
958
|
+
fields.push({ name: v });
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Deduplicate
|
|
964
|
+
return [...new Map(fields.map(f => [f.name, f])).values()];
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function extractResponsePatterns(content) {
|
|
968
|
+
const patterns = [];
|
|
969
|
+
|
|
970
|
+
// Match response access patterns: response.data.user, data.data.token, etc.
|
|
971
|
+
const accessPatterns = /(\w+|response)(\.\w+)+/g;
|
|
972
|
+
let match;
|
|
973
|
+
|
|
974
|
+
while ((match = accessPatterns.exec(content)) !== null) {
|
|
975
|
+
patterns.push({
|
|
976
|
+
accessPath: match[0]
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return patterns;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function extractMongooseFields(content) {
|
|
984
|
+
const fields = [];
|
|
985
|
+
|
|
986
|
+
// Match Mongoose schema fields like: name: { type: String }
|
|
987
|
+
const fieldRegex = /(\w+)\s*:\s*\{?\s*type\s*:\s*(String|Number|Boolean|Date)|(\w+)\s*:\s*(String|Number|Boolean|Date)/g;
|
|
988
|
+
let match;
|
|
989
|
+
|
|
990
|
+
while ((match = fieldRegex.exec(content)) !== null) {
|
|
991
|
+
const fieldName = match[1] || match[3];
|
|
992
|
+
if (fieldName) {
|
|
993
|
+
fields.push({ name: fieldName });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Remove duplicates
|
|
998
|
+
return [...new Map(fields.map(f => [f.name, f])).values()];
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function isSimilarField(field1, field2) {
|
|
1002
|
+
// Check for common patterns
|
|
1003
|
+
const patterns = [
|
|
1004
|
+
{ old: 'fullname', new: 'name' },
|
|
1005
|
+
{ old: 'firstname', new: 'name' },
|
|
1006
|
+
{ old: 'last_name', new: 'lastname' },
|
|
1007
|
+
{ old: 'phone_number', new: 'phone' },
|
|
1008
|
+
{ old: 'user_email', new: 'email' }
|
|
1009
|
+
];
|
|
1010
|
+
|
|
1011
|
+
return patterns.some(p =>
|
|
1012
|
+
(field1.toLowerCase() === p.old && field2.toLowerCase() === p.new) ||
|
|
1013
|
+
(field1.toLowerCase() === p.new && field2.toLowerCase() === p.old)
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function escapeRegex(string) {
|
|
1018
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Parse API path to extract resource name and route path
|
|
1022
|
+
* Examples:
|
|
1023
|
+
* /api/admin/dashboard → {resourceName: 'admin', routePath: '/dashboard'}
|
|
1024
|
+
* /api/user/invoices → {resourceName: 'user', routePath: '/invoices'}
|
|
1025
|
+
* /api/products/:id → {resourceName: 'products', routePath: '/:id'}
|
|
1026
|
+
*/
|
|
1027
|
+
function parseApiPath(apiPath) {
|
|
1028
|
+
const normalizedPath = normalizeRoutePath(apiPath);
|
|
1029
|
+
if (!normalizedPath.startsWith(`/api/`)) {
|
|
1030
|
+
return { resourceName: null, routePath: null, method: 'GET' };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const parts = normalizedPath.replace(/^\/api\//, '').split('/').filter(Boolean);
|
|
1034
|
+
const resourceName = parts[0] || null;
|
|
1035
|
+
const routePath = '/' + parts.slice(1).join('/');
|
|
1036
|
+
const method = 'GET'; // Default method
|
|
1037
|
+
|
|
1038
|
+
return { resourceName, routePath: routePath === '/' ? '/' : routePath, method };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function isUnsafeAutoRoute(routePath) {
|
|
1042
|
+
const normalized = normalizeRoutePath(routePath);
|
|
1043
|
+
return normalized.includes(':') || normalized.includes('*') || normalized.includes('?') || normalized.includes('$');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Generate a missing route dynamically
|
|
1048
|
+
*/
|
|
1049
|
+
function generateMissingRoute(routePath, method, resourceName) {
|
|
1050
|
+
if (!routePath) return '';
|
|
1051
|
+
|
|
1052
|
+
const methodLower = method.toLowerCase();
|
|
1053
|
+
const cleanPath = routePath === '/' ? '/' : routePath;
|
|
1054
|
+
|
|
1055
|
+
let responseLogic = `
|
|
1056
|
+
// TODO: Implement ${routePath} logic
|
|
1057
|
+
const result = {};
|
|
1058
|
+
|
|
1059
|
+
res.status(200).json({
|
|
1060
|
+
success: true,
|
|
1061
|
+
data: result
|
|
1062
|
+
});`;
|
|
1063
|
+
|
|
1064
|
+
// Detect common patterns
|
|
1065
|
+
if (routePath.includes('dashboard') || routePath.includes('stats')) {
|
|
1066
|
+
responseLogic = `
|
|
1067
|
+
// Dashboard/Stats endpoint
|
|
1068
|
+
const stats = {
|
|
1069
|
+
total: 0,
|
|
1070
|
+
active: 0,
|
|
1071
|
+
revenue: 0
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
res.status(200).json({
|
|
1075
|
+
success: true,
|
|
1076
|
+
stats: stats
|
|
1077
|
+
});`;
|
|
1078
|
+
} else if (routePath.includes('analytics') || routePath.includes('reports')) {
|
|
1079
|
+
responseLogic = `
|
|
1080
|
+
// Analytics endpoint
|
|
1081
|
+
const analytics = {
|
|
1082
|
+
data: [],
|
|
1083
|
+
summary: {}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
res.status(200).json({
|
|
1087
|
+
success: true,
|
|
1088
|
+
analytics: analytics
|
|
1089
|
+
});`;
|
|
1090
|
+
} else if (routePath.includes('list') || routePath.includes('all')) {
|
|
1091
|
+
responseLogic = `
|
|
1092
|
+
// List endpoint
|
|
1093
|
+
const items = [];
|
|
1094
|
+
|
|
1095
|
+
res.status(200).json({
|
|
1096
|
+
success: true,
|
|
1097
|
+
count: items.length,
|
|
1098
|
+
items: items
|
|
1099
|
+
});`;
|
|
1100
|
+
} else if (method === 'POST' || method === 'PUT') {
|
|
1101
|
+
responseLogic = `
|
|
1102
|
+
// Update/Create endpoint
|
|
1103
|
+
const result = { ...req.body, _id: new Date().getTime() };
|
|
1104
|
+
|
|
1105
|
+
res.status(${method === 'POST' ? '201' : '200'}).json({
|
|
1106
|
+
success: true,
|
|
1107
|
+
message: '${routePath} processed successfully',
|
|
1108
|
+
data: result
|
|
1109
|
+
});`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return `
|
|
1113
|
+
// Auto-generated route for ${routePath}
|
|
1114
|
+
router.${methodLower}('${cleanPath}', async (req, res) => {
|
|
1115
|
+
try {${responseLogic}
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
res.status(500).json({
|
|
1118
|
+
success: false,
|
|
1119
|
+
message: 'Error processing ${routePath}',
|
|
1120
|
+
error: error.message
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
});`;
|
|
1124
|
+
}
|
|
1125
|
+
|