lighthouse-audit-utils 1.0.1 → 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 +258 -192
- 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/package.json +25 -2
package/dist/index.js
CHANGED
|
@@ -1,196 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import lighthouse from 'lighthouse';
|
|
4
|
-
|
|
5
|
-
// lighthouse-audit-utils 1.0.1
|
|
6
|
-
|
|
7
|
-
// src/log-recommendations.ts
|
|
8
|
-
var resolveMetricSavings = (audit) => Object.entries(audit.metricSavings ?? {}).filter(([, ms]) => !!ms);
|
|
9
|
-
var maxMetricSavings = (audit) => Math.max(0, ...resolveMetricSavings(audit).map(([, ms]) => ms));
|
|
10
|
-
var metricValueType = (metric) => metric === "CLS" ? "numeric" : "ms";
|
|
11
|
-
var MEASURE_TYPES = [
|
|
12
|
-
"bytes",
|
|
13
|
-
"ms",
|
|
14
|
-
"timespanMs",
|
|
15
|
-
"numeric"
|
|
16
|
-
];
|
|
17
|
-
var formatValue = (value, valueType, maxValueLength) => {
|
|
18
|
-
if (value === void 0 || typeof value === "boolean") {
|
|
19
|
-
return "";
|
|
20
|
-
}
|
|
21
|
-
if (typeof value === "number") {
|
|
22
|
-
if (valueType === "bytes") {
|
|
23
|
-
const kib = value / 1024;
|
|
24
|
-
const rounded = kib >= 100 ? Math.round(kib) : Math.round(kib * 10) / 10;
|
|
25
|
-
return `${rounded} KiB`;
|
|
26
|
-
}
|
|
27
|
-
if (valueType === "ms" || valueType === "timespanMs") {
|
|
28
|
-
return `${Math.round(value)} ms`;
|
|
29
|
-
}
|
|
30
|
-
return String(Math.round(value * 100) / 100);
|
|
31
|
-
}
|
|
32
|
-
const text = (() => {
|
|
33
|
-
if (typeof value === "string") {
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
if (!("type" in value)) {
|
|
37
|
-
return "";
|
|
38
|
-
}
|
|
39
|
-
switch (value.type) {
|
|
40
|
-
case "url":
|
|
41
|
-
return value.value;
|
|
42
|
-
case "link":
|
|
43
|
-
return value.url || value.text;
|
|
44
|
-
case "code":
|
|
45
|
-
return String(value.value);
|
|
46
|
-
case "node":
|
|
47
|
-
return value.nodeLabel ?? value.selector ?? value.snippet ?? "";
|
|
48
|
-
case "source-location":
|
|
49
|
-
return `${value.url}:${value.line}:${value.column}`;
|
|
50
|
-
default:
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
})();
|
|
54
|
-
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
55
|
-
return singleLine.length > maxValueLength ? `${singleLine.slice(0, maxValueLength)}\u2026` : singleLine;
|
|
56
|
-
};
|
|
57
|
-
var formatItems = (details, { maxItems, maxValueLength }) => {
|
|
58
|
-
if (details?.type !== "opportunity" && details?.type !== "table") {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
const [identity, ...measures] = details.headings.filter(({ key }) => !!key);
|
|
62
|
-
if (!identity) {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
const lines = details.items.slice(0, maxItems).map((item) => {
|
|
66
|
-
const columns = [
|
|
67
|
-
formatValue(item[identity.key ?? ""], identity.valueType, maxValueLength),
|
|
68
|
-
...measures.filter(({ valueType }) => MEASURE_TYPES.includes(valueType)).map(
|
|
69
|
-
(heading) => `${heading.label}: ${formatValue(item[heading.key ?? ""], heading.valueType, maxValueLength)}`
|
|
70
|
-
)
|
|
71
|
-
];
|
|
72
|
-
return ` - ${columns.filter(Boolean).join(" \xB7 ")}`;
|
|
73
|
-
});
|
|
74
|
-
const remaining = details.items.length - maxItems;
|
|
75
|
-
if (remaining > 0) {
|
|
76
|
-
lines.push(` - \u2026and ${remaining} more`);
|
|
77
|
-
}
|
|
78
|
-
return lines;
|
|
79
|
-
};
|
|
80
|
-
var logRecommendations = (lhr, {
|
|
81
|
-
label,
|
|
82
|
-
maxItems = 5,
|
|
83
|
-
maxValueLength = 120
|
|
84
|
-
}) => {
|
|
85
|
-
const lines = [
|
|
86
|
-
"",
|
|
87
|
-
`\u2500\u2500\u2500\u2500\u2500 Lighthouse recommendations${label ? `: ${label}` : ""} \u2014 ${lhr.finalDisplayedUrl} \u2500\u2500\u2500\u2500\u2500`
|
|
88
|
-
];
|
|
89
|
-
for (const category of Object.values(lhr.categories)) {
|
|
90
|
-
const categoryScoreValue = category.score === null ? "N/A" : Math.round(category.score * 100);
|
|
91
|
-
const categoryLine = `${category.title}: ${categoryScoreValue}`;
|
|
92
|
-
const failing = category.auditRefs.map(({ id }) => lhr.audits[id]).filter((audit) => !!audit && audit.score !== null && audit.score < 1).sort(
|
|
93
|
-
(a, b) => maxMetricSavings(b) - maxMetricSavings(a) || (a.score ?? 0) - (b.score ?? 0)
|
|
94
|
-
);
|
|
95
|
-
if (!failing.length) {
|
|
96
|
-
lines.push("", `${categoryLine} \u2014 nothing to fix`);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
lines.push("", categoryLine);
|
|
100
|
-
for (const audit of failing) {
|
|
101
|
-
const displayValue = audit.displayValue ? ` (${audit.displayValue})` : "";
|
|
102
|
-
const savingsValue = (() => {
|
|
103
|
-
if (audit.displayValue?.toLowerCase().includes("savings")) {
|
|
104
|
-
return "";
|
|
105
|
-
}
|
|
106
|
-
const overallSavingsBytes = audit.details?.type === "opportunity" ? audit.details.overallSavingsBytes : void 0;
|
|
107
|
-
const estSavings = [
|
|
108
|
-
...resolveMetricSavings(audit).map(
|
|
109
|
-
([metric, value]) => `${metric} ${formatValue(value, metricValueType(metric), maxValueLength)}`
|
|
110
|
-
),
|
|
111
|
-
formatValue(overallSavingsBytes, "bytes", maxValueLength)
|
|
112
|
-
].filter(Boolean);
|
|
113
|
-
return estSavings.length ? ` \u2014 est. savings: ${estSavings.join(", ")}` : "";
|
|
114
|
-
})();
|
|
115
|
-
const scoreValue = audit.score === null ? "N/A" : Math.round(audit.score * 100);
|
|
116
|
-
lines.push(` \u2022 ${audit.title}${displayValue}${savingsValue}`);
|
|
117
|
-
lines.push(` ${audit.id} \xB7 score ${scoreValue}`);
|
|
118
|
-
lines.push(...formatItems(audit.details, { maxItems, maxValueLength }));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
lines.push("");
|
|
122
|
-
console.log(lines.join("\n"));
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// src/thresholds.ts
|
|
126
|
-
var thresholdFailureMessage = (failures) => [
|
|
127
|
-
"Lighthouse thresholds not met:",
|
|
128
|
-
...failures.map(
|
|
129
|
-
({ category, minimum, score }) => `${category} scored ${score}, below the ${minimum} threshold`
|
|
130
|
-
)
|
|
131
|
-
].join("\n");
|
|
132
|
-
var checkAgainstThresholds = (lhr, { thresholds = 100, ignoreError }) => {
|
|
133
|
-
const isFlat = typeof thresholds === "number";
|
|
134
|
-
const defaultMinimum = isFlat ? thresholds : 100;
|
|
135
|
-
const minimums = isFlat ? {} : thresholds ?? {};
|
|
136
|
-
const failures = Object.values(lhr.categories).filter((c) => c.score !== null).map(({ id, score }) => ({
|
|
137
|
-
category: id,
|
|
138
|
-
minimum: minimums[id] ?? defaultMinimum,
|
|
139
|
-
score: score * 100
|
|
140
|
-
})).filter(({ score, minimum }) => score < minimum);
|
|
141
|
-
if (!failures.length) {
|
|
142
|
-
return void 0;
|
|
143
|
-
}
|
|
144
|
-
if (!ignoreError) {
|
|
145
|
-
throw new Error(thresholdFailureMessage(failures));
|
|
146
|
-
}
|
|
147
|
-
return failures;
|
|
148
|
-
};
|
|
149
|
-
var writeReports = async (result, { directory, name }) => {
|
|
150
|
-
const formats = [result.lhr.configSettings.output].flat();
|
|
151
|
-
await mkdir(directory, { recursive: true });
|
|
152
|
-
await Promise.all(
|
|
153
|
-
[result.report].flat().map(
|
|
154
|
-
(report, i) => writeFile(path.join(directory, `${name}.${formats[i]}`), report)
|
|
155
|
-
)
|
|
156
|
-
);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// src/handle-audit-result.ts
|
|
160
|
-
var handleAuditResult = async ({
|
|
161
|
-
result,
|
|
162
|
-
reports,
|
|
163
|
-
thresholds,
|
|
164
|
-
ignoreError,
|
|
165
|
-
recommendations
|
|
166
|
-
}) => {
|
|
167
|
-
if (reports) {
|
|
168
|
-
await writeReports(result, reports);
|
|
169
|
-
}
|
|
170
|
-
if (recommendations !== false) {
|
|
171
|
-
logRecommendations(result.lhr, {
|
|
172
|
-
label: reports?.name,
|
|
173
|
-
...recommendations
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
const failures = checkAgainstThresholds(result.lhr, {
|
|
177
|
-
thresholds,
|
|
178
|
-
ignoreError
|
|
179
|
-
});
|
|
180
|
-
return failures;
|
|
181
|
-
};
|
|
182
|
-
var runAudit = async ({
|
|
183
|
-
lighthouseArgs: { url, flags, config },
|
|
184
|
-
...handleArgs
|
|
185
|
-
}) => {
|
|
186
|
-
const result = await lighthouse(url, flags, config);
|
|
187
|
-
if (!result) {
|
|
188
|
-
throw new Error(`Lighthouse returned no result for ${url}`);
|
|
189
|
-
}
|
|
190
|
-
const failures = await handleAuditResult({ result, ...handleArgs });
|
|
191
|
-
return { result, failures };
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
export { checkAgainstThresholds, handleAuditResult, logRecommendations, runAudit, writeReports };
|
|
1
|
+
export { checkAgainstThresholds, handleAuditResult, logRecommendations, runAudit, writeReports } from './chunk-4DICEJP6.js';
|
|
195
2
|
//# sourceMappingURL=index.js.map
|
|
196
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/log-recommendations.ts","../src/thresholds.ts","../src/write-reports.ts","../src/handle-audit-result.ts","../src/run-audit.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { RunnerResult } from 'lighthouse'\nimport type Details from 'lighthouse/types/lhr/audit-details'\n\ntype Lhr = RunnerResult['lhr']\ntype Audit = Lhr['audits'][string]\ntype AuditDetails = NonNullable<Audit['details']>\n\n/** resolves to values that are only non-zero numbers */\nconst resolveMetricSavings = (audit: Audit) =>\n Object.entries(audit.metricSavings ?? {}).filter(([, ms]) => !!ms) as [\n string,\n number,\n ][]\n\nconst maxMetricSavings = (audit: Audit) =>\n Math.max(0, ...resolveMetricSavings(audit).map(([, ms]) => ms))\n\n/** CLS savings are a unitless shift score; every other metric is milliseconds. */\nconst metricValueType = (metric: string): Details.ItemValueType =>\n metric === 'CLS' ? 'numeric' : 'ms'\n\n/** Columns worth repeating alongside an item's identity (URL, node, ...). */\nconst MEASURE_TYPES: Details.ItemValueType[] = [\n 'bytes',\n 'ms',\n 'timespanMs',\n 'numeric',\n]\n\nexport type FormattingArgs = {\n /**\n * Rows shown per audit before collapsing to \"…and N more\"\n * @default 5\n */\n maxItems?: number\n /**\n * Max length of a single value before it's truncated with an \"…\"\n * @default 120\n */\n maxValueLength?: number\n}\n\nconst formatValue = (\n value: Details.ItemValue | undefined,\n valueType: Details.ItemValueType,\n maxValueLength: number\n): string => {\n if (value === undefined || typeof value === 'boolean') {\n return ''\n }\n\n if (typeof value === 'number') {\n if (valueType === 'bytes') {\n // Keep a decimal on small values so sub-KiB assets don't read as \"0 KiB\".\n const kib = value / 1024\n const rounded = kib >= 100 ? Math.round(kib) : Math.round(kib * 10) / 10\n return `${rounded} KiB`\n }\n if (valueType === 'ms' || valueType === 'timespanMs') {\n return `${Math.round(value)} ms`\n }\n return String(Math.round(value * 100) / 100)\n }\n\n // `IcuMessage` is the only object value without a `type` tag.\n const text = (() => {\n if (typeof value === 'string') {\n return value\n }\n if (!('type' in value)) {\n return ''\n }\n switch (value.type) {\n case 'url':\n return value.value\n case 'link':\n return value.url || value.text\n case 'code':\n return String(value.value)\n case 'node':\n return value.nodeLabel ?? value.selector ?? value.snippet ?? ''\n case 'source-location':\n return `${value.url}:${value.line}:${value.column}`\n default:\n return ''\n }\n })()\n\n // Node labels and snippets carry the element's own line breaks — flatten them\n // so one item stays on one line.\n const singleLine = text.replace(/\\s+/g, ' ').trim()\n\n return singleLine.length > maxValueLength\n ? `${singleLine.slice(0, maxValueLength)}…`\n : singleLine\n}\n\n/**\n * The individual offenders the report UI lists under an audit — the specific\n * render-blocking scripts, oversized images, failing DOM nodes, and so on.\n */\nconst formatItems = (\n details: AuditDetails | undefined,\n { maxItems, maxValueLength }: Required<FormattingArgs>\n): string[] => {\n if (details?.type !== 'opportunity' && details?.type !== 'table') {\n return []\n }\n\n const [identity, ...measures] = details.headings.filter(({ key }) => !!key)\n if (!identity) {\n return []\n }\n\n const lines = details.items.slice(0, maxItems).map((item) => {\n const columns = [\n formatValue(item[identity.key ?? ''], identity.valueType, maxValueLength),\n ...measures\n .filter(({ valueType }) => MEASURE_TYPES.includes(valueType))\n .map(\n (heading) =>\n `${heading.label}: ${formatValue(item[heading.key ?? ''], heading.valueType, maxValueLength)}`\n ),\n ]\n return ` - ${columns.filter(Boolean).join(' · ')}`\n })\n\n const remaining = details.items.length - maxItems\n if (remaining > 0) {\n lines.push(` - …and ${remaining} more`)\n }\n return lines\n}\n\n/**\n * Print the same fix list the Lighthouse report UI shows, so a failing run is\n * actionable from the terminal/CI log without opening the HTML report.\n */\nexport const logRecommendations = (\n /** The Lighthouse result object */\n lhr: Lhr,\n {\n label,\n maxItems = 5,\n maxValueLength = 120,\n }: {\n /** Distinguishes runs of the same URL in one log (e.g. `desktop`/`mobile`). */\n label?: string\n } & FormattingArgs\n) => {\n const lines = [\n '',\n `───── Lighthouse recommendations${label ? `: ${label}` : ''} — ${lhr.finalDisplayedUrl} ─────`,\n ]\n\n for (const category of Object.values(lhr.categories)) {\n const categoryScoreValue =\n category.score === null ? 'N/A' : Math.round(category.score * 100)\n const categoryLine = `${category.title}: ${categoryScoreValue}`\n\n const failing = category.auditRefs\n .map(({ id }) => lhr.audits[id])\n .filter((audit) => !!audit && audit.score !== null && audit.score < 1)\n .sort(\n (a, b) =>\n maxMetricSavings(b) - maxMetricSavings(a) ||\n (a.score ?? 0) - (b.score ?? 0)\n )\n\n if (!failing.length) {\n lines.push('', `${categoryLine} — nothing to fix`)\n continue\n }\n\n lines.push('', categoryLine)\n\n for (const audit of failing) {\n const displayValue = audit.displayValue ? ` (${audit.displayValue})` : ''\n\n const savingsValue: string = (() => {\n // Most opportunities already spell out their savings in `displayValue`.\n if (audit.displayValue?.toLowerCase().includes('savings')) {\n return ''\n }\n\n const overallSavingsBytes =\n audit.details?.type === 'opportunity'\n ? audit.details.overallSavingsBytes\n : undefined\n const estSavings = [\n ...resolveMetricSavings(audit).map(\n ([metric, value]) =>\n `${metric} ${formatValue(value, metricValueType(metric), maxValueLength)}`\n ),\n formatValue(overallSavingsBytes, 'bytes', maxValueLength),\n ].filter(Boolean)\n\n return estSavings.length\n ? ` — est. savings: ${estSavings.join(', ')}`\n : ''\n })()\n\n const scoreValue =\n audit.score === null ? 'N/A' : Math.round(audit.score * 100)\n\n lines.push(` • ${audit.title}${displayValue}${savingsValue}`)\n lines.push(` ${audit.id} · score ${scoreValue}`)\n lines.push(...formatItems(audit.details, { maxItems, maxValueLength }))\n }\n }\n\n lines.push('')\n console.log(lines.join('\\n'))\n}\n","import type { RunnerResult } from 'lighthouse'\n\ntype Lhr = RunnerResult['lhr']\ntype CategoryResult = Lhr['categories'][string]\n\nexport type Category =\n | 'performance'\n | 'accessibility'\n | 'best-practices'\n | 'seo'\n | 'agentic-browsing'\n\n/** Per-category minimums, or one number applied to every category. */\nexport type LighthouseThresholds = number | Partial<Record<Category, number>>\n\n/** A category that scored below the minimum it was given. */\nexport type ThresholdFailure = {\n /** The category's id as the run reported it — usually a `Category`. */\n category: string\n /** The minimum this category was checked against (0-100). */\n minimum: number\n /** What it actually scored (0-100). */\n score: number\n}\n\nconst thresholdFailureMessage = (failures: ThresholdFailure[]) =>\n [\n 'Lighthouse thresholds not met:',\n ...failures.map(\n ({ category, minimum, score }) =>\n `${category} scored ${score}, below the ${minimum} threshold`\n ),\n ].join('\\n')\n\nexport type ThresholdsArgs = {\n /**\n * Minimum category scores (0-100), either per category or one number for all\n * of them. If per category, any category omitted must score 100.\n */\n thresholds?: LighthouseThresholds\n /** Return the failures rather than throwing them. */\n ignoreError?: boolean\n}\n\n/**\n * Checks every category the run scored, throwing an error describing the ones\n * that fell short. Pass `ignoreError` to get those shortfalls back instead, so\n * the caller can log the recommendations before failing.\n */\nexport const checkAgainstThresholds = (\n /** The Lighthouse result object */\n lhr: Lhr,\n { thresholds = 100, ignoreError }: ThresholdsArgs\n): ThresholdFailure[] | undefined => {\n const isFlat = typeof thresholds === 'number'\n const defaultMinimum = isFlat ? thresholds : 100\n const minimums: Record<string, number | undefined> = isFlat\n ? {}\n : (thresholds ?? {})\n\n const failures = Object.values(lhr.categories)\n .filter((c): c is CategoryResult & { score: number } => c.score !== null)\n .map(({ id, score }) => ({\n category: id,\n minimum: minimums[id] ?? defaultMinimum,\n score: score * 100,\n }))\n .filter(({ score, minimum }) => score < minimum)\n\n if (!failures.length) {\n return undefined\n }\n\n if (!ignoreError) {\n throw new Error(thresholdFailureMessage(failures))\n }\n\n return failures\n}\n","import { mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { RunnerResult } from 'lighthouse'\n\nexport type WriteReportsArgs = { directory: string; name: string }\n\n/**\n * Writes each report to `<directory>/<name>.<format>`, using the formats the\n * run's `output` flag asked for.\n */\nexport const writeReports = async (\n result: RunnerResult,\n { directory, name }: WriteReportsArgs\n) => {\n const formats = [result.lhr.configSettings.output].flat()\n\n await mkdir(directory, { recursive: true })\n await Promise.all(\n [result.report]\n .flat()\n .map((report, i) =>\n writeFile(path.join(directory, `${name}.${formats[i]}`), report)\n )\n )\n}\n","import type { RunnerResult } from 'lighthouse'\n\nimport { type FormattingArgs, logRecommendations } from './log-recommendations'\nimport { checkAgainstThresholds, type ThresholdsArgs } from './thresholds'\nimport { type WriteReportsArgs, writeReports } from './write-reports'\n\nexport type HandleAuditResultArgs = {\n /** The full `RunnerResult` from a Lighthouse run */\n result: RunnerResult\n /** Where to write the reports. Omit to skip writing them. */\n reports?: WriteReportsArgs\n /** Options for recommendations logging. Set to `false` to disable. */\n recommendations?:\n | (FormattingArgs & {\n /** Distinguishes runs of the same URL in one log. Defaults to `reports.name`. */\n label?: string\n })\n | false\n} & ThresholdsArgs\n\n/**\n * Everything you'd do with a finished Lighthouse run:\n * 1. write the reports\n * 2. log the recommendations\n * 3. check the scores against the thresholds (goes last so reporting occurs before throwing).\n *\n * @returns the threshold failures, if `ignoreError` kept them from throwing\n */\nexport const handleAuditResult = async ({\n result,\n reports,\n thresholds,\n ignoreError,\n recommendations,\n}: HandleAuditResultArgs) => {\n if (reports) {\n await writeReports(result, reports)\n }\n\n if (recommendations !== false) {\n logRecommendations(result.lhr, {\n label: reports?.name,\n ...recommendations,\n })\n }\n\n const failures = checkAgainstThresholds(result.lhr, {\n thresholds,\n ignoreError,\n })\n\n return failures\n}\n","import lighthouse, { type Config, type Flags } from 'lighthouse'\n\nimport {\n type HandleAuditResultArgs,\n handleAuditResult,\n} from './handle-audit-result'\n\n/** What `lighthouse()` itself takes, in the order it takes it. */\nexport type LighthouseArgs = {\n /** The URL to audit */\n url: string\n /** Settings for the run, e.g. the `port` Lighthouse connects to */\n flags?: Flags\n /** Overrides the default config, e.g. `desktopConfig` */\n config?: Config\n}\n\n/**\n * Audits a URL and hands the result straight to {@link handleAuditResult}.\n *\n * @returns the run's result, plus the threshold failures if `ignoreError` kept\n * them from throwing\n */\nexport const runAudit = async ({\n lighthouseArgs: { url, flags, config },\n ...handleArgs\n}: {\n /** Passed through to `lighthouse()` */\n lighthouseArgs: LighthouseArgs\n} & Omit<HandleAuditResultArgs, 'result'>) => {\n const result = await lighthouse(url, flags, config)\n if (!result) {\n throw new Error(`Lighthouse returned no result for ${url}`)\n }\n\n const failures = await handleAuditResult({ result, ...handleArgs })\n\n return { result, failures }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -13,7 +13,7 @@ export type FormattingArgs = {
|
|
|
13
13
|
maxValueLength?: number;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* Print the same
|
|
16
|
+
* Print the same recommendations the Lighthouse report UI shows, so a run is
|
|
17
17
|
* actionable from the terminal/CI log without opening the HTML report.
|
|
18
18
|
*/
|
|
19
19
|
export declare const logRecommendations: (
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { chromium, type PlaywrightTestArgs, type PlaywrightTestOptions, type PlaywrightWorkerArgs, type PlaywrightWorkerOptions, type TestInfo, type TestType } from '@playwright/test';
|
|
2
|
+
import type { HandleAuditResultArgs } from './handle-audit-result';
|
|
3
|
+
import type { LighthouseArgs } from './run-audit';
|
|
4
|
+
import { runAudit as runLighthouse } from './run-audit';
|
|
5
|
+
import type { WriteReportsArgs } from './write-reports';
|
|
6
|
+
/** What `runAudit` takes on top of the overrides it shares with the fixture. */
|
|
7
|
+
export type RunAuditOptions = {
|
|
8
|
+
/** Names this run's reports, so audits in the same test don't overwrite each other. */
|
|
9
|
+
name?: string;
|
|
10
|
+
} & AuditOverrides;
|
|
11
|
+
/** Settable per fixture and again per `runAudit` call, the later one winning. */
|
|
12
|
+
type AuditOverrides = {
|
|
13
|
+
/**
|
|
14
|
+
* - Passed to `lighthouse()`, minus the `url` — that's whatever page the test
|
|
15
|
+
* is on.
|
|
16
|
+
* - `flags` merge over the worker's `port`, the only one set for you
|
|
17
|
+
* - `config` deep merges, so `settings` layer rather than replacing each other.
|
|
18
|
+
* - Config arrays merge by index, so list settings like `skipAudits` are best
|
|
19
|
+
* set at one level only.
|
|
20
|
+
*/
|
|
21
|
+
lighthouseArgs?: Omit<LighthouseArgs, 'url'>;
|
|
22
|
+
} & Omit<HandleAuditResultArgs, 'result' | 'reports'>;
|
|
23
|
+
export type LighthouseFixtures = {
|
|
24
|
+
/**
|
|
25
|
+
* Runs one Lighthouse audit against the current page. Call it again for a
|
|
26
|
+
* second form factor, passing that form factor's `config` and `name`.
|
|
27
|
+
*/
|
|
28
|
+
runAudit: (options?: RunAuditOptions) => ReturnType<typeof runLighthouse>;
|
|
29
|
+
};
|
|
30
|
+
export type LighthouseWorkerFixtures = {
|
|
31
|
+
/** CDP port Lighthouse connects to. Unique per worker so parallel workers don't collide. */
|
|
32
|
+
lighthousePort: number;
|
|
33
|
+
};
|
|
34
|
+
/** Everything the reports' location can be decided from. */
|
|
35
|
+
export type ReportsContext = {
|
|
36
|
+
testInfo: TestInfo;
|
|
37
|
+
/** The `name` the `runAudit` call passed, if any. */
|
|
38
|
+
name?: string;
|
|
39
|
+
};
|
|
40
|
+
export type LighthouseFixtureOptions = {
|
|
41
|
+
/** First worker's CDP port, e.g. `9222`; each further worker gets the next one up. */
|
|
42
|
+
basePort: number;
|
|
43
|
+
/** Where each run's reports go. Pass `false` to skip writing them. */
|
|
44
|
+
reports?: ((context: ReportsContext) => WriteReportsArgs) | false;
|
|
45
|
+
/** Merged into the persistent context launch, which already sets the CDP port and `baseURL`. */
|
|
46
|
+
launchOptions?: Parameters<typeof chromium.launchPersistentContext>[1];
|
|
47
|
+
} & AuditOverrides;
|
|
48
|
+
type BaseTest = TestType<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
|
|
49
|
+
/**
|
|
50
|
+
* Adds a `runAudit` fixture that audits whatever page the test is currently on.
|
|
51
|
+
*
|
|
52
|
+
* Lighthouse navigates over the CDP port itself rather than driving the
|
|
53
|
+
* Playwright `page`, so `context` is overridden to launch a persistent Chrome
|
|
54
|
+
* profile reachable on that port — the page Playwright drives and the page
|
|
55
|
+
* Lighthouse audits then live in the same profile.
|
|
56
|
+
*
|
|
57
|
+
* @param test the test to extend, so callers can layer this onto their own fixtures
|
|
58
|
+
*/
|
|
59
|
+
export declare const withLighthouse: <T extends BaseTest>({ basePort, reports, launchOptions, ...fixtureOverrides }: LighthouseFixtureOptions, test?: T) => TestType<PlaywrightTestArgs & PlaywrightTestOptions & LighthouseFixtures, PlaywrightWorkerArgs & PlaywrightWorkerOptions & LighthouseWorkerFixtures>;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { runAudit, toMerged } from './chunk-4DICEJP6.js';
|
|
2
|
+
import { rm } from 'fs/promises';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { test, chromium } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
var defaultReports = ({ testInfo, name }) => ({
|
|
8
|
+
directory: testInfo.outputPath("lighthouse"),
|
|
9
|
+
name: name ?? "lighthouse"
|
|
10
|
+
});
|
|
11
|
+
var mergeThresholds = (base, override) => {
|
|
12
|
+
if (override === void 0) {
|
|
13
|
+
return base;
|
|
14
|
+
}
|
|
15
|
+
if (typeof override === "number" || typeof base !== "object") {
|
|
16
|
+
return override;
|
|
17
|
+
}
|
|
18
|
+
return { ...base, ...override };
|
|
19
|
+
};
|
|
20
|
+
var withLighthouse = ({
|
|
21
|
+
basePort,
|
|
22
|
+
reports = defaultReports,
|
|
23
|
+
launchOptions,
|
|
24
|
+
...fixtureOverrides
|
|
25
|
+
}, test$1 = test) => test$1.extend({
|
|
26
|
+
runAudit: async ({ page, lighthousePort: port }, use, testInfo) => {
|
|
27
|
+
await use(({ name, lighthouseArgs, thresholds, ...callArgs } = {}) => {
|
|
28
|
+
const {
|
|
29
|
+
lighthouseArgs: baseArgs,
|
|
30
|
+
thresholds: baseThresholds,
|
|
31
|
+
...handleArgs
|
|
32
|
+
} = { ...fixtureOverrides, ...callArgs };
|
|
33
|
+
return runAudit({
|
|
34
|
+
lighthouseArgs: {
|
|
35
|
+
url: page.url(),
|
|
36
|
+
flags: { port, ...baseArgs?.flags, ...lighthouseArgs?.flags },
|
|
37
|
+
config: toMerged(
|
|
38
|
+
baseArgs?.config ?? {},
|
|
39
|
+
lighthouseArgs?.config ?? {}
|
|
40
|
+
)
|
|
41
|
+
},
|
|
42
|
+
reports: reports ? reports({ testInfo, name }) : void 0,
|
|
43
|
+
thresholds: mergeThresholds(baseThresholds, thresholds),
|
|
44
|
+
...handleArgs
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
lighthousePort: [
|
|
49
|
+
// biome-ignore lint/correctness/noEmptyPattern: Playwright requires destructuring for the fixtures arg
|
|
50
|
+
async ({}, use, workerInfo) => {
|
|
51
|
+
await use(basePort + workerInfo.parallelIndex);
|
|
52
|
+
},
|
|
53
|
+
{ scope: "worker" }
|
|
54
|
+
],
|
|
55
|
+
context: async ({ lighthousePort: port, baseURL }, use) => {
|
|
56
|
+
const userDataDir = path.join(
|
|
57
|
+
os.tmpdir(),
|
|
58
|
+
`pw-lighthouse-${port}-${Date.now()}`
|
|
59
|
+
);
|
|
60
|
+
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
61
|
+
baseURL,
|
|
62
|
+
...launchOptions,
|
|
63
|
+
args: [
|
|
64
|
+
`--remote-debugging-port=${port}`,
|
|
65
|
+
...launchOptions?.args ?? []
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
await use(context);
|
|
69
|
+
await context.close();
|
|
70
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(
|
|
71
|
+
() => void 0
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export { withLighthouse };
|
|
77
|
+
//# sourceMappingURL=playwright.js.map
|
|
78
|
+
//# sourceMappingURL=playwright.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/playwright.ts"],"names":["test","baseTest"],"mappings":"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","file":"playwright.js","sourcesContent":["import { rm } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport {\n test as baseTest,\n chromium,\n type PlaywrightTestArgs,\n type PlaywrightTestOptions,\n type PlaywrightWorkerArgs,\n type PlaywrightWorkerOptions,\n type TestInfo,\n type TestType,\n} from '@playwright/test'\nimport { toMerged } from 'es-toolkit'\n\nimport type { HandleAuditResultArgs } from './handle-audit-result'\nimport type { LighthouseArgs } from './run-audit'\nimport { runAudit as runLighthouse } from './run-audit'\nimport type { WriteReportsArgs } from './write-reports'\n\n/** What `runAudit` takes on top of the overrides it shares with the fixture. */\nexport type RunAuditOptions = {\n /** Names this run's reports, so audits in the same test don't overwrite each other. */\n name?: string\n} & AuditOverrides\n\n/** Settable per fixture and again per `runAudit` call, the later one winning. */\ntype AuditOverrides = {\n /**\n * - Passed to `lighthouse()`, minus the `url` — that's whatever page the test\n * is on.\n * - `flags` merge over the worker's `port`, the only one set for you\n * - `config` deep merges, so `settings` layer rather than replacing each other.\n * - Config arrays merge by index, so list settings like `skipAudits` are best\n * set at one level only.\n */\n lighthouseArgs?: Omit<LighthouseArgs, 'url'>\n} & Omit<HandleAuditResultArgs, 'result' | 'reports'>\n\nexport type LighthouseFixtures = {\n /**\n * Runs one Lighthouse audit against the current page. Call it again for a\n * second form factor, passing that form factor's `config` and `name`.\n */\n runAudit: (options?: RunAuditOptions) => ReturnType<typeof runLighthouse>\n}\n\nexport type LighthouseWorkerFixtures = {\n /** CDP port Lighthouse connects to. Unique per worker so parallel workers don't collide. */\n lighthousePort: number\n}\n\n/** Everything the reports' location can be decided from. */\nexport type ReportsContext = {\n testInfo: TestInfo\n /** The `name` the `runAudit` call passed, if any. */\n name?: string\n}\n\nexport type LighthouseFixtureOptions = {\n /** First worker's CDP port, e.g. `9222`; each further worker gets the next one up. */\n basePort: number\n /** Where each run's reports go. Pass `false` to skip writing them. */\n reports?: ((context: ReportsContext) => WriteReportsArgs) | false\n /** Merged into the persistent context launch, which already sets the CDP port and `baseURL`. */\n launchOptions?: Parameters<typeof chromium.launchPersistentContext>[1]\n} & AuditOverrides\n\nconst defaultReports = ({ testInfo, name }: ReportsContext) => ({\n directory: testInfo.outputPath('lighthouse'),\n name: name ?? 'lighthouse',\n})\n\n/** Object thresholds merge; anything else replaces, since a flat number can't be partially overridden. */\nconst mergeThresholds = (\n base: AuditOverrides['thresholds'],\n override: AuditOverrides['thresholds']\n): AuditOverrides['thresholds'] => {\n if (override === undefined) {\n return base\n }\n if (typeof override === 'number' || typeof base !== 'object') {\n return override\n }\n return { ...base, ...override }\n}\n\ntype BaseTest = TestType<\n PlaywrightTestArgs & PlaywrightTestOptions,\n PlaywrightWorkerArgs & PlaywrightWorkerOptions\n>\n\n/**\n * Adds a `runAudit` fixture that audits whatever page the test is currently on.\n *\n * Lighthouse navigates over the CDP port itself rather than driving the\n * Playwright `page`, so `context` is overridden to launch a persistent Chrome\n * profile reachable on that port — the page Playwright drives and the page\n * Lighthouse audits then live in the same profile.\n *\n * @param test the test to extend, so callers can layer this onto their own fixtures\n */\nexport const withLighthouse = <T extends BaseTest>(\n {\n basePort,\n reports = defaultReports,\n launchOptions,\n ...fixtureOverrides\n }: LighthouseFixtureOptions,\n test: T = baseTest as unknown as T\n) =>\n test.extend<LighthouseFixtures, LighthouseWorkerFixtures>({\n runAudit: async ({ page, lighthousePort: port }, use, testInfo) => {\n // `lighthouseArgs` and `thresholds` merge with the fixture's rather than\n // replacing them, so a call only has to name what it changes.\n await use(({ name, lighthouseArgs, thresholds, ...callArgs } = {}) => {\n const {\n lighthouseArgs: baseArgs,\n thresholds: baseThresholds,\n ...handleArgs\n } = { ...fixtureOverrides, ...callArgs }\n\n return runLighthouse({\n lighthouseArgs: {\n url: page.url(),\n flags: { port, ...baseArgs?.flags, ...lighthouseArgs?.flags },\n config: toMerged(\n baseArgs?.config ?? {},\n lighthouseArgs?.config ?? {}\n ),\n },\n reports: reports ? reports({ testInfo, name }) : undefined,\n thresholds: mergeThresholds(baseThresholds, thresholds),\n ...handleArgs,\n })\n })\n },\n\n lighthousePort: [\n // biome-ignore lint/correctness/noEmptyPattern: Playwright requires destructuring for the fixtures arg\n async ({}, use, workerInfo) => {\n await use(basePort + workerInfo.parallelIndex)\n },\n { scope: 'worker' },\n ],\n\n context: async ({ lighthousePort: port, baseURL }, use) => {\n const userDataDir = path.join(\n os.tmpdir(),\n `pw-lighthouse-${port}-${Date.now()}`\n )\n\n const context = await chromium.launchPersistentContext(userDataDir, {\n baseURL,\n ...launchOptions,\n args: [\n `--remote-debugging-port=${port}`,\n ...(launchOptions?.args ?? []),\n ],\n })\n\n await use(context)\n await context.close()\n\n // Best-effort cleanup; a leftover profile dir doesn't affect test correctness.\n await rm(userDataDir, { recursive: true, force: true }).catch(\n () => undefined\n )\n },\n })\n"]}
|
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lighthouse-audit-utils",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.3",
|
|
4
|
+
"description": "Programmatic Lighthouse audit utilities for CI: check scores against thresholds, write HTML/JSON reports to disk, and log recommendations to the terminal. Includes a Playwright fixture.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lighthouse",
|
|
7
|
+
"lighthouse-ci",
|
|
7
8
|
"playwright",
|
|
9
|
+
"playwright-test",
|
|
8
10
|
"performance",
|
|
11
|
+
"performance-testing",
|
|
12
|
+
"performance-budget",
|
|
13
|
+
"web-vitals",
|
|
14
|
+
"core-web-vitals",
|
|
9
15
|
"ci",
|
|
16
|
+
"continuous-integration",
|
|
17
|
+
"audit",
|
|
18
|
+
"accessibility",
|
|
19
|
+
"seo",
|
|
10
20
|
"terminal"
|
|
11
21
|
],
|
|
12
22
|
"repository": {
|
|
@@ -24,6 +34,10 @@
|
|
|
24
34
|
".": {
|
|
25
35
|
"types": "./dist/index.d.ts",
|
|
26
36
|
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./playwright": {
|
|
39
|
+
"types": "./dist/playwright.d.ts",
|
|
40
|
+
"default": "./dist/playwright.js"
|
|
27
41
|
}
|
|
28
42
|
},
|
|
29
43
|
"files": [
|
|
@@ -47,11 +61,20 @@
|
|
|
47
61
|
"ts:check": "tsc --noEmit"
|
|
48
62
|
},
|
|
49
63
|
"peerDependencies": {
|
|
64
|
+
"@playwright/test": "^1",
|
|
50
65
|
"lighthouse": "^12 || ^13"
|
|
51
66
|
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@playwright/test": {
|
|
69
|
+
"optional": true,
|
|
70
|
+
"reason": "only needed for the lighthouse-audit-utils/playwright entrypoint"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
52
73
|
"devDependencies": {
|
|
53
74
|
"@biomejs/biome": "^2",
|
|
75
|
+
"@playwright/test": "^1",
|
|
54
76
|
"@types/node": "^24",
|
|
77
|
+
"es-toolkit": "^1",
|
|
55
78
|
"lighthouse": "^13",
|
|
56
79
|
"tsup": "^8",
|
|
57
80
|
"typescript": "^7"
|