UNPKG

lighthouse

Version:

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

321 lines (280 loc) • 11.5 kB
/** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import {Audit} from '../audits/audit.js'; /** @type {Record<keyof LH.BaseArtifacts, string>} */ const baseArtifactKeySource = { fetchTime: '', LighthouseRunWarnings: '', BenchmarkIndex: '', HostDPR: '', settings: '', Timing: '', URL: '', PageLoadError: '', HostFormFactor: '', HostUserAgent: '', HostProduct: '', GatherContext: '', }; const baseArtifactKeys = Object.keys(baseArtifactKeySource); // Some audits are used by the report for additional information. // Keep these audits unless they are *directly* skipped with `skipAudits`. /** @type {string[]} */ const filterResistantAuditIds = []; // Some artifacts are used by the report for additional information. // Always run these artifacts even if audits do not request them. // These are similar to base artifacts but they cannot be run in all 3 modes. const filterResistantArtifactIds = ['Stacks', 'NetworkUserAgent', 'FullPageScreenshot']; /** * Returns the set of audit IDs used in the list of categories. * If `onlyCategories` is not set, this function returns the list of all audit IDs across all * categories. * * @param {LH.Config.ResolvedConfig['categories']} allCategories * @param {string[] | undefined} onlyCategories * @return {Set<string>} */ function getAuditIdsInCategories(allCategories, onlyCategories) { if (!allCategories) return new Set(); onlyCategories = onlyCategories || Object.keys(allCategories); const categories = onlyCategories.map(categoryId => allCategories[categoryId]); const auditRefs = categories.flatMap(category => category?.auditRefs || []); return new Set(auditRefs.map(auditRef => auditRef.id)); } /** * Filters an array of artifacts down to the set that's required by the specified audits. * * @param {LH.Config.ResolvedConfig['artifacts']} artifacts * @param {LH.Config.ResolvedConfig['audits']} audits * @return {LH.Config.ResolvedConfig['artifacts']} */ function filterArtifactsByAvailableAudits(artifacts, audits) { if (!artifacts) return null; if (!audits) return artifacts; const artifactsById = new Map(artifacts.map(artifact => [artifact.id, artifact])); /** @type {Set<string>} */ const artifactIdsToKeep = new Set([ ...filterResistantArtifactIds, ...audits.flatMap(audit => audit.implementation.meta.requiredArtifacts), ]); // Keep all artifacts in the dependency tree of required artifacts. // Iterate through all kept artifacts, adding their dependencies along the way, until the set does not change. let previousSize = 0; while (previousSize !== artifactIdsToKeep.size) { previousSize = artifactIdsToKeep.size; for (const artifactId of artifactIdsToKeep) { const artifact = artifactsById.get(artifactId); // This shouldn't happen because the config has passed validation by this point. if (!artifact) continue; // If the artifact doesn't have any dependencies, we can move on. if (!artifact.dependencies) continue; // Add all of the artifact's dependencies to our set. for (const dep of Object.values(artifact.dependencies)) { artifactIdsToKeep.add(dep.id); } } } return artifacts.filter(artifact => artifactIdsToKeep.has(artifact.id)); } /** * Filters an array of artifacts down to the set that supports the specified gather mode. * * @param {LH.Config.ResolvedConfig['artifacts']} artifacts * @param {LH.Gatherer.GatherMode} mode * @return {LH.Config.ResolvedConfig['artifacts']} */ function filterArtifactsByGatherMode(artifacts, mode) { if (!artifacts) return null; return artifacts.filter(artifact => { return artifact.gatherer.instance.meta.supportedModes.includes(mode); }); } /** * Filters an array of audits down to the set that can be computed using only the specified artifacts. * * @param {LH.Config.ResolvedConfig['audits']} audits * @param {Array<LH.Config.AnyArtifactDefn>} availableArtifacts * @return {LH.Config.ResolvedConfig['audits']} */ function filterAuditsByAvailableArtifacts(audits, availableArtifacts) { if (!audits) return null; const availableArtifactIds = new Set( availableArtifacts.map(artifact => artifact.id).concat(baseArtifactKeys) ); return audits.filter(audit => { const meta = audit.implementation.meta; return meta.requiredArtifacts.every(id => availableArtifactIds.has(id)); }); } /** * Optional `supportedModes` property can explicitly exclude an audit even if all required artifacts are available. * * @param {LH.Config.ResolvedConfig['audits']} audits * @param {LH.Gatherer.GatherMode} mode * @return {LH.Config.ResolvedConfig['audits']} */ function filterAuditsByGatherMode(audits, mode) { if (!audits) return null; return audits.filter(audit => { const meta = audit.implementation.meta; return !meta.supportedModes || meta.supportedModes.includes(mode); }); } /** * Optional `supportedModes` property can explicitly exclude a category even if some audits are available. * * @param {LH.Config.ResolvedConfig['categories']} categories * @param {LH.Gatherer.GatherMode} mode * @return {LH.Config.ResolvedConfig['categories']} */ function filterCategoriesByGatherMode(categories, mode) { if (!categories) return null; const categoriesToKeep = Object.entries(categories) .filter(([_, category]) => { return !category.supportedModes || category.supportedModes.includes(mode); }); return Object.fromEntries(categoriesToKeep); } /** * Filters a categories object and their auditRefs down to the specified category ids. * * @param {LH.Config.ResolvedConfig['categories']} categories * @param {string[] | null | undefined} onlyCategories * @return {LH.Config.ResolvedConfig['categories']} */ function filterCategoriesByExplicitFilters(categories, onlyCategories) { if (!categories || !onlyCategories) return categories; const categoriesToKeep = Object.entries(categories) .filter(([categoryId]) => onlyCategories.includes(categoryId)); return Object.fromEntries(categoriesToKeep); } /** * Throw an error if any specified onlyCategory is not a known category that can * be included. * * @param {LH.Config.ResolvedConfig['categories']} allCategories * @param {string[] | null} onlyCategories * @return {void} */ function errorOnUnknownOnlyCategories(allCategories, onlyCategories) { if (!onlyCategories) return; const unknown = onlyCategories.filter(c => !allCategories?.[c]); if (unknown.length) { throw new Error(`unrecognized category in 'onlyCategories': ${unknown.join(', ')}`); } } /** * Filters a categories object and their auditRefs down to the set that can be computed using * only the specified audits. * * @param {LH.Config.ResolvedConfig['categories']} categories * @param {Array<LH.Config.AuditDefn>} availableAudits * @return {LH.Config.ResolvedConfig['categories']} */ function filterCategoriesByAvailableAudits(categories, availableAudits) { if (!categories) return categories; const availableAuditIdToMeta = new Map( availableAudits.map(audit => [audit.implementation.meta.id, audit.implementation.meta]) ); const categoryEntries = Object.entries(categories) .map(([categoryId, category]) => { const filteredCategory = { ...category, auditRefs: category.auditRefs.filter(ref => availableAuditIdToMeta.has(ref.id)), }; const didFilter = filteredCategory.auditRefs.length < category.auditRefs.length; const hasOnlyManualAudits = filteredCategory.auditRefs.every(ref => { const meta = availableAuditIdToMeta.get(ref.id); if (!meta) return false; return meta.scoreDisplayMode === Audit.SCORING_MODES.MANUAL; }); // If we filtered out audits and the only ones left are manual, remove them too. if (didFilter && hasOnlyManualAudits) filteredCategory.auditRefs = []; return [categoryId, filteredCategory]; }) .filter(entry => typeof entry[1] === 'object' && entry[1].auditRefs.length); return Object.fromEntries(categoryEntries); } /** * Filters a config's artifacts, audits, and categories down to the set that supports the specified gather mode. * * @param {LH.Config.ResolvedConfig} resolvedConfig * @param {LH.Gatherer.GatherMode} mode * @return {LH.Config.ResolvedConfig} */ function filterConfigByGatherMode(resolvedConfig, mode) { const artifacts = filterArtifactsByGatherMode(resolvedConfig.artifacts, mode); const supportedAudits = filterAuditsByGatherMode(resolvedConfig.audits, mode); const audits = filterAuditsByAvailableArtifacts(supportedAudits, artifacts || []); const supportedCategories = filterCategoriesByGatherMode(resolvedConfig.categories, mode); const categories = filterCategoriesByAvailableAudits(supportedCategories, audits || []); return { ...resolvedConfig, artifacts, audits, categories, }; } /** * Filters a config's artifacts, audits, and categories down to the requested set. * Skip audits overrides inclusion via `onlyAudits`/`onlyCategories`. * * @param {LH.Config.ResolvedConfig} resolvedConfig * @param {Pick<LH.Config.Settings, 'onlyAudits'|'onlyCategories'|'skipAudits'>} filters * @return {LH.Config.ResolvedConfig} */ function filterConfigByExplicitFilters(resolvedConfig, filters) { const {onlyAudits, onlyCategories, skipAudits} = filters; if (onlyAudits && !onlyAudits.length) { throw new Error(`onlyAudits cannot be an empty array.`); } if (onlyCategories && !onlyCategories.length) { throw new Error(`onlyCategories cannot be an empty array.`); } errorOnUnknownOnlyCategories(resolvedConfig.categories, onlyCategories); let baseAuditIds = getAuditIdsInCategories(resolvedConfig.categories, undefined); if (onlyCategories) { baseAuditIds = getAuditIdsInCategories(resolvedConfig.categories, onlyCategories); } else if (onlyAudits) { baseAuditIds = new Set(); } else if (!resolvedConfig.categories || !Object.keys(resolvedConfig.categories).length) { baseAuditIds = new Set(resolvedConfig.audits?.map(audit => audit.implementation.meta.id)); } const auditIdsToKeep = new Set( [ ...baseAuditIds, // Start with our base audits. ...(onlyAudits || []), // Additionally include the opt-in audits from `onlyAudits`. ...filterResistantAuditIds, // Always include any filter-resistant audits. ].filter(auditId => !skipAudits || !skipAudits.includes(auditId)) ); const audits = auditIdsToKeep.size && resolvedConfig.audits ? resolvedConfig.audits.filter(audit => auditIdsToKeep.has(audit.implementation.meta.id)) : resolvedConfig.audits; const availableCategories = filterCategoriesByAvailableAudits(resolvedConfig.categories, audits || []); const categories = filterCategoriesByExplicitFilters(availableCategories, onlyCategories); let artifacts = filterArtifactsByAvailableAudits(resolvedConfig.artifacts, audits); if (artifacts && resolvedConfig.settings.disableFullPageScreenshot) { artifacts = artifacts.filter(({id}) => id !== 'FullPageScreenshot'); } return { ...resolvedConfig, artifacts, audits, categories, }; } export { filterConfigByGatherMode, filterConfigByExplicitFilters, filterArtifactsByGatherMode, filterArtifactsByAvailableAudits, filterAuditsByAvailableArtifacts, filterAuditsByGatherMode, filterCategoriesByAvailableAudits, filterCategoriesByExplicitFilters, filterCategoriesByGatherMode, };