laxy-verify 1.2.2 → 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 +181 -47
- package/dist/a11y-deep.d.ts +20 -0
- package/dist/a11y-deep.js +161 -0
- package/dist/ai-analysis.d.ts +28 -0
- package/dist/ai-analysis.js +32 -0
- package/dist/audit/broken-links.d.ts +5 -1
- package/dist/audit/broken-links.js +23 -12
- package/dist/bundle-size.d.ts +14 -0
- package/dist/bundle-size.js +209 -0
- package/dist/cli.js +391 -13
- package/dist/compare-env.d.ts +23 -0
- package/dist/compare-env.js +55 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +106 -1
- package/dist/entitlement.d.ts +2 -0
- package/dist/entitlement.js +5 -1
- package/dist/init-analysis.d.ts +6 -0
- package/dist/init-analysis.js +302 -0
- package/dist/init.js +66 -0
- package/dist/lighthouse.d.ts +31 -1
- package/dist/lighthouse.js +76 -3
- package/dist/outdated-check.d.ts +17 -0
- package/dist/outdated-check.js +123 -0
- package/dist/report-markdown.d.ts +14 -0
- package/dist/report-markdown.js +21 -0
- package/dist/route-discovery.d.ts +7 -0
- package/dist/route-discovery.js +108 -0
- package/dist/secret-scan.d.ts +15 -0
- package/dist/secret-scan.js +218 -0
- package/dist/security-audit.d.ts +9 -1
- package/dist/security-audit.js +87 -24
- 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 +117 -0
- package/dist/verification-core/types.d.ts +58 -2
- package/dist/visual-diff.d.ts +8 -1
- package/dist/visual-diff.js +53 -8
- package/dist/vitals-budget.d.ts +23 -0
- package/dist/vitals-budget.js +168 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -58,8 +58,18 @@ const report_markdown_js_1 = require("./report-markdown.js");
|
|
|
58
58
|
const visual_diff_js_1 = require("./visual-diff.js");
|
|
59
59
|
const security_audit_js_1 = require("./security-audit.js");
|
|
60
60
|
const broken_links_js_1 = require("./audit/broken-links.js");
|
|
61
|
+
const typecheck_js_1 = require("./typecheck.js");
|
|
62
|
+
const secret_scan_js_1 = require("./secret-scan.js");
|
|
63
|
+
const bundle_size_js_1 = require("./bundle-size.js");
|
|
64
|
+
const outdated_check_js_1 = require("./outdated-check.js");
|
|
65
|
+
const a11y_deep_js_1 = require("./a11y-deep.js");
|
|
66
|
+
const seo_deep_js_1 = require("./seo-deep.js");
|
|
67
|
+
const vitals_budget_js_1 = require("./vitals-budget.js");
|
|
61
68
|
const index_js_1 = require("./verification-core/index.js");
|
|
62
69
|
const trend_js_1 = require("./trend.js");
|
|
70
|
+
const ai_analysis_js_1 = require("./ai-analysis.js");
|
|
71
|
+
const compare_env_js_1 = require("./compare-env.js");
|
|
72
|
+
const route_discovery_js_1 = require("./route-discovery.js");
|
|
63
73
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
64
74
|
let activeDevServerPid;
|
|
65
75
|
let activeDevServerCleanup = null;
|
|
@@ -161,12 +171,38 @@ function parseArgs() {
|
|
|
161
171
|
port: flags.port !== undefined ? Number(flags.port) : undefined,
|
|
162
172
|
share: flags.share !== undefined,
|
|
163
173
|
help: flags.help !== undefined || flags.h !== undefined,
|
|
174
|
+
compareUrl: flags.compare,
|
|
175
|
+
typecheck: flags.typecheck !== undefined,
|
|
176
|
+
secretScan: flags["secret-scan"] !== undefined,
|
|
177
|
+
bundleSize: flags["bundle-size"] !== undefined,
|
|
178
|
+
outdatedCheck: flags["outdated-check"] !== undefined,
|
|
179
|
+
a11yDeep: flags["a11y-deep"] !== undefined,
|
|
180
|
+
seoDeep: flags["seo-deep"] !== undefined,
|
|
181
|
+
vitalsBudget: flags["vitals-budget"] !== undefined,
|
|
164
182
|
};
|
|
165
183
|
}
|
|
166
184
|
function writeResultFile(projectDir, result) {
|
|
167
185
|
const filePath = path.join(projectDir, ".laxy-result.json");
|
|
168
186
|
fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
169
187
|
}
|
|
188
|
+
function uniqueRouteList(routes) {
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const result = [];
|
|
191
|
+
for (const route of routes) {
|
|
192
|
+
if (!route)
|
|
193
|
+
continue;
|
|
194
|
+
const normalized = route.startsWith("/") ? route : `/${route}`;
|
|
195
|
+
const trimmed = normalized.split("?")[0]?.split("#")[0]?.replace(/\/+/g, "/") ?? "/";
|
|
196
|
+
const finalRoute = trimmed.endsWith("/") && trimmed !== "/" ? trimmed.slice(0, -1) : trimmed;
|
|
197
|
+
if (finalRoute === "/" || finalRoute.startsWith("/api/") || finalRoute.startsWith("/_next/"))
|
|
198
|
+
continue;
|
|
199
|
+
if (seen.has(finalRoute))
|
|
200
|
+
continue;
|
|
201
|
+
seen.add(finalRoute);
|
|
202
|
+
result.push(finalRoute);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
170
206
|
function summarizeViewportIssues(scores, thresholds) {
|
|
171
207
|
if (!scores)
|
|
172
208
|
return { count: 0 };
|
|
@@ -208,12 +244,30 @@ function consoleOutput(result) {
|
|
|
208
244
|
const lh = result.lighthouse;
|
|
209
245
|
const t = result.thresholds;
|
|
210
246
|
const check = (passed) => (passed ? " OK" : " FAIL");
|
|
211
|
-
|
|
247
|
+
const routeLabel = result.lighthouseRoutes && result.lighthouseRoutes.length > 1
|
|
248
|
+
? ` (weighted avg of ${result.lighthouseRoutes.length} routes)`
|
|
249
|
+
: "";
|
|
250
|
+
console.log(` Lighthouse${routeLabel}:`);
|
|
212
251
|
console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
|
|
213
252
|
console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
|
|
214
253
|
console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
|
|
215
254
|
console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
|
|
216
255
|
console.log(` Runs: ${lh.runs}`);
|
|
256
|
+
if (result.lighthouseRoutes && result.lighthouseRoutes.length > 1) {
|
|
257
|
+
console.log(" Per-route:");
|
|
258
|
+
for (const r of result.lighthouseRoutes) {
|
|
259
|
+
if (r.scores) {
|
|
260
|
+
const pass = r.scores.performance >= t.performance &&
|
|
261
|
+
r.scores.accessibility >= t.accessibility &&
|
|
262
|
+
r.scores.seo >= t.seo &&
|
|
263
|
+
r.scores.bestPractices >= t.bestPractices;
|
|
264
|
+
console.log(` ${r.route.padEnd(20)} P=${r.scores.performance} A=${r.scores.accessibility} S=${r.scores.seo} BP=${r.scores.bestPractices}${pass ? "" : " FAIL"}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.log(` ${r.route.padEnd(20)} (no data)`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
217
271
|
}
|
|
218
272
|
else {
|
|
219
273
|
console.log(" Lighthouse: skipped");
|
|
@@ -238,6 +292,27 @@ function consoleOutput(result) {
|
|
|
238
292
|
const ml = result.mobileLighthouse;
|
|
239
293
|
console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
|
|
240
294
|
}
|
|
295
|
+
if (result.typecheck && !result.typecheck.skipped) {
|
|
296
|
+
console.log(` TypeScript: ${result.typecheck.passed ? "OK" : `${result.typecheck.errorCount} error(s)`}`);
|
|
297
|
+
}
|
|
298
|
+
if (result.secretScan && !result.secretScan.skipped) {
|
|
299
|
+
console.log(` Secret scan: ${result.secretScan.passed ? "OK" : `${result.secretScan.findings.length} finding(s)`} (${result.secretScan.filesScanned} files)`);
|
|
300
|
+
}
|
|
301
|
+
if (result.bundleSize && !result.bundleSize.skipped) {
|
|
302
|
+
console.log(` Bundle size: ${result.bundleSize.advisory}`);
|
|
303
|
+
}
|
|
304
|
+
if (result.outdatedCheck && !result.outdatedCheck.skipped) {
|
|
305
|
+
console.log(` Outdated: ${result.outdatedCheck.advisory}`);
|
|
306
|
+
}
|
|
307
|
+
if (result.a11yDeep && !result.a11yDeep.skipped) {
|
|
308
|
+
console.log(` A11y deep: ${result.a11yDeep.summary}`);
|
|
309
|
+
}
|
|
310
|
+
if (result.seoDeep && !result.seoDeep.skipped) {
|
|
311
|
+
console.log(` SEO deep: ${result.seoDeep.summary}`);
|
|
312
|
+
}
|
|
313
|
+
if (result.vitalsBudget && !result.vitalsBudget.skipped) {
|
|
314
|
+
console.log(` Vitals budget: ${result.vitalsBudget.summary}`);
|
|
315
|
+
}
|
|
241
316
|
if (result.verification) {
|
|
242
317
|
const view = result.verification.view;
|
|
243
318
|
const verboseFailure = result._verbose_failure ?? true;
|
|
@@ -293,6 +368,9 @@ function consoleOutput(result) {
|
|
|
293
368
|
if (result.github.status === "status_set")
|
|
294
369
|
console.log(` Status check: ${result.github.grade}`);
|
|
295
370
|
}
|
|
371
|
+
if (result.compareEnv) {
|
|
372
|
+
(0, compare_env_js_1.printEnvComparison)(result.compareEnv);
|
|
373
|
+
}
|
|
296
374
|
console.log(" Result: .laxy-result.json");
|
|
297
375
|
if (result.markdownReportPath) {
|
|
298
376
|
console.log(` Report: ${path.basename(result.markdownReportPath)}`);
|
|
@@ -329,10 +407,18 @@ async function run() {
|
|
|
329
407
|
--skip-lighthouse Skip Lighthouse but still run build and E2E
|
|
330
408
|
--port <port> Use an already-running dev server on this port (skip build & server start)
|
|
331
409
|
--share Create a public share link for this verification result (Pro)
|
|
410
|
+
--compare <url> Compare Lighthouse scores against a reference environment URL (Pro)
|
|
332
411
|
--plan-override free | pro | team (testing metadata only)
|
|
333
412
|
--multi-viewport Lighthouse on desktop/tablet/mobile
|
|
334
|
-
--crawl Crawl the app to discover routes before E2E
|
|
413
|
+
--crawl Crawl the app to discover routes before E2E (also enables multi-route Lighthouse)
|
|
335
414
|
--badge Print shields.io badge markdown
|
|
415
|
+
--typecheck Run TypeScript type check (tsc --noEmit)
|
|
416
|
+
--secret-scan Scan for hardcoded secrets and credentials
|
|
417
|
+
--bundle-size Analyze bundle size (Next.js/Vite)
|
|
418
|
+
--outdated-check Check for outdated dependencies
|
|
419
|
+
--a11y-deep Deep accessibility audit (axe-core)
|
|
420
|
+
--seo-deep Deep SEO audit (meta, OG, JSON-LD)
|
|
421
|
+
--vitals-budget Core Web Vitals budget check (LCP, CLS, INP)
|
|
336
422
|
--help Show this help
|
|
337
423
|
|
|
338
424
|
Exit codes:
|
|
@@ -347,6 +433,7 @@ async function run() {
|
|
|
347
433
|
npx laxy-verify . --fail-on silver # Block Bronze or worse
|
|
348
434
|
npx laxy-verify . --port 3001 # Use existing dev server on port 3001
|
|
349
435
|
npx laxy-verify . --share # Save and share a public verification link
|
|
436
|
+
npx laxy-verify . --compare https://staging.example.com # Compare vs reference env (Pro)
|
|
350
437
|
|
|
351
438
|
Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
|
|
352
439
|
`);
|
|
@@ -437,6 +524,13 @@ async function run() {
|
|
|
437
524
|
cliFlags: {
|
|
438
525
|
failOn: args.failOn,
|
|
439
526
|
skipLighthouse: args.skipLighthouse,
|
|
527
|
+
typecheck: args.typecheck,
|
|
528
|
+
secretScan: args.secretScan,
|
|
529
|
+
bundleSize: args.bundleSize,
|
|
530
|
+
outdatedCheck: args.outdatedCheck,
|
|
531
|
+
a11yDeep: args.a11yDeep,
|
|
532
|
+
seoDeep: args.seoDeep,
|
|
533
|
+
vitalsBudget: args.vitalsBudget,
|
|
440
534
|
},
|
|
441
535
|
teamThresholds,
|
|
442
536
|
});
|
|
@@ -446,6 +540,21 @@ async function run() {
|
|
|
446
540
|
exitGracefully(2);
|
|
447
541
|
return;
|
|
448
542
|
}
|
|
543
|
+
if (args.compareUrl) {
|
|
544
|
+
try {
|
|
545
|
+
const u = new URL(args.compareUrl);
|
|
546
|
+
if (!["http:", "https:"].includes(u.protocol)) {
|
|
547
|
+
console.error(`Config error: --compare URL must use http or https protocol: ${args.compareUrl}`);
|
|
548
|
+
exitGracefully(2);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
console.error(`Config error: --compare value is not a valid URL: ${args.compareUrl}`);
|
|
554
|
+
exitGracefully(2);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
449
558
|
let detected;
|
|
450
559
|
try {
|
|
451
560
|
detected = (0, detect_js_1.detect)(args.projectDir);
|
|
@@ -517,11 +626,11 @@ async function run() {
|
|
|
517
626
|
}
|
|
518
627
|
const features = entitlements ?? {
|
|
519
628
|
plan: "free",
|
|
520
|
-
// All verification features run on every plan
|
|
521
|
-
// Automation features ??not available on Free
|
|
522
629
|
github_actions: false,
|
|
523
630
|
queue_priority: false,
|
|
524
631
|
parallel_execution: false,
|
|
632
|
+
ai_failure_analysis: false,
|
|
633
|
+
compare_env: false,
|
|
525
634
|
};
|
|
526
635
|
let effectiveFeatures = features;
|
|
527
636
|
if (args.planOverride) {
|
|
@@ -539,17 +648,28 @@ async function run() {
|
|
|
539
648
|
if (config.lighthouse_runs < 3) {
|
|
540
649
|
config = { ...config, lighthouse_runs: 3 };
|
|
541
650
|
}
|
|
651
|
+
let multiRouteLhResult = null;
|
|
542
652
|
let multiViewportScores = null;
|
|
543
653
|
let allViewportsOk = false;
|
|
544
654
|
let e2eResult;
|
|
545
655
|
let crossBrowserResults;
|
|
546
656
|
let e2eCoverageGaps = [];
|
|
547
657
|
let e2eConsoleErrors = [];
|
|
658
|
+
let lastCrawlRoutes = [];
|
|
659
|
+
let runtimeDiscoveredRoutes = [];
|
|
548
660
|
let e2eStabilityPassed = true;
|
|
549
661
|
let visualDiffResult = null;
|
|
550
662
|
let securityAuditResult = null;
|
|
551
663
|
let mobileLighthouseScores = null;
|
|
552
664
|
let brokenLinksResult = null;
|
|
665
|
+
let compareEnvResult = null;
|
|
666
|
+
let typecheckResult = null;
|
|
667
|
+
let secretScanResult = null;
|
|
668
|
+
let bundleSizeResult = null;
|
|
669
|
+
let outdatedCheckResult = null;
|
|
670
|
+
let a11yDeepResult = null;
|
|
671
|
+
let seoDeepResult = null;
|
|
672
|
+
let vitalsBudgetResult = null;
|
|
553
673
|
const failureEvidence = [];
|
|
554
674
|
if (buildResult.success) {
|
|
555
675
|
let servePid;
|
|
@@ -559,13 +679,37 @@ async function run() {
|
|
|
559
679
|
servePid = serve.pid;
|
|
560
680
|
activeDevServerPid = serve.pid;
|
|
561
681
|
}
|
|
562
|
-
const verifyUrl = `http://
|
|
682
|
+
const verifyUrl = `http://localhost:${port}/`;
|
|
563
683
|
const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
|
|
684
|
+
try {
|
|
685
|
+
const runtimeRouteDiscovery = await (0, route_discovery_js_1.discoverRuntimeRoutes)(verifyUrl);
|
|
686
|
+
runtimeDiscoveredRoutes = runtimeRouteDiscovery.routes;
|
|
687
|
+
if (runtimeDiscoveredRoutes.length > 0) {
|
|
688
|
+
console.log(` Runtime route discovery: found ${runtimeDiscoveredRoutes.length} route(s) from app bundles.`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (routeErr) {
|
|
692
|
+
console.error(`Runtime route discovery warning: ${routeErr instanceof Error ? routeErr.message : String(routeErr)}`);
|
|
693
|
+
}
|
|
564
694
|
if (!args.skipLighthouse) {
|
|
565
695
|
try {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
696
|
+
const explicitRoutes = config.lighthouse_routes?.length
|
|
697
|
+
? config.lighthouse_routes
|
|
698
|
+
: config.extra_routes?.length
|
|
699
|
+
? config.extra_routes
|
|
700
|
+
: undefined;
|
|
701
|
+
if (explicitRoutes && explicitRoutes.length > 0) {
|
|
702
|
+
// Multi-route mode: run on all explicitly configured routes
|
|
703
|
+
multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, explicitRoutes);
|
|
704
|
+
lighthouseErrorCount = multiRouteLhResult.perRoute.reduce((n, r) => n + r.errors.length, 0);
|
|
705
|
+
scores = multiRouteLhResult.aggregated ?? undefined;
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
// Single-route mode (default): root only
|
|
709
|
+
const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
|
|
710
|
+
lighthouseErrorCount = lhResult.errors.length;
|
|
711
|
+
scores = lhResult.scores ?? undefined;
|
|
712
|
+
}
|
|
569
713
|
if (scores) {
|
|
570
714
|
lighthouseResult = {
|
|
571
715
|
performance: scores.performance,
|
|
@@ -599,8 +743,10 @@ async function run() {
|
|
|
599
743
|
}
|
|
600
744
|
// Security audit (npm audit)
|
|
601
745
|
try {
|
|
602
|
-
securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
|
|
603
|
-
if (securityAuditResult.critical > 0 ||
|
|
746
|
+
securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir, verifyUrl);
|
|
747
|
+
if (securityAuditResult.critical > 0 ||
|
|
748
|
+
securityAuditResult.high > 0 ||
|
|
749
|
+
securityAuditResult.missingHeaders.length > 0) {
|
|
604
750
|
failureEvidence.push(`Security: ${securityAuditResult.summary}`);
|
|
605
751
|
}
|
|
606
752
|
}
|
|
@@ -630,11 +776,22 @@ async function run() {
|
|
|
630
776
|
const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
|
|
631
777
|
e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
|
|
632
778
|
}
|
|
779
|
+
// Store crawl-discovered routes for post-E2E Lighthouse pass
|
|
780
|
+
lastCrawlRoutes = e2eRuns.crawlResult.pages
|
|
781
|
+
.map((p) => p.path)
|
|
782
|
+
.filter((p) => typeof p === "string" && p.startsWith("/"));
|
|
633
783
|
}
|
|
634
784
|
// Broken links audit — runs after E2E so we have the crawl result
|
|
635
|
-
|
|
785
|
+
const auditRoutes = uniqueRouteList([
|
|
786
|
+
...(config.extra_routes ?? []),
|
|
787
|
+
...runtimeDiscoveredRoutes,
|
|
788
|
+
...lastCrawlRoutes,
|
|
789
|
+
]);
|
|
790
|
+
if (e2eRuns.crawlResult || auditRoutes.length > 0) {
|
|
636
791
|
try {
|
|
637
|
-
brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl
|
|
792
|
+
brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl, {
|
|
793
|
+
extraRoutes: auditRoutes,
|
|
794
|
+
});
|
|
638
795
|
if (brokenLinksResult.hasBrokenLinks) {
|
|
639
796
|
failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
|
|
640
797
|
}
|
|
@@ -670,6 +827,34 @@ async function run() {
|
|
|
670
827
|
catch (e2eErr) {
|
|
671
828
|
console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
|
|
672
829
|
}
|
|
830
|
+
// Crawl-based multi-route Lighthouse (only if no explicit lighthouse_routes configured)
|
|
831
|
+
if (!args.skipLighthouse &&
|
|
832
|
+
!config.lighthouse_routes?.length &&
|
|
833
|
+
uniqueRouteList([
|
|
834
|
+
...(config.extra_routes ?? []),
|
|
835
|
+
...runtimeDiscoveredRoutes,
|
|
836
|
+
...lastCrawlRoutes,
|
|
837
|
+
]).length > 1) {
|
|
838
|
+
try {
|
|
839
|
+
const crawlRoutes = uniqueRouteList([
|
|
840
|
+
...(config.extra_routes ?? []),
|
|
841
|
+
...runtimeDiscoveredRoutes,
|
|
842
|
+
...lastCrawlRoutes,
|
|
843
|
+
]).slice(0, config.max_lighthouse_routes);
|
|
844
|
+
console.log(`\n Running Lighthouse on ${crawlRoutes.length} discovered routes...`);
|
|
845
|
+
multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, crawlRoutes);
|
|
846
|
+
if (multiRouteLhResult.aggregated) {
|
|
847
|
+
scores = multiRouteLhResult.aggregated;
|
|
848
|
+
lighthouseResult = {
|
|
849
|
+
...multiRouteLhResult.aggregated,
|
|
850
|
+
runs: config.lighthouse_runs,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch (crawlLhErr) {
|
|
855
|
+
console.error(`Crawl-based Lighthouse error: ${crawlLhErr instanceof Error ? crawlLhErr.message : String(crawlLhErr)}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
673
858
|
// Cross-browser E2E via Playwright (if non-chromium browsers configured)
|
|
674
859
|
const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
|
|
675
860
|
if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
|
|
@@ -694,11 +879,118 @@ async function run() {
|
|
|
694
879
|
}
|
|
695
880
|
}
|
|
696
881
|
try {
|
|
697
|
-
visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify"
|
|
882
|
+
visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify", {
|
|
883
|
+
pixelmatchThreshold: config.visual_diff.pixelmatchThreshold,
|
|
884
|
+
warnThreshold: config.visual_diff.warnThreshold,
|
|
885
|
+
rollbackThreshold: config.visual_diff.rollbackThreshold,
|
|
886
|
+
ignoreSelectors: config.visual_diff.ignoreSelectors,
|
|
887
|
+
disableAnimations: config.visual_diff.disableAnimations,
|
|
888
|
+
});
|
|
698
889
|
}
|
|
699
890
|
catch (visualErr) {
|
|
700
891
|
console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
|
|
701
892
|
}
|
|
893
|
+
// Mobile Lighthouse (standalone — not part of multi-viewport)
|
|
894
|
+
if (!args.skipLighthouse && !args.multiViewport) {
|
|
895
|
+
try {
|
|
896
|
+
mobileLighthouseScores = await (0, multi_viewport_js_1.runMobileLighthouse)(port);
|
|
897
|
+
}
|
|
898
|
+
catch (mlErr) {
|
|
899
|
+
console.error(`Mobile Lighthouse error: ${mlErr instanceof Error ? mlErr.message : String(mlErr)}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// TypeScript type check
|
|
903
|
+
if (config.typecheck) {
|
|
904
|
+
try {
|
|
905
|
+
typecheckResult = await (0, typecheck_js_1.runTypecheck)(args.projectDir);
|
|
906
|
+
if (!typecheckResult.skipped && !typecheckResult.passed) {
|
|
907
|
+
failureEvidence.push(`TypeScript: ${typecheckResult.errorCount} type error(s)`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch (tcErr) {
|
|
911
|
+
console.error(`Typecheck error: ${tcErr instanceof Error ? tcErr.message : String(tcErr)}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// Secret scan
|
|
915
|
+
if (config.secret_scan) {
|
|
916
|
+
try {
|
|
917
|
+
secretScanResult = await (0, secret_scan_js_1.runSecretScan)(args.projectDir, {
|
|
918
|
+
ignorePaths: config.secret_scan_ignore_paths,
|
|
919
|
+
});
|
|
920
|
+
if (!secretScanResult.skipped && !secretScanResult.passed) {
|
|
921
|
+
failureEvidence.push(`Secret scan: ${secretScanResult.findings.length} finding(s)`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
catch (ssErr) {
|
|
925
|
+
console.error(`Secret scan error: ${ssErr instanceof Error ? ssErr.message : String(ssErr)}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
// Bundle size analysis (advisory)
|
|
929
|
+
if (config.bundle_size) {
|
|
930
|
+
try {
|
|
931
|
+
bundleSizeResult = await (0, bundle_size_js_1.runBundleSize)(args.projectDir);
|
|
932
|
+
}
|
|
933
|
+
catch (bsErr) {
|
|
934
|
+
console.error(`Bundle size error: ${bsErr instanceof Error ? bsErr.message : String(bsErr)}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// Outdated dependency check (advisory)
|
|
938
|
+
if (config.outdated_check) {
|
|
939
|
+
try {
|
|
940
|
+
outdatedCheckResult = await (0, outdated_check_js_1.runOutdatedCheck)(args.projectDir);
|
|
941
|
+
}
|
|
942
|
+
catch (ocErr) {
|
|
943
|
+
console.error(`Outdated check error: ${ocErr instanceof Error ? ocErr.message : String(ocErr)}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Deep accessibility audit (axe-core)
|
|
947
|
+
if (config.a11y_deep) {
|
|
948
|
+
try {
|
|
949
|
+
a11yDeepResult = await (0, a11y_deep_js_1.runA11yDeep)(verifyUrl);
|
|
950
|
+
if (!a11yDeepResult.skipped && !a11yDeepResult.passed) {
|
|
951
|
+
failureEvidence.push(`A11y deep: ${a11yDeepResult.summary}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
catch (a11yErr) {
|
|
955
|
+
console.error(`A11y deep error: ${a11yErr instanceof Error ? a11yErr.message : String(a11yErr)}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
// Deep SEO audit
|
|
959
|
+
if (config.seo_deep) {
|
|
960
|
+
try {
|
|
961
|
+
seoDeepResult = await (0, seo_deep_js_1.runSeoDeep)(verifyUrl);
|
|
962
|
+
if (!seoDeepResult.skipped && !seoDeepResult.passed) {
|
|
963
|
+
failureEvidence.push(`SEO deep: ${seoDeepResult.summary}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
catch (seoErr) {
|
|
967
|
+
console.error(`SEO deep error: ${seoErr instanceof Error ? seoErr.message : String(seoErr)}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
// Core Web Vitals budget check
|
|
971
|
+
if (config.vitals_budget) {
|
|
972
|
+
try {
|
|
973
|
+
vitalsBudgetResult = await (0, vitals_budget_js_1.runVitalsBudget)(verifyUrl);
|
|
974
|
+
if (!vitalsBudgetResult.skipped && !vitalsBudgetResult.passed) {
|
|
975
|
+
failureEvidence.push(`Vitals budget: ${vitalsBudgetResult.summary}`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
catch (vbErr) {
|
|
979
|
+
console.error(`Vitals budget error: ${vbErr instanceof Error ? vbErr.message : String(vbErr)}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
// Pro: multi-environment comparison
|
|
983
|
+
if (effectiveFeatures.compare_env && args.compareUrl) {
|
|
984
|
+
try {
|
|
985
|
+
compareEnvResult = await (0, compare_env_js_1.runEnvComparison)(port, args.compareUrl, 1);
|
|
986
|
+
}
|
|
987
|
+
catch (cmpErr) {
|
|
988
|
+
console.error(`Env comparison error: ${cmpErr instanceof Error ? cmpErr.message : String(cmpErr)}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
else if (args.compareUrl && !effectiveFeatures.compare_env) {
|
|
992
|
+
console.log(" [warn] --compare requires a logged-in Pro account. Skipping env comparison.");
|
|
993
|
+
}
|
|
702
994
|
}
|
|
703
995
|
catch (serveErr) {
|
|
704
996
|
console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
|
|
@@ -711,6 +1003,21 @@ async function run() {
|
|
|
711
1003
|
}
|
|
712
1004
|
const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
|
|
713
1005
|
const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
|
|
1006
|
+
// Multi-route Lighthouse: add per-route failures to evidence
|
|
1007
|
+
const allRoutesPass = multiRouteLhResult
|
|
1008
|
+
? multiRouteLhResult.allRoutesPass(adjustedThresholds)
|
|
1009
|
+
: true;
|
|
1010
|
+
if (multiRouteLhResult && !allRoutesPass) {
|
|
1011
|
+
const failingRoutes = multiRouteLhResult.perRoute.filter((r) => r.scores !== null &&
|
|
1012
|
+
(r.scores.performance < adjustedThresholds.performance ||
|
|
1013
|
+
r.scores.accessibility < adjustedThresholds.accessibility ||
|
|
1014
|
+
r.scores.seo < adjustedThresholds.seo ||
|
|
1015
|
+
r.scores.bestPractices < adjustedThresholds.bestPractices));
|
|
1016
|
+
for (const fr of failingRoutes.slice(0, 3)) {
|
|
1017
|
+
const s = fr.scores;
|
|
1018
|
+
failureEvidence.push(`Lighthouse ${fr.route}: P=${s.performance} A=${s.accessibility} SEO=${s.seo} BP=${s.bestPractices}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
714
1021
|
failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
|
|
715
1022
|
? e2eResult.results
|
|
716
1023
|
.filter((scenario) => !scenario.passed)
|
|
@@ -754,11 +1061,38 @@ async function run() {
|
|
|
754
1061
|
summary: brokenLinksResult.summary,
|
|
755
1062
|
}
|
|
756
1063
|
: undefined,
|
|
1064
|
+
typecheck: typecheckResult
|
|
1065
|
+
? { passed: typecheckResult.passed, errorCount: typecheckResult.errorCount, skipped: typecheckResult.skipped }
|
|
1066
|
+
: undefined,
|
|
1067
|
+
secretScan: secretScanResult
|
|
1068
|
+
? { passed: secretScanResult.passed, findingsCount: secretScanResult.findings.length, filesScanned: secretScanResult.filesScanned, skipped: secretScanResult.skipped }
|
|
1069
|
+
: undefined,
|
|
1070
|
+
a11yDeep: a11yDeepResult
|
|
1071
|
+
? { passed: a11yDeepResult.passed, criticalCount: a11yDeepResult.criticalCount, seriousCount: a11yDeepResult.seriousCount, summary: a11yDeepResult.summary, skipped: a11yDeepResult.skipped }
|
|
1072
|
+
: undefined,
|
|
1073
|
+
seoDeep: seoDeepResult
|
|
1074
|
+
? { passed: seoDeepResult.passed, errorCount: seoDeepResult.errorCount, warningCount: seoDeepResult.warningCount, summary: seoDeepResult.summary, skipped: seoDeepResult.skipped }
|
|
1075
|
+
: undefined,
|
|
1076
|
+
vitalsBudget: vitalsBudgetResult
|
|
1077
|
+
? { passed: vitalsBudgetResult.passed, lcp: vitalsBudgetResult.lcp, cls: vitalsBudgetResult.cls, inp: vitalsBudgetResult.inp, summary: vitalsBudgetResult.summary, skipped: vitalsBudgetResult.skipped }
|
|
1078
|
+
: undefined,
|
|
1079
|
+
bundleSize: bundleSizeResult
|
|
1080
|
+
? { framework: bundleSizeResult.framework, firstLoadJsKb: bundleSizeResult.firstLoadJsKb, largestChunkKb: bundleSizeResult.largestChunkKb, advisory: bundleSizeResult.advisory, skipped: bundleSizeResult.skipped }
|
|
1081
|
+
: undefined,
|
|
1082
|
+
outdatedCheck: outdatedCheckResult
|
|
1083
|
+
? { outdatedCount: outdatedCheckResult.outdatedCount, majorOutdated: outdatedCheckResult.majorOutdated, advisory: outdatedCheckResult.advisory, skipped: outdatedCheckResult.skipped }
|
|
1084
|
+
: undefined,
|
|
757
1085
|
failureEvidence,
|
|
758
1086
|
}, {
|
|
759
1087
|
tier: verificationTier,
|
|
760
1088
|
thresholds: adjustedThresholds,
|
|
761
1089
|
});
|
|
1090
|
+
// Gold eligibility: every route must individually pass all thresholds.
|
|
1091
|
+
// If the weighted-average aggregate earned Gold but some routes individually failed,
|
|
1092
|
+
// downgrade to Silver to enforce the per-route requirement.
|
|
1093
|
+
if (multiRouteLhResult && !allRoutesPass && verificationReport.grade === "gold") {
|
|
1094
|
+
verificationReport.grade = "silver";
|
|
1095
|
+
}
|
|
762
1096
|
const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
|
|
763
1097
|
const unifiedGrade = verificationReport.grade;
|
|
764
1098
|
const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
|
|
@@ -773,9 +1107,19 @@ async function run() {
|
|
|
773
1107
|
e2e: e2eResult,
|
|
774
1108
|
crossBrowser: crossBrowserResults,
|
|
775
1109
|
lighthouse: lighthouseResult,
|
|
1110
|
+
lighthouseRoutes: multiRouteLhResult
|
|
1111
|
+
? multiRouteLhResult.perRoute.map((r) => ({ route: r.route, scores: r.scores }))
|
|
1112
|
+
: undefined,
|
|
776
1113
|
mobileLighthouse: mobileLighthouseScores,
|
|
777
1114
|
security: securityAuditResult,
|
|
778
1115
|
visualDiff: visualDiffResult,
|
|
1116
|
+
typecheck: typecheckResult,
|
|
1117
|
+
secretScan: secretScanResult,
|
|
1118
|
+
bundleSize: bundleSizeResult,
|
|
1119
|
+
outdatedCheck: outdatedCheckResult,
|
|
1120
|
+
a11yDeep: a11yDeepResult,
|
|
1121
|
+
seoDeep: seoDeepResult,
|
|
1122
|
+
vitalsBudget: vitalsBudgetResult,
|
|
779
1123
|
thresholds: adjustedThresholds,
|
|
780
1124
|
ciMode: config.ciMode,
|
|
781
1125
|
framework: detected.framework,
|
|
@@ -789,6 +1133,7 @@ async function run() {
|
|
|
789
1133
|
report: verificationReport,
|
|
790
1134
|
view: verificationView,
|
|
791
1135
|
},
|
|
1136
|
+
compareEnv: compareEnvResult ?? undefined,
|
|
792
1137
|
};
|
|
793
1138
|
const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
|
|
794
1139
|
if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
|
|
@@ -849,6 +1194,7 @@ async function run() {
|
|
|
849
1194
|
? "client-ready"
|
|
850
1195
|
: "hold",
|
|
851
1196
|
share: args.share,
|
|
1197
|
+
blockers: verificationView.blockers.slice(0, 5).map((b) => b.title),
|
|
852
1198
|
scores: {
|
|
853
1199
|
performance: scores?.performance,
|
|
854
1200
|
accessibility: scores?.accessibility,
|
|
@@ -890,6 +1236,38 @@ async function run() {
|
|
|
890
1236
|
}
|
|
891
1237
|
}
|
|
892
1238
|
writeResultFile(args.projectDir, resultObj);
|
|
1239
|
+
// Pro: AI failure analysis — runs after result is finalized, before output
|
|
1240
|
+
if (effectiveFeatures.ai_failure_analysis &&
|
|
1241
|
+
verificationReport.verdict !== "client-ready" &&
|
|
1242
|
+
verificationReport.verdict !== "release-ready" &&
|
|
1243
|
+
args.format !== "json") {
|
|
1244
|
+
try {
|
|
1245
|
+
console.log("\n Requesting AI failure analysis...");
|
|
1246
|
+
const aiAnalysis = await (0, ai_analysis_js_1.requestAiFailureAnalysis)({
|
|
1247
|
+
grade: unifiedGrade,
|
|
1248
|
+
blockers: verificationView.blockers.map((b) => ({ title: b.title, action: b.action })),
|
|
1249
|
+
lighthouseScores: scores ?? null,
|
|
1250
|
+
thresholds: adjustedThresholds,
|
|
1251
|
+
buildErrors: buildResult.errors.slice(0, 3),
|
|
1252
|
+
e2eFailed: e2eResult ? e2eResult.failed : 0,
|
|
1253
|
+
e2eTotal: e2eResult ? e2eResult.total : 0,
|
|
1254
|
+
securitySummary: securityAuditResult?.summary,
|
|
1255
|
+
});
|
|
1256
|
+
if (aiAnalysis) {
|
|
1257
|
+
console.log("\n AI Analysis (Pro):");
|
|
1258
|
+
console.log(` Root cause: ${aiAnalysis.rootCause}`);
|
|
1259
|
+
if (aiAnalysis.topFixes.length > 0) {
|
|
1260
|
+
console.log(" Top fixes:");
|
|
1261
|
+
for (const fix of aiAnalysis.topFixes) {
|
|
1262
|
+
console.log(` - ${fix}`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
catch {
|
|
1268
|
+
// AI analysis errors are non-critical
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
893
1271
|
if (args.format === "json") {
|
|
894
1272
|
console.log(JSON.stringify(resultObj, null, 2));
|
|
895
1273
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-environment Lighthouse comparison (Pro feature).
|
|
3
|
+
*
|
|
4
|
+
* Compares Lighthouse scores between the local build and a reference
|
|
5
|
+
* environment URL (e.g. staging, production) to detect regressions
|
|
6
|
+
* introduced by the current PR.
|
|
7
|
+
*/
|
|
8
|
+
import type { LighthouseScores } from "./grade.js";
|
|
9
|
+
export interface EnvComparisonResult {
|
|
10
|
+
localUrl: string;
|
|
11
|
+
compareUrl: string;
|
|
12
|
+
local: LighthouseScores | null;
|
|
13
|
+
compare: LighthouseScores | null;
|
|
14
|
+
/** Positive = local is better, negative = compare env is better */
|
|
15
|
+
delta: {
|
|
16
|
+
performance: number | null;
|
|
17
|
+
accessibility: number | null;
|
|
18
|
+
seo: number | null;
|
|
19
|
+
bestPractices: number | null;
|
|
20
|
+
} | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function runEnvComparison(localPort: number, compareUrl: string, runs?: number): Promise<EnvComparisonResult>;
|
|
23
|
+
export declare function printEnvComparison(result: EnvComparisonResult): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runEnvComparison = runEnvComparison;
|
|
4
|
+
exports.printEnvComparison = printEnvComparison;
|
|
5
|
+
const lighthouse_js_1 = require("./lighthouse.js");
|
|
6
|
+
function scoreDelta(local, compare) {
|
|
7
|
+
if (!local || !compare)
|
|
8
|
+
return null;
|
|
9
|
+
return {
|
|
10
|
+
performance: local.performance - compare.performance,
|
|
11
|
+
accessibility: local.accessibility - compare.accessibility,
|
|
12
|
+
seo: local.seo - compare.seo,
|
|
13
|
+
bestPractices: local.bestPractices - compare.bestPractices,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function runEnvComparison(localPort, compareUrl, runs = 1) {
|
|
17
|
+
const localUrl = `http://127.0.0.1:${localPort}/`;
|
|
18
|
+
console.log(`\n [compare] Running env comparison: local vs ${compareUrl}`);
|
|
19
|
+
const [localResult, compareResult] = await Promise.all([
|
|
20
|
+
(0, lighthouse_js_1.runLighthouseOnUrl)(localUrl, runs),
|
|
21
|
+
(0, lighthouse_js_1.runLighthouseOnUrl)(compareUrl, runs),
|
|
22
|
+
]);
|
|
23
|
+
return {
|
|
24
|
+
localUrl,
|
|
25
|
+
compareUrl,
|
|
26
|
+
local: localResult.scores,
|
|
27
|
+
compare: compareResult.scores,
|
|
28
|
+
delta: scoreDelta(localResult.scores, compareResult.scores),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function printEnvComparison(result) {
|
|
32
|
+
console.log("\n Environment comparison:");
|
|
33
|
+
console.log(` Local: ${result.localUrl}`);
|
|
34
|
+
console.log(` Compare: ${result.compareUrl}`);
|
|
35
|
+
if (!result.local && !result.compare) {
|
|
36
|
+
console.log(" Both environments returned no Lighthouse data.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const fmt = (v) => (v === null ? "—" : String(v));
|
|
40
|
+
const fmtDelta = (d) => {
|
|
41
|
+
if (d === null)
|
|
42
|
+
return "";
|
|
43
|
+
if (d > 0)
|
|
44
|
+
return ` (+${d})`;
|
|
45
|
+
if (d < 0)
|
|
46
|
+
return ` (${d})`;
|
|
47
|
+
return " (=)";
|
|
48
|
+
};
|
|
49
|
+
const d = result.delta;
|
|
50
|
+
console.log(" Metric Local Compare Delta");
|
|
51
|
+
console.log(` Performance ${fmt(result.local?.performance ?? null).padEnd(6)} ${fmt(result.compare?.performance ?? null).padEnd(8)}${fmtDelta(d?.performance ?? null)}`);
|
|
52
|
+
console.log(` Accessibility ${fmt(result.local?.accessibility ?? null).padEnd(6)} ${fmt(result.compare?.accessibility ?? null).padEnd(8)}${fmtDelta(d?.accessibility ?? null)}`);
|
|
53
|
+
console.log(` SEO ${fmt(result.local?.seo ?? null).padEnd(6)} ${fmt(result.compare?.seo ?? null).padEnd(8)}${fmtDelta(d?.seo ?? null)}`);
|
|
54
|
+
console.log(` Best Practices ${fmt(result.local?.bestPractices ?? null).padEnd(6)} ${fmt(result.compare?.bestPractices ?? null).padEnd(8)}${fmtDelta(d?.bestPractices ?? null)}`);
|
|
55
|
+
}
|