lighthouse 13.5.0-dev.20261019 → 13.5.0-dev.20261021

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 (46) hide show
  1. package/cli/bin.d.ts +2 -2
  2. package/cli/bin.js +5 -4
  3. package/cli/cli-flags.d.ts +16 -1
  4. package/cli/cli-flags.js +51 -0
  5. package/cli/run.d.ts +5 -5
  6. package/cli/run.js +97 -47
  7. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +1 -1
  8. package/core/audits/baseline.d.ts +3 -0
  9. package/core/audits/baseline.js +1 -1
  10. package/core/audits/byte-efficiency/byte-efficiency-audit.js +2 -2
  11. package/core/audits/script-treemap-data.js +12 -2
  12. package/core/computed/lcp-image-record.d.ts +2 -23
  13. package/core/computed/lcp-image-record.js +3 -67
  14. package/core/computed/lcp-network-record.d.ts +24 -0
  15. package/core/computed/lcp-network-record.js +74 -0
  16. package/core/computed/metrics/lcp-breakdown.js +2 -2
  17. package/core/config/config-helpers.d.ts +5 -0
  18. package/core/config/config-helpers.js +6 -24
  19. package/core/gather/base-artifacts.js +6 -0
  20. package/core/gather/gatherers/full-page-screenshot.js +2 -2
  21. package/core/lib/baseline/web-features-data.json +14 -1
  22. package/core/lib/i18n/i18n.d.ts +0 -1
  23. package/core/lib/i18n/i18n.js +16 -9
  24. package/core/lib/sentry.d.ts +1 -0
  25. package/core/lib/sentry.js +3 -0
  26. package/dist/report/bundle.esm.js +2 -2
  27. package/dist/report/flow.js +13 -13
  28. package/dist/report/standalone.js +2 -2
  29. package/flow-report/src/util.d.ts +1 -1
  30. package/package.json +42 -50
  31. package/readme.md +11 -17
  32. package/report/generator/file-namer.d.ts +1 -1
  33. package/report/generator/file-namer.js +4 -3
  34. package/report/renderer/performance-category-renderer.js +2 -1
  35. package/report/renderer/text-encoding.js +2 -2
  36. package/report/types/augment-dom.d.ts +4 -0
  37. package/shared/localization/locales/en-US.json +1 -1
  38. package/shared/localization/locales/en-XL.json +1 -1
  39. package/tsconfig.json +0 -1
  40. package/types/externs.d.ts +4 -0
  41. package/types/internal/test.d.ts +1 -0
  42. package/third-party/esbuild-plugins-polyfills/LICENSE +0 -3
  43. package/third-party/esbuild-plugins-polyfills/README.md +0 -7
  44. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.d.ts +0 -9
  45. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.js +0 -129
  46. 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
+
@@ -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
  }
@@ -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
@@ -21,21 +21,6 @@ const require = createRequire(import.meta.url);
21
21
  /** @typedef {typeof import('../audits/audit.js')['Audit']} Audit */
22
22
  /** @typedef {InstanceType<GathererConstructor>} Gatherer */
23
23
 
