botversion-sdk 1.1.6 → 2.0.0
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/scan-frontend.js +49 -0
- package/client.js +12 -6
- package/index.js +150 -23
- package/interceptor.js +332 -86
- package/package.json +2 -2
- package/scanner.js +7 -106
- package/bin/init.js +0 -605
- package/cli/detector.js +0 -1149
- package/cli/generator.js +0 -187
- package/cli/prompts.js +0 -163
- package/cli/writer.js +0 -511
package/cli/detector.js
DELETED
|
@@ -1,1149 +0,0 @@
|
|
|
1
|
-
// botversion-sdk/cli/detector.js
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const path = require("path");
|
|
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
|
-
|
|
23
|
-
// ─── PACKAGE JSON ────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
function readPackageJson(cwd) {
|
|
26
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
27
|
-
if (!fs.existsSync(pkgPath)) return null;
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
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
|
-
|
|
304
|
-
// ─── MONOREPO DETECTION ──────────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
function detectMonorepo(cwd) {
|
|
307
|
-
const rootPkg = readPackageJson(cwd);
|
|
308
|
-
if (rootPkg && rootPkg.workspaces) {
|
|
309
|
-
const workspaceDirs = [];
|
|
310
|
-
const patterns = Array.isArray(rootPkg.workspaces)
|
|
311
|
-
? rootPkg.workspaces
|
|
312
|
-
: rootPkg.workspaces.packages || [];
|
|
313
|
-
|
|
314
|
-
patterns.forEach((pattern) => {
|
|
315
|
-
const base = pattern.replace(/\/\*$/, "");
|
|
316
|
-
const fullBase = path.join(cwd, base);
|
|
317
|
-
if (fs.existsSync(fullBase)) {
|
|
318
|
-
fs.readdirSync(fullBase).forEach((dir) => {
|
|
319
|
-
const dirPath = path.join(fullBase, dir);
|
|
320
|
-
if (
|
|
321
|
-
fs.statSync(dirPath).isDirectory() &&
|
|
322
|
-
fs.existsSync(path.join(dirPath, "package.json"))
|
|
323
|
-
) {
|
|
324
|
-
workspaceDirs.push(dirPath);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
if (workspaceDirs.length > 0) {
|
|
331
|
-
return { isMonorepo: true, packages: workspaceDirs };
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Check for common monorepo structures
|
|
336
|
-
const monorepoDirs = ["packages", "apps", "services"];
|
|
337
|
-
for (const dir of monorepoDirs) {
|
|
338
|
-
const dirPath = path.join(cwd, dir);
|
|
339
|
-
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
340
|
-
const subDirs = fs
|
|
341
|
-
.readdirSync(dirPath)
|
|
342
|
-
.map((d) => path.join(dirPath, d))
|
|
343
|
-
.filter(
|
|
344
|
-
(d) =>
|
|
345
|
-
fs.statSync(d).isDirectory() &&
|
|
346
|
-
fs.existsSync(path.join(d, "package.json")),
|
|
347
|
-
);
|
|
348
|
-
if (subDirs.length > 0) {
|
|
349
|
-
return { isMonorepo: true, packages: subDirs };
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return { isMonorepo: false };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// ─── FRAMEWORK DETECTION ─────────────────────────────────────────────────────
|
|
358
|
-
|
|
359
|
-
const SUPPORTED_FRAMEWORKS = ["next", "express"];
|
|
360
|
-
const UNSUPPORTED_FRAMEWORKS = [
|
|
361
|
-
"fastify",
|
|
362
|
-
"koa",
|
|
363
|
-
"@hapi/hapi",
|
|
364
|
-
"nestjs",
|
|
365
|
-
"@nestjs/core",
|
|
366
|
-
];
|
|
367
|
-
|
|
368
|
-
function detectFramework(pkg) {
|
|
369
|
-
if (!pkg) return { name: null, supported: false };
|
|
370
|
-
|
|
371
|
-
const deps = {
|
|
372
|
-
...(pkg.dependencies || {}),
|
|
373
|
-
...(pkg.devDependencies || {}),
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
for (const fw of UNSUPPORTED_FRAMEWORKS) {
|
|
377
|
-
if (deps[fw]) return { name: fw, supported: false };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// If BOTH next and express exist, always pick next
|
|
381
|
-
// because express is just being used as a custom server wrapper
|
|
382
|
-
if (deps["next"]) return { name: "next", supported: true };
|
|
383
|
-
|
|
384
|
-
for (const fw of SUPPORTED_FRAMEWORKS) {
|
|
385
|
-
if (deps[fw]) return { name: fw, supported: true };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return { name: null, supported: false };
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ─── MODULE SYSTEM DETECTION ─────────────────────────────────────────────────
|
|
392
|
-
|
|
393
|
-
function detectModuleSystem(pkg) {
|
|
394
|
-
if (pkg && pkg.type === "module") return "esm";
|
|
395
|
-
return "cjs";
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ─── TYPESCRIPT DETECTION ────────────────────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
function detectTypeScript(cwd) {
|
|
401
|
-
return (
|
|
402
|
-
fs.existsSync(path.join(cwd, "tsconfig.json")) ||
|
|
403
|
-
fs.existsSync(path.join(cwd, "tsconfig.base.json"))
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function readTsConfig(cwd) {
|
|
408
|
-
const tsconfigPath = path.join(cwd, "tsconfig.json");
|
|
409
|
-
if (!fs.existsSync(tsconfigPath)) return null;
|
|
410
|
-
try {
|
|
411
|
-
const raw = fs.readFileSync(tsconfigPath, "utf8");
|
|
412
|
-
const stripped = raw
|
|
413
|
-
.replace(/\/\/.*$/gm, "")
|
|
414
|
-
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
415
|
-
return JSON.parse(stripped);
|
|
416
|
-
} catch {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function shouldGenerateTs(cwd, isTypeScript) {
|
|
422
|
-
if (!isTypeScript) return false;
|
|
423
|
-
const tsconfig = readTsConfig(cwd);
|
|
424
|
-
if (!tsconfig) return false;
|
|
425
|
-
const allowJs = tsconfig.compilerOptions && tsconfig.compilerOptions.allowJs;
|
|
426
|
-
return allowJs === false;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// ─── SRC DIRECTORY DETECTION ─────────────────────────────────────────────────
|
|
430
|
-
|
|
431
|
-
function detectSrcDir(cwd) {
|
|
432
|
-
return fs.existsSync(path.join(cwd, "src"));
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// ─── NEXT.JS ROUTER DETECTION ────────────────────────────────────────────────
|
|
436
|
-
|
|
437
|
-
function detectNextRouter(cwd) {
|
|
438
|
-
const hasSrc = detectSrcDir(cwd);
|
|
439
|
-
const bases = hasSrc ? [path.join(cwd, "src"), cwd] : [cwd];
|
|
440
|
-
|
|
441
|
-
let hasPages = false;
|
|
442
|
-
let hasApp = false;
|
|
443
|
-
|
|
444
|
-
for (const base of bases) {
|
|
445
|
-
if (fs.existsSync(path.join(base, "pages", "api"))) hasPages = true;
|
|
446
|
-
if (fs.existsSync(path.join(base, "pages"))) hasPages = true;
|
|
447
|
-
if (fs.existsSync(path.join(base, "app"))) hasApp = true;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return {
|
|
451
|
-
pagesRouter: hasPages,
|
|
452
|
-
appRouter: hasApp,
|
|
453
|
-
srcDir: hasSrc,
|
|
454
|
-
baseDir: hasSrc ? path.join(cwd, "src") : cwd,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// ─── NEXT.JS VERSION DETECTION ───────────────────────────────────────────────
|
|
459
|
-
|
|
460
|
-
function detectNextVersion(pkg) {
|
|
461
|
-
if (!pkg) return null;
|
|
462
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
463
|
-
const version = deps["next"];
|
|
464
|
-
if (!version) return null;
|
|
465
|
-
const major = parseInt(version.replace(/[^0-9]/, ""), 10);
|
|
466
|
-
return { raw: version, major };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// ─── SCORING FUNCTIONS ────────────────────────────────────────────────────────
|
|
470
|
-
|
|
471
|
-
function scoreExpressFile(content, filePath) {
|
|
472
|
-
let score = 0;
|
|
473
|
-
const filename = path.basename(filePath);
|
|
474
|
-
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
475
|
-
|
|
476
|
-
// ── EXISTING POSITIVE SIGNALS ──────────────────────────────────
|
|
477
|
-
if (/express\s*\(\s*\)/.test(content)) score += 10;
|
|
478
|
-
if (/class\s+\w+\s+extends\s+express/.test(content)) score += 10;
|
|
479
|
-
if (/(?:function|const|let|var)\s+(create|make|build|setup)App/.test(content))
|
|
480
|
-
score += 8;
|
|
481
|
-
if (/\.listen\s*\(/.test(content)) score += 8;
|
|
482
|
-
if (/\.use\s*\(\s*['"`\/]/.test(content)) score += 5;
|
|
483
|
-
if (/app\.use\s*\(/.test(content)) score += 4;
|
|
484
|
-
if (
|
|
485
|
-
/require\s*\(\s*['"]express['"]\s*\)|from\s+['"]express['"]/.test(content)
|
|
486
|
-
)
|
|
487
|
-
score += 1;
|
|
488
|
-
|
|
489
|
-
// ── NEW POSITIVE SIGNALS — real API server ─────────────────────
|
|
490
|
-
const hasApiRoutes = /\.(get|post|put|delete|patch)\s*\(\s*['"`]\//.test(
|
|
491
|
-
content,
|
|
492
|
-
);
|
|
493
|
-
const returnsJson = /res\.json\s*\(/.test(content);
|
|
494
|
-
const hasRouterFiles = /require\s*\(['"`]\.\/routes/.test(content);
|
|
495
|
-
const hasControllers = /require\s*\(['"`]\.\/controllers/.test(content);
|
|
496
|
-
|
|
497
|
-
if (hasApiRoutes) score += 6;
|
|
498
|
-
if (returnsJson) score += 5;
|
|
499
|
-
if (hasRouterFiles) score += 4;
|
|
500
|
-
if (hasControllers) score += 3;
|
|
501
|
-
|
|
502
|
-
// ── EXISTING PENALTIES ─────────────────────────────────────────
|
|
503
|
-
if (/^(?:const|let|var)\s+router\s*=\s*express\.Router\s*\(/m.test(content))
|
|
504
|
-
score -= 8;
|
|
505
|
-
if (/(test_|_test|\.test\.|\.spec\.|conftest)/.test(filename)) score -= 10;
|
|
506
|
-
if (/dist\/|build\//.test(filePath)) score -= 10;
|
|
507
|
-
|
|
508
|
-
// ── NEW PENALTIES ──────────────────────────────────────────────
|
|
509
|
-
|
|
510
|
-
// Category 1: PHP servers
|
|
511
|
-
if (content.includes("php-express")) score -= 20;
|
|
512
|
-
if (content.includes("phpExpress")) score -= 20;
|
|
513
|
-
if (/app\.engine\s*\(\s*['"]php['"]/.test(content)) score -= 20;
|
|
514
|
-
|
|
515
|
-
// Category 2: View-only servers (any template engine, no API)
|
|
516
|
-
const hasViewEngine = /app\.set\s*\(\s*['"]view engine['"]/.test(content);
|
|
517
|
-
if (hasViewEngine && !hasApiRoutes && !returnsJson) score -= 15;
|
|
518
|
-
|
|
519
|
-
// Category 3: Static-only servers
|
|
520
|
-
const hasStatic = /express\.static\s*\(/.test(content);
|
|
521
|
-
const hasOnlyStatic = hasStatic && !hasApiRoutes && !returnsJson;
|
|
522
|
-
if (hasOnlyStatic) score -= 15;
|
|
523
|
-
|
|
524
|
-
// Category 4: Proxy servers
|
|
525
|
-
if (
|
|
526
|
-
content.includes("http-proxy-middleware") ||
|
|
527
|
-
content.includes("createProxyMiddleware")
|
|
528
|
-
)
|
|
529
|
-
score -= 15;
|
|
530
|
-
|
|
531
|
-
// Category 5: WebSocket-only servers
|
|
532
|
-
const hasSocketIO =
|
|
533
|
-
content.includes("socket.io") || content.includes("new Server(");
|
|
534
|
-
if (hasSocketIO && !hasApiRoutes) score -= 15;
|
|
535
|
-
|
|
536
|
-
// Category 6: Job/Queue servers
|
|
537
|
-
if (
|
|
538
|
-
(content.includes("require('bull')") ||
|
|
539
|
-
content.includes('require("bull")') ||
|
|
540
|
-
content.includes("bullmq") ||
|
|
541
|
-
content.includes("agenda")) &&
|
|
542
|
-
!hasApiRoutes
|
|
543
|
-
)
|
|
544
|
-
score -= 15;
|
|
545
|
-
|
|
546
|
-
// Category 7: Webhook-only servers
|
|
547
|
-
const hasWebhook = /['"`]\/webhook['"`]/.test(content);
|
|
548
|
-
const onlyWebhook = hasWebhook && !hasApiRoutes;
|
|
549
|
-
if (onlyWebhook) score -= 10;
|
|
550
|
-
|
|
551
|
-
// Category 8: Dev tooling servers
|
|
552
|
-
if (
|
|
553
|
-
content.includes("webpack-dev-middleware") ||
|
|
554
|
-
content.includes("webpackDevMiddleware") ||
|
|
555
|
-
content.includes("storybook")
|
|
556
|
-
)
|
|
557
|
-
score -= 15;
|
|
558
|
-
|
|
559
|
-
// Category 9: Test/mock servers (path-based)
|
|
560
|
-
if (/__mocks__|fixtures\/|test\/|__tests__\//.test(normalizedPath))
|
|
561
|
-
score -= 15;
|
|
562
|
-
|
|
563
|
-
// ── FILENAME BONUSES ───────────────────────────────────────────
|
|
564
|
-
if (["server.js", "server.ts", "app.js", "app.ts"].includes(filename))
|
|
565
|
-
score += 3;
|
|
566
|
-
if (["index.js", "index.ts", "main.js", "main.ts"].includes(filename))
|
|
567
|
-
score += 2;
|
|
568
|
-
|
|
569
|
-
return score;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// ─── PARSE ENTRY FROM CONFIG FILES ───────────────────────────────────────────
|
|
573
|
-
|
|
574
|
-
function parseEntryFromConfigFiles(cwd) {
|
|
575
|
-
/**
|
|
576
|
-
* Extracts likely entry point from Procfile or Dockerfile.
|
|
577
|
-
* Covers patterns like:
|
|
578
|
-
* Procfile: web: node server.js
|
|
579
|
-
* Procfile: web: nodemon src/index.js
|
|
580
|
-
* Dockerfile: CMD ["node", "server.js"]
|
|
581
|
-
* Dockerfile: CMD ["nodemon", "src/index.js"]
|
|
582
|
-
*/
|
|
583
|
-
|
|
584
|
-
function resolveFile(cwd, filePath) {
|
|
585
|
-
const full = path.join(cwd, filePath);
|
|
586
|
-
if (fs.existsSync(full)) return full;
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// ── 1. Check Procfile ─────────────────────────────────────────────────────
|
|
591
|
-
const procfilePath = path.join(cwd, "Procfile");
|
|
592
|
-
if (fs.existsSync(procfilePath)) {
|
|
593
|
-
try {
|
|
594
|
-
const lines = fs.readFileSync(procfilePath, "utf8").split("\n");
|
|
595
|
-
for (const line of lines) {
|
|
596
|
-
if (!line.trim() || line.startsWith("#")) continue;
|
|
597
|
-
|
|
598
|
-
// node/nodemon/ts-node/tsx server.js or src/index.ts
|
|
599
|
-
const match = line.match(
|
|
600
|
-
/(?:node|nodemon|ts-node|ts-node-dev|tsx)\s+([\w./\\-]+\.[jt]s)/,
|
|
601
|
-
);
|
|
602
|
-
if (match) {
|
|
603
|
-
const result = resolveFile(cwd, match[1]);
|
|
604
|
-
if (result) return result;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
} catch {}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// ── 2. Check Dockerfile ───────────────────────────────────────────────────
|
|
611
|
-
const dockerfilePath = path.join(cwd, "Dockerfile");
|
|
612
|
-
if (fs.existsSync(dockerfilePath)) {
|
|
613
|
-
try {
|
|
614
|
-
const lines = fs.readFileSync(dockerfilePath, "utf8").split("\n");
|
|
615
|
-
for (const line of lines) {
|
|
616
|
-
if (!line.trim() || line.startsWith("#")) continue;
|
|
617
|
-
|
|
618
|
-
// CMD ["node", "server.js"]
|
|
619
|
-
// CMD ["nodemon", "src/index.js"]
|
|
620
|
-
const cmdMatch = line.match(
|
|
621
|
-
/(?:CMD|ENTRYPOINT)\s*\[.*?"(?:node|nodemon|ts-node|tsx)",\s*"([\w./\\-]+\.[jt]s)"/,
|
|
622
|
-
);
|
|
623
|
-
if (cmdMatch) {
|
|
624
|
-
const result = resolveFile(cwd, cmdMatch[1]);
|
|
625
|
-
if (result) return result;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// CMD node server.js (shell form)
|
|
629
|
-
const shellMatch = line.match(
|
|
630
|
-
/(?:CMD|ENTRYPOINT)\s+(?:node|nodemon|ts-node|tsx)\s+([\w./\\-]+\.[jt]s)/,
|
|
631
|
-
);
|
|
632
|
-
if (shellMatch) {
|
|
633
|
-
const result = resolveFile(cwd, shellMatch[1]);
|
|
634
|
-
if (result) return result;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
} catch {}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function detectExpressEntry(cwd, pkg) {
|
|
644
|
-
const SKIP_ENTRY_DIRS = ["dist", "build", "out", ".next", "coverage"];
|
|
645
|
-
|
|
646
|
-
function isCompiledPath(filePath) {
|
|
647
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
648
|
-
return SKIP_ENTRY_DIRS.some(
|
|
649
|
-
(dir) =>
|
|
650
|
-
normalized.includes(`/${dir}/`) ||
|
|
651
|
-
normalized.endsWith(`/${dir}`) ||
|
|
652
|
-
normalized.startsWith(`${dir}/`),
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// ── Strategy 1: package.json "main" field ─────────────────────────────────
|
|
657
|
-
if (pkg && pkg.main) {
|
|
658
|
-
const mainPath = path.join(cwd, pkg.main);
|
|
659
|
-
if (fs.existsSync(mainPath) && !isCompiledPath(pkg.main)) {
|
|
660
|
-
return mainPath;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// ── Strategy 2: scripts.start / scripts.dev ───────────────────────────────
|
|
665
|
-
if (pkg && pkg.scripts) {
|
|
666
|
-
const scripts = [
|
|
667
|
-
pkg.scripts.dev,
|
|
668
|
-
pkg.scripts.serve,
|
|
669
|
-
pkg.scripts["dev-server"],
|
|
670
|
-
pkg.scripts.start,
|
|
671
|
-
];
|
|
672
|
-
for (const script of scripts) {
|
|
673
|
-
if (!script) continue;
|
|
674
|
-
const match = script.match(
|
|
675
|
-
/(?:node|nodemon|ts-node|ts-node-dev|tsx)\s+([^\s]+\.(js|ts))/,
|
|
676
|
-
);
|
|
677
|
-
if (match) {
|
|
678
|
-
const scriptPath = match[1];
|
|
679
|
-
if (isCompiledPath(scriptPath)) continue;
|
|
680
|
-
const filePath = path.join(cwd, scriptPath);
|
|
681
|
-
if (fs.existsSync(filePath)) return filePath;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Strategy 2b: compiled path fallback — find TS source equivalent
|
|
686
|
-
for (const script of scripts) {
|
|
687
|
-
if (!script) continue;
|
|
688
|
-
const match = script.match(
|
|
689
|
-
/(?:node|nodemon|ts-node|ts-node-dev|tsx)\s+([^\s]+\.(js|ts))/,
|
|
690
|
-
);
|
|
691
|
-
if (match) {
|
|
692
|
-
const tsEquivalent = match[1]
|
|
693
|
-
.replace(/^backend\/dist\//, "backend/")
|
|
694
|
-
.replace(/^dist\//, "")
|
|
695
|
-
.replace(/\.js$/, ".ts");
|
|
696
|
-
const filePath = path.join(cwd, tsEquivalent);
|
|
697
|
-
if (fs.existsSync(filePath)) return filePath;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// ── Strategy 3: Scoring — scan all JS/TS files ────────────────────────────
|
|
703
|
-
const skipDirs = new Set([
|
|
704
|
-
...SKIP_DIRS,
|
|
705
|
-
"dist",
|
|
706
|
-
"build",
|
|
707
|
-
"out",
|
|
708
|
-
"coverage",
|
|
709
|
-
"tests",
|
|
710
|
-
"test",
|
|
711
|
-
"__tests__",
|
|
712
|
-
"scripts",
|
|
713
|
-
"docs",
|
|
714
|
-
]);
|
|
715
|
-
|
|
716
|
-
const scoredCandidates = [];
|
|
717
|
-
|
|
718
|
-
function walk(directory, depth) {
|
|
719
|
-
if (depth > 3) return;
|
|
720
|
-
let entries;
|
|
721
|
-
try {
|
|
722
|
-
entries = fs.readdirSync(directory);
|
|
723
|
-
} catch {
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
for (const entry of entries) {
|
|
728
|
-
if (skipDirs.has(entry) || entry.startsWith(".")) continue;
|
|
729
|
-
|
|
730
|
-
const fullPath = path.join(directory, entry);
|
|
731
|
-
let stat;
|
|
732
|
-
try {
|
|
733
|
-
stat = fs.statSync(fullPath);
|
|
734
|
-
} catch {
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (stat.isDirectory()) {
|
|
739
|
-
walk(fullPath, depth + 1);
|
|
740
|
-
} else if (/\.(js|ts)$/.test(entry) && !/\.d\.ts$/.test(entry)) {
|
|
741
|
-
try {
|
|
742
|
-
const content = fs.readFileSync(fullPath, "utf8");
|
|
743
|
-
const score = scoreExpressFile(content, fullPath);
|
|
744
|
-
if (score > 0) {
|
|
745
|
-
scoredCandidates.push({ score, filePath: fullPath });
|
|
746
|
-
}
|
|
747
|
-
} catch {
|
|
748
|
-
continue;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
walk(cwd, 0);
|
|
755
|
-
|
|
756
|
-
if (scoredCandidates.length > 0) {
|
|
757
|
-
scoredCandidates.sort((a, b) => b.score - a.score);
|
|
758
|
-
const best = scoredCandidates[0];
|
|
759
|
-
|
|
760
|
-
// If best score is weak, cross-check with Procfile/Dockerfile
|
|
761
|
-
if (best.score <= 3) {
|
|
762
|
-
const configEntry = parseEntryFromConfigFiles(cwd);
|
|
763
|
-
if (configEntry) return configEntry;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
return best.filePath;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// ── Strategy 4: Procfile/Dockerfile fallback ──────────────────────────────
|
|
770
|
-
const configEntry = parseEntryFromConfigFiles(cwd);
|
|
771
|
-
if (configEntry) return configEntry;
|
|
772
|
-
|
|
773
|
-
// ── Strategy 5: Last resort deep scan ────────────────────────────────────
|
|
774
|
-
return findFileWithContent(cwd, ".listen(", [".js", ".ts"], 2);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// ─── app.listen() LOCATION ───────────────────────────────────────────────────
|
|
778
|
-
|
|
779
|
-
function findListenCall(filePath, appVarName) {
|
|
780
|
-
appVarName = appVarName || "app";
|
|
781
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
782
|
-
const lines = content.split("\n");
|
|
783
|
-
const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
784
|
-
|
|
785
|
-
for (let i = 0; i < lines.length; i++) {
|
|
786
|
-
if (regex.test(lines[i])) {
|
|
787
|
-
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function findModuleExportsApp(filePath) {
|
|
794
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
795
|
-
const lines = content.split("\n");
|
|
796
|
-
for (let i = 0; i < lines.length; i++) {
|
|
797
|
-
if (/module\.exports\s*=\s*app/.test(lines[i])) {
|
|
798
|
-
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
return null;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function findListenInsideCallback(filePath, appVarName) {
|
|
805
|
-
appVarName = appVarName || "app";
|
|
806
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
807
|
-
const lines = content.split("\n");
|
|
808
|
-
const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
809
|
-
|
|
810
|
-
for (let i = 0; i < lines.length; i++) {
|
|
811
|
-
if (regex.test(lines[i])) {
|
|
812
|
-
const indentation = lines[i].match(/^(\s*)/)[1].length;
|
|
813
|
-
if (indentation > 0) {
|
|
814
|
-
return { lineIndex: i, lineNumber: i + 1, insideCallback: true };
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return null;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function findCreateServer(filePath) {
|
|
822
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
823
|
-
const lines = content.split("\n");
|
|
824
|
-
for (let i = 0; i < lines.length; i++) {
|
|
825
|
-
if (/createServer\s*\(\s*app\s*\)/.test(lines[i])) {
|
|
826
|
-
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return null;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// ─── PACKAGE MANAGER DETECTION ───────────────────────────────────────────────
|
|
833
|
-
|
|
834
|
-
function detectPackageManager(cwd) {
|
|
835
|
-
if (fs.existsSync(path.join(cwd, "bun.lockb"))) return "bun";
|
|
836
|
-
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
837
|
-
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
838
|
-
return "npm";
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// ─── EXISTING BOTVERSION DETECTION ───────────────────────────────────────────
|
|
842
|
-
|
|
843
|
-
function detectExistingBotVersion(filePath) {
|
|
844
|
-
if (!filePath || !fs.existsSync(filePath)) return false;
|
|
845
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
846
|
-
return content.includes("botversion-sdk") || content.includes("BotVersion");
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// ─── HELPER: find file containing a string ───────────────────────────────────
|
|
850
|
-
|
|
851
|
-
function findFileWithContent(dir, searchString, extensions, maxDepth) {
|
|
852
|
-
maxDepth = maxDepth || 2;
|
|
853
|
-
|
|
854
|
-
function walk(currentDir, depth) {
|
|
855
|
-
if (depth > maxDepth) return null;
|
|
856
|
-
|
|
857
|
-
let entries;
|
|
858
|
-
try {
|
|
859
|
-
entries = fs.readdirSync(currentDir);
|
|
860
|
-
} catch {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
for (const entry of entries) {
|
|
865
|
-
if (SKIP_DIRS.includes(entry)) continue;
|
|
866
|
-
|
|
867
|
-
const fullPath = path.join(currentDir, entry);
|
|
868
|
-
let stat;
|
|
869
|
-
try {
|
|
870
|
-
stat = fs.statSync(fullPath);
|
|
871
|
-
} catch {
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
if (stat.isDirectory()) {
|
|
876
|
-
const result = walk(fullPath, depth + 1);
|
|
877
|
-
if (result) return result;
|
|
878
|
-
} else if (extensions.some((ext) => entry.endsWith(ext))) {
|
|
879
|
-
try {
|
|
880
|
-
const content = fs.readFileSync(fullPath, "utf8");
|
|
881
|
-
if (content.includes(searchString)) return fullPath;
|
|
882
|
-
} catch {
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return walk(dir, 0);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function detectAppVarName(filePath) {
|
|
895
|
-
try {
|
|
896
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
897
|
-
|
|
898
|
-
// Pattern 1: const app = express()
|
|
899
|
-
const match1 = content.match(
|
|
900
|
-
/(?:const|let|var)\s+(\w+)\s*=\s*express\s*\(/,
|
|
901
|
-
);
|
|
902
|
-
if (match1) return match1[1];
|
|
903
|
-
|
|
904
|
-
// Pattern 2: const app = require('express')()
|
|
905
|
-
const match2 = content.match(
|
|
906
|
-
/(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"]express['"]\s*\)\s*\(/,
|
|
907
|
-
);
|
|
908
|
-
if (match2) return match2[1];
|
|
909
|
-
|
|
910
|
-
// Pattern 3: factory function — createApp(), makeApp() etc.
|
|
911
|
-
// const app = createApp()
|
|
912
|
-
const match3 = content.match(
|
|
913
|
-
/(?:const|let|var)\s+(\w+)\s*=\s*(?:create|make|build|setup)App\s*\(/,
|
|
914
|
-
);
|
|
915
|
-
if (match3) return match3[1];
|
|
916
|
-
|
|
917
|
-
// Pattern 4: subclass — class MyApp extends express
|
|
918
|
-
// const app = new MyApp()
|
|
919
|
-
const subclassNames = [
|
|
920
|
-
...content.matchAll(/class\s+(\w+)\s+extends\s+express/g),
|
|
921
|
-
].map((m) => m[1]);
|
|
922
|
-
for (const name of subclassNames) {
|
|
923
|
-
const subMatch = content.match(
|
|
924
|
-
new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*new\\s+${name}\\s*\\(`),
|
|
925
|
-
);
|
|
926
|
-
if (subMatch) return subMatch[1];
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Pattern 5: const server = app.listen( → return "app"
|
|
930
|
-
const match5 = content.match(
|
|
931
|
-
/(?:const|let|var)\s+(\w+)\s*=\s*(\w+)\.listen\s*\(/,
|
|
932
|
-
);
|
|
933
|
-
if (match5) return match5[2];
|
|
934
|
-
|
|
935
|
-
// Pattern 6: app.listen( or server.listen(
|
|
936
|
-
const match6 = content.match(
|
|
937
|
-
/\b(app|server|router|handler|httpServer)\s*\.listen\s*\(/,
|
|
938
|
-
);
|
|
939
|
-
if (match6) return match6[1];
|
|
940
|
-
|
|
941
|
-
return "app";
|
|
942
|
-
} catch {
|
|
943
|
-
return "app";
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function detectDotenv(cwd, pkg, entryPoint) {
|
|
948
|
-
// Check if dotenv is in dependencies
|
|
949
|
-
const deps = {
|
|
950
|
-
...(pkg.dependencies || {}),
|
|
951
|
-
...(pkg.devDependencies || {}),
|
|
952
|
-
};
|
|
953
|
-
const hasDotenvPackage = !!deps["dotenv"];
|
|
954
|
-
|
|
955
|
-
// Check if dotenv is actually being called in entry file
|
|
956
|
-
let hasDotenvCall = false;
|
|
957
|
-
if (entryPoint && fs.existsSync(entryPoint)) {
|
|
958
|
-
try {
|
|
959
|
-
const content = fs.readFileSync(entryPoint, "utf8");
|
|
960
|
-
hasDotenvCall =
|
|
961
|
-
content.includes("dotenv") ||
|
|
962
|
-
content.includes("require('dotenv')") ||
|
|
963
|
-
content.includes('require("dotenv")');
|
|
964
|
-
} catch {}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
return hasDotenvPackage || hasDotenvCall;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// ─── MAIN DETECT FUNCTION ────────────────────────────────────────────────────
|
|
971
|
-
|
|
972
|
-
function detect(cwd) {
|
|
973
|
-
const pkg = readPackageJson(cwd);
|
|
974
|
-
const monorepo = detectMonorepo(cwd);
|
|
975
|
-
let framework = detectFramework(pkg);
|
|
976
|
-
|
|
977
|
-
// ── If framework not found in root, scan ALL package.json files ───────────
|
|
978
|
-
let backendDir = cwd;
|
|
979
|
-
let frontendDir = null;
|
|
980
|
-
let frontendPkg = null;
|
|
981
|
-
|
|
982
|
-
if (!framework.name) {
|
|
983
|
-
const allPackages = scanAllPackageJsons(cwd);
|
|
984
|
-
|
|
985
|
-
for (const { dir, pkg: subPkg } of allPackages) {
|
|
986
|
-
// Skip the root package.json — already checked
|
|
987
|
-
if (dir === cwd) continue;
|
|
988
|
-
|
|
989
|
-
const classification = classifyPackageJson(subPkg);
|
|
990
|
-
|
|
991
|
-
// AFTER
|
|
992
|
-
if (
|
|
993
|
-
(classification === "backend" || classification === "fullstack") &&
|
|
994
|
-
!framework.name
|
|
995
|
-
) {
|
|
996
|
-
framework = detectFramework(subPkg);
|
|
997
|
-
backendDir = dir;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
if (
|
|
1001
|
-
(classification === "frontend" || classification === "fullstack") &&
|
|
1002
|
-
!frontendDir &&
|
|
1003
|
-
dir !== backendDir
|
|
1004
|
-
) {
|
|
1005
|
-
frontendDir = dir;
|
|
1006
|
-
frontendPkg = subPkg;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
} else {
|
|
1010
|
-
// Framework found in root — scan for separate frontend folder
|
|
1011
|
-
const allPackages = scanAllPackageJsons(cwd);
|
|
1012
|
-
for (const { dir, pkg: subPkg } of allPackages) {
|
|
1013
|
-
if (dir === cwd) continue;
|
|
1014
|
-
if (dir === backendDir) continue;
|
|
1015
|
-
const classification = classifyPackageJson(subPkg);
|
|
1016
|
-
if (classification === "frontend" || classification === "fullstack") {
|
|
1017
|
-
frontendDir = dir;
|
|
1018
|
-
frontendPkg = subPkg;
|
|
1019
|
-
break;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// ── Use backendDir for all backend-specific detection ─────────────────────
|
|
1025
|
-
// Guard: if frontendDir ended up being the same as backendDir
|
|
1026
|
-
// (e.g. a fullstack Next.js folder detected as both), clear frontendDir
|
|
1027
|
-
// so we don't try to inject script tag into the wrong place
|
|
1028
|
-
if (frontendDir && frontendDir === backendDir) {
|
|
1029
|
-
frontendDir = null;
|
|
1030
|
-
frontendPkg = null;
|
|
1031
|
-
}
|
|
1032
|
-
const backendPkg = readPackageJson(backendDir) || pkg;
|
|
1033
|
-
const moduleSystem = detectModuleSystem(backendPkg);
|
|
1034
|
-
const isTypeScript = detectTypeScript(backendDir);
|
|
1035
|
-
const hasSrc = detectSrcDir(backendDir);
|
|
1036
|
-
const generateTs = shouldGenerateTs(backendDir, isTypeScript);
|
|
1037
|
-
|
|
1038
|
-
// ── Find frontend main file ───────────────────────────────────────────────
|
|
1039
|
-
let frontendMainFile = null;
|
|
1040
|
-
if (frontendDir && frontendPkg) {
|
|
1041
|
-
frontendMainFile = findMainFrontendFile(frontendDir, frontendPkg);
|
|
1042
|
-
} else if (framework.name === "next") {
|
|
1043
|
-
frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
|
|
1044
|
-
} else if (framework.name === "express" && !frontendDir) {
|
|
1045
|
-
// Express + frontend in same root folder (e.g. public/index.html)
|
|
1046
|
-
frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
const packageManager =
|
|
1050
|
-
detectPackageManager(backendDir) !== "npm"
|
|
1051
|
-
? detectPackageManager(backendDir)
|
|
1052
|
-
: detectPackageManager(cwd);
|
|
1053
|
-
|
|
1054
|
-
const result = {
|
|
1055
|
-
cwd: backendDir, // use backend dir as working dir
|
|
1056
|
-
rootCwd: cwd, // keep original root for reference
|
|
1057
|
-
pkg: backendPkg,
|
|
1058
|
-
monorepo,
|
|
1059
|
-
framework,
|
|
1060
|
-
moduleSystem,
|
|
1061
|
-
isTypeScript,
|
|
1062
|
-
generateTs,
|
|
1063
|
-
hasSrc,
|
|
1064
|
-
packageManager,
|
|
1065
|
-
ext: generateTs ? ".ts" : ".js",
|
|
1066
|
-
// Frontend info
|
|
1067
|
-
frontendDir,
|
|
1068
|
-
frontendPkg,
|
|
1069
|
-
frontendMainFile,
|
|
1070
|
-
};
|
|
1071
|
-
|
|
1072
|
-
// ── Framework-specific detection ──────────────────────────────────────────
|
|
1073
|
-
if (framework.name === "next") {
|
|
1074
|
-
result.next = detectNextRouter(backendDir);
|
|
1075
|
-
result.nextVersion = detectNextVersion(backendPkg);
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
if (framework.name === "express") {
|
|
1079
|
-
result.entryPoint = detectExpressEntry(backendDir, backendPkg);
|
|
1080
|
-
result.hasDotenv = detectDotenv(backendDir, backendPkg, result.entryPoint);
|
|
1081
|
-
if (result.entryPoint) {
|
|
1082
|
-
result.appVarName = detectAppVarName(result.entryPoint); // detect FIRST
|
|
1083
|
-
result.listenCall = findListenCall(result.entryPoint, result.appVarName);
|
|
1084
|
-
result.listenInsideCallback = findListenInsideCallback(
|
|
1085
|
-
result.entryPoint,
|
|
1086
|
-
result.appVarName,
|
|
1087
|
-
);
|
|
1088
|
-
result.moduleExportsApp = findModuleExportsApp(result.entryPoint);
|
|
1089
|
-
result.createServer = findCreateServer(result.entryPoint);
|
|
1090
|
-
|
|
1091
|
-
const appFileCandidates = [
|
|
1092
|
-
"src/app.js",
|
|
1093
|
-
"src/app.ts",
|
|
1094
|
-
"app.js",
|
|
1095
|
-
"app.ts",
|
|
1096
|
-
];
|
|
1097
|
-
for (const candidate of appFileCandidates) {
|
|
1098
|
-
const fullPath = path.join(backendDir, candidate);
|
|
1099
|
-
if (fs.existsSync(fullPath) && fullPath !== result.entryPoint) {
|
|
1100
|
-
const exportCall = findModuleExportsApp(fullPath);
|
|
1101
|
-
if (exportCall) {
|
|
1102
|
-
result.appFile = fullPath;
|
|
1103
|
-
result.appFileExport = exportCall;
|
|
1104
|
-
break;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// ── Already initialized check ─────────────────────────────────────────────
|
|
1112
|
-
result.alreadyInitialized =
|
|
1113
|
-
detectExistingBotVersion(result.entryPoint) ||
|
|
1114
|
-
(framework.name === "next" &&
|
|
1115
|
-
(fs.existsSync(path.join(backendDir, "instrumentation.js")) ||
|
|
1116
|
-
fs.existsSync(path.join(backendDir, "instrumentation.ts")) ||
|
|
1117
|
-
fs.existsSync(path.join(backendDir, "src", "instrumentation.js")) ||
|
|
1118
|
-
fs.existsSync(path.join(backendDir, "src", "instrumentation.ts"))));
|
|
1119
|
-
|
|
1120
|
-
return result;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
module.exports = {
|
|
1124
|
-
detect,
|
|
1125
|
-
readPackageJson,
|
|
1126
|
-
scanAllPackageJsons,
|
|
1127
|
-
classifyPackageJson,
|
|
1128
|
-
detectFrontendFramework,
|
|
1129
|
-
findMainFrontendFile,
|
|
1130
|
-
detectMonorepo,
|
|
1131
|
-
detectFramework,
|
|
1132
|
-
detectModuleSystem,
|
|
1133
|
-
detectTypeScript,
|
|
1134
|
-
readTsConfig,
|
|
1135
|
-
shouldGenerateTs,
|
|
1136
|
-
detectSrcDir,
|
|
1137
|
-
detectNextRouter,
|
|
1138
|
-
detectPackageManager,
|
|
1139
|
-
detectExistingBotVersion,
|
|
1140
|
-
findFileWithContent,
|
|
1141
|
-
findListenCall,
|
|
1142
|
-
findModuleExportsApp,
|
|
1143
|
-
findListenInsideCallback,
|
|
1144
|
-
findCreateServer,
|
|
1145
|
-
detectAppVarName,
|
|
1146
|
-
detectDotenv,
|
|
1147
|
-
parseEntryFromConfigFiles,
|
|
1148
|
-
scoreExpressFile,
|
|
1149
|
-
};
|