archunit
Version:
ArchUnit TypeScript is an architecture testing library, to specify and assert architecture rules in your TypeScript app
602 lines (534 loc) • 22.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MetricsExporter = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class MetricsExporter {
/**
* Export metrics summary as HTML file
*/
static async exportAsHTML(summary, options) {
const html = this.generateHTML(summary, options);
await this.writeFile(options.outputPath, html);
}
/**
* Export comprehensive metrics (all types) as HTML file with default path
*/
static async exportComprehensiveAsHTML(tsConfigPath, options = {}) {
// Set default output path if not provided
const defaultPath = path.join('reports', 'metrics-report.html');
const outputPath = options.outputPath || defaultPath;
// Gather all metrics
const comprehensive = await this.gatherComprehensiveMetrics(tsConfigPath);
const finalOptions = {
outputPath,
title: 'Comprehensive ArchUnitTS Metrics Report',
includeTimestamp: true,
...options,
};
await this.exportAsHTML(comprehensive, finalOptions);
}
/**
* Gather all available metrics for comprehensive reporting
*/
static async gatherComprehensiveMetrics(tsConfigPath) {
// Dynamically import metrics to avoid circular dependencies
const { metrics } = await Promise.resolve().then(() => __importStar(require('./metrics')));
const { DistanceMetricsBuilder } = await Promise.resolve().then(() => __importStar(require('./metrics/distance-metrics')));
// Get all metrics
const countSummary = await metrics().count().summary();
const lcomSummary = await metrics().lcom().summary();
const distanceSummary = await new DistanceMetricsBuilder(tsConfigPath).summary();
return {
count: countSummary,
lcom: lcomSummary,
distance: distanceSummary,
};
}
static generateHTML(summary, options) {
const title = options.title || 'ArchUnitTS Metrics Report';
const timestamp = options.includeTimestamp !== false ? new Date().toLocaleString() : '';
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
${this.getDefaultStyles()}
${options.customCss || ''}
</style>
</head>
<body>
<div class="container">
<header>
<h1>${title}, Beta</h1>
${timestamp ? `<p class="timestamp">Generated on: ${timestamp}</p>` : ''}
<strong>Use with caution, beta report.</strong>
</header>
<main>
${summary.count ? this.generateCountMetricsSection(summary.count) : ''}
${summary.lcom ? this.generateLCOMMetricsSection(summary.lcom) : ''}
${summary.distance ? this.generateDistanceMetricsSection(summary.distance) : ''}
</main>
<footer>
<p>Generated by ArchUnitTS Metrics System</p>
</footer>
</div>
</body>
</html>`;
}
static generateCountMetricsSection(summary) {
return `
<section class="metrics-section">
<h2>📊 Count Metrics</h2>
<div class="metrics-grid">
<div class="metric-card">
<h3>Project Overview</h3>
<div class="metric-value">${summary.totalFiles}</div>
<div class="metric-label">Total Files</div>
</div>
<div class="metric-card">
<h3>Classes</h3>
<div class="metric-value">${summary.totalClasses}</div>
<div class="metric-label">Total Classes</div>
</div>
</div>
<div class="metrics-grid">
<div class="metric-card">
<h3>Average Methods</h3>
<div class="metric-value">${summary.averageMethodsPerClass.toFixed(2)}</div>
<div class="metric-label">per Class</div>
</div>
<div class="metric-card">
<h3>Average Fields</h3>
<div class="metric-value">${summary.averageFieldsPerClass.toFixed(2)}</div>
<div class="metric-label">per Class</div>
</div>
<div class="metric-card">
<h3>Average Lines</h3>
<div class="metric-value">${summary.averageLinesOfCodePerFile.toFixed(2)}</div>
<div class="metric-label">per File</div>
</div>
<div class="metric-card">
<h3>Average Statements</h3>
<div class="metric-value">${summary.averageStatementsPerFile.toFixed(2)}</div>
<div class="metric-label">per File</div>
</div>
</div> <div class="highlights">
<div class="highlight-card">
<h4>📁 Largest File</h4>
<p><strong>${summary.largestFile.path}</strong></p>
<p>${summary.largestFile.lines} lines</p>
</div>
<div class="highlight-card">
<h4>🏗️ Largest Class</h4>
<p><strong>${summary.largestClass.name}</strong></p>
<p>${summary.largestClass.methods} methods</p>
</div>
</div>
</section>`;
}
static generateLCOMMetricsSection(summary) {
return `
<section class="metrics-section">
<h2>🔗 LCOM (Lack of Cohesion of Methods) Metrics</h2>
<div class="cohesion-overview">
<div class="metric-card large">
<h3>High Cohesion Classes</h3>
<div class="metric-value">${summary.highCohesionClassCount}</div>
<div class="metric-label">out of ${summary.totalClasses} total classes</div>
<div class="percentage">${((summary.highCohesionClassCount / summary.totalClasses) * 100).toFixed(1)}%</div>
</div>
</div>
<div class="lcom-variants">
<h3>LCOM Variants (Average Values)</h3>
<div class="metrics-grid">
<div class="metric-card">
<h4>LCOM96a</h4>
<div class="metric-value">${summary.averageLCOM96a.toFixed(3)}</div>
</div>
<div class="metric-card">
<h4>LCOM96b</h4>
<div class="metric-value">${summary.averageLCOM96b.toFixed(3)}</div>
</div>
<div class="metric-card">
<h4>LCOM1</h4>
<div class="metric-value">${summary.averageLCOM1.toFixed(3)}</div>
</div>
<div class="metric-card">
<h4>LCOM2</h4>
<div class="metric-value">${summary.averageLCOM2.toFixed(3)}</div>
</div>
<div class="metric-card">
<h4>LCOM3</h4>
<div class="metric-value">${summary.averageLCOM3.toFixed(3)}</div>
</div>
<div class="metric-card">
<h4>LCOM4</h4>
<div class="metric-value">${summary.averageLCOM4.toFixed(3)}</div>
</div>
<div class="metric-card">
<h4>LCOM5</h4>
<div class="metric-value">${summary.averageLCOM5.toFixed(3)}</div>
</div>
<div class="metric-card">
<h4>LCOM*</h4>
<div class="metric-value">${summary.averageLCOMStar.toFixed(3)}</div>
</div>
</div>
</div>
</section>`;
}
static generateDistanceMetricsSection(summary) {
// Determine architectural zones
const zoneOfPainWarning = summary.averageAbstractness < 0.3 && summary.averageInstability < 0.3;
const zoneOfUselessnessWarning = summary.averageAbstractness > 0.7 && summary.averageInstability > 0.7;
return `
<section class="metrics-section">
<h2>📏 Distance Metrics & Architectural Analysis</h2>
<div class="distance-overview">
<p class="section-description">
Distance metrics measure the architectural balance between abstraction and instability,
helping identify components that may need refactoring.
</p>
<div class="metrics-grid">
<div class="metric-card">
<h3>📁 Total Files</h3>
<div class="metric-value">${summary.totalFiles}</div>
<div class="metric-label">Files analyzed</div>
</div>
<div class="metric-card">
<h3>🎯 Files on Main Sequence</h3>
<div class="metric-value">${summary.filesOnMainSequence}</div>
<div class="metric-label">Well-balanced architecture</div>
</div>
</div>
<h3>🏗️ Core Architectural Metrics</h3>
<div class="metrics-grid">
<div class="metric-card ${summary.averageAbstractness < 0.3 ? 'warning' : summary.averageAbstractness > 0.7 ? 'good' : ''}">
<h3>📐 Average Abstractness (A)</h3>
<div class="metric-value">${summary.averageAbstractness.toFixed(3)}</div>
<div class="metric-label">0.0 (concrete) ↔ 1.0 (abstract)</div>
</div>
<div class="metric-card ${summary.averageInstability < 0.3 ? 'stable' : summary.averageInstability > 0.7 ? 'warning' : ''}">
<h3>⚖️ Average Instability (I)</h3>
<div class="metric-value">${summary.averageInstability.toFixed(3)}</div>
<div class="metric-label">0.0 (stable) ↔ 1.0 (unstable)</div>
</div>
<div class="metric-card ${summary.averageDistance < 0.2 ? 'good' : summary.averageDistance > 0.5 ? 'warning' : ''}">
<h3>📏 Distance from Main Sequence (D)</h3>
<div class="metric-value">${summary.averageDistance.toFixed(3)}</div>
<div class="metric-label">Deviation from ideal line (A + I = 1)</div>
</div>
</div>
<h3>🔗 Advanced Coupling Metrics</h3>
<div class="metrics-grid">
<div class="metric-card">
<h3>🔗 Average Coupling Factor (CF)</h3>
<div class="metric-value">${summary.averageCouplingFactor.toFixed(3)}</div>
<div class="metric-label">Degree of coupling between components</div>
</div>
<div class="metric-card">
<h3>📊 Normalized Distance (ND)</h3>
<div class="metric-value">${summary.averageNormalizedDistance.toFixed(3)}</div>
<div class="metric-label">Distance normalized by project context</div>
</div>
</div>
${zoneOfPainWarning || zoneOfUselessnessWarning
? `
<div class="architectural-alerts">
<h3>⚠️ Architectural Alerts</h3>
${zoneOfPainWarning
? `
<div class="alert alert-warning">
<strong>Zone of Pain Detected:</strong> Low abstractness (${summary.averageAbstractness.toFixed(3)})
and low instability (${summary.averageInstability.toFixed(3)}) indicate rigid, concrete components
that are difficult to change.
</div>`
: ''}
${zoneOfUselessnessWarning
? `
<div class="alert alert-info">
<strong>Zone of Uselessness Detected:</strong> High abstractness (${summary.averageAbstractness.toFixed(3)})
and high instability (${summary.averageInstability.toFixed(3)}) indicate abstract components
with excessive dependencies.
</div>`
: ''}
</div>`
: ''}
<div class="architectural-guidance">
<h3>📚 Interpretation Guide</h3>
<div class="guidance-grid">
<div class="guidance-card">
<h4>🎯 Ideal Zone (Main Sequence)</h4>
<p>Components on or near the main sequence (A + I ≈ 1) represent well-balanced architecture.</p>
</div>
<div class="guidance-card">
<h4>🔥 Zone of Pain</h4>
<p>Concrete & Stable (low A, low I) - Hard to extend, but changes are risky.</p>
</div>
<div class="guidance-card">
<h4>💸 Zone of Uselessness</h4>
<p>Abstract & Unstable (high A, high I) - Overly complex with little benefit.</p>
</div>
</div>
</div>
</div>
</section>`;
}
static getDefaultStyles() {
return `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f7fa;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
font-weight: 300;
}
.timestamp {
font-size: 1rem;
opacity: 0.9;
}
.metrics-section {
background: white;
margin-bottom: 30px;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.metrics-section h2 {
font-size: 1.8rem;
margin-bottom: 25px;
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.metric-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 20px;
border-radius: 8px;
text-align: center;
border: 1px solid #dee2e6;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.metric-card.large {
grid-column: span 2;
}
.metric-card h3, .metric-card h4 {
color: #495057;
margin-bottom: 10px;
font-size: 1.1rem;
}
.metric-value {
font-size: 2rem;
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.metric-label {
font-size: 0.9rem;
color: #6c757d;
}
.percentage {
font-size: 1.2rem;
color: #28a745;
font-weight: bold;
margin-top: 5px;
}
.highlights {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 25px;
}
.highlight-card {
background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
padding: 20px;
border-radius: 8px;
border-left: 4px solid #fdcb6e;
}
.highlight-card h4 {
color: #2d3436;
margin-bottom: 10px;
font-size: 1.2rem;
}
.highlight-card p {
margin-bottom: 5px;
color: #2d3436;
}
.cohesion-overview {
margin-bottom: 30px;
}
.lcom-variants h3 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 1.3rem;
} .distance-overview .section-description {
background: #e8f4f8;
padding: 15px;
border-radius: 6px;
margin-bottom: 25px;
color: #34495e;
font-style: italic;
}
/* New metric card status classes */
.metric-card.warning {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border-left: 4px solid #f39c12;
}
.metric-card.good {
background: linear-gradient(135deg, #d1f2eb 0%, #a3e9d0 100%);
border-left: 4px solid #27ae60;
}
.metric-card.stable {
background: linear-gradient(135deg, #d6eaf8 0%, #aed6f1 100%);
border-left: 4px solid #3498db;
}
/* Architectural alerts */
.architectural-alerts {
margin: 20px 0;
}
.alert {
padding: 15px;
border-radius: 6px;
margin-bottom: 15px;
border: 1px solid transparent;
}
.alert-warning {
background-color: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.alert-info {
background-color: #d1ecf1;
border-color: #b8daff;
color: #0c5460;
}
/* Guidance grid */
.guidance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.guidance-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 15px;
border-radius: 6px;
border-left: 4px solid #6c757d;
}
.guidance-card h4 {
color: #495057;
margin-bottom: 10px;
font-size: 1rem;
}
.guidance-card p {
color: #6c757d;
font-size: 0.9rem;
margin: 0;
}
footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: #6c757d;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
header h1 {
font-size: 2rem;
}
.metrics-grid {
grid-template-columns: 1fr;
}
.metric-card.large {
grid-column: span 1;
}
}
@media print {
body {
background-color: white;
}
.container {
max-width: none;
margin: 0;
padding: 0;
}
.metrics-section {
box-shadow: none;
border: 1px solid #ddd;
break-inside: avoid;
}
}`;
}
static async writeFile(filePath, content) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, content, 'utf8');
}
}
exports.MetricsExporter = MetricsExporter;
//# sourceMappingURL=export-utils.js.map