lwc-linter
Version:
A comprehensive CLI tool for linting Lightning Web Components v8.0.0+ with modern LWC patterns, decorators, lifecycle hooks, and Salesforce platform integration
1,536 lines ⢠67.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OutputFormatter = void 0;
const chalk_1 = __importDefault(require("chalk"));
const table_1 = require("table");
const html_minifier_terser_1 = require("html-minifier-terser");
class OutputFormatter {
constructor(format = 'cli') {
this.outputFormat = format;
}
async format(results) {
switch (this.outputFormat) {
case 'json':
return this.formatJSON(results);
case 'html':
return await this.formatHTML(results);
case 'cli':
default:
return this.formatCLI(results);
}
}
async formatWithServer(results, port) {
return await this.formatHTML(results, port);
}
formatCLI(results) {
if (results.length === 0) {
return chalk_1.default.green('\nā
No issues found! Your LWC code looks great.\n');
}
let output = '\n';
let totalIssues = 0;
let totalErrors = 0;
let totalWarnings = 0;
let totalFixed = 0;
results.forEach(result => {
if (result.issues.length > 0) {
output += chalk_1.default.bold.underline(`\nš ${result.filePath}\n`);
result.issues.forEach(issue => {
const icon = this.getSeverityIcon(issue.severity);
const location = issue.line ? ` (line ${issue.line}${issue.column ? `:${issue.column}` : ''})` : '';
const fixableText = issue.fixable ? chalk_1.default.dim(' [fixable]') : '';
output += ` ${icon} ${issue.message}${location}${fixableText}\n`;
output += ` ${chalk_1.default.dim(`Rule: ${issue.rule} | Category: ${issue.category}`)}\n`;
totalIssues++;
if (issue.severity === 'error')
totalErrors++;
if (issue.severity === 'warn')
totalWarnings++;
});
}
if (result.fixedCount && result.fixedCount > 0) {
totalFixed += result.fixedCount;
}
});
// Summary
output += '\n' + chalk_1.default.bold('š Summary:\n');
const summaryData = [
['Files processed', results.length.toString()],
['Total issues', totalIssues.toString()],
['Errors', chalk_1.default.red(totalErrors.toString())],
['Warnings', chalk_1.default.yellow(totalWarnings.toString())],
['Info', chalk_1.default.gray((totalIssues - totalErrors - totalWarnings).toString())]
];
if (totalFixed > 0) {
summaryData.push(['Fixed', chalk_1.default.green(totalFixed.toString())]);
}
output += (0, table_1.table)(summaryData, {
border: {
topBody: '',
topJoin: '',
topLeft: '',
topRight: '',
bottomBody: '',
bottomJoin: '',
bottomLeft: '',
bottomRight: '',
bodyLeft: ' ',
bodyRight: '',
bodyJoin: ': '
},
columnDefault: {
paddingLeft: 0,
paddingRight: 1
},
drawHorizontalLine: () => false
});
if (totalErrors > 0) {
output += chalk_1.default.red('\nā Linting completed with errors.\n');
}
else if (totalWarnings > 0) {
output += chalk_1.default.yellow('\nā ļø Linting completed with warnings.\n');
}
else {
output += chalk_1.default.green('\nā
Linting completed successfully.\n');
}
return output;
}
formatJSON(results) {
const summary = this.generateSummary(results);
return JSON.stringify({
summary,
results: results.map(result => ({
filePath: result.filePath,
issueCount: result.issues.length,
fixedCount: result.fixedCount || 0,
issues: result.issues
}))
}, null, 2);
}
async formatHTML(results, serverPort) {
const isServerMode = !!serverPort;
const timestamp = new Date().toISOString();
const summary = this.generateSummary(results);
let html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LWC Linter Report</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
min-height: 100vh;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 30px;
text-align: center;
position: relative;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.controls {
padding: 30px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
justify-content: space-between;
}
.control-group { display: flex; gap: 10px; align-items: center; }
.control-group label { font-weight: 500; color: #495057; }
select, button {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
background: white;
font-size: 14px;
cursor: pointer;
}
button {
background: #007bff;
color: white;
border: none;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
button:hover { background: #0056b3; }
button.export { background: #28a745; }
button.export:hover { background: #1e7e34; }
button.refresh { background: #17a2b8; }
button.refresh:hover { background: #138496; }
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 30px;
background: white;
}
.stat-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 25px;
border-radius: 12px;
text-align: center;
border: 1px solid #dee2e6;
transition: transform 0.2s, box-shadow 0.3s;
position: relative;
cursor: pointer;
}
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.stat-number { font-size: 2.5em; font-weight: bold; margin-bottom: 8px; transition: all 0.3s ease; }
.stat-label { color: #6c757d; font-size: 0.9em; font-weight: 500; }
.error { color: #dc3545; }
.warning { color: #ffc107; }
.info { color: #17a2b8; }
.success { color: #28a745; }
.files-container { padding: 0 30px 30px 30px; }
.file-section {
margin-bottom: 25px;
border: 1px solid #e9ecef;
border-radius: 12px;
overflow: hidden;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: box-shadow 0.3s;
}
.file-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 20px;
border-bottom: 1px solid #e9ecef;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
}
.file-header:hover { background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%); }
.file-header h3 { margin: 0; font-size: 1.1em; color: #495057; }
.file-stats { display: flex; gap: 15px; font-size: 0.9em; }
.expand-icon {
font-size: 1.2em;
transition: transform 0.3s;
color: #6c757d;
}
.expanded .expand-icon { transform: rotate(180deg); }
.issues-container {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.expanded .issues-container { max-height: 2000px; }
.issue {
padding: 20px;
border-bottom: 1px solid #f8f9fa;
transition: background 0.2s;
}
.issue:hover { background: #f8f9fa; }
.issue:last-child { border-bottom: none; }
.issue-header { display: flex; justify-content: between; align-items: flex-start; gap: 15px; }
.issue-severity {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75em;
font-weight: bold;
text-transform: uppercase;
min-width: 60px;
text-align: center;
}
.severity-error { background: #f8d7da; color: #721c24; }
.severity-warn { background: #fff3cd; color: #856404; }
.severity-info { background: #d1ecf1; color: #0c5460; }
.issue-content { flex: 1; }
.issue-message {
font-weight: 500;
margin-bottom: 8px;
color: #495057;
font-size: 1.05em;
}
.issue-meta {
font-size: 0.85em;
color: #6c757d;
margin-bottom: 15px;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.meta-item {
background: #f8f9fa;
padding: 4px 8px;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.fixable-tag {
background: #d4edda;
color: #155724;
font-weight: 500;
}
.code-snippet {
margin-top: 15px;
border-radius: 8px;
overflow: hidden;
background: #f8f9fa;
border: 1px solid #e9ecef;
}
.snippet-header {
background: #e9ecef;
padding: 10px 15px;
font-weight: 500;
color: #495057;
font-size: 0.9em;
}
.snippet-content {
padding: 15px;
background: #2d3748;
color: #e2e8f0;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.85em;
line-height: 1.5;
overflow-x: auto;
}
.code-before { color: #fed7d7; }
.code-after { color: #c6f6d5; }
.code-line-number { color: #a0aec0; margin-right: 15px; }
.no-issues {
text-align: center;
padding: 60px 20px;
color: #28a745;
font-size: 1.3em;
font-weight: 500;
}
.no-issues-icon { font-size: 3em; margin-bottom: 20px; }
.hidden { display: none !important; }
.toast {
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
max-width: 350px;
font-weight: 500;
}
.toast.show { opacity: 1; transform: translateX(0); }
.toast.success { background: #28a745; }
.toast.error { background: #dc3545; }
.toast.warning { background: #ffc107; color: #212529; }
.toast.info { background: #17a2b8; }
/* Loading Spinner Styles */
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
.loading-spinner-large {
width: 32px;
height: 32px;
border-width: 3px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.btn-loading {
position: relative;
pointer-events: none;
opacity: 0.7;
}
.btn-loading::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid #ffffff;
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.processing-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
visibility: hidden;
opacity: 0;
transition: all 0.3s ease;
}
.processing-overlay.show {
visibility: visible;
opacity: 1;
}
.processing-content {
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.processing-text {
margin-top: 15px;
font-size: 16px;
color: #333;
}
@media (max-width: 768px) {
.controls { flex-direction: column; align-items: stretch; }
.control-group { justify-content: space-between; }
.summary { grid-template-columns: repeat(2, 1fr); }
.stat-number { font-size: 2em; }
.file-header { flex-direction: column; align-items: flex-start; gap: 10px; }
.issue-header { flex-direction: column; align-items: flex-start; }
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
</head>
<body>
<div class="container">
<div class="header">
<h1>š LWC Linter Report</h1>
<p id="lastUpdated">Generated on ${new Date().toLocaleString()}</p>
</div>
<div class="controls">
<div class="control-group">
<label for="severityFilter">Filter by Severity:</label>
<select id="severityFilter" onchange="filterBySeverity()">
<option value="all">All Issues</option>
<option value="error">Errors Only</option>
<option value="warn">Warnings Only</option>
<option value="info">Info Only</option>
</select>
</div>
<div class="control-group">
<label for="categoryFilter">Filter by Category:</label>
<select id="categoryFilter" onchange="filterByCategory()">
<option value="all">All Categories</option>
<option value="code-quality">Code Quality</option>
<option value="accessibility">Accessibility</option>
<option value="performance">Performance</option>
<option value="security">Security</option>
</select>
</div>
<div class="control-group">
<button onclick="expandAll()">Expand All</button>
<button onclick="collapseAll()">Collapse All</button>
</div>
${isServerMode ? `
<div class="control-group">
<button class="refresh" onclick="manualRefresh()">š Refresh Now</button>
</div>` : ''}
<div class="control-group">
<button class="export" onclick="exportToExcel()">š Export to Excel</button>
</div>
</div>
<div class="summary" id="summaryContainer">
<div class="stat-card" onclick="filterBySeverity('all')">
<div class="stat-number" id="totalFiles">${summary.totalFiles}</div>
<div class="stat-label">Files Processed</div>
</div>
<div class="stat-card" onclick="filterBySeverity('error')">
<div class="stat-number error" id="totalErrors">${summary.totalErrors}</div>
<div class="stat-label">Errors</div>
</div>
<div class="stat-card" onclick="filterBySeverity('warn')">
<div class="stat-number warning" id="totalWarnings">${summary.totalWarnings}</div>
<div class="stat-label">Warnings</div>
</div>
<div class="stat-card" onclick="filterBySeverity('info')">
<div class="stat-number info" id="totalInfo">${summary.totalInfo}</div>
<div class="stat-label">Info</div>
</div>
</div>
<div class="files-container" id="filesContainer">`;
if (results.length === 0) {
html += `
<div class="no-issues">
<div class="no-issues-icon">ā
</div>
<div>No issues found! Your LWC code looks great.</div>
</div>`;
}
else {
results.forEach((result, index) => {
if (result.issues.length > 0) {
const errorCount = result.issues.filter(i => i.severity === 'error').length;
const warnCount = result.issues.filter(i => i.severity === 'warn').length;
const infoCount = result.issues.filter(i => i.severity === 'info').length;
html += `
<div class="file-section" data-file-index="${index}" data-file-path="${result.filePath}">
<div class="file-header" onclick="toggleFile(${index})">
<h3>š ${result.filePath}</h3>
<div class="file-stats">
<span class="error">${errorCount} errors</span>
<span class="warning">${warnCount} warnings</span>
<span class="info">${infoCount} info</span>
<span class="expand-icon">ā¼</span>
</div>
</div>
<div class="issues-container">`;
result.issues.forEach((issue, issueIndex) => {
const location = issue.line ? ` (line ${issue.line}${issue.column ? `:${issue.column}` : ''})` : '';
const fixableClass = issue.fixable ? ' fixable-tag' : '';
const codeSnippet = this.generateCodeSnippet(issue);
html += `
<div class="issue" data-severity="${issue.severity}" data-category="${issue.category}">
<div class="issue-header">
<div class="issue-severity severity-${issue.severity}">${issue.severity}</div>
<div class="issue-content">
<div class="issue-message">${this.escapeHtml(issue.message)}${location}</div>
<div class="issue-meta">
<span class="meta-item">Rule: ${issue.rule}</span>
<span class="meta-item">Category: ${issue.category}</span>
${issue.fixable ? '<span class="meta-item fixable-tag">Fixable</span>' : ''}
</div>
${codeSnippet}
</div>
</div>
</div>`;
});
html += `
</div>
</div>`;
}
});
}
html += `
</div>
</div>
<!-- Toast notification -->
<div class="toast" id="toast"></div>
<!-- Processing overlay -->
<div class="processing-overlay" id="processingOverlay">
<div class="processing-content">
<div class="loading-spinner loading-spinner-large"></div>
<div class="processing-text" id="processingText">Processing...</div>
</div>
</div>
<!-- Safe JSON data embedding -->
<script type="application/json" id="lint-data">${JSON.stringify(results)}</script>
<script>
// Global error handler for server logging
window.addEventListener('error', function(event) {
const errorData = {
message: event.message,
line: event.lineno,
column: event.colno,
error: event.error ? event.error.toString() : '',
stack: event.error ? event.error.stack : '',
userAgent: navigator.userAgent
};
// Log to server if available
if (${isServerMode} && fetch) {
fetch('/api/log-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
}).catch(() => {}); // Fail silently
}
});
try {
// Debug logging
console.log('š LWC Linter Dashboard Loading...');
console.log('Server Mode:', ${isServerMode});
// Global data and state with error handling
try {
const lintDataElement = document.getElementById('lint-data');
if (lintDataElement) {
window.lintData = JSON.parse(lintDataElement.textContent);
} else {
console.warn('ā ļø Lint data element not found, using empty array');
window.lintData = [];
}
} catch (jsonError) {
console.error('ā Failed to parse lint data:', jsonError);
window.lintData = [];
}
window.lastUpdate = "${timestamp}";
window.serverMode = ${isServerMode};
window.userState = {
expandedFiles: new Set(),
currentSeverityFilter: 'all',
currentCategoryFilter: 'all'
};
console.log('ā
Global variables initialized');
// Manual refresh with error handling and loading state
function manualRefresh() {
try {
console.log('š Manual refresh triggered');
if (!window.serverMode) {
showToast('Manual refresh requires server mode', 'warning');
return;
}
// Show loading state
const refreshBtn = document.querySelector('.btn:contains("š Refresh Now")') || document.querySelector('[onclick*="manualRefresh"]');
showProcessingOverlay('Refreshing data...');
setButtonLoading(refreshBtn, true);
fetch('/api/lint-results')
.then(response => response.json())
.then(data => {
try {
window.lintData = data.results;
window.lastUpdate = data.timestamp;
document.getElementById('lastUpdated').textContent = 'Last updated: ' + new Date().toLocaleString();
regenerateFilesDisplay(data.results);
showToast('Manual refresh completed!', 'success');
} catch (updateError) {
console.error('ā Error updating content:', updateError);
showToast('Error updating content: ' + updateError.message, 'error');
}
})
.catch(error => {
console.error('ā Manual refresh failed:', error);
showToast('Manual refresh failed: ' + error.message, 'error');
})
.finally(() => {
hideProcessingOverlay();
setButtonLoading(refreshBtn, false);
});
} catch (error) {
console.error('ā Error in manualRefresh:', error);
showToast('Manual refresh error: ' + error.message, 'error');
hideProcessingOverlay();
}
}
// Show toast notification
function showToast(message, type = 'success') {
try {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast ' + type + ' show';
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
} catch (error) {
console.error('ā Error showing toast:', error);
}
}
// Lazy loading configuration
const ITEMS_PER_BATCH = 10; // Load 10 files at a time
let currentlyLoadedFiles = 0;
let allResults = [];
// Regenerate the files display with lazy loading
function regenerateFilesDisplay(results) {
try {
console.log('š Regenerating files display with lazy loading...');
const container = document.getElementById('filesContainer');
if (results.length === 0) {
container.innerHTML = '<div class="no-issues"><div class="no-issues-icon">ā
</div><div>No issues found! Your LWC code looks great.</div></div>';
return;
}
// Store results globally for lazy loading
allResults = results.filter(result => result.issues.length > 0);
currentlyLoadedFiles = 0;
// Clear container and load initial batch
container.innerHTML = '';
loadNextBatch();
// Setup scroll listener for lazy loading
setupLazyScrolling();
// Update statistics with full data
const newStats = calculateStats(results);
document.getElementById('totalFiles').textContent = newStats.totalFiles;
document.getElementById('totalErrors').textContent = newStats.totalErrors;
document.getElementById('totalWarnings').textContent = newStats.totalWarnings;
document.getElementById('totalInfo').textContent = newStats.totalInfo;
console.log('ā
Files display regenerated with lazy loading');
} catch (error) {
console.error('ā Error regenerating files display:', error);
showToast('Error updating display: ' + error.message, 'error');
}
}
// Load the next batch of files
function loadNextBatch() {
try {
if (currentlyLoadedFiles >= allResults.length) {
return; // All files loaded
}
const container = document.getElementById('filesContainer');
const endIndex = Math.min(currentlyLoadedFiles + ITEMS_PER_BATCH, allResults.length);
for (let i = currentlyLoadedFiles; i < endIndex; i++) {
const result = allResults[i];
const fileElement = createFileElement(result, i);
container.appendChild(fileElement);
}
currentlyLoadedFiles = endIndex;
// Show loading indicator if more files to load
if (currentlyLoadedFiles < allResults.length) {
showLoadingIndicator();
} else {
hideLoadingIndicator();
}
console.log('ā
Loaded batch:', currentlyLoadedFiles, '/', allResults.length);
} catch (error) {
console.error('ā Error loading next batch:', error);
}
}
// Create a file element
function createFileElement(result, index) {
const errorCount = result.issues.filter(i => i.severity === 'error').length;
const warnCount = result.issues.filter(i => i.severity === 'warn').length;
const infoCount = result.issues.filter(i => i.severity === 'info').length;
const fileDiv = document.createElement('div');
fileDiv.className = 'file-section';
fileDiv.setAttribute('data-file-index', index);
fileDiv.setAttribute('data-file-path', result.filePath);
let html = '<div class="file-header" onclick="toggleFile(' + index + ')">';
html += '<h3>š ' + result.filePath + '</h3>';
html += '<div class="file-stats">';
html += '<span class="error">' + errorCount + ' errors</span>';
html += '<span class="warning">' + warnCount + ' warnings</span>';
html += '<span class="info">' + infoCount + ' info</span>';
html += '<span class="expand-icon">ā¼</span>';
html += '</div></div>';
html += '<div class="issues-container">';
result.issues.forEach((issue, issueIndex) => {
const location = issue.line ? ' (line ' + issue.line + (issue.column ? ':' + issue.column : '') + ')' : '';
const codeSnippet = generateCodeSnippetForIssue(issue);
html += '<div class="issue" data-severity="' + issue.severity + '" data-category="' + issue.category + '">';
html += '<div class="issue-header">';
html += '<div class="issue-severity severity-' + issue.severity + '">' + issue.severity + '</div>';
html += '<div class="issue-content">';
html += '<div class="issue-message">' + escapeHtml(issue.message) + location + '</div>';
html += '<div class="issue-meta">';
html += '<span class="meta-item">Rule: ' + issue.rule + '</span>';
html += '<span class="meta-item">Category: ' + issue.category + '</span>';
if (issue.fixable) {
html += '<span class="meta-item fixable-tag">Fixable</span>';
}
html += '</div>';
html += codeSnippet;
html += '</div></div></div>';
});
html += '</div></div>';
fileDiv.innerHTML = html;
return fileDiv;
}
// Setup lazy scrolling
function setupLazyScrolling() {
// Remove existing listener if any
if (window.lazyScrollHandler) {
window.removeEventListener('scroll', window.lazyScrollHandler);
}
window.lazyScrollHandler = function() {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 1000) {
// User is near bottom, load more
if (currentlyLoadedFiles < allResults.length) {
loadNextBatch();
}
}
};
window.addEventListener('scroll', window.lazyScrollHandler);
}
// Show loading indicator
function showLoadingIndicator() {
let indicator = document.getElementById('loadingIndicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'loadingIndicator';
indicator.innerHTML = '<div class="loading-spinner">š Loading more files...</div>';
indicator.style.cssText = 'text-align: center; padding: 20px; color: #6c757d; font-style: italic;';
document.getElementById('filesContainer').appendChild(indicator);
}
}
// Hide loading indicator
function hideLoadingIndicator() {
const indicator = document.getElementById('loadingIndicator');
if (indicator) {
indicator.remove();
}
}
// Calculate statistics
function calculateStats(results) {
try {
let totalErrors = 0, totalWarnings = 0, totalInfo = 0;
results.forEach(result => {
result.issues.forEach(issue => {
switch (issue.severity) {
case 'error': totalErrors++; break;
case 'warn': totalWarnings++; break;
case 'info': totalInfo++; break;
}
});
});
return {
totalFiles: results.length,
totalErrors,
totalWarnings,
totalInfo
};
} catch (error) {
console.error('ā Error calculating stats:', error);
return { totalFiles: 0, totalErrors: 0, totalWarnings: 0, totalInfo: 0 };
}
}
// Generate code snippet for an issue
function generateCodeSnippetForIssue(issue) {
try {
const codeExamples = {
'no-var': { before: 'var userName = "john";', after: 'const userName = "john";' },
'prefer-const': { before: 'let config = { debug: true };', after: 'const config = { debug: true };' },
'no-console': { before: 'console.log("Debug message");', after: '// console.log("Debug message"); // Remove in production' },
'eslint-integration': {
before: issue.message.includes('quotes') ? 'const message = "Hello World";' : 'const result = data',
after: issue.message.includes('quotes') ? "const message = 'Hello World';" : 'const result = data;'
},
'prettier-formatting': { before: 'const obj={name:"test",value:123};', after: 'const obj = { name: "test", value: 123 };' },
'aria-required': { before: '<button onclick={handleClick}></button>', after: '<button onclick={handleClick} aria-label="Submit form"></button>' },
'alt-text-required': { before: '<img src="logo.png">', after: '<img src="logo.png" alt="Company logo">' },
'no-innerhtml': { before: 'element.innerHTML = userInput;', after: 'element.textContent = userInput; // Safer approach' },
'no-eval': { before: 'eval(userCode);', after: '// Use safer alternatives like JSON.parse() or specific parsers' },
'no-unused-vars': { before: 'let unusedVar = "test";', after: '// Remove unused variables or prefix with _ if intentional' },
'import-organization': { before: 'import { c, a, b } from "module";', after: 'import { a, b, c } from "module";' }
};
const example = codeExamples[issue.rule];
if (!example) return '';
return '<div class="code-snippet">' +
'<div class="snippet-header">š” Suggested Fix</div>' +
'<div class="snippet-content">' +
'<div class="code-before"><span class="code-line-number">-</span>' + escapeHtml(example.before) + '</div>' +
'<div class="code-after"><span class="code-line-number">+</span>' + escapeHtml(example.after) + '</div>' +
'</div></div>';
} catch (error) {
console.error('ā Error generating code snippet:', error);
return '';
}
}
// Escape HTML
function escapeHtml(text) {
try {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
} catch (error) {
console.error('ā Error escaping HTML:', error);
return text || '';
}
}
// Toggle file expansion with loading state
function toggleFile(index) {
try {
const section = document.querySelector('[data-file-index="' + index + '"]');
if (section) {
const header = section.querySelector('.file-header');
const expandIcon = section.querySelector('.expand-icon');
// Show mini loading
if (expandIcon) {
const originalText = expandIcon.textContent;
expandIcon.innerHTML = '<span class="loading-spinner"></span>';
setTimeout(() => {
section.classList.toggle('expanded');
expandIcon.textContent = section.classList.contains('expanded') ? 'ā²' : 'ā¼';
}, 150);
} else {
section.classList.toggle('expanded');
}
}
} catch (error) {
console.error('ā Error toggling file:', error);
}
}
// Expand all files with loading state
function expandAll() {
try {
const expandBtn = document.querySelector('[onclick*="expandAll"]');
setButtonLoading(expandBtn, true);
showProcessingOverlay('Expanding all files...');
setTimeout(() => {
document.querySelectorAll('.file-section').forEach(section => {
section.classList.add('expanded');
});
// Update all expand icons
document.querySelectorAll('.expand-icon').forEach(icon => {
icon.textContent = 'ā²';
});
showToast('All files expanded', 'success');
hideProcessingOverlay();
setButtonLoading(expandBtn, false);
}, 300);
} catch (error) {
console.error('ā Error expanding all:', error);
showToast('Error expanding files', 'error');
hideProcessingOverlay();
}
}
// Collapse all files with loading state
function collapseAll() {
try {
const collapseBtn = document.querySelector('[onclick*="collapseAll"]');
setButtonLoading(collapseBtn, true);
showProcessingOverlay('Collapsing all files...');
setTimeout(() => {
document.querySelectorAll('.file-section').forEach(section => {
section.classList.remove('expanded');
});
// Update all expand icons
document.querySelectorAll('.expand-icon').forEach(icon => {
icon.textContent = 'ā¼';
});
showToast('All files collapsed', 'success');
hideProcessingOverlay();
setButtonLoading(collapseBtn, false);
}, 300);
} catch (error) {
console.error('ā Error collapsing all:', error);
showToast('Error collapsing files', 'error');
hideProcessingOverlay();
}
}
// Filter by severity with loading state
function filterBySeverity() {
try {
const filterSelect = document.getElementById('severityFilter');
showMiniSpinner(filterSelect);
setTimeout(() => {
const filter = filterSelect.value;
document.querySelectorAll('.issue').forEach(issue => {
const severity = issue.dataset.severity;
if (filter === 'all' || severity === filter) {
issue.classList.remove('hidden');
} else {
issue.classList.add('hidden');
}
});
updateFileVisibility();
updateDashboardCounts(); // Update counts after filtering
hideMiniSpinner(filterSelect);
showToast('Filter applied successfully', 'success');
}, 100); // Small delay to show spinner
} catch (error) {
console.error('ā Error filtering by severity:', error);
showToast('Error applying filter', 'error');
}
}
// Filter by category with loading state
function filterByCategory() {
try {
const filterSelect = document.getElementById('categoryFilter');
showMiniSpinner(filterSelect);
setTimeout(() => {
const filter = filterSelect.value;
document.querySelectorAll('.issue').forEach(issue => {
const category = issue.dataset.category;
if (filter === 'all' || category === filter) {
issue.classList.remove('hidden');
} else {
issue.classList.add('hidden');
}
});
updateFileVisibility();
updateDashboardCounts(); // Update counts after filtering
hideMiniSpinner(filterSelect);
showToast('Filter applied successfully', 'success');
}, 100); // Small delay to show spinner
} catch (error) {
console.error('ā Error filtering by category:', error);
showToast('Error applying filter', 'error');
}
}
// Update file visibility based on visible issues
function updateFileVisibility() {
try {
document.querySelectorAll('.file-section').forEach(fileSection => {
const visibleIssues = fileSection.querySelectorAll('.issue:not(.hidden)');
if (visibleIssues.length === 0) {
fileSection.classList.add('hidden');
} else {
fileSection.classList.remove('hidden');
}
});
} catch (error) {
console.error('ā Error updating file visibility:', error);
}
}
// Update dashboard counts based on visible/filtered items
function updateDashboardCounts() {
try {
let visibleFiles = 0;
let visibleErrors = 0;
let visibleWarnings = 0;
let visibleInfo = 0;
document.querySelectorAll('.file-section:not(.hidden)').forEach(fileSection => {
visibleFiles++;
fileSection.querySelectorAll('.issue:not(.hidden)').forEach(issue => {
const severity = issue.dataset.severity;
switch (severity) {
case 'error': visibleErrors++; break;
case 'warn': visibleWarnings++; break;
case 'info': visibleInfo++; break;
}
});
});
// Update the dashboard counts
document.getElementById('totalFiles').textContent = visibleFiles;
document.getElementById('totalErrors').textContent = visibleErrors;
document.getElementById('totalWarnings').textContent = visibleWarnings;
document.getElementById('totalInfo').textContent = visibleInfo;
console.log('ā
Dashboard counts updated:', { visibleFiles, visibleErrors, visibleWarnings, visibleInfo });
} catch (error) {
console.error('ā Error updating dashboard counts:', error);
}
}
// Export to Excel with loading state
function exportToExcel() {
try {
if (typeof XLSX === 'undefined') {
showToast('Excel export library not loaded', 'error');
return;
}
const exportBtn = document.querySelector('[onclick*="exportToExcel"]');
showProcessingOverlay('Preparing Excel export...');
setButtonLoading(exportBtn, true);
setTimeout(() => {
try {
const data = [];
data.push(['File', 'Rule', 'Severity', 'Category', 'Message', 'Line', 'Column', 'Fixable']);
window.lintData.forEach(result => {
result.issues.forEach(issue => {
data.push([
result.filePath,
issue.rule,
issue.severity,
issue.category,
issue.message,
issue.line || '',
issue.column || '',
issue.fixable ? 'Yes' : 'No'
]);
});
});
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(data);
XLSX.utils.book_append_sheet(wb, ws, 'LWC Lint Results');
XLSX.writeFile(wb, 'lwc-lint-results-' + new Date().toISOString().split('T')[0] + '.xlsx');
showToast('Excel file downloaded successfully!', 'success');
} catch (exportError) {
console.error('ā Error during export:', exportError);
showToast('Export failed: ' + exportError.message, 'error');
} finally {
hideProcessingOverlay();
setButtonLoading(exportBtn, false);
}
}, 500); // Show loading for at least 500ms
} catch (error) {
console.error('ā Error exporting to Excel:', error);
showToast('Export failed: ' + error.message, 'error');
hideProcessingOverlay();
}
}
} catch (error) {
console.error('ā Critical dashboard error:', error);
// Show user-friendly error message
document.body.innerHTML = '<div style="padding: 40px; text-align: center; font-family: Arial, sans-serif;">' +
'<h2 style="color: #dc3545;">ā ļø Dashboard Error</h2>' +
'<p>There was an error loading the dashboard. Please refresh the page or check the console for details.</p>' +
'<p style="font-size: 0.9em; color: #6c757d;">Error: ' + error.message + '</p>' +
'</div>';
}
// Loading state helper functions
function showProcessingOverlay(message = 'Processing...') {
try {
const overlay = document.getElementById('processingOverlay');
const text = document.getElementById('processingText');
if (overlay && text) {
text.textContent = message;
overlay.classList.add('show');
}
} catch (error) {
console.error('ā Error showing processing overlay:', error);
}
}
function hideProcessingOverlay() {
try {
const overlay = document.getElementById('processingOverlay');
if (overlay) {
overlay.classList.remove('show');
}
} catch (error) {
console.error('ā Error hiding processing overlay:', error);
}
}
function setButtonLoading(button, isLoading) {
try {
if (!button) return;
if (isLoading) {
button.classList.add('btn-loading');
button.disabled = true;
if (!button.dataset.originalText) {
button.dataset.originalText = button.textContent;
}
} else {
button.classList.remove('btn-loading');
button.disabled = false;
if (button.dataset.originalText) {
button.textContent = button.dataset.originalText;
}
}
} catch (error) {
console.error('ā Error setting button loading state:', error);
}
}
function showMiniSpinner(element) {
try {
if (!element) return;
element.style.position = 'relative';
const spinner = document.createElement('div');
spinner.className = 'loading-spinner';
spinner.style.cssText = 'position: absolute; right: 10px; top: 50%; transform: translateY(-50%); z-index: 10;';
element.parentNode.appendChild(spinner);
element.dataset.hasSpinner = 'true';
} catch (error) {
console.error('ā Error showing mini spinner:', error);
}
}
function hideMiniSpinner(element) {
try {
if (!element || !element.dataset.hasSpinner) return;
const spinner = element.parentNode.querySelector('.loading-spinner');
if (spinner) {
spinner.remove();
}
delete element.dataset.hasSpinner;
} catch (error) {
console.error('ā Error hiding mini spinner:', error);
}
}
// Attach functions to global window object for HTML onclick handlers
window.toggleFile = toggleFile;
window.expandAll = expandAll;
window.collapseAll = collapseAll;
window.filterBySeverity = filterBySeverity;
window.filterByCategory = filterByCategory;
window.exportToExcel = exportToExcel;
window.showToast = showToast;
window.updateDashboardCounts = updateDashboardCounts;
window.loadNextBatch = loadNextBatch;
window.showProcessingOverlay = showProcessingOverlay;
window.hideProcessingOverlay = hideProcessingOverlay;
window.setButtonLoading = setButtonLoading;
window.showMiniSpinner = showMiniSpinner;
window.hideMiniSpinner = hideMiniSpinner;
if (${isServerMode}) {
window.manualRefresh = manualRefresh;
}
console.log('ā
Functions attached to window object');
// Function validation on load
document.addEventListener('DOMContentLoaded', function() {
try {
const requiredFunctions = ['expandAll', 'collapseAll', 'filterBySeverity', 'filterByCategory', 'exportToExcel', 'toggleFile'];
if (${isServerMode}) {
requiredFunctions.push('manualRefresh');
}
const missingFunctions = requiredFunctions.filter(func => typeof window[func] !== 'function');
if (missingFunctions.length > 0) {
console.error('ā Missing functions:', missingFunctions);
showToast('Some dashboard features may not work: ' + missingFunctions.join(', '), 'warning');
} else {
console.log('ā
All required functions are available');
}
console.log('š LWC Linter Dashboard loaded successfully!');
showToast('Dashboard loaded successfully!', 'success');
} catch (error) {
console.error('ā Error during DOMContentLoaded:', error);
}
});
</script>
</body>
</html>`;
// Minify and compress the HTML for faster loading
try {
const minifiedHTML = await this.compressHTML(html);
return minifiedHTML;
}
catch (error) {
console.warn('ā ļø HTML minification failed, returning uncompressed:', error.message);
return html;
}
}
async compressHTML(html) {
try {
const minifiedHTML = await (0, html_minifier_terser_1.minify)(html, {
removeComments: true,
removeRedundantAttributes: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeEmptyElements: false, // Keep for dynamic content
useShortDoctype: true,
collapseWhitespace: true,
conservativeCollapse: true,
collapseBooleanAttributes: true,
caseSensitive: false,
minifyCSS: {
level: 2,
inline: ['all'],
compatibility: '*'
},
minifyJS: true,
processConditionalComments: true,
removeAttributeQuotes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true
});
const originalSize = Buffer.byteLength(html, 'utf8');
const compressedSize = Buffer.byteLength(minifiedHTML, 'utf8');
const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
console.log(`š¦ HTML compressed: ${originalSize} ā ${compressedSize} bytes (${compressionRatio}% reduction)`);
return minifiedHTML;
}
catch (error) {
console.warn('ā ļø HTML compression failed:', error.message);
return html;
}
}
generateCodeSnippet(issue) {
const codeExamples = {
// ESLint Integration Rules
'no-var': {
before: 'var userName = "john";\nvar config = { debug: true };',
after: 'const userName = "john";\nconst config = { debug: true };'
},
'prefer-const': {
before: 'let config = { debug: true };\nlet API_URL = "https://api.example.com";',
after: 'const config = { debug: true };\nconst API_URL = "https://api.example.com";'
},
'no-console': {
before: 'console.log("Debug message");\nconsole.error("Error occurred");',
after: '// console.log("Debug message"); // Remove in production\n// Use proper logging framework instead'
},
'no-unused-vars': {
before: 'let unusedVar = "test";\nconst data = fetchData();\nreturn data.results;',
after: '// Remove unused variables\nconst data = fetchData();\nreturn data.results;'
},
'quotes': {
before: 'const message = "Hello World";\nconst name = "John";',
after: "const message = 'Hello World';\nconst name = 'John';"
},
'semi': {
before: 'const result = getData()\nconst user = { name: "John" }',
after: 'const result = getData();\nconst user = { name: "John" };'
},
// Prettier Integration Rules
'prettier-formatting': {
before: 'const obj={name:"test",value:123,active:true};\nfunction process(a,b,c){return a+b+c;}',
after: 'const obj = { name: "test", value: 123, active: true };\nfunction process(a, b, c) {\n return a + b + c;\n}'
},
'eslint-integration': {
before: issue.message.includes('quotes') ?
'const message = "Hello World";\nconst error = "Something went wrong";' :
'const result=data;const items=array.map(item=>item.id)',
after: issue.message.includes('quotes') ?
"const message = 'Hello World';\nconst error = 'Something went wrong';" :
'const result = data;\nconst items = array.map(item => item.id);'
},
'import-organization': {
before: 'import { getUserData, processData, validateInput } from "utils";\nimport { z, a, m } from "helpers";',
after: 'import { getUserData, processData, validateInput } from "utils";\nimport { a, m, z } from "helpers";'
},
// Accessibility Rules
'aria-required': {
before: '<button onclick={handleClick}>Submit</button>\n<input type="text" placeholder="Enter name" />',
after: '<button onclick={handleClick} aria-label="Submit form">Submit</button>\n<input type="text" placeholder="Enter name" aria-label="User name" />'
},
'alt-text-required': {
before: '<img src="logo.png" />\n<img src="chart.jpg" />',
after: '<img src="logo.png" alt="Company logo" />\n<img src="chart.jpg" alt="Sales performance chart" />'
},
'keyboard-navigation': {
before: '<div onclick={handleClick}>Click me</div>\n<span onclick={handleAction}>Action</span>',
after: '<button onclick={handleClick}>Click me</button>\n<button onclick={handleAction}>Action</button>'
},
'color-contrast': {
before: '.text { color: #ccc; background: #fff; }\n.warning { color: #ffff00; }',
after: '.text { color: #666; background: #fff; }\n.warning { color: #b8860b; /* Better contrast */ }'
},
// Security Rules
'no-innerhtml': {
before: 'element.innerHTML = userInput;\ndiv.innerHTML = `<p>${data}</p>`;',
after: 'element.textContent = userInput; // Safer approach\n// Use template literals with sanitization instead'
},
'no-eval': {
before: 'eval(userCode);\nconst result = eval(`return ${expression}`);',
after: '// Use safer alternatives like JSON.parse() or specific parsers\n// Consider using Function constructor with validation'
},
'validate-inputs': {
before: 'function processUser(data) {\n return database.save(data);\n}',
after: 'function processUser(data) {\n if (!data || !data.email) throw new Error("Invalid input");\n return database.save(sanitize(data));\n}'
},
'xss-prevention': {
before: 'const html = `<div>${userInput}</div>`;\ndocument.body.innerHTML = html;',
after: 'const html = `<div>${escapeHtml(userInput)}</div>`;\n// Use safe DOM manipulation methods'
},
// Performance Rules
'avoid-dom-queries': {
before: 'for (let i = 0; i < items.length; i++) {\n document.getElementById("list").appendChild(item);\n}',
after: 'const list = document.getElementById("list");\nfor (let i = 0; i < items.length; i++) {\n list.appendChild(item);\n}'
},
'lazy-loading': {
before: 'import { heavyLibrary } from "heavy-lib";\nconst result = heavyLibrary.process();',
after: '// Lazy load heavy dependencies\nconst { heavyLibrary } = await import("heavy-lib");\nconst result = heavyLibrary.process();'
},
'memory-leaks': {
before: 'window.addEventListener("resize", handleResize);\n// Event listener never removed',
after: 'window.addEventListener("resize", handleResize);\n// Remember to cleanup:\n// window.removeEventListener("resize", handleResize);'
},
'efficient-loops': {
before: 'for (let i = 0; i < array.length; i++) {\n if (array[i].heavy_operation()) { /* process */ }\n}',
after: 'const length = array.length;\nfor (let i = 0; i < length; i++) {\n const item = array[i];\n if (item.heavy_operation()) { /* process */ }\n}'
},
// Code Quality Rules
'use-strict-equality': {
before: 'if (value == null) { return; }\nif (count != 0) { process(); }',
after: 'if (value === null || value === undefined) { return; }\nif (count !== 0) { process(); }'
},
'error-handling': {
before: 'function fetchData() {\n const result = api.call();\n return result.data;\n}',
after: 'function fetchData() {\n try {\n const result = api.call();\n return result.data;\n } catch (error) {\n console.error("API call failed:", error);\n throw error;\n }\n}'
},
'consistent-naming': {
before: 'const user_name = "john";\nconst UserAge = 25;\nconst ISACTIVE = true;',
after: 'const userName = "john";\nconst userAge = 25;\nconst isActive = true;'
},
'function-complexity': {
before: 'function complexFunction(a, b, c, d, e) {\n if (a) { if (b) { if (c) { /* nested logic */ } } }\n return result;\n}',
after: 'function processData(data) {\n validateInput(data);\n return transformData(data);\n}\n\nfunction validateInput(data) { /* validation */ }\nfunction transformData(data) { /* transformation */ }'
},
// LWC Specific Rules
'lwc-component-naming': {
before: 'export default class myComponent extends LightningElement {\n // component logic\n}',
after: 'export default class MyComponent extends LightningElement {\n // component logic\n}'
},
'lwc-property-decorators': {
before: 'export default class MyComponent extends LightningElement {\n userInfo;\n @api recordId;\n}',
after: 'export default class MyComponent extends LightningElement {\n @api recordId;\n userInfo; // Private properties after public ones\n}'
},
'lwc-event-handling': {
before: '<button onclick="handleClick()">Click</button>',
after: '<button onclick={handleClick}>Click</button>'
},
// CSS Rules
'css-organization': {
before: '.component { font-size: 14px; color: red; margin: 10px; font-size: 16px; }',
after: '.component {\n margin: 10px;\n font-size: 16px; /* Remove duplicate */\n color: red;\n}'
},
'css-vendor-prefixes': {
before: '.box { transform: scale(1.1); transition: all 0.3s; }',
after: '.box {\n -webkit-transform: scale(1.1);\n transform: scale(1.1);\n -webkit-transition: all 0.3s;\n transition: all 0.3s;\n}'
}
};
const example = codeExamples[issue.rule];
if (!example)
return '';
return `
<div class="code-snippet">
<div class="snippet-header">š” Suggested Fix for "${issue.rule}"</div>
<div class="snippet-content">
<div class="code-before">
<span class="code-line-number">-</span>${this.escapeHtml(example.before)}
</div>
<div class="code-after">
<span class="code-line-number">+</span>${this.escapeHtml(example.after)}
</div>
</div>
</div>`;
}
getSeverityIcon(severity) {
switch (severity) {
case 'error':
return chalk_1.default.red('ā');
case 'warn':
return chalk_1.default.yellow('ā ļø ');
case 'info':
default:
return chalk_1.default.blue('ā¹ļø ');
}
}
generateSummary(results) {
let totalErrors = 0;
let totalWarnings = 0;
let totalInfo = 0;
let totalFixed = 0;
results.forEach(result => {
result.issues.forEach(issue => {
switch (issue.severity) {
case 'error':
totalErrors++;
break;
case 'warn':
totalWarnings++;
break;
case 'info':
totalInfo++;
break;
}
});
if (result.fixedCount) {
totalFixed += result.fixedCount;
}
});
return {
totalFiles: results.length,
totalIssues: totalErrors + totalWarnings + totalInfo,
totalErrors,
totalWarnings,
totalInfo
};
}
escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}
exports.OutputFormatter = OutputFormatter;
//# sourceMappingURL=output-formatter.js.map