310 lines
8.3 KiB
JavaScript
310 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
/**
|
|
* Crawler pour détecter les liens cassés dans la documentation Veza
|
|
* Génère un inventaire des slugs et détecte les liens cassés
|
|
*/
|
|
|
|
// Configuration
|
|
const DOCS_DIR = path.join(__dirname, '..', 'docs');
|
|
const META_DIR = path.join(__dirname, '..', 'meta');
|
|
const OUTPUT_CSV = path.join(META_DIR, 'broken-links.csv');
|
|
const SLUG_INVENTORY = path.join(META_DIR, 'slug-inventory.json');
|
|
|
|
// Patterns pour détecter les liens
|
|
const LINK_PATTERNS = [
|
|
// Liens internes Docusaurus
|
|
/\[([^\]]+)\]\(\/docs\/([^)]+)\)/g,
|
|
// Liens avec ancres
|
|
/\[([^\]]+)\]\(\/docs\/([^#)]+)(#[^)]+)?\)/g,
|
|
// Liens relatifs
|
|
/\[([^\]]+)\]\(([^)]+\.mdx?)(#[^)]+)?\)/g,
|
|
// Liens avec placeholders
|
|
/\[([^\]]+)\]\(\/docs\/([^{}]+)\{([^}]+)\}([^)]+)?\)/g,
|
|
];
|
|
|
|
// Inventaire des slugs existants
|
|
const slugInventory = new Map();
|
|
const brokenLinks = [];
|
|
|
|
/**
|
|
* Extrait tous les slugs des fichiers MDX
|
|
*/
|
|
function extractSlugs() {
|
|
console.log('🔍 Extraction des slugs...');
|
|
|
|
function scanDirectory(dir) {
|
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
|
|
for (const item of items) {
|
|
const fullPath = path.join(dir, item.name);
|
|
|
|
if (item.isDirectory()) {
|
|
scanDirectory(fullPath);
|
|
} else if (item.isFile() && (item.name.endsWith('.mdx') || item.name.endsWith('.md'))) {
|
|
const relativePath = path.relative(DOCS_DIR, fullPath);
|
|
const slug = relativePath
|
|
.replace(/\.mdx?$/, '')
|
|
.replace(/\\/g, '/');
|
|
|
|
// Ajouter différentes variantes du slug
|
|
slugInventory.set(slug, {
|
|
file: relativePath,
|
|
fullPath,
|
|
exists: true
|
|
});
|
|
|
|
// Ajouter la version avec index
|
|
if (item.name === 'index.mdx' || item.name === 'index.md') {
|
|
const parentSlug = path.dirname(slug);
|
|
if (parentSlug !== '.') {
|
|
slugInventory.set(parentSlug, {
|
|
file: relativePath,
|
|
fullPath,
|
|
exists: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
scanDirectory(DOCS_DIR);
|
|
console.log(`✅ ${slugInventory.size} slugs extraits`);
|
|
}
|
|
|
|
/**
|
|
* Analyse un fichier pour détecter les liens cassés
|
|
*/
|
|
function analyzeFile(filePath) {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const relativePath = path.relative(DOCS_DIR, filePath);
|
|
|
|
// Diviser le contenu en lignes pour détecter les blocs de code
|
|
const lines = content.split('\n');
|
|
let inCodeBlock = false;
|
|
let codeBlockType = '';
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
// Détecter le début d'un bloc de code
|
|
if (line.trim().startsWith('```')) {
|
|
if (!inCodeBlock) {
|
|
inCodeBlock = true;
|
|
codeBlockType = line.trim().substring(3);
|
|
} else {
|
|
inCodeBlock = false;
|
|
codeBlockType = '';
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Ignorer les lignes dans des blocs de code
|
|
if (inCodeBlock) {
|
|
continue;
|
|
}
|
|
|
|
// Analyser les liens dans cette ligne
|
|
for (const pattern of LINK_PATTERNS) {
|
|
let match;
|
|
while ((match = pattern.exec(line)) !== null) {
|
|
const [fullMatch, linkText, targetPath, anchor] = match;
|
|
|
|
// Nettoyer le chemin cible
|
|
let cleanPath = targetPath;
|
|
if (cleanPath.startsWith('/docs/')) {
|
|
cleanPath = cleanPath.substring(6); // Enlever /docs/
|
|
}
|
|
|
|
// Vérifier si le lien est cassé
|
|
const isBroken = !isValidLink(cleanPath, anchor);
|
|
|
|
if (isBroken) {
|
|
brokenLinks.push({
|
|
sourceFile: relativePath,
|
|
linkText: linkText.trim(),
|
|
targetPath: cleanPath,
|
|
anchor: anchor || '',
|
|
fullMatch,
|
|
line: i + 1,
|
|
type: detectLinkType(fullMatch)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie si un lien est valide
|
|
*/
|
|
function isValidLink(targetPath, anchor = '') {
|
|
// Nettoyer le chemin
|
|
let cleanPath = targetPath;
|
|
|
|
// Enlever les placeholders temporairement
|
|
cleanPath = cleanPath.replace(/\{[^}]+\}/g, 'PLACEHOLDER');
|
|
|
|
// Vérifier les variantes possibles
|
|
const variants = [
|
|
cleanPath,
|
|
cleanPath + '/index',
|
|
cleanPath.replace(/\/$/, ''),
|
|
cleanPath.replace(/\/$/, '') + '/index'
|
|
];
|
|
|
|
for (const variant of variants) {
|
|
if (slugInventory.has(variant)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Vérifier les liens externes
|
|
if (targetPath.startsWith('http') || targetPath.startsWith('mailto:')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Détecte le type de lien
|
|
*/
|
|
function detectLinkType(fullMatch) {
|
|
if (fullMatch.includes('{')) return 'placeholder';
|
|
if (fullMatch.includes('http')) return 'external';
|
|
if (fullMatch.includes('/docs/')) return 'internal';
|
|
return 'relative';
|
|
}
|
|
|
|
/**
|
|
* Obtient le numéro de ligne d'une correspondance
|
|
*/
|
|
function getLineNumber(content, match) {
|
|
const lines = content.substring(0, content.indexOf(match)).split('\n');
|
|
return lines.length;
|
|
}
|
|
|
|
/**
|
|
* Scanne tous les fichiers pour les liens cassés
|
|
*/
|
|
function scanBrokenLinks() {
|
|
console.log('🔍 Scan des liens cassés...');
|
|
|
|
function scanDirectory(dir) {
|
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
|
|
for (const item of items) {
|
|
const fullPath = path.join(dir, item.name);
|
|
|
|
if (item.isDirectory()) {
|
|
scanDirectory(fullPath);
|
|
} else if (item.isFile() && (item.name.endsWith('.mdx') || item.name.endsWith('.md'))) {
|
|
analyzeFile(fullPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
scanDirectory(DOCS_DIR);
|
|
console.log(`❌ ${brokenLinks.length} liens cassés détectés`);
|
|
}
|
|
|
|
/**
|
|
* Génère le CSV des liens cassés
|
|
*/
|
|
function generateCSV() {
|
|
console.log('📊 Génération du CSV...');
|
|
|
|
const csvHeader = 'Source File,Link Text,Target Path,Anchor,Type,Line,Full Match\n';
|
|
const csvRows = brokenLinks.map(link =>
|
|
`"${link.sourceFile}","${link.linkText}","${link.targetPath}","${link.anchor}","${link.type}",${link.line},"${link.fullMatch.replace(/"/g, '""')}"`
|
|
).join('\n');
|
|
|
|
const csvContent = csvHeader + csvRows;
|
|
fs.writeFileSync(OUTPUT_CSV, csvContent, 'utf8');
|
|
console.log(`✅ CSV généré: ${OUTPUT_CSV}`);
|
|
}
|
|
|
|
/**
|
|
* Génère l'inventaire des slugs
|
|
*/
|
|
function generateSlugInventory() {
|
|
console.log('📋 Génération de l\'inventaire des slugs...');
|
|
|
|
const inventory = {
|
|
generated: new Date().toISOString(),
|
|
totalSlugs: slugInventory.size,
|
|
slugs: Object.fromEntries(slugInventory)
|
|
};
|
|
|
|
fs.writeFileSync(SLUG_INVENTORY, JSON.stringify(inventory, null, 2), 'utf8');
|
|
console.log(`✅ Inventaire généré: ${SLUG_INVENTORY}`);
|
|
}
|
|
|
|
/**
|
|
* Génère un rapport de synthèse
|
|
*/
|
|
function generateReport() {
|
|
const report = {
|
|
timestamp: new Date().toISOString(),
|
|
totalSlugs: slugInventory.size,
|
|
brokenLinks: brokenLinks.length,
|
|
brokenLinksByType: brokenLinks.reduce((acc, link) => {
|
|
acc[link.type] = (acc[link.type] || 0) + 1;
|
|
return acc;
|
|
}, {}),
|
|
brokenLinksByFile: brokenLinks.reduce((acc, link) => {
|
|
acc[link.sourceFile] = (acc[link.sourceFile] || 0) + 1;
|
|
return acc;
|
|
}, {})
|
|
};
|
|
|
|
console.log('\n📊 RAPPORT DE SYNTHÈSE');
|
|
console.log('====================');
|
|
console.log(`Total slugs: ${report.totalSlugs}`);
|
|
console.log(`Liens cassés: ${report.brokenLinks}`);
|
|
console.log('\nPar type:');
|
|
Object.entries(report.brokenLinksByType).forEach(([type, count]) => {
|
|
console.log(` ${type}: ${count}`);
|
|
});
|
|
console.log('\nPar fichier:');
|
|
Object.entries(report.brokenLinksByFile).forEach(([file, count]) => {
|
|
console.log(` ${file}: ${count}`);
|
|
});
|
|
}
|
|
|
|
// Exécution principale
|
|
async function main() {
|
|
console.log('🚀 Démarrage du crawler Veza Docs');
|
|
console.log('==================================');
|
|
|
|
// Créer le répertoire meta s'il n'existe pas
|
|
if (!fs.existsSync(META_DIR)) {
|
|
fs.mkdirSync(META_DIR, { recursive: true });
|
|
}
|
|
|
|
try {
|
|
extractSlugs();
|
|
scanBrokenLinks();
|
|
generateSlugInventory();
|
|
generateCSV();
|
|
generateReport();
|
|
|
|
console.log('\n✅ Crawler terminé avec succès');
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.error('❌ Erreur lors du crawl:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|
|
|