botversion-sdk 1.0.0 → 1.0.2
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/init.js +201 -62
- package/cli/detector.js +473 -71
- package/cli/generator.js +298 -137
- package/cli/writer.js +270 -6
- package/client.js +4 -0
- package/index.js +12 -3
- package/interceptor.js +2 -2
- package/package.json +1 -1
- package/scanner.js +21 -22
package/cli/detector.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
// botversion-sdk/cli/detector.js
|
|
2
|
-
|
|
3
2
|
"use strict";
|
|
4
3
|
|
|
5
4
|
const fs = require("fs");
|
|
6
5
|
const path = require("path");
|
|
7
6
|
|
|
7
|
+
// ─── SKIP DIRS (used everywhere) ─────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const SKIP_DIRS = [
|
|
10
|
+
"node_modules",
|
|
11
|
+
".git",
|
|
12
|
+
".next",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
".cache",
|
|
16
|
+
"coverage",
|
|
17
|
+
".turbo",
|
|
18
|
+
"out",
|
|
19
|
+
".output",
|
|
20
|
+
".svelte-kit",
|
|
21
|
+
];
|
|
22
|
+
|
|
8
23
|
// ─── PACKAGE JSON ────────────────────────────────────────────────────────────
|
|
9
24
|
|
|
10
25
|
function readPackageJson(cwd) {
|
|
@@ -17,22 +32,286 @@ function readPackageJson(cwd) {
|
|
|
17
32
|
}
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
// ─── SCAN ALL PACKAGE.JSON FILES ─────────────────────────────────────────────
|
|
36
|
+
// Recursively finds ALL package.json files in the project
|
|
37
|
+
|
|
38
|
+
function scanAllPackageJsons(cwd) {
|
|
39
|
+
const results = []; // [{ dir, pkg }]
|
|
40
|
+
|
|
41
|
+
function walk(currentDir, depth) {
|
|
42
|
+
if (depth > 5) return;
|
|
43
|
+
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(currentDir);
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
53
|
+
|
|
54
|
+
const fullPath = path.join(currentDir, entry);
|
|
55
|
+
let stat;
|
|
56
|
+
try {
|
|
57
|
+
stat = fs.statSync(fullPath);
|
|
58
|
+
} catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
walk(fullPath, depth + 1);
|
|
64
|
+
} else if (entry === "package.json") {
|
|
65
|
+
try {
|
|
66
|
+
const pkg = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
|
67
|
+
results.push({ dir: currentDir, pkg });
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
walk(cwd, 0);
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── CLASSIFY PACKAGE.JSON ────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const BACKEND_PACKAGES = [
|
|
82
|
+
"express",
|
|
83
|
+
"fastify",
|
|
84
|
+
"koa",
|
|
85
|
+
"@nestjs/core",
|
|
86
|
+
"@hapi/hapi",
|
|
87
|
+
"restify",
|
|
88
|
+
"polka",
|
|
89
|
+
"micro",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const FULLSTACK_PACKAGES = ["next", "@sveltejs/kit"];
|
|
93
|
+
|
|
94
|
+
const FRONTEND_PACKAGES = [
|
|
95
|
+
"react",
|
|
96
|
+
"react-dom",
|
|
97
|
+
"vue",
|
|
98
|
+
"@angular/core",
|
|
99
|
+
"svelte",
|
|
100
|
+
"@sveltejs/kit",
|
|
101
|
+
"solid-js",
|
|
102
|
+
"preact",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
function classifyPackageJson(pkg) {
|
|
106
|
+
if (!pkg) return "unknown";
|
|
107
|
+
|
|
108
|
+
const deps = {
|
|
109
|
+
...(pkg.dependencies || {}),
|
|
110
|
+
...(pkg.devDependencies || {}),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const isFullstack = FULLSTACK_PACKAGES.some((p) => !!deps[p]);
|
|
114
|
+
if (isFullstack) return "fullstack";
|
|
115
|
+
|
|
116
|
+
const isBackend = BACKEND_PACKAGES.some((p) => !!deps[p]);
|
|
117
|
+
const isFrontend = FRONTEND_PACKAGES.some((p) => !!deps[p]);
|
|
118
|
+
|
|
119
|
+
if (isBackend && isFrontend) return "fullstack";
|
|
120
|
+
if (isBackend) return "backend";
|
|
121
|
+
if (isFrontend) return "frontend";
|
|
122
|
+
return "unknown";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── DETECT FRONTEND FRAMEWORK ───────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function detectFrontendFramework(pkg) {
|
|
128
|
+
if (!pkg) return null;
|
|
129
|
+
|
|
130
|
+
const deps = {
|
|
131
|
+
...(pkg.dependencies || {}),
|
|
132
|
+
...(pkg.devDependencies || {}),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (deps["next"]) return "next";
|
|
136
|
+
if (deps["@sveltejs/kit"]) return "sveltekit";
|
|
137
|
+
if (deps["svelte"]) return "svelte";
|
|
138
|
+
if (deps["@angular/core"]) return "angular";
|
|
139
|
+
if (deps["vue"]) return "vue";
|
|
140
|
+
if (deps["react-dom"] || deps["react"]) {
|
|
141
|
+
// Distinguish CRA vs Vite
|
|
142
|
+
if (deps["vite"] || deps["@vitejs/plugin-react"]) return "react-vite";
|
|
143
|
+
return "react-cra";
|
|
144
|
+
}
|
|
145
|
+
if (deps["solid-js"]) return "solid";
|
|
146
|
+
if (deps["preact"]) return "preact";
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── FIND MAIN FRONTEND FILE ──────────────────────────────────────────────────
|
|
152
|
+
// Returns { file, type } or null
|
|
153
|
+
|
|
154
|
+
function findMainFrontendFile(dir, pkg) {
|
|
155
|
+
const framework = detectFrontendFramework(pkg);
|
|
156
|
+
|
|
157
|
+
// ── Next.js ───────────────────────────────────────────────────────────────
|
|
158
|
+
// For Next.js we inject into _app.js (Pages) or layout.js (App Router)
|
|
159
|
+
if (framework === "next") {
|
|
160
|
+
const candidates = [
|
|
161
|
+
"pages/_app.js",
|
|
162
|
+
"pages/_app.tsx",
|
|
163
|
+
"pages/_app.ts",
|
|
164
|
+
"src/pages/_app.js",
|
|
165
|
+
"src/pages/_app.tsx",
|
|
166
|
+
"src/pages/_app.ts",
|
|
167
|
+
"app/layout.js",
|
|
168
|
+
"app/layout.tsx",
|
|
169
|
+
"src/app/layout.js",
|
|
170
|
+
"src/app/layout.tsx",
|
|
171
|
+
];
|
|
172
|
+
for (const candidate of candidates) {
|
|
173
|
+
const fullPath = path.join(dir, candidate);
|
|
174
|
+
if (fs.existsSync(fullPath)) {
|
|
175
|
+
return { file: fullPath, type: "next" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Angular ───────────────────────────────────────────────────────────────
|
|
182
|
+
if (framework === "angular") {
|
|
183
|
+
const candidate = path.join(dir, "src", "index.html");
|
|
184
|
+
if (fs.existsSync(candidate)) {
|
|
185
|
+
return { file: candidate, type: "html" };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── React Vite / Vue Vite / Svelte / SvelteKit / Solid / Preact ──────────
|
|
191
|
+
// All Vite-based projects have index.html in root of the project folder
|
|
192
|
+
if (
|
|
193
|
+
framework === "react-vite" ||
|
|
194
|
+
framework === "vue" ||
|
|
195
|
+
framework === "svelte" ||
|
|
196
|
+
framework === "sveltekit" ||
|
|
197
|
+
framework === "solid" ||
|
|
198
|
+
framework === "preact"
|
|
199
|
+
) {
|
|
200
|
+
// Check root index.html first
|
|
201
|
+
const rootHtml = path.join(dir, "index.html");
|
|
202
|
+
if (fs.existsSync(rootHtml)) {
|
|
203
|
+
return { file: rootHtml, type: "html" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fallback: public/index.html
|
|
207
|
+
const publicHtml = path.join(dir, "public", "index.html");
|
|
208
|
+
if (fs.existsSync(publicHtml)) {
|
|
209
|
+
return { file: publicHtml, type: "html" };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── React CRA ─────────────────────────────────────────────────────────────
|
|
216
|
+
if (framework === "react-cra") {
|
|
217
|
+
// CRA always puts index.html in public/
|
|
218
|
+
const publicHtml = path.join(dir, "public", "index.html");
|
|
219
|
+
if (fs.existsSync(publicHtml)) {
|
|
220
|
+
return { file: publicHtml, type: "html" };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Fallback: root index.html (custom CRA config)
|
|
224
|
+
const rootHtml = path.join(dir, "index.html");
|
|
225
|
+
if (fs.existsSync(rootHtml)) {
|
|
226
|
+
return { file: rootHtml, type: "html" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Unknown frontend — scan for any index.html ────────────────────────────
|
|
233
|
+
const htmlCandidates = [
|
|
234
|
+
"index.html",
|
|
235
|
+
"public/index.html",
|
|
236
|
+
"src/index.html",
|
|
237
|
+
"static/index.html",
|
|
238
|
+
"www/index.html",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for (const candidate of htmlCandidates) {
|
|
242
|
+
const fullPath = path.join(dir, candidate);
|
|
243
|
+
if (fs.existsSync(fullPath)) {
|
|
244
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
245
|
+
// Make sure it's a real HTML file with a body tag
|
|
246
|
+
if (content.includes("<body") || content.includes("<html")) {
|
|
247
|
+
return { file: fullPath, type: "html" };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Last resort — deep scan for any .html file
|
|
253
|
+
const found = findHtmlFile(dir);
|
|
254
|
+
if (found) return { file: found, type: "html" };
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── DEEP SCAN FOR HTML FILE ─────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
function findHtmlFile(dir) {
|
|
262
|
+
function walk(currentDir, depth) {
|
|
263
|
+
if (depth > 3) return null;
|
|
264
|
+
|
|
265
|
+
let entries;
|
|
266
|
+
try {
|
|
267
|
+
entries = fs.readdirSync(currentDir);
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
274
|
+
|
|
275
|
+
const fullPath = path.join(currentDir, entry);
|
|
276
|
+
let stat;
|
|
277
|
+
try {
|
|
278
|
+
stat = fs.statSync(fullPath);
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (stat.isDirectory()) {
|
|
284
|
+
const result = walk(fullPath, depth + 1);
|
|
285
|
+
if (result) return result;
|
|
286
|
+
} else if (entry.endsWith(".html")) {
|
|
287
|
+
try {
|
|
288
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
289
|
+
if (content.includes("<body") || content.includes("<html")) {
|
|
290
|
+
return fullPath;
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return walk(dir, 0);
|
|
302
|
+
}
|
|
303
|
+
|
|
20
304
|
// ─── MONOREPO DETECTION ──────────────────────────────────────────────────────
|
|
21
305
|
|
|
22
306
|
function detectMonorepo(cwd) {
|
|
23
|
-
const entries = fs.readdirSync(cwd);
|
|
24
|
-
|
|
25
|
-
// Check for workspaces in root package.json
|
|
26
307
|
const rootPkg = readPackageJson(cwd);
|
|
27
308
|
if (rootPkg && rootPkg.workspaces) {
|
|
28
|
-
// Find all workspace package.json files
|
|
29
309
|
const workspaceDirs = [];
|
|
30
310
|
const patterns = Array.isArray(rootPkg.workspaces)
|
|
31
311
|
? rootPkg.workspaces
|
|
32
312
|
: rootPkg.workspaces.packages || [];
|
|
33
313
|
|
|
34
314
|
patterns.forEach((pattern) => {
|
|
35
|
-
// Handle simple patterns like "packages/*"
|
|
36
315
|
const base = pattern.replace(/\/\*$/, "");
|
|
37
316
|
const fullBase = path.join(cwd, base);
|
|
38
317
|
if (fs.existsSync(fullBase)) {
|
|
@@ -94,17 +373,12 @@ function detectFramework(pkg) {
|
|
|
94
373
|
...(pkg.devDependencies || {}),
|
|
95
374
|
};
|
|
96
375
|
|
|
97
|
-
// Check unsupported first so we can warn clearly
|
|
98
376
|
for (const fw of UNSUPPORTED_FRAMEWORKS) {
|
|
99
|
-
if (deps[fw]) {
|
|
100
|
-
return { name: fw, supported: false };
|
|
101
|
-
}
|
|
377
|
+
if (deps[fw]) return { name: fw, supported: false };
|
|
102
378
|
}
|
|
103
379
|
|
|
104
380
|
for (const fw of SUPPORTED_FRAMEWORKS) {
|
|
105
|
-
if (deps[fw]) {
|
|
106
|
-
return { name: fw, supported: true };
|
|
107
|
-
}
|
|
381
|
+
if (deps[fw]) return { name: fw, supported: true };
|
|
108
382
|
}
|
|
109
383
|
|
|
110
384
|
return { name: null, supported: false };
|
|
@@ -131,7 +405,6 @@ function readTsConfig(cwd) {
|
|
|
131
405
|
if (!fs.existsSync(tsconfigPath)) return null;
|
|
132
406
|
try {
|
|
133
407
|
const raw = fs.readFileSync(tsconfigPath, "utf8");
|
|
134
|
-
// Strip comments — tsconfig supports JSON with comments
|
|
135
408
|
const stripped = raw
|
|
136
409
|
.replace(/\/\/.*$/gm, "")
|
|
137
410
|
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -141,11 +414,6 @@ function readTsConfig(cwd) {
|
|
|
141
414
|
}
|
|
142
415
|
}
|
|
143
416
|
|
|
144
|
-
// Decide whether to generate .ts or .js files
|
|
145
|
-
// - Not TypeScript → always .js
|
|
146
|
-
// - TypeScript + allowJs: true (Next.js default) → .js is fine
|
|
147
|
-
// - TypeScript + allowJs: false (manually set by user) → must use .ts
|
|
148
|
-
// - TypeScript + allowJs not set → Next.js default is true → .js is fine
|
|
149
417
|
function shouldGenerateTs(cwd, isTypeScript) {
|
|
150
418
|
if (!isTypeScript) return false;
|
|
151
419
|
const tsconfig = readTsConfig(cwd);
|
|
@@ -179,7 +447,6 @@ function detectNextRouter(cwd) {
|
|
|
179
447
|
pagesRouter: hasPages,
|
|
180
448
|
appRouter: hasApp,
|
|
181
449
|
srcDir: hasSrc,
|
|
182
|
-
// Resolve the actual base directory
|
|
183
450
|
baseDir: hasSrc ? path.join(cwd, "src") : cwd,
|
|
184
451
|
};
|
|
185
452
|
}
|
|
@@ -209,7 +476,6 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
209
476
|
const scripts = [pkg.scripts.start, pkg.scripts.dev, pkg.scripts.serve];
|
|
210
477
|
for (const script of scripts) {
|
|
211
478
|
if (!script) continue;
|
|
212
|
-
// e.g. "node server.js" or "nodemon src/index.js" or "ts-node index.ts"
|
|
213
479
|
const match = script.match(
|
|
214
480
|
/(?:node|nodemon|ts-node|tsx)\s+([^\s]+\.(js|ts))/,
|
|
215
481
|
);
|
|
@@ -220,7 +486,7 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
220
486
|
}
|
|
221
487
|
}
|
|
222
488
|
|
|
223
|
-
// Strategy 3: common file names
|
|
489
|
+
// Strategy 3: common file names
|
|
224
490
|
const candidates = [
|
|
225
491
|
"server.js",
|
|
226
492
|
"server.ts",
|
|
@@ -243,7 +509,6 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
243
509
|
for (const candidate of candidates) {
|
|
244
510
|
const filePath = path.join(cwd, candidate);
|
|
245
511
|
if (fs.existsSync(filePath)) {
|
|
246
|
-
// Verify it actually contains express
|
|
247
512
|
const content = fs.readFileSync(filePath, "utf8");
|
|
248
513
|
if (content.includes("express") || content.includes("app.listen")) {
|
|
249
514
|
return filePath;
|
|
@@ -252,17 +517,58 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
252
517
|
}
|
|
253
518
|
|
|
254
519
|
// Strategy 4: any .js/.ts file containing app.listen()
|
|
255
|
-
return findFileWithContent(cwd, "
|
|
520
|
+
return findFileWithContent(cwd, ".listen(", [".js", ".ts"], 2);
|
|
256
521
|
}
|
|
257
522
|
|
|
258
523
|
// ─── app.listen() LOCATION ───────────────────────────────────────────────────
|
|
259
524
|
|
|
260
|
-
function findListenCall(filePath) {
|
|
525
|
+
function findListenCall(filePath, appVarName) {
|
|
526
|
+
appVarName = appVarName || "app";
|
|
527
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
528
|
+
const lines = content.split("\n");
|
|
529
|
+
const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
530
|
+
|
|
531
|
+
for (let i = 0; i < lines.length; i++) {
|
|
532
|
+
if (regex.test(lines[i])) {
|
|
533
|
+
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function findModuleExportsApp(filePath) {
|
|
540
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
541
|
+
const lines = content.split("\n");
|
|
542
|
+
for (let i = 0; i < lines.length; i++) {
|
|
543
|
+
if (/module\.exports\s*=\s*app/.test(lines[i])) {
|
|
544
|
+
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function findListenInsideCallback(filePath, appVarName) {
|
|
551
|
+
appVarName = appVarName || "app";
|
|
261
552
|
const content = fs.readFileSync(filePath, "utf8");
|
|
262
553
|
const lines = content.split("\n");
|
|
554
|
+
const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
263
555
|
|
|
264
556
|
for (let i = 0; i < lines.length; i++) {
|
|
265
|
-
if (
|
|
557
|
+
if (regex.test(lines[i])) {
|
|
558
|
+
const indentation = lines[i].match(/^(\s*)/)[1].length;
|
|
559
|
+
if (indentation > 0) {
|
|
560
|
+
return { lineIndex: i, lineNumber: i + 1, insideCallback: true };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function findCreateServer(filePath) {
|
|
568
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
569
|
+
const lines = content.split("\n");
|
|
570
|
+
for (let i = 0; i < lines.length; i++) {
|
|
571
|
+
if (/createServer\s*\(\s*app\s*\)/.test(lines[i])) {
|
|
266
572
|
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
267
573
|
}
|
|
268
574
|
}
|
|
@@ -355,12 +661,7 @@ function detectAuth(pkg) {
|
|
|
355
661
|
"jwt",
|
|
356
662
|
"express-session",
|
|
357
663
|
].includes(lib.name);
|
|
358
|
-
return {
|
|
359
|
-
name: lib.name,
|
|
360
|
-
version,
|
|
361
|
-
package: pkg2,
|
|
362
|
-
supported,
|
|
363
|
-
};
|
|
664
|
+
return { name: lib.name, version, package: pkg2, supported };
|
|
364
665
|
}
|
|
365
666
|
}
|
|
366
667
|
|
|
@@ -370,7 +671,6 @@ function detectAuth(pkg) {
|
|
|
370
671
|
// ─── NEXT-AUTH CONFIG LOCATION ───────────────────────────────────────────────
|
|
371
672
|
|
|
372
673
|
function findNextAuthConfig(cwd) {
|
|
373
|
-
// Common locations for authOptions
|
|
374
674
|
const candidates = [
|
|
375
675
|
"pages/api/auth/[...nextauth].js",
|
|
376
676
|
"pages/api/auth/[...nextauth].ts",
|
|
@@ -387,7 +687,7 @@ function findNextAuthConfig(cwd) {
|
|
|
387
687
|
"utils/auth.js",
|
|
388
688
|
"utils/auth.ts",
|
|
389
689
|
"auth.js",
|
|
390
|
-
"auth.ts",
|
|
690
|
+
"auth.ts",
|
|
391
691
|
];
|
|
392
692
|
|
|
393
693
|
for (const candidate of candidates) {
|
|
@@ -397,13 +697,9 @@ function findNextAuthConfig(cwd) {
|
|
|
397
697
|
}
|
|
398
698
|
}
|
|
399
699
|
|
|
400
|
-
// Search for authOptions in files
|
|
401
700
|
const found = findFileWithContent(cwd, "authOptions", [".js", ".ts"], 3);
|
|
402
701
|
if (found) {
|
|
403
|
-
return {
|
|
404
|
-
path: found,
|
|
405
|
-
relativePath: path.relative(cwd, found),
|
|
406
|
-
};
|
|
702
|
+
return { path: found, relativePath: path.relative(cwd, found) };
|
|
407
703
|
}
|
|
408
704
|
|
|
409
705
|
return null;
|
|
@@ -434,16 +730,6 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
|
|
|
434
730
|
function walk(currentDir, depth) {
|
|
435
731
|
if (depth > maxDepth) return null;
|
|
436
732
|
|
|
437
|
-
// Skip node_modules, .git, .next, dist, build
|
|
438
|
-
const skipDirs = [
|
|
439
|
-
"node_modules",
|
|
440
|
-
".git",
|
|
441
|
-
".next",
|
|
442
|
-
"dist",
|
|
443
|
-
"build",
|
|
444
|
-
".cache",
|
|
445
|
-
];
|
|
446
|
-
|
|
447
733
|
let entries;
|
|
448
734
|
try {
|
|
449
735
|
entries = fs.readdirSync(currentDir);
|
|
@@ -452,7 +738,7 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
|
|
|
452
738
|
}
|
|
453
739
|
|
|
454
740
|
for (const entry of entries) {
|
|
455
|
-
if (
|
|
741
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
456
742
|
|
|
457
743
|
const fullPath = path.join(currentDir, entry);
|
|
458
744
|
let stat;
|
|
@@ -481,23 +767,103 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
|
|
|
481
767
|
return walk(dir, 0);
|
|
482
768
|
}
|
|
483
769
|
|
|
770
|
+
function detectAppVarName(filePath) {
|
|
771
|
+
try {
|
|
772
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
773
|
+
const match = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*express\s*\(/);
|
|
774
|
+
return match ? match[1] : "app";
|
|
775
|
+
} catch {
|
|
776
|
+
return "app";
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
484
780
|
// ─── MAIN DETECT FUNCTION ────────────────────────────────────────────────────
|
|
485
781
|
|
|
486
782
|
function detect(cwd) {
|
|
487
783
|
const pkg = readPackageJson(cwd);
|
|
488
784
|
const monorepo = detectMonorepo(cwd);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
785
|
+
let framework = detectFramework(pkg);
|
|
786
|
+
|
|
787
|
+
// ── If framework not found in root, scan ALL package.json files ───────────
|
|
788
|
+
let backendDir = cwd;
|
|
789
|
+
let frontendDir = null;
|
|
790
|
+
let frontendPkg = null;
|
|
791
|
+
|
|
792
|
+
if (!framework.name) {
|
|
793
|
+
const allPackages = scanAllPackageJsons(cwd);
|
|
794
|
+
|
|
795
|
+
for (const { dir, pkg: subPkg } of allPackages) {
|
|
796
|
+
// Skip the root package.json — already checked
|
|
797
|
+
if (dir === cwd) continue;
|
|
798
|
+
|
|
799
|
+
const classification = classifyPackageJson(subPkg);
|
|
800
|
+
|
|
801
|
+
// AFTER
|
|
802
|
+
if (
|
|
803
|
+
(classification === "backend" || classification === "fullstack") &&
|
|
804
|
+
!framework.name
|
|
805
|
+
) {
|
|
806
|
+
framework = detectFramework(subPkg);
|
|
807
|
+
backendDir = dir;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (
|
|
811
|
+
(classification === "frontend" || classification === "fullstack") &&
|
|
812
|
+
!frontendDir &&
|
|
813
|
+
dir !== backendDir
|
|
814
|
+
) {
|
|
815
|
+
frontendDir = dir;
|
|
816
|
+
frontendPkg = subPkg;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
// Framework found in root — scan for separate frontend folder
|
|
821
|
+
const allPackages = scanAllPackageJsons(cwd);
|
|
822
|
+
for (const { dir, pkg: subPkg } of allPackages) {
|
|
823
|
+
if (dir === cwd) continue;
|
|
824
|
+
if (dir === backendDir) continue;
|
|
825
|
+
const classification = classifyPackageJson(subPkg);
|
|
826
|
+
if (classification === "frontend" || classification === "fullstack") {
|
|
827
|
+
frontendDir = dir;
|
|
828
|
+
frontendPkg = subPkg;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── Use backendDir for all backend-specific detection ─────────────────────
|
|
835
|
+
// Guard: if frontendDir ended up being the same as backendDir
|
|
836
|
+
// (e.g. a fullstack Next.js folder detected as both), clear frontendDir
|
|
837
|
+
// so we don't try to inject script tag into the wrong place
|
|
838
|
+
if (frontendDir && frontendDir === backendDir) {
|
|
839
|
+
frontendDir = null;
|
|
840
|
+
frontendPkg = null;
|
|
841
|
+
}
|
|
842
|
+
const backendPkg = readPackageJson(backendDir) || pkg;
|
|
843
|
+
const moduleSystem = detectModuleSystem(backendPkg);
|
|
844
|
+
const isTypeScript = detectTypeScript(backendDir);
|
|
845
|
+
const hasSrc = detectSrcDir(backendDir);
|
|
846
|
+
const auth = detectAuth(backendPkg);
|
|
847
|
+
const generateTs = shouldGenerateTs(backendDir, isTypeScript);
|
|
848
|
+
|
|
849
|
+
// ── Find frontend main file ───────────────────────────────────────────────
|
|
850
|
+
let frontendMainFile = null;
|
|
851
|
+
if (frontendDir && frontendPkg) {
|
|
852
|
+
frontendMainFile = findMainFrontendFile(frontendDir, frontendPkg);
|
|
853
|
+
} else if (framework.name === "next") {
|
|
854
|
+
// Next.js is fullstack — find its frontend file in backendDir
|
|
855
|
+
frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
|
|
856
|
+
}
|
|
495
857
|
|
|
496
|
-
const
|
|
858
|
+
const packageManager =
|
|
859
|
+
detectPackageManager(backendDir) !== "npm"
|
|
860
|
+
? detectPackageManager(backendDir)
|
|
861
|
+
: detectPackageManager(cwd);
|
|
497
862
|
|
|
498
863
|
const result = {
|
|
499
|
-
cwd,
|
|
500
|
-
|
|
864
|
+
cwd: backendDir, // use backend dir as working dir
|
|
865
|
+
rootCwd: cwd, // keep original root for reference
|
|
866
|
+
pkg: backendPkg,
|
|
501
867
|
monorepo,
|
|
502
868
|
framework,
|
|
503
869
|
moduleSystem,
|
|
@@ -506,34 +872,62 @@ function detect(cwd) {
|
|
|
506
872
|
hasSrc,
|
|
507
873
|
auth,
|
|
508
874
|
packageManager,
|
|
509
|
-
// generateTs: true means user has allowJs:false — must use .ts
|
|
510
|
-
// generateTs: false means .js files are fine (most users)
|
|
511
875
|
ext: generateTs ? ".ts" : ".js",
|
|
876
|
+
// Frontend info
|
|
877
|
+
frontendDir,
|
|
878
|
+
frontendPkg,
|
|
879
|
+
frontendMainFile,
|
|
512
880
|
};
|
|
513
881
|
|
|
514
|
-
// Framework-specific detection
|
|
882
|
+
// ── Framework-specific detection ──────────────────────────────────────────
|
|
515
883
|
if (framework.name === "next") {
|
|
516
|
-
result.next = detectNextRouter(
|
|
517
|
-
result.nextVersion = detectNextVersion(
|
|
884
|
+
result.next = detectNextRouter(backendDir);
|
|
885
|
+
result.nextVersion = detectNextVersion(backendPkg);
|
|
518
886
|
if (auth.name === "next-auth") {
|
|
519
|
-
result.nextAuthConfig = findNextAuthConfig(
|
|
887
|
+
result.nextAuthConfig = findNextAuthConfig(backendDir);
|
|
520
888
|
}
|
|
521
889
|
}
|
|
522
890
|
|
|
523
891
|
if (framework.name === "express") {
|
|
524
|
-
result.entryPoint = detectExpressEntry(
|
|
892
|
+
result.entryPoint = detectExpressEntry(backendDir, backendPkg);
|
|
525
893
|
if (result.entryPoint) {
|
|
526
|
-
result.
|
|
894
|
+
result.appVarName = detectAppVarName(result.entryPoint); // detect FIRST
|
|
895
|
+
result.listenCall = findListenCall(result.entryPoint, result.appVarName);
|
|
896
|
+
result.listenInsideCallback = findListenInsideCallback(
|
|
897
|
+
result.entryPoint,
|
|
898
|
+
result.appVarName,
|
|
899
|
+
);
|
|
900
|
+
result.moduleExportsApp = findModuleExportsApp(result.entryPoint);
|
|
901
|
+
result.createServer = findCreateServer(result.entryPoint);
|
|
902
|
+
|
|
903
|
+
const appFileCandidates = [
|
|
904
|
+
"src/app.js",
|
|
905
|
+
"src/app.ts",
|
|
906
|
+
"app.js",
|
|
907
|
+
"app.ts",
|
|
908
|
+
];
|
|
909
|
+
for (const candidate of appFileCandidates) {
|
|
910
|
+
const fullPath = path.join(backendDir, candidate);
|
|
911
|
+
if (fs.existsSync(fullPath) && fullPath !== result.entryPoint) {
|
|
912
|
+
const exportCall = findModuleExportsApp(fullPath);
|
|
913
|
+
if (exportCall) {
|
|
914
|
+
result.appFile = fullPath;
|
|
915
|
+
result.appFileExport = exportCall;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
527
920
|
}
|
|
528
921
|
}
|
|
529
922
|
|
|
923
|
+
// ── Already initialized check ─────────────────────────────────────────────
|
|
530
924
|
result.alreadyInitialized =
|
|
531
925
|
detectExistingBotVersion(result.entryPoint) ||
|
|
532
926
|
(framework.name === "next" &&
|
|
533
|
-
(fs.existsSync(path.join(
|
|
534
|
-
fs.existsSync(path.join(
|
|
535
|
-
fs.existsSync(path.join(
|
|
536
|
-
fs.existsSync(path.join(
|
|
927
|
+
(fs.existsSync(path.join(backendDir, "instrumentation.js")) ||
|
|
928
|
+
fs.existsSync(path.join(backendDir, "instrumentation.ts")) ||
|
|
929
|
+
fs.existsSync(path.join(backendDir, "src", "instrumentation.js")) ||
|
|
930
|
+
fs.existsSync(path.join(backendDir, "src", "instrumentation.ts"))));
|
|
537
931
|
|
|
538
932
|
return result;
|
|
539
933
|
}
|
|
@@ -541,6 +935,10 @@ function detect(cwd) {
|
|
|
541
935
|
module.exports = {
|
|
542
936
|
detect,
|
|
543
937
|
readPackageJson,
|
|
938
|
+
scanAllPackageJsons,
|
|
939
|
+
classifyPackageJson,
|
|
940
|
+
detectFrontendFramework,
|
|
941
|
+
findMainFrontendFile,
|
|
544
942
|
detectMonorepo,
|
|
545
943
|
detectFramework,
|
|
546
944
|
detectModuleSystem,
|
|
@@ -555,4 +953,8 @@ module.exports = {
|
|
|
555
953
|
detectExistingBotVersion,
|
|
556
954
|
findFileWithContent,
|
|
557
955
|
findListenCall,
|
|
956
|
+
findModuleExportsApp,
|
|
957
|
+
findListenInsideCallback,
|
|
958
|
+
findCreateServer,
|
|
959
|
+
detectAppVarName,
|
|
558
960
|
};
|