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
@@ -72,6 +72,10 @@ export const UIStrings = {
72
72
  * @description Translation is not needed, this will never be exposed in production code.
73
73
  */
74
74
  ExampleBrowserProcessDeprecation: "This is an example for showing the code required for a browser process reported deprecation.",
75
+ /**
76
+ * @description Warning displayed to developers that they are using the Fenced Frame API which has been deprecated and is scheduled to be removed.
77
+ */
78
+ FencedFrame: "Fenced Frame API is deprecated and scheduled for removal.",
75
79
  /**
76
80
  * @description A deprecation warning shown in the DevTools Issues tab. It's shown when one of the Protected Audience APIs like `navigator.joinAdInterestGroup`, `navigator.getInterestGroupAdAuctionData` or `navigator.runAdAuction` are called.
77
81
  */
@@ -240,6 +244,10 @@ export const UIStrings = {
240
244
  * @description A deprecation warning shown in the DevTools Issues tab. It's shown when a video conferencing website uses a non-standard API for controlling the crypto method used, but is not having an effect because the desired behavior is already enabled-by-default.
241
245
  */
242
246
  RTCConstraintEnableDtlsSrtpTrue: "The constraint `DtlsSrtpKeyAgreement` is removed. You have specified a `true` value for this constraint, which had no effect, but you can remove this constraint for tidiness.",
247
+ /**
248
+ * @description Warning displayed to developers when the non-standard RTCPeerConnection.createDTMFSender() method is used. Developers should use the standard RTCRtpSender.dtmf attribute instead.
249
+ */
250
+ RTCPeerConnectionCreateDTMFSender: "RTCPeerConnection.createDTMFSender() is deprecated and will be removed. Please use RTCRtpSender.dtmf instead.",
243
251
  /**
244
252
  * @description WebRTC is set of JavaScript APIs for sending and receiving data, audio and video. getStats() is a method used to obtain network and quality metrics. There are two versions of this method, one is being deprecated because it is non-standard.
245
253
  */
@@ -252,14 +260,6 @@ export const UIStrings = {
252
260
  * @description A deprecation warning shown in the DevTools Issues tab. The placeholder is always the noun 'SharedArrayBuffer' which refers to a JavaScript construct.
253
261
  */
254
262
  SharedArrayBufferConstructedWithoutIsolation: "`SharedArrayBuffer` will require cross-origin isolation. See https://developer.chrome.com/blog/enabling-shared-array-buffer/ for more details.",
255
- /**
256
- * @description A deprecation warning shown in the DevTools Issues tab. It's shown when one of the Shared Storage APIs like `sharedStorage.set()`, `sharedStorage.worklet.addModule()`, `sharedStorage.selectURL()`, etc., along with `<img sharedstoragewritable>`, `<iframe sharedstoragewritable>`, or `fetch(url, {sharedStorageWritable: true})` are used.
257
- */
258
- SharedStorage: "The Shared Storage API is deprecated and will be removed in a future release.",
259
- /**
260
- * @description A deprecation warning shown in the DevTools Issues tab. It's shown when the `document.requestStorageAccessFor` API is called. The placeholder will always be the string `document.requestStorageAccessFor`.
261
- */
262
- StorageAccessAPI_requestStorageAccessFor_Method: "`document.requestStorageAccessFor` is deprecated and will be removed. See https://privacysandbox.com/news/update-on-plans-for-privacy-sandbox-technologies/ for more details.",
263
263
  /**
264
264
  * @description A deprecation warning shown in the DevTools Issues tab. It's shown when the speech synthesis API is called before the page receives a user activation.
265
265
  */
@@ -276,6 +276,10 @@ export const UIStrings = {
276
276
  * @description A deprecation warning shown in the DevTools Issues tab. The placeholder is always the noun 'SharedArrayBuffer' which refers to a JavaScript construct. 'Extensions' refers to Chrome extensions. The warning is shown when Chrome Extensions attempt to use 'SharedArrayBuffer's under insecure circumstances.
277
277
  */
278
278
  V8SharedArrayBufferConstructedInExtensionWithoutIsolation: "Extensions should opt into cross-origin isolation to continue using `SharedArrayBuffer`. See https://developer.chrome.com/docs/extensions/mv3/cross-origin-isolation/.",
279
+ /**
280
+ * @description Warning displayed to developers when an SVG filter is applied to view transition content, which is disallowed because the content may contain cross-origin iframes or plugins. `::view-transition-old()` and `::view-transition-new()` are CSS pseudo-element names and should not be translated.
281
+ */
282
+ ViewTransitionPreventSvgFilterPaint: "SVG filters cannot be applied to view transition content, such as `::view-transition-old()` and `::view-transition-new()`, as it may contain cross-origin iframes or plugins.",
279
283
  /**
280
284
  * @description This warning occurs when the deprecated `BluetoothRemoteGATTCharacteristic.writeValue()` method is used. Developers should use `writeValueWithResponse()` or `writeValueWithoutResponse()` instead.
281
285
  */
@@ -350,6 +354,10 @@ export const DEPRECATIONS_METADATA = {
350
354
  "chromeStatusFeature": 5095987863486464,
351
355
  "milestone": 151
352
356
  },
357
+ "FencedFrame": {
358
+ "chromeStatusFeature": 6366274495053824,
359
+ "milestone": 156
360
+ },
353
361
  "IdentityInCanMakePaymentEvent": {
354
362
  "chromeStatusFeature": 5190978431352832
355
363
  },
@@ -420,6 +428,10 @@ export const DEPRECATIONS_METADATA = {
420
428
  "RTCConstraintEnableDtlsSrtpTrue": {
421
429
  "milestone": 97
422
430
  },
431
+ "RTCPeerConnectionCreateDTMFSender": {
432
+ "chromeStatusFeature": 5160724576993280,
433
+ "milestone": 160
434
+ },
423
435
  "RTCPeerConnectionGetStatsLegacyNonCompliant": {
424
436
  "chromeStatusFeature": 4631626228695040,
425
437
  "milestone": 117
@@ -437,12 +449,6 @@ export const DEPRECATIONS_METADATA = {
437
449
  "SharedArrayBufferConstructedWithoutIsolation": {
438
450
  "milestone": 106
439
451
  },
440
- "SharedStorage": {
441
- "chromeStatusFeature": 5076349064708096
442
- },
443
- "StorageAccessAPI_requestStorageAccessFor_Method": {
444
- "chromeStatusFeature": 5162221567082496
445
- },
446
452
  "TextToSpeech_DisallowedByAutoplay": {
447
453
  "chromeStatusFeature": 5687444770914304,
448
454
  "milestone": 71
@@ -1,4 +1,3 @@
1
- export type LhlMessages = import("../../../shared/localization/locales").LhlMessages;
2
1
  export namespace UIStrings {
3
2
  let ms: string;
4
3
  let seconds: string;
@@ -78,4 +77,6 @@ export function createIcuMessageFn(filename: string, fileStrings?: Record<string
78
77
  * @return {value is string|LH.IcuMessage}
79
78
  */
80
79
  export function isStringOrIcuMessage(value: unknown): value is string | LH.IcuMessage;
80
+ import { registerLocaleData } from '../../../shared/localization/format.js';
81
+ export { registerLocaleData };
81
82
  //# sourceMappingURL=i18n.d.ts.map
@@ -4,18 +4,19 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- /** @typedef {import('../../../shared/localization/locales').LhlMessages} LhlMessages */
8
-
9
7
  import path from 'path';
10
8
  import url from 'url';
11
9
 
12
- import lookupClosestLocale from 'lookup-closest-locale';
13
10
  import log from 'lighthouse-logger';
14
11
 
15
- import {getAvailableLocales} from '../../../shared/localization/format.js';
12
+ import {
13
+ getAvailableLocales,
14
+ isIcuMessage,
15
+ formatMessage,
16
+ registerLocaleData,
17
+ DEFAULT_LOCALE,
18
+ } from '../../../shared/localization/format.js';
16
19
  import {LH_ROOT} from '../../../shared/root.js';
17
- import {isIcuMessage, formatMessage, DEFAULT_LOCALE} from '../../../shared/localization/format.js';
18
- import {getModulePath} from '../../../shared/esm-utils.js';
19
20
 
20
21
  const UIStrings = {
21
22
  /** Used to show the duration in milliseconds that something lasted. The `{timeInMs}` placeholder will be replaced with the time duration, shown in milliseconds (e.g. 63 ms) */
@@ -145,12 +146,22 @@ function lookupLocale(locales, possibleLocales) {
145
146
  // Filter by what's available in this runtime.
146
147
  const availableLocales = Intl.NumberFormat.supportedLocalesOf(canonicalLocales);
147
148
 
148
- // Get available locales and transform into object to match `lookupClosestLocale`'s API.
149
- const localesWithMessages = possibleLocales || getAvailableLocales();
150
- const localesWithmessagesObj = /** @type {Record<LH.Locale, LhlMessages>} */ (
151
- Object.fromEntries(localesWithMessages.map(l => [l, {}])));
152
-
153
- const closestLocale = lookupClosestLocale(availableLocales, localesWithmessagesObj);
149
+ const localesWithMessages = new Set(possibleLocales || getAvailableLocales());
150
+
151
+ /** @type {LH.Locale|undefined} */
152
+ let closestLocale;
153
+ for (const locale of availableLocales) {
154
+ const parts = locale.split('-');
155
+ while (parts.length) {
156
+ const candidate = /** @type {LH.Locale} */ (parts.join('-'));
157
+ if (localesWithMessages.has(candidate)) {
158
+ closestLocale = candidate;
159
+ break;
160
+ }
161
+ parts.pop();
162
+ }
163
+ if (closestLocale) break;
164
+ }
154
165
 
155
166
  if (!closestLocale) {
156
167
  // Log extra info if we're pretty sure this version of Node was built with `--with-intl=small-icu`.
@@ -198,7 +209,7 @@ function createIcuMessageFn(filename, fileStrings = {}) {
198
209
  // one of the common strings found in `i18n.UIStrings`.
199
210
  const filenameToLookup = keyname in fileStrings ?
200
211
  filename :
201
- path.relative(LH_ROOT, getModulePath(import.meta));
212
+ path.relative(LH_ROOT, import.meta.filename);
202
213
  const unixStyleFilename = filenameToLookup.replace(/\\/g, '/');
203
214
  const i18nId = `${unixStyleFilename} | ${keyname}`;
204
215
 
@@ -226,4 +237,5 @@ export {
226
237
  lookupLocale,
227
238
  createIcuMessageFn,
228
239
  isStringOrIcuMessage,
240
+ registerLocaleData,
229
241
  };
@@ -2,6 +2,7 @@ export type Pattern = {
2
2
  name: string;
3
3
  expression: string;
4
4
  estimateBytes?: (content: string) => number;
5
+ resolveName?: (match: string) => string;
5
6
  };
6
7
  export type PatternMatchResult = {
7
8
  name: string;
@@ -14,10 +15,10 @@ export type Result = {
14
15
  };
15
16
  /**
16
17
  * @param {string} content
17
- * @param {import('../cdt/generated/SourceMap.js')|null} map
18
+ * @param {import('../cdt/generated/SourceMap.js').default|null} map
18
19
  * @return {Result}
19
20
  */
20
- export function detectLegacyJavaScript(content: string, map: import("../cdt/generated/SourceMap.js") | null): Result;
21
+ export function detectLegacyJavaScript(content: string, map: import("../cdt/generated/SourceMap.js").default | null): Result;
21
22
  /**
22
23
  * @return {Pattern[]}
23
24
  */
@@ -11,7 +11,7 @@
11
11
  * ./core/scripts/legacy-javascript - verification tool.
12
12
  */
13
13
 
14
- /** @typedef {{name: string, expression: string, estimateBytes?: (content: string) => number}} Pattern */
14
+ /** @typedef {{name: string, expression: string, estimateBytes?: (content: string) => number, resolveName?: (match: string) => string}} Pattern */
15
15
  /** @typedef {{name: string, line: number, column: number}} PatternMatchResult */
16
16
  /** @typedef {{matches: PatternMatchResult[], estimatedByteSavings: number}} Result */
17
17
 
@@ -27,7 +27,7 @@ const graph = graph_;
27
27
  /**
28
28
  * Takes a list of patterns (consisting of a name identifier and a RegExp expression string)
29
29
  * and via `match` returns match results with line / column information for a given code input.
30
- * Only returns the first match per pattern given.
30
+ * Only returns the first match per pattern name.
31
31
  */
32
32
  class CodePatternMatcher {
33
33
  /**
@@ -45,12 +45,13 @@ class CodePatternMatcher {
45
45
  if (!this.re) {
46
46
  const patternsExpression =
47
47
  this.patterns.map(pattern => `(${pattern.expression})`).join('|');
48
- this.re = new RegExp(`(^\r\n|\r|\n)|${patternsExpression}`, 'g');
48
+ this.re = new RegExp(patternsExpression, 'g');
49
49
  }
50
50
 
51
51
  // Reset RegExp state.
52
52
  this.re.lastIndex = 0;
53
53
 
54
+ /** @type {Set<string>} */
54
55
  const seen = new Set();
55
56
  /** @type {PatternMatchResult[]} */
56
57
  const matches = [];
@@ -58,29 +59,41 @@ class CodePatternMatcher {
58
59
  let result;
59
60
  let line = 0;
60
61
  let lineBeginsAtIndex = 0;
62
+ let scannedToIndex = 0;
61
63
  // Each pattern maps to one subgroup in the generated regex. For each iteration of RegExp.exec,
62
64
  // only one subgroup will be defined. Exec until no more matches.
63
65
  while ((result = this.re.exec(code)) !== null) {
64
- // Discard first value in `result` - it's just the entire match.
65
- const captureGroups = result.slice(1);
66
- // isNewline - truthy if matching a newline, used to track the line number.
67
- // `patternExpressionMatches` maps to each possible pattern in `this.patterns`.
68
- // Only one of [isNewline, ...patternExpressionMatches] is ever truthy.
69
- const [isNewline, ...patternExpressionMatches] = captureGroups;
70
- if (isNewline) {
71
- line++;
72
- lineBeginsAtIndex = result.index + 1;
73
- continue;
66
+ let patternIndex = -1;
67
+ for (let i = 1; i < result.length; i++) {
68
+ if (result[i] !== undefined) {
69
+ patternIndex = i - 1;
70
+ break;
71
+ }
74
72
  }
75
- const pattern = this.patterns[patternExpressionMatches.findIndex(Boolean)];
73
+ if (patternIndex === -1) continue;
76
74
 
77
- if (seen.has(pattern)) {
75
+ const pattern = this.patterns[patternIndex];
76
+ const name = pattern.resolveName ? pattern.resolveName(result[0]) : pattern.name;
77
+ if (seen.has(name)) {
78
78
  continue;
79
79
  }
80
- seen.add(pattern);
80
+ seen.add(name);
81
+
82
+ // Lazily advance line and column tracking up to `result.index`.
83
+ // Because `[scannedToIndex, result.index)` includes the text of any previous matches,
84
+ // this also accounts for newlines inside multi-line matches.
85
+ // Scanning for `\n` handles both LF (`\n`) and CRLF (`\r\n`) line endings since
86
+ // the next line always begins at `nlIdx + 1`.
87
+ let nlIdx = code.indexOf('\n', scannedToIndex);
88
+ while (nlIdx !== -1 && nlIdx < result.index) {
89
+ line++;
90
+ lineBeginsAtIndex = nlIdx + 1;
91
+ nlIdx = code.indexOf('\n', lineBeginsAtIndex);
92
+ }
93
+ scannedToIndex = result.index;
81
94
 
82
95
  matches.push({
83
- name: pattern.name,
96
+ name,
84
97
  line,
85
98
  column: result.index - lineBeginsAtIndex,
86
99
  });
@@ -90,70 +103,6 @@ class CodePatternMatcher {
90
103
  }
91
104
  }
92
105
 
93
- /**
94
- * @param {string?} object
95
- * @param {string} property
96
- * @param {string} coreJs3Module
97
- */
98
- function buildPolyfillExpression(object, property, coreJs3Module) {
99
- const qt = (/** @type {string} */ token) =>
100
- `['"]${token}['"]`; // don't worry about matching string delims
101
-
102
- let expression = '';
103
-
104
- if (object) {
105
- // String.prototype.startsWith =
106
- expression += `${object}\\.${property}\\s?=[^=]`;
107
- } else {
108
- // Promise =
109
- // window.Promise =// Promise =Z
110
- // but not: SomePromise =
111
- expression += `(?:window\\.|[\\s;]+)${property}\\s?=[^=]`;
112
- }
113
-
114
- // String.prototype['startsWith'] =
115
- if (object) {
116
- expression += `|${object}\\[${qt(property)}\\]\\s?=[^=]`;
117
- }
118
-
119
- // Object.defineProperty(String.prototype, 'startsWith'
120
- expression += `|defineProperty\\(${object || 'window'},\\s?${qt(property)}`;
121
-
122
- // es-shims
123
- // no(Object,{entries:r},{entries:function
124
- if (object) {
125
- expression += `|\\(${object},\\s*{${property}:.*},\\s*{${property}`;
126
- }
127
-
128
- // core-js
129
- if (object) {
130
- const objectWithoutPrototype = object.replace('.prototype', '');
131
- // e(e.S,"Object",{values
132
- // Minified + mangled pattern found in CDN babel-polyfill.
133
- // see https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.2.5/polyfill.min.js
134
- // TODO: perhaps this is the wrong place to check for a CDN polyfill. Remove?
135
- // expression += `|;e\\([^,]+,${qt(objectWithoutPrototype)},{${property}:`;
136
-
137
- // core-js@3 minified pattern.
138
- // {target:"Array",proto:true},{fill:fill
139
- // {target:"Array",proto:true,forced:!HAS_SPECIES_SUPPORT||!USES_TO_LENGTH},{filter:
140
- expression += `|{target:${qt(objectWithoutPrototype)}[^;]*},{${property}:`;
141
- } else {
142
- // Detect polyfills for new classes: Map, Set, WeakSet, etc.
143
- // TODO: so far, no class polyfills are enabled for detection.
144
- // See `modulesToSkip` in create-polyfill-module-data.js
145
-
146
- // collection("Map",
147
- // expression += `|collection\\(${qt(property)},`;
148
- }
149
-
150
- // Un-minified code may have module names.
151
- // core-js/modules/es.object.is-frozen
152
- expression += `|${coreJs3Module.replaceAll('.', '\\.')}(?:\\.js)?"`;
153
-
154
- return expression;
155
- }
156
-
157
106
  function getCoreJsPolyfillData() {
158
107
  return polyfillModuleData.filter(d => d.corejs).map(d => {
159
108
  return {
@@ -164,34 +113,171 @@ function getCoreJsPolyfillData() {
164
113
  }
165
114
 
166
115
  /**
116
+ * Builds factored polyfill patterns grouped by receiver object, core-js target, and
117
+ * core-js module prefix so the regex engine evaluates a few shared branches instead
118
+ * of repeating common prefixes (like `defineProperty(`, `{target:`, `Array.prototype.`)
119
+ * once per polyfill.
120
+ *
167
121
  * @return {Pattern[]}
168
122
  */
169
123
  function getPolyfillPatterns() {
170
- /** @type {Pattern[]} */
171
- const patterns = [];
124
+ /** @param {string} str */
125
+ const escapeRegExp = str => str.replaceAll('.', '\\.');
126
+
127
+ /** @type {Map<string, string[]>} */
128
+ const byObject = new Map();
129
+ /** @type {Map<string, string[]>} */
130
+ const byTarget = new Map();
131
+ /** @type {Map<string, string>} */
132
+ const targetPropToName = new Map();
133
+ /** @type {Map<string, string>} */
134
+ const coreJsModuleToName = new Map();
135
+ /** @type {Map<string, string[]>} */
136
+ const byModulePrefix = new Map();
172
137
 
173
138
  for (const {name, coreJs3Module} of getCoreJsPolyfillData()) {
174
139
  const parts = name.split('.');
175
- const object = parts.length > 1 ? parts.slice(0, parts.length - 1).join('.') : null;
140
+ const object = parts.slice(0, parts.length - 1).join('.');
176
141
  const property = parts[parts.length - 1];
177
- patterns.push({
178
- name,
179
- expression: buildPolyfillExpression(object, property, coreJs3Module),
180
- });
142
+
143
+ let objProps = byObject.get(object);
144
+ if (!objProps) {
145
+ objProps = [];
146
+ byObject.set(object, objProps);
147
+ }
148
+ objProps.push(property);
149
+
150
+ const target = object.replace('.prototype', '');
151
+ let targetProps = byTarget.get(target);
152
+ if (!targetProps) {
153
+ targetProps = [];
154
+ byTarget.set(target, targetProps);
155
+ }
156
+ targetProps.push(property);
157
+ targetPropToName.set(`${target}.${property}`, name);
158
+
159
+ coreJsModuleToName.set(coreJs3Module, name);
160
+ const modParts = coreJs3Module.split('.');
161
+ const modPrefix = modParts.slice(0, 2).join('.');
162
+ const modRest = modParts.slice(2).join('.');
163
+ let modRests = byModulePrefix.get(modPrefix);
164
+ if (!modRests) {
165
+ modRests = [];
166
+ byModulePrefix.set(modPrefix, modRests);
167
+ }
168
+ modRests.push(modRest);
169
+ }
170
+
171
+ // 1. Direct property assignment: `String.prototype.startsWith =`
172
+ const directBranches = [];
173
+ for (const [object, props] of byObject) {
174
+ directBranches.push(`${escapeRegExp(object)}\\.(?:${props.join('|')})\\s?=[^=]`);
175
+ }
176
+
177
+ // 2. Bracket property assignment: `String.prototype['startsWith'] =`
178
+ const bracketBranches = [];
179
+ for (const [object, props] of byObject) {
180
+ bracketBranches.push(`${escapeRegExp(object)}\\[['"](?:${props.join('|')})['"]\\]\\s?=[^=]`);
181
+ }
182
+
183
+ // 3. Object.defineProperty: `Object.defineProperty(String.prototype, 'startsWith'`
184
+ const definePropBranches = [];
185
+ for (const [object, props] of byObject) {
186
+ definePropBranches.push(`${escapeRegExp(object)},\\s?['"](?:${props.join('|')})['"]`);
187
+ }
188
+
189
+ // 4. es-shims: `no(Object,{entries:r},{entries:function`
190
+ const esShimsBranches = [];
191
+ for (const [object, props] of byObject) {
192
+ const propBranches = props.map(p => `${p}:[^};]*},\\s*{${p}\\b`).join('|');
193
+ esShimsBranches.push(`${escapeRegExp(object)},\\s*{(?:${propBranches})`);
194
+ }
195
+
196
+ // 5. core-js@3 minified pattern:
197
+ // `{target:"Array",proto:true},{fill:fill`
198
+ // `{target:"Array",proto:true,forced:!HAS_SPECIES_SUPPORT||!USES_TO_LENGTH},{filter:`
199
+ const targetBranches = [];
200
+ for (const [target, props] of byTarget) {
201
+ targetBranches.push(`['"]${escapeRegExp(target)}['"][^;]*?},{(?:${props.join('|')}):`);
202
+ }
203
+
204
+ // 6. Un-minified core-js module names: `core-js/modules/es.object.is-frozen`
205
+ const moduleBranches = [];
206
+ for (const [prefix, rests] of byModulePrefix) {
207
+ moduleBranches.push(
208
+ `${escapeRegExp(prefix)}\\.(?:${rests.map(escapeRegExp).join('|')})`
209
+ );
181
210
  }
182
211
 
183
- return patterns;
212
+ return [
213
+ {
214
+ name: 'direct-assignment',
215
+ expression: directBranches.join('|'),
216
+ resolveName: match => match.slice(0, match.indexOf('=')).trimEnd(),
217
+ },
218
+ {
219
+ name: 'bracket-assignment',
220
+ expression: bracketBranches.join('|'),
221
+ resolveName: match => {
222
+ const bracketIdx = match.indexOf('[');
223
+ const closeIdx = match.indexOf(']', bracketIdx);
224
+ return `${match.slice(0, bracketIdx)}.${match.slice(bracketIdx + 2, closeIdx - 1)}`;
225
+ },
226
+ },
227
+ {
228
+ name: 'defineProperty',
229
+ expression: `defineProperty\\((?:${definePropBranches.join('|')})`,
230
+ resolveName: match => {
231
+ const openIdx = match.indexOf('(');
232
+ const commaIdx = match.indexOf(',', openIdx);
233
+ const obj = match.slice(openIdx + 1, commaIdx);
234
+ const prop = match.slice(commaIdx + 1).trimStart().slice(1, -1);
235
+ return `${obj}.${prop}`;
236
+ },
237
+ },
238
+ {
239
+ name: 'es-shims',
240
+ expression: `\\((?:${esShimsBranches.join('|')})`,
241
+ resolveName: match => {
242
+ const commaIdx = match.indexOf(',');
243
+ const obj = match.slice(1, commaIdx);
244
+ const braceIdx = match.indexOf('{', commaIdx);
245
+ const colonIdx = match.indexOf(':', braceIdx);
246
+ const prop = match.slice(braceIdx + 1, colonIdx);
247
+ return `${obj}.${prop}`;
248
+ },
249
+ },
250
+ {
251
+ name: 'core-js-target',
252
+ expression: `{target:(?:${targetBranches.join('|')})`,
253
+ resolveName: match => {
254
+ const targetEnd = match.indexOf(match[8], 9);
255
+ const target = match.slice(9, targetEnd);
256
+ const lastBrace = match.lastIndexOf('{');
257
+ const prop = match.slice(lastBrace + 1, -1);
258
+ return /** @type {string} */ (targetPropToName.get(`${target}.${prop}`));
259
+ },
260
+ },
261
+ {
262
+ name: 'core-js-module',
263
+ expression: `(?:${moduleBranches.join('|')})(?:\\.js)?"`,
264
+ resolveName: match => {
265
+ const mod = match.endsWith('.js"') ? match.slice(0, -4) : match.slice(0, -1);
266
+ return /** @type {string} */ (coreJsModuleToName.get(mod));
267
+ },
268
+ },
269
+ ];
184
270
  }
185
271
 
186
272
  /**
187
273
  * @return {Pattern[]}
188
274
  */
189
275
  function getTransformPatterns() {
190
- /**
191
- * @param {string} content
192
- * @param {RegExp|string} pattern
193
- * @return {number}
194
- */
276
+ /**
277
+ * @param {string} content
278
+ * @param {RegExp|string} pattern
279
+ * @return {number}
280
+ */
195
281
  const count = (content, pattern) => {
196
282
  // Split is slightly faster than match.
197
283
  if (typeof pattern === 'string') {
@@ -259,6 +345,17 @@ function getTransformPatterns() {
259
345
  ];
260
346
  }
261
347
 
348
+ const transformPatterns = getTransformPatterns();
349
+ const transformPatternsByName = new Map(transformPatterns.map(p => [p.name, p]));
350
+
351
+ /** @type {Map<string, string>} */
352
+ const moduleToPolyfillName = new Map();
353
+ for (const {name, modules} of polyfillModuleData) {
354
+ for (const mod of modules) {
355
+ moduleToPolyfillName.set(mod, name);
356
+ }
357
+ }
358
+
262
359
  /**
263
360
  * @param {string} content
264
361
  * @param {PatternMatchResult[]} matches
@@ -287,7 +384,7 @@ function estimateWastedBytes(content, matches) {
287
384
  let estimatedWastedBytesFromTransforms = 0;
288
385
 
289
386
  for (const result of transformResults) {
290
- const pattern = getTransformPatterns().find(p => p.name === result.name);
387
+ const pattern = transformPatternsByName.get(result.name);
291
388
  if (!pattern || !pattern.estimateBytes || !content) continue;
292
389
  estimatedWastedBytesFromTransforms += pattern.estimateBytes(content);
293
390
  }
@@ -299,12 +396,12 @@ function estimateWastedBytes(content, matches) {
299
396
 
300
397
  const matcher = new CodePatternMatcher([
301
398
  ...getPolyfillPatterns(),
302
- ...getTransformPatterns(),
399
+ ...transformPatterns,
303
400
  ]);
304
401
 
305
402
  /**
306
403
  * @param {string} content
307
- * @param {import('../cdt/generated/SourceMap.js')|null} map
404
+ * @param {import('../cdt/generated/SourceMap.js').default|null} map
308
405
  * @return {Result}
309
406
  */
310
407
  function detectLegacyJavaScript(content, map) {
@@ -315,20 +412,47 @@ function detectLegacyJavaScript(content, map) {
315
412
 
316
413
  // If it's a bundle with source maps, add in the polyfill modules by name too.
317
414
  if (map) {
318
- for (const {name, modules} of polyfillModuleData) {
319
- // Skip if the pattern matching found a match for this polyfill.
320
- if (matches.some(m => m.name === name)) continue;
321
-
322
- const source = map.sourceURLs().find(source => modules.some(module => {
323
- return source.endsWith(`/${module}.js`) || source.includes(`node_modules/${module}/`);
324
- }));
325
- if (!source) continue;
326
-
327
- const mapping = map.mappings().find(m => m.sourceURL === source);
328
- if (mapping) {
329
- matches.push({name, line: mapping.lineNumber, column: mapping.columnNumber});
330
- } else {
331
- matches.push({name, line: 0, column: 0});
415
+ const matchedNames = new Set(matches.map(m => m.name));
416
+ /** @type {Array<{name: string, source: string}>} */
417
+ const foundInMap = [];
418
+
419
+ for (const source of map.sourceURLs()) {
420
+ if (source.endsWith('.js')) {
421
+ const slashIdx = source.lastIndexOf('/');
422
+ if (slashIdx !== -1) {
423
+ const polyfillName = moduleToPolyfillName.get(source.slice(slashIdx + 1, -3));
424
+ if (polyfillName && !matchedNames.has(polyfillName)) {
425
+ matchedNames.add(polyfillName);
426
+ foundInMap.push({name: polyfillName, source});
427
+ }
428
+ }
429
+ }
430
+ if (source.includes('node_modules/')) {
431
+ let nmIdx = source.indexOf('node_modules/');
432
+ while (nmIdx !== -1) {
433
+ const modStart = nmIdx + 13; // 'node_modules/'.length
434
+ const nextSlash = source.indexOf('/', modStart);
435
+ if (nextSlash !== -1) {
436
+ const polyfillName = moduleToPolyfillName.get(source.slice(modStart, nextSlash));
437
+ if (polyfillName && !matchedNames.has(polyfillName)) {
438
+ matchedNames.add(polyfillName);
439
+ foundInMap.push({name: polyfillName, source});
440
+ }
441
+ }
442
+ nmIdx = source.indexOf('node_modules/', modStart);
443
+ }
444
+ }
445
+ }
446
+
447
+ if (foundInMap.length > 0) {
448
+ const mappings = map.mappings();
449
+ for (const {name, source} of foundInMap) {
450
+ const mapping = mappings.find(m => m.sourceURL === source);
451
+ if (mapping) {
452
+ matches.push({name, line: mapping.lineNumber, column: mapping.columnNumber});
453
+ } else {
454
+ matches.push({name, line: 0, column: 0});
455
+ }
332
456
  }
333
457
  }
334
458
  }