Files
NianAIGC/tests/http-route-surface-contract.test.ts

75 lines
2.7 KiB
TypeScript

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<string[]> {
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<RouteContract["routes"]> {
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);
});
});