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,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend API Client Generator
|
|
3
|
+
* Generates API client files in frontend (src/api/*.js)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate API client files for each resource
|
|
11
|
+
*/
|
|
12
|
+
export function generateApiClients(projectPath, resources) {
|
|
13
|
+
const apiDir = path.join(projectPath, 'src', 'api');
|
|
14
|
+
|
|
15
|
+
// Create api directory if it doesn't exist
|
|
16
|
+
if (!fs.existsSync(apiDir)) {
|
|
17
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Detect if project uses Vite
|
|
21
|
+
const isVite = detectVite(projectPath);
|
|
22
|
+
|
|
23
|
+
const generatedFiles = [];
|
|
24
|
+
|
|
25
|
+
// Generate a client file for each resource
|
|
26
|
+
for (const resource of resources) {
|
|
27
|
+
const fileName = `${resource.singular}.js`;
|
|
28
|
+
const filePath = path.join(apiDir, fileName);
|
|
29
|
+
|
|
30
|
+
const clientCode = generateClientCode(resource, isVite);
|
|
31
|
+
fs.writeFileSync(filePath, clientCode, 'utf8');
|
|
32
|
+
|
|
33
|
+
generatedFiles.push(`src/api/${fileName}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate index.js to export all clients
|
|
37
|
+
const indexPath = path.join(apiDir, 'index.js');
|
|
38
|
+
const indexCode = generateIndexCode(resources);
|
|
39
|
+
fs.writeFileSync(indexPath, indexCode, 'utf8');
|
|
40
|
+
generatedFiles.push('src/api/index.js');
|
|
41
|
+
|
|
42
|
+
return generatedFiles;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detect if project uses Vite
|
|
47
|
+
*/
|
|
48
|
+
function detectVite(projectPath) {
|
|
49
|
+
try {
|
|
50
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
51
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
52
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
53
|
+
const deps = packageJson.dependencies || {};
|
|
54
|
+
const devDeps = packageJson.devDependencies || {};
|
|
55
|
+
|
|
56
|
+
if (deps.vite || devDeps.vite) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const viteConfigFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'];
|
|
62
|
+
return viteConfigFiles.some((file) => fs.existsSync(path.join(projectPath, file)));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate client code for a resource
|
|
70
|
+
*/
|
|
71
|
+
function generateClientCode(resource, isVite = false) {
|
|
72
|
+
const resourceName = resource.name;
|
|
73
|
+
const singular = resource.singular;
|
|
74
|
+
const capitalizedSingular = capitalize(singular);
|
|
75
|
+
|
|
76
|
+
const envVar = isVite ? 'import.meta.env.VITE_API_URL' : 'process.env.REACT_APP_API_URL';
|
|
77
|
+
const fallbackUrl = "'http://localhost:5000'";
|
|
78
|
+
|
|
79
|
+
return `/**
|
|
80
|
+
* API Client for ${capitalizedSingular}
|
|
81
|
+
* Auto-generated by offbyt
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
const API_BASE_URL = ${envVar} || ${fallbackUrl};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all ${resourceName}
|
|
88
|
+
*/
|
|
89
|
+
export const getAll${capitalize(resourceName)} = async (params = {}) => {
|
|
90
|
+
const query = new URLSearchParams(params).toString();
|
|
91
|
+
const url = \`\${API_BASE_URL}/api/${resourceName}\${query ? '?' + query : ''}\`;
|
|
92
|
+
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: 'GET',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json'
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(\`Failed to fetch ${resourceName}\`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return response.json();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get single ${singular} by ID
|
|
109
|
+
*/
|
|
110
|
+
export const get${capitalizedSingular}ById = async (id) => {
|
|
111
|
+
const url = \`\${API_BASE_URL}/api/${resourceName}/\${id}\`;
|
|
112
|
+
|
|
113
|
+
const response = await fetch(url, {
|
|
114
|
+
method: 'GET',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json'
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(\`Failed to fetch ${singular}\`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return response.json();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create new ${singular}
|
|
129
|
+
*/
|
|
130
|
+
export const create${capitalizedSingular} = async (data) => {
|
|
131
|
+
const url = \`\${API_BASE_URL}/api/${resourceName}\`;
|
|
132
|
+
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json'
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(data)
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(\`Failed to create ${singular}\`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return response.json();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Update ${singular} (partial update)
|
|
150
|
+
*/
|
|
151
|
+
export const update${capitalizedSingular} = async (id, data) => {
|
|
152
|
+
const url = \`\${API_BASE_URL}/api/${resourceName}/\${id}\`;
|
|
153
|
+
|
|
154
|
+
const response = await fetch(url, {
|
|
155
|
+
method: 'PATCH',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json'
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify(data)
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(\`Failed to update ${singular}\`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return response.json();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Delete ${singular}
|
|
171
|
+
*/
|
|
172
|
+
export const delete${capitalizedSingular} = async (id) => {
|
|
173
|
+
const url = \`\${API_BASE_URL}/api/${resourceName}/\${id}\`;
|
|
174
|
+
|
|
175
|
+
const response = await fetch(url, {
|
|
176
|
+
method: 'DELETE',
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Type': 'application/json'
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new Error(\`Failed to delete ${singular}\`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return response.json();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export default {
|
|
190
|
+
getAll${capitalize(resourceName)},
|
|
191
|
+
get${capitalizedSingular}ById,
|
|
192
|
+
create${capitalizedSingular},
|
|
193
|
+
update${capitalizedSingular},
|
|
194
|
+
delete${capitalizedSingular}
|
|
195
|
+
};
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Generate index.js that exports all clients
|
|
201
|
+
*/
|
|
202
|
+
function generateIndexCode(resources) {
|
|
203
|
+
let code = `/**
|
|
204
|
+
* API Clients Index
|
|
205
|
+
* Auto-generated by offbyt
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
// Import all clients
|
|
211
|
+
for (const resource of resources) {
|
|
212
|
+
code += `import ${resource.singular}API from './${resource.singular}.js';\n`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
code += '\n';
|
|
216
|
+
|
|
217
|
+
// Export all
|
|
218
|
+
code += 'export {\n';
|
|
219
|
+
for (const resource of resources) {
|
|
220
|
+
code += ` ${resource.singular}API,\n`;
|
|
221
|
+
}
|
|
222
|
+
code += '};\n\n';
|
|
223
|
+
|
|
224
|
+
// Default export
|
|
225
|
+
code += 'export default {\n';
|
|
226
|
+
for (const resource of resources) {
|
|
227
|
+
code += ` ${resource.singular}: ${resource.singular}API,\n`;
|
|
228
|
+
}
|
|
229
|
+
code += '};\n';
|
|
230
|
+
|
|
231
|
+
return code;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Capitalize first letter
|
|
236
|
+
*/
|
|
237
|
+
function capitalize(str) {
|
|
238
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default { generateApiClients };
|
|
242
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Scanner - Detects backend routes from route files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { globSync } from 'glob';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scan backend routes from route files
|
|
11
|
+
*/
|
|
12
|
+
export async function scanBackendRoutes(backendPath) {
|
|
13
|
+
const routes = [];
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
|
|
16
|
+
// Find all route files
|
|
17
|
+
const routeFiles = globSync('routes/**/*.js', {
|
|
18
|
+
cwd: backendPath,
|
|
19
|
+
absolute: true
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const file of routeFiles) {
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
25
|
+
const detectedRoutes = extractRoutesFromFile(content, file);
|
|
26
|
+
for (const route of detectedRoutes) {
|
|
27
|
+
const key = `${route.method}:${route.path}`;
|
|
28
|
+
if (!seen.has(key)) {
|
|
29
|
+
seen.add(key);
|
|
30
|
+
routes.push(route);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Ignore file read errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return routes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract routes from a route file
|
|
43
|
+
*/
|
|
44
|
+
function extractRoutesFromFile(content, filePath) {
|
|
45
|
+
const routes = [];
|
|
46
|
+
const fileName = path.basename(filePath, '.js');
|
|
47
|
+
|
|
48
|
+
// Detect base resource from filename (e.g., auth.routes.js -> auth)
|
|
49
|
+
const resourceName = fileName.replace('.routes', '');
|
|
50
|
+
|
|
51
|
+
// Regex patterns to detect Express routes
|
|
52
|
+
const routePatterns = [
|
|
53
|
+
/router\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
54
|
+
/app\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const pattern of routePatterns) {
|
|
58
|
+
let match;
|
|
59
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
60
|
+
const method = match[1].toUpperCase();
|
|
61
|
+
const routePath = buildApiRoutePath(resourceName, match[2]);
|
|
62
|
+
|
|
63
|
+
routes.push({
|
|
64
|
+
method,
|
|
65
|
+
path: routePath,
|
|
66
|
+
file: path.basename(filePath)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return routes;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildApiRoutePath(resourceName, routePath) {
|
|
75
|
+
const normalizedRoute = normalizePath(routePath);
|
|
76
|
+
const basePath = normalizePath(`/api/${resourceName}`);
|
|
77
|
+
|
|
78
|
+
if (normalizedRoute === '/') return basePath;
|
|
79
|
+
return normalizePath(`${basePath}${normalizedRoute}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizePath(routePath) {
|
|
83
|
+
if (!routePath) return '/';
|
|
84
|
+
|
|
85
|
+
let normalized = String(routePath)
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/\/+/g, '/');
|
|
88
|
+
|
|
89
|
+
if (!normalized.startsWith('/')) normalized = `/${normalized}`;
|
|
90
|
+
if (normalized.length > 1) normalized = normalized.replace(/\/+$/g, '');
|
|
91
|
+
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default { scanBackendRoutes };
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend Code Injector
|
|
3
|
+
* Injects API calls into frontend code where resources are used
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { globSync } from 'glob';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Inject API calls into frontend files
|
|
12
|
+
*/
|
|
13
|
+
export function injectApiCalls(projectPath, resources) {
|
|
14
|
+
const injectedFiles = [];
|
|
15
|
+
const files = getFrontendFiles(projectPath);
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
try {
|
|
19
|
+
const fullPath = path.join(projectPath, file);
|
|
20
|
+
let content = fs.readFileSync(fullPath, 'utf8');
|
|
21
|
+
let modified = false;
|
|
22
|
+
|
|
23
|
+
for (const resource of resources) {
|
|
24
|
+
// Check if this file uses this resource
|
|
25
|
+
if (usesResource(content, resource)) {
|
|
26
|
+
const result = injectForResource(content, resource);
|
|
27
|
+
if (result.modified) {
|
|
28
|
+
content = result.content;
|
|
29
|
+
modified = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (modified) {
|
|
35
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
36
|
+
injectedFiles.push(file);
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// Ignore errors
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return injectedFiles;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getFrontendFiles(projectPath) {
|
|
47
|
+
const patterns = [
|
|
48
|
+
'src/**/*.{js,jsx,ts,tsx}',
|
|
49
|
+
'src/pages/**/*.{js,jsx,ts,tsx}',
|
|
50
|
+
'src/components/**/*.{js,jsx,ts,tsx}',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const files = new Set();
|
|
54
|
+
for (const pattern of patterns) {
|
|
55
|
+
const found = globSync(pattern, { nodir: true, cwd: projectPath });
|
|
56
|
+
for (const file of found) {
|
|
57
|
+
files.add(file);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [...files];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if file uses a resource
|
|
66
|
+
*/
|
|
67
|
+
function usesResource(content, resource) {
|
|
68
|
+
const varName = resource.name;
|
|
69
|
+
const setterName = `set${capitalize(varName)}`;
|
|
70
|
+
const statePattern = new RegExp(`\\[\\s*${varName}\\s*,\\s*${setterName}\\s*\\]\\s*=\\s*useState\\(`, 'g');
|
|
71
|
+
if (statePattern.test(content)) return true;
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Inject API calls for a specific resource
|
|
78
|
+
*/
|
|
79
|
+
function injectForResource(content, resource) {
|
|
80
|
+
let modified = false;
|
|
81
|
+
let nextContent = content;
|
|
82
|
+
|
|
83
|
+
const resourceName = resource.name;
|
|
84
|
+
const singular = resource.singular || resourceName.slice(0, -1);
|
|
85
|
+
const capitalSingular = capitalize(singular);
|
|
86
|
+
const capitalResource = capitalize(resourceName);
|
|
87
|
+
|
|
88
|
+
const importResult = ensureUseEffectImport(nextContent);
|
|
89
|
+
nextContent = importResult.content;
|
|
90
|
+
modified = modified || importResult.changed;
|
|
91
|
+
|
|
92
|
+
const apiUrlResult = ensureApiUrlConstant(nextContent);
|
|
93
|
+
nextContent = apiUrlResult.content;
|
|
94
|
+
modified = modified || apiUrlResult.changed;
|
|
95
|
+
|
|
96
|
+
const stateInitResult = normalizeStateArray(nextContent, resourceName, capitalResource);
|
|
97
|
+
nextContent = stateInitResult.content;
|
|
98
|
+
modified = modified || stateInitResult.changed;
|
|
99
|
+
|
|
100
|
+
const fetchFunctionName = `fetch${capitalResource}`;
|
|
101
|
+
|
|
102
|
+
const fetchFnResult = ensureFetchFunction(nextContent, resourceName, capitalResource, fetchFunctionName);
|
|
103
|
+
nextContent = fetchFnResult.content;
|
|
104
|
+
modified = modified || fetchFnResult.changed;
|
|
105
|
+
|
|
106
|
+
const effectResult = ensureFetchEffect(nextContent, fetchFunctionName, resourceName, capitalResource);
|
|
107
|
+
nextContent = effectResult.content;
|
|
108
|
+
modified = modified || effectResult.changed;
|
|
109
|
+
|
|
110
|
+
const createResult = replaceCreateHandler(nextContent, {
|
|
111
|
+
resourceName,
|
|
112
|
+
singular,
|
|
113
|
+
capitalSingular,
|
|
114
|
+
fetchFunctionName,
|
|
115
|
+
});
|
|
116
|
+
nextContent = createResult.content;
|
|
117
|
+
modified = modified || createResult.changed;
|
|
118
|
+
|
|
119
|
+
const deleteResult = replaceDeleteHandler(nextContent, {
|
|
120
|
+
resourceName,
|
|
121
|
+
singular,
|
|
122
|
+
capitalSingular,
|
|
123
|
+
fetchFunctionName,
|
|
124
|
+
});
|
|
125
|
+
nextContent = deleteResult.content;
|
|
126
|
+
modified = modified || deleteResult.changed;
|
|
127
|
+
|
|
128
|
+
const jsxResult = ensureMongoIdsInJsx(nextContent, capitalSingular);
|
|
129
|
+
nextContent = jsxResult.content;
|
|
130
|
+
modified = modified || jsxResult.changed;
|
|
131
|
+
|
|
132
|
+
return { content: nextContent, modified };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ensureUseEffectImport(content) {
|
|
136
|
+
if (/\buseEffect\b/.test(content)) {
|
|
137
|
+
return { content, changed: false };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const reactNamedImport = /import\s+\{([^}]*)\}\s+from\s+['"]react['"];?/;
|
|
141
|
+
if (reactNamedImport.test(content)) {
|
|
142
|
+
const next = content.replace(reactNamedImport, (_, names) => {
|
|
143
|
+
const cleaned = names
|
|
144
|
+
.split(',')
|
|
145
|
+
.map(name => name.trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
if (!cleaned.includes('useEffect')) {
|
|
148
|
+
cleaned.push('useEffect');
|
|
149
|
+
}
|
|
150
|
+
return `import { ${cleaned.join(', ')} } from 'react';`;
|
|
151
|
+
});
|
|
152
|
+
return { content: next, changed: next !== content };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { content, changed: false };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensureApiUrlConstant(content) {
|
|
159
|
+
if (/\b(API_URL|BACKEND_URL)\b/.test(content)) {
|
|
160
|
+
return { content, changed: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const importMatches = [...content.matchAll(/^import.*$/gm)];
|
|
164
|
+
if (importMatches.length === 0) {
|
|
165
|
+
return { content, changed: false };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const lastImport = importMatches[importMatches.length - 1];
|
|
169
|
+
const insertPos = lastImport.index + lastImport[0].length;
|
|
170
|
+
const next = `${content.slice(0, insertPos)}\n\nconst API_URL = 'http://localhost:5000/api';${content.slice(insertPos)}`;
|
|
171
|
+
return { content: next, changed: true };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeStateArray(content, resourceName, capitalResource) {
|
|
175
|
+
const stateRegex = new RegExp(`(const\\s+\\[\\s*${resourceName}\\s*,\\s*set${capitalResource}\\s*\\]\\s*=\\s*useState\\()([\\s\\S]*?)(\\)\\s*;?)`);
|
|
176
|
+
const match = content.match(stateRegex);
|
|
177
|
+
if (!match) return { content, changed: false };
|
|
178
|
+
|
|
179
|
+
const currentValue = match[2].trim();
|
|
180
|
+
if (currentValue === '[]') return { content, changed: false };
|
|
181
|
+
|
|
182
|
+
const next = content.replace(stateRegex, `$1[]$3`);
|
|
183
|
+
return { content: next, changed: next !== content };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function ensureFetchFunction(content, resourceName, capitalResource, fetchFunctionName) {
|
|
187
|
+
if (new RegExp(`const\\s+${fetchFunctionName}\\s*=\\s*async`).test(content)) {
|
|
188
|
+
return { content, changed: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stateRegex = new RegExp(`const\\s+\\[\\s*${resourceName}\\s*,\\s*set${capitalResource}\\s*\\]\\s*=\\s*useState\\([^)]*\\)\\s*;?`);
|
|
192
|
+
const stateMatch = content.match(stateRegex);
|
|
193
|
+
if (!stateMatch) {
|
|
194
|
+
return { content, changed: false };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const insertPos = content.indexOf(stateMatch[0]) + stateMatch[0].length;
|
|
198
|
+
const snippet = `\n\n const ${fetchFunctionName} = async () => {\n try {\n const res = await fetch(\`\${API_URL}/${resourceName}\`);\n const payload = await res.json();\n set${capitalResource}(payload.data || []);\n } catch (error) {\n console.error('Error fetching ${resourceName}:', error);\n }\n };`;
|
|
199
|
+
|
|
200
|
+
const next = `${content.slice(0, insertPos)}${snippet}${content.slice(insertPos)}`;
|
|
201
|
+
return { content: next, changed: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ensureFetchEffect(content, fetchFunctionName, resourceName, capitalResource) {
|
|
205
|
+
const effectRegex = new RegExp(`useEffect\\(\\(\\)\\s*=>\\s*\\{\\s*${fetchFunctionName}\\(\\);?\\s*\\},\\s*\\[\\s*\\]\\s*\\);?`, 'm');
|
|
206
|
+
if (effectRegex.test(content)) {
|
|
207
|
+
return { content, changed: false };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const fetchFnRegex = new RegExp(`const\\s+${fetchFunctionName}\\s*=\\s*async[\\s\\S]*?\\n\\s*};`);
|
|
211
|
+
const fetchMatch = content.match(fetchFnRegex);
|
|
212
|
+
|
|
213
|
+
const effectSnippet = `\n\n useEffect(() => {\n ${fetchFunctionName}();\n }, []);`;
|
|
214
|
+
|
|
215
|
+
if (fetchMatch) {
|
|
216
|
+
const insertPos = content.indexOf(fetchMatch[0]) + fetchMatch[0].length;
|
|
217
|
+
const next = `${content.slice(0, insertPos)}${effectSnippet}${content.slice(insertPos)}`;
|
|
218
|
+
return { content: next, changed: true };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const stateRegex = new RegExp(`const\\s+\\[\\s*${resourceName}\\s*,\\s*set${capitalResource}\\s*\\]\\s*=\\s*useState\\([^)]*\\)\\s*;?`);
|
|
222
|
+
const stateMatch = content.match(stateRegex);
|
|
223
|
+
if (!stateMatch) {
|
|
224
|
+
return { content, changed: false };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const insertPos = content.indexOf(stateMatch[0]) + stateMatch[0].length;
|
|
228
|
+
const next = `${content.slice(0, insertPos)}${effectSnippet}${content.slice(insertPos)}`;
|
|
229
|
+
return { content: next, changed: true };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function replaceCreateHandler(content, ctx) {
|
|
233
|
+
const { resourceName, singular, capitalSingular, fetchFunctionName } = ctx;
|
|
234
|
+
const functionName = `handle${capitalSingular}Submit`;
|
|
235
|
+
const block = findConstArrowFunction(content, functionName);
|
|
236
|
+
if (!block) {
|
|
237
|
+
return { content, changed: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const currentBlock = content.slice(block.start, block.end);
|
|
241
|
+
if (/method:\s*['"]POST['"]/.test(currentBlock)) {
|
|
242
|
+
return { content, changed: false };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const newStateName = `new${capitalSingular}`;
|
|
246
|
+
const setNewStateName = `setNew${capitalSingular}`;
|
|
247
|
+
const initialStateLiteral = extractInitialStateLiteral(content, newStateName, setNewStateName) || '{}';
|
|
248
|
+
const nonEmptyGuard = buildNonEmptyGuard(newStateName, initialStateLiteral);
|
|
249
|
+
|
|
250
|
+
const replacement = `const ${functionName} = async (e) => {\n e.preventDefault();\n ${nonEmptyGuard}\n\n try {\n const res = await fetch(\`\${API_URL}/${resourceName}\`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(${newStateName}),\n });\n\n if (res.ok) {\n await ${fetchFunctionName}();\n ${setNewStateName}(${initialStateLiteral});\n }\n } catch (error) {\n console.error('Error adding ${singular}:', error);\n }\n };\n\n`;
|
|
251
|
+
|
|
252
|
+
const next = `${content.slice(0, block.start)}${replacement}${content.slice(block.end)}`;
|
|
253
|
+
return { content: next, changed: true };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function replaceDeleteHandler(content, ctx) {
|
|
257
|
+
const { resourceName, singular, capitalSingular, fetchFunctionName } = ctx;
|
|
258
|
+
const functionName = `delete${capitalSingular}`;
|
|
259
|
+
const block = findConstArrowFunction(content, functionName);
|
|
260
|
+
if (!block) {
|
|
261
|
+
return { content, changed: false };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const currentBlock = content.slice(block.start, block.end);
|
|
265
|
+
if (/method:\s*['"]DELETE['"]/.test(currentBlock)) {
|
|
266
|
+
return { content, changed: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const replacement = `const ${functionName} = async (id) => {\n try {\n const res = await fetch(\`\${API_URL}/${resourceName}/\${id}\`, {\n method: 'DELETE',\n });\n\n if (res.ok) {\n await ${fetchFunctionName}();\n }\n } catch (error) {\n console.error('Error deleting ${singular}:', error);\n }\n };\n\n`;
|
|
270
|
+
|
|
271
|
+
const next = `${content.slice(0, block.start)}${replacement}${content.slice(block.end)}`;
|
|
272
|
+
return { content: next, changed: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ensureMongoIdsInJsx(content, capitalSingular) {
|
|
276
|
+
let next = content;
|
|
277
|
+
let changed = false;
|
|
278
|
+
|
|
279
|
+
const keyReplaced = next.replace(/key=\{(\w+)\.id\}/g, 'key={$1._id}');
|
|
280
|
+
if (keyReplaced !== next) {
|
|
281
|
+
next = keyReplaced;
|
|
282
|
+
changed = true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const deleteCallPattern = new RegExp(`onClick=\\{\\(\\)\\s*=>\\s*delete${capitalSingular}\\((\\w+)\\.id\\)\\}`, 'g');
|
|
286
|
+
const clickReplaced = next.replace(deleteCallPattern, `onClick={() => delete${capitalSingular}($1._id)}`);
|
|
287
|
+
if (clickReplaced !== next) {
|
|
288
|
+
next = clickReplaced;
|
|
289
|
+
changed = true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { content: next, changed };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function extractInitialStateLiteral(content, stateName, setterName) {
|
|
296
|
+
const regex = new RegExp(`const\\s+\\[\\s*${stateName}\\s*,\\s*${setterName}\\s*\\]\\s*=\\s*useState\\((\\{[\\s\\S]*?\\})\\)`);
|
|
297
|
+
const match = content.match(regex);
|
|
298
|
+
return match ? match[1] : null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildNonEmptyGuard(stateName, objectLiteral) {
|
|
302
|
+
const keys = [...objectLiteral.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:/g)].map(m => m[1]);
|
|
303
|
+
if (keys.length === 0) {
|
|
304
|
+
return `if (!${stateName}) return;`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const checks = keys.map(key => `!${stateName}.${key}`).join(' || ');
|
|
308
|
+
return `if (${checks}) return;`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function findConstArrowFunction(content, functionName) {
|
|
312
|
+
const startRegex = new RegExp(`const\\s+${functionName}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{`);
|
|
313
|
+
const startMatch = startRegex.exec(content);
|
|
314
|
+
if (!startMatch) return null;
|
|
315
|
+
|
|
316
|
+
const braceStart = content.indexOf('{', startMatch.index);
|
|
317
|
+
if (braceStart === -1) return null;
|
|
318
|
+
|
|
319
|
+
let depth = 0;
|
|
320
|
+
let end = -1;
|
|
321
|
+
for (let i = braceStart; i < content.length; i++) {
|
|
322
|
+
const char = content[i];
|
|
323
|
+
if (char === '{') depth++;
|
|
324
|
+
if (char === '}') {
|
|
325
|
+
depth--;
|
|
326
|
+
if (depth === 0) {
|
|
327
|
+
end = i + 1;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (end === -1) return null;
|
|
334
|
+
|
|
335
|
+
while (end < content.length && /[;\s]/.test(content[end])) {
|
|
336
|
+
end++;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { start: startMatch.index, end };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Capitalize string
|
|
344
|
+
*/
|
|
345
|
+
function capitalize(str) {
|
|
346
|
+
if (!str) return '';
|
|
347
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export default { injectApiCalls };
|