UNPKG

beam-cli

Version:

A beautifully simple CLI for running Lighthouse audits on a statically generated (SSG) website

165 lines (164 loc) 6.13 kB
import ora from 'ora'; import lighthouse from 'lighthouse'; import speakingurl from 'speakingurl'; import chalk from 'chalk'; import desktopConfig from 'lighthouse/lighthouse-core/config/desktop-config.js'; import multimatch from 'multimatch'; import puppeteer from 'puppeteer'; import { analyseLighthouseResults } from '../analyse/analyse-lighthouse-results.js'; import { buildProgressBar } from '../ui/non-interactive/progress-bar.js'; import { saveReportsToDisk } from './save-reports.js'; import { saveResultsToDisk } from './save-results.js'; /** * Build the Flags object required for Lighthouse. */ const buildLighthouseFlags = (port, selectedLighthouseOptions, output) => { const categories = selectedLighthouseOptions.categories === 'all' ? undefined : selectedLighthouseOptions.categories; const lighthouseFlags = { logLevel: 'silent', output, onlyCategories: categories, port, }; const runTypes = []; if (selectedLighthouseOptions.desktop) runTypes.push('desktop'); if (selectedLighthouseOptions.mobile || runTypes.length === 0) runTypes.push('mobile'); return { lighthouseFlags, runTypes }; }; /** * Picks the relevent Lighthouse Options for the specified URL. */ const pickLighthouseOptions = (urlPath, options) => { if (!Array.isArray(options.lighthouse)) return options.lighthouse; const config = options.lighthouse.find(cfg => { if (!cfg.targets) return true; return multimatch(urlPath, cfg.targets).length > 0; }); return config; }; /** * Builds the string which will be evaluated within the * puppeteer page as it is opened. * The eval string will set the `pageWindowFlags` from the * lighthouse options as varibles on the 'window' (global). * @param flags * @returns */ const puppeteerWindowFlagCommands = (flags) => ` var __puppeteerFlags = JSON.parse('${JSON.stringify(flags)}'); for (const k in __puppeteerFlags) { window[k] = __puppeteerFlags[k]; } `; const determineTestsToRun = (urls, options, chromePort, output) => { const testsToRun = []; for (const url of urls) { const selectedLighthouseOptions = pickLighthouseOptions(url, options); const { lighthouseFlags, runTypes } = buildLighthouseFlags(chromePort, selectedLighthouseOptions, output); for (const runType of runTypes) { testsToRun.push({ runType, lighthouseFlags, url, mediaFeatures: selectedLighthouseOptions?.mediaFeatures, pageWindowFlags: selectedLighthouseOptions?.pageWindowFlags, }); } } return testsToRun; }; export const runLighthouseAudits = async (urls, port, options) => { const spinner = ora({ color: 'cyan', }); let browser; let chromePort = 0; try { spinner.start('Launching Chrome (headless)'); browser = await puppeteer.launch({ headless: true, defaultViewport: null, }); chromePort = Number.parseInt(new URL(browser.wsEndpoint()).port, 10); spinner.succeed('Launched Chrome (headless).'); } catch { spinner.fail('Error launching Chrome'); } if (!browser || !chromePort) return; const output = []; if (options.output.folder) { if (options.output.html) output.push('html'); if (options.output.json) output.push('json'); } const testsToRun = determineTestsToRun(urls, options, chromePort, output); const bar = buildProgressBar(testsToRun.length); spinner.start(`Running Lighthouse Tests\n ${bar(0)}`); const startTime = Date.now().valueOf(); const runnerResults = {}; try { let count = 0; for (const testRun of testsToRun) { const { url, runType, lighthouseFlags, mediaFeatures, pageWindowFlags } = testRun; spinner.text = `Running Lighthouse Tests\n ${bar(count)} ${chalk.grey(url)} (${runType})`; const fullUrl = `http://localhost:${port}/${url}`; const slug = `${speakingurl(url)}-${runType}`; const config = runType === 'desktop' ? desktopConfig : undefined; browser.once('targetchanged', async (target) => { const page = await target.page(); if (!page) return; if (mediaFeatures) { await page.emulateMediaFeatures(mediaFeatures); } if (pageWindowFlags) { await page.evaluate(puppeteerWindowFlagCommands(pageWindowFlags)); } }); // ! Don't have full typing for this import, thus disabling compiler checks /* eslint-disable no-await-in-loop */ /* eslint-disable @typescript-eslint/no-unsafe-call */ const runResult = (await lighthouse(fullUrl, lighthouseFlags, config)); /* eslint-enable @typescript-eslint/no-unsafe-call */ /* eslint-enable no-await-in-loop */ runnerResults[slug] = { result: runResult, type: runType, url, }; count += 1; } const endTime = Date.now().valueOf(); const duration = Math.round((endTime - startTime) / 100) / 10; spinner.succeed(`Lighthouse Tests Completed. [${duration} s]`); } catch (error) { spinner.fail('Tests aborted.'); throw error; } finally { spinner.start('Closing Chrome'); await browser.close(); spinner.succeed('Closed Chrome.'); } let generatedHtmlPaths = {}; if (output && output.length > 0) { generatedHtmlPaths = await saveReportsToDisk(runnerResults, options, output); } const results = analyseLighthouseResults(runnerResults, generatedHtmlPaths); if (output && output.length > 0 && options.output.folder) { await saveResultsToDisk(results, options.output.folder); } return results; };