devghost
Version:
👻 Find dead code, dead imports, and dead dependencies before they haunt your project
831 lines (780 loc) • 28.6 kB
JavaScript
"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 `
<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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
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