perf-lens
Version:
AI-powered frontend performance optimizer
1,130 lines (1,017 loc) • 33.4 kB
JavaScript
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
/**
* Generates a markdown report from performance analysis data
* @param {PerformanceReport} data - The performance analysis data to format
* @returns {string} A formatted markdown report
*/
export function generateMarkdownReport(data) {
return `# Performance Analysis Report
${data.metadata
? `
Generated on: ${new Date(data.metadata.timestamp).toLocaleString()}
Analysis Duration: ${Math.round(data.metadata.duration / 1000)}s
`
: ''}
## 🌟 Lighthouse Performance Report
### Metrics
\`\`\`
${data.lighthouse.metrics}
\`\`\`
### Detailed Report
${data.lighthouse.report}
### Analysis
${data.lighthouse.analysis}
## 🧠 Code Analysis
### Critical Issues
${data.codeAnalysis.critical.length > 0
? data.codeAnalysis.critical.join('\n\n')
: '_No critical issues found_'}
### Warnings
${data.codeAnalysis.warnings.length > 0
? data.codeAnalysis.warnings.join('\n\n')
: '_No warnings found_'}
### Suggestions
${data.codeAnalysis.suggestions.length > 0
? data.codeAnalysis.suggestions.join('\n\n')
: '_No suggestions found_'}
`;
}
/**
* Converts markdown text to HTML with proper formatting and syntax highlighting
* @param {string} text - The markdown text to convert
* @returns {string} The converted HTML text
*/
function formatMarkdownToHtml(text) {
// First, escape any existing HTML to prevent injection
text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
// Store code blocks temporarily with a unique marker
const codeBlocks = [];
text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`;
// Clean up the code block content
code = code
.trim()
.replace(/<\/?p>/g, '') // Remove paragraph tags
.replace(/^\s{4}/gm, '') // Remove common leading indentation
.replace(/<(\/?(?:em|strong|code|pre)>)/g, '<$1') // Restore intended HTML tags
.replace(/&([lg]t;)/g, '&$1'); // Fix HTML entities
codeBlocks.push(`<pre><code class="language-${lang}">${code}</code></pre>`);
return placeholder;
});
// Convert inline code with proper escaping
const inlineCodeBlocks = [];
text = text.replace(/`([^`]+)`/g, (match, code) => {
const placeholder = `__INLINE_CODE_${inlineCodeBlocks.length}__`;
inlineCodeBlocks.push(`<code>${code.replace(/<(\/?(?:em|strong|code|pre)>)/g, '<$1')}</code>`);
return placeholder;
});
// Convert headers (ensuring proper hierarchy)
text = text
.replace(/^# (.*$)/gm, '<h2>$1</h2>')
.replace(/^## (.*$)/gm, '<h3>$1</h3>')
.replace(/^### (.*$)/gm, '<h4>$1</h4>')
.replace(/^#### (.*$)/gm, '<h5>$1</h5>')
.replace(/^##### (.*$)/gm, '<h6>$1</h6>');
// Convert bold text
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Convert italic text
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
// Handle lists with better nesting support
const processLists = (input) => {
const lines = input.split('\n');
const output = [];
const listStack = [];
let currentListType = null;
lines.forEach(line => {
const trimmedLine = line.trimStart();
const indentLevel = Math.floor((line.length - trimmedLine.length) / 2);
// Check for list items
const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)/);
const numberMatch = trimmedLine.match(/^(\d+)\.\s+(.+)/);
if (bulletMatch || numberMatch) {
const content = bulletMatch ? bulletMatch[1] : numberMatch[2];
const listType = bulletMatch ? 'ul' : 'ol';
// If we're starting a new list
if (!currentListType) {
currentListType = listType;
output.push(`<${listType}>`);
listStack.push({ type: listType, level: indentLevel });
}
// If we're changing list types at the same level
else if (currentListType !== listType &&
listStack[listStack.length - 1]?.level === indentLevel) {
while (listStack.length > 0 && listStack[listStack.length - 1].level >= indentLevel) {
const last = listStack.pop();
output.push(`</li>\n</${last.type}>`);
}
currentListType = listType;
output.push(`<${listType}>`);
listStack.push({ type: listType, level: indentLevel });
}
// Handle nesting
else if (indentLevel > (listStack[listStack.length - 1]?.level ?? -1)) {
output.push(`<${listType}>`);
listStack.push({ type: listType, level: indentLevel });
currentListType = listType;
}
// If we're un-nesting
else if (indentLevel < (listStack[listStack.length - 1]?.level ?? 0)) {
while (listStack.length > 0 && listStack[listStack.length - 1].level > indentLevel) {
const last = listStack.pop();
output.push(`</li>\n</${last.type}>`);
}
if (listStack.length > 0) {
output.push('</li>');
}
}
// Same level, same type
else if (listStack.length > 0) {
output.push('</li>');
}
output.push(`<li>${content}`);
}
else if (trimmedLine.trim()) {
// Close all open lists when hitting non-list content
while (listStack.length > 0) {
const last = listStack.pop();
output.push(`</li>\n</${last.type}>`);
}
currentListType = null;
output.push(trimmedLine);
}
});
// Close any remaining open lists
while (listStack.length > 0) {
const last = listStack.pop();
output.push(`</li>\n</${last.type}>`);
}
return output.join('\n');
};
// Process lists
text = processLists(text);
// Convert paragraphs (lines with content)
text = text
.split('\n')
.map(line => {
if (line.trim() &&
!line.match(/^<(div|pre|p|h[1-6]|ul|ol|li)[^>]*>/i) && // Only exclude block-level elements
!line.includes('__CODE_BLOCK_') &&
!line.includes('__INLINE_CODE_')) {
return `<p>${line}</p>`;
}
return line;
})
.join('\n');
// Restore code blocks
codeBlocks.forEach((block, i) => {
text = text.replace(`__CODE_BLOCK_${i}__`, block);
});
// Restore inline code blocks
inlineCodeBlocks.forEach((block, i) => {
text = text.replace(`__INLINE_CODE_${i}__`, block);
});
// Clean up formatting
return text
.replace(/\n{3,}/g, '\n\n')
.replace(/<p>\s*<\/p>/g, '')
.replace(/(<\/(ul|ol)>)\s*<p>\s*<\/p>/g, '$1')
.replace(/<p>\s*<(ul|ol)>/g, '<$1>')
.replace(/<\/(ul|ol)>\s*<\/p>/g, '</$1>')
.replace(/<li>\s+/g, '<li>')
.replace(/\s+<\/li>/g, '</li>')
.replace(/<\/(ul|ol)>\s*<(ul|ol)>/g, '</$1>\n<$2>')
.replace(/\n+/g, '\n')
.replace(/>\s+</g, '><') // Remove extra spaces between tags
.replace(/>>+/g, '>') // Remove duplicate closing brackets
.trim();
}
/**
* Generates an HTML report from performance analysis data with styling
* @param {PerformanceReport} data - The performance analysis data to format
* @returns {string} A formatted HTML report with embedded styles
*/
export function generateHtmlReport(data) {
const formatMetricValue = (value) => {
if (!value)
return 'N/A';
return value;
};
const updatedCssVariables = `
:root {
--primary: #2563eb;
--critical: #dc2626;
--warning: #d97706;
--suggestion: #3b82f6;
--success: #22c55e;
--background: #f8fafc;
--text: #1e293b;
--text-light: #475569;
--border: #e2e8f0;
--code-bg: #1e293b;
--code-text: #e2e8f0;
--card-bg: #ffffff;
}
`;
const tabStyles = `
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
border-bottom: 2px solid var(--border);
padding-bottom: 0;
position: sticky;
top: 0;
background: var(--background);
z-index: 100;
padding: 1rem 0;
}
.tab {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 500;
color: var(--text-light);
background: transparent;
border: none;
transition: all 0.2s;
position: relative;
}
.tab:hover {
color: var(--primary);
background: rgba(37, 99, 235, 0.1);
}
.tab.active {
color: var(--primary);
background: rgba(37, 99, 235, 0.1);
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease-in-out;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.tab-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
margin-left: 0.5rem;
color: white;
}
.tab-indicator.critical { background: var(--critical); }
.tab-indicator.warning { background: var(--warning); }
.tab-indicator.suggestion { background: var(--suggestion); }
`;
const updatedSummaryCardStyles = `
.summary-card {
flex: 1;
padding: 1.5rem;
border-radius: 0.5rem;
color: white;
display: flex;
flex-direction: column;
align-items: center;
}
.summary-card.critical {
background: var(--critical);
}
.summary-card.warning {
background: var(--warning);
}
.summary-card.suggestion {
background: var(--suggestion);
}
.summary-number {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 0.75rem;
}
.summary-label {
font-size: 1rem;
font-weight: 500;
}
`;
const updatedCodeStyles = `
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
background: var(--code-bg);
color: var(--code-text);
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.9em;
}
pre {
background: var(--code-bg);
padding: 1.5rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1.5rem 0;
border: 1px solid var(--border);
position: relative;
}
pre code {
padding: 0;
background: none;
color: var(--code-text);
font-size: 0.9rem;
display: block;
line-height: 1.5;
}
`;
const updatedListStyles = `
ul, ol {
margin: 1rem 0;
padding-left: 2rem;
}
li {
margin-bottom: 0.5rem;
}
ul ul, ol ul, ul ol, ol ol {
margin: 0.5rem 0;
}
li > ul, li > ol {
margin-top: 0.25rem;
}
`;
const accordionStyles = `
.accordion {
margin-bottom: 1rem;
}
.accordion-header {
padding: 1rem;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
}
.accordion-header:hover {
background: rgba(37, 99, 235, 0.1);
}
.accordion-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text);
}
.accordion-icon {
transition: transform 0.2s;
}
.accordion-content {
display: none;
padding: 1rem;
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 0.5rem 0.5rem;
background: var(--card-bg);
}
.accordion.active > .accordion-header {
border-radius: 0.5rem 0.5rem 0 0;
background: rgba(37, 99, 235, 0.1);
.accordion-icon {
transform: rotate(180deg);
}
}
.accordion.active > .accordion-content {
display: block;
}
.expandable-list {
position: relative;
max-height: 300px;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.expandable-list.expanded {
max-height: none;
}
.expandable-list:not(.expanded)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(transparent, var(--card-bg));
}
.expand-button {
display: inline-block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: var(--primary);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
}
.expand-button:hover {
opacity: 0.9;
}
.nested-accordions {
margin: -1rem;
}
.nested-accordions .accordion {
margin: 0;
border: 1px solid var(--border);
border-top: none;
}
.nested-accordions .accordion:first-child {
border-top: 1px solid var(--border);
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.nested-accordions .accordion:last-child {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
.nested-accordions .accordion-header {
background: rgba(37, 99, 235, 0.05);
border: none;
border-radius: 0;
padding-left: 3rem;
}
.nested-accordions .accordion.active .accordion-header {
background: rgba(37, 99, 235, 0.1);
border-radius: 0;
}
.nested-accordions .accordion-content {
border: none;
border-radius: 0;
padding-left: 3rem;
h2 {
margin-top: 1rem;
}
}
`;
const styles = `
${updatedCssVariables}
${tabStyles}
${accordionStyles}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: var(--text);
background: var(--background);
}
h5 {
font-size: 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 3rem;
padding: 2rem;
background: var(--card-bg);
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.metadata {
margin-top: 1rem;
color: var(--text-light);
font-size: 0.9rem;
}
.metadata p {
margin: 0.25rem 0;
}
h1 {
color: var(--primary);
font-size: 2.5rem;
margin-bottom: 1rem;
}
h2 {
color: var(--text);
font-size: 1.8rem;
margin: 2rem 0 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
h3 {
color: var(--text);
font-size: 1.4rem;
margin: 1.5rem 0 1rem;
border-bottom: 2px solid var(--border);
padding-bottom: 0.5rem;
}
h4 {
color: var(--text);
font-size: 1.2rem;
margin: 1.2rem 0 0.8rem;
}
p {
margin-bottom: 1rem;
line-height: 1.8;
}
ul {
margin: 1rem 0;
padding-left: 2rem;
}
li {
margin-bottom: 0.5rem;
}
${updatedCodeStyles}
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.metric-card {
background: var(--card-bg);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
.metric-title {
color: var(--text);
font-size: 0.9rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--primary);
}
.issue {
border-left: 4px solid transparent;
padding: 1rem;
margin-bottom: 1rem;
background: white;
border-radius: 0 0.5rem 0.5rem 0;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
.issue.critical {
border-left-color: var(--critical);
}
.issue.warning {
border-left-color: var(--warning);
}
.issue.suggestion {
border-left-color: var(--suggestion);
}
.issue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.issue-title {
font-weight: 600;
}
.issue-path {
font-family: monospace;
color: var(--text-light);
font-size: 0.9rem;
}
.resource-list {
list-style: none;
}
.resource-item {
display: flex;
justify-content: space-between;
padding: 0.5rem;
border-bottom: 1px solid var(--border);
}
.resource-item:last-child {
border-bottom: none;
}
.resource-url {
color: var(--text-light);
font-size: 0.9rem;
word-break: break-all;
}
.resource-size {
color: var(--primary);
font-weight: 500;
white-space: nowrap;
margin-left: 1rem;
}
.summary {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
.summary-card {
flex: 1;
padding: 1rem;
border-radius: 0.5rem;
color: white;
display: flex;
flex-direction: column;
align-items: center;
}
.summary-card.critical { background: var(--critical); }
.summary-card.warning { background: var(--warning); }
.summary-card.suggestion { background: var(--suggestion); }
.summary-number {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.summary-label {
font-size: 0.9rem;
opacity: 0.9;
}
${updatedSummaryCardStyles}
${updatedListStyles}
.lighthouse-attribution {
text-align: center;
margin: 20px 0;
padding: 10px;
border-top: 1px solid #eee;
color: #666;
a {
color: #0066cc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
width: 48px;
height: 48px;
vertical-align: middle;
}
}
`;
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance Analysis Report</title>
<style>
${styles}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 Performance Analysis Report</h1>
${data.metadata
? `
<div class="metadata">
<p>Generated on: ${new Date(data.metadata.timestamp).toLocaleString()}</p>
<p>Analysis Duration: ${Math.round(data.metadata.duration / 1000)}s</p>
</div>`
: ''}
</div>
<div class="summary">
<div class="summary-card critical">
<div class="summary-number">${data.codeAnalysis.critical.length}</div>
<div class="summary-label">Critical Issues</div>
</div>
<div class="summary-card warning">
<div class="summary-number">${data.codeAnalysis.warnings.length}</div>
<div class="summary-label">Warnings</div>
</div>
<div class="summary-card suggestion">
<div class="summary-number">${data.codeAnalysis.suggestions.length}</div>
<div class="summary-label">Suggestions</div>
</div>
</div>
<div class="tabs">
<button class="tab active" data-tab="overview">📊 Overview</button>
<button class="tab" data-tab="lighthouse">
🌟 Lighthouse
${data.lighthouse.metrics ? '<span class="tab-indicator suggestion">1</span>' : ''}
</button>
<button class="tab" data-tab="code-analysis">
🧠 Code Analysis
${data.codeAnalysis.critical.length +
data.codeAnalysis.warnings.length +
data.codeAnalysis.suggestions.length >
0
? `<span class="tab-indicator ${data.codeAnalysis.critical.length > 0
? 'critical'
: data.codeAnalysis.warnings.length > 0
? 'warning'
: 'suggestion'}">${data.codeAnalysis.critical.length +
data.codeAnalysis.warnings.length +
data.codeAnalysis.suggestions.length}</span>`
: ''}
</button>
</div>
<div class="tab-content active" id="overview">
<div class="metrics">
${data.lighthouse.metrics
.split('\n')
.map(line => {
if (!line.trim())
return '';
const [title, value] = line.split(':').map(s => s.trim());
if (!title || !value)
return '';
return `
<div class="metric-card">
<div class="metric-title">${title}</div>
<div class="metric-value">${formatMetricValue(value)}</div>
</div>
`;
})
.join('')}
</div>
<div class="card">
<h3>Quick Summary</h3>
<p>This report provides a comprehensive analysis of your application's performance across multiple dimensions:</p>
<ul>
<li><strong>Lighthouse Analysis:</strong> Core web vitals and performance metrics</li>
<li><strong>Code Analysis:</strong> Static analysis of your codebase for performance optimizations</li>
</ul>
${data.codeAnalysis.critical.length > 0
? `<div class="alert critical">
<h4>🚨 Critical Issues Detected</h4>
<p>There are ${data.codeAnalysis.critical.length} critical issues that need immediate attention.</p>
</div>`
: ''}
</div>
</div>
<div class="tab-content" id="lighthouse">
<div class="accordion active">
<div class="accordion-header">
<span class="accordion-title">🎯 Core Web Vitals</span>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
${formatMarkdownToHtml(data.lighthouse.report.split('## Performance Opportunities')[0])}
</div>
</div>
<div class="accordion">
<div class="accordion-header">
<span class="accordion-title">🚀 Performance Opportunities</span>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
${(() => {
const section = data.lighthouse.report
.split('## Performance Opportunities')[1]
?.split('## JavaScript Issues')[0] || '';
return formatMarkdownToHtml(section);
})()}
</div>
</div>
<div class="accordion">
<div class="accordion-header">
<span class="accordion-title">🔍 JavaScript Analysis</span>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
${(() => {
const section = data.lighthouse.report.split('## JavaScript Issues')[1] || '';
const parts = section.split('### Unused JavaScript');
const unminifiedParts = section.split('### Unminified JavaScript');
let content = '';
if (parts.length > 1) {
const unusedJsSection = parts[1].split('###')[0];
content +=
formatMarkdownToHtml(parts[0] + '### Unused JavaScript') +
`
<div class="expandable-list">
${formatMarkdownToHtml(unusedJsSection)}
</div>
<button class="expand-button">Show More</button>
`;
}
if (unminifiedParts.length > 1) {
const unminifiedJsSection = unminifiedParts[1].split('###')[0];
content +=
formatMarkdownToHtml('### Unminified JavaScript') +
`
<div class="expandable-list">
${formatMarkdownToHtml(unminifiedJsSection)}
</div>
<button class="expand-button">Show More</button>
`;
// Add any remaining sections
const remainingSections = unminifiedParts[1].split('###').slice(1);
if (remainingSections.length > 0) {
content += formatMarkdownToHtml('###' + remainingSections.join('###'));
}
}
return content || formatMarkdownToHtml(section);
})()}
</div>
</div>
<div class="accordion">
<div class="accordion-header">
<span class="accordion-title">📊 Analysis & Recommendations</span>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
<div class="nested-accordions">
<div class="accordion">
<div class="accordion-header">
<span class="accordion-title">🎯 Core Web Vitals Analysis</span>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
${formatMarkdownToHtml(data.lighthouse.analysis.coreWebVitals)}
</div>
</div>
<div class="accordion">
<div class="accordion-header">
<span class="accordion-title">🚀 Performance Opportunities Analysis</span>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
${formatMarkdownToHtml(data.lighthouse.analysis.performanceOpportunities)}
</div>
</div>
<div class="accordion">
<div class="accordion-header">
<span class="accordion-title">🔍 Diagnostics Analysis</span>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
${formatMarkdownToHtml(data.lighthouse.analysis.diagnostics)}
</div>
</div>
</div>
</div>
</div>
<div class="lighthouse-attribution">
<img src="https://developer.chrome.com/static/docs/lighthouse/overview/image/lighthouse-logo.svg" alt="Lighthouse" />
Powered by <a href="https://www.google.com/url?q=https://developers.google.com/web/tools/lighthouse&sa=D&source=editors&ust=1742372714584851&usg=AOvVaw0H4et58bVq5T5hpQYr3br5" target="_blank">Lighthouse</a> (v12.4.0)
• To run a full Lighthouse report, go <a href="https://www.google.com/url?q=https://developers.google.com/web/tools/lighthouse%23devtools&sa=D&source=editors&ust=1742372714585325&usg=AOvVaw3Y_g-R7icqIAg0laMFq1ol" target="_blank">here</a>.
</div>
</div>
<div class="tab-content" id="code-analysis">
${data.codeAnalysis.critical.length > 0
? `
<h3>Critical Issues</h3>
${data.codeAnalysis.critical
.map(issue => `
<div class="issue critical">
${formatIssue(issue)}
</div>
`)
.join('\n')}
`
: ''}
${data.codeAnalysis.warnings.length > 0
? `
<h3>Warnings</h3>
${data.codeAnalysis.warnings
.map(issue => `
<div class="issue warning">
${formatIssue(issue)}
</div>
`)
.join('\n')}
`
: ''}
${data.codeAnalysis.suggestions.length > 0
? `
<h3>Suggestions</h3>
${data.codeAnalysis.suggestions
.map(issue => `
<div class="issue suggestion">
${formatIssue(issue)}
</div>
`)
.join('\n')}
`
: ''}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const tabId = tab.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
function handleAccordionClick(header, isNested = false) {
const accordion = header.parentElement;
const wasActive = accordion.classList.contains('active');
if (!isNested) {
const mainAccordions = header.closest('.tab-content').querySelectorAll(':scope > .accordion');
mainAccordions.forEach(acc => {
if (acc !== accordion) {
acc.classList.remove('active');
}
});
} else {
const nestedAccordions = header.closest('.nested-accordions').querySelectorAll(':scope > .accordion');
nestedAccordions.forEach(acc => {
if (acc !== accordion) {
acc.classList.remove('active');
}
});
}
accordion.classList.toggle('active', !wasActive);
header.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
const mainAccordions = document.querySelectorAll('.tab-content > .accordion > .accordion-header');
mainAccordions.forEach(header => {
header.addEventListener('click', (e) => {
e.stopPropagation();
handleAccordionClick(header);
});
});
const nestedAccordions = document.querySelectorAll('.nested-accordions .accordion > .accordion-header');
nestedAccordions.forEach(header => {
header.addEventListener('click', (e) => {
e.stopPropagation();
handleAccordionClick(header, true);
});
});
const expandButtons = document.querySelectorAll('.expand-button');
expandButtons.forEach(button => {
button.addEventListener('click', () => {
const list = button.previousElementSibling;
const isExpanded = list.classList.contains('expanded');
list.classList.toggle('expanded');
button.textContent = isExpanded ? 'Show More' : 'Show Less';
});
});
});
</script>
</body>
</html>`;
}
/**
* Formats an issue message with appropriate styling
* @param {string} issue - The issue message to format
* @returns {string} The formatted issue message
*/
function formatIssue(issue) {
const parts = issue.split('\n');
const firstLine = parts[0];
// Use unicode escape sequences for emojis
const match = firstLine.match(/^(?:🚨|⚠️|💡)\s+([^:]+):(\d+-\d+)/u);
if (!match)
return issue;
const [, path, lines] = match;
const restOfIssue = parts.slice(1).join('\n');
return `
<div class="issue-header">
<span class="issue-title">${path}</span>
<span class="issue-path">Lines ${lines}</span>
</div>
${formatMarkdownToHtml(restOfIssue)}
`;
}
/**
* Saves a performance report in the specified format
* @param {PerformanceReport} data - The performance analysis data to save
* @param {'md' | 'html'} format - The output format (markdown or HTML)
* @param {string} [outputPath] - Optional path to save the report
* @returns {string} The path where the report was saved
*/
export function saveReport(data, format = 'html', outputPath) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const defaultFileName = `performance-report-${timestamp}`;
const filePath = outputPath || path.join(process.cwd(), defaultFileName + '.' + format);
// Create output directory if it doesn't exist
const outputDir = path.dirname(filePath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const content = format === 'md' ? generateMarkdownReport(data) : generateHtmlReport(data);
try {
fs.writeFileSync(filePath, content);
return filePath;
}
catch (error) {
console.error(chalk.red('Error saving report:'), error);
throw error;
}
}