lighthouse 13.5.0 → 13.6.0-dev.20261027

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/.agents/skills/update-ard-port/SKILL.md +171 -0
  2. package/.agents/skills/update-trace-engine/SKILL.md +154 -0
  3. package/cli/bin.d.ts +2 -2
  4. package/cli/bin.js +5 -4
  5. package/cli/cli-flags.d.ts +16 -1
  6. package/cli/cli-flags.js +53 -2
  7. package/cli/printer.js +5 -8
  8. package/cli/run.d.ts +5 -5
  9. package/cli/run.js +97 -47
  10. package/cli/sentry-prompt.js +3 -4
  11. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +1 -1
  12. package/cli/test/smokehouse/config/exclusions.js +3 -1
  13. package/cli/test/smokehouse/frontends/smokehouse-bin.js +5 -3
  14. package/cli/test/smokehouse/report-assert-test.js +65 -0
  15. package/cli/test/smokehouse/report-assert.js +24 -39
  16. package/cli/test/smokehouse/smokehouse-test.d.ts +2 -0
  17. package/cli/test/smokehouse/smokehouse-test.js +120 -0
  18. package/cli/test/smokehouse/smokehouse.js +37 -15
  19. package/cli/test/smokehouse/version-check-test.js +5 -0
  20. package/cli/test/smokehouse/version-check.d.ts +1 -1
  21. package/cli/test/smokehouse/version-check.js +1 -1
  22. package/commitlint.config.js +1 -1
  23. package/core/audits/accessibility/accesskeys.js +1 -1
  24. package/core/audits/accessibility/aria-allowed-attr.js +1 -1
  25. package/core/audits/accessibility/aria-allowed-role.js +1 -1
  26. package/core/audits/accessibility/aria-command-name.js +1 -1
  27. package/core/audits/accessibility/aria-conditional-attr.js +1 -1
  28. package/core/audits/accessibility/aria-deprecated-role.js +1 -1
  29. package/core/audits/accessibility/aria-dialog-name.js +1 -1
  30. package/core/audits/accessibility/aria-hidden-body.js +1 -1
  31. package/core/audits/accessibility/aria-hidden-focus.js +1 -1
  32. package/core/audits/accessibility/aria-input-field-name.js +1 -1
  33. package/core/audits/accessibility/aria-meter-name.js +1 -1
  34. package/core/audits/accessibility/aria-progressbar-name.js +1 -1
  35. package/core/audits/accessibility/aria-prohibited-attr.js +1 -1
  36. package/core/audits/accessibility/aria-required-attr.js +1 -1
  37. package/core/audits/accessibility/aria-required-children.js +1 -1
  38. package/core/audits/accessibility/aria-required-parent.js +1 -1
  39. package/core/audits/accessibility/aria-roles.js +1 -1
  40. package/core/audits/accessibility/aria-text.js +1 -1
  41. package/core/audits/accessibility/aria-toggle-field-name.js +1 -1
  42. package/core/audits/accessibility/aria-tooltip-name.js +1 -1
  43. package/core/audits/accessibility/aria-treeitem-name.js +1 -1
  44. package/core/audits/accessibility/aria-valid-attr-value.js +1 -1
  45. package/core/audits/accessibility/aria-valid-attr.js +1 -1
  46. package/core/audits/accessibility/autocomplete-valid.js +1 -1
  47. package/core/audits/accessibility/button-name.js +1 -1
  48. package/core/audits/accessibility/bypass.js +1 -1
  49. package/core/audits/accessibility/color-contrast.js +1 -1
  50. package/core/audits/accessibility/definition-list.js +1 -1
  51. package/core/audits/accessibility/dlitem.js +1 -1
  52. package/core/audits/accessibility/document-title.js +1 -1
  53. package/core/audits/accessibility/duplicate-id-aria.js +1 -1
  54. package/core/audits/accessibility/empty-heading.js +1 -1
  55. package/core/audits/accessibility/form-field-multiple-labels.js +1 -1
  56. package/core/audits/accessibility/frame-title.js +1 -1
  57. package/core/audits/accessibility/heading-order.js +1 -1
  58. package/core/audits/accessibility/html-has-lang.js +1 -1
  59. package/core/audits/accessibility/html-lang-valid.js +1 -1
  60. package/core/audits/accessibility/html-xml-lang-mismatch.js +1 -1
  61. package/core/audits/accessibility/identical-links-same-purpose.js +1 -1
  62. package/core/audits/accessibility/image-alt.js +1 -1
  63. package/core/audits/accessibility/image-redundant-alt.js +1 -1
  64. package/core/audits/accessibility/input-button-name.js +1 -1
  65. package/core/audits/accessibility/input-image-alt.js +1 -1
  66. package/core/audits/accessibility/label-content-name-mismatch.js +1 -1
  67. package/core/audits/accessibility/label.js +1 -1
  68. package/core/audits/accessibility/landmark-one-main.js +1 -1
  69. package/core/audits/accessibility/link-in-text-block.js +1 -1
  70. package/core/audits/accessibility/link-name.js +1 -1
  71. package/core/audits/accessibility/list.js +1 -1
  72. package/core/audits/accessibility/listitem.js +1 -1
  73. package/core/audits/accessibility/meta-refresh.js +1 -1
  74. package/core/audits/accessibility/meta-viewport.js +1 -1
  75. package/core/audits/accessibility/object-alt.js +1 -1
  76. package/core/audits/accessibility/presentation-role-conflict.js +1 -1
  77. package/core/audits/accessibility/select-name.js +1 -1
  78. package/core/audits/accessibility/skip-link.js +1 -1
  79. package/core/audits/accessibility/svg-img-alt.js +1 -1
  80. package/core/audits/accessibility/tabindex.js +1 -1
  81. package/core/audits/accessibility/table-duplicate-name.js +1 -1
  82. package/core/audits/accessibility/table-fake-caption.js +1 -1
  83. package/core/audits/accessibility/target-size.js +1 -1
  84. package/core/audits/accessibility/td-has-header.js +1 -1
  85. package/core/audits/accessibility/td-headers-attr.js +1 -1
  86. package/core/audits/accessibility/th-has-data-cells.js +1 -1
  87. package/core/audits/accessibility/valid-lang.js +1 -1
  88. package/core/audits/accessibility/video-caption.js +1 -1
  89. package/core/audits/agentic/ard-schema.d.ts +13 -0
  90. package/core/audits/agentic/ard-schema.js +81 -33
  91. package/core/audits/agentic/llms-txt.js +5 -2
  92. package/core/audits/baseline.d.ts +29 -7
  93. package/core/audits/baseline.js +19 -6
  94. package/core/audits/byte-efficiency/byte-efficiency-audit.js +2 -2
  95. package/core/audits/insights/insight-audit.d.ts +9 -0
  96. package/core/audits/insights/insight-audit.js +35 -28
  97. package/core/audits/long-tasks.js +2 -1
  98. package/core/audits/predictive-perf.js +2 -2
  99. package/core/audits/screenshot-thumbnails.d.ts +0 -13
  100. package/core/audits/screenshot-thumbnails.js +0 -45
  101. package/core/audits/script-treemap-data.js +26 -6
  102. package/core/audits/seo/robots-txt.js +3 -8
  103. package/core/audits/unsized-images.d.ts +0 -7
  104. package/core/audits/unsized-images.js +7 -26
  105. package/core/computed/lcp-image-record.d.ts +2 -23
  106. package/core/computed/lcp-image-record.js +3 -67
  107. package/core/computed/lcp-network-record.d.ts +24 -0
  108. package/core/computed/lcp-network-record.js +74 -0
  109. package/core/computed/metrics/lantern-metric.js +1 -1
  110. package/core/computed/metrics/lcp-breakdown.js +6 -6
  111. package/core/computed/metrics/time-to-first-byte.js +5 -3
  112. package/core/computed/navigation-insights.js +3 -2
  113. package/core/computed/page-dependency-graph.js +55 -5
  114. package/core/computed/speedline.js +20 -5
  115. package/core/computed/trace-engine-result.js +25 -5
  116. package/core/computed/unused-css.js +15 -2
  117. package/core/config/config-helpers.d.ts +5 -0
  118. package/core/config/config-helpers.js +8 -28
  119. package/core/config/config.js +1 -2
  120. package/core/config/constants.d.ts +2 -28
  121. package/core/config/lr-desktop-config.js +13 -0
  122. package/core/config/lr-mobile-config.js +13 -0
  123. package/core/gather/base-artifacts.js +7 -1
  124. package/core/gather/driver/storage.js +3 -2
  125. package/core/gather/gatherers/agentic/ard.d.ts +59 -4
  126. package/core/gather/gatherers/agentic/ard.js +218 -56
  127. package/core/gather/gatherers/full-page-screenshot.js +2 -2
  128. package/core/gather/gatherers/image-elements.d.ts +7 -0
  129. package/core/gather/gatherers/image-elements.js +75 -24
  130. package/core/gather/gatherers/inspector-issues.js +2 -0
  131. package/core/gather/gatherers/js-usage.js +2 -0
  132. package/core/gather/gatherers/scripts.d.ts +16 -3
  133. package/core/gather/gatherers/scripts.js +157 -14
  134. package/core/gather/gatherers/stylesheets.js +2 -0
  135. package/core/gather/gatherers/trace-elements.js +2 -1
  136. package/core/gather/gatherers/trace.d.ts +6 -0
  137. package/core/gather/gatherers/trace.js +56 -16
  138. package/core/lib/arbitrary-equality-map.d.ts +14 -3
  139. package/core/lib/arbitrary-equality-map.js +113 -6
  140. package/core/lib/baseline/web-features-data.json +32 -11
  141. package/core/lib/baseline/web-features-metadata.json +1 -1
  142. package/core/lib/bf-cache-strings.js +237 -254
  143. package/core/lib/cdt/Common.d.ts +1 -1
  144. package/core/lib/cdt/Common.js +2 -2
  145. package/core/lib/cdt/Platform.d.ts +7 -7
  146. package/core/lib/cdt/Platform.js +11 -8
  147. package/core/lib/cdt/SDK.d.ts +5 -1
  148. package/core/lib/cdt/SDK.js +4 -2
  149. package/core/lib/cdt/generated/ParsedURL.d.ts +0 -1
  150. package/core/lib/cdt/generated/ParsedURL.js +3 -9
  151. package/core/lib/cdt/generated/SourceMap.d.ts +24 -25
  152. package/core/lib/cdt/generated/SourceMap.js +7 -15
  153. package/core/lib/deprecations-strings.d.ts +93 -88
  154. package/core/lib/deprecations-strings.js +20 -14
  155. package/core/lib/i18n/i18n.d.ts +0 -1
  156. package/core/lib/i18n/i18n.js +17 -11
  157. package/core/lib/legacy-javascript/legacy-javascript.d.ts +3 -2
  158. package/core/lib/legacy-javascript/legacy-javascript.js +235 -111
  159. package/core/lib/legacy-javascript/polyfill-graph-data.json +94 -89
  160. package/core/lib/legacy-javascript/polyfill-module-data.json +48 -7
  161. package/core/lib/minification-estimator.js +10 -10
  162. package/core/lib/network-request.d.ts +2 -0
  163. package/core/lib/network-request.js +21 -0
  164. package/core/lib/page-functions.js +2 -5
  165. package/core/lib/sentry.d.ts +1 -0
  166. package/core/lib/sentry.js +3 -0
  167. package/core/lib/trace-engine.d.ts +1 -0
  168. package/core/lib/trace-engine.js +2 -0
  169. package/core/runner.js +9 -3
  170. package/dist/report/bundle.esm.js +10 -10
  171. package/dist/report/flow.js +13 -13
  172. package/dist/report/standalone.js +5 -5
  173. package/flow-report/src/common.tsx +5 -2
  174. package/flow-report/src/i18n/i18n.d.ts +2 -0
  175. package/flow-report/src/summary/summary.tsx +9 -1
  176. package/flow-report/src/util.d.ts +1 -1
  177. package/package.json +62 -98
  178. package/readme.md +13 -18
  179. package/report/generator/file-namer.d.ts +1 -1
  180. package/report/generator/file-namer.js +5 -4
  181. package/report/generator/flow-report-assets.js +1 -3
  182. package/report/generator/report-assets.js +1 -2
  183. package/report/renderer/category-renderer.js +2 -2
  184. package/report/renderer/details-renderer.js +3 -2
  185. package/report/renderer/dom.d.ts +8 -2
  186. package/report/renderer/dom.js +29 -1
  187. package/report/renderer/element-screenshot-renderer.js +3 -0
  188. package/report/renderer/performance-category-renderer.js +3 -1
  189. package/report/renderer/report-renderer.js +4 -0
  190. package/report/renderer/report-utils.d.ts +1 -0
  191. package/report/renderer/report-utils.js +2 -0
  192. package/report/renderer/text-encoding.js +2 -2
  193. package/report/types/augment-dom.d.ts +4 -0
  194. package/shared/esm-utils.d.ts +0 -8
  195. package/shared/esm-utils.js +0 -18
  196. package/shared/localization/format.js +1 -2
  197. package/shared/localization/locales/ar-XB.json +157 -142
  198. package/shared/localization/locales/ar.json +157 -142
  199. package/shared/localization/locales/bg.json +154 -139
  200. package/shared/localization/locales/ca.json +157 -142
  201. package/shared/localization/locales/cs.json +148 -133
  202. package/shared/localization/locales/da.json +153 -138
  203. package/shared/localization/locales/de.json +152 -137
  204. package/shared/localization/locales/el.json +153 -138
  205. package/shared/localization/locales/en-GB.json +160 -145
  206. package/shared/localization/locales/en-US.json +239 -218
  207. package/shared/localization/locales/en-XL.json +239 -218
  208. package/shared/localization/locales/es-419.json +150 -135
  209. package/shared/localization/locales/es.json +149 -134
  210. package/shared/localization/locales/fi.json +151 -136
  211. package/shared/localization/locales/fil.json +154 -139
  212. package/shared/localization/locales/fr.json +150 -135
  213. package/shared/localization/locales/he.json +160 -145
  214. package/shared/localization/locales/hi.json +156 -141
  215. package/shared/localization/locales/hr.json +154 -139
  216. package/shared/localization/locales/hu.json +111 -96
  217. package/shared/localization/locales/id.json +153 -138
  218. package/shared/localization/locales/it.json +153 -138
  219. package/shared/localization/locales/ja.json +150 -135
  220. package/shared/localization/locales/ko.json +42 -27
  221. package/shared/localization/locales/lt.json +152 -137
  222. package/shared/localization/locales/lv.json +93 -78
  223. package/shared/localization/locales/nl.json +149 -134
  224. package/shared/localization/locales/no.json +152 -137
  225. package/shared/localization/locales/pl.json +153 -138
  226. package/shared/localization/locales/pt-PT.json +152 -137
  227. package/shared/localization/locales/pt.json +157 -142
  228. package/shared/localization/locales/ro.json +149 -134
  229. package/shared/localization/locales/ru.json +76 -61
  230. package/shared/localization/locales/sk.json +153 -138
  231. package/shared/localization/locales/sl.json +149 -134
  232. package/shared/localization/locales/sr-Latn.json +151 -136
  233. package/shared/localization/locales/sr.json +151 -136
  234. package/shared/localization/locales/sv.json +152 -137
  235. package/shared/localization/locales/ta.json +156 -141
  236. package/shared/localization/locales/te.json +157 -142
  237. package/shared/localization/locales/th.json +111 -96
  238. package/shared/localization/locales/tr.json +153 -138
  239. package/shared/localization/locales/uk.json +149 -134
  240. package/shared/localization/locales/vi.json +162 -147
  241. package/shared/localization/locales/zh-HK.json +150 -135
  242. package/shared/localization/locales/zh-TW.json +154 -139
  243. package/shared/localization/locales/zh.json +152 -137
  244. package/shared/localization/swap-locale.js +10 -5
  245. package/shared/root.js +1 -3
  246. package/third-party/ard/README.md +19 -10
  247. package/third-party/ard/ard-test.js +189 -266
  248. package/third-party/ard/ard.d.ts +18 -1
  249. package/third-party/ard/ard.js +282 -45
  250. package/third-party/ard/schema-validator.d.ts +14 -5
  251. package/third-party/ard/schema-validator.js +921 -1177
  252. package/third-party/ard/spec/schemas/ard-entry.schema.json +176 -0
  253. package/tsconfig.json +0 -2
  254. package/types/artifacts.d.ts +43 -7
  255. package/types/externs.d.ts +4 -0
  256. package/types/internal/test.d.ts +1 -0
  257. package/build-tracker.config.js +0 -16
  258. package/core/lib/cdt/package.json +0 -4
  259. package/eslint-local-rules.d.cts +0 -9
  260. package/eslint.config.mjs +0 -242
  261. package/third-party/ard/spec/schemas/ai-catalog.schema.json +0 -292
  262. package/third-party/esbuild-plugins-polyfills/LICENSE +0 -3
  263. package/third-party/esbuild-plugins-polyfills/README.md +0 -7
  264. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.d.ts +0 -9
  265. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.js +0 -129
  266. package/types/internal/lookup-closest-locale.d.ts +0 -13
