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 +90 -24
- package/dist/chunk-4DICEJP6.js +510 -0
- package/dist/chunk-4DICEJP6.js.map +1 -0
- package/dist/index.js +1 -194
- package/dist/index.js.map +1 -1
- package/dist/log-recommendations.d.ts +1 -1
- package/dist/playwright.d.ts +60 -0
- package/dist/playwright.js +78 -0
- package/dist/playwright.js.map +1 -0
- package/dist/thresholds.d.ts +1 -1
- package/package.json +82 -59
package/README.md
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# lighthouse-audit-utils
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
|
145
|
+
Accessibility: 100 — nothing to flag
|
|
144
146
|
```
|
|
145
147
|
|
|
146
|
-
##
|
|
148
|
+
## Individual utilities
|
|
147
149
|
|
|
148
|
-
|
|
149
|
-
|
|
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
|