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