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
232 lines
6.2 KiB
JavaScript
Executable File
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);
|
|
});
|