fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

74
node_modules/lighthouse/core/config/budget.d.ts generated vendored Normal file
View File

@@ -0,0 +1,74 @@
export class Budget {
/**
* Asserts that obj has no own properties, throwing a nice error message if it does.
* `objectName` is included for nicer logging.
* @param {Record<string, unknown>} obj
* @param {string} objectName
*/
static assertNoExcessProperties(obj: Record<string, unknown>, objectName: string): void;
/**
* Asserts that `strings` has no duplicate strings in it, throwing an error if
* it does. `arrayName` is included for nicer logging.
* @param {Array<string>} strings
* @param {string} arrayName
*/
static assertNoDuplicateStrings(strings: Array<string>, arrayName: string): void;
/**
* @param {Record<string, unknown>} resourceBudget
* @return {LH.Budget.ResourceBudget}
*/
static validateResourceBudget(resourceBudget: Record<string, unknown>): LH.Budget.ResourceBudget;
/**
* @param {unknown} path
* @param {string} error
*/
static throwInvalidPathError(path: unknown, error: string): void;
/**
* Validates that path is either: a) undefined or ) properly formed.
* Verifies the quantity and location of the two robot.txt regex characters: $, *
* @param {unknown} path
* @return {undefined|string}
*/
static validatePath(path: unknown): undefined | string;
/**
* Returns the budget that applies to a given URL.
* If multiple budgets match based on thier 'path' property,
* then the last-listed of those budgets is returned.
* @param {LH.Util.Immutable<Array<LH.Budget>>|null} budgets
* @param {string|undefined} url
* @return {LH.Util.Immutable<LH.Budget> | undefined} budget
*/
static getMatchingBudget(budgets: LH.Util.Immutable<Array<LH.Budget>> | null, url: string | undefined): LH.Util.Immutable<LH.Budget> | undefined;
/**
* Determines whether a URL matches against a robots.txt-style "path".
* Pattern should use the robots.txt format. E.g. "/*-article.html" or "/". Reference:
* https://developers.google.com/search/reference/robots_txt#url-matching-based-on-path-values
* @param {string} url
* @param {string=} pattern
* @return {boolean}
*/
static urlMatchesPattern(url: string, pattern?: string | undefined): boolean;
/**
* @param {Record<string, unknown>} timingBudget
* @return {LH.Budget.TimingBudget}
*/
static validateTimingBudget(timingBudget: Record<string, unknown>): LH.Budget.TimingBudget;
/**
* @param {string} hostname
* @return {string}
*/
static validateHostname(hostname: string): string;
/**
* @param {unknown} hostnames
* @return {undefined|Array<string>}
*/
static validateHostnames(hostnames: unknown): undefined | Array<string>;
/**
* More info on the Budget format:
* https://github.com/GoogleChrome/lighthouse/issues/6053#issuecomment-428385930
* @param {unknown} budgetJson
* @return {Array<LH.Budget>}
*/
static initializeBudget(budgetJson: unknown): Array<LH.Budget>;
}
//# sourceMappingURL=budget.d.ts.map

340
node_modules/lighthouse/core/config/budget.js generated vendored Normal file
View File

@@ -0,0 +1,340 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @param {unknown} arr
* @return {arr is Array<Record<string, unknown>>}
*/
function isArrayOfUnknownObjects(arr) {
return Array.isArray(arr) && arr.every(isObjectOfUnknownProperties);
}
/**
* @param {unknown} val
* @return {val is Record<string, unknown>}
*/
function isObjectOfUnknownProperties(val) {
return typeof val === 'object' && val !== null && !Array.isArray(val);
}
/**
* Returns whether `val` is numeric. Will not coerce to a number. `NaN` will
* return false, however ±Infinity will return true.
* @param {unknown} val
* @return {val is number}
*/
function isNumber(val) {
return typeof val === 'number' && !isNaN(val);
}
class Budget {
/**
* Asserts that obj has no own properties, throwing a nice error message if it does.
* `objectName` is included for nicer logging.
* @param {Record<string, unknown>} obj
* @param {string} objectName
*/
static assertNoExcessProperties(obj, objectName) {
const invalidKeys = Object.keys(obj);
if (invalidKeys.length > 0) {
const keys = invalidKeys.join(', ');
throw new Error(`${objectName} has unrecognized properties: [${keys}]`);
}
}
/**
* Asserts that `strings` has no duplicate strings in it, throwing an error if
* it does. `arrayName` is included for nicer logging.
* @param {Array<string>} strings
* @param {string} arrayName
*/
static assertNoDuplicateStrings(strings, arrayName) {
const foundStrings = new Set();
for (const string of strings) {
if (foundStrings.has(string)) {
throw new Error(`${arrayName} has duplicate entry of type '${string}'`);
}
foundStrings.add(string);
}
}
/**
* @param {Record<string, unknown>} resourceBudget
* @return {LH.Budget.ResourceBudget}
*/
static validateResourceBudget(resourceBudget) {
const {resourceType, budget, ...invalidRest} = resourceBudget;
Budget.assertNoExcessProperties(invalidRest, 'Resource Budget');
/** @type {Array<LH.Budget.ResourceType>} */
const validResourceTypes = [
'total',
'document',
'script',
'stylesheet',
'image',
'media',
'font',
'other',
'third-party',
];
// Assume resourceType is an allowed string, throw if not.
if (!validResourceTypes.includes(/** @type {LH.Budget.ResourceType} */ (resourceType))) {
throw new Error(`Invalid resource type: ${resourceType}. \n` +
`Valid resource types are: ${validResourceTypes.join(', ') }`);
}
if (!isNumber(budget)) {
throw new Error(`Invalid budget: ${budget}`);
}
return {
resourceType: /** @type {LH.Budget.ResourceType} */ (resourceType),
budget,
};
}
/**
* @param {unknown} path
* @param {string} error
*/
static throwInvalidPathError(path, error) {
throw new Error(`Invalid path ${path}. ${error}\n` +
`'Path' should be specified using the 'robots.txt' format.\n` +
`Learn more about the 'robots.txt' format here:\n` +
`https://developers.google.com/search/reference/robots_txt#url-matching-based-on-path-values`);
}
/**
* Validates that path is either: a) undefined or ) properly formed.
* Verifies the quantity and location of the two robot.txt regex characters: $, *
* @param {unknown} path
* @return {undefined|string}
*/
static validatePath(path) {
if (path === undefined) {
return undefined;
} else if (typeof path !== 'string') {
this.throwInvalidPathError(path, `Path should be a string.`);
return;
} else if (!path.startsWith('/')) {
this.throwInvalidPathError(path, `Path should start with '/'.`);
} else if ((path.match(/\*/g) || []).length > 1) {
this.throwInvalidPathError(path, `Path should only contain one '*'.`);
} else if ((path.match(/\$/g) || []).length > 1) {
this.throwInvalidPathError(path, `Path should only contain one '$' character.`);
} else if (path.includes('$') && !path.endsWith('$')) {
this.throwInvalidPathError(path, `'$' character should only occur at end of path.`);
}
return path;
}
/**
* Returns the budget that applies to a given URL.
* If multiple budgets match based on thier 'path' property,
* then the last-listed of those budgets is returned.
* @param {LH.Util.Immutable<Array<LH.Budget>>|null} budgets
* @param {string|undefined} url
* @return {LH.Util.Immutable<LH.Budget> | undefined} budget
*/
static getMatchingBudget(budgets, url) {
if (budgets === null || url === undefined) return;
// Applies the LAST matching budget.
for (let i = budgets.length - 1; i >= 0; i--) {
const budget = budgets[i];
if (this.urlMatchesPattern(url, budget.path)) {
return budget;
}
}
}
/**
* Determines whether a URL matches against a robots.txt-style "path".
* Pattern should use the robots.txt format. E.g. "/*-article.html" or "/". Reference:
* https://developers.google.com/search/reference/robots_txt#url-matching-based-on-path-values
* @param {string} url
* @param {string=} pattern
* @return {boolean}
*/
static urlMatchesPattern(url, pattern = '/') {
const urlObj = new URL(url);
const urlPath = urlObj.pathname + urlObj.search;
const hasWildcard = pattern.includes('*');
const hasDollarSign = pattern.includes('$');
/**
* There are 4 different cases of path strings.
* Paths should have already been validated with #validatePath.
*
* Case #1: No special characters
* Example: "/cat"
* Behavior: URL should start with given pattern.
*/
if (!hasWildcard && !hasDollarSign) {
return urlPath.startsWith(pattern);
/**
* Case #2: $ only
* Example: "/js$"
* Behavior: URL should be identical to pattern.
*/
} else if (!hasWildcard && hasDollarSign) {
return urlPath === pattern.slice(0, -1);
/**
* Case #3: * only
* Example: "/vendor*chunk"
* Behavior: URL should start with the string pattern that comes before the wildcard
* & later in the string contain the string pattern that comes after the wildcard.
*/
} else if (hasWildcard && !hasDollarSign) {
const [beforeWildcard, afterWildcard] = pattern.split('*');
const remainingUrl = urlPath.slice(beforeWildcard.length);
return urlPath.startsWith(beforeWildcard) && remainingUrl.includes(afterWildcard);
/**
* Case #4: $ and *
* Example: "/vendor*chunk.js$"
* Behavior: URL should start with the string pattern that comes before the wildcard
* & later in the string end with the string pattern that comes after the wildcard.
*/
} else if (hasWildcard && hasDollarSign) {
const [beforeWildcard, afterWildcard] = pattern.split('*');
const urlEnd = urlPath.slice(beforeWildcard.length);
return urlPath.startsWith(beforeWildcard) && urlEnd.endsWith(afterWildcard.slice(0, -1));
}
return false;
}
/**
* @param {Record<string, unknown>} timingBudget
* @return {LH.Budget.TimingBudget}
*/
static validateTimingBudget(timingBudget) {
const {metric, budget, ...invalidRest} = timingBudget;
Budget.assertNoExcessProperties(invalidRest, 'Timing Budget');
/** @type {Array<LH.Budget.TimingMetric>} */
const validTimingMetrics = [
'first-contentful-paint',
'interactive',
'first-meaningful-paint',
'max-potential-fid',
'total-blocking-time',
'speed-index',
'largest-contentful-paint',
'cumulative-layout-shift',
];
// Assume metric is an allowed string, throw if not.
if (!validTimingMetrics.includes(/** @type {LH.Budget.TimingMetric} */ (metric))) {
throw new Error(`Invalid timing metric: ${metric}. \n` +
`Valid timing metrics are: ${validTimingMetrics.join(', ')}`);
}
if (!isNumber(budget)) {
throw new Error(`Invalid budget: ${budget}`);
}
return {
metric: /** @type {LH.Budget.TimingMetric} */ (metric),
budget,
};
}
/**
* @param {string} hostname
* @return {string}
*/
static validateHostname(hostname) {
const errMsg = `${hostname} is not a valid hostname.`;
if (hostname.length === 0) {
throw new Error(errMsg);
}
if (hostname.includes('/')) {
throw new Error(errMsg);
}
if (hostname.includes(':')) {
throw new Error(errMsg);
}
if (hostname.includes('*')) {
if (!hostname.startsWith('*.') || hostname.lastIndexOf('*') > 0) {
throw new Error(errMsg);
}
}
return hostname;
}
/**
* @param {unknown} hostnames
* @return {undefined|Array<string>}
*/
static validateHostnames(hostnames) {
if (Array.isArray(hostnames) && hostnames.every(host => typeof host === 'string')) {
return hostnames.map(Budget.validateHostname);
} else if (hostnames !== undefined) {
throw new Error(`firstPartyHostnames should be defined as an array of strings.`);
}
}
/**
* More info on the Budget format:
* https://github.com/GoogleChrome/lighthouse/issues/6053#issuecomment-428385930
* @param {unknown} budgetJson
* @return {Array<LH.Budget>}
*/
static initializeBudget(budgetJson) {
// Clone to prevent modifications of original and to deactivate any live properties.
budgetJson = JSON.parse(JSON.stringify(budgetJson));
if (!isArrayOfUnknownObjects(budgetJson)) {
throw new Error('Budget file is not defined as an array of budgets.');
}
const budgets = budgetJson.map((b, index) => {
/** @type {LH.Budget} */
const budget = {};
const {path, options, resourceSizes, resourceCounts, timings, ...invalidRest} = b;
Budget.assertNoExcessProperties(invalidRest, 'Budget');
budget.path = Budget.validatePath(path);
if (isObjectOfUnknownProperties(options)) {
const {firstPartyHostnames, ...invalidRest} = options;
Budget.assertNoExcessProperties(invalidRest, 'Options property');
budget.options = {};
budget.options.firstPartyHostnames = Budget.validateHostnames(firstPartyHostnames);
} else if (options !== undefined) {
throw new Error(`Invalid options property in budget at index ${index}`);
}
if (isArrayOfUnknownObjects(resourceSizes)) {
budget.resourceSizes = resourceSizes.map(Budget.validateResourceBudget);
Budget.assertNoDuplicateStrings(budget.resourceSizes.map(r => r.resourceType),
`budgets[${index}].resourceSizes`);
} else if (resourceSizes !== undefined) {
throw new Error(`Invalid resourceSizes entry in budget at index ${index}`);
}
if (isArrayOfUnknownObjects(resourceCounts)) {
budget.resourceCounts = resourceCounts.map(Budget.validateResourceBudget);
Budget.assertNoDuplicateStrings(budget.resourceCounts.map(r => r.resourceType),
`budgets[${index}].resourceCounts`);
} else if (resourceCounts !== undefined) {
throw new Error(`Invalid resourceCounts entry in budget at index ${index}`);
}
if (isArrayOfUnknownObjects(timings)) {
budget.timings = timings.map(Budget.validateTimingBudget);
Budget.assertNoDuplicateStrings(budget.timings.map(r => r.metric),
`budgets[${index}].timings`);
} else if (timings !== undefined) {
throw new Error(`Invalid timings entry in budget at index ${index}`);
}
return budget;
});
return budgets;
}
}
export {Budget};

View File

