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
package/README.md
ADDED
package/cli/index.js
ADDED
package/cli.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { offlineMode } from './lib/modes/offline.js';
|
|
8
|
+
import { connectFrontendBackend } from './lib/modes/connect.js';
|
|
9
|
+
import { runDoctor } from './lib/utils/doctor.js';
|
|
10
|
+
import { getInteractiveSetup, displaySetupSummary } from './lib/modes/interactiveSetup.js';
|
|
11
|
+
import { generateWithConfig } from './lib/modes/configBasedGenerator.js';
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
function parsePort(value) {
|
|
16
|
+
const port = Number.parseInt(value, 10);
|
|
17
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
18
|
+
throw new Error('Port must be a number between 1 and 65535');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return port;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('offbyt')
|
|
26
|
+
.description('Hybrid Backend Generator - Offline + AI Powered')
|
|
27
|
+
.version('1.0.0');
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('generate [path]')
|
|
31
|
+
.description('Complete backend generation with automatic API detection & injection')
|
|
32
|
+
.option('--no-auto-connect', 'Skip auto-connect after generation')
|
|
33
|
+
.option('--quick', 'Use default configuration (no questions)')
|
|
34
|
+
.option('--no-api-detect', 'Skip automatic API detection from frontend')
|
|
35
|
+
.action(async (projectPath, options) => {
|
|
36
|
+
try {
|
|
37
|
+
const workingPath = projectPath || process.cwd();
|
|
38
|
+
let config;
|
|
39
|
+
|
|
40
|
+
if (options.quick) {
|
|
41
|
+
// Use default configuration
|
|
42
|
+
config = {
|
|
43
|
+
database: 'mongodb',
|
|
44
|
+
framework: 'express',
|
|
45
|
+
enableSocket: true,
|
|
46
|
+
enableAuth: true,
|
|
47
|
+
authType: 'jwt',
|
|
48
|
+
enableValidation: true,
|
|
49
|
+
enableCaching: false,
|
|
50
|
+
enableLogging: true
|
|
51
|
+
};
|
|
52
|
+
console.log(chalk.cyan('Using default configuration...\n'));
|
|
53
|
+
} else {
|
|
54
|
+
// Interactive setup
|
|
55
|
+
config = await getInteractiveSetup();
|
|
56
|
+
displaySetupSummary(config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generate backend with config
|
|
60
|
+
const confirmSpinner = ora('Ready to generate backend...').start();
|
|
61
|
+
confirmSpinner.succeed('Configuration confirmed\n');
|
|
62
|
+
|
|
63
|
+
await generateWithConfig(workingPath, config);
|
|
64
|
+
|
|
65
|
+
// AUTOMATIC: Smart API detection & generation
|
|
66
|
+
if (options.apiDetect !== false) {
|
|
67
|
+
console.log(chalk.cyan('\n\n🎯 Running Smart API Detection...\n'));
|
|
68
|
+
const { generateSmartAPI } = await import('./lib/modes/generateApi.js');
|
|
69
|
+
try {
|
|
70
|
+
await generateSmartAPI(workingPath, { inject: true, config });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(chalk.red('\nError in Smart API Generation:'));
|
|
73
|
+
console.error(chalk.red(error.message));
|
|
74
|
+
if (error.stack) {
|
|
75
|
+
console.error(chalk.gray(error.stack));
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Auto-connect if enabled
|
|
82
|
+
if (options.autoConnect) {
|
|
83
|
+
console.log(chalk.cyan('Auto-connecting frontend & backend...\n'));
|
|
84
|
+
await connectFrontendBackend(workingPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(chalk.red('Error:', error.message));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command('connect [path]')
|
|
95
|
+
.description('Auto-connect frontend & backend (fixes URLs, fields, responses)')
|
|
96
|
+
.action(async (projectPath) => {
|
|
97
|
+
try {
|
|
98
|
+
await connectFrontendBackend(projectPath || process.cwd());
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(chalk.red('Error:', error.message));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command('sync [path]')
|
|
107
|
+
.description('Sync backend with frontend changes (update backend for new/changed frontend APIs)')
|
|
108
|
+
.action(async (projectPath) => {
|
|
109
|
+
try {
|
|
110
|
+
const { syncBackendWithFrontend } = await import('./lib/modes/sync.js');
|
|
111
|
+
await syncBackendWithFrontend(projectPath || process.cwd());
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(chalk.red('Error:', error.message));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command('benchmark [path]')
|
|
120
|
+
.description('Run scalability & performance tests on your backend')
|
|
121
|
+
.option('--levels <levels>', 'Load levels to test (e.g., 10,100,1000)', '10,100,1000,10000')
|
|
122
|
+
.option('--duration <seconds>', 'Duration of each test in seconds', '10')
|
|
123
|
+
.option('--startup-mode', 'Simulate startup growth over time')
|
|
124
|
+
.action(async (projectPath, options) => {
|
|
125
|
+
try {
|
|
126
|
+
const { runBenchmark } = await import('./lib/modes/benchmark.js');
|
|
127
|
+
await runBenchmark(projectPath || process.cwd(), options);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(chalk.red('Error:', error.message));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
program
|
|
135
|
+
.command('deploy [path]')
|
|
136
|
+
.description('Deploy frontend + backend and auto-connect API URLs')
|
|
137
|
+
.option('--full', 'Use default stack (Frontend: Vercel, Backend: Railway)')
|
|
138
|
+
.option('--frontend <provider>', 'Frontend provider: vercel | netlify | cloudflare | skip')
|
|
139
|
+
.option('--backend <provider>', 'Backend provider: railway | render | cloudflare | skip')
|
|
140
|
+
.option('--backend-service-id <id>', 'Backend service ID where required (e.g., Render)')
|
|
141
|
+
.option('--backend-project-name <name>', 'Backend project name where supported (e.g., Railway, Cloudflare Pages)')
|
|
142
|
+
.option('--backend-service-name <name>', 'Backend service name where supported (e.g., Railway)')
|
|
143
|
+
.action(async (projectPath, options) => {
|
|
144
|
+
try {
|
|
145
|
+
const { runDeploymentFlow } = await import('./deploy/index.js');
|
|
146
|
+
await runDeploymentFlow(projectPath || process.cwd(), options);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(chalk.red('Error:', error.message));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
program
|
|
154
|
+
.command('generate-api [path]')
|
|
155
|
+
.description('Smart API generation - Detect resources from frontend state & generate full-stack APIs')
|
|
156
|
+
.option('--no-inject', 'Skip frontend code injection')
|
|
157
|
+
.action(async (projectPath, options) => {
|
|
158
|
+
try {
|
|
159
|
+
const { generateSmartAPI } = await import('./lib/modes/generateApi.js');
|
|
160
|
+
await generateSmartAPI(projectPath || process.cwd(), options);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(chalk.red('Error:', error.message));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command('doctor')
|
|
169
|
+
.description('Diagnose your system readiness')
|
|
170
|
+
.option('--json', 'Output diagnostics in JSON format')
|
|
171
|
+
.option('--no-strict', 'Do not treat warnings as blocking issues')
|
|
172
|
+
.option('--port <number>', 'Port to check for availability (default: 5000)', parsePort, 5000)
|
|
173
|
+
.action(async options => {
|
|
174
|
+
const report = await runDoctor(options);
|
|
175
|
+
process.exitCode = report.exitCode;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
program
|
|
179
|
+
.command('doctor-ai [path]')
|
|
180
|
+
.description('AI-powered backend debugger. Monitors a command for errors and generates fixes.')
|
|
181
|
+
.option('--cmd <command>', 'Command to run and monitor', 'npm run dev')
|
|
182
|
+
.action(async (projectPath, options) => {
|
|
183
|
+
try {
|
|
184
|
+
const { runDoctorAi } = await import('./lib/modes/doctorAi.js');
|
|
185
|
+
await runDoctorAi(projectPath || process.cwd(), options);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(chalk.red('Error:', error.message));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
program.parse(process.argv);
|
|
193
|
+
|
|
194
|
+
if (!process.argv.slice(2).length) {
|
|
195
|
+
program.outputHelp();
|
|
196
|
+
console.log(chalk.cyan('\nQuick Start:\n'));
|
|
197
|
+
console.log(chalk.white(' Option 1 (Recommended):'));
|
|
198
|
+
console.log(chalk.gray(' offbyt generate # Generate + Auto-connect\n'));
|
|
199
|
+
console.log(chalk.white(' Option 2 (Skip auto-connect):'));
|
|
200
|
+
console.log(chalk.gray(' offbyt generate --no-auto-connect # Generate only\n'));
|
|
201
|
+
console.log(chalk.white(' Option 3 (Just connect):'));
|
|
202
|
+
console.log(chalk.gray(' offbyt connect [path] # Auto-connect existing project\n'));
|
|
203
|
+
console.log(chalk.white(' Option 4 (Deploy live):'));
|
|
204
|
+
console.log(chalk.gray(' offbyt deploy [path] # Deploy + auto-connect URLs\n'));
|
|
205
|
+
}
|
|
206
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import babelTraverse from '@babel/traverse';
|
|
2
|
+
|
|
3
|
+
const traverse = babelTraverse.default || babelTraverse;
|
|
4
|
+
|
|
5
|
+
const AXIOS_NAMES = new Set(['axios', 'api', 'client', 'http', 'request']);
|
|
6
|
+
const METHOD_NAMES = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
7
|
+
|
|
8
|
+
function getStringValue(node) {
|
|
9
|
+
if (!node) return null;
|
|
10
|
+
if (node.type === 'StringLiteral') return node.value;
|
|
11
|
+
if (node.type === 'TemplateLiteral') {
|
|
12
|
+
return node.quasis.map((q) => q.value.cooked || '').join(':id');
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeRoute(route) {
|
|
18
|
+
if (!route) return null;
|
|
19
|
+
const cleaned = route.split('?')[0];
|
|
20
|
+
if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
|
|
21
|
+
const index = cleaned.indexOf(`/api/`);
|
|
22
|
+
if (index >= 0) return cleaned.substring(index);
|
|
23
|
+
}
|
|
24
|
+
return cleaned.startsWith('/') ? cleaned : `/${cleaned}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractFieldsFromNode(node) {
|
|
28
|
+
if (!node) return [];
|
|
29
|
+
if (node.type !== 'ObjectExpression') return [];
|
|
30
|
+
|
|
31
|
+
return node.properties
|
|
32
|
+
.filter((p) => p.type === 'ObjectProperty')
|
|
33
|
+
.map((p) => (p.key.type === 'Identifier' ? p.key.name : p.key.value))
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function detectAxios(ast, file) {
|
|
38
|
+
if (!ast) return [];
|
|
39
|
+
|
|
40
|
+
const detections = [];
|
|
41
|
+
|
|
42
|
+
traverse(ast, {
|
|
43
|
+
CallExpression(path) {
|
|
44
|
+
const { node } = path;
|
|
45
|
+
|
|
46
|
+
if (node.callee.type === 'MemberExpression') {
|
|
47
|
+
const object = node.callee.object;
|
|
48
|
+
const property = node.callee.property;
|
|
49
|
+
|
|
50
|
+
const objectName = object.type === 'Identifier' ? object.name : null;
|
|
51
|
+
const methodName = property.type === 'Identifier' ? property.name : null;
|
|
52
|
+
|
|
53
|
+
if (!objectName || !methodName) return;
|
|
54
|
+
if (!AXIOS_NAMES.has(objectName) || !METHOD_NAMES.has(methodName)) return;
|
|
55
|
+
|
|
56
|
+
const route = normalizeRoute(getStringValue(node.arguments[0]));
|
|
57
|
+
if (!route) return;
|
|
58
|
+
if (!route.includes(`/api`) && !route.startsWith('/')) return;
|
|
59
|
+
|
|
60
|
+
const method = methodName.toUpperCase();
|
|
61
|
+
const dataArgIndex = methodName === 'get' ? -1 : 1;
|
|
62
|
+
const fields = dataArgIndex >= 0 ? extractFieldsFromNode(node.arguments[dataArgIndex]) : [];
|
|
63
|
+
|
|
64
|
+
detections.push({
|
|
65
|
+
file,
|
|
66
|
+
type: 'axios',
|
|
67
|
+
method,
|
|
68
|
+
route,
|
|
69
|
+
fields,
|
|
70
|
+
hasQueryParams: route.includes('?'),
|
|
71
|
+
line: node.loc?.start?.line || 0,
|
|
72
|
+
source: 'ast'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'axios' && node.arguments[0]?.type === 'ObjectExpression') {
|
|
77
|
+
const config = node.arguments[0];
|
|
78
|
+
|
|
79
|
+
const methodProp = config.properties.find((p) => p.type === 'ObjectProperty' && ((p.key.type === 'Identifier' && p.key.name === 'method') || (p.key.type === 'StringLiteral' && p.key.value === 'method')));
|
|
80
|
+
const urlProp = config.properties.find((p) => p.type === 'ObjectProperty' && ((p.key.type === 'Identifier' && p.key.name === 'url') || (p.key.type === 'StringLiteral' && p.key.value === 'url')));
|
|
81
|
+
const dataProp = config.properties.find((p) => p.type === 'ObjectProperty' && ((p.key.type === 'Identifier' && p.key.name === 'data') || (p.key.type === 'StringLiteral' && p.key.value === 'data')));
|
|
82
|
+
|
|
83
|
+
const route = normalizeRoute(getStringValue(urlProp?.value));
|
|
84
|
+
if (!route) return;
|
|
85
|
+
if (!route.includes(`/api`) && !route.startsWith('/')) return;
|
|
86
|
+
|
|
87
|
+
const method = (getStringValue(methodProp?.value) || 'GET').toUpperCase();
|
|
88
|
+
const fields = extractFieldsFromNode(dataProp?.value);
|
|
89
|
+
|
|
90
|
+
detections.push({
|
|
91
|
+
file,
|
|
92
|
+
type: 'axios',
|
|
93
|
+
method,
|
|
94
|
+
route,
|
|
95
|
+
fields,
|
|
96
|
+
hasQueryParams: route.includes('?'),
|
|
97
|
+
line: node.loc?.start?.line || 0,
|
|
98
|
+
source: 'ast'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return detections;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default detectAxios;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import babelTraverse from '@babel/traverse';
|
|
2
|
+
|
|
3
|
+
const traverse = babelTraverse.default || babelTraverse;
|
|
4
|
+
|
|
5
|
+
function getMethodFromFetchConfig(configNode) {
|
|
6
|
+
if (!configNode || configNode.type !== 'ObjectExpression') return 'GET';
|
|
7
|
+
|
|
8
|
+
for (const prop of configNode.properties) {
|
|
9
|
+
if (prop.type !== 'ObjectProperty') continue;
|
|
10
|
+
|
|
11
|
+
const keyName = prop.key.type === 'Identifier'
|
|
12
|
+
? prop.key.name
|
|
13
|
+
: prop.key.type === 'StringLiteral'
|
|
14
|
+
? prop.key.value
|
|
15
|
+
: null;
|
|
16
|
+
|
|
17
|
+
if (keyName !== 'method') continue;
|
|
18
|
+
|
|
19
|
+
if (prop.value.type === 'StringLiteral') return prop.value.value.toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasBody = configNode.properties.some((prop) => {
|
|
23
|
+
if (prop.type !== 'ObjectProperty') return false;
|
|
24
|
+
const keyName = prop.key.type === 'Identifier'
|
|
25
|
+
? prop.key.name
|
|
26
|
+
: prop.key.type === 'StringLiteral'
|
|
27
|
+
? prop.key.value
|
|
28
|
+
: null;
|
|
29
|
+
return keyName === 'body';
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return hasBody ? 'POST' : 'GET';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRoute(node) {
|
|
36
|
+
if (!node) return null;
|
|
37
|
+
|
|
38
|
+
if (node.type === 'StringLiteral') {
|
|
39
|
+
return node.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (node.type === 'TemplateLiteral') {
|
|
43
|
+
const text = node.quasis.map((q) => q.value.cooked || '').join(':id');
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractBodyFields(configNode) {
|
|
51
|
+
if (!configNode || configNode.type !== 'ObjectExpression') return [];
|
|
52
|
+
|
|
53
|
+
const bodyProp = configNode.properties.find((prop) => {
|
|
54
|
+
if (prop.type !== 'ObjectProperty') return false;
|
|
55
|
+
const keyName = prop.key.type === 'Identifier'
|
|
56
|
+
? prop.key.name
|
|
57
|
+
: prop.key.type === 'StringLiteral'
|
|
58
|
+
? prop.key.value
|
|
59
|
+
: null;
|
|
60
|
+
return keyName === 'body';
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!bodyProp) return [];
|
|
64
|
+
|
|
65
|
+
const value = bodyProp.value;
|
|
66
|
+
|
|
67
|
+
if (value.type === 'ObjectExpression') {
|
|
68
|
+
return value.properties
|
|
69
|
+
.filter((p) => p.type === 'ObjectProperty')
|
|
70
|
+
.map((p) => (p.key.type === 'Identifier' ? p.key.name : p.key.value))
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
value.type === 'CallExpression' &&
|
|
76
|
+
value.callee.type === 'MemberExpression' &&
|
|
77
|
+
value.callee.object.type === 'Identifier' &&
|
|
78
|
+
value.callee.object.name === 'JSON' &&
|
|
79
|
+
value.callee.property.type === 'Identifier' &&
|
|
80
|
+
value.callee.property.name === 'stringify' &&
|
|
81
|
+
value.arguments[0] &&
|
|
82
|
+
value.arguments[0].type === 'ObjectExpression'
|
|
83
|
+
) {
|
|
84
|
+
return value.arguments[0].properties
|
|
85
|
+
.filter((p) => p.type === 'ObjectProperty')
|
|
86
|
+
.map((p) => (p.key.type === 'Identifier' ? p.key.name : p.key.value))
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeRoute(route) {
|
|
94
|
+
if (!route) return null;
|
|
95
|
+
const cleaned = route.split('?')[0];
|
|
96
|
+
if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
|
|
97
|
+
const index = cleaned.indexOf(`/api/`);
|
|
98
|
+
if (index >= 0) return cleaned.substring(index);
|
|
99
|
+
}
|
|
100
|
+
return cleaned.startsWith('/') ? cleaned : `/${cleaned}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function detectFetch(ast, file) {
|
|
104
|
+
if (!ast) return [];
|
|
105
|
+
|
|
106
|
+
const detections = [];
|
|
107
|
+
|
|
108
|
+
traverse(ast, {
|
|
109
|
+
CallExpression(path) {
|
|
110
|
+
const { node } = path;
|
|
111
|
+
const callee = node.callee;
|
|
112
|
+
|
|
113
|
+
const isDirectFetch = callee.type === 'Identifier' && callee.name === 'fetch';
|
|
114
|
+
const isWindowFetch =
|
|
115
|
+
callee.type === 'MemberExpression' &&
|
|
116
|
+
callee.object.type === 'Identifier' &&
|
|
117
|
+
callee.object.name === 'window' &&
|
|
118
|
+
callee.property.type === 'Identifier' &&
|
|
119
|
+
callee.property.name === 'fetch';
|
|
120
|
+
|
|
121
|
+
if (!isDirectFetch && !isWindowFetch) return;
|
|
122
|
+
|
|
123
|
+
const rawRoute = getRoute(node.arguments[0]);
|
|
124
|
+
const route = normalizeRoute(rawRoute);
|
|
125
|
+
if (!route) return;
|
|
126
|
+
if (!route.includes(`/api`) && !route.startsWith('/')) return;
|
|
127
|
+
|
|
128
|
+
const configNode = node.arguments[1];
|
|
129
|
+
const method = getMethodFromFetchConfig(configNode);
|
|
130
|
+
const fields = extractBodyFields(configNode);
|
|
131
|
+
|
|
132
|
+
detections.push({
|
|
133
|
+
file,
|
|
134
|
+
type: 'fetch',
|
|
135
|
+
method,
|
|
136
|
+
route,
|
|
137
|
+
fields,
|
|
138
|
+
hasQueryParams: route.includes('?'),
|
|
139
|
+
line: node.loc?.start?.line || 0,
|
|
140
|
+
source: 'ast'
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return detections;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default detectFetch;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import babelTraverse from '@babel/traverse';
|
|
2
|
+
|
|
3
|
+
const traverse = babelTraverse.default || babelTraverse;
|
|
4
|
+
|
|
5
|
+
export function detectForms(ast, file) {
|
|
6
|
+
if (!ast) return [];
|
|
7
|
+
|
|
8
|
+
const forms = [];
|
|
9
|
+
|
|
10
|
+
traverse(ast, {
|
|
11
|
+
VariableDeclarator(path) {
|
|
12
|
+
const { node } = path;
|
|
13
|
+
|
|
14
|
+
if (node.init?.type !== 'CallExpression') return;
|
|
15
|
+
if (node.init.callee.type !== 'Identifier' || node.init.callee.name !== 'useState') return;
|
|
16
|
+
if (!node.init.arguments[0] || node.init.arguments[0].type !== 'ObjectExpression') return;
|
|
17
|
+
|
|
18
|
+
const fields = node.init.arguments[0].properties
|
|
19
|
+
.filter((p) => p.type === 'ObjectProperty')
|
|
20
|
+
.map((p) => (p.key.type === 'Identifier' ? p.key.name : p.key.value))
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
|
|
23
|
+
if (fields.length === 0) return;
|
|
24
|
+
|
|
25
|
+
forms.push({
|
|
26
|
+
file,
|
|
27
|
+
type: 'form',
|
|
28
|
+
fields,
|
|
29
|
+
line: node.loc?.start?.line || 0,
|
|
30
|
+
source: 'ast'
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
CallExpression(path) {
|
|
34
|
+
const { node } = path;
|
|
35
|
+
|
|
36
|
+
if (node.callee.type !== 'MemberExpression') return;
|
|
37
|
+
if (node.callee.property.type !== 'Identifier' || node.callee.property.name !== 'append') return;
|
|
38
|
+
|
|
39
|
+
const keyArg = node.arguments[0];
|
|
40
|
+
if (!keyArg || keyArg.type !== 'StringLiteral') return;
|
|
41
|
+
|
|
42
|
+
forms.push({
|
|
43
|
+
file,
|
|
44
|
+
type: 'form-data',
|
|
45
|
+
fields: [keyArg.value],
|
|
46
|
+
line: node.loc?.start?.line || 0,
|
|
47
|
+
source: 'ast'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return forms;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default detectForms;
|