create-backlist 6.0.0 → 6.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.
Files changed (45) hide show
  1. package/bin/backlist.js +227 -0
  2. package/package.json +10 -4
  3. package/src/analyzer.js +210 -89
  4. package/src/db/prisma.ts +4 -0
  5. package/src/generators/dotnet.js +120 -94
  6. package/src/generators/java.js +157 -109
  7. package/src/generators/node.js +262 -85
  8. package/src/generators/template.js +38 -2
  9. package/src/scanner/index.js +99 -0
  10. package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
  11. package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
  12. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +7 -2
  13. package/src/templates/java-spring/partials/AuthController.java.ejs +23 -10
  14. package/src/templates/java-spring/partials/Controller.java.ejs +17 -6
  15. package/src/templates/java-spring/partials/Dockerfile.ejs +6 -1
  16. package/src/templates/java-spring/partials/Entity.java.ejs +15 -5
  17. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +30 -7
  18. package/src/templates/java-spring/partials/JwtService.java.ejs +38 -10
  19. package/src/templates/java-spring/partials/Repository.java.ejs +10 -1
  20. package/src/templates/java-spring/partials/Service.java.ejs +45 -7
  21. package/src/templates/java-spring/partials/User.java.ejs +17 -4
  22. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
  23. package/src/templates/java-spring/partials/UserRepository.java.ejs +8 -0
  24. package/src/templates/java-spring/partials/docker-compose.yml.ejs +16 -8
  25. package/src/templates/node-ts-express/base/server.ts +12 -5
  26. package/src/templates/node-ts-express/base/tsconfig.json +13 -3
  27. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
  28. package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
  29. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
  30. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
  31. package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
  32. package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
  33. package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
  34. package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
  35. package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
  36. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
  37. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
  38. package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
  39. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
  40. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
  41. package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
  42. package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
  43. package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
  44. package/src/utils.js +19 -4
  45. package/bin/index.js +0 -141
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+
3
+ const inquirer = require('inquirer');
4
+ const chalk = require('chalk');
5
+ const fs = require('fs-extra');
6
+ const path = require('path');
7
+ const { Command } = require('commander');
8
+ const chokidar = require('chokidar');
9
+
10
+ const { isCommandAvailable } = require('../src/utils');
11
+
12
+ const { generateNodeProject } = require('../src/generators/node');
13
+ const { generateDotnetProject } = require('../src/generators/dotnet');
14
+ const { generateJavaProject } = require('../src/generators/java');
15
+ const { generatePythonProject } = require('../src/generators/python');
16
+
17
+ const { scanFrontend, writeContracts } = require('../src/scanner');
18
+
19
+ function resolveOptionsFromFlags(flags) {
20
+ return {
21
+ projectName: flags.projectName || 'backend',
22
+ srcPath: flags.srcPath || 'src',
23
+ stack: flags.stack || 'node-ts-express',
24
+ dbType: flags.dbType,
25
+ addAuth: flags.addAuth,
26
+ addSeeder: flags.addSeeder,
27
+ extraFeatures: flags.extraFeatures || [],
28
+ projectDir: path.resolve(process.cwd(), flags.projectName || 'backend'),
29
+ frontendSrcDir: path.resolve(process.cwd(), flags.srcPath || 'src'),
30
+ };
31
+ }
32
+
33
+ async function runGeneration(options, contracts) {
34
+ switch (options.stack) {
35
+ case 'node-ts-express':
36
+ await generateNodeProject({ ...options, contracts });
37
+ break;
38
+
39
+ case 'dotnet-webapi':
40
+ if (!await isCommandAvailable('dotnet')) {
41
+ throw new Error('.NET SDK is not installed. Please install it from https://dotnet.microsoft.com/download');
42
+ }
43
+ await generateDotnetProject({ ...options, contracts });
44
+ break;
45
+
46
+ case 'java-spring':
47
+ if (!await isCommandAvailable('java')) {
48
+ throw new Error('Java (JDK 17 or newer) is not installed. Please install a JDK to continue.');
49
+ }
50
+ await generateJavaProject({ ...options, contracts });
51
+ break;
52
+
53
+ case 'python-fastapi':
54
+ if (!await isCommandAvailable('python')) {
55
+ throw new Error('Python is not installed. Please install Python (3.8+) and pip to continue.');
56
+ }
57
+ await generatePythonProject({ ...options, contracts });
58
+ break;
59
+
60
+ default:
61
+ throw new Error(`The selected stack '${options.stack}' is not supported yet.`);
62
+ }
63
+ }
64
+
65
+ async function interactiveMain() {
66
+ console.log(chalk.cyan.bold('Welcome to Backlist! The Polyglot Backend Generator.'));
67
+
68
+ const answers = await inquirer.prompt([
69
+ {
70
+ type: 'input',
71
+ name: 'projectName',
72
+ message: 'Enter a name for your backend directory:',
73
+ default: 'backend',
74
+ validate: input => input ? true : 'Project name cannot be empty.'
75
+ },
76
+ {
77
+ type: 'list',
78
+ name: 'stack',
79
+ message: 'Select the backend stack:',
80
+ choices: [
81
+ { name: 'Node.js (TypeScript, Express)', value: 'node-ts-express' },
82
+ { name: 'C# (ASP.NET Core Web API)', value: 'dotnet-webapi' },
83
+ { name: 'Java (Spring Boot)', value: 'java-spring' },
84
+ { name: 'Python (FastAPI)', value: 'python-fastapi' },
85
+ ],
86
+ },
87
+ {
88
+ type: 'input',
89
+ name: 'srcPath',
90
+ message: 'Enter the path to your frontend `src` directory:',
91
+ default: 'src',
92
+ },
93
+ {
94
+ type: 'list',
95
+ name: 'dbType',
96
+ message: 'Select your database type for Node.js:',
97
+ choices: [
98
+ { name: 'NoSQL (MongoDB with Mongoose)', value: 'mongoose' },
99
+ { name: 'SQL (PostgreSQL/MySQL with Prisma)', value: 'prisma' },
100
+ ],
101
+ when: (answers) => answers.stack === 'node-ts-express'
102
+ },
103
+ {
104
+ type: 'confirm',
105
+ name: 'addAuth',
106
+ message: 'Add JWT authentication boilerplate?',
107
+ default: true,
108
+ when: (answers) => answers.stack === 'node-ts-express'
109
+ },
110
+ {
111
+ type: 'confirm',
112
+ name: 'addSeeder',
113
+ message: 'Add a database seeder with sample data?',
114
+ default: true,
115
+ when: (answers) => answers.stack === 'node-ts-express' && answers.addAuth
116
+ },
117
+ {
118
+ type: 'checkbox',
119
+ name: 'extraFeatures',
120
+ message: 'Select additional features for Node.js:',
121
+ choices: [
122
+ { name: 'Docker Support (Dockerfile & docker-compose.yml)', value: 'docker', checked: true },
123
+ { name: 'API Testing Boilerplate (Jest & Supertest)', value: 'testing', checked: true },
124
+ { name: 'API Documentation (Swagger UI)', value: 'swagger', checked: true },
125
+ ],
126
+ when: (answers) => answers.stack === 'node-ts-express'
127
+ }
128
+ ]);
129
+
130
+ const options = {
131
+ ...answers,
132
+ projectDir: path.resolve(process.cwd(), answers.projectName),
133
+ frontendSrcDir: path.resolve(process.cwd(), answers.srcPath),
134
+ };
135
+
136
+ const contracts = await scanFrontend({ frontendSrcDir: options.frontendSrcDir });
137
+
138
+ try {
139
+ console.log(chalk.blue(`\nStarting backend generation for: ${chalk.bold(options.stack)}`));
140
+ await runGeneration(options, contracts);
141
+
142
+ console.log(chalk.green.bold('\nBackend generation complete!'));
143
+ console.log('\nNext Steps:');
144
+ console.log(chalk.cyan(` cd ${options.projectName}`));
145
+ console.log(chalk.cyan(' (Check the generated README.md for instructions)'));
146
+ } catch (error) {
147
+ console.error(chalk.red.bold('\nAn error occurred during generation:'));
148
+ console.error(error);
149
+
150
+ if (fs.existsSync(options.projectDir)) {
151
+ console.log(chalk.yellow(' -> Cleaning up failed installation...'));
152
+ fs.removeSync(options.projectDir);
153
+ }
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ async function main() {
159
+ const program = new Command();
160
+
161
+ program
162
+ .name('backlist')
163
+ .description('Backlist CLI - generate backend from frontend via AST scan')
164
+ .version('1.0.0');
165
+
166
+ program.command('scan')
167
+ .description('Scan frontend and write contracts JSON')
168
+ .option('-s, --srcPath <path>', 'frontend src path', 'src')
169
+ .option('-o, --out <file>', 'output contracts file', '.backlist/contracts.json')
170
+ .action(async (flags) => {
171
+ const frontendSrcDir = path.resolve(process.cwd(), flags.srcPath);
172
+ const outFile = path.resolve(process.cwd(), flags.out);
173
+ const contracts = await scanFrontend({ frontendSrcDir });
174
+ await writeContracts(outFile, contracts);
175
+ console.log(chalk.green(`Wrote contracts to ${outFile}`));
176
+ });
177
+
178
+ program.command('generate')
179
+ .description('Generate backend using contracts')
180
+ .requiredOption('-k, --stack <stack>', 'stack: node-ts-express | dotnet-webapi | java-spring | python-fastapi')
181
+ .option('-p, --projectName <name>', 'backend directory', 'backend')
182
+ .option('-s, --srcPath <path>', 'frontend src path', 'src')
183
+ .option('-c, --contracts <file>', 'contracts file', '.backlist/contracts.json')
184
+ .action(async (flags) => {
185
+ const options = resolveOptionsFromFlags(flags);
186
+ const contractsPath = path.resolve(process.cwd(), flags.contracts);
187
+ const contracts = fs.existsSync(contractsPath)
188
+ ? await fs.readJson(contractsPath)
189
+ : await scanFrontend({ frontendSrcDir: options.frontendSrcDir });
190
+
191
+ await runGeneration(options, contracts);
192
+ console.log(chalk.green('Generation complete.'));
193
+ });
194
+
195
+ program.command('watch')
196
+ .description('Watch frontend and regenerate backend on changes')
197
+ .requiredOption('-k, --stack <stack>', 'stack')
198
+ .option('-p, --projectName <name>', 'backend directory', 'backend')
199
+ .option('-s, --srcPath <path>', 'frontend src path', 'src')
200
+ .action(async (flags) => {
201
+ const options = resolveOptionsFromFlags(flags);
202
+ const watcher = chokidar.watch(options.frontendSrcDir, { ignoreInitial: true });
203
+
204
+ const run = async () => {
205
+ const contracts = await scanFrontend({ frontendSrcDir: options.frontendSrcDir });
206
+ await runGeneration(options, contracts);
207
+ console.log(chalk.green(`[watch] regenerated at ${new Date().toLocaleTimeString()}`));
208
+ };
209
+
210
+ await run();
211
+ watcher.on('add', run).on('change', run).on('unlink', run);
212
+ console.log(chalk.cyan(`[watch] watching ${options.frontendSrcDir}`));
213
+ });
214
+
215
+ // If no args => old interactive mode
216
+ if (process.argv.length <= 2) {
217
+ await interactiveMain();
218
+ return;
219
+ }
220
+
221
+ await program.parseAsync(process.argv);
222
+ }
223
+
224
+ main().catch((e) => {
225
+ console.error(e);
226
+ process.exit(1);
227
+ });
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "6.0.0",
3
+ "version": "6.0.2",
4
4
  "description": "An advanced, multi-language backend generator based on frontend analysis.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
- "create-backlist": "bin/index.js"
7
+ "create-backlist": "bin/index.js",
8
+ "backlist": "bin/index.js"
8
9
  },
9
10
  "files": [
10
11
  "bin",
11
- "src"
12
+ "src",
13
+ "templates"
12
14
  ],
13
15
  "scripts": {
14
16
  "start": "node bin/index.js"
@@ -20,11 +22,15 @@
20
22
  "@babel/traverse": "^7.22.8",
21
23
  "axios": "^1.13.1",
22
24
  "chalk": "^4.1.2",
25
+ "chokidar": "^5.0.0",
26
+ "commander": "^14.0.2",
23
27
  "ejs": "^3.1.9",
24
28
  "execa": "^6.1.0",
29
+ "fast-glob": "^3.3.3",
25
30
  "fs-extra": "^11.1.1",
26
31
  "glob": "^10.3.3",
27
32
  "inquirer": "^8.2.4",
33
+ "ts-morph": "^27.0.2",
28
34
  "unzipper": "^0.12.3"
29
35
  }
30
- }
36
+ }
package/src/analyzer.js CHANGED
@@ -4,121 +4,242 @@ const parser = require('@babel/parser');
4
4
  const traverse = require('@babel/traverse').default;
