#!/usr/bin/env node /** * Compares runtime Express method registrations against OpenAPI path keys. * Fails if coverage drops below OPENAPI_MIN_COVERAGE (default 55) or if * required ops endpoints are missing from the OpenAPI document. */ import fs from 'node:fs' import path from 'node:path' const root = path.resolve(process.cwd()) const apiSrc = path.join(root, 'apps', 'api', 'src') const minCoverage = Number(process.env.OPENAPI_MIN_COVERAGE ?? 55) function walk(dir, out = []) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, entry.name) if (entry.isDirectory()) { if (entry.name === 'node_modules' || entry.name === 'dist') continue walk(p, out) } else if (/\.(ts|js)$/.test(entry.name) && !/\.test\./.test(entry.name)) { out.push(p) } } return out } const routeCall = /\b(router|app)\.(get|post|put|patch|delete)\(\s*['"`]([^'"`]+)['"`]/gi const files = walk(apiSrc) const runtime = new Set() for (const file of files) { const text = fs.readFileSync(file, 'utf8') let match while ((match = routeCall.exec(text))) { const method = match[2].toUpperCase() let routePath = match[3] if (!routePath.startsWith('/')) routePath = `/${routePath}` runtime.add(`${method} ${routePath}`) } } const openapiPath = path.join(apiSrc, 'swagger', 'openapi.ts') const openapiText = fs.readFileSync(openapiPath, 'utf8') const pathKeys = [...openapiText.matchAll(/^\s*'(\/[^']+)':\s*\{/gm)].map((m) => m[1]) const openapiPaths = new Set(pathKeys) const required = ['/health', '/ready', '/metrics'] const missingRequired = required.filter((p) => !openapiPaths.has(p)) // Rough coverage: unique OpenAPI path keys vs unique runtime path strings const runtimePaths = new Set([...runtime].map((entry) => entry.split(' ').slice(1).join(' '))) const covered = [...openapiPaths].filter((p) => { if (runtimePaths.has(p)) return true // parameterised OpenAPI paths often match runtime `/:id` style return [...runtimePaths].some((rp) => rp.split('/').length === p.split('/').length) }) const coverage = openapiPaths.size === 0 ? 0 : Math.round((covered.length / Math.max(runtimePaths.size, 1)) * 100) console.log(`OpenAPI paths: ${openapiPaths.size}`) console.log(`Runtime route registrations: ${runtime.size} (${runtimePaths.size} unique path patterns)`) console.log(`Approximate path coverage: ${coverage}% (min ${minCoverage}%)`) if (missingRequired.length) { console.error(`Missing required OpenAPI paths: ${missingRequired.join(', ')}`) process.exit(1) } if (coverage < minCoverage) { console.error(`OpenAPI coverage ${coverage}% is below minimum ${minCoverage}%`) process.exit(1) } console.log('OpenAPI coverage check passed')