express.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /**
  2. * Express/Node.js Framework Resolver
  3. *
  4. * Handles Express and general Node.js patterns.
  5. */
  6. import { Node } from '../../types';
  7. import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
  8. export const expressResolver: FrameworkResolver = {
  9. name: 'express',
  10. detect(context: ResolutionContext): boolean {
  11. // Check for Express in package.json
  12. const packageJson = context.readFile('package.json');
  13. if (packageJson) {
  14. try {
  15. const pkg = JSON.parse(packageJson);
  16. const deps = { ...pkg.dependencies, ...pkg.devDependencies };
  17. if (deps.express || deps.fastify || deps.koa || deps.hapi) {
  18. return true;
  19. }
  20. } catch {
  21. // Invalid JSON
  22. }
  23. }
  24. // Check for common Express patterns
  25. const allFiles = context.getAllFiles();
  26. for (const file of allFiles) {
  27. if (
  28. file.includes('routes') ||
  29. file.includes('controllers') ||
  30. file.includes('middleware')
  31. ) {
  32. const content = context.readFile(file);
  33. if (content && (content.includes('express') || content.includes('app.get') || content.includes('router.get'))) {
  34. return true;
  35. }
  36. }
  37. }
  38. return false;
  39. },
  40. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  41. // Pattern 1: Middleware references
  42. if (isMiddlewareName(ref.referenceName)) {
  43. const result = resolveMiddleware(ref.referenceName, context);
  44. if (result) {
  45. return {
  46. original: ref,
  47. targetNodeId: result,
  48. confidence: 0.8,
  49. resolvedBy: 'framework',
  50. };
  51. }
  52. }
  53. // Pattern 2: Controller method references
  54. const controllerMatch = ref.referenceName.match(/^(\w+)Controller\.(\w+)$/);
  55. if (controllerMatch) {
  56. const [, controller, method] = controllerMatch;
  57. const result = resolveController(controller!, method!, context);
  58. if (result) {
  59. return {
  60. original: ref,
  61. targetNodeId: result,
  62. confidence: 0.85,
  63. resolvedBy: 'framework',
  64. };
  65. }
  66. }
  67. // Pattern 3: Service/helper references
  68. const serviceMatch = ref.referenceName.match(/^(\w+)(Service|Helper|Utils?)\.(\w+)$/);
  69. if (serviceMatch) {
  70. const [, name, suffix, method] = serviceMatch;
  71. const result = resolveService(name! + suffix!, method!, context);
  72. if (result) {
  73. return {
  74. original: ref,
  75. targetNodeId: result,
  76. confidence: 0.8,
  77. resolvedBy: 'framework',
  78. };
  79. }
  80. }
  81. return null;
  82. },
  83. extractNodes(filePath: string, content: string): Node[] {
  84. const nodes: Node[] = [];
  85. const now = Date.now();
  86. // Extract route definitions
  87. // app.get('/path', handler) or router.get('/path', handler)
  88. const routePatterns = [
  89. /(app|router)\.(get|post|put|patch|delete|all|use)\(\s*['"]([^'"]+)['"]/g,
  90. ];
  91. for (const pattern of routePatterns) {
  92. let match;
  93. while ((match = pattern.exec(content)) !== null) {
  94. const [, _obj, method, path] = match;
  95. const line = content.slice(0, match.index).split('\n').length;
  96. // Skip middleware use() without paths
  97. if (method === 'use' && !path?.startsWith('/')) {
  98. continue;
  99. }
  100. nodes.push({
  101. id: `route:${filePath}:${method!.toUpperCase()}:${path}:${line}`,
  102. kind: 'route',
  103. name: `${method!.toUpperCase()} ${path}`,
  104. qualifiedName: `${filePath}::${method!.toUpperCase()}:${path}`,
  105. filePath,
  106. startLine: line,
  107. endLine: line,
  108. startColumn: 0,
  109. endColumn: match[0].length,
  110. language: detectLanguage(filePath),
  111. updatedAt: now,
  112. });
  113. }
  114. }
  115. return nodes;
  116. },
  117. };
  118. /**
  119. * Check if a name looks like middleware
  120. */
  121. function isMiddlewareName(name: string): boolean {
  122. const middlewarePatterns = [
  123. /^auth$/i,
  124. /^authenticate$/i,
  125. /^authorization$/i,
  126. /^validate/i,
  127. /^sanitize/i,
  128. /^rateLimit/i,
  129. /^cors$/i,
  130. /^helmet$/i,
  131. /^logger$/i,
  132. /^errorHandler$/i,
  133. /^notFound$/i,
  134. /Middleware$/i,
  135. ];
  136. return middlewarePatterns.some((p) => p.test(name));
  137. }
  138. /**
  139. * Resolve middleware reference
  140. */
  141. function resolveMiddleware(
  142. name: string,
  143. context: ResolutionContext
  144. ): string | null {
  145. // Look in middleware directories
  146. const middlewareDirs = ['middleware', 'middlewares', 'src/middleware', 'src/middlewares'];
  147. for (const dir of middlewareDirs) {
  148. const allFiles = context.getAllFiles();
  149. for (const file of allFiles) {
  150. if (file.startsWith(dir) || file.includes('/middleware/')) {
  151. const nodes = context.getNodesInFile(file);
  152. const match = nodes.find(
  153. (n) =>
  154. n.name.toLowerCase() === name.toLowerCase() ||
  155. n.name.toLowerCase() === name.replace(/Middleware$/i, '').toLowerCase()
  156. );
  157. if (match) {
  158. return match.id;
  159. }
  160. }
  161. }
  162. }
  163. return null;
  164. }
  165. /**
  166. * Resolve controller method
  167. */
  168. function resolveController(
  169. controller: string,
  170. method: string,
  171. context: ResolutionContext
  172. ): string | null {
  173. const controllerDirs = ['controllers', 'src/controllers', 'app/controllers'];
  174. for (const dir of controllerDirs) {
  175. const allFiles = context.getAllFiles();
  176. for (const file of allFiles) {
  177. if (
  178. (file.startsWith(dir) || file.includes('/controllers/')) &&
  179. file.toLowerCase().includes(controller.toLowerCase())
  180. ) {
  181. const nodes = context.getNodesInFile(file);
  182. const methodNode = nodes.find(
  183. (n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
  184. );
  185. if (methodNode) {
  186. return methodNode.id;
  187. }
  188. }
  189. }
  190. }
  191. return null;
  192. }
  193. /**
  194. * Resolve service/helper
  195. */
  196. function resolveService(
  197. serviceName: string,
  198. method: string,
  199. context: ResolutionContext
  200. ): string | null {
  201. const serviceDirs = ['services', 'src/services', 'helpers', 'src/helpers', 'utils', 'src/utils'];
  202. for (const dir of serviceDirs) {
  203. const allFiles = context.getAllFiles();
  204. for (const file of allFiles) {
  205. if (
  206. (file.startsWith(dir) || file.includes('/services/') || file.includes('/helpers/') || file.includes('/utils/')) &&
  207. file.toLowerCase().includes(serviceName.toLowerCase().replace(/(service|helper|utils?)$/i, ''))
  208. ) {
  209. const nodes = context.getNodesInFile(file);
  210. const methodNode = nodes.find(
  211. (n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
  212. );
  213. if (methodNode) {
  214. return methodNode.id;
  215. }
  216. }
  217. }
  218. }
  219. return null;
  220. }
  221. /**
  222. * Detect language from file extension
  223. */
  224. function detectLanguage(filePath: string): 'typescript' | 'javascript' {
  225. if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
  226. return 'typescript';
  227. }
  228. return 'javascript';
  229. }