UNPKG

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
"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 ` <!DOCTYPE html> <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