lighthouse
Version:
Automated auditing, performance metrics, and best practices for the web.
135 lines (117 loc) • 4.16 kB
JavaScript
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import BaseGatherer from '../base-gatherer.js';
/**
* @template T, U
* @param {Array<T>} values
* @param {(value: T) => Promise<U>} promiseMapper
* @param {boolean} runInSeries
* @return {Promise<Array<U>>}
*/
async function runInSeriesOrParallel(values, promiseMapper, runInSeries) {
if (runInSeries) {
const results = [];
for (const value of values) {
const result = await promiseMapper(value);
results.push(result);
}
return results;
} else {
const promises = values.map(promiseMapper);
return await Promise.all(promises);
}
}
/**
* Returns true if the script was created via our own calls
* to Runtime.evaluate.
* @param {LH.Crdp.Debugger.ScriptParsedEvent} script
*/
function isLighthouseRuntimeEvaluateScript(script) {
// Scripts created by Runtime.evaluate that run on the main session/frame
// result in an empty string for the embedderName.
// Or, it means the script was dynamically created (eval, new Function, onload, ...)
if (!script.embedderName) return true;
// Otherwise, when running our own code inside other frames, the embedderName
// is set to the frame's url. In that case, we rely on the special sourceURL that
// we set.
return script.hasSourceURL && script.url === '_lighthouse-eval.js';
}
/**
* @fileoverview Gets JavaScript file contents.
*/
class Scripts extends BaseGatherer {
static symbol = Symbol('Scripts');
/** @type {LH.Gatherer.GathererMeta} */
meta = {
symbol: Scripts.symbol,
supportedModes: ['timespan', 'navigation'],
};
/** @type {LH.Crdp.Debugger.ScriptParsedEvent[]} */
_scriptParsedEvents = [];
/** @type {Array<string | undefined>} */
_scriptContents = [];
constructor() {
super();
this.onScriptParsed = this.onScriptParsed.bind(this);
}
/**
* @param {LH.Crdp.Debugger.ScriptParsedEvent} params
*/
onScriptParsed(params) {
if (!isLighthouseRuntimeEvaluateScript(params)) {
this._scriptParsedEvents.push(params);
}
}
/**
* @param {LH.Gatherer.Context} context
*/
async startInstrumentation(context) {
const session = context.driver.defaultSession;
session.on('Debugger.scriptParsed', this.onScriptParsed);
await session.sendCommand('Debugger.enable');
}
/**
* @param {LH.Gatherer.Context} context
*/
async stopInstrumentation(context) {
const session = context.driver.defaultSession;
const formFactor = context.baseArtifacts.HostFormFactor;
session.off('Debugger.scriptParsed', this.onScriptParsed);
// If run on a mobile device, be sensitive to memory limitations and only
// request one at a time.
this._scriptContents = await runInSeriesOrParallel(
this._scriptParsedEvents,
({scriptId}) => {
return session.sendCommand('Debugger.getScriptSource', {scriptId})
.then((resp) => resp.scriptSource)
.catch(() => undefined);
},
formFactor === 'mobile' /* runInSeries */
);
await session.sendCommand('Debugger.disable');
}
async getArtifact() {
/** @type {LH.Artifacts['Scripts']} */
const scripts = this._scriptParsedEvents.map((event, i) => {
// 'embedderName' and 'url' are confusingly named, so we rewrite them here.
// On the protocol, 'embedderName' always refers to the URL of the script (or HTML if inline).
// Same for 'url' ... except, magic "sourceURL=" comments will override the value.
// It's nice to display the user-provided value in Lighthouse, so we add a field 'name'
// to make it clear this is for presentational purposes.
// See https://chromium-review.googlesource.com/c/v8/v8/+/2317310
return {
name: event.url,
...event,
// embedderName is optional on the protocol because backends like Node may not set it.
// For our purposes, it is always set. But just in case it isn't... fallback to the url.
url: event.embedderName || event.url,
content: this._scriptContents[i],
};
});
return scripts;
}
}
export default Scripts;