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
|
@@ -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
|