Files
location-mqtt-tracker-app/scripts/check-docs.js
Joachim Hummel 17aaf130a8 Add automatic documentation synchronization system
Implements Option C (Hybrid) solution to prevent outdated documentation:

New Features:
- Pre-commit git hook that validates documentation before every commit
- npm run docs:check - Validates README against current project state
- npm run docs:sync - Automatically updates NPM Scripts section in README

What gets checked:
- NPM Scripts in package.json vs README
- API routes in app/api/* vs README
- App structure (directories in app/) vs README
- Components vs README
- Scripts vs README

Workflow:
1. Make code changes
2. git commit triggers pre-commit hook
3. Hook warns if documentation is outdated
4. Run docs:sync to auto-update or edit manually
5. Commit with updated README

Benefits:
- No more forgetting to update README
- Non-blocking (can use --no-verify if needed)
- Automatic NPM scripts synchronization
- Clear warnings show exactly what needs updating

Scripts added:
- scripts/check-docs.js - Validation script
- scripts/sync-docs.js - Synchronization script
- .git/hooks/pre-commit - Git hook (not tracked)

Documentation:
- Added complete workflow section in README
- Examples and usage tips included

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 12:27:52 +00:00

217 lines
5.7 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
}