@@ -0,0 +1,94 @@
export type GathererConstructor = typeof import('../gather/gatherers/gatherer.js').Gatherer;
export type Audit = typeof import('../audits/audit.js')['Audit'];
export type Gatherer = InstanceType<GathererConstructor>;
/**
* // TODO(bckenny): could adopt "jsonified" type to ensure T will survive JSON
* round trip: https://github.com/Microsoft/TypeScript/issues/21838
* @template T
* @param {T} json
* @return {T}
*/
export function deepClone<T>(json: T): T;
/**
* Deep clone a config, copying over any "live" gatherer or audit that
* wouldn't make the JSON round trip.
* @param {LH.Config} json
* @return {LH.Config}
*/
export function deepCloneConfigJson(json: LH.Config): LH.Config;
/**
* Until support of jsdoc templates with constraints, type in config.d.ts.
* See https://github.com/Microsoft/TypeScript/issues/24283
* @type {LH.Config.Merge}
*/
export const mergeConfigFragment: LH.Config.Merge;
/**
* Merge an array of items by a caller-defined key. `mergeConfigFragment` is used to merge any items
* with a matching key.
*
* @template {Record<string, any>} T
* @param {Array<T>|null|undefined} baseArray
* @param {Array<T>|null|undefined} extensionArray
* @param {(item: T) => string} keyFn
* @return {Array<T>}
*/
export function mergeConfigFragmentArrayByKey<T extends Record<string, any>>(baseArray: T[] | null | undefined, extensionArray: T[] | null | undefined, keyFn: (item: T) => string): T[];
/**
* If any items with identical `path` properties are found in the input array,
* merge their `options` properties into the first instance and then discard any
* other instances.
* @template {{path?: string, options: Record<string, unknown>}} T
* @param {T[]} items
* @return T[]
*/
export function mergeOptionsOfItems<T extends {
path?: string | undefined;
options: Record<string, unknown>;
}>(items: T[]): T[];
/**
* @param {LH.Config} config
* @param {string | undefined} configDir
* @param {{plugins?: string[]} | undefined} flags
* @return {Promise<LH.Config>}
*/
export function mergePlugins(config: LH.Config, configDir: string | undefined, flags: {
plugins?: string[];
} | undefined): Promise<LH.Config>;
/**
* Take an array of audits and audit paths and require any paths (possibly
* relative to the optional `configDir`) using `resolveModule`,
* leaving only an array of AuditDefns.
* @param {LH.Config['audits']} audits
* @param {string=} configDir
* @return {Promise<Array<LH.Config.AuditDefn>|null>}
*/
export function resolveAuditsToDefns(audits: LH.Config['audits'], configDir?: string | undefined): Promise<Array<LH.Config.AuditDefn> | null>;
/**
* Turns a GathererJson into a GathererDefn which involves a few main steps:
* - Expanding the JSON shorthand the full definition format.
* - `require`ing in the implementation.
* - Creating a gatherer instance from the implementation.
* @param {LH.Config.GathererJson} gathererJson
* @param {Array<string>} coreGathererList
* @param {string=} configDir
* @return {Promise<LH.Config.GathererDefn>}
*/
export function resolveGathererToDefn(gathererJson: LH.Config.GathererJson, coreGathererList: Array<string>, configDir?: string | undefined): Promise<LH.Config.GathererDefn>;
/**
* Resolves the location of the specified module and returns an absolute
* string path to the file. Used for loading custom audits and gatherers.
* Throws an error if no module is found.
* @param {string} moduleIdentifier
* @param {string=} configDir The absolute path to the directory of the config file, if there is one.
* @param {string=} category Optional plugin category (e.g. 'audit') for better error messages.
* @return {string}
* @throws {Error}
*/
export function resolveModulePath(moduleIdentifier: string, configDir?: string | undefined, category?: string | undefined): string;
/**
* @param {LH.SharedFlagsSettings} settingsJson
* @param {LH.Flags|undefined} overrides
* @return {LH.Config.Settings}
*/
export function resolveSettings(settingsJson?: LH.SharedFlagsSettings, overrides?: LH.Flags | undefined): LH.Config.Settings;
//# sourceMappingURL=config-helpers.d.ts.map

630
node_modules/lighthouse/core/config/config-helpers.js generated vendored Normal file
View File

@@ -0,0 +1,630 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import path from 'path';
import {createRequire} from 'module';
import url from 'url';
import isDeepEqual from 'lodash/isEqual.js';
import * as constants from './constants.js';
import {Budget} from './budget.js';
import ConfigPlugin from './config-plugin.js';
import {Runner} from '../runner.js';
import * as i18n from '../lib/i18n/i18n.js';
import * as validation from './validation.js';
import {getModuleDirectory} from '../../esm-utils.js';
const require = createRequire(import.meta.url);
/** @typedef {typeof import('../gather/gatherers/gatherer.js').Gatherer} GathererConstructor */
/** @typedef {typeof import('../audits/audit.js')['Audit']} Audit */
/** @typedef {InstanceType<GathererConstructor>} Gatherer */
function isBundledEnvironment() {
// If we're in DevTools or LightRider, we are definitely bundled.
// TODO: refactor and delete `global.isDevtools`.
if (global.isDevtools || global.isLightrider) return true;
try {
// Not foolproof, but `lighthouse-logger` is a dependency of lighthouse that should always be resolvable.
// `require.resolve` will only throw in atypical/bundled environments.
require.resolve('lighthouse-logger');
return false;
} catch (err) {
return true;
}
}
/**
* If any items with identical `path` properties are found in the input array,
* merge their `options` properties into the first instance and then discard any
* other instances.
* @template {{path?: string, options: Record<string, unknown>}} T
* @param {T[]} items
* @return T[]
*/
const mergeOptionsOfItems = function(items) {
/** @type {T[]} */
const mergedItems = [];
for (const item of items) {
const existingItem = item.path && mergedItems.find(candidate => candidate.path === item.path);
if (!existingItem) {
mergedItems.push(item);
continue;
}
existingItem.options = Object.assign({}, existingItem.options, item.options);
}
return mergedItems;
};
/**
* Recursively merges config fragment objects in a somewhat Lighthouse-specific way.
*
* - `null` is treated similarly to `undefined` for whether a value should be overridden.
* - `overwriteArrays` controls array extension behavior:
* - true: Arrays are overwritten without any merging or concatenation.
* - false: Arrays are concatenated and de-duped by isDeepEqual.
* - Objects are recursively merged.
* - If the `settings` key is encountered while traversing an object, its arrays are *always*
* overridden, not concatenated. (`overwriteArrays` is flipped to `true`)
*
* More widely typed than exposed merge() function, below.
* @param {Object<string, any>|Array<any>|undefined|null} base
* @param {Object<string, any>|Array<any>} extension
* @param {boolean=} overwriteArrays
*/
function _mergeConfigFragment(base, extension, overwriteArrays = false) {
// If the default value doesn't exist or is explicitly null, defer to the extending value
if (typeof base === 'undefined' || base === null) {
return extension;
} else if (typeof extension === 'undefined') {
return base;
} else if (Array.isArray(extension)) {
if (overwriteArrays) return extension;
if (!Array.isArray(base)) throw new TypeError(`Expected array but got ${typeof base}`);
const merged = base.slice();
extension.forEach(item => {
if (!merged.some(candidate => isDeepEqual(candidate, item))) merged.push(item);
});
return merged;
} else if (typeof extension === 'object') {
if (typeof base !== 'object') throw new TypeError(`Expected object but got ${typeof base}`);
if (Array.isArray(base)) throw new TypeError('Expected object but got Array');
Object.keys(extension).forEach(key => {
const localOverwriteArrays = overwriteArrays ||
(key === 'settings' && typeof base[key] === 'object');
base[key] = _mergeConfigFragment(base[key], extension[key], localOverwriteArrays);
});
return base;
}
return extension;
}
/**
* Until support of jsdoc templates with constraints, type in config.d.ts.
* See https://github.com/Microsoft/TypeScript/issues/24283
* @type {LH.Config.Merge}
*/
const mergeConfigFragment = _mergeConfigFragment;
/**
* Merge an array of items by a caller-defined key. `mergeConfigFragment` is used to merge any items
* with a matching key.
*
* @template {Record<string, any>} T
* @param {Array<T>|null|undefined} baseArray
* @param {Array<T>|null|undefined} extensionArray
* @param {(item: T) => string} keyFn
* @return {Array<T>}
*/
function mergeConfigFragmentArrayByKey(baseArray, extensionArray, keyFn) {
/** @type {Map<string, {index: number, item: T}>} */
const itemsByKey = new Map();
const mergedArray = baseArray || [];
for (let i = 0; i < mergedArray.length; i++) {
const item = mergedArray[i];
itemsByKey.set(keyFn(item), {index: i, item});
}
for (const item of extensionArray || []) {
const baseItemEntry = itemsByKey.get(keyFn(item));
if (baseItemEntry) {
const baseItem = baseItemEntry.item;
const merged = typeof item === 'object' && typeof baseItem === 'object' ?
mergeConfigFragment(baseItem, item, true) :
item;
mergedArray[baseItemEntry.index] = merged;
} else {
mergedArray.push(item);
}
}
return mergedArray;
}
/**
* Expands a gatherer from user-specified to an internal gatherer definition format.
*
* Input Examples:
* - 'my-gatherer'
* - class MyGatherer extends Gatherer { }
* - {instance: myGathererInstance}
*
* @param {LH.Config.GathererJson} gatherer
* @return {{instance?: Gatherer, implementation?: GathererConstructor, path?: string}} passes
*/
function expandGathererShorthand(gatherer) {
if (typeof gatherer === 'string') {
// just 'path/to/gatherer'
return {path: gatherer};
} else if ('implementation' in gatherer || 'instance' in gatherer) {
// {implementation: GathererConstructor, ...} or {instance: GathererInstance, ...}
return gatherer;
} else if ('path' in gatherer) {
// {path: 'path/to/gatherer', ...}
if (typeof gatherer.path !== 'string') {
throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
}
return gatherer;
} else if (typeof gatherer === 'function') {
// just GathererConstructor
return {implementation: gatherer};
} else if (gatherer && typeof gatherer.beforePass === 'function') {
// just GathererInstance
return {instance: gatherer};
} else {
throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
}
}
/**
* Expands the audits from user-specified JSON to an internal audit definition format.
* @param {LH.Config.AuditJson} audit
* @return {{id?: string, path: string, options?: {}} | {id?: string, implementation: Audit, path?: string, options?: {}}}
*/
function expandAuditShorthand(audit) {
if (typeof audit === 'string') {
// just 'path/to/audit'
return {path: audit, options: {}};
} else if ('implementation' in audit && typeof audit.implementation.audit === 'function') {
// {implementation: AuditClass, ...}
return audit;
} else if ('path' in audit && typeof audit.path === 'string') {
// {path: 'path/to/audit', ...}
return audit;
} else if ('audit' in audit && typeof audit.audit === 'function') {
// just AuditClass
return {implementation: audit, options: {}};
} else {
throw new Error('Invalid Audit type ' + JSON.stringify(audit));
}
}
/** @type {Map<string, Promise<any>>} */
const bundledModules = new Map(/* BUILD_REPLACE_BUNDLED_MODULES */);
/**
* Wraps `import`/`require` with an entrypoint for bundled dynamic modules.
* See build-bundle.js
* @param {string} requirePath
*/
async function requireWrapper(requirePath) {
// For windows.
if (path.isAbsolute(requirePath)) {
requirePath = url.pathToFileURL(requirePath).href;
}
/** @type {any} */
let module;
if (bundledModules.has(requirePath)) {
module = await bundledModules.get(requirePath);
} else if (requirePath.match(/\.(js|mjs|cjs)$/)) {
module = await import(requirePath);
} else {
requirePath += '.js';
module = await import(requirePath);
}
if (module.default) return module.default;
// Find a valid named export.
const methods = new Set(['meta']);
const possibleNamedExports = Object.keys(module).filter(key => {
if (!(module[key] && module[key] instanceof Object)) return false;
return Object.getOwnPropertyNames(module[key]).some(method => methods.has(method));
});
if (possibleNamedExports.length === 1) return possibleNamedExports[0];
if (possibleNamedExports.length > 1) {
throw new Error(`module '${requirePath}' has too many possible exports`);
}
throw new Error(`module '${requirePath}' missing default export`);
}
/**
* @param {string} gathererPath
* @param {Array<string>} coreGathererList
* @param {string=} configDir
* @return {Promise<LH.Config.GathererDefn>}
*/
async function requireGatherer(gathererPath, coreGathererList, configDir) {
const coreGatherer = coreGathererList.find(a => a === `${gathererPath}.js`);
let requirePath = `../gather/gatherers/${gathererPath}`;
if (!coreGatherer) {
// Otherwise, attempt to find it elsewhere. This throws if not found.
requirePath = resolveModulePath(gathererPath, configDir, 'gatherer');
}
const GathererClass = /** @type {GathererConstructor} */ (await requireWrapper(requirePath));
return {
instance: new GathererClass(),
implementation: GathererClass,
path: gathererPath,
};
}
/**
* @param {string} auditPath
* @param {Array<string>} coreAuditList
* @param {string=} configDir
* @return {Promise<LH.Config.AuditDefn['implementation']>}
*/
function requireAudit(auditPath, coreAuditList, configDir) {
// See if the audit is a Lighthouse core audit.
const auditPathJs = `${auditPath}.js`;
const coreAudit = coreAuditList.find(a => a === auditPathJs);
let requirePath = `../audits/${auditPath}`;
if (!coreAudit) {
if (isBundledEnvironment()) {
// This is for pubads bundling.
requirePath = auditPath;
} else {
// Otherwise, attempt to find it elsewhere. This throws if not found.
const absolutePath = resolveModulePath(auditPath, configDir, 'audit');
if (isBundledEnvironment()) {
// Use a relative path so bundler can easily expose it.
requirePath = path.relative(getModuleDirectory(import.meta), absolutePath);
} else {
requirePath = absolutePath;
}
}
}
return requireWrapper(requirePath);
}
/**
* Creates a settings object from potential flags object by dropping all the properties
* that don't exist on Config.Settings.
* @param {Partial<LH.Flags>=} flags
* @return {LH.Util.RecursivePartial<LH.Config.Settings>}
*/
function cleanFlagsForSettings(flags = {}) {
/** @type {LH.Util.RecursivePartial<LH.Config.Settings>} */
const settings = {};
for (const key of Object.keys(flags)) {
if (key in constants.defaultSettings) {
// @ts-expect-error tsc can't yet express that key is only a single type in each iteration, not a union of types.
settings[key] = flags[key];
}
}
return settings;
}
/**
* @param {LH.SharedFlagsSettings} settingsJson
* @param {LH.Flags|undefined} overrides
* @return {LH.Config.Settings}
*/
function resolveSettings(settingsJson = {}, overrides = undefined) {
// If a locale is requested in flags or settings, use it. A typical CLI run will not have one,
// however `lookupLocale` will always determine which of our supported locales to use (falling
// back if necessary).
// TODO: could do more work to sniff out the user's locale
const locale = i18n.lookupLocale(overrides?.locale || settingsJson.locale);
// Fill in missing settings with defaults
const {defaultSettings} = constants;
const settingWithDefaults = mergeConfigFragment(deepClone(defaultSettings), settingsJson, true);
// Override any applicable settings with CLI flags
const settingsWithFlags = mergeConfigFragment(
settingWithDefaults,
cleanFlagsForSettings(overrides),
true
);
if (settingsWithFlags.budgets) {
settingsWithFlags.budgets = Budget.initializeBudget(settingsWithFlags.budgets);
}
// Locale is special and comes only from flags/settings/lookupLocale.
settingsWithFlags.locale = locale;
// Default constants uses the mobile UA. Explicitly stating to true asks LH to use the associated UA.
// It's a little awkward, but the alternatives are not allowing `true` or a dedicated `disableUAEmulation` setting.
if (settingsWithFlags.emulatedUserAgent === true) {
settingsWithFlags.emulatedUserAgent = constants.userAgents[settingsWithFlags.formFactor];
}
validation.assertValidSettings(settingsWithFlags);
return settingsWithFlags;
}
/**
* @param {LH.Config} config
* @param {string | undefined} configDir
* @param {{plugins?: string[]} | undefined} flags
* @return {Promise<LH.Config>}
*/
async function mergePlugins(config, configDir, flags) {
const configPlugins = config.plugins || [];
const flagPlugins = flags?.plugins || [];
const pluginNames = new Set([...configPlugins, ...flagPlugins]);
for (const pluginName of pluginNames) {
validation.assertValidPluginName(config, pluginName);
// In bundled contexts, `resolveModulePath` will fail, so use the raw pluginName directly.
const pluginPath = isBundledEnvironment() ?
pluginName :
resolveModulePath(pluginName, configDir, 'plugin');
const rawPluginJson = await requireWrapper(pluginPath);
const pluginJson = ConfigPlugin.parsePlugin(rawPluginJson, pluginName);
config = mergeConfigFragment(config, pluginJson);
}
return config;
}
/**
* Turns a GathererJson into a GathererDefn which involves a few main steps:
* - Expanding the JSON shorthand the full definition format.
* - `require`ing in the implementation.
* - Creating a gatherer instance from the implementation.
* @param {LH.Config.GathererJson} gathererJson
* @param {Array<string>} coreGathererList
* @param {string=} configDir
* @return {Promise<LH.Config.GathererDefn>}
*/
async function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
const gathererDefn = expandGathererShorthand(gathererJson);
if (gathererDefn.instance) {
return {
instance: gathererDefn.instance,
implementation: gathererDefn.implementation,
path: gathererDefn.path,
};
} else if (gathererDefn.implementation) {
const GathererClass = gathererDefn.implementation;
return {
instance: new GathererClass(),
implementation: gathererDefn.implementation,
path: gathererDefn.path,
};
} else if (gathererDefn.path) {
const path = gathererDefn.path;
return requireGatherer(path, coreGathererList, configDir);
} else {
throw new Error('Invalid expanded Gatherer: ' + JSON.stringify(gathererDefn));
}
}
/**
* Take an array of audits and audit paths and require any paths (possibly
* relative to the optional `configDir`) using `resolveModule`,
* leaving only an array of AuditDefns.
* @param {LH.Config['audits']} audits
* @param {string=} configDir
* @return {Promise<Array<LH.Config.AuditDefn>|null>}
*/
async function resolveAuditsToDefns(audits, configDir) {
if (!audits) {
return null;
}
const coreList = Runner.getAuditList();
const auditDefnsPromises = audits.map(async (auditJson) => {
const auditDefn = expandAuditShorthand(auditJson);
let implementation;
if ('implementation' in auditDefn) {
implementation = auditDefn.implementation;
} else {
implementation = await requireAudit(auditDefn.path, coreList, configDir);
}
return {
implementation,
path: auditDefn.path,
options: auditDefn.options || {},
};
});
const auditDefns = await Promise.all(auditDefnsPromises);
const mergedAuditDefns = mergeOptionsOfItems(auditDefns);
mergedAuditDefns.forEach(audit => validation.assertValidAudit(audit));
return mergedAuditDefns;
}
/**
* Resolves the location of the specified module and returns an absolute
* string path to the file. Used for loading custom audits and gatherers.
* Throws an error if no module is found.
* @param {string} moduleIdentifier
* @param {string=} configDir The absolute path to the directory of the config file, if there is one.
* @param {string=} category Optional plugin category (e.g. 'audit') for better error messages.
* @return {string}
* @throws {Error}
*/
function resolveModulePath(moduleIdentifier, configDir, category) {
// module in a node_modules/ that is...
// | | Lighthouse globally installed | Lighthouse locally installed |
// |--------------------------------|-------------------------------|------------------------------|
// | global | 1. | 1. |
// | in current working directory | 2. | 1. |
// | relative to config.js file | 5. | - |
// module given by a path that is...
// | | Lighthouse globally/locally installed |
// |-------------------------------------------|---------------------------------------|
// | absolute | 1. |
// | relative to the current working directory | 3. |
// | relative to the config.js file | 4. |
// 1.
// First try straight `require()`. Unlikely to be specified relative to this
// file, but adds support for Lighthouse modules from npm since
// `require()` walks up parent directories looking inside any node_modules/
// present. Also handles absolute paths.
try {
return require.resolve(moduleIdentifier);
} catch (e) {}
// 2.
// Lighthouse globally installed, node_modules/ in current working directory.
// ex: lighthouse https://test.com
//
// working directory/
// |-- node_modules/
// |-- package.json
try {
return require.resolve(moduleIdentifier, {paths: [process.cwd()]});
} catch (e) {}
// 3.
// See if the module resolves relative to the current working directory.
// Most useful to handle the case of invoking Lighthouse as a module, since
// then the config is an object and so has no path.
const cwdPath = path.resolve(process.cwd(), moduleIdentifier);
try {
return require.resolve(cwdPath);
} catch (e) {}
const errorString = 'Unable to locate ' + (category ? `${category}: ` : '') +
`\`${moduleIdentifier}\`.
Tried to resolve the module from these locations:
${getModuleDirectory(import.meta)}
${cwdPath}`;
if (!configDir) {
throw new Error(errorString);
}
// 4.
// Try looking up relative to the config file path. Just like the
// relative path passed to `require()` is found relative to the file it's
// in, this allows module paths to be specified relative to the config file.
const relativePath = path.resolve(configDir, moduleIdentifier);
try {
return require.resolve(relativePath);
} catch (requireError) {}
// 5.
// Lighthouse globally installed, node_modules/ in config directory.
// ex: lighthouse https://test.com --config-path=./config/config.js
//
// working directory/
// |-- config/
// |-- node_modules/
// |-- config.js
// |-- package.json
try {
return require.resolve(moduleIdentifier, {paths: [configDir]});
} catch (requireError) {}
throw new Error(errorString + `
${relativePath}`);
}
/**
* Many objects in the config can be an object whose properties are not serializable.
* We use a shallow clone for these objects instead.
* Any value that isn't an object will not be cloned.
*
* @template T
* @param {T} item
* @return {T}
*/
function shallowClone(item) {
if (typeof item === 'object') {
// Return copy of instance and prototype chain (in case item is instantiated class).
return Object.assign(
Object.create(
Object.getPrototypeOf(item)
),
item
);
}
return item;
}
/**
* // TODO(bckenny): could adopt "jsonified" type to ensure T will survive JSON
* round trip: https://github.com/Microsoft/TypeScript/issues/21838
* @template T
* @param {T} json
* @return {T}
*/
function deepClone(json) {
return JSON.parse(JSON.stringify(json));
}
/**
* Deep clone a config, copying over any "live" gatherer or audit that
* wouldn't make the JSON round trip.
* @param {LH.Config} json
* @return {LH.Config}
*/
function deepCloneConfigJson(json) {
const cloned = deepClone(json);
// Copy arrays that could contain non-serializable properties to allow for programmatic
// injection of audit and gatherer implementations.
if (Array.isArray(cloned.passes) && Array.isArray(json.passes)) {
for (let i = 0; i < cloned.passes.length; i++) {
const pass = cloned.passes[i];
pass.gatherers = (json.passes[i].gatherers || []).map(gatherer => shallowClone(gatherer));
}
}
if (Array.isArray(json.audits)) {
cloned.audits = json.audits.map(audit => shallowClone(audit));
}
if (Array.isArray(json.artifacts)) {
cloned.artifacts = json.artifacts.map(artifact => ({
...artifact,
gatherer: shallowClone(artifact.gatherer),
}));
}
return cloned;
}
export {
deepClone,
deepCloneConfigJson,
mergeConfigFragment,
mergeConfigFragmentArrayByKey,
mergeOptionsOfItems,
mergePlugins,
resolveAuditsToDefns,
resolveGathererToDefn,
resolveModulePath,
resolveSettings,
};

