lighthouse
Version:
Automated auditing, performance metrics, and best practices for the web.
295 lines (246 loc) • 9.18 kB
JavaScript
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'path';
import log from 'lighthouse-logger';
import {Runner} from '../runner.js';
import defaultConfig from './default-config.js';
import {nonSimulatedSettingsOverrides} from './constants.js';
import {
throwInvalidDependencyOrder,
isValidArtifactDependency,
throwInvalidArtifactDependency,
assertValidConfig,
} from './validation.js';
import {filterConfigByGatherMode, filterConfigByExplicitFilters} from './filters.js';
import {
deepCloneConfigJson,
resolveSettings,
resolveAuditsToDefns,
resolveGathererToDefn,
mergePlugins,
mergeConfigFragment,
mergeConfigFragmentArrayByKey,
} from './config-helpers.js';
import {getModuleDirectory} from '../../shared/esm-utils.js';
import * as format from '../../shared/localization/format.js';
const defaultConfigPath = path.join(
getModuleDirectory(import.meta),
'../../config/default-config.js'
);
/**
* Certain gatherers are destructive to the page state.
* We should ensure that these gatherers run after any custom gatherers.
* The default priority should be 0.
* TODO: Make this an official part of the config or design a different solution.
* @type {Record<string, number|undefined>}
*/
const internalArtifactPriorities = {
FullPageScreenshot: 1,
BFCacheFailures: 1,
};
/**
* @param {LH.Config|undefined} config
* @param {{configPath?: string}} context
* @return {{configWorkingCopy: LH.Config, configDir?: string, configPath?: string}}
*/
function resolveWorkingCopy(config, context) {
let {configPath} = context;
if (configPath && !path.isAbsolute(configPath)) {
throw new Error('configPath must be an absolute path');
}
if (!config) {
config = defaultConfig;
configPath = defaultConfigPath;
}
// The directory of the config path, if one was provided.
const configDir = configPath ? path.dirname(configPath) : undefined;
return {
configWorkingCopy: deepCloneConfigJson(config),
configPath,
configDir,
};
}
/**
* @param {LH.Config} config
* @return {LH.Config}
*/
function resolveExtensions(config) {
if (!config.extends) return config;
if (config.extends !== 'lighthouse:default') {
throw new Error('`lighthouse:default` is the only valid extension method.');
}
const {artifacts, ...extensionJSON} = config;
const defaultClone = deepCloneConfigJson(defaultConfig);
const mergedConfig = mergeConfigFragment(defaultClone, extensionJSON);
mergedConfig.artifacts = mergeConfigFragmentArrayByKey(
defaultClone.artifacts,
artifacts,
artifact => artifact.id
);
return mergedConfig;
}
/**
* Looks up the required artifact IDs for each dependency, throwing if no earlier artifact satisfies the dependency.
*
* @param {LH.Config.ArtifactJson} artifact
* @param {LH.Config.AnyGathererDefn} gatherer
* @param {Map<Symbol, LH.Config.AnyArtifactDefn>} artifactDefnsBySymbol
* @return {LH.Config.AnyArtifactDefn['dependencies']}
*/
function resolveArtifactDependencies(artifact, gatherer, artifactDefnsBySymbol) {
if (!('dependencies' in gatherer.instance.meta)) return undefined;
const dependencies = Object.entries(gatherer.instance.meta.dependencies).map(
([dependencyName, artifactSymbol]) => {
const dependency = artifactDefnsBySymbol.get(artifactSymbol);
// Check that dependency was defined before us.
if (!dependency) throwInvalidDependencyOrder(artifact.id, dependencyName);
// Check that the phase relationship is OK too.
const validDependency = isValidArtifactDependency(gatherer, dependency.gatherer);
if (!validDependency) throwInvalidArtifactDependency(artifact.id, dependencyName);
return [dependencyName, {id: dependency.id}];
}
);
return Object.fromEntries(dependencies);
}
/**
*
* @param {LH.Config.ArtifactJson[]|null|undefined} artifacts
* @param {string|undefined} configDir
* @return {Promise<LH.Config.AnyArtifactDefn[] | null>}
*/
async function resolveArtifactsToDefns(artifacts, configDir) {
if (!artifacts) return null;
const status = {msg: 'Resolve artifact definitions', id: 'lh:config:resolveArtifactsToDefns'};
log.time(status, 'verbose');
const sortedArtifacts = [...artifacts];
sortedArtifacts.sort((a, b) => {
const aPriority = internalArtifactPriorities[a.id] || 0;
const bPriority = internalArtifactPriorities[b.id] || 0;
return aPriority - bPriority;
});
/** @type {Map<Symbol, LH.Config.AnyArtifactDefn>} */
const artifactDefnsBySymbol = new Map();
const coreGathererList = Runner.getGathererList();
const artifactDefns = [];
for (const artifactJson of sortedArtifacts) {
const gathererJson = artifactJson.gatherer;
const gatherer = await resolveGathererToDefn(gathererJson, coreGathererList, configDir);
/** @type {LH.Config.AnyArtifactDefn} */
// @ts-expect-error - Typescript can't validate the gatherer and dependencies match
// even though it knows that they're each valid on their own.
const artifact = {
id: artifactJson.id,
gatherer,
dependencies: resolveArtifactDependencies(artifactJson, gatherer, artifactDefnsBySymbol),
};
const symbol = artifact.gatherer.instance.meta.symbol;
if (symbol) artifactDefnsBySymbol.set(symbol, artifact);
artifactDefns.push(artifact);
}
log.timeEnd(status);
return artifactDefns;
}
/**
* Overrides the settings that may not apply to the chosen gather mode.
*
* @param {LH.Config.Settings} settings
* @param {LH.Gatherer.GatherMode} gatherMode
*/
function overrideSettingsForGatherMode(settings, gatherMode) {
if (gatherMode === 'timespan') {
if (settings.throttlingMethod === 'simulate') {
settings.throttlingMethod = 'devtools';
}
}
}
/**
* Overrides the quiet windows when throttlingMethod requires observation.
*
* @param {LH.Config.Settings} settings
*/
function overrideThrottlingWindows(settings) {
if (settings.throttlingMethod === 'simulate') return;
settings.cpuQuietThresholdMs = Math.max(
settings.cpuQuietThresholdMs || 0,
nonSimulatedSettingsOverrides.cpuQuietThresholdMs
);
settings.networkQuietThresholdMs = Math.max(
settings.networkQuietThresholdMs || 0,
nonSimulatedSettingsOverrides.networkQuietThresholdMs
);
settings.pauseAfterFcpMs = Math.max(
settings.pauseAfterFcpMs || 0,
nonSimulatedSettingsOverrides.pauseAfterFcpMs
);
settings.pauseAfterLoadMs = Math.max(
settings.pauseAfterLoadMs || 0,
nonSimulatedSettingsOverrides.pauseAfterLoadMs
);
}
/**
* @param {LH.Gatherer.GatherMode} gatherMode
* @param {LH.Config=} config
* @param {LH.Flags=} flags
* @return {Promise<{resolvedConfig: LH.Config.ResolvedConfig}>}
*/
async function initializeConfig(gatherMode, config, flags = {}) {
const status = {msg: 'Initialize config', id: 'lh:config'};
log.time(status, 'verbose');
let {configWorkingCopy, configDir} = resolveWorkingCopy(config, flags);
configWorkingCopy = resolveExtensions(configWorkingCopy);
configWorkingCopy = await mergePlugins(configWorkingCopy, configDir, flags);
const settings = resolveSettings(configWorkingCopy.settings || {}, flags);
overrideSettingsForGatherMode(settings, gatherMode);
overrideThrottlingWindows(settings);
const artifacts = await resolveArtifactsToDefns(configWorkingCopy.artifacts, configDir);
/** @type {LH.Config.ResolvedConfig} */
let resolvedConfig = {
artifacts,
audits: await resolveAuditsToDefns(configWorkingCopy.audits, configDir),
categories: configWorkingCopy.categories || null,
groups: configWorkingCopy.groups || null,
settings,
};
assertValidConfig(resolvedConfig);
resolvedConfig = filterConfigByGatherMode(resolvedConfig, gatherMode);
resolvedConfig = filterConfigByExplicitFilters(resolvedConfig, settings);
log.timeEnd(status);
return {resolvedConfig};
}
/**
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @return {string}
*/
function getConfigDisplayString(resolvedConfig) {
/** @type {LH.Config.ResolvedConfig} */
const resolvedConfigCopy = JSON.parse(JSON.stringify(resolvedConfig));
if (resolvedConfigCopy.artifacts) {
for (const artifactDefn of resolvedConfigCopy.artifacts) {
// @ts-expect-error Breaking the Config.AnyArtifactDefn type.
artifactDefn.gatherer = artifactDefn.gatherer.path;
// Dependencies are not declared on Config JSON
artifactDefn.dependencies = undefined;
}
}
if (resolvedConfigCopy.audits) {
for (const auditDefn of resolvedConfigCopy.audits) {
// @ts-expect-error Breaking the Config.AuditDefn type.
auditDefn.implementation = undefined;
if (Object.keys(auditDefn.options).length === 0) {
// @ts-expect-error Breaking the Config.AuditDefn type.
auditDefn.options = undefined;
}
}
}
// Printed config is more useful with localized strings.
format.replaceIcuMessages(resolvedConfigCopy, resolvedConfigCopy.settings.locale);
return JSON.stringify(resolvedConfigCopy, null, 2);
}
export {
resolveWorkingCopy,
initializeConfig,
getConfigDisplayString,
};