lighthouse 13.0.3 → 13.1.0

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/CONTRIBUTING.md +1 -1
  2. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +10 -10
  3. package/cli/test/smokehouse/config/exclusions.js +44 -0
  4. package/cli/test/smokehouse/frontends/smokehouse-bin.js +5 -4
  5. package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.d.ts +14 -0
  6. package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.js +141 -0
  7. package/core/audits/baseline.d.ts +25 -0
  8. package/core/audits/baseline.js +190 -0
  9. package/core/computed/document-urls.js +5 -2
  10. package/core/computed/main-resource.js +7 -3
  11. package/core/computed/network-analysis.js +4 -1
  12. package/core/config/default-config.js +2 -0
  13. package/core/gather/driver/wait-for-condition.js +11 -1
  14. package/core/gather/gatherers/trace.js +3 -0
  15. package/core/lib/baseline/web-features-metadata.json +3 -0
  16. package/core/lib/deprecations-strings.d.ts +169 -89
  17. package/core/lib/deprecations-strings.js +119 -24
  18. package/core/lib/navigation-error.js +11 -2
  19. package/core/lib/network-recorder.js +2 -1
  20. package/core/lib/network-request.js +1 -0
  21. package/core/scoring.d.ts +24 -0
  22. package/dist/report/bundle.esm.js +31 -3
  23. package/dist/report/flow.js +32 -4
  24. package/dist/report/standalone.js +32 -4
  25. package/package.json +11 -10
  26. package/report/assets/styles.css +28 -0
  27. package/report/renderer/components.js +1 -1
  28. package/report/renderer/details-renderer.d.ts +5 -0
  29. package/report/renderer/details-renderer.js +16 -0
  30. package/report/types/report-renderer.d.ts +1 -1
  31. package/report/types/report-result.d.ts +1 -1
  32. package/shared/localization/locales/en-US.json +12 -0
  33. package/shared/localization/locales/en-XL.json +12 -0
  34. package/tsconfig.json +2 -0
  35. package/types/artifacts.d.ts +33 -30
  36. package/types/audit.d.ts +1 -1
  37. package/types/config.d.ts +1 -1
  38. package/types/gatherer.d.ts +1 -1
  39. package/types/lhr/audit-details.d.ts +10 -4
  40. package/types/lhr/flow-result.d.ts +1 -1
  41. package/types/lhr/lhr.d.ts +1 -1
  42. package/types/lhr/treemap.d.ts +1 -1
  43. package/types/protocol.d.ts +1 -1
  44. package/types/puppeteer.d.ts +1 -1
  45. package/types/user-flow.d.ts +1 -1
  46. package/types/utility-types.d.ts +1 -1
package/CONTRIBUTING.md CHANGED
@@ -8,7 +8,7 @@ We tag issues that are good candidates for those new to the code with [`good fir
8
8
 
9
9
  ## Follow the coding style
10
10
 
11
- The `.eslintrc.cjs` file defines all. We use [JSDoc](http://usejsdoc.org/) with [TypeScript `checkJs`](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc). Annotations are encouraged for all contributions.
11
+ The `.eslintrc.cjs` file defines all. We use [JSDoc](https://jsdoc.app/) with [TypeScript `checkJs`](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc). Annotations are encouraged for all contributions.
12
12
 
13
13
  ## Learn about the architecture
14
14
 
@@ -3,7 +3,7 @@
3
3
  exports[`getAssertionReport works (multiple failing) 1`] = `
4
4
  "X difference at cumulative-layout-shift audit.details.items.length
5
5
  expected: []
6
- found: [{\\"cumulativeLayoutShiftMainFrame\\":0.09957584751767748,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.09957584751767748,\\"cumulativeLayoutShiftMainFrame\\":0.09957584751767748},\\"newEngineResultDiffered\\":false}]
6
+ found: [{\\"cumulativeLayoutShiftMainFrame\\":0.099576,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.099576,\\"cumulativeLayoutShiftMainFrame\\":0.099576},\\"newEngineResultDiffered\\":false}]
7
7
 
8
8
 
9
9
  X difference at cumulative-layout-shift audit.details.blah
