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.
Files changed (103) hide show
  1. package/README.md +2 -0
  2. package/cli/index.js +2 -0
  3. package/cli.js +206 -0
  4. package/core/detector/detectAxios.js +107 -0
  5. package/core/detector/detectFetch.js +148 -0
  6. package/core/detector/detectForms.js +55 -0
  7. package/core/detector/detectSocket.js +341 -0
  8. package/core/generator/generateControllers.js +17 -0
  9. package/core/generator/generateModels.js +25 -0
  10. package/core/generator/generateRoutes.js +17 -0
  11. package/core/generator/generateServer.js +18 -0
  12. package/core/generator/generateSocket.js +160 -0
  13. package/core/index.js +14 -0
  14. package/core/ir/IRTypes.js +25 -0
  15. package/core/ir/buildIR.js +83 -0
  16. package/core/parser/parseJS.js +26 -0
  17. package/core/parser/parseTS.js +27 -0
  18. package/core/rules/relationRules.js +38 -0
  19. package/core/rules/resourceRules.js +32 -0
  20. package/core/rules/schemaInference.js +26 -0
  21. package/core/scanner/scanProject.js +58 -0
  22. package/deploy/cloudflare.js +41 -0
  23. package/deploy/cloudflareWorker.js +122 -0
  24. package/deploy/connect.js +198 -0
  25. package/deploy/flyio.js +51 -0
  26. package/deploy/index.js +322 -0
  27. package/deploy/netlify.js +29 -0
  28. package/deploy/railway.js +215 -0
  29. package/deploy/render.js +195 -0
  30. package/deploy/utils.js +383 -0
  31. package/deploy/vercel.js +29 -0
  32. package/index.js +18 -0
  33. package/lib/generator/advancedCrudGenerator.js +475 -0
  34. package/lib/generator/crudCodeGenerator.js +486 -0
  35. package/lib/generator/irBasedGenerator.js +360 -0
  36. package/lib/ir-builder/index.js +16 -0
  37. package/lib/ir-builder/irBuilder.js +330 -0
  38. package/lib/ir-builder/rulesEngine.js +353 -0
  39. package/lib/ir-builder/templateEngine.js +193 -0
  40. package/lib/ir-builder/templates/index.js +14 -0
  41. package/lib/ir-builder/templates/model.template.js +47 -0
  42. package/lib/ir-builder/templates/routes-generic.template.js +66 -0
  43. package/lib/ir-builder/templates/routes-user.template.js +105 -0
  44. package/lib/ir-builder/templates/routes.template.js +102 -0
  45. package/lib/ir-builder/templates/validation.template.js +15 -0
  46. package/lib/ir-integration.js +349 -0
  47. package/lib/modes/benchmark.js +162 -0
  48. package/lib/modes/configBasedGenerator.js +2258 -0
  49. package/lib/modes/connect.js +1125 -0
  50. package/lib/modes/doctorAi.js +172 -0
  51. package/lib/modes/generateApi.js +435 -0
  52. package/lib/modes/interactiveSetup.js +548 -0
  53. package/lib/modes/offline.clean.js +14 -0
  54. package/lib/modes/offline.enhanced.js +787 -0
  55. package/lib/modes/offline.js +295 -0
  56. package/lib/modes/offline.v2.js +13 -0
  57. package/lib/modes/sync.js +629 -0
  58. package/lib/scanner/apiEndpointExtractor.js +387 -0
  59. package/lib/scanner/authPatternDetector.js +54 -0
  60. package/lib/scanner/frontendScanner.js +642 -0
  61. package/lib/utils/apiClientGenerator.js +242 -0
  62. package/lib/utils/apiScanner.js +95 -0
  63. package/lib/utils/codeInjector.js +350 -0
  64. package/lib/utils/doctor.js +381 -0
  65. package/lib/utils/envGenerator.js +36 -0
  66. package/lib/utils/loadTester.js +61 -0
  67. package/lib/utils/performanceAnalyzer.js +298 -0
  68. package/lib/utils/resourceDetector.js +281 -0
  69. package/package.json +20 -0
  70. package/templates/.env.template +31 -0
  71. package/templates/advanced.model.template.js +201 -0
  72. package/templates/advanced.route.template.js +341 -0
  73. package/templates/auth.middleware.template.js +87 -0
  74. package/templates/auth.routes.template.js +238 -0
  75. package/templates/auth.user.model.template.js +78 -0
  76. package/templates/cache.middleware.js +34 -0
  77. package/templates/chat.models.template.js +260 -0
  78. package/templates/chat.routes.template.js +478 -0
  79. package/templates/compression.middleware.js +19 -0
  80. package/templates/database.config.js +74 -0
  81. package/templates/errorHandler.middleware.js +54 -0
  82. package/templates/express/controller.ejs +26 -0
  83. package/templates/express/model.ejs +9 -0
  84. package/templates/express/route.ejs +18 -0
  85. package/templates/express/server.ejs +16 -0
  86. package/templates/frontend.env.template +14 -0
  87. package/templates/model.template.js +86 -0
  88. package/templates/package.production.json +51 -0
  89. package/templates/package.template.json +41 -0
  90. package/templates/pagination.utility.js +110 -0
  91. package/templates/production.server.template.js +233 -0
  92. package/templates/rateLimiter.middleware.js +36 -0
  93. package/templates/requestLogger.middleware.js +19 -0
  94. package/templates/response.helper.js +179 -0
  95. package/templates/route.template.js +130 -0
  96. package/templates/security.middleware.js +78 -0
  97. package/templates/server.template.js +91 -0
  98. package/templates/socket.server.template.js +433 -0
  99. package/templates/utils.helper.js +157 -0
  100. package/templates/validation.middleware.js +63 -0
  101. package/templates/validation.schema.js +128 -0
  102. package/utils/fileWriter.js +15 -0
  103. package/utils/logger.js +18 -0
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ Generate Backends instantly!!
2
+ OFFBYT
package/cli/index.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../cli.js';
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;