codesight 1.1.1 → 1.3.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,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[];
@@ -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;