sonarqube-issues-exporter
Version:
Enterprise-level SonarQube issues exporter with TypeScript support for generating comprehensive HTML reports with dark theme
206 lines • 8.21 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HtmlExporter = void 0;
const promises_1 = require("fs/promises");
const fs_1 = require("fs");
const path_1 = require("path");
const handlebars_1 = __importDefault(require("handlebars"));
const utils_1 = require("../utils");
const sonarqube_1 = require("../services/sonarqube");
class HtmlExporter {
logger = (0, utils_1.getLogger)();
config;
constructor(config) {
this.config = config;
this.registerHandlebarsHelpers();
}
registerHandlebarsHelpers() {
// Helper to add two numbers
handlebars_1.default.registerHelper('add', (a, b) => {
return (a || 0) + (b || 0);
});
// Helper to format dates
handlebars_1.default.registerHelper('formatDate', (date) => {
return (0, utils_1.formatDate)(date);
});
// Helper to escape HTML
handlebars_1.default.registerHelper('escapeHtml', (str) => {
return (0, utils_1.escapeHtml)(str);
});
// Helper for equality comparison
handlebars_1.default.registerHelper('eq', (a, b) => {
return a === b;
});
// Helper for greater than comparison
handlebars_1.default.registerHelper('gt', (a, b) => {
return a > b;
});
// Helper for logical OR
handlebars_1.default.registerHelper('or', (...args) => {
// Remove the options object (last argument)
const values = args.slice(0, -1);
return values.some(Boolean);
});
// Helper for conditional rendering
handlebars_1.default.registerHelper('if_eq', function (a, b, options) {
if (a === b) {
return options.fn(this);
}
return options.inverse(this);
});
// Helper to get current year
handlebars_1.default.registerHelper('currentYear', () => {
return new Date().getFullYear();
});
}
processIssues(issues) {
return issues.map((issue) => ({
key: issue.key,
file: (0, utils_1.extractFilename)(issue.component),
line: issue.line || 'N/A',
message: (0, utils_1.escapeHtml)(issue.message),
severity: issue.severity,
status: issue.status,
type: issue.type,
rule: issue.rule,
component: issue.component,
creationDate: (0, utils_1.formatDate)(issue.creationDate),
updateDate: issue.updateDate ? (0, utils_1.formatDate)(issue.updateDate) : undefined,
assignee: issue.assignee,
author: issue.author,
tags: issue.tags || [],
effort: issue.effort,
debt: issue.debt,
}));
}
calculateReportMetrics(issues) {
const metricsCalculator = (0, utils_1.calculateMetrics)(issues, {
severities: (issue) => issue.severity,
types: (issue) => issue.type,
statuses: (issue) => issue.status,
components: (issue) => issue.file,
rules: (issue) => issue.rule,
});
return {
total: issues.length,
severities: metricsCalculator.severities || {},
types: metricsCalculator.types || {},
statuses: metricsCalculator.statuses || {},
components: metricsCalculator.components || {},
rules: metricsCalculator.rules || {},
};
}
createReportMetadata(issues) {
return {
generatedAt: (0, utils_1.formatDate)(new Date()),
projectKey: this.config.sonarqube.projectKey,
sonarQubeUrl: this.config.sonarqube.url,
totalIssues: issues.length,
reportVersion: '2.0.0',
filters: {
excludedStatuses: this.config.export.excludeStatuses,
includeResolvedIssues: this.config.export.includeResolvedIssues,
},
};
}
async loadTemplate(templateName) {
const templatePath = (0, path_1.join)(__dirname, '..', 'templates', `${templateName}.hbs`);
if (!(0, fs_1.existsSync)(templatePath)) {
throw new Error(`Template not found: ${templatePath}`);
}
try {
return await (0, promises_1.readFile)(templatePath, 'utf-8');
}
catch (error) {
throw new Error(`Failed to read template: ${error}`);
}
}
async export(issues, options = {}) {
const { outputPath = this.config.export.outputPath, filename = this.config.export.filename, template = this.config.export.template, } = options;
try {
this.logger.info(`Starting HTML export with ${issues.length} issues`);
// Process issues
const processedIssues = this.processIssues(issues);
const metrics = this.calculateReportMetrics(processedIssues);
const metadata = this.createReportMetadata(processedIssues);
// Fetch enhanced data using SonarQube service
const enhancedData = await this.fetchEnhancedData();
// Prepare template data
const templateData = {
issues: processedIssues,
metrics,
metadata,
...enhancedData,
};
// Load and compile template
const templateContent = await this.loadTemplate(template);
const compiledTemplate = handlebars_1.default.compile(templateContent);
// Generate HTML
const html = compiledTemplate(templateData);
// Ensure output directory exists
const fullOutputPath = (0, path_1.join)(process.cwd(), outputPath);
if (!(0, fs_1.existsSync)(fullOutputPath)) {
await (0, promises_1.mkdir)(fullOutputPath, { recursive: true });
}
// Write HTML file
const filePath = (0, path_1.join)(fullOutputPath, filename);
await (0, promises_1.writeFile)(filePath, html, 'utf-8');
this.logger.info(`HTML report generated successfully: ${filePath}`);
return {
success: true,
outputPath: filePath,
issuesCount: issues.length,
metrics,
};
}
catch (error) {
this.logger.error('Failed to generate HTML report:', error);
return {
success: false,
outputPath: '',
issuesCount: 0,
metrics: {
total: 0,
severities: {},
types: {},
statuses: {},
components: {},
rules: {},
},
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Fetch enhanced data from SonarQube API
*/
async fetchEnhancedData() {
const sonarQubeService = new sonarqube_1.SonarQubeService(this.config.sonarqube);
try {
const [qualityGate, projectMeasures, securityHotspots] = await Promise.all([
sonarQubeService.getQualityGateStatus(),
sonarQubeService.getProjectMeasures(),
sonarQubeService.getSecurityHotspots(),
]);
return {
qualityGate,
projectMeasures,
securityHotspots,
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.warn('Failed to fetch enhanced data, using defaults:', errorMessage);
return {
qualityGate: { status: 'NONE', conditions: [] },
projectMeasures: {},
securityHotspots: { total: 0, byPriority: {}, byCategory: {}, hotspots: [] },
};
}
}
}
exports.HtmlExporter = HtmlExporter;
//# sourceMappingURL=html.js.map