lighthouse-audit-utils 1.0.0 → 1.2.3

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/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # lighthouse-audit-utils
2
2
 
3
- Everything you'd do with a finished Lighthouse run, in one call: write the
4
- reports to disk, print the fix list to the terminal, and fail the run if a
5
- category scored below its threshold. Each step can be configured or disabled.
3
+ Programmatic [Lighthouse](https://github.com/GoogleChrome/lighthouse) audit utilities for CI and performance testing: score-threshold checking, HTML/JSON report writing, and a recommendations logger (ie, what is seen in a lighthouse report UI), all from a finished Lighthouse run in one call. Each step can be configured or disabled.
4
+
5
+ Running the audits from Playwright? [`lighthouse-audit-utils/playwright`](#playwright)
6
+ ships the CDP wiring as a fixture, so a test can audit whatever page it's on —
7
+ handy for Lighthouse CI-style performance budgets inside a Playwright suite.
6
8
 
7
9
  ```bash
8
10
  npm install --save-dev lighthouse-audit-utils
9
11
  ```
10
12
 
11
- `lighthouse` is a peer dependency.
13
+ `lighthouse` is a peer dependency; `@playwright/test` is an optional one, needed
14
+ only if using the [Playwright entrypoint](#playwright).
12
15
 
13
16
  ## Usage
14
17
 
@@ -56,16 +59,15 @@ await handleAuditResult({
56
59
  ```
57
60
 
58
61
  The three steps run in that order — reports, recommendations, thresholds — so a
59
- failing run still prints its fix list before throwing.
62
+ failing run still prints its recommendations before throwing.
60
63
 
61
64
  | Option | Default | Description |
62
65
  | ----------------- | ---------- | ------------------------------------------------------------ |
63
66
  | `result` | _required_ | The full `RunnerResult` from a Lighthouse run |
64
67
  | `reports` | _none_ | Where to write the reports; omit to skip writing them |
65
- | `recommendations` | _none_ | Recommendation logging options, or `false` to skip |
68
+ | `recommendations` | _none_ | Recommendation logging options, or `false` to skip |
66
69
  | `thresholds` | `100` | Minimum scores (0-100) — one number for all, or per category |
67
- | `ignoreError` | `false` | Return the threshold failures rather than throwing on them |
68
-
70
+ | `ignoreError` | `false` | Return the threshold failures rather than throwing on them |
69
71
 
70
72
  ```ts
71
73
  // just the log
@@ -115,7 +117,7 @@ Pass `recommendations: false` to skip the log entirely.
115
117
 
116
118
  #### Output
117
119
 
118
- The log is the same fix list the report UI shows — failing audits, their
120
+ The log is the same recommendations the report UI shows — failing audits, their
119
121
  estimated savings, and the individual offending URLs/nodes — so a failing CI run
120
122
  is actionable without downloading and opening the HTML report. Audits are
121
123
  grouped by category and sorted by estimated savings, so the biggest wins come
@@ -140,13 +142,88 @@ Performance: 87
140
142
  render-blocking-insight · score 50
141
143
  - https://example.com/assets/index.css · Transfer Size: 12.4 KiB · Duration: 52 ms
142
144
 
143
- Accessibility: 100 — nothing to fix
145
+ Accessibility: 100 — nothing to flag
144
146
  ```
145
147
 
146
- ## Example: Playwright
148
+ ## Individual utilities
147
149
 
148
- Lighthouse navigates over the CDP port itself, so launch a persistent context on
149
- that port and point `lighthouse` at it.
150
+ The three steps are also exported on their own, each taking the report first and
151
+ its options second:
152
+
153
+ ```ts
154
+ writeReports(result, { directory, name })
155
+ logRecommendations(lhr, { label, maxItems, maxValueLength })
156
+ checkAgainstThresholds(lhr, { thresholds, ignoreError })
157
+ ```
158
+
159
+ ## Playwright
160
+
161
+ `lighthouse-audit-utils/playwright` ships the wiring as a fixture.
162
+
163
+ ```ts
164
+ // fixtures.ts
165
+ import { desktopConfig } from 'lighthouse'
166
+ import { withLighthouse } from 'lighthouse-audit-utils/playwright'
167
+
168
+ export const lighthouseTest = withLighthouse({
169
+ basePort: 9222,
170
+ lighthouseArgs: {
171
+ flags: { disableStorageReset: true, output: ['html', 'json'] },
172
+ config: {
173
+ extends: 'lighthouse:default',
174
+ settings: { skipAudits: ['color-contrast'] },
175
+ },
176
+ },
177
+ thresholds: { performance: 70 },
178
+ })
179
+
180
+ // a.spec.ts
181
+ lighthouseTest('home page', async ({ page, runAudit }) => {
182
+ await page.goto('/')
183
+ await runAudit({
184
+ name: 'desktop',
185
+ lighthouseArgs: { config: desktopConfig },
186
+ })
187
+ await runAudit({ name: 'mobile', thresholds: { performance: 60 } }) // merges over the fixture's value
188
+ })
189
+ ```
190
+
191
+ - Each call to `runAudit` audits whatever page the test is currently on and writes its reports to the test's output directory.
192
+ - Run `runAudit` more than once for more than one form factor
193
+ — wrap the calls in `test.step` if you want them grouped in the report.
194
+ - `withLighthouse(options, test)` takes the test to extend second, so you can layer
195
+ it onto your own fixtures; omit it to start from Playwright's `test`.
196
+
197
+ | Option | Required | Description |
198
+ | ---------------- | -------- | ---------------------------------------------------------------------------------------- |
199
+ | `basePort` | yes | First worker's CDP port; each further worker gets the next one up |
200
+ | `lighthouseArgs` | no | `flags` and `config` for `lighthouse()`; `url` and `flags.port` are set for you |
201
+ | `reports` | no | `(context) => { directory, name }`, or `false` to skip writing them |
202
+ | `launchOptions` | no | Merged into the persistent context launch, which already sets the CDP port and `baseURL` |
203
+
204
+ Plus everything [`runAudit`](#usage) from `lighthouse-audit-utils` takes —
205
+ `thresholds`, `ignoreError`, `recommendations`.
206
+
207
+ The `runAudit` fixture takes `name` — which names that run's reports, so two
208
+ audits in one test don't overwrite each other — and `lighthouseArgs`,
209
+ `thresholds`, `ignoreError` and `recommendations`, to overwrite the overall fixture's:
210
+
211
+ ```ts
212
+ const { result, failures } = await runAudit({
213
+ name: 'logged-in',
214
+ thresholds: { performance: 50 },
215
+ lighthouseArgs: { config: { settings: { onlyCategories: ['performance'] } } },
216
+ })
217
+ ```
218
+
219
+ `thresholds` merge when both are objects; anything else replaces, since a flat
220
+ number can't be partially overridden.
221
+
222
+ `context` is overridden to launch a persistent Chrome profile on the CDP port,
223
+ since Lighthouse navigates over that port itself rather than driving the
224
+ Playwright `page` — this way both see the same browser session.
225
+
226
+ ### Doing it by hand
150
227
 
151
228
  ```ts
152
229
  import { desktopConfig } from 'lighthouse'
@@ -163,17 +240,6 @@ await runAudit({
163
240
  })
164
241
  ```
165
242
 
166
- ## Individual utilities
167
-
168
- The three steps are also exported on their own, each taking the report first and
169
- its options second:
170
-
171
- ```ts
172
- writeReports(result, { directory, name })
173
- logRecommendations(lhr, { label, maxItems, maxValueLength })
174
- checkAgainstThresholds(lhr, { thresholds, ignoreError })
175
- ```
176
-
177
243
  ## Development
178
244
 
179
245
  ```bash
@@ -0,0 +1,510 @@
1
+ import { mkdir, writeFile } from 'fs/promises';
2
+ import path from 'path';
3
+ import lighthouse from 'lighthouse';
4
+
5
+ // lighthouse-audit-utils 1.2.3
6
+
7
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/predicate/isPrimitive.mjs
8
+ function isPrimitive(value) {
9
+ return value == null || typeof value !== "object" && typeof value !== "function";
10
+ }
11
+
12
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/predicate/isTypedArray.mjs
13
+ function isTypedArray(x) {
14
+ return ArrayBuffer.isView(x) && !(x instanceof DataView);
15
+ }
16
+
17
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/object/clone.mjs
18
+ function clone(obj) {
19
+ if (isPrimitive(obj)) return obj;
20
+ if (Array.isArray(obj) || isTypedArray(obj) || obj instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && obj instanceof SharedArrayBuffer) return obj.slice(0);
21
+ const prototype = Object.getPrototypeOf(obj);
22
+ if (prototype == null) return Object.assign(Object.create(prototype), obj);
23
+ const Constructor = prototype.constructor;
24
+ if (obj instanceof Date || obj instanceof Map || obj instanceof Set) return new Constructor(obj);
25
+ if (obj instanceof RegExp) {
26
+ const newRegExp = new Constructor(obj);
27
+ newRegExp.lastIndex = obj.lastIndex;
28
+ return newRegExp;
29
+ }
30
+ if (obj instanceof DataView) return new Constructor(obj.buffer.slice(0));
31
+ if (obj instanceof Error) {
32
+ let newError;
33
+ if (obj instanceof AggregateError) newError = new Constructor(obj.errors, obj.message, { cause: obj.cause });
34
+ else newError = new Constructor(obj.message, { cause: obj.cause });
35
+ newError.stack = obj.stack;
36
+ Object.assign(newError, obj);
37
+ return newError;
38
+ }
39
+ if (typeof File !== "undefined" && obj instanceof File) return new Constructor([obj], obj.name, {
40
+ type: obj.type,
41
+ lastModified: obj.lastModified
42
+ });
43
+ if (typeof obj === "object") return Object.assign(Object.create(prototype), obj);
44
+ return obj;
45
+ }
46
+
47
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/compat/_internal/getSymbols.mjs
48
+ function getSymbols(object) {
49
+ return Object.getOwnPropertySymbols(object).filter((symbol) => Object.prototype.propertyIsEnumerable.call(object, symbol));
50
+ }
51
+
52
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/compat/_internal/getTag.mjs
53
+ function getTag(value) {
54
+ if (value == null) return value === void 0 ? "[object Undefined]" : "[object Null]";
55
+ return Object.prototype.toString.call(value);
56
+ }
57
+
58
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/compat/_internal/tags.mjs
59
+ var regexpTag = "[object RegExp]";
60
+ var stringTag = "[object String]";
61
+ var numberTag = "[object Number]";
62
+ var booleanTag = "[object Boolean]";
63
+ var argumentsTag = "[object Arguments]";
64
+ var symbolTag = "[object Symbol]";
65
+ var dateTag = "[object Date]";
66
+ var mapTag = "[object Map]";
67
+ var setTag = "[object Set]";
68
+ var arrayTag = "[object Array]";
69
+ var arrayBufferTag = "[object ArrayBuffer]";
70
+ var objectTag = "[object Object]";
71
+ var dataViewTag = "[object DataView]";
72
+ var uint8ArrayTag = "[object Uint8Array]";
73
+ var uint8ClampedArrayTag = "[object Uint8ClampedArray]";
74
+ var uint16ArrayTag = "[object Uint16Array]";
75
+ var uint32ArrayTag = "[object Uint32Array]";
76
+ var int8ArrayTag = "[object Int8Array]";
77
+ var int16ArrayTag = "[object Int16Array]";
78
+ var int32ArrayTag = "[object Int32Array]";
79
+ var float32ArrayTag = "[object Float32Array]";
80
+ var float64ArrayTag = "[object Float64Array]";
81
+
82
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/_internal/globalThis.mjs
83
+ var globalThis_ = typeof globalThis === "object" && globalThis || typeof window === "object" && window || typeof self === "object" && self || typeof global === "object" && global || /* @__PURE__ */ (function() {
84
+ return this;
85
+ })();
86
+
87
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/predicate/isBuffer.mjs
88
+ function isBuffer(x) {
89
+ return typeof globalThis_.Buffer !== "undefined" && globalThis_.Buffer.isBuffer(x);
90
+ }
91
+
92
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/object/cloneDeepWith.mjs
93
+ function cloneDeepWithImpl(valueToClone, keyToClone, objectToClone, stack = /* @__PURE__ */ new Map(), cloneValue = void 0) {
94
+ const cloned = cloneValue?.(valueToClone, keyToClone, objectToClone, stack);
95
+ if (cloned !== void 0) return cloned;
96
+ if (isPrimitive(valueToClone)) return valueToClone;
97
+ if (stack.has(valueToClone)) return stack.get(valueToClone);
98
+ if (Array.isArray(valueToClone)) {
99
+ const result = new Array(valueToClone.length);
100
+ stack.set(valueToClone, result);
101
+ for (let i = 0; i < valueToClone.length; i++) result[i] = cloneDeepWithImpl(valueToClone[i], i, objectToClone, stack, cloneValue);
102
+ if (Object.hasOwn(valueToClone, "index")) result.index = valueToClone.index;
103
+ if (Object.hasOwn(valueToClone, "input")) result.input = valueToClone.input;
104
+ return result;
105
+ }
106
+ if (valueToClone instanceof Date) return new Date(valueToClone.getTime());
107
+ if (valueToClone instanceof RegExp) {
108
+ const result = new RegExp(valueToClone.source, valueToClone.flags);
109
+ result.lastIndex = valueToClone.lastIndex;
110
+ return result;
111
+ }
112
+ if (valueToClone instanceof Map) {
113
+ const result = /* @__PURE__ */ new Map();
114
+ stack.set(valueToClone, result);
115
+ for (const [key, value] of valueToClone) result.set(key, cloneDeepWithImpl(value, key, objectToClone, stack, cloneValue));
116
+ return result;
117
+ }
118
+ if (valueToClone instanceof Set) {
119
+ const result = /* @__PURE__ */ new Set();
120
+ stack.set(valueToClone, result);
121
+ for (const value of valueToClone) result.add(cloneDeepWithImpl(value, void 0, objectToClone, stack, cloneValue));
122
+ return result;
123
+ }
124
+ if (isBuffer(valueToClone)) return valueToClone.subarray();
125
+ if (isTypedArray(valueToClone)) {
126
+ const result = new (Object.getPrototypeOf(valueToClone)).constructor(valueToClone.length);
127
+ stack.set(valueToClone, result);
128
+ for (let i = 0; i < valueToClone.length; i++) result[i] = cloneDeepWithImpl(valueToClone[i], i, objectToClone, stack, cloneValue);
129
+ return result;
130
+ }
131
+ if (valueToClone instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && valueToClone instanceof SharedArrayBuffer) return valueToClone.slice(0);
132
+ if (valueToClone instanceof DataView) {
133
+ const result = new DataView(valueToClone.buffer.slice(0), valueToClone.byteOffset, valueToClone.byteLength);
134
+ stack.set(valueToClone, result);
135
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
136
+ return result;
137
+ }
138
+ if (typeof File !== "undefined" && valueToClone instanceof File) {
139
+ const result = new File([valueToClone], valueToClone.name, { type: valueToClone.type });
140
+ stack.set(valueToClone, result);
141
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
142
+ return result;
143
+ }
144
+ if (typeof Blob !== "undefined" && valueToClone instanceof Blob) {
145
+ const result = new Blob([valueToClone], { type: valueToClone.type });
146
+ stack.set(valueToClone, result);
147
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
148
+ return result;
149
+ }
150
+ if (valueToClone instanceof Error) {
151
+ const result = structuredClone(valueToClone);
152
+ stack.set(valueToClone, result);
153
+ result.message = valueToClone.message;
154
+ result.name = valueToClone.name;
155
+ result.stack = valueToClone.stack;
156
+ result.cause = valueToClone.cause;
157
+ result.constructor = valueToClone.constructor;
158
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
159
+ return result;
160
+ }
161
+ if (valueToClone instanceof Boolean) {
162
+ const result = new Boolean(valueToClone.valueOf());
163
+ stack.set(valueToClone, result);
164
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
165
+ return result;
166
+ }
167
+ if (valueToClone instanceof Number) {
168
+ const result = new Number(valueToClone.valueOf());
169
+ stack.set(valueToClone, result);
170
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
171
+ return result;
172
+ }
173
+ if (valueToClone instanceof String) {
174
+ const result = new String(valueToClone.valueOf());
175
+ stack.set(valueToClone, result);
176
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
177
+ return result;
178
+ }
179
+ if (typeof valueToClone === "object" && isCloneableObject(valueToClone)) {
180
+ const result = Object.create(Object.getPrototypeOf(valueToClone));
181
+ stack.set(valueToClone, result);
182
+ copyProperties(result, valueToClone, objectToClone, stack, cloneValue);
183
+ return result;
184
+ }
185
+ return valueToClone;
186
+ }
187
+ function copyProperties(target, source, objectToClone = target, stack, cloneValue) {
188
+ const keys = [...Object.keys(source), ...getSymbols(source)];
189
+ for (let i = 0; i < keys.length; i++) {
190
+ const key = keys[i];
191
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
192
+ if (descriptor == null || descriptor.writable) target[key] = cloneDeepWithImpl(source[key], key, objectToClone, stack, cloneValue);
193
+ }
194
+ }
195
+ function isCloneableObject(object) {
196
+ switch (getTag(object)) {
197
+ case argumentsTag:
198
+ case arrayTag:
199
+ case arrayBufferTag:
200
+ case dataViewTag:
201
+ case booleanTag:
202
+ case dateTag:
203
+ case float32ArrayTag:
204
+ case float64ArrayTag:
205
+ case int8ArrayTag:
206
+ case int16ArrayTag:
207
+ case int32ArrayTag:
208
+ case mapTag:
209
+ case numberTag:
210
+ case objectTag:
211
+ case regexpTag:
212
+ case setTag:
213
+ case stringTag:
214
+ case symbolTag:
215
+ case uint8ArrayTag:
216
+ case uint8ClampedArrayTag:
217
+ case uint16ArrayTag:
218
+ case uint32ArrayTag:
219
+ return true;
220
+ default:
221
+ return false;
222
+ }
223
+ }
224
+
225
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/object/cloneDeep.mjs
226
+ function cloneDeep(obj) {
227
+ return cloneDeepWithImpl(obj, void 0, obj, /* @__PURE__ */ new Map(), void 0);
228
+ }
229
+
230
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
231
+ function isPlainObject(value) {
232
+ if (!value || typeof value !== "object") return false;
233
+ const proto = Object.getPrototypeOf(value);
234
+ if (!(proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null)) return false;
235
+ return Object.prototype.toString.call(value) === "[object Object]";
236
+ }
237
+
238
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/_internal/isUnsafeProperty.mjs
239
+ function isUnsafeProperty(key) {
240
+ return key === "__proto__";
241
+ }
242
+
243
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/object/mergeWith.mjs
244
+ function mergeWith(target, source, merge) {
245
+ const sourceKeys = Object.keys(source);
246
+ for (let i = 0; i < sourceKeys.length; i++) {
247
+ const key = sourceKeys[i];
248
+ if (isUnsafeProperty(key)) continue;
249
+ const sourceValue = source[key];
250
+ const targetValue = target[key];
251
+ const merged = merge(targetValue, sourceValue, key, target, source);
252
+ if (merged !== void 0) target[key] = merged;
253
+ else if (Array.isArray(sourceValue)) if (Array.isArray(targetValue)) target[key] = mergeWith(targetValue, sourceValue, merge);
254
+ else target[key] = mergeWith([], sourceValue, merge);
255
+ else if (isPlainObject(sourceValue)) if (isPlainObject(targetValue)) target[key] = mergeWith(targetValue, sourceValue, merge);
256
+ else target[key] = mergeWith({}, sourceValue, merge);
257
+ else if (targetValue === void 0 || sourceValue !== void 0) target[key] = sourceValue;
258
+ }
259
+ return target;
260
+ }
261
+
262
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/object/toMerged.mjs
263
+ function toMerged(target, source) {
264
+ return mergeWith(cloneDeep(target), source, function mergeRecursively(targetValue, sourceValue) {
265
+ if (Array.isArray(sourceValue)) if (Array.isArray(targetValue)) return mergeWith(clone(targetValue), sourceValue, mergeRecursively);
266
+ else return mergeWith([], sourceValue, mergeRecursively);
267
+ else if (isPlainObject(sourceValue)) if (isPlainObject(targetValue)) return mergeWith(clone(targetValue), sourceValue, mergeRecursively);
268
+ else return mergeWith({}, sourceValue, mergeRecursively);
269
+ });
270
+ }
271
+
272
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/array/compact.mjs
273
+ function compact(arr) {
274
+ const result = [];
275
+ for (let i = 0; i < arr.length; i++) {
276
+ const item = arr[i];
277
+ if (item) result.push(item);
278
+ }
279
+ return result;
280
+ }
281
+
282
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/_internal/compareValues.mjs
283
+ function nullishRank(value) {
284
+ if (value === null) return 1;
285
+ if (value === void 0) return 2;
286
+ return 0;
287
+ }
288
+ function compareAscending(a, b) {
289
+ const aRank = nullishRank(a);
290
+ const bRank = nullishRank(b);
291
+ if (aRank < bRank) return -1;
292
+ if (aRank > bRank) return 1;
293
+ if (aRank !== 0) return 0;
294
+ if (a < b) return -1;
295
+ if (a > b) return 1;
296
+ return 0;
297
+ }
298
+ function compareValues(a, b, order) {
299
+ return order === "asc" ? compareAscending(a, b) : compareAscending(b, a);
300
+ }
301
+
302
+ // node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/array/orderBy.mjs
303
+ function orderBy(arr, criteria, orders) {
304
+ return arr.slice().sort((a, b) => {
305
+ const ordersLength = orders.length;
306
+ for (let i = 0; i < criteria.length; i++) {
307
+ const order = ordersLength > i ? orders[i] : orders[ordersLength - 1];
308
+ const criterion = criteria[i];
309
+ const criterionIsFunction = typeof criterion === "function";
310
+ const result = compareValues(criterionIsFunction ? criterion(a) : a[criterion], criterionIsFunction ? criterion(b) : b[criterion], order);
311
+ if (result !== 0) return result;
312
+ }
313
+ return 0;
314
+ });
315
+ }
316
+
317
+ // src/log-recommendations.ts
318
+ var resolveMetricSavings = (audit) => Object.entries(audit.metricSavings ?? {}).filter(([, ms]) => !!ms);
319
+ var metricValueType = (metric) => metric === "CLS" ? "numeric" : "ms";
320
+ var formatValue = (value, valueType, maxValueLength) => {
321
+ if (value === void 0 || typeof value === "boolean") {
322
+ return "";
323
+ }
324
+ if (typeof value === "number") {
325
+ if (valueType === "bytes") {
326
+ const kib = value / 1024;
327
+ const rounded = kib >= 100 ? Math.round(kib) : Math.round(kib * 10) / 10;
328
+ return `${rounded} KiB`;
329
+ }
330
+ if (valueType === "ms" || valueType === "timespanMs") {
331
+ return `${Math.round(value)} ms`;
332
+ }
333
+ return String(Math.round(value * 100) / 100);
334
+ }
335
+ const text = (() => {
336
+ if (typeof value === "string") {
337
+ return value;
338
+ }
339
+ if (!("type" in value)) {
340
+ return "";
341
+ }
342
+ switch (value.type) {
343
+ case "url":
344
+ return value.value;
345
+ case "link":
346
+ return value.url || value.text;
347
+ case "code":
348
+ return String(value.value);
349
+ case "node":
350
+ return value.nodeLabel ?? value.selector ?? value.snippet ?? "";
351
+ case "source-location":
352
+ return `${value.url}:${value.line}:${value.column}`;
353
+ default:
354
+ return "";
355
+ }
356
+ })();
357
+ const singleLine = text.replace(/\s+/g, " ").trim();
358
+ return singleLine.length > maxValueLength ? `${singleLine.slice(0, maxValueLength)}\u2026` : singleLine;
359
+ };
360
+ var formatItems = (details, { maxItems, maxValueLength }) => {
361
+ if (details?.type !== "opportunity" && details?.type !== "table") {
362
+ return [];
363
+ }
364
+ const [identity, ...measures] = details.headings.filter(({ key }) => !!key);
365
+ if (!identity) {
366
+ return [];
367
+ }
368
+ const MEASURE_TYPES = [
369
+ "bytes",
370
+ "ms",
371
+ "timespanMs",
372
+ "numeric"
373
+ ];
374
+ const lines = details.items.slice(0, maxItems).map((item) => {
375
+ const columns = [
376
+ formatValue(item[identity.key ?? ""], identity.valueType, maxValueLength),
377
+ ...measures.filter(({ valueType }) => MEASURE_TYPES.includes(valueType)).map(
378
+ (heading) => `${heading.label}: ${formatValue(item[heading.key ?? ""], heading.valueType, maxValueLength)}`
379
+ )
380
+ ];
381
+ return ` - ${compact(columns).join(" \xB7 ")}`;
382
+ });
383
+ const remaining = details.items.length - maxItems;
384
+ if (remaining > 0) {
385
+ lines.push(` - \u2026and ${remaining} more`);
386
+ }
387
+ return lines;
388
+ };
389
+ var logRecommendations = (lhr, {
390
+ label,
391
+ maxItems = 5,
392
+ maxValueLength = 120
393
+ }) => {
394
+ const lines = [
395
+ "",
396
+ `\u2500\u2500\u2500\u2500\u2500 Lighthouse recommendations${label ? `: ${label}` : ""} \u2014 ${lhr.finalDisplayedUrl} \u2500\u2500\u2500\u2500\u2500`
397
+ ];
398
+ for (const category of Object.values(lhr.categories)) {
399
+ const categoryScoreValue = category.score === null ? "N/A" : Math.round(category.score * 100);
400
+ const categoryLine = `${category.title}: ${categoryScoreValue}`;
401
+ const failing = orderBy(
402
+ category.auditRefs.map(({ id }) => lhr.audits[id]).filter((audit) => !!audit && audit.score !== null && audit.score < 1),
403
+ [
404
+ (audit) => Math.max(0, ...resolveMetricSavings(audit).map(([, ms]) => ms)),
405
+ ({ score }) => score ?? 0
406
+ ],
407
+ ["desc", "asc"]
408
+ );
409
+ if (!failing.length) {
410
+ lines.push("", `${categoryLine} \u2014 nothing to flag`);
411
+ continue;
412
+ }
413
+ lines.push("", categoryLine);
414
+ for (const audit of failing) {
415
+ const displayValue = audit.displayValue ? ` (${audit.displayValue})` : "";
416
+ const savingsValue = (() => {
417
+ if (audit.displayValue?.toLowerCase().includes("savings")) {
418
+ return "";
419
+ }
420
+ const overallSavingsBytes = audit.details?.type === "opportunity" ? audit.details.overallSavingsBytes : void 0;
421
+ const estSavings = compact([
422
+ ...resolveMetricSavings(audit).map(
423
+ ([metric, value]) => `${metric} ${formatValue(value, metricValueType(metric), maxValueLength)}`
424
+ ),
425
+ formatValue(overallSavingsBytes, "bytes", maxValueLength)
426
+ ]);
427
+ return estSavings.length ? ` \u2014 est. savings: ${estSavings.join(", ")}` : "";
428
+ })();
429
+ const scoreValue = audit.score === null ? "N/A" : Math.round(audit.score * 100);
430
+ lines.push(` \u2022 ${audit.title}${displayValue}${savingsValue}`);
431
+ lines.push(` ${audit.id} \xB7 score ${scoreValue}`);
432
+ lines.push(...formatItems(audit.details, { maxItems, maxValueLength }));
433
+ }
434
+ }
435
+ lines.push("");
436
+ console.log(lines.join("\n"));
437
+ };
438
+
439
+ // src/thresholds.ts
440
+ var thresholdFailureMessage = (failures) => [
441
+ "Lighthouse thresholds not met:",
442
+ ...failures.map(
443
+ ({ category, minimum, score }) => `${category} scored ${score}, below the ${minimum} threshold`
444
+ )
445
+ ].join("\n");
446
+ var checkAgainstThresholds = (lhr, { thresholds = 100, ignoreError }) => {
447
+ const isFlat = typeof thresholds === "number";
448
+ const defaultMinimum = isFlat ? thresholds : 100;
449
+ const minimums = isFlat ? {} : thresholds ?? {};
450
+ const failures = Object.values(lhr.categories).filter((c) => c.score !== null).map(({ id, score }) => ({
451
+ category: id,
452
+ minimum: minimums[id] ?? defaultMinimum,
453
+ score: score * 100
454
+ })).filter(({ score, minimum }) => score < minimum);
455
+ if (!failures.length) {
456
+ return void 0;
457
+ }
458
+ if (!ignoreError) {
459
+ throw new Error(thresholdFailureMessage(failures));
460
+ }
461
+ return failures;
462
+ };
463
+ var writeReports = async (result, { directory, name }) => {
464
+ const formats = [result.lhr.configSettings.output].flat();
465
+ await mkdir(directory, { recursive: true });
466
+ await Promise.all(
467
+ [result.report].flat().map(
468
+ (report, i) => writeFile(path.join(directory, `${name}.${formats[i]}`), report)
469
+ )
470
+ );
471
+ };
472
+
473
+ // src/handle-audit-result.ts
474
+ var handleAuditResult = async ({
475
+ result,
476
+ reports,
477
+ thresholds,
478
+ ignoreError,
479
+ recommendations
480
+ }) => {
481
+ if (reports) {
482
+ await writeReports(result, reports);
483
+ }
484
+ if (recommendations !== false) {
485
+ logRecommendations(result.lhr, {
486
+ label: reports?.name,
487
+ ...recommendations
488
+ });
489
+ }
490
+ const failures = checkAgainstThresholds(result.lhr, {
491
+ thresholds,
492
+ ignoreError
493
+ });
494
+ return failures;
495
+ };
496
+ var runAudit = async ({
497
+ lighthouseArgs: { url, flags, config },
498
+ ...handleArgs
499
+ }) => {
500
+ const result = await lighthouse(url, flags, config);
501
+ if (!result) {
502
+ throw new Error(`Lighthouse returned no result for ${url}`);
503
+ }
504
+ const failures = await handleAuditResult({ result, ...handleArgs });
505
+ return { result, failures };
506
+ };
507
+
508
+ export { checkAgainstThresholds, handleAuditResult, logRecommendations, runAudit, toMerged, writeReports };
509
+ //# sourceMappingURL=chunk-4DICEJP6.js.map
510
+ //# sourceMappingURL=chunk-4DICEJP6.js.map