24
- function isBundledEnvironment() {
25
- // If we're in DevTools or LightRider, we are definitely bundled.
26
- // TODO: refactor and delete `global.isDevtools`.
27
- if (global.isDevtools || global.isLightrider) return true;
28
-
29
- try {
30
- // Not foolproof, but `lighthouse-logger` is a dependency of lighthouse that should always be resolvable.
31
- // `require.resolve` will only throw in atypical/bundled environments.
32
- require.resolve('lighthouse-logger');
33
- return false;
34
- } catch (err) {
35
- return true;
36
- }
37
- }
38
-
39
24
  /**
40
25
  * If any items with identical `path` properties are found in the input array,
41
26
  * merge their `options` properties into the first instance and then discard any
@@ -224,6 +209,8 @@ async function requireWrapper(requirePath) {
224
209
  let module;
225
210
  if (bundledModules.has(requirePath)) {
226
211
  module = await bundledModules.get(requirePath);
212
+ } else if (bundledModules.size > 0) {
213
+ throw new Error(`Unable to locate module '${requirePath}' in bundled Lighthouse`);
227
214
  } else if (requirePath.match(/\.(js|mjs|cjs)$/)) {
228
215
  module = await import(requirePath);
229
216
  } else {
@@ -283,18 +270,12 @@ function requireAudit(auditPath, coreAuditList, configDir) {
283
270
  const coreAudit = coreAuditList.find(a => a === auditPathJs);
284
271
  let requirePath = `../audits/${auditPath}`;
285
272
  if (!coreAudit) {
286
- if (isBundledEnvironment()) {
273
+ if (bundledModules.has(auditPath)) {
287
274
  // This is for plugin bundling.
288
275
  requirePath = auditPath;
289
276
  } else {
290
277
  // Otherwise, attempt to find it elsewhere. This throws if not found.
291
- const absolutePath = resolveModulePath(auditPath, configDir, 'audit');
292
- if (isBundledEnvironment()) {
293
- // Use a relative path so bundler can easily expose it.
294
- requirePath = path.relative(import.meta.dirname, absolutePath);
295
- } else {
296
- requirePath = absolutePath;
297
- }
278
+ requirePath = resolveModulePath(auditPath, configDir, 'audit');
298
279
  }
299
280
  }
300
281
 
@@ -372,7 +353,7 @@ async function mergePlugins(config, configDir, flags) {
372
353
  validation.assertValidPluginName(config, pluginName);
373
354
 
374
355
  // In bundled contexts, `resolveModulePath` will fail, so use the raw pluginName directly.
375
- const pluginPath = isBundledEnvironment() ?
356
+ const pluginPath = bundledModules.has(pluginName) ?
376
357
  pluginName :
377
358
  resolveModulePath(pluginName, configDir, 'plugin');
378
359
  const rawPluginJson = await requireWrapper(pluginPath);
@@ -604,6 +585,7 @@ function deepCloneConfigJson(json) {
604
585
  }
605
586
 
606
587
  export {
588
+ bundledModules,
607
589
  deepClone,
608
590
  deepCloneConfigJson,
609
591
  mergeConfigFragment,
@@ -11,6 +11,7 @@ import {
11
11
  getDevicePixelRatio,
12
12
  } from './driver/environment.js';
13
13
  import {isEqual} from '../lib/arbitrary-equality-map.js';
14
+ import {Sentry} from '../lib/sentry.js';
14
15
 
15
16
  /**
16
17
  * @param {LH.Config.ResolvedConfig} resolvedConfig
@@ -23,6 +24,11 @@ async function getBaseArtifacts(resolvedConfig, driver, context) {
23
24
  const {userAgent, product} = await getBrowserVersion(driver.defaultSession);
24
25
  const HostDPR = await getDevicePixelRatio(driver.executionContext);
25
26
 
27
+ const [name, version] = product.split('/');
28
+ if (name && version) {
29
+ Sentry.setContext('browser', {name, version});
30
+ }
31
+
26
32
  return {
27
33
  // Meta artifacts.
28
34
  fetchTime: new Date().toJSON(),
@@ -95,7 +95,7 @@ class FullPageScreenshot extends BaseGatherer {
95
95
  mobile: deviceMetrics.mobile,
96
96
  deviceScaleFactor: 1,
97
97
  height,
98
- width: 0, // Leave width unchanged
98
+ width: deviceMetrics.width,
99
99
  });
100
100
 
101
101
  // Now that the viewport is taller, give the page some time to fetch new resources that
@@ -235,7 +235,7 @@ class FullPageScreenshot extends BaseGatherer {
235
235
  mobile: deviceMetrics.mobile,
236
236
  deviceScaleFactor: deviceMetrics.deviceScaleFactor,
237
237
  height: deviceMetrics.height,
238
- width: 0, // Leave width unchanged
238
+ width: deviceMetrics.width,
239
239
  });
240
240
  }
241
241
  }
@@ -204,7 +204,7 @@
204
204
  "font-face": "2016-09-20",
205
205
  "font-family": "2015-07-29",
206
206
  "font-family-system": "2021-09-07",
207
- "font-feature-settings": "2017-04-05",
207
+ "font-feature-settings": "2016-03-21",
208
208
  "font-kerning": "2020-01-15",
209
209
  "font-loading": "2020-01-15",
210
210
  "font-optical-sizing": "2020-03-24",
@@ -645,6 +645,7 @@
645
645
  "word-break": "2015-09-30",
646
646
  "word-spacing": "2015-07-29",
647
647
  "writing-mode": "2017-03-27",
648
+ "x-frame-options": "2015-07-29",
648
649
  "xhr": "2015-07-29",
649
650
  "xml-serializer": "2015-07-29",
650
651
  "xpath": "2015-07-29",
@@ -657,6 +658,7 @@
657
658
  "align-content-block": "2024-04-16",
658
659
  "alpha": "2026-09-14",
659
660
  "alt-text-generated-content": "2024-07-09",
661
+ "animations-css-animation-accessors": "2026-09-14",
660
662
  "arianotify": "2026-09-14",
661
663
  "async-clipboard": "2024-06-11",
662
664
  "async-iterable-streams": "2026-09-14",
@@ -726,6 +728,7 @@
726
728
  "request-video-frame-callback": "2024-10-29",
727
729
  "requestclose": "2025-05-27",
728
730
  "resizable-buffers": "2024-07-09",
731
+ "revert-rule": "2026-09-14",
729
732
  "rex": "2026-01-13",
730
733
  "ric": "2026-01-13",
731
734
  "round-mod-rem": "2024-05-17",
@@ -856,6 +859,7 @@
856
859
  "cross-document-view-transitions",
857
860
  "cross-fade",
858
861
  "css-modules",
862
+ "css-object-model-csspseudoelement",
859
863
  "css-object-model-discouraged",
860
864
  "css-typed-om",
861
865
  "cursor",
@@ -975,7 +979,9 @@
975
979
  "inverted-colors",
976
980
  "is-error",
977
981
  "is-input-pending",
982
+ "iterator-includes",
978
983
  "iterator-join",
984
+ "iterator-zip-zipkeyed",
979
985
  "jpegxl",
980
986
  "justify-self-block",
981
987
  "keyboard-lock",
@@ -1076,6 +1082,7 @@
1076
1082
  "reporting-permissions-policy-violations",
1077
1083
  "requestidlecallback",
1078
1084
  "resize",
1085
+ "retriable-module-loading",
1079
1086
  "rhythmic-sizing",
1080
1087
  "ruby-overhang",
1081
1088
  "sanitizer",
@@ -1084,6 +1091,7 @@
1084
1091
  "scoped-custom-element-registries",
1085
1092
  "screen-capture",
1086
1093
  "screen-orientation-lock",
1094
+ "scroll-axis-lock",
1087
1095
  "scroll-buttons",
1088
1096
  "scroll-driven-animations",
1089
1097
  "scroll-initial-target",
@@ -1093,6 +1101,7 @@
1093
1101
  "scroll-promises",
1094
1102
  "scroll-snap-events",
1095
1103
  "scroll-target-group",
1104
+ "scroll-triggered-animations",
1096
1105
  "secure-payment-confirmation",
1097
1106
  "selection",
1098
1107
  "serial",
@@ -1116,6 +1125,7 @@
1116
1125
  "spelling-grammar-error",
1117
1126
  "storage-buckets",
1118
1127
  "stretch",
1128
+ "string-substr",
1119
1129
  "string-trim-leftright",
1120
1130
  "style-query-range-syntax",
1121
1131
  "summarizer",
@@ -1124,6 +1134,7 @@
1124
1134
  "svg-discouraged",
1125
1135
  "switch-control",
1126
1136
  "symbols-function",
1137
+ "system-color-accentcolor",
1127
1138
  "table-discouraged",
1128
1139
  "target-within",
1129
1140
  "template-for",
@@ -1132,6 +1143,7 @@
1132
1143
  "text-box",
1133
1144
  "text-decoration-line-blink",
1134
1145
  "text-decoration-selection",
1146
+ "text-decoration-skip-spaces",
1135
1147
  "text-fit",
1136
1148
  "text-justify",
1137
1149
  "text-modules",
@@ -1198,6 +1210,7 @@
1198
1210
  "webxr-layers",
1199
1211
  "webxr-lighting-estimation",
1200
1212
  "wheel-events",
1213
+ "wheel-events-momentum",
1201
1214
  "widows-orphans",
1202
1215
  "window-controls-overlay",
1203
1216
  "window-drag",
@@ -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;
@@ -4,12 +4,9 @@
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
12
  import {getAvailableLocales} from '../../../shared/localization/format.js';
@@ -144,12 +141,22 @@ function lookupLocale(locales, possibleLocales) {
144
141
  // Filter by what's available in this runtime.
145
142
  const availableLocales = Intl.NumberFormat.supportedLocalesOf(canonicalLocales);
146
143
 
147
- // Get available locales and transform into object to match `lookupClosestLocale`'s API.
148
- const localesWithMessages = possibleLocales || getAvailableLocales();
149
- const localesWithmessagesObj = /** @type {Record<LH.Locale, LhlMessages>} */ (
150
- Object.fromEntries(localesWithMessages.map(l => [l, {}])));
151
-
152
- const closestLocale = lookupClosestLocale(availableLocales, localesWithmessagesObj);
144
+ const localesWithMessages = new Set(possibleLocales || getAvailableLocales());
145
+
146
+ /** @type {LH.Locale|undefined} */
147
+ let closestLocale;
148
+ for (const locale of availableLocales) {
149
+ const parts = locale.split('-');
150
+ while (parts.length) {
151
+ const candidate = /** @type {LH.Locale} */ (parts.join('-'));
152
+ if (localesWithMessages.has(candidate)) {
153
+ closestLocale = candidate;
154
+ break;
155
+ }
156
+ parts.pop();
157
+ }
158
+ if (closestLocale) break;
159
+ }
153
160
 
