#!/usr/bin/env python3 """JavaScript endpoint detector for Express and Fastify.""" import re from pathlib import Path from typing import Optional from docgen.detectors.base import BaseDetector from docgen.models import Endpoint, HTTPMethod class JavaScriptDetector(BaseDetector): """Detector for JavaScript web frameworks.""" extensions = [".js", ".ts", ".jsx", ".tsx"] framework_name = "javascript" EXPRESS_PATTERN = re.compile( r'(app|router)\.(get|post|put|patch|delete|options|head)\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE, ) EXPRESS_APP_ROUTE_PATTERN = re.compile( r'(app|router)\.(route)\s*\(\s*["\']([^"\']+)["\'][\s\S]*?\.((?:get|post|put|patch|delete|options|head))\s*\(', re.MULTILINE, ) FASTIFY_PATTERN = re.compile( r'(?:fastify|instance)\s*\.\s*(get|post|put|patch|delete|options|head)\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE, ) FASTIFY_DECORATOR_PATTERN = re.compile( r'@?(?:get|post|put|patch|delete|options|head)\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE, ) METHOD_MAP = { "get": HTTPMethod.GET, "post": HTTPMethod.POST, "put": HTTPMethod.PUT, "patch": HTTPMethod.PATCH, "delete": HTTPMethod.DELETE, "options": HTTPMethod.OPTIONS, "head": HTTPMethod.HEAD, } def detect_endpoints(self, file_path: Path) -> list[Endpoint]: """Detect endpoints in a JavaScript/TypeScript file.""" content = file_path.read_text() endpoints = [] framework = self._detect_framework(content) if framework == "fastify": endpoints.extend(self._detect_fastify(content, file_path)) else: endpoints.extend(self._detect_express(content, file_path)) return endpoints def _detect_framework(self, content: str) -> Optional[str]: """Auto-detect the JavaScript framework.""" fastify_patterns = [ "from '@fastify/", "from 'fastify'", "import fastify", ] if any(p in content for p in fastify_patterns): return "fastify" if 'require("fastify")' in content or "require('fastify')" in content: return "fastify" if "from 'express'" in content or "import express" in content: return "express" return None def _detect_express(self, content: str, file_path: Path) -> list[Endpoint]: """Detect Express endpoints.""" endpoints = [] for match in self.EXPRESS_PATTERN.finditer(content): router, method, path = match.groups() endpoint = Endpoint( path=path, method=self.METHOD_MAP.get(method.lower(), HTTPMethod.GET), summary=f"{method.upper()} {path}", file_path=str(file_path), line_number=content[:match.start()].count("\n") + 1, ) endpoints.append(endpoint) for match in self.EXPRESS_APP_ROUTE_PATTERN.finditer(content): router, path, sub_method = match.groups() endpoint = Endpoint( path=path, method=self.METHOD_MAP.get(sub_method.lower(), HTTPMethod.GET), summary=f"{sub_method.upper()} {path}", file_path=str(file_path), line_number=content[:match.start()].count("\n") + 1, ) endpoints.append(endpoint) return endpoints def _detect_fastify(self, content: str, file_path: Path) -> list[Endpoint]: """Detect Fastify endpoints.""" endpoints = [] for match in self.FASTIFY_PATTERN.finditer(content): method, path = match.groups() endpoint = Endpoint( path=path, method=self.METHOD_MAP.get(method.lower(), HTTPMethod.GET), summary=f"{method.upper()} {path}", file_path=str(file_path), line_number=content[:match.start()].count("\n") + 1, ) endpoints.append(endpoint) for match in self.FASTIFY_DECORATOR_PATTERN.finditer(content): path = match.group(1) endpoint = Endpoint( path=path, method=HTTPMethod.GET, summary=f"GET {path}", file_path=str(file_path), line_number=content[:match.start()].count("\n") + 1, ) endpoints.append(endpoint) return endpoints