chrome-devtools-frontend
Version:
Chrome DevTools UI
278 lines (242 loc) • 10.2 kB
text/typescript
// Copyright (c) 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Root from '../../core/root/root.js';
import * as Puppeteer from '../../services/puppeteer/puppeteer.js';
import type * as SDK from '../../core/sdk/sdk.js';
function disableLoggingForTest(): void {
console.log = (): void => undefined; // eslint-disable-line no-console
}
/**
* LegacyPort is provided to Lighthouse as the CDP connection in legacyNavigation mode.
* Its complement is https://github.com/GoogleChrome/lighthouse/blob/v9.3.1/lighthouse-core/gather/connections/raw.js
* It speaks pure CDP via notifyFrontendViaWorkerMessage
*
* Any message that comes back from Lighthouse has to go via a so-called "port".
* This class holds the relevant callbacks that Lighthouse provides and that
* can be called in the onmessage callback of the worker, so that the frontend
* can communicate to Lighthouse. Lighthouse itself communicates to the frontend
* via status updates defined below.
*/
class LegacyPort {
onMessage?: (message: string) => void;
onClose?: () => void;
on(eventName: string, callback: (arg?: string) => void): void {
if (eventName === 'message') {
this.onMessage = callback;
} else if (eventName === 'close') {
this.onClose = callback;
}
}
send(message: string): void {
notifyFrontendViaWorkerMessage('sendProtocolMessage', {message});
}
close(): void {
}
}
/**
* ConnectionProxy is a SDK interface, but the implementation has no knowledge it's a parallelConnection.
* The CDP traffic is smuggled back and forth by the system described in LighthouseProtocolService
*/
class ConnectionProxy implements SDK.Connections.ParallelConnectionInterface {
sessionId: string;
onMessage: ((arg0: Object) => void)|null;
onDisconnect: ((arg0: string) => void)|null;
constructor(sessionId: string) {
this.sessionId = sessionId;
this.onMessage = null;
this.onDisconnect = null;
}
setOnMessage(onMessage: (arg0: (Object|string)) => void): void {
this.onMessage = onMessage;
}
setOnDisconnect(onDisconnect: (arg0: string) => void): void {
this.onDisconnect = onDisconnect;
}
getOnDisconnect(): (((arg0: string) => void)|null) {
return this.onDisconnect;
}
getSessionId(): string {
return this.sessionId;
}
sendRawMessage(message: string): void {
notifyFrontendViaWorkerMessage('sendProtocolMessage', {message});
}
async disconnect(): Promise<void> {
this.onDisconnect?.('force disconnect');
this.onDisconnect = null;
this.onMessage = null;
}
}
const legacyPort = new LegacyPort();
let cdpConnection: ConnectionProxy|undefined;
let endTimespan: (() => unknown)|undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function invokeLH(action: string, args: any): Promise<unknown> {
if (Root.Runtime.Runtime.queryParam('isUnderTest')) {
disableLoggingForTest();
args.flags.maxWaitForLoad = 2 * 1000;
}
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
self.listenForStatus(message => {
notifyFrontendViaWorkerMessage('statusUpdate', {message: message[1]});
});
let puppeteerConnection: Awaited<ReturnType<typeof Puppeteer.PuppeteerConnection['getPuppeteerConnection']>>|
undefined;
try {
// For timespan we only need to perform setup on startTimespan.
// Config, flags, locale, etc. should be stored in the closure of endTimespan.
if (action === 'endTimespan') {
if (!endTimespan) {
throw new Error('Cannot end a timespan before starting one');
}
const result = await endTimespan();
endTimespan = undefined;
return result;
}
const locale = await fetchLocaleData(args.locales);
const flags = args.flags;
flags.logLevel = flags.logLevel || 'info';
flags.channel = 'devtools';
flags.locale = locale;
// TODO: Remove this filter once pubads is mode restricted
// https://github.com/googleads/publisher-ads-lighthouse-plugin/pull/339
if (action === 'startTimespan' || action === 'snapshot') {
args.categoryIDs = args.categoryIDs.filter((c: string) => c !== 'lighthouse-plugin-publisher-ads');
}
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
const config = args.config || self.createConfig(args.categoryIDs, flags.emulatedFormFactor);
const url = args.url;
// Handle legacy Lighthouse runner path.
if (action === 'navigation' && flags.legacyNavigation) {
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
const connection = self.setUpWorkerConnection(legacyPort);
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
return await self.runLighthouse(url, flags, config, connection);
}
const {mainTargetId, mainFrameId, mainSessionId} = args.target;
cdpConnection = new ConnectionProxy(mainSessionId);
puppeteerConnection =
await Puppeteer.PuppeteerConnection.getPuppeteerConnection(cdpConnection, mainFrameId, mainTargetId);
const {page} = puppeteerConnection;
const configContext = {
logLevel: flags.logLevel,
settingsOverrides: flags,
};
if (action === 'snapshot') {
// TODO: Remove `configContext` once Lighthouse roll removes it
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
return await self.runLighthouseSnapshot({config, page, configContext, flags});
}
if (action === 'startTimespan') {
// TODO: Remove `configContext` once Lighthouse roll removes it
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
const timespan = await self.startLighthouseTimespan({config, page, configContext, flags});
endTimespan = timespan.endTimespan;
return;
}
// TODO: Remove `configContext` once Lighthouse roll removes it
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
return await self.runLighthouseNavigation(url, {config, page, configContext, flags});
} catch (err) {
return ({
fatal: true,
message: err.message,
stack: err.stack,
});
} finally {
// endTimespan will need to use the same connection as startTimespan.
if (action !== 'startTimespan') {
puppeteerConnection?.browser.disconnect();
}
}
}
/**
* Finds a locale supported by Lighthouse from the user's system locales.
* If no matching locale is found, or if fetching locale data fails, this function returns nothing
* and Lighthouse will use `en-US` by default.
*/
async function fetchLocaleData(locales: string[]): Promise<string|void> {
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
const locale = self.lookupLocale(locales);
// If the locale is en-US, no need to fetch locale data.
if (locale === 'en-US' || locale === 'en') {
return;
}
// Try to load the locale data.
try {
const remoteBase = Root.Runtime.getRemoteBase();
let localeUrl: string;
if (remoteBase && remoteBase.base) {
localeUrl = `${remoteBase.base}third_party/lighthouse/locales/${locale}.json`;
} else {
localeUrl = new URL(`../../third_party/lighthouse/locales/${locale}.json`, import.meta.url).toString();
}
const timeoutPromise = new Promise<string>(
(resolve, reject) => setTimeout(() => reject(new Error('timed out fetching locale')), 5000));
const localeData = await Promise.race([timeoutPromise, fetch(localeUrl).then(result => result.json())]);
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
self.registerLocaleData(locale, localeData);
return locale;
} catch (err) {
console.error(err);
}
return;
}
/**
* `notifyFrontendViaWorkerMessage` and `onFrontendMessage` work with the FE's ProtocolService.
*
* onFrontendMessage takes action-wrapped messages and either invoking lighthouse or delivering it CDP traffic.
* notifyFrontendViaWorkerMessage posts action-wrapped messages to the FE.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function notifyFrontendViaWorkerMessage(action: string, args: any): void {
self.postMessage({action, args});
}
async function onFrontendMessage(event: MessageEvent): Promise<void> {
const messageFromFrontend = event.data;
switch (messageFromFrontend.action) {
case 'startTimespan':
case 'endTimespan':
case 'snapshot':
case 'navigation': {
const result = await invokeLH(messageFromFrontend.action, messageFromFrontend.args);
if (result && typeof result === 'object') {
// Report isn't used upstream.
if ('report' in result) {
// @ts-expect-error
delete result.report;
}
// Logger PerformanceTiming objects cannot be cloned by this worker's `postMessage` function.
if ('artifacts' in result) {
// @ts-expect-error
result.artifacts.Timing = JSON.parse(JSON.stringify(result.artifacts.Timing));
}
}
self.postMessage({id: messageFromFrontend.id, result});
break;
}
case 'dispatchProtocolMessage': {
cdpConnection?.onMessage?.(messageFromFrontend.args.message);
legacyPort.onMessage?.(JSON.stringify(messageFromFrontend.args.message));
break;
}
default: {
throw new Error(`Unknown event: ${event.data}`);
}
}
}
self.onmessage = onFrontendMessage;
// Make lighthouse and traceviewer happy.
// @ts-ignore https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global = self;
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.isVinn = true;
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.document = {};
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.document.documentElement = {};
// @ts-expect-error https://github.com/GoogleChrome/lighthouse/issues/11628
globalThis.global.document.documentElement.style = {
WebkitAppearance: 'WebkitAppearance',
};