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
@@ -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
@@ -5,19 +5,31 @@
5
5
  */
6
6
 
7
7
  import BaseGatherer from '../base-gatherer.js';
8
+ import Trace from './trace.js';
9
+
10
+ /**
11
+ * @typedef {Object} TraceScript
12
+ * @property {string} scriptId
13
+ * @property {string} [sourceText]
14
+ * @property {string[]} [chunks]
15
+ * @property {string} [url]
16
+ * @property {string} [sourceUrl]
17
+ * @property {number} [executionContextId]
18
+ * @property {string} [frame]
19
+ */
8
20
 
9
21
  /**
10
22
  * @template T, U
11
23
  * @param {Array<T>} values
12
- * @param {(value: T) => Promise<U>} promiseMapper
24
+ * @param {(value: T, index: number) => Promise<U>} promiseMapper
13
25
  * @param {boolean} runInSeries
14
26
  * @return {Promise<Array<U>>}
15
27
  */
16
28
  async function runInSeriesOrParallel(values, promiseMapper, runInSeries) {
17
29
  if (runInSeries) {
18
30
  const results = [];
19
- for (const value of values) {
20
- const result = await promiseMapper(value);
31
+ for (let i = 0; i < values.length; i++) {
32
+ const result = await promiseMapper(values[i], i);
21
33
  results.push(result);
22
34
  }
23
35
  return results;
@@ -27,6 +39,121 @@ async function runInSeriesOrParallel(values, promiseMapper, runInSeries) {
27
39
  }
28
40
  }
29
41
 
42
+ /**
43
+ * Extracts script source text and disambiguating metadata from trace rundown events,
44
+ * grouped by `scriptId`.
45
+ * @param {LH.TraceEvent[]} traceEvents
46
+ * @return {Map<string, TraceScript[]>}
47
+ */
48
+ function collectTraceScripts(traceEvents) {
49
+ /** @type {Map<string, TraceScript>} */
50
+ const byIsolateAndScriptId = new Map();
51
+
52
+ /**
53
+ * @param {NonNullable<LH.TraceEvent['args']['data']>} data
54
+ * @return {TraceScript}
55
+ */
56
+ const getOrCreateRecord = (data) => {
57
+ const scriptId = String(data.scriptId);
58
+ const key = `${data.isolate ?? ''}.${scriptId}`;
59
+ let record = byIsolateAndScriptId.get(key);
60
+ if (!record) {
61
+ record = {scriptId};
62
+ byIsolateAndScriptId.set(key, record);
63
+ }
64
+ return record;
65
+ };
66
+
67
+ for (const event of traceEvents) {
68
+ const data = event.args?.data;
69
+ if (!data || data.scriptId === undefined) continue;
70
+
71
+ if (event.cat === 'disabled-by-default-devtools.target-rundown') {
72
+ if (typeof data.frame === 'string') {
73
+ getOrCreateRecord(data).frame = data.frame;
74
+ }
75
+ } else if (event.cat === 'disabled-by-default-devtools.v8-source-rundown' &&
76
+ event.name === 'ScriptCatchup') {
77
+ const record = getOrCreateRecord(data);
78
+ if (typeof data.url === 'string') record.url = data.url;
79
+ if (typeof data.sourceUrl === 'string') record.sourceUrl = data.sourceUrl;
80
+ if (typeof data.executionContextId === 'number') {
81
+ record.executionContextId = data.executionContextId;
82
+ }
83
+ } else if (event.cat === 'disabled-by-default-devtools.v8-source-rundown-sources' &&
84
+ typeof data.sourceText === 'string') {
85
+ const record = getOrCreateRecord(data);
86
+ if (event.name === 'ScriptCatchup') {
87
+ record.sourceText = data.sourceText;
88
+ } else if (event.name === 'LargeScriptCatchup') {
89
+ if (!record.chunks || data.splitIndex === 0) record.chunks = [];
90
+ record.chunks.push(data.sourceText);
91
+ }
92
+ }
93
+ }
94
+
95
+ /** @type {Map<string, TraceScript[]>} */
96
+ const byScriptId = new Map();
97
+ for (const record of byIsolateAndScriptId.values()) {
98
+ if (record.chunks) record.sourceText = record.chunks.join('');
99
+ if (record.sourceText === undefined) continue;
100
+
101
+ let list = byScriptId.get(record.scriptId);
102
+ if (!list) {
103
+ list = [];
104
+ byScriptId.set(record.scriptId, list);
105
+ }
106
+ list.push(record);
107
+ }
108
+
109
+ return byScriptId;
110
+ }
111
+
112
+ /**
113
+ * @param {TraceScript} traceScript
114
+ * @param {LH.Crdp.Debugger.ScriptParsedEvent} script
115
+ * @return {boolean}
116
+ */
117
+ function matchesScriptParsedEvent(traceScript, script) {
118
+ const frameId = script.executionContextAuxData?.frameId;
119
+ if (frameId && traceScript.frame !== undefined && traceScript.frame !== frameId) {
120
+ return false;
121
+ }
122
+ const scriptUrl = script.embedderName || script.url;
123
+ if (traceScript.url !== undefined &&
124
+ traceScript.url !== scriptUrl &&
125
+ traceScript.sourceUrl !== script.url) {
126
+ return false;
127
+ }
128
+ if (script.executionContextId !== undefined &&
129
+ traceScript.executionContextId !== undefined &&
130
+ traceScript.executionContextId !== script.executionContextId) {
131
+ return false;
132
+ }
133
+ return true;
134
+ }
135
+
136
+ /**
137
+ * @param {LH.Crdp.Debugger.ScriptParsedEvent} script
138
+ * @param {Map<string, TraceScript[]>} traceScriptsByScriptId
139
+ * @return {string|undefined}
140
+ */
141
+ function findTraceScriptSource(script, traceScriptsByScriptId) {
142
+ const candidates = traceScriptsByScriptId.get(script.scriptId);
143
+ if (!candidates) return undefined;
144
+
145
+ const matches = candidates.filter((candidate) => matchesScriptParsedEvent(candidate, script));
146
+ if (matches.length === 1) return matches[0].sourceText;
147
+
148
+ const frameId = script.executionContextAuxData?.frameId;
149
+ if (matches.length > 1 && frameId) {
150
+ const frameMatches = matches.filter((candidate) => candidate.frame === frameId);
151
+ if (frameMatches.length === 1) return frameMatches[0].sourceText;
152
+ }
153
+
154
+ return undefined;
155
+ }
156
+
30
157
  /**
31
158
  * Returns true if the script was created via our own calls
32
159
  * to Runtime.evaluate.
@@ -50,18 +177,16 @@ function isLighthouseRuntimeEvaluateScript(script) {
50
177
  class Scripts extends BaseGatherer {
51
178
  static symbol = Symbol('Scripts');
52
179
 
53
- /** @type {LH.Gatherer.GathererMeta} */
180
+ /** @type {LH.Gatherer.GathererMeta<'Trace'>} */
54
181
  meta = {
55
182
  symbol: Scripts.symbol,
56
183
  supportedModes: ['timespan', 'navigation'],
184
+ dependencies: {Trace: Trace.symbol},
57
185
  };
58
186
 
59
187
  /** @type {LH.Crdp.Debugger.ScriptParsedEvent[]} */
60
188
  _scriptParsedEvents = [];
61
189
 
62
- /** @type {Array<string | undefined>} */
63
- _scriptContents = [];
64
-
65
190
  constructor() {
66
191
  super();
67
192
  this.onScriptParsed = this.onScriptParsed.bind(this);
@@ -89,26 +214,42 @@ class Scripts extends BaseGatherer {
89
214
  * @param {LH.Gatherer.Context} context
90
215
  */
91
216
  async stopInstrumentation(context) {
217
+ const session = context.driver.defaultSession;
218
+ session.off('Debugger.scriptParsed', this.onScriptParsed);
219
+ }
220
+
221
+ /**
222
+ * @param {LH.Gatherer.Context<'Trace'>} context
223
+ * @return {Promise<LH.Artifacts['Scripts']>}
224
+ */
225
+ async getArtifact(context) {
92
226
  const session = context.driver.defaultSession;
93
227
  const formFactor = context.baseArtifacts.HostFormFactor;
228
+ const traceScriptsByScriptId = collectTraceScripts(context.dependencies.Trace.traceEvents);
229
+ const traceSources = this._scriptParsedEvents.map(
230
+ (script) => findTraceScriptSource(script, traceScriptsByScriptId)
231
+ );
94
232
 
95
- session.off('Debugger.scriptParsed', this.onScriptParsed);
233
+ if (traceSources.some((source) => source === undefined)) {
234
+ // `disableAsyncStacks()` in `prepare.js` runs after `stopInstrumentation` and calls
235
+ // `Debugger.disable`, so re-enable `Debugger` if we need to call `Debugger.getScriptSource`.
236
+ await session.sendCommand('Debugger.enable');
237
+ }
96
238
 
97
239
  // If run on a mobile device, be sensitive to memory limitations and only
98
240
  // request one at a time.
99
- this._scriptContents = await runInSeriesOrParallel(
241
+ const scriptContents = await runInSeriesOrParallel(
100
242
  this._scriptParsedEvents,
101
- ({scriptId}) => {
102
- return session.sendCommand('Debugger.getScriptSource', {scriptId})
243
+ (script, i) => {
244
+ if (traceSources[i] !== undefined) return Promise.resolve(traceSources[i]);
245
+ return session.sendCommand('Debugger.getScriptSource', {scriptId: script.scriptId})
103
246
  .then((resp) => resp.scriptSource)
104
247
  .catch(() => undefined);
105
248
  },
106
249
  formFactor === 'mobile' /* runInSeries */
107
250
  );
108
251
  await session.sendCommand('Debugger.disable');
109
- }
110
252
 
111
- async getArtifact() {
112
253
  /** @type {LH.Artifacts['Scripts']} */
113
254
  const scripts = this._scriptParsedEvents.map((event, i) => {
114
255
  // 'embedderName' and 'url' are confusingly named, so we rewrite them here.
@@ -123,10 +264,12 @@ class Scripts extends BaseGatherer {
123
264
  // embedderName is optional on the protocol because backends like Node may not set it.
124
265
  // For our purposes, it is always set. But just in case it isn't... fallback to the url.
125
266
  url: event.embedderName || event.url,
126
- content: this._scriptContents[i],
267
+ content: scriptContents[i],
127
268
  };
128
269
  });
129
270
 
271
+ this._scriptParsedEvents = [];
272
+
130
273
  return scripts;
131
274
  }
132
275
  }
@@ -82,6 +82,8 @@ class Stylesheets extends BaseGatherer {
82
82
 
83
83
  // Ensure we finish fetching all stylesheet contents before disabling the CSS domain
84
84
  const sheets = await Promise.all(this._sheetPromises.values());
85
+ this._sheetPromises.clear();
86
+ this._session = undefined;
85
87
 
86
88
  await session.sendCommand('CSS.disable');
87
89
  await session.sendCommand('DOM.disable');
@@ -21,6 +21,7 @@ import {ProcessedTrace} from '../../computed/processed-trace.js';
21
21
  import {Responsiveness} from '../../computed/metrics/responsiveness.js';
22
22
  import {CumulativeLayoutShift} from '../../computed/metrics/cumulative-layout-shift.js';
23
23
  import {TraceEngineResult} from '../../computed/trace-engine-result.js';
24
+ import {Helpers} from '../../lib/trace-engine.js';
24
25
  import SourceMaps from './source-maps.js';
25
26
 
26
27
  /** @typedef {{nodeId: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[], type?: string}} TraceElementData */
@@ -80,7 +81,7 @@ class TraceElements extends BaseGatherer {
80
81
  }
81
82
 
82
83
  if (navigationId) {
83
- if (insightSet.navigation?.args.data?.navigationId !== navigationId) {
84
+ if (Helpers.Trace.navigationIDForEvent(insightSet.navigation) !== navigationId) {
84
85
  return [];
85
86
  }
86
87
  } else {
@@ -1,6 +1,12 @@
1
1
  export default Trace;
2
2
  declare class Trace extends BaseGatherer {
3
3
  static getDefaultTraceCategories(): string[];
4
+ /**
5
+ * @param {LH.Gatherer.ProtocolSession} session
6
+ * @param {string} handle
7
+ * @return {Promise<LH.Trace>}
8
+ */
9
+ static _readTraceFromStream(session: LH.Gatherer.ProtocolSession, handle: string): Promise<LH.Trace>;
4
10
  /**
5
11
  * @param {LH.Gatherer.ProtocolSession} session
6
12
  * @return {Promise<LH.Trace>}
@@ -73,29 +73,68 @@ class Trace extends BaseGatherer {
73
73
 
74
74
  /**
75
75
  * @param {LH.Gatherer.ProtocolSession} session
76
+ * @param {string} handle
76
77
  * @return {Promise<LH.Trace>}
77
78
  */
78
- static async endTraceAndCollectEvents(session) {
79
+ static async _readTraceFromStream(session, handle) {
79
80
  /** @type {Array<LH.TraceEvent>} */
80
81
  const traceEvents = [];
82
+ const decoder = new TextDecoder();
83
+ let isEof = false;
84
+ let remainder = '';
85
+
86
+ try {
87
+ while (!isEof) {
88
+ const response = await session.sendCommand('IO.read', {handle});
89
+ isEof = Boolean(response.eof);
90
+ remainder += response.base64Encoded ?
91
+ decoder.decode(Buffer.from(response.data, 'base64'), {stream: !isEof}) :
92
+ response.data;
93
+
94
+ // Perfetto's `ExportJson` serializes each trace event and trailing `metadata`
95
+ // as compact single-line JSON (with string newlines escaped as `\\n`), emitting
96
+ // raw `,\n` only between top-level elements of `traceEvents`.
97
+ // Maintain the invariant that `remainder` always starts with `{"traceEvents":[`.
98
+ const lastEventSep = remainder.lastIndexOf('},\n');
99
+ if (lastEventSep !== -1) {
100
+ const {traceEvents: batch} = JSON.parse(`${remainder.slice(0, lastEventSep + 1)}]}`);
101
+ for (const event of batch) {
102
+ traceEvents.push(event);
103
+ }
104
+ remainder = `{"traceEvents":[${remainder.slice(lastEventSep + 3)}`;
105
+ }
106
+ }
107
+ } finally {
108
+ await session.sendCommand('IO.close', {handle});
109
+ }
81
110
 
82
- /**
83
- * Listener for when dataCollected events fire for each trace chunk
84
- * @param {LH.Crdp.Tracing.DataCollectedEvent} data
85
- */
86
- const dataListener = function(data) {
87
- traceEvents.push(...data.value);
88
- };
89
- session.on('Tracing.dataCollected', dataListener);
90
-
91
- return new Promise((resolve, reject) => {
92
- session.once('Tracing.tracingComplete', _ => {
93
- session.off('Tracing.dataCollected', dataListener);
94
- resolve({traceEvents});
95
- });
111
+ const traceJson = JSON.parse(remainder);
112
+ for (const event of traceJson.traceEvents) {
113
+ traceEvents.push(event);
114
+ }
115
+ traceJson.traceEvents = traceEvents;
116
+ return traceJson;
117
+ }
96
118
 
97
- session.sendCommand('Tracing.end').catch(reject);
119
+ /**
120
+ * @param {LH.Gatherer.ProtocolSession} session
121
+ * @return {Promise<LH.Trace>}
122
+ */
123
+ static async endTraceAndCollectEvents(session) {
124
+ /** @type {LH.Crdp.Tracing.TracingCompleteEvent} */
125
+ const completeEvent = await new Promise((resolve, reject) => {
126
+ session.once('Tracing.tracingComplete', resolve);
127
+ session.sendCommand('Tracing.end').catch(err => {
128
+ session.off('Tracing.tracingComplete', resolve);
129
+ reject(err);
130
+ });
98
131
  });
132
+
133
+ if (!completeEvent.stream) {
134
+ throw new Error('Tracing.tracingComplete did not return a stream handle');
135
+ }
136
+
137
+ return Trace._readTraceFromStream(session, completeEvent.stream);
99
138
  }
100
139
 
101
140
  static symbol = Symbol('Trace');
@@ -115,6 +154,7 @@ class Trace extends BaseGatherer {
115
154
  await driver.defaultSession.sendCommand('Page.enable');
116
155
  await driver.defaultSession.sendCommand('Tracing.start', {
117
156
  categories: traceCategories.join(','),
157
+ transferMode: 'ReturnAsStream',
118
158
  options: 'sampling-frequency=10000', // 1000 is default and too slow.
119
159
  });
120
160
 
package/core/index.d.ts CHANGED
@@ -68,6 +68,8 @@ export function auditFlowArtifacts(flowArtifacts: LH.UserFlow.FlowArtifacts, con
68
68
  }>;
69
69
  export function getAuditList(): string[];
70
70
  export const traceCategories: string[];
71
+ import * as i18n from './lib/i18n/i18n.js';
71
72
  import * as LH from '../types/lh.js';
72
73
  import { UserFlow } from './user-flow.js';
74
+ export { i18n };
73
75
  //# sourceMappingURL=index.d.ts.map
package/core/index.js CHANGED
@@ -11,6 +11,7 @@ import {ReportGenerator} from '../report/generator/report-generator.js';
11
11
  import {startTimespanGather} from './gather/timespan-runner.js';
12
12
  import {snapshotGather} from './gather/snapshot-runner.js';
13
13
  import {navigationGather} from './gather/navigation-runner.js';
14
+ import * as i18n from './lib/i18n/i18n.js';
14
15
  import * as LH from '../types/lh.js';
15
16
 
16
17
  /*
@@ -131,4 +132,5 @@ export {
131
132
  auditFlowArtifacts,
132
133
  getAuditList,
133
134
  traceCategories,
135
+ i18n,
134
136
  };
@@ -5,9 +5,7 @@
5
5
  */
6
6
  export class ArbitraryEqualityMap {
7
7
  /**
8
- * Determines whether two objects are deeply equal. Defers to lodash isEqual, but is kept here for
9
- * easy usage by consumers.
10
- * See https://lodash.com/docs/4.17.5#isEqual.
8
+ * Determines whether two objects are deeply equal.
11
9
  * @param {*} objA
12
10
  * @param {*} objB
13
11
  * @return {boolean}
@@ -44,4 +42,17 @@ export class ArbitraryEqualityMap {
44
42
  */
45
43
  _findIndexOf(key: any): number;
46
44
  }
45
+ /**
46
+ * @license
47
+ * Copyright 2018 Google LLC
48
+ * SPDX-License-Identifier: Apache-2.0
49
+ */
50
+ /**
51
+ * @param {*} a
52
+ * @param {*} b
53
+ * @param {WeakMap<object, WeakSet<object>>=} seen
54
+ * @return {boolean}
55
+ */
56
+ declare function deepEquals(a: any, b: any, seen?: WeakMap<object, WeakSet<object>> | undefined): boolean;
57
+ export { deepEquals as isEqual };
47
58
  //# sourceMappingURL=arbitrary-equality-map.d.ts.map