#!/usr/bin/env node /** * Prueft, dass Dokumentation und Code nicht auseinanderlaufen. Laeuft in CI und * vor jedem Commit. Geprueft wird: * * 1. Die Version aus package.json hat einen Abschnitt im Changelog. * 2. Jede Issue-Datei steht in der Uebersicht – und umgekehrt. * 3. Geschaetzte und tatsaechliche Zeiten stimmen zwischen Datei und Uebersicht. * 4. Alle in der Anleitung verlinkten Bilder sind vorhanden. * 5. Kein Markdown verweist auf eine Datei, die es nicht gibt. */ import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, dirname, resolve } from "node:path"; const wurzel = resolve(import.meta.dirname, ".."); const befunde = []; function melde(text) { befunde.push(text); } function lies(pfad) { return readFileSync(join(wurzel, pfad), "utf8"); } function feld(text, name) { const treffer = text.match(new RegExp(`\\*\\*${name}:\\*\\*\\s*(.+?)(?=\\s*\\*\\*|$)`, "m")); return treffer?.[1]?.trim() ?? ""; } // ---- 1. Version im Changelog ---------------------------------------------- { const { version } = JSON.parse(lies("package.json")); const changelog = lies("CHANGELOG.md"); const veroeffentlicht = changelog.includes(`## [${version}]`); const unveroeffentlicht = /^## \[Unveröffentlicht\]/m.test(changelog); if (!veroeffentlicht && !unveroeffentlicht) { melde( `Version ${version} hat keinen Abschnitt im Changelog und es gibt keinen Abschnitt ` + `[Unveröffentlicht].`, ); } } // ---- 2. und 3. Issue-Dateien gegen die Uebersicht -------------------------- { const verz = "docs/issues"; const dateien = readdirSync(join(wurzel, verz)) .filter((n) => /^\d{3}-.+\.md$/.test(n)) .sort(); const uebersicht = lies(join(verz, "README.md")); for (const name of dateien) { if (!uebersicht.includes(`(${name})`)) { melde(`${verz}/${name} fehlt in ${verz}/README.md.`); continue; } const text = lies(join(verz, name)); const kopf = text.match(/^#\s*#(\d+)\s+(.+)$/m); if (!kopf) { melde(`${verz}/${name} hat keine Kopfzeile der Form "# #007 Titel".`); continue; } const [, nummer, titel] = kopf; if (Number(nummer) !== Number(name.slice(0, 3))) { melde(`${verz}/${name} nennt #${nummer}, der Dateiname sagt ${name.slice(0, 3)}.`); } const zeile = uebersicht.split("\n").find((z) => z.includes(`(${name})`)); for (const [beschriftung, wert] of [ ["Status", feld(text, "Status")], ["Geschätzt", feld(text, "Geschätzt")], ["Tatsächlich", feld(text, "Tatsächlich")], ]) { if (wert && !zeile.includes(`| ${wert} |`)) { melde(`${verz}/${name}: "${beschriftung}: ${wert}" weicht von der Übersicht ab.`); } } if (!zeile.includes(titel)) { melde(`${verz}/${name}: Titel "${titel}" weicht von der Übersicht ab.`); } } for (const treffer of uebersicht.matchAll(/\((\d{3}-[^)]+\.md)\)/g)) { if (!dateien.includes(treffer[1])) { melde(`${verz}/README.md verweist auf ${treffer[1]}, die Datei fehlt.`); } } } // ---- 4. Verlinkte Bilder in der Anleitung ---------------------------------- { const verz = join(wurzel, "docs/anleitung"); if (existsSync(verz)) { const sammeln = (ordner) => readdirSync(ordner, { withFileTypes: true }).flatMap((eintrag) => { const pfad = join(ordner, eintrag.name); if (eintrag.isDirectory()) return sammeln(pfad); return eintrag.name.endsWith(".md") ? [pfad] : []; }); for (const datei of sammeln(verz)) { const text = readFileSync(datei, "utf8"); for (const treffer of text.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)) { const ziel = treffer[1]; if (/^https?:/.test(ziel)) continue; if (!existsSync(resolve(dirname(datei), ziel))) { melde(`${datei.replace(`${wurzel}/`, "")} verlinkt das fehlende Bild ${ziel}.`); } } } } } // ---- 5. Verweise zwischen Markdown-Dateien -------------------------------- { const sammeln = (ordner) => readdirSync(ordner, { withFileTypes: true }).flatMap((eintrag) => { if (["node_modules", ".git", ".next"].includes(eintrag.name)) return []; const pfad = join(ordner, eintrag.name); if (eintrag.isDirectory()) return sammeln(pfad); return eintrag.name.endsWith(".md") ? [pfad] : []; }); for (const datei of sammeln(wurzel)) { const text = readFileSync(datei, "utf8"); for (const treffer of text.matchAll(/(? 0) { console.error(`\nDokumentation und Code laufen auseinander (${befunde.length}):\n`); for (const b of befunde) console.error(` - ${b}`); console.error(""); process.exit(1); } console.log("Dokumentation stimmt.");