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 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('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');
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
- const { isCommandAvailable } = require('../src/utils');
11
+ // FIX: repo has utils.js at root
12
+ const { isCommandAvailable } = require("../utils");
11
13
 
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');
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('../src/scanner');
19
+ const { scanFrontend, writeContracts } = require("../src/scanner");
18
20
 
19
21
  function resolveOptionsFromFlags(flags) {
20
22
  return {
21
- projectName: flags.projectName || 'backend',
22
- srcPath: flags.srcPath || 'src',
23
- stack: flags.stack || 'node-ts-express',
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 || 'backend'),
29
- frontendSrcDir: path.resolve(process.cwd(), flags.srcPath || 'src'),
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 'node-ts-express':
37
+ case "node-ts-express":
36
38
  await generateNodeProject({ ...options, contracts });
37
39
  break;
38
40
 
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');
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 '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.');
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 'python-fastapi':
54
- if (!await isCommandAvailable('python')) {
55
- throw new Error('Python is not installed. Please install Python (3.8+) and pip to continue.');
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(`The selected stack '${options.stack}' is not supported yet.`);
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('Welcome to Backlist! The Polyglot Backend Generator.'));
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: '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.'
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: 'list',
78
- name: 'stack',
79
- message: 'Select the backend stack:',
79
+ type: "list",
80
+ name: "stack",
81
+ message: "Select the backend stack:",
80
82
  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' },
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: 'input',
89
- name: 'srcPath',
90
- message: 'Enter the path to your frontend `src` directory:',
91
- default: 'src',
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: 'list',
95
- name: 'dbType',
96
- message: 'Select your database type for Node.js:',
96
+ type: "list",
97
+ name: "dbType",
98
+ message: "Select your database type for Node.js:",
97
99
  choices: [
98
- { name: 'NoSQL (MongoDB with Mongoose)', value: 'mongoose' },
99
- { name: 'SQL (PostgreSQL/MySQL with Prisma)', value: 'prisma' },
100
+ { name: "NoSQL (MongoDB with Mongoose)", value: "mongoose" },
101
+ { name: "SQL (PostgreSQL/MySQL with Prisma)", value: "prisma" },
100
102
  ],
101
- when: (answers) => answers.stack === 'node-ts-express'
103
+ when: (a) => a.stack === "node-ts-express",
102
104
  },
103
105
  {
104
- type: 'confirm',
105
- name: 'addAuth',
106
- message: 'Add JWT authentication boilerplate?',
106
+ type: "confirm",
107
+ name: "addAuth",
108
+ message: "Add JWT authentication boilerplate?",
107
109
  default: true,
108
- when: (answers) => answers.stack === 'node-ts-express'
110
+ when: (a) => a.stack === "node-ts-express",
109
111
  },
110
112
  {
111
- type: 'confirm',
112
- name: 'addSeeder',
113
- message: 'Add a database seeder with sample data?',
113
+ type: "confirm",
114
+ name: "addSeeder",
115
+ message: "Add a database seeder with sample data?",
114
116
  default: true,
115
- when: (answers) => answers.stack === 'node-ts-express' && answers.addAuth
117
+ when: (a) => a.stack === "node-ts-express" && a.addAuth,
116
118
  },
117
119
  {
118
- type: 'checkbox',
119
- name: 'extraFeatures',
120
- message: 'Select additional features for Node.js:',
120
+ type: "checkbox",
121
+ name: "extraFeatures",
122
+ message: "Select additional features for Node.js:",
121
123
  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 },
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: (answers) => answers.stack === 'node-ts-express'
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('\nBackend generation complete!'));
143
- console.log('\nNext Steps:');
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('\nAn error occurred during generation:'));
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(' -> Cleaning up failed installation...'));
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
- .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')
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.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')
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('Generation complete.'));
193
+ console.log(chalk.green("Generation complete."));
193
194
  });
194
195
 
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')
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('add', run).on('change', run).on('unlink', run);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "6.0.5",
3
+ "version": "6.0.7",
4
4
  "description": "An advanced, multi-language backend generator based on frontend analysis.",
5
5
  "type": "commonjs",
6
6
  "bin": {
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 schemaFields = {};
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
- schemaFields[key] = inferTypeFromNode(prop.value);
183
+ schema[key] = inferTypeFromNode(prop.value);
135
184
  }
136
- return schemaFields;
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 declPath = binding.path;
144
- if (!declPath || !declPath.node) return null;
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 && optionsNode.type === "ObjectExpression") {
286
- const methodProp = optionsNode.properties.find(
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 (methodProp && methodProp.value.type === "StringLiteral") {
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 bodyProp = optionsNode.properties.find(
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 (bodyProp) {
305
- const v = bodyProp.value;
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.type === "Identifier" &&
258
+ v.callee.object?.type === "Identifier" &&
311
259
  v.callee.object.name === "JSON" &&
312
- v.callee.property.type === "Identifier" &&
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 === "ObjectExpression") {
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
- schemaFields = extractObjectSchema(dataArg);
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 = extractApiPath(urlValue);
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: findAuthUsageInRepo(rootDir),
382
- // If CLI expects addAuth directly, keep both:
383
- addAuth: findAuthUsageInRepo(rootDir),
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.get('/api/xxx')
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: match[1].toUpperCase(),
72
- path: match[2],
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('/api/xxx')
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: "GET",
82
- path: match[1],
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
- const { execa } = require('execa');
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ const { execa } = require("execa");
2
3
 
3
4
  const VERSION_ARGS = {
4
- java: ['-version'],
5
- python: ['--version'],
6
- python3: ['--version'],
7
- node: ['--version'],
8
- npm: ['--version'],
9
- dotnet: ['--version'],
10
- mvn: ['-v'],
11
- git: ['--version'],
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] || ['--version'];
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
- // If command not found, execa throws with code 'ENOENT'
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
  }