@nitpicker/analyze-lighthouse 0.4.1
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/CHANGELOG.md +8 -0
- package/LICENSE +191 -0
- package/README.md +13 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.js +156 -0
- package/lib/types.d.ts +148 -0
- package/lib/types.js +1 -0
- package/package.json +33 -0
- package/src/index.ts +228 -0
- package/src/types.ts +189 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { LHReport } from './types.js';
|
|
2
|
+
import type { TableValue, Violation } from '@nitpicker/types';
|
|
3
|
+
import type { Config } from 'lighthouse';
|
|
4
|
+
|
|
5
|
+
import { definePlugin } from '@nitpicker/core';
|
|
6
|
+
import * as chromeLauncher from 'chrome-launcher';
|
|
7
|
+
import lighthouse from 'lighthouse';
|
|
8
|
+
import { ReportUtils } from 'lighthouse/report/renderer/report-utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Plugin options for the Lighthouse analysis.
|
|
12
|
+
*/
|
|
13
|
+
type Options = {
|
|
14
|
+
/**
|
|
15
|
+
* Custom Lighthouse configuration object.
|
|
16
|
+
* Passed directly to `lighthouse()` as the third argument,
|
|
17
|
+
* allowing callers to override categories, throttling, etc.
|
|
18
|
+
* @see https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md
|
|
19
|
+
*/
|
|
20
|
+
config?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-URL index that accumulates Lighthouse scores and individual audit results.
|
|
25
|
+
* Keyed by URL string so that the final report can iterate over all analyzed pages.
|
|
26
|
+
*/
|
|
27
|
+
type IndexData = Record<
|
|
28
|
+
string,
|
|
29
|
+
{
|
|
30
|
+
/** Reserved for future structured detail strings. */
|
|
31
|
+
details: string[];
|
|
32
|
+
/** Category-level scores (0-100) keyed by category id. */
|
|
33
|
+
scores: Record<string, number>;
|
|
34
|
+
/** Individual audit results that did not pass. */
|
|
35
|
+
results: Result[];
|
|
36
|
+
}
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A single non-passing audit result extracted from the Lighthouse report.
|
|
41
|
+
*/
|
|
42
|
+
type Result = {
|
|
43
|
+
/** Rating level: `"fail"`, `"average"`, or `"error"`. */
|
|
44
|
+
severity: string;
|
|
45
|
+
/** Category title with optional group suffix (e.g. `"Performance(metrics)"`). */
|
|
46
|
+
rule: string;
|
|
47
|
+
/** Formatted score string (e.g. `"(Score: 45)"`). */
|
|
48
|
+
code: string;
|
|
49
|
+
/** Audit title, display value, and description concatenated. */
|
|
50
|
+
message: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Analyze plugin that runs Google Lighthouse against each page.
|
|
55
|
+
*
|
|
56
|
+
* A fresh Chrome instance is launched via `chrome-launcher` for every page
|
|
57
|
+
* because Lighthouse requires exclusive control of the browser's DevTools
|
|
58
|
+
* protocol. Sharing a browser across pages would cause protocol conflicts.
|
|
59
|
+
*
|
|
60
|
+
* The plugin evaluates the four standard Lighthouse categories:
|
|
61
|
+
* Performance, Accessibility, Best Practices, and SEO.
|
|
62
|
+
* Each category score (0-100) is reported as a separate column, and
|
|
63
|
+
* individual non-passing audits are collected as violations.
|
|
64
|
+
*
|
|
65
|
+
* When Lighthouse throws (e.g. navigation timeout), the plugin returns
|
|
66
|
+
* zero scores with an "Error" note rather than failing the entire analysis,
|
|
67
|
+
* so that partial results from other pages are preserved.
|
|
68
|
+
* @example
|
|
69
|
+
* ```jsonc
|
|
70
|
+
* // nitpicker.config.json
|
|
71
|
+
* {
|
|
72
|
+
* "plugins": {
|
|
73
|
+
* "analyze": {
|
|
74
|
+
* "@nitpicker/analyze-lighthouse": {}
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export default definePlugin((options: Options) => {
|
|
81
|
+
return {
|
|
82
|
+
label: 'Lighthouse: パフォーマンス監査',
|
|
83
|
+
headers: {
|
|
84
|
+
performance: 'Performance',
|
|
85
|
+
accessibility: 'Accessibility',
|
|
86
|
+
'best-practices': 'Best Practices',
|
|
87
|
+
seo: 'SEO',
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async eachPage({ url }) {
|
|
91
|
+
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
|
|
92
|
+
const config = options.config as Config;
|
|
93
|
+
const reports: IndexData = {};
|
|
94
|
+
|
|
95
|
+
const result = await lighthouse(url.href, { port: chrome.port }, config).catch(
|
|
96
|
+
(error: unknown) => {
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
return error;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!result || result instanceof Error) {
|
|
105
|
+
return {
|
|
106
|
+
page: {
|
|
107
|
+
performance: { value: 0, note: 'Error' },
|
|
108
|
+
accessibility: { value: 0, note: 'Error' },
|
|
109
|
+
'best-practices': { value: 0, note: 'Error' },
|
|
110
|
+
seo: { value: 0, note: 'Error' },
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const report: LHReport = ReportUtils.prepareReportResult(result.lhr);
|
|
116
|
+
|
|
117
|
+
const scores: Record<string, number> = {};
|
|
118
|
+
for (const cat of Object.values(report.categories)) {
|
|
119
|
+
scores[cat.id] = Math.round(cat.score * 100);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const results: Result[] = [];
|
|
123
|
+
for (const cat of Object.values(report.categories)) {
|
|
124
|
+
for (const audit of cat.auditRefs) {
|
|
125
|
+
const result = audit.result;
|
|
126
|
+
if (
|
|
127
|
+
result.scoreDisplayMode === 'notApplicable' ||
|
|
128
|
+
result.scoreDisplayMode === 'error'
|
|
129
|
+
) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const rating = ReportUtils.calculateRating(
|
|
133
|
+
result.score,
|
|
134
|
+
result.scoreDisplayMode,
|
|
135
|
+
) as 'pass' | 'average' | 'fail' | 'error';
|
|
136
|
+
if (rating === 'pass') {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const label = cat.title + (audit.group ? `(${audit.group})` : '');
|
|
140
|
+
const score = `(Score: ${Math.round(result.score * 100)})`;
|
|
141
|
+
const value = result.displayValue ? ` (${result.displayValue})` : '';
|
|
142
|
+
results.push({
|
|
143
|
+
severity: rating,
|
|
144
|
+
rule: label,
|
|
145
|
+
code: score,
|
|
146
|
+
message: `${result.title}:${value} ${result.description}`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
reports[url.href] = {
|
|
152
|
+
details: [],
|
|
153
|
+
scores,
|
|
154
|
+
results,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await chrome.kill();
|
|
158
|
+
type Header = {
|
|
159
|
+
performance: string;
|
|
160
|
+
accessibility: string;
|
|
161
|
+
'best-practices': string;
|
|
162
|
+
seo: string;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const data: Record<string, Record<keyof Header, TableValue>> = {};
|
|
166
|
+
const totalPoints: Record<keyof Header, number> = {
|
|
167
|
+
performance: 0,
|
|
168
|
+
accessibility: 0,
|
|
169
|
+
'best-practices': 0,
|
|
170
|
+
seo: 0,
|
|
171
|
+
};
|
|
172
|
+
const violations: Violation[] = [];
|
|
173
|
+
const pageList = Object.keys(reports);
|
|
174
|
+
for (const url of pageList) {
|
|
175
|
+
const scores = reports[url]?.scores ?? {};
|
|
176
|
+
data[url] = {
|
|
177
|
+
performance: { value: scores.performance ?? 0 },
|
|
178
|
+
accessibility: { value: scores.accessibility ?? 0 },
|
|
179
|
+
['best-practices']: { value: scores['best-practices'] ?? 0 },
|
|
180
|
+
seo: { value: scores.seo ?? 0 },
|
|
181
|
+
};
|
|
182
|
+
totalPoints.performance += scores.performance ?? 0;
|
|
183
|
+
totalPoints.accessibility += scores.accessibility ?? 0;
|
|
184
|
+
totalPoints['best-practices'] += scores['best-practices'] ?? 0;
|
|
185
|
+
totalPoints.seo += scores.seo ?? 0;
|
|
186
|
+
const results = reports[url]?.results ?? [];
|
|
187
|
+
violations.push(
|
|
188
|
+
...results.map((result) => ({
|
|
189
|
+
validator: 'lighthouse',
|
|
190
|
+
severity: result.severity,
|
|
191
|
+
rule: result.rule,
|
|
192
|
+
code: result.code,
|
|
193
|
+
message: result.message,
|
|
194
|
+
url,
|
|
195
|
+
})),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
page: {
|
|
200
|
+
performance: {
|
|
201
|
+
value: Math.round(report.categories.performance.score * 100),
|
|
202
|
+
note: report.categories.performance.auditRefs
|
|
203
|
+
.map((a) => `${a.result.title}: ${a.result.description}`)
|
|
204
|
+
.join('\n'),
|
|
205
|
+
},
|
|
206
|
+
accessibility: {
|
|
207
|
+
value: Math.round(report.categories.accessibility.score * 100),
|
|
208
|
+
note: report.categories.accessibility.auditRefs
|
|
209
|
+
.map((a) => `${a.result.title}: ${a.result.description}`)
|
|
210
|
+
.join('\n'),
|
|
211
|
+
},
|
|
212
|
+
'best-practices': {
|
|
213
|
+
value: Math.round(report.categories['best-practices'].score * 100),
|
|
214
|
+
note: report.categories['best-practices'].auditRefs
|
|
215
|
+
.map((a) => `${a.result.title}: ${a.result.description}`)
|
|
216
|
+
.join('\n'),
|
|
217
|
+
},
|
|
218
|
+
seo: {
|
|
219
|
+
value: Math.round(report.categories.seo.score * 100),
|
|
220
|
+
note: report.categories.seo.auditRefs
|
|
221
|
+
.map((a) => `${a.result.title}: ${a.result.description}`)
|
|
222
|
+
.join('\n'),
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed subset of a Lighthouse report produced by `Util.prepareReportResult()`.
|
|
3
|
+
*
|
|
4
|
+
* Only the five standard Lighthouse categories are included because
|
|
5
|
+
* the plugin creates dedicated score columns for each one.
|
|
6
|
+
* The `auditRefs` array carries the resolved `result` inline so that
|
|
7
|
+
* consumers can iterate category -> audit without an extra lookup.
|
|
8
|
+
*/
|
|
9
|
+
export type LHReport = {
|
|
10
|
+
categories: Record<
|
|
11
|
+
'performance' | 'accessibility' | 'best-practices' | 'seo' | 'pwa',
|
|
12
|
+
{
|
|
13
|
+
/** Machine-readable category identifier matching the record key. */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Human-readable category title (e.g. "Performance"). */
|
|
16
|
+
title: string;
|
|
17
|
+
/** Markdown description of the category. */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Description for audits that require manual verification. */
|
|
20
|
+
manualDescription: string;
|
|
21
|
+
/** Aggregate score for the category in the 0-1 range. */
|
|
22
|
+
score: number;
|
|
23
|
+
/** Ordered list of audits that contribute to this category. */
|
|
24
|
+
auditRefs: {
|
|
25
|
+
/** Audit identifier (e.g. `"first-contentful-paint"`). */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Relative weight of this audit within the category score. */
|
|
28
|
+
weight: number;
|
|
29
|
+
/** Audit group label (e.g. `"metrics"`, `"opportunities"`). */
|
|
30
|
+
group?: string;
|
|
31
|
+
/** The resolved audit result, inlined by `prepareReportResult()`. */
|
|
32
|
+
result: ApplicableAuditResult | NotApplicableAuditResult | ErrorAuditResult;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Base shape shared by all audit result variants.
|
|
40
|
+
*/
|
|
41
|
+
type AuditResult = {
|
|
42
|
+
/** Audit identifier. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** Short human-readable title describing what the audit checks. */
|
|
45
|
+
title: string;
|
|
46
|
+
/** Markdown description with remediation guidance. */
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* An audit that ran successfully and produced a numeric or binary score.
|
|
52
|
+
*/
|
|
53
|
+
type ApplicableAuditResult = AuditResult & {
|
|
54
|
+
/** Score in the 0-1 range. */
|
|
55
|
+
score: number;
|
|
56
|
+
/**
|
|
57
|
+
* How the score should be displayed.
|
|
58
|
+
* - `numeric` / `binary` produce pass/fail ratings.
|
|
59
|
+
* - `manual` requires human verification.
|
|
60
|
+
* - `informative` is shown for context only (no pass/fail).
|
|
61
|
+
*/
|
|
62
|
+
scoreDisplayMode: 'numeric' | 'binary' | 'manual' | 'informative';
|
|
63
|
+
/** Raw metric value in the audit's native unit (ms, bytes, etc.). */
|
|
64
|
+
numericValue: number;
|
|
65
|
+
/** Formatted display string (e.g. "1.2 s", "350 KiB"). */
|
|
66
|
+
displayValue: string;
|
|
67
|
+
/** Non-fatal warnings raised during the audit. */
|
|
68
|
+
warnings?: unknown[];
|
|
69
|
+
/** Detailed data table or opportunity breakdown. */
|
|
70
|
+
details?: OpportunityDetails | TableDetails;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* An audit that was skipped because it does not apply to the page
|
|
75
|
+
* (e.g. no `<video>` elements for a video-related audit).
|
|
76
|
+
*/
|
|
77
|
+
type NotApplicableAuditResult = AuditResult & {
|
|
78
|
+
score: null;
|
|
79
|
+
scoreDisplayMode: 'notApplicable';
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* An audit that failed with an internal error.
|
|
84
|
+
*/
|
|
85
|
+
type ErrorAuditResult = AuditResult & {
|
|
86
|
+
score: null;
|
|
87
|
+
scoreDisplayMode: 'error';
|
|
88
|
+
/** Human-readable error message explaining why the audit failed. */
|
|
89
|
+
errorMessage: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Discriminant base for Lighthouse detail types.
|
|
94
|
+
*/
|
|
95
|
+
type Details = {
|
|
96
|
+
type: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A detail type that contains a table of items with column headings.
|
|
101
|
+
* Used as a base for both `opportunity` and `table` detail types.
|
|
102
|
+
*/
|
|
103
|
+
type HeadnessDetails = Details & {
|
|
104
|
+
headings: {
|
|
105
|
+
/** Key into each item record. */
|
|
106
|
+
key: string;
|
|
107
|
+
/** Display type (e.g. `"url"`, `"bytes"`, `"ms"`). */
|
|
108
|
+
valueType: string;
|
|
109
|
+
/** Column header label. */
|
|
110
|
+
label: string;
|
|
111
|
+
}[];
|
|
112
|
+
/** Row data; each record maps heading keys to cell values. */
|
|
113
|
+
items: Record<string, string | number | Code | Node>[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* An opportunity detail highlighting potential savings.
|
|
118
|
+
* The `overallSavingsMs` value drives the opportunity's display in the report.
|
|
119
|
+
*/
|
|
120
|
+
type OpportunityDetails = HeadnessDetails & {
|
|
121
|
+
type: 'opportunity';
|
|
122
|
+
/** Total estimated time savings in milliseconds. */
|
|
123
|
+
overallSavingsMs: number;
|
|
124
|
+
/** Total estimated transfer-size savings in bytes, if applicable. */
|
|
125
|
+
overallSavingsBytes?: number;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A simple tabular detail without savings metadata.
|
|
130
|
+
*/
|
|
131
|
+
type TableDetails = HeadnessDetails & {
|
|
132
|
+
type: 'table';
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// type CriticalRequestChainDetails = Details & {
|
|
136
|
+
// type: 'criticalrequestchain';
|
|
137
|
+
// chains: Record<
|
|
138
|
+
// string,
|
|
139
|
+
// {
|
|
140
|
+
// request: CriticalRequestChainRequest;
|
|
141
|
+
// children: Record<
|
|
142
|
+
// string,
|
|
143
|
+
// {
|
|
144
|
+
// request: CriticalRequestChainRequest;
|
|
145
|
+
// }
|
|
146
|
+
// >;
|
|
147
|
+
// }
|
|
148
|
+
// >;
|
|
149
|
+
// longestChain: {
|
|
150
|
+
// duration: number;
|
|
151
|
+
// length: number;
|
|
152
|
+
// transferSize: number;
|
|
153
|
+
// };
|
|
154
|
+
// };
|
|
155
|
+
|
|
156
|
+
// type CriticalRequestChainRequest = {
|
|
157
|
+
// url: string;
|
|
158
|
+
// startTime: number;
|
|
159
|
+
// endTime: number;
|
|
160
|
+
// responseReceivedTime: number;
|
|
161
|
+
// transferSize: number;
|
|
162
|
+
// };
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* An inline code snippet cell value within a Lighthouse detail table.
|
|
166
|
+
*/
|
|
167
|
+
type Code = {
|
|
168
|
+
type: 'code';
|
|
169
|
+
/** The raw code string (e.g. a CSS selector or JS expression). */
|
|
170
|
+
value: string;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A DOM node reference within a Lighthouse detail table.
|
|
175
|
+
* Provides enough context to locate and identify the element in DevTools.
|
|
176
|
+
*/
|
|
177
|
+
type Node = {
|
|
178
|
+
type: 'node';
|
|
179
|
+
/** CSS selector that uniquely identifies the element. */
|
|
180
|
+
selector: string;
|
|
181
|
+
/** DevTools-style DOM tree path (e.g. `"1,HTML,1,BODY,3,DIV"`). */
|
|
182
|
+
path: string;
|
|
183
|
+
/** Truncated outer HTML of the element. */
|
|
184
|
+
snippet: string;
|
|
185
|
+
/** Why this node was flagged by the audit. */
|
|
186
|
+
explanation: string;
|
|
187
|
+
/** Accessible label or text content of the node. */
|
|
188
|
+
nodeLabel: string;
|
|
189
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"outDir": "./lib",
|
|
6
|
+
"rootDir": "./src"
|
|
7
|
+
},
|
|
8
|
+
"references": [{ "path": "../core" }],
|
|
9
|
+
"include": ["./src/**/*"],
|
|
10
|
+
"exclude": ["node_modules", "lib", "./src/**/*.spec.ts"]
|
|
11
|
+
}
|