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.
@@ -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,2 @@
1
+ import type { DependencyGraph, ProjectInfo } from "../types.js";
2
+ export declare function detectDependencyGraph(files: string[], project: ProjectInfo): Promise<DependencyGraph>;
@@ -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,2 @@
1
+ import type { LibExport, ProjectInfo } from "../types.js";
2
+ export declare function detectLibs(files: string[], project: ProjectInfo): Promise<LibExport[]>;
@@ -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,2 @@
1
+ import type { MiddlewareInfo, ProjectInfo } from "../types.js";
2
+ export declare function detectMiddleware(files: string[], project: ProjectInfo): Promise<MiddlewareInfo[]>;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { RouteInfo, ProjectInfo } from "../types.js";
2
+ export declare function detectRoutes(files: string[], project: ProjectInfo): Promise<RouteInfo[]>;