create-backlist 6.0.4 → 6.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "6.0.4",
3
+ "version": "6.0.6",
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
@@ -27,7 +27,6 @@ function readJSONSafe(p) {
27
27
  // AUTH detection (for addAuth)
28
28
  // -------------------------
29
29
  function findAuthUsageInRepo(rootDir) {
30
- // 1) package.json quick check
31
30
  const pkgPath = path.join(rootDir, "package.json");
32
31
  const pkg = readJSONSafe(pkgPath) || {};
33
32
  const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
@@ -44,7 +43,6 @@ function findAuthUsageInRepo(rootDir) {
44
43
 
45
44
  if (authDeps.some((d) => deps[d])) return true;
46
45
 
47
- // 2) Source scan for common auth identifiers
48
46
  const scanDirs = ["src", "app", "pages", "components", "lib", "utils"]
49
47
  .map((d) => path.join(rootDir, d))
50
48
  .filter((d) => fs.existsSync(d));
@@ -86,7 +84,7 @@ function findAuthUsageInRepo(rootDir) {
86
84
  }
87
85
 
88
86
  // -------------------------
89
- // Frontend API call analyzer (your code)
87
+ // Helper functions for frontend API scan
90
88
  // -------------------------
91
89
  function toTitleCase(str) {
92
90
  if (!str) return "Default";
@@ -100,61 +98,64 @@ function normalizeRouteForBackend(urlValue) {
100
98
  return urlValue.replace(/\{(\w+)\}/g, ":$1");
101
99
  }
102
100
 
103
- function inferTypeFromNode(node) {
104
- if (!node) return "String";
105
- switch (node.type) {
106
- case "StringLiteral":
107
- return "String";
108
- case "NumericLiteral":
109
- return "Number";
110
- case "BooleanLiteral":
111
- return "Boolean";
112
- case "NullLiteral":
113
- return "String";
114
- default:
115
- return "String";
116
- }
101
+ function extractPathParams(route) {
102
+ const params = [];
103
+ const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
104
+ let m;
105
+ while ((m = re.exec(route))) params.push(m[1]);
106
+ return Array.from(new Set(params));
117
107
  }
118
108
 
119
- function extractObjectSchema(objExpr) {
120
- const schemaFields = {};
121
- if (!objExpr || objExpr.type !== "ObjectExpression") return null;
122
-
123
- for (const prop of objExpr.properties) {
124
- if (prop.type !== "ObjectProperty") continue;
125
-
126
- const key =
127
- prop.key.type === "Identifier"
128
- ? prop.key.name
129
- : prop.key.type === "StringLiteral"
130
- ? prop.key.value
131
- : null;
109
+ function extractQueryParamsFromUrl(urlValue) {
110
+ try {
111
+ const qIndex = urlValue.indexOf("?");
112
+ if (qIndex === -1) return [];
113
+ const qs = urlValue.slice(qIndex + 1);
114
+ return qs
115
+ .split("&")
116
+ .map((p) => p.split("=")[0])
117
+ .filter(Boolean);
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
132
122
 
133
- if (!key) continue;
134
- schemaFields[key] = inferTypeFromNode(prop.value);
123
+ function deriveControllerNameFromUrl(urlValue) {
124
+ const parts = String(urlValue || "").split("/").filter(Boolean);
125
+ const apiIndex = parts.indexOf("api");
126
+ let seg = null;
127
+ if (apiIndex >= 0) {
128
+ seg = parts[apiIndex + 1] || null;
129
+ if (seg && /^v\d+$/i.test(seg)) {
130
+ seg = parts[apiIndex + 2] || seg;
131
+ }
132
+ } else {
133
+ seg = parts[0] || null;
135
134
  }
136
- return schemaFields;
135
+ return toTitleCase(seg);
137
136
  }
138
137
 
139
- function resolveIdentifierToInit(pathObj, identifierName) {
140
- try {
141
- const binding = pathObj.scope.getBinding(identifierName);
142
- if (!binding) return null;
143
- const declPath = binding.path;
144
- if (!declPath || !declPath.node) return null;
138
+ function deriveActionName(method, route) {
139
+ const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
140
+ const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
141
+ return `${String(method).toLowerCase()}${toTitleCase(last)}`;
142
+ }
145
143
 
146
- if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
147
- return null;
148
- } catch {
149
- return null;
150
- }
144
+ // -------------------------
145
+ // Axios/Fetch detection
146
+ // -------------------------
147
+ function detectAxiosLikeMethod(node) {
148
+ if (!node.callee || node.callee.type !== "MemberExpression") return null;
149
+ const prop = node.callee.property;
150
+ if (!prop || prop.type !== "Identifier") return null;
151
+ const name = prop.name.toLowerCase();
152
+ if (!HTTP_METHODS.has(name)) return null;
153
+ return name.toUpperCase();
151
154
  }
152
155
 
153
156
  function getUrlValue(urlNode) {
154
157
  if (!urlNode) return null;
155
-
156
158
  if (urlNode.type === "StringLiteral") return urlNode.value;
157
-
158
159
  if (urlNode.type === "TemplateLiteral") {
159
160
  const quasis = urlNode.quasis || [];
160
161
  const exprs = urlNode.expressions || [];
@@ -168,81 +169,52 @@ function getUrlValue(urlNode) {
168
169
  }
169
170
  return out;
170
171
  }
171
-
172
172
  return null;
173
173
  }
174
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;
175
+ // -------------------------
176
+ // Extract request schema (simple)
177
+ function inferTypeFromNode(node) {
178
+ if (!node) return "String";
179
+ switch (node.type) {
180
+ case "StringLiteral": return "String";
181
+ case "NumericLiteral": return "Number";
182
+ case "BooleanLiteral": return "Boolean";
183
+ case "NullLiteral": return "String";
184
+ default: return "String";
196
185
  }
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
186
  }
206
187
 
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));
188
+ function extractObjectSchema(objExpr) {
189
+ const schema = {};
190
+ if (!objExpr || objExpr.type !== "ObjectExpression") return null;
191
+ for (const prop of objExpr.properties) {
192
+ if (prop.type !== "ObjectProperty") continue;
193
+ const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.value;
194
+ schema[key] = inferTypeFromNode(prop.value);
195
+ }
196
+ return schema;
213
197
  }
214
198
 
215
- function extractQueryParamsFromUrl(urlValue) {
199
+ function resolveIdentifierToInit(pathObj, identifierName) {
216
200
  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);
201
+ const binding = pathObj.scope.getBinding(identifierName);
202
+ if (!binding) return null;
203
+ const decl = binding.path.node;
204
+ if (!decl) return null;
205
+ if (decl.type === "VariableDeclarator") return decl.init || null;
206
+ return null;
224
207
  } catch {
225
- return [];
208
+ return null;
226
209
  }
227
210
  }
228
211
 
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
-
212
+ // -------------------------
213
+ // Main frontend scanner
214
+ // -------------------------
241
215
  async function analyzeFrontend(srcPath) {
242
216
  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
- }
217
+ if (!fs.existsSync(srcPath)) throw new Error(`Source dir '${srcPath}' does not exist`);
246
218
 
247
219
  const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
248
220
  ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
@@ -252,26 +224,15 @@ async function analyzeFrontend(srcPath) {
252
224
 
253
225
  for (const file of files) {
254
226
  let code;
255
- try {
256
- code = await fs.readFile(file, "utf-8");
257
- } catch {
258
- continue;
259
- }
260
-
227
+ try { code = await fs.readFile(file, "utf-8"); } catch { continue; }
261
228
  let ast;
262
- try {
263
- ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
264
- } catch {
265
- continue;
266
- }
229
+ try { ast = parser.parse(code, { sourceType: "module", plugins: ["jsx","typescript"] }); } catch { continue; }
267
230
 
268
231
  traverse(ast, {
269
232
  CallExpression(callPath) {
270
233
  const node = callPath.node;
271
-
272
234
  const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
273
235
  const axiosMethod = detectAxiosLikeMethod(node);
274
-
275
236
  if (!isFetch && !axiosMethod) return;
276
237
 
277
238
  let urlValue = null;
@@ -281,44 +242,19 @@ async function analyzeFrontend(srcPath) {
281
242
  if (isFetch) {
282
243
  urlValue = getUrlValue(node.arguments[0]);
283
244
  const optionsNode = node.arguments[1];
284
-
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"
291
- );
292
- if (methodProp && methodProp.value.type === "StringLiteral") {
293
- method = methodProp.value.value.toUpperCase();
294
- }
295
-
296
- 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"
302
- );
303
-
304
- if (bodyProp) {
305
- const v = bodyProp.value;
306
-
307
- if (
308
- v.type === "CallExpression" &&
309
- v.callee.type === "MemberExpression" &&
310
- v.callee.object.type === "Identifier" &&
311
- v.callee.object.name === "JSON" &&
312
- v.callee.property.type === "Identifier" &&
313
- v.callee.property.name === "stringify"
314
- ) {
245
+ if (optionsNode?.type === "ObjectExpression") {
246
+ const mProp = optionsNode.properties.find(p => p.key?.name==="method");
247
+ if (mProp?.value?.type==="StringLiteral") method = mProp.value.value.toUpperCase();
248
+ if (["POST","PUT","PATCH"].includes(method)) {
249
+ const bProp = optionsNode.properties.find(p => p.key?.name==="body");
250
+ if (bProp) {
251
+ const v = bProp.value;
252
+ if (v.type==="CallExpression" && v.callee.object?.name==="JSON" && v.callee.property?.name==="stringify") {
315
253
  const arg0 = v.arguments[0];
316
-
317
- if (arg0?.type === "ObjectExpression") {
318
- schemaFields = extractObjectSchema(arg0);
319
- } else if (arg0?.type === "Identifier") {
254
+ if (arg0?.type==="ObjectExpression") schemaFields = extractObjectSchema(arg0);
255
+ else if (arg0?.type==="Identifier") {
320
256
  const init = resolveIdentifierToInit(callPath, arg0.name);
321
- if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
257
+ if (init?.type==="ObjectExpression") schemaFields = extractObjectSchema(init);
322
258
  }
323
259
  }
324
260
  }
@@ -329,26 +265,24 @@ async function analyzeFrontend(srcPath) {
329
265
  if (axiosMethod) {
330
266
  method = axiosMethod;
331
267
  urlValue = getUrlValue(node.arguments[0]);
332
-
333
- if (["POST", "PUT", "PATCH"].includes(method)) {
268
+ if (["POST","PUT","PATCH"].includes(method)) {
334
269
  const dataArg = node.arguments[1];
335
- if (dataArg?.type === "ObjectExpression") {
336
- schemaFields = extractObjectSchema(dataArg);
337
- } else if (dataArg?.type === "Identifier") {
270
+ if (dataArg?.type==="ObjectExpression") schemaFields = extractObjectSchema(dataArg);
271
+ else if (dataArg?.type==="Identifier") {
338
272
  const init = resolveIdentifierToInit(callPath, dataArg.name);
339
- if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
273
+ if (init?.type==="ObjectExpression") schemaFields = extractObjectSchema(init);
340
274
  }
341
275
  }
342
276
  }
343
277
 
344
- const apiPath = extractApiPath(urlValue);
278
+ const apiPath = urlValue?.includes("/api/") ? urlValue.slice(urlValue.indexOf("/api/")) : null;
345
279
  if (!apiPath) return;
346
280
 
347
281
  const route = normalizeRouteForBackend(apiPath.split("?")[0]);
348
282
  const controllerName = deriveControllerNameFromUrl(apiPath);
349
283
  const actionName = deriveActionName(method, route);
350
-
351
284
  const key = `${method}:${route}`;
285
+
352
286
  if (!endpoints.has(key)) {
353
287
  endpoints.set(key, {
354
288
  path: apiPath,
@@ -358,12 +292,11 @@ async function analyzeFrontend(srcPath) {
358
292
  actionName,
359
293
  pathParams: extractPathParams(route),
360
294
  queryParams: extractQueryParamsFromUrl(apiPath),
361
- schemaFields,
362
295
  requestBody: schemaFields ? { fields: schemaFields } : null,
363
296
  sourceFile: normalizeSlashes(file),
364
297
  });
365
298
  }
366
- },
299
+ }
367
300
  });
368
301
  }
369
302
 
@@ -371,20 +304,25 @@ async function analyzeFrontend(srcPath) {
371
304
  }
372
305
 
373
306
  // -------------------------
374
- // Main analyze() exported for CLI
307
+ // Main analyze() for CLI
375
308
  // -------------------------
376
- function analyze(projectRoot = process.cwd()) {
309
+ async function analyze(projectRoot = process.cwd()) {
377
310
  const rootDir = path.resolve(projectRoot);
311
+ const frontendSrc = ["src", "app", "pages"]
312
+ .map(d => path.join(rootDir,d))
313
+ .find(d => fs.existsSync(d));
314
+
315
+ const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
378
316
 
379
317
  return {
380
318
  rootDir: normalizeSlashes(rootDir),
381
319
  hasAuth: findAuthUsageInRepo(rootDir),
382
- // If CLI expects addAuth directly, keep both:
383
320
  addAuth: findAuthUsageInRepo(rootDir),
321
+ endpoints
384
322
  };
385
323
  }
386
324
 
387
325
  module.exports = {
388
326
  analyze,
389
- analyzeFrontend,
390
- };
327
+ analyzeFrontend
328
+ };
@@ -0,0 +1,146 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ const fs = require("fs-extra");
3
+ const path = require("path");
4
+
5
+ /**
6
+ * Normalize paths (Windows → Unix style)
7
+ */
8
+ function normalizeSlashes(p) {
9
+ return String(p || "").replace(/\\/g, "/");
10
+ }
11
+
12
+ /**
13
+ * Detect auth usage in entire repo
14
+ * (basic heuristic – good enough for competition demo)
15
+ */
16
+ function findAuthUsageInRepo(rootDir) {
17
+ const keywords = ["auth", "jwt", "token", "passport", "oauth"];
18
+ let found = false;
19
+
20
+ function walk(dir) {
21
+ if (found) return;
22
+
23
+ const files = fs.readdirSync(dir);
24
+ for (const file of files) {
25
+ const fullPath = path.join(dir, file);
26
+
27
+ if (fs.statSync(fullPath).isDirectory()) {
28
+ walk(fullPath);
29
+ } else if (file.endsWith(".js") || file.endsWith(".ts")) {
30
+ const content = fs.readFileSync(fullPath, "utf8").toLowerCase();
31
+ if (keywords.some(k => content.includes(k))) {
32
+ found = true;
33
+ return;
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ walk(rootDir);
40
+ return found;
41
+ }
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
+
64
+ /**
65
+ * Analyze frontend source to extract API endpoints
66
+ * (axios / fetch based – simple & safe)
67
+ */
68
+ function analyzeFrontend(frontendDir) {
69
+ const endpoints = [];
70
+
71
+ function walk(dir) {
72
+ const files = fs.readdirSync(dir);
73
+
74
+ for (const file of files) {
75
+ const fullPath = path.join(dir, file);
76
+
77
+ if (fs.statSync(fullPath).isDirectory()) {
78
+ walk(fullPath);
79
+ } else if (
80
+ file.endsWith(".js") ||
81
+ file.endsWith(".ts") ||
82
+ file.endsWith(".jsx") ||
83
+ file.endsWith(".tsx")
84
+ ) {
85
+ const content = fs.readFileSync(fullPath, "utf8");
86
+
87
+ // --- axios ---
88
+ const axiosRegex = /axios\.(get|post|put|delete|patch)\(\s*['"`](.*?)['"`]/g;
89
+ let match;
90
+ while ((match = axiosRegex.exec(content)) !== null) {
91
+ const url = match[2].startsWith("/") ? match[2] : "/" + match[2];
92
+ const method = match[1].toUpperCase();
93
+ endpoints.push({
94
+ method,
95
+ path: url,
96
+ controllerName: deriveControllerName(url),
97
+ actionName: deriveActionName(method, url),
98
+ source: normalizeSlashes(fullPath)
99
+ });
100
+ }
101
+
102
+ // --- fetch ---
103
+ const fetchRegex = /fetch\(\s*['"`](.*?)['"`]/g;
104
+ while ((match = fetchRegex.exec(content)) !== null) {
105
+ const url = match[1].startsWith("/") ? match[1] : "/" + match[1];
106
+ const method = "GET";
107
+ endpoints.push({
108
+ method,
109
+ path: url,
110
+ controllerName: deriveControllerName(url),
111
+ actionName: deriveActionName(method, url),
112
+ source: normalizeSlashes(fullPath)
113
+ });
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ walk(frontendDir);
120
+ return endpoints;
121
+ }
122
+
123
+ /**
124
+ * MAIN analyzer function (used by CLI)
125
+ */
126
+ function analyze(projectRoot = process.cwd()) {
127
+ const rootDir = path.resolve(projectRoot);
128
+
129
+ const frontendSrc = ["src", "app", "pages"]
130
+ .map(d => path.join(rootDir, d))
131
+ .find(d => fs.existsSync(d));
132
+
133
+ const endpoints = frontendSrc ? analyzeFrontend(frontendSrc) : [];
134
+
135
+ return {
136
+ rootDir: normalizeSlashes(rootDir),
137
+ hasAuth: findAuthUsageInRepo(rootDir),
138
+ addAuth: findAuthUsageInRepo(rootDir),
139
+ endpoints
140
+ };
141
+ }
142
+
143
+ module.exports = {
144
+ analyze,
145
+ analyzeFrontend
146
+ };
@@ -26,4 +26,4 @@ const PORT: number | string = process.env.PORT || 8000;
26
26
 
27
27
  app.listen(PORT, () => {
28
28
  console.log(`Server running on http://localhost:${PORT}`);
29
- });
29
+ });