46
node_modules/lighthouse/core/config/config-plugin.d.ts generated vendored Normal file
View File

@@ -0,0 +1,46 @@
export default ConfigPlugin;
/**
* A set of methods for extracting and validating a Lighthouse plugin config.
*/
declare class ConfigPlugin {
/**
* Extract and validate the list of AuditDefns added by the plugin (or undefined
* if no additional audits are being added by the plugin).
* @param {unknown} auditsJson
* @param {string} pluginName
* @return {Array<{path: string}>|undefined}
*/
static _parseAuditsList(auditsJson: unknown, pluginName: string): Array<{
path: string;
}> | undefined;
/**
* Extract and validate the list of category AuditRefs added by the plugin.
* @param {unknown} auditRefsJson
* @param {string} pluginName
* @return {Array<LH.Config.AuditRefJson>}
*/
static _parseAuditRefsList(auditRefsJson: unknown, pluginName: string): Array<LH.Config.AuditRefJson>;
/**
* Extract and validate the category added by the plugin.
* @param {unknown} categoryJson
* @param {string} pluginName
* @return {LH.Config.CategoryJson}
*/
static _parseCategory(categoryJson: unknown, pluginName: string): LH.Config.CategoryJson;
/**
* Extract and validate groups JSON added by the plugin.
* @param {unknown} groupsJson
* @param {string} pluginName
* @return {Record<string, LH.Config.GroupJson>|undefined}
*/
static _parseGroups(groupsJson: unknown, pluginName: string): Record<string, LH.Config.GroupJson> | undefined;
/**
* Extracts and validates a config from the provided plugin input, throwing
* if it deviates from the expected object shape.
* @param {unknown} pluginJson
* @param {string} pluginName
* @return {LH.Config}
*/
static parsePlugin(pluginJson: unknown, pluginName: string): LH.Config;
}
//# sourceMappingURL=config-plugin.d.ts.map

250
node_modules/lighthouse/core/config/config-plugin.js generated vendored Normal file
View File

@@ -0,0 +1,250 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import * as i18n from '../lib/i18n/i18n.js';
/**
* @param {unknown} arr
* @return {arr is Array<Record<string, unknown>>}
*/
function isArrayOfUnknownObjects(arr) {
return Array.isArray(arr) && arr.every(isObjectOfUnknownProperties);
}
/**
* @param {unknown} val
* @return {val is Record<string, unknown>}
*/
function isObjectOfUnknownProperties(val) {
return typeof val === 'object' && val !== null && !Array.isArray(val);
}
/**
* @param {unknown} str
* @return {str is LH.Gatherer.GatherMode}
*/
function objectIsGatherMode(str) {
if (typeof str !== 'string') return false;
return str === 'navigation' || str === 'timespan' || str === 'snapshot';
}
/**
* @param {unknown} arr
* @return {arr is Array<LH.Gatherer.GatherMode>}
*/
function isArrayOfGatherModes(arr) {
if (!Array.isArray(arr)) return false;
return arr.every(objectIsGatherMode);
}
/**
* Asserts that obj has no own properties, throwing a nice error message if it does.
* Plugin and object name are included for nicer logging.
* @param {Record<string, unknown>} obj
* @param {string} pluginName
* @param {string=} objectName
*/
function assertNoExcessProperties(obj, pluginName, objectName = '') {
if (objectName) {
objectName += ' ';
}
const invalidKeys = Object.keys(obj);
if (invalidKeys.length > 0) {
const keys = invalidKeys.join(', ');
throw new Error(`${pluginName} has unrecognized ${objectName}properties: [${keys}]`);
}
}
/**
* A set of methods for extracting and validating a Lighthouse plugin config.
*/
class ConfigPlugin {
/**
* Extract and validate the list of AuditDefns added by the plugin (or undefined
* if no additional audits are being added by the plugin).
* @param {unknown} auditsJson
* @param {string} pluginName
* @return {Array<{path: string}>|undefined}
*/
static _parseAuditsList(auditsJson, pluginName) {
// Plugin audits aren't required (relying on LH default audits) so fall back to [].
if (auditsJson === undefined) {
return undefined;
} else if (!isArrayOfUnknownObjects(auditsJson)) {
throw new Error(`${pluginName} has an invalid audits array.`);
}
return auditsJson.map(auditDefnJson => {
const {path, ...invalidRest} = auditDefnJson;
assertNoExcessProperties(invalidRest, pluginName, 'audit');
if (typeof path !== 'string') {
throw new Error(`${pluginName} has a missing audit path.`);
}
return {
path,
};
});
}
/**
* Extract and validate the list of category AuditRefs added by the plugin.
* @param {unknown} auditRefsJson
* @param {string} pluginName
* @return {Array<LH.Config.AuditRefJson>}
*/
static _parseAuditRefsList(auditRefsJson, pluginName) {
if (!isArrayOfUnknownObjects(auditRefsJson)) {
throw new Error(`${pluginName} has no valid auditsRefs.`);
}
return auditRefsJson.map(auditRefJson => {
const {id, weight, group, ...invalidRest} = auditRefJson;
assertNoExcessProperties(invalidRest, pluginName, 'auditRef');
if (typeof id !== 'string') {
throw new Error(`${pluginName} has an invalid auditRef id.`);
}
if (typeof weight !== 'number') {
throw new Error(`${pluginName} has an invalid auditRef weight.`);
}
if (typeof group !== 'string' && typeof group !== 'undefined') {
throw new Error(`${pluginName} has an invalid auditRef group.`);
}
const prependedGroup = group ? `${pluginName}-${group}` : group;
return {
id,
weight,
group: prependedGroup,
};
});
}
/**
* Extract and validate the category added by the plugin.
* @param {unknown} categoryJson
* @param {string} pluginName
* @return {LH.Config.CategoryJson}
*/
static _parseCategory(categoryJson, pluginName) {
if (!isObjectOfUnknownProperties(categoryJson)) {
throw new Error(`${pluginName} has no valid category.`);
}
const {
title,
description,
manualDescription,
auditRefs: auditRefsJson,
supportedModes,
...invalidRest
} = categoryJson;
assertNoExcessProperties(invalidRest, pluginName, 'category');
if (!i18n.isStringOrIcuMessage(title)) {
throw new Error(`${pluginName} has an invalid category tile.`);
}
if (!i18n.isStringOrIcuMessage(description) && description !== undefined) {
throw new Error(`${pluginName} has an invalid category description.`);
}
if (!i18n.isStringOrIcuMessage(manualDescription) && manualDescription !== undefined) {
throw new Error(`${pluginName} has an invalid category manualDescription.`);
}
if (!isArrayOfGatherModes(supportedModes) && supportedModes !== undefined) {
throw new Error(
`${pluginName} supportedModes must be an array, ` +
`valid array values are "navigation", "timespan", and "snapshot".`
);
}
const auditRefs = ConfigPlugin._parseAuditRefsList(auditRefsJson, pluginName);
return {
title,
auditRefs,
description: description,
manualDescription: manualDescription,
supportedModes,
};
}
/**
* Extract and validate groups JSON added by the plugin.
* @param {unknown} groupsJson
* @param {string} pluginName
* @return {Record<string, LH.Config.GroupJson>|undefined}
*/
static _parseGroups(groupsJson, pluginName) {
if (groupsJson === undefined) {
return undefined;
}
if (!isObjectOfUnknownProperties(groupsJson)) {
throw new Error(`${pluginName} groups json is not defined as an object.`);
}
const groups = Object.entries(groupsJson);
/** @type {Record<string, LH.Config.GroupJson>} */
const parsedGroupsJson = {};
groups.forEach(([groupId, groupJson]) => {
if (!isObjectOfUnknownProperties(groupJson)) {
throw new Error(`${pluginName} has a group not defined as an object.`);
}
const {title, description, ...invalidRest} = groupJson;
assertNoExcessProperties(invalidRest, pluginName, 'group');
if (!i18n.isStringOrIcuMessage(title)) {
throw new Error(`${pluginName} has an invalid group title.`);
}
if (!i18n.isStringOrIcuMessage(description) && description !== undefined) {
throw new Error(`${pluginName} has an invalid group description.`);
}
parsedGroupsJson[`${pluginName}-${groupId}`] = {
title,
description,
};
});
return parsedGroupsJson;
}
/**
* Extracts and validates a config from the provided plugin input, throwing
* if it deviates from the expected object shape.
* @param {unknown} pluginJson
* @param {string} pluginName
* @return {LH.Config}
*/
static parsePlugin(pluginJson, pluginName) {
// Clone to prevent modifications of original and to deactivate any live properties.
pluginJson = JSON.parse(JSON.stringify(pluginJson));
if (!isObjectOfUnknownProperties(pluginJson)) {
throw new Error(`${pluginName} is not defined as an object.`);
}
const {
audits: pluginAuditsJson,
category: pluginCategoryJson,
groups: pluginGroupsJson,
...invalidRest
} = pluginJson;
assertNoExcessProperties(invalidRest, pluginName);
return {
audits: ConfigPlugin._parseAuditsList(pluginAuditsJson, pluginName),
categories: {
[pluginName]: ConfigPlugin._parseCategory(pluginCategoryJson, pluginName),
},
groups: ConfigPlugin._parseGroups(pluginGroupsJson, pluginName),
};
}
}
export default ConfigPlugin;

