UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

159 lines (136 loc) 5.13 kB
/** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview A runner that launches Chrome and executes Lighthouse via a * bundle to test that bundling has produced correct and runnable code. * Currently uses `lighthouse-dt-bundle.js`. * Runs in a worker to avoid messing up marky's global state. */ import fs from 'fs'; import os from 'os'; import {Worker, isMainThread, parentPort, workerData} from 'worker_threads'; import {once} from 'events'; import puppeteer from 'puppeteer-core'; import * as ChromeLauncher from 'chrome-launcher'; import thirdPartyWebLib from 'third-party-web/nostats-subset.js'; import {LH_ROOT} from '../../../../shared/root.js'; import {loadArtifacts, saveArtifacts} from '../../../../core/lib/asset-saver.js'; import {LocalConsole} from '../lib/local-console.js'; // This runs only in the worker. The rest runs on the main thread. if (!isMainThread && parentPort) { (async () => { const {url, config, testRunnerOptions} = workerData; try { const result = await runBundledLighthouse(url, config, testRunnerOptions); // Save to assets directory because LighthouseError won't survive postMessage. const assetsDir = fs.mkdtempSync(os.tmpdir() + '/smoke-bundle-assets-'); await saveArtifacts(result.artifacts, assetsDir); const value = { lhr: result.lhr, assetsDir, }; parentPort?.postMessage({type: 'result', value}); } catch (err) { console.error(err); parentPort?.postMessage({type: 'error', value: err.toString()}); } })(); } /** * @param {string} url * @param {LH.Config|undefined} config * @param {Smokehouse.SmokehouseOptions['testRunnerOptions']} testRunnerOptions * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts}>} */ async function runBundledLighthouse(url, config, testRunnerOptions) { if (isMainThread || !parentPort) { throw new Error('must be called in worker'); } const originalBuffer = global.Buffer; const originalRequire = global.require; const originalProcess = global.process; if (typeof globalThis === 'undefined') { // @ts-expect-error - exposing for loading of dt-bundle. global.globalThis = global; } // Load bundle, which creates a `global.runBundledLighthouse`. await import(LH_ROOT + '/dist/lighthouse-dt-bundle.js'); global.require = originalRequire; global.Buffer = originalBuffer; global.process = originalProcess; /** @type {import('../../../../core/index.js')['default']} */ // @ts-expect-error - not worth giving test global an actual type. const lighthouse = global.runBundledLighthouse; /** @type {import('../../../../core/lib/third-party-web.js')['default']} */ // @ts-expect-error const thirdPartyWeb = global.thirdPartyWeb; thirdPartyWeb.provideThirdPartyWeb(thirdPartyWebLib); // Launch and connect to Chrome. const launchedChrome = await ChromeLauncher.launch({ chromeFlags: [ testRunnerOptions?.headless ? '--headless=new' : '', ], }); const port = launchedChrome.port; // Run Lighthouse. try { const logLevel = testRunnerOptions?.isDebug ? 'verbose' : 'info'; // Puppeteer is not included in the bundle, we must create the page here. const browser = await puppeteer.connect({browserURL: `http://127.0.0.1:${port}`}); const page = await browser.newPage(); const runnerResult = await lighthouse(url, {port, logLevel}, config, page); if (!runnerResult) throw new Error('No runnerResult'); return { lhr: runnerResult.lhr, artifacts: runnerResult.artifacts, }; } finally { // Clean up and return results. launchedChrome.kill(); } } /** * Launch Chrome and do a full Lighthouse run via the Lighthouse DevTools bundle. * @param {string} url * @param {LH.Config=} config * @param {LocalConsole=} logger * @param {Smokehouse.SmokehouseOptions['testRunnerOptions']=} testRunnerOptions * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts}>} */ async function runLighthouse(url, config, logger, testRunnerOptions = {}) { logger = logger || new LocalConsole(); const worker = new Worker(new URL(import.meta.url), { stdout: true, stderr: true, workerData: {url, config, testRunnerOptions}, }); worker.stdout.setEncoding('utf8'); worker.stderr.setEncoding('utf8'); worker.stdout.addListener('data', (data) => { logger.log(`[STDOUT] ${data}`); }); worker.stderr.addListener('data', (data) => { logger.log(`[STDERR] ${data}`); }); const [workerResponse] = await once(worker, 'message'); if (workerResponse.type === 'error') { const log = logger.getLog(); throw new Error(`Worker returned an error: ${workerResponse.value}\nLog:\n${log}\n`); } const result = workerResponse.value; if (!result.lhr || !result.assetsDir) { throw new Error(`invalid response from worker:\n${JSON.stringify(result, null, 2)}`); } const artifacts = loadArtifacts(result.assetsDir); fs.rmSync(result.assetsDir, {recursive: true}); return { lhr: result.lhr, artifacts, }; } export { runLighthouse, };