Files
FEDEO/docs/scripts/sync-funktionsdoku.mjs
florianfederspiel 9fea18b215
Some checks failed
Build and Push Docker Images / verify-docs-sync (push) Successful in 8s
Build and Push Docker Images / build-backend (push) Successful in 2m50s
Build and Push Docker Images / build-frontend (push) Successful in 3m18s
Build and Push Docker Images / build-docs (push) Failing after 1m29s
Behebe fehlschlagenden Docs-Workflow durch deterministische Doku-Generierung
2026-04-22 14:38:51 +02:00

232 lines
6.2 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
const ROOT = process.cwd();
const DOCS_DIR = path.join(ROOT, "docs");
const BACKEND_ROUTES_DIR = path.join(ROOT, "backend", "src", "routes");
const FRONTEND_PAGES_DIR = path.join(ROOT, "frontend", "pages");
const MOBILE_APP_DIR = path.join(ROOT, "mobile", "app");
const OUT_BACKEND = path.join(DOCS_DIR, "funktionen", "backend-api.md");
const OUT_FRONTEND = path.join(DOCS_DIR, "funktionen", "frontend-web.md");
const OUT_MOBILE = path.join(DOCS_DIR, "funktionen", "mobile-app.md");
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
async function walkFiles(dir, extension) {
const result = [];
async function walk(current) {
const entries = await fs.readdir(current, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
await walk(full);
} else if (entry.isFile() && full.endsWith(extension)) {
result.push(full);
}
}
}
await walk(dir);
return result.sort();
}
function normalizePosix(p) {
return p.split(path.sep).join("/");
}
function toRelative(filePath) {
return normalizePosix(path.relative(ROOT, filePath));
}
function extractBackendEndpoints(sourceText) {
const endpoints = [];
const regex = /\b(?:server|subApp|m2mApp|devicesApp)\.(get|post|put|patch|delete|options|head)\(\s*["'`](.+?)["'`]/g;
let match;
while ((match = regex.exec(sourceText)) !== null) {
const method = String(match[1] || "").toUpperCase();
const routePath = String(match[2] || "");
if (!HTTP_METHODS.includes(method.toLowerCase())) {
continue;
}
endpoints.push({ method, path: routePath });
}
return endpoints.sort((a, b) => {
if (a.path === b.path) {
return a.method.localeCompare(b.method);
}
return a.path.localeCompare(b.path);
});
}
function sortAndUnique(items) {
return [...new Set(items)].sort((a, b) => a.localeCompare(b));
}
function filePathToNuxtRoute(filePath, baseDir) {
const relative = normalizePosix(path.relative(baseDir, filePath));
let route = relative.replace(/\.vue$/, "");
route = route.replace(/\.client$/, "").replace(/\.server$/, "");
route = route
.replace(/\[\[\.\.\.(.+?)\]\]/g, ":$1*?")
.replace(/\[\.\.\.(.+?)\]/g, ":$1*")
.replace(/\[\[(.+?)\]\]/g, ":$1?")
.replace(/\[(.+?)\]/g, ":$1");
route = route.replace(/\/index$/g, "");
if (route === "index") {
route = "";
}
if (!route.startsWith("/")) {
route = `/${route}`;
}
if (route === "") {
return "/";
}
return route || "/";
}
function filePathToExpoRoute(filePath, baseDir) {
const relative = normalizePosix(path.relative(baseDir, filePath));
const baseName = path.basename(relative);
if (baseName.startsWith("_")) {
return null;
}
let route = relative.replace(/\.tsx$/, "");
route = route
.replace(/\[\[(.+?)\]\]/g, ":$1?")
.replace(/\[(.+?)\]/g, ":$1");
route = route.replace(/\/index$/g, "");
if (route === "index") {
route = "";
}
if (!route.startsWith("/")) {
route = `/${route}`;
}
if (route === "") {
return "/";
}
return route;
}
async function generateBackendDoc() {
const files = await walkFiles(BACKEND_ROUTES_DIR, ".ts");
let output = "# Backend API Funktionskatalog\n\n";
output += "Automatisch generiert (deterministisch, ohne Zeitstempel).\n\n";
output += "Hinweis: Diese Datei wird durch `docs/scripts/sync-funktionsdoku.mjs` erzeugt.\n\n";
const allEndpoints = [];
for (const file of files) {
const source = await fs.readFile(file, "utf-8");
const endpoints = extractBackendEndpoints(source);
const relativeFile = toRelative(file);
if (endpoints.length === 0) {
continue;
}
output += `## ${relativeFile}\n\n`;
output += "| Methode | Pfad |\n";
output += "|---|---|\n";
for (const endpoint of endpoints) {
output += `| ${endpoint.method} | \`${endpoint.path}\` |\n`;
allEndpoints.push(`${endpoint.method} ${endpoint.path}`);
}
output += "\n";
}
const uniqueCount = sortAndUnique(allEndpoints).length;
output += `Gesamtzahl erkannter Endpunkte: **${uniqueCount}**\n`;
await fs.writeFile(OUT_BACKEND, output, "utf-8");
}
async function generateFrontendDoc() {
const files = await walkFiles(FRONTEND_PAGES_DIR, ".vue");
const rows = files.map((file) => {
const route = filePathToNuxtRoute(file, FRONTEND_PAGES_DIR);
return { route, file: toRelative(file) };
});
rows.sort((a, b) => a.route.localeCompare(b.route));
let output = "# Frontend Web Funktionskatalog\n\n";
output += "Automatisch generiert (deterministisch, ohne Zeitstempel).\n\n";
output += "Hinweis: Diese Datei wird durch `docs/scripts/sync-funktionsdoku.mjs` erzeugt.\n\n";
output += "| Route (Nuxt) | Datei |\n";
output += "|---|---|\n";
for (const row of rows) {
output += `| \`${row.route}\` | \`${row.file}\` |\n`;
}
output += `\nGesamtzahl erkannter Web-Routen: **${rows.length}**\n`;
await fs.writeFile(OUT_FRONTEND, output, "utf-8");
}
async function generateMobileDoc() {
const files = await walkFiles(MOBILE_APP_DIR, ".tsx");
const rows = files
.map((file) => {
const route = filePathToExpoRoute(file, MOBILE_APP_DIR);
if (!route) {
return null;
}
return { route, file: toRelative(file) };
})
.filter(Boolean);
rows.sort((a, b) => a.route.localeCompare(b.route));
let output = "# Mobile App Funktionskatalog\n\n";
output += "Automatisch generiert (deterministisch, ohne Zeitstempel).\n\n";
output += "Hinweis: Diese Datei wird durch `docs/scripts/sync-funktionsdoku.mjs` erzeugt.\n\n";
output += "| Route (Expo Router) | Datei |\n";
output += "|---|---|\n";
for (const row of rows) {
output += `| \`${row.route}\` | \`${row.file}\` |\n`;
}
output += `\nGesamtzahl erkannter Mobile-Screens: **${rows.length}**\n`;
await fs.writeFile(OUT_MOBILE, output, "utf-8");
}
async function main() {
await generateBackendDoc();
await generateFrontendDoc();
await generateMobileDoc();
console.log("Funktionsdokumentation erfolgreich synchronisiert.");
}
main().catch((error) => {
console.error("Fehler bei der Doku-Synchronisierung:", error);
process.exit(1);
});