create-backlist 6.0.0 → 6.0.3
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/bin/backlist.js +227 -0
- package/package.json +10 -4
- package/src/analyzer.js +245 -91
- package/src/db/prisma.ts +4 -0
- package/src/generators/dotnet.js +120 -94
- package/src/generators/java.js +157 -109
- package/src/generators/node.js +262 -85
- package/src/generators/template.js +38 -2
- package/src/scanner/index.js +99 -0
- package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
- package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
- package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +7 -2
- package/src/templates/java-spring/partials/AuthController.java.ejs +23 -10
- package/src/templates/java-spring/partials/Controller.java.ejs +17 -6
- package/src/templates/java-spring/partials/Dockerfile.ejs +6 -1
- package/src/templates/java-spring/partials/Entity.java.ejs +15 -5
- package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +30 -7
- package/src/templates/java-spring/partials/JwtService.java.ejs +38 -10
- package/src/templates/java-spring/partials/Repository.java.ejs +10 -1
- package/src/templates/java-spring/partials/Service.java.ejs +45 -7
- package/src/templates/java-spring/partials/User.java.ejs +17 -4
- package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
- package/src/templates/java-spring/partials/UserRepository.java.ejs +8 -0
- package/src/templates/java-spring/partials/docker-compose.yml.ejs +16 -8
- package/src/templates/node-ts-express/base/server.ts +12 -5
- package/src/templates/node-ts-express/base/tsconfig.json +13 -3
- package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
- package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
- package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
- package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
- package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
- package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
- package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
- package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
- package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
- package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
- package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
- package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
- package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
- package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
- package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
- package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
- package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
- package/src/utils.js +19 -4
- package/bin/index.js +0 -141
package/bin/backlist.js
ADDED
|
@@ -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.
|
|
3
|
+
"version": "6.0.3",
|
|
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
|
@@ -3,123 +3,277 @@ const { glob } = require('glob');
|
|
|
3
3
|
const parser = require('@babel/parser');
|
|
4
4
|
const traverse = require('@babel/traverse').default;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* e.g., 'user-orders' -> 'UserOrders'
|
|
9
|
-
* @param {string} str The input string.
|
|
10
|
-
* @returns {string} The TitleCased string.
|
|
11
|
-
*/
|
|
6
|
+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
7
|
+
|
|
12
8
|
function toTitleCase(str) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
if (!str) return 'Default';
|
|
10
|
+
return String(str)
|
|
11
|
+
.replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
|
|
12
|
+
.replace(/^\w/, c => c.toUpperCase())
|
|
13
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeRouteForBackend(urlValue) {
|
|
17
|
+
return urlValue.replace(/\{(\w+)\}/g, ':$1');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function inferTypeFromNode(node) {
|
|
21
|
+
if (!node) return 'String';
|
|
22
|
+
switch (node.type) {
|
|
23
|
+
case 'StringLiteral': return 'String';
|
|
24
|
+
case 'NumericLiteral': return 'Number';
|
|
25
|
+
case 'BooleanLiteral': return 'Boolean';
|
|
26
|
+
case 'NullLiteral': return 'String';
|
|
27
|
+
default: return 'String';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractObjectSchema(objExpr) {
|
|
32
|
+
const schemaFields = {};
|
|
33
|
+
if (!objExpr || objExpr.type !== 'ObjectExpression') return null;
|
|
34
|
+
|
|
35
|
+
for (const prop of objExpr.properties) {
|
|
36
|
+
if (prop.type !== 'ObjectProperty') continue;
|
|
37
|
+
|
|
38
|
+
const key =
|
|
39
|
+
prop.key.type === 'Identifier' ? prop.key.name :
|
|
40
|
+
prop.key.type === 'StringLiteral' ? prop.key.value :
|
|
41
|
+
null;
|
|
42
|
+
|
|
43
|
+
if (!key) continue;
|
|
44
|
+
schemaFields[key] = inferTypeFromNode(prop.value);
|
|
45
|
+
}
|
|
46
|
+
return schemaFields;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveIdentifierToInit(path, identifierName) {
|
|
50
|
+
try {
|
|
51
|
+
const binding = path.scope.getBinding(identifierName);
|
|
52
|
+
if (!binding) return null;
|
|
53
|
+
const declPath = binding.path;
|
|
54
|
+
if (!declPath || !declPath.node) return null;
|
|
55
|
+
|
|
56
|
+
if (declPath.node.type === 'VariableDeclarator') return declPath.node.init || null;
|
|
57
|
+
return null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getUrlValue(urlNode) {
|
|
64
|
+
if (!urlNode) return null;
|
|
65
|
+
|
|
66
|
+
if (urlNode.type === 'StringLiteral') return urlNode.value;
|
|
67
|
+
|
|
68
|
+
if (urlNode.type === 'TemplateLiteral') {
|
|
69
|
+
// `/api/users/${id}` -> `/api/users/{id}` or `{param1}`
|
|
70
|
+
const quasis = urlNode.quasis || [];
|
|
71
|
+
const exprs = urlNode.expressions || [];
|
|
72
|
+
let out = '';
|
|
73
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
74
|
+
out += quasis[i].value.raw;
|
|
75
|
+
if (exprs[i]) {
|
|
76
|
+
if (exprs[i].type === 'Identifier') out += `{${exprs[i].name}}`;
|
|
77
|
+
else out += `{param${i + 1}}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractApiPath(urlValue) {
|
|
87
|
+
// supports:
|
|
88
|
+
// - /api/...
|
|
89
|
+
// - http://localhost:5000/api/...
|
|
90
|
+
if (!urlValue) return null;
|
|
91
|
+
const idx = urlValue.indexOf('/api/');
|
|
92
|
+
if (idx === -1) return null;
|
|
93
|
+
return urlValue.slice(idx); // => /api/...
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function deriveControllerNameFromUrl(urlValue) {
|
|
97
|
+
const apiPath = extractApiPath(urlValue) || urlValue;
|
|
98
|
+
const parts = String(apiPath).split('/').filter(Boolean); // ["api","v1","products"]
|
|
99
|
+
const apiIndex = parts.indexOf('api');
|
|
100
|
+
|
|
101
|
+
let seg = null;
|
|
102
|
+
|
|
103
|
+
if (apiIndex >= 0) {
|
|
104
|
+
seg = parts[apiIndex + 1] || null;
|
|
105
|
+
|
|
106
|
+
// skip version segment (v1, v2, v10...)
|
|
107
|
+
if (seg && /^v\d+$/i.test(seg)) {
|
|
108
|
+
seg = parts[apiIndex + 2] || seg;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
seg = parts[0] || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return toTitleCase(seg);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function deriveActionName(method, route) {
|
|
118
|
+
const cleaned = String(route).replace(/^\/api\//, '/').replace(/[/:{}-]/g, ' ');
|
|
119
|
+
const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || 'Action';
|
|
120
|
+
return `${String(method).toLowerCase()}${toTitleCase(last)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractPathParams(route) {
|
|
124
|
+
const params = [];
|
|
125
|
+
const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
|
|
126
|
+
let m;
|
|
127
|
+
while ((m = re.exec(route))) params.push(m[1]);
|
|
128
|
+
return Array.from(new Set(params));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractQueryParamsFromUrl(urlValue) {
|
|
132
|
+
try {
|
|
133
|
+
const qIndex = urlValue.indexOf('?');
|
|
134
|
+
if (qIndex === -1) return [];
|
|
135
|
+
const qs = urlValue.slice(qIndex + 1);
|
|
136
|
+
return qs.split('&').map(p => p.split('=')[0]).filter(Boolean);
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function detectAxiosLikeMethod(node) {
|
|
143
|
+
// axios.get(...) / api.get(...) / httpClient.post(...) etc
|
|
144
|
+
if (!node.callee || node.callee.type !== 'MemberExpression') return null;
|
|
145
|
+
|
|
146
|
+
const prop = node.callee.property;
|
|
147
|
+
if (!prop || prop.type !== 'Identifier') return null;
|
|
148
|
+
|
|
149
|
+
const name = prop.name.toLowerCase();
|
|
150
|
+
if (!HTTP_METHODS.has(name)) return null;
|
|
151
|
+
|
|
152
|
+
return name.toUpperCase();
|
|
17
153
|
}
|
|
18
154
|
|
|
19
|
-
/**
|
|
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.
|
|
23
|
-
*/
|
|
24
155
|
async function analyzeFrontend(srcPath) {
|
|
25
156
|
if (!fs.existsSync(srcPath)) {
|
|
26
157
|
throw new Error(`The source directory '${srcPath}' does not exist.`);
|
|
27
158
|
}
|
|
28
159
|
|
|
29
|
-
const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, {
|
|
160
|
+
const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, {
|
|
161
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**']
|
|
162
|
+
});
|
|
163
|
+
|
|
30
164
|
const endpoints = new Map();
|
|
31
165
|
|
|
32
166
|
for (const file of files) {
|
|
33
167
|
const code = await fs.readFile(file, 'utf-8');
|
|
168
|
+
|
|
169
|
+
let ast;
|
|
34
170
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let schemaFields = null;
|
|
59
|
-
|
|
60
|
-
const optionsNode = path.node.arguments[1];
|
|
171
|
+
ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
traverse(ast, {
|
|
177
|
+
CallExpression(callPath) {
|
|
178
|
+
const node = callPath.node;
|
|
179
|
+
|
|
180
|
+
const isFetch = node.callee.type === 'Identifier' && node.callee.name === 'fetch';
|
|
181
|
+
const axiosMethod = detectAxiosLikeMethod(node);
|
|
182
|
+
|
|
183
|
+
if (!isFetch && !axiosMethod) return;
|
|
184
|
+
|
|
185
|
+
let urlValue = null;
|
|
186
|
+
let method = 'GET';
|
|
187
|
+
let schemaFields = null;
|
|
188
|
+
|
|
189
|
+
// ---- fetch() ----
|
|
190
|
+
if (isFetch) {
|
|
191
|
+
urlValue = getUrlValue(node.arguments[0]);
|
|
192
|
+
const optionsNode = node.arguments[1];
|
|
193
|
+
|
|
61
194
|
if (optionsNode && optionsNode.type === 'ObjectExpression') {
|
|
62
|
-
|
|
63
|
-
|
|
195
|
+
const methodProp = optionsNode.properties.find(
|
|
196
|
+
p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'method'
|
|
197
|
+
);
|
|
64
198
|
if (methodProp && methodProp.value.type === 'StringLiteral') {
|
|
65
199
|
method = methodProp.value.value.toUpperCase();
|
|
66
200
|
}
|
|
67
201
|
|
|
68
|
-
//
|
|
69
|
-
if (
|
|
70
|
-
const bodyProp = optionsNode.properties.find(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
schemaFields[key] = 'String';
|
|
94
|
-
}
|
|
95
|
-
});
|
|
202
|
+
// body schema for POST/PUT/PATCH
|
|
203
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
204
|
+
const bodyProp = optionsNode.properties.find(
|
|
205
|
+
p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'body'
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (bodyProp) {
|
|
209
|
+
const v = bodyProp.value;
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
v.type === 'CallExpression' &&
|
|
213
|
+
v.callee.type === 'MemberExpression' &&
|
|
214
|
+
v.callee.object.type === 'Identifier' &&
|
|
215
|
+
v.callee.object.name === 'JSON' &&
|
|
216
|
+
v.callee.property.type === 'Identifier' &&
|
|
217
|
+
v.callee.property.name === 'stringify'
|
|
218
|
+
) {
|
|
219
|
+
const arg0 = v.arguments[0];
|
|
220
|
+
|
|
221
|
+
if (arg0?.type === 'ObjectExpression') {
|
|
222
|
+
schemaFields = extractObjectSchema(arg0);
|
|
223
|
+
} else if (arg0?.type === 'Identifier') {
|
|
224
|
+
const init = resolveIdentifierToInit(callPath, arg0.name);
|
|
225
|
+
if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
|
|
226
|
+
}
|
|
96
227
|
}
|
|
97
228
|
}
|
|
98
229
|
}
|
|
99
230
|
}
|
|
231
|
+
}
|
|
100
232
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
233
|
+
// ---- axios-like client ----
|
|
234
|
+
if (axiosMethod) {
|
|
235
|
+
method = axiosMethod;
|
|
236
|
+
urlValue = getUrlValue(node.arguments[0]);
|
|
237
|
+
|
|
238
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
239
|
+
const dataArg = node.arguments[1];
|
|
240
|
+
if (dataArg?.type === 'ObjectExpression') {
|
|
241
|
+
schemaFields = extractObjectSchema(dataArg);
|
|
242
|
+
} else if (dataArg?.type === 'Identifier') {
|
|
243
|
+
const init = resolveIdentifierToInit(callPath, dataArg.name);
|
|
244
|
+
if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
|
|
245
|
+
}
|
|
113
246
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// accept only URLs that contain /api/
|
|
250
|
+
const apiPath = extractApiPath(urlValue);
|
|
251
|
+
if (!apiPath) return;
|
|
252
|
+
|
|
253
|
+
const route = normalizeRouteForBackend(apiPath.split('?')[0]);
|
|
254
|
+
const controllerName = deriveControllerNameFromUrl(apiPath);
|
|
255
|
+
const actionName = deriveActionName(method, route);
|
|
256
|
+
|
|
257
|
+
const key = `${method}:${route}`;
|
|
258
|
+
if (!endpoints.has(key)) {
|
|
259
|
+
endpoints.set(key, {
|
|
260
|
+
path: apiPath,
|
|
261
|
+
route,
|
|
262
|
+
method,
|
|
263
|
+
controllerName,
|
|
264
|
+
actionName,
|
|
265
|
+
pathParams: extractPathParams(route),
|
|
266
|
+
queryParams: extractQueryParamsFromUrl(apiPath),
|
|
267
|
+
schemaFields,
|
|
268
|
+
requestBody: schemaFields ? { fields: schemaFields } : null,
|
|
269
|
+
sourceFile: file
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
119
274
|
}
|
|
120
275
|
|
|
121
|
-
// Return all found endpoints as an array
|
|
122
276
|
return Array.from(endpoints.values());
|
|
123
277
|
}
|
|
124
278
|
|
|
125
|
-
module.exports = { analyzeFrontend };
|
|
279
|
+
module.exports = { analyzeFrontend };
|
package/src/db/prisma.ts
ADDED