create-backlist 6.0.7 → 6.0.8
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/index.js +141 -0
- package/package.json +4 -10
- package/src/analyzer.js +104 -315
- package/src/generators/dotnet.js +94 -120
- package/src/generators/java.js +109 -157
- package/src/generators/node.js +85 -262
- package/src/generators/template.js +2 -38
- package/src/templates/dotnet/partials/Controller.cs.ejs +14 -7
- package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +2 -7
- package/src/templates/java-spring/partials/AuthController.java.ejs +10 -23
- package/src/templates/java-spring/partials/Controller.java.ejs +6 -17
- package/src/templates/java-spring/partials/Dockerfile.ejs +1 -6
- package/src/templates/java-spring/partials/Entity.java.ejs +5 -15
- package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +7 -30
- package/src/templates/java-spring/partials/JwtService.java.ejs +10 -38
- package/src/templates/java-spring/partials/Repository.java.ejs +1 -10
- package/src/templates/java-spring/partials/Service.java.ejs +7 -45
- package/src/templates/java-spring/partials/User.java.ejs +4 -17
- package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +4 -10
- package/src/templates/java-spring/partials/UserRepository.java.ejs +0 -8
- package/src/templates/java-spring/partials/docker-compose.yml.ejs +8 -16
- package/src/templates/node-ts-express/base/server.ts +6 -13
- package/src/templates/node-ts-express/base/tsconfig.json +3 -13
- package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +7 -17
- package/src/templates/node-ts-express/partials/App.test.ts.ejs +26 -49
- package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +62 -56
- package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +10 -21
- 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 +11 -9
- package/src/templates/node-ts-express/partials/Model.cs.ejs +7 -25
- package/src/templates/node-ts-express/partials/Model.ts.ejs +12 -20
- package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +55 -72
- package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +12 -27
- package/src/templates/node-ts-express/partials/README.md.ejs +12 -9
- package/src/templates/node-ts-express/partials/Seeder.ts.ejs +64 -44
- package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +16 -31
- package/src/templates/node-ts-express/partials/package.json.ejs +1 -3
- package/src/templates/node-ts-express/partials/routes.ts.ejs +24 -35
- package/src/utils.js +5 -17
- package/bin/backlist.js +0 -228
- package/src/db/prisma.ts +0 -4
- package/src/scanner/analyzeFrontend.js +0 -146
- package/src/scanner/index.js +0 -99
- package/src/templates/dotnet/partials/Dto.cs.ejs +0 -8
- package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +0 -4
package/bin/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
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'); // FIX: Correctly require the 'path' module
|
|
7
|
+
const { isCommandAvailable } = require('../src/utils');
|
|
8
|
+
|
|
9
|
+
// Import ALL generators
|
|
10
|
+
const { generateNodeProject } = require('../src/generators/node');
|
|
11
|
+
const { generateDotnetProject } = require('../src/generators/dotnet');
|
|
12
|
+
const { generateJavaProject } = require('../src/generators/java');
|
|
13
|
+
const { generatePythonProject } = require('../src/generators/python');
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log(chalk.cyan.bold('🚀 Welcome to Backlist! The Polyglot Backend Generator.'));
|
|
17
|
+
|
|
18
|
+
const answers = await inquirer.prompt([
|
|
19
|
+
// --- General Questions ---
|
|
20
|
+
{
|
|
21
|
+
type: 'input',
|
|
22
|
+
name: 'projectName',
|
|
23
|
+
message: 'Enter a name for your backend directory:',
|
|
24
|
+
default: 'backend',
|
|
25
|
+
validate: input => input ? true : 'Project name cannot be empty.'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: 'list',
|
|
29
|
+
name: 'stack',
|
|
30
|
+
message: 'Select the backend stack:',
|
|
31
|
+
choices: [
|
|
32
|
+
{ name: 'Node.js (TypeScript, Express)', value: 'node-ts-express' },
|
|
33
|
+
{ name: 'C# (ASP.NET Core Web API)', value: 'dotnet-webapi' },
|
|
34
|
+
{ name: 'Java (Spring Boot)', value: 'java-spring' },
|
|
35
|
+
{ name: 'Python (FastAPI)', value: 'python-fastapi' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'srcPath',
|
|
41
|
+
message: 'Enter the path to your frontend `src` directory:',
|
|
42
|
+
default: 'src',
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// --- Node.js Specific Questions ---
|
|
46
|
+
{
|
|
47
|
+
type: 'list',
|
|
48
|
+
name: 'dbType',
|
|
49
|
+
message: 'Select your database type for Node.js:',
|
|
50
|
+
choices: [
|
|
51
|
+
{ name: 'NoSQL (MongoDB with Mongoose)', value: 'mongoose' },
|
|
52
|
+
{ name: 'SQL (PostgreSQL/MySQL with Prisma)', value: 'prisma' },
|
|
53
|
+
],
|
|
54
|
+
when: (answers) => answers.stack === 'node-ts-express'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'confirm',
|
|
58
|
+
name: 'addAuth',
|
|
59
|
+
message: 'Add JWT authentication boilerplate?',
|
|
60
|
+
default: true,
|
|
61
|
+
when: (answers) => answers.stack === 'node-ts-express'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'addSeeder',
|
|
66
|
+
message: 'Add a database seeder with sample data?',
|
|
67
|
+
default: true,
|
|
68
|
+
// Seeder only makes sense if there's an auth/user model to seed
|
|
69
|
+
when: (answers) => answers.stack === 'node-ts-express' && answers.addAuth
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'checkbox',
|
|
73
|
+
name: 'extraFeatures',
|
|
74
|
+
message: 'Select additional features for Node.js:',
|
|
75
|
+
choices: [
|
|
76
|
+
{ name: 'Docker Support (Dockerfile & docker-compose.yml)', value: 'docker', checked: true },
|
|
77
|
+
{ name: 'API Testing Boilerplate (Jest & Supertest)', value: 'testing', checked: true },
|
|
78
|
+
{ name: 'API Documentation (Swagger UI)', value: 'swagger', checked: true },
|
|
79
|
+
],
|
|
80
|
+
when: (answers) => answers.stack === 'node-ts-express'
|
|
81
|
+
}
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const options = {
|
|
85
|
+
...answers,
|
|
86
|
+
projectDir: path.resolve(process.cwd(), answers.projectName),
|
|
87
|
+
frontendSrcDir: path.resolve(process.cwd(), answers.srcPath),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
console.log(chalk.blue(`\n✨ Starting backend generation for: ${chalk.bold(options.stack)}`));
|
|
92
|
+
|
|
93
|
+
// --- Dispatcher Logic for ALL Stacks ---
|
|
94
|
+
switch (options.stack) {
|
|
95
|
+
case 'node-ts-express':
|
|
96
|
+
await generateNodeProject(options);
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case 'dotnet-webapi':
|
|
100
|
+
if (!await isCommandAvailable('dotnet')) {
|
|
101
|
+
throw new Error('.NET SDK is not installed. Please install it from https://dotnet.microsoft.com/download');
|
|
102
|
+
}
|
|
103
|
+
await generateDotnetProject(options);
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'java-spring':
|
|
107
|
+
if (!await isCommandAvailable('java')) {
|
|
108
|
+
throw new Error('Java (JDK 17 or newer) is not installed. Please install a JDK to continue.');
|
|
109
|
+
}
|
|
110
|
+
await generateJavaProject(options);
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'python-fastapi':
|
|
114
|
+
if (!await isCommandAvailable('python')) {
|
|
115
|
+
throw new Error('Python is not installed. Please install Python (3.8+) and pip to continue.');
|
|
116
|
+
}
|
|
117
|
+
await generatePythonProject(options);
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`The selected stack '${options.stack}' is not supported yet.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(chalk.green.bold('\n✅ Backend generation complete!'));
|
|
125
|
+
console.log('\nNext Steps:');
|
|
126
|
+
console.log(chalk.cyan(` cd ${options.projectName}`));
|
|
127
|
+
console.log(chalk.cyan(' (Check the generated README.md for instructions)'));
|
|
128
|
+
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(chalk.red.bold('\n❌ An error occurred during generation:'));
|
|
131
|
+
console.error(error);
|
|
132
|
+
|
|
133
|
+
if (fs.existsSync(options.projectDir)) {
|
|
134
|
+
console.log(chalk.yellow(' -> Cleaning up failed installation...'));
|
|
135
|
+
fs.removeSync(options.projectDir);
|
|
136
|
+
}
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-backlist",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.8",
|
|
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"
|
|
8
|
-
"backlist": "bin/index.js"
|
|
7
|
+
"create-backlist": "bin/index.js"
|
|
9
8
|
},
|
|
10
9
|
"files": [
|
|
11
10
|
"bin",
|
|
12
|
-
"src"
|
|
13
|
-
"templates"
|
|
11
|
+
"src"
|
|
14
12
|
],
|
|
15
13
|
"scripts": {
|
|
16
14
|
"start": "node bin/index.js"
|
|
@@ -22,15 +20,11 @@
|
|
|
22
20
|
"@babel/traverse": "^7.22.8",
|
|
23
21
|
"axios": "^1.13.1",
|
|
24
22
|
"chalk": "^4.1.2",
|
|
25
|
-
"chokidar": "^5.0.0",
|
|
26
|
-
"commander": "^14.0.2",
|
|
27
23
|
"ejs": "^3.1.9",
|
|
28
24
|
"execa": "^6.1.0",
|
|
29
|
-
"fast-glob": "^3.3.3",
|
|
30
25
|
"fs-extra": "^11.1.1",
|
|
31
26
|
"glob": "^10.3.3",
|
|
32
27
|
"inquirer": "^8.2.4",
|
|
33
|
-
"ts-morph": "^27.0.2",
|
|
34
28
|
"unzipper": "^0.12.3"
|
|
35
29
|
}
|
|
36
|
-
}
|
|
30
|
+
}
|
package/src/analyzer.js
CHANGED
|
@@ -1,336 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return String(p || "").replace(/\\/g, "/");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readJSONSafe(p) {
|
|
16
|
-
try {
|
|
17
|
-
return fs.readJsonSync(p);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function findAuthUsageInRepo(rootDir) {
|
|
24
|
-
const pkgPath = path.join(rootDir, "package.json");
|
|
25
|
-
const pkg = readJSONSafe(pkgPath) || {};
|
|
26
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
27
|
-
|
|
28
|
-
const authDeps = [
|
|
29
|
-
"next-auth",
|
|
30
|
-
"@auth/core",
|
|
31
|
-
"@clerk/nextjs",
|
|
32
|
-
"@supabase/auth-helpers-nextjs",
|
|
33
|
-
"@supabase/supabase-js",
|
|
34
|
-
"firebase",
|
|
35
|
-
"lucia",
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
if (authDeps.some((d) => deps[d])) return true;
|
|
39
|
-
|
|
40
|
-
const scanDirs = ["src", "app", "pages", "components", "lib", "utils"]
|
|
41
|
-
.map((d) => path.join(rootDir, d))
|
|
42
|
-
.filter((d) => fs.existsSync(d));
|
|
43
|
-
|
|
44
|
-
const patterns = [
|
|
45
|
-
"next-auth",
|
|
46
|
-
"getServerSession",
|
|
47
|
-
"useSession",
|
|
48
|
-
"SessionProvider",
|
|
49
|
-
"@clerk/nextjs",
|
|
50
|
-
"ClerkProvider",
|
|
51
|
-
"auth()",
|
|
52
|
-
"currentUser",
|
|
53
|
-
"createServerClient",
|
|
54
|
-
"jwt",
|
|
55
|
-
"bearer",
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
for (const dir of scanDirs) {
|
|
59
|
-
const files = glob.sync(`${normalizeSlashes(dir)}/**/*.{js,ts,jsx,tsx}`, {
|
|
60
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
for (const f of files) {
|
|
64
|
-
try {
|
|
65
|
-
const content = fs.readFileSync(f, "utf8");
|
|
66
|
-
if (patterns.some((p) => content.includes(p))) return true;
|
|
67
|
-
} catch {}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const { glob } = require('glob');
|
|
3
|
+
const parser = require('@babel/parser');
|
|
4
|
+
const traverse = require('@babel/traverse').default;
|
|
5
|
+
|
|
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.
|
|
11
|
+
*/
|
|
74
12
|
function toTitleCase(str) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.replace(/[^a-zA-Z0-9]/g, "");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function normalizeRouteForBackend(urlValue) {
|
|
83
|
-
return urlValue.replace(/\{(\w+)\}/g, ":$1");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function extractPathParams(route) {
|
|
87
|
-
const params = [];
|
|
88
|
-
const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
|
|
89
|
-
let m;
|
|
90
|
-
while ((m = re.exec(route))) params.push(m[1]);
|
|
91
|
-
return Array.from(new Set(params));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function extractQueryParamsFromUrl(urlValue) {
|
|
95
|
-
try {
|
|
96
|
-
const qIndex = urlValue.indexOf("?");
|
|
97
|
-
if (qIndex === -1) return [];
|
|
98
|
-
const qs = urlValue.slice(qIndex + 1);
|
|
99
|
-
return qs
|
|
100
|
-
.split("&")
|
|
101
|
-
.map((p) => p.split("=")[0])
|
|
102
|
-
.filter(Boolean);
|
|
103
|
-
} catch {
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function deriveControllerNameFromUrl(urlValue) {
|
|
109
|
-
const parts = String(urlValue || "").split("/").filter(Boolean);
|
|
110
|
-
const apiIndex = parts.indexOf("api");
|
|
111
|
-
let seg = null;
|
|
112
|
-
if (apiIndex >= 0) {
|
|
113
|
-
seg = parts[apiIndex + 1] || null;
|
|
114
|
-
if (seg && /^v\d+$/i.test(seg)) seg = parts[apiIndex + 2] || seg;
|
|
115
|
-
} else {
|
|
116
|
-
seg = parts[0] || null;
|
|
117
|
-
}
|
|
118
|
-
return toTitleCase(seg);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function deriveActionName(method, route) {
|
|
122
|
-
const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
|
|
123
|
-
const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
|
|
124
|
-
return `${String(method).toLowerCase()}${toTitleCase(last)}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function detectAxiosLikeMethod(node) {
|
|
128
|
-
if (!node.callee || node.callee.type !== "MemberExpression") return null;
|
|
129
|
-
const prop = node.callee.property;
|
|
130
|
-
if (!prop || prop.type !== "Identifier") return null;
|
|
131
|
-
const name = prop.name.toLowerCase();
|
|
132
|
-
if (!HTTP_METHODS.has(name)) return null;
|
|
133
|
-
return name.toUpperCase();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getUrlValue(urlNode) {
|
|
137
|
-
if (!urlNode) return null;
|
|
138
|
-
if (urlNode.type === "StringLiteral") return urlNode.value;
|
|
139
|
-
if (urlNode.type === "TemplateLiteral") {
|
|
140
|
-
const quasis = urlNode.quasis || [];
|
|
141
|
-
const exprs = urlNode.expressions || [];
|
|
142
|
-
let out = "";
|
|
143
|
-
for (let i = 0; i < quasis.length; i++) {
|
|
144
|
-
out += quasis[i].value.raw;
|
|
145
|
-
if (exprs[i]) {
|
|
146
|
-
if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
|
|
147
|
-
else out += `{param${i + 1}}`;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function inferTypeFromNode(node) {
|
|
156
|
-
if (!node) return "String";
|
|
157
|
-
switch (node.type) {
|
|
158
|
-
case "StringLiteral":
|
|
159
|
-
return "String";
|
|
160
|
-
case "NumericLiteral":
|
|
161
|
-
return "Number";
|
|
162
|
-
case "BooleanLiteral":
|
|
163
|
-
return "Boolean";
|
|
164
|
-
case "NullLiteral":
|
|
165
|
-
return "String";
|
|
166
|
-
default:
|
|
167
|
-
return "String";
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function extractObjectSchema(objExpr) {
|
|
172
|
-
const schema = {};
|
|
173
|
-
if (!objExpr || objExpr.type !== "ObjectExpression") return null;
|
|
174
|
-
for (const prop of objExpr.properties) {
|
|
175
|
-
if (prop.type !== "ObjectProperty") continue;
|
|
176
|
-
const key =
|
|
177
|
-
prop.key.type === "Identifier"
|
|
178
|
-
? prop.key.name
|
|
179
|
-
: prop.key.type === "StringLiteral"
|
|
180
|
-
? prop.key.value
|
|
181
|
-
: null;
|
|
182
|
-
if (!key) continue;
|
|
183
|
-
schema[key] = inferTypeFromNode(prop.value);
|
|
184
|
-
}
|
|
185
|
-
return schema;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function resolveIdentifierToInit(pathObj, identifierName) {
|
|
189
|
-
try {
|
|
190
|
-
const binding = pathObj.scope.getBinding(identifierName);
|
|
191
|
-
if (!binding) return null;
|
|
192
|
-
const decl = binding.path.node;
|
|
193
|
-
if (!decl) return null;
|
|
194
|
-
if (decl.type === "VariableDeclarator") return decl.init || null;
|
|
195
|
-
return null;
|
|
196
|
-
} catch {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
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, '');
|
|
199
17
|
}
|
|
200
18
|
|
|
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
|
+
*/
|
|
201
24
|
async function analyzeFrontend(srcPath) {
|
|
202
|
-
if (!srcPath)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
|
|
206
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
|
|
207
|
-
});
|
|
25
|
+
if (!fs.existsSync(srcPath)) {
|
|
26
|
+
throw new Error(`The source directory '${srcPath}' does not exist.`);
|
|
27
|
+
}
|
|
208
28
|
|
|
29
|
+
const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, { ignore: 'node_modules/**' });
|
|
209
30
|
const endpoints = new Map();
|
|
210
31
|
|
|
211
32
|
for (const file of files) {
|
|
212
|
-
|
|
33
|
+
const code = await fs.readFile(file, 'utf-8');
|
|
213
34
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (mProp?.value?.type === "StringLiteral") method = mProp.value.value.toUpperCase();
|
|
247
|
-
|
|
248
|
-
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
249
|
-
const bProp = optionsNode.properties.find(
|
|
250
|
-
(p) => p.type === "ObjectProperty" && p.key?.type === "Identifier" && p.key.name === "body"
|
|
251
|
-
);
|
|
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];
|
|
61
|
+
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
|
+
}
|
|
252
67
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
});
|
|
269
96
|
}
|
|
270
97
|
}
|
|
271
98
|
}
|
|
272
99
|
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (axiosMethod) {
|
|
276
|
-
method = axiosMethod;
|
|
277
|
-
urlValue = getUrlValue(node.arguments[0]);
|
|
278
100
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
});
|
|
286
113
|
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const route = normalizeRouteForBackend(apiPath.split("?")[0]);
|
|
293
|
-
const controllerName = deriveControllerNameFromUrl(apiPath);
|
|
294
|
-
const actionName = deriveActionName(method, route);
|
|
295
|
-
const key = `${method}:${route}`;
|
|
296
|
-
|
|
297
|
-
if (!endpoints.has(key)) {
|
|
298
|
-
endpoints.set(key, {
|
|
299
|
-
path: apiPath,
|
|
300
|
-
route,
|
|
301
|
-
method,
|
|
302
|
-
controllerName,
|
|
303
|
-
actionName,
|
|
304
|
-
pathParams: extractPathParams(route),
|
|
305
|
-
queryParams: extractQueryParamsFromUrl(apiPath),
|
|
306
|
-
schemaFields,
|
|
307
|
-
requestBody: schemaFields ? { fields: schemaFields } : null,
|
|
308
|
-
sourceFile: normalizeSlashes(file),
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Ignore files that babel can't parse (e.g., CSS-in-JS files)
|
|
118
|
+
}
|
|
313
119
|
}
|
|
314
120
|
|
|
121
|
+
// Return all found endpoints as an array
|
|
315
122
|
return Array.from(endpoints.values());
|
|
316
123
|
}
|
|
317
124
|
|
|
318
|
-
|
|
319
|
-
const rootDir = path.resolve(projectRoot);
|
|
320
|
-
|
|
321
|
-
const frontendSrc = ["src", "app", "pages"]
|
|
322
|
-
.map((d) => path.join(rootDir, d))
|
|
323
|
-
.find((d) => fs.existsSync(d));
|
|
324
|
-
|
|
325
|
-
const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
|
|
326
|
-
const hasAuth = findAuthUsageInRepo(rootDir);
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
rootDir: normalizeSlashes(rootDir),
|
|
330
|
-
hasAuth,
|
|
331
|
-
addAuth: hasAuth,
|
|
332
|
-
endpoints,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
module.exports = { analyze, analyzeFrontend };
|
|
125
|
+
module.exports = { analyzeFrontend };
|