import { readFile, readdir } from "node:fs/promises"; import { dirname, join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; type RouteContract = { version: 1; routes: Array<{ method: "DELETE" | "GET" | "PATCH" | "POST" | "PUT"; path: string; }>; }; const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); const appRoot = join(repositoryRoot, "app"); const contractPath = join(repositoryRoot, "contracts", "http", "route-surface.v1.json"); const exportedMethod = /^\s*export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b/gm; async function findRouteFiles(directory: string): Promise { const entries = await readdir(directory, { withFileTypes: true }); const nested = await Promise.all( entries.map(async (entry) => { const path = join(directory, entry.name); if (entry.isDirectory()) return findRouteFiles(path); return entry.isFile() && entry.name === "route.ts" ? [path] : []; }), ); return nested.flat().sort(); } function routePath(routeFile: string): string { const routeDirectory = relative(appRoot, dirname(routeFile)); const segments = routeDirectory.split(sep).map((segment) => { if (segment.startsWith("[...") && segment.endsWith("]")) { return `{${segment.slice(4, -1)}...}`; } if (segment.startsWith("[") && segment.endsWith("]")) { return `{${segment.slice(1, -1)}}`; } return segment; }); return `/${segments.join("/")}`; } async function deriveRouteSurface(routeFiles: string[]): Promise { const routes = await Promise.all( routeFiles.map(async (routeFile) => { const source = await readFile(routeFile, "utf8"); return Array.from(source.matchAll(exportedMethod), (match) => ({ method: match[1] as RouteContract["routes"][number]["method"], path: routePath(routeFile), })); }), ); return routes.flat().sort((left, right) => { const leftKey = `${left.path}\u0000${left.method}`; const rightKey = `${right.path}\u0000${right.method}`; return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; }); } describe("HTTP route surface contract", () => { it("exactly records every exported Next route method without loading handlers", async () => { const routeFiles = await findRouteFiles(appRoot); const actual = await deriveRouteSurface(routeFiles); const contract = JSON.parse(await readFile(contractPath, "utf8")) as RouteContract; expect(routeFiles).toHaveLength(47); expect(actual).toHaveLength(66); expect(contract.version).toBe(1); expect(contract.routes).toEqual(actual); }); });