import { globSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; /** * Routen-Discovery aus dem Dateisystem für den Driftschutz (Definition of * Done #1). Reine Logik, offline lauffähig (kein Server/DB). */ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); /** * Öffentliche Präfixe (Middleware-Allowlist). Routen, die mit einem dieser * Präfixe beginnen, brauchen KEINEN Manifest-Eintrag, weil sie absichtlich * anonym erreichbar sind. */ export const PUBLIC_ALLOWLIST: readonly string[] = [ "/login", "/api/auth", "/api/health", "/_next", "/favicon.ico", "/robots.txt", ]; /** * Wandelt einen Datei-Pfad (page.tsx | route.ts) in den zugehörigen URL-Pfad * um. Route-Groups `(name)` werden entfernt, dynamische Segmente `[id]` * bleiben als Platzhalter erhalten. */ export function filePathToRoute(filePath: string): string { const withoutPrefix = filePath .replace(/^.*src\/app\//, "") .replace(/^(page|route)\.(tsx?|jsx?)$/, "") .replace(/\/(page|route)\.(tsx?|jsx?)$/, ""); const segments = withoutPrefix .split("/") .filter((seg) => seg.length > 0 && !/^\(.*\)$/.test(seg)); return "/" + segments.join("/"); } function isPublic(route: string): boolean { // Nur exakter Treffer oder echtes Unterpfad-Segment zählt als öffentlich. // KEIN reiner String-Präfix: sonst wären z. B. "/loginhelp" oder // "/api/healthz" fälschlich anonym erreichbar und entkämen dem Drift-Check. return PUBLIC_ALLOWLIST.some( (p) => route === p || route.startsWith(p + "/"), ); } /** * Findet Routen, die im Dateisystem existieren, aber weder im Manifest * deklariert noch öffentlich sind. */ export function findUndeclaredRoutes( discovered: readonly string[], declared: ReadonlySet, ): string[] { return discovered .filter((route) => !isPublic(route)) .filter((route) => !declared.has(route)); } /** Scannt src/app/** nach page.tsx und route.ts und liefert URL-Pfade. */ export function discoverAppRoutes(): string[] { const files = globSync("src/app/**/{page,route}.{ts,tsx}", { cwd: REPO_ROOT, }); const routes = new Set(); for (const f of files) routes.add(filePathToRoute(f)); return [...routes].sort(); }