laxy-verify 1.3.0 → 1.3.2
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 +486 -474
- package/dist/ai-analysis.d.ts +28 -0
- package/dist/ai-analysis.js +32 -0
- package/dist/audit/broken-links.d.ts +25 -25
- package/dist/audit/broken-links.js +97 -97
- package/dist/badge.d.ts +2 -2
- package/dist/badge.js +18 -18
- package/dist/cli.js +1242 -1250
- package/dist/compare-env.d.ts +23 -0
- package/dist/compare-env.js +55 -0
- package/dist/config.d.ts +102 -102
- package/dist/config.js +360 -360
- package/dist/entitlement.d.ts +15 -15
- package/dist/entitlement.js +98 -98
- package/dist/init-analysis.d.ts +6 -0
- package/dist/init-analysis.js +302 -0
- package/dist/init.js +132 -132
- package/dist/lighthouse.d.ts +37 -37
- package/dist/lighthouse.js +231 -231
- package/dist/report-markdown.d.ts +53 -53
- package/dist/report-markdown.js +407 -407
- package/dist/route-discovery.d.ts +7 -0
- package/dist/route-discovery.js +108 -0
- package/dist/security-audit.d.ts +17 -17
- package/dist/security-audit.js +127 -127
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +36 -0
- package/dist/verification-core/report.js +526 -526
- package/dist/verification-core/types.d.ts +164 -164
- package/dist/visual-diff.d.ts +33 -33
- package/dist/visual-diff.js +213 -213
- package/package.json +1 -1
package/dist/lighthouse.js
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.runLighthouse = runLighthouse;
|
|
37
|
-
exports.runLighthouseOnUrl = runLighthouseOnUrl;
|
|
38
|
-
exports.runLighthouseOnRoutes = runLighthouseOnRoutes;
|
|
39
|
-
const node_child_process_1 = require("node:child_process");
|
|
40
|
-
const fs = __importStar(require("node:fs"));
|
|
41
|
-
const path = __importStar(require("node:path"));
|
|
42
|
-
// laxy-verify 패키지의 node_modules 절대 경로
|
|
43
|
-
// runner script를 CJS로 실행하고 NODE_PATH에 이 경로를 추가해서
|
|
44
|
-
// 어느 프로젝트 디렉토리에서 실행해도 lighthouse/chrome-launcher를 찾도록 함
|
|
45
|
-
// __dirname = dist/ 이므로 한 단계 올라가서 node_modules를 가리킴
|
|
46
|
-
const LH_NODE_MODULES_DIR = path.resolve(__dirname, "..", "node_modules");
|
|
47
|
-
function median(values) {
|
|
48
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
49
|
-
const mid = Math.floor(sorted.length / 2);
|
|
50
|
-
return sorted.length % 2 !== 0
|
|
51
|
-
? sorted[mid]
|
|
52
|
-
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
53
|
-
}
|
|
54
|
-
function sleep(ms) {
|
|
55
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
-
}
|
|
57
|
-
async function removeDirWithRetries(dirPath, retries = 5) {
|
|
58
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
59
|
-
try {
|
|
60
|
-
if (fs.existsSync(dirPath)) {
|
|
61
|
-
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
62
|
-
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
if (attempt === retries - 1)
|
|
67
|
-
return;
|
|
68
|
-
await sleep(250);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// CJS 방식으로 runner script를 작성
|
|
73
|
-
// NODE_PATH를 통해 laxy-verify node_modules를 참조
|
|
74
|
-
function writeRunnerScript(runnerPath) {
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runLighthouse = runLighthouse;
|
|
37
|
+
exports.runLighthouseOnUrl = runLighthouseOnUrl;
|
|
38
|
+
exports.runLighthouseOnRoutes = runLighthouseOnRoutes;
|
|
39
|
+
const node_child_process_1 = require("node:child_process");
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
// laxy-verify 패키지의 node_modules 절대 경로
|
|
43
|
+
// runner script를 CJS로 실행하고 NODE_PATH에 이 경로를 추가해서
|
|
44
|
+
// 어느 프로젝트 디렉토리에서 실행해도 lighthouse/chrome-launcher를 찾도록 함
|
|
45
|
+
// __dirname = dist/ 이므로 한 단계 올라가서 node_modules를 가리킴
|
|
46
|
+
const LH_NODE_MODULES_DIR = path.resolve(__dirname, "..", "node_modules");
|
|
47
|
+
function median(values) {
|
|
48
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
49
|
+
const mid = Math.floor(sorted.length / 2);
|
|
50
|
+
return sorted.length % 2 !== 0
|
|
51
|
+
? sorted[mid]
|
|
52
|
+
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
53
|
+
}
|
|
54
|
+
function sleep(ms) {
|
|
55
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
}
|
|
57
|
+
async function removeDirWithRetries(dirPath, retries = 5) {
|
|
58
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(dirPath)) {
|
|
61
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
if (attempt === retries - 1)
|
|
67
|
+
return;
|
|
68
|
+
await sleep(250);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// CJS 방식으로 runner script를 작성
|
|
73
|
+
// NODE_PATH를 통해 laxy-verify node_modules를 참조
|
|
74
|
+
function writeRunnerScript(runnerPath) {
|
|
75
75
|
const source = `"use strict";
|
|
76
76
|
const fs = require("node:fs/promises");
|
|
77
77
|
const _lhModule = require("lighthouse");
|
|
@@ -112,160 +112,160 @@ const [url, reportPath, chromeDir] = process.argv.slice(2);
|
|
|
112
112
|
process.stderr.write(err.message + "\\n");
|
|
113
113
|
process.exit(1);
|
|
114
114
|
});
|
|
115
|
-
`;
|
|
116
|
-
// .cjs 확장자로 저장해서 Node가 CommonJS로 실행하도록 함
|
|
117
|
-
fs.writeFileSync(runnerPath, source, "utf-8");
|
|
118
|
-
}
|
|
119
|
-
async function runLighthouseCore(fullUrl, runs, label) {
|
|
120
|
-
const baseTmpDir = path.join(process.cwd(), ".laxy-tmp", `lighthouse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
121
|
-
const reportsDir = path.join(baseTmpDir, "reports");
|
|
122
|
-
const runnerPath = path.join(baseTmpDir, "run-lighthouse.cjs");
|
|
123
|
-
fs.mkdirSync(reportsDir, { recursive: true });
|
|
124
|
-
writeRunnerScript(runnerPath);
|
|
125
|
-
console.log(`\n Running Lighthouse on ${label} (${runs} run${runs > 1 ? "s" : ""})...`);
|
|
126
|
-
const errors = [];
|
|
127
|
-
const allScores = [];
|
|
128
|
-
for (let runIndex = 0; runIndex < runs; runIndex++) {
|
|
129
|
-
const runNumber = runIndex + 1;
|
|
130
|
-
const reportPath = path.join(reportsDir, `lhr-${runNumber}.json`);
|
|
131
|
-
const chromeDir = path.join(baseTmpDir, `chrome-${runNumber}`);
|
|
132
|
-
fs.mkdirSync(chromeDir, { recursive: true });
|
|
133
|
-
console.log(` [lh] Run ${runNumber}/${runs}`);
|
|
134
|
-
const child = (0, node_child_process_1.spawn)("node", [runnerPath, fullUrl, reportPath, chromeDir], {
|
|
135
|
-
shell: false,
|
|
136
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
137
|
-
env: {
|
|
138
|
-
...process.env,
|
|
139
|
-
// NODE_PATH를 추가해서 laxy-verify node_modules에서 패키지를 찾도록 함
|
|
140
|
-
NODE_PATH: [LH_NODE_MODULES_DIR, process.env.NODE_PATH].filter(Boolean).join(path.delimiter),
|
|
141
|
-
TEMP: baseTmpDir,
|
|
142
|
-
TMP: baseTmpDir,
|
|
143
|
-
TMPDIR: baseTmpDir,
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
child.stdout?.on("data", (chunk) => {
|
|
147
|
-
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
148
|
-
for (const line of lines)
|
|
149
|
-
console.log(` [lh] ${line}`);
|
|
150
|
-
});
|
|
151
|
-
child.stderr?.on("data", (chunk) => {
|
|
152
|
-
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
153
|
-
errors.push(...lines);
|
|
154
|
-
for (const line of lines)
|
|
155
|
-
console.error(` [lh] ${line}`);
|
|
156
|
-
});
|
|
157
|
-
const exitCode = await new Promise((resolve) => {
|
|
158
|
-
child.on("exit", (code) => resolve(code ?? 1));
|
|
159
|
-
});
|
|
160
|
-
try {
|
|
161
|
-
if (!fs.existsSync(reportPath)) {
|
|
162
|
-
errors.push(`Run ${runNumber}: Lighthouse exited with code ${exitCode} and produced no JSON report.`);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
166
|
-
allScores.push({
|
|
167
|
-
performance: Math.round((report.categories.performance?.score ?? 0) * 100),
|
|
168
|
-
accessibility: Math.round((report.categories.accessibility?.score ?? 0) * 100),
|
|
169
|
-
seo: Math.round((report.categories.seo?.score ?? 0) * 100),
|
|
170
|
-
bestPractices: Math.round((report.categories["best-practices"]?.score ?? 0) * 100),
|
|
171
|
-
});
|
|
172
|
-
if (exitCode !== 0) {
|
|
173
|
-
errors.push(`Run ${runNumber}: Lighthouse exited with code ${exitCode}, but the JSON report was recovered.`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
errors.push(`Run ${runNumber}: Failed to read Lighthouse report: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
await removeDirWithRetries(chromeDir);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
if (allScores.length === 0) {
|
|
185
|
-
console.error(" Lighthouse exited without usable reports.");
|
|
186
|
-
return { scores: null, errors };
|
|
187
|
-
}
|
|
188
|
-
const scores = {
|
|
189
|
-
performance: Math.round(median(allScores.map((score) => score.performance))),
|
|
190
|
-
accessibility: Math.round(median(allScores.map((score) => score.accessibility))),
|
|
191
|
-
seo: Math.round(median(allScores.map((score) => score.seo))),
|
|
192
|
-
bestPractices: Math.round(median(allScores.map((score) => score.bestPractices))),
|
|
193
|
-
};
|
|
194
|
-
console.log(` Lighthouse median: P=${scores.performance} A=${scores.accessibility} S=${scores.seo} BP=${scores.bestPractices}`);
|
|
195
|
-
return { scores, errors };
|
|
196
|
-
}
|
|
197
|
-
finally {
|
|
198
|
-
await removeDirWithRetries(baseTmpDir);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
async function runLighthouse(port, runs, routePath = "/") {
|
|
202
|
-
const normalizedPath = routePath.startsWith("/") ? routePath : `/${routePath}`;
|
|
203
|
-
const label = normalizedPath === "/" ? "/" : normalizedPath;
|
|
204
|
-
return runLighthouseCore(`http://127.0.0.1:${port}${normalizedPath}`, runs, label);
|
|
205
|
-
}
|
|
206
|
-
/** Run Lighthouse against an arbitrary external URL (Pro: compare-env feature). */
|
|
207
|
-
async function runLighthouseOnUrl(fullUrl, runs) {
|
|
208
|
-
return runLighthouseCore(fullUrl, runs, fullUrl);
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Run Lighthouse on multiple routes and aggregate results.
|
|
212
|
-
*
|
|
213
|
-
* Aggregation strategy (weighted average):
|
|
214
|
-
* - Root route "/" receives weight 2 (most visible page)
|
|
215
|
-
* - All other routes receive weight 1
|
|
216
|
-
*
|
|
217
|
-
* Gold eligibility: every route must individually pass all thresholds.
|
|
218
|
-
* Silver/Bronze eligibility: based on the weighted-average aggregate score.
|
|
219
|
-
*/
|
|
220
|
-
async function runLighthouseOnRoutes(port, runs, routes) {
|
|
221
|
-
// Deduplicate and ensure "/" comes first
|
|
222
|
-
const seen = new Set();
|
|
223
|
-
const ordered = [];
|
|
224
|
-
for (const r of ["/", ...routes]) {
|
|
225
|
-
const normalized = r.startsWith("/") ? r : `/${r}`;
|
|
226
|
-
if (!seen.has(normalized)) {
|
|
227
|
-
seen.add(normalized);
|
|
228
|
-
ordered.push(normalized);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const perRoute = [];
|
|
232
|
-
for (const route of ordered) {
|
|
233
|
-
const result = await runLighthouse(port, runs, route);
|
|
234
|
-
perRoute.push({ route, scores: result.scores, errors: result.errors });
|
|
235
|
-
}
|
|
236
|
-
// Weighted average across routes that produced scores
|
|
237
|
-
const withScores = perRoute.filter((r) => r.scores !== null);
|
|
238
|
-
let aggregated = null;
|
|
239
|
-
if (withScores.length > 0) {
|
|
240
|
-
let totalWeight = 0;
|
|
241
|
-
let sumPerf = 0;
|
|
242
|
-
let sumA11y = 0;
|
|
243
|
-
let sumSeo = 0;
|
|
244
|
-
let sumBp = 0;
|
|
245
|
-
for (const r of withScores) {
|
|
246
|
-
const weight = r.route === "/" ? 2 : 1;
|
|
247
|
-
totalWeight += weight;
|
|
248
|
-
sumPerf += r.scores.performance * weight;
|
|
249
|
-
sumA11y += r.scores.accessibility * weight;
|
|
250
|
-
sumSeo += r.scores.seo * weight;
|
|
251
|
-
sumBp += r.scores.bestPractices * weight;
|
|
252
|
-
}
|
|
253
|
-
aggregated = {
|
|
254
|
-
performance: Math.round(sumPerf / totalWeight),
|
|
255
|
-
accessibility: Math.round(sumA11y / totalWeight),
|
|
256
|
-
seo: Math.round(sumSeo / totalWeight),
|
|
257
|
-
bestPractices: Math.round(sumBp / totalWeight),
|
|
258
|
-
};
|
|
259
|
-
console.log(` Lighthouse aggregate (weighted avg): P=${aggregated.performance} A=${aggregated.accessibility} S=${aggregated.seo} BP=${aggregated.bestPractices}`);
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
aggregated,
|
|
263
|
-
perRoute,
|
|
264
|
-
allRoutesPass(thresholds) {
|
|
265
|
-
return withScores.every((r) => r.scores.performance >= thresholds.performance &&
|
|
266
|
-
r.scores.accessibility >= thresholds.accessibility &&
|
|
267
|
-
r.scores.seo >= thresholds.seo &&
|
|
268
|
-
r.scores.bestPractices >= thresholds.bestPractices);
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
}
|
|
115
|
+
`;
|
|
116
|
+
// .cjs 확장자로 저장해서 Node가 CommonJS로 실행하도록 함
|
|
117
|
+
fs.writeFileSync(runnerPath, source, "utf-8");
|
|
118
|
+
}
|
|
119
|
+
async function runLighthouseCore(fullUrl, runs, label) {
|
|
120
|
+
const baseTmpDir = path.join(process.cwd(), ".laxy-tmp", `lighthouse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
121
|
+
const reportsDir = path.join(baseTmpDir, "reports");
|
|
122
|
+
const runnerPath = path.join(baseTmpDir, "run-lighthouse.cjs");
|
|
123
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
124
|
+
writeRunnerScript(runnerPath);
|
|
125
|
+
console.log(`\n Running Lighthouse on ${label} (${runs} run${runs > 1 ? "s" : ""})...`);
|
|
126
|
+
const errors = [];
|
|
127
|
+
const allScores = [];
|
|
128
|
+
for (let runIndex = 0; runIndex < runs; runIndex++) {
|
|
129
|
+
const runNumber = runIndex + 1;
|
|
130
|
+
const reportPath = path.join(reportsDir, `lhr-${runNumber}.json`);
|
|
131
|
+
const chromeDir = path.join(baseTmpDir, `chrome-${runNumber}`);
|
|
132
|
+
fs.mkdirSync(chromeDir, { recursive: true });
|
|
133
|
+
console.log(` [lh] Run ${runNumber}/${runs}`);
|
|
134
|
+
const child = (0, node_child_process_1.spawn)("node", [runnerPath, fullUrl, reportPath, chromeDir], {
|
|
135
|
+
shell: false,
|
|
136
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
137
|
+
env: {
|
|
138
|
+
...process.env,
|
|
139
|
+
// NODE_PATH를 추가해서 laxy-verify node_modules에서 패키지를 찾도록 함
|
|
140
|
+
NODE_PATH: [LH_NODE_MODULES_DIR, process.env.NODE_PATH].filter(Boolean).join(path.delimiter),
|
|
141
|
+
TEMP: baseTmpDir,
|
|
142
|
+
TMP: baseTmpDir,
|
|
143
|
+
TMPDIR: baseTmpDir,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
child.stdout?.on("data", (chunk) => {
|
|
147
|
+
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
148
|
+
for (const line of lines)
|
|
149
|
+
console.log(` [lh] ${line}`);
|
|
150
|
+
});
|
|
151
|
+
child.stderr?.on("data", (chunk) => {
|
|
152
|
+
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
153
|
+
errors.push(...lines);
|
|
154
|
+
for (const line of lines)
|
|
155
|
+
console.error(` [lh] ${line}`);
|
|
156
|
+
});
|
|
157
|
+
const exitCode = await new Promise((resolve) => {
|
|
158
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
if (!fs.existsSync(reportPath)) {
|
|
162
|
+
errors.push(`Run ${runNumber}: Lighthouse exited with code ${exitCode} and produced no JSON report.`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
166
|
+
allScores.push({
|
|
167
|
+
performance: Math.round((report.categories.performance?.score ?? 0) * 100),
|
|
168
|
+
accessibility: Math.round((report.categories.accessibility?.score ?? 0) * 100),
|
|
169
|
+
seo: Math.round((report.categories.seo?.score ?? 0) * 100),
|
|
170
|
+
bestPractices: Math.round((report.categories["best-practices"]?.score ?? 0) * 100),
|
|
171
|
+
});
|
|
172
|
+
if (exitCode !== 0) {
|
|
173
|
+
errors.push(`Run ${runNumber}: Lighthouse exited with code ${exitCode}, but the JSON report was recovered.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
errors.push(`Run ${runNumber}: Failed to read Lighthouse report: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await removeDirWithRetries(chromeDir);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
if (allScores.length === 0) {
|
|
185
|
+
console.error(" Lighthouse exited without usable reports.");
|
|
186
|
+
return { scores: null, errors };
|
|
187
|
+
}
|
|
188
|
+
const scores = {
|
|
189
|
+
performance: Math.round(median(allScores.map((score) => score.performance))),
|
|
190
|
+
accessibility: Math.round(median(allScores.map((score) => score.accessibility))),
|
|
191
|
+
seo: Math.round(median(allScores.map((score) => score.seo))),
|
|
192
|
+
bestPractices: Math.round(median(allScores.map((score) => score.bestPractices))),
|
|
193
|
+
};
|
|
194
|
+
console.log(` Lighthouse median: P=${scores.performance} A=${scores.accessibility} S=${scores.seo} BP=${scores.bestPractices}`);
|
|
195
|
+
return { scores, errors };
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await removeDirWithRetries(baseTmpDir);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function runLighthouse(port, runs, routePath = "/") {
|
|
202
|
+
const normalizedPath = routePath.startsWith("/") ? routePath : `/${routePath}`;
|
|
203
|
+
const label = normalizedPath === "/" ? "/" : normalizedPath;
|
|
204
|
+
return runLighthouseCore(`http://127.0.0.1:${port}${normalizedPath}`, runs, label);
|
|
205
|
+
}
|
|
206
|
+
/** Run Lighthouse against an arbitrary external URL (Pro: compare-env feature). */
|
|
207
|
+
async function runLighthouseOnUrl(fullUrl, runs) {
|
|
208
|
+
return runLighthouseCore(fullUrl, runs, fullUrl);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Run Lighthouse on multiple routes and aggregate results.
|
|
212
|
+
*
|
|
213
|
+
* Aggregation strategy (weighted average):
|
|
214
|
+
* - Root route "/" receives weight 2 (most visible page)
|
|
215
|
+
* - All other routes receive weight 1
|
|
216
|
+
*
|
|
217
|
+
* Gold eligibility: every route must individually pass all thresholds.
|
|
218
|
+
* Silver/Bronze eligibility: based on the weighted-average aggregate score.
|
|
219
|
+
*/
|
|
220
|
+
async function runLighthouseOnRoutes(port, runs, routes) {
|
|
221
|
+
// Deduplicate and ensure "/" comes first
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
const ordered = [];
|
|
224
|
+
for (const r of ["/", ...routes]) {
|
|
225
|
+
const normalized = r.startsWith("/") ? r : `/${r}`;
|
|
226
|
+
if (!seen.has(normalized)) {
|
|
227
|
+
seen.add(normalized);
|
|
228
|
+
ordered.push(normalized);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const perRoute = [];
|
|
232
|
+
for (const route of ordered) {
|
|
233
|
+
const result = await runLighthouse(port, runs, route);
|
|
234
|
+
perRoute.push({ route, scores: result.scores, errors: result.errors });
|
|
235
|
+
}
|
|
236
|
+
// Weighted average across routes that produced scores
|
|
237
|
+
const withScores = perRoute.filter((r) => r.scores !== null);
|
|
238
|
+
let aggregated = null;
|
|
239
|
+
if (withScores.length > 0) {
|
|
240
|
+
let totalWeight = 0;
|
|
241
|
+
let sumPerf = 0;
|
|
242
|
+
let sumA11y = 0;
|
|
243
|
+
let sumSeo = 0;
|
|
244
|
+
let sumBp = 0;
|
|
245
|
+
for (const r of withScores) {
|
|
246
|
+
const weight = r.route === "/" ? 2 : 1;
|
|
247
|
+
totalWeight += weight;
|
|
248
|
+
sumPerf += r.scores.performance * weight;
|
|
249
|
+
sumA11y += r.scores.accessibility * weight;
|
|
250
|
+
sumSeo += r.scores.seo * weight;
|
|
251
|
+
sumBp += r.scores.bestPractices * weight;
|
|
252
|
+
}
|
|
253
|
+
aggregated = {
|
|
254
|
+
performance: Math.round(sumPerf / totalWeight),
|
|
255
|
+
accessibility: Math.round(sumA11y / totalWeight),
|
|
256
|
+
seo: Math.round(sumSeo / totalWeight),
|
|
257
|
+
bestPractices: Math.round(sumBp / totalWeight),
|
|
258
|
+
};
|
|
259
|
+
console.log(` Lighthouse aggregate (weighted avg): P=${aggregated.performance} A=${aggregated.accessibility} S=${aggregated.seo} BP=${aggregated.bestPractices}`);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
aggregated,
|
|
263
|
+
perRoute,
|
|
264
|
+
allRoutesPass(thresholds) {
|
|
265
|
+
return withScores.every((r) => r.scores.performance >= thresholds.performance &&
|
|
266
|
+
r.scores.accessibility >= thresholds.accessibility &&
|
|
267
|
+
r.scores.seo >= thresholds.seo &&
|
|
268
|
+
r.scores.bestPractices >= thresholds.bestPractices);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import type { E2EScenarioResult } from "./e2e.js";
|
|
2
|
-
import type { LighthouseScores } from "./grade.js";
|
|
3
|
-
import type { TierVerificationView, VerificationReport } from "./verification-core/index.js";
|
|
4
|
-
import { type VisualDiffResult } from "./visual-diff.js";
|
|
5
|
-
import type { TypecheckResult } from "./typecheck.js";
|
|
6
|
-
import type { SecretScanResult } from "./secret-scan.js";
|
|
7
|
-
import type { BundleSizeResult } from "./bundle-size.js";
|
|
8
|
-
import type { OutdatedCheckResult } from "./outdated-check.js";
|
|
9
|
-
import type { A11yDeepResult } from "./a11y-deep.js";
|
|
10
|
-
import type { SeoDeepResult } from "./seo-deep.js";
|
|
11
|
-
import type { VitalsBudgetResult } from "./vitals-budget.js";
|
|
12
|
-
export interface MarkdownReportResult {
|
|
13
|
-
grade: string;
|
|
14
|
-
timestamp: string;
|
|
15
|
-
build: {
|
|
16
|
-
success: boolean;
|
|
17
|
-
durationMs: number;
|
|
18
|
-
errors: string[];
|
|
19
|
-
};
|
|
20
|
-
e2e?: {
|
|
21
|
-
passed: number;
|
|
22
|
-
failed: number;
|
|
23
|
-
total: number;
|
|
24
|
-
results: E2EScenarioResult[];
|
|
25
|
-
};
|
|
26
|
-
lighthouse: (LighthouseScores & {
|
|
27
|
-
runs: number;
|
|
28
|
-
}) | null;
|
|
29
|
-
visualDiff?: VisualDiffResult | null;
|
|
30
|
-
typecheck?: TypecheckResult | null;
|
|
31
|
-
secretScan?: SecretScanResult | null;
|
|
32
|
-
bundleSize?: BundleSizeResult | null;
|
|
33
|
-
outdatedCheck?: OutdatedCheckResult | null;
|
|
34
|
-
a11yDeep?: A11yDeepResult | null;
|
|
35
|
-
seoDeep?: SeoDeepResult | null;
|
|
36
|
-
vitalsBudget?: VitalsBudgetResult | null;
|
|
37
|
-
thresholds: {
|
|
38
|
-
performance: number;
|
|
39
|
-
accessibility: number;
|
|
40
|
-
seo: number;
|
|
41
|
-
bestPractices: number;
|
|
42
|
-
};
|
|
43
|
-
framework: string | null;
|
|
44
|
-
_plan?: string;
|
|
45
|
-
verification?: {
|
|
46
|
-
tier: VerificationReport["tier"];
|
|
47
|
-
report: VerificationReport;
|
|
48
|
-
view: TierVerificationView;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
export declare function shouldWriteMarkdownReport(result: MarkdownReportResult): boolean;
|
|
52
|
-
export declare function getMarkdownReportPath(projectDir: string): string;
|
|
53
|
-
export declare function buildMarkdownReport(projectDir: string, result: MarkdownReportResult): string;
|
|
1
|
+
import type { E2EScenarioResult } from "./e2e.js";
|
|
2
|
+
import type { LighthouseScores } from "./grade.js";
|
|
3
|
+
import type { TierVerificationView, VerificationReport } from "./verification-core/index.js";
|
|
4
|
+
import { type VisualDiffResult } from "./visual-diff.js";
|
|
5
|
+
import type { TypecheckResult } from "./typecheck.js";
|
|
6
|
+
import type { SecretScanResult } from "./secret-scan.js";
|
|
7
|
+
import type { BundleSizeResult } from "./bundle-size.js";
|
|
8
|
+
import type { OutdatedCheckResult } from "./outdated-check.js";
|
|
9
|
+
import type { A11yDeepResult } from "./a11y-deep.js";
|
|
10
|
+
import type { SeoDeepResult } from "./seo-deep.js";
|
|
11
|
+
import type { VitalsBudgetResult } from "./vitals-budget.js";
|
|
12
|
+
export interface MarkdownReportResult {
|
|
13
|
+
grade: string;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
build: {
|
|
16
|
+
success: boolean;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
errors: string[];
|
|
19
|
+
};
|
|
20
|
+
e2e?: {
|
|
21
|
+
passed: number;
|
|
22
|
+
failed: number;
|
|
23
|
+
total: number;
|
|
24
|
+
results: E2EScenarioResult[];
|
|
25
|
+
};
|
|
26
|
+
lighthouse: (LighthouseScores & {
|
|
27
|
+
runs: number;
|
|
28
|
+
}) | null;
|
|
29
|
+
visualDiff?: VisualDiffResult | null;
|
|
30
|
+
typecheck?: TypecheckResult | null;
|
|
31
|
+
secretScan?: SecretScanResult | null;
|
|
32
|
+
bundleSize?: BundleSizeResult | null;
|
|
33
|
+
outdatedCheck?: OutdatedCheckResult | null;
|
|
34
|
+
a11yDeep?: A11yDeepResult | null;
|
|
35
|
+
seoDeep?: SeoDeepResult | null;
|
|
36
|
+
vitalsBudget?: VitalsBudgetResult | null;
|
|
37
|
+
thresholds: {
|
|
38
|
+
performance: number;
|
|
39
|
+
accessibility: number;
|
|
40
|
+
seo: number;
|
|
41
|
+
bestPractices: number;
|
|
42
|
+
};
|
|
43
|
+
framework: string | null;
|
|
44
|
+
_plan?: string;
|
|
45
|
+
verification?: {
|
|
46
|
+
tier: VerificationReport["tier"];
|
|
47
|
+
report: VerificationReport;
|
|
48
|
+
view: TierVerificationView;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export declare function shouldWriteMarkdownReport(result: MarkdownReportResult): boolean;
|
|
52
|
+
export declare function getMarkdownReportPath(projectDir: string): string;
|
|
53
|
+
export declare function buildMarkdownReport(projectDir: string, result: MarkdownReportResult): string;
|