lhci-collect
Version:
327 lines • 15.7 kB
JavaScript
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