@@ -17,7 +17,7 @@ exports[`getAssertionReport works (multiple failing) 1`] = `
17
17
  \\"description\\": \\"Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more about the Cumulative Layout Shift metric](https://web.dev/articles/cls).\\",
18
18
  \\"score\\": 0.9,
19
19
  \\"scoreDisplayMode\\": \\"numeric\\",
20
- \\"numericValue\\": 0.09957584751767748,
20
+ \\"numericValue\\": 0.099576,
21
21
  \\"numericUnit\\": \\"unitless\\",
22
22
  \\"displayValue\\": \\"0.1\\",
23
23
  \\"scoringOptions\\": {
@@ -28,10 +28,10 @@ exports[`getAssertionReport works (multiple failing) 1`] = `
28
28
  \\"type\\": \\"debugdata\\",
29
29
  \\"items\\": [
30
30
  {
31
- \\"cumulativeLayoutShiftMainFrame\\": 0.09957584751767748,
31
+ \\"cumulativeLayoutShiftMainFrame\\": 0.099576,
32
32
  \\"newEngineResult\\": {
33
- \\"cumulativeLayoutShift\\": 0.09957584751767748,
34
- \\"cumulativeLayoutShiftMainFrame\\": 0.09957584751767748
33
+ \\"cumulativeLayoutShift\\": 0.099576,
34
+ \\"cumulativeLayoutShiftMainFrame\\": 0.099576
35
35
  },
36
36
  \\"newEngineResultDiffered\\": false
37
37
  }
@@ -43,7 +43,7 @@ exports[`getAssertionReport works (multiple failing) 1`] = `
43
43
  exports[`getAssertionReport works (trivial failing) 1`] = `
44
44
  "X difference at cumulative-layout-shift audit.details.items.length
45
45
  expected: []
46
- found: [{\\"cumulativeLayoutShiftMainFrame\\":0.09957584751767748,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.09957584751767748,\\"cumulativeLayoutShiftMainFrame\\":0.09957584751767748},\\"newEngineResultDiffered\\":false}]
46
+ found: [{\\"cumulativeLayoutShiftMainFrame\\":0.099576,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.099576,\\"cumulativeLayoutShiftMainFrame\\":0.099576},\\"newEngineResultDiffered\\":false}]
47
47
 
48
48
  found result:
