UNPKG

lighthouse

Version:

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

188 lines (161 loc) 5.84 kB
/** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import LinkHeader from 'http-link-header'; import BaseGatherer from '../base-gatherer.js'; import {pageFunctions} from '../../lib/page-functions.js'; import DevtoolsLog from './devtools-log.js'; import {MainResource} from '../../computed/main-resource.js'; import {Util} from '../../../shared/util.js'; import * as i18n from '../../lib/i18n/i18n.js'; /* globals getNodeDetails */ /** * @fileoverview * This gatherer collects all the effect `link` elements, both in the page and declared in the * headers of the main resource. */ const UIStrings = { /** * @description Warning message explaining that there was an error parsing a link header in an HTTP response. `error` will be an english string with more details on the error. `header` will be the value of the header that caused the error. `link` is a type of HTTP header and should not be translated. * @example {Expected attribute delimiter at offset 94} error * @example {<https://assets.calendly.com/assets/booking/css/booking-d0ac32b1.css>; rel=preload; as=style; nopush} error */ headerParseWarning: 'Error parsing `link` header ({error}): `{header}`', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); /** * * @param {string} url * @param {string} finalDisplayedUrl * @return {string|null} */ function normalizeUrlOrNull(url, finalDisplayedUrl) { try { return new URL(url, finalDisplayedUrl).href; } catch (_) { return null; } } /** * @param {string|undefined} value * @return {LH.Artifacts.LinkElement['crossOrigin']} */ function getCrossoriginFromHeader(value) { if (value === 'anonymous') return 'anonymous'; if (value === 'use-credentials') return 'use-credentials'; return null; } /** * @return {LH.Artifacts['LinkElements']} */ /* c8 ignore start */ function getLinkElementsInDOM() { /** @type {Array<HTMLOrSVGElement>} */ // @ts-expect-error - getElementsInDocument put into scope via stringification const browserElements = getElementsInDocument('link'); // eslint-disable-line no-undef /** @type {LH.Artifacts['LinkElements']} */ const linkElements = []; for (const link of browserElements) { // We're only interested in actual LinkElements, not `<link>` tagName elements inside SVGs. // https://github.com/GoogleChrome/lighthouse/issues/9764 if (!(link instanceof HTMLLinkElement)) continue; const hrefRaw = link.getAttribute('href') || ''; const source = link.closest('head') ? 'head' : 'body'; linkElements.push({ rel: link.rel, href: link.href, hreflang: link.hreflang, as: link.as, crossOrigin: link.crossOrigin, hrefRaw, source, fetchPriority: link.fetchPriority, // @ts-expect-error - put into scope via stringification node: getNodeDetails(link), }); } return linkElements; } /* c8 ignore stop */ class LinkElements extends BaseGatherer { /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */ meta = { supportedModes: ['timespan', 'navigation'], dependencies: {DevtoolsLog: DevtoolsLog.symbol}, }; /** * @param {LH.Gatherer.Context} context * @return {Promise<LH.Artifacts['LinkElements']>} */ static getLinkElementsInDOM(context) { // We'll use evaluateAsync because the `node.getAttribute` method doesn't actually normalize // the values like access from JavaScript does. return context.driver.executionContext.evaluate(getLinkElementsInDOM, { args: [], useIsolation: true, deps: [ pageFunctions.getNodeDetails, pageFunctions.getElementsInDocument, ], }); } /** * @param {LH.Gatherer.Context} context * @param {LH.Artifacts['DevtoolsLog']} devtoolsLog * @return {Promise<LH.Artifacts['LinkElements']>} */ static async getLinkElementsInHeaders(context, devtoolsLog) { const mainDocument = await MainResource.request({devtoolsLog, URL: context.baseArtifacts.URL}, context); /** @type {LH.Artifacts['LinkElements']} */ const linkElements = []; for (const header of mainDocument.responseHeaders) { if (header.name.toLowerCase() !== 'link') continue; /** @type {LinkHeader.Reference[]} */ let parsedRefs = []; try { parsedRefs = LinkHeader.parse(header.value).refs; } catch (err) { const truncatedHeader = Util.truncate(header.value, 100); const warning = str_(UIStrings.headerParseWarning, { error: err.message, header: truncatedHeader, }); context.baseArtifacts.LighthouseRunWarnings.push(warning); } for (const link of parsedRefs) { linkElements.push({ rel: link.rel || '', href: normalizeUrlOrNull(link.uri, context.baseArtifacts.URL.finalDisplayedUrl), hrefRaw: link.uri || '', hreflang: link.hreflang || '', as: link.as || '', crossOrigin: getCrossoriginFromHeader(link.crossorigin), source: 'headers', fetchPriority: link.fetchpriority, node: null, }); } } return linkElements; } /** * @param {LH.Gatherer.Context<'DevtoolsLog'>} context * @return {Promise<LH.Artifacts['LinkElements']>} */ async getArtifact(context) { const devtoolsLog = context.dependencies.DevtoolsLog; const fromDOM = await LinkElements.getLinkElementsInDOM(context); const fromHeaders = await LinkElements.getLinkElementsInHeaders(context, devtoolsLog); const linkElements = fromDOM.concat(fromHeaders); for (const link of linkElements) { // Normalize the rel for easy consumption/filtering link.rel = link.rel.toLowerCase(); } return linkElements; } } export default LinkElements; export {UIStrings};