#!/usr/bin/env node /** * Documentation validation script * Checks if README.md is up to date with the current project structure */ const fs = require('fs'); const path = require('path'); // Colors for terminal output const colors = { reset: '\x1b[0m', red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } // Read README.md const readmePath = path.join(__dirname, '..', 'README.md'); const readme = fs.readFileSync(readmePath, 'utf-8'); let issuesFound = []; /** * Check 1: NPM Scripts in package.json vs README */ function checkNpmScripts() { const packageJson = require('../package.json'); const scripts = Object.keys(packageJson.scripts); const missingScripts = scripts.filter(script => { const patterns = [ new RegExp(`npm run ${script}`, 'g'), new RegExp(`\`${script}\``, 'g'), ]; return !patterns.some(pattern => pattern.test(readme)); }); if (missingScripts.length > 0) { issuesFound.push({ section: 'NPM Scripts', issues: missingScripts.map(s => `Script "${s}" not documented`), }); } } /** * Check 2: API Routes in app/api vs README */ function checkApiRoutes() { const apiDir = path.join(__dirname, '..', 'app', 'api'); if (!fs.existsSync(apiDir)) return; const apiRoutes = []; function scanDir(dir, prefix = '') { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const routePath = path.join(prefix, entry.name); if (entry.isDirectory()) { // Skip if it's a dynamic route segment in the path (we'll check parent) if (!entry.name.startsWith('[')) { apiRoutes.push(routePath.replace(/\\/g, '/')); } scanDir(fullPath, routePath); } } } scanDir(apiDir); const missingRoutes = apiRoutes.filter(route => { const searchPattern = route.replace(/\\/g, '/'); return !readme.includes(searchPattern); }); if (missingRoutes.length > 0) { issuesFound.push({ section: 'API Routes', issues: missingRoutes.slice(0, 5).map(r => `Route "api/${r}" not documented`), hasMore: missingRoutes.length > 5 ? missingRoutes.length - 5 : 0, }); } } /** * Check 3: Main directories in app/ vs README */ function checkAppStructure() { const appDir = path.join(__dirname, '..', 'app'); if (!fs.existsSync(appDir)) return; const entries = fs.readdirSync(appDir, { withFileTypes: true }); const directories = entries .filter(e => e.isDirectory() && !e.name.startsWith('.')) .map(e => e.name); const missingDirs = directories.filter(dir => { // Check if directory is mentioned in project structure section const structureSection = readme.match(/## šŸ“‚ Projektstruktur[\s\S]*?(?=\n## |$)/); if (!structureSection) return true; return !structureSection[0].includes(`app/${dir}/`); }); if (missingDirs.length > 0) { issuesFound.push({ section: 'App Structure', issues: missingDirs.map(d => `Directory "app/${d}/" not in project structure`), }); } } /** * Check 4: Components vs README */ function checkComponents() { const componentsDir = path.join(__dirname, '..', 'components'); if (!fs.existsSync(componentsDir)) return; const components = []; function scanComponents(dir, prefix = '') { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { scanComponents(fullPath, path.join(prefix, entry.name)); } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) { const componentPath = path.join(prefix, entry.name).replace(/\\/g, '/'); components.push(componentPath); } } } scanComponents(componentsDir); const missingComponents = components.filter(comp => { return !readme.includes(comp); }); if (missingComponents.length > 0) { issuesFound.push({ section: 'Components', issues: missingComponents.slice(0, 3).map(c => `Component "${c}" not documented`), hasMore: missingComponents.length > 3 ? missingComponents.length - 3 : 0, }); } } /** * Check 5: Scripts vs README */ function checkScripts() { const scriptsDir = path.join(__dirname, '..', 'scripts'); if (!fs.existsSync(scriptsDir)) return; const scripts = fs.readdirSync(scriptsDir) .filter(f => f.endsWith('.js') || f.endsWith('.ts')); const missingScripts = scripts.filter(script => { return !readme.includes(script); }); if (missingScripts.length > 0) { issuesFound.push({ section: 'Scripts', issues: missingScripts.slice(0, 3).map(s => `Script "${s}" not in project structure`), hasMore: missingScripts.length > 3 ? missingScripts.length - 3 : 0, }); } } // Run all checks log('\nšŸ” Checking documentation...', 'cyan'); checkNpmScripts(); checkApiRoutes(); checkAppStructure(); checkComponents(); checkScripts(); // Report results if (issuesFound.length === 0) { log('\nāœ… Documentation is up to date!', 'green'); process.exit(0); } else { log('\nāš ļø Documentation may be outdated:', 'yellow'); log('', 'reset'); issuesFound.forEach(({ section, issues, hasMore }) => { log(` ${section}:`, 'yellow'); issues.forEach(issue => { log(` - ${issue}`, 'reset'); }); if (hasMore) { log(` ... and ${hasMore} more`, 'reset'); } log('', 'reset'); }); log('šŸ’” Run: npm run docs:sync', 'cyan'); log(' Or commit with: git commit --no-verify', 'cyan'); log('', 'reset'); process.exit(1); }