docstodev 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,34 +2,56 @@
2
2
  import { globby } from "globby";
3
3
  import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
4
4
  import path from "node:path";
5
- import puppeteer from "puppeteer";
5
+ import puppeteer, { Browser, Page } from "puppeteer";
6
6
  import { exportToHTML } from "../exporters/html.js";
7
7
  import { askAI } from "../ai/analyzer.js";
8
+ import { getAnalyzer, getSupportedExtensions } from "../analyzers/languageAnalyzer.js";
9
+ import { CacheManager } from "../cache/cacheManager.js";
8
10
 
9
- type TreeNode = { [key: string]: TreeNode | null };
11
+ type TreeNode = { [key: string]: TreeNode };
12
+
13
+ interface FileAnalysis {
14
+ path: string;
15
+ lines: number;
16
+ exports: string[];
17
+ // Ajout de '| undefined' pour supporter les propriétés optionnelles en mode strict
18
+ imports: Array<{ name: string; type: string; usage?: string | undefined }>;
19
+ functions: string[];
20
+ classes: string[];
21
+ types: string[];
22
+ complexity: number;
23
+ }
10
24
 
11
25
  const i18n = {
12
26
  fr: {
13
- role: "Vocation du fichier",
14
- density: "Densité et maintenance",
15
- exports: "Capacités offertes",
16
- imports: "Collaborations sollicitées",
17
- structure: "Architecture des dossiers",
18
- deps: "Inventaire des modules externes",
19
- aiTitle: "Synthèse Métier (par MakazouIA)",
27
+ role: "Rôle et responsabilités",
28
+ density: "Complexité et maintenance",
29
+ exports: "Exports publics",
30
+ imports: "Dépendances",
31
+ structure: "Architecture du projet",
32
+ deps: "Modules externes",
33
+ aiTitle: "Analyse Intelligente",
20
34
  genAt: "Généré le",
21
- reportTitle: "Rapport d'Analyse Technique DocsToDev"
35
+ reportTitle: "Rapport Technique DocsToDev",
36
+ functions: "Fonctions",
37
+ classes: "Classes",
38
+ types: "Types/Interfaces",
39
+ complexity: "Score de complexité"
22
40
  },
23
41
  en: {
24
- role: "File Purpose",
25
- density: "Density & Maintenance",
26
- exports: "Capabilities offered",
27
- imports: "External services",
42
+ role: "Role and responsibilities",
43
+ density: "Complexity & Maintenance",
44
+ exports: "Public exports",
45
+ imports: "Dependencies",
28
46
  structure: "Project Architecture",
29
- deps: "External Modules Inventory",
30
- aiTitle: "Business Insights (by MakazouIA)",
47
+ deps: "External Modules",
48
+ aiTitle: "Smart Analysis",
31
49
  genAt: "Generated on",
32
- reportTitle: "DocsToDev Technical Report"
50
+ reportTitle: "DocsToDev Technical Report",
51
+ functions: "Functions",
52
+ classes: "Classes",
53
+ types: "Types/Interfaces",
54
+ complexity: "Complexity score"
33
55
  }
34
56
  };
35
57
 
@@ -37,9 +59,11 @@ function buildTree(files: string[]): TreeNode {
37
59
  const root: TreeNode = {};
38
60
  for (const file of files) {
39
61
  const parts = file.split(path.sep);
40
- let current = root;
62
+ let current: TreeNode = root;
41
63
  for (const part of parts) {
42
- if (!current[part]) current[part] = {};
64
+ if (!current[part]) {
65
+ current[part] = {};
66
+ }
43
67
  current = current[part] as TreeNode;
44
68
  }
45
69
  }
@@ -57,100 +81,313 @@ function renderTree(tree: TreeNode, prefix = "", currentPath = ""): string {
57
81
  const isFile = !child || Object.keys(child).length === 0;
58
82
  const label = isFile ? `[${key}](../${fullPath.replace(/\\/g, '/')})` : `**${key}/**`;
59
83
  output += `${prefix}${connector}${label}\n`;
60
- if (child && Object.keys(child).length > 0) {
84
+ if (child && typeof child === 'object' && Object.keys(child).length > 0) {
61
85
  output += renderTree(child, prefix + (isLast ? " " : "│ "), fullPath);
62
86
  }
63
87
  });
64
88
  return output;
65
89
  }
66
90
 
67
- export async function runCommand(language: "fr" | "en" = "fr"): Promise<number> {
68
- const t = i18n[language];
69
- console.log(language === "fr" ? "🚀 DocsToDev : Analyse Deep Scan & Graphe..." : "🚀 DocsToDev: Deep Scan & Graph analysis...");
91
+ function analyzeFile(file: string, content: string): FileAnalysis {
92
+ const lines = content.split(/\r?\n/);
93
+ const analyzer = getAnalyzer(file);
94
+
95
+ if (analyzer) {
96
+ const result = analyzer.analyzeFile(content);
97
+
98
+ const ifCount = (content.match(/\b(if|elif|else if)\s*\(/g) || []).length;
99
+ const forCount = (content.match(/\b(for|foreach|while)\s*\(/g) || []).length;
100
+ const whileCount = (content.match(/\bwhile\s*\(/g) || []).length;
101
+
102
+ const complexity =
103
+ lines.length * 0.1 +
104
+ result.functions.length * 2 +
105
+ result.classes.length * 3 +
106
+ result.imports.length * 1.5 +
107
+ ifCount * 1 +
108
+ forCount * 1.5 +
109
+ whileCount * 1.5;
110
+
111
+ return {
112
+ path: file,
113
+ lines: lines.length,
114
+ exports: result.exports,
115
+ imports: result.imports,
116
+ functions: result.functions,
117
+ classes: result.classes,
118
+ types: result.types,
119
+ complexity: Math.round(complexity)
120
+ };
121
+ }
122
+
123
+ return {
124
+ path: file,
125
+ lines: lines.length,
126
+ exports: [],
127
+ imports: [],
128
+ functions: [],
129
+ classes: [],
130
+ types: [],
131
+ complexity: 0
132
+ };
133
+ }
134
+
135
+ function getComplexityLevel(score: number): string {
136
+ if (score < 50) return "🟢 Faible";
137
+ if (score < 150) return "🟡 Modérée";
138
+ if (score < 300) return "🟠 Élevée";
139
+ return "🔴 Très élevée";
140
+ }
70
141
 
142
+ function formatAISummary(summary: string): string {
143
+ let formatted = summary.replace(/([a-zA-Z0-9_-]+\.(ts|js|py|java|cs|go|rs|tsx|jsx|mjs|cjs))/g, '`$1`');
144
+ formatted = formatted.replace(/([a-zA-Z0-9_-]+\/[a-zA-Z0-9_\/-]+)/g, '`$1`');
145
+ formatted = formatted.replace(/\b([a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*|[A-Z][a-z][a-zA-Z0-9]*)\b/g, '`$1`');
146
+ formatted = formatted.replace(/\.\s+/g, '.\n\n');
147
+ formatted = formatted.replace(/`+/g, '`');
148
+ return formatted.trim();
149
+ }
150
+
151
+ export async function runCommand(
152
+ language: "fr" | "en" = "fr",
153
+ options: { incremental?: boolean; clearCache?: boolean } = {}
154
+ ): Promise<number> {
155
+ const t = i18n[language];
156
+
157
+ console.log("\n╔══════════════════════════════════════════╗");
158
+ console.log("║ 🚀 DocsToDev - Analyse Avancée ║");
159
+ console.log("╚══════════════════════════════════════════╝\n");
160
+
161
+ const projectRoot = process.cwd();
162
+ const cache = new CacheManager(projectRoot);
163
+
164
+ if (options.clearCache) {
165
+ console.log("🗑️ Nettoyage du cache...");
166
+ cache.invalidateAll();
167
+ console.log(" ✓ Cache vidé\n");
168
+ }
169
+
170
+ console.log("📂 Étape 1/6 : Scan des fichiers du projet...");
171
+ const supportedExts = getSupportedExtensions();
71
172
  const files = await globby(["**/*"], {
72
173
  gitignore: true,
73
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/docs/**"]
174
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/docs/**", "**/.docstodev/**"]
74
175
  });
176
+
177
+ const sourceFiles = files.filter(f => {
178
+ const ext = path.extname(f);
179
+ return supportedExts.includes(ext);
180
+ });
181
+
182
+ console.log(` ✓ ${files.length} fichiers détectés (${sourceFiles.length} fichiers sources)\n`);
75
183
 
76
184
  const docsDir = "docs";
77
- if (!existsSync(docsDir)) mkdirSync(docsDir, { recursive: true });
185
+ if (!existsSync(docsDir)) {
186
+ console.log("📁 Création du dossier 'docs'...");
187
+ mkdirSync(docsDir, { recursive: true });
188
+ }
189
+
190
+ let filesToAnalyze = sourceFiles;
191
+ let cachedCount = 0;
192
+
193
+ if (options.incremental) {
194
+ console.log("⚡ Mode incrémental activé - Détection des modifications...");
195
+ const modifiedFiles = cache.getModifiedFiles(sourceFiles);
196
+ filesToAnalyze = modifiedFiles;
197
+ cachedCount = sourceFiles.length - modifiedFiles.length;
198
+ console.log(` ✓ ${modifiedFiles.length} fichiers modifiés, ${cachedCount} en cache\n`);
199
+ }
78
200
 
79
- let aiContext = `Analyse ces fichiers pour DocsToDev. Explique pourquoi ils utilisent leurs dépendances. Format: "Utilise [X] pour [Y]".\n\n`;
201
+ console.log("🔍 Étape 2/6 : Analyse approfondie des fichiers sources...");
202
+ let aiContext = `Tu es un expert en analyse de code. Analyse ces fichiers et explique leur architecture, leurs responsabilités et comment ils collaborent ensemble. Identifie le rôle de chaque fichier (Page, Component, Service, API, Utility, etc.). Sois précis et technique.\n\n`;
80
203
  let detailContent = `## 🔬 Analyse détaillée des composants\n\n`;
81
- let mermaidGraph = "";
204
+ let mermaidGraph = "graph LR\n";
82
205
  const allDeps = new Map<string, string>();
83
-
84
- for (const file of files) {
85
- const ext = path.extname(file);
86
- if (![".ts", ".js", ".tsx", ".jsx"].includes(ext)) continue;
206
+ const analyses: FileAnalysis[] = [];
207
+
208
+ let processedCount = 0;
209
+
210
+ if (options.incremental) {
211
+ for (const file of sourceFiles) {
212
+ if (!filesToAnalyze.includes(file)) {
213
+ const cached = cache.get(file);
214
+ if (cached) {
215
+ analyses.push(cached.analysis);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ for (const file of filesToAnalyze) {
222
+ processedCount++;
223
+ if (processedCount % 5 === 0) {
224
+ process.stdout.write(` Analysé: ${processedCount}/${filesToAnalyze.length} fichiers...\r`);
225
+ }
87
226
 
88
227
  const content = readFileSync(file, "utf-8");
89
- const lines = content.split(/\r?\n/);
90
- const fileNameSanitized = path.basename(file).replace(/\./g, '_');
91
-
92
- const exports = lines
93
- .filter(l => l.startsWith("export "))
94
- .map(l => l.match(/(?:function|const|class|type|interface)\s+([a-zA-Z0-9_]+)/)?.[1])
95
- .filter(Boolean) as string[];
96
-
97
- const importLines = lines.filter(l => l.trim().startsWith("import "));
98
-
99
- detailContent += `### 📄 [\`${file}\`](../${file.replace(/\\/g, '/')})\n`;
100
- detailContent += `• **${t.density} :** ${lines.length} lignes\n`;
101
- if (exports.length > 0) detailContent += `• **${t.exports} :** ${exports.map(e => `\`${e}\``).join(", ")}\n`;
102
-
103
- if (importLines.length > 0) {
104
- detailContent += `• **${t.imports} :**\n`;
105
- importLines.forEach(line => {
106
- const match = line.match(/from ['"]([^'"]+)['"]/);
107
- if (match?.[1]) {
108
- const name = match[1];
109
- const type = name.startsWith('.') ? 'Internal' : (name.startsWith('node:') ? 'Node.js' : 'External');
110
- allDeps.set(name, type);
111
- detailContent += ` - Utilise \`${name}\` (${type})\n`;
112
-
113
- if (type === 'Internal') {
114
- const targetName = path.basename(name).replace(/\./g, '_');
115
- mermaidGraph += ` ${fileNameSanitized} --> ${targetName}\n`;
116
- }
228
+ const analysis = analyzeFile(file, content);
229
+ analyses.push(analysis);
230
+
231
+ if (options.incremental) {
232
+ cache.set(file, content, analysis);
233
+ }
234
+
235
+ const fileNameSanitized = path.basename(file).replace(/[.\-]/g, '_');
236
+
237
+ detailContent += `### 📄 [\`${file}\`](../${file.replace(/\\/g, '/')})\n\n`;
238
+ detailContent += `**${t.density}** : ${analysis.lines} lignes • ${getComplexityLevel(analysis.complexity)} (${analysis.complexity})\n\n`;
239
+
240
+ if (analysis.functions.length > 0) {
241
+ detailContent += `**${t.functions}** : ${analysis.functions.map(f => `\`${f}()\``).join(", ")}\n\n`;
242
+ }
243
+
244
+ if (analysis.classes.length > 0) {
245
+ detailContent += `**${t.classes}** : ${analysis.classes.map(c => `\`${c}\``).join(", ")}\n\n`;
246
+ }
247
+
248
+ if (analysis.types.length > 0) {
249
+ detailContent += `**${t.types}** : ${analysis.types.map(t => `\`${t}\``).join(", ")}\n\n`;
250
+ }
251
+
252
+ if (analysis.exports.length > 0) {
253
+ detailContent += `**${t.exports}** : ${analysis.exports.map(e => `\`${e}\``).join(", ")}\n\n`;
254
+ }
255
+
256
+ if (analysis.imports.length > 0) {
257
+ detailContent += `**${t.imports}** :\n\n`;
258
+ analysis.imports.forEach(imp => {
259
+ allDeps.set(imp.name, imp.type);
260
+ const usageInfo = imp.usage ? ` → utilise \`${imp.usage}\`` : '';
261
+ detailContent += `• \`${imp.name}\` (${imp.type})${usageInfo}\n`;
262
+
263
+ if (imp.type === 'Interne') {
264
+ const targetName = path.basename(imp.name).replace(/[.\-]/g, '_');
265
+ mermaidGraph += ` ${fileNameSanitized} --> ${targetName}\n`;
117
266
  }
118
267
  });
268
+ detailContent += `\n`;
119
269
  }
120
- detailContent += `\n---\n\n`;
121
- aiContext += `FICHIER: ${file}\nEXPORTS: ${exports.join(",")}\nCODE:\n${lines.slice(0, 40).join("\n")}\n\n`;
270
+
271
+ detailContent += `---\n\n`;
272
+
273
+ aiContext += `═══ FICHIER: ${file} ═══\n`;
274
+ aiContext += `Langage: ${path.extname(file)}\n`;
275
+ aiContext += `Complexité: ${analysis.complexity}\n`;
276
+ aiContext += `Fonctions: ${analysis.functions.join(", ") || "Aucune"}\n`;
277
+ aiContext += `Classes: ${analysis.classes.join(", ") || "Aucune"}\n`;
278
+ aiContext += `Exports: ${analysis.exports.join(", ") || "Aucun"}\n`;
279
+ aiContext += `Dépendances: ${analysis.imports.map(i => i.name).join(", ")}\n`;
280
+ aiContext += `\nExtrait du code:\n${content.split(/\r?\n/).slice(0, 50).join("\n")}\n\n`;
122
281
  }
282
+
283
+ const totalAnalyzed = processedCount + cachedCount;
284
+ console.log(` ✓ ${totalAnalyzed} fichiers analysés (${processedCount} nouvelles analyses, ${cachedCount} depuis cache)\n`);
123
285
 
124
- const aiSummary = await askAI(aiContext) || "Synthèse IA indisponible.";
286
+ console.log("🤖 Étape 3/6 : Génération de l'analyse intelligente (IA)...");
287
+ const aiSummary = await askAI(aiContext) || "⚠️ Analyse IA indisponible. Vérifiez votre configuration.";
288
+ const formattedAiSummary = formatAISummary(aiSummary);
289
+ console.log(` ✓ Synthèse IA générée\n`);
125
290
 
291
+ console.log("📝 Étape 4/6 : Construction du rapport Markdown...");
126
292
  let finalMD = `# ${t.reportTitle}\n\n`;
127
- finalMD += `> 📅 ${t.genAt} : ${new Date().toLocaleString()}\n\n`;
128
- finalMD += `## 💡 ${t.aiTitle}\n${aiSummary}\n\n`;
129
- finalMD += `## 📂 ${t.structure}\n\n${renderTree(buildTree(files))}\n\n`;
293
+ finalMD += `> 📅 ${t.genAt} : ${new Date().toLocaleString("fr-FR", { dateStyle: "full", timeStyle: "short" })}\n`;
294
+ finalMD += `> 📊 ${totalAnalyzed} fichiers analysés • ${allDeps.size} dépendances identifiées\n`;
295
+ if (options.incremental && cachedCount > 0) {
296
+ finalMD += `> ⚡ Mode incrémental : ${cachedCount} fichiers depuis cache\n`;
297
+ }
298
+ finalMD += `\n`;
299
+ finalMD += `## 💡 ${t.aiTitle}\n\n${formattedAiSummary}\n\n`;
300
+ finalMD += `## 📂 ${t.structure}\n\n`;
301
+ finalMD += "```\n";
302
+ finalMD += renderTree(buildTree(files));
303
+ finalMD += "```\n\n";
130
304
  finalMD += detailContent;
131
- finalMD += `## 📦 ${t.deps}\n\n| Module | Type |\n| :--- | :--- |\n`;
132
- Array.from(allDeps.entries()).sort().forEach(([n, ty]) => finalMD += `| \`${n}\` | ${ty} |\n`);
305
+ finalMD += `## 📦 ${t.deps}\n\n`;
306
+ finalMD += `| Module | Type | Occurrences |\n`;
307
+ finalMD += `| :--- | :--- | :---: |\n`;
308
+
309
+ const depCount = new Map<string, number>();
310
+ analyses.forEach(a => {
311
+ a.imports.forEach(imp => {
312
+ depCount.set(imp.name, (depCount.get(imp.name) || 0) + 1);
313
+ });
314
+ });
315
+
316
+ Array.from(allDeps.entries())
317
+ .sort(([a], [b]) => a.localeCompare(b))
318
+ .forEach(([name, type]) => {
319
+ finalMD += `| \`${name}\` | ${type} | ${depCount.get(name) || 1} |\n`;
320
+ });
133
321
 
134
322
  writeFileSync(path.join(docsDir, "docs-to-dev.md"), finalMD);
135
- exportToHTML(docsDir, finalMD, mermaidGraph, language);
323
+ console.log(` ✓ Rapport Markdown généré : docs/docs-to-dev.md\n`);
324
+
325
+ console.log("🎨 Étape 5/6 : Export HTML avec graphiques...");
326
+ const fileTreeStructure = buildTree(files);
327
+ exportToHTML(docsDir, finalMD, mermaidGraph, language, fileTreeStructure);
328
+ console.log(` ✓ Rapport HTML généré : docs/report.html\n`);
136
329
 
137
- console.log("📄 Génération du PDF automatique...");
330
+ console.log("📄 Étape 6/6 : Génération du PDF...");
138
331
  await generatePDF(path.join(docsDir, "report.html"), path.join(docsDir, "report.pdf"));
332
+ console.log(` ✓ PDF exporté : docs/report.pdf\n`);
333
+
334
+ console.log("╔══════════════════════════════════════════╗");
335
+ console.log("║ ✨ Analyse terminée avec succès ! ║");
336
+ console.log("╚══════════════════════════════════════════╝\n");
337
+ console.log(`📊 Statistiques :`);
338
+ console.log(` • ${files.length} fichiers scannés`);
339
+ console.log(` • ${totalAnalyzed} fichiers analysés`);
340
+ if (options.incremental) {
341
+ console.log(` • ${cachedCount} fichiers depuis cache`);
342
+ }
343
+ console.log(` • ${allDeps.size} dépendances uniques`);
344
+ const avgComp = analyses.length > 0 ? Math.round(analyses.reduce((sum, a) => sum + a.complexity, 0) / analyses.length) : 0;
345
+ console.log(` • Complexité moyenne : ${avgComp}`);
346
+
347
+ if (options.incremental) {
348
+ const cacheStats = cache.getStats();
349
+ console.log(`\n💾 Cache :`);
350
+ console.log(` • ${cacheStats.total} entrées`);
351
+ console.log(` • ${Math.round(cacheStats.size / 1024)} KB`);
352
+ }
353
+ console.log("");
139
354
 
140
355
  return files.length;
141
356
  }
142
357
 
143
- async function generatePDF(htmlPath: string, outputPath: string) {
358
+ async function generatePDF(htmlPath: string, outputPath: string): Promise<void> {
359
+ let browser: Browser | null = null;
144
360
  try {
145
- const browser = await puppeteer.launch({ headless: true });
146
- const page = await browser.newPage();
147
- await page.goto(`file://${path.resolve(htmlPath)}`, { waitUntil: "networkidle0" });
361
+ browser = await puppeteer.launch({
362
+ headless: true,
363
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
364
+ });
365
+ const page: Page = await browser.newPage();
366
+
367
+ await page.goto(`file://${path.resolve(htmlPath)}`, {
368
+ waitUntil: "networkidle0",
369
+ timeout: 30000
370
+ });
371
+
372
+ await page.waitForFunction(() => {
373
+ return typeof (window as any).mermaid !== 'undefined';
374
+ }, { timeout: 5000 }).catch(() => {});
375
+
376
+ await new Promise(resolve => setTimeout(resolve, 2000));
377
+
148
378
  await page.pdf({
149
379
  path: outputPath,
150
380
  format: 'A4',
151
381
  printBackground: true,
152
- margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' }
382
+ margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
383
+ preferCSSPageSize: true
153
384
  });
385
+
154
386
  await browser.close();
155
- } catch (e) { console.error("Erreur PDF:", e); }
387
+ } catch (e) {
388
+ console.error(" ⚠️ Erreur lors de la génération du PDF:", e);
389
+ if (browser) {
390
+ await browser.close().catch(() => {});
391
+ }
392
+ }
156
393
  }