UNPKG

devghost

Version:

👻 Find dead code, dead imports, and dead dependencies before they haunt your project

831 lines (780 loc) 28.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateReportTemplate = generateReportTemplate; const styles_1 = require("./styles"); /** * Generate the complete HTML report template */ function generateReportTemplate(results, generatedAt) { const totalIssues = results.unusedImports.length + results.unusedFiles.length + results.unusedDependencies.length + results.unusedExports.length + results.unusedFunctions.length + results.unusedTypes.length + results.unusedVariables.length; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="DevGhost Dead Code Analysis Report"> <title>DevGhost Analysis Report - ${generatedAt}</title> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> <style>${styles_1.reportStyles}</style> </head> <body> <!-- Theme Toggle --> <button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme" title="Toggle Dark/Light Mode"> <span id="theme-icon">🌙</span> </button> <div class="container"> <!-- Header --> <header class="header"> <div class="header-content"> <h1> <span>👻</span> DevGhost Analysis Report </h1> <p class="header-subtitle">Dead Code Detection & Analysis</p> <div class="header-meta"> <div>📅 Generated: ${generatedAt}</div> <div>📊 Files Scanned: ${results.stats.filesScanned.toLocaleString()}</div> <div>🔍 Issues Found: ${totalIssues.toLocaleString()}</div> </div> </div> </header> ${totalIssues === 0 ? '' : generateActionButtons(results)} ${totalIssues === 0 ? generateEmptyState() : generateFullReport(results)} <!-- Footer --> <footer class="footer"> <p>Generated by <a href="https://github.com/Haileyesus-22/devghost" target="_blank">DevGhost</a> v${getVersion()}</p> <p style="margin-top: 0.5rem; font-size: 0.9rem;">The Exorcist for Your Codebase</p> </footer> </div> <script> ${generateJavaScript(results, generatedAt)} </script> </body> </html>`; } /** * Generate empty state when no issues found */ function generateEmptyState() { return ` <div class="empty-state"> <div class="empty-state-icon">🎉</div> <h2 class="empty-state-title">No Dead Code Found!</h2> <p class="empty-state-message">Your project is clean and ghost-free. Great job!</p> </div> `; } /** * Generate action buttons section */ function generateActionButtons(results) { const timeEstimate = estimateTimeToFix(results); return ` <div class="action-buttons-section"> <div class="action-buttons-container"> <button class="action-btn primary" onclick="copyFixCommands()" title="Copy commands to fix all issues"> <span class="btn-icon">📋</span> <span>Copy Fix Commands</span> </button> <button class="action-btn secondary" onclick="exportToPDF()" title="Print or save as PDF"> <span class="btn-icon">📄</span> <span>Export as PDF</span> </button> <div class="time-estimate"> <span class="estimate-icon">⏱️</span> <span class="estimate-text"> <strong>Manual review time:</strong> ${timeEstimate} <span style="display: block; font-size: 0.8rem; opacity: 0.8; margin-top: 0.25rem;"> 💡 Use commands above to auto-fix in minutes! </span> </span> </div> </div> </div> `; } /** * Estimate time to fix all issues */ function estimateTimeToFix(results) { const timePerImport = 0.5; const timePerExport = 1; const timePerFunction = 3; const timePerType = 2; const timePerVariable = 1; const timePerFile = 5; const timePerDep = 2; const totalMinutes = results.unusedImports.length * timePerImport + results.unusedExports.length * timePerExport + results.unusedFunctions.length * timePerFunction + results.unusedTypes.length * timePerType + results.unusedVariables.length * timePerVariable + results.unusedFiles.length * timePerFile + results.unusedDependencies.length * timePerDep; if (totalMinutes < 5) return '< 5 minutes'; if (totalMinutes < 15) return '5-15 minutes'; if (totalMinutes < 30) return '15-30 minutes'; if (totalMinutes < 60) return '30-60 minutes'; const hours = Math.ceil(totalMinutes / 60); return `~${hours} hour${hours > 1 ? 's' : ''}`; } /** * Generate the full report with all sections */ function generateFullReport(results) { return ` <!-- Statistics Grid --> <div class="stats-grid"> ${generateStatCards(results)} </div> <!-- Charts Section --> ${generateChartsSection(results)} <!-- Issues Section --> ${generateIssuesSection(results)} <!-- Savings Summary --> ${generateSavingsSummary(results)} `; } /** * Generate stat cards */ function generateStatCards(results) { const totalIssues = results.unusedImports.length + results.unusedFiles.length + results.unusedDependencies.length + results.unusedExports.length + results.unusedFunctions.length + results.unusedTypes.length + results.unusedVariables.length; return ` <div class="stat-card error"> <div class="stat-header"> <span class="stat-label">Total Issues</span> <span class="stat-icon">🚨</span> </div> <div class="stat-value">${totalIssues.toLocaleString()}</div> <div class="stat-subtitle">Unused code detected</div> </div> <div class="stat-card warning"> <div class="stat-header"> <span class="stat-label">Code Savings</span> <span class="stat-icon">📉</span> </div> <div class="stat-value">${formatBytes(results.stats.potentialSavings.bytes)}</div> <div class="stat-subtitle">${results.stats.potentialSavings.lines.toLocaleString()} lines of code</div> </div> <div class="stat-card warning"> <div class="stat-header"> <span class="stat-label">Dependencies</span> <span class="stat-icon">📦</span> </div> <div class="stat-value">${results.unusedDependencies.length}</div> <div class="stat-subtitle">${formatBytes(results.stats.potentialSavings.dependenciesSize)} unused</div> </div> <div class="stat-card success"> <div class="stat-header"> <span class="stat-label">Files Scanned</span> <span class="stat-icon">📁</span> </div> <div class="stat-value">${results.stats.filesScanned.toLocaleString()}</div> <div class="stat-subtitle">Out of ${results.stats.totalFiles.toLocaleString()} total</div> </div> `; } /** * Generate charts section */ function generateChartsSection(_results) { return ` <section class="charts-section"> <h2 class="section-title mb-3">Visual Analysis</h2> <div class="charts-grid"> <div class="chart-card"> <h3>Issues by Category</h3> <div class="chart-container"> <canvas id="issuesCategoryChart"></canvas> </div> </div> <div class="chart-card"> <h3>Potential Savings</h3> <div class="chart-container"> <canvas id="savingsChart"></canvas> </div> </div> </div> </section> `; } /** * Generate issues section with file-grouped, collapsible view */ function generateIssuesSection(results) { // Group all issues by file const fileIssues = groupIssuesByFile(results); const html = ` <section class="issues-section"> <div class="section-header"> <h2 class="section-title">Issue Explorer</h2> <div class="search-controls"> <input type="text" id="issueSearch" class="search-input" placeholder="🔍 Search by file name or issue type..." onkeyup="filterIssues()" /> <div class="filter-buttons"> <button class="filter-btn active" onclick="filterByCategory('all')">All</button> <button class="filter-btn" onclick="filterByCategory('import')">Imports</button> <button class="filter-btn" onclick="filterByCategory('export')">Exports</button> <button class="filter-btn" onclick="filterByCategory('function')">Functions</button> <button class="filter-btn" onclick="filterByCategory('file')">Files</button> </div> </div> </div> <div class="file-issues-container"> ${generateFileIssueCards(fileIssues, results)} </div> </section> `; return html; } function groupIssuesByFile(results) { const fileMap = new Map(); // Helper to add issue to map const addIssue = (file, issue) => { if (!fileMap.has(file)) { fileMap.set(file, []); } fileMap.get(file)?.push(issue); }; // Group all issue types for (const imp of results.unusedImports) { addIssue(imp.file, { ...imp, type: 'import', icon: '📥' }); } for (const exp of results.unusedExports) { addIssue(exp.file, { ...exp, type: 'export', icon: '📤' }); } for (const func of results.unusedFunctions) { addIssue(func.file, { ...func, type: 'function', icon: '⚙️' }); } for (const type of results.unusedTypes) { addIssue(type.file, { ...type, type: 'type', icon: '🔤' }); } for (const v of results.unusedVariables) { addIssue(v.file, { ...v, type: 'variable', icon: '📝' }); } for (const file of results.unusedFiles) { addIssue(file.path, { ...file, type: 'file', icon: '📄' }); } return fileMap; } /** * Generate file-based issue cards */ function generateFileIssueCards(fileMap, results) { let html = ''; // Sort files by number of issues (descending) const sortedFiles = Array.from(fileMap.entries()).sort((a, b) => b[1].length - a[1].length); sortedFiles.forEach(([filePath, issues], index) => { const issueCount = issues.length; const isExpanded = index < 3; // Auto-expand top 3 files // Get unique issue types for this file const types = [...new Set(issues.map((i) => i.type))]; const typeLabels = types .map((t) => { const count = issues.filter((i) => i.type === t).length; return `<span class="type-badge type-${t}">${count} ${t}${count > 1 ? 's' : ''}</span>`; }) .join(''); html += ` <div class="file-card" data-file="${escapeHtml(filePath)}" data-types="${types.join(',')}"> <div class="file-card-header" onclick="toggleFileCard(this)"> <div class="file-info"> <span class="file-icon">📄</span> <span class="file-path">${escapeHtml(filePath)}</span> </div> <div class="file-meta"> ${typeLabels} <span class="issue-count-badge">${issueCount} issue${issueCount > 1 ? 's' : ''}</span> <span class="expand-icon ${isExpanded ? 'expanded' : ''}"></span> </div> </div> <div class="file-card-content ${isExpanded ? 'expanded' : ''}"> <ul class="issue-list"> ${issues.map((issue) => generateIssueItem(issue)).join('\n')} </ul> </div> </div> `; }); // Add unused dependencies separately if any if (results.unusedDependencies.length > 0) { html += ` <div class="file-card" data-types="dependency"> <div class="file-card-header" onclick="toggleFileCard(this)"> <div class="file-info"> <span class="file-icon">📦</span> <span class="file-path">Unused Dependencies</span> </div> <div class="file-meta"> <span class="issue-count-badge">${results.unusedDependencies.length} package${results.unusedDependencies.length > 1 ? 's' : ''}</span> <span class="expand-icon"></span> </div> </div> <div class="file-card-content"> <ul class="issue-list"> ${results.unusedDependencies .map((dep) => ` <li class="issue-item"> <div class="issue-header"> <span class="issue-icon">📦</span> <span class="issue-title">${escapeHtml(dep.name)}</span> <span class="issue-badge">${dep.type}</span> </div> <div class="issue-details"> ${dep.size > 0 ? `${formatBytes(dep.size)} in node_modules` : 'Not installed'} </div> </li> `) .join('\n')} </ul> </div> </div> `; } return html || '<div class="empty-state-message">No issues found in this category</div>'; } /** * Generate a single issue item */ function generateIssueItem(issue) { let title = ''; let details = ''; switch (issue.type) { case 'import': title = `Unused import '${escapeHtml(issue.importName)}' from '${escapeHtml(issue.source)}'`; details = `Line ${issue.line + 1} • ${escapeHtml(issue.entireLine)}`; break; case 'export': title = `Unused ${issue.exportType} export '${escapeHtml(issue.exportName)}'`; details = `Line ${issue.line + 1} • ${escapeHtml(issue.entireLine)}`; break; case 'function': title = `Unused ${issue.functionType} '${escapeHtml(issue.functionName)}'`; details = `Line ${issue.line + 1} • ${issue.isExported ? 'Exported' : 'Internal'}`; break; case 'type': title = `Unused ${issue.typeKind} '${escapeHtml(issue.typeName)}'`; details = `Line ${issue.line + 1} • ${issue.isExported ? 'Exported' : 'Internal'}`; break; case 'variable': title = `Unused ${issue.variableType} '${escapeHtml(issue.variableName)}'`; details = `Line ${issue.line + 1} • ${issue.scopeType} scope`; break; case 'file': title = escapeHtml(issue.reason); details = `${formatBytes(issue.size)} • ${issue.lines} lines`; break; } return ` <li class="issue-item"> <div class="issue-header"> <span class="issue-icon">${issue.icon}</span> <span class="issue-title">${title}</span> </div> <div class="issue-details">${details}</div> </li> `; } /** * Generate an issue category section */ function _generateIssueCategory(title, icon, count, badgeClass, items, moreItem = '') { return ` <div class="issue-category"> <div class="category-header"> <h3 class="category-title"> <span>${icon}</span> ${title} </h3> <span class="count-badge ${badgeClass}">${count}</span> </div> <ul class="issue-list"> ${items.join('\n')} ${moreItem} </ul> </div> `; } /** * Generate savings summary */ function generateSavingsSummary(results) { const { potentialSavings } = results.stats; return ` <section class="issue-category"> <div class="category-header"> <h3 class="category-title"> <span>💾</span> Potential Savings </h3> </div> <div class="stats-grid"> <div style="text-align: center; padding: 1rem;"> <div style="font-size: 2rem; font-weight: 700; color: var(--success);"> ${formatBytes(potentialSavings.bytes)} </div> <div style="color: var(--text-secondary); margin-top: 0.5rem;">Code Size</div> </div> <div style="text-align: center; padding: 1rem;"> <div style="font-size: 2rem; font-weight: 700; color: var(--success);"> ${potentialSavings.lines.toLocaleString()} </div> <div style="color: var(--text-secondary); margin-top: 0.5rem;">Lines of Code</div> </div> <div style="text-align: center; padding: 1rem;"> <div style="font-size: 2rem; font-weight: 700; color: var(--success);"> ${formatBytes(potentialSavings.dependenciesSize)} </div> <div style="color: var(--text-secondary); margin-top: 0.5rem;">Dependencies</div> </div> </div> </section> `; } /** * Generate embedded JavaScript */ function generateJavaScript(results, generatedAt) { return ` // Theme toggle let isDark = localStorage.getItem('theme') === 'dark'; function applyTheme() { if (isDark) { document.documentElement.setAttribute('data-theme', 'dark'); document.getElementById('theme-icon').textContent = '☀️'; } else { document.documentElement.removeAttribute('data-theme'); document.getElementById('theme-icon').textContent = '🌙'; } } function toggleTheme() { isDark = !isDark; localStorage.setItem('theme', isDark ? 'dark' : 'light'); applyTheme(); } // Apply theme on load applyTheme(); // Initialize charts if (typeof Chart !== 'undefined') { // Issues by Category Chart const ctx1 = document.getElementById('issuesCategoryChart'); if (ctx1) { new Chart(ctx1, { type: 'doughnut', data: { labels: [ 'Unused Imports (${results.unusedImports.length})', 'Unused Exports (${results.unusedExports.length})', 'Unused Functions (${results.unusedFunctions.length})', 'Unused Types (${results.unusedTypes.length})', 'Unused Variables (${results.unusedVariables.length})', 'Unused Files (${results.unusedFiles.length})', 'Unused Dependencies (${results.unusedDependencies.length})' ], datasets: [{ data: [ ${results.unusedImports.length}, ${results.unusedExports.length}, ${results.unusedFunctions.length}, ${results.unusedTypes.length}, ${results.unusedVariables.length}, ${results.unusedFiles.length}, ${results.unusedDependencies.length} ], backgroundColor: [ '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6' ], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'bottom', labels: { padding: 15, font: { size: 12 }, color: getComputedStyle(document.documentElement).getPropertyValue('--text-primary') } } } } }); } // Savings Chart const ctx2 = document.getElementById('savingsChart'); if (ctx2) { new Chart(ctx2, { type: 'bar', data: { labels: ['Code (KB)', 'Lines', 'Dependencies (MB)'], datasets: [{ label: 'Potential Savings', data: [ ${(results.stats.potentialSavings.bytes / 1024).toFixed(2)}, ${results.stats.potentialSavings.lines}, ${(results.stats.potentialSavings.dependenciesSize / (1024 * 1024)).toFixed(2)} ], backgroundColor: [ 'rgba(99, 102, 241, 0.8)', 'rgba(139, 92, 246, 0.8)', 'rgba(16, 185, 129, 0.8)' ], borderColor: [ 'rgb(99, 102, 241)', 'rgb(139, 92, 246)', 'rgb(16, 185, 129)' ], borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary') }, grid: { color: getComputedStyle(document.documentElement).getPropertyValue('--border-color') } }, x: { ticks: { color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary') }, grid: { display: false } } } } }); } } // File card toggle function function toggleFileCard(header) { const card = header.parentElement; const content = card.querySelector('.file-card-content'); const icon = card.querySelector('.expand-icon'); content.classList.toggle('expanded'); icon.classList.toggle('expanded'); } //Search filter function function filterIssues() { const searchTerm = document.getElementById('issueSearch').value.toLowerCase(); const fileCards = document.querySelectorAll('.file-card'); fileCards.forEach(card => { const filePath = card.getAttribute('data-file') || card.textContent.toLowerCase(); if (filePath.toLowerCase().includes(searchTerm)) { card.classList.remove('hidden'); } else { card.classList.add('hidden'); } }); } // Category filter function let activeCategory = 'all'; function filterByCategory(category) { activeCategory = category; // Update button states const buttons = document.querySelectorAll('.filter-btn'); buttons.forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); // Filter cards by category const fileCards = document.querySelectorAll('.file-card'); fileCards.forEach(card => { const types = (card.getAttribute('data-types') || '').split(','); if (category === 'all' || types.includes(category)) { card.classList.remove('hidden'); } else { card.classList.add('hidden'); } }); } // Copy fix commands to clipboard function copyFixCommands() { const commands = []; const totalImports = ${results.unusedImports.length}; const totalDeps = ${results.unusedDependencies.length}; const totalFunctions = ${results.unusedFunctions.length}; commands.push('# DevGhost Auto-Fix Commands'); commands.push('# Generated: ${generatedAt.replace(/'/g, "\\'")}'); commands.push('# Run these commands to auto-fix issues quickly!'); commands.push(''); if (totalImports > 0) { commands.push('# Fix unused imports (' + totalImports + ' issues)'); commands.push('devghost --fix'); commands.push(''); } if (totalDeps > 0) { commands.push('# Remove unused dependencies (' + totalDeps + ' packages)'); commands.push('devghost --fix-deps --yes'); commands.push(''); } if (totalFunctions > 0) { commands.push('# Remove unused functions (' + totalFunctions + ' functions)'); commands.push('devghost --fix-functions --yes'); commands.push(''); } commands.push('# Generate a fresh report'); commands.push('devghost --report html'); const commandText = commands.join('\\n'); // Try modern clipboard API first if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(commandText).then(() => { showCopySuccess(); }).catch(err => { // Fallback to textarea method fallbackCopy(commandText); }); } else { // Fallback for older browsers fallbackCopy(commandText); } } function fallbackCopy(text) { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.top = '0'; textarea.style.left = '0'; textarea.style.width = '2em'; textarea.style.height = '2em'; textarea.style.padding = '0'; textarea.style.border = 'none'; textarea.style.outline = 'none'; textarea.style.boxShadow = 'none'; textarea.style.background = 'transparent'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); let success = false; try { success = document.execCommand('copy'); if (success) { showCopySuccess(); } else { // If execCommand fails, show the text in a modal-like alert showCopyModal(text); } } catch (err) { showCopyModal(text); } finally { document.body.removeChild(textarea); } } function showCopyModal(text) { // Create a better modal instead of alert const modal = document.createElement('div'); modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 2rem;'; const content = document.createElement('div'); content.style.cssText = 'background: var(--bg-card); border-radius: 1rem; padding: 2rem; max-width: 600px; width: 100%; max-height: 80vh; overflow: auto;'; content.innerHTML = \` <h3 style="margin-bottom: 1rem; color: var(--text-primary);">📋 Copy Fix Commands</h3> <p style="margin-bottom: 1rem; color: var(--text-secondary);">Select all text below (Ctrl+A) and copy (Ctrl+C):</p> <textarea readonly style="width: 100%; min-height: 200px; padding: 1rem; font-family: monospace; font-size: 0.9rem; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 0.5rem; color: var(--text-primary);">\${text}</textarea> <button onclick="this.parentElement.parentElement.remove()" style="margin-top: 1rem; padding: 0.75rem 1.5rem; background: var(--gradient-primary); color: white; border: none; border-radius: 0.5rem; cursor: pointer; font-weight: 600;">Close</button> \`; modal.appendChild(content); document.body.appendChild(modal); // Auto-select the textarea const ta = content.querySelector('textarea'); ta.focus(); ta.select(); // Close on click outside modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); } function showCopySuccess() { const btn = event.target.closest('.action-btn'); const originalText = btn.innerHTML; btn.innerHTML = '<span class="btn-icon"></span><span>Copied!</span>'; btn.style.background = 'var(--gradient-success)'; setTimeout(() => { btn.innerHTML = originalText; btn.style.background = ''; }, 2000); } // Export report as PDF function exportToPDF() { window.print(); } `; } /** * Format bytes to human-readable string */ function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`; } /** * Escape HTML special characters */ function escapeHtml(text) { const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;', }; return text.replace(/[&<>"']/g, (m) => map[m]); } /** * Get DevGhost version */ function getVersion() { try { // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); return pkg.version; } catch { return '0.3.8'; } } //# sourceMappingURL=reportTemplate.js.map