create-backlist 6.0.6 → 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 +71 -63
- 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,9 +20,6 @@ function readJSONSafe(p) {
|
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
// -------------------------
|
|
27
|
-
// AUTH detection (for addAuth)
|
|
28
|
-
// -------------------------
|
|
29
23
|
function findAuthUsageInRepo(rootDir) {
|
|
30
24
|
const pkgPath = path.join(rootDir, "package.json");
|
|
31
25
|
const pkg = readJSONSafe(pkgPath) || {};
|
|
@@ -57,35 +51,26 @@ function findAuthUsageInRepo(rootDir) {
|
|
|
57
51
|
"auth()",
|
|
58
52
|
"currentUser",
|
|
59
53
|
"createServerClient",
|
|
54
|
+
"jwt",
|
|
55
|
+
"bearer",
|
|
60
56
|
];
|
|
61
57
|
|
|
62
58
|
for (const dir of scanDirs) {
|
|
63
59
|
const files = glob.sync(`${normalizeSlashes(dir)}/**/*.{js,ts,jsx,tsx}`, {
|
|
64
|
-
ignore: [
|
|
65
|
-
"**/node_modules/**",
|
|
66
|
-
"**/dist/**",
|
|
67
|
-
"**/build/**",
|
|
68
|
-
"**/.next/**",
|
|
69
|
-
"**/coverage/**",
|
|
70
|
-
],
|
|
60
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
|
|
71
61
|
});
|
|
72
62
|
|
|
73
63
|
for (const f of files) {
|
|
74
64
|
try {
|
|
75
65
|
const content = fs.readFileSync(f, "utf8");
|
|
76
66
|
if (patterns.some((p) => content.includes(p))) return true;
|
|
77
|
-
} catch {
|
|
78
|
-
// ignore read errors
|
|
79
|
-
}
|
|
67
|
+
} catch {}
|
|
80
68
|
}
|
|
81
69
|
}
|
|
82
70
|
|
|
83
71
|
return false;
|
|
84
72
|
}
|
|
85
73
|
|
|
86
|
-
// -------------------------
|
|
87
|
-
// Helper functions for frontend API scan
|
|
88
|
-
// -------------------------
|
|
89
74
|
function toTitleCase(str) {
|
|
90
75
|
if (!str) return "Default";
|
|
91
76
|
return String(str)
|
|
@@ -126,9 +111,7 @@ function deriveControllerNameFromUrl(urlValue) {
|
|
|
126
111
|
let seg = null;
|
|
127
112
|
if (apiIndex >= 0) {
|
|
128
113
|
seg = parts[apiIndex + 1] || null;
|
|
129
|
-
if (seg && /^v\d+$/i.test(seg))
|
|
130
|
-
seg = parts[apiIndex + 2] || seg;
|
|
131
|
-
}
|
|
114
|
+
if (seg && /^v\d+$/i.test(seg)) seg = parts[apiIndex + 2] || seg;
|
|
132
115
|
} else {
|
|
133
116
|
seg = parts[0] || null;
|
|
134
117
|
}
|
|
@@ -141,9 +124,6 @@ function deriveActionName(method, route) {
|
|
|
141
124
|
return `${String(method).toLowerCase()}${toTitleCase(last)}`;
|
|
142
125
|
}
|
|
143
126
|
|
|
144
|
-
// -------------------------
|
|
145
|
-
// Axios/Fetch detection
|
|
146
|
-
// -------------------------
|
|
147
127
|
function detectAxiosLikeMethod(node) {
|
|
148
128
|
if (!node.callee || node.callee.type !== "MemberExpression") return null;
|
|
149
129
|
const prop = node.callee.property;
|
|
@@ -172,16 +152,19 @@ function getUrlValue(urlNode) {
|
|
|
172
152
|
return null;
|
|
173
153
|
}
|
|
174
154
|
|
|
175
|
-
// -------------------------
|
|
176
|
-
// Extract request schema (simple)
|
|
177
155
|
function inferTypeFromNode(node) {
|
|
178
156
|
if (!node) return "String";
|
|
179
157
|
switch (node.type) {
|
|
180
|
-
case "StringLiteral":
|
|
181
|
-
|
|
182
|
-
case "
|
|
183
|
-
|
|
184
|
-
|
|
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";
|
|
185
168
|
}
|
|
186
169
|
}
|
|
187
170
|
|
|
@@ -190,7 +173,13 @@ function extractObjectSchema(objExpr) {
|
|
|
190
173
|
if (!objExpr || objExpr.type !== "ObjectExpression") return null;
|
|
191
174
|
for (const prop of objExpr.properties) {
|
|
192
175
|
if (prop.type !== "ObjectProperty") continue;
|
|
193
|
-
const key =
|
|
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;
|
|
194
183
|
schema[key] = inferTypeFromNode(prop.value);
|
|
195
184
|
}
|
|
196
185
|
return schema;
|
|
@@ -209,9 +198,6 @@ function resolveIdentifierToInit(pathObj, identifierName) {
|
|
|
209
198
|
}
|
|
210
199
|
}
|
|
211
200
|
|
|
212
|
-
// -------------------------
|
|
213
|
-
// Main frontend scanner
|
|
214
|
-
// -------------------------
|
|
215
201
|
async function analyzeFrontend(srcPath) {
|
|
216
202
|
if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
|
|
217
203
|
if (!fs.existsSync(srcPath)) throw new Error(`Source dir '${srcPath}' does not exist`);
|
|
@@ -224,13 +210,23 @@ async function analyzeFrontend(srcPath) {
|
|
|
224
210
|
|
|
225
211
|
for (const file of files) {
|
|
226
212
|
let code;
|
|
227
|
-
try {
|
|
213
|
+
try {
|
|
214
|
+
code = await fs.readFile(file, "utf-8");
|
|
215
|
+
} catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
228
219
|
let ast;
|
|
229
|
-
try {
|
|
220
|
+
try {
|
|
221
|
+
ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
|
|
222
|
+
} catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
230
225
|
|
|
231
226
|
traverse(ast, {
|
|
232
227
|
CallExpression(callPath) {
|
|
233
228
|
const node = callPath.node;
|
|
229
|
+
|
|
234
230
|
const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
|
|
235
231
|
const axiosMethod = detectAxiosLikeMethod(node);
|
|
236
232
|
if (!isFetch && !axiosMethod) return;
|
|
@@ -242,19 +238,33 @@ async function analyzeFrontend(srcPath) {
|
|
|
242
238
|
if (isFetch) {
|
|
243
239
|
urlValue = getUrlValue(node.arguments[0]);
|
|
244
240
|
const optionsNode = node.arguments[1];
|
|
241
|
+
|
|
245
242
|
if (optionsNode?.type === "ObjectExpression") {
|
|
246
|
-
const mProp = optionsNode.properties.find(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
const mProp = optionsNode.properties.find(
|
|
244
|
+
(p) => p.type === "ObjectProperty" && p.key?.type === "Identifier" && p.key.name === "method"
|
|
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
|
+
);
|
|
252
|
+
|
|
250
253
|
if (bProp) {
|
|
251
254
|
const v = bProp.value;
|
|
252
|
-
if (
|
|
255
|
+
if (
|
|
256
|
+
v.type === "CallExpression" &&
|
|
257
|
+
v.callee.type === "MemberExpression" &&
|
|
258
|
+
v.callee.object?.type === "Identifier" &&
|
|
259
|
+
v.callee.object.name === "JSON" &&
|
|
260
|
+
v.callee.property?.type === "Identifier" &&
|
|
261
|
+
v.callee.property.name === "stringify"
|
|
262
|
+
) {
|
|
253
263
|
const arg0 = v.arguments[0];
|
|
254
|
-
if (arg0?.type==="ObjectExpression") schemaFields = extractObjectSchema(arg0);
|
|
255
|
-
else if (arg0?.type==="Identifier") {
|
|
264
|
+
if (arg0?.type === "ObjectExpression") schemaFields = extractObjectSchema(arg0);
|
|
265
|
+
else if (arg0?.type === "Identifier") {
|
|
256
266
|
const init = resolveIdentifierToInit(callPath, arg0.name);
|
|
257
|
-
if (init?.type==="ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
267
|
+
if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
270
|
}
|
|
@@ -265,12 +275,13 @@ async function analyzeFrontend(srcPath) {
|
|
|
265
275
|
if (axiosMethod) {
|
|
266
276
|
method = axiosMethod;
|
|
267
277
|
urlValue = getUrlValue(node.arguments[0]);
|
|
268
|
-
|
|
278
|
+
|
|
279
|
+
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
269
280
|
const dataArg = node.arguments[1];
|
|
270
|
-
if (dataArg?.type==="ObjectExpression") schemaFields = extractObjectSchema(dataArg);
|
|
271
|
-
else if (dataArg?.type==="Identifier") {
|
|
281
|
+
if (dataArg?.type === "ObjectExpression") schemaFields = extractObjectSchema(dataArg);
|
|
282
|
+
else if (dataArg?.type === "Identifier") {
|
|
272
283
|
const init = resolveIdentifierToInit(callPath, dataArg.name);
|
|
273
|
-
if (init?.type==="ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
284
|
+
if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
274
285
|
}
|
|
275
286
|
}
|
|
276
287
|
}
|
|
@@ -292,37 +303,34 @@ async function analyzeFrontend(srcPath) {
|
|
|
292
303
|
actionName,
|
|
293
304
|
pathParams: extractPathParams(route),
|
|
294
305
|
queryParams: extractQueryParamsFromUrl(apiPath),
|
|
306
|
+
schemaFields,
|
|
295
307
|
requestBody: schemaFields ? { fields: schemaFields } : null,
|
|
296
308
|
sourceFile: normalizeSlashes(file),
|
|
297
309
|
});
|
|
298
310
|
}
|
|
299
|
-
}
|
|
311
|
+
},
|
|
300
312
|
});
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
return Array.from(endpoints.values());
|
|
304
316
|
}
|
|
305
317
|
|
|
306
|
-
// -------------------------
|
|
307
|
-
// Main analyze() for CLI
|
|
308
|
-
// -------------------------
|
|
309
318
|
async function analyze(projectRoot = process.cwd()) {
|
|
310
319
|
const rootDir = path.resolve(projectRoot);
|
|
320
|
+
|
|
311
321
|
const frontendSrc = ["src", "app", "pages"]
|
|
312
|
-
.map(d => path.join(rootDir,d))
|
|
313
|
-
.find(d => fs.existsSync(d));
|
|
322
|
+
.map((d) => path.join(rootDir, d))
|
|
323
|
+
.find((d) => fs.existsSync(d));
|
|
314
324
|
|
|
315
325
|
const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
|
|
326
|
+
const hasAuth = findAuthUsageInRepo(rootDir);
|
|
316
327
|
|
|
317
328
|
return {
|
|
318
329
|
rootDir: normalizeSlashes(rootDir),
|
|
319
|
-
hasAuth
|
|
320
|
-
addAuth:
|
|
321
|
-
endpoints
|
|
330
|
+
hasAuth,
|
|
331
|
+
addAuth: hasAuth,
|
|
332
|
+
endpoints,
|
|
322
333
|
};
|
|
323
334
|
}
|
|
324
335
|
|
|
325
|
-
module.exports = {
|
|
326
|
-
analyze,
|
|
327
|
-
analyzeFrontend
|
|
328
|
-
};
|
|
336
|
+
module.exports = { analyze, analyzeFrontend };
|
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
|
}
|