lighthouse 13.5.0 → 13.6.0-dev.20261028

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 (272) 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 +17 -28
  119. package/core/config/config-plugin.d.ts +7 -0
  120. package/core/config/config-plugin.js +49 -0
  121. package/core/config/config.js +10 -2
  122. package/core/config/constants.d.ts +2 -28
  123. package/core/config/filters.js +1 -0
  124. package/core/config/lr-desktop-config.js +13 -0
  125. package/core/config/lr-mobile-config.js +13 -0
  126. package/core/gather/base-artifacts.js +7 -1
  127. package/core/gather/driver/storage.js +3 -2
  128. package/core/gather/gatherers/agentic/ard.d.ts +59 -4
  129. package/core/gather/gatherers/agentic/ard.js +218 -56
  130. package/core/gather/gatherers/full-page-screenshot.js +2 -2
  131. package/core/gather/gatherers/image-elements.d.ts +7 -0
  132. package/core/gather/gatherers/image-elements.js +75 -24
  133. package/core/gather/gatherers/inspector-issues.js +2 -0
  134. package/core/gather/gatherers/js-usage.js +2 -0
  135. package/core/gather/gatherers/scripts.d.ts +16 -3
  136. package/core/gather/gatherers/scripts.js +157 -14
  137. package/core/gather/gatherers/stylesheets.js +2 -0
  138. package/core/gather/gatherers/trace-elements.js +2 -1
  139. package/core/gather/gatherers/trace.d.ts +6 -0
  140. package/core/gather/gatherers/trace.js +56 -16
  141. package/core/index.d.ts +2 -0
  142. package/core/index.js +2 -0
  143. package/core/lib/arbitrary-equality-map.d.ts +14 -3
  144. package/core/lib/arbitrary-equality-map.js +113 -6
  145. package/core/lib/baseline/web-features-data.json +32 -11
  146. package/core/lib/baseline/web-features-metadata.json +1 -1
  147. package/core/lib/bf-cache-strings.js +237 -254
  148. package/core/lib/cdt/Common.d.ts +1 -1
  149. package/core/lib/cdt/Common.js +2 -2
  150. package/core/lib/cdt/Platform.d.ts +7 -7
  151. package/core/lib/cdt/Platform.js +11 -8
  152. package/core/lib/cdt/SDK.d.ts +5 -1
  153. package/core/lib/cdt/SDK.js +4 -2
  154. package/core/lib/cdt/generated/ParsedURL.d.ts +0 -1
  155. package/core/lib/cdt/generated/ParsedURL.js +3 -9
  156. package/core/lib/cdt/generated/SourceMap.d.ts +24 -25
  157. package/core/lib/cdt/generated/SourceMap.js +7 -15
  158. package/core/lib/deprecations-strings.d.ts +93 -88
  159. package/core/lib/deprecations-strings.js +20 -14
  160. package/core/lib/i18n/i18n.d.ts +2 -1
  161. package/core/lib/i18n/i18n.js +25 -13
  162. package/core/lib/legacy-javascript/legacy-javascript.d.ts +3 -2
  163. package/core/lib/legacy-javascript/legacy-javascript.js +235 -111
  164. package/core/lib/legacy-javascript/polyfill-graph-data.json +94 -89
  165. package/core/lib/legacy-javascript/polyfill-module-data.json +48 -7
  166. package/core/lib/minification-estimator.js +10 -10
  167. package/core/lib/network-request.d.ts +2 -0
  168. package/core/lib/network-request.js +21 -0
  169. package/core/lib/page-functions.js +2 -5
  170. package/core/lib/sentry.d.ts +1 -0
  171. package/core/lib/sentry.js +3 -0
  172. package/core/lib/trace-engine.d.ts +1 -0
  173. package/core/lib/trace-engine.js +2 -0
  174. package/core/runner.js +9 -3
  175. package/dist/report/bundle.esm.js +10 -10
  176. package/dist/report/flow.js +13 -13
  177. package/dist/report/standalone.js +5 -5
  178. package/flow-report/src/common.tsx +5 -2
  179. package/flow-report/src/i18n/i18n.d.ts +2 -0
  180. package/flow-report/src/summary/summary.tsx +9 -1
  181. package/flow-report/src/util.d.ts +1 -1
  182. package/package.json +62 -98
  183. package/readme.md +13 -18
  184. package/report/generator/file-namer.d.ts +1 -1
  185. package/report/generator/file-namer.js +5 -4
  186. package/report/generator/flow-report-assets.js +1 -3
  187. package/report/generator/report-assets.js +1 -2
  188. package/report/renderer/category-renderer.js +2 -2
  189. package/report/renderer/details-renderer.js +3 -2
  190. package/report/renderer/dom.d.ts +8 -2
  191. package/report/renderer/dom.js +29 -1
  192. package/report/renderer/element-screenshot-renderer.js +3 -0
  193. package/report/renderer/performance-category-renderer.js +3 -1
  194. package/report/renderer/report-renderer.js +4 -0
  195. package/report/renderer/report-utils.d.ts +1 -0
  196. package/report/renderer/report-utils.js +2 -0
  197. package/report/renderer/text-encoding.js +2 -2
  198. package/report/types/augment-dom.d.ts +4 -0
  199. package/shared/esm-utils.d.ts +0 -8
  200. package/shared/esm-utils.js +0 -18
  201. package/shared/localization/format.js +5 -3
  202. package/shared/localization/locales/ar-XB.json +157 -142
  203. package/shared/localization/locales/ar.json +157 -142
  204. package/shared/localization/locales/bg.json +154 -139
  205. package/shared/localization/locales/ca.json +157 -142
  206. package/shared/localization/locales/cs.json +148 -133
  207. package/shared/localization/locales/da.json +153 -138
  208. package/shared/localization/locales/de.json +152 -137
  209. package/shared/localization/locales/el.json +153 -138
  210. package/shared/localization/locales/en-GB.json +160 -145
  211. package/shared/localization/locales/en-US.json +239 -218
  212. package/shared/localization/locales/en-XL.json +239 -218
  213. package/shared/localization/locales/es-419.json +150 -135
  214. package/shared/localization/locales/es.json +149 -134
  215. package/shared/localization/locales/fi.json +151 -136
  216. package/shared/localization/locales/fil.json +154 -139
  217. package/shared/localization/locales/fr.json +150 -135
  218. package/shared/localization/locales/he.json +160 -145
  219. package/shared/localization/locales/hi.json +156 -141
  220. package/shared/localization/locales/hr.json +154 -139
  221. package/shared/localization/locales/hu.json +111 -96
  222. package/shared/localization/locales/id.json +153 -138
  223. package/shared/localization/locales/it.json +153 -138
  224. package/shared/localization/locales/ja.json +150 -135
  225. package/shared/localization/locales/ko.json +42 -27
  226. package/shared/localization/locales/lt.json +152 -137
  227. package/shared/localization/locales/lv.json +93 -78
  228. package/shared/localization/locales/nl.json +149 -134
  229. package/shared/localization/locales/no.json +152 -137
  230. package/shared/localization/locales/pl.json +153 -138
  231. package/shared/localization/locales/pt-PT.json +152 -137
  232. package/shared/localization/locales/pt.json +157 -142
  233. package/shared/localization/locales/ro.json +149 -134
  234. package/shared/localization/locales/ru.json +76 -61
  235. package/shared/localization/locales/sk.json +153 -138
  236. package/shared/localization/locales/sl.json +149 -134
  237. package/shared/localization/locales/sr-Latn.json +151 -136
  238. package/shared/localization/locales/sr.json +151 -136
  239. package/shared/localization/locales/sv.json +152 -137
  240. package/shared/localization/locales/ta.json +156 -141
  241. package/shared/localization/locales/te.json +157 -142
  242. package/shared/localization/locales/th.json +111 -96
  243. package/shared/localization/locales/tr.json +153 -138
  244. package/shared/localization/locales/uk.json +149 -134
  245. package/shared/localization/locales/vi.json +162 -147
  246. package/shared/localization/locales/zh-HK.json +150 -135
  247. package/shared/localization/locales/zh-TW.json +154 -139
  248. package/shared/localization/locales/zh.json +152 -137
  249. package/shared/localization/swap-locale.js +10 -5
  250. package/shared/root.js +1 -3
  251. package/third-party/ard/README.md +19 -10
  252. package/third-party/ard/ard-test.js +189 -266
  253. package/third-party/ard/ard.d.ts +18 -1
  254. package/third-party/ard/ard.js +282 -45
  255. package/third-party/ard/schema-validator.d.ts +14 -5
  256. package/third-party/ard/schema-validator.js +921 -1177
  257. package/third-party/ard/spec/schemas/ard-entry.schema.json +176 -0
  258. package/tsconfig.json +0 -2
  259. package/types/artifacts.d.ts +43 -7
  260. package/types/config.d.ts +6 -1
  261. package/types/externs.d.ts +4 -0
  262. package/types/internal/test.d.ts +1 -0
  263. package/build-tracker.config.js +0 -16
  264. package/core/lib/cdt/package.json +0 -4
  265. package/eslint-local-rules.d.cts +0 -9
  266. package/eslint.config.mjs +0 -242
  267. package/third-party/ard/spec/schemas/ai-catalog.schema.json +0 -292
  268. package/third-party/esbuild-plugins-polyfills/LICENSE +0 -3
  269. package/third-party/esbuild-plugins-polyfills/README.md +0 -7
  270. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.d.ts +0 -9
  271. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.js +0 -129
  272. package/types/internal/lookup-closest-locale.d.ts +0 -13
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {makeComputedArtifact} from './computed-artifact.js';
8
+ import {NetworkRecords} from './network-records.js';
9
+ import {ProcessedNavigation} from './processed-navigation.js';
10
+ import {LighthouseError} from '../lib/lh-error.js';
11
+
12
+ /**
13
+ * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
14
+ * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
15
+ * was a pseudo-element associated with a node containing a smaller background-image.
16
+ */
17
+
18
+ class LCPNetworkRecord {
19
+ /**
20
+ * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
21
+ * @param {LH.Artifacts.ComputedContext} context
22
+ * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
23
+ */
24
+ static async compute_(data, context) {
25
+ const {trace, devtoolsLog} = data;
26
+ const networkRecords = await NetworkRecords.request(devtoolsLog, context);
27
+ const processedNavigation = await ProcessedNavigation.request(trace, context);
28
+ if (processedNavigation.timings.largestContentfulPaint === undefined) {
29
+ throw new LighthouseError(LighthouseError.errors.NO_LCP);
30
+ }
31
+
32
+ // Use main-frame-only LCP to match the metric value.
33
+ const lcpEvent = processedNavigation.largestContentfulPaintEvt;
34
+ if (!lcpEvent) return;
35
+
36
+ const lcpImagePaintEvent = trace.traceEvents.filter(e => {
37
+ return e.name === 'LargestImagePaint::Candidate' &&
38
+ e.args.frame === lcpEvent.args.frame &&
39
+ e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
40
+ e.args.data?.size === lcpEvent.args.data?.size;
41
+ // Get last candidate, in case there was more than one.
42
+ }).sort((a, b) => b.ts - a.ts)[0];
43
+
44
+ const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
45
+ if (!lcpUrl) return;
46
+
47
+ const candidates = networkRecords.filter(record => {
48
+ return record.url === lcpUrl &&
49
+ record.finished &&
50
+ // Same frame as LCP trace event.
51
+ record.frameId === lcpImagePaintEvent.args.frame &&
52
+ record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
53
+ }).map(record => {
54
+ // Follow any redirects to find the real image request.
55
+ while (record.redirectDestination) {
56
+ record = record.redirectDestination;
57
+ }
58
+ return record;
59
+ }).filter(record => {
60
+ // Don't select if also loaded by some other means (xhr, etc). `resourceType`
61
+ // isn't set on redirect _sources_, so have to check after following redirects.
62
+ return record.resourceType === 'Image' || record.resourceType === 'Media';
63
+ });
64
+
65
+ // If there are still multiple candidates, at this point it appears the page
66
+ // simply made multiple requests for the image. The first loaded is the best
67
+ // guess of the request that made the image available for use.
68
+ return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
69
+ }
70
+ }
71
+
72
+ const LCPNetworkRecordComputed = makeComputedArtifact(LCPNetworkRecord, ['devtoolsLog', 'trace']);
73
+ export {LCPNetworkRecordComputed as LCPNetworkRecord};
74
+
@@ -40,7 +40,7 @@ async function getComputationDataParamsFromTrace(data, context) {
40
40
  const traceEngineResult = await TraceEngineResult.request(data, context);
41
41
  const frameId = traceEngineResult.data.Meta.mainFrameId;
42
42
  const navigation =
43
- traceEngineResult.data.Meta.mainFrameNavigations[0];
43
+ traceEngineResult.data.Meta.mainFrameNavigations.at(-1);
44
44
  if (!navigation) {
45
45
  throw new Error(`Lantern metrics could not be calculated due to missing navigation id`);
46
46
  }
@@ -9,7 +9,7 @@ import {LighthouseError} from '../../lib/lh-error.js';
9
9
  import {LargestContentfulPaint} from './largest-contentful-paint.js';
10
10
  import {ProcessedNavigation} from '../processed-navigation.js';
11
11
  import {TimeToFirstByte} from './time-to-first-byte.js';
12
- import {LCPImageRecord} from '../lcp-image-record.js';
12
+ import {LCPNetworkRecord} from '../lcp-network-record.js';
13
13
  import {NavigationInsights} from '../navigation-insights.js';
14
14
 
15
15
  /**
@@ -32,7 +32,7 @@ class LCPBreakdown {
32
32
 
33
33
  const {timing: ttfb} = await TimeToFirstByte.request(data, context);
34
34
 
35
- const lcpRecord = await LCPImageRecord.request(data, context);
35
+ const lcpRecord = await LCPNetworkRecord.request(data, context);
36
36
  if (!lcpRecord) {
37
37
  return {ttfb};
38
38
  }
@@ -42,15 +42,15 @@ class LCPBreakdown {
42
42
  const throttleRatio = metricLcp / observedLcp;
43
43
 
44
44
  const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
45
- const loadDelay = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
45
+ const loadStart = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
46
46
 
47
47
  const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
48
- const loadDuration = Math.max(loadDelay, Math.min(unclampedLoadEnd, metricLcp));
48
+ const loadEnd = Math.max(loadStart, Math.min(unclampedLoadEnd, metricLcp));
49
49
 
50
50
  return {
51
51
  ttfb,
52
- loadDelay,
53
- loadDuration,
52
+ loadDelay: loadStart - ttfb,
53
+ loadDuration: loadEnd - loadStart,
54
54
  };
55
55
  }
56
56
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {calculateDocFirstByteTs} from '@paulirish/trace_engine/models/trace/insights/Common.js';
8
8
 
9
+ import {Helpers} from '../../lib/trace-engine.js';
9
10
  import {makeComputedArtifact} from '../computed-artifact.js';
10
11
  import {NavigationMetric} from './navigation-metric.js';
11
12
  import {MainResource} from '../main-resource.js';
@@ -52,15 +53,16 @@ class TimeToFirstByte extends NavigationMetric {
52
53
  await NavigationInsights.request({trace, settings, SourceMaps, HostDPR}, context);
53
54
  const lcpBreakdown = navInsights.model.LCPBreakdown;
54
55
 
56
+ const navigationId = Helpers.Trace.navigationIDForEvent(navInsights.navigation);
57
+
55
58
  // Defer to LCP breakdown, but if there's no LCP fallback to manual calculation.
56
59
  if (lcpBreakdown && !(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
57
60
  return {
58
61
  timing: lcpBreakdown.subparts.ttfb.range / 1000,
59
62
  timestamp: lcpBreakdown.subparts.ttfb.max,
60
63
  };
61
- } else if (navInsights.navigation?.args.data?.navigationId) {
62
- const request = traceEngineResult.data.NetworkRequests.byId.get(
63
- navInsights.navigation.args.data.navigationId);
64
+ } else if (navInsights.navigation && navigationId) {
65
+ const request = traceEngineResult.data.NetworkRequests.byId.get(navigationId);
64
66
  if (!request) {
65
67
  throw new Error();
66
68
  }
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import {Helpers} from '../lib/trace-engine.js';
7
8
  import {makeComputedArtifact} from './computed-artifact.js';
8
9
  import {ProcessedTrace} from './processed-trace.js';
9
10
  import {TraceEngineResult} from './trace-engine-result.js';
@@ -26,8 +27,8 @@ class NavigationInsights {
26
27
  const navigationId = processedTrace.timeOriginEvt.args.data?.navigationId;
27
28
  if (!navigationId) throw new Error('No navigationId found');
28
29
 
29
- const navInsights = [...traceEngineResult.insights.values()]
30
- .find(insightSet => insightSet.navigation?.args.data?.navigationId === navigationId);
30
+ const navInsights = [...traceEngineResult.insights.values()].find(insightSet =>
31
+ Helpers.Trace.navigationIDForEvent(insightSet.navigation) === navigationId);
31
32
  if (!navInsights) throw new Error('No navigations insights found');
32
33
 
33
34
  return navInsights;
@@ -11,6 +11,26 @@ import {ProcessedTrace} from './processed-trace.js';
11
11
  import {NetworkRecords} from './network-records.js';
12
12
  import {TraceEngineResult} from './trace-engine-result.js';
13
13
 
14
+ /**
15
+ * ResourceSendRequest URLs in the trace retain #hash fragments, whereas CDP
16
+ * Network.requestWillBeSent, Page.frameNavigated, initiator.url, documentURL,
17
+ * and V8 script events strip #hash fragments, and Lantern's NetworkAnalyzer
18
+ * requires request.url not to have a fragment.
19
+ * TODO: Remove once @paulirish/trace_engine includes the fix in LanternComputationData.
20
+ * @param {string} urlStr
21
+ * @return {string}
22
+ */
23
+ function stripUrlHash(urlStr) {
24
+ if (!urlStr.includes('#')) return urlStr;
25
+ try {
26
+ const url = new URL(urlStr);
27
+ url.hash = '';
28
+ return url.href;
29
+ } catch {
30
+ return urlStr;
31
+ }
32
+ }
33
+
14
34
  class PageDependencyGraph {
15
35
  /**
16
36
  * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, settings: LH.Audit.Context['settings'], URL: LH.Artifacts['URL'], SourceMaps: LH.Artifacts['SourceMaps'], HostDPR: LH.Artifacts['HostDPR'], fromTrace: boolean}} data
@@ -27,7 +47,41 @@ class PageDependencyGraph {
27
47
  if (data.fromTrace) {
28
48
  const traceEngineResult =
29
49
  await TraceEngineResult.request({trace, settings, SourceMaps, HostDPR}, context);
30
- const parsedTrace = traceEngineResult.data;
50
+ let parsedTrace = traceEngineResult.data;
51
+ if (parsedTrace.NetworkRequests.byTime.some(
52
+ r => r.args.data.url.includes('#') ||
53
+ r.args.data.redirects.some(red => red.url.includes('#'))
54
+ )) {
55
+ const byTime = parsedTrace.NetworkRequests.byTime.map(request => {
56
+ const url = stripUrlHash(request.args.data.url);
57
+ const redirects = request.args.data.redirects.map(redirect => ({
58
+ ...redirect,
59
+ url: stripUrlHash(redirect.url),
60
+ }));
61
+ if (url === request.args.data.url &&
62
+ redirects.every((red, i) => red.url === request.args.data.redirects[i].url)) {
63
+ return request;
64
+ }
65
+ return {
66
+ ...request,
67
+ args: {
68
+ ...request.args,
69
+ data: {
70
+ ...request.args.data,
71
+ url,
72
+ redirects,
73
+ },
74
+ },
75
+ };
76
+ });
77
+ parsedTrace = {
78
+ ...parsedTrace,
79
+ NetworkRequests: {
80
+ ...parsedTrace.NetworkRequests,
81
+ byTime,
82
+ },
83
+ };
84
+ }
31
85
  const requests =
32
86
  Lantern.TraceEngineComputationData.createNetworkRequests(trace, parsedTrace);
33
87
  const graph =
@@ -36,10 +90,6 @@ class PageDependencyGraph {
36
90
  return graph;
37
91
  }
38
92
 
39
- // TODO: currently the trace version has no requests that failed, or requests that have "Preflight".
40
- // so the following gets the devtools log version _closer_ to the exact same results as the trace.
41
- // const lanternRequests = networkRecords.map(NetworkRequest.asLanternNetworkRequest).filter(r => !r.failed && r.resourceType !== 'Preflight');
42
-
43
93
  const lanternRequests = networkRecords.map(NetworkRequest.asLanternNetworkRequest);
44
94
  return Lantern.Graph.PageDependencyGraph.createGraph(mainThreadEvents, lanternRequests, URL);
45
95
  }
@@ -20,13 +20,10 @@ class Speedline {
20
20
  // speedline() may throw without a promise, so we resolve immediately
21
21
  // to get in a promise chain.
22
22
  return ProcessedTrace.request(trace, context).then(processedTrace => {
23
- // Use a shallow copy of traceEvents so speedline can sort as it pleases.
24
- // See https://github.com/GoogleChrome/lighthouse/issues/2333
25
- const traceEvents = trace.traceEvents.slice();
26
23
  // Force use of timeOrigin as reference point for speedline
27
24
  // See https://github.com/GoogleChrome/lighthouse/issues/2095
28
25
  const timeOrigin = processedTrace.timestamps.timeOrigin;
29
- return speedline(traceEvents, {
26
+ return speedline(trace.traceEvents, {
30
27
  timeOrigin,
31
28
  fastMode: true,
32
29
  include: 'speedIndex',
@@ -46,7 +43,25 @@ class Speedline {
46
43
  throw new LighthouseError(LighthouseError.errors.SPEEDINDEX_OF_ZERO);
47
44
  }
48
45
 
49
- return speedline;
46
+ // Evict the ~1 MB per-frame decoded RGBA buffers and histograms cached inside
47
+ // speedline-core's frame closures, keeping only the getters Lighthouse uses.
48
+ // TODO(https://github.com/paulirish/speedline/issues/122): Upstream to speedline-core
49
+ // so `frame()` in `lib/frame.js` does not cache `_parsedImage` on frame closures.
50
+ const frames = speedline.frames.map(frame => {
51
+ const imgBuff = frame.getImage();
52
+ const timeStamp = frame.getTimeStamp();
53
+ const isProgressInterpolated = frame.isProgressInterpolated();
54
+ return {
55
+ getImage: () => imgBuff,
56
+ getTimeStamp: () => timeStamp,
57
+ isProgressInterpolated: () => isProgressInterpolated,
58
+ };
59
+ });
60
+
61
+ return {
62
+ ...speedline,
63
+ frames,
64
+ };
50
65
  });
51
66
  }
52
67
  }
@@ -7,6 +7,7 @@
7
7
  import log from 'lighthouse-logger';
8
8
 
9
9
  import * as i18n from '../lib/i18n/i18n.js';
10
+ import * as Lantern from '../lib/lantern/lantern.js';
10
11
  import * as TraceEngine from '../lib/trace-engine.js';
11
12
  import {makeComputedArtifact} from './computed-artifact.js';
12
13
  import {CumulativeLayoutShift} from './metrics/cumulative-layout-shift.js';
@@ -26,7 +27,9 @@ class TraceEngineResult {
26
27
  * @return {Promise<LH.Artifacts.TraceEngineResult>}
27
28
  */
28
29
  static async runTraceEngine(_traceEvents, settings, SourceMaps, HostDPR) {
29
- const processor = new TraceEngine.TraceProcessor(TraceEngine.TraceHandlers);
30
+ const config = TraceEngine.Types.Configuration.defaults();
31
+ config.enableSoftNavigation = false;
32
+ const processor = new TraceEngine.TraceProcessor(TraceEngine.TraceHandlers, config);
30
33
  const traceEvents =
31
34
  /** @type {import('@paulirish/trace_engine').Types.Events.Event[]} */ (_traceEvents);
32
35
 
@@ -66,8 +69,20 @@ class TraceEngineResult {
66
69
  });
67
70
  if (!processor.data) throw new Error('No data');
68
71
  if (!processor.insights) throw new Error('No insights');
69
- this.localizeInsights(processor.insights);
70
- return {data: processor.data, insights: processor.insights};
72
+ const data = processor.data;
73
+ const insights = processor.insights;
74
+ this.localizeInsights(insights);
75
+
76
+ // Release intermediate handler state retained in module-level TraceHandlers singletons
77
+ // and static Lantern simulation node timings after trace processing completes.
78
+ for (const handler of Object.values(TraceEngine.TraceHandlers)) {
79
+ handler.reset();
80
+ }
81
+ if (!process.env.LANTERN_DEBUG) {
82
+ Lantern.Simulation.Simulator.allNodeTimings.clear();
83
+ }
84
+
85
+ return {data, insights};
71
86
  }
72
87
 
73
88
  /**
@@ -236,8 +251,9 @@ class TraceEngineResult {
236
251
  const layoutShiftEvents = new Set(
237
252
  CumulativeLayoutShift.getLayoutShiftEvents(processedTrace).map(e => e.event));
238
253
 
239
- // Avoid modifying the input array.
240
- const traceEvents = [...data.trace.traceEvents];
254
+ // Avoid modifying the input array, copying only if an event is modified.
255
+ let traceEvents = data.trace.traceEvents;
256
+ let copied = false;
241
257
  for (let i = 0; i < traceEvents.length; i++) {
242
258
  let event = traceEvents[i];
243
259
  if (event.name !== 'LayoutShift') continue;
@@ -245,6 +261,10 @@ class TraceEngineResult {
245
261
 
246
262
  const isConsidered = layoutShiftEvents.has(event);
247
263
  if (event.args.data.had_recent_input && isConsidered) {
264
+ if (!copied) {
265
+ traceEvents = [...traceEvents];
266
+ copied = true;
267
+ }
248
268
  event = JSON.parse(JSON.stringify(event));
249
269
  // @ts-expect-error impossible for data to be missing.
250
270
  event.args.data.had_recent_input = false;
@@ -66,8 +66,21 @@ class UnusedCSS {
66
66
  let usedUncompressedBytes = 0;
67
67
  const totalUncompressedBytes = stylesheetInfo.content.length;
68
68
 
69
- for (const usedRule of stylesheetInfo.usedRules) {
70
- usedUncompressedBytes += usedRule.endOffset - usedRule.startOffset;
69
+ // CSS rule usage tracking can return overlapping ranges (e.g. a nested rule inside its
70
+ // parent rule's range) or duplicate ranges (parent rules are emitted once per used child rule).
71
+ // Sort and merge overlapping ranges so used bytes are only counted once.
72
+ const usedRules = stylesheetInfo.usedRules
73
+ .slice()
74
+ .sort((a, b) => a.startOffset - b.startOffset);
75
+
76
+ let lastEndOffset = -1;
77
+ for (const usedRule of usedRules) {
78
+ if (usedRule.endOffset <= lastEndOffset) {
79
+ continue;
80
+ }
81
+ const startOffset = Math.max(usedRule.startOffset, lastEndOffset);
82
+ usedUncompressedBytes += usedRule.endOffset - startOffset;
83
+ lastEndOffset = usedRule.endOffset;
71
84
  }
72
85
 
73
86
  const compressedSize = estimateCompressedContentSize(
@@ -1,6 +1,8 @@
1
1
  export type GathererConstructor = typeof import("../gather/base-gatherer.js").default;
2
2
  export type Audit = typeof import("../audits/audit.js")["Audit"];
3
3
  export type Gatherer = InstanceType<GathererConstructor>;
4
+ /** @type {Map<string, Promise<any>>} */
5
+ export const bundledModules: Map<string, Promise<any>>;
4
6
  /**
5
7
  * // TODO(bckenny): could adopt "jsonified" type to ensure T will survive JSON
6
8
  * round trip: https://github.com/Microsoft/TypeScript/issues/21838
@@ -33,6 +35,9 @@ export const mergeConfigFragment: LH.Config.Merge;
33
35
  * @return {Array<T>}
34
36
  */
35
37
  export function mergeConfigFragmentArrayByKey<T extends Record<string, any>>(baseArray: Array<T> | null | undefined, extensionArray: Array<T> | null | undefined, keyFn: (item: T) => string): Array<T>;
38
+ /** @typedef {typeof import('../gather/base-gatherer.js').default} GathererConstructor */
39
+ /** @typedef {typeof import('../audits/audit.js')['Audit']} Audit */
40
+ /** @typedef {InstanceType<GathererConstructor>} Gatherer */
36
41
  /**
37
42
  * If any items with identical `path` properties are found in the input array,
38
43
  * merge their `options` properties into the first instance and then discard any
@@ -7,15 +7,13 @@ import path from 'path';
7
7
  import {createRequire} from 'module';
8
8
  import url from 'url';
9
9
 
10
- import {isEqual} from 'lodash-es';
11
-
12
10
  import * as LH from '../../types/lh.js';
13
11
  import * as constants from './constants.js';
14
12
  import ConfigPlugin from './config-plugin.js';
15
13
  import {Runner} from '../runner.js';
16
14
  import * as i18n from '../lib/i18n/i18n.js';
17
15
  import * as validation from './validation.js';
18
- import {getModuleDirectory} from '../../shared/esm-utils.js';
16
+ import {isEqual} from '../lib/arbitrary-equality-map.js';
19
17
 
20
18
  const require = createRequire(import.meta.url);
21
19
 
@@ -23,21 +21,6 @@ const require = createRequire(import.meta.url);
23
21
  /** @typedef {typeof import('../audits/audit.js')['Audit']} Audit */
24
22
  /** @typedef {InstanceType<GathererConstructor>} Gatherer */
25
23
 
26
- function isBundledEnvironment() {
27
- // If we're in DevTools or LightRider, we are definitely bundled.
28
- // TODO: refactor and delete `global.isDevtools`.
29
- if (global.isDevtools || global.isLightrider) return true;
30
-
31
- try {
32
- // Not foolproof, but `lighthouse-logger` is a dependency of lighthouse that should always be resolvable.
33
- // `require.resolve` will only throw in atypical/bundled environments.
34
- require.resolve('lighthouse-logger');
35
- return false;
36
- } catch (err) {
37
- return true;
38
- }
39
- }
40
-
41
24
  /**
42
25
  * If any items with identical `path` properties are found in the input array,
43
26
  * merge their `options` properties into the first instance and then discard any
@@ -226,6 +209,8 @@ async function requireWrapper(requirePath) {
226
209
  let module;
227
210
  if (bundledModules.has(requirePath)) {
228
211
  module = await bundledModules.get(requirePath);
212
+ } else if (bundledModules.size > 0) {
213
+ throw new Error(`Unable to locate module '${requirePath}' in bundled Lighthouse`);
229
214
  } else if (requirePath.match(/\.(js|mjs|cjs)$/)) {
230
215
  module = await import(requirePath);
231
216
  } else {
@@ -285,18 +270,12 @@ function requireAudit(auditPath, coreAuditList, configDir) {
285
270
  const coreAudit = coreAuditList.find(a => a === auditPathJs);
286
271
  let requirePath = `../audits/${auditPath}`;
287
272
  if (!coreAudit) {
288
- if (isBundledEnvironment()) {
273
+ if (bundledModules.has(auditPath)) {
289
274
  // This is for plugin bundling.
290
275
  requirePath = auditPath;
291
276
  } else {
292
277
  // Otherwise, attempt to find it elsewhere. This throws if not found.
293
- const absolutePath = resolveModulePath(auditPath, configDir, 'audit');
294
- if (isBundledEnvironment()) {
295
- // Use a relative path so bundler can easily expose it.
296
- requirePath = path.relative(getModuleDirectory(import.meta), absolutePath);
297
- } else {
298
- requirePath = absolutePath;
299
- }
278
+ requirePath = resolveModulePath(auditPath, configDir, 'audit');
300
279
  }
301
280
  }
302
281
 
@@ -370,11 +349,16 @@ async function mergePlugins(config, configDir, flags) {
370
349
  const flagPlugins = flags?.plugins || [];
371
350
  const pluginNames = new Set([...configPlugins, ...flagPlugins]);
372
351
 
352
+ const configLocales = config.locales;
353
+ if (configLocales) {
354
+ delete config.locales;
355
+ }
356
+
373
357
  for (const pluginName of pluginNames) {
374
358
  validation.assertValidPluginName(config, pluginName);
375
359
 
376
360
  // In bundled contexts, `resolveModulePath` will fail, so use the raw pluginName directly.
377
- const pluginPath = isBundledEnvironment() ?
361
+ const pluginPath = bundledModules.has(pluginName) ?
378
362
  pluginName :
379
363
  resolveModulePath(pluginName, configDir, 'plugin');
380
364
  const rawPluginJson = await requireWrapper(pluginPath);
@@ -383,6 +367,10 @@ async function mergePlugins(config, configDir, flags) {
383
367
  config = mergeConfigFragment(config, pluginJson);
384
368
  }
385
369
 
370
+ if (configLocales) {
371
+ config.locales = mergeConfigFragment(config.locales, configLocales);
372
+ }
373
+
386
374
  return config;
387
375
  }
388
376
 
@@ -513,7 +501,7 @@ function resolveModulePath(moduleIdentifier, configDir, category) {
513
501
  const errorString = 'Unable to locate ' + (category ? `${category}: ` : '') +
514
502
  `\`${moduleIdentifier}\`.
515
503
  Tried to resolve the module from these locations:
516
- ${getModuleDirectory(import.meta)}
504
+ ${import.meta.dirname}
517
505
  ${cwdPath}`;
518
506
 
519
507
  if (!configDir) {
@@ -606,6 +594,7 @@ function deepCloneConfigJson(json) {
606
594
  }
607
595
 
608
596
  export {
597
+ bundledModules,
609
598
  deepClone,
610
599
  deepCloneConfigJson,
611
600
  mergeConfigFragment,
@@ -34,6 +34,13 @@ declare class ConfigPlugin {
34
34
  * @return {Record<string, LH.Config.GroupJson>|undefined}
35
35
  */
36
36
  static _parseGroups(groupsJson: unknown, pluginName: string): Record<string, LH.Config.GroupJson> | undefined;
37
+ /**
38
+ * Extract and validate locales JSON added by the plugin.
39
+ * @param {unknown} localesJson
40
+ * @param {string} pluginName
41
+ * @return {NonNullable<LH.Config['locales']>|undefined}
42
+ */
43
+ static _parseLocales(localesJson: unknown, pluginName: string): NonNullable<LH.Config["locales"]> | undefined;
37
44
  /**
38
45
  * Extracts and validates a config from the provided plugin input, throwing
39
46
  * if it deviates from the expected object shape.
@@ -215,6 +215,53 @@ class ConfigPlugin {
215
215
  return parsedGroupsJson;
216
216
  }
217
217
 
218
+ /**
219
+ * Extract and validate locales JSON added by the plugin.
220
+ * @param {unknown} localesJson
221
+ * @param {string} pluginName
222
+ * @return {NonNullable<LH.Config['locales']>|undefined}
223
+ */
224
+ static _parseLocales(localesJson, pluginName) {
225
+ if (localesJson === undefined) {
226
+ return undefined;
227
+ }
228
+
229
+ if (!isObjectOfUnknownProperties(localesJson)) {
230
+ throw new Error(`${pluginName} locales json is not defined as an object.`);
231
+ }
232
+
233
+ /** @type {NonNullable<LH.Config['locales']>} */
234
+ const parsedLocalesJson = {};
235
+ for (const [locale, localeMessagesJson] of Object.entries(localesJson)) {
236
+ if (!isObjectOfUnknownProperties(localeMessagesJson)) {
237
+ throw new Error(`${pluginName} has a locale (${locale}) not defined as an object.`);
238
+ }
239
+
240
+ /** @type {Record<string, {message: string}>} */
241
+ const parsedMessages = {};
242
+ for (const [icuMessageId, messageEntry] of Object.entries(localeMessagesJson)) {
243
+ if (!isObjectOfUnknownProperties(messageEntry)) {
244
+ throw new Error(
245
+ `${pluginName} has a locale message (${icuMessageId}) not defined as an object.`
246
+ );
247
+ }
248
+
249
+ const {message, ...invalidRest} = messageEntry;
250
+ assertNoExcessProperties(invalidRest, pluginName, 'locale message');
251
+
252
+ if (typeof message !== 'string') {
253
+ throw new Error(`${pluginName} has an invalid locale message (${icuMessageId}).`);
254
+ }
255
+
256
+ parsedMessages[icuMessageId] = {message};
257
+ }
258
+
259
+ parsedLocalesJson[/** @type {LH.Locale} */ (locale)] = parsedMessages;
260
+ }
261
+
262
+ return parsedLocalesJson;
263
+ }
264
+
218
265
  /**
219
266
  * Extracts and validates a config from the provided plugin input, throwing
220
267
  * if it deviates from the expected object shape.
@@ -233,6 +280,7 @@ class ConfigPlugin {
233
280
  audits: pluginAuditsJson,
234
281
  category: pluginCategoryJson,
235
282
  groups: pluginGroupsJson,
283
+ locales: pluginLocalesJson,
236
284
  ...invalidRest
237
285
  } = pluginJson;
238
286
 
@@ -244,6 +292,7 @@ class ConfigPlugin {
244
292
  [pluginName]: ConfigPlugin._parseCategory(pluginCategoryJson, pluginName),
245
293
  },
246
294
  groups: ConfigPlugin._parseGroups(pluginGroupsJson, pluginName),
295
+ locales: ConfigPlugin._parseLocales(pluginLocalesJson, pluginName),
247
296
  };
248
297
  }
249
298
  }
@@ -28,11 +28,10 @@ import {
28
28
  mergeConfigFragment,
29
29
  mergeConfigFragmentArrayByKey,
30
30
  } from './config-helpers.js';
31
- import {getModuleDirectory} from '../../shared/esm-utils.js';
32
31
  import * as format from '../../shared/localization/format.js';
33
32
 
34
33
  const defaultConfigPath = path.join(
35
- getModuleDirectory(import.meta),
34
+ import.meta.dirname,
36
35
  '../../config/default-config.js'
37
36
  );
38
37
 
@@ -164,6 +163,7 @@ async function resolveArtifactsToDefns(artifacts, configDir) {
164
163
  id: artifactJson.id,
165
164
  gatherer,
166
165
  dependencies: resolveArtifactDependencies(artifactJson, gatherer, artifactDefnsBySymbol),
166
+ alwaysGather: artifactJson.alwaysGather,
167
167
  };
168
168
 
169
169
  const symbol = artifact.gatherer.instance.meta.symbol;
@@ -230,6 +230,14 @@ async function initializeConfig(gatherMode, config, flags = {}) {
230
230
  configWorkingCopy = resolveExtensions(configWorkingCopy);
231
231
  configWorkingCopy = await mergePlugins(configWorkingCopy, configDir, flags);
232
232
 
233
+ if (configWorkingCopy.locales) {
234
+ for (const [locale, lhlMessages] of Object.entries(configWorkingCopy.locales)) {
235
+ if (lhlMessages) {
236
+ format.registerLocaleData(/** @type {LH.Locale} */ (locale), lhlMessages);
237
+ }
238
+ }
239
+ }
240
+
233
241
  const settings = resolveSettings(configWorkingCopy.settings || {}, flags);
234
242
  overrideSettingsForGatherMode(settings, gatherMode);
235
243
  overrideThrottlingWindows(settings);