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 +1 -1
- package/src/analyzer.js +221 -21
- package/src/env-resolver.js +70 -0
- package/src/project-detector.js +131 -0
package/package.json
CHANGED
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
|
|
38
|
-
|
|
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
|
|
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")
|
|
106
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|