import lighthouse from 'lighthouse'; import { launch } from 'chrome-launcher'; import PageSpeedTest from '../models/pageSpeedTest.model.js'; import path from 'path'; import fs from 'fs'; const reportsDir = path.join(process.cwd(), 'public', 'lighthouse-treemap'); // Ensure folder exists if (!fs.existsSync(reportsDir)) fs.mkdirSync(reportsDir, { recursive: true }); const launchChromeAndRunLighthouse = async (url, device = 'mobile') => { const chrome = await launch({ chromeFlags: ['--headless'] }); const options = { port: chrome.port, emulatedFormFactor: device, throttlingMethod: device === 'mobile' ? 'simulate' : 'devtools', output: 'json', // JSON for metrics }; const runnerResult = await lighthouse(url, options); const lhr = runnerResult.lhr; // Create HTML treemap report (only once, for mobile) let treemapFile = null; if (device === 'mobile') { const fileName = `treemap-${Date.now()}.html`; treemapFile = `/lighthouse-treemap/${fileName}`; // Generate HTML report const htmlReport = await lighthouse(url, { port: chrome.port, emulatedFormFactor: device, throttlingMethod: 'simulate', output: 'html', }); fs.writeFileSync(path.join(reportsDir, fileName), htmlReport.report); } await chrome.kill(); // Structured result const result = { url, device, scores: { performance: Math.round(lhr.categories.performance?.score * 100), accessibility: Math.round(lhr.categories.accessibility?.score * 100), bestPractices: Math.round(lhr.categories['best-practices']?.score * 100), seo: Math.round(lhr.categories.seo?.score * 100), pwa: lhr.categories.pwa?.score ? Math.round(lhr.categories.pwa.score * 100) : null, }, metrics: { firstContentfulPaint: lhr.audits['first-contentful-paint']?.displayValue || null, largestContentfulPaint: lhr.audits['largest-contentful-paint']?.displayValue || null, totalBlockingTime: lhr.audits['total-blocking-time']?.displayValue || null, timeToInteractive: lhr.audits['interactive']?.displayValue || null, speedIndex: lhr.audits['speed-index']?.displayValue || null, cumulativeLayoutShift: lhr.audits['cumulative-layout-shift']?.displayValue || null, }, opportunities: Object.values(lhr.audits) .filter(a => a.details?.type === 'opportunity') .map(a => ({ title: a.title, description: a.description, estimatedSavings: a.details?.overallSavingsMs ? `${Math.round(a.details.overallSavingsMs)} ms` : null, })), diagnostics: { usesHTTPS: lhr.audits['is-on-https']?.score === 1, usesEfficientCachePolicy: lhr.audits['uses-long-cache-ttl']?.score === 1, imageCompression: lhr.audits['uses-optimized-images']?.score === 1, }, failedAudits: Object.values(lhr.audits) .filter(a => a.score !== null && a.score !== 1 && a.scoreDisplayMode !== 'notApplicable') .map(a => ({ title: a.title, description: a.description })), passedAudits: Object.values(lhr.audits) .filter(a => a.score === 1 && a.scoreDisplayMode !== 'notApplicable' && !a.details?.type) .map(a => a.title), notApplicableAudits: Object.values(lhr.audits) .filter(a => a.scoreDisplayMode === 'notApplicable') .map(a => a.title), screenshot: lhr.audits['final-screenshot']?.details?.data || null, createdAt: new Date(), treemapPath: treemapFile, }; const report = await PageSpeedTest.create(result); return { report }; }; export const runAudit = async (req, res, next) => { try { const { url } = req.body; if (!url) return res.status(400).json({ message: 'URL is required' }); const mobileResult = await launchChromeAndRunLighthouse(url, 'mobile'); const desktopResult = await launchChromeAndRunLighthouse(url, 'desktop'); res.status(200).json({ message: 'Audit completed successfully', results: { mobile: mobileResult.report, desktop: desktopResult.report, treemap: mobileResult.report.treemapPath, // HTML report }, }); } catch (err) { next(err); } };