laxy-verify 1.2.2 → 1.3.0
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 +204 -47
- package/dist/a11y-deep.d.ts +20 -0
- package/dist/a11y-deep.js +161 -0
- package/dist/audit/broken-links.d.ts +25 -21
- package/dist/audit/broken-links.js +97 -86
- package/dist/badge.d.ts +2 -1
- package/dist/badge.js +18 -14
- package/dist/bundle-size.d.ts +14 -0
- package/dist/bundle-size.js +209 -0
- package/dist/cli.js +1256 -865
- package/dist/config.d.ts +102 -65
- package/dist/config.js +360 -255
- package/dist/entitlement.d.ts +15 -11
- package/dist/entitlement.js +98 -90
- package/dist/init.js +153 -87
- package/dist/lighthouse.d.ts +37 -7
- package/dist/lighthouse.js +231 -158
- package/dist/outdated-check.d.ts +17 -0
- package/dist/outdated-check.js +123 -0
- package/dist/report-markdown.d.ts +53 -39
- package/dist/report-markdown.js +407 -386
- package/dist/secret-scan.d.ts +15 -0
- package/dist/secret-scan.js +218 -0
- package/dist/security-audit.d.ts +17 -9
- package/dist/security-audit.js +127 -64
- package/dist/seo-deep.d.ts +24 -0
- package/dist/seo-deep.js +147 -0
- package/dist/typecheck.d.ts +8 -0
- package/dist/typecheck.js +99 -0
- package/dist/verification-core/report.js +526 -409
- package/dist/verification-core/types.d.ts +164 -108
- package/dist/visual-diff.d.ts +33 -26
- package/dist/visual-diff.js +223 -178
- package/dist/vitals-budget.d.ts +23 -0
- package/dist/vitals-budget.js +168 -0
- package/package.json +1 -1
package/dist/lighthouse.js
CHANGED
|
@@ -1,75 +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
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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) {
|
|
73
75
|
const source = `"use strict";
|
|
74
76
|
const fs = require("node:fs/promises");
|
|
75
77
|
const _lhModule = require("lighthouse");
|
|
@@ -110,89 +112,160 @@ const [url, reportPath, chromeDir] = process.argv.slice(2);
|
|
|
110
112
|
process.stderr.write(err.message + "\\n");
|
|
111
113
|
process.exit(1);
|
|
112
114
|
});
|
|
113
|
-
`;
|
|
114
|
-
// .cjs 확장자로 저장해서 Node가 CommonJS로 실행하도록 함
|
|
115
|
-
fs.writeFileSync(runnerPath, source, "utf-8");
|
|
116
|
-
}
|
|
117
|
-
async function
|
|
118
|
-
const baseTmpDir = path.join(process.cwd(), ".laxy-tmp", `lighthouse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
119
|
-
const reportsDir = path.join(baseTmpDir, "reports");
|
|
120
|
-
const runnerPath = path.join(baseTmpDir, "run-lighthouse.cjs");
|
|
121
|
-
fs.mkdirSync(reportsDir, { recursive: true });
|
|
122
|
-
writeRunnerScript(runnerPath);
|
|
123
|
-
console.log(`\n Running Lighthouse (${runs} run${runs > 1 ? "s" : ""})...`);
|
|
124
|
-
const errors = [];
|
|
125
|
-
const allScores = [];
|
|
126
|
-
for (let runIndex = 0; runIndex < runs; runIndex++) {
|
|
127
|
-
const runNumber = runIndex + 1;
|
|
128
|
-
const reportPath = path.join(reportsDir, `lhr-${runNumber}.json`);
|
|
129
|
-
const chromeDir = path.join(baseTmpDir, `chrome-${runNumber}`);
|
|
130
|
-
fs.mkdirSync(chromeDir, { recursive: true });
|
|
131
|
-
console.log(` [lh] Run ${runNumber}/${runs}`);
|
|
132
|
-
const child = (0, node_child_process_1.spawn)("node", [runnerPath,
|
|
133
|
-
shell: false,
|
|
134
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
135
|
-
env: {
|
|
136
|
-
...process.env,
|
|
137
|
-
// NODE_PATH를 추가해서 laxy-verify node_modules에서 패키지를 찾도록 함
|
|
138
|
-
NODE_PATH: [LH_NODE_MODULES_DIR, process.env.NODE_PATH].filter(Boolean).join(path.delimiter),
|
|
139
|
-
TEMP: baseTmpDir,
|
|
140
|
-
TMP: baseTmpDir,
|
|
141
|
-
TMPDIR: baseTmpDir,
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
child.stdout?.on("data", (chunk) => {
|
|
145
|
-
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
146
|
-
for (const line of lines)
|
|
147
|
-
console.log(` [lh] ${line}`);
|
|
148
|
-
});
|
|
149
|
-
child.stderr?.on("data", (chunk) => {
|
|
150
|
-
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
151
|
-
errors.push(...lines);
|
|
152
|
-
for (const line of lines)
|
|
153
|
-
console.error(` [lh] ${line}`);
|
|
154
|
-
});
|
|
155
|
-
const exitCode = await new Promise((resolve) => {
|
|
156
|
-
child.on("exit", (code) => resolve(code ?? 1));
|
|
157
|
-
});
|
|
158
|
-
try {
|
|
159
|
-
if (!fs.existsSync(reportPath)) {
|
|
160
|
-
errors.push(`Run ${runNumber}: Lighthouse exited with code ${exitCode} and produced no JSON report.`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
164
|
-
allScores.push({
|
|
165
|
-
performance: Math.round((report.categories.performance?.score ?? 0) * 100),
|
|
166
|
-
accessibility: Math.round((report.categories.accessibility?.score ?? 0) * 100),
|
|
167
|
-
seo: Math.round((report.categories.seo?.score ?? 0) * 100),
|
|
168
|
-
bestPractices: Math.round((report.categories["best-practices"]?.score ?? 0) * 100),
|
|
169
|
-
});
|
|
170
|
-
if (exitCode !== 0) {
|
|
171
|
-
errors.push(`Run ${runNumber}: Lighthouse exited with code ${exitCode}, but the JSON report was recovered.`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
errors.push(`Run ${runNumber}: Failed to read Lighthouse report: ${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
-
}
|
|
177
|
-
finally {
|
|
178
|
-
await removeDirWithRetries(chromeDir);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
try {
|
|
182
|
-
if (allScores.length === 0) {
|
|
183
|
-
console.error(" Lighthouse exited without usable reports.");
|
|
184
|
-
return { scores: null, errors };
|
|
185
|
-
}
|
|
186
|
-
const scores = {
|
|
187
|
-
performance: Math.round(median(allScores.map((score) => score.performance))),
|
|
188
|
-
accessibility: Math.round(median(allScores.map((score) => score.accessibility))),
|
|
189
|
-
seo: Math.round(median(allScores.map((score) => score.seo))),
|
|
190
|
-
bestPractices: Math.round(median(allScores.map((score) => score.bestPractices))),
|
|
191
|
-
};
|
|
192
|
-
console.log(` Lighthouse median: P=${scores.performance} A=${scores.accessibility} S=${scores.seo} BP=${scores.bestPractices}`);
|
|
193
|
-
return { scores, errors };
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
await removeDirWithRetries(baseTmpDir);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface OutdatedPackage {
|
|
2
|
+
name: string;
|
|
3
|
+
current: string;
|
|
4
|
+
wanted: string;
|
|
5
|
+
latest: string;
|
|
6
|
+
severity: "major" | "minor" | "patch";
|
|
7
|
+
}
|
|
8
|
+
export interface OutdatedCheckResult {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
totalChecked: number;
|
|
11
|
+
outdatedCount: number;
|
|
12
|
+
majorOutdated: number;
|
|
13
|
+
packages: OutdatedPackage[];
|
|
14
|
+
advisory: string;
|
|
15
|
+
skipped: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function runOutdatedCheck(projectDir: string, timeoutMs?: number): Promise<OutdatedCheckResult>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runOutdatedCheck = runOutdatedCheck;
|
|
4
|
+
/**
|
|
5
|
+
* Outdated dependency checker.
|
|
6
|
+
*
|
|
7
|
+
* Runs `npm outdated --json` and reports packages that are behind
|
|
8
|
+
* their latest version. Advisory-only — does not block deployment.
|
|
9
|
+
*/
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function classifySemverDiff(current, latest) {
|
|
12
|
+
const parseVer = (v) => {
|
|
13
|
+
const cleaned = v.replace(/^\^|~|>=?|v/g, "").split(".")[0] ?? "0";
|
|
14
|
+
return parseInt(cleaned, 10) || 0;
|
|
15
|
+
};
|
|
16
|
+
const parseMinor = (v) => {
|
|
17
|
+
const parts = v.replace(/^\^|~|>=?|v/g, "").split(".");
|
|
18
|
+
return parseInt(parts[1] ?? "0", 10) || 0;
|
|
19
|
+
};
|
|
20
|
+
const curMajor = parseVer(current);
|
|
21
|
+
const latMajor = parseVer(latest);
|
|
22
|
+
if (latMajor > curMajor)
|
|
23
|
+
return "major";
|
|
24
|
+
const curMinor = parseMinor(current);
|
|
25
|
+
const latMinor = parseMinor(latest);
|
|
26
|
+
if (latMinor > curMinor)
|
|
27
|
+
return "minor";
|
|
28
|
+
return "patch";
|
|
29
|
+
}
|
|
30
|
+
async function runOutdatedCheck(projectDir, timeoutMs = 30000) {
|
|
31
|
+
console.log(" Running outdated dependency check...");
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
const proc = process.platform === "win32"
|
|
35
|
+
? (0, node_child_process_1.spawn)(process.env.ComSpec || "cmd.exe", ["/d", "/c", "npm outdated --json"], { stdio: ["ignore", "pipe", "pipe"], cwd: projectDir, shell: false })
|
|
36
|
+
: (0, node_child_process_1.spawn)("npm", ["outdated", "--json"], {
|
|
37
|
+
shell: true,
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
cwd: projectDir,
|
|
40
|
+
});
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
try {
|
|
43
|
+
proc.kill();
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
resolve({
|
|
47
|
+
passed: true,
|
|
48
|
+
totalChecked: 0,
|
|
49
|
+
outdatedCount: 0,
|
|
50
|
+
majorOutdated: 0,
|
|
51
|
+
packages: [],
|
|
52
|
+
advisory: "Outdated check timed out",
|
|
53
|
+
skipped: false,
|
|
54
|
+
});
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
proc.stdout?.on("data", (chunk) => chunks.push(chunk.toString()));
|
|
57
|
+
proc.stderr?.on("data", () => { });
|
|
58
|
+
proc.on("exit", (code) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
const output = chunks.join("");
|
|
61
|
+
// npm outdated exits with code 1 when outdated packages exist
|
|
62
|
+
// and code 0 when everything is up to date
|
|
63
|
+
if (code === 0 || !output.trim()) {
|
|
64
|
+
console.log(" Outdated: all packages up to date");
|
|
65
|
+
resolve({
|
|
66
|
+
passed: true,
|
|
67
|
+
totalChecked: 0,
|
|
68
|
+
outdatedCount: 0,
|
|
69
|
+
majorOutdated: 0,
|
|
70
|
+
packages: [],
|
|
71
|
+
advisory: "All packages up to date",
|
|
72
|
+
skipped: false,
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const data = JSON.parse(output);
|
|
78
|
+
const packages = [];
|
|
79
|
+
for (const [name, info] of Object.entries(data)) {
|
|
80
|
+
if (!info.current || !info.latest)
|
|
81
|
+
continue;
|
|
82
|
+
const severity = classifySemverDiff(info.current, info.latest);
|
|
83
|
+
packages.push({
|
|
84
|
+
name,
|
|
85
|
+
current: info.current,
|
|
86
|
+
wanted: info.wanted ?? info.current,
|
|
87
|
+
latest: info.latest,
|
|
88
|
+
severity,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const majorOutdated = packages.filter((p) => p.severity === "major").length;
|
|
92
|
+
const outdatedCount = packages.length;
|
|
93
|
+
const advisory = majorOutdated > 0
|
|
94
|
+
? `${majorOutdated} major version(s) behind latest; ${outdatedCount} total outdated`
|
|
95
|
+
: `${outdatedCount} package(s) behind latest (no major)`;
|
|
96
|
+
console.log(` Outdated: ${advisory}`);
|
|
97
|
+
for (const pkg of packages.filter((p) => p.severity === "major").slice(0, 5)) {
|
|
98
|
+
console.log(` ${pkg.name}: ${pkg.current} → ${pkg.latest} (${pkg.severity})`);
|
|
99
|
+
}
|
|
100
|
+
resolve({
|
|
101
|
+
passed: majorOutdated === 0,
|
|
102
|
+
totalChecked: 0,
|
|
103
|
+
outdatedCount,
|
|
104
|
+
majorOutdated,
|
|
105
|
+
packages,
|
|
106
|
+
advisory,
|
|
107
|
+
skipped: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
resolve({
|
|
112
|
+
passed: true,
|
|
113
|
+
totalChecked: 0,
|
|
114
|
+
outdatedCount: 0,
|
|
115
|
+
majorOutdated: 0,
|
|
116
|
+
packages: [],
|
|
117
|
+
advisory: "Could not parse npm outdated output",
|
|
118
|
+
skipped: false,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -1,39 +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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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;
|