@@ -1,26 +1,81 @@
1
1
  export default AgentResourceDiscovery;
2
+ export type ArdLinks = {
3
+ ard: string | null;
4
+ legacy: string | null;
5
+ };
6
+ export type DiscoverySignals = LH.Artifacts["AgentResourceDiscovery"]["discoverySignals"];
7
+ export type FetchResult = {
8
+ status: number | null;
9
+ content: string | null;
10
+ headers: Record<string, string> | null;
11
+ errorMessage?: string | undefined;
12
+ };
13
+ /**
14
+ * A location to try when resolving the manifest. `advertised` locations are ones the site
15
+ * points at (robots.txt, link tags, Link headers); the well-known paths are probed by
16
+ * Lighthouse, so a failure there is not the site's fault and is not reported.
17
+ */
18
+ export type ManifestCandidate = {
19
+ source: LH.Artifacts.ArdDiscoverySource;
20
+ url: string | null;
21
+ advertised: boolean;
22
+ };
2
23
  declare class AgentResourceDiscovery extends BaseGatherer {
3
24
  /**
25
+ * Returns the absolute URL from the first `Agentmap:` directive in robots.txt, if any.
4
26
  * @param {LH.Artifacts['RobotsTxt']|null|undefined} robotsTxt
5
27
  * @param {string} finalDisplayedUrl
6
28
  * @return {string|null}
7
29
  */
8
30
  static getRobotsTxtAgentmap(robotsTxt: LH.Artifacts["RobotsTxt"] | null | undefined, finalDisplayedUrl: string): string | null;
9
31
  /**
32
+ * Finds `<link rel="ard">` and `<link rel="ai-catalog">` in the page.
10
33
  * @param {LH.Gatherer.Context} context
11
34
  * @param {string} finalDisplayedUrl
12
- * @return {Promise<string|null>}
35
+ * @return {Promise<ArdLinks>}
13
36
  */
14
- static getHtmlLinkFromDom(context: LH.Gatherer.Context, finalDisplayedUrl: string): Promise<string | null>;
37
+ static getHtmlLinksFromDom(context: LH.Gatherer.Context, finalDisplayedUrl: string): Promise<ArdLinks>;
15
38
  /**
39
+ * Finds `rel="ard"` and `rel="ai-catalog"` in the main document's HTTP `Link` header.
16
40
  * @param {LH.Gatherer.Context} context
17
41
  * @param {string} finalDisplayedUrl
18
- * @return {Promise<string|null>}
42
+ * @return {Promise<ArdLinks>}
19
43
  */
20
- static getHttpHeaderLink(context: LH.Gatherer.Context, finalDisplayedUrl: string): Promise<string | null>;
44
+ static getHttpHeaderLinks(context: LH.Gatherer.Context, finalDisplayedUrl: string): Promise<ArdLinks>;
45
+ /**
46
+ * Fetches a candidate manifest URL. Never throws; network errors are returned as a
47
+ * null status with an `errorMessage`.
48
+ * @param {LH.Gatherer.Context} context
49
+ * @param {string} url
50
+ * @return {Promise<FetchResult>}
51
+ */
52
+ static fetchManifest(context: LH.Gatherer.Context, url: string): Promise<FetchResult>;
53
+ /**
54
+ * Tries each candidate in order and returns the first that loads. Advertised candidates
55
+ * that fail are recorded in `failedSources` and skipped. If nothing loads, the result
56
+ * describes the first advertised location that failed, or else the `/.well-known/ard.json`
57
+ * path consumers are required to fetch.
58
+ * @param {LH.Gatherer.Context} context
59
+ * @param {ManifestCandidate[]} candidates
60
+ * @return {Promise<{
61
+ * discoverySource: LH.Artifacts.ArdDiscoverySource,
62
+ * catalogUrl: string,
63
+ * fetchResult: FetchResult,
64
+ * failedSources: LH.Artifacts['AgentResourceDiscovery']['failedSources'],
65
+ * }>}
66
+ */
67
+ static resolveManifest(context: LH.Gatherer.Context, candidates: ManifestCandidate[]): Promise<{
68
+ discoverySource: LH.Artifacts.ArdDiscoverySource;
69
+ catalogUrl: string;
70
+ fetchResult: FetchResult;
71
+ failedSources: LH.Artifacts["AgentResourceDiscovery"]["failedSources"];
72
+ }>;
21
73
  /** @type {LH.Gatherer.GathererMeta<'RobotsTxt'>} */
22
74
  meta: LH.Gatherer.GathererMeta<"RobotsTxt">;
23
75
  /**
76
+ * Resolves the manifest following ARD §5.1: explicit ARD signals first, then the
77
+ * `/.well-known/ard.json` path consumers MUST fetch, and only then the predecessor
78
+ * `ai-catalog` relation and well-known path, which consumers MAY consult.
24
79
  * @param {LH.Gatherer.Context<'RobotsTxt'>} context
25
80
  * @return {Promise<LH.Artifacts['AgentResourceDiscovery']>}
26
81
  */
@@ -4,17 +4,112 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ /**
8
+ * @fileoverview Gatherer that discovers and fetches a site's Agentic Resource Discovery (ARD)
9
+ * manifest. It follows the discovery mechanisms in ARD §5.1 and also consults the predecessor
10
+ * `ai-catalog` names so that sites that have not yet migrated are still audited.
11
+ * @see https://agenticresourcediscovery.org/spec/
12
+ */
13
+
7
14
  import log from 'lighthouse-logger';
8
15
  import LinkHeader from 'http-link-header';
9
16
 
10
17
  import BaseGatherer from '../../base-gatherer.js';
11
18
  import RobotsTxt from '../seo/robots-txt.js';
12
- import {pageFunctions} from '../../../lib/page-functions.js';
13
- function getAiCatalogLinkInDOM() {
14
- const link = document.querySelector('link[rel~="ai-catalog" i]');
15
- return link instanceof HTMLLinkElement ? link.href : null;
19
+
20
+ /**
21
+ * Link relations for ARD entry sources. `ai-catalog` is the predecessor relation, which
22
+ * consumers MAY consult but are not required to (ARD §5.1).
23
+ */
24
+ const REL_ARD = 'ard';
25
+ const REL_LEGACY = 'ai-catalog';
26
+
27
+ const WELL_KNOWN_PATH = '/.well-known/ard.json';
28
+ const LEGACY_WELL_KNOWN_PATH = '/.well-known/ai-catalog.json';
29
+
30
+ const HTTP_OK = 200;
31
+
32
+ /** @typedef {{ard: string|null, legacy: string|null}} ArdLinks */
33
+
34
+ /** @typedef {LH.Artifacts['AgentResourceDiscovery']['discoverySignals']} DiscoverySignals */
35
+
36
+ /**
37
+ * @typedef FetchResult
38
+ * @property {number|null} status
39
+ * @property {string|null} content
40
+ * @property {Record<string, string>|null} headers
41
+ * @property {string} [errorMessage]
42
+ */
43
+
44
+ /**
45
+ * A location to try when resolving the manifest. `advertised` locations are ones the site
46
+ * points at (robots.txt, link tags, Link headers); the well-known paths are probed by
47
+ * Lighthouse, so a failure there is not the site's fault and is not reported.
48
+ * @typedef ManifestCandidate
49
+ * @property {LH.Artifacts.ArdDiscoverySource} source
50
+ * @property {string|null} url
51
+ * @property {boolean} advertised
52
+ */
53
+
54
+ /** @type {ReadonlyArray<LH.Artifacts.ArdDiscoverySource>} */
55
+ const SOURCE_PRIORITY = [
56
+ 'robotsTxtAgentmap',
57
+ 'htmlLink',
58
+ 'httpHeaderLink',
59
+ 'wellKnown',
60
+ 'legacyHtmlLink',
61
+ 'legacyHttpHeaderLink',
62
+ 'legacyWellKnown',
63
+ ];
64
+
65
+ /** @type {Set<LH.Artifacts.ArdDiscoverySource>} */
66
+ const PROBED_SOURCES = new Set(['wellKnown', 'legacyWellKnown']);
67
+
68
+ /**
69
+ * @param {DiscoverySignals} discoverySignals
70
+ * @return {ManifestCandidate[]}
71
+ */
72
+ function getManifestCandidates(discoverySignals) {
73
+ return SOURCE_PRIORITY.map(source => ({
74
+ source,
75
+ url: discoverySignals[source],
76
+ advertised: !PROBED_SOURCES.has(source),
77
+ }));
78
+ }
79
+
80
+ /**
81
+ * Resolves `href` against `baseUrl`, returning null for empty or unparseable values.
82
+ * @param {string|null|undefined} href
83
+ * @param {string} baseUrl
84
+ * @return {string|null}
85
+ */
86
+ function resolveUrl(href, baseUrl) {
87
+ if (!href) return null;
88
+ try {
89
+ return new URL(href, baseUrl).href;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Runs in the page. Returns the `href` of the first `<link>` for each relation, or null.
97
+ * @param {string} relArd
98
+ * @param {string} relLegacy
99
+ * @return {ArdLinks}
100
+ */
101
+ function getArdLinksInDOM(relArd, relLegacy) {
102
+ /** @param {string} rel */
103
+ const getHref = rel => {
104
+ const link = document.querySelector(`link[rel~="${rel}" i]`);
105
+ return link instanceof HTMLLinkElement ? link.href : null;
106
+ };
107
+ return {ard: getHref(relArd), legacy: getHref(relLegacy)};
16
108
  }
17
109
 
110
+ /** @type {ArdLinks} */
111
+ const NO_LINKS = {ard: null, legacy: null};
112
+
18
113
  class AgentResourceDiscovery extends BaseGatherer {
19
114
  /** @type {LH.Gatherer.GathererMeta<'RobotsTxt'>} */
20
115
  meta = {
@@ -23,6 +118,7 @@ class AgentResourceDiscovery extends BaseGatherer {
23
118
  };
24
119
 
25
120
  /**
121
+ * Returns the absolute URL from the first `Agentmap:` directive in robots.txt, if any.
26
122
  * @param {LH.Artifacts['RobotsTxt']|null|undefined} robotsTxt
27
123
  * @param {string} finalDisplayedUrl
28
124
  * @return {string|null}
@@ -31,93 +127,159 @@ class AgentResourceDiscovery extends BaseGatherer {
31
127
  if (!robotsTxt?.content) return null;
32
128
  const match = robotsTxt.content.match(/^\s*Agentmap:\s*(\S+)/im);
33
129
  if (!match) return null;
34
- try {
35
- return new URL(match[1], finalDisplayedUrl).href;
36
- } catch {
37
- return null;
38
- }
130
+ return resolveUrl(match[1], finalDisplayedUrl);
39
131
  }
40
132
 
41
133
  /**
134
+ * Finds `<link rel="ard">` and `<link rel="ai-catalog">` in the page.
42
135
  * @param {LH.Gatherer.Context} context
43
136
  * @param {string} finalDisplayedUrl
44
- * @return {Promise<string|null>}
137
+ * @return {Promise<ArdLinks>}
45
138
  */
46
- static async getHtmlLinkFromDom(context, finalDisplayedUrl) {
139
+ static async getHtmlLinksFromDom(context, finalDisplayedUrl) {
47
140
  try {
48
- const href = await context.driver.executionContext.evaluate(getAiCatalogLinkInDOM, {
49
- args: [],
141
+ const links = await context.driver.executionContext.evaluate(getArdLinksInDOM, {
142
+ args: [REL_ARD, REL_LEGACY],
50
143
  useIsolation: true,
51
- deps: [pageFunctions.getNodeDetails],
52
144
  });
53
- if (!href) return null;
54
- return new URL(href, finalDisplayedUrl).href;
55
- } catch {
56
- return null;
145
+ return {
146
+ ard: resolveUrl(links?.ard, finalDisplayedUrl),
147
+ legacy: resolveUrl(links?.legacy, finalDisplayedUrl),
148
+ };
149
+ } catch (err) {
150
+ log.verbose('AgentResourceDiscovery', `Could not read link tags: ${err.message}`);
151
+ return NO_LINKS;
57
152
  }
58
153
  }
59
154
 
60
155
  /**
156
+ * Finds `rel="ard"` and `rel="ai-catalog"` in the main document's HTTP `Link` header.
61
157
  * @param {LH.Gatherer.Context} context
62
158
  * @param {string} finalDisplayedUrl
63
- * @return {Promise<string|null>}
159
+ * @return {Promise<ArdLinks>}
64
160
  */
65
- static async getHttpHeaderLink(context, finalDisplayedUrl) {
66
- // Only check in navigation mode
67
- if (context.gatherMode !== 'navigation') return null;
161
+ static async getHttpHeaderLinks(context, finalDisplayedUrl) {
162
+ if (context.gatherMode !== 'navigation') return NO_LINKS;
68
163
 
69
164
  try {
70
165
  const mainResourceResponse = await context.driver.fetcher.fetchResource(finalDisplayedUrl);
71
- const linkHeader = mainResourceResponse.headers?.['link'];
72
- if (!linkHeader) return null;
166
+ const linkHeader = mainResourceResponse.headers?.link;
167
+ if (!linkHeader) return NO_LINKS;
168
+
169
+ const parsed = LinkHeader.parse(linkHeader.replace(/\n/g, ','));
170
+ /** @param {string} rel */
171
+ const getUri = rel =>
172
+ resolveUrl(parsed.refs.find(ref => ref.rel?.toLowerCase() === rel)?.uri, finalDisplayedUrl);
173
+ return {ard: getUri(REL_ARD), legacy: getUri(REL_LEGACY)};
174
+ } catch (err) {
175
+ log.verbose('AgentResourceDiscovery', `Could not read Link header: ${err.message}`);
176
+ return NO_LINKS;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Fetches a candidate manifest URL. Never throws; network errors are returned as a
182
+ * null status with an `errorMessage`.
183
+ * @param {LH.Gatherer.Context} context
184
+ * @param {string} url
185
+ * @return {Promise<FetchResult>}
186
+ */
187
+ static async fetchManifest(context, url) {
188
+ try {
189
+ const {status, content, headers} = await context.driver.fetcher.fetchResource(url);
190
+ return {status, content, headers: headers || null};
191
+ } catch (err) {
192
+ log.error('AgentResourceDiscovery', err);
193
+ return {status: null, content: null, headers: null, errorMessage: err.message};
194
+ }
195
+ }
73
196
 
74
- const parsed = LinkHeader.parse(linkHeader);
75
- const aiCatalogRef = parsed.get('rel', 'ai-catalog')[0];
76
- if (!aiCatalogRef?.uri) return null;
197
+ /**
198
+ * Tries each candidate in order and returns the first that loads. Advertised candidates
199
+ * that fail are recorded in `failedSources` and skipped. If nothing loads, the result
200
+ * describes the first advertised location that failed, or else the `/.well-known/ard.json`
201
+ * path consumers are required to fetch.
202
+ * @param {LH.Gatherer.Context} context
203
+ * @param {ManifestCandidate[]} candidates
204
+ * @return {Promise<{
205
+ * discoverySource: LH.Artifacts.ArdDiscoverySource,
206
+ * catalogUrl: string,
207
+ * fetchResult: FetchResult,
208
+ * failedSources: LH.Artifacts['AgentResourceDiscovery']['failedSources'],
209
+ * }>}
210
+ */
211
+ static async resolveManifest(context, candidates) {
212
+ /** @type {Array<{source: LH.Artifacts.ArdDiscoverySource, url: string, advertised: boolean, fetchResult: FetchResult}>} */
213
+ const attempts = [];
214
+ const failedSources = () => attempts
215
+ .filter(attempt => attempt.advertised)
216
+ .map(({source, url, fetchResult}) => ({
217
+ source,
218
+ url,
219
+ status: fetchResult.status,
220
+ errorMessage: fetchResult.errorMessage,
221
+ }));
222
+
223
+ for (const {source, url, advertised} of candidates) {
224
+ if (!url || attempts.some(attempt => attempt.url === url)) continue;
77
225
 
78
- return new URL(aiCatalogRef.uri, finalDisplayedUrl).href;
79
- } catch {
80
- return null;
226
+ const fetchResult = await AgentResourceDiscovery.fetchManifest(context, url);
227
+ if (fetchResult.status === HTTP_OK && fetchResult.content) {
228
+ return {discoverySource: source, catalogUrl: url, fetchResult,
229
+ failedSources: failedSources()};
230
+ }
231
+ attempts.push({source, url, advertised, fetchResult});
81
232
  }
233
+
234
+ const fallback = attempts.find(attempt => attempt.advertised) ||
235
+ attempts.find(attempt => attempt.source === 'wellKnown');
236
+ if (!fallback) throw new Error('ARD discovery attempted no candidates');
237
+
238
+ return {
239
+ discoverySource: fallback.source,
240
+ catalogUrl: fallback.url,
241
+ fetchResult: fallback.fetchResult,
242
+ failedSources: failedSources(),
243
+ };
82
244
  }
83
245
 
84
246
  /**
247
+ * Resolves the manifest following ARD §5.1: explicit ARD signals first, then the
248
+ * `/.well-known/ard.json` path consumers MUST fetch, and only then the predecessor
249
+ * `ai-catalog` relation and well-known path, which consumers MAY consult.
85
250
  * @param {LH.Gatherer.Context<'RobotsTxt'>} context
86
251
  * @return {Promise<LH.Artifacts['AgentResourceDiscovery']>}
87
252
  */
88
253
  async getArtifact(context) {
89
254
  const {finalDisplayedUrl} = context.baseArtifacts.URL;
90
- const robotsTxt = context.dependencies.RobotsTxt;
91
255
  const robotsTxtAgentmap = AgentResourceDiscovery.getRobotsTxtAgentmap(
92
- robotsTxt, finalDisplayedUrl);
93
- const [htmlLink, httpHeaderLink] = await Promise.all([
94
- AgentResourceDiscovery.getHtmlLinkFromDom(context, finalDisplayedUrl),
95
- AgentResourceDiscovery.getHttpHeaderLink(context, finalDisplayedUrl),
256
+ context.dependencies.RobotsTxt, finalDisplayedUrl);
257
+ const [htmlLinks, httpHeaderLinks] = await Promise.all([
258
+ AgentResourceDiscovery.getHtmlLinksFromDom(context, finalDisplayedUrl),
259
+ AgentResourceDiscovery.getHttpHeaderLinks(context, finalDisplayedUrl),
96
260
  ]);
97
- const wellKnown = new URL('/.well-known/ai-catalog.json', finalDisplayedUrl).href;
98
- const catalogUrl = robotsTxtAgentmap || htmlLink || httpHeaderLink || wellKnown;
99
-
100
- /** @type {string|undefined} */
101
- let errorMessage;
102
- const fetchResult = await context.driver.fetcher.fetchResource(catalogUrl)
103
- .catch(err => {
104
- log.error('AgentResourceDiscovery', err);
105
- errorMessage = err.message;
106
- return {status: null, content: null, headers: null};
107
- });
261
+
262
+ /** @type {DiscoverySignals} */
263
+ const discoverySignals = {
264
+ robotsTxtAgentmap,
265
+ htmlLink: htmlLinks.ard,
266
+ httpHeaderLink: httpHeaderLinks.ard,
267
+ legacyHtmlLink: htmlLinks.legacy,
268
+ legacyHttpHeaderLink: httpHeaderLinks.legacy,
269
+ wellKnown: new URL(WELL_KNOWN_PATH, finalDisplayedUrl).href,
270
+ legacyWellKnown: new URL(LEGACY_WELL_KNOWN_PATH, finalDisplayedUrl).href,
271
+ };
272
+
273
+ const candidates = getManifestCandidates(discoverySignals);
274
+ const {discoverySource, catalogUrl, fetchResult, failedSources} =
275
+ await AgentResourceDiscovery.resolveManifest(context, candidates);
108
276
 
109
277
  return {
110
- status: fetchResult.status,
111
- content: fetchResult.content,
112
- headers: fetchResult.headers || null,
278
+ ...fetchResult,
113
279
  catalogUrl,
114
- discoverySignals: {
115
- robotsTxtAgentmap,
116
- htmlLink,
117
- httpHeaderLink,
118
- wellKnown,
119
- },
120
- errorMessage,
280
+ discoverySource,
281
+ discoverySignals,
282
+ failedSources,
121
283
  };
122
284
  }
123
285
  }
@@ -95,7 +95,7 @@ class FullPageScreenshot extends BaseGatherer {
95
95
  mobile: deviceMetrics.mobile,
96
96
  deviceScaleFactor: 1,
97
97
  height,
98
- width: 0, // Leave width unchanged
98
+ width: deviceMetrics.width,
99
99
  });
100
100
 
101
101
  // Now that the viewport is taller, give the page some time to fetch new resources that
@@ -235,7 +235,7 @@ class FullPageScreenshot extends BaseGatherer {
235
235
  mobile: deviceMetrics.mobile,
236
236
  deviceScaleFactor: deviceMetrics.deviceScaleFactor,
237
237
  height: deviceMetrics.height,
238
- width: 0, // Leave width unchanged
238
+ width: deviceMetrics.width,
239
239
  });
240
240
  }
241
241
  }
@@ -1,5 +1,12 @@
1
1
  export default ImageElements;
2
2
  declare class ImageElements extends BaseGatherer {
3
+ /**
4
+ * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
5
+ * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
6
+ * @param {string | null} attrValue
7
+ * @return {boolean}
8
+ */
9
+ static doesHtmlAttrProvideExplicitSize(attrValue: string | null): boolean;
3
10
  /** @type {Map<string, {naturalWidth: number, naturalHeight: number}>} */
4
11
  _naturalSizeCache: Map<string, {
5
12
  naturalWidth: number;
@@ -238,6 +238,21 @@ function getEffectiveSizingRule({attributesStyle, inlineStyle, matchedCSSRules},
238
238
  return null;
239
239
  }
240
240
 
241
+ /**
242
+ * @param {LH.Artifacts.ImageElement} element
243
+ * @return {boolean}
244
+ */
245
+ function needsSourceRules(element) {
246
+ if (element.isInShadowDOM || element.isCss) return false;
247
+ if (
248
+ ImageElements.doesHtmlAttrProvideExplicitSize(element.attributeWidth) &&
249
+ ImageElements.doesHtmlAttrProvideExplicitSize(element.attributeHeight)
250
+ ) {
251
+ return false;
252
+ }
253
+ return true;
254
+ }
255
+
241
256
  /**
242
257
  * @param {LH.Artifacts.ImageElement} element
243
258
  * @return {number}
@@ -255,6 +270,28 @@ class ImageElements extends BaseGatherer {
255
270
  supportedModes: ['snapshot', 'timespan', 'navigation'],
256
271
  };
257
272
 
273
+ /**
274
+ * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
275
+ * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
276
+ * @param {string | null} attrValue
277
+ * @return {boolean}
278
+ */
279
+ static doesHtmlAttrProvideExplicitSize(attrValue) {
280
+ if (!attrValue) return false;
281
+
282
+ // First, superweird edge case of using the positive-sign. The spec _sorta_ says it's valid...
283
+ // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
284
+ // > Otherwise, if the character is … (+): Advance position to the next character.
285
+ // > (The "+" is ignored, but it is not conforming.)
286
+ // lol. Irrelevant though b/c Chrome (at least) doesn't ignore. It rejects this as a non-conforming value.
287
+ if (attrValue.startsWith('+')) return false;
288
+
289
+ // parseInt isn't exactly the same as the html's spec for parsing integers, but it's close enough
290
+ // https://tc39.es/ecma262/#sec-parseint-string-radix
291
+ const int = parseInt(attrValue, 10);
292
+ return int >= 0;
293
+ }
294
+
258
295
  constructor() {
259
296
  super();
260
297
  /** @type {Map<string, {naturalWidth: number, naturalHeight: number}>} */
@@ -322,24 +359,32 @@ class ImageElements extends BaseGatherer {
322
359
  async collectExtraDetails(driver, elements) {
323
360
  // Don't do more than 5s of this expensive devtools protocol work. See #11289
324
361
  let reachedGatheringBudget = false;
325
- setTimeout(_ => (reachedGatheringBudget = true), 5000);
362
+ const timeoutId = setTimeout(_ => (reachedGatheringBudget = true), 5000);
326
363
  let skippedCount = 0;
327
364
 
328
- for (const element of elements) {
329
- if (reachedGatheringBudget) {
330
- skippedCount++;
331
- continue;
332
- }
333
-
334
- // Need source rules to determine if sized via CSS (for unsized-images).
335
- if (!element.isInShadowDOM && !element.isCss) {
336
- await this.fetchSourceRules(driver.defaultSession, element.node.devtoolsNodePath, element);
337
- }
338
- // Images within `picture` behave strangely and natural size information isn't accurate,
339
- // CSS images have no natural size information at all. Try to get the actual size if we can.
340
- if (element.isPicture || element.isCss || element.srcset) {
341
- await this.fetchElementWithSizeInformation(driver, element);
365
+ try {
366
+ for (const element of elements) {
367
+ if (reachedGatheringBudget) {
368
+ skippedCount++;
369
+ continue;
370
+ }
371
+
372
+ // Need source rules to determine if sized via CSS (for unsized-images).
373
+ if (needsSourceRules(element)) {
374
+ await this.fetchSourceRules(
375
+ driver.defaultSession,
376
+ element.node.devtoolsNodePath,
377
+ element
378
+ );
379
+ }
380
+ // Images within `picture` behave strangely and natural size information isn't accurate,
381
+ // CSS images have no natural size information at all. Try to get the actual size if we can.
382
+ if (element.isPicture || element.isCss || element.srcset) {
383
+ await this.fetchElementWithSizeInformation(driver, element);
384
+ }
342
385
  }
386
+ } finally {
387
+ clearTimeout(timeoutId);
343
388
  }
344
389
 
345
390
  if (reachedGatheringBudget) {
@@ -369,11 +414,14 @@ class ImageElements extends BaseGatherer {
369
414
  ],
370
415
  });
371
416
 
372
- await Promise.all([
373
- session.sendCommand('DOM.enable'),
374
- session.sendCommand('CSS.enable'),
375
- session.sendCommand('DOM.getDocument', {depth: -1, pierce: true}),
376
- ]);
417
+ const shouldFetchSourceRules = elements.some(needsSourceRules);
418
+ if (shouldFetchSourceRules) {
419
+ await Promise.all([
420
+ session.sendCommand('DOM.enable'),
421
+ session.sendCommand('CSS.enable'),
422
+ session.sendCommand('DOM.getDocument', {depth: -1, pierce: true}),
423
+ ]);
424
+ }
377
425
 
378
426
  // Spend our extra details budget on highest impact images.
379
427
  // Our best approximation of impact without network records is to use pixel area.
@@ -381,11 +429,14 @@ class ImageElements extends BaseGatherer {
381
429
 
382
430
  await this.collectExtraDetails(context.driver, elements);
383
431
 
384
- await Promise.all([
385
- session.sendCommand('DOM.disable'),
386
- session.sendCommand('CSS.disable'),
387
- ]);
432
+ if (shouldFetchSourceRules) {
433
+ await Promise.all([
434
+ session.sendCommand('DOM.disable'),
435
+ session.sendCommand('CSS.disable'),
436
+ ]);
437
+ }
388
438
 
439
+ this._naturalSizeCache.clear();
389
440
  return elements;
390
441
  }
391
442
  }
@@ -96,6 +96,8 @@ class InspectorIssues extends BaseGatherer {
96
96
  }
97
97
  }
98
98
 
99
+ this._issues = [];
100
+
99
101
  return artifact;
100
102
  }
101
103
  }
@@ -67,6 +67,8 @@ class JsUsage extends BaseGatherer {
67
67
  usageByScriptId[scriptUsage.scriptId] = scriptUsage;
68
68
  }
69
69
 
70
+ this._scriptUsages = [];
71
+
70
72
  return usageByScriptId;
71
73
  }
72
74
  }
@@ -1,13 +1,22 @@
1
1
  export default Scripts;
2
+ export type TraceScript = {
3
+ scriptId: string;
4
+ sourceText?: string | undefined;
5
+ chunks?: string[] | undefined;
6
+ url?: string | undefined;
7
+ sourceUrl?: string | undefined;
8
+ executionContextId?: number | undefined;
9
+ frame?: string | undefined;
10
+ };
2
11
  /**
3
12
  * @fileoverview Gets JavaScript file contents.
4
13
  */
5
14
  declare class Scripts extends BaseGatherer {
6
15
  static symbol: symbol;
16
+ /** @type {LH.Gatherer.GathererMeta<'Trace'>} */
17
+ meta: LH.Gatherer.GathererMeta<"Trace">;
7
18
  /** @type {LH.Crdp.Debugger.ScriptParsedEvent[]} */
8
19
  _scriptParsedEvents: LH.Crdp.Debugger.ScriptParsedEvent[];
9
- /** @type {Array<string | undefined>} */
10
- _scriptContents: Array<string | undefined>;
11
20
  /**
12
21
  * @param {LH.Crdp.Debugger.ScriptParsedEvent} params
13
22
  */
@@ -20,7 +29,11 @@ declare class Scripts extends BaseGatherer {
20
29
  * @param {LH.Gatherer.Context} context
21
30
  */
22
31
  stopInstrumentation(context: LH.Gatherer.Context): Promise<void>;
23
- getArtifact(): Promise<import("../../index.js").Artifacts.Script[]>;
32
+ /**
33
+ * @param {LH.Gatherer.Context<'Trace'>} context
34
+ * @return {Promise<LH.Artifacts['Scripts']>}
35
+ */
36
+ getArtifact(context: LH.Gatherer.Context<"Trace">): Promise<LH.Artifacts["Scripts"]>;
24
37
  }
25
38
  import BaseGatherer from '../base-gatherer.js';
26
39
  //# sourceMappingURL=scripts.d.ts.map