28
node_modules/lighthouse/core/config/config.d.ts generated vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* @param {LH.Config|undefined} config
* @param {{configPath?: string}} context
* @return {{configWorkingCopy: LH.Config, configDir?: string, configPath?: string}}
*/
export function resolveWorkingCopy(config: LH.Config | undefined, context: {
configPath?: string | undefined;
}): {
configWorkingCopy: LH.Config;
configDir?: string | undefined;
configPath?: string | undefined;
};
/**
* @param {LH.Gatherer.GatherMode} gatherMode
* @param {LH.Config=} config
* @param {LH.Flags=} flags
* @return {Promise<{resolvedConfig: LH.Config.ResolvedConfig, warnings: string[]}>}
*/
export function initializeConfig(gatherMode: LH.Gatherer.GatherMode, config?: LH.Config | undefined, flags?: LH.Flags | undefined): Promise<{
resolvedConfig: LH.Config.ResolvedConfig;
warnings: string[];
}>;
/**
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @return {string}
*/
export function getConfigDisplayString(resolvedConfig: LH.Config.ResolvedConfig): string;
//# sourceMappingURL=config.d.ts.map

344
node_modules/lighthouse/core/config/config.js generated vendored Normal file
View File

@@ -0,0 +1,344 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import path from 'path';
import log from 'lighthouse-logger';
import {Runner} from '../runner.js';
import defaultConfig from './default-config.js';
import {defaultNavigationConfig, nonSimulatedPassConfigOverrides} from './constants.js'; // eslint-disable-line max-len
import {
isFRGathererDefn,
throwInvalidDependencyOrder,
isValidArtifactDependency,
throwInvalidArtifactDependency,
assertArtifactTopologicalOrder,
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 '../../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.AnyFRGathererDefn} 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) {
/** @type {LH.Config.GathererJson} */
// @ts-expect-error - remove when legacy runner path is removed.
const gathererJson = artifactJson.gatherer;
const gatherer = await resolveGathererToDefn(gathererJson, coreGathererList, configDir);
if (!isFRGathererDefn(gatherer)) {
throw new Error(`${gatherer.instance.name} gatherer does not have a Fraggle Rock meta obj`);
}
/** @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.NavigationDefn} navigation
* @param {LH.Config.Settings} settings
*/
function overrideNavigationThrottlingWindows(navigation, settings) {
if (navigation.disableThrottling) return;
if (settings.throttlingMethod === 'simulate') return;
navigation.cpuQuietThresholdMs = Math.max(
navigation.cpuQuietThresholdMs || 0,
nonSimulatedPassConfigOverrides.cpuQuietThresholdMs
);
navigation.networkQuietThresholdMs = Math.max(
navigation.networkQuietThresholdMs || 0,
nonSimulatedPassConfigOverrides.networkQuietThresholdMs
);
navigation.pauseAfterFcpMs = Math.max(
navigation.pauseAfterFcpMs || 0,
nonSimulatedPassConfigOverrides.pauseAfterFcpMs
);
navigation.pauseAfterLoadMs = Math.max(
navigation.pauseAfterLoadMs || 0,
nonSimulatedPassConfigOverrides.pauseAfterLoadMs
);
}
/**
* @param {LH.Config.AnyArtifactDefn[]|null|undefined} artifactDefns
* @param {LH.Config.Settings} settings
* @return {LH.Config.NavigationDefn[] | null}
*/
function resolveFakeNavigations(artifactDefns, settings) {
if (!artifactDefns) return null;
const status = {msg: 'Resolve navigation definitions', id: 'lh:config:resolveNavigationsToDefns'};
log.time(status, 'verbose');
const resolvedNavigation = {
...defaultNavigationConfig,
artifacts: artifactDefns,
pauseAfterFcpMs: settings.pauseAfterFcpMs,
pauseAfterLoadMs: settings.pauseAfterLoadMs,
networkQuietThresholdMs: settings.networkQuietThresholdMs,
cpuQuietThresholdMs: settings.cpuQuietThresholdMs,
blankPage: settings.blankPage,
};
overrideNavigationThrottlingWindows(resolvedNavigation, settings);
const navigations = [resolvedNavigation];
assertArtifactTopologicalOrder(navigations);
log.timeEnd(status);
return navigations;
}
/**
* @param {LH.Gatherer.GatherMode} gatherMode
* @param {LH.Config=} config
* @param {LH.Flags=} flags
* @return {Promise<{resolvedConfig: LH.Config.ResolvedConfig, warnings: string[]}>}
*/
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);
const artifacts = await resolveArtifactsToDefns(configWorkingCopy.artifacts, configDir);
const navigations = resolveFakeNavigations(artifacts, settings);
/** @type {LH.Config.ResolvedConfig} */
let resolvedConfig = {
artifacts,
navigations,
audits: await resolveAuditsToDefns(configWorkingCopy.audits, configDir),
categories: configWorkingCopy.categories || null,
groups: configWorkingCopy.groups || null,
settings,
};
const {warnings} = assertValidConfig(resolvedConfig);
resolvedConfig = filterConfigByGatherMode(resolvedConfig, gatherMode);
resolvedConfig = filterConfigByExplicitFilters(resolvedConfig, settings);
log.timeEnd(status);
return {resolvedConfig, warnings};
}
/**
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @return {string}
*/
function getConfigDisplayString(resolvedConfig) {
/** @type {LH.Config.ResolvedConfig} */
const resolvedConfigCopy = JSON.parse(JSON.stringify(resolvedConfig));
if (resolvedConfigCopy.navigations) {
for (const navigation of resolvedConfigCopy.navigations) {
for (let i = 0; i < navigation.artifacts.length; ++i) {
// @ts-expect-error Breaking the Config.AnyArtifactDefn type.
navigation.artifacts[i] = navigation.artifacts[i].id;
}
}
}
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,
};

86
node_modules/lighthouse/core/config/constants.d.ts generated vendored Normal file
View File

@@ -0,0 +1,86 @@
export namespace throttling {
export { DEVTOOLS_RTT_ADJUSTMENT_FACTOR };
export { DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR };
export namespace mobileSlow4G {
const rttMs: number;
const throughputKbps: number;
const requestLatencyMs: number;
const downloadThroughputKbps: number;
const uploadThroughputKbps: number;
const cpuSlowdownMultiplier: number;
}
export namespace mobileRegular3G {
const rttMs_1: number;
export { rttMs_1 as rttMs };
const throughputKbps_1: number;
export { throughputKbps_1 as throughputKbps };
const requestLatencyMs_1: number;
export { requestLatencyMs_1 as requestLatencyMs };
const downloadThroughputKbps_1: number;
export { downloadThroughputKbps_1 as downloadThroughputKbps };
const uploadThroughputKbps_1: number;
export { uploadThroughputKbps_1 as uploadThroughputKbps };
const cpuSlowdownMultiplier_1: number;
export { cpuSlowdownMultiplier_1 as cpuSlowdownMultiplier };
}
export namespace desktopDense4G {
const rttMs_2: number;
export { rttMs_2 as rttMs };
const throughputKbps_2: number;
export { throughputKbps_2 as throughputKbps };
const cpuSlowdownMultiplier_2: number;
export { cpuSlowdownMultiplier_2 as cpuSlowdownMultiplier };
const requestLatencyMs_2: number;
export { requestLatencyMs_2 as requestLatencyMs };
const downloadThroughputKbps_2: number;
export { downloadThroughputKbps_2 as downloadThroughputKbps };
const uploadThroughputKbps_2: number;
export { uploadThroughputKbps_2 as uploadThroughputKbps };
}
}
export namespace screenEmulationMetrics {
export { MOTOGPOWER_EMULATION_METRICS as mobile };
export { DESKTOP_EMULATION_METRICS as desktop };
}
export namespace userAgents {
export { MOTOG4_USERAGENT as mobile };
export { DESKTOP_USERAGENT as desktop };
}
/** @type {LH.Config.Settings} */
export const defaultSettings: LH.Config.Settings;
/** @type {LH.Config.Pass} */
export const defaultPassConfig: LH.Config.Pass;
/** @type {Required<LH.Config.NavigationJson>} */
export const defaultNavigationConfig: Required<LH.Config.NavigationJson>;
export namespace nonSimulatedPassConfigOverrides {
const pauseAfterFcpMs: number;
const pauseAfterLoadMs: number;
const networkQuietThresholdMs: number;
const cpuQuietThresholdMs: number;
}
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* Adjustments needed for DevTools network throttling to simulate
* more realistic network conditions.
* @see https://crbug.com/721112
* @see https://docs.google.com/document/d/10lfVdS1iDWCRKQXPfbxEn4Or99D64mvNlugP1AQuFlE/edit
*/
declare const DEVTOOLS_RTT_ADJUSTMENT_FACTOR: 3.75;
declare const DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR: 0.9;
/**
* @type {Required<LH.SharedFlagsSettings['screenEmulation']>}
*/
declare const MOTOGPOWER_EMULATION_METRICS: Required<LH.SharedFlagsSettings['screenEmulation']>;
/**
* Desktop metrics adapted from emulated_devices/module.json
* @type {Required<LH.SharedFlagsSettings['screenEmulation']>}
*/
declare const DESKTOP_EMULATION_METRICS: Required<LH.SharedFlagsSettings['screenEmulation']>;
declare const MOTOG4_USERAGENT: "Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36";
declare const DESKTOP_USERAGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36";
export {};
//# sourceMappingURL=constants.d.ts.map

175
node_modules/lighthouse/core/config/constants.js generated vendored Normal file
View File