5
5
 
6
6
  /**
7
- * Converts a string to TitleCase, which is suitable for model and controller names.
8
- * e.g., 'user-orders' -> 'UserOrders'
9
- * @param {string} str The input string.
10
- * @returns {string} The TitleCased string.
7
+ * Convert segment -> ControllerName (Users -> Users, user-orders -> UserOrders)
11
8
  */
12
9
  function toTitleCase(str) {
13
- if (!str) return 'Default';
14
- return str.replace(/-_(\w)/g, g => g[1].toUpperCase()) // handle snake_case and kebab-case
15
- .replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
16
- .replace(/[^a-zA-Z0-9]/g, '');
10
+ if (!str) return 'Default';
11
+ return String(str)
12
+ .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase()) // kebab/snake to camel
13
+ .replace(/^\w/, c => c.toUpperCase())
14
+ .replace(/[^a-zA-Z0-9]/g, '');
15
+ }
16
+
17
+ function normalizeRouteForBackend(urlValue) {
18
+ // Convert template placeholders {id} -> :id
19
+ return urlValue.replace(/\{(\w+)\}/g, ':$1');
20
+ }
21
+
22
+ function inferTypeFromNode(node) {
23
+ if (!node) return 'String';
24
+ switch (node.type) {
25
+ case 'StringLiteral': return 'String';
26
+ case 'NumericLiteral': return 'Number';
27
+ case 'BooleanLiteral': return 'Boolean';
28
+ case 'NullLiteral': return 'String';
29
+ default: return 'String';
30
+ }
31
+ }
32
+
33
+ function extractObjectSchema(objExpr) {
34
+ const schemaFields = {};
35
+ if (!objExpr || objExpr.type !== 'ObjectExpression') return null;
36
+
37
+ for (const prop of objExpr.properties) {
38
+ if (prop.type !== 'ObjectProperty') continue;
39
+
40
+ const key =
41
+ prop.key.type === 'Identifier' ? prop.key.name :
42
+ prop.key.type === 'StringLiteral' ? prop.key.value :
43
+ null;
44
+
45
+ if (!key) continue;
46
+
47
+ schemaFields[key] = inferTypeFromNode(prop.value);
48
+ }
49
+ return schemaFields;
17
50
  }
