veza/veza-docs/scripts/discover_and_migrate_docs.ts

439 lines
13 KiB
TypeScript
Raw Normal View History

#!/usr/bin/env node
import * as fs from 'fs';
import * as path from 'path';
import { glob } from 'glob';
interface DocFile {
originalPath: string;
relativePath: string;
content: string;
frontmatter: Record<string, any>;
track: 'current' | 'vision';
domain: string;
slug: string;
title: string;
images: string[];
}
interface PathMapping {
[originalPath: string]: string;
}
interface MigrationReport {
totalFiles: number;
migratedFiles: number;
currentFiles: number;
visionFiles: number;
domainDistribution: Record<string, number>;
unclassifiedFiles: string[];
brokenLinks: string[];
pathMapping: PathMapping;
}
class DocMigrator {
private projectRoot: string;
private docsRoot: string;
private currentRoot: string;
private visionRoot: string;
private assetsRoot: string;
private report: MigrationReport;
private pathMapping: PathMapping = {};
constructor(projectRoot: string) {
this.projectRoot = projectRoot;
this.docsRoot = path.join(projectRoot, 'veza-docs');
this.currentRoot = path.join(this.docsRoot, 'current');
this.visionRoot = path.join(this.docsRoot, 'vision');
this.assetsRoot = path.join(this.docsRoot, 'current', 'assets');
this.report = {
totalFiles: 0,
migratedFiles: 0,
currentFiles: 0,
visionFiles: 0,
domainDistribution: {},
unclassifiedFiles: [],
brokenLinks: [],
pathMapping: {}
};
}
async discoverDocs(): Promise<DocFile[]> {
console.log('🔍 Découverte des fichiers de documentation...');
const patterns = [
'**/*.md',
'**/*.mdx'
];
const excludePatterns = [
'node_modules/**',
'target/**',
'dist/**',
'build/**',
'vendor/**',
'.git/**',
'veza-docs/**',
'veza-docs-backup/**',
'*.log',
'*.pid',
'**/node_modules/**',
'**/target/**',
'**/dist/**',
'**/build/**'
];
const allFiles: string[] = [];
for (const pattern of patterns) {
const files = await glob(pattern, {
cwd: this.projectRoot,
ignore: excludePatterns
});
allFiles.push(...files);
}
console.log(`📄 ${allFiles.length} fichiers de documentation trouvés`);
const docFiles: DocFile[] = [];
for (const file of allFiles) {
const fullPath = path.join(this.projectRoot, file);
const content = fs.readFileSync(fullPath, 'utf-8');
const docFile = this.parseDocFile(file, content);
if (docFile) {
docFiles.push(docFile);
}
}
return docFiles;
}
private parseDocFile(relativePath: string, content: string): DocFile | null {
const frontmatter = this.extractFrontmatter(content);
const track = this.determineTrack(relativePath, content, frontmatter);
const domain = this.determineDomain(relativePath, content, frontmatter);
const slug = this.generateSlug(relativePath);
const title = this.extractTitle(content, relativePath);
const images = this.extractImages(content);
return {
originalPath: path.join(this.projectRoot, relativePath),
relativePath,
content,
frontmatter,
track,
domain,
slug,
title,
images
};
}
private extractFrontmatter(content: string): Record<string, any> {
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
const match = content.match(frontmatterRegex);
if (!match) return {};
const frontmatterText = match[1];
const frontmatter: Record<string, any> = {};
frontmatterText.split('\n').forEach(line => {
const [key, ...valueParts] = line.split(':');
if (key && valueParts.length > 0) {
const value = valueParts.join(':').trim();
frontmatter[key.trim()] = value.replace(/^["']|["']$/g, '');
}
});
return frontmatter;
}
private determineTrack(relativePath: string, content: string, frontmatter: Record<string, any>): 'current' | 'vision' {
// Priorité au frontmatter
if (frontmatter.track === 'vision' || frontmatter.track === 'current') {
return frontmatter.track;
}
// Mots-clés dans le chemin
const visionKeywords = ['vision', 'target', 'future', 'roadmap', 'spec', 'cible', 'but visé'];
const pathLower = relativePath.toLowerCase();
if (visionKeywords.some(keyword => pathLower.includes(keyword))) {
return 'vision';
}
// Mots-clés dans le contenu
const contentLower = content.toLowerCase();
if (visionKeywords.some(keyword => contentLower.includes(keyword))) {
return 'vision';
}
// Par défaut, c'est l'état actuel
return 'current';
}
private determineDomain(relativePath: string, content: string, frontmatter: Record<string, any>): string {
// Priorité au frontmatter
if (frontmatter.domain) {
return frontmatter.domain;
}
const pathLower = relativePath.toLowerCase();
const contentLower = content.toLowerCase();
// Backend
if (pathLower.includes('backend') || pathLower.includes('api') || pathLower.includes('go') ||
contentLower.includes('backend') || contentLower.includes('api') || contentLower.includes('golang')) {
return 'backend';
}
// Frontend
if (pathLower.includes('frontend') || pathLower.includes('react') || pathLower.includes('ui') ||
pathLower.includes('vite') || contentLower.includes('frontend') || contentLower.includes('react')) {
return 'frontend';
}
// Rust
if (pathLower.includes('rust') || pathLower.includes('chat') || pathLower.includes('stream') ||
contentLower.includes('rust') || contentLower.includes('cargo')) {
return 'rust';
}
// Infrastructure
if (pathLower.includes('infra') || pathLower.includes('docker') || pathLower.includes('compose') ||
pathLower.includes('incus') || pathLower.includes('haproxy') || pathLower.includes('redis') ||
pathLower.includes('postgres') || contentLower.includes('infrastructure') || contentLower.includes('deployment')) {
return 'infra';
}
// Security
if (pathLower.includes('security') || pathLower.includes('auth') || pathLower.includes('waf') ||
pathLower.includes('coraza') || contentLower.includes('security') || contentLower.includes('auth')) {
return 'security';
}
// Operations
if (pathLower.includes('ops') || pathLower.includes('ci') || pathLower.includes('cd') ||
pathLower.includes('monitoring') || pathLower.includes('logs') || pathLower.includes('zfs') ||
contentLower.includes('operations') || contentLower.includes('monitoring')) {
return 'ops';
}
// Product
if (pathLower.includes('product') || pathLower.includes('ux') || pathLower.includes('personas') ||
contentLower.includes('product') || contentLower.includes('ux') || contentLower.includes('roadmap')) {
return 'product';
}
// Legal
if (pathLower.includes('legal') || pathLower.includes('cgu') || pathLower.includes('mentions') ||
contentLower.includes('legal') || contentLower.includes('cgu')) {
return 'legal';
}
return 'misc';
}
private generateSlug(relativePath: string): string {
const baseName = path.basename(relativePath, path.extname(relativePath));
return baseName
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
private extractTitle(content: string, relativePath: string): string {
// Chercher un titre H1
const h1Match = content.match(/^#\s+(.+)$/m);
if (h1Match) {
return h1Match[1].trim();
}
// Utiliser le nom du fichier
const baseName = path.basename(relativePath, path.extname(relativePath));
return baseName
.replace(/[-_]/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
}
private extractImages(content: string): string[] {
const imageRegex = /!\[.*?\]\((.*?)\)/g;
const images: string[] = [];
let match;
while ((match = imageRegex.exec(content)) !== null) {
images.push(match[1]);
}
return images;
}
async migrateDocs(docFiles: DocFile[]): Promise<void> {
console.log('📦 Migration des documents...');
for (const docFile of docFiles) {
await this.migrateDocFile(docFile);
}
// Copier les assets
await this.copyAssets(docFiles);
}
private async migrateDocFile(docFile: DocFile): Promise<void> {
const targetDir = docFile.track === 'current' ? this.currentRoot : this.visionRoot;
const domainDir = path.join(targetDir, 'domains', docFile.domain);
// Créer le répertoire si nécessaire
if (!fs.existsSync(domainDir)) {
fs.mkdirSync(domainDir, { recursive: true });
}
const targetPath = path.join(domainDir, `${docFile.slug}.md`);
// Préparer le contenu avec frontmatter
const frontmatter = {
id: docFile.slug,
title: docFile.title,
sidebar_label: docFile.title,
...docFile.frontmatter
};
const frontmatterText = Object.entries(frontmatter)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
const content = `---
${frontmatterText}
---
${docFile.content}`;
// Ajouter le badge de piste
const badge = docFile.track === 'current'
? '> NOTE: Cette page reflète l\'ÉTAT ACTUEL.'
: '> NOTE: Cette page décrit la CIBLE (but visé).';
const finalContent = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, `---\n${frontmatterText}\n---\n\n${badge}\n\n`);
fs.writeFileSync(targetPath, finalContent);
// Mettre à jour le mapping
this.pathMapping[docFile.relativePath] = `/${docFile.track}/domains/${docFile.domain}/${docFile.slug}`;
// Mettre à jour les statistiques
this.report.migratedFiles++;
if (docFile.track === 'current') {
this.report.currentFiles++;
} else {
this.report.visionFiles++;
}
if (!this.report.domainDistribution[docFile.domain]) {
this.report.domainDistribution[docFile.domain] = 0;
}
this.report.domainDistribution[docFile.domain]++;
console.log(`✅ Migré: ${docFile.relativePath}${docFile.track}/domains/${docFile.domain}/${docFile.slug}.md`);
}
private async copyAssets(docFiles: DocFile[]): Promise<void> {
console.log('🖼️ Copie des assets...');
const allImages = new Set<string>();
docFiles.forEach(doc => {
doc.images.forEach(img => allImages.add(img));
});
for (const imagePath of allImages) {
if (imagePath.startsWith('http')) continue; // Ignorer les URLs
const sourcePath = path.resolve(this.projectRoot, imagePath);
if (fs.existsSync(sourcePath)) {
const fileName = path.basename(imagePath);
const targetPath = path.join(this.assetsRoot, fileName);
fs.copyFileSync(sourcePath, targetPath);
console.log(`📷 Copié: ${imagePath} → assets/${fileName}`);
} else {
this.report.brokenLinks.push(imagePath);
console.log(`❌ Image manquante: ${imagePath}`);
}
}
}
async generateReport(): Promise<void> {
console.log('📊 Génération du rapport de migration...');
this.report.totalFiles = this.report.migratedFiles;
this.report.pathMapping = this.pathMapping;
const reportPath = path.join(this.docsRoot, '_reports', 'migration_report.md');
const pathMapPath = path.join(this.docsRoot, '_reports', 'path_map.json');
// Générer le rapport Markdown
const reportContent = `# Rapport de Migration de Documentation
## Résumé
- **Total des fichiers traités**: ${this.report.totalFiles}
- **Fichiers migrés**: ${this.report.migratedFiles}
- **État actuel**: ${this.report.currentFiles}
- **Vision**: ${this.report.visionFiles}
## Distribution par domaine
${Object.entries(this.report.domainDistribution)
.map(([domain, count]) => `- **${domain}**: ${count} fichiers`)
.join('\n')}
## Fichiers non classés
${this.report.unclassifiedFiles.length > 0
? this.report.unclassifiedFiles.map(file => `- ${file}`).join('\n')
: 'Aucun'}
## Liens brisés
${this.report.brokenLinks.length > 0
? this.report.brokenLinks.map(link => `- ${link}`).join('\n')
: 'Aucun'}
## Mapping des chemins
Le mapping complet des anciens vers les nouveaux chemins est disponible dans \`path_map.json\`.
`;
fs.writeFileSync(reportPath, reportContent);
// Générer le mapping JSON
fs.writeFileSync(pathMapPath, JSON.stringify(this.pathMapping, null, 2));
console.log(`📋 Rapport généré: ${reportPath}`);
console.log(`🗺️ Mapping généré: ${pathMapPath}`);
}
async run(): Promise<void> {
console.log('🚀 Démarrage de la migration de documentation...');
try {
const docFiles = await this.discoverDocs();
await this.migrateDocs(docFiles);
await this.generateReport();
console.log('✅ Migration terminée avec succès!');
} catch (error) {
console.error('❌ Erreur lors de la migration:', error);
process.exit(1);
}
}
}
// Exécution du script
if (require.main === module) {
const projectRoot = process.argv[2] || path.join(__dirname, '..', '..');
const migrator = new DocMigrator(projectRoot);
migrator.run();
}
export { DocMigrator };