lighthouse 10.0.2-dev.20230321 → 10.0.2-dev.20230322

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.
@@ -225,7 +225,7 @@ class Audit {
225
225
  const lineNumber = lineIndex + 1;
226
226
  /** @type LH.Audit.Details.SnippetValue['lines'][0] */
227
227
  const lineDetail = {
228
- content: line.slice(0, maxLineLength),
228
+ content: Util.truncate(line, maxLineLength),
229
229
  lineNumber,
230
230
  };
231
231
  if (line.length > maxLineLength) {
@@ -26,6 +26,8 @@ export type ByteEfficiencyProduct = {
26
26
  export class ByteEfficiencyAudit extends Audit {
27
27
  /**
28
28
  * Creates a score based on the wastedMs value using linear interpolation between control points.
29
+ * A negative wastedMs is scored as 1, assuming time is not being wasted with respect to the
30
+ * opportunity being measured.
29
31
  *
30
32
  * @param {number} wastedMs
31
33
  * @return {number}
@@ -39,12 +39,14 @@ const WASTED_MS_FOR_SCORE_OF_ZERO = 5000;
39
39
  class ByteEfficiencyAudit extends Audit {
40
40
  /**
41
41
  * Creates a score based on the wastedMs value using linear interpolation between control points.
42
+ * A negative wastedMs is scored as 1, assuming time is not being wasted with respect to the
43
+ * opportunity being measured.
42
44
  *
43
45
  * @param {number} wastedMs
44
46
  * @return {number}
45
47
  */
46
48
  static scoreForWastedMs(wastedMs) {
47
- if (wastedMs === 0) {
49
+ if (wastedMs <= 0) {
48
50
  return 1;
49
51
  } else if (wastedMs < WASTED_MS_FOR_AVERAGE) {
50
52
  return linearInterpolation(0, 1, WASTED_MS_FOR_AVERAGE, 0.75, wastedMs);
@@ -214,6 +216,8 @@ class ByteEfficiencyAudit extends Audit {
214
216
 
215
217
  const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
216
218
 
219
+ // `wastedMs` may be negative, if making the opportunity change could be detrimental.
220
+ // This is useful information in the LHR and should be preserved.
217
221
  let wastedMs;
218
222
  if (gatherContext.gatherMode === 'navigation') {
219
223
  if (!graph) throw Error('Page dependency graph should always be computed in navigation mode');
@@ -8,6 +8,7 @@ import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
8
8
  import * as i18n from '../../lib/i18n/i18n.js';
9
9
  import {computeJSTokenLength as computeTokenLength} from '../../lib/minification-estimator.js';
10
10
  import {getRequestForScript, isInline} from '../../lib/script-helpers.js';
11
+ import {Util} from '../../../shared/util.js';
11
12
 
12
13
  const UIStrings = {
13
14
  /** Imperative title of a Lighthouse audit that tells the user to minify the page’s JS code to reduce file size. This is displayed in a list of audit titles that Lighthouse generates. */
@@ -84,7 +85,7 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
84
85
  const networkRecord = getRequestForScript(networkRecords, script);
85
86
 
86
87
  const displayUrl = isInline(script) ?
87
- `inline: ${script.content.substring(0, 40)}...` :
88
+ `inline: ${Util.truncate(script.content, 40)}` :
88
89
  script.url;
89
90
  try {
90
91
  const result = UnminifiedJavaScript.computeWaste(script.content, displayUrl, networkRecord);
@@ -67,10 +67,11 @@ class Doctype extends Audit {
67
67
  const doctypeSystemId = artifacts.Doctype.systemId;
68
68
  const compatMode = artifacts.Doctype.documentCompatMode;
69
69
 
70
+ const trace = artifacts.traces?.[Audit.DEFAULT_PASS];
71
+
70
72
  /** @type {LH.Crdp.Audits.QuirksModeIssueDetails[]} */
71
73
  let quirksModeIssues = [];
72
- if (artifacts.traces && artifacts.InspectorIssues) {
73
- const trace = artifacts.traces[Audit.DEFAULT_PASS];
74
+ if (trace && artifacts.InspectorIssues) {
74
75
  const processedTrace = await ProcessedTrace.request(trace, context);
75
76
  const mainFrameId = processedTrace.mainFrameInfo.frameId;
76
77
  quirksModeIssues =
@@ -13,17 +13,16 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
13
13
  totalCumulativeLayoutShift: number;
14
14
  }>;
15
15
  };
16
- /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
17
16
  declare class CumulativeLayoutShift {
18
17
  /**
19
18
  * Returns all LayoutShift events that had no recent input.
20
19
  * Only a `weightedScore` per event is returned. For non-main-frame events, this is
21
20
  * the only score that matters. For main-frame events, `weighted_score_delta === score`.
22
21
  * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
23
- * @param {LH.TraceEvent[]} traceEvents
22
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
24
23
  * @return {Array<LayoutShiftEvent>}
25
24
  */
26
- static getLayoutShiftEvents(traceEvents: LH.TraceEvent[]): Array<LayoutShiftEvent>;
25
+ static getLayoutShiftEvents(processedTrace: LH.Artifacts.ProcessedTrace): Array<LayoutShiftEvent>;
27
26
  /**
28
27
  * Calculates cumulative layout shifts per cluster (session) of LayoutShift
29
28
  * events -- where a new cluster is created when there's a gap of more than
@@ -50,4 +49,5 @@ declare class CumulativeLayoutShift {
50
49
  totalCumulativeLayoutShift: number;
51
50
  }>;
52
51
  }
52
+ import { ProcessedTrace } from "../processed-trace.js";
53
53
  //# sourceMappingURL=cumulative-layout-shift.d.ts.map
@@ -9,16 +9,18 @@ import {ProcessedTrace} from '../processed-trace.js';
9
9
 
10
10
  /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
11
11
 
12
+ const RECENT_INPUT_WINDOW = 500;
13
+
12
14
  class CumulativeLayoutShift {
13
15
  /**
14
16
  * Returns all LayoutShift events that had no recent input.
15
17
  * Only a `weightedScore` per event is returned. For non-main-frame events, this is
16
18
  * the only score that matters. For main-frame events, `weighted_score_delta === score`.
17
19
  * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
18
- * @param {LH.TraceEvent[]} traceEvents
20
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
19
21
  * @return {Array<LayoutShiftEvent>}
20
22
  */
21
- static getLayoutShiftEvents(traceEvents) {
23
+ static getLayoutShiftEvents(processedTrace) {
22
24
  const layoutShiftEvents = [];
23
25
 
24
26
  // Chromium will set `had_recent_input` if there was recent user input, which
@@ -26,9 +28,20 @@ class CumulativeLayoutShift {
26
28
  // Lighthouse changes the emulation size. This results in the first few shift
27
29
  // events having `had_recent_input` set, so ignore it for those events.
28
30
  // See https://bugs.chromium.org/p/chromium/issues/detail?id=1094974.
29
- let ignoreHadRecentInput = true;
31
+ let mustRespectHadRecentInput = false;
32
+
33
+ // Even if emulation was applied before navigating, Chrome will issue a viewport
34
+ // change event after a navigation starts which is treated as an interaction when
35
+ // deciding the `had_recent_input` flag. Anything within 500ms of this event should
36
+ // always be counted for CLS regardless of the `had_recent_input` flag.
37
+ // See https://bugs.chromium.org/p/chromium/issues/detail?id=1302667
38
+ let viewportChangeTs = processedTrace.timestamps.timeOrigin;
39
+ const firstViewportEvent = processedTrace.frameEvents.find(event => event.name === 'viewport');
40
+ if (firstViewportEvent) {
41
+ viewportChangeTs = firstViewportEvent.ts;
42
+ }
30
43
 
31
- for (const event of traceEvents) {
44
+ for (const event of processedTrace.frameTreeEvents) {
32
45
  if (event.name !== 'LayoutShift' ||
33
46
  !event.args.data ||
34
47
  event.args.data.is_main_frame === undefined) {
@@ -42,11 +55,10 @@ class CumulativeLayoutShift {
42
55
  }
43
56
 
44
57
  if (event.args.data.had_recent_input) {
45
- // `had_recent_input` events aren't used unless currently ignoring.
46
- if (!ignoreHadRecentInput) continue;
58
+ const timing = (event.ts - viewportChangeTs) / 1000;
59
+ if (timing > RECENT_INPUT_WINDOW || mustRespectHadRecentInput) continue;
47
60
  } else {
48
- // After a false `had_recent_input`, stop ignoring property.
49
- ignoreHadRecentInput = false;
61
+ mustRespectHadRecentInput = true;
50
62
  }
51
63
 
52
64
  layoutShiftEvents.push({
@@ -106,7 +118,7 @@ class CumulativeLayoutShift {
106
118
  const processedTrace = await ProcessedTrace.request(trace, context);
107
119
 
108
120
  const allFrameShiftEvents =
109
- CumulativeLayoutShift.getLayoutShiftEvents(processedTrace.frameTreeEvents);
121
+ CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
110
122
  const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
111
123
 
112
124
  // The original Cumulative Layout Shift metric, the sum of all main-frame shift events.
@@ -7,6 +7,7 @@
7
7
  import {makeComputedArtifact} from './computed-artifact.js';
8
8
  import {ByteEfficiencyAudit} from '../audits/byte-efficiency/byte-efficiency-audit.js';
9
9
  import {NetworkRecords} from './network-records.js';
10
+ import {Util} from '../../shared/util.js';
10
11
 
11
12
  const PREVIEW_LENGTH = 100;
12
13
 
@@ -87,8 +88,7 @@ class UnusedCSS {
87
88
  * @return {string}
88
89
  */
89
90
  static determineContentPreview(content) {
90
- let preview = (content || '')
91
- .slice(0, PREVIEW_LENGTH * 5)
91
+ let preview = Util.truncate(content || '', PREVIEW_LENGTH * 5, '')
92
92
  .replace(/( {2,}|\t)+/g, ' ') // remove leading indentation if present
93
93
  .replace(/\n\s+}/g, '\n}') // completely remove indentation of closing braces
94
94
  .trim(); // trim the leading whitespace
@@ -101,16 +101,17 @@ class UnusedCSS {
101
101
  firstRuleStart > firstRuleEnd ||
102
102
  firstRuleStart > PREVIEW_LENGTH) {
103
103
  // We couldn't determine the first rule-set or it's not within the preview
104
- preview = preview.slice(0, PREVIEW_LENGTH) + '...';
104
+ preview = Util.truncate(preview, PREVIEW_LENGTH);
105
105
  } else if (firstRuleEnd < PREVIEW_LENGTH) {
106
106
  // The entire first rule-set fits within the preview
107
- preview = preview.slice(0, firstRuleEnd + 1) + ' ...';
107
+ preview = preview.slice(0, firstRuleEnd + 1) + ' ';
108
108
  } else {
109
109
  // The first rule-set doesn't fit within the preview, just show as many as we can
110
- const lastSemicolonIndex = preview.slice(0, PREVIEW_LENGTH).lastIndexOf(';');
110
+ const truncated = Util.truncate(preview, PREVIEW_LENGTH, '');
111
+ const lastSemicolonIndex = truncated.lastIndexOf(';');
111
112
  preview = lastSemicolonIndex < firstRuleStart ?
112
- preview.slice(0, PREVIEW_LENGTH) + '... } ...' :
113
- preview.slice(0, lastSemicolonIndex + 1) + ' ... } ...';
113
+ truncated + ' } ' :
114
+ preview.slice(0, lastSemicolonIndex + 1) + ' } ';
114
115
  }
115
116
  }
116
117
 
@@ -167,10 +167,23 @@ async function prepareTargetForTimespanMode(driver, settings) {
167
167
  disableThrottling: false,
168
168
  blockedUrlPatterns: undefined,
169
169
  });
170
+ await warmUpIntlSegmenter(driver);
170
171
 
171
172
  log.timeEnd(status);
172
173
  }
173
174
 
175
+ /**
176
+ * Ensure the `Intl.Segmenter` created in `pageFunctions.truncate` is cached by v8 before
177
+ * recording the trace begins.
178
+ *
179
+ * @param {LH.Gatherer.FRTransitionalDriver} driver
180
+ */
181
+ async function warmUpIntlSegmenter(driver) {
182
+ await driver.executionContext.evaluate(pageFunctions.truncate, {
183
+ args: ['aaa', 2],
184
+ });
185
+ }
186
+
174
187
  /**
175
188
  * Prepares a target to be analyzed in navigation mode by enabling protocol domains, emulation, and new document
176
189
  * handlers for global APIs or error handling.
@@ -197,6 +210,8 @@ async function prepareTargetForNavigationMode(driver, settings) {
197
210
  await shimRequestIdleCallbackOnNewDocument(driver, settings);
198
211
  }
199
212
 
213
+ await warmUpIntlSegmenter(driver);
214
+
200
215
  log.timeEnd(status);
201
216
  }
202
217
 
@@ -10,16 +10,12 @@ export namespace pageFunctions {
10
10
  export { isPositionFixed };
11
11
  export { wrapRequestIdleCallback };
12
12
  export { getBoundingClientRect };
13
+ export { truncate };
13
14
  }
14
15
  /**
15
16
  * `typed-query-selector`'s CSS selector parser.
16
17
  */
17
18
  export type ParseSelector<T extends string> = import('typed-query-selector/parser').ParseSelector<T>;
18
- /**
19
- * @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
20
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
21
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
22
- */
23
19
  /**
24
20
  * @fileoverview
25
21
  * Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
@@ -65,6 +61,9 @@ declare function getElementsInDocument<T extends string>(selector: T): import("t
65
61
  * @return {string}
66
62
  */
67
63
  declare function getOuterHTMLSnippet(element: Element | ShadowRoot, ignoreAttrs?: Array<string> | undefined, snippetCharacterLimit?: number): string;
64
+ declare namespace getOuterHTMLSnippet {
65
+ function toString(): string;
66
+ }
68
67
  /**
69
68
  * Computes a memory/CPU performance benchmark index to determine rough device class.
70
69
  * @see https://github.com/GoogleChrome/lighthouse/issues/9085
@@ -125,6 +124,9 @@ declare function getNodeSelector(element: Element): string;
125
124
  * @return {string | null}
126
125
  */
127
126
  declare function getNodeLabel(element: Element): string | null;
127
+ declare namespace getNodeLabel {
128
+ function toString(): string;
129
+ }
128
130
  /**
129
131
  * This function checks if an element or an ancestor of an element is `position:fixed`.
130
132
  * In addition we ensure that the element is capable of behaving as a `position:fixed`
@@ -145,5 +147,15 @@ declare function wrapRequestIdleCallback(cpuSlowdownMultiplier: number): void;
145
147
  * @return {LH.Artifacts.Rect}
146
148
  */
147
149
  declare function getBoundingClientRect(element: Element): LH.Artifacts.Rect;
150
+ /**
151
+ *
152
+ * @param {string} string
153
+ * @param {number} characterLimit
154
+ * @return {string}
155
+ */
156
+ declare function truncate(string: string, characterLimit: number): string;
157
+ declare namespace truncate {
158
+ function toString(): string;
159
+ }
148
160
  export {};
149
161
  //# sourceMappingURL=page-functions.d.ts.map
@@ -4,6 +4,8 @@
4
4
  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
5
  */
6
6
 
7
+ import {Util} from '../../shared/util.js';
8
+
7
9
  /**
8
10
  * @fileoverview
9
11
  * Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
@@ -132,10 +134,9 @@ function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit =
132
134
  }
133
135
 
134
136
  // Elide attribute value if too long.
135
- if (attributeValue.length > ATTRIBUTE_CHAR_LIMIT) {
136
- attributeValue = attributeValue.slice(0, ATTRIBUTE_CHAR_LIMIT - 1) + '…';
137
- dirty = true;
138
- }
137
+ const truncatedString = truncate(attributeValue, ATTRIBUTE_CHAR_LIMIT);
138
+ if (truncatedString !== attributeValue) dirty = true;
139
+ attributeValue = truncatedString;
139
140
 
140
141
  if (dirty) {
141
142
  // Style attributes can be blocked by the CSP if they are set via `setAttribute`.
@@ -372,22 +373,6 @@ function isPositionFixed(element) {
372
373
  * @return {string | null}
373
374
  */
374
375
  function getNodeLabel(element) {
375
- // Inline so that audits that import getNodeLabel don't
376
- // also need to import truncate
377
- /**
378
- * @param {string} str
379
- * @param {number} maxLength
380
- * @return {string}
381
- */
382
- function truncate(str, maxLength) {
383
- if (str.length <= maxLength) {
384
- return str;
385
- }
386
- // Take advantage of string iterator multi-byte character awareness.
387
- // Regular `.slice` will ignore unicode character boundaries and lead to malformed text.
388
- return Array.from(str).slice(0, maxLength - 1).join('') + '…';
389
- }
390
-
391
376
  const tagName = element.tagName.toLowerCase();
392
377
  // html and body content is too broad to be useful, since they contain all page content
393
378
  if (tagName !== 'html' && tagName !== 'body') {
@@ -518,14 +503,47 @@ function getNodeDetails(element) {
518
503
  return details;
519
504
  }
520
505
 
506
+ /**
507
+ *
508
+ * @param {string} string
509
+ * @param {number} characterLimit
510
+ * @return {string}
511
+ */
512
+ function truncate(string, characterLimit) {
513
+ return Util.truncate(string, characterLimit);
514
+ }
515
+
516
+ /** @type {string} */
517
+ const truncateRawString = truncate.toString();
518
+ truncate.toString = () => `function truncate(string, characterLimit) {
519
+ const Util = { ${Util.truncate} };
520
+ return (${truncateRawString})(string, characterLimit);
521
+ }`;
522
+
523
+ /** @type {string} */
524
+ const getNodeLabelRawString = getNodeLabel.toString();
525
+ getNodeLabel.toString = () => `function getNodeLabel(element) {
526
+ ${truncate};
527
+ return (${getNodeLabelRawString})(element);
528
+ }`;
529
+
530
+ /** @type {string} */
531
+ const getOuterHTMLSnippetRawString = getOuterHTMLSnippet.toString();
532
+ // eslint-disable-next-line max-len
533
+ getOuterHTMLSnippet.toString = () => `function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit = 500) {
534
+ ${truncate};
535
+ return (${getOuterHTMLSnippetRawString})(element, ignoreAttrs, snippetCharacterLimit);
536
+ }`;
537
+
521
538
  /** @type {string} */
522
539
  const getNodeDetailsRawString = getNodeDetails.toString();
523
540
  getNodeDetails.toString = () => `function getNodeDetails(element) {
541
+ ${truncate};
524
542
  ${getNodePath};
525
543
  ${getNodeSelector};
526
544
  ${getBoundingClientRect};
527
- ${getOuterHTMLSnippet};
528
- ${getNodeLabel};
545
+ ${getOuterHTMLSnippetRawString};
546
+ ${getNodeLabelRawString};
529
547
  return (${getNodeDetailsRawString})(element);
530
548
  }`;
531
549
 
@@ -541,4 +559,5 @@ export const pageFunctions = {
541
559
  isPositionFixed,
542
560
  wrapRequestIdleCallback,
543
561
  getBoundingClientRect,
562
+ truncate,
544
563
  };
@@ -179,6 +179,14 @@ export class TraceProcessor {
179
179
  * @return {evt is LCPCandidateEvent}
180
180
  */
181
181
  static isLCPCandidateEvent(evt: LH.TraceEvent): evt is LCPCandidateEvent;
182
+ /**
183
+ * The associated frame ID is set in different locations for different trace events.
184
+ * This function checks all known locations for the frame ID and returns `undefined` if it's not found.
185
+ *
186
+ * @param {LH.TraceEvent} evt
187
+ * @return {string|undefined}
188
+ */
189
+ static getFrameId(evt: LH.TraceEvent): string | undefined;
182
190
  /**
183
191
  * Returns the maximum LCP event across all frames in `events`.
184
192
  * Sets `invalidated` flag if LCP of every frame is invalidated.
@@ -567,6 +567,19 @@ class TraceProcessor {
567
567
  );
568
568
  }
569
569
 
570
+ /**
571
+ * The associated frame ID is set in different locations for different trace events.
572
+ * This function checks all known locations for the frame ID and returns `undefined` if it's not found.
573
+ *
574
+ * @param {LH.TraceEvent} evt
575
+ * @return {string|undefined}
576
+ */
577
+ static getFrameId(evt) {
578
+ return evt.args?.data?.frame ||
579
+ evt.args.data?.frameID ||
580
+ evt.args.frame;
581
+ }
582
+
570
583
  /**
571
584
  * Returns the maximum LCP event across all frames in `events`.
572
585
  * Sets `invalidated` flag if LCP of every frame is invalidated.
@@ -712,13 +725,13 @@ class TraceProcessor {
712
725
  // Filter to just events matching the main frame ID, just to make sure.
713
726
  /** @param {LH.TraceEvent} e */
714
727
  function associatedToMainFrame(e) {
715
- const frameId = e.args?.data?.frame || e.args.frame;
728
+ const frameId = TraceProcessor.getFrameId(e);
716
729
  return frameId === mainFrameInfo.frameId;
717
730
  }
718
731
 
719
732
  /** @param {LH.TraceEvent} e */
720
733
  function associatedToAllFrames(e) {
721
- const frameId = e.args?.data?.frame || e.args.frame;
734
+ const frameId = TraceProcessor.getFrameId(e);
722
735
  return frameId ? inspectedTreeFrameIds.includes(frameId) : false;
723
736
  }
724
737
  const frameEvents = keyEvents.filter(e => associatedToMainFrame(e));
@@ -138,7 +138,7 @@ class UrlUtils {
138
138
  static elideDataURI(url) {
139
139
  try {
140
140
  const parsed = new URL(url);
141
- return parsed.protocol === 'data:' ? url.slice(0, 100) : url;
141
+ return parsed.protocol === 'data:' ? Util.truncate(url, 100) : url;
142
142
  } catch (e) {
143
143
  return url;
144
144
  }
@@ -153,6 +153,39 @@ class Util {
153
153
  return segments;
154
154
  }
155
155
 
156
+ /**
157
+ * @param {string} string
158
+ * @param {number} characterLimit
159
+ * @param {string} ellipseSuffix
160
+ */
161
+ static truncate(string, characterLimit, ellipseSuffix = '…') {
162
+ // Early return for the case where there are fewer bytes than the character limit.
163
+ if (string.length <= characterLimit) {
164
+ return string;
165
+ }
166
+
167
+ const segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
168
+ const iterator = segmenter.segment(string)[Symbol.iterator]();
169
+
170
+ let lastSegmentIndex = 0;
171
+ for (let i = 0; i <= characterLimit - ellipseSuffix.length; i++) {
172
+ const result = iterator.next();
173
+ if (result.done) {
174
+ return string;
175
+ }
176
+
177
+ lastSegmentIndex = result.value.index;
178
+ }
179
+
180
+ for (let i = 0; i < ellipseSuffix.length; i++) {
181
+ if (iterator.next().done) {
182
+ return string;
183
+ }
184
+ }
185
+
186
+ return string.slice(0, lastSegmentIndex) + ellipseSuffix;
187
+ }
188
+
156
189
  /**
157
190
  * @param {URL} parsedUrl
158
191
  * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
@@ -14,7 +14,7 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */const oe="…",re={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},se=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class le{static get RATINGS(){return re}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const a=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const t=n[e];if(!t)continue;const i=e%2!=0;a.push({isCode:i,text:t})}return a}static splitMarkdownLink(e){const a=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,t,i]=n.splice(0,3);e&&a.push({isLink:!1,text:e}),t&&i&&a.push({isLink:!0,text:t,linkHref:i})}return a}static getURLDisplayName(e,a){const n=void 0!==(a=a||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?a.numPathParts:2,t=void 0===a.preserveQuery||a.preserveQuery,i=a.preserveHost||!1;let o;if("about:"===e.protocol||"data:"===e.protocol)o=e.href;else{o=e.pathname;const a=o.split("/").filter((e=>e.length));n&&a.length>n&&(o=oe+a.slice(-1*n).join("/")),i&&(o=`${e.host}/${o.replace(/^\//,"")}`),t&&(o=`${o}${e.search}`)}if("data:"!==e.protocol&&(o=o.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),o=o.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),o=o.replace(/(\d{3})\d{6,}/g,"$1…"),o=o.replace(/\u2026+/g,oe),o.length>64&&o.includes("?")&&(o=o.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),o.length>64&&(o=o.replace(/\?.*/,"?…")))),o.length>64){const e=o.lastIndexOf(".");o=e>=0?o.slice(0,63-(o.length-e))+`…${o.slice(e)}`:o.slice(0,63)+oe}return o}static parseURL(e){const a=new URL(e);return{file:le.getURLDisplayName(a),hostname:a.hostname,origin:a.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const a=e.split(".").slice(-2);return se.includes(a[0])?`.${a.join(".")}`:`.${a[a.length-1]}`}static getRootDomain(e){const a=le.createOrReturnURL(e).hostname,n=le.getTld(a).split(".");return a.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,a,n){if(0===a.length)return e.slice(0,2*n+1);const t=new Set;return(a=a.sort(((e,a)=>(e.lineNumber||0)-(a.lineNumber||0)))).forEach((({lineNumber:e})=>{let a=e-n,i=e+n;for(;a<1;)a++,i++;t.has(a-3-1)&&(a-=3);for(let e=a;e<=i;e++){const a=e;t.add(a)}})),e.filter((e=>t.has(e.lineNumber)))}}
17
+ */const oe="…",re={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},se=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class le{static get RATINGS(){return re}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const a=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const t=n[e];if(!t)continue;const i=e%2!=0;a.push({isCode:i,text:t})}return a}static splitMarkdownLink(e){const a=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,t,i]=n.splice(0,3);e&&a.push({isLink:!1,text:e}),t&&i&&a.push({isLink:!0,text:t,linkHref:i})}return a}static truncate(e,a,n="…"){if(e.length<=a)return e;const t=new Intl.Segmenter(void 0,{granularity:"grapheme"}).segment(e)[Symbol.iterator]();let i=0;for(let o=0;o<=a-n.length;o++){const a=t.next();if(a.done)return e;i=a.value.index}for(let a=0;a<n.length;a++)if(t.next().done)return e;return e.slice(0,i)+n}static getURLDisplayName(e,a){const n=void 0!==(a=a||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?a.numPathParts:2,t=void 0===a.preserveQuery||a.preserveQuery,i=a.preserveHost||!1;let o;if("about:"===e.protocol||"data:"===e.protocol)o=e.href;else{o=e.pathname;const a=o.split("/").filter((e=>e.length));n&&a.length>n&&(o=oe+a.slice(-1*n).join("/")),i&&(o=`${e.host}/${o.replace(/^\//,"")}`),t&&(o=`${o}${e.search}`)}if("data:"!==e.protocol&&(o=o.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),o=o.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),o=o.replace(/(\d{3})\d{6,}/g,"$1…"),o=o.replace(/\u2026+/g,oe),o.length>64&&o.includes("?")&&(o=o.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),o.length>64&&(o=o.replace(/\?.*/,"?…")))),o.length>64){const e=o.lastIndexOf(".");o=e>=0?o.slice(0,63-(o.length-e))+`…${o.slice(e)}`:o.slice(0,63)+oe}return o}static parseURL(e){const a=new URL(e);return{file:le.getURLDisplayName(a),hostname:a.hostname,origin:a.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const a=e.split(".").slice(-2);return se.includes(a[0])?`.${a.join(".")}`:`.${a[a.length-1]}`}static getRootDomain(e){const a=le.createOrReturnURL(e).hostname,n=le.getTld(a).split(".");return a.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,a,n){if(0===a.length)return e.slice(0,2*n+1);const t=new Set;return(a=a.sort(((e,a)=>(e.lineNumber||0)-(a.lineNumber||0)))).forEach((({lineNumber:e})=>{let a=e-n,i=e+n;for(;a<1;)a++,i++;t.has(a-3-1)&&(a-=3);for(let e=a;e<=i;e++){const a=e;t.add(a)}})),e.filter((e=>t.has(e.lineNumber)))}}
18
18
  /**
19
19
  * @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
20
20
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
@@ -14,7 +14,7 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */const e="…",t={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},n=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class r{static get RATINGS(){return t}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const t=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const r=n[e];if(!r)continue;const o=e%2!=0;t.push({isCode:o,text:r})}return t}static splitMarkdownLink(e){const t=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,r,o]=n.splice(0,3);e&&t.push({isLink:!1,text:e}),r&&o&&t.push({isLink:!0,text:r,linkHref:o})}return t}static getURLDisplayName(t,n){const r=void 0!==(n=n||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?n.numPathParts:2,o=void 0===n.preserveQuery||n.preserveQuery,i=n.preserveHost||!1;let a;if("about:"===t.protocol||"data:"===t.protocol)a=t.href;else{a=t.pathname;const n=a.split("/").filter((e=>e.length));r&&n.length>r&&(a=e+n.slice(-1*r).join("/")),i&&(a=`${t.host}/${a.replace(/^\//,"")}`),o&&(a=`${a}${t.search}`)}if("data:"!==t.protocol&&(a=a.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),a=a.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),a=a.replace(/(\d{3})\d{6,}/g,"$1…"),a=a.replace(/\u2026+/g,e),a.length>64&&a.includes("?")&&(a=a.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),a.length>64&&(a=a.replace(/\?.*/,"?…")))),a.length>64){const t=a.lastIndexOf(".");a=t>=0?a.slice(0,63-(a.length-t))+`…${a.slice(t)}`:a.slice(0,63)+e}return a}static parseURL(e){const t=new URL(e);return{file:r.getURLDisplayName(t),hostname:t.hostname,origin:t.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const t=e.split(".").slice(-2);return n.includes(t[0])?`.${t.join(".")}`:`.${t[t.length-1]}`}static getRootDomain(e){const t=r.createOrReturnURL(e).hostname,n=r.getTld(t).split(".");return t.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,t,n){if(0===t.length)return e.slice(0,2*n+1);const r=new Set;return(t=t.sort(((e,t)=>(e.lineNumber||0)-(t.lineNumber||0)))).forEach((({lineNumber:e})=>{let t=e-n,o=e+n;for(;t<1;)t++,o++;r.has(t-3-1)&&(t-=3);for(let e=t;e<=o;e++){const t=e;r.add(t)}})),e.filter((e=>r.has(e.lineNumber)))}}
17
+ */const e="…",t={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},n=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class r{static get RATINGS(){return t}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const t=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const r=n[e];if(!r)continue;const o=e%2!=0;t.push({isCode:o,text:r})}return t}static splitMarkdownLink(e){const t=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,r,o]=n.splice(0,3);e&&t.push({isLink:!1,text:e}),r&&o&&t.push({isLink:!0,text:r,linkHref:o})}return t}static truncate(e,t,n="…"){if(e.length<=t)return e;const r=new Intl.Segmenter(void 0,{granularity:"grapheme"}).segment(e)[Symbol.iterator]();let o=0;for(let i=0;i<=t-n.length;i++){const t=r.next();if(t.done)return e;o=t.value.index}for(let t=0;t<n.length;t++)if(r.next().done)return e;return e.slice(0,o)+n}static getURLDisplayName(t,n){const r=void 0!==(n=n||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?n.numPathParts:2,o=void 0===n.preserveQuery||n.preserveQuery,i=n.preserveHost||!1;let a;if("about:"===t.protocol||"data:"===t.protocol)a=t.href;else{a=t.pathname;const n=a.split("/").filter((e=>e.length));r&&n.length>r&&(a=e+n.slice(-1*r).join("/")),i&&(a=`${t.host}/${a.replace(/^\//,"")}`),o&&(a=`${a}${t.search}`)}if("data:"!==t.protocol&&(a=a.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),a=a.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),a=a.replace(/(\d{3})\d{6,}/g,"$1…"),a=a.replace(/\u2026+/g,e),a.length>64&&a.includes("?")&&(a=a.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),a.length>64&&(a=a.replace(/\?.*/,"?…")))),a.length>64){const t=a.lastIndexOf(".");a=t>=0?a.slice(0,63-(a.length-t))+`…${a.slice(t)}`:a.slice(0,63)+e}return a}static parseURL(e){const t=new URL(e);return{file:r.getURLDisplayName(t),hostname:t.hostname,origin:t.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const t=e.split(".").slice(-2);return n.includes(t[0])?`.${t.join(".")}`:`.${t[t.length-1]}`}static getRootDomain(e){const t=r.createOrReturnURL(e).hostname,n=r.getTld(t).split(".");return t.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,t,n){if(0===t.length)return e.slice(0,2*n+1);const r=new Set;return(t=t.sort(((e,t)=>(e.lineNumber||0)-(t.lineNumber||0)))).forEach((({lineNumber:e})=>{let t=e-n,o=e+n;for(;t<1;)t++,o++;r.has(t-3-1)&&(t-=3);for(let e=t;e<=o;e++){const t=e;r.add(t)}})),e.filter((e=>r.has(e.lineNumber)))}}
18
18
  /**
19
19
  * @license
20
20
  * Copyright 2017 The Lighthouse Authors. All Rights Reserved.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "10.0.2-dev.20230321",
4
+ "version": "10.0.2-dev.20230322",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -2,7 +2,7 @@
2
2
  "extends": "../tsconfig-base.json",
3
3
  "compilerOptions": {
4
4
  // Limit defs to base JS.
5
- "lib": ["es2020"],
5
+ "lib": ["es2020", "es2022.intl"],
6
6
  // Only include `@types/node` from node_modules/.
7
7
  "types": ["node"],
8
8
  // "listFiles": true,
package/shared/util.d.ts CHANGED
@@ -67,6 +67,12 @@ export class Util {
67
67
  isLink: false;
68
68
  text: string;
69
69
  }>;
70
+ /**
71
+ * @param {string} string
72
+ * @param {number} characterLimit
73
+ * @param {string} ellipseSuffix
74
+ */
75
+ static truncate(string: string, characterLimit: number, ellipseSuffix?: string): string;
70
76
  /**
71
77
  * @param {URL} parsedUrl
72
78
  * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
package/shared/util.js CHANGED
@@ -153,6 +153,39 @@ class Util {
153
153
  return segments;
154
154
  }
155
155
 
156
+ /**
157
+ * @param {string} string
158
+ * @param {number} characterLimit
159
+ * @param {string} ellipseSuffix
160
+ */
161
+ static truncate(string, characterLimit, ellipseSuffix = '…') {
162
+ // Early return for the case where there are fewer bytes than the character limit.
163
+ if (string.length <= characterLimit) {
164
+ return string;
165
+ }
166
+
167
+ const segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
168
+ const iterator = segmenter.segment(string)[Symbol.iterator]();
169
+
170
+ let lastSegmentIndex = 0;
171
+ for (let i = 0; i <= characterLimit - ellipseSuffix.length; i++) {
172
+ const result = iterator.next();
173
+ if (result.done) {
174
+ return string;
175
+ }
176
+
177
+ lastSegmentIndex = result.value.index;
178
+ }
179
+
180
+ for (let i = 0; i < ellipseSuffix.length; i++) {
181
+ if (iterator.next().done) {
182
+ return string;
183
+ }
184
+ }
185
+
186
+ return string.slice(0, lastSegmentIndex) + ellipseSuffix;
187
+ }
188
+
156
189
  /**
157
190
  * @param {URL} parsedUrl
158
191
  * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
@@ -974,6 +974,7 @@ export interface TraceEvent {
974
974
  };
975
975
  data?: {
976
976
  frame?: string;
977
+ frameID?: string;
977
978
  processId?: number;
978
979
  isLoadingMainFrame?: boolean;
979
980
  documentLoaderURL?: string;