UNPKG

lighthouse

Version:

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

257 lines (218 loc) • 7.79 kB
/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import data from '../lib/baseline/web-features-data.json' with {type: 'json'}; import {Audit} from './audit.js'; import metadata from '../lib/baseline/web-features-metadata.json' with {type: 'json'}; import * as i18n from '../lib/i18n/i18n.js'; const UIStrings = { /** Title of the Baseline audit. Shown when the page is compatible with the target baseline. */ title: 'Baseline Features', /** * @description Description of the Baseline audit. * @example {2026-03-18} date */ description: 'Lists web features used on the page and their Baseline status as of {date}. ' + '[Learn more about Baseline](https://webstatus.dev/).', /** Label for the column displaying the feature ID. */ columnFeature: 'Web-features', /** Label for the column displaying the feature\'s baseline status. */ columnStatus: 'Baseline Status', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); /** @typedef {LH.TraceEvent & {args: {feature: string, url?: string, lineNumber?: number, columnNumber?: number}}} DXFeatureEvent */ class Baseline extends Audit { static featureData = data; /** * @return {LH.Audit.Meta} */ static get meta() { return { id: 'baseline', scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE, title: str_(UIStrings.title), description: str_(UIStrings.description, {date: metadata.date}), requiredArtifacts: ['Trace'], }; } /** * Determines the baseline status and display string for a given feature ID. * @param {string} featureId * @param {{high: Record<string, string>, low: Record<string, string>, limited: string[]}} featureData * @param {Date} currentDate * @return {{displayStatus: string, baselineTier: 'high' | 'low' | 'limited'} | null} */ static getFeatureStatus(featureId, featureData, currentDate) { if (featureId in featureData.high) { const highData = /** @type {Record<string, string>} */ (featureData.high); return { displayStatus: `Widely Available (${highData[featureId]})`, baselineTier: 'high', }; } if (featureId in featureData.low) { const lowData = /** @type {Record<string, string>} */ (featureData.low); const widelyAvailableDate = new Date(lowData[featureId]); widelyAvailableDate.setUTCMonth(widelyAvailableDate.getUTCMonth() + 30); const widelyAvailableDateStr = widelyAvailableDate.toISOString().slice(0, 10); if (widelyAvailableDate <= currentDate) { return { displayStatus: `Widely Available (${widelyAvailableDateStr})`, baselineTier: 'high', }; } else { return { displayStatus: `Newly Available (${lowData[featureId]})`, baselineTier: 'low', }; } } if (featureData.limited.includes(featureId)) { return { displayStatus: 'Limited Availability', baselineTier: 'limited', }; } return null; } /** * @param {string} featureId * @param {{high: Record<string, string>, low: Record<string, string>, limited: string[]}} featureData * @return {string|null} */ static getLowDate(featureId, featureData) { if (featureId in featureData.low) { const lowData = /** @type {Record<string, string>} */ (featureData.low); return lowData[featureId]; } if (featureId in featureData.high) { const highData = /** @type {Record<string, string>} */ (featureData.high); const date = new Date(highData[featureId]); date.setUTCMonth(date.getUTCMonth() - 30); return date.toISOString().slice(0, 10); } return null; } /** * @param {LH.Artifacts} artifacts * @return {Promise<LH.Audit.Product>} */ static async audit(artifacts) { const trace = artifacts.Trace; /** @type {Map<string, {featureId: string, source: LH.Audit.Details.SourceLocationValue | undefined}>} */ const featuresMap = new Map(); for (const e of trace.traceEvents || []) { if (e.cat !== 'blink.webdx_feature_usage' || !e.args?.feature) { continue; } const event = /** @type {DXFeatureEvent} */ (e); const feature = /** @type {string} */ (event.args.feature); if (featuresMap.has(feature)) continue; /** @type {LH.Audit.Details.SourceLocationValue | undefined} */ let source; if (event.args.url) { const line = event.args.lineNumber ? event.args.lineNumber - 1 : 0; const column = event.args.columnNumber ? event.args.columnNumber - 1 : 0; source = Audit.makeSourceLocation(event.args.url, line, column); } featuresMap.set(feature, { featureId: feature, source, }); } const baselineFeatures = Array.from(featuresMap.values()); const baselineStatus = []; const currentDate = new Date; for (const feature of baselineFeatures) { if (!feature.featureId) { continue; } const featureId = feature.featureId; const status = Baseline.getFeatureStatus(featureId, Baseline.featureData, currentDate); if (!status) continue; const {displayStatus, baselineTier} = status; const lowDate = Baseline.getLowDate(featureId, Baseline.featureData) || ''; baselineStatus.push({ featureId: { type: /** @type {const} */ ('link'), text: feature.featureId, url: `https://webstatus.dev/features/${feature.featureId}`, }, displayStatus: { type: /** @type {const} */ ('baseline-status'), status: baselineTier, displayString: displayStatus, }, source: feature.source, lowDate, }); } /** @type {LH.Audit.Details.Table['headings']} */ const webFeatureHeadings = [ { key: 'featureId', valueType: 'link', label: str_(UIStrings.columnFeature), }, { key: 'displayStatus', valueType: 'baseline-status', label: str_(UIStrings.columnStatus), }, { key: 'source', valueType: 'source-location', label: str_(i18n.UIStrings.columnSource), }, ]; /** @type {Record<string, number>} */ const TIER_RANKS = { limited: 1, low: 2, high: 3, }; const sortedStatuses = baselineStatus.sort((featureA, featureB) => { const rankA = TIER_RANKS[featureA.displayStatus.status] || 4; const rankB = TIER_RANKS[featureB.displayStatus.status] || 4; if (rankA !== rankB) { return rankA - rankB; } return featureB.lowDate.localeCompare(featureA.lowDate); }); const hasLimited = baselineStatus.some(item => item.displayStatus.status === 'limited'); /** @type {LH.Audit.Details.DebugData | undefined} */ let debugData; if (!hasLimited && sortedStatuses.length > 0) { const newestFeature = sortedStatuses[0]; const featureName = newestFeature.featureId.text; const lowDate = newestFeature.lowDate; if (lowDate) { const year = lowDate.substring(0, 4); debugData = { type: 'debugdata', newestFeatureId: featureName, newestFeatureYear: year, newestFeatureLowDate: lowDate, }; } } // Remove `lowDate` property from items before generating details table. const tableItems = sortedStatuses.map(item => { const {lowDate: _, ...rest} = item; return rest; }); const details = Audit.makeTableDetails(webFeatureHeadings, tableItems); if (debugData) { details.debugData = debugData; } return { score: 1, details, }; } } export default Baseline; export {UIStrings};