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,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Analyzer - Analyzes load test results and generates insights
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Analyze performance results
|
|
9
|
+
*/
|
|
10
|
+
export function analyzePerformance(results, routes) {
|
|
11
|
+
const analysis = {
|
|
12
|
+
summary: {},
|
|
13
|
+
bottlenecks: [],
|
|
14
|
+
recommendations: [],
|
|
15
|
+
scalabilityScore: 0
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Analyze each load level
|
|
19
|
+
for (const levelResult of results) {
|
|
20
|
+
const level = levelResult.level;
|
|
21
|
+
const routeResults = levelResult.routes;
|
|
22
|
+
|
|
23
|
+
let totalLatency = 0;
|
|
24
|
+
let slowRoutes = [];
|
|
25
|
+
let errorRoutes = [];
|
|
26
|
+
|
|
27
|
+
for (const routeResult of routeResults) {
|
|
28
|
+
const avgLatency = routeResult.latency.mean;
|
|
29
|
+
totalLatency += avgLatency;
|
|
30
|
+
|
|
31
|
+
// Detect slow routes
|
|
32
|
+
if (avgLatency > 500) {
|
|
33
|
+
slowRoutes.push({
|
|
34
|
+
route: routeResult.route,
|
|
35
|
+
latency: avgLatency.toFixed(2)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Detect errors
|
|
40
|
+
if (routeResult.errors > 0 || routeResult.non2xx > 0) {
|
|
41
|
+
errorRoutes.push({
|
|
42
|
+
route: routeResult.route,
|
|
43
|
+
errors: routeResult.errors,
|
|
44
|
+
non2xx: routeResult.non2xx
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const avgLatency = routeResults.length > 0 ? totalLatency / routeResults.length : 0;
|
|
50
|
+
|
|
51
|
+
analysis.summary[level] = {
|
|
52
|
+
avgLatency: avgLatency.toFixed(2),
|
|
53
|
+
slowRoutes,
|
|
54
|
+
errorRoutes
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Identify bottlenecks
|
|
58
|
+
if (avgLatency > 500) {
|
|
59
|
+
analysis.bottlenecks.push({
|
|
60
|
+
level,
|
|
61
|
+
issue: `High average latency (${avgLatency.toFixed(2)}ms)`,
|
|
62
|
+
severity: 'critical'
|
|
63
|
+
});
|
|
64
|
+
} else if (avgLatency > 200) {
|
|
65
|
+
analysis.bottlenecks.push({
|
|
66
|
+
level,
|
|
67
|
+
issue: `Moderate latency (${avgLatency.toFixed(2)}ms)`,
|
|
68
|
+
severity: 'warning'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (slowRoutes.length > 0) {
|
|
73
|
+
for (const slow of slowRoutes) {
|
|
74
|
+
analysis.bottlenecks.push({
|
|
75
|
+
level,
|
|
76
|
+
issue: `${slow.route} is slow (${slow.latency}ms)`,
|
|
77
|
+
severity: 'critical'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (errorRoutes.length > 0) {
|
|
83
|
+
for (const err of errorRoutes) {
|
|
84
|
+
analysis.bottlenecks.push({
|
|
85
|
+
level,
|
|
86
|
+
issue: `${err.route} has errors (${err.errors} errors, ${err.non2xx} non-2xx)`,
|
|
87
|
+
severity: 'critical'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate recommendations
|
|
94
|
+
analysis.recommendations = generateRecommendations(analysis);
|
|
95
|
+
|
|
96
|
+
// Calculate scalability score (0-100)
|
|
97
|
+
analysis.scalabilityScore = calculateScalabilityScore(analysis);
|
|
98
|
+
|
|
99
|
+
return analysis;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate recommendations based on analysis
|
|
104
|
+
*/
|
|
105
|
+
function generateRecommendations(analysis) {
|
|
106
|
+
const recommendations = [];
|
|
107
|
+
|
|
108
|
+
// Check if there are critical bottlenecks
|
|
109
|
+
const criticalBottlenecks = analysis.bottlenecks.filter(b => b.severity === 'critical');
|
|
110
|
+
|
|
111
|
+
if (criticalBottlenecks.length > 0) {
|
|
112
|
+
recommendations.push({
|
|
113
|
+
priority: 'high',
|
|
114
|
+
issue: 'Critical performance bottlenecks detected',
|
|
115
|
+
suggestions: [
|
|
116
|
+
'Add database indexes on frequently queried fields',
|
|
117
|
+
'Implement caching (Redis/Memcached) for read-heavy operations',
|
|
118
|
+
'Optimize database queries (avoid N+1 queries)',
|
|
119
|
+
'Use connection pooling for database connections',
|
|
120
|
+
'Consider horizontal scaling (load balancing)'
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check latency trends
|
|
126
|
+
const levels = Object.keys(analysis.summary).map(Number).sort((a, b) => a - b);
|
|
127
|
+
if (levels.length >= 2) {
|
|
128
|
+
const firstLatency = parseFloat(analysis.summary[levels[0]].avgLatency);
|
|
129
|
+
const lastLatency = parseFloat(analysis.summary[levels[levels.length - 1]].avgLatency);
|
|
130
|
+
|
|
131
|
+
if (lastLatency > firstLatency * 3) {
|
|
132
|
+
recommendations.push({
|
|
133
|
+
priority: 'high',
|
|
134
|
+
issue: 'Performance degrades significantly at scale',
|
|
135
|
+
suggestions: [
|
|
136
|
+
'Implement API rate limiting',
|
|
137
|
+
'Add request queuing for burst traffic',
|
|
138
|
+
'Scale backend horizontally (multiple instances)',
|
|
139
|
+
'Use a CDN for static assets',
|
|
140
|
+
'Implement database read replicas'
|
|
141
|
+
]
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// General optimization recommendations
|
|
147
|
+
recommendations.push({
|
|
148
|
+
priority: 'medium',
|
|
149
|
+
issue: 'General optimizations',
|
|
150
|
+
suggestions: [
|
|
151
|
+
'Enable GZIP compression for responses',
|
|
152
|
+
'Implement pagination for list endpoints',
|
|
153
|
+
'Use async/await properly to avoid blocking',
|
|
154
|
+
'Monitor database query performance',
|
|
155
|
+
'Set up proper logging and monitoring'
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return recommendations;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Calculate scalability score (0-100)
|
|
164
|
+
*/
|
|
165
|
+
function calculateScalabilityScore(analysis) {
|
|
166
|
+
let score = 100;
|
|
167
|
+
|
|
168
|
+
// Deduct for bottlenecks
|
|
169
|
+
const criticalCount = analysis.bottlenecks.filter(b => b.severity === 'critical').length;
|
|
170
|
+
const warningCount = analysis.bottlenecks.filter(b => b.severity === 'warning').length;
|
|
171
|
+
|
|
172
|
+
score -= criticalCount * 15;
|
|
173
|
+
score -= warningCount * 5;
|
|
174
|
+
|
|
175
|
+
// Deduct for high latency
|
|
176
|
+
const levels = Object.keys(analysis.summary);
|
|
177
|
+
for (const level of levels) {
|
|
178
|
+
const avgLatency = parseFloat(analysis.summary[level].avgLatency);
|
|
179
|
+
if (avgLatency > 1000) score -= 10;
|
|
180
|
+
else if (avgLatency > 500) score -= 5;
|
|
181
|
+
else if (avgLatency > 200) score -= 2;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Math.max(0, Math.min(100, score));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate formatted report
|
|
189
|
+
*/
|
|
190
|
+
export function generateReport(analysis, startupMode = false) {
|
|
191
|
+
let report = '';
|
|
192
|
+
|
|
193
|
+
report += chalk.bold.cyan('\nâ•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—\n');
|
|
194
|
+
report += chalk.bold.cyan('â•' offbyt SCALABILITY REPORT â•'\n');
|
|
195
|
+
report += chalk.bold.cyan('╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•\n\n');
|
|
196
|
+
|
|
197
|
+
// Scalability Score
|
|
198
|
+
const score = analysis.scalabilityScore;
|
|
199
|
+
let scoreColor = chalk.green;
|
|
200
|
+
let scoreLabel = 'Excellent';
|
|
201
|
+
|
|
202
|
+
if (score < 50) {
|
|
203
|
+
scoreColor = chalk.red;
|
|
204
|
+
scoreLabel = 'Poor';
|
|
205
|
+
} else if (score < 70) {
|
|
206
|
+
scoreColor = chalk.yellow;
|
|
207
|
+
scoreLabel = 'Fair';
|
|
208
|
+
} else if (score < 85) {
|
|
209
|
+
scoreColor = chalk.blue;
|
|
210
|
+
scoreLabel = 'Good';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
report += chalk.bold('📊 Scalability Score: ') + scoreColor.bold(`${score}/100 (${scoreLabel})`) + '\n\n';
|
|
214
|
+
|
|
215
|
+
// Summary for each load level
|
|
216
|
+
report += chalk.bold.yellow('📈 Performance Summary:\n\n');
|
|
217
|
+
|
|
218
|
+
const levels = Object.keys(analysis.summary).map(Number).sort((a, b) => a - b);
|
|
219
|
+
for (const level of levels) {
|
|
220
|
+
const summary = analysis.summary[level];
|
|
221
|
+
const latency = parseFloat(summary.avgLatency);
|
|
222
|
+
|
|
223
|
+
let icon = '✅';
|
|
224
|
+
let color = chalk.green;
|
|
225
|
+
|
|
226
|
+
if (latency > 500) {
|
|
227
|
+
icon = 'âŒ';
|
|
228
|
+
color = chalk.red;
|
|
229
|
+
} else if (latency > 200) {
|
|
230
|
+
icon = 'âš ï¸ ';
|
|
231
|
+
color = chalk.yellow;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
report += color(`${icon} ${level} concurrent users → Avg latency: ${summary.avgLatency}ms\n`);
|
|
235
|
+
|
|
236
|
+
if (summary.slowRoutes.length > 0) {
|
|
237
|
+
for (const slow of summary.slowRoutes) {
|
|
238
|
+
report += color(` └─ ${slow.route} is slow (${slow.latency}ms)\n`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
report += '\n';
|
|
244
|
+
|
|
245
|
+
// Startup Growth Simulation (if enabled)
|
|
246
|
+
if (startupMode) {
|
|
247
|
+
report += chalk.bold.magenta('🚀 Startup Growth Simulation:\n\n');
|
|
248
|
+
report += chalk.gray('Month 1 → 100 users ✅ Stable\n');
|
|
249
|
+
report += chalk.gray('Month 3 → 1,000 users ✅ Stable\n');
|
|
250
|
+
|
|
251
|
+
if (score < 70) {
|
|
252
|
+
report += chalk.yellow('Month 6 → 10,000 users âš ï¸ Performance issues expected\n');
|
|
253
|
+
report += chalk.red('Month 12 → 100,000 users ⌠System will struggle\n\n');
|
|
254
|
+
report += chalk.red.bold('âš ï¸ System needs optimization before scaling to 10k+ users\n');
|
|
255
|
+
} else {
|
|
256
|
+
report += chalk.green('Month 6 → 10,000 users ✅ Stable\n');
|
|
257
|
+
report += chalk.cyan('Month 12 → 100,000 users 🔵 Consider scaling plan\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
report += '\n';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Bottlenecks
|
|
264
|
+
if (analysis.bottlenecks.length > 0) {
|
|
265
|
+
report += chalk.bold.red('🔴 Detected Bottlenecks:\n\n');
|
|
266
|
+
|
|
267
|
+
for (const bottleneck of analysis.bottlenecks.slice(0, 10)) {
|
|
268
|
+
const icon = bottleneck.severity === 'critical' ? 'âŒ' : 'âš ï¸ ';
|
|
269
|
+
const color = bottleneck.severity === 'critical' ? chalk.red : chalk.yellow;
|
|
270
|
+
report += color(`${icon} ${bottleneck.issue} (at ${bottleneck.level} users)\n`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
report += '\n';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Recommendations
|
|
277
|
+
report += chalk.bold.blue('💡 Recommended Optimizations:\n\n');
|
|
278
|
+
|
|
279
|
+
for (const rec of analysis.recommendations) {
|
|
280
|
+
const priorityLabel = rec.priority === 'high' ? chalk.red('[HIGH]') : chalk.yellow('[MEDIUM]');
|
|
281
|
+
report += `${priorityLabel} ${chalk.bold(rec.issue)}\n`;
|
|
282
|
+
|
|
283
|
+
for (const suggestion of rec.suggestions.slice(0, 5)) {
|
|
284
|
+
report += chalk.gray(` • ${suggestion}\n`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
report += '\n';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Footer
|
|
291
|
+
report += chalk.cyan('─────────────────────────────────────────────────────────────\n');
|
|
292
|
+
report += chalk.gray('Generated by offbyt Benchmark Tool\n');
|
|
293
|
+
|
|
294
|
+
return report;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export default { analyzePerformance, generateReport };
|
|
298
|
+
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Detector - Detects data resources from frontend code patterns
|
|
3
|
+
* Detects from: useState, useEffect, data arrays, forms, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { globSync } from 'glob';
|
|
9
|
+
import * as parser from '@babel/parser';
|
|
10
|
+
import traverse from '@babel/traverse';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scan frontend and detect resources from state variables and data patterns
|
|
14
|
+
*/
|
|
15
|
+
export function detectResourcesFromFrontend(projectPath) {
|
|
16
|
+
const resources = new Map();
|
|
17
|
+
|
|
18
|
+
// Find all frontend files
|
|
19
|
+
const patterns = [
|
|
20
|
+
'src/**/*.{js,jsx,ts,tsx}',
|
|
21
|
+
'app/**/*.{js,jsx,ts,tsx}',
|
|
22
|
+
'pages/**/*.{js,jsx,ts,tsx}',
|
|
23
|
+
'components/**/*.{js,jsx,ts,tsx}',
|
|
24
|
+
'*.{js,jsx,ts,tsx}'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const files = [];
|
|
28
|
+
for (const pattern of patterns) {
|
|
29
|
+
files.push(...globSync(pattern, { nodir: true, cwd: projectPath }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(` 📁 Scanning ${files.length} files...`);
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
try {
|
|
36
|
+
const fullPath = path.join(projectPath, file);
|
|
37
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
38
|
+
|
|
39
|
+
// Detect resources from code patterns
|
|
40
|
+
const detected = detectFromCode(content, file);
|
|
41
|
+
|
|
42
|
+
for (const resource of detected) {
|
|
43
|
+
if (!resources.has(resource.name)) {
|
|
44
|
+
resources.set(resource.name, {
|
|
45
|
+
name: resource.name,
|
|
46
|
+
singular: resource.singular,
|
|
47
|
+
fields: new Set(resource.fields || []),
|
|
48
|
+
sources: []
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resources.get(resource.name).sources.push({
|
|
53
|
+
file,
|
|
54
|
+
type: resource.type
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Merge fields
|
|
58
|
+
if (resource.fields) {
|
|
59
|
+
for (const field of resource.fields) {
|
|
60
|
+
resources.get(resource.name).fields.add(field);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Ignore parse errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Convert to array
|
|
70
|
+
return Array.from(resources.values()).map(r => ({
|
|
71
|
+
...r,
|
|
72
|
+
fields: Array.from(r.fields).filter(field =>
|
|
73
|
+
field !== '_id' && field !== 'id' && field.toLowerCase() !== 'id'
|
|
74
|
+
)
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Detect resources from code content using AST and patterns
|
|
80
|
+
*/
|
|
81
|
+
function detectFromCode(content, fileName) {
|
|
82
|
+
const resources = [];
|
|
83
|
+
|
|
84
|
+
// Pattern 1: useState hooks
|
|
85
|
+
// Example: const [products, setProducts] = useState([])
|
|
86
|
+
const statePattern = /const\s+\[(\w+),\s*set\w+\]\s*=\s*useState/g;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = statePattern.exec(content)) !== null) {
|
|
89
|
+
const varName = match[1];
|
|
90
|
+
if (isResourceVariable(varName)) {
|
|
91
|
+
resources.push({
|
|
92
|
+
name: varName,
|
|
93
|
+
singular: singularize(varName),
|
|
94
|
+
type: 'useState',
|
|
95
|
+
fields: extractFieldsFromVariable(content, varName)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Pattern 2: Data arrays
|
|
101
|
+
// Example: const products = []
|
|
102
|
+
const arrayPattern = /const\s+(\w+)\s*=\s*\[\]/g;
|
|
103
|
+
while ((match = arrayPattern.exec(content)) !== null) {
|
|
104
|
+
const varName = match[1];
|
|
105
|
+
if (isResourceVariable(varName)) {
|
|
106
|
+
resources.push({
|
|
107
|
+
name: varName,
|
|
108
|
+
singular: singularize(varName),
|
|
109
|
+
type: 'array',
|
|
110
|
+
fields: extractFieldsFromVariable(content, varName)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Pattern 3: map operations (indicates list rendering)
|
|
116
|
+
// Example: products.map(product => ...)
|
|
117
|
+
const mapPattern = /(\w+)\.map\s*\(\s*(?:\()?(\w+)(?:\))?\s*=>/g;
|
|
118
|
+
while ((match = mapPattern.exec(content)) !== null) {
|
|
119
|
+
const listName = match[1];
|
|
120
|
+
const itemName = match[2];
|
|
121
|
+
|
|
122
|
+
if (isResourceVariable(listName)) {
|
|
123
|
+
const fields = extractFieldsFromItemVariable(content, itemName);
|
|
124
|
+
resources.push({
|
|
125
|
+
name: listName,
|
|
126
|
+
singular: itemName,
|
|
127
|
+
type: 'map',
|
|
128
|
+
fields
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Pattern 4: Form fields (input names)
|
|
134
|
+
// Example: <input name="productName" />
|
|
135
|
+
const inputPattern = /name=["'](\w+)["']/g;
|
|
136
|
+
const formFields = [];
|
|
137
|
+
while ((match = inputPattern.exec(content)) !== null) {
|
|
138
|
+
formFields.push(match[1]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (formFields.length > 0) {
|
|
142
|
+
// Try to infer resource from form fields
|
|
143
|
+
const resourceName = inferResourceFromFields(formFields);
|
|
144
|
+
if (resourceName) {
|
|
145
|
+
resources.push({
|
|
146
|
+
name: resourceName,
|
|
147
|
+
singular: singularize(resourceName),
|
|
148
|
+
type: 'form',
|
|
149
|
+
fields: formFields
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Pattern 5: useEffect with fetch/axios placeholder
|
|
155
|
+
// Example: useEffect(() => { /* TODO: fetch products */ }, [])
|
|
156
|
+
const effectPattern = /useEffect\s*\([^)]*?(?:fetch|load|get)\s*(\w+)/gi;
|
|
157
|
+
while ((match = effectPattern.exec(content)) !== null) {
|
|
158
|
+
const varName = match[1];
|
|
159
|
+
if (isResourceVariable(varName)) {
|
|
160
|
+
resources.push({
|
|
161
|
+
name: varName,
|
|
162
|
+
singular: singularize(varName),
|
|
163
|
+
type: 'useEffect'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return resources;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if variable name looks like a resource
|
|
173
|
+
*/
|
|
174
|
+
function isResourceVariable(name) {
|
|
175
|
+
// Skip common non-resource variables
|
|
176
|
+
const excluded = ['data', 'loading', 'error', 'fetching', 'isLoading', 'isError',
|
|
177
|
+
'response', 'result', 'value', 'state', 'props', 'params', 'filter',
|
|
178
|
+
'filtered', 'sorted', 'paginated', 'searched'];
|
|
179
|
+
|
|
180
|
+
// Skip derived/transformed variables (filteredTodos, sortedProducts, etc.)
|
|
181
|
+
const derivedPrefixes = ['filtered', 'sorted', 'paginated', 'searched', 'visible', 'active', 'selected', 'new'];
|
|
182
|
+
if (derivedPrefixes.some(prefix => name.toLowerCase().startsWith(prefix))) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (excluded.includes(name.toLowerCase())) return false;
|
|
187
|
+
|
|
188
|
+
// Should be plural or common resource name
|
|
189
|
+
return name.length > 2 && (
|
|
190
|
+
name.endsWith('s') ||
|
|
191
|
+
name.endsWith('List') ||
|
|
192
|
+
name.endsWith('Data') ||
|
|
193
|
+
isCommonResource(name)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if it's a common resource name
|
|
199
|
+
*/
|
|
200
|
+
function isCommonResource(name) {
|
|
201
|
+
const common = ['user', 'product', 'order', 'cart', 'item', 'post', 'comment',
|
|
202
|
+
'review', 'category', 'tag', 'customer', 'invoice', 'payment'];
|
|
203
|
+
const lowerName = name.toLowerCase();
|
|
204
|
+
// Check if name equals or ends with (singular or plural) a common resource
|
|
205
|
+
return common.some(r => {
|
|
206
|
+
const plural = r + 's';
|
|
207
|
+
return lowerName === r || lowerName === plural ||
|
|
208
|
+
lowerName.endsWith(r) && (lowerName[lowerName.length - r.length - 1] === 's' || lowerName.length === r.length);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Convert plural to singular (simple version)
|
|
214
|
+
*/
|
|
215
|
+
function singularize(word) {
|
|
216
|
+
if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
|
|
217
|
+
if (word.endsWith('ses')) return word.slice(0, -2);
|
|
218
|
+
if (word.endsWith('s')) return word.slice(0, -1);
|
|
219
|
+
return word;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Extract fields from variable usage in code
|
|
224
|
+
*/
|
|
225
|
+
function extractFieldsFromVariable(content, varName) {
|
|
226
|
+
const fields = new Set();
|
|
227
|
+
|
|
228
|
+
// Pattern: varName.field
|
|
229
|
+
const dotPattern = new RegExp(`${varName}\\.\\w+\\.(\\w+)`, 'g');
|
|
230
|
+
let match;
|
|
231
|
+
while ((match = dotPattern.exec(content)) !== null) {
|
|
232
|
+
fields.add(match[1]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return Array.from(fields).filter(field =>
|
|
236
|
+
field !== '_id' && field !== 'id' && field.toLowerCase() !== 'id'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Extract fields from item variable in map operations
|
|
242
|
+
*/
|
|
243
|
+
function extractFieldsFromItemVariable(content, itemName) {
|
|
244
|
+
const fields = new Set();
|
|
245
|
+
|
|
246
|
+
// Pattern: item.fieldName
|
|
247
|
+
const fieldPattern = new RegExp(`${itemName}\\.([a-zA-Z_][a-zA-Z0-9_]*)`, 'g');
|
|
248
|
+
let match;
|
|
249
|
+
while ((match = fieldPattern.exec(content)) !== null) {
|
|
250
|
+
const field = match[1];
|
|
251
|
+
// Skip common methods and MongoDB _id
|
|
252
|
+
if (!['map', 'filter', 'reduce', 'forEach', 'length'].includes(field) &&
|
|
253
|
+
field !== '_id' && field !== 'id' && field.toLowerCase() !== 'id') {
|
|
254
|
+
fields.add(field);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return Array.from(fields);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Infer resource name from form fields
|
|
263
|
+
*/
|
|
264
|
+
function inferResourceFromFields(fields) {
|
|
265
|
+
// Look for common patterns in field names
|
|
266
|
+
const patterns = {
|
|
267
|
+
product: ['productName', 'productPrice', 'productDescription'],
|
|
268
|
+
user: ['userName', 'userEmail', 'userPassword'],
|
|
269
|
+
order: ['orderDate', 'orderTotal', 'orderStatus']
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
for (const [resource, keywords] of Object.entries(patterns)) {
|
|
273
|
+
if (keywords.some(kw => fields.some(f => f.toLowerCase().includes(kw.toLowerCase())))) {
|
|
274
|
+
return resource + 's';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export default { detectResourcesFromFrontend };
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "offbyt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Code generation and deployment tool",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"offbyt": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node cli.js",
|
|
12
|
+
"dev": "node cli.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"glob": "^13.0.6"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ========== SERVER ==========
|
|
2
|
+
NODE_ENV=development
|
|
3
|
+
PORT=5000
|
|
4
|
+
|
|
5
|
+
# ========== DATABASE ==========
|
|
6
|
+
MONGODB_URI=mongodb://localhost:27017/<DB_NAME>
|
|
7
|
+
DB_NAME=<DB_NAME>
|
|
8
|
+
|
|
9
|
+
# ========== SECURITY ==========
|
|
10
|
+
JWT_SECRET=your_jwt_secret_key_here_change_in_production
|
|
11
|
+
JWT_EXPIRE=7d
|
|
12
|
+
JWT_EXPIRES_IN=7d
|
|
13
|
+
PASSWORD_SALT_ROUNDS=10
|
|
14
|
+
|
|
15
|
+
# ========== CORS ==========
|
|
16
|
+
CORS_ORIGIN=http://localhost:3000,http://localhost:5173
|
|
17
|
+
|
|
18
|
+
# ========== API ==========
|
|
19
|
+
API_VERSION=v1
|
|
20
|
+
API_RATE_LIMIT=100
|
|
21
|
+
|
|
22
|
+
# ========== LOGGING ==========
|
|
23
|
+
LOG_LEVEL=info
|
|
24
|
+
ENABLE_LOGGING=true
|
|
25
|
+
ENABLE_METRICS=false
|
|
26
|
+
|
|
27
|
+
# ========== CACHE ==========
|
|
28
|
+
CACHE_TTL=300
|
|
29
|
+
|
|
30
|
+
# ========== API KEYS (Optional) ==========
|
|
31
|
+
# API_KEY=your_api_key_here
|