@@ -0,0 +1,175 @@
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* Adjustments needed for DevTools network throttling to simulate
* more realistic network conditions.
* @see https://crbug.com/721112
* @see https://docs.google.com/document/d/10lfVdS1iDWCRKQXPfbxEn4Or99D64mvNlugP1AQuFlE/edit
*/
const DEVTOOLS_RTT_ADJUSTMENT_FACTOR = 3.75;
const DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR = 0.9;
const throttling = {
DEVTOOLS_RTT_ADJUSTMENT_FACTOR,
DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR,
// These values align with WebPageTest's definition of "Fast 3G"
// But offer similar charateristics to roughly the 75th percentile of 4G connections.
mobileSlow4G: {
rttMs: 150,
throughputKbps: 1.6 * 1024,
requestLatencyMs: 150 * DEVTOOLS_RTT_ADJUSTMENT_FACTOR,
downloadThroughputKbps: 1.6 * 1024 * DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR,
uploadThroughputKbps: 750 * DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR,
cpuSlowdownMultiplier: 4,
},
// These values partially align with WebPageTest's definition of "Regular 3G".
// These values are meant to roughly align with Chrome UX report's 3G definition which are based
// on HTTP RTT of 300-1400ms and downlink throughput of <700kbps.
mobileRegular3G: {
rttMs: 300,
throughputKbps: 700,
requestLatencyMs: 300 * DEVTOOLS_RTT_ADJUSTMENT_FACTOR,
downloadThroughputKbps: 700 * DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR,
uploadThroughputKbps: 700 * DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR,
cpuSlowdownMultiplier: 4,
},
// Using a "broadband" connection type
// Corresponds to "Dense 4G 25th percentile" in https://docs.google.com/document/d/1Ft1Bnq9-t4jK5egLSOc28IL4TvR-Tt0se_1faTA4KTY/edit#heading=h.bb7nfy2x9e5v
desktopDense4G: {
rttMs: 40,
throughputKbps: 10 * 1024,
cpuSlowdownMultiplier: 1,
requestLatencyMs: 0, // 0 means unset
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
};
/**
* @type {Required<LH.SharedFlagsSettings['screenEmulation']>}
*/
const MOTOGPOWER_EMULATION_METRICS = {
mobile: true,
width: 412,
height: 823,
// This value has some interesting ramifications for image-size-responsive, see:
// https://github.com/GoogleChrome/lighthouse/issues/10741#issuecomment-626903508
deviceScaleFactor: 1.75,
disabled: false,
};
/**
* Desktop metrics adapted from emulated_devices/module.json
* @type {Required<LH.SharedFlagsSettings['screenEmulation']>}
*/
const DESKTOP_EMULATION_METRICS = {
mobile: false,
width: 1350,
height: 940,
deviceScaleFactor: 1,
disabled: false,
};
const screenEmulationMetrics = {
mobile: MOTOGPOWER_EMULATION_METRICS,
desktop: DESKTOP_EMULATION_METRICS,
};
const MOTOG4_USERAGENT = 'Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36'; // eslint-disable-line max-len
const DESKTOP_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'; // eslint-disable-line max-len
const userAgents = {
mobile: MOTOG4_USERAGENT,
desktop: DESKTOP_USERAGENT,
};
/** @type {LH.Config.Settings} */
const defaultSettings = {
output: 'json',
maxWaitForFcp: 30 * 1000,
maxWaitForLoad: 45 * 1000,
pauseAfterFcpMs: 1000,
pauseAfterLoadMs: 1000,
networkQuietThresholdMs: 1000,
cpuQuietThresholdMs: 1000,
formFactor: 'mobile',
throttling: throttling.mobileSlow4G,
throttlingMethod: 'simulate',
screenEmulation: screenEmulationMetrics.mobile,
emulatedUserAgent: userAgents.mobile,
auditMode: false,
gatherMode: false,
disableStorageReset: false,
debugNavigation: false,
channel: 'node',
usePassiveGathering: false,
disableFullPageScreenshot: false,
skipAboutBlank: false,
blankPage: 'about:blank',
// the following settings have no defaults but we still want ensure that `key in settings`
// in config will work in a typechecked way
budgets: null,
locale: 'en-US', // actual default determined by Config using lib/i18n
blockedUrlPatterns: null,
additionalTraceCategories: null,
extraHeaders: null,
precomputedLanternData: null,
onlyAudits: null,
onlyCategories: null,
skipAudits: null,
};
/** @type {LH.Config.Pass} */
const defaultPassConfig = {
passName: 'defaultPass',
loadFailureMode: 'fatal',
recordTrace: false,
useThrottling: false,
pauseAfterFcpMs: 0,
pauseAfterLoadMs: 0,
networkQuietThresholdMs: 0,
cpuQuietThresholdMs: 0,
blockedUrlPatterns: [],
blankPage: 'about:blank',
gatherers: [],
};
/** @type {Required<LH.Config.NavigationJson>} */
const defaultNavigationConfig = {
id: 'default',
loadFailureMode: 'fatal',
disableThrottling: false,
disableStorageReset: false,
pauseAfterFcpMs: 0,
pauseAfterLoadMs: 0,
networkQuietThresholdMs: 0,
cpuQuietThresholdMs: 0,
blockedUrlPatterns: [],
blankPage: 'about:blank',
artifacts: [],
};
const nonSimulatedPassConfigOverrides = {
pauseAfterFcpMs: 5250,
pauseAfterLoadMs: 5250,
networkQuietThresholdMs: 5250,
cpuQuietThresholdMs: 5250,
};
export {
throttling,
screenEmulationMetrics,
userAgents,
defaultSettings,
defaultPassConfig,
defaultNavigationConfig,
nonSimulatedPassConfigOverrides,
};

View File

@@ -0,0 +1,5 @@
export default defaultConfig;
/** @type {LH.Config} */
declare const defaultConfig: LH.Config;
import * as LH from '../../types/lh.js';
//# sourceMappingURL=default-config.d.ts.map

662
node_modules/lighthouse/core/config/default-config.js generated vendored Normal file
View File

@@ -0,0 +1,662 @@
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/* eslint-disable max-len */
import * as LH from '../../types/lh.js';
import * as constants from './constants.js';
import * as i18n from '../lib/i18n/i18n.js';
import {metricsToAudits} from './metrics-to-audits.js';
const UIStrings = {
/** Title of the Performance category of audits. Equivalent to 'Web performance', this term is inclusive of all web page speed and loading optimization topics. Also used as a label of a score gauge; try to limit to 20 characters. */
performanceCategoryTitle: 'Performance',
/** Title of the Budgets section of the Performance Category. 'Budgets' refers to a budget (like a financial budget), but applied to the amount of resources on a page, rather than money. */
budgetsGroupTitle: 'Budgets',
/** Description of the Budgets section of the Performance category. Within this section the budget results are displayed. */
budgetsGroupDescription: 'Performance budgets set standards for the performance of your site.',
/** Title of the speed metrics section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. */
metricGroupTitle: 'Metrics',
/** Title of the opportunity section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the loading performance of their web page. 'Suggestion'/'Optimization'/'Recommendation' are reasonable synonyms for 'opportunity' in this case. */
loadOpportunitiesGroupTitle: 'Opportunities',
/** Description of the opportunity section of the Performance category. 'Suggestions' could also be 'recommendations'. Within this section are audits with imperative titles that suggest actions the user can take to improve the loading performance of their web page. */
loadOpportunitiesGroupDescription: 'These suggestions can help your page load faster. They don\'t [directly affect](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/) the Performance score.',
/** Title of an opportunity sub-section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the time of the first initial render of the webpage. */
firstPaintImprovementsGroupTitle: 'First Paint Improvements',
/** Description of an opportunity sub-section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the time of the first initial render of the webpage. */
firstPaintImprovementsGroupDescription: 'The most critical aspect of performance is how quickly pixels are rendered onscreen. Key metrics: First Contentful Paint, First Meaningful Paint',
/** Title of an opportunity sub-section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the overall loading performance of their web page. */
overallImprovementsGroupTitle: 'Overall Improvements',
/** Description of an opportunity sub-section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the overall loading performance of their web page. */
overallImprovementsGroupDescription: 'Enhance the overall loading experience, so the page is responsive and ready to use as soon as possible. Key metrics: Time to Interactive, Speed Index',
/** Title of the diagnostics section of the Performance category. Within this section are audits with non-imperative titles that provide more detail on the page's page load performance characteristics. Whereas the 'Opportunities' suggest an action along with expected time savings, diagnostics do not. Within this section, the user may read the details and deduce additional actions they could take. */
diagnosticsGroupTitle: 'Diagnostics',
/** Description of the diagnostics section of the Performance category. Within this section are audits with non-imperative titles that provide more detail on a web page's load performance characteristics. Within this section, the user may read the details and deduce additional actions they could take to improve performance. */
diagnosticsGroupDescription: 'More information about the performance of your application. These numbers don\'t [directly affect](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/) the Performance score.',
/** Title of the Accessibility category of audits. This section contains audits focused on making web content accessible to all users. Also used as a label of a score gauge; try to limit to 20 characters. */
a11yCategoryTitle: 'Accessibility',
/** Description of the Accessibility category. This is displayed at the top of a list of audits focused on making web content accessible to all users. No character length limits. 'improve the accessibility of your web app' becomes link text to additional documentation. */
a11yCategoryDescription: 'These checks highlight opportunities to [improve the accessibility of your web app](https://developer.chrome.com/docs/lighthouse/accessibility/). Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.',
/** Description of the Accessibility manual checks category. This description is displayed above a list of accessibility audits that currently have no automated test and so must be verified manually by the user. No character length limits. 'conducting an accessibility review' becomes link text to additional documentation. */
a11yCategoryManualDescription: 'These items address areas which an automated testing tool cannot cover. Learn more in our guide on [conducting an accessibility review](https://web.dev/how-to-review/).',
/** Title of the best practices section of the Accessibility category. Within this section are audits with descriptive titles that highlight common accessibility best practices. */
a11yBestPracticesGroupTitle: 'Best practices',
/** Description of the best practices section within the Accessibility category. Within this section are audits with descriptive titles that highlight common accessibility best practices. */
a11yBestPracticesGroupDescription: 'These items highlight common accessibility best practices.',
/** Title of the color contrast section within the Accessibility category. Within this section are audits with descriptive titles that highlight the color and vision aspects of the page's accessibility that are passing or failing. */
a11yColorContrastGroupTitle: 'Contrast',
/** Description of the color contrast section within the Accessibility category. Within this section are audits with descriptive titles that highlight the color and vision aspects of the page's accessibility that are passing or failing. */
a11yColorContrastGroupDescription: 'These are opportunities to improve the legibility of your content.',
/** Title of the HTML element naming section within the Accessibility category. Within this section are audits with descriptive titles that highlight if the non-textual HTML elements on the page have names discernible by a screen reader. */
a11yNamesLabelsGroupTitle: 'Names and labels',
/** Description of the HTML element naming section within the Accessibility category. Within this section are audits with descriptive titles that highlight if the non-textual HTML elements on the page have names discernible by a screen reader. */
a11yNamesLabelsGroupDescription: 'These are opportunities to improve the semantics of the controls in your application. This may enhance the experience for users of assistive technology, like a screen reader.',
/** Title of the navigation section within the Accessibility category. Within this section are audits with descriptive titles that highlight opportunities to improve keyboard navigation. */
a11yNavigationGroupTitle: 'Navigation',
/** Description of the navigation section within the Accessibility category. Within this section are audits with descriptive titles that highlight opportunities to improve keyboard navigation. */
a11yNavigationGroupDescription: 'These are opportunities to improve keyboard navigation in your application.',
/** Title of the ARIA validity section within the Accessibility category. Within this section are audits with descriptive titles that highlight if whether all the aria-* HTML attributes have been used properly. */
a11yAriaGroupTitle: 'ARIA',
/** Description of the ARIA validity section within the Accessibility category. Within this section are audits with descriptive titles that highlight if whether all the aria-* HTML attributes have been used properly. */
a11yAriaGroupDescription: 'These are opportunities to improve the usage of ARIA in your application which may enhance the experience for users of assistive technology, like a screen reader.',
/** Title of the language section within the Accessibility category. Within this section are audits with descriptive titles that highlight if the language has been annotated in the correct HTML attributes on the page. */
a11yLanguageGroupTitle: 'Internationalization and localization',
/** Description of the language section within the Accessibility category. Within this section are audits with descriptive titles that highlight if the language has been annotated in the correct HTML attributes on the page. */
a11yLanguageGroupDescription: 'These are opportunities to improve the interpretation of your content by users in different locales.',
/** Title of the navigation section within the Accessibility category. Within this section are audits with descriptive titles that highlight opportunities to provide alternative content for audio and video. */
a11yAudioVideoGroupTitle: 'Audio and video',
/** Description of the navigation section within the Accessibility category. Within this section are audits with descriptive titles that highlight opportunities to provide alternative content for audio and video. */
a11yAudioVideoGroupDescription: 'These are opportunities to provide alternative content for audio and video. This may improve the experience for users with hearing or vision impairments.',
/** Title of the navigation section within the Accessibility category. Within this section are audits with descriptive titles that highlight opportunities to improve the experience of reading tabular or list data using assistive technology. */
a11yTablesListsVideoGroupTitle: 'Tables and lists',
/** Description of the navigation section within the Accessibility category. Within this section are audits with descriptive titles that highlight opportunities to improve the experience of reading tabular or list data using assistive technology. */
a11yTablesListsVideoGroupDescription: 'These are opportunities to improve the experience of reading tabular or list data using assistive technology, like a screen reader.',
/** Title of the Search Engine Optimization (SEO) category of audits. This is displayed at the top of a list of audits focused on topics related to optimizing a website for indexing by search engines. Also used as a label of a score gauge; try to limit to 20 characters. */
seoCategoryTitle: 'SEO',
/** Description of the Search Engine Optimization (SEO) category. This is displayed at the top of a list of audits focused on optimizing a website for indexing by search engines. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
seoCategoryDescription: 'These checks ensure that your page is following basic search engine optimization advice. ' +
'There are many additional factors Lighthouse does not score here that may affect your search ranking, ' +
'including performance on [Core Web Vitals](https://web.dev/learn-core-web-vitals/). [Learn more about Google Search Essentials](https://support.google.com/webmasters/answer/35769).',
/** Description of the Search Engine Optimization (SEO) manual checks category, the additional validators must be run by hand in order to check all SEO best practices. This is displayed at the top of a list of manually run audits focused on optimizing a website for indexing by search engines. No character length limits. */
seoCategoryManualDescription: 'Run these additional validators on your site to check additional SEO best practices.',
/** Title of the navigation section within the Search Engine Optimization (SEO) category. Within this section are audits with descriptive titles that highlight opportunities to make a page more usable on mobile devices. */
seoMobileGroupTitle: 'Mobile Friendly',
/** Description of the navigation section within the Search Engine Optimization (SEO) category. Within this section are audits with descriptive titles that highlight opportunities to make a page more usable on mobile devices. */
seoMobileGroupDescription: 'Make sure your pages are mobile friendly so users dont have to pinch or zoom ' +
'in order to read the content pages. [Learn how to make pages mobile-friendly](https://developers.google.com/search/mobile-sites/).',
/** Title of the navigation section within the Search Engine Optimization (SEO) category. Within this section are audits with descriptive titles that highlight ways to make a website content more easily understood by search engine crawler bots. */
seoContentGroupTitle: 'Content Best Practices',
/** Description of the navigation section within the Search Engine Optimization (SEO) category. Within this section are audits with descriptive titles that highlight ways to make a website content more easily understood by search engine crawler bots. */
seoContentGroupDescription: 'Format your HTML in a way that enables crawlers to better understand your apps content.',
/** Title of the navigation section within the Search Engine Optimization (SEO) category. Within this section are audits with descriptive titles that highlight ways to make a website accessible to search engine crawlers. */
seoCrawlingGroupTitle: 'Crawling and Indexing',
/** Description of the navigation section within the Search Engine Optimization (SEO) category. Within this section are audits with descriptive titles that highlight ways to make a website accessible to search engine crawlers. */
seoCrawlingGroupDescription: 'To appear in search results, crawlers need access to your app.',
/** Title of the Progressive Web Application (PWA) category of audits. This is displayed at the top of a list of audits focused on topics related to whether or not a site is a progressive web app, e.g. responds offline, uses a service worker, is on https, etc. Also used as a label of a score gauge. */
pwaCategoryTitle: 'PWA',
/** Description of the Progressive Web Application (PWA) category. This is displayed at the top of a list of audits focused on topics related to whether or not a site is a progressive web app, e.g. responds offline, uses a service worker, is on https, etc. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
pwaCategoryDescription: 'These checks validate the aspects of a Progressive Web App. ' +
'[Learn what makes a good Progressive Web App](https://web.dev/pwa-checklist/).',
/** Description of the Progressive Web Application (PWA) manual checks category, containing a list of additional validators must be run by hand in order to check all PWA best practices. This is displayed at the top of a list of manually run audits focused on topics related to whether or not a site is a progressive web app, e.g. responds offline, uses a service worker, is on https, etc.. No character length limits. */
pwaCategoryManualDescription: 'These checks are required by the baseline ' +
'[PWA Checklist](https://web.dev/pwa-checklist/) but are ' +
'not automatically checked by Lighthouse. They do not affect your score but it\'s important that you verify them manually.',
/** Title of the Best Practices category of audits. This is displayed at the top of a list of audits focused on topics related to following web development best practices and accepted guidelines. Also used as a label of a score gauge; try to limit to 20 characters. */
bestPracticesCategoryTitle: 'Best Practices',
/** Title of the Trust & Safety group of audits. This is displayed at the top of a list of audits focused on maintaining user trust and protecting security in web development. */
bestPracticesTrustSafetyGroupTitle: 'Trust and Safety',
/** Title of the User Experience group of the Best Practices category. Within this section are the audits related to the end user's experience of the webpage. */
bestPracticesUXGroupTitle: 'User Experience',
/** Title of the Browser Compatibility group of the Best Practices category. Within this section are the audits related to whether the page is interpreted consistently by browsers. */
bestPracticesBrowserCompatGroupTitle: 'Browser Compatibility',
/** Title of the General group of the Best Practices category. Within this section are the audits that don't belong to a specific group but are of general interest. */
bestPracticesGeneralGroupTitle: 'General',
/** Title of the Installable section of the web app category. Within this section are audits that check if Chrome supports installing the web site as an app on their device. */
pwaInstallableGroupTitle: 'Installable',
/** Title of the "PWA Optimized" section of the web app category. Within this section are audits that check if the developer has taken advantage of features to make their web page more enjoyable and engaging for the user. */
pwaOptimizedGroupTitle: 'PWA Optimized',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
/** @type {LH.Config} */
const defaultConfig = {
settings: constants.defaultSettings,
artifacts: [
// Artifacts which can be depended on come first.
{id: 'DevtoolsLog', gatherer: 'devtools-log'},
{id: 'Trace', gatherer: 'trace'},
{id: 'Accessibility', gatherer: 'accessibility'},
{id: 'AnchorElements', gatherer: 'anchor-elements'},
{id: 'CacheContents', gatherer: 'cache-contents'},
{id: 'ConsoleMessages', gatherer: 'console-messages'},
{id: 'CSSUsage', gatherer: 'css-usage'},
{id: 'Doctype', gatherer: 'dobetterweb/doctype'},
{id: 'DOMStats', gatherer: 'dobetterweb/domstats'},
{id: 'EmbeddedContent', gatherer: 'seo/embedded-content'},
{id: 'FontSize', gatherer: 'seo/font-size'},
{id: 'Inputs', gatherer: 'inputs'},
{id: 'GlobalListeners', gatherer: 'global-listeners'},
{id: 'IFrameElements', gatherer: 'iframe-elements'},
{id: 'ImageElements', gatherer: 'image-elements'},
{id: 'InstallabilityErrors', gatherer: 'installability-errors'},
{id: 'InspectorIssues', gatherer: 'inspector-issues'},
{id: 'JsUsage', gatherer: 'js-usage'},
{id: 'LinkElements', gatherer: 'link-elements'},
{id: 'MainDocumentContent', gatherer: 'main-document-content'},
{id: 'MetaElements', gatherer: 'meta-elements'},
{id: 'NetworkUserAgent', gatherer: 'network-user-agent'},
{id: 'OptimizedImages', gatherer: 'dobetterweb/optimized-images'},
{id: 'ResponseCompression', gatherer: 'dobetterweb/response-compression'},
{id: 'RobotsTxt', gatherer: 'seo/robots-txt'},
{id: 'ServiceWorker', gatherer: 'service-worker'},
{id: 'ScriptElements', gatherer: 'script-elements'},
{id: 'Scripts', gatherer: 'scripts'},
{id: 'SourceMaps', gatherer: 'source-maps'},
{id: 'Stacks', gatherer: 'stacks'},
{id: 'TagsBlockingFirstPaint', gatherer: 'dobetterweb/tags-blocking-first-paint'},
{id: 'TapTargets', gatherer: 'seo/tap-targets'},
{id: 'TraceElements', gatherer: 'trace-elements'},
{id: 'ViewportDimensions', gatherer: 'viewport-dimensions'},
{id: 'WebAppManifest', gatherer: 'web-app-manifest'},
// Artifact copies are renamed for compatibility with legacy artifacts.
{id: 'devtoolsLogs', gatherer: 'devtools-log-compat'},
{id: 'traces', gatherer: 'trace-compat'},
// FullPageScreenshot comes at the end so all other node analysis is captured.
{id: 'FullPageScreenshot', gatherer: 'full-page-screenshot'},
// BFCacheFailures comes at the very end because it can perform a page navigation.
{id: 'BFCacheFailures', gatherer: 'bf-cache-failures'},
],
audits: [
'is-on-https',
'service-worker',
'viewport',
'metrics/first-contentful-paint',
'metrics/largest-contentful-paint',
'metrics/first-meaningful-paint',
'metrics/speed-index',
'screenshot-thumbnails',
'final-screenshot',
'metrics/total-blocking-time',
'metrics/max-potential-fid',
'metrics/cumulative-layout-shift',
'metrics/experimental-interaction-to-next-paint',
'errors-in-console',
'server-response-time',
'metrics/interactive',
'user-timings',
'critical-request-chains',
'redirects',
'installable-manifest',
'splash-screen',
'themed-omnibox',
'maskable-icon',
'content-width',
'image-aspect-ratio',
'image-size-responsive',
'preload-fonts',
'deprecations',
'mainthread-work-breakdown',
'bootup-time',
'uses-rel-preload',
'uses-rel-preconnect',
'font-display',
'diagnostics',
'network-requests',
'network-rtt',
'network-server-latency',
'main-thread-tasks',
'metrics',
'performance-budget',
'timing-budget',
'resource-summary',
'third-party-summary',
'third-party-facades',
'largest-contentful-paint-element',
'lcp-lazy-loaded',
'layout-shift-elements',
'long-tasks',
'no-unload-listeners',
'non-composited-animations',
'unsized-images',
'valid-source-maps',
'prioritize-lcp-image',
'csp-xss',
'script-treemap-data',
'manual/pwa-cross-browser',
'manual/pwa-page-transitions',
'manual/pwa-each-page-has-url',
'accessibility/accesskeys',
'accessibility/aria-allowed-attr',
'accessibility/aria-command-name',
'accessibility/aria-dialog-name',
'accessibility/aria-hidden-body',
'accessibility/aria-hidden-focus',
'accessibility/aria-input-field-name',
'accessibility/aria-meter-name',
'accessibility/aria-progressbar-name',
'accessibility/aria-required-attr',
'accessibility/aria-required-children',
'accessibility/aria-required-parent',
'accessibility/aria-roles',
'accessibility/aria-text',
'accessibility/aria-toggle-field-name',
'accessibility/aria-tooltip-name',
'accessibility/aria-treeitem-name',
'accessibility/aria-valid-attr-value',
'accessibility/aria-valid-attr',
'accessibility/button-name',
'accessibility/bypass',
'accessibility/color-contrast',
'accessibility/definition-list',
'accessibility/dlitem',
'accessibility/document-title',
'accessibility/duplicate-id-active',
'accessibility/duplicate-id-aria',
'accessibility/empty-heading',
'accessibility/form-field-multiple-labels',
'accessibility/frame-title',
'accessibility/heading-order',
'accessibility/html-has-lang',
'accessibility/html-lang-valid',
'accessibility/html-xml-lang-mismatch',
'accessibility/identical-links-same-purpose',
'accessibility/image-alt',
'accessibility/input-button-name',
'accessibility/input-image-alt',
'accessibility/label',
'accessibility/landmark-one-main',
'accessibility/link-name',
'accessibility/link-in-text-block',
'accessibility/list',
'accessibility/listitem',
'accessibility/meta-refresh',
'accessibility/meta-viewport',
'accessibility/object-alt',
'accessibility/select-name',
'accessibility/tabindex',
'accessibility/table-fake-caption',
'accessibility/target-size',
'accessibility/td-has-header',
'accessibility/td-headers-attr',
'accessibility/th-has-data-cells',
'accessibility/valid-lang',
'accessibility/video-caption',
'accessibility/manual/custom-controls-labels',
'accessibility/manual/custom-controls-roles',
'accessibility/manual/focus-traps',
'accessibility/manual/focusable-controls',
'accessibility/manual/interactive-element-affordance',
'accessibility/manual/logical-tab-order',
'accessibility/manual/managed-focus',
'accessibility/manual/offscreen-content-hidden',
'accessibility/manual/use-landmarks',
'accessibility/manual/visual-order-follows-dom',
'byte-efficiency/uses-long-cache-ttl',
'byte-efficiency/total-byte-weight',
'byte-efficiency/offscreen-images',
'byte-efficiency/render-blocking-resources',
'byte-efficiency/unminified-css',
'byte-efficiency/unminified-javascript',
'byte-efficiency/unused-css-rules',
'byte-efficiency/unused-javascript',
'byte-efficiency/modern-image-formats',
'byte-efficiency/uses-optimized-images',
'byte-efficiency/uses-text-compression',
'byte-efficiency/uses-responsive-images',
'byte-efficiency/efficient-animated-content',
'byte-efficiency/duplicated-javascript',
'byte-efficiency/legacy-javascript',
'byte-efficiency/uses-responsive-images-snapshot',
'dobetterweb/doctype',
'dobetterweb/charset',
'dobetterweb/dom-size',
'dobetterweb/geolocation-on-start',
'dobetterweb/inspector-issues',
'dobetterweb/no-document-write',
'dobetterweb/js-libraries',
'dobetterweb/notification-on-start',
'dobetterweb/paste-preventing-inputs',
'dobetterweb/uses-http2',
'dobetterweb/uses-passive-event-listeners',
'seo/meta-description',
'seo/http-status-code',
'seo/font-size',
'seo/link-text',
'seo/crawlable-anchors',
'seo/is-crawlable',
'seo/robots-txt',
'seo/tap-targets',
'seo/hreflang',
'seo/plugins',
'seo/canonical',
'seo/manual/structured-data',
'work-during-interaction',
'bf-cache',
],
groups: {
'metrics': {
title: str_(UIStrings.metricGroupTitle),
},
'load-opportunities': {
title: str_(UIStrings.loadOpportunitiesGroupTitle),
description: str_(UIStrings.loadOpportunitiesGroupDescription),
},
'budgets': {
title: str_(UIStrings.budgetsGroupTitle),
description: str_(UIStrings.budgetsGroupDescription),
},
'diagnostics': {
title: str_(UIStrings.diagnosticsGroupTitle),
description: str_(UIStrings.diagnosticsGroupDescription),
},
'pwa-installable': {
title: str_(UIStrings.pwaInstallableGroupTitle),
},
'pwa-optimized': {
title: str_(UIStrings.pwaOptimizedGroupTitle),
},
'a11y-best-practices': {
title: str_(UIStrings.a11yBestPracticesGroupTitle),
description: str_(UIStrings.a11yBestPracticesGroupDescription),
},
'a11y-color-contrast': {
title: str_(UIStrings.a11yColorContrastGroupTitle),
description: str_(UIStrings.a11yColorContrastGroupDescription),
},
'a11y-names-labels': {
title: str_(UIStrings.a11yNamesLabelsGroupTitle),
description: str_(UIStrings.a11yNamesLabelsGroupDescription),
},
'a11y-navigation': {
title: str_(UIStrings.a11yNavigationGroupTitle),
description: str_(UIStrings.a11yNavigationGroupDescription),
},
'a11y-aria': {
title: str_(UIStrings.a11yAriaGroupTitle),
description: str_(UIStrings.a11yAriaGroupDescription),
},
'a11y-language': {
title: str_(UIStrings.a11yLanguageGroupTitle),
description: str_(UIStrings.a11yLanguageGroupDescription),
},
'a11y-audio-video': {
title: str_(UIStrings.a11yAudioVideoGroupTitle),
description: str_(UIStrings.a11yAudioVideoGroupDescription),
},
'a11y-tables-lists': {
title: str_(UIStrings.a11yTablesListsVideoGroupTitle),
description: str_(UIStrings.a11yTablesListsVideoGroupDescription),
},
'seo-mobile': {
title: str_(UIStrings.seoMobileGroupTitle),
description: str_(UIStrings.seoMobileGroupDescription),
},
'seo-content': {
title: str_(UIStrings.seoContentGroupTitle),
description: str_(UIStrings.seoContentGroupDescription),
},
'seo-crawl': {
title: str_(UIStrings.seoCrawlingGroupTitle),
description: str_(UIStrings.seoCrawlingGroupDescription),
},
'best-practices-trust-safety': {
title: str_(UIStrings.bestPracticesTrustSafetyGroupTitle),
},
'best-practices-ux': {
title: str_(UIStrings.bestPracticesUXGroupTitle),
},
'best-practices-browser-compat': {
title: str_(UIStrings.bestPracticesBrowserCompatGroupTitle),
},
'best-practices-general': {
title: str_(UIStrings.bestPracticesGeneralGroupTitle),
},
// Group for audits that should not be displayed.
'hidden': {title: ''},
},
categories: {
'performance': {
title: str_(UIStrings.performanceCategoryTitle),
supportedModes: ['navigation', 'timespan', 'snapshot'],
auditRefs: [
{id: 'first-contentful-paint', weight: 10, group: 'metrics', acronym: 'FCP', relevantAudits: metricsToAudits.fcpRelevantAudits},
{id: 'largest-contentful-paint', weight: 25, group: 'metrics', acronym: 'LCP', relevantAudits: metricsToAudits.lcpRelevantAudits},
{id: 'total-blocking-time', weight: 30, group: 'metrics', acronym: 'TBT', relevantAudits: metricsToAudits.tbtRelevantAudits},
{id: 'cumulative-layout-shift', weight: 25, group: 'metrics', acronym: 'CLS', relevantAudits: metricsToAudits.clsRelevantAudits},
{id: 'speed-index', weight: 10, group: 'metrics', acronym: 'SI'},
{id: 'experimental-interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP', relevantAudits: metricsToAudits.inpRelevantAudits},
// These are our "invisible" metrics. Not displayed, but still in the LHR.
{id: 'interactive', weight: 0, group: 'hidden', acronym: 'TTI'},
{id: 'max-potential-fid', weight: 0, group: 'hidden'},
{id: 'first-meaningful-paint', weight: 0, acronym: 'FMP', group: 'hidden'},
// These audits will be put in "load-opportunities" or "diagnostics" based on their details type.
{id: 'render-blocking-resources', weight: 0},
{id: 'uses-responsive-images', weight: 0},
{id: 'offscreen-images', weight: 0},
{id: 'unminified-css', weight: 0},
{id: 'unminified-javascript', weight: 0},
{id: 'unused-css-rules', weight: 0},
{id: 'unused-javascript', weight: 0},
{id: 'uses-optimized-images', weight: 0},
{id: 'modern-image-formats', weight: 0},
{id: 'uses-text-compression', weight: 0},
{id: 'uses-rel-preconnect', weight: 0},
{id: 'server-response-time', weight: 0},
{id: 'redirects', weight: 0},
{id: 'uses-rel-preload', weight: 0},
{id: 'uses-http2', weight: 0},
{id: 'efficient-animated-content', weight: 0},
{id: 'duplicated-javascript', weight: 0},
{id: 'legacy-javascript', weight: 0},
{id: 'prioritize-lcp-image', weight: 0},
{id: 'total-byte-weight', weight: 0},
{id: 'uses-long-cache-ttl', weight: 0},
{id: 'dom-size', weight: 0},
{id: 'critical-request-chains', weight: 0},
{id: 'user-timings', weight: 0},
{id: 'bootup-time', weight: 0},
{id: 'mainthread-work-breakdown', weight: 0},
{id: 'font-display', weight: 0},
{id: 'resource-summary', weight: 0},
{id: 'third-party-summary', weight: 0},
{id: 'third-party-facades', weight: 0},
{id: 'largest-contentful-paint-element', weight: 0},
{id: 'lcp-lazy-loaded', weight: 0},
{id: 'layout-shift-elements', weight: 0},
{id: 'uses-passive-event-listeners', weight: 0},
{id: 'no-document-write', weight: 0},
{id: 'long-tasks', weight: 0},
{id: 'non-composited-animations', weight: 0},
{id: 'unsized-images', weight: 0},
{id: 'viewport', weight: 0},
{id: 'uses-responsive-images-snapshot', weight: 0},
{id: 'work-during-interaction', weight: 0},
{id: 'bf-cache', weight: 0},
// Budget audits.
{id: 'performance-budget', weight: 0, group: 'budgets'},
{id: 'timing-budget', weight: 0, group: 'budgets'},
// Audits past this point contain useful data but are not displayed with other audits.
{id: 'network-requests', weight: 0, group: 'hidden'},
{id: 'network-rtt', weight: 0, group: 'hidden'},
{id: 'network-server-latency', weight: 0, group: 'hidden'},
{id: 'main-thread-tasks', weight: 0, group: 'hidden'},
{id: 'diagnostics', weight: 0, group: 'hidden'},
{id: 'metrics', weight: 0, group: 'hidden'},
{id: 'screenshot-thumbnails', weight: 0, group: 'hidden'},
{id: 'final-screenshot', weight: 0, group: 'hidden'},
{id: 'script-treemap-data', weight: 0, group: 'hidden'},
],
},
'accessibility': {
title: str_(UIStrings.a11yCategoryTitle),
description: str_(UIStrings.a11yCategoryDescription),
manualDescription: str_(UIStrings.a11yCategoryManualDescription),
supportedModes: ['navigation', 'snapshot'],
// Audit weights are meant to match the aXe scoring system of
// minor, moderate, serious, and critical.
// See the audits listed at dequeuniversity.com/rules/axe/4.1.
// Click on an audit and check the right hand column to see its severity.
auditRefs: [
{id: 'accesskeys', weight: 3, group: 'a11y-navigation'},
{id: 'aria-allowed-attr', weight: 10, group: 'a11y-aria'},
{id: 'aria-command-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-dialog-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-hidden-body', weight: 10, group: 'a11y-aria'},
{id: 'aria-hidden-focus', weight: 3, group: 'a11y-aria'},
{id: 'aria-input-field-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-meter-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-progressbar-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-required-attr', weight: 10, group: 'a11y-aria'},
{id: 'aria-required-children', weight: 10, group: 'a11y-aria'},
{id: 'aria-required-parent', weight: 10, group: 'a11y-aria'},
{id: 'aria-roles', weight: 10, group: 'a11y-aria'},
{id: 'aria-text', weight: 3, group: 'a11y-aria'},
{id: 'aria-toggle-field-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-tooltip-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-treeitem-name', weight: 3, group: 'a11y-aria'},
{id: 'aria-valid-attr-value', weight: 10, group: 'a11y-aria'},
{id: 'aria-valid-attr', weight: 10, group: 'a11y-aria'},
{id: 'button-name', weight: 10, group: 'a11y-names-labels'},
{id: 'bypass', weight: 3, group: 'a11y-navigation'},
{id: 'color-contrast', weight: 3, group: 'a11y-color-contrast'},
{id: 'definition-list', weight: 3, group: 'a11y-tables-lists'},
{id: 'dlitem', weight: 3, group: 'a11y-tables-lists'},
{id: 'document-title', weight: 3, group: 'a11y-names-labels'},
{id: 'duplicate-id-active', weight: 3, group: 'a11y-navigation'},
{id: 'duplicate-id-aria', weight: 10, group: 'a11y-aria'},
{id: 'form-field-multiple-labels', weight: 2, group: 'a11y-names-labels'},
{id: 'frame-title', weight: 3, group: 'a11y-names-labels'},
{id: 'heading-order', weight: 2, group: 'a11y-navigation'},
{id: 'html-has-lang', weight: 3, group: 'a11y-language'},
{id: 'html-lang-valid', weight: 3, group: 'a11y-language'},
{id: 'html-xml-lang-mismatch', weight: 2, group: 'a11y-language'},
{id: 'image-alt', weight: 10, group: 'a11y-names-labels'},
{id: 'input-button-name', weight: 10, group: 'a11y-names-labels'},
{id: 'input-image-alt', weight: 10, group: 'a11y-names-labels'},
{id: 'label', weight: 10, group: 'a11y-names-labels'},
{id: 'link-in-text-block', weight: 3, group: 'a11y-color-contrast'},
{id: 'link-name', weight: 3, group: 'a11y-names-labels'},
{id: 'list', weight: 3, group: 'a11y-tables-lists'},
{id: 'listitem', weight: 3, group: 'a11y-tables-lists'},
{id: 'meta-refresh', weight: 10, group: 'a11y-best-practices'},
{id: 'meta-viewport', weight: 10, group: 'a11y-best-practices'},
{id: 'object-alt', weight: 3, group: 'a11y-names-labels'},
{id: 'select-name', weight: 3, group: 'a11y-names-labels'},
{id: 'tabindex', weight: 3, group: 'a11y-navigation'},
{id: 'table-fake-caption', weight: 3, group: 'a11y-tables-lists'},
{id: 'td-has-header', weight: 10, group: 'a11y-tables-lists'},
{id: 'td-headers-attr', weight: 3, group: 'a11y-tables-lists'},
{id: 'th-has-data-cells', weight: 3, group: 'a11y-tables-lists'},
{id: 'valid-lang', weight: 3, group: 'a11y-language'},
{id: 'video-caption', weight: 10, group: 'a11y-audio-video'},
// Manual audits
{id: 'logical-tab-order', weight: 0},
{id: 'focusable-controls', weight: 0},
{id: 'interactive-element-affordance', weight: 0},
{id: 'managed-focus', weight: 0},
{id: 'focus-traps', weight: 0},
{id: 'custom-controls-labels', weight: 0},
{id: 'custom-controls-roles', weight: 0},
{id: 'visual-order-follows-dom', weight: 0},
{id: 'offscreen-content-hidden', weight: 0},
{id: 'use-landmarks', weight: 0},
// Hidden audits
{id: 'empty-heading', weight: 0, group: 'hidden'},
{id: 'identical-links-same-purpose', weight: 0, group: 'hidden'},
{id: 'landmark-one-main', weight: 0, group: 'hidden'},
{id: 'target-size', weight: 0, group: 'hidden'},
],
},
'best-practices': {
title: str_(UIStrings.bestPracticesCategoryTitle),
supportedModes: ['navigation', 'timespan', 'snapshot'],
auditRefs: [
// Trust & Safety
{id: 'is-on-https', weight: 1, group: 'best-practices-trust-safety'},
{id: 'geolocation-on-start', weight: 1, group: 'best-practices-trust-safety'},
{id: 'notification-on-start', weight: 1, group: 'best-practices-trust-safety'},
{id: 'csp-xss', weight: 0, group: 'best-practices-trust-safety'},
// User Experience
{id: 'paste-preventing-inputs', weight: 1, group: 'best-practices-ux'},
{id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'},
{id: 'image-size-responsive', weight: 1, group: 'best-practices-ux'},
{id: 'preload-fonts', weight: 1, group: 'best-practices-ux'},
// Browser Compatibility
{id: 'doctype', weight: 1, group: 'best-practices-browser-compat'},
{id: 'charset', weight: 1, group: 'best-practices-browser-compat'},
// General Group
{id: 'no-unload-listeners', weight: 1, group: 'best-practices-general'},
{id: 'js-libraries', weight: 0, group: 'best-practices-general'},
{id: 'deprecations', weight: 1, group: 'best-practices-general'},
{id: 'errors-in-console', weight: 1, group: 'best-practices-general'},
{id: 'valid-source-maps', weight: 0, group: 'best-practices-general'},
{id: 'inspector-issues', weight: 1, group: 'best-practices-general'},
],
},
'seo': {
title: str_(UIStrings.seoCategoryTitle),
description: str_(UIStrings.seoCategoryDescription),
manualDescription: str_(UIStrings.seoCategoryManualDescription),
supportedModes: ['navigation', 'snapshot'],
auditRefs: [
{id: 'viewport', weight: 1, group: 'seo-mobile'},
{id: 'document-title', weight: 1, group: 'seo-content'},
{id: 'meta-description', weight: 1, group: 'seo-content'},
{id: 'http-status-code', weight: 1, group: 'seo-crawl'},
{id: 'link-text', weight: 1, group: 'seo-content'},
{id: 'crawlable-anchors', weight: 1, group: 'seo-crawl'},
{id: 'is-crawlable', weight: 1, group: 'seo-crawl'},
{id: 'robots-txt', weight: 1, group: 'seo-crawl'},
{id: 'image-alt', weight: 1, group: 'seo-content'},
{id: 'hreflang', weight: 1, group: 'seo-content'},
{id: 'canonical', weight: 1, group: 'seo-content'},
{id: 'font-size', weight: 1, group: 'seo-mobile'},
{id: 'plugins', weight: 1, group: 'seo-content'},
{id: 'tap-targets', weight: 1, group: 'seo-mobile'},
// Manual audits
{id: 'structured-data', weight: 0},
],
},
'pwa': {
title: str_(UIStrings.pwaCategoryTitle),
description: str_(UIStrings.pwaCategoryDescription),
manualDescription: str_(UIStrings.pwaCategoryManualDescription),
supportedModes: ['navigation'],
auditRefs: [
// Installable
{id: 'installable-manifest', weight: 2, group: 'pwa-installable'},
// PWA Optimized
{id: 'service-worker', weight: 1, group: 'pwa-optimized'},
{id: 'splash-screen', weight: 1, group: 'pwa-optimized'},
{id: 'themed-omnibox', weight: 1, group: 'pwa-optimized'},
{id: 'content-width', weight: 1, group: 'pwa-optimized'},
{id: 'viewport', weight: 2, group: 'pwa-optimized'},
{id: 'maskable-icon', weight: 1, group: 'pwa-optimized'},
// Manual audits
{id: 'pwa-cross-browser', weight: 0},
{id: 'pwa-page-transitions', weight: 0},
{id: 'pwa-each-page-has-url', weight: 0},
],
},
},
};
// Use `defineProperty` so that the strings are accesible from original but ignored when we copy it
Object.defineProperty(defaultConfig, 'UIStrings', {
enumerable: false,
get: () => UIStrings,
});
export default defaultConfig;

View File

@@ -0,0 +1,5 @@
export default config;
/** @type {LH.Config} */
declare const config: LH.Config;
import * as LH from '../../types/lh.js';
//# sourceMappingURL=desktop-config.d.ts.map

21
node_modules/lighthouse/core/config/desktop-config.js generated vendored Normal file
View File

@@ -0,0 +1,21 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import * as LH from '../../types/lh.js';
import * as constants from './constants.js';
/** @type {LH.Config} */
const config = {
extends: 'lighthouse:default',
settings: {
formFactor: 'desktop',
throttling: constants.throttling.desktopDense4G,
screenEmulation: constants.screenEmulationMetrics.desktop,
emulatedUserAgent: constants.userAgents.desktop,
},
};
export default config;

View File

@@ -0,0 +1,13 @@
export default config;
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Config for new audits that aren't quite ready for
* being enabled by default.
*/
/** @type {LH.Config} */
declare const config: LH.Config;
//# sourceMappingURL=experimental-config.d.ts.map

View File

@@ -0,0 +1,29 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Config for new audits that aren't quite ready for
* being enabled by default.
*/
/** @type {LH.Config} */
const config = {
extends: 'lighthouse:default',
audits: [
'autocomplete',
],
categories: {
// @ts-ignore: `title` is required in CategoryJson. setting to the same value as the default
// config is awkward - easier to omit the property here. Will defer to default config.
'best-practices': {
auditRefs: [
{id: 'autocomplete', weight: 0, group: 'best-practices-ux'},
],
},
},
};
export default config;

83
node_modules/lighthouse/core/config/filters.d.ts generated vendored Normal file
View File

@@ -0,0 +1,83 @@
/**
* 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}
*/
export function filterConfigByGatherMode(resolvedConfig: LH.Config.ResolvedConfig, mode: LH.Gatherer.GatherMode): LH.Config.ResolvedConfig;
/**
* 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}
*/
export function filterConfigByExplicitFilters(resolvedConfig: LH.Config.ResolvedConfig, filters: Pick<LH.Config.Settings, 'onlyAudits' | 'onlyCategories' | 'skipAudits'>): LH.Config.ResolvedConfig;
/**
* 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']}
*/
export function filterArtifactsByGatherMode(artifacts: LH.Config.ResolvedConfig['artifacts'], mode: LH.Gatherer.GatherMode): LH.Config.ResolvedConfig['artifacts'];
/**
* 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']}
*/
export function filterArtifactsByAvailableAudits(artifacts: LH.Config.ResolvedConfig['artifacts'], audits: LH.Config.ResolvedConfig['audits']): LH.Config.ResolvedConfig['artifacts'];
/**
* Filters an array of navigations down to the set supported by the available artifacts.
*
* @param {LH.Config.ResolvedConfig['navigations']} navigations
* @param {Array<LH.Config.AnyArtifactDefn>} availableArtifacts
* @return {LH.Config.ResolvedConfig['navigations']}
*/
export function filterNavigationsByAvailableArtifacts(navigations: LH.Config.ResolvedConfig['navigations'], availableArtifacts: Array<LH.Config.AnyArtifactDefn>): LH.Config.ResolvedConfig['navigations'];
/**
* 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']}
*/
export function filterAuditsByAvailableArtifacts(audits: LH.Config.ResolvedConfig['audits'], availableArtifacts: Array<LH.Config.AnyArtifactDefn>): LH.Config.ResolvedConfig['audits'];
/**
* 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']}
*/
export function filterAuditsByGatherMode(audits: LH.Config.ResolvedConfig['audits'], mode: LH.Gatherer.GatherMode): LH.Config.ResolvedConfig['audits'];
/**
* 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']}
*/
export function filterCategoriesByAvailableAudits(categories: LH.Config.ResolvedConfig['categories'], availableAudits: Array<LH.Config.AuditDefn>): LH.Config.ResolvedConfig['categories'];
/**
* 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']}
*/
export function filterCategoriesByExplicitFilters(categories: LH.Config.ResolvedConfig['categories'], onlyCategories: string[] | null | undefined): LH.Config.ResolvedConfig['categories'];
/**
* 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']}
*/
export function filterCategoriesByGatherMode(categories: LH.Config.ResolvedConfig['categories'], mode: LH.Gatherer.GatherMode): LH.Config.ResolvedConfig['categories'];
//# sourceMappingURL=filters.d.ts.map

344
node_modules/lighthouse/core/config/filters.js generated vendored Normal file
View File

@@ -0,0 +1,344 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import log from 'lighthouse-logger';
import {Audit} from '../audits/audit.js';
/** @type {Record<keyof LH.FRBaseArtifacts, string>} */
const baseArtifactKeySource = {
fetchTime: '',
LighthouseRunWarnings: '',
BenchmarkIndex: '',
BenchmarkIndexes: '',
settings: '',
Timing: '',
URL: '',
PageLoadError: '',
HostFormFactor: '',
HostUserAgent: '',
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 navigations down to the set supported by the available artifacts.
*
* @param {LH.Config.ResolvedConfig['navigations']} navigations
* @param {Array<LH.Config.AnyArtifactDefn>} availableArtifacts
* @return {LH.Config.ResolvedConfig['navigations']}
*/
function filterNavigationsByAvailableArtifacts(navigations, availableArtifacts) {
if (!navigations) return navigations;
const availableArtifactIds = new Set(
availableArtifacts.map(artifact => artifact.id).concat(baseArtifactKeys)
);
return navigations
.map(navigation => {
return {
...navigation,
artifacts: navigation.artifacts.filter((artifact) => availableArtifactIds.has(artifact.id)),
};
})
.filter(navigation => navigation.artifacts.length);
}
/**
* 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);
}
/**
* Logs a warning 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 warnOnUnknownOnlyCategories(allCategories, onlyCategories) {
if (!onlyCategories) return;
for (const onlyCategoryId of onlyCategories) {
if (!allCategories?.[onlyCategoryId]) {
log.warn('config', `unrecognized category in 'onlyCategories': ${onlyCategoryId}`);
}
}
}
/**
* 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;
warnOnUnknownOnlyCategories(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');
}
const navigations =
filterNavigationsByAvailableArtifacts(resolvedConfig.navigations, artifacts || []);
return {
...resolvedConfig,
artifacts,
navigations,
audits,
categories,
};
}
export {
filterConfigByGatherMode,
filterConfigByExplicitFilters,
filterArtifactsByGatherMode,
filterArtifactsByAvailableAudits,
filterNavigationsByAvailableArtifacts,
filterAuditsByAvailableArtifacts,
filterAuditsByGatherMode,
filterCategoriesByAvailableAudits,
filterCategoriesByExplicitFilters,
filterCategoriesByGatherMode,
};

9
node_modules/lighthouse/core/config/full-config.d.ts generated vendored Normal file
View File

@@ -0,0 +1,9 @@
export default fullConfig;
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/** @type {LH.Config} */
declare const fullConfig: LH.Config;
//# sourceMappingURL=full-config.d.ts.map

13
node_modules/lighthouse/core/config/full-config.js generated vendored Normal file
View File

@@ -0,0 +1,13 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/** @type {LH.Config} */
const fullConfig = {
extends: 'lighthouse:default',
settings: {},
};
export default fullConfig;

View File

@@ -0,0 +1,4 @@
export default config;
/** @type {LH.Config} */
declare const config: LH.Config;
//# sourceMappingURL=lr-desktop-config.d.ts.map

View File

@@ -0,0 +1,28 @@
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import * as constants from './constants.js';
/** @type {LH.Config} */
const config = {
extends: 'lighthouse:default',
settings: {
maxWaitForFcp: 15 * 1000,
maxWaitForLoad: 35 * 1000,
formFactor: 'desktop',
throttling: constants.throttling.desktopDense4G,
screenEmulation: constants.screenEmulationMetrics.desktop,
emulatedUserAgent: constants.userAgents.desktop,
skipAudits: [
// Skip the h2 audit so it doesn't lie to us. See https://github.com/GoogleChrome/lighthouse/issues/6539
'uses-http2',
// There are always bf-cache failures when testing in headless. Reenable when headless can give us realistic bf-cache insights.
'bf-cache',
],
},
};
export default config;

View File

@@ -0,0 +1,9 @@
export default config;
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/** @type {LH.Config} */
declare const config: LH.Config;
//# sourceMappingURL=lr-mobile-config.d.ts.map

View File

@@ -0,0 +1,22 @@
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/** @type {LH.Config} */
const config = {
extends: 'lighthouse:default',
settings: {
maxWaitForFcp: 15 * 1000,
maxWaitForLoad: 35 * 1000,
skipAudits: [
// Skip the h2 audit so it doesn't lie to us. See https://github.com/GoogleChrome/lighthouse/issues/6539
'uses-http2',
// There are always bf-cache failures when testing in headless. Reenable when headless can give us realistic bf-cache insights.
'bf-cache',
],
},
};
export default config;

View File

@@ -0,0 +1,19 @@
export namespace metricsToAudits {
export { fcpRelevantAudits };
export { lcpRelevantAudits };
export { tbtRelevantAudits };
export { clsRelevantAudits };
export { inpRelevantAudits };
}
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
declare const fcpRelevantAudits: string[];
declare const lcpRelevantAudits: string[];
declare const tbtRelevantAudits: string[];
declare const clsRelevantAudits: string[];
declare const inpRelevantAudits: string[];
export {};
//# sourceMappingURL=metrics-to-audits.d.ts.map

View File

@@ -0,0 +1,61 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
// go/lh-audit-metric-mapping
const fcpRelevantAudits = [
'server-response-time',
'render-blocking-resources',
'redirects',
'critical-request-chains',
'uses-text-compression',
'uses-rel-preconnect',
'uses-rel-preload',
'font-display',
'unminified-javascript',
'unminified-css',
'unused-css-rules',
];
const lcpRelevantAudits = [
...fcpRelevantAudits,
'largest-contentful-paint-element',
'prioritize-lcp-image',
'unused-javascript',
'efficient-animated-content',
'total-byte-weight',
'lcp-lazy-loaded',
];
const tbtRelevantAudits = [
'long-tasks',
'third-party-summary',
'third-party-facades',
'bootup-time',
'mainthread-work-breakdown',
'dom-size',
'duplicated-javascript',
'legacy-javascript',
'viewport',
];
const clsRelevantAudits = [
'layout-shift-elements',
'non-composited-animations',
'unsized-images',
// 'preload-fonts', // actually in BP, rather than perf
];
const inpRelevantAudits = [
'work-during-interaction',
];
export const metricsToAudits = {
fcpRelevantAudits,
lcpRelevantAudits,
tbtRelevantAudits,
clsRelevantAudits,
inpRelevantAudits,
};

9
node_modules/lighthouse/core/config/perf-config.d.ts generated vendored Normal file
View File

@@ -0,0 +1,9 @@
export default perfConfig;
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/** @type {LH.Config} */
declare const perfConfig: LH.Config;
//# sourceMappingURL=perf-config.d.ts.map

16
node_modules/lighthouse/core/config/perf-config.js generated vendored Normal file
View File

@@ -0,0 +1,16 @@
/**
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/** @type {LH.Config} */
const perfConfig = {
extends: 'lighthouse:default',
settings: {
throttlingMethod: 'devtools',
onlyCategories: ['performance'],
},
};
export default perfConfig;

77
node_modules/lighthouse/core/config/validation.d.ts generated vendored Normal file
View File

@@ -0,0 +1,77 @@
/**
* @param {LH.Config.GathererDefn | LH.Config.AnyFRGathererDefn} gathererDefn
* @return {gathererDefn is LH.Config.AnyFRGathererDefn}
*/
export function isFRGathererDefn(gathererDefn: LH.Config.GathererDefn | LH.Config.AnyFRGathererDefn): gathererDefn is import("../../types/config.js").default.AnyFRGathererDefn;
/**
* Determines if the artifact dependency direction is valid. The dependency's minimum supported mode
* must be less than or equal to the dependent's.
*
* @param {LH.Config.AnyFRGathererDefn} dependent The artifact that depends on the other.
* @param {LH.Config.AnyFRGathererDefn} dependency The artifact that is being depended on by the other.
* @return {boolean}
*/
export function isValidArtifactDependency(dependent: LH.Config.AnyFRGathererDefn, dependency: LH.Config.AnyFRGathererDefn): boolean;
/**
* Throws if pluginName is invalid or (somehow) collides with a category in the
* config being added to.
* @param {LH.Config} config
* @param {string} pluginName
*/
export function assertValidPluginName(config: LH.Config, pluginName: string): void;
/**
* Throws an error if the provided object does not implement the required Fraggle Rock gatherer interface.
* @param {LH.Config.AnyFRGathererDefn} gathererDefn
*/
export function assertValidFRGatherer(gathererDefn: LH.Config.AnyFRGathererDefn): void;
/**
* Throws an error if the provided object does not implement the required navigations interface.
* @param {LH.Config.ResolvedConfig['navigations']} navigationsDefn
* @return {{warnings: string[]}}
*/
export function assertValidFRNavigations(navigationsDefn: LH.Config.ResolvedConfig['navigations']): {
warnings: string[];
};
/**
* Throws an error if the provided object does not implement the required properties of an audit
* definition.
* @param {LH.Config.AuditDefn} auditDefinition
*/
export function assertValidAudit(auditDefinition: LH.Config.AuditDefn): void;
/**
* @param {LH.Config.ResolvedConfig['categories']} categories
* @param {LH.Config.ResolvedConfig['audits']} audits
* @param {LH.Config.ResolvedConfig['groups']} groups
*/
export function assertValidCategories(categories: LH.Config.ResolvedConfig['categories'], audits: LH.Config.ResolvedConfig['audits'], groups: LH.Config.ResolvedConfig['groups']): void;
/**
* Validate the settings after they've been built.
* @param {LH.Config.Settings} settings
*/
export function assertValidSettings(settings: LH.Config.Settings): void;
/**
* Asserts that artifacts are in a valid dependency order that can be computed.
*
* @param {Array<LH.Config.NavigationDefn>} navigations
*/
export function assertArtifactTopologicalOrder(navigations: Array<LH.Config.NavigationDefn>): void;
/**
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @return {{warnings: string[]}}
*/
export function assertValidConfig(resolvedConfig: LH.Config.ResolvedConfig): {
warnings: string[];
};
/**
* @param {string} artifactId
* @param {string} dependencyKey
* @return {never}
*/
export function throwInvalidDependencyOrder(artifactId: string, dependencyKey: string): never;
/**
* @param {string} artifactId
* @param {string} dependencyKey
* @return {never}
*/
export function throwInvalidArtifactDependency(artifactId: string, dependencyKey: string): never;
//# sourceMappingURL=validation.d.ts.map

324
node_modules/lighthouse/core/config/validation.js generated vendored Normal file
View File

@@ -0,0 +1,324 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import {Audit} from '../audits/audit.js';
import BaseFRGatherer from '../gather/base-gatherer.js';
import * as i18n from '../lib/i18n/i18n.js';
/**
* @param {LH.Config.GathererDefn | LH.Config.AnyFRGathererDefn} gathererDefn
* @return {gathererDefn is LH.Config.AnyFRGathererDefn}
*/
function isFRGathererDefn(gathererDefn) {
return 'meta' in gathererDefn.instance;
}
/**
* Determines if the artifact dependency direction is valid. The dependency's minimum supported mode
* must be less than or equal to the dependent's.
*
* @param {LH.Config.AnyFRGathererDefn} dependent The artifact that depends on the other.
* @param {LH.Config.AnyFRGathererDefn} dependency The artifact that is being depended on by the other.
* @return {boolean}
*/
function isValidArtifactDependency(dependent, dependency) {
const levels = {timespan: 0, snapshot: 1, navigation: 2};
const dependentLevel = Math.min(...dependent.instance.meta.supportedModes.map(l => levels[l]));
const dependencyLevel = Math.min(...dependency.instance.meta.supportedModes.map(l => levels[l]));
// A timespan artifact cannot depend on a snapshot/navigation artifact because it might run without a snapshot.
if (dependentLevel === levels.timespan) return dependencyLevel === levels.timespan;
// A snapshot artifact cannot depend on a timespan/navigation artifact because it might run without a timespan.
if (dependentLevel === levels.snapshot) return dependencyLevel === levels.snapshot;
// A navigation artifact can depend on anything.
return true;
}
/**
* Throws if pluginName is invalid or (somehow) collides with a category in the
* config being added to.
* @param {LH.Config} config
* @param {string} pluginName
*/
function assertValidPluginName(config, pluginName) {
if (!pluginName.startsWith('lighthouse-plugin-')) {
throw new Error(`plugin name '${pluginName}' does not start with 'lighthouse-plugin-'`);
}
if (config.categories?.[pluginName]) {
throw new Error(`plugin name '${pluginName}' not allowed because it is the id of a category already found in config`); // eslint-disable-line max-len
}
}
/**
* Throws an error if the provided object does not implement the required Fraggle Rock gatherer interface.
* @param {LH.Config.AnyFRGathererDefn} gathererDefn
*/
function assertValidFRGatherer(gathererDefn) {
const gatherer = gathererDefn.instance;
const gathererName = gatherer.name;
if (typeof gatherer.meta !== 'object') {
throw new Error(`${gathererName} gatherer did not provide a meta object.`);
}
if (gatherer.meta.supportedModes.length === 0) {
throw new Error(`${gathererName} gatherer did not support any gather modes.`);
}
if (
typeof gatherer.getArtifact !== 'function' ||
gatherer.getArtifact === BaseFRGatherer.prototype.getArtifact
) {
throw new Error(`${gathererName} gatherer did not define a "getArtifact" method.`);
}
}
/**
* Throws an error if the provided object does not implement the required navigations interface.
* @param {LH.Config.ResolvedConfig['navigations']} navigationsDefn
* @return {{warnings: string[]}}
*/
function assertValidFRNavigations(navigationsDefn) {
if (!navigationsDefn || !navigationsDefn.length) return {warnings: []};
/** @type {string[]} */
const warnings = [];
// Assert that the first navigation has loadFailureMode fatal.
const firstNavigation = navigationsDefn[0];
if (firstNavigation.loadFailureMode !== 'fatal') {
const currentMode = firstNavigation.loadFailureMode;
const warning = [
`"${firstNavigation.id}" is the first navigation but had a failure mode of ${currentMode}.`,
`The first navigation will always be treated as loadFailureMode=fatal.`,
].join(' ');
warnings.push(warning);
firstNavigation.loadFailureMode = 'fatal';
}
// Assert that navigations have unique IDs.
const navigationIds = navigationsDefn.map(navigation => navigation.id);
const duplicateId = navigationIds.find(
(id, i) => navigationIds.slice(i + 1).some(other => id === other)
);
if (duplicateId) {
throw new Error(`Navigation must have unique identifiers, but "${duplicateId}" was repeated.`);
}
return {warnings};
}
/**
* Throws an error if the provided object does not implement the required properties of an audit
* definition.
* @param {LH.Config.AuditDefn} auditDefinition
*/
function assertValidAudit(auditDefinition) {
const {implementation, path: auditPath} = auditDefinition;
const auditName = auditPath ||
implementation?.meta?.id ||
'Unknown audit';
if (typeof implementation.audit !== 'function' || implementation.audit === Audit.audit) {
throw new Error(`${auditName} has no audit() method.`);
}
if (typeof implementation.meta.id !== 'string') {
throw new Error(`${auditName} has no meta.id property, or the property is not a string.`);
}
if (!i18n.isStringOrIcuMessage(implementation.meta.title)) {
throw new Error(`${auditName} has no meta.title property, or the property is not a string.`);
}
// If it'll have a ✔ or ✖ displayed alongside the result, it should have failureTitle
const scoreDisplayMode = implementation.meta.scoreDisplayMode || Audit.SCORING_MODES.BINARY;
if (
!i18n.isStringOrIcuMessage(implementation.meta.failureTitle) &&
scoreDisplayMode === Audit.SCORING_MODES.BINARY
) {
throw new Error(`${auditName} has no meta.failureTitle and should.`);
}
if (!i18n.isStringOrIcuMessage(implementation.meta.description)) {
throw new Error(
`${auditName} has no meta.description property, or the property is not a string.`
);
} else if (implementation.meta.description === '') {
throw new Error(
`${auditName} has an empty meta.description string. Please add a description for the UI.`
);
}
if (!Array.isArray(implementation.meta.requiredArtifacts)) {
throw new Error(
`${auditName} has no meta.requiredArtifacts property, or the property is not an array.`
);
}
}
/**
* @param {LH.Config.ResolvedConfig['categories']} categories
* @param {LH.Config.ResolvedConfig['audits']} audits
* @param {LH.Config.ResolvedConfig['groups']} groups
*/
function assertValidCategories(categories, audits, groups) {
if (!categories) {
return;
}
/** @type {Map<string, LH.Config.AuditDefn>} */
const auditsKeyedById = new Map((audits || []).map(audit => {
return [audit.implementation.meta.id, audit];
}));
Object.keys(categories).forEach(categoryId => {
categories[categoryId].auditRefs.forEach((auditRef, index) => {
if (!auditRef.id) {
throw new Error(`missing an audit id at ${categoryId}[${index}]`);
}
const audit = auditsKeyedById.get(auditRef.id);
if (!audit) {
throw new Error(`could not find ${auditRef.id} audit for category ${categoryId}`);
}
const auditImpl = audit.implementation;
const isManual = auditImpl.meta.scoreDisplayMode === 'manual';
if (categoryId === 'accessibility' && !auditRef.group && !isManual) {
throw new Error(`${auditRef.id} accessibility audit does not have a group`);
}
if (auditRef.weight > 0 && isManual) {
throw new Error(`${auditRef.id} is manual but has a positive weight`);
}
if (auditRef.group && (!groups || !groups[auditRef.group])) {
throw new Error(`${auditRef.id} references unknown group ${auditRef.group}`);
}
});
});
}
/**
* Validate the settings after they've been built.
* @param {LH.Config.Settings} settings
*/
function assertValidSettings(settings) {
if (!settings.formFactor) {
throw new Error(`\`settings.formFactor\` must be defined as 'mobile' or 'desktop'. See https://github.com/GoogleChrome/lighthouse/blob/main/docs/emulation.md`);
}
if (!settings.screenEmulation.disabled) {
// formFactor doesn't control emulation. So we don't want a mismatch:
// Bad mismatch A: user wants mobile emulation but scoring is configured for desktop
// Bad mismtach B: user wants everything desktop and set formFactor, but accidentally not screenEmulation
if (settings.screenEmulation.mobile !== (settings.formFactor === 'mobile')) {
throw new Error(`Screen emulation mobile setting (${settings.screenEmulation.mobile}) does not match formFactor setting (${settings.formFactor}). See https://github.com/GoogleChrome/lighthouse/blob/main/docs/emulation.md`);
}
}
const skippedAndOnlyAuditId =
settings.skipAudits?.find(auditId => settings.onlyAudits?.includes(auditId));
if (skippedAndOnlyAuditId) {
throw new Error(`${skippedAndOnlyAuditId} appears in both skipAudits and onlyAudits`);
}
}
/**
* Asserts that artifacts are in a valid dependency order that can be computed.
*
* @param {Array<LH.Config.NavigationDefn>} navigations
*/
function assertArtifactTopologicalOrder(navigations) {
const availableArtifacts = new Set();
for (const navigation of navigations) {
for (const artifact of navigation.artifacts) {
availableArtifacts.add(artifact.id);
if (!artifact.dependencies) continue;
for (const [dependencyKey, {id: dependencyId}] of Object.entries(artifact.dependencies)) {
if (availableArtifacts.has(dependencyId)) continue;
throwInvalidDependencyOrder(artifact.id, dependencyKey);
}
}
}
}
/**
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @return {{warnings: string[]}}
*/
function assertValidConfig(resolvedConfig) {
const {warnings} = assertValidFRNavigations(resolvedConfig.navigations);
/** @type {Set<string>} */
const artifactIds = new Set();
for (const artifactDefn of resolvedConfig.artifacts || []) {
if (artifactIds.has(artifactDefn.id)) {
throw new Error(`Config defined multiple artifacts with id '${artifactDefn.id}'`);
}
artifactIds.add(artifactDefn.id);
assertValidFRGatherer(artifactDefn.gatherer);
}
for (const auditDefn of resolvedConfig.audits || []) {
assertValidAudit(auditDefn);
}
assertValidCategories(resolvedConfig.categories, resolvedConfig.audits, resolvedConfig.groups);
assertValidSettings(resolvedConfig.settings);
return {warnings};
}
/**
* @param {string} artifactId
* @param {string} dependencyKey
* @return {never}
*/
function throwInvalidDependencyOrder(artifactId, dependencyKey) {
throw new Error(
[
`Failed to find dependency "${dependencyKey}" for "${artifactId}" artifact`,
`Check that...`,
` 1. A gatherer exposes a matching Symbol that satisfies "${dependencyKey}".`,
` 2. "${dependencyKey}" is configured to run before "${artifactId}"`,
].join('\n')
);
}
/**
* @param {string} artifactId
* @param {string} dependencyKey
* @return {never}
*/
function throwInvalidArtifactDependency(artifactId, dependencyKey) {
throw new Error(
[
`Dependency "${dependencyKey}" for "${artifactId}" artifact is invalid.`,
`The dependency must be collected before the dependent.`,
].join('\n')
);
}
export {
isFRGathererDefn,
isValidArtifactDependency,
assertValidPluginName,
assertValidFRGatherer,
assertValidFRNavigations,
assertValidAudit,
assertValidCategories,
assertValidSettings,
assertArtifactTopologicalOrder,
assertValidConfig,
throwInvalidDependencyOrder,
throwInvalidArtifactDependency,
};