lighthouse 11.1.0-dev.20231003 → 11.1.0-dev.20231005

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 (53) hide show
  1. package/core/audits/audit.js +17 -2
  2. package/core/audits/bf-cache.js +1 -0
  3. package/core/audits/bootup-time.js +4 -1
  4. package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
  5. package/core/audits/byte-efficiency/duplicated-javascript.js +1 -1
  6. package/core/audits/byte-efficiency/efficient-animated-content.js +1 -1
  7. package/core/audits/byte-efficiency/legacy-javascript.js +1 -1
  8. package/core/audits/byte-efficiency/modern-image-formats.js +1 -1
  9. package/core/audits/byte-efficiency/offscreen-images.js +1 -1
  10. package/core/audits/byte-efficiency/render-blocking-resources.js +2 -3
  11. package/core/audits/byte-efficiency/total-byte-weight.js +3 -1
  12. package/core/audits/byte-efficiency/unminified-css.js +1 -1
  13. package/core/audits/byte-efficiency/unminified-javascript.js +1 -1
  14. package/core/audits/byte-efficiency/unused-css-rules.js +1 -1
  15. package/core/audits/byte-efficiency/unused-javascript.js +1 -1
  16. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +2 -7
  17. package/core/audits/byte-efficiency/uses-optimized-images.js +1 -1
  18. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +1 -0
  19. package/core/audits/byte-efficiency/uses-responsive-images.js +1 -1
  20. package/core/audits/byte-efficiency/uses-text-compression.js +1 -1
  21. package/core/audits/dobetterweb/dom-size.js +3 -1
  22. package/core/audits/dobetterweb/no-document-write.js +1 -0
  23. package/core/audits/dobetterweb/uses-http2.js +2 -3
  24. package/core/audits/dobetterweb/uses-passive-event-listeners.js +1 -0
  25. package/core/audits/font-display.js +1 -0
  26. package/core/audits/largest-contentful-paint-element.js +3 -2
  27. package/core/audits/layout-shift-elements.js +8 -4
  28. package/core/audits/lcp-lazy-loaded.js +1 -0
  29. package/core/audits/long-tasks.js +8 -1
  30. package/core/audits/mainthread-work-breakdown.js +3 -1
  31. package/core/audits/prioritize-lcp-image.js +2 -3
  32. package/core/audits/redirects.js +2 -3
  33. package/core/audits/server-response-time.js +2 -0
  34. package/core/audits/third-party-facades.js +1 -0
  35. package/core/audits/third-party-summary.js +5 -1
  36. package/core/audits/unsized-images.js +1 -1
  37. package/core/audits/uses-rel-preconnect.js +2 -3
  38. package/core/audits/uses-rel-preload.js +2 -3
  39. package/core/audits/viewport.js +1 -0
  40. package/core/audits/work-during-interaction.js +6 -2
  41. package/core/gather/session.js +2 -0
  42. package/dist/report/bundle.esm.js +1 -1
  43. package/dist/report/flow.js +15 -16
  44. package/dist/report/standalone.js +1 -1
  45. package/package.json +6 -6
  46. package/readme.md +1 -1
  47. package/report/renderer/performance-category-renderer.js +15 -11
  48. package/shared/localization/format.d.ts +21 -16
  49. package/shared/localization/format.js +73 -36
  50. package/shared/type-verifiers.d.ts +1 -1
  51. package/types/audit.d.ts +2 -0
  52. package/types/internal/rxjs.d.ts +3 -0
  53. package/types/lhr/audit-result.d.ts +7 -0
@@ -12,6 +12,19 @@ import {getModuleDirectory} from '../esm-utils.js';
12
12
  import {isObjectOfUnknownValues, isObjectOrArrayOfUnknownValues} from '../type-verifiers.js';
13
13
  import {locales} from './locales.js';
14
14
 