49
49
  {
@@ -52,7 +52,7 @@ exports[`getAssertionReport works (trivial failing) 1`] = `
52
52
  \\"description\\": \\"Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more about the Cumulative Layout Shift metric](https://web.dev/articles/cls).\\",
53
53
  \\"score\\": 0.9,
54
54
  \\"scoreDisplayMode\\": \\"numeric\\",
55
- \\"numericValue\\": 0.09957584751767748,
55
+ \\"numericValue\\": 0.099576,
56
56
  \\"numericUnit\\": \\"unitless\\",
57
57
  \\"displayValue\\": \\"0.1\\",
58
58
  \\"scoringOptions\\": {
@@ -63,10 +63,10 @@ exports[`getAssertionReport works (trivial failing) 1`] = `
63
63
  \\"type\\": \\"debugdata\\",
64
64
  \\"items\\": [
65
65
  {
66
- \\"cumulativeLayoutShiftMainFrame\\": 0.09957584751767748,
66
+ \\"cumulativeLayoutShiftMainFrame\\": 0.099576,
67
67
  \\"newEngineResult\\": {
68
- \\"cumulativeLayoutShift\\": 0.09957584751767748,
69
- \\"cumulativeLayoutShiftMainFrame\\": 0.09957584751767748
68
+ \\"cumulativeLayoutShift\\": 0.099576,
69
+ \\"cumulativeLayoutShiftMainFrame\\": 0.099576
70
70
  },
71
71
  \\"newEngineResultDiffered\\": false
72
72
  }
@@ -31,6 +31,50 @@ const exclusions = {
31
31
  // Disabled because is timing out.
32
32
  'oopif-scripts',
33
33
  ],
34
+ 'devtools-mcp': [
35
+ // Disabled because performance tracing related audits are not supported.
36
+ 'byte-efficiency',
37
+ 'byte-gzip',
38
+ 'dbw',
39
+ 'errors-expired-ssl',
40
+ 'errors-iframe-expired-ssl',
41
+ 'errors-infinite-loop',
42
+ 'forms-autocomplete',
43
+ 'fps-max-passive',
44
+ 'fps-max',
45
+ 'fps-overflow-x',
46
+ 'lantern-fetch',
47
+ 'lantern-idle-callback-long',
48
+ 'lantern-idle-callback-short',
49
+ 'lantern-online',
50
+ 'lantern-set-timeout',
51
+ 'lantern-xhr',
52
+ 'legacy-javascript',
53
+ 'metrics-debugger',
54
+ 'metrics-delayed-fcp',
55
+ 'metrics-delayed-lcp',
56
+ 'metrics-tricky-tti-late-fcp',
57
+ 'metrics-tricky-tti',
58
+ 'oopif-requests',
59
+ 'oopif-scripts',
60
+ 'perf-debug',
61
+ 'perf-diagnostics-animations',
62
+ 'perf-diagnostics-unsized-images',
63
+ 'perf-fonts',
64
+ 'perf-frame-metrics',
65
+ 'perf-preload',
66
+ 'perf-trace-elements',
67
+ 'redirects-client-paint-server',
68
+ 'redirects-history-push-state',
69
+ 'redirects-multiple-server',
70
+ 'redirects-scripts',
71
+ 'redirects-single-client',
72
+ 'redirects-single-server',
73
+ 'screenshot',
74
+ 'shift-attribution',
75
+ 'source-maps',
76
+ 'timing',
77
+ ],
34
78
  };
35
79
 
36
80
  for (const array of Object.values(exclusions)) {
@@ -33,9 +33,10 @@ const coreTestDefnsPath =
33
33
  * contingent on having built all the bundles.
34
34
  */
35
35
  const runnerPaths = {
36
- cli: '../lighthouse-runners/cli.js',
37
- bundle: '../lighthouse-runners/bundle.js',
38
- devtools: '../lighthouse-runners/devtools.js',
36
+ 'cli': '../lighthouse-runners/cli.js',
37
+ 'bundle': '../lighthouse-runners/bundle.js',
38
+ 'devtools': '../lighthouse-runners/devtools.js',
39
+ 'devtools-mcp': '../lighthouse-runners/devtools-mcp.js',
39
40
  };
40
41
 
41
42
  /**
@@ -146,7 +147,7 @@ async function begin() {
146
147
  },
147
148
  'runner': {
148
149
  default: 'cli',
149
- choices: ['cli', 'bundle', 'devtools'],
150
+ choices: ['cli', 'bundle', 'devtools', 'devtools-mcp'],
150
151
  describe: 'The method of running Lighthouse',
151
152
  },
152
153
  'tests-path': {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Launch Chrome and do a full Lighthouse run via the Lighthouse DevTools MCP bundle.
3
+ * @param {string} url
4
+ * @param {LH.Config=} config
5
+ * @param {LocalConsole=} logger
6
+ * @param {Smokehouse.SmokehouseOptions['testRunnerOptions']=} testRunnerOptions
7
+ * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts}>}
8
+ */
9
+ export function runLighthouse(url: string, config?: LH.Config | undefined, logger?: LocalConsole | undefined, testRunnerOptions?: Smokehouse.SmokehouseOptions["testRunnerOptions"] | undefined): Promise<{
10
+ lhr: LH.Result;
11
+ artifacts: LH.Artifacts;
12
+ }>;
13
+ import { LocalConsole } from '../lib/local-console.js';
14
+ //# sourceMappingURL=devtools-mcp.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview A runner that launches Chrome and executes Lighthouse via the
9
+ * DevTools MCP bundle.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import os from 'os';
14
+ import {Worker, isMainThread, parentPort, workerData} from 'worker_threads';
15
+ import {once} from 'events';
16
+
17
+ import puppeteer from 'puppeteer-core';
18
+ import * as ChromeLauncher from 'chrome-launcher';
19
+
20
+ import {LH_ROOT} from '../../../../shared/root.js';
21
+ import {loadArtifacts, saveArtifacts} from '../../../../core/lib/asset-saver.js';
22
+ import {LocalConsole} from '../lib/local-console.js';
23
+
24
+ // This runs only in the worker. The rest runs on the main thread.
25
+ if (!isMainThread && parentPort) {
26
+ (async () => {
27
+ const {url, config, testRunnerOptions} = workerData;
28
+ try {
29
+ const result = await runBundledLighthouse(url, config, testRunnerOptions);
30
+ // Save to assets directory because LighthouseError won't survive postMessage.
31
+ const assetsDir = fs.mkdtempSync(os.tmpdir() + '/smoke-mcp-assets-');
32
+ await saveArtifacts(result.artifacts, assetsDir);
33
+ const value = {
34
+ lhr: result.lhr,
35
+ assetsDir,
36
+ };
37
+ parentPort?.postMessage({type: 'result', value});
38
+ } catch (err) {
39
+ console.error(err);
40
+ parentPort?.postMessage({type: 'error', value: err.toString()});
41
+ }
42
+ })();
43
+ }
44
+
45
+ /**
46
+ * @param {string} url
47
+ * @param {LH.Config|undefined} config
48
+ * @param {Smokehouse.SmokehouseOptions['testRunnerOptions']} testRunnerOptions
49
+ * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts}>}
50
+ */
51
+ async function runBundledLighthouse(url, config, testRunnerOptions) {
52
+ if (isMainThread || !parentPort) {
53
+ throw new Error('must be called in worker');
54
+ }
55
+
56
+ // Load bundle.
57
+ const {navigation} = await import(LH_ROOT + '/dist/lighthouse-devtools-mcp-bundle.js');
58
+
59
+ // Launch and connect to Chrome.
60
+ const launchedChrome = await ChromeLauncher.launch({
61
+ chromeFlags: [
62
+ testRunnerOptions?.headless ? '--headless=new' : '',
63
+ ],
64
+ });
65
+ const port = launchedChrome.port;
66
+
67
+ // Run Lighthouse.
68
+ try {
69
+ const logLevel = testRunnerOptions?.isDebug ? 'verbose' : 'info';
70
+
71
+ // Puppeteer is not included in the bundle, we must create the page here.
72
+ const browser = await puppeteer.connect({browserURL: `http://127.0.0.1:${port}`});
73
+ const page = await browser.newPage();
74
+
75
+ // navigation(page, requestor, options)
76
+ const runnerResult = await navigation(page, url, {config, flags: {port, logLevel}});
77
+
78
+ if (!runnerResult) throw new Error('No runnerResult');
79
+
80
+ return {
81
+ lhr: runnerResult.lhr,
82
+ artifacts: runnerResult.artifacts,
83
+ };
84
+ } finally {
85
+ // Clean up and return results.
86
+ launchedChrome.kill();
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Launch Chrome and do a full Lighthouse run via the Lighthouse DevTools MCP bundle.
92
+ * @param {string} url
93
+ * @param {LH.Config=} config
94
+ * @param {LocalConsole=} logger
95
+ * @param {Smokehouse.SmokehouseOptions['testRunnerOptions']=} testRunnerOptions
96
+ * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts}>}
97
+ */
98
+ async function runLighthouse(url, config, logger, testRunnerOptions = {}) {
99
+ logger = logger || new LocalConsole();
100
+
101
+ const worker = new Worker(new URL(import.meta.url), {
102
+ stdout: true,
103
+ stderr: true,
104
+ workerData: {url, config, testRunnerOptions},
105
+ });
106
+ worker.stdout.setEncoding('utf8');
107
+ worker.stderr.setEncoding('utf8');
108
+ worker.stdout.addListener('data', (data) => {
109
+ logger.log(`[STDOUT] ${data}`);
110
+ });
111
+ worker.stderr.addListener('data', (data) => {
112
+ logger.log(`[STDERR] ${data}`);
113
+ });
114
+ const [workerResponse] = await once(worker, 'message');
115
+
116
+ if (workerResponse.type === 'error') {
117
+ const log = logger.getLog();
118
+ throw new Error(`Worker returned an error: ${workerResponse.value}
119
+ Log:
120
+ ${log}
121
+ `);
122
+ }
123
+
124
+ const result = workerResponse.value;
125
+ if (!result.lhr || !result.assetsDir) {
126
+ throw new Error(`invalid response from worker:
127
+ ${JSON.stringify(result, null, 2)}`);
128
+ }
129
+
130
+ const artifacts = loadArtifacts(result.assetsDir);
131
+ fs.rmSync(result.assetsDir, {recursive: true});
132
+
133
+ return {
134
+ lhr: result.lhr,
135
+ artifacts,
136
+ };
137
+ }
138
+
139
+ export {
140
+ runLighthouse,
141
+ };
@@ -0,0 +1,25 @@
1
+ export default Baseline;
2
+ export type DXFeatureEvent = LH.TraceEvent & {
3
+ args: {
4
+ feature: string;
5
+ url?: string;
6
+ lineNumber?: number;
7
+ columnNumber?: number;
8
+ };
9
+ };
10
+ /** @typedef {LH.TraceEvent & {args: {feature: string, url?: string, lineNumber?: number, columnNumber?: number}}} DXFeatureEvent */
11
+ declare class Baseline extends Audit {
12
+ /**
13
+ * @param {LH.Artifacts} artifacts
14
+ * @return {Promise<LH.Audit.Product>}
15
+ */
16
+ static audit(artifacts: LH.Artifacts): Promise<LH.Audit.Product>;
17
+ }
18
+ export namespace UIStrings {
19
+ let title: string;
20
+ let description: string;
21
+ let columnFeature: string;
22
+ let columnStatus: string;
23
+ }
24
+ import { Audit } from './audit.js';
25
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import data from 'web-features/data.json' with {type: 'json'};
8
+
9
+ import {Audit} from './audit.js';
10
+ import metadata from '../lib/baseline/web-features-metadata.json' with {type: 'json'};
11
+ import * as i18n from '../lib/i18n/i18n.js';
12
+
13
+ const UIStrings = {
14
+ /** Title of the Baseline audit. Shown when the page is compatible with the target baseline. */
15
+ title: 'Baseline Features',
16
+ /**
17
+ * @description Description of the Baseline audit.
18
+ * @example {2026-03-18} date
19
+ */
20
+ description:
21
+ 'Lists web features used on the page and their Baseline status as of {date}. ' +
22
+ '[Learn more about Baseline](https://webstatus.dev/).',
23
+ /** Label for the column displaying the feature ID. */
24
+ columnFeature: 'Web-features',
25
+ /** Label for the column displaying the feature\'s baseline status. */
26
+ columnStatus: 'Baseline Status',
27
+ };
28
+
29
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
30
+ const {features} = /** @type {any} */ (data);
31
+
32
+
33
+ /** @typedef {LH.TraceEvent & {args: {feature: string, url?: string, lineNumber?: number, columnNumber?: number}}} DXFeatureEvent */
34
+
35
+ class Baseline extends Audit {
36
+ /**
37
+ * @return {LH.Audit.Meta}
38
+ */
39
+ static get meta() {
40
+ return {
41
+ id: 'baseline',
42
+ scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
43
+ title: str_(UIStrings.title),
44
+ description: str_(UIStrings.description, {date: metadata.date}),
45
+ requiredArtifacts: ['Trace'],
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {LH.Artifacts} artifacts
51
+ * @return {Promise<LH.Audit.Product>}
52
+ */
53
+ static async audit(artifacts) {
54
+ const trace = artifacts.Trace;
55
+
56
+ /** @type {Map<string, {featureId: string, source: LH.Audit.Details.SourceLocationValue | undefined}>} */
57
+ const featuresMap = new Map();
58
+
59
+ const dxEvents = /** @type {DXFeatureEvent[]} */ (
60
+ (trace.traceEvents || []).filter(e => e.cat === 'blink.webdx_feature_usage' &&
61
+ e.args?.feature)
62
+ );
63
+
64
+ for (const event of dxEvents) {
65
+ const key = `${event.args.feature}`;
66
+
67
+ if (featuresMap.has(key)) continue;
68
+
69
+ /** @type {LH.Audit.Details.SourceLocationValue | undefined} */
70
+ let source;
71
+
72
+ if (event.args.url) {
73
+ const line = event.args.lineNumber ? event.args.lineNumber - 1 : 0;
74
+ const column = event.args.columnNumber ? event.args.columnNumber - 1 : 0;
75
+
76
+ source = Audit.makeSourceLocation(event.args.url, line, column);
77
+ }
78
+
79
+ featuresMap.set(key, {
80
+ featureId: event.args.feature,
81
+ source,
82
+ });
83
+ }
84
+
85
+ const baselineFeatures = Array.from(featuresMap.values());
86
+ const baselineStatus = [];
87
+
88
+ for (const feature of baselineFeatures) {
89
+ if (!feature.featureId) {
90
+ continue;
91
+ }
92
+
93
+ const featureData = features[feature.featureId];
94
+
95
+ if (!featureData || !('status' in featureData)) {
96
+ continue;
97
+ }
98
+
99
+ let displayStatus = 'Limited Availability';
100
+ let baselineTier = 'limited';
101
+
102
+ if (featureData.status.baseline === 'high') {
103
+ displayStatus = `Widely Available (${featureData.status.baseline_low_date})`;
104
+ baselineTier = 'high';
105
+ } else if (featureData.status.baseline === 'low') {
106
+ displayStatus = `Newly Available (${featureData.status.baseline_low_date})`;
107
+ baselineTier = 'low';
108
+ }
109
+
110
+ baselineStatus.push({
111
+ featureId: {
112
+ type: /** @type {const} */ ('link'),
113
+ text: feature.featureId,
114
+ url: `https://webstatus.dev/features/${feature.featureId}`,
115
+ },
116
+ displayStatus: {
117
+ type: /** @type {const} */ ('baseline-status'),
118
+ status: baselineTier,
119
+ displayString: displayStatus,
120
+ },
121
+ source: feature.source,
122
+ });
123
+ }
124
+
125
+ /** @type {LH.Audit.Details.Table['headings']} */
126
+ const headings = [
127
+ {
128
+ key: 'featureId',
129
+ valueType: 'link',
130
+ label: str_(UIStrings.columnFeature),
131
+ },
132
+ {
133
+ key: 'displayStatus',
134
+ valueType: 'baseline-status',
135
+ label: str_(UIStrings.columnStatus),
136
+ },
137
+ {
138
+ key: 'source',
139
+ valueType: 'source-location',
140
+ label: str_(i18n.UIStrings.columnSource),
141
+ },
142
+ ];
143
+
144
+ /**
145
+ * Determines the sorting rank of a baseline status.
146
+ * @param {string} status The display status string.
147
+ * @return {number} The numerical rank (1 is the highest priority).
148
+ */
149
+ const getStatusRank = (status) => {
150
+ if (status.startsWith('Limited')) {
151
+ return 1;
152
+ }
153
+ if (status.startsWith('Newly')) {
154
+ return 2;
155
+ }
156
+ if (status.startsWith('Widely')) {
157
+ return 3;
158
+ }
159
+ return 4;
160
+ };
161
+
162
+ const sortedStatuses = baselineStatus.sort((featureA, featureB) => {
163
+ const rankA = getStatusRank(featureA.displayStatus.displayString);
164
+ const rankB = getStatusRank(featureB.displayStatus.displayString);
165
+
166
+ if (rankA !== rankB) {
167
+ return rankA - rankB;
168
+ }
169
+
170
+ const hasSourceA = !!featureA.source;
171
+ const hasSourceB = !!featureB.source;
172
+
173
+ if (hasSourceA !== hasSourceB) {
174
+ return hasSourceA ? -1 : 1;
175
+ }
176
+
177
+ return 0;
178
+ });
179
+
180
+ const details = Audit.makeTableDetails(headings, sortedStatuses);
181
+
182
+ return {
183
+ score: 1,
184
+ details,
185
+ };
186
+ }
187
+ }
188
+
189
+ export default Baseline;
190
+ export {UIStrings};
@@ -41,8 +41,11 @@ class DocumentUrls {
41
41
  }
42
42
  if (!requestedUrl || !mainDocumentUrl) throw new Error('No main frame navigations found');
43
43
 
44
- const initialRequest =
45
- Lantern.Core.NetworkAnalyzer.findResourceForUrl(networkRecords, requestedUrl);
44
+ const initialRequest = Lantern.Core.NetworkAnalyzer.findResourceForUrl(
45
+ // @ts-expect-error - trace engine types for InitiatorType are outdated
46
+ networkRecords,
47
+ requestedUrl
48
+ );
46
49
  if (initialRequest?.redirects?.length) requestedUrl = initialRequest.redirects[0].url;
47
50
 
48
51
  return {requestedUrl, mainDocumentUrl};
@@ -23,17 +23,21 @@ class MainResource {
23
23
  if (!mainDocumentUrl) throw new Error('mainDocumentUrl must exist to get the main resource');
24
24
  const records = await NetworkRecords.request(data.devtoolsLog, context);
25
25
 
26
- // We could have more than one record matching the main doucment url,
26
+ // We could have more than one record matching the main document url,
27
27
  // if the page did `location.reload()`. Since `mainDocumentUrl` refers to the _last_
28
28
  // document request, we should return the last candidate here. Besides, the browser
29
29
  // would have evicted the first request by the time `MainDocumentRequest` (a consumer
30
30
  // of this computed artifact) attempts to fetch the contents, resulting in a protocol error.
31
- const mainResource =
32
- Lantern.Core.NetworkAnalyzer.findLastDocumentForUrl(records, mainDocumentUrl);
31
+ const mainResource = Lantern.Core.NetworkAnalyzer.findLastDocumentForUrl(
32
+ // @ts-expect-error - trace engine types for InitiatorType are outdated
33
+ records,
34
+ mainDocumentUrl
35
+ );
33
36
  if (!mainResource) {
34
37
  throw new Error('Unable to identify the main resource');
35
38
  }
36
39
 
40
+ // @ts-expect-error - Return type is typed as Lantern request by trace engine, but it is a raw record at runtime since we passed raw records.
37
41
  return mainResource;
38
42
  }
39
43
  }
@@ -18,7 +18,10 @@ class NetworkAnalysis {
18
18
  */
19
19
  static async compute_(devtoolsLog, context) {
20
20
  const records = await NetworkRecords.request(devtoolsLog, context);
21
- const analysis = Lantern.Core.NetworkAnalyzer.analyze(records);
21
+ const analysis = Lantern.Core.NetworkAnalyzer.analyze(
22
+ // @ts-expect-error - trace engine types for InitiatorType are outdated
23
+ records
24
+ );
22
25
  if (!analysis) {
23
26
  log.error('NetworkAnalysis', 'Network analysis failed due to lack of transfer data');
24
27
  return {
@@ -266,6 +266,7 @@ const defaultConfig = {
266
266
  'dobetterweb/js-libraries',
267
267
  'dobetterweb/notification-on-start',
268
268
  'dobetterweb/paste-preventing-inputs',
269
+ 'baseline',
269
270
  'seo/meta-description',
270
271
  'seo/http-status-code',
271
272
  'seo/link-text',
@@ -559,6 +560,7 @@ const defaultConfig = {
559
560
  // Browser Compatibility
560
561
  {id: 'doctype', weight: 1, group: 'best-practices-browser-compat'},
561
562
  {id: 'charset', weight: 1, group: 'best-practices-browser-compat'},
563
+ {id: 'baseline', weight: 0, group: 'best-practices-browser-compat'},
562
564
  // General Group
563
565
  {id: 'js-libraries', weight: 0, group: 'best-practices-general'},
564
566
  {id: 'deprecations', weight: 5, group: 'best-practices-general'},
@@ -387,11 +387,21 @@ async function isPageHung(session) {
387
387
  await session.sendCommand('Runtime.evaluate', {
388
388
  expression: '"ping"',
389
389
  returnByValue: true,
390
- timeout: 1000,
391
390
  });
392
391
 
393
392
  return false;
394
393
  } catch (err) {
394
+ // If the session has crashed, we want to rethrow that error instead of assuming it's a hang.
395
+ // session.sendCommand normally handles this, but if PROTOCOL_TIMEOUT wins the race,
396
+ // we might have missed the TARGET_CRASHED error.
397
+ try {
398
+ // Check if it's already crashed.
399
+ await Promise.race([session.onCrashPromise(), Promise.resolve()]);
400
+ } catch (crashErr) {
401
+ if (crashErr.code === 'TARGET_CRASHED') throw crashErr;
402
+ }
403
+
404
+ if (err.code === 'TARGET_CRASHED') throw err;
395
405
  return true;
396
406
  }
397
407
  }
@@ -61,6 +61,9 @@ class Trace extends BaseGatherer {
61
61
  'disabled-by-default-devtools.v8-source-rundown-sources',
62
62
  'disabled-by-default-devtools.v8-source-rundown',
63
63
 
64
+ // Required for Baseline Audit to detect feature usage.
65
+ 'blink.webdx_feature_usage',
66
+
64
67
  // Not used by Lighthouse (yet) but included for users that want JS samples when looking at
65
68
  // a trace collected by Lighthouse (e.g. "View Trace" workflow in DevTools)
66
69
  // TODO: Re-enable after investigating b/325659693
@@ -0,0 +1,3 @@
1
+ {
2
+ "date": "2026-03-18"
3
+ }