offbyt 1.0.0 → 1.0.2
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/cli.js +117 -2
- package/industry-mode.json +22 -0
- package/lib/ir-integration.js +4 -4
- package/lib/modes/configBasedGenerator.js +27 -27
- package/lib/modes/connect.js +26 -25
- package/lib/modes/generateApi.js +217 -76
- package/lib/modes/interactiveSetup.js +28 -29
- package/lib/utils/cliFormatter.js +131 -0
- package/lib/utils/codeInjector.js +144 -26
- package/lib/utils/industryMode.js +419 -0
- package/lib/utils/postGenerationVerifier.js +256 -0
- package/lib/utils/resourceDetector.js +234 -169
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Formatting Utilities
|
|
3
|
+
* Provides attractive visual output for Offbyt CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
export function printBanner() {
|
|
9
|
+
console.log('\n');
|
|
10
|
+
console.log(chalk.bold.cyan('╔════════════════════════════════════════════════════════════════╗'));
|
|
11
|
+
console.log(chalk.bold.cyan('║ ║'));
|
|
12
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.blue(' ██████╗ ███████╗███████╗██████╗ ██╗ ██╗████████╗ ') + chalk.bold.cyan('║'));
|
|
13
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.blue(' ██╔═══██╗██╔════╝██╔════╝██╔══██╗╚██╗ ██╔╝╚══██╔══╝ ') + chalk.bold.cyan('║'));
|
|
14
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.blue(' ██║ ██║█████╗ █████╗ ██████╔╝ ╚████╔╝ ██║ ') + chalk.bold.cyan('║'));
|
|
15
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.blue(' ██║ ██║██╔══╝ ██╔══╝ ██╔══██╗ ╚██╔╝ ██║ ') + chalk.bold.cyan('║'));
|
|
16
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.blue(' ╚██████╔╝██║ ██║ ██████╔╝ ██║ ██║ ') + chalk.bold.cyan('║'));
|
|
17
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.blue(' ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ') + chalk.bold.cyan('║'));
|
|
18
|
+
console.log(chalk.bold.cyan('║ ║'));
|
|
19
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.white('Backend Generator - Offline + AI Powered') + chalk.bold.cyan(' ║'));
|
|
20
|
+
console.log(chalk.bold.cyan('║ ║'));
|
|
21
|
+
console.log(chalk.bold.cyan('╚════════════════════════════════════════════════════════════════╝'));
|
|
22
|
+
console.log('\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function printSection(title) {
|
|
26
|
+
console.log(chalk.bold.cyan('\n╔' + '═'.repeat(title.length + 4) + '╗'));
|
|
27
|
+
console.log(chalk.bold.cyan('║ ' + title + ' ║'));
|
|
28
|
+
console.log(chalk.bold.cyan('╚' + '═'.repeat(title.length + 4) + '╝\n'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function printStep(number, total, title) {
|
|
32
|
+
console.log(chalk.bold.magenta(`\n>> STEP ${number}/${total}`) + chalk.bold.white(` :: ${title}`));
|
|
33
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function printSuccess(message) {
|
|
37
|
+
console.log(chalk.green('[OK] ') + chalk.white(message));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function printWarning(message) {
|
|
41
|
+
console.log(chalk.yellow('[WARN] ') + chalk.white(message));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function printError(message) {
|
|
45
|
+
console.log(chalk.red('[ERR] ') + chalk.white(message));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function printInfo(message) {
|
|
49
|
+
console.log(chalk.cyan('[INFO] ') + chalk.white(message));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function printBox(title, items = []) {
|
|
53
|
+
console.log(chalk.bold.cyan('┌─ ' + title));
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
console.log(chalk.cyan('│ ') + chalk.white(item));
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.cyan('└─\n'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function printSummary(title, items = []) {
|
|
61
|
+
const safeItems = Array.isArray(items) ? items.map((i) => String(i)) : [];
|
|
62
|
+
const titleText = ` ${String(title || '').toUpperCase()}`;
|
|
63
|
+
const contentWidth = Math.max(64, titleText.length, ...safeItems.map((i) => i.length + 4));
|
|
64
|
+
|
|
65
|
+
console.log(chalk.bold.cyan(`\n╔${'═'.repeat(contentWidth)}╗`));
|
|
66
|
+
console.log(chalk.bold.cyan('║') + chalk.bold.green(chalk.bold.white(titleText.padEnd(contentWidth))) + chalk.bold.cyan('║'));
|
|
67
|
+
console.log(chalk.bold.cyan(`╠${'═'.repeat(contentWidth)}╣`));
|
|
68
|
+
|
|
69
|
+
for (const item of safeItems) {
|
|
70
|
+
const line = ` - ${item}`;
|
|
71
|
+
console.log(chalk.bold.cyan('║') + chalk.white(line.padEnd(contentWidth)) + chalk.bold.cyan('║'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.bold.cyan(`╚${'═'.repeat(contentWidth)}╝\n`));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function printFooter(stepsInput = []) {
|
|
78
|
+
const nextSteps = Array.isArray(stepsInput) ? stepsInput.map((s) => String(s)) : [String(stepsInput)];
|
|
79
|
+
const contentWidth = Math.max(64, ' NEXT STEPS'.length, ...nextSteps.map((s, i) => `${i + 1}. ${s}`.length + 2));
|
|
80
|
+
|
|
81
|
+
console.log(chalk.bold.cyan(`\n╔${'═'.repeat(contentWidth)}╗`));
|
|
82
|
+
console.log(chalk.bold.cyan('║') + chalk.bold.yellow(' NEXT STEPS'.padEnd(contentWidth)) + chalk.bold.cyan('║'));
|
|
83
|
+
console.log(chalk.bold.cyan(`╠${'═'.repeat(contentWidth)}╣`));
|
|
84
|
+
|
|
85
|
+
nextSteps.forEach((step, idx) => {
|
|
86
|
+
const line = ` ${idx + 1}. ${step}`;
|
|
87
|
+
console.log(chalk.bold.cyan('║') + chalk.white(line.padEnd(contentWidth)) + chalk.bold.cyan('║'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(chalk.bold.cyan(`╚${'═'.repeat(contentWidth)}╝\n`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function printTable(headers, rows) {
|
|
94
|
+
const colWidths = headers.map((h, i) => {
|
|
95
|
+
return Math.max(h.length, Math.max(...rows.map(r => String(r[i] || '').length)));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Header
|
|
99
|
+
console.log(chalk.bold.cyan('┌' + colWidths.map(w => '─'.repeat(w + 2)).join('┬') + '┐'));
|
|
100
|
+
console.log(
|
|
101
|
+
chalk.bold.cyan('│'),
|
|
102
|
+
colWidths.map((w, i) => chalk.bold.white(headers[i].padEnd(w))).join(chalk.bold.cyan(' │ ')),
|
|
103
|
+
chalk.bold.cyan('│')
|
|
104
|
+
);
|
|
105
|
+
console.log(chalk.bold.cyan('├' + colWidths.map(w => '─'.repeat(w + 2)).join('┼') + '┤'));
|
|
106
|
+
|
|
107
|
+
// Rows
|
|
108
|
+
rows.forEach((row) => {
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.cyan('│'),
|
|
111
|
+
colWidths.map((w, i) => String(row[i] || '').padEnd(w)).join(chalk.cyan(' │ ')),
|
|
112
|
+
chalk.cyan('│')
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(chalk.bold.cyan('└' + colWidths.map(w => '─'.repeat(w + 2)).join('┴') + '┘\n'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default {
|
|
120
|
+
printBanner,
|
|
121
|
+
printSection,
|
|
122
|
+
printStep,
|
|
123
|
+
printSuccess,
|
|
124
|
+
printWarning,
|
|
125
|
+
printError,
|
|
126
|
+
printInfo,
|
|
127
|
+
printBox,
|
|
128
|
+
printSummary,
|
|
129
|
+
printFooter,
|
|
130
|
+
printTable
|
|
131
|
+
};
|
|
@@ -10,9 +10,10 @@ import { globSync } from 'glob';
|
|
|
10
10
|
/**
|
|
11
11
|
* Inject API calls into frontend files
|
|
12
12
|
*/
|
|
13
|
-
export function injectApiCalls(projectPath, resources) {
|
|
13
|
+
export function injectApiCalls(projectPath, resources, options = {}) {
|
|
14
14
|
const injectedFiles = [];
|
|
15
15
|
const files = getFrontendFiles(projectPath);
|
|
16
|
+
const idField = options.idField || '_id';
|
|
16
17
|
|
|
17
18
|
for (const file of files) {
|
|
18
19
|
try {
|
|
@@ -23,7 +24,7 @@ export function injectApiCalls(projectPath, resources) {
|
|
|
23
24
|
for (const resource of resources) {
|
|
24
25
|
// Check if this file uses this resource
|
|
25
26
|
if (usesResource(content, resource)) {
|
|
26
|
-
const result = injectForResource(content, resource);
|
|
27
|
+
const result = injectForResource(content, resource, { idField });
|
|
27
28
|
if (result.modified) {
|
|
28
29
|
content = result.content;
|
|
29
30
|
modified = true;
|
|
@@ -70,13 +71,19 @@ function usesResource(content, resource) {
|
|
|
70
71
|
const statePattern = new RegExp(`\\[\\s*${varName}\\s*,\\s*${setterName}\\s*\\]\\s*=\\s*useState\\(`, 'g');
|
|
71
72
|
if (statePattern.test(content)) return true;
|
|
72
73
|
|
|
74
|
+
const staticArrayPattern = new RegExp(`const\\s+${varName}\\s*=\\s*\\[[\\s\\S]*?\\]\\s*;?`, 'g');
|
|
75
|
+
if (staticArrayPattern.test(content)) return true;
|
|
76
|
+
|
|
77
|
+
const mapPattern = new RegExp(`${varName}\\.map\\s*\\(`, 'g');
|
|
78
|
+
if (mapPattern.test(content)) return true;
|
|
79
|
+
|
|
73
80
|
return false;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
/**
|
|
77
84
|
* Inject API calls for a specific resource
|
|
78
85
|
*/
|
|
79
|
-
function injectForResource(content, resource) {
|
|
86
|
+
function injectForResource(content, resource, options = {}) {
|
|
80
87
|
let modified = false;
|
|
81
88
|
let nextContent = content;
|
|
82
89
|
|
|
@@ -85,10 +92,20 @@ function injectForResource(content, resource) {
|
|
|
85
92
|
const capitalSingular = capitalize(singular);
|
|
86
93
|
const capitalResource = capitalize(resourceName);
|
|
87
94
|
|
|
88
|
-
const
|
|
95
|
+
const hasStaticArray = new RegExp(`const\\s+${resourceName}\\s*=\\s*\\[[\\s\\S]*?\\]\\s*;?`).test(nextContent);
|
|
96
|
+
const hasApiImplementation = hasResourceApiCalls(nextContent, resourceName);
|
|
97
|
+
if (hasApiImplementation && !hasStaticArray && options.force !== true) {
|
|
98
|
+
return { content: nextContent, modified: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const importResult = ensureReactHooksImport(nextContent, ['useEffect', 'useState']);
|
|
89
102
|
nextContent = importResult.content;
|
|
90
103
|
modified = modified || importResult.changed;
|
|
91
104
|
|
|
105
|
+
const staticToStateResult = convertStaticArrayToState(nextContent, resourceName, capitalResource);
|
|
106
|
+
nextContent = staticToStateResult.content;
|
|
107
|
+
modified = modified || staticToStateResult.changed;
|
|
108
|
+
|
|
92
109
|
const apiUrlResult = ensureApiUrlConstant(nextContent);
|
|
93
110
|
nextContent = apiUrlResult.content;
|
|
94
111
|
modified = modified || apiUrlResult.changed;
|
|
@@ -125,15 +142,20 @@ function injectForResource(content, resource) {
|
|
|
125
142
|
nextContent = deleteResult.content;
|
|
126
143
|
modified = modified || deleteResult.changed;
|
|
127
144
|
|
|
128
|
-
const jsxResult =
|
|
145
|
+
const jsxResult = ensureIdsInJsx(nextContent, capitalSingular, options.idField || '_id');
|
|
129
146
|
nextContent = jsxResult.content;
|
|
130
147
|
modified = modified || jsxResult.changed;
|
|
131
148
|
|
|
132
149
|
return { content: nextContent, modified };
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
function
|
|
136
|
-
if (
|
|
152
|
+
function ensureReactHooksImport(content, requiredHooks = []) {
|
|
153
|
+
if (!requiredHooks.length) {
|
|
154
|
+
return { content, changed: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const missing = requiredHooks.filter((hook) => !new RegExp(`\\b${hook}\\b`).test(content));
|
|
158
|
+
if (missing.length === 0) {
|
|
137
159
|
return { content, changed: false };
|
|
138
160
|
}
|
|
139
161
|
|
|
@@ -144,19 +166,64 @@ function ensureUseEffectImport(content) {
|
|
|
144
166
|
.split(',')
|
|
145
167
|
.map(name => name.trim())
|
|
146
168
|
.filter(Boolean);
|
|
147
|
-
|
|
148
|
-
|
|
169
|
+
|
|
170
|
+
for (const hook of requiredHooks) {
|
|
171
|
+
if (!cleaned.includes(hook)) {
|
|
172
|
+
cleaned.push(hook);
|
|
173
|
+
}
|
|
149
174
|
}
|
|
175
|
+
|
|
150
176
|
return `import { ${cleaned.join(', ')} } from 'react';`;
|
|
151
177
|
});
|
|
152
178
|
return { content: next, changed: next !== content };
|
|
153
179
|
}
|
|
154
180
|
|
|
181
|
+
const reactDefaultImport = /import\s+([A-Za-z_$][\w$]*)\s+from\s+['"]react['"];?/;
|
|
182
|
+
if (reactDefaultImport.test(content)) {
|
|
183
|
+
const next = content.replace(reactDefaultImport, (_, defaultName) => {
|
|
184
|
+
return `import ${defaultName}, { ${requiredHooks.join(', ')} } from 'react';`;
|
|
185
|
+
});
|
|
186
|
+
return { content: next, changed: next !== content };
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
return { content, changed: false };
|
|
156
190
|
}
|
|
157
191
|
|
|
192
|
+
function convertStaticArrayToState(content, resourceName, capitalResource) {
|
|
193
|
+
const stateRegex = new RegExp(`const\\s+\\[\\s*${resourceName}\\s*,\\s*set${capitalResource}\\s*\\]\\s*=\\s*useState\\(`);
|
|
194
|
+
if (stateRegex.test(content)) {
|
|
195
|
+
return { content, changed: false };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const staticRegex = new RegExp(`const\\s+${resourceName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*;?`);
|
|
199
|
+
const staticMatch = content.match(staticRegex);
|
|
200
|
+
if (!staticMatch) {
|
|
201
|
+
return { content, changed: false };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const replacement = `const [${resourceName}, set${capitalResource}] = useState([]);`;
|
|
205
|
+
const matchIndex = staticMatch.index ?? -1;
|
|
206
|
+
|
|
207
|
+
const componentStart = findPrimaryComponentStart(content);
|
|
208
|
+
if (componentStart !== -1 && matchIndex !== -1 && matchIndex < componentStart) {
|
|
209
|
+
// Remove top-level static data and declare hook state inside component scope.
|
|
210
|
+
const withoutStatic = content.replace(staticRegex, '');
|
|
211
|
+
const componentStartAfterRemoval = findPrimaryComponentStart(withoutStatic);
|
|
212
|
+
const insertPos = findFirstLineBreakAfterBrace(withoutStatic, componentStartAfterRemoval);
|
|
213
|
+
if (insertPos !== -1) {
|
|
214
|
+
const indentedReplacement = `\n ${replacement}`;
|
|
215
|
+
const next = `${withoutStatic.slice(0, insertPos)}${indentedReplacement}${withoutStatic.slice(insertPos)}`;
|
|
216
|
+
return { content: next, changed: next !== content };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If static array is already inside a component/function scope, replace in-place.
|
|
221
|
+
const next = content.replace(staticRegex, replacement);
|
|
222
|
+
return { content: next, changed: next !== content };
|
|
223
|
+
}
|
|
224
|
+
|
|
158
225
|
function ensureApiUrlConstant(content) {
|
|
159
|
-
if (/\b(API_URL|BACKEND_URL)\b/.test(content)) {
|
|
226
|
+
if (/\b(API_URL|API_BASE_URL|BACKEND_URL)\b/.test(content)) {
|
|
160
227
|
return { content, changed: false };
|
|
161
228
|
}
|
|
162
229
|
|
|
@@ -167,7 +234,7 @@ function ensureApiUrlConstant(content) {
|
|
|
167
234
|
|
|
168
235
|
const lastImport = importMatches[importMatches.length - 1];
|
|
169
236
|
const insertPos = lastImport.index + lastImport[0].length;
|
|
170
|
-
const next = `${content.slice(0, insertPos)}\n\nconst
|
|
237
|
+
const next = `${content.slice(0, insertPos)}\n\nconst API_BASE_URL =\n+ (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_API_URL) ||\n+ (typeof process !== 'undefined' && process.env && process.env.REACT_APP_API_URL) ||\n+ 'http://localhost:5000';\n+const API_URL = \`${'${API_BASE_URL}'}/api\`;${content.slice(insertPos)}`;
|
|
171
238
|
return { content: next, changed: true };
|
|
172
239
|
}
|
|
173
240
|
|
|
@@ -195,7 +262,7 @@ function ensureFetchFunction(content, resourceName, capitalResource, fetchFuncti
|
|
|
195
262
|
}
|
|
196
263
|
|
|
197
264
|
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}(
|
|
265
|
+
const snippet = `\n\n const ${fetchFunctionName} = async () => {\n try {\n const res = await fetch(\`\${API_URL}/${resourceName}\`);\n if (!res.ok) {\n throw new Error(\`Failed to fetch ${resourceName}: \${res.status}\`);\n }\n const payload = await res.json();\n const list = Array.isArray(payload)\n ? payload\n : payload.data || payload.items || payload.results || [];\n set${capitalResource}(Array.isArray(list) ? list : []);\n } catch (error) {\n console.error('Error fetching ${resourceName}:', error);\n }\n };`;
|
|
199
266
|
|
|
200
267
|
const next = `${content.slice(0, insertPos)}${snippet}${content.slice(insertPos)}`;
|
|
201
268
|
return { content: next, changed: true };
|
|
@@ -247,7 +314,7 @@ function replaceCreateHandler(content, ctx) {
|
|
|
247
314
|
const initialStateLiteral = extractInitialStateLiteral(content, newStateName, setNewStateName) || '{}';
|
|
248
315
|
const nonEmptyGuard = buildNonEmptyGuard(newStateName, initialStateLiteral);
|
|
249
316
|
|
|
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`;
|
|
317
|
+
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 } else {\n console.error('Failed to add ${singular}:', res.status);\n }\n } catch (error) {\n console.error('Error adding ${singular}:', error);\n }\n };\n\n`;
|
|
251
318
|
|
|
252
319
|
const next = `${content.slice(0, block.start)}${replacement}${content.slice(block.end)}`;
|
|
253
320
|
return { content: next, changed: true };
|
|
@@ -266,34 +333,49 @@ function replaceDeleteHandler(content, ctx) {
|
|
|
266
333
|
return { content, changed: false };
|
|
267
334
|
}
|
|
268
335
|
|
|
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`;
|
|
336
|
+
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 } else {\n console.error('Failed to delete ${singular}:', res.status);\n }\n } catch (error) {\n console.error('Error deleting ${singular}:', error);\n }\n };\n\n`;
|
|
270
337
|
|
|
271
338
|
const next = `${content.slice(0, block.start)}${replacement}${content.slice(block.end)}`;
|
|
272
339
|
return { content: next, changed: true };
|
|
273
340
|
}
|
|
274
341
|
|
|
275
|
-
function
|
|
342
|
+
function ensureIdsInJsx(content, capitalSingular, idField = '_id') {
|
|
276
343
|
let next = content;
|
|
277
344
|
let changed = false;
|
|
278
345
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
next
|
|
282
|
-
|
|
283
|
-
|
|
346
|
+
if (idField === '_id') {
|
|
347
|
+
const keyReplaced = next.replace(/key=\{(\w+)\.id\}/g, 'key={$1._id}');
|
|
348
|
+
if (keyReplaced !== next) {
|
|
349
|
+
next = keyReplaced;
|
|
350
|
+
changed = true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const deleteCallPattern = new RegExp(`onClick=\\{\\(\\)\\s*=>\\s*delete${capitalSingular}\\((\\w+)\\.id\\)\\}`, 'g');
|
|
354
|
+
const clickReplaced = next.replace(deleteCallPattern, `onClick={() => delete${capitalSingular}($1._id)}`);
|
|
355
|
+
if (clickReplaced !== next) {
|
|
356
|
+
next = clickReplaced;
|
|
357
|
+
changed = true;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
const keyReplaced = next.replace(/key=\{(\w+)\._id\}/g, 'key={$1.id}');
|
|
361
|
+
if (keyReplaced !== next) {
|
|
362
|
+
next = keyReplaced;
|
|
363
|
+
changed = true;
|
|
364
|
+
}
|
|
284
365
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
366
|
+
const deleteCallPattern = new RegExp(`onClick=\\{\\(\\)\\s*=>\\s*delete${capitalSingular}\\((\\w+)\\._id\\)\\}`, 'g');
|
|
367
|
+
const clickReplaced = next.replace(deleteCallPattern, `onClick={() => delete${capitalSingular}($1.id)}`);
|
|
368
|
+
if (clickReplaced !== next) {
|
|
369
|
+
next = clickReplaced;
|
|
370
|
+
changed = true;
|
|
371
|
+
}
|
|
290
372
|
}
|
|
291
373
|
|
|
292
374
|
return { content: next, changed };
|
|
293
375
|
}
|
|
294
376
|
|
|
295
377
|
function extractInitialStateLiteral(content, stateName, setterName) {
|
|
296
|
-
const regex = new RegExp(`const\\s+\\[\\s*${stateName}\\s*,\\s*${setterName}\\s*\\]\\s*=\\s*useState\\((\\{[\\s\\S]*?\\})\\)`);
|
|
378
|
+
const regex = new RegExp(`const\\s+\\[\\s*${stateName}\\s*,\\s*${setterName}\\s*\\]\\s*=\\s*(?:React\\.)?useState\\((\\{[\\s\\S]*?\\})\\)`);
|
|
297
379
|
const match = content.match(regex);
|
|
298
380
|
return match ? match[1] : null;
|
|
299
381
|
}
|
|
@@ -339,6 +421,42 @@ function findConstArrowFunction(content, functionName) {
|
|
|
339
421
|
return { start: startMatch.index, end };
|
|
340
422
|
}
|
|
341
423
|
|
|
424
|
+
function findPrimaryComponentStart(content) {
|
|
425
|
+
const functionComponentMatch = /function\s+[A-Z][A-Za-z0-9_$]*\s*\([^)]*\)\s*\{/.exec(content);
|
|
426
|
+
if (functionComponentMatch) {
|
|
427
|
+
return functionComponentMatch.index;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const arrowComponentMatch = /const\s+[A-Z][A-Za-z0-9_$]*\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/.exec(content);
|
|
431
|
+
if (arrowComponentMatch) {
|
|
432
|
+
return arrowComponentMatch.index;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return -1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function findFirstLineBreakAfterBrace(content, startIndex) {
|
|
439
|
+
const braceIndex = content.indexOf('{', startIndex);
|
|
440
|
+
if (braceIndex === -1) return -1;
|
|
441
|
+
|
|
442
|
+
const lineBreakIndex = content.indexOf('\n', braceIndex);
|
|
443
|
+
if (lineBreakIndex === -1) return -1;
|
|
444
|
+
|
|
445
|
+
return lineBreakIndex + 1;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function hasResourceApiCalls(content, resourceName) {
|
|
449
|
+
const escaped = escapeRegExp(resourceName);
|
|
450
|
+
const quotedOrTemplatePath = '(["\'])[^\\n]*?\\/api\\/' + escaped + '[^\\n]*?\\1|`[^`]*?\\/api\\/' + escaped + '[^`]*?`';
|
|
451
|
+
const fetchPattern = new RegExp('fetch\\s*\\(\\s*(?:' + quotedOrTemplatePath + ')', 'i');
|
|
452
|
+
const axiosPattern = new RegExp('axios\\.(get|post|put|patch|delete)\\s*\\(\\s*(?:' + quotedOrTemplatePath + ')', 'i');
|
|
453
|
+
return fetchPattern.test(content) || axiosPattern.test(content);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function escapeRegExp(value) {
|
|
457
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
458
|
+
}
|
|
459
|
+
|
|
342
460
|
/**
|
|
343
461
|
* Capitalize string
|
|
344
462
|
*/
|