create-backlist 7.3.0 → 7.3.1

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": "7.3.0",
3
+ "version": "7.3.1",
4
4
  "description": "An advanced, multi-language backend generator based on frontend analysis. Smart Freemium SaaS CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/analyzer.js CHANGED
@@ -8,6 +8,12 @@ const traverse = _traverse.default || _traverse;
8
8
 
9
9
  const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete"]);
10
10
 
11
+ const STATIC_ASSET_EXTENSIONS = new Set([
12
+ ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico",
13
+ ".css", ".scss", ".less", ".woff", ".woff2", ".ttf", ".eot",
14
+ ".mp3", ".mp4", ".webm", ".pdf",
15
+ ]);
16
+
11
17
  // -------------------------
12
18
  // Utils
13
19
  // -------------------------
@@ -28,14 +34,43 @@ function normalizeRouteForBackend(urlValue) {
28
34
  return String(urlValue || "").replace(/\{(\w+)\}/g, ":$1");
29
35
  }
30
36
 
31
- function extractApiPath(urlValue) {
32
- // supports:
33
- // - /api/...
34
- // - http://localhost:5000/api/...
37
+ function extractApiPath(urlValue, envMap = new Map()) {
35
38
  if (!urlValue) return null;
39
+
40
+ // 1. Original behavior: if URL contains /api/, extract from there
36
41
  const idx = urlValue.indexOf("/api/");
37
- if (idx === -1) return null;
38
- return urlValue.slice(idx); // => /api/...
42
+ if (idx !== -1) return urlValue.slice(idx);
43
+
44
+ // 2. Resolve env variable placeholders left by getUrlValue
45
+ let resolved = urlValue;
46
+ const envPattern = /\{(NEXT_PUBLIC_|REACT_APP_|VITE_)[^}]+\}/g;
47
+ resolved = resolved.replace(envPattern, (match) => {
48
+ const varName = match.slice(1, -1);
49
+ return envMap.get(varName) || "";
50
+ });
51
+
52
+ // Re-check after resolution
53
+ const idx2 = resolved.indexOf("/api/");
54
+ if (idx2 !== -1) return resolved.slice(idx2);
55
+
56
+ // 3. If resolved URL starts with http(s), extract the path portion
57
+ if (/^https?:\/\//.test(resolved)) {
58
+ try {
59
+ const url = new URL(resolved);
60
+ const pathname = url.pathname;
61
+ if (pathname && pathname !== "/") return pathname;
62
+ } catch {}
63
+ }
64
+
65
+ // 4. If it's a relative path starting with /, accept it
66
+ if (resolved.startsWith("/") && resolved.length > 1) {
67
+ // Filter out static assets
68
+ const ext = path.extname(resolved.split("?")[0]).toLowerCase();
69
+ if (STATIC_ASSET_EXTENSIONS.has(ext)) return null;
70
+ return resolved;
71
+ }
72
+
73
+ return null;
39
74
  }
40
75
 