18
51
 
19
52
  /**
20
- * Analyzes frontend source files to find API endpoints and their details.
21
- * @param {string} srcPath The path to the frontend source directory.
22
- * @returns {Promise<Array<object>>} A promise that resolves to an array of endpoint objects.
53
+ * Try to resolve Identifier -> its init value if it's const payload = {...}
23
54
  */
55
+ function resolveIdentifierToInit(path, identifierName) {
56
+ try {
57
+ const binding = path.scope.getBinding(identifierName);
58
+ if (!binding) return null;
59
+ const declPath = binding.path; // VariableDeclarator path usually
60
+ if (!declPath || !declPath.node) return null;
61
+
62
+ // VariableDeclarator: id = init
63
+ if (declPath.node.type === 'VariableDeclarator') {
64
+ return declPath.node.init || null;
65
+ }
66
+ return null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function getUrlValue(urlNode) {
73
+ if (!urlNode) return null;
74
+
75
+ if (urlNode.type === 'StringLiteral') return urlNode.value;
76
+
77
+ if (urlNode.type === 'TemplateLiteral') {
78
+ // `/api/users/${id}` -> `/api/users/{id}`
79
+ const quasis = urlNode.quasis || [];
80
+ const exprs = urlNode.expressions || [];
81
+ let out = '';
82
+ for (let i = 0; i < quasis.length; i++) {
83
+ out += quasis[i].value.raw;
84
+ if (exprs[i]) {
85
+ if (exprs[i].type === 'Identifier') out += `{${exprs[i].name}}`;
86
+ else out += `{param}`;
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ function deriveControllerNameFromUrl(urlValue) {
96
+ // supports: /api/users, /api/v1/users, /api/admin/users
97
+ const parts = urlValue.split('/').filter(Boolean); // ["api","v1","users"]
98
+ const apiIndex = parts.indexOf('api');
99
+ const seg = (apiIndex >= 0 && parts.length > apiIndex + 1)
100
+ ? parts[apiIndex + 1]
101
+ : parts[0];
102
+
103
+ return toTitleCase(seg);
104
+ }
105
+
106
+ function deriveActionName(method, route) {
107
+ // method + last segment heuristic
108
+ const cleaned = route.replace(/^\/api\//, '/').replace(/[/:{}-]/g, ' ');
109
+ const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || 'Action';
110
+ return `${method.toLowerCase()}${toTitleCase(last)}`;
111
+ }
112
+
113
+ function extractPathParams(route) {
114
+ const params = [];
115
+ const re = /[:{]([a-zA-Z0-9_]+)[}]/g; // matches :id or {id}
116
+ let m;
117
+ while ((m = re.exec(route))) params.push(m[1]);
118
+ return Array.from(new Set(params));
119
+ }
120
+
121
+ function extractQueryParamsFromUrl(urlValue) {
122
+ // if url has ?a=b&c=d as string literal
123
+ try {
124
+ const qIndex = urlValue.indexOf('?');
125
+ if (qIndex === -1) return [];
126
+ const qs = urlValue.slice(qIndex + 1);
127
+ return qs.split('&').map(p => p.split('=')[0]).filter(Boolean);
128
+ } catch {
129
+ return [];
130
+ }
131
+ }
132
+
24
133
  async function analyzeFrontend(srcPath) {
25
134
  if (!fs.existsSync(srcPath)) {
26
135
  throw new Error(`The source directory '${srcPath}' does not exist.`);
27
136
  }
28
137
 
29
- const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, { ignore: 'node_modules/**' });
138
+ const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, { ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] });
30
139
  const endpoints = new Map();
31
140
 
32
141
  for (const file of files) {
33
142
  const code = await fs.readFile(file, 'utf-8');
143
+
144
+ let ast;
34
145
  try {
35
- const ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
36
-
37
- traverse(ast, {
38
- CallExpression(path) {
39
- // We are only interested in 'fetch' calls
40
- if (path.node.callee.name !== 'fetch') return;
41
-
42
- const urlNode = path.node.arguments[0];
43
-
44
- let urlValue;
45
- if (urlNode.type === 'StringLiteral') {
46
- urlValue = urlNode.value;
47
- } else if (urlNode.type === 'TemplateLiteral' && urlNode.quasis.length > 0) {
48
- // Reconstruct path for dynamic URLs like `/api/users/${id}` -> `/api/users/{id}`
49
- urlValue = urlNode.quasis.map((q, i) => {
50
- return q.value.raw + (urlNode.expressions[i] ? `{${urlNode.expressions[i].name || 'id'}}` : '');
51
- }).join('');
52
- }
53
-
54
- // Only process API calls that start with '/api/'
55
- if (!urlValue || !urlValue.startsWith('/api/')) return;
56
-
57
- let method = 'GET';
58
- let schemaFields = null;
59
-
60
- const optionsNode = path.node.arguments[1];
146
+ ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
147
+ } catch {
148
+ continue;
149
+ }
150
+
151
+ traverse(ast, {
152
+ CallExpression(callPath) {
153
+ const node = callPath.node;
154
+
155
+ // --- Detect fetch(url, options) ---
156
+ const isFetch = node.callee.type === 'Identifier' && node.callee.name === 'fetch';
157
+
158
+ // --- Detect axios.<method>(url, data?, config?) ---
159
+ const isAxiosMethod =
160
+ node.callee.type === 'MemberExpression' &&
161
+ node.callee.object.type === 'Identifier' &&
162
+ node.callee.object.name === 'axios' &&
163
+ node.callee.property.type === 'Identifier';
164
+
165
+ if (!isFetch && !isAxiosMethod) return;
166
+
167
+ let urlValue = null;
168
+ let method = 'GET';
169
+ let schemaFields = null;
170
+
171
+ if (isFetch) {
172
+ urlValue = getUrlValue(node.arguments[0]);
173
+ const optionsNode = node.arguments[1];
174
+
61
175
  if (optionsNode && optionsNode.type === 'ObjectExpression') {
62
- // Find the HTTP method
63
- const methodProp = optionsNode.properties.find(p => p.key.name === 'method');
64
- if (methodProp && methodProp.value.type === 'StringLiteral') {
65
- method = methodProp.value.value.toUpperCase();
66
- }
176
+ const methodProp = optionsNode.properties.find(p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'method');
177
+ if (methodProp && methodProp.value.type === 'StringLiteral') method = methodProp.value.value.toUpperCase();
178
+
179
+ const bodyProp = optionsNode.properties.find(p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'body');
180
+
181
+ if (bodyProp) {
182
+ // body: JSON.stringify(objOrVar)
183
+ const v = bodyProp.value;
184
+
185
+ if (v.type === 'CallExpression' && v.callee.type === 'MemberExpression' &&
186
+ v.callee.object.type === 'Identifier' && v.callee.object.name === 'JSON' &&
187
+ v.callee.property.type === 'Identifier' && v.callee.property.name === 'stringify'
188
+ ) {
189
+ const arg0 = v.arguments[0];
67
190
 
68
- // --- NEW LOGIC: Analyze the 'body' for POST/PUT requests ---
69
- if (method === 'POST' || method === 'PUT') {
70
- const bodyProp = optionsNode.properties.find(p => p.key.name === 'body');
71
-
72
- // Check if body is wrapped in JSON.stringify
73
- if (bodyProp && bodyProp.value.callee && bodyProp.value.callee.name === 'JSON.stringify') {
74
- const dataObjectNode = bodyProp.value.arguments[0];
75
-
76
- // This is a simplified analysis assuming the object is defined inline.
77
- // A more robust solution would trace variables back to their definition.
78
- if (dataObjectNode.type === 'ObjectExpression') {
79
- schemaFields = {};
80
- dataObjectNode.properties.forEach(prop => {
81
- const key = prop.key.name;
82
- const valueNode = prop.value;
83
-
84
- // Infer Mongoose schema type based on the value's literal type
85
- if (valueNode.type === 'StringLiteral') {
86
- schemaFields[key] = 'String';
87
- } else if (valueNode.type === 'NumericLiteral') {
88
- schemaFields[key] = 'Number';
89
- } else if (valueNode.type === 'BooleanLiteral') {
90
- schemaFields[key] = 'Boolean';
91
- } else {
92
- // Default to String if the type is complex or a variable
93
- schemaFields[key] = 'String';
94
- }
95
- });
191
+ if (arg0?.type === 'ObjectExpression') {
192
+ schemaFields = extractObjectSchema(arg0);
193
+ } else if (arg0?.type === 'Identifier') {
194
+ const init = resolveIdentifierToInit(callPath, arg0.name);
195
+ if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
96
196
  }
97
197
  }
98
198
  }
99
199
  }
200
+ }
100
201
 
101
- // Generate a clean controller name (e.g., /api/user-orders -> UserOrders)
102
- const controllerName = toTitleCase(urlValue.split('/')[2]);
103
- const key = `${method}:${urlValue}`;
104
-
105
- // Avoid adding duplicate endpoints
106
- if (!endpoints.has(key)) {
107
- endpoints.set(key, {
108
- path: urlValue,
109
- method,
110
- controllerName,
111
- schemaFields // This will be null for GET/DELETE, and an object for POST/PUT
112
- });
202
+ if (isAxiosMethod) {
203
+ method = node.callee.property.name.toUpperCase();
204
+ urlValue = getUrlValue(node.arguments[0]);
205
+
206
+ // axios.post(url, data)
207
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
208
+ const dataArg = node.arguments[1];
209
+ if (dataArg?.type === 'ObjectExpression') {
210
+ schemaFields = extractObjectSchema(dataArg);
211
+ } else if (dataArg?.type === 'Identifier') {
212
+ const init = resolveIdentifierToInit(callPath, dataArg.name);
213
+ if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
214
+ }
113
215
  }
114
- },
115
- });
116
- } catch (e) {
117
- // Ignore files that babel can't parse (e.g., CSS-in-JS files)
118
- }
216
+ }
217
+
218
+ if (!urlValue || !urlValue.startsWith('/api/')) return;
219
+
220
+ const route = normalizeRouteForBackend(urlValue.split('?')[0]); // drop query string
221
+ const controllerName = deriveControllerNameFromUrl(urlValue);
222
+ const actionName = deriveActionName(method, route);
223
+
224
+ const key = `${method}:${route}`;
225
+ if (!endpoints.has(key)) {
226
+ endpoints.set(key, {
227
+ path: urlValue,
228
+ route, // normalized for backend: /api/users/:id
229
+ method,
230
+ controllerName,
231
+ actionName,
232
+ pathParams: extractPathParams(route),
233
+ queryParams: extractQueryParamsFromUrl(urlValue),
234
+ schemaFields, // backward compat (your generators use this)
235
+ requestBody: schemaFields ? { fields: schemaFields } : null,
236
+ sourceFile: file
237
+ });
238
+ }
239
+ },
240
+ });
119
241
  }
120
242
 
121
- // Return all found endpoints as an array
122
243
  return Array.from(endpoints.values());
123
244
  }
124
245
 
@@ -0,0 +1,4 @@
1
+ // Auto-generated by create-backlist
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ export const prisma = new PrismaClient();