UNPKG

perf-audit-cli

Version:

CLI tool for continuous performance monitoring and analysis

225 lines 10.6 kB
import { PerformanceDatabaseService } from "../core/database/index.js"; import { formatSize } from "./size.js"; export class CIIntegration { static detectCIEnvironment() { const env = process.env; if (env.GITHUB_ACTIONS) { return { isCI: true, provider: 'github', branch: env.GITHUB_REF_NAME, commitHash: env.GITHUB_SHA, pullRequestId: env.GITHUB_EVENT_NAME === 'pull_request' ? env.GITHUB_EVENT_NUMBER : undefined, buildNumber: env.GITHUB_RUN_NUMBER, }; } if (env.GITLAB_CI) { return { isCI: true, provider: 'gitlab', branch: env.CI_COMMIT_REF_NAME, commitHash: env.CI_COMMIT_SHA, pullRequestId: env.CI_MERGE_REQUEST_IID, buildNumber: env.CI_PIPELINE_ID, }; } if (env.JENKINS_URL) { return { isCI: true, provider: 'jenkins', branch: env.GIT_BRANCH, commitHash: env.GIT_COMMIT, buildNumber: env.BUILD_NUMBER, }; } if (env.CI) { return { isCI: true, provider: 'unknown', branch: env.BRANCH, commitHash: env.COMMIT_SHA, }; } return { isCI: false, provider: 'unknown', }; } static async generateGitHubActionsSummary(result) { const serverTotalSize = result.serverBundles.reduce((sum, b) => sum + b.size, 0); const serverTotalGzipSize = result.serverBundles.reduce((sum, b) => sum + (b.gzipSize || 0), 0); const clientTotalSize = result.clientBundles.reduce((sum, b) => sum + b.size, 0); const clientTotalGzipSize = result.clientBundles.reduce((sum, b) => sum + (b.gzipSize || 0), 0); const statusEmoji = { ok: '✅', warning: '⚠️', error: '❌', }; let summary = `# 🎯 Performance Audit Report\n\n`; summary += `**Status:** ${statusEmoji[result.budgetStatus]} ${result.budgetStatus.toUpperCase()}\n`; summary += `**Server Total Size:** ${formatSize(serverTotalSize)} (${formatSize(serverTotalGzipSize)} gzipped)\n`; summary += `**Server Bundles:** ${result.serverBundles.length}\n`; summary += `**Client Total Size:** ${formatSize(clientTotalSize)} (${formatSize(clientTotalGzipSize)} gzipped)\n`; summary += `**Client Bundles:** ${result.clientBundles.length}\n\n`; summary += `## 📦 Server Bundle Analysis\n\n`; summary += `| Bundle | Size | Gzipped | Status |\n`; summary += `|--------|------|---------|--------|\n`; result.serverBundles.forEach(bundle => { const gzipText = bundle.gzipSize ? formatSize(bundle.gzipSize) : 'N/A'; const statusIcon = statusEmoji[bundle.status]; summary += `| \`${bundle.name}\` | ${formatSize(bundle.size)} | ${gzipText} | ${statusIcon} ${bundle.status} |\n`; }); summary += `## 📦 Client Bundle Analysis\n\n`; summary += `| Bundle | Size | Gzipped | Status |\n`; summary += `|--------|------|---------|--------|\n`; result.clientBundles.forEach(bundle => { const gzipText = bundle.gzipSize ? formatSize(bundle.gzipSize) : 'N/A'; const statusIcon = statusEmoji[bundle.status]; summary += `| \`${bundle.name}\` | ${formatSize(bundle.size)} | ${gzipText} | ${statusIcon} ${bundle.status} |\n`; }); if (result.lighthouse) { summary += `\n## 📊 Lighthouse Scores\n\n`; summary += `| Category | Score |\n`; summary += `|----------|-------|\n`; summary += `| Performance | ${result.lighthouse.performance}/100 |\n`; if (result.lighthouse.accessibility) { summary += `| Accessibility | ${result.lighthouse.accessibility}/100 |\n`; } if (result.lighthouse.bestPractices) { summary += `| Best Practices | ${result.lighthouse.bestPractices}/100 |\n`; } if (result.lighthouse.seo) { summary += `| SEO | ${result.lighthouse.seo}/100 |\n`; } if (result.lighthouse.metrics) { summary += `\n## 🚀 Core Web Vitals\n\n`; summary += `| Metric | Value |\n`; summary += `|--------|-------|\n`; summary += `| First Contentful Paint | ${result.lighthouse.metrics.fcp}ms |\n`; summary += `| Largest Contentful Paint | ${result.lighthouse.metrics.lcp}ms |\n`; summary += `| Cumulative Layout Shift | ${result.lighthouse.metrics.cls} |\n`; summary += `| Time to Interactive | ${result.lighthouse.metrics.tti}ms |\n`; } } if (result.recommendations.length > 0) { summary += `\n## 💡 Recommendations\n\n`; result.recommendations.forEach(rec => { summary += `- ${rec}\n`; }); } const comparison = await this.getHistoricalComparison(); if (comparison) { summary += `\n## 📈 Trend Analysis\n\n`; summary += comparison; } summary += `\n---\n`; summary += `Generated by perf-audit-cli on ${new Date(result.timestamp).toLocaleString()}\n`; return summary; } static generateJunitXml(result) { const testCases = []; result.serverBundles.forEach(bundle => { const testName = `Server Bundle size check: ${bundle.name}`; const className = 'ServerBundleBudgetTests'; if (bundle.status === 'ok') { testCases.push(` <testcase name="${testName}" classname="${className}" time="0"/>`); } else { const message = `Bundle ${bundle.name} exceeds budget: ${formatSize(bundle.size)}`; const type = bundle.status === 'error' ? 'failure' : 'error'; testCases.push(` <testcase name="${testName}" classname="${className}" time="0"> <${type} message="${message}"/> </testcase>`); } }); result.clientBundles.forEach(bundle => { const testName = `Client Bundle size check: ${bundle.name}`; const className = 'ClientBundleBudgetTests'; if (bundle.status === 'ok') { testCases.push(` <testcase name="${testName}" classname="${className}" time="0"/>`); } else { const message = `Bundle ${bundle.name} exceeds budget: ${formatSize(bundle.size)}`; const type = bundle.status === 'error' ? 'failure' : 'error'; testCases.push(` <testcase name="${testName}" classname="${className}" time="0"> <${type} message="${message}"/> </testcase>`); } }); if (result.lighthouse) { const performanceTest = `Performance score: ${result.lighthouse.performance}`; if (result.lighthouse.performance >= 90) { testCases.push(` <testcase name="${performanceTest}" classname="LighthouseTests" time="0"/>`); } else { testCases.push(` <testcase name="${performanceTest}" classname="LighthouseTests" time="0"> <failure message="Performance score below threshold: ${result.lighthouse.performance}/100"/> </testcase>`); } } const totalTests = testCases.length; const failures = testCases.filter(tc => tc.includes('<failure')).length; const errors = testCases.filter(tc => tc.includes('<error')).length; return `<?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="Performance Audit" tests="${totalTests}" failures="${failures}" errors="${errors}" time="0"> ${testCases.join('\n')} </testsuite> </testsuites>`; } static async outputCIAnnotations(result, ciContext) { if (!ciContext.isCI) return; if (ciContext.provider === 'github') { result.serverBundles.forEach(bundle => { if (bundle.status === 'error') { console.log(`::error::Server Bundle ${bundle.name} exceeds size budget: ${formatSize(bundle.size)}`); } else if (bundle.status === 'warning') { console.log(`::warning::Server Bundle ${bundle.name} approaching size budget: ${formatSize(bundle.size)}`); } }); result.clientBundles.forEach(bundle => { if (bundle.status === 'error') { console.log(`::error::Client Bundle ${bundle.name} exceeds size budget: ${formatSize(bundle.size)}`); } else if (bundle.status === 'warning') { console.log(`::warning::Client Bundle ${bundle.name} approaching size budget: ${formatSize(bundle.size)}`); } }); if (result.lighthouse && result.lighthouse.performance < 90) { console.log(`::warning::Lighthouse performance score below target: ${result.lighthouse.performance}/100`); } const summary = await this.generateGitHubActionsSummary(result); console.log(`\n${summary}`); } } static async getHistoricalComparison() { try { const db = await PerformanceDatabaseService.instance(); const recent = await db.getRecentBuilds({ limit: 2, orderBy: 'ASC' }); if (recent.length < 2) { await db.close(); return null; } const [current, previous] = recent; const comparison = await db.getBuildComparison(current.id, previous.id); await db.close(); if (comparison.bundleDiff.length === 0) return null; let trend = `Compared to previous build:\n`; comparison.bundleDiff.forEach(diff => { const change = diff.delta > 0 ? `+${formatSize(diff.delta)}` : formatSize(diff.delta); const arrow = diff.delta > 0 ? '📈' : '📉'; trend += `- \`${diff.name}\`: ${change} ${arrow}\n`; }); return trend; } catch { return null; } } } //# sourceMappingURL=ci-integration.js.map