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.
@@ -0,0 +1,302 @@
1
+ import { parseSourceFile, getDecorators, parseDecorator, getText } from "./loader.js";
2
+ const AUDIT_FIELDS = new Set([
3
+ "createdAt", "updatedAt", "deletedAt",
4
+ "created_at", "updated_at", "deleted_at",
5
+ ]);
6
+ /**
7
+ * Extract Drizzle schema from a file using AST.
8
+ */
9
+ export function extractDrizzleSchemaAST(ts, filePath, content) {
10
+ try {
11
+ const sf = parseSourceFile(ts, filePath, content);
12
+ return extractDrizzleTables(ts, sf, content);
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ }
18
+ /**
19
+ * Extract TypeORM entities from a file using AST.
20
+ */
21
+ export function extractTypeORMSchemaAST(ts, filePath, content) {
22
+ try {
23
+ const sf = parseSourceFile(ts, filePath, content);
24
+ return extractTypeORMEntities(ts, sf);
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
30
+ // ─── Drizzle ───
31
+ const DRIZZLE_TABLE_FUNCS = new Set(["pgTable", "mysqlTable", "sqliteTable"]);
32
+ function extractDrizzleTables(ts, sf, _content) {
33
+ const models = [];
34
+ const SK = ts.SyntaxKind;
35
+ function visit(node) {
36
+ // Look for: const xxx = pgTable("name", { ... })
37
+ if (node.kind === SK.CallExpression) {
38
+ const callee = node.expression;
39
+ const funcName = callee?.kind === SK.Identifier ? getText(sf, callee) : "";
40
+ if (DRIZZLE_TABLE_FUNCS.has(funcName) && node.arguments?.length >= 2) {
41
+ const nameArg = node.arguments[0];
42
+ const fieldsArg = node.arguments[1];
43
+ // Table name from first argument
44
+ let tableName = "";
45
+ if (nameArg.kind === SK.StringLiteral || nameArg.kind === SK.NoSubstitutionTemplateLiteral) {
46
+ tableName = nameArg.text;
47
+ }
48
+ if (!tableName) {
49
+ ts.forEachChild(node, visit);
50
+ return;
51
+ }
52
+ // Fields from second argument (ObjectLiteralExpression or arrow returning one)
53
+ let fieldsObj = null;
54
+ if (fieldsArg.kind === SK.ObjectLiteralExpression) {
55
+ fieldsObj = fieldsArg;
56
+ }
57
+ else if (fieldsArg.kind === SK.ArrowFunction || fieldsArg.kind === SK.FunctionExpression) {
58
+ // (t) => ({ ... }) — body might be ParenthesizedExpression containing ObjectLiteralExpression
59
+ const body = fieldsArg.body;
60
+ if (body?.kind === SK.ObjectLiteralExpression) {
61
+ fieldsObj = body;
62
+ }
63
+ else if (body?.kind === SK.ParenthesizedExpression && body.expression?.kind === SK.ObjectLiteralExpression) {
64
+ fieldsObj = body.expression;
65
+ }
66
+ }
67
+ if (!fieldsObj) {
68
+ ts.forEachChild(node, visit);
69
+ return;
70
+ }
71
+ const fields = [];
72
+ const relations = [];
73
+ for (const prop of fieldsObj.properties || []) {
74
+ if (prop.kind !== SK.PropertyAssignment)
75
+ continue;
76
+ const fieldName = prop.name ? getText(sf, prop.name) : "";
77
+ if (!fieldName || AUDIT_FIELDS.has(fieldName))
78
+ continue;
79
+ // Parse the initializer chain: serial("id").primaryKey()
80
+ const { type, flags, refTarget } = parseFieldChain(ts, sf, prop.initializer);
81
+ if (refTarget) {
82
+ relations.push(`${fieldName} -> ${refTarget}`);
83
+ }
84
+ if (fieldName.endsWith("Id") || fieldName.endsWith("_id")) {
85
+ if (!flags.includes("fk"))
86
+ flags.push("fk");
87
+ }
88
+ fields.push({ name: fieldName, type, flags });
89
+ }
90
+ if (fields.length > 0) {
91
+ models.push({ name: tableName, fields, relations, orm: "drizzle", confidence: "ast" });
92
+ }
93
+ }
94
+ }
95
+ ts.forEachChild(node, visit);
96
+ }
97
+ visit(sf);
98
+ // Also extract Drizzle relations() calls
99
+ extractDrizzleRelations(ts, sf, models);
100
+ return models;
101
+ }
102
+ function parseFieldChain(ts, sf, node) {
103
+ const SK = ts.SyntaxKind;
104
+ const flags = [];
105
+ let type = "unknown";
106
+ let refTarget = null;
107
+ // Walk the chain from outermost to innermost call
108
+ // e.g., serial("id").primaryKey().notNull() is:
109
+ // CallExpression(.notNull)
110
+ // expression: PropertyAccessExpression
111
+ // expression: CallExpression(.primaryKey)
112
+ // expression: PropertyAccessExpression
113
+ // expression: CallExpression(serial)
114
+ function walkChain(n) {
115
+ if (!n)
116
+ return;
117
+ if (n.kind === SK.CallExpression) {
118
+ const expr = n.expression;
119
+ if (expr?.kind === SK.PropertyAccessExpression) {
120
+ const methodName = getText(sf, expr.name);
121
+ switch (methodName) {
122
+ case "primaryKey":
123
+ flags.push("pk");
124
+ break;
125
+ case "notNull":
126
+ flags.push("required");
127
+ break;
128
+ case "unique":
129
+ flags.push("unique");
130
+ break;
131
+ case "default":
132
+ case "defaultNow":
133
+ case "$default":
134
+ case "$defaultFn":
135
+ flags.push("default");
136
+ break;
137
+ case "references":
138
+ flags.push("fk");
139
+ // Try to extract reference target: .references(() => users.id)
140
+ if (n.arguments?.length > 0) {
141
+ const refArg = n.arguments[0];
142
+ if (refArg.kind === SK.ArrowFunction || refArg.kind === SK.FunctionExpression) {
143
+ const refBody = refArg.body;
144
+ if (refBody?.kind === SK.PropertyAccessExpression) {
145
+ refTarget = getText(sf, refBody);
146
+ }
147
+ }
148
+ }
149
+ break;
150
+ }
151
+ // Recurse into the receiver
152
+ walkChain(expr.expression);
153
+ }
154
+ else if (expr?.kind === SK.Identifier) {
155
+ // Base function call: serial("id"), text("name"), etc.
156
+ type = getText(sf, expr);
157
+ }
158
+ else if (expr?.kind === SK.PropertyAccessExpression) {
159
+ // Could be t.serial("id") — method on a prefix
160
+ type = getText(sf, expr.name);
161
+ // Walk further for nested chains
162
+ }
163
+ }
164
+ }
165
+ walkChain(node);
166
+ return { type, flags, refTarget };
167
+ }
168
+ function extractDrizzleRelations(ts, sf, models) {
169
+ const SK = ts.SyntaxKind;
170
+ function visit(node) {
171
+ // relations(tableVar, ({ one, many }) => ({ ... }))
172
+ if (node.kind === SK.CallExpression) {
173
+ const callee = node.expression;
174
+ const funcName = callee?.kind === SK.Identifier ? getText(sf, callee) : "";
175
+ if (funcName === "relations" && node.arguments?.length >= 2) {
176
+ const tableArg = node.arguments[0];
177
+ const tableName = tableArg?.kind === SK.Identifier ? getText(sf, tableArg) : "";
178
+ // Find matching model by variable name (approximate match)
179
+ const model = models.find((m) => m.name === tableName ||
180
+ m.name === tableName.replace(/s$/, "") ||
181
+ tableName.startsWith(m.name));
182
+ if (!model) {
183
+ ts.forEachChild(node, visit);
184
+ return;
185
+ }
186
+ const relArg = node.arguments[1];
187
+ // Arrow function body should be an ObjectLiteralExpression
188
+ let relObj = null;
189
+ if (relArg?.kind === SK.ArrowFunction) {
190
+ const body = relArg.body;
191
+ if (body?.kind === SK.ObjectLiteralExpression) {
192
+ relObj = body;
193
+ }
194
+ else if (body?.kind === SK.ParenthesizedExpression && body.expression?.kind === SK.ObjectLiteralExpression) {
195
+ relObj = body.expression;
196
+ }
197
+ }
198
+ if (!relObj) {
199
+ ts.forEachChild(node, visit);
200
+ return;
201
+ }
202
+ for (const prop of relObj.properties || []) {
203
+ if (prop.kind !== SK.PropertyAssignment)
204
+ continue;
205
+ const relName = prop.name ? getText(sf, prop.name) : "";
206
+ if (!relName)
207
+ continue;
208
+ const init = prop.initializer;
209
+ if (init?.kind === SK.CallExpression && init.expression?.kind === SK.Identifier) {
210
+ const relType = getText(sf, init.expression); // "one" or "many"
211
+ const targetArg = init.arguments?.[0];
212
+ const target = targetArg?.kind === SK.Identifier ? getText(sf, targetArg) : "?";
213
+ const existing = model.relations.find((r) => r.startsWith(`${relName}:`));
214
+ if (!existing) {
215
+ model.relations.push(`${relName}: ${relType}(${target})`);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ts.forEachChild(node, visit);
222
+ }
223
+ visit(sf);
224
+ }
225
+ // ─── TypeORM ───
226
+ function extractTypeORMEntities(ts, sf) {
227
+ const models = [];
228
+ const SK = ts.SyntaxKind;
229
+ function visitNode(node) {
230
+ if (node.kind === SK.ClassDeclaration) {
231
+ const decorators = getDecorators(ts, node);
232
+ let isEntity = false;
233
+ let entityName = "";
234
+ for (const dec of decorators) {
235
+ const parsed = parseDecorator(ts, sf, dec);
236
+ if (parsed.name === "Entity") {
237
+ isEntity = true;
238
+ entityName = parsed.arg || "";
239
+ break;
240
+ }
241
+ }
242
+ if (!isEntity) {
243
+ ts.forEachChild(node, visitNode);
244
+ return;
245
+ }
246
+ // Class name as fallback
247
+ const className = node.name ? getText(sf, node.name) : "Unknown";
248
+ const name = entityName || className;
249
+ const fields = [];
250
+ const relations = [];
251
+ for (const member of node.members || []) {
252
+ if (member.kind !== SK.PropertyDeclaration)
253
+ continue;
254
+ const memberDecs = getDecorators(ts, member);
255
+ const memberName = member.name ? getText(sf, member.name) : "";
256
+ if (!memberName || AUDIT_FIELDS.has(memberName))
257
+ continue;
258
+ // Get type annotation
259
+ const memberType = member.type ? getText(sf, member.type) : "unknown";
260
+ for (const dec of memberDecs) {
261
+ const parsed = parseDecorator(ts, sf, dec);
262
+ // Column decorators
263
+ if (parsed.name === "PrimaryGeneratedColumn" || parsed.name === "PrimaryColumn") {
264
+ const flags = ["pk"];
265
+ if (parsed.name === "PrimaryGeneratedColumn")
266
+ flags.push("default");
267
+ fields.push({ name: memberName, type: parsed.arg || memberType, flags });
268
+ break;
269
+ }
270
+ if (parsed.name === "Column" || parsed.name === "CreateDateColumn" || parsed.name === "UpdateDateColumn") {
271
+ const flags = [];
272
+ // Parse column options from decorator argument
273
+ const decExpr = dec.expression;
274
+ if (decExpr?.kind === SK.CallExpression && decExpr.arguments?.length > 0) {
275
+ const optArg = decExpr.arguments[0];
276
+ const optText = getText(sf, optArg);
277
+ if (optText.includes("unique: true") || optText.includes("unique:true"))
278
+ flags.push("unique");
279
+ if (optText.includes("nullable: true") || optText.includes("nullable:true"))
280
+ flags.push("nullable");
281
+ if (optText.includes("default:"))
282
+ flags.push("default");
283
+ }
284
+ fields.push({ name: memberName, type: parsed.arg || memberType, flags });
285
+ break;
286
+ }
287
+ // Relation decorators
288
+ if (["OneToMany", "ManyToOne", "OneToOne", "ManyToMany"].includes(parsed.name)) {
289
+ relations.push(`${memberName}: ${parsed.name}(${memberType})`);
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ if (fields.length > 0) {
295
+ models.push({ name, fields, relations, orm: "typeorm", confidence: "ast" });
296
+ }
297
+ }
298
+ ts.forEachChild(node, visitNode);
299
+ }
300
+ visitNode(sf);
301
+ return models;
302
+ }
@@ -0,0 +1,20 @@
1
+ export declare function loadTypeScript(projectRoot: string): any | null;
2
+ export declare function resetCache(): void;
3
+ export declare function parseSourceFile(ts: any, fileName: string, content: string): any;
4
+ /**
5
+ * Get decorators from a node, handling both TS 4.x (node.decorators)
6
+ * and TS 5.x (node.modifiers with SyntaxKind.Decorator).
7
+ */
8
+ export declare function getDecorators(ts: any, node: any): any[];
9
+ /**
10
+ * Extract the name and first string argument from a decorator.
11
+ * @returns { name: string, arg: string | null }
12
+ */
13
+ export declare function parseDecorator(ts: any, sf: any, decorator: any): {
14
+ name: string;
15
+ arg: string | null;
16
+ };
17
+ /**
18
+ * Get text from a node safely.
19
+ */
20
+ export declare function getText(sf: any, node: any): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Dynamic TypeScript compiler loader.
3
+ * Loads the TypeScript compiler from the scanned project's node_modules.
4
+ * Zero new dependencies — borrows TS from the project being analyzed.
5
+ * Falls back gracefully when TypeScript is not available.
6
+ */
7
+ import { createRequire } from "node:module";
8
+ import { readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ let cached = undefined; // undefined = not tried, null = tried and failed
11
+ let cachedRoot = "";
12
+ export function loadTypeScript(projectRoot) {
13
+ if (cached !== undefined && cachedRoot === projectRoot)
14
+ return cached;
15
+ cachedRoot = projectRoot;
16
+ // Strategy 1: createRequire from project root (works for npm/yarn)
17
+ try {
18
+ const req = createRequire(join(projectRoot, "package.json"));
19
+ cached = req("typescript");
20
+ return cached;
21
+ }
22
+ catch { }
23
+ // Strategy 2: Direct path (works for pnpm with public-hoist-pattern)
24
+ try {
25
+ const directPath = join(projectRoot, "node_modules", "typescript");
26
+ const req = createRequire(join(directPath, "package.json"));
27
+ cached = req(directPath);
28
+ return cached;
29
+ }
30
+ catch { }
31
+ // Strategy 3: Find in pnpm .pnpm store (strict mode fallback)
32
+ try {
33
+ const pnpmDir = join(projectRoot, "node_modules", ".pnpm");
34
+ const entries = readdirSync(pnpmDir);
35
+ const tsDir = entries.find((e) => e.startsWith("typescript@"));
36
+ if (tsDir) {
37
+ const tsPath = join(pnpmDir, tsDir, "node_modules", "typescript");
38
+ const req = createRequire(join(tsPath, "package.json"));
39
+ cached = req(tsPath);
40
+ return cached;
41
+ }
42
+ }
43
+ catch { }
44
+ // TypeScript not available — fall back to regex
45
+ cached = null;
46
+ return null;
47
+ }
48
+ export function resetCache() {
49
+ cached = undefined;
50
+ }
51
+ export function parseSourceFile(ts, fileName, content) {
52
+ return ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true // setParentNodes — needed for walking up the tree
53
+ );
54
+ }
55
+ /**
56
+ * Get decorators from a node, handling both TS 4.x (node.decorators)
57
+ * and TS 5.x (node.modifiers with SyntaxKind.Decorator).
58
+ */
59
+ export function getDecorators(ts, node) {
60
+ if (node.decorators)
61
+ return Array.from(node.decorators);
62
+ if (node.modifiers) {
63
+ return node.modifiers.filter((m) => m.kind === ts.SyntaxKind.Decorator);
64
+ }
65
+ return [];
66
+ }
67
+ /**
68
+ * Extract the name and first string argument from a decorator.
69
+ * @returns { name: string, arg: string | null }
70
+ */
71
+ export function parseDecorator(ts, sf, decorator) {
72
+ const SK = ts.SyntaxKind;
73
+ const expr = decorator.expression;
74
+ if (!expr)
75
+ return { name: "", arg: null };
76
+ // @Get() or @Get('path') — CallExpression
77
+ if (expr.kind === SK.CallExpression) {
78
+ const callee = expr.expression;
79
+ const name = callee.kind === SK.Identifier ? callee.getText(sf) : "";
80
+ let arg = null;
81
+ if (expr.arguments?.length > 0) {
82
+ const first = expr.arguments[0];
83
+ if (first.kind === SK.StringLiteral || first.kind === SK.NoSubstitutionTemplateLiteral) {
84
+ arg = first.text;
85
+ }
86
+ }
87
+ return { name, arg };
88
+ }
89
+ // @Controller (without parens) — Identifier
90
+ if (expr.kind === SK.Identifier) {
91
+ return { name: expr.getText(sf), arg: null };
92
+ }
93
+ return { name: "", arg: null };
94
+ }
95
+ /**
96
+ * Get text from a node safely.
97
+ */
98
+ export function getText(sf, node) {
99
+ try {
100
+ return node.getText(sf);
101
+ }
102
+ catch {
103
+ return node.escapedText || node.text || "";
104
+ }
105
+ }
@@ -1,5 +1,7 @@
1
1
  import { relative, basename, extname } from "node:path";
2
2
  import { readFileSafe } from "../scanner.js";
3
+ import { loadTypeScript } from "../ast/loader.js";
4
+ import { extractReactComponentsAST } from "../ast/extract-components.js";
3
5
  // shadcn/ui + radix primitives to filter out
4
6
  const UI_PRIMITIVES = new Set([
5
7
  "accordion",
@@ -84,6 +86,7 @@ async function detectReactComponents(files, project) {
84
86
  !f.endsWith(".stories.tsx") &&
85
87
  !f.endsWith(".stories.jsx"));
86
88
  const components = [];
89
+ const ts = loadTypeScript(project.root);
87
90
  for (const file of componentFiles) {
88
91
  if (isUIPrimitive(file))
89
92
  continue;
@@ -91,6 +94,14 @@ async function detectReactComponents(files, project) {
91
94
  if (!content)
92
95
  continue;
93
96
  const rel = relative(project.root, file);
97
+ // Try AST first — extracts prop types, handles forwardRef/memo
98
+ if (ts) {
99
+ const astComponents = extractReactComponentsAST(ts, file, content, rel);
100
+ if (astComponents.length > 0) {
101
+ components.push(...astComponents);
102
+ continue;
103
+ }
104
+ }
94
105
  // Detect component name from export
95
106
  let name = "";
96
107
  // export default function ComponentName