codesight 1.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/README.md +213 -0
- package/dist/detectors/components.d.ts +2 -0
- package/dist/detectors/components.js +237 -0
- package/dist/detectors/config.d.ts +2 -0
- package/dist/detectors/config.js +142 -0
- package/dist/detectors/contracts.d.ts +6 -0
- package/dist/detectors/contracts.js +118 -0
- package/dist/detectors/graph.d.ts +2 -0
- package/dist/detectors/graph.js +113 -0
- package/dist/detectors/libs.d.ts +2 -0
- package/dist/detectors/libs.js +206 -0
- package/dist/detectors/middleware.d.ts +2 -0
- package/dist/detectors/middleware.js +116 -0
- package/dist/detectors/routes.d.ts +2 -0
- package/dist/detectors/routes.js +356 -0
- package/dist/detectors/schema.d.ts +2 -0
- package/dist/detectors/schema.js +283 -0
- package/dist/detectors/tokens.d.ts +6 -0
- package/dist/detectors/tokens.js +48 -0
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.js +268 -0
- package/dist/generators/ai-config.d.ts +2 -0
- package/dist/generators/ai-config.js +137 -0
- package/dist/generators/html-report.d.ts +2 -0
- package/dist/generators/html-report.js +200 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +304 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +171 -0
- package/dist/scanner.d.ts +4 -0
- package/dist/scanner.js +329 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.js +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFileSafe } from "../scanner.js";
|
|
3
|
+
/**
|
|
4
|
+
* Enhances route info with request/response type information
|
|
5
|
+
* by scanning the route handler files for type annotations
|
|
6
|
+
*/
|
|
7
|
+
export async function enrichRouteContracts(routes, project) {
|
|
8
|
+
// Group routes by file to avoid re-reading
|
|
9
|
+
const fileCache = new Map();
|
|
10
|
+
for (const route of routes) {
|
|
11
|
+
const absPath = join(project.root, route.file);
|
|
12
|
+
let content = fileCache.get(route.file);
|
|
13
|
+
if (!content) {
|
|
14
|
+
content = await readFileSafe(absPath);
|
|
15
|
+
fileCache.set(route.file, content);
|
|
16
|
+
}
|
|
17
|
+
// Extract URL params from path like :id, [id], {id}
|
|
18
|
+
const params = [];
|
|
19
|
+
const paramPatterns = [
|
|
20
|
+
/:(\w+)/g, // Express/Hono style :param
|
|
21
|
+
/\[(\w+)\]/g, // Next.js style [param]
|
|
22
|
+
/\{(\w+)\}/g, // FastAPI/Django style {param}
|
|
23
|
+
/<(\w+)>/g, // Flask style <param>
|
|
24
|
+
];
|
|
25
|
+
for (const pattern of paramPatterns) {
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = pattern.exec(route.path)) !== null) {
|
|
28
|
+
params.push(match[1]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (params.length > 0)
|
|
32
|
+
route.params = params;
|
|
33
|
+
// Try to extract response type based on framework
|
|
34
|
+
switch (route.framework) {
|
|
35
|
+
case "hono":
|
|
36
|
+
case "express":
|
|
37
|
+
case "fastify":
|
|
38
|
+
case "koa":
|
|
39
|
+
enrichTSRoute(route, content);
|
|
40
|
+
break;
|
|
41
|
+
case "next-app":
|
|
42
|
+
enrichNextRoute(route, content);
|
|
43
|
+
break;
|
|
44
|
+
case "fastapi":
|
|
45
|
+
enrichFastAPIRoute(route, content);
|
|
46
|
+
break;
|
|
47
|
+
case "flask":
|
|
48
|
+
enrichFlaskRoute(route, content);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return routes;
|
|
53
|
+
}
|
|
54
|
+
function enrichTSRoute(route, content) {
|
|
55
|
+
// Look for c.json<Type>(...) or res.json({...}) patterns near the route method
|
|
56
|
+
// Hono: return c.json<ResponseType>(data)
|
|
57
|
+
const jsonTypeMatch = content.match(/c\.json\s*<\s*([^>]+)\s*>/);
|
|
58
|
+
if (jsonTypeMatch) {
|
|
59
|
+
route.responseType = jsonTypeMatch[1].trim();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Look for zod validation schemas: .input(z.object({...})) or validate(schema)
|
|
63
|
+
const zodInputMatch = content.match(/zValidator\s*\(\s*['"](?:json|form)['"],\s*(\w+)/);
|
|
64
|
+
if (zodInputMatch) {
|
|
65
|
+
route.requestType = zodInputMatch[1];
|
|
66
|
+
}
|
|
67
|
+
// Look for explicit return type annotations on handler
|
|
68
|
+
const handlerReturnMatch = content.match(/:\s*Promise\s*<\s*Response\s*<\s*([^>]+)\s*>\s*>/);
|
|
69
|
+
if (handlerReturnMatch) {
|
|
70
|
+
route.responseType = handlerReturnMatch[1].trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function enrichNextRoute(route, content) {
|
|
74
|
+
// NextResponse.json({ ... }) or Response.json({ ... })
|
|
75
|
+
const responseMatch = content.match(/(?:NextResponse|Response)\.json\s*\(\s*\{([^}]{1,200})\}/);
|
|
76
|
+
if (responseMatch) {
|
|
77
|
+
// Extract key names from the response object
|
|
78
|
+
const keys = responseMatch[1]
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((s) => s.trim().split(/[:\s]/)[0])
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
if (keys.length > 0 && keys.length <= 8) {
|
|
83
|
+
route.responseType = `{ ${keys.join(", ")} }`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function enrichFastAPIRoute(route, content) {
|
|
88
|
+
// @app.get("/path", response_model=SchemaName)
|
|
89
|
+
const responseModelMatch = content.match(new RegExp(`response_model\\s*=\\s*(\\w+)`));
|
|
90
|
+
if (responseModelMatch) {
|
|
91
|
+
route.responseType = responseModelMatch[1];
|
|
92
|
+
}
|
|
93
|
+
// Find the handler function after the decorator and check for Pydantic param types
|
|
94
|
+
// def handler(item: ItemCreate, db: Session = Depends(...))
|
|
95
|
+
const funcPattern = new RegExp(`@\\w+\\.${route.method.toLowerCase()}\\s*\\([^)]*\\)\\s*\\n\\s*(?:async\\s+)?def\\s+\\w+\\s*\\(([^)]+)\\)`);
|
|
96
|
+
const funcMatch = content.match(funcPattern);
|
|
97
|
+
if (funcMatch) {
|
|
98
|
+
const params = funcMatch[1];
|
|
99
|
+
// Find non-dependency params with type hints (skip Depends, Query, etc.)
|
|
100
|
+
const bodyParam = params.match(/(\w+)\s*:\s*(\w+)(?!\s*=\s*(?:Depends|Query|Path|Header))/);
|
|
101
|
+
if (bodyParam && !["Session", "Request", "Response", "str", "int", "float", "bool"].includes(bodyParam[2])) {
|
|
102
|
+
route.requestType = bodyParam[2];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function enrichFlaskRoute(route, content) {
|
|
107
|
+
// Look for jsonify({ ... }) or return {"key": ...}
|
|
108
|
+
const jsonifyMatch = content.match(/jsonify\s*\(\s*\{([^}]{1,200})\}/);
|
|
109
|
+
if (jsonifyMatch) {
|
|
110
|
+
const keys = jsonifyMatch[1]
|
|
111
|
+
.split(",")
|
|
112
|
+
.map((s) => s.trim().split(/['":\s]/)[0].replace(/['"]/g, ""))
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
if (keys.length > 0 && keys.length <= 8) {
|
|
115
|
+
route.responseType = `{ ${keys.join(", ")} }`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { relative, dirname, resolve, extname } from "node:path";
|
|
2
|
+
import { readFileSafe } from "../scanner.js";
|
|
3
|
+
export async function detectDependencyGraph(files, project) {
|
|
4
|
+
const edges = [];
|
|
5
|
+
const importCount = new Map();
|
|
6
|
+
const codeFiles = files.filter((f) => f.match(/\.(ts|tsx|js|jsx|mjs|py|go)$/));
|
|
7
|
+
for (const file of codeFiles) {
|
|
8
|
+
const content = await readFileSafe(file);
|
|
9
|
+
if (!content)
|
|
10
|
+
continue;
|
|
11
|
+
const rel = relative(project.root, file);
|
|
12
|
+
const ext = extname(file);
|
|
13
|
+
if (ext === ".py") {
|
|
14
|
+
extractPythonImports(content, rel, edges, importCount);
|
|
15
|
+
}
|
|
16
|
+
else if (ext === ".go") {
|
|
17
|
+
extractGoImports(content, rel, edges, importCount);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
extractTSImports(content, rel, file, project, files, edges, importCount);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Sort by most imported
|
|
24
|
+
const hotFiles = Array.from(importCount.entries())
|
|
25
|
+
.map(([file, count]) => ({ file, importedBy: count }))
|
|
26
|
+
.sort((a, b) => b.importedBy - a.importedBy)
|
|
27
|
+
.slice(0, 20);
|
|
28
|
+
return { edges, hotFiles };
|
|
29
|
+
}
|
|
30
|
+
function extractTSImports(content, rel, absPath, project, allFiles, edges, importCount) {
|
|
31
|
+
// Match: import ... from "./path" or import("./path") or require("./path")
|
|
32
|
+
const patterns = [
|
|
33
|
+
/(?:import|export)\s+.*?from\s+['"]([^'"]+)['"]/g,
|
|
34
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
35
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
36
|
+
];
|
|
37
|
+
for (const pattern of patterns) {
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
40
|
+
const importPath = match[1];
|
|
41
|
+
// Only track local imports (starting with . or @/ alias)
|
|
42
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("@/") && !importPath.startsWith("~/"))
|
|
43
|
+
continue;
|
|
44
|
+
// Resolve to relative path
|
|
45
|
+
let resolvedPath;
|
|
46
|
+
if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
|
|
47
|
+
resolvedPath = importPath.replace(/^[@~]\//, "src/");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const dir = dirname(absPath);
|
|
51
|
+
resolvedPath = relative(project.root, resolve(dir, importPath));
|
|
52
|
+
}
|
|
53
|
+
// Strip extension and try to find the actual file
|
|
54
|
+
const normalized = normalizeImportPath(resolvedPath, allFiles, project.root);
|
|
55
|
+
if (normalized && normalized !== rel) {
|
|
56
|
+
edges.push({ from: rel, to: normalized });
|
|
57
|
+
importCount.set(normalized, (importCount.get(normalized) || 0) + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function extractPythonImports(content, rel, edges, importCount) {
|
|
63
|
+
// from .module import something or from ..package.module import something
|
|
64
|
+
const fromPattern = /^from\s+(\.+\w[\w.]*)\s+import/gm;
|
|
65
|
+
let match;
|
|
66
|
+
while ((match = fromPattern.exec(content)) !== null) {
|
|
67
|
+
const target = match[1].replace(/\./g, "/") + ".py";
|
|
68
|
+
edges.push({ from: rel, to: target });
|
|
69
|
+
importCount.set(target, (importCount.get(target) || 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function extractGoImports(content, rel, edges, importCount) {
|
|
73
|
+
// Go doesn't have relative imports in the same way, but we can track internal package imports
|
|
74
|
+
const importBlock = content.match(/import\s*\(([\s\S]*?)\)/);
|
|
75
|
+
if (!importBlock)
|
|
76
|
+
return;
|
|
77
|
+
// Look for internal package paths (not standard library)
|
|
78
|
+
const lines = importBlock[1].split("\n");
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const pathMatch = line.match(/["']([^"']+)["']/);
|
|
81
|
+
if (pathMatch && pathMatch[1].includes("/") && !pathMatch[1].startsWith("github.com") && !pathMatch[1].includes(".")) {
|
|
82
|
+
const target = pathMatch[1];
|
|
83
|
+
edges.push({ from: rel, to: target });
|
|
84
|
+
importCount.set(target, (importCount.get(target) || 0) + 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function normalizeImportPath(importPath, allFiles, root) {
|
|
89
|
+
// Try exact match first
|
|
90
|
+
for (const file of allFiles) {
|
|
91
|
+
const rel = relative(root, file);
|
|
92
|
+
if (rel === importPath)
|
|
93
|
+
return rel;
|
|
94
|
+
}
|
|
95
|
+
// Try with extensions
|
|
96
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
|
|
97
|
+
for (const ext of extensions) {
|
|
98
|
+
for (const file of allFiles) {
|
|
99
|
+
const rel = relative(root, file);
|
|
100
|
+
if (rel === importPath + ext)
|
|
101
|
+
return rel;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Try index files
|
|
105
|
+
for (const ext of extensions) {
|
|
106
|
+
for (const file of allFiles) {
|
|
107
|
+
const rel = relative(root, file);
|
|
108
|
+
if (rel === importPath + "/index" + ext)
|
|
109
|
+
return rel;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { relative, extname } from "node:path";
|
|
2
|
+
import { readFileSafe } from "../scanner.js";
|
|
3
|
+
const SKIP_DIRS = [
|
|
4
|
+
"/components/",
|
|
5
|
+
"/pages/",
|
|
6
|
+
"/app/",
|
|
7
|
+
"/routes/",
|
|
8
|
+
"/views/",
|
|
9
|
+
"/templates/",
|
|
10
|
+
"/__tests__/",
|
|
11
|
+
"/__mocks__/",
|
|
12
|
+
"/test/",
|
|
13
|
+
"/tests/",
|
|
14
|
+
"/stories/",
|
|
15
|
+
];
|
|
16
|
+
export async function detectLibs(files, project) {
|
|
17
|
+
const libFiles = files.filter((f) => {
|
|
18
|
+
const ext = extname(f);
|
|
19
|
+
if (![".ts", ".js", ".mjs", ".py", ".go"].includes(ext))
|
|
20
|
+
return false;
|
|
21
|
+
if (f.endsWith(".test.ts") || f.endsWith(".spec.ts"))
|
|
22
|
+
return false;
|
|
23
|
+
if (f.endsWith(".test.js") || f.endsWith(".spec.js"))
|
|
24
|
+
return false;
|
|
25
|
+
if (f.endsWith(".d.ts"))
|
|
26
|
+
return false;
|
|
27
|
+
if (f.endsWith("_test.py") || f.endsWith("_test.go"))
|
|
28
|
+
return false;
|
|
29
|
+
// Skip component/page/route files
|
|
30
|
+
if (f.endsWith(".tsx") || f.endsWith(".jsx"))
|
|
31
|
+
return false;
|
|
32
|
+
if (SKIP_DIRS.some((d) => f.includes(d)))
|
|
33
|
+
return false;
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
const libs = [];
|
|
37
|
+
for (const file of libFiles) {
|
|
38
|
+
const content = await readFileSafe(file);
|
|
39
|
+
if (!content)
|
|
40
|
+
continue;
|
|
41
|
+
const rel = relative(project.root, file);
|
|
42
|
+
const ext = extname(file);
|
|
43
|
+
let exports;
|
|
44
|
+
if (ext === ".py") {
|
|
45
|
+
exports = extractPythonExports(content);
|
|
46
|
+
}
|
|
47
|
+
else if (ext === ".go") {
|
|
48
|
+
exports = extractGoExports(content);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
exports = extractTSExports(content);
|
|
52
|
+
}
|
|
53
|
+
// Only include files with at least one function/class export
|
|
54
|
+
const hasMeaningful = exports.some((e) => e.kind === "function" || e.kind === "class");
|
|
55
|
+
if (hasMeaningful && exports.length > 0) {
|
|
56
|
+
libs.push({ file: rel, exports });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return libs;
|
|
60
|
+
}
|
|
61
|
+
function extractTSExports(content) {
|
|
62
|
+
const exports = [];
|
|
63
|
+
// export function name(params): returnType
|
|
64
|
+
const fnPattern = /export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{]+))?/g;
|
|
65
|
+
let match;
|
|
66
|
+
while ((match = fnPattern.exec(content)) !== null) {
|
|
67
|
+
const params = compactParams(match[2]);
|
|
68
|
+
const ret = match[3]?.trim() || "void";
|
|
69
|
+
exports.push({
|
|
70
|
+
name: match[1],
|
|
71
|
+
kind: "function",
|
|
72
|
+
signature: `(${params}) => ${ret}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// export const name = (...) => or export const name = function
|
|
76
|
+
const constFnPattern = /export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*(?::\s*[^=]+)?\s*=>/g;
|
|
77
|
+
while ((match = constFnPattern.exec(content)) !== null) {
|
|
78
|
+
exports.push({
|
|
79
|
+
name: match[1],
|
|
80
|
+
kind: "function",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// export class Name
|
|
84
|
+
const classPattern = /export\s+(?:abstract\s+)?class\s+(\w+)/g;
|
|
85
|
+
while ((match = classPattern.exec(content)) !== null) {
|
|
86
|
+
exports.push({ name: match[1], kind: "class" });
|
|
87
|
+
}
|
|
88
|
+
// export interface Name
|
|
89
|
+
const ifacePattern = /export\s+interface\s+(\w+)/g;
|
|
90
|
+
while ((match = ifacePattern.exec(content)) !== null) {
|
|
91
|
+
exports.push({ name: match[1], kind: "interface" });
|
|
92
|
+
}
|
|
93
|
+
// export type Name
|
|
94
|
+
const typePattern = /export\s+type\s+(\w+)/g;
|
|
95
|
+
while ((match = typePattern.exec(content)) !== null) {
|
|
96
|
+
exports.push({ name: match[1], kind: "type" });
|
|
97
|
+
}
|
|
98
|
+
// export enum Name
|
|
99
|
+
const enumPattern = /export\s+(?:const\s+)?enum\s+(\w+)/g;
|
|
100
|
+
while ((match = enumPattern.exec(content)) !== null) {
|
|
101
|
+
exports.push({ name: match[1], kind: "enum" });
|
|
102
|
+
}
|
|
103
|
+
// export const Name (non-function)
|
|
104
|
+
const constPattern = /export\s+const\s+(\w+)\s*(?::\s*([^=\n]+))?\s*=/g;
|
|
105
|
+
while ((match = constPattern.exec(content)) !== null) {
|
|
106
|
+
// Skip if already captured as a function
|
|
107
|
+
if (exports.some((e) => e.name === match[1]))
|
|
108
|
+
continue;
|
|
109
|
+
const type = match[2]?.trim();
|
|
110
|
+
exports.push({
|
|
111
|
+
name: match[1],
|
|
112
|
+
kind: "const",
|
|
113
|
+
signature: type || undefined,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return exports;
|
|
117
|
+
}
|
|
118
|
+
function extractPythonExports(content) {
|
|
119
|
+
const exports = [];
|
|
120
|
+
// def function_name(params) -> return_type:
|
|
121
|
+
const fnPattern = /^def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^\n:]+))?:/gm;
|
|
122
|
+
let match;
|
|
123
|
+
while ((match = fnPattern.exec(content)) !== null) {
|
|
124
|
+
if (match[1].startsWith("_"))
|
|
125
|
+
continue; // skip private
|
|
126
|
+
const params = compactParams(match[2]);
|
|
127
|
+
const ret = match[3]?.trim() || "";
|
|
128
|
+
exports.push({
|
|
129
|
+
name: match[1],
|
|
130
|
+
kind: "function",
|
|
131
|
+
signature: ret ? `(${params}) -> ${ret}` : `(${params})`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// async def
|
|
135
|
+
const asyncFnPattern = /^async\s+def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^\n:]+))?:/gm;
|
|
136
|
+
while ((match = asyncFnPattern.exec(content)) !== null) {
|
|
137
|
+
if (match[1].startsWith("_"))
|
|
138
|
+
continue;
|
|
139
|
+
const params = compactParams(match[2]);
|
|
140
|
+
const ret = match[3]?.trim() || "";
|
|
141
|
+
exports.push({
|
|
142
|
+
name: match[1],
|
|
143
|
+
kind: "function",
|
|
144
|
+
signature: ret ? `(${params}) -> ${ret}` : `(${params})`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// class ClassName:
|
|
148
|
+
const classPattern = /^class\s+(\w+)/gm;
|
|
149
|
+
while ((match = classPattern.exec(content)) !== null) {
|
|
150
|
+
if (match[1].startsWith("_"))
|
|
151
|
+
continue;
|
|
152
|
+
exports.push({ name: match[1], kind: "class" });
|
|
153
|
+
}
|
|
154
|
+
return exports;
|
|
155
|
+
}
|
|
156
|
+
function extractGoExports(content) {
|
|
157
|
+
const exports = [];
|
|
158
|
+
// func FunctionName(params) returnType
|
|
159
|
+
const fnPattern = /^func\s+(\w+)\s*\(([^)]*)\)\s*([^\n{]*)/gm;
|
|
160
|
+
let match;
|
|
161
|
+
while ((match = fnPattern.exec(content)) !== null) {
|
|
162
|
+
// Go exports start with uppercase
|
|
163
|
+
if (match[1][0] !== match[1][0].toUpperCase())
|
|
164
|
+
continue;
|
|
165
|
+
const params = compactParams(match[2]);
|
|
166
|
+
const ret = match[3]?.trim() || "";
|
|
167
|
+
exports.push({
|
|
168
|
+
name: match[1],
|
|
169
|
+
kind: "function",
|
|
170
|
+
signature: `(${params}) ${ret}`.trim(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// type StructName struct
|
|
174
|
+
const structPattern = /^type\s+(\w+)\s+struct/gm;
|
|
175
|
+
while ((match = structPattern.exec(content)) !== null) {
|
|
176
|
+
if (match[1][0] !== match[1][0].toUpperCase())
|
|
177
|
+
continue;
|
|
178
|
+
exports.push({ name: match[1], kind: "class" });
|
|
179
|
+
}
|
|
180
|
+
// type InterfaceName interface
|
|
181
|
+
const ifacePattern = /^type\s+(\w+)\s+interface/gm;
|
|
182
|
+
while ((match = ifacePattern.exec(content)) !== null) {
|
|
183
|
+
if (match[1][0] !== match[1][0].toUpperCase())
|
|
184
|
+
continue;
|
|
185
|
+
exports.push({ name: match[1], kind: "interface" });
|
|
186
|
+
}
|
|
187
|
+
return exports;
|
|
188
|
+
}
|
|
189
|
+
function compactParams(params) {
|
|
190
|
+
if (!params.trim())
|
|
191
|
+
return "";
|
|
192
|
+
// Remove type annotations for compactness, keep param names
|
|
193
|
+
return params
|
|
194
|
+
.split(",")
|
|
195
|
+
.map((p) => {
|
|
196
|
+
const trimmed = p.trim();
|
|
197
|
+
// For destructured params, keep the whole thing compact
|
|
198
|
+
if (trimmed.startsWith("{"))
|
|
199
|
+
return "{...}";
|
|
200
|
+
// Get just the name
|
|
201
|
+
const name = trimmed.split(/[=:]/)[0].trim();
|
|
202
|
+
return name;
|
|
203
|
+
})
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.join(", ");
|
|
206
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { relative, basename } from "node:path";
|
|
2
|
+
import { readFileSafe } from "../scanner.js";
|
|
3
|
+
const MIDDLEWARE_PATTERNS = [
|
|
4
|
+
[
|
|
5
|
+
"auth",
|
|
6
|
+
[
|
|
7
|
+
/auth/i,
|
|
8
|
+
/jwt/i,
|
|
9
|
+
/bearer/i,
|
|
10
|
+
/passport/i,
|
|
11
|
+
/clerk/i,
|
|
12
|
+
/better-?auth/i,
|
|
13
|
+
/session/i,
|
|
14
|
+
/requireAuth/i,
|
|
15
|
+
/isAuthenticated/i,
|
|
16
|
+
/verifyToken/i,
|
|
17
|
+
/protect/i,
|
|
18
|
+
],
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"rate-limit",
|
|
22
|
+
[
|
|
23
|
+
/rate.?limit/i,
|
|
24
|
+
/throttle/i,
|
|
25
|
+
/rateLimit/i,
|
|
26
|
+
/rateLimiter/i,
|
|
27
|
+
/slowDown/i,
|
|
28
|
+
],
|
|
29
|
+
],
|
|
30
|
+
["cors", [/cors/i, /cross.?origin/i, /Access-Control/i]],
|
|
31
|
+
[
|
|
32
|
+
"validation",
|
|
33
|
+
[
|
|
34
|
+
/zod/i,
|
|
35
|
+
/joi/i,
|
|
36
|
+
/yup/i,
|
|
37
|
+
/validator/i,
|
|
38
|
+
/validate/i,
|
|
39
|
+
/pydantic/i,
|
|
40
|
+
/valibot/i,
|
|
41
|
+
],
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"logging",
|
|
45
|
+
[
|
|
46
|
+
/logger/i,
|
|
47
|
+
/morgan/i,
|
|
48
|
+
/pino/i,
|
|
49
|
+
/winston/i,
|
|
50
|
+
/requestLogger/i,
|
|
51
|
+
/httpLogger/i,
|
|
52
|
+
],
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"error-handler",
|
|
56
|
+
[
|
|
57
|
+
/errorHandler/i,
|
|
58
|
+
/error.?middleware/i,
|
|
59
|
+
/onError/i,
|
|
60
|
+
/exception.?handler/i,
|
|
61
|
+
],
|
|
62
|
+
],
|
|
63
|
+
];
|
|
64
|
+
function classifyMiddleware(name, content) {
|
|
65
|
+
const combined = name + " " + content.slice(0, 500);
|
|
66
|
+
for (const [type, patterns] of MIDDLEWARE_PATTERNS) {
|
|
67
|
+
if (patterns.some((p) => p.test(combined))) {
|
|
68
|
+
return type;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return "custom";
|
|
72
|
+
}
|
|
73
|
+
export async function detectMiddleware(files, project) {
|
|
74
|
+
const middleware = [];
|
|
75
|
+
// Look for middleware files
|
|
76
|
+
const middlewareFiles = files.filter((f) => f.includes("middleware") ||
|
|
77
|
+
f.includes("guard") ||
|
|
78
|
+
f.includes("interceptor") ||
|
|
79
|
+
basename(f).startsWith("auth") ||
|
|
80
|
+
basename(f).includes("rate") ||
|
|
81
|
+
basename(f).includes("cors"));
|
|
82
|
+
for (const file of middlewareFiles) {
|
|
83
|
+
const content = await readFileSafe(file);
|
|
84
|
+
if (!content)
|
|
85
|
+
continue;
|
|
86
|
+
const rel = relative(project.root, file);
|
|
87
|
+
const name = basename(file).replace(/\.[^.]+$/, "");
|
|
88
|
+
middleware.push({
|
|
89
|
+
name,
|
|
90
|
+
file: rel,
|
|
91
|
+
type: classifyMiddleware(name, content),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Scan for inline middleware usage in route files
|
|
95
|
+
const routeFiles = files.filter((f) => (f.match(/\.(ts|js|mjs|py|go)$/) &&
|
|
96
|
+
!f.includes("node_modules") &&
|
|
97
|
+
!middlewareFiles.includes(f)));
|
|
98
|
+
for (const file of routeFiles) {
|
|
99
|
+
const content = await readFileSafe(file);
|
|
100
|
+
const rel = relative(project.root, file);
|
|
101
|
+
// app.use(cors()) or app.use(rateLimit(...))
|
|
102
|
+
const usePattern = /\.use\s*\(\s*(\w+)\s*\(/g;
|
|
103
|
+
let match;
|
|
104
|
+
while ((match = usePattern.exec(content)) !== null) {
|
|
105
|
+
const fnName = match[1];
|
|
106
|
+
const type = classifyMiddleware(fnName, "");
|
|
107
|
+
if (type !== "custom") {
|
|
108
|
+
// Deduplicate
|
|
109
|
+
if (!middleware.some((m) => m.name === fnName)) {
|
|
110
|
+
middleware.push({ name: fnName, file: rel, type });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return middleware;
|
|
116
|
+
}
|