import { CountMetricsSummary } from './metrics/count-metrics'; import { LCOMMetricsSummary } from './metrics/lcom-metrics'; import { DistanceMetricsSummary } from './metrics/distance-metrics'; import * as fs from 'fs'; import * as path from 'path'; export interface ExportOptions { outputPath?: string; title?: string; includeTimestamp?: boolean; customCss?: string; } export interface ComprehensiveMetricsSummary { count: CountMetricsSummary; lcom: LCOMMetricsSummary; distance: DistanceMetricsSummary; } export interface ProjectMetricsSummary { count?: CountMetricsSummary; lcom?: LCOMMetricsSummary; distance?: DistanceMetricsSummary; } export class MetricsExporter { /** * Export metrics summary as HTML file */ static async exportAsHTML( summary: ProjectMetricsSummary, options: ExportOptions ): Promise { 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?: string, options: Partial = {} ): Promise { // 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: ExportOptions = { 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?: string ): Promise { // Dynamically import metrics to avoid circular dependencies const { metrics } = await import('./metrics'); const { DistanceMetricsBuilder } = await import('./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, }; } private static generateHTML( summary: ProjectMetricsSummary, options: ExportOptions ): string { const title = options.title || 'ArchUnitTS Metrics Report'; const timestamp = options.includeTimestamp !== false ? new Date().toLocaleString() : ''; return ` ${title}

${title}, Beta

${timestamp ? `

Generated on: ${timestamp}

` : ''} Use with caution, beta report.
${summary.count ? this.generateCountMetricsSection(summary.count) : ''} ${summary.lcom ? this.generateLCOMMetricsSection(summary.lcom) : ''} ${summary.distance ? this.generateDistanceMetricsSection(summary.distance) : ''}

Generated by ArchUnitTS Metrics System

`; } private static generateCountMetricsSection(summary: CountMetricsSummary): string { return `

📊 Count Metrics

Project Overview

${summary.totalFiles}
Total Files

Classes

${summary.totalClasses}
Total Classes

Average Methods

${summary.averageMethodsPerClass.toFixed(2)}
per Class

Average Fields

${summary.averageFieldsPerClass.toFixed(2)}
per Class

Average Lines

${summary.averageLinesOfCodePerFile.toFixed(2)}
per File

Average Statements

${summary.averageStatementsPerFile.toFixed(2)}
per File

📁 Largest File

${summary.largestFile.path}

${summary.largestFile.lines} lines

🏗️ Largest Class

${summary.largestClass.name}

${summary.largestClass.methods} methods

`; } private static generateLCOMMetricsSection(summary: LCOMMetricsSummary): string { return `

🔗 LCOM (Lack of Cohesion of Methods) Metrics

High Cohesion Classes

${summary.highCohesionClassCount}
out of ${summary.totalClasses} total classes
${((summary.highCohesionClassCount / summary.totalClasses) * 100).toFixed(1)}%

LCOM Variants (Average Values)

LCOM96a

${summary.averageLCOM96a.toFixed(3)}

LCOM96b

${summary.averageLCOM96b.toFixed(3)}

LCOM1

${summary.averageLCOM1.toFixed(3)}

LCOM2

${summary.averageLCOM2.toFixed(3)}

LCOM3

${summary.averageLCOM3.toFixed(3)}

LCOM4

${summary.averageLCOM4.toFixed(3)}

LCOM5

${summary.averageLCOM5.toFixed(3)}

LCOM*

${summary.averageLCOMStar.toFixed(3)}
`; } private static generateDistanceMetricsSection( summary: DistanceMetricsSummary ): string { // Determine architectural zones const zoneOfPainWarning = summary.averageAbstractness < 0.3 && summary.averageInstability < 0.3; const zoneOfUselessnessWarning = summary.averageAbstractness > 0.7 && summary.averageInstability > 0.7; return `

📏 Distance Metrics & Architectural Analysis

Distance metrics measure the architectural balance between abstraction and instability, helping identify components that may need refactoring.

📁 Total Files

${summary.totalFiles}
Files analyzed

🎯 Files on Main Sequence

${summary.filesOnMainSequence}
Well-balanced architecture

🏗️ Core Architectural Metrics

📐 Average Abstractness (A)

${summary.averageAbstractness.toFixed(3)}
0.0 (concrete) ↔ 1.0 (abstract)

⚖️ Average Instability (I)

${summary.averageInstability.toFixed(3)}
0.0 (stable) ↔ 1.0 (unstable)

📏 Distance from Main Sequence (D)

${summary.averageDistance.toFixed(3)}
Deviation from ideal line (A + I = 1)

🔗 Advanced Coupling Metrics

🔗 Average Coupling Factor (CF)

${summary.averageCouplingFactor.toFixed(3)}
Degree of coupling between components

📊 Normalized Distance (ND)

${summary.averageNormalizedDistance.toFixed(3)}
Distance normalized by project context
${ zoneOfPainWarning || zoneOfUselessnessWarning ? `

⚠️ Architectural Alerts

${ zoneOfPainWarning ? `
Zone of Pain Detected: Low abstractness (${summary.averageAbstractness.toFixed(3)}) and low instability (${summary.averageInstability.toFixed(3)}) indicate rigid, concrete components that are difficult to change.
` : '' } ${ zoneOfUselessnessWarning ? `
Zone of Uselessness Detected: High abstractness (${summary.averageAbstractness.toFixed(3)}) and high instability (${summary.averageInstability.toFixed(3)}) indicate abstract components with excessive dependencies.
` : '' }
` : '' }

📚 Interpretation Guide

🎯 Ideal Zone (Main Sequence)

Components on or near the main sequence (A + I ≈ 1) represent well-balanced architecture.

🔥 Zone of Pain

Concrete & Stable (low A, low I) - Hard to extend, but changes are risky.

💸 Zone of Uselessness

Abstract & Unstable (high A, high I) - Overly complex with little benefit.

`; } private static getDefaultStyles(): string { 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; } }`; } private static async writeFile(filePath: string, content: string): Promise { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content, 'utf8'); } }