@getmikk/core 1.3.2 → 1.5.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.
@@ -1,5 +1,5 @@
1
1
  import ts from 'typescript'
2
- import type { ParsedFunction, ParsedClass, ParsedImport, ParsedExport, ParsedParam, ParsedGeneric } from '../types.js'
2
+ import type { ParsedFunction, ParsedClass, ParsedImport, ParsedExport, ParsedParam, ParsedGeneric, ParsedRoute } from '../types.js'
3
3
  import { hashContent } from '../../hash/file-hasher.js'
4
4
 
5
5
  /**
@@ -18,10 +18,18 @@ export class TypeScriptExtractor {
18
18
  content,
19
19
  ts.ScriptTarget.Latest,
20
20
  true, // setParentNodes
21
- filePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
21
+ this.inferScriptKind(filePath)
22
22
  )
23
23
  }
24
24
 
25
+ /** Infer TypeScript ScriptKind from file extension (supports JS/JSX/TS/TSX) */
26
+ private inferScriptKind(filePath: string): ts.ScriptKind {
27
+ if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX
28
+ if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX
29
+ if (filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return ts.ScriptKind.JS
30
+ return ts.ScriptKind.TS
31
+ }
32
+
25
33
  /** Extract all top-level and variable-assigned functions */
26
34
  extractFunctions(): ParsedFunction[] {
27
35
  const functions: ParsedFunction[] = []
@@ -119,7 +127,7 @@ export class TypeScriptExtractor {
119
127
  return false
120
128
  }
121
129
 
122
- /** Extract all import statements */
130
+ /** Extract all import statements (static and dynamic) */
123
131
  extractImports(): ParsedImport[] {
124
132
  const imports: ParsedImport[] = []
125
133
  this.walkNode(this.sourceFile, (node) => {
@@ -128,6 +136,25 @@ export class TypeScriptExtractor {
128
136
  if (parsed) imports.push(parsed)
129
137
  }
130
138
  })
139
+
140
+ // Also detect dynamic import() calls: await import('./path')
141
+ const walkDynamic = (n: ts.Node) => {
142
+ if (ts.isCallExpression(n) && n.expression.kind === ts.SyntaxKind.ImportKeyword) {
143
+ const arg = n.arguments[0]
144
+ if (arg && ts.isStringLiteral(arg)) {
145
+ imports.push({
146
+ source: arg.text,
147
+ resolvedPath: '', // Filled in by resolver
148
+ names: [],
149
+ isDefault: false,
150
+ isDynamic: true,
151
+ })
152
+ }
153
+ }
154
+ ts.forEachChild(n, walkDynamic)
155
+ }
156
+ ts.forEachChild(this.sourceFile, walkDynamic)
157
+
131
158
  return imports
132
159
  }
133
160
 
@@ -205,6 +232,83 @@ export class TypeScriptExtractor {
205
232
  return exports
206
233
  }
207
234
 
235
+ /**
236
+ * Extract HTTP route registrations.
237
+ * Detects Express/Koa/Hono patterns like:
238
+ * router.get("/path", handler)
239
+ * app.post("/path", middleware, handler)
240
+ * app.use("/prefix", subrouter)
241
+ * router.use(middleware)
242
+ */
243
+ extractRoutes(): ParsedRoute[] {
244
+ const routes: ParsedRoute[] = []
245
+ const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'all', 'use'])
246
+ // Only detect routes on receiver objects that look like routers/apps
247
+ const ROUTER_NAMES = new Set(['app', 'router', 'server', 'route', 'api', 'express'])
248
+
249
+ const walk = (node: ts.Node) => {
250
+ if (
251
+ ts.isCallExpression(node) &&
252
+ ts.isPropertyAccessExpression(node.expression)
253
+ ) {
254
+ const methodName = node.expression.name.text.toLowerCase()
255
+ if (HTTP_METHODS.has(methodName)) {
256
+ // Check if the receiver is a known router/app-like identifier
257
+ const receiver = node.expression.expression
258
+ let receiverName = ''
259
+ if (ts.isIdentifier(receiver)) {
260
+ receiverName = receiver.text.toLowerCase()
261
+ } else if (ts.isPropertyAccessExpression(receiver) && ts.isIdentifier(receiver.expression)) {
262
+ receiverName = receiver.expression.text.toLowerCase()
263
+ }
264
+
265
+ // Skip if receiver doesn't look like a router (e.g. prisma.file.delete)
266
+ if (!ROUTER_NAMES.has(receiverName)) {
267
+ ts.forEachChild(node, walk)
268
+ return
269
+ }
270
+
271
+ const args = node.arguments
272
+ let routePath = ''
273
+ const middlewares: string[] = []
274
+ let handler = 'anonymous'
275
+
276
+ for (let i = 0; i < args.length; i++) {
277
+ const arg = args[i]
278
+ // First string literal is the route path
279
+ if (ts.isStringLiteral(arg) && !routePath) {
280
+ routePath = arg.text
281
+ } else if (ts.isIdentifier(arg)) {
282
+ // Last identifier is the handler; earlier ones are middleware
283
+ if (i === args.length - 1) {
284
+ handler = arg.text
285
+ } else {
286
+ middlewares.push(arg.text)
287
+ }
288
+ } else if (ts.isCallExpression(arg)) {
289
+ // e.g. upload.single("file") — middleware call
290
+ middlewares.push(arg.expression.getText(this.sourceFile))
291
+ } else if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
292
+ handler = 'anonymous'
293
+ }
294
+ }
295
+
296
+ routes.push({
297
+ method: methodName.toUpperCase(),
298
+ path: routePath || '*',
299
+ handler,
300
+ middlewares,
301
+ file: this.filePath,
302
+ line: this.getLineNumber(node.getStart()),
303
+ })
304
+ }
305
+ }
306
+ ts.forEachChild(node, walk)
307
+ }
308
+ ts.forEachChild(this.sourceFile, walk)
309
+ return routes
310
+ }
311
+
208
312
  // ─── Private Helpers ──────────────────────────────────────