15
+ // From @formatjs/icu-messageformat-parser - copy here so we don't need to bundle all that.
16
+ const TYPE = /** @type {const} */ ({
17
+ literal: 0,
18
+ argument: 1,
19
+ number: 2,
20
+ date: 3,
21
+ time: 4,
22
+ select: 5,
23
+ plural: 6,
24
+ pound: 7,
25
+ tag: 8,
26
+ });
27
+
15
28
  const moduleDir = getModuleDirectory(import.meta);
16
29
 
17
30
  /** Contains available locales with messages. May be an empty object if bundled. */
@@ -30,11 +43,11 @@ const CANONICAL_LOCALES = fs.readdirSync(moduleDir + '/locales/')
30
43
  .map(locale => locale.replace('.json', ''))
31
44
  .sort();
32
45
 
33
- /** @typedef {import('intl-messageformat-parser').Element} MessageElement */
34
- /** @typedef {import('intl-messageformat-parser').ArgumentElement} ArgumentElement */
46
+ /** @typedef {import('@formatjs/icu-messageformat-parser').MessageFormatElement} MessageFormatElement */
35
47
 
36
48
  const MESSAGE_I18N_ID_REGEX = / | [^\s]+$/;
37
49
 
50
+ /** @type {Partial<import('intl-messageformat').Formats>} */
38
51
  const formats = {
39
52
  number: {
40
53
  bytes: {
@@ -57,40 +70,39 @@ const formats = {
57
70
  };
58
71
 
59
72
  /**
60
- * Function to retrieve all 'argumentElement's from an ICU message. An argumentElement
61
- * is an ICU element with an argument in it, like '{varName}' or '{varName, number, bytes}'. This
62
- * differs from 'messageElement's which are just arbitrary text in a message.
73
+ * Function to retrieve all elements from an ICU message AST that are associated
74
+ * with a named input, like '{varName}' or '{varName, number, bytes}'. This
75
+ * differs from literal message types which are just arbitrary text.
63
76
  *
64
- * Notes:
65
- * This function will recursively inspect plural elements for nested argumentElements.
77
+ * This function recursively inspects plural elements for nested elements,
78
+ * but since the output is a Map they are deduplicated.
79
+ * e.g. "=1{hello {icu}} =other{hello {icu}}" will produce one element in the output,
80
+ * with "icu" as its key.
66
81
  *
67
- * We need to find all the elements from the plural format sections, but
68
- * they need to be deduplicated. I.e. "=1{hello {icu}} =other{hello {icu}}"
69
- * the variable "icu" would appear twice if it wasn't de duplicated. And they cannot
70
- * be stored in a set because they are not equal since their locations are different,
71
- * thus they are stored via a Map keyed on the "id" which is the ICU varName.
82
+ * TODO: don't do that deduplication because messages within a plural message could be number
83
+ * messages with different styles.
72
84
  *
73
- * @param {Array<MessageElement>} icuElements
74
- * @param {Map<string, ArgumentElement>} [seenElementsById]
75
- * @return {Map<string, ArgumentElement>}
85
+ * @param {Array<MessageFormatElement>} icuElements
86
+ * @param {Map<string, MessageFormatElement>} [customElements]
87
+ * @return {Map<string, MessageFormatElement>}
76
88
  */
77
- function collectAllCustomElementsFromICU(icuElements, seenElementsById = new Map()) {
89
+ function collectAllCustomElementsFromICU(icuElements, customElements = new Map()) {
78
90
  for (const el of icuElements) {
79
- // We are only interested in elements that need ICU formatting (argumentElements)
80
- if (el.type !== 'argumentElement') continue;
91
+ if (el.type === TYPE.literal || el.type === TYPE.pound) continue;
81
92
 
82
- seenElementsById.set(el.id, el);
93
+ customElements.set(el.value, el);
83
94
 
84
95
  // Plurals need to be inspected recursively
85
- if (!el.format || el.format.type !== 'pluralFormat') continue;
86
- // Look at all options of the plural (=1{} =other{}...)
87
- for (const option of el.format.options) {
88
- // Run collections on each option's elements
89
- collectAllCustomElementsFromICU(option.value.elements, seenElementsById);
96
+ if (el.type === TYPE.plural) {
97
+ // Look at all options of the plural (=1{} =other{}...)
98
+ for (const option of Object.values(el.options)) {
99
+ // Run collections on each option's elements
100
+ collectAllCustomElementsFromICU(option.value, customElements);
101
+ }
90
102
  }
91
103
  }
92
104
 
93
- return seenElementsById;
105
+ return customElements;
94
106
  }
95
107
 
96
108
  /**
@@ -103,15 +115,14 @@ function collectAllCustomElementsFromICU(icuElements, seenElementsById = new Map
103
115
  * @return {Record<string, string | number>}
104
116
  */
105
117
  function _preformatValues(messageFormatter, values = {}, lhlMessage) {
106
- const elementMap = collectAllCustomElementsFromICU(messageFormatter.getAst().elements);
107
- const argumentElements = [...elementMap.values()];
118
+ const customElements = collectAllCustomElementsFromICU(messageFormatter.getAst());
108
119
 
109
120
  /** @type {Record<string, string | number>} */
110
121
  const formattedValues = {};
111
122
 
112
- for (const {id, format} of argumentElements) {
123
+ for (const [id, element] of customElements) {
113
124
  // Throw an error if a message's value isn't provided
114
- if (id && (id in values) === false) {
125
+ if (!(id in values)) {
115
126
  throw new Error(`ICU Message "${lhlMessage}" contains a value reference ("${id}") ` +
116
127
  `that wasn't provided`);
117
128
  }
@@ -119,7 +130,7 @@ function _preformatValues(messageFormatter, values = {}, lhlMessage) {
119
130
  const value = values[id];
120
131
 
121
132
  // Direct `{id}` replacement and non-numeric values need no formatting.
122
- if (!format || format.type !== 'numberFormat') {
133
+ if (element.type !== TYPE.number) {
123
134
  formattedValues[id] = value;
124
135
  continue;
125
136
  }
@@ -130,13 +141,13 @@ function _preformatValues(messageFormatter, values = {}, lhlMessage) {
130
141
  }
131
142
 
132
143
  // Format values for known styles.
133
- if (format.style === 'milliseconds') {
144
+ if (element.style === 'milliseconds') {
134
145
  // Round all milliseconds to the nearest 10.
135
146
  formattedValues[id] = Math.round(value / 10) * 10;
136
- } else if (format.style === 'seconds' && id === 'timeInMs') {
147
+ } else if (element.style === 'seconds' && id === 'timeInMs') {
137
148
  // Convert all seconds to the correct unit (currently only for `timeInMs`).
138
149
  formattedValues[id] = Math.round(value / 100) / 10;
139
- } else if (format.style === 'bytes') {
150
+ } else if (element.style === 'bytes') {
140
151
  // Replace all the bytes with KB.
141
152
  formattedValues[id] = value / 1024;
142
153
  } else {
@@ -162,6 +173,19 @@ function _preformatValues(messageFormatter, values = {}, lhlMessage) {
162
173
  return formattedValues;
163
174
  }
164
175
 
176
+ /**
177
+ * Our strings were made when \ was the escape character, but now it is '. To avoid churn,
178
+ * let's convert to the new style in memory.
179
+ * @param {string} message
180
+ * @return {string}
181
+ */
182
+ function escapeIcuMessage(message) {
183
+ return message
184
+ .replace(/'/g, `''`)
185
+ .replace(/\\{/g, `'{`)
186
+ .replace(/\\}/g, `'}`);
187
+ }
188
+
165
189
  /**
166
190
  * Format string `message` by localizing `values` and inserting them. `message`
167
191
  * is assumed to already be in the given locale.
@@ -172,19 +196,31 @@ function _preformatValues(messageFormatter, values = {}, lhlMessage) {
172
196
  * @return {string}
173
197
  */
174
198
  function formatMessage(message, values, locale) {
199
+ message = escapeIcuMessage(message);
200
+
175
201
  // Parsing and formatting can be slow. Don't attempt if the string can't
176
202
  // contain ICU placeholders, in which case formatting is already complete.
177
- if (!message.includes('{') && values === undefined) return message;
178
203
 
179
204
  // When using accented english, force the use of a different locale for number formatting.
180
205
  const localeForMessageFormat = (locale === 'en-XA' || locale === 'en-XL') ? 'de-DE' : locale;
181
206
 
182
- const formatter = new IntlMessageFormat(message, localeForMessageFormat, formats);
207
+ // This package is not correctly bundled.
208
+ /** @type {typeof IntlMessageFormat} */
209
+ // @ts-expect-error bundler woes
210
+ const IntlMessageFormatCtor = IntlMessageFormat.IntlMessageFormat || IntlMessageFormat;
211
+ const formatter = new IntlMessageFormatCtor(message, localeForMessageFormat, formats, {
212
+ ignoreTag: true,
213
+ });
183
214
 
184
215
  // Preformat values for the message format like KB and milliseconds.
185
216
  const valuesForMessageFormat = _preformatValues(formatter, values, message);
186
217
 
187
- return formatter.format(valuesForMessageFormat);
218
+ const formattedResult = formatter.format(valuesForMessageFormat);
219
+ // We only format to strings.
220
+ if (typeof formattedResult !== 'string') {
221
+ throw new Error('unexpected formatted result');
222
+ }
223
+ return formattedResult;
188
224
  }
189
225
 
190
226
  /**
@@ -459,4 +495,5 @@ export {
459
495
  getIcuMessageIdParts,
460
496
  getAvailableLocales,
461
497
  getCanonicalLocales,
498
+ escapeIcuMessage,
462
499
  };
@@ -19,5 +19,5 @@ export function isObjectOfUnknownValues(val: unknown): val is Record<string, unk
19
19
  * @param {unknown} val
20
20
  * @return {val is Record<string, unknown>|Array<unknown>}
21
21
  */
22
- export function isObjectOrArrayOfUnknownValues(val: unknown): val is unknown[] | Record<string, unknown>;
22
+ export function isObjectOrArrayOfUnknownValues(val: unknown): val is Record<string, unknown> | unknown[];
23
23
  //# sourceMappingURL=type-verifiers.d.ts.map
package/types/audit.d.ts CHANGED
@@ -90,6 +90,8 @@ declare module Audit {
90
90
  metricSavings?: MetricSavings;
91
91
  /** Score details including p10 and median for calculating an audit's log-normal score. */
92
92
  scoringOptions?: ScoreOptions;
93
+ /** A string identifying how the score should be interpreted for display. Overrides audit meta `scoreDisplayMode` if defined. */
94
+ scoreDisplayMode?: AuditResult.ScoreDisplayMode;
93
95
  }
94
96
 
95
97
  /** The Audit.Product type for audits that do not return a `numericValue`. */
@@ -22,5 +22,8 @@ declare module 'rxjs' {
22
22
  export const tap: any;
23
23
  export const throwIfEmpty: any;
24
24
  export const firstValueFrom: any;
25
+ export const delay: any;
26
+ export const startWith: any;
27
+ export const switchMap: any;
25
28
  }
26
29
 
@@ -12,6 +12,13 @@ interface ScoreDisplayModes {
12
12
  NUMERIC: 'numeric';
13
13
  /** Pass/fail audit (0 and 1 are only possible scores). */
14
14
  BINARY: 'binary';
15
+ /**
16
+ * Audit result score is determined by the metric savings and product score:
17
+ * 1 - audit passed based on product score
18
+ * 0.5 - audit failed and had no metric savings
19
+ * 0 - audit failed and had metric savings
20
+ */
21
+ METRIC_SAVINGS: 'metricSavings';
15
22
  /** The audit exists only to tell you to review something yourself. Score is null and should be ignored. */
16
23
  MANUAL: 'manual';
17
24
  /** The audit is an FYI only, and can't be interpreted as pass/fail. Score is null and should be ignored. */