/** Extract literal Minimal API routes and their receiver-specific MapGroup prefix. */ export function extractRoutesFromSource(source, file) { const prefixes = new Map([["app", ""]]); const groupPattern = /\b(?:var|const|RouteGroupBuilder)\s+(\w+)\s*=\s*(\w+)\.MapGroup\(\s*"([^"]+)"\s*\)/g; for (const match of source.matchAll(groupPattern)) { const parent = prefixes.get(match[2]); if (parent === undefined) throw new Error(`Unknown MapGroup receiver ${match[2]} in ${file}`); prefixes.set(match[1], parent + match[3]); } const routes = []; const mapPattern = /\b(\w+)\.Map(Get|Post|Put|Delete|Patch)\(\s*"([^"]*)"/g; for (const match of source.matchAll(mapPattern)) { const prefix = prefixes.get(match[1]); if (prefix === undefined) throw new Error(`Unknown route receiver ${match[1]} in ${file}`); const path = (prefix + match[3]).replace(/\{([a-zA-Z0-9_]+)(:[a-zA-Z]+)?\}/g, "{$1}"); if (!path.startsWith("/api/v1")) { if (!path.startsWith("/health")) throw new Error(`Unexpected non-API route ${path} in ${file}`); continue; } routes.push({ method: match[2].toUpperCase(), path: path.slice("/api/v1".length) || "/", file }); } return routes; }