/** * Route discovery from a Fastify instance. * Pure functions, no side effects. * * Fastify 5 removed the public `routes` array. We capture routes via the `onRoute` * hook during plugin registration and store them in a WeakMap keyed by the instance. */ import { extractContract } from './contract.js' import type { RouteContract } from '../types.js' interface CapturedRoute { method: string url: string schema?: Record prefix?: string } // WeakMap to store captured routes per Fastify instance (no memory leaks) const capturedRoutes = new WeakMap() /** * Capture a route for discovery. * Called from the plugin's `onRoute` hook. */ export const captureRoute = ( instance: object, route: CapturedRoute ): void => { const existing = capturedRoutes.get(instance) ?? [] existing.push(route) capturedRoutes.set(instance, existing) } /** * Fallback route discovery for Fastify 5 when routes were registered before * the APOPHIS plugin (e.g., external apps loaded by CLI). * Uses hasRoute to test known route patterns. */ function discoverRoutesFallback( instance: { hasRoute?: (opts: { method: string; url: string }) => boolean } ): RouteContract[] { if (typeof instance.hasRoute !== 'function') { return [] } // Common HTTP methods to test const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] // We can't enumerate all possible routes, but we can check if the instance // has any routes at all by testing a few common patterns // This is a best-effort fallback const routes: RouteContract[] = [] // Try to extract routes from the instance's internal state // Fastify stores routes in find-my-way router, but it's not directly accessible // We'll use a heuristic: check if the instance responds to common route methods // Check if instance has any routes by looking at prototype methods const hasRouting = typeof (instance as any).routing === 'function' if (!hasRouting) { return [] } // Since we can't enumerate routes in Fastify 5 without the onRoute hook, // we return empty and let the caller handle the "no routes" case return [] } /** * Discover routes from a Fastify instance. * * First checks captured routes (from onRoute hook), then falls back to * the legacy `routes` array for Fastify 4 compatibility. */ export const discoverRoutes = (instance: { routes?: Array<{ method: string; url: string; schema?: Record }>; hasRoute?: (opts: { method: string; url: string }) => boolean }): RouteContract[] => { // Fastify 5: routes captured via onRoute hook const captured = capturedRoutes.get(instance) if (captured && captured.length > 0) { return captured.map((route) => extractContract(route.url, route.method, route.schema) ) } // Fastify 4 fallback if (Array.isArray(instance.routes) && instance.routes.length > 0) { return instance.routes.map((route) => extractContract(route.url, route.method, route.schema) ) } // Fastify 5 fallback: routes registered before plugin return discoverRoutesFallback(instance) } /** * Clear captured routes for an instance (useful for testing). */ export const clearCapturedRoutes = (instance: object): void => { capturedRoutes.delete(instance) }