41
76
  function extractPathParams(route) {
@@ -89,26 +124,80 @@ function deriveActionName(method, route) {
89
124
  // -------------------------
90
125
  // URL extraction
91
126
  // -------------------------
92
- function getUrlValue(urlNode) {
127
+ function extractEnvVarName(node) {
128
+ // process.env.NEXT_PUBLIC_API_URL
129
+ if (
130
+ node.type === "MemberExpression" &&
131
+ node.object?.type === "MemberExpression" &&
132
+ node.object.object?.type === "Identifier" &&
133
+ node.object.object.name === "process" &&
134
+ node.object.property?.name === "env" &&
135
+ node.property?.type === "Identifier"
136
+ ) {
137
+ return node.property.name;
138
+ }
139
+ // import.meta.env.VITE_API_URL
140
+ if (
141
+ node.type === "MemberExpression" &&
142
+ node.object?.type === "MemberExpression" &&
143
+ node.object.object?.type === "MetaProperty" &&
144
+ node.object.property?.name === "env" &&
145
+ node.property?.type === "Identifier"
146
+ ) {
147
+ return node.property.name;
148
+ }
149
+ return null;
150
+ }
151
+
152
+ function getUrlValue(urlNode, envMap = new Map()) {
93
153
  if (!urlNode) return null;
94
154
 
95
155
  if (urlNode.type === "StringLiteral") return urlNode.value;
96
156
 
97
157
  if (urlNode.type === "TemplateLiteral") {
98
- // `/api/users/${id}` -> `/api/users/{id}` or `{param1}`
99
158
  const quasis = urlNode.quasis || [];
100
159
  const exprs = urlNode.expressions || [];
101
160
  let out = "";
102
161
  for (let i = 0; i < quasis.length; i++) {
103
162
  out += quasis[i].value.raw;
104
163
  if (exprs[i]) {
105
- if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
106
- else out += `{param${i + 1}}`;
164
+ if (exprs[i].type === "Identifier") {
165
+ out += `{${exprs[i].name}}`;
166
+ } else if (exprs[i].type === "MemberExpression") {
167
+ const envName = extractEnvVarName(exprs[i]);
168
+ if (envName && envMap.has(envName)) {
169
+ out += envMap.get(envName);
170
+ } else if (envName) {
171
+ out += `{${envName}}`;
172
+ } else {
173
+ out += `{param${i + 1}}`;
174
+ }
175
+ } else {
176
+ out += `{param${i + 1}}`;
177
+ }
107
178
  }
108
179
  }
109
180
  return out;
110
181
  }
111
182
 
183
+ // Handle string concatenation: baseUrl + "/users"
184
+ if (urlNode.type === "BinaryExpression" && urlNode.operator === "+") {
185
+ const left = getUrlValue(urlNode.left, envMap);
186
+ const right = getUrlValue(urlNode.right, envMap);
187
+ if (left || right) return (left || "") + (right || "");
188
+ }
189
+
190
+ // Handle process.env.X / import.meta.env.X directly as URL
191
+ if (urlNode.type === "MemberExpression") {
192
+ const envName = extractEnvVarName(urlNode);
193
+ if (envName && envMap.has(envName)) {
194
+ return envMap.get(envName);
195
+ }
196
+ if (envName) {
197
+ return `{${envName}}`;
198
+ }
199
+ }
200
+
112
201
  return null;
113
202
  }
114
203
 
@@ -343,19 +432,29 @@ function generateSeedsFromModels(models, perModel = 3) {
343
432
  // -------------------------
344
433
  // MAIN frontend analyzer
345
434
  // -------------------------
346
- export async function analyzeFrontend(srcPath) {
435
+ export async function analyzeFrontend(srcPath, options = {}) {
347
436
  if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
348
437
  if (!fs.existsSync(srcPath)) {
349
438
  throw new Error(`The source directory '${srcPath}' does not exist.`);
350
439
  }
440
+ return analyzeFrontendMulti([srcPath], options);
441
+ }
351
442
 
352
- const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
353
- ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
354
- });
443
+ export async function analyzeFrontendMulti(scanDirs, options = {}) {
444
+ const { envMap = new Map() } = options;
445
+
446
+ const allFiles = new Set();
447
+ for (const dir of scanDirs) {
448
+ if (!fs.existsSync(dir)) continue;
449
+ const files = await glob(`${normalizeSlashes(dir)}/**/*.{js,ts,jsx,tsx}`, {
450
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
451
+ });
452
+ files.forEach((f) => allFiles.add(path.resolve(f)));
453
+ }
355
454
 
356
455
  const endpoints = new Map();
357
456
 
358
- for (const file of files) {
457
+ for (const file of allFiles) {
359
458
  let code;
360
459
  try {
361
460
  code = await fs.readFile(file, "utf-8");
@@ -383,9 +482,8 @@ export async function analyzeFrontend(srcPath) {
383
482
  let method = "GET";
384
483
  let schemaFields = null;
385
484
 
386
- // ---- fetch(url, options) ----
387
485
  if (isFetch) {
388
- urlValue = getUrlValue(node.arguments[0]);
486
+ urlValue = getUrlValue(node.arguments[0], envMap);
389
487
  const optionsNode = node.arguments[1];
390
488
 
391
489
  if (optionsNode && optionsNode.type === "ObjectExpression") {
@@ -425,10 +523,9 @@ export async function analyzeFrontend(srcPath) {
425
523
  }
426
524
  }
427
525
 
428
- // ---- axios-like client ----
429
526
  if (axiosMethod) {
430
527
  method = axiosMethod;
431
- urlValue = getUrlValue(node.arguments[0]);
528
+ urlValue = getUrlValue(node.arguments[0], envMap);
432
529
 
433
530
  if (["POST", "PUT", "PATCH"].includes(method)) {
434
531
  const dataArg = node.arguments[1];
@@ -441,8 +538,7 @@ export async function analyzeFrontend(srcPath) {
441
538
  }
442
539
  }
443
540
 
444
- // accept only URLs that contain /api/ anywhere
445
- const apiPath = extractApiPath(urlValue);
541
+ const apiPath = extractApiPath(urlValue, envMap);
446
542
  if (!apiPath) return;
447
543
 
448
544
  const route = normalizeRouteForBackend(apiPath.split("?")[0]);
@@ -501,6 +597,110 @@ export async function analyze(projectRoot = process.cwd()) {
501
597
  };
502
598
  }
503
599
 
600
+ // -------------------------
601
+ // Next.js API Route Detection
602
+ // -------------------------
603
+ const NEXTJS_HTTP_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
604
+
605
+ export async function detectNextjsApiRoutes(apiRouteDirs) {
606
+ const routes = [];
607
+
608
+ for (const dir of apiRouteDirs) {
609
+ if (!fs.existsSync(dir)) continue;
610
+
611
+ // App Router: app/**/route.{ts,js}
612
+ const appRouterFiles = await glob(
613
+ `${normalizeSlashes(dir)}/**/route.{ts,js,tsx,jsx}`,
614
+ { ignore: ["**/node_modules/**"] }
615
+ );
616
+
617
+ for (const file of appRouterFiles) {
618
+ const relativePath = path.relative(dir, path.dirname(file));
619
+ let routePath = "/" + relativePath
620
+ .replace(/\\/g, "/")
621
+ .replace(/\(([^)]+)\)\//g, "")
622
+ .replace(/\[\.\.\.([^\]]+)\]/g, ":$1")
623
+ .replace(/\[([^\]]+)\]/g, ":$1");
624
+
625
+ if (routePath === "/.") routePath = "/";
626
+
627
+ const methods = await extractExportedHttpMethods(file);
628
+ for (const method of methods) {
629
+ routes.push({
630
+ route: routePath,
631
+ method,
632
+ controllerName: deriveControllerNameFromUrl(routePath),
633
+ sourceFile: normalizeSlashes(file),
634
+ isServerRoute: true,
635
+ });
636
+ }
637
+ }
638
+
639
+ // Pages Router: pages/api/**/*.{ts,js}
640
+ const pagesApiDir = path.join(dir, "api");
641
+ if (fs.existsSync(pagesApiDir)) {
642
+ const pagesApiFiles = await glob(
643
+ `${normalizeSlashes(pagesApiDir)}/**/*.{ts,js,tsx,jsx}`,
644
+ { ignore: ["**/node_modules/**"] }
645
+ );
646
+
647
+ for (const file of pagesApiFiles) {
648
+ const relativePath = path.relative(pagesApiDir, file);
649
+ let routePath = "/api/" + relativePath
650
+ .replace(/\\/g, "/")
651
+ .replace(/\.(ts|js|tsx|jsx)$/, "")
652
+ .replace(/\/index$/, "")
653
+ .replace(/\[\.\.\.([^\]]+)\]/g, ":$1")
654
+ .replace(/\[([^\]]+)\]/g, ":$1");
655
+
656
+ routes.push({
657
+ route: routePath,
658
+ method: "ALL",
659
+ controllerName: deriveControllerNameFromUrl(routePath),
660
+ sourceFile: normalizeSlashes(file),
661
+ isServerRoute: true,
662
+ });
663
+ }
664
+ }
665
+ }
666
+
667
+ return routes;
668
+ }
669
+
670
+ async function extractExportedHttpMethods(file) {
671
+ const methods = [];
672
+ try {
673
+ const code = await fs.readFile(file, "utf-8");
674
+ const ast = parser.parse(code, {
675
+ sourceType: "module",
676
+ plugins: ["jsx", "typescript"],
677
+ });
678
+
679
+ traverse(ast, {
680
+ ExportNamedDeclaration(nodePath) {
681
+ const decl = nodePath.node.declaration;
682
+ if (!decl) return;
683
+
684
+ if (decl.type === "FunctionDeclaration" && decl.id) {
685
+ const name = decl.id.name;
686
+ if (NEXTJS_HTTP_EXPORTS.has(name)) methods.push(name);
687
+ }
688
+
689
+ if (decl.type === "VariableDeclaration") {
690
+ for (const declarator of decl.declarations) {
691
+ if (declarator.id?.type === "Identifier") {
692
+ const name = declarator.id.name;
693
+ if (NEXTJS_HTTP_EXPORTS.has(name)) methods.push(name);
694
+ }
695
+ }
696
+ }
697
+ },
698
+ });
699
+ } catch {}
700
+
701
+ return methods.length > 0 ? methods : ["GET"];
702
+ }
703
+
504
704
  // -------------------------
505
705
  // NEW v7.0: Low-Cost Path Scanner (Standard Tier)
506
706
  // -------------------------
@@ -0,0 +1,70 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+
4
+ const ENV_FILES = [
5
+ ".env.local",
6
+ ".env.development",
7
+ ".env.development.local",
8
+ ".env",
9
+ ];
10
+
11
+ const API_URL_PATTERNS = [
12
+ "API_URL",
13
+ "API_BASE_URL",
14
+ "API_BASE",
15
+ "BASE_URL",
16
+ "BACKEND_URL",
17
+ "SERVER_URL",
18
+ ];
19
+
20
+ export async function parseEnvFiles(projectRoot) {
21
+ const envMap = new Map();
22
+
23
+ for (const envFile of ENV_FILES) {
24
+ const filePath = path.join(projectRoot, envFile);
25
+ if (!(await fs.pathExists(filePath))) continue;
26
+
27
+ let content;
28
+ try {
29
+ content = await fs.readFile(filePath, "utf-8");
30
+ } catch {
31
+ continue;
32
+ }
33
+
34
+ for (const line of content.split("\n")) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed || trimmed.startsWith("#")) continue;
37
+
38
+ const eqIndex = trimmed.indexOf("=");
39
+ if (eqIndex === -1) continue;
40
+
41
+ const key = trimmed.slice(0, eqIndex).trim();
42
+ let value = trimmed.slice(eqIndex + 1).trim();
43
+
44
+ if (
45
+ (value.startsWith('"') && value.endsWith('"')) ||
46
+ (value.startsWith("'") && value.endsWith("'"))
47
+ ) {
48
+ value = value.slice(1, -1);
49
+ }
50
+
51
+ if (key && value && !envMap.has(key)) {
52
+ envMap.set(key, value);
53
+ }
54
+ }
55
+ }
56
+
57
+ return envMap;
58
+ }
59
+
60
+ export function extractBaseUrlFromEnv(envMap) {
61
+ for (const [key, value] of envMap) {
62
+ const upperKey = key.toUpperCase();
63
+ for (const pattern of API_URL_PATTERNS) {
64
+ if (upperKey.includes(pattern) && value.startsWith("http")) {
65
+ return value.replace(/\/$/, "");
66
+ }
67
+ }
68
+ }
69
+ return null;
70
+ }
@@ -0,0 +1,131 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+
4
+ const NEXTJS_SCAN_DIRS = [
5
+ "app",
6
+ "pages",
7
+ "src",
8
+ "components",
9
+ "lib",
10
+ "hooks",
11
+ "services",
12
+ "utils",
13
+ "features",
14
+ "modules",
15
+ ];
16
+
17
+ const REACT_SCAN_DIRS = ["src"];
18
+
19
+ export async function detectProjectType(projectRoot) {
20
+ const result = {
21
+ type: "unknown",
22
+ scanDirs: [],
23
+ apiRouteDirs: [],
24
+ framework: "unknown",
25
+ };
26
+
27
+ const pkgPath = path.join(projectRoot, "package.json");
28
+ let pkg = null;
29
+ if (await fs.pathExists(pkgPath)) {
30
+ try {
31
+ pkg = await fs.readJson(pkgPath);
32
+ } catch {}
33
+ }
34
+
35
+ const deps = pkg
36
+ ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
37
+ : {};
38
+
39
+ const hasNext = !!deps.next || (await hasNextConfig(projectRoot));
40
+ const hasReact = !!deps.react;
41
+ const hasVite = !!deps.vite;
42
+
43
+ const hasAppDir = await fs.pathExists(path.join(projectRoot, "app"));
44
+ const hasPagesDir = await fs.pathExists(path.join(projectRoot, "pages"));
45
+ const hasSrcApp = await fs.pathExists(path.join(projectRoot, "src", "app"));
46
+ const hasSrcPages = await fs.pathExists(
47
+ path.join(projectRoot, "src", "pages")
48
+ );
49
+
50
+ if (hasNext) {
51
+ result.framework = "next";
52
+
53
+ if ((hasAppDir || hasSrcApp) && (hasPagesDir || hasSrcPages)) {
54
+ result.type = "nextjs-hybrid";
55
+ } else if (hasAppDir || hasSrcApp) {
56
+ result.type = "nextjs-app";
57
+ } else if (hasPagesDir || hasSrcPages) {
58
+ result.type = "nextjs-pages";
59
+ } else {
60
+ result.type = "nextjs-app";
61
+ }
62
+
63
+ const candidateDirs = [...NEXTJS_SCAN_DIRS];
64
+ for (const dir of candidateDirs) {
65
+ const abs = path.join(projectRoot, dir);
66
+ if (await fs.pathExists(abs)) {
67
+ result.scanDirs.push(abs);
68
+ }
69
+ const srcNested = path.join(projectRoot, "src", dir);
70
+ if (dir !== "src" && (await fs.pathExists(srcNested))) {
71
+ result.scanDirs.push(srcNested);
72
+ }
73
+ }
74
+
75
+ if (hasAppDir) {
76
+ const appApiDir = path.join(projectRoot, "app", "api");
77
+ if (await fs.pathExists(appApiDir)) result.apiRouteDirs.push(appApiDir);
78
+ result.apiRouteDirs.push(path.join(projectRoot, "app"));
79
+ }
80
+ if (hasSrcApp) {
81
+ const srcAppApiDir = path.join(projectRoot, "src", "app", "api");
82
+ if (await fs.pathExists(srcAppApiDir))
83
+ result.apiRouteDirs.push(srcAppApiDir);
84
+ result.apiRouteDirs.push(path.join(projectRoot, "src", "app"));
85
+ }
86
+ if (hasPagesDir) {
87
+ const pagesApiDir = path.join(projectRoot, "pages", "api");
88
+ if (await fs.pathExists(pagesApiDir))
89
+ result.apiRouteDirs.push(pagesApiDir);
90
+ }
91
+ if (hasSrcPages) {
92
+ const srcPagesApiDir = path.join(projectRoot, "src", "pages", "api");
93
+ if (await fs.pathExists(srcPagesApiDir))
94
+ result.apiRouteDirs.push(srcPagesApiDir);
95
+ }
96
+ } else if (hasReact) {
97
+ result.framework = hasVite ? "vite" : "react";
98
+ result.type = hasVite ? "vite-react" : "react";
99
+
100
+ for (const dir of REACT_SCAN_DIRS) {
101
+ const abs = path.join(projectRoot, dir);
102
+ if (await fs.pathExists(abs)) {
103
+ result.scanDirs.push(abs);
104
+ }
105
+ }
106
+ }
107
+
108
+ if (result.scanDirs.length === 0) {
109
+ const fallback = path.join(projectRoot, "src");
110
+ if (await fs.pathExists(fallback)) {
111
+ result.scanDirs.push(fallback);
112
+ }
113
+ }
114
+
115
+ result.scanDirs = [...new Set(result.scanDirs)];
116
+ result.apiRouteDirs = [...new Set(result.apiRouteDirs)];
117
+
118
+ return result;
119
+ }
120
+
121
+ async function hasNextConfig(projectRoot) {
122
+ const configs = [
123
+ "next.config.js",
124
+ "next.config.mjs",
125
+ "next.config.ts",
126
+ ];
127
+ for (const cfg of configs) {
128
+ if (await fs.pathExists(path.join(projectRoot, cfg))) return true;
129
+ }
130
+ return false;
131
+ }