154
161
  if (!closestLocale) {
155
162
  // Log extra info if we're pretty sure this version of Node was built with `--with-intl=small-icu`.
@@ -3,6 +3,7 @@ export namespace Sentry {
3
3
  export { noop as captureMessage };
4
4
  export { noop as captureBreadcrumb };
5
5
  export { noop as getContext };
6
+ export { noop as setContext };
6
7
  export let captureException: (error: Error, options: {
7
8
  level?: string;
8
9
  tags?: {
@@ -32,6 +32,8 @@ const sentryDelegate = {
32
32
  captureBreadcrumb: noop,
33
33
  /** @type {() => any} */
34
34
  getContext: noop,
35
+ /** @type {(name: string, context: {[key: string]: any} | null) => void} */
36
+ setContext: noop,
35
37
  /** @type {(error: Error, options: {level?: string, tags?: {[key: string]: any}, extra?: {[key: string]: any}}) => Promise<void>} */
36
38
  captureException: async () => { },
37
39
  _shouldSample() {
@@ -88,6 +90,7 @@ async function init(opts) {
88
90
  sentryDelegate.captureMessage = (...args) => Sentry.captureMessage(...args);
89
91
  sentryDelegate.captureBreadcrumb = (...args) => Sentry.addBreadcrumb(...args);
90
92
  sentryDelegate.getContext = () => extras;
93
+ sentryDelegate.setContext = (...args) => Sentry.setContext(...args);
91
94
 
92
95
  // Keep a record of exceptions per audit/gatherer so we can just report once
93
96
  const sentryExceptionCache = new Map();