209
313
 
210
314
  private parseFunctionDeclaration(node: ts.FunctionDeclaration): ParsedFunction {
@@ -286,7 +390,32 @@ export class TypeScriptExtractor {
286
390
  const typeParameters = this.extractTypeParameters(node.typeParameters)
287
391
 
288
392
  for (const member of node.members) {
289
- if (ts.isMethodDeclaration(member) && member.name) {
393
+ if (ts.isConstructorDeclaration(member)) {
394
+ // Track class constructors as methods
395
+ const mStartLine = this.getLineNumber(member.getStart())
396
+ const mEndLine = this.getLineNumber(member.getEnd())
397
+ const params = this.extractParams(member.parameters)
398
+ const calls = this.extractCalls(member)
399
+ const bodyText = member.getText(this.sourceFile)
400
+
401
+ methods.push({
402
+ id: `fn:${this.filePath}:${name}.constructor`,
403
+ name: `${name}.constructor`,
404
+ file: this.filePath,
405
+ startLine: mStartLine,
406
+ endLine: mEndLine,
407
+ params,
408
+ returnType: name,
409
+ isExported: this.hasExportModifier(node),
410
+ isAsync: false,
411
+ calls,
412
+ hash: hashContent(bodyText),
413
+ purpose: this.extractPurpose(member),
414
+ edgeCasesHandled: this.extractEdgeCases(member),
415
+ errorHandling: this.extractErrorHandling(member),
416
+ detailedLines: this.extractDetailedLines(member),
417
+ })
418
+ } else if (ts.isMethodDeclaration(member) && member.name) {
290
419
  const methodName = member.name.getText(this.sourceFile)
291
420
  const mStartLine = this.getLineNumber(member.getStart())
292
421
  const mEndLine = this.getLineNumber(member.getEnd())
@@ -373,7 +502,7 @@ export class TypeScriptExtractor {
373
502
  }
374
503
  }
375
504
 
376
- /** Extract function/method call expressions from a node */
505
+ /** Extract function/method call expressions from a node (including new Foo()) */
377
506
  private extractCalls(node: ts.Node): string[] {
378
507
  const calls: string[] = []
379
508
  const walkCalls = (n: ts.Node) => {
@@ -386,6 +515,15 @@ export class TypeScriptExtractor {
386
515
  calls.push(callee.getText(this.sourceFile))
387
516
  }
388
517
  }
518
+ // Track constructor calls: new Foo(...) → "Foo"
519
+ if (ts.isNewExpression(n)) {
520
+ const callee = n.expression
521
+ if (ts.isIdentifier(callee)) {
522
+ calls.push(callee.text)
523
+ } else if (ts.isPropertyAccessExpression(callee)) {
524
+ calls.push(callee.getText(this.sourceFile))
525
+ }
526
+ }
389
527
  ts.forEachChild(n, walkCalls)
390
528
  }
391
529
  ts.forEachChild(node, walkCalls)
@@ -414,9 +552,9 @@ export class TypeScriptExtractor {
414
552
  if (clean) meaningfulLines.push(clean)
415
553
  }
416
554
 
417
- // Return the last few meaningful lines or just the one closest to the node
418
- // Often the first line of JSDoc or the line right above the node
419
- return meaningfulLines.length > 0 ? meaningfulLines[meaningfulLines.length - 1].split('\n')[0].trim() : ''
555
+ // Return the first meaningful line in JSDoc, the first line is the summary.
556
+ // e.g. "Decrypt data using AES-256-GCM" (not a later detail line)
557
+ return meaningfulLines.length > 0 ? meaningfulLines[0].split('\n')[0].trim() : ''
420
558
  }
421
559
 
422
560
  /** Extract edge cases handled (if statements returning early) */
@@ -12,14 +12,41 @@ export class TypeScriptParser extends BaseParser {
12
12
  /** Parse a single TypeScript file */
13
13
  parse(filePath: string, content: string): ParsedFile {
14
14
  const extractor = new TypeScriptExtractor(filePath, content)
15
+ const functions = extractor.extractFunctions()
16
+ const classes = extractor.extractClasses()
17
+ const generics = extractor.extractGenerics()
18
+ const imports = extractor.extractImports()
19
+ const exports = extractor.extractExports()
20
+ const routes = extractor.extractRoutes()
21
+
22
+ // Cross-reference: if a function/class/generic is named in an export { Name }
23
+ // or export default declaration, mark it as exported.
24
+ const exportedNames = new Set(exports.map(e => e.name))
25
+ for (const fn of functions) {
26
+ if (!fn.isExported && exportedNames.has(fn.name)) {
27
+ fn.isExported = true
28
+ }
29
+ }
30
+ for (const cls of classes) {
31
+ if (!cls.isExported && exportedNames.has(cls.name)) {
32
+ cls.isExported = true
33
+ }
34
+ }
35
+ for (const gen of generics) {
36
+ if (!gen.isExported && exportedNames.has(gen.name)) {
37
+ gen.isExported = true
38
+ }
39
+ }
40
+
15
41
  return {
16
42
  path: filePath,
17
43
  language: 'typescript',
18
- functions: extractor.extractFunctions(),
19
- classes: extractor.extractClasses(),
20
- generics: extractor.extractGenerics(),
21
- imports: extractor.extractImports(),
22
- exports: extractor.extractExports(),
44
+ functions,
45
+ classes,
46
+ generics,
47
+ imports,
48
+ exports,
49
+ routes,
23
50
  hash: hashContent(content),
24
51
  parsedAt: Date.now(),
25
52
  }