laxy-verify 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -64
- 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 +432 -16
- package/dist/compare-env.d.ts +23 -0
- package/dist/compare-env.js +55 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +149 -4
- 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;
|
|
@@ -159,13 +169,40 @@ function parseArgs() {
|
|
|
159
169
|
crawl: flags.crawl !== undefined,
|
|
160
170
|
planOverride: flags["plan-override"],
|
|
161
171
|
port: flags.port !== undefined ? Number(flags.port) : undefined,
|
|
172
|
+
share: flags.share !== undefined,
|
|
162
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,
|
|
163
182
|
};
|
|
164
183
|
}
|
|
165
184
|
function writeResultFile(projectDir, result) {
|
|
166
185
|
const filePath = path.join(projectDir, ".laxy-result.json");
|
|
167
186
|
fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
168
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
|
+
}
|
|
169
206
|
function summarizeViewportIssues(scores, thresholds) {
|
|
170
207
|
if (!scores)
|
|
171
208
|
return { count: 0 };
|
|
@@ -207,12 +244,30 @@ function consoleOutput(result) {
|
|
|
207
244
|
const lh = result.lighthouse;
|
|
208
245
|
const t = result.thresholds;
|
|
209
246
|
const check = (passed) => (passed ? " OK" : " FAIL");
|
|
210
|
-
|
|
247
|
+
const routeLabel = result.lighthouseRoutes && result.lighthouseRoutes.length > 1
|
|
248
|
+
? ` (weighted avg of ${result.lighthouseRoutes.length} routes)`
|
|
249
|
+
: "";
|
|
250
|
+
console.log(` Lighthouse${routeLabel}:`);
|
|
211
251
|
console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
|
|
212
252
|
console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
|
|
213
253
|
console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
|
|
214
254
|
console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
|
|
215
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
|
+
}
|
|
216
271
|
}
|
|
217
272
|
else {
|
|
218
273
|
console.log(" Lighthouse: skipped");
|
|
@@ -237,6 +292,27 @@ function consoleOutput(result) {
|
|
|
237
292
|
const ml = result.mobileLighthouse;
|
|
238
293
|
console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
|
|
239
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
|
+
}
|
|
240
316
|
if (result.verification) {
|
|
241
317
|
const view = result.verification.view;
|
|
242
318
|
const verboseFailure = result._verbose_failure ?? true;
|
|
@@ -292,10 +368,16 @@ function consoleOutput(result) {
|
|
|
292
368
|
if (result.github.status === "status_set")
|
|
293
369
|
console.log(` Status check: ${result.github.grade}`);
|
|
294
370
|
}
|
|
371
|
+
if (result.compareEnv) {
|
|
372
|
+
(0, compare_env_js_1.printEnvComparison)(result.compareEnv);
|
|
373
|
+
}
|
|
295
374
|
console.log(" Result: .laxy-result.json");
|
|
296
375
|
if (result.markdownReportPath) {
|
|
297
376
|
console.log(` Report: ${path.basename(result.markdownReportPath)}`);
|
|
298
377
|
}
|
|
378
|
+
if (result.share?.url) {
|
|
379
|
+
console.log(` Share: ${result.share.url}`);
|
|
380
|
+
}
|
|
299
381
|
console.log(` Exit code: ${result.exitCode}`);
|
|
300
382
|
}
|
|
301
383
|
async function run() {
|
|
@@ -324,10 +406,19 @@ async function run() {
|
|
|
324
406
|
--fail-on unverified | bronze | silver | gold
|
|
325
407
|
--skip-lighthouse Skip Lighthouse but still run build and E2E
|
|
326
408
|
--port <port> Use an already-running dev server on this port (skip build & server start)
|
|
409
|
+
--share Create a public share link for this verification result (Pro)
|
|
410
|
+
--compare <url> Compare Lighthouse scores against a reference environment URL (Pro)
|
|
327
411
|
--plan-override free | pro | team (testing metadata only)
|
|
328
412
|
--multi-viewport Lighthouse on desktop/tablet/mobile
|
|
329
|
-
--crawl Crawl the app to discover routes before E2E
|
|
413
|
+
--crawl Crawl the app to discover routes before E2E (also enables multi-route Lighthouse)
|
|
330
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)
|
|
331
422
|
--help Show this help
|
|
332
423
|
|
|
333
424
|
Exit codes:
|
|
@@ -341,6 +432,8 @@ async function run() {
|
|
|
341
432
|
npx laxy-verify . --ci # CI mode
|
|
342
433
|
npx laxy-verify . --fail-on silver # Block Bronze or worse
|
|
343
434
|
npx laxy-verify . --port 3001 # Use existing dev server on port 3001
|
|
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)
|
|
344
437
|
|
|
345
438
|
Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
|
|
346
439
|
`);
|
|
@@ -417,6 +510,11 @@ async function run() {
|
|
|
417
510
|
exitGracefully(0);
|
|
418
511
|
return;
|
|
419
512
|
}
|
|
513
|
+
// 팀 공통 임계값을 서버에서 로드 (토큰 없음/네트워크 오류 시 null)
|
|
514
|
+
const teamThresholds = await (0, config_js_1.fetchTeamThresholds)();
|
|
515
|
+
if (teamThresholds && args.format !== "json") {
|
|
516
|
+
console.log(" Team thresholds loaded from laxy.dev");
|
|
517
|
+
}
|
|
420
518
|
let config;
|
|
421
519
|
try {
|
|
422
520
|
config = (0, config_js_1.loadConfig)({
|
|
@@ -426,7 +524,15 @@ async function run() {
|
|
|
426
524
|
cliFlags: {
|
|
427
525
|
failOn: args.failOn,
|
|
428
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,
|
|
429
534
|
},
|
|
535
|
+
teamThresholds,
|
|
430
536
|
});
|
|
431
537
|
}
|
|
432
538
|
catch (err) {
|
|
@@ -434,6 +540,21 @@ async function run() {
|
|
|
434
540
|
exitGracefully(2);
|
|
435
541
|
return;
|
|
436
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
|
+
}
|
|
437
558
|
let detected;
|
|
438
559
|
try {
|
|
439
560
|
detected = (0, detect_js_1.detect)(args.projectDir);
|
|
@@ -465,7 +586,9 @@ async function run() {
|
|
|
465
586
|
exitGracefully(2);
|
|
466
587
|
return;
|
|
467
588
|
}
|
|
468
|
-
|
|
589
|
+
if (args.format !== "json") {
|
|
590
|
+
console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
|
|
591
|
+
}
|
|
469
592
|
}
|
|
470
593
|
let buildResult;
|
|
471
594
|
if (useExistingServer) {
|
|
@@ -503,11 +626,11 @@ async function run() {
|
|
|
503
626
|
}
|
|
504
627
|
const features = entitlements ?? {
|
|
505
628
|
plan: "free",
|
|
506
|
-
// All verification features run on every plan
|
|
507
|
-
// Automation features ??not available on Free
|
|
508
629
|
github_actions: false,
|
|
509
630
|
queue_priority: false,
|
|
510
631
|
parallel_execution: false,
|
|
632
|
+
ai_failure_analysis: false,
|
|
633
|
+
compare_env: false,
|
|
511
634
|
};
|
|
512
635
|
let effectiveFeatures = features;
|
|
513
636
|
if (args.planOverride) {
|
|
@@ -525,17 +648,28 @@ async function run() {
|
|
|
525
648
|
if (config.lighthouse_runs < 3) {
|
|
526
649
|
config = { ...config, lighthouse_runs: 3 };
|
|
527
650
|
}
|
|
651
|
+
let multiRouteLhResult = null;
|
|
528
652
|
let multiViewportScores = null;
|
|
529
653
|
let allViewportsOk = false;
|
|
530
654
|
let e2eResult;
|
|
531
655
|
let crossBrowserResults;
|
|
532
656
|
let e2eCoverageGaps = [];
|
|
533
657
|
let e2eConsoleErrors = [];
|
|
658
|
+
let lastCrawlRoutes = [];
|
|
659
|
+
let runtimeDiscoveredRoutes = [];
|
|
534
660
|
let e2eStabilityPassed = true;
|
|
535
661
|
let visualDiffResult = null;
|
|
536
662
|
let securityAuditResult = null;
|
|
537
663
|
let mobileLighthouseScores = null;
|
|
538
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;
|
|
539
673
|
const failureEvidence = [];
|
|
540
674
|
if (buildResult.success) {
|
|
541
675
|
let servePid;
|
|
@@ -545,13 +679,37 @@ async function run() {
|
|
|
545
679
|
servePid = serve.pid;
|
|
546
680
|
activeDevServerPid = serve.pid;
|
|
547
681
|
}
|
|
548
|
-
const verifyUrl = `http://
|
|
682
|
+
const verifyUrl = `http://localhost:${port}/`;
|
|
549
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
|
+
}
|
|
550
694
|
if (!args.skipLighthouse) {
|
|
551
695
|
try {
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
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
|
+
}
|
|
555
713
|
if (scores) {
|
|
556
714
|
lighthouseResult = {
|
|
557
715
|
performance: scores.performance,
|
|
@@ -585,8 +743,10 @@ async function run() {
|
|
|
585
743
|
}
|
|
586
744
|
// Security audit (npm audit)
|
|
587
745
|
try {
|
|
588
|
-
securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
|
|
589
|
-
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) {
|
|
590
750
|
failureEvidence.push(`Security: ${securityAuditResult.summary}`);
|
|
591
751
|
}
|
|
592
752
|
}
|
|
@@ -616,11 +776,22 @@ async function run() {
|
|
|
616
776
|
const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
|
|
617
777
|
e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
|
|
618
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("/"));
|
|
619
783
|
}
|
|
620
784
|
// Broken links audit — runs after E2E so we have the crawl result
|
|
621
|
-
|
|
785
|
+
const auditRoutes = uniqueRouteList([
|
|
786
|
+
...(config.extra_routes ?? []),
|
|
787
|
+
...runtimeDiscoveredRoutes,
|
|
788
|
+
...lastCrawlRoutes,
|
|
789
|
+
]);
|
|
790
|
+
if (e2eRuns.crawlResult || auditRoutes.length > 0) {
|
|
622
791
|
try {
|
|
623
|
-
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
|
+
});
|
|
624
795
|
if (brokenLinksResult.hasBrokenLinks) {
|
|
625
796
|
failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
|
|
626
797
|
}
|
|
@@ -656,6 +827,34 @@ async function run() {
|
|
|
656
827
|
catch (e2eErr) {
|
|
657
828
|
console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
|
|
658
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
|
+
}
|
|
659
858
|
// Cross-browser E2E via Playwright (if non-chromium browsers configured)
|
|
660
859
|
const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
|
|
661
860
|
if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
|
|
@@ -680,11 +879,118 @@ async function run() {
|
|
|
680
879
|
}
|
|
681
880
|
}
|
|
682
881
|
try {
|
|
683
|
-
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
|
+
});
|
|
684
889
|
}
|
|
685
890
|
catch (visualErr) {
|
|
686
891
|
console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
|
|
687
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
|
+
}
|
|
688
994
|
}
|
|
689
995
|
catch (serveErr) {
|
|
690
996
|
console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
|
|
@@ -697,6 +1003,21 @@ async function run() {
|
|
|
697
1003
|
}
|
|
698
1004
|
const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
|
|
699
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
|
+
}
|
|
700
1021
|
failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
|
|
701
1022
|
? e2eResult.results
|
|
702
1023
|
.filter((scenario) => !scenario.passed)
|
|
@@ -740,11 +1061,38 @@ async function run() {
|
|
|
740
1061
|
summary: brokenLinksResult.summary,
|
|
741
1062
|
}
|
|
742
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,
|
|
743
1085
|
failureEvidence,
|
|
744
1086
|
}, {
|
|
745
1087
|
tier: verificationTier,
|
|
746
1088
|
thresholds: adjustedThresholds,
|
|
747
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
|
+
}
|
|
748
1096
|
const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
|
|
749
1097
|
const unifiedGrade = verificationReport.grade;
|
|
750
1098
|
const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
|
|
@@ -759,9 +1107,19 @@ async function run() {
|
|
|
759
1107
|
e2e: e2eResult,
|
|
760
1108
|
crossBrowser: crossBrowserResults,
|
|
761
1109
|
lighthouse: lighthouseResult,
|
|
1110
|
+
lighthouseRoutes: multiRouteLhResult
|
|
1111
|
+
? multiRouteLhResult.perRoute.map((r) => ({ route: r.route, scores: r.scores }))
|
|
1112
|
+
: undefined,
|
|
762
1113
|
mobileLighthouse: mobileLighthouseScores,
|
|
763
1114
|
security: securityAuditResult,
|
|
764
1115
|
visualDiff: visualDiffResult,
|
|
1116
|
+
typecheck: typecheckResult,
|
|
1117
|
+
secretScan: secretScanResult,
|
|
1118
|
+
bundleSize: bundleSizeResult,
|
|
1119
|
+
outdatedCheck: outdatedCheckResult,
|
|
1120
|
+
a11yDeep: a11yDeepResult,
|
|
1121
|
+
seoDeep: seoDeepResult,
|
|
1122
|
+
vitalsBudget: vitalsBudgetResult,
|
|
765
1123
|
thresholds: adjustedThresholds,
|
|
766
1124
|
ciMode: config.ciMode,
|
|
767
1125
|
framework: detected.framework,
|
|
@@ -775,6 +1133,7 @@ async function run() {
|
|
|
775
1133
|
report: verificationReport,
|
|
776
1134
|
view: verificationView,
|
|
777
1135
|
},
|
|
1136
|
+
compareEnv: compareEnvResult ?? undefined,
|
|
778
1137
|
};
|
|
779
1138
|
const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
|
|
780
1139
|
if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
|
|
@@ -814,6 +1173,9 @@ async function run() {
|
|
|
814
1173
|
// Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
|
|
815
1174
|
const token = (0, auth_js_1.loadToken)();
|
|
816
1175
|
const isPro = effectiveFeatures.plan === "pro" || effectiveFeatures.plan === "team";
|
|
1176
|
+
if (args.share && (!token || !isPro)) {
|
|
1177
|
+
console.warn(" [warn] --share requires a logged-in Pro or Team account.");
|
|
1178
|
+
}
|
|
817
1179
|
if (token && isPro) {
|
|
818
1180
|
try {
|
|
819
1181
|
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
@@ -828,7 +1190,11 @@ async function run() {
|
|
|
828
1190
|
repo_id: repoId,
|
|
829
1191
|
project_name: path.basename(path.resolve(args.projectDir)),
|
|
830
1192
|
grade: unifiedGrade,
|
|
831
|
-
verdict: verificationReport.verdict
|
|
1193
|
+
verdict: verificationReport.verdict === "client-ready" || verificationReport.verdict === "release-ready"
|
|
1194
|
+
? "client-ready"
|
|
1195
|
+
: "hold",
|
|
1196
|
+
share: args.share,
|
|
1197
|
+
blockers: verificationView.blockers.slice(0, 5).map((b) => b.title),
|
|
832
1198
|
scores: {
|
|
833
1199
|
performance: scores?.performance,
|
|
834
1200
|
accessibility: scores?.accessibility,
|
|
@@ -840,18 +1206,68 @@ async function run() {
|
|
|
840
1206
|
console_error_count: e2eConsoleErrors.length,
|
|
841
1207
|
broken_link_count: brokenLinksResult?.brokenLinks.length,
|
|
842
1208
|
},
|
|
843
|
-
full_result: {
|
|
1209
|
+
full_result: {
|
|
1210
|
+
framework: detected.framework,
|
|
1211
|
+
build: { success: buildResult.success },
|
|
1212
|
+
e2e: e2eResult ? { passed: e2eResult.passed, total: e2eResult.total } : null,
|
|
1213
|
+
lighthouse: lighthouseResult,
|
|
1214
|
+
verification: {
|
|
1215
|
+
report: verificationReport,
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
844
1218
|
}),
|
|
845
1219
|
});
|
|
846
1220
|
if (!saveRes.ok) {
|
|
847
1221
|
const errBody = await saveRes.json().catch(() => ({}));
|
|
848
1222
|
console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
|
|
849
1223
|
}
|
|
1224
|
+
else {
|
|
1225
|
+
const saveData = await saveRes.json().catch(() => ({}));
|
|
1226
|
+
if (saveData.share_id && saveData.share_url) {
|
|
1227
|
+
resultObj.share = {
|
|
1228
|
+
id: saveData.share_id,
|
|
1229
|
+
url: saveData.share_url,
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
850
1233
|
}
|
|
851
1234
|
catch {
|
|
852
1235
|
// 네트워크 오류는 검증 결과에 영향 없음
|
|
853
1236
|
}
|
|
854
1237
|
}
|
|
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
|
+
}
|
|
855
1271
|
if (args.format === "json") {
|
|
856
1272
|
console.log(JSON.stringify(resultObj, null, 2));
|
|
857
1273
|
}
|