UNPKG

lhci-collect

Version:

327 lines 15.7 kB
import { Command } from "commander"; import * as XLSX from 'xlsx'; import * as fs from 'fs'; import * as path from 'path'; import * as YAML from 'yaml'; export const program = new Command(); program .name('excel') .description('Generate Excel by Lighthouse CI Results') .option('-i, --input <dir>', 'Input dir path (default: .lighthouseci)', '.lighthouseci') .option('-o, --output <file>', 'Output file path (default: lighthouseci-results.xlsx)', 'lighthouseci-results.xlsx') .option('-t, --type <type>', 'Output type (excel, csv, json, yaml)', 'excel') .action(main); function extractTimestampFromFilename(filename) { const match = filename.match(/lhr-(\d+)\.json$/); return match ? match[1] : ''; } function parseLighthouseResult(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); const data = JSON.parse(content); const timestamp = extractTimestampFromFilename(path.basename(filePath)); return { timestamp, requestedUrl: data.requestedUrl || '', finalDisplayedUrl: data.finalDisplayedUrl || data.mainDocumentUrl || '', runWarnings: data.runWarnings || [], performance: data.categories?.performance?.score || 0, accessibility: data.categories?.accessibility?.score || 0, bestPractices: data.categories?.['best-practices']?.score || 0, seo: data.categories?.seo?.score || 0, fetchTime: data.fetchTime || '', lighthouseVersion: data.lighthouseVersion || '' }; } catch (error) { console.error(`Error parsing ${filePath}:`, error); return null; } } function calculateStats(results) { if (results.length === 0) { const emptyStats = { averageScore: 0, maxScore: 0, minScore: 0, totalScore: 0 }; return { performance: emptyStats, accessibility: emptyStats, bestPractices: emptyStats, seo: emptyStats, overall: emptyStats }; } const performanceScores = results.map(r => r.performance); const accessibilityScores = results.map(r => r.accessibility); const bestPracticesScores = results.map(r => r.bestPractices); const seoScores = results.map(r => r.seo); const overallScores = results.map(r => (r.performance + r.accessibility + r.bestPractices + r.seo) / 4); const calcStats = (scores) => ({ averageScore: scores.reduce((a, b) => a + b, 0) / scores.length, maxScore: Math.max(...scores), minScore: Math.min(...scores), totalScore: scores.reduce((a, b) => a + b, 0) }); return { performance: calcStats(performanceScores), accessibility: calcStats(accessibilityScores), bestPractices: calcStats(bestPracticesScores), seo: calcStats(seoScores), overall: calcStats(overallScores) }; } function main(options) { const inputDir = path.resolve(options.input); const outputFile = path.resolve(options.output); console.log(`Reading Lighthouse results from: ${inputDir}`); console.log(`Output file: ${outputFile}`); // 读取所有 lhr-*.json 文件 const files = fs.readdirSync(inputDir) .filter(file => file.match(/^lhr-\d+\.json$/)) .sort(); if (files.length === 0) { console.error('No Lighthouse result files found in the input directory.'); return; } console.log(`Found ${files.length} Lighthouse result files.`); // 解析所有文件 const results = []; for (const file of files) { const filePath = path.join(inputDir, file); const result = parseLighthouseResult(filePath); if (result) { results.push(result); } } if (results.length === 0) { console.error('No valid Lighthouse results parsed.'); return; } console.log(`Successfully parsed ${results.length} results.`); // 计算统计信息 const stats = calculateStats(results); // 准备Excel数据 const worksheetData = [ // 表头 [ 'Timestamp', 'Requested URL', 'Final URL', 'Run Warnings', 'Performance Score', 'Accessibility Score', 'Best Practices Score', 'SEO Score', 'Average Score', 'Fetch Time', 'Lighthouse Version' ], // 数据行 ...results.map(result => [ result.timestamp, result.requestedUrl, result.finalDisplayedUrl, result.runWarnings.join('; '), Math.round(result.performance * 100), // 转换为0-100整数 Math.round(result.accessibility * 100), Math.round(result.bestPractices * 100), Math.round(result.seo * 100), Math.round(((result.performance + result.accessibility + result.bestPractices + result.seo) / 4) * 100), result.fetchTime, result.lighthouseVersion ]), // 空行 [], // 统计信息 ['Statistics', '', '', '', '', '', '', '', '', '', ''], ['Performance Stats:', 'Average', 'Max', 'Min', 'Total', '', '', '', '', '', ''], ['Performance', Math.round(stats.performance.averageScore * 100), Math.round(stats.performance.maxScore * 100), Math.round(stats.performance.minScore * 100), Math.round(stats.performance.totalScore * 100)], ['Accessibility', Math.round(stats.accessibility.averageScore * 100), Math.round(stats.accessibility.maxScore * 100), Math.round(stats.accessibility.minScore * 100), Math.round(stats.accessibility.totalScore * 100)], ['Best Practices', Math.round(stats.bestPractices.averageScore * 100), Math.round(stats.bestPractices.maxScore * 100), Math.round(stats.bestPractices.minScore * 100), Math.round(stats.bestPractices.totalScore * 100)], ['SEO', Math.round(stats.seo.averageScore * 100), Math.round(stats.seo.maxScore * 100), Math.round(stats.seo.minScore * 100), Math.round(stats.seo.totalScore * 100)], ['Overall', Math.round(stats.overall.averageScore * 100), Math.round(stats.overall.maxScore * 100), Math.round(stats.overall.minScore * 100), Math.round(stats.overall.totalScore * 100)] ]; // 创建工作簿和工作表 const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.aoa_to_sheet(worksheetData); // 设置列宽 const columnWidths = [ { wch: 15 }, // Timestamp { wch: 50 }, // Requested URL { wch: 50 }, // Final URL { wch: 80 }, // Run Warnings { wch: 18 }, // Performance Score { wch: 20 }, // Accessibility Score { wch: 20 }, // Best Practices Score { wch: 12 }, // SEO Score { wch: 15 }, // Average Score { wch: 25 }, // Fetch Time { wch: 18 } // Lighthouse Version ]; worksheet['!cols'] = columnWidths; // 添加工作表到工作簿 XLSX.utils.book_append_sheet(workbook, worksheet, 'Lighthouse Results'); // 写入文件 if (options.type === 'csv') { const csvOutput = outputFile.replace(/\.xlsx?$/, '.csv'); XLSX.writeFile(workbook, csvOutput); console.log(`CSV file generated: ${csvOutput}`); } else if (options.type === 'json') { const jsonOutput = outputFile.replace(/\.xlsx?$/, '.json'); const jsonData = { summary: { totalResults: results.length, generatedAt: new Date().toISOString(), statistics: { performance: { averageScore: Math.round(stats.performance.averageScore * 100), maxScore: Math.round(stats.performance.maxScore * 100), minScore: Math.round(stats.performance.minScore * 100), totalScore: Math.round(stats.performance.totalScore * 100) }, accessibility: { averageScore: Math.round(stats.accessibility.averageScore * 100), maxScore: Math.round(stats.accessibility.maxScore * 100), minScore: Math.round(stats.accessibility.minScore * 100), totalScore: Math.round(stats.accessibility.totalScore * 100) }, bestPractices: { averageScore: Math.round(stats.bestPractices.averageScore * 100), maxScore: Math.round(stats.bestPractices.maxScore * 100), minScore: Math.round(stats.bestPractices.minScore * 100), totalScore: Math.round(stats.bestPractices.totalScore * 100) }, seo: { averageScore: Math.round(stats.seo.averageScore * 100), maxScore: Math.round(stats.seo.maxScore * 100), minScore: Math.round(stats.seo.minScore * 100), totalScore: Math.round(stats.seo.totalScore * 100) }, overall: { averageScore: Math.round(stats.overall.averageScore * 100), maxScore: Math.round(stats.overall.maxScore * 100), minScore: Math.round(stats.overall.minScore * 100), totalScore: Math.round(stats.overall.totalScore * 100) } } }, results: results.map(result => ({ ...result, performance: Math.round(result.performance * 100), accessibility: Math.round(result.accessibility * 100), bestPractices: Math.round(result.bestPractices * 100), seo: Math.round(result.seo * 100), averageScore: Math.round(((result.performance + result.accessibility + result.bestPractices + result.seo) / 4) * 100) })) }; fs.writeFileSync(jsonOutput, JSON.stringify(jsonData, null, 2), 'utf8'); console.log(`JSON file generated: ${jsonOutput}`); } else if (options.type === 'yaml') { const yamlOutput = outputFile.replace(/\.xlsx?$/, '.yaml'); const yamlData = { summary: { totalResults: results.length, generatedAt: new Date().toISOString(), statistics: { performance: { averageScore: Math.round(stats.performance.averageScore * 100), maxScore: Math.round(stats.performance.maxScore * 100), minScore: Math.round(stats.performance.minScore * 100), totalScore: Math.round(stats.performance.totalScore * 100) }, accessibility: { averageScore: Math.round(stats.accessibility.averageScore * 100), maxScore: Math.round(stats.accessibility.maxScore * 100), minScore: Math.round(stats.accessibility.minScore * 100), totalScore: Math.round(stats.accessibility.totalScore * 100) }, bestPractices: { averageScore: Math.round(stats.bestPractices.averageScore * 100), maxScore: Math.round(stats.bestPractices.maxScore * 100), minScore: Math.round(stats.bestPractices.minScore * 100), totalScore: Math.round(stats.bestPractices.totalScore * 100) }, seo: { averageScore: Math.round(stats.seo.averageScore * 100), maxScore: Math.round(stats.seo.maxScore * 100), minScore: Math.round(stats.seo.minScore * 100), totalScore: Math.round(stats.seo.totalScore * 100) }, overall: { averageScore: Math.round(stats.overall.averageScore * 100), maxScore: Math.round(stats.overall.maxScore * 100), minScore: Math.round(stats.overall.minScore * 100), totalScore: Math.round(stats.overall.totalScore * 100) } } }, results: results.map(result => ({ ...result, performance: Math.round(result.performance * 100), accessibility: Math.round(result.accessibility * 100), bestPractices: Math.round(result.bestPractices * 100), seo: Math.round(result.seo * 100), averageScore: Math.round(((result.performance + result.accessibility + result.bestPractices + result.seo) / 4) * 100) })) }; fs.writeFileSync(yamlOutput, YAML.stringify(yamlData), 'utf8'); console.log(`YAML file generated: ${yamlOutput}`); } else { XLSX.writeFile(workbook, outputFile); console.log(`Excel file generated: ${outputFile}`); } // 输出摘要信息 console.log('\n=== Summary ==='); console.log(`Total results processed: ${results.length}`); // 准备对齐的数据 const summaryData = [ { category: 'Performance', avg: Math.round(stats.performance.averageScore * 100), max: Math.round(stats.performance.maxScore * 100), min: Math.round(stats.performance.minScore * 100) }, { category: 'Accessibility', avg: Math.round(stats.accessibility.averageScore * 100), max: Math.round(stats.accessibility.maxScore * 100), min: Math.round(stats.accessibility.minScore * 100) }, { category: 'Best Practices', avg: Math.round(stats.bestPractices.averageScore * 100), max: Math.round(stats.bestPractices.maxScore * 100), min: Math.round(stats.bestPractices.minScore * 100) }, { category: 'SEO', avg: Math.round(stats.seo.averageScore * 100), max: Math.round(stats.seo.maxScore * 100), min: Math.round(stats.seo.minScore * 100) }, { category: 'Overall', avg: Math.round(stats.overall.averageScore * 100), max: Math.round(stats.overall.maxScore * 100), min: Math.round(stats.overall.minScore * 100) } ]; // 计算最大宽度 const maxCategoryWidth = Math.max(...summaryData.map(item => item.category.length)); const maxAvgWidth = Math.max(...summaryData.map(item => item.avg.toString().length)); const maxMaxWidth = Math.max(...summaryData.map(item => item.max.toString().length)); const maxMinWidth = Math.max(...summaryData.map(item => item.min.toString().length)); // 输出对齐的摘要信息 console.log(''); console.log(`${'Category'.padEnd(maxCategoryWidth)} | ${'Avg'.padStart(maxAvgWidth)} | ${'Max'.padStart(maxMaxWidth)} | ${'Min'.padStart(maxMinWidth)}`); console.log(`${'-'.repeat(maxCategoryWidth)}-+-${'-'.repeat(maxAvgWidth)}-+-${'-'.repeat(maxMaxWidth)}-+-${'-'.repeat(maxMinWidth)}`); summaryData.forEach(item => { const category = item.category.padEnd(maxCategoryWidth); const avg = item.avg.toString().padStart(maxAvgWidth); const max = item.max.toString().padStart(maxMaxWidth); const min = item.min.toString().padStart(maxMinWidth); console.log(`${category} | ${avg} | ${max} | ${min}`); }); } //# sourceMappingURL=index.js.map