UNPKG

vibe-janitor

Version:

A CLI tool that cleans AI-generated JavaScript/TypeScript projects efficiently and intelligently

453 lines (452 loc) โ€ข 21.2 kB
import fs from 'fs-extra'; import path from 'path'; import { Logger } from './logger.js'; /** * Generates human-readable and machine-readable reports of cleaning results */ export class Reporter { options; reportDir; constructor(options = {}) { this.options = { outputPath: options.outputPath ?? 'vibe-janitor-report', generateJson: options.generateJson ?? true, generateMarkdown: options.generateMarkdown ?? true, verbose: options.verbose ?? false, }; // Ensure we always use the vibe-janitor-report directory regardless of the output path this.reportDir = 'vibe-janitor-report'; } /** * Ensure report directory exists */ async ensureReportDirectory() { try { await fs.ensureDir(this.reportDir); if (this.options.verbose) { Logger.info(`Report directory created: ${this.reportDir}`); } } catch (error) { Logger.error(`Failed to create report directory: ${error instanceof Error ? error.message : String(error)}`); } } /** * Generate a console summary of the cleaning results */ generateConsoleSummary(cleanerResult, assetResult, showDetailed = false, styleResult) { Logger.log('\n๐Ÿ“ Cleanup Summary:'); // Cleaner results const totalImports = cleanerResult.unusedImports.reduce((acc, item) => acc + item.imports.length, 0); const totalVariables = cleanerResult.unusedVariables.reduce((acc, item) => acc + item.variables.length, 0); const totalFunctions = cleanerResult.unusedFunctions.reduce((acc, item) => acc + item.functions.length, 0); Logger.log(` - Unused imports: ${totalImports} across ${cleanerResult.unusedImports.length} files`); // Show detailed imports info if requested if (showDetailed && cleanerResult.unusedImports.length > 0) { Logger.log('\n ๐Ÿ“‹ Unused imports details:'); cleanerResult.unusedImports.forEach((file) => { const relativePath = file.file.split('/').slice(-3).join('/'); // Show last 3 path segments for brevity Logger.log(` - ${relativePath} (${file.imports.length} unused):`); file.imports.forEach((importName) => { Logger.log(` โ€ข ${importName}`); }); }); if (!cleanerResult.modifiedFiles.length) { Logger.log('\n ๐Ÿ’ก To fix these issues, run: npx vibe-janitor --remove-unused'); } } Logger.log(` - Unused variables: ${totalVariables} across ${cleanerResult.unusedVariables.length} files`); // Show detailed variables info if requested if (showDetailed && cleanerResult.unusedVariables.length > 0) { Logger.log('\n ๐Ÿ“‹ Unused variables details:'); cleanerResult.unusedVariables.forEach((file) => { const relativePath = file.file.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath} (${file.variables.length} unused):`); file.variables.forEach((varName) => { Logger.log(` โ€ข ${varName}`); }); }); } Logger.log(` - Unused functions: ${totalFunctions} across ${cleanerResult.unusedFunctions.length} files`); // Show detailed functions info if requested if (showDetailed && cleanerResult.unusedFunctions.length > 0) { Logger.log('\n ๐Ÿ“‹ Unused functions details:'); cleanerResult.unusedFunctions.forEach((file) => { const relativePath = file.file.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath} (${file.functions.length} unused):`); file.functions.forEach((funcName) => { Logger.log(` โ€ข ${funcName}`); }); }); } Logger.log(` - Potentially unused files: ${cleanerResult.unusedFiles.length}`); if (cleanerResult.unusedFilesSize > 0) { Logger.log(` - Potential space savings from unused files: ${this.formatSize(cleanerResult.unusedFilesSize)}`); } // Show detailed unused files if requested if (showDetailed && cleanerResult.unusedFiles.length > 0) { Logger.log('\n ๐Ÿ“‹ Potentially unused files:'); cleanerResult.unusedFiles.forEach((file) => { const relativePath = file.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath}`); }); } if (cleanerResult.deletedFiles && cleanerResult.deletedFiles.length > 0) { Logger.log(` - Deleted ${cleanerResult.deletedFiles.length} unused files`); // Show deleted files if requested if (showDetailed) { Logger.log('\n ๐Ÿ“‹ Deleted files:'); cleanerResult.deletedFiles.forEach((file) => { const relativePath = file.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath}`); }); } } // Asset results if (assetResult) { Logger.log(`\n๐Ÿ–ผ๏ธ Asset Analysis:`); Logger.log(` - Unused images: ${assetResult.unusedImages.length}`); Logger.log(` - Unused fonts: ${assetResult.unusedFonts.length}`); Logger.log(` - Unused style files: ${assetResult.unusedStyles.length}`); // Format size const size = this.formatSize(assetResult.totalSize); Logger.log(` - Total potential space savings: ${size}`); if (assetResult.deletedAssets.length > 0) { Logger.log(` - Deleted ${assetResult.deletedAssets.length} assets`); } // Show detailed asset info if requested if (showDetailed && (assetResult.unusedImages.length > 0 || assetResult.unusedFonts.length > 0 || assetResult.unusedStyles.length > 0)) { if (assetResult.unusedImages.length > 0) { Logger.log('\n ๐Ÿ“‹ Unused images:'); assetResult.unusedImages.forEach((image) => { const relativePath = image.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath}`); }); } if (assetResult.unusedFonts.length > 0) { Logger.log('\n ๐Ÿ“‹ Unused fonts:'); assetResult.unusedFonts.forEach((font) => { const relativePath = font.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath}`); }); } if (assetResult.unusedStyles.length > 0) { Logger.log('\n ๐Ÿ“‹ Unused style files:'); assetResult.unusedStyles.forEach((style) => { const relativePath = style.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath}`); }); } if (!assetResult.deletedAssets.length) { Logger.log('\n ๐Ÿ’ก To remove these unused assets, run: npx vibe-janitor --deep-scrub --remove-unused'); } } } // Show general help if issues were found but not fixed if ((totalImports > 0 || totalVariables > 0 || totalFunctions > 0 || cleanerResult.unusedFiles.length > 0 || (assetResult && (assetResult.unusedImages.length > 0 || assetResult.unusedFonts.length > 0 || assetResult.unusedStyles.length > 0))) && cleanerResult.modifiedFiles.length === 0 && (!assetResult || assetResult.deletedAssets.length === 0) && !showDetailed) { Logger.log('\n๐Ÿ’ก For detailed information on these issues, run: npx vibe-janitor --list'); Logger.log('๐Ÿ’ก To automatically fix these issues, run: npx vibe-janitor --remove-unused'); } // Style cleaning results if (styleResult) { Logger.log(`\n๐ŸŽจ CSS Style Analysis:`); Logger.log(` - Analyzed CSS files: ${styleResult.analyzedFiles}`); Logger.log(` - Total CSS selectors found: ${styleResult.totalSelectorsFound}`); Logger.log(` - Unused CSS selectors: ${styleResult.totalUnusedSelectors}`); if (styleResult.modifiedFiles.length > 0) { Logger.log(` - Cleaned ${styleResult.modifiedFiles.length} CSS files`); } // Show detailed style info if requested if (showDetailed && styleResult.unusedSelectors.length > 0) { Logger.log('\n ๐Ÿ“‹ Unused CSS selectors details:'); styleResult.unusedSelectors.forEach((file) => { const relativePath = file.file.split('/').slice(-3).join('/'); Logger.log(` - ${relativePath} (${file.selectors.length} unused):`); // Show up to 10 selectors to avoid flooding the console const MAX_SELECTORS_TO_SHOW = 10; file.selectors.slice(0, MAX_SELECTORS_TO_SHOW).forEach((selector) => { Logger.log(` โ€ข ${selector}`); }); if (file.selectors.length > MAX_SELECTORS_TO_SHOW) { Logger.log(` โ€ข ... and ${file.selectors.length - MAX_SELECTORS_TO_SHOW} more`); } }); if (styleResult.modifiedFiles.length === 0) { Logger.log('\n ๐Ÿ’ก To remove unused CSS selectors, run: npx vibe-janitor --clean-styles --remove-unused'); } } } } /** * Format file size in a human-readable way */ formatSize(bytes) { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } /** * Generate JSON report file */ async generateJsonReport(cleanerResult, assetResult, styleResult) { if (!this.options.generateJson) { return ''; } // Ensure report directory exists await this.ensureReportDirectory(); const report = { timestamp: new Date().toISOString(), codeCleanup: { unusedImports: cleanerResult.unusedImports, unusedVariables: cleanerResult.unusedVariables, unusedFunctions: cleanerResult.unusedFunctions, unusedFiles: cleanerResult.unusedFiles, modifiedFiles: cleanerResult.modifiedFiles, }, assetCleanup: assetResult ? { unusedImages: assetResult.unusedImages, unusedFonts: assetResult.unusedFonts, unusedStyles: assetResult.unusedStyles, totalSize: assetResult.totalSize, deletedAssets: assetResult.deletedAssets, } : undefined, styleCleaning: styleResult ? { analyzedFiles: styleResult.analyzedFiles, totalSelectorsFound: styleResult.totalSelectorsFound, unusedSelectors: styleResult.unusedSelectors, totalUnusedSelectors: styleResult.totalUnusedSelectors, modifiedFiles: styleResult.modifiedFiles, bytesRemoved: styleResult.bytesRemoved, } : undefined, }; // Create filename with proper prefix in the report directory const outputPath = this.options.outputPath ?? 'vibe-janitor-report'; const filename = `${path.basename(outputPath)}-main.json`; const reportPath = path.join(this.reportDir, filename); try { await fs.outputJson(reportPath, report, { spaces: 2 }); if (this.options.verbose) { Logger.success(`JSON report saved to: ${reportPath}`); } return reportPath; } catch (error) { Logger.error(`Failed to generate JSON report: ${error instanceof Error ? error.message : String(error)}`); return ''; } } /** * Generate Markdown report file */ async generateMarkdownReport(cleanerResult, assetResult, styleResult) { if (!this.options.generateMarkdown) { return ''; } // Ensure report directory exists await this.ensureReportDirectory(); // Build the report content let markdown = `# Vibe Janitor Cleanup Report\n\n`; markdown += `Generated: ${new Date().toLocaleString()}\n\n`; // Code cleanup section markdown += `## ๐Ÿงน Code Cleanup\n\n`; // Unused imports const totalImports = cleanerResult.unusedImports.reduce((acc, item) => acc + item.imports.length, 0); markdown += `### Unused Imports (${totalImports} total)\n\n`; if (cleanerResult.unusedImports.length > 0) { for (const file of cleanerResult.unusedImports) { markdown += `- **${file.file}**\n`; for (const importName of file.imports) { markdown += ` - \`${importName}\`\n`; } } } else { markdown += `No unused imports found.\n`; } markdown += '\n'; // Unused variables const totalVariables = cleanerResult.unusedVariables.reduce((acc, item) => acc + item.variables.length, 0); markdown += `### Unused Variables (${totalVariables} total)\n\n`; if (cleanerResult.unusedVariables.length > 0) { for (const file of cleanerResult.unusedVariables) { markdown += `- **${file.file}**\n`; for (const varName of file.variables) { markdown += ` - \`${varName}\`\n`; } } } else { markdown += `No unused variables found.\n`; } markdown += '\n'; // Unused functions const totalFunctions = cleanerResult.unusedFunctions.reduce((acc, item) => acc + item.functions.length, 0); markdown += `### Unused Functions (${totalFunctions} total)\n\n`; if (cleanerResult.unusedFunctions.length > 0) { for (const file of cleanerResult.unusedFunctions) { markdown += `- **${file.file}**\n`; for (const funcName of file.functions) { markdown += ` - \`${funcName}\`\n`; } } } else { markdown += `No unused functions found.\n`; } markdown += '\n'; // Unused files markdown += `### Potentially Unused Files (${cleanerResult.unusedFiles.length} total)\n\n`; if (cleanerResult.unusedFiles.length > 0) { for (const file of cleanerResult.unusedFiles) { markdown += `- ${file}\n`; } if (cleanerResult.unusedFilesSize > 0) { markdown += `\n**Potential space savings:** ${this.formatSize(cleanerResult.unusedFilesSize)}\n`; } } else { markdown += `No unused files found.\n`; } markdown += '\n'; // Deleted files if (cleanerResult.deletedFiles && cleanerResult.deletedFiles.length > 0) { markdown += `### Deleted Files (${cleanerResult.deletedFiles.length} total)\n\n`; for (const file of cleanerResult.deletedFiles) { markdown += `- ${file}\n`; } markdown += '\n'; } // Asset cleanup section if (assetResult) { markdown += `## ๐Ÿ–ผ๏ธ Asset Cleanup\n\n`; // Unused images markdown += `### Unused Images (${assetResult.unusedImages.length} total)\n\n`; if (assetResult.unusedImages.length > 0) { for (const image of assetResult.unusedImages) { markdown += `- ${image}\n`; } } else { markdown += `No unused images found.\n`; } markdown += '\n'; // Unused fonts markdown += `### Unused Fonts (${assetResult.unusedFonts.length} total)\n\n`; if (assetResult.unusedFonts.length > 0) { for (const font of assetResult.unusedFonts) { markdown += `- ${font}\n`; } } else { markdown += `No unused fonts found.\n`; } markdown += '\n'; // Unused styles markdown += `### Unused Style Files (${assetResult.unusedStyles.length} total)\n\n`; if (assetResult.unusedStyles.length > 0) { for (const style of assetResult.unusedStyles) { markdown += `- ${style}\n`; } } else { markdown += `No unused style files found.\n`; } markdown += '\n'; // Total size markdown += `### Total Potential Space Savings\n\n`; markdown += `- ${this.formatSize(assetResult.totalSize)}\n\n`; // Deleted assets if (assetResult.deletedAssets.length > 0) { markdown += `### Deleted Assets (${assetResult.deletedAssets.length} total)\n\n`; for (const asset of assetResult.deletedAssets) { markdown += `- ${asset}\n`; } markdown += '\n'; } } // Style cleanup section if (styleResult) { markdown += `## ๐ŸŽจ CSS Style Cleanup\n\n`; markdown += `### Summary\n\n`; markdown += `- Analyzed CSS files: ${styleResult.analyzedFiles}\n`; markdown += `- Total CSS selectors found: ${styleResult.totalSelectorsFound}\n`; markdown += `- Unused CSS selectors: ${styleResult.totalUnusedSelectors}\n`; if (styleResult.modifiedFiles.length > 0) { markdown += `- Modified CSS files: ${styleResult.modifiedFiles.length}\n`; markdown += `- Bytes removed: ${this.formatSize(styleResult.bytesRemoved)}\n`; } markdown += '\n'; // Unused selectors markdown += `### Unused CSS Selectors (${styleResult.totalUnusedSelectors} total)\n\n`; if (styleResult.unusedSelectors.length > 0) { for (const file of styleResult.unusedSelectors) { markdown += `- **${file.file}** (${file.selectors.length} unused selectors)\n`; // Group selectors into chunks of 10 for readability const CHUNK_SIZE = 10; for (let i = 0; i < file.selectors.length; i += CHUNK_SIZE) { const chunk = file.selectors.slice(i, i + CHUNK_SIZE); const selectorList = chunk.map((s) => `\`${s}\``).join(', '); markdown += ` - ${selectorList}\n`; } } } else { markdown += `No unused CSS selectors found.\n`; } markdown += '\n'; // Modified files if (styleResult.modifiedFiles.length > 0) { markdown += `### Modified CSS Files (${styleResult.modifiedFiles.length} total)\n\n`; for (const file of styleResult.modifiedFiles) { markdown += `- ${file}\n`; } markdown += '\n'; } } // Create filename with proper prefix in the report directory const outputPath = this.options.outputPath ?? 'vibe-janitor-report'; const filename = `${path.basename(outputPath)}-main.md`; const reportPath = path.join(this.reportDir, filename); try { await fs.outputFile(reportPath, markdown); if (this.options.verbose) { Logger.success(`Markdown report saved to: ${reportPath}`); } return reportPath; } catch (error) { Logger.error(`Failed to generate Markdown report: ${error instanceof Error ? error.message : String(error)}`); return ''; } } /** * Generate full report suite */ async generateReports(cleanerResult, assetResult, styleResult) { const jsonPath = await this.generateJsonReport(cleanerResult, assetResult, styleResult); const markdownPath = await this.generateMarkdownReport(cleanerResult, assetResult, styleResult); return { jsonPath, markdownPath }; } }