create-backlist 6.0.5 → 6.0.7
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 +95 -94
- package/package.json +1 -1
- package/src/analyzer.js +108 -162
- package/src/scanner/analyzeFrontend.js +36 -7
- package/src/utils.js +12 -15
package/bin/backlist.js
CHANGED
|
@@ -1,130 +1,132 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
3
|
|
|
3
|
-
const inquirer = require(
|
|
4
|
-
const chalk = require(
|
|
5
|
-
const fs = require(
|
|
6
|
-
const path = require(
|
|
7
|
-
const { Command } = require(
|
|
8
|
-
const chokidar = require(
|
|
4
|
+
const inquirer = require("inquirer");
|
|
5
|
+
const chalk = require("chalk");
|
|
6
|
+
const fs = require("fs-extra");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { Command } = require("commander");
|
|
9
|
+
const chokidar = require("chokidar");
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// FIX: repo has utils.js at root
|
|
12
|
+
const { isCommandAvailable } = require("../utils");
|
|
11
13
|
|
|
12
|
-
const { generateNodeProject } = require(
|
|
13
|
-
const { generateDotnetProject } = require(
|
|
14
|
-
const { generateJavaProject } = require(
|
|
15
|
-
const { generatePythonProject } = require(
|
|
14
|
+
const { generateNodeProject } = require("../src/generators/node");
|
|
15
|
+
const { generateDotnetProject } = require("../src/generators/dotnet");
|
|
16
|
+
const { generateJavaProject } = require("../src/generators/java");
|
|
17
|
+
const { generatePythonProject } = require("../src/generators/python");
|
|
16
18
|
|
|
17
|
-
const { scanFrontend, writeContracts } = require(
|
|
19
|
+
const { scanFrontend, writeContracts } = require("../src/scanner");
|
|
18
20
|
|
|
19
21
|
function resolveOptionsFromFlags(flags) {
|
|
20
22
|
return {
|
|
21
|
-
projectName: flags.projectName ||
|
|
22
|
-
srcPath: flags.srcPath ||
|
|
23
|
-
stack: flags.stack ||
|
|
23
|
+
projectName: flags.projectName || "backend",
|
|
24
|
+
srcPath: flags.srcPath || "src",
|
|
25
|
+
stack: flags.stack || "node-ts-express",
|
|
24
26
|
dbType: flags.dbType,
|
|
25
27
|
addAuth: flags.addAuth,
|
|
26
28
|
addSeeder: flags.addSeeder,
|
|
27
29
|
extraFeatures: flags.extraFeatures || [],
|
|
28
|
-
projectDir: path.resolve(process.cwd(), flags.projectName ||
|
|
29
|
-
frontendSrcDir: path.resolve(process.cwd(), flags.srcPath ||
|
|
30
|
+
projectDir: path.resolve(process.cwd(), flags.projectName || "backend"),
|
|
31
|
+
frontendSrcDir: path.resolve(process.cwd(), flags.srcPath || "src"),
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
async function runGeneration(options, contracts) {
|
|
34
36
|
switch (options.stack) {
|
|
35
|
-
case
|
|
37
|
+
case "node-ts-express":
|
|
36
38
|
await generateNodeProject({ ...options, contracts });
|
|
37
39
|
break;
|
|
38
40
|
|
|
39
|
-
case
|
|
40
|
-
if (!await isCommandAvailable(
|
|
41
|
-
throw new Error(
|
|
41
|
+
case "dotnet-webapi":
|
|
42
|
+
if (!(await isCommandAvailable("dotnet"))) {
|
|
43
|
+
throw new Error(".NET SDK is not installed. Install: https://dotnet.microsoft.com/download");
|
|
42
44
|
}
|
|
43
45
|
await generateDotnetProject({ ...options, contracts });
|
|
44
46
|
break;
|
|
45
47
|
|
|
46
|
-
case
|
|
47
|
-
if (!await isCommandAvailable(
|
|
48
|
-
throw new Error(
|
|
48
|
+
case "java-spring":
|
|
49
|
+
if (!(await isCommandAvailable("java"))) {
|
|
50
|
+
throw new Error("Java (JDK 17+) is not installed. Install a JDK to continue.");
|
|
49
51
|
}
|
|
50
52
|
await generateJavaProject({ ...options, contracts });
|
|
51
53
|
break;
|
|
52
54
|
|
|
53
|
-
case
|
|
54
|
-
if (!await isCommandAvailable(
|
|
55
|
-
throw new Error(
|
|
55
|
+
case "python-fastapi":
|
|
56
|
+
if (!(await isCommandAvailable("python"))) {
|
|
57
|
+
throw new Error("Python is not installed. Please install Python (3.8+) and pip.");
|
|
56
58
|
}
|
|
57
59
|
await generatePythonProject({ ...options, contracts });
|
|
58
60
|
break;
|
|
59
61
|
|
|
60
62
|
default:
|
|
61
|
-
throw new Error(`
|
|
63
|
+
throw new Error(`Unsupported stack '${options.stack}'.`);
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
async function interactiveMain() {
|
|
66
|
-
console.log(chalk.cyan.bold(
|
|
68
|
+
console.log(chalk.cyan.bold("Welcome to Backlist! The Polyglot Backend Generator."));
|
|
67
69
|
|
|
68
70
|
const answers = await inquirer.prompt([
|
|
69
71
|
{
|
|
70
|
-
type:
|
|
71
|
-
name:
|
|
72
|
-
message:
|
|
73
|
-
default:
|
|
74
|
-
validate: input => input ? true :
|
|
72
|
+
type: "input",
|
|
73
|
+
name: "projectName",
|
|
74
|
+
message: "Enter a name for your backend directory:",
|
|
75
|
+
default: "backend",
|
|
76
|
+
validate: (input) => (input ? true : "Project name cannot be empty."),
|
|
75
77
|
},
|
|
76
78
|
{
|
|
77
|
-
type:
|
|
78
|
-
name:
|
|
79
|
-
message:
|
|
79
|
+
type: "list",
|
|
80
|
+
name: "stack",
|
|
81
|
+
message: "Select the backend stack:",
|
|
80
82
|
choices: [
|
|
81
|
-
{ name:
|
|
82
|
-
{ name:
|
|
83
|
-
{ name:
|
|
84
|
-
{ name:
|
|
83
|
+
{ name: "Node.js (TypeScript, Express)", value: "node-ts-express" },
|
|
84
|
+
{ name: "C# (ASP.NET Core Web API)", value: "dotnet-webapi" },
|
|
85
|
+
{ name: "Java (Spring Boot)", value: "java-spring" },
|
|
86
|
+
{ name: "Python (FastAPI)", value: "python-fastapi" },
|
|
85
87
|
],
|
|
86
88
|
},
|
|
87
89
|
{
|
|
88
|
-
type:
|
|
89
|
-
name:
|
|
90
|
-
message:
|
|
91
|
-
default:
|
|
90
|
+
type: "input",
|
|
91
|
+
name: "srcPath",
|
|
92
|
+
message: "Enter the path to your frontend `src` directory:",
|
|
93
|
+
default: "src",
|
|
92
94
|
},
|
|
93
95
|
{
|
|
94
|
-
type:
|
|
95
|
-
name:
|
|
96
|
-
message:
|
|
96
|
+
type: "list",
|
|
97
|
+
name: "dbType",
|
|
98
|
+
message: "Select your database type for Node.js:",
|
|
97
99
|
choices: [
|
|
98
|
-
{ name:
|
|
99
|
-
{ name:
|
|
100
|
+
{ name: "NoSQL (MongoDB with Mongoose)", value: "mongoose" },
|
|
101
|
+
{ name: "SQL (PostgreSQL/MySQL with Prisma)", value: "prisma" },
|
|
100
102
|
],
|
|
101
|
-
when: (
|
|
103
|
+
when: (a) => a.stack === "node-ts-express",
|
|
102
104
|
},
|
|
103
105
|
{
|
|
104
|
-
type:
|
|
105
|
-
name:
|
|
106
|
-
message:
|
|
106
|
+
type: "confirm",
|
|
107
|
+
name: "addAuth",
|
|
108
|
+
message: "Add JWT authentication boilerplate?",
|
|
107
109
|
default: true,
|
|
108
|
-
when: (
|
|
110
|
+
when: (a) => a.stack === "node-ts-express",
|
|
109
111
|
},
|
|
110
112
|
{
|
|
111
|
-
type:
|
|
112
|
-
name:
|
|
113
|
-
message:
|
|
113
|
+
type: "confirm",
|
|
114
|
+
name: "addSeeder",
|
|
115
|
+
message: "Add a database seeder with sample data?",
|
|
114
116
|
default: true,
|
|
115
|
-
when: (
|
|
117
|
+
when: (a) => a.stack === "node-ts-express" && a.addAuth,
|
|
116
118
|
},
|
|
117
119
|
{
|
|
118
|
-
type:
|
|
119
|
-
name:
|
|
120
|
-
message:
|
|
120
|
+
type: "checkbox",
|
|
121
|
+
name: "extraFeatures",
|
|
122
|
+
message: "Select additional features for Node.js:",
|
|
121
123
|
choices: [
|
|
122
|
-
{ name:
|
|
123
|
-
{ name:
|
|
124
|
-
{ name:
|
|
124
|
+
{ name: "Docker Support", value: "docker", checked: true },
|
|
125
|
+
{ name: "API Testing Boilerplate (Jest & Supertest)", value: "testing", checked: true },
|
|
126
|
+
{ name: "API Documentation (Swagger UI)", value: "swagger", checked: true },
|
|
125
127
|
],
|
|
126
|
-
when: (
|
|
127
|
-
}
|
|
128
|
+
when: (a) => a.stack === "node-ts-express",
|
|
129
|
+
},
|
|
128
130
|
]);
|
|
129
131
|
|
|
130
132
|
const options = {
|
|
@@ -139,16 +141,15 @@ async function interactiveMain() {
|
|
|
139
141
|
console.log(chalk.blue(`\nStarting backend generation for: ${chalk.bold(options.stack)}`));
|
|
140
142
|
await runGeneration(options, contracts);
|
|
141
143
|
|
|
142
|
-
console.log(chalk.green.bold(
|
|
143
|
-
console.log(
|
|
144
|
+
console.log(chalk.green.bold("\nBackend generation complete!"));
|
|
145
|
+
console.log("\nNext Steps:");
|
|
144
146
|
console.log(chalk.cyan(` cd ${options.projectName}`));
|
|
145
|
-
console.log(chalk.cyan(' (Check the generated README.md for instructions)'));
|
|
146
147
|
} catch (error) {
|
|
147
|
-
console.error(chalk.red.bold(
|
|
148
|
+
console.error(chalk.red.bold("\nAn error occurred during generation:"));
|
|
148
149
|
console.error(error);
|
|
149
150
|
|
|
150
151
|
if (fs.existsSync(options.projectDir)) {
|
|
151
|
-
console.log(chalk.yellow(
|
|
152
|
+
console.log(chalk.yellow(" -> Cleaning up failed installation..."));
|
|
152
153
|
fs.removeSync(options.projectDir);
|
|
153
154
|
}
|
|
154
155
|
process.exit(1);
|
|
@@ -158,15 +159,13 @@ async function interactiveMain() {
|
|
|
158
159
|
async function main() {
|
|
159
160
|
const program = new Command();
|
|
160
161
|
|
|
162
|
+
program.name("backlist").description("Backlist CLI").version("5.1.0");
|
|
163
|
+
|
|
161
164
|
program
|
|
162
|
-
.
|
|
163
|
-
.description(
|
|
164
|
-
.
|
|
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')
|
|
165
|
+
.command("scan")
|
|
166
|
+
.description("Scan frontend and write contracts JSON")
|
|
167
|
+
.option("-s, --srcPath <path>", "frontend src path", "src")
|
|
168
|
+
.option("-o, --out <file>", "output contracts file", ".backlist/contracts.json")
|
|
170
169
|
.action(async (flags) => {
|
|
171
170
|
const frontendSrcDir = path.resolve(process.cwd(), flags.srcPath);
|
|
172
171
|
const outFile = path.resolve(process.cwd(), flags.out);
|
|
@@ -175,28 +174,31 @@ async function main() {
|
|
|
175
174
|
console.log(chalk.green(`Wrote contracts to ${outFile}`));
|
|
176
175
|
});
|
|
177
176
|
|
|
178
|
-
program
|
|
179
|
-
.
|
|
180
|
-
.
|
|
181
|
-
.
|
|
182
|
-
.option(
|
|
183
|
-
.option(
|
|
177
|
+
program
|
|
178
|
+
.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
184
|
.action(async (flags) => {
|
|
185
185
|
const options = resolveOptionsFromFlags(flags);
|
|
186
|
+
|
|
186
187
|
const contractsPath = path.resolve(process.cwd(), flags.contracts);
|
|
187
188
|
const contracts = fs.existsSync(contractsPath)
|
|
188
189
|
? await fs.readJson(contractsPath)
|
|
189
190
|
: await scanFrontend({ frontendSrcDir: options.frontendSrcDir });
|
|
190
191
|
|
|
191
192
|
await runGeneration(options, contracts);
|
|
192
|
-
console.log(chalk.green(
|
|
193
|
+
console.log(chalk.green("Generation complete."));
|
|
193
194
|
});
|
|
194
195
|
|
|
195
|
-
program
|
|
196
|
-
.
|
|
197
|
-
.
|
|
198
|
-
.
|
|
199
|
-
.option(
|
|
196
|
+
program
|
|
197
|
+
.command("watch")
|
|
198
|
+
.description("Watch frontend and regenerate backend on changes")
|
|
199
|
+
.requiredOption("-k, --stack <stack>", "stack")
|
|
200
|
+
.option("-p, --projectName <name>", "backend directory", "backend")
|
|
201
|
+
.option("-s, --srcPath <path>", "frontend src path", "src")
|
|
200
202
|
.action(async (flags) => {
|
|
201
203
|
const options = resolveOptionsFromFlags(flags);
|
|
202
204
|
const watcher = chokidar.watch(options.frontendSrcDir, { ignoreInitial: true });
|
|
@@ -208,11 +210,10 @@ async function main() {
|
|
|
208
210
|
};
|
|
209
211
|
|
|
210
212
|
await run();
|
|
211
|
-
watcher.on(
|
|
213
|
+
watcher.on("add", run).on("change", run).on("unlink", run);
|
|
212
214
|
console.log(chalk.cyan(`[watch] watching ${options.frontendSrcDir}`));
|
|
213
215
|
});
|
|
214
216
|
|
|
215
|
-
// If no args => old interactive mode
|
|
216
217
|
if (process.argv.length <= 2) {
|
|
217
218
|
await interactiveMain();
|
|
218
219
|
return;
|
package/package.json
CHANGED
package/src/analyzer.js
CHANGED
|
@@ -8,9 +8,6 @@ const traverse = require("@babel/traverse").default;
|
|
|
8
8
|
|
|
9
9
|
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete"]);
|
|
10
10
|
|
|
11
|
-
// -------------------------
|
|
12
|
-
// Small utils
|
|
13
|
-
// -------------------------
|
|
14
11
|
function normalizeSlashes(p) {
|
|
15
12
|
return String(p || "").replace(/\\/g, "/");
|
|
16
13
|
}
|
|
@@ -23,11 +20,7 @@ function readJSONSafe(p) {
|
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
// -------------------------
|
|
27
|
-
// AUTH detection (for addAuth)
|
|
28
|
-
// -------------------------
|
|
29
23
|
function findAuthUsageInRepo(rootDir) {
|
|
30
|
-
// 1) package.json quick check
|
|
31
24
|
const pkgPath = path.join(rootDir, "package.json");
|
|
32
25
|
const pkg = readJSONSafe(pkgPath) || {};
|
|
33
26
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
@@ -44,7 +37,6 @@ function findAuthUsageInRepo(rootDir) {
|
|
|
44
37
|
|
|
45
38
|
if (authDeps.some((d) => deps[d])) return true;
|
|
46
39
|
|
|
47
|
-
// 2) Source scan for common auth identifiers
|
|
48
40
|
const scanDirs = ["src", "app", "pages", "components", "lib", "utils"]
|
|
49
41
|
.map((d) => path.join(rootDir, d))
|
|
50
42
|
.filter((d) => fs.existsSync(d));
|
|
@@ -59,35 +51,26 @@ function findAuthUsageInRepo(rootDir) {
|
|
|
59
51
|
"auth()",
|
|
60
52
|
"currentUser",
|
|
61
53
|
"createServerClient",
|
|
54
|
+
"jwt",
|
|
55
|
+
"bearer",
|
|
62
56
|
];
|
|
63
57
|
|
|
64
58
|
for (const dir of scanDirs) {
|
|
65
59
|
const files = glob.sync(`${normalizeSlashes(dir)}/**/*.{js,ts,jsx,tsx}`, {
|
|
66
|
-
ignore: [
|
|
67
|
-
"**/node_modules/**",
|
|
68
|
-
"**/dist/**",
|
|
69
|
-
"**/build/**",
|
|
70
|
-
"**/.next/**",
|
|
71
|
-
"**/coverage/**",
|
|
72
|
-
],
|
|
60
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
|
|
73
61
|
});
|
|
74
62
|
|
|
75
63
|
for (const f of files) {
|
|
76
64
|
try {
|
|
77
65
|
const content = fs.readFileSync(f, "utf8");
|
|
78
66
|
if (patterns.some((p) => content.includes(p))) return true;
|
|
79
|
-
} catch {
|
|
80
|
-
// ignore read errors
|
|
81
|
-
}
|
|
67
|
+
} catch {}
|
|
82
68
|
}
|
|
83
69
|
}
|
|
84
70
|
|
|
85
71
|
return false;
|
|
86
72
|
}
|
|
87
73
|
|
|
88
|
-
// -------------------------
|
|
89
|
-
// Frontend API call analyzer (your code)
|
|
90
|
-
// -------------------------
|
|
91
74
|
function toTitleCase(str) {
|
|
92
75
|
if (!str) return "Default";
|
|
93
76
|
return String(str)
|
|
@@ -100,6 +83,75 @@ function normalizeRouteForBackend(urlValue) {
|
|
|
100
83
|
return urlValue.replace(/\{(\w+)\}/g, ":$1");
|
|
101
84
|
}
|
|
102
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
|
+
|
|
103
155
|
function inferTypeFromNode(node) {
|
|
104
156
|
if (!node) return "String";
|
|
105
157
|
switch (node.type) {
|
|
@@ -117,132 +169,38 @@ function inferTypeFromNode(node) {
|
|
|
117
169
|
}
|
|
118
170
|
|
|
119
171
|
function extractObjectSchema(objExpr) {
|
|
120
|
-
const
|
|
172
|
+
const schema = {};
|
|
121
173
|
if (!objExpr || objExpr.type !== "ObjectExpression") return null;
|
|
122
|
-
|
|
123
174
|
for (const prop of objExpr.properties) {
|
|
124
175
|
if (prop.type !== "ObjectProperty") continue;
|
|
125
|
-
|
|
126
176
|
const key =
|
|
127
177
|
prop.key.type === "Identifier"
|
|
128
178
|
? prop.key.name
|
|
129
179
|
: prop.key.type === "StringLiteral"
|
|
130
180
|
? prop.key.value
|
|
131
181
|
: null;
|
|
132
|
-
|
|
133
182
|
if (!key) continue;
|
|
134
|
-
|
|
183
|
+
schema[key] = inferTypeFromNode(prop.value);
|
|
135
184
|
}
|
|
136
|
-
return
|
|
185
|
+
return schema;
|
|
137
186
|
}
|
|
138
187
|
|
|
139
188
|
function resolveIdentifierToInit(pathObj, identifierName) {
|
|
140
189
|
try {
|
|
141
190
|
const binding = pathObj.scope.getBinding(identifierName);
|
|
142
191
|
if (!binding) return null;
|
|
143
|
-
const
|
|
144
|
-
if (!
|
|
145
|
-
|
|
146
|
-
if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
|
|
192
|
+
const decl = binding.path.node;
|
|
193
|
+
if (!decl) return null;
|
|
194
|
+
if (decl.type === "VariableDeclarator") return decl.init || null;
|
|
147
195
|
return null;
|
|
148
196
|
} catch {
|
|
149
197
|
return null;
|
|
150
198
|
}
|
|
151
199
|
}
|
|
152
200
|
|
|
153
|
-
function getUrlValue(urlNode) {
|
|
154
|
-
if (!urlNode) return null;
|
|
155
|
-
|
|
156
|
-
if (urlNode.type === "StringLiteral") return urlNode.value;
|
|
157
|
-
|
|
158
|
-
if (urlNode.type === "TemplateLiteral") {
|
|
159
|
-
const quasis = urlNode.quasis || [];
|
|
160
|
-
const exprs = urlNode.expressions || [];
|
|
161
|
-
let out = "";
|
|
162
|
-
for (let i = 0; i < quasis.length; i++) {
|
|
163
|
-
out += quasis[i].value.raw;
|
|
164
|
-
if (exprs[i]) {
|
|
165
|
-
if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
|
|
166
|
-
else out += `{param${i + 1}}`;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return out;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function extractApiPath(urlValue) {
|
|
176
|
-
if (!urlValue) return null;
|
|
177
|
-
const idx = urlValue.indexOf("/api/");
|
|
178
|
-
if (idx === -1) return null;
|
|
179
|
-
return urlValue.slice(idx);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function deriveControllerNameFromUrl(urlValue) {
|
|
183
|
-
const apiPath = extractApiPath(urlValue) || urlValue;
|
|
184
|
-
const parts = String(apiPath).split("/").filter(Boolean);
|
|
185
|
-
const apiIndex = parts.indexOf("api");
|
|
186
|
-
|
|
187
|
-
let seg = null;
|
|
188
|
-
|
|
189
|
-
if (apiIndex >= 0) {
|
|
190
|
-
seg = parts[apiIndex + 1] || null;
|
|
191
|
-
if (seg && /^v\d+$/i.test(seg)) {
|
|
192
|
-
seg = parts[apiIndex + 2] || seg;
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
seg = parts[0] || null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return toTitleCase(seg);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function deriveActionName(method, route) {
|
|
202
|
-
const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
|
|
203
|
-
const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
|
|
204
|
-
return `${String(method).toLowerCase()}${toTitleCase(last)}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function extractPathParams(route) {
|
|
208
|
-
const params = [];
|
|
209
|
-
const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
|
|
210
|
-
let m;
|
|
211
|
-
while ((m = re.exec(route))) params.push(m[1]);
|
|
212
|
-
return Array.from(new Set(params));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function extractQueryParamsFromUrl(urlValue) {
|
|
216
|
-
try {
|
|
217
|
-
const qIndex = urlValue.indexOf("?");
|
|
218
|
-
if (qIndex === -1) return [];
|
|
219
|
-
const qs = urlValue.slice(qIndex + 1);
|
|
220
|
-
return qs
|
|
221
|
-
.split("&")
|
|
222
|
-
.map((p) => p.split("=")[0])
|
|
223
|
-
.filter(Boolean);
|
|
224
|
-
} catch {
|
|
225
|
-
return [];
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function detectAxiosLikeMethod(node) {
|
|
230
|
-
if (!node.callee || node.callee.type !== "MemberExpression") return null;
|
|
231
|
-
|
|
232
|
-
const prop = node.callee.property;
|
|
233
|
-
if (!prop || prop.type !== "Identifier") return null;
|
|
234
|
-
|
|
235
|
-
const name = prop.name.toLowerCase();
|
|
236
|
-
if (!HTTP_METHODS.has(name)) return null;
|
|
237
|
-
|
|
238
|
-
return name.toUpperCase();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
201
|
async function analyzeFrontend(srcPath) {
|
|
242
202
|
if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
|
|
243
|
-
if (!fs.existsSync(srcPath)) {
|
|
244
|
-
throw new Error(`The source directory '${srcPath}' does not exist.`);
|
|
245
|
-
}
|
|
203
|
+
if (!fs.existsSync(srcPath)) throw new Error(`Source dir '${srcPath}' does not exist`);
|
|
246
204
|
|
|
247
205
|
const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
|
|
248
206
|
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
|
|
@@ -271,7 +229,6 @@ async function analyzeFrontend(srcPath) {
|
|
|
271
229
|
|
|
272
230
|
const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
|
|
273
231
|
const axiosMethod = detectAxiosLikeMethod(node);
|
|
274
|
-
|
|
275
232
|
if (!isFetch && !axiosMethod) return;
|
|
276
233
|
|
|
277
234
|
let urlValue = null;
|
|
@@ -282,41 +239,30 @@ async function analyzeFrontend(srcPath) {
|
|
|
282
239
|
urlValue = getUrlValue(node.arguments[0]);
|
|
283
240
|
const optionsNode = node.arguments[1];
|
|
284
241
|
|
|
285
|
-
if (optionsNode
|
|
286
|
-
const
|
|
287
|
-
(p) =>
|
|
288
|
-
p.type === "ObjectProperty" &&
|
|
289
|
-
p.key.type === "Identifier" &&
|
|
290
|
-
p.key.name === "method"
|
|
242
|
+
if (optionsNode?.type === "ObjectExpression") {
|
|
243
|
+
const mProp = optionsNode.properties.find(
|
|
244
|
+
(p) => p.type === "ObjectProperty" && p.key?.type === "Identifier" && p.key.name === "method"
|
|
291
245
|
);
|
|
292
|
-
if (
|
|
293
|
-
method = methodProp.value.value.toUpperCase();
|
|
294
|
-
}
|
|
246
|
+
if (mProp?.value?.type === "StringLiteral") method = mProp.value.value.toUpperCase();
|
|
295
247
|
|
|
296
248
|
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
297
|
-
const
|
|
298
|
-
(p) =>
|
|
299
|
-
p.type === "ObjectProperty" &&
|
|
300
|
-
p.key.type === "Identifier" &&
|
|
301
|
-
p.key.name === "body"
|
|
249
|
+
const bProp = optionsNode.properties.find(
|
|
250
|
+
(p) => p.type === "ObjectProperty" && p.key?.type === "Identifier" && p.key.name === "body"
|
|
302
251
|
);
|
|
303
252
|
|
|
304
|
-
if (
|
|
305
|
-
const v =
|
|
306
|
-
|
|
253
|
+
if (bProp) {
|
|
254
|
+
const v = bProp.value;
|
|
307
255
|
if (
|
|
308
256
|
v.type === "CallExpression" &&
|
|
309
257
|
v.callee.type === "MemberExpression" &&
|
|
310
|
-
v.callee.object
|
|
258
|
+
v.callee.object?.type === "Identifier" &&
|
|
311
259
|
v.callee.object.name === "JSON" &&
|
|
312
|
-
v.callee.property
|
|
260
|
+
v.callee.property?.type === "Identifier" &&
|
|
313
261
|
v.callee.property.name === "stringify"
|
|
314
262
|
) {
|
|
315
263
|
const arg0 = v.arguments[0];
|
|
316
|
-
|
|
317
|
-
if (arg0?.type === "
|
|
318
|
-
schemaFields = extractObjectSchema(arg0);
|
|
319
|
-
} else if (arg0?.type === "Identifier") {
|
|
264
|
+
if (arg0?.type === "ObjectExpression") schemaFields = extractObjectSchema(arg0);
|
|
265
|
+
else if (arg0?.type === "Identifier") {
|
|
320
266
|
const init = resolveIdentifierToInit(callPath, arg0.name);
|
|
321
267
|
if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
322
268
|
}
|
|
@@ -332,23 +278,22 @@ async function analyzeFrontend(srcPath) {
|
|
|
332
278
|
|
|
333
279
|
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
334
280
|
const dataArg = node.arguments[1];
|
|
335
|
-
if (dataArg?.type === "ObjectExpression")
|
|
336
|
-
|
|
337
|
-
} else if (dataArg?.type === "Identifier") {
|
|
281
|
+
if (dataArg?.type === "ObjectExpression") schemaFields = extractObjectSchema(dataArg);
|
|
282
|
+
else if (dataArg?.type === "Identifier") {
|
|
338
283
|
const init = resolveIdentifierToInit(callPath, dataArg.name);
|
|
339
284
|
if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
340
285
|
}
|
|
341
286
|
}
|
|
342
287
|
}
|
|
343
288
|
|
|
344
|
-
const apiPath =
|
|
289
|
+
const apiPath = urlValue?.includes("/api/") ? urlValue.slice(urlValue.indexOf("/api/")) : null;
|
|
345
290
|
if (!apiPath) return;
|
|
346
291
|
|
|
347
292
|
const route = normalizeRouteForBackend(apiPath.split("?")[0]);
|
|
348
293
|
const controllerName = deriveControllerNameFromUrl(apiPath);
|
|
349
294
|
const actionName = deriveActionName(method, route);
|
|
350
|
-
|
|
351
295
|
const key = `${method}:${route}`;
|
|
296
|
+
|
|
352
297
|
if (!endpoints.has(key)) {
|
|
353
298
|
endpoints.set(key, {
|
|
354
299
|
path: apiPath,
|
|
@@ -370,21 +315,22 @@ async function analyzeFrontend(srcPath) {
|
|
|
370
315
|
return Array.from(endpoints.values());
|
|
371
316
|
}
|
|
372
317
|
|
|
373
|
-
|
|
374
|
-
// Main analyze() exported for CLI
|
|
375
|
-
// -------------------------
|
|
376
|
-
function analyze(projectRoot = process.cwd()) {
|
|
318
|
+
async function analyze(projectRoot = process.cwd()) {
|
|
377
319
|
const rootDir = path.resolve(projectRoot);
|
|
378
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
|
+
|
|
379
328
|
return {
|
|
380
329
|
rootDir: normalizeSlashes(rootDir),
|
|
381
|
-
hasAuth
|
|
382
|
-
|
|
383
|
-
|
|
330
|
+
hasAuth,
|
|
331
|
+
addAuth: hasAuth,
|
|
332
|
+
endpoints,
|
|
384
333
|
};
|
|
385
334
|
}
|
|
386
335
|
|
|
387
|
-
module.exports = {
|
|
388
|
-
analyze,
|
|
389
|
-
analyzeFrontend,
|
|
390
|
-
};
|
|
336
|
+
module.exports = { analyze, analyzeFrontend };
|
|
@@ -6,7 +6,7 @@ const path = require("path");
|
|
|
6
6
|
* Normalize paths (Windows → Unix style)
|
|
7
7
|
*/
|
|
8
8
|
function normalizeSlashes(p) {
|
|
9
|
-
return p.replace(/\\/g, "/");
|
|
9
|
+
return String(p || "").replace(/\\/g, "/");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -40,6 +40,27 @@ function findAuthUsageInRepo(rootDir) {
|
|
|
40
40
|
return found;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Derive a controller name from URL
|
|
45
|
+
*/
|
|
46
|
+
function deriveControllerName(urlPath) {
|
|
47
|
+
if (!urlPath) return "Default";
|
|
48
|
+
const parts = urlPath.split("/").filter(Boolean);
|
|
49
|
+
const apiIndex = parts.indexOf("api");
|
|
50
|
+
if (apiIndex >= 0) return parts[apiIndex + 1] || "Default";
|
|
51
|
+
return parts[0] || "Default";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Derive an action name from method + URL
|
|
56
|
+
*/
|
|
57
|
+
function deriveActionName(method, urlPath) {
|
|
58
|
+
if (!urlPath) return `${method.toLowerCase()}Action`;
|
|
59
|
+
const cleaned = urlPath.replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
|
|
60
|
+
const lastSegment = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
|
|
61
|
+
return `${method.toLowerCase()}${lastSegment.charAt(0).toUpperCase()}${lastSegment.slice(1)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
/**
|
|
44
65
|
* Analyze frontend source to extract API endpoints
|
|
45
66
|
* (axios / fetch based – simple & safe)
|
|
@@ -63,23 +84,31 @@ function analyzeFrontend(frontendDir) {
|
|
|
63
84
|
) {
|
|
64
85
|
const content = fs.readFileSync(fullPath, "utf8");
|
|
65
86
|
|
|
66
|
-
// axios
|
|
87
|
+
// --- axios ---
|
|
67
88
|
const axiosRegex = /axios\.(get|post|put|delete|patch)\(\s*['"`](.*?)['"`]/g;
|
|
68
89
|
let match;
|
|
69
90
|
while ((match = axiosRegex.exec(content)) !== null) {
|
|
91
|
+
const url = match[2].startsWith("/") ? match[2] : "/" + match[2];
|
|
92
|
+
const method = match[1].toUpperCase();
|
|
70
93
|
endpoints.push({
|
|
71
|
-
method
|
|
72
|
-
path:
|
|
94
|
+
method,
|
|
95
|
+
path: url,
|
|
96
|
+
controllerName: deriveControllerName(url),
|
|
97
|
+
actionName: deriveActionName(method, url),
|
|
73
98
|
source: normalizeSlashes(fullPath)
|
|
74
99
|
});
|
|
75
100
|
}
|
|
76
101
|
|
|
77
|
-
// fetch
|
|
102
|
+
// --- fetch ---
|
|
78
103
|
const fetchRegex = /fetch\(\s*['"`](.*?)['"`]/g;
|
|
79
104
|
while ((match = fetchRegex.exec(content)) !== null) {
|
|
105
|
+
const url = match[1].startsWith("/") ? match[1] : "/" + match[1];
|
|
106
|
+
const method = "GET";
|
|
80
107
|
endpoints.push({
|
|
81
|
-
method
|
|
82
|
-
path:
|
|
108
|
+
method,
|
|
109
|
+
path: url,
|
|
110
|
+
controllerName: deriveControllerName(url),
|
|
111
|
+
actionName: deriveActionName(method, url),
|
|
83
112
|
source: normalizeSlashes(fullPath)
|
|
84
113
|
});
|
|
85
114
|
}
|
package/src/utils.js
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
const { execa } = require("execa");
|
|
2
3
|
|
|
3
4
|
const VERSION_ARGS = {
|
|
4
|
-
java: [
|
|
5
|
-
python: [
|
|
6
|
-
python3: [
|
|
7
|
-
node: [
|
|
8
|
-
npm: [
|
|
9
|
-
dotnet: [
|
|
10
|
-
mvn: [
|
|
11
|
-
git: [
|
|
5
|
+
java: ["-version"],
|
|
6
|
+
python: ["--version"],
|
|
7
|
+
python3: ["--version"],
|
|
8
|
+
node: ["--version"],
|
|
9
|
+
npm: ["--version"],
|
|
10
|
+
dotnet: ["--version"],
|
|
11
|
+
mvn: ["-v"],
|
|
12
|
+
git: ["--version"],
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
async function isCommandAvailable(command) {
|
|
15
|
-
const args = VERSION_ARGS[command] || [
|
|
16
|
-
|
|
16
|
+
const args = VERSION_ARGS[command] || ["--version"];
|
|
17
17
|
try {
|
|
18
|
-
// Reject false only if spawn fails (ENOENT). Non-zero exit still counts as "available".
|
|
19
18
|
await execa(command, args, { reject: false });
|
|
20
19
|
return true;
|
|
21
20
|
} catch (err) {
|
|
22
|
-
|
|
23
|
-
if (err && err.code === 'ENOENT') return false;
|
|
24
|
-
// Other unexpected errors: treat as not available
|
|
21
|
+
if (err && err.code === "ENOENT") return false;
|
|
25
22
|
return false;
|
|
26
23
|
}
|
|
27
24
|
}
|