438 lines
13 KiB
TypeScript
438 lines
13 KiB
TypeScript
#!/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 };
|