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
@@ -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 };