codesight 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -2
- package/dist/ast/extract-components.d.ts +12 -0
- package/dist/ast/extract-components.js +180 -0
- package/dist/ast/extract-routes.d.ts +13 -0
- package/dist/ast/extract-routes.js +271 -0
- package/dist/ast/extract-schema.d.ts +15 -0
- package/dist/ast/extract-schema.js +302 -0
- package/dist/ast/loader.d.ts +20 -0
- package/dist/ast/loader.js +105 -0
- package/dist/detectors/components.js +11 -0
- package/dist/detectors/routes.js +107 -26
- package/dist/detectors/schema.js +20 -0
- package/dist/index.js +12 -2
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
### Your AI assistant wastes thousands of tokens every conversation just figuring out your project. codesight fixes that in one command.
|
|
4
4
|
|
|
5
|
-
**Zero dependencies. 25+ framework detectors. 8 ORM parsers. 8 MCP tools. Blast radius analysis. One `npx` call.**
|
|
5
|
+
**Zero dependencies. AST precision. 25+ framework detectors. 8 ORM parsers. 8 MCP tools. Blast radius analysis. One `npx` call.**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/codesight)
|
|
8
8
|
[](https://www.npmjs.com/package/codesight)
|
|
@@ -56,12 +56,70 @@ Every AI coding conversation starts the same way. Your assistant reads files, gr
|
|
|
56
56
|
|
|
57
57
|
codesight scans your codebase once and generates a structured context map. Routes, database schema, components, dependencies, environment variables, middleware, all condensed into ~3,000 to 5,000 tokens of structured markdown. Your AI reads one file and knows the entire project.
|
|
58
58
|
|
|
59
|
+
**v1.3.0: AST-level precision.** When TypeScript is available in your project, codesight uses the TypeScript compiler API for structural parsing instead of regex. This gives exact route paths, proper controller prefix combining (NestJS), accurate tRPC procedure nesting, precise Drizzle field types, and React prop extraction from type annotations. Zero new dependencies — borrows TypeScript from your project's node_modules. Falls back to regex when TypeScript is not available.
|
|
60
|
+
|
|
59
61
|
```
|
|
60
62
|
Output size: ~3,200 tokens
|
|
61
63
|
Exploration cost: ~52,000 tokens (without codesight)
|
|
62
64
|
Saved: ~48,800 tokens per conversation
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
## How It Works
|
|
68
|
+
|
|
69
|
+
```mermaid
|
|
70
|
+
flowchart LR
|
|
71
|
+
A["Your Codebase"] --> B["codesight"]
|
|
72
|
+
B --> C["AST Parser"]
|
|
73
|
+
B --> D["Regex Fallback"]
|
|
74
|
+
C --> E["Structured Context Map"]
|
|
75
|
+
D --> E
|
|
76
|
+
E --> F["CLAUDE.md"]
|
|
77
|
+
E --> G[".cursorrules"]
|
|
78
|
+
E --> H["codex.md"]
|
|
79
|
+
E --> I["MCP Server"]
|
|
80
|
+
|
|
81
|
+
style B fill:#f59e0b,stroke:#d97706,color:#000
|
|
82
|
+
style C fill:#10b981,stroke:#059669,color:#000
|
|
83
|
+
style E fill:#3b82f6,stroke:#2563eb,color:#fff
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```mermaid
|
|
87
|
+
flowchart TD
|
|
88
|
+
subgraph Detectors["8 Parallel Detectors"]
|
|
89
|
+
R["Routes<br/>25+ frameworks"]
|
|
90
|
+
S["Schema<br/>8 ORMs"]
|
|
91
|
+
CP["Components<br/>React/Vue/Svelte"]
|
|
92
|
+
G["Dep Graph<br/>Import analysis"]
|
|
93
|
+
M["Middleware<br/>Auth/CORS/etc"]
|
|
94
|
+
CF["Config<br/>Env vars"]
|
|
95
|
+
L["Libraries<br/>Exports + sigs"]
|
|
96
|
+
CT["Contracts<br/>Params + types"]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
A["File Scanner"] --> Detectors
|
|
100
|
+
Detectors --> O["~3K-5K tokens<br/>vs ~50K-70K exploration"]
|
|
101
|
+
|
|
102
|
+
style O fill:#10b981,stroke:#059669,color:#000
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```mermaid
|
|
106
|
+
flowchart LR
|
|
107
|
+
subgraph Without["Without codesight"]
|
|
108
|
+
W1["AI reads files"] --> W2["AI greps patterns"]
|
|
109
|
+
W2 --> W3["AI opens configs"]
|
|
110
|
+
W3 --> W4["AI explores deps"]
|
|
111
|
+
W4 --> W5["50,000+ tokens burned"]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
subgraph With["With codesight"]
|
|
115
|
+
C1["AI reads CODESIGHT.md"] --> C2["Full project context"]
|
|
116
|
+
C2 --> C3["~3,000 tokens"]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
style W5 fill:#ef4444,stroke:#dc2626,color:#fff
|
|
120
|
+
style C3 fill:#10b981,stroke:#059669,color:#000
|
|
121
|
+
```
|
|
122
|
+
|
|
65
123
|
## What It Generates
|
|
66
124
|
|
|
67
125
|
```
|
|
@@ -77,6 +135,41 @@ Saved: ~48,800 tokens per conversation
|
|
|
77
135
|
report.html Interactive visual dashboard (with --html or --open)
|
|
78
136
|
```
|
|
79
137
|
|
|
138
|
+
## AST Precision
|
|
139
|
+
|
|
140
|
+
When TypeScript is installed in the project being scanned, codesight uses the actual TypeScript compiler API to parse your code structurally. No regex guessing.
|
|
141
|
+
|
|
142
|
+
| What AST enables | Regex alone |
|
|
143
|
+
|---|---|
|
|
144
|
+
| Follows `router.use('/prefix', subRouter)` chains | Misses nested routers |
|
|
145
|
+
| Combines `@Controller('users')` + `@Get(':id')` into `/users/:id` | May miss prefix |
|
|
146
|
+
| Parses `router({ users: userRouter })` tRPC nesting | Line-by-line matching |
|
|
147
|
+
| Extracts exact Drizzle field types from `.primaryKey().notNull()` chains | Pattern matching |
|
|
148
|
+
| Gets React props from TypeScript interfaces and destructuring | Regex on `{ prop }` |
|
|
149
|
+
| Detects middleware in route chains: `app.get('/path', auth, handler)` | Not captured |
|
|
150
|
+
|
|
151
|
+
```mermaid
|
|
152
|
+
flowchart TD
|
|
153
|
+
F["Source File"] --> Check{"TypeScript<br/>in node_modules?"}
|
|
154
|
+
Check -->|Yes| AST["AST Parse<br/>(TypeScript Compiler API)"]
|
|
155
|
+
Check -->|No| Regex["Regex Parse<br/>(Pattern Matching)"]
|
|
156
|
+
AST --> Result["Routes / Schema / Components"]
|
|
157
|
+
AST -->|"Parse failed"| Regex
|
|
158
|
+
Regex --> Result
|
|
159
|
+
|
|
160
|
+
style AST fill:#10b981,stroke:#059669,color:#000
|
|
161
|
+
style Regex fill:#f59e0b,stroke:#d97706,color:#000
|
|
162
|
+
style Result fill:#3b82f6,stroke:#2563eb,color:#fff
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
AST detection is indicated in the output:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
Analyzing... done (AST: 65 routes, 18 models, 16 components)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
No configuration needed. If TypeScript is in your `node_modules`, AST kicks in automatically. Works with npm, yarn, and pnpm (including strict mode). Falls back to regex for non-TypeScript projects or frameworks without AST support.
|
|
172
|
+
|
|
80
173
|
## Routes
|
|
81
174
|
|
|
82
175
|
Not just paths. Methods, URL parameters, what each route touches (auth, database, cache, payments, AI, email, queues), and where the handler lives. Detects routes across 25+ frameworks automatically.
|
|
@@ -121,6 +214,25 @@ The files imported the most are the ones that break the most things when changed
|
|
|
121
214
|
|
|
122
215
|
## Blast Radius
|
|
123
216
|
|
|
217
|
+
```mermaid
|
|
218
|
+
graph TD
|
|
219
|
+
DB["src/lib/db.ts<br/>(you change this)"] --> U["src/routes/users.ts"]
|
|
220
|
+
DB --> P["src/routes/projects.ts"]
|
|
221
|
+
DB --> B["src/routes/billing.ts"]
|
|
222
|
+
DB --> A["src/routes/auth.ts"]
|
|
223
|
+
U --> MW["src/middleware/auth.ts"]
|
|
224
|
+
P --> MW
|
|
225
|
+
B --> S["src/services/stripe.ts"]
|
|
226
|
+
|
|
227
|
+
style DB fill:#ef4444,stroke:#dc2626,color:#fff
|
|
228
|
+
style U fill:#f59e0b,stroke:#d97706,color:#000
|
|
229
|
+
style P fill:#f59e0b,stroke:#d97706,color:#000
|
|
230
|
+
style B fill:#f59e0b,stroke:#d97706,color:#000
|
|
231
|
+
style A fill:#f59e0b,stroke:#d97706,color:#000
|
|
232
|
+
style MW fill:#fbbf24,stroke:#f59e0b,color:#000
|
|
233
|
+
style S fill:#fbbf24,stroke:#f59e0b,color:#000
|
|
234
|
+
```
|
|
235
|
+
|
|
124
236
|
See exactly what breaks if you change a file. BFS through the import graph finds all transitively affected files, routes, models, and middleware.
|
|
125
237
|
|
|
126
238
|
```bash
|
|
@@ -234,6 +346,21 @@ Runs as a Model Context Protocol server. Claude Code and Cursor call it directly
|
|
|
234
346
|
}
|
|
235
347
|
```
|
|
236
348
|
|
|
349
|
+
```mermaid
|
|
350
|
+
flowchart LR
|
|
351
|
+
AI["Claude Code<br/>or Cursor"] <-->|"JSON-RPC 2.0<br/>over stdio"| MCP["codesight<br/>MCP Server"]
|
|
352
|
+
MCP --> Cache["Session Cache<br/>(scan once)"]
|
|
353
|
+
MCP --> T1["get_summary"]
|
|
354
|
+
MCP --> T2["get_routes"]
|
|
355
|
+
MCP --> T3["get_schema"]
|
|
356
|
+
MCP --> T4["get_blast_radius"]
|
|
357
|
+
MCP --> T5["get_env"]
|
|
358
|
+
MCP --> T6["get_hot_files"]
|
|
359
|
+
|
|
360
|
+
style MCP fill:#f59e0b,stroke:#d97706,color:#000
|
|
361
|
+
style Cache fill:#10b981,stroke:#059669,color:#000
|
|
362
|
+
```
|
|
363
|
+
|
|
237
364
|
Exposes 8 specialized tools, each returning only what your AI needs:
|
|
238
365
|
|
|
239
366
|
| Tool | What it does |
|
|
@@ -332,6 +459,7 @@ Most AI context tools dump your entire codebase into one file. codesight takes a
|
|
|
332
459
|
|
|
333
460
|
| | codesight | File concatenation tools | AST-based tools |
|
|
334
461
|
|---|---|---|---|
|
|
462
|
+
| **Parsing** | AST when available, regex fallback | None | Tree-sitter / custom |
|
|
335
463
|
| **Output** | Structured routes, schema, components, deps | Raw file contents | Call graphs, class diagrams |
|
|
336
464
|
| **Token cost** | ~3,000-5,000 tokens | 50,000-500,000+ tokens | Varies |
|
|
337
465
|
| **Route detection** | 25+ frameworks auto-detected | None | Limited |
|
|
@@ -340,7 +468,7 @@ Most AI context tools dump your entire codebase into one file. codesight takes a
|
|
|
340
468
|
| **AI tool profiles** | 5 tools (Claude, Cursor, Codex, Copilot, Windsurf) | None | None |
|
|
341
469
|
| **MCP server** | 8 specialized tools with session caching | None | Some |
|
|
342
470
|
| **Setup** | `npx codesight` (zero deps, zero config) | Copy/paste | Install compilers, runtimes |
|
|
343
|
-
| **Dependencies** | Zero | Varies | Tree-sitter, SQLite, etc. |
|
|
471
|
+
| **Dependencies** | Zero (borrows TS from your project) | Varies | Tree-sitter, SQLite, etc. |
|
|
344
472
|
|
|
345
473
|
## Contributing
|
|
346
474
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based component extraction for React.
|
|
3
|
+
* Provides higher accuracy than regex for:
|
|
4
|
+
* - Component name detection from function/arrow function declarations
|
|
5
|
+
* - Prop extraction from destructured parameters and Props interface/type
|
|
6
|
+
* - Distinguishes client/server components via directive detection
|
|
7
|
+
*/
|
|
8
|
+
import type { ComponentInfo } from "../types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Extract React components from a file using AST.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractReactComponentsAST(ts: any, filePath: string, content: string, relPath: string): ComponentInfo[];
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { parseSourceFile, getText } from "./loader.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract React components from a file using AST.
|
|
4
|
+
*/
|
|
5
|
+
export function extractReactComponentsAST(ts, filePath, content, relPath) {
|
|
6
|
+
try {
|
|
7
|
+
const sf = parseSourceFile(ts, filePath, content);
|
|
8
|
+
return extractComponents(ts, sf, content, relPath);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function extractComponents(ts, sf, content, relPath) {
|
|
15
|
+
const components = [];
|
|
16
|
+
const SK = ts.SyntaxKind;
|
|
17
|
+
const isClient = content.slice(0, 80).includes("use client");
|
|
18
|
+
const isServer = content.slice(0, 80).includes("use server");
|
|
19
|
+
// Collect all Props interfaces/types in the file
|
|
20
|
+
const propsTypes = new Map(); // type name -> prop names
|
|
21
|
+
function collectPropsTypes(node) {
|
|
22
|
+
// interface FooProps { ... } or type FooProps = { ... }
|
|
23
|
+
if (node.kind === SK.InterfaceDeclaration || node.kind === SK.TypeAliasDeclaration) {
|
|
24
|
+
const name = node.name ? getText(sf, node.name) : "";
|
|
25
|
+
if (!name.includes("Props") && !name.includes("props")) {
|
|
26
|
+
ts.forEachChild(node, collectPropsTypes);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const props = [];
|
|
30
|
+
// For interfaces: node.members
|
|
31
|
+
if (node.members) {
|
|
32
|
+
for (const member of node.members) {
|
|
33
|
+
if (member.kind === SK.PropertySignature && member.name) {
|
|
34
|
+
const propName = getText(sf, member.name);
|
|
35
|
+
if (propName !== "children")
|
|
36
|
+
props.push(propName);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// For type aliases: node.type might be TypeLiteral
|
|
41
|
+
if (node.type?.kind === SK.TypeLiteral && node.type.members) {
|
|
42
|
+
for (const member of node.type.members) {
|
|
43
|
+
if (member.kind === SK.PropertySignature && member.name) {
|
|
44
|
+
const propName = getText(sf, member.name);
|
|
45
|
+
if (propName !== "children")
|
|
46
|
+
props.push(propName);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (props.length > 0)
|
|
51
|
+
propsTypes.set(name, props);
|
|
52
|
+
}
|
|
53
|
+
ts.forEachChild(node, collectPropsTypes);
|
|
54
|
+
}
|
|
55
|
+
collectPropsTypes(sf);
|
|
56
|
+
// Find exported functions/consts that start with uppercase (components)
|
|
57
|
+
function findComponents(node) {
|
|
58
|
+
// export function ComponentName(...) or export default function ComponentName(...)
|
|
59
|
+
if (node.kind === SK.FunctionDeclaration) {
|
|
60
|
+
const name = node.name ? getText(sf, node.name) : "";
|
|
61
|
+
if (!name || !/^[A-Z]/.test(name)) {
|
|
62
|
+
ts.forEachChild(node, findComponents);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Check if exported
|
|
66
|
+
const isExported = node.modifiers?.some((m) => m.kind === SK.ExportKeyword || m.kind === SK.DefaultKeyword);
|
|
67
|
+
if (!isExported) {
|
|
68
|
+
ts.forEachChild(node, findComponents);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const props = extractPropsFromParams(ts, sf, node.parameters, propsTypes);
|
|
72
|
+
components.push({
|
|
73
|
+
name,
|
|
74
|
+
file: relPath,
|
|
75
|
+
props: props.slice(0, 10),
|
|
76
|
+
isClient,
|
|
77
|
+
isServer,
|
|
78
|
+
confidence: "ast",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// export const ComponentName = (...) => { ... }
|
|
82
|
+
if (node.kind === SK.VariableStatement) {
|
|
83
|
+
const isExported = node.modifiers?.some((m) => m.kind === SK.ExportKeyword);
|
|
84
|
+
if (!isExported) {
|
|
85
|
+
ts.forEachChild(node, findComponents);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const decl of node.declarationList?.declarations || []) {
|
|
89
|
+
if (decl.kind !== SK.VariableDeclaration)
|
|
90
|
+
continue;
|
|
91
|
+
const name = decl.name ? getText(sf, decl.name) : "";
|
|
92
|
+
if (!name || !/^[A-Z]/.test(name))
|
|
93
|
+
continue;
|
|
94
|
+
// Check if the initializer is an arrow function or function expression
|
|
95
|
+
const init = decl.initializer;
|
|
96
|
+
if (!init)
|
|
97
|
+
continue;
|
|
98
|
+
let params = null;
|
|
99
|
+
if (init.kind === SK.ArrowFunction || init.kind === SK.FunctionExpression) {
|
|
100
|
+
params = init.parameters;
|
|
101
|
+
}
|
|
102
|
+
// React.forwardRef((...) => { ... })
|
|
103
|
+
if (init.kind === SK.CallExpression) {
|
|
104
|
+
const callee = init.expression;
|
|
105
|
+
const calleeName = callee?.kind === SK.PropertyAccessExpression
|
|
106
|
+
? getText(sf, callee.name)
|
|
107
|
+
: callee?.kind === SK.Identifier ? getText(sf, callee) : "";
|
|
108
|
+
if (calleeName === "forwardRef" || calleeName === "memo") {
|
|
109
|
+
const innerFn = init.arguments?.[0];
|
|
110
|
+
if (innerFn && (innerFn.kind === SK.ArrowFunction || innerFn.kind === SK.FunctionExpression)) {
|
|
111
|
+
params = innerFn.parameters;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (params) {
|
|
116
|
+
const props = extractPropsFromParams(ts, sf, params, propsTypes);
|
|
117
|
+
components.push({
|
|
118
|
+
name,
|
|
119
|
+
file: relPath,
|
|
120
|
+
props: props.slice(0, 10),
|
|
121
|
+
isClient,
|
|
122
|
+
isServer,
|
|
123
|
+
confidence: "ast",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
ts.forEachChild(node, findComponents);
|
|
129
|
+
}
|
|
130
|
+
findComponents(sf);
|
|
131
|
+
return components;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract prop names from function parameters.
|
|
135
|
+
* Handles: ({ prop1, prop2 }: Props) and (props: Props)
|
|
136
|
+
*/
|
|
137
|
+
function extractPropsFromParams(ts, sf, params, propsTypes) {
|
|
138
|
+
if (!params || params.length === 0)
|
|
139
|
+
return [];
|
|
140
|
+
const SK = ts.SyntaxKind;
|
|
141
|
+
const firstParam = params[0];
|
|
142
|
+
// Destructured: ({ prop1, prop2, ...rest }: Props)
|
|
143
|
+
if (firstParam.name?.kind === SK.ObjectBindingPattern) {
|
|
144
|
+
const props = [];
|
|
145
|
+
for (const element of firstParam.name.elements || []) {
|
|
146
|
+
if (element.dotDotDotToken)
|
|
147
|
+
continue; // skip ...rest
|
|
148
|
+
const propName = element.name ? getText(sf, element.name) : "";
|
|
149
|
+
if (propName && propName !== "children")
|
|
150
|
+
props.push(propName);
|
|
151
|
+
}
|
|
152
|
+
// If we got props from destructuring, great
|
|
153
|
+
if (props.length > 0)
|
|
154
|
+
return props;
|
|
155
|
+
// Fall back to type annotation if destructuring is empty
|
|
156
|
+
}
|
|
157
|
+
// Type annotation: (props: FooProps) or ({ ... }: FooProps)
|
|
158
|
+
if (firstParam.type) {
|
|
159
|
+
// TypeReference: FooProps
|
|
160
|
+
if (firstParam.type.kind === SK.TypeReference) {
|
|
161
|
+
const typeName = firstParam.type.typeName ? getText(sf, firstParam.type.typeName) : "";
|
|
162
|
+
const typeProps = propsTypes.get(typeName);
|
|
163
|
+
if (typeProps)
|
|
164
|
+
return typeProps;
|
|
165
|
+
}
|
|
166
|
+
// TypeLiteral: { prop1: string; prop2: number }
|
|
167
|
+
if (firstParam.type.kind === SK.TypeLiteral) {
|
|
168
|
+
const props = [];
|
|
169
|
+
for (const member of firstParam.type.members || []) {
|
|
170
|
+
if (member.kind === SK.PropertySignature && member.name) {
|
|
171
|
+
const propName = getText(sf, member.name);
|
|
172
|
+
if (propName !== "children")
|
|
173
|
+
props.push(propName);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return props;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based route extraction for TypeScript/JavaScript frameworks.
|
|
3
|
+
* Provides higher accuracy than regex for:
|
|
4
|
+
* - Express/Hono/Fastify/Koa/Elysia: method calls with path strings
|
|
5
|
+
* - NestJS: decorator-based routes with controller prefix combining
|
|
6
|
+
* - tRPC: router object with procedure chains and nesting
|
|
7
|
+
*/
|
|
8
|
+
import type { RouteInfo, Framework } from "../types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Try AST-based route extraction for a single file.
|
|
11
|
+
* Returns routes with confidence: 'ast', or empty array if AST cannot handle this file.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractRoutesAST(ts: any, filePath: string, content: string, framework: Framework, tags: string[]): RouteInfo[];
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { parseSourceFile, getDecorators, parseDecorator, getText } from "./loader.js";
|
|
2
|
+
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
3
|
+
function extractPathParams(path) {
|
|
4
|
+
const params = [];
|
|
5
|
+
const regex = /[:{}](\w+)/g;
|
|
6
|
+
let m;
|
|
7
|
+
while ((m = regex.exec(path)) !== null)
|
|
8
|
+
params.push(m[1]);
|
|
9
|
+
return params;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Try AST-based route extraction for a single file.
|
|
13
|
+
* Returns routes with confidence: 'ast', or empty array if AST cannot handle this file.
|
|
14
|
+
*/
|
|
15
|
+
export function extractRoutesAST(ts, filePath, content, framework, tags) {
|
|
16
|
+
try {
|
|
17
|
+
const sf = parseSourceFile(ts, filePath, content);
|
|
18
|
+
switch (framework) {
|
|
19
|
+
case "express":
|
|
20
|
+
case "hono":
|
|
21
|
+
case "fastify":
|
|
22
|
+
case "koa":
|
|
23
|
+
case "elysia":
|
|
24
|
+
return extractHttpFrameworkRoutes(ts, sf, filePath, content, framework, tags);
|
|
25
|
+
case "nestjs":
|
|
26
|
+
return extractNestJSRoutes(ts, sf, filePath, content, tags);
|
|
27
|
+
case "trpc":
|
|
28
|
+
return extractTRPCRoutes(ts, sf, filePath, content, tags);
|
|
29
|
+
default:
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return []; // AST parsing failed — caller falls back to regex
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ─── Express / Hono / Fastify / Koa / Elysia ───
|
|
38
|
+
function extractHttpFrameworkRoutes(ts, sf, filePath, _content, framework, tags) {
|
|
39
|
+
const routes = [];
|
|
40
|
+
const SK = ts.SyntaxKind;
|
|
41
|
+
// Track router.use('/prefix', subRouter) for prefix resolution
|
|
42
|
+
const prefixMap = new Map(); // variable name -> prefix
|
|
43
|
+
function visit(node) {
|
|
44
|
+
if (node.kind === SK.CallExpression) {
|
|
45
|
+
const expr = node.expression;
|
|
46
|
+
if (expr?.kind === SK.PropertyAccessExpression) {
|
|
47
|
+
const methodName = getText(sf, expr.name).toLowerCase();
|
|
48
|
+
const receiverName = expr.expression?.kind === SK.Identifier
|
|
49
|
+
? getText(sf, expr.expression)
|
|
50
|
+
: "";
|
|
51
|
+
// Track .use('/prefix', variable) for prefix chains
|
|
52
|
+
if (methodName === "use" && node.arguments?.length >= 2) {
|
|
53
|
+
const first = node.arguments[0];
|
|
54
|
+
const second = node.arguments[1];
|
|
55
|
+
if ((first.kind === SK.StringLiteral || first.kind === SK.NoSubstitutionTemplateLiteral) &&
|
|
56
|
+
second.kind === SK.Identifier) {
|
|
57
|
+
const prefix = first.text;
|
|
58
|
+
const routerVar = getText(sf, second);
|
|
59
|
+
prefixMap.set(routerVar, prefix);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Route registration: .get('/path', ...) .post('/path', ...) etc.
|
|
63
|
+
if (HTTP_METHODS.has(methodName) && node.arguments?.length > 0) {
|
|
64
|
+
const pathArg = node.arguments[0];
|
|
65
|
+
let path = null;
|
|
66
|
+
if (pathArg.kind === SK.StringLiteral || pathArg.kind === SK.NoSubstitutionTemplateLiteral) {
|
|
67
|
+
path = pathArg.text;
|
|
68
|
+
}
|
|
69
|
+
if (path !== null) {
|
|
70
|
+
// Filter: route paths must start with / or : — skip context.get("key") calls
|
|
71
|
+
if (!path.startsWith("/") && !path.startsWith(":")) {
|
|
72
|
+
ts.forEachChild(node, visit);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Apply prefix if this receiver has one registered
|
|
76
|
+
const prefix = prefixMap.get(receiverName) || "";
|
|
77
|
+
const fullPath = prefix ? (prefix + path).replace(/\/\//g, "/") : path;
|
|
78
|
+
// Extract middleware names from intermediate arguments
|
|
79
|
+
const middleware = [];
|
|
80
|
+
for (let i = 1; i < node.arguments.length; i++) {
|
|
81
|
+
const arg = node.arguments[i];
|
|
82
|
+
if (arg.kind === SK.Identifier) {
|
|
83
|
+
middleware.push(getText(sf, arg));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
routes.push({
|
|
87
|
+
method: methodName.toUpperCase() === "ALL" ? "ALL" : methodName.toUpperCase(),
|
|
88
|
+
path: fullPath,
|
|
89
|
+
file: filePath,
|
|
90
|
+
tags,
|
|
91
|
+
framework,
|
|
92
|
+
params: extractPathParams(fullPath),
|
|
93
|
+
confidence: "ast",
|
|
94
|
+
middleware,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ts.forEachChild(node, visit);
|
|
101
|
+
}
|
|
102
|
+
visit(sf);
|
|
103
|
+
return routes;
|
|
104
|
+
}
|
|
105
|
+
// ─── NestJS ───
|
|
106
|
+
const NEST_METHOD_MAP = {
|
|
107
|
+
Get: "GET",
|
|
108
|
+
Post: "POST",
|
|
109
|
+
Put: "PUT",
|
|
110
|
+
Patch: "PATCH",
|
|
111
|
+
Delete: "DELETE",
|
|
112
|
+
Options: "OPTIONS",
|
|
113
|
+
Head: "HEAD",
|
|
114
|
+
All: "ALL",
|
|
115
|
+
};
|
|
116
|
+
function extractNestJSRoutes(ts, sf, filePath, _content, tags) {
|
|
117
|
+
const routes = [];
|
|
118
|
+
const SK = ts.SyntaxKind;
|
|
119
|
+
function visitNode(node) {
|
|
120
|
+
if (node.kind === SK.ClassDeclaration) {
|
|
121
|
+
const decorators = getDecorators(ts, node);
|
|
122
|
+
// Find @Controller decorator and extract prefix
|
|
123
|
+
let controllerPrefix = "";
|
|
124
|
+
let isController = false;
|
|
125
|
+
for (const dec of decorators) {
|
|
126
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
127
|
+
if (parsed.name === "Controller") {
|
|
128
|
+
isController = true;
|
|
129
|
+
controllerPrefix = parsed.arg || "";
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!isController) {
|
|
134
|
+
ts.forEachChild(node, visitNode);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Extract guards at class level
|
|
138
|
+
const classGuards = [];
|
|
139
|
+
for (const dec of decorators) {
|
|
140
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
141
|
+
if (parsed.name === "UseGuards" && parsed.arg) {
|
|
142
|
+
classGuards.push(parsed.arg);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Visit methods
|
|
146
|
+
for (const member of node.members || []) {
|
|
147
|
+
if (member.kind !== SK.MethodDeclaration)
|
|
148
|
+
continue;
|
|
149
|
+
const methodDecorators = getDecorators(ts, member);
|
|
150
|
+
for (const dec of methodDecorators) {
|
|
151
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
152
|
+
if (!parsed.name || !NEST_METHOD_MAP[parsed.name])
|
|
153
|
+
continue;
|
|
154
|
+
const methodPath = parsed.arg || "";
|
|
155
|
+
const combined = [controllerPrefix, methodPath].filter(Boolean).join("/");
|
|
156
|
+
const fullPath = "/" + combined.replace(/^\/+/, "").replace(/\/+/g, "/");
|
|
157
|
+
const normalizedPath = fullPath.replace(/\/$/, "") || "/";
|
|
158
|
+
// Extract @Param, @Body, @Query from method parameters
|
|
159
|
+
const params = [];
|
|
160
|
+
const middleware = [...classGuards];
|
|
161
|
+
for (const param of member.parameters || []) {
|
|
162
|
+
const paramDecs = getDecorators(ts, param);
|
|
163
|
+
for (const pd of paramDecs) {
|
|
164
|
+
const pp = parseDecorator(ts, sf, pd);
|
|
165
|
+
if (pp.name === "Param" && pp.arg)
|
|
166
|
+
params.push(pp.arg);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Method-level guards
|
|
170
|
+
for (const mdec of methodDecorators) {
|
|
171
|
+
const mp = parseDecorator(ts, sf, mdec);
|
|
172
|
+
if (mp.name === "UseGuards" && mp.arg)
|
|
173
|
+
middleware.push(mp.arg);
|
|
174
|
+
}
|
|
175
|
+
routes.push({
|
|
176
|
+
method: NEST_METHOD_MAP[parsed.name],
|
|
177
|
+
path: normalizedPath,
|
|
178
|
+
file: filePath,
|
|
179
|
+
tags,
|
|
180
|
+
framework: "nestjs",
|
|
181
|
+
params: params.length > 0 ? params : extractPathParams(normalizedPath),
|
|
182
|
+
confidence: "ast",
|
|
183
|
+
middleware: middleware.length > 0 ? middleware : undefined,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
ts.forEachChild(node, visitNode);
|
|
189
|
+
}
|
|
190
|
+
visitNode(sf);
|
|
191
|
+
return routes;
|
|
192
|
+
}
|
|
193
|
+
// ─── tRPC ───
|
|
194
|
+
function extractTRPCRoutes(ts, sf, filePath, _content, tags) {
|
|
195
|
+
const routes = [];
|
|
196
|
+
const SK = ts.SyntaxKind;
|
|
197
|
+
function isRouterCall(node) {
|
|
198
|
+
if (node.kind !== SK.CallExpression)
|
|
199
|
+
return false;
|
|
200
|
+
const callee = node.expression;
|
|
201
|
+
if (callee.kind === SK.Identifier) {
|
|
202
|
+
const name = getText(sf, callee);
|
|
203
|
+
return name === "router" || name === "createTRPCRouter";
|
|
204
|
+
}
|
|
205
|
+
if (callee.kind === SK.PropertyAccessExpression) {
|
|
206
|
+
return getText(sf, callee.name) === "router";
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function findProcedureMethod(node) {
|
|
211
|
+
if (!node || node.kind !== SK.CallExpression)
|
|
212
|
+
return null;
|
|
213
|
+
const expr = node.expression;
|
|
214
|
+
if (expr?.kind === SK.PropertyAccessExpression) {
|
|
215
|
+
const name = getText(sf, expr.name);
|
|
216
|
+
if (name === "query")
|
|
217
|
+
return "QUERY";
|
|
218
|
+
if (name === "mutation")
|
|
219
|
+
return "MUTATION";
|
|
220
|
+
if (name === "subscription")
|
|
221
|
+
return "SUBSCRIPTION";
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function extractFromRouter(node, prefix) {
|
|
226
|
+
if (!isRouterCall(node) || !node.arguments?.length)
|
|
227
|
+
return;
|
|
228
|
+
const arg = node.arguments[0];
|
|
229
|
+
if (arg.kind !== SK.ObjectLiteralExpression)
|
|
230
|
+
return;
|
|
231
|
+
for (const prop of arg.properties || []) {
|
|
232
|
+
if (prop.kind === SK.PropertyAssignment) {
|
|
233
|
+
const name = prop.name ? getText(sf, prop.name) : "";
|
|
234
|
+
if (!name)
|
|
235
|
+
continue;
|
|
236
|
+
const init = prop.initializer;
|
|
237
|
+
// Nested router
|
|
238
|
+
if (isRouterCall(init)) {
|
|
239
|
+
extractFromRouter(init, prefix ? `${prefix}.${name}` : name);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Procedure: look for .query() / .mutation() / .subscription()
|
|
243
|
+
const method = findProcedureMethod(init);
|
|
244
|
+
if (method) {
|
|
245
|
+
routes.push({
|
|
246
|
+
method,
|
|
247
|
+
path: prefix ? `${prefix}.${name}` : name,
|
|
248
|
+
file: filePath,
|
|
249
|
+
tags,
|
|
250
|
+
framework: "trpc",
|
|
251
|
+
confidence: "ast",
|
|
252
|
+
});
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Could be a reference to another router variable — can't resolve without types
|
|
256
|
+
}
|
|
257
|
+
if (prop.kind === SK.SpreadAssignment) {
|
|
258
|
+
// ...otherRoutes — can't resolve statically
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Find all router() calls in the file
|
|
263
|
+
function visit(node) {
|
|
264
|
+
if (isRouterCall(node)) {
|
|
265
|
+
extractFromRouter(node, "");
|
|
266
|
+
}
|
|
267
|
+
ts.forEachChild(node, visit);
|
|
268
|
+
}
|
|
269
|
+
visit(sf);
|
|
270
|
+
return routes;
|
|
271
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based schema extraction for TypeScript/JavaScript ORMs.
|
|
3
|
+
* Provides higher accuracy than regex for:
|
|
4
|
+
* - Drizzle: pgTable/mysqlTable/sqliteTable with field types and chained modifiers
|
|
5
|
+
* - TypeORM: @Entity + @Column/@PrimaryGeneratedColumn decorators
|
|
6
|
+
*/
|
|
7
|
+
import type { SchemaModel } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Extract Drizzle schema from a file using AST.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractDrizzleSchemaAST(ts: any, filePath: string, content: string): SchemaModel[];
|
|
12
|
+
/**
|
|
13
|
+
* Extract TypeORM entities from a file using AST.
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractTypeORMSchemaAST(ts: any, filePath: string, content: string): SchemaModel[];
|