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/security-audit.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditSecurityHeaders = auditSecurityHeaders;
|
|
3
4
|
exports.runSecurityAudit = runSecurityAudit;
|
|
4
5
|
/**
|
|
5
|
-
* npm audit wrapper
|
|
6
|
+
* npm audit wrapper plus runtime security-header checks.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* The package audit catches known dependency vulnerabilities.
|
|
9
|
+
* The header audit adds shallow runtime coverage for common missing
|
|
10
|
+
* browser-enforced protections on the running app.
|
|
9
11
|
*/
|
|
10
12
|
const node_child_process_1 = require("node:child_process");
|
|
11
|
-
async function
|
|
12
|
-
console.log(" Running security audit (npm audit)...");
|
|
13
|
+
async function runNpmAudit(cwd, timeoutMs) {
|
|
13
14
|
return new Promise((resolve) => {
|
|
14
15
|
const chunks = [];
|
|
15
16
|
const proc = process.platform === "win32"
|
|
@@ -27,38 +28,100 @@ async function runSecurityAudit(cwd, timeoutMs = 30000) {
|
|
|
27
28
|
proc.kill();
|
|
28
29
|
}
|
|
29
30
|
catch { }
|
|
30
|
-
resolve({ totalVulnerabilities: 0, critical: 0, high: 0, moderate: 0, low: 0
|
|
31
|
+
resolve({ totalVulnerabilities: 0, critical: 0, high: 0, moderate: 0, low: 0 });
|
|
31
32
|
}, timeoutMs);
|
|
32
33
|
proc.stdout?.on("data", (chunk) => chunks.push(chunk.toString()));
|
|
33
|
-
proc.stderr?.on("data", () => { });
|
|
34
|
+
proc.stderr?.on("data", () => { });
|
|
34
35
|
proc.on("exit", () => {
|
|
35
36
|
clearTimeout(timer);
|
|
36
|
-
const raw = chunks.join("");
|
|
37
37
|
try {
|
|
38
|
-
const json = JSON.parse(
|
|
39
|
-
// npm audit v2 format
|
|
38
|
+
const json = JSON.parse(chunks.join(""));
|
|
40
39
|
const meta = json.metadata?.vulnerabilities ?? json.vulnerabilities ?? {};
|
|
41
40
|
const critical = meta.critical ?? 0;
|
|
42
41
|
const high = meta.high ?? 0;
|
|
43
42
|
const moderate = meta.moderate ?? 0;
|
|
44
43
|
const low = meta.low ?? 0;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
parts.push(`${moderate} moderate`);
|
|
53
|
-
if (low > 0)
|
|
54
|
-
parts.push(`${low} low`);
|
|
55
|
-
const summary = total === 0 ? "No known vulnerabilities" : parts.join(", ");
|
|
56
|
-
console.log(` Security: ${summary}`);
|
|
57
|
-
resolve({ totalVulnerabilities: total, critical, high, moderate, low, summary });
|
|
44
|
+
resolve({
|
|
45
|
+
totalVulnerabilities: critical + high + moderate + low,
|
|
46
|
+
critical,
|
|
47
|
+
high,
|
|
48
|
+
moderate,
|
|
49
|
+
low,
|
|
50
|
+
});
|
|
58
51
|
}
|
|
59
52
|
catch {
|
|
60
|
-
resolve({ totalVulnerabilities: 0, critical: 0, high: 0, moderate: 0, low: 0
|
|
53
|
+
resolve({ totalVulnerabilities: 0, critical: 0, high: 0, moderate: 0, low: 0 });
|
|
61
54
|
}
|
|
62
55
|
});
|
|
63
56
|
});
|
|
64
57
|
}
|
|
58
|
+
async function auditSecurityHeaders(url) {
|
|
59
|
+
const requiredHeaders = ["X-Frame-Options", "Content-Security-Policy"];
|
|
60
|
+
const urlObj = new URL(url);
|
|
61
|
+
if (urlObj.protocol === "https:") {
|
|
62
|
+
requiredHeaders.push("Strict-Transport-Security");
|
|
63
|
+
}
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await fetch(url, {
|
|
67
|
+
method: "HEAD",
|
|
68
|
+
redirect: "follow",
|
|
69
|
+
signal: AbortSignal.timeout(5000),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
try {
|
|
74
|
+
response = await fetch(url, {
|
|
75
|
+
method: "GET",
|
|
76
|
+
redirect: "follow",
|
|
77
|
+
signal: AbortSignal.timeout(5000),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
missingHeaders: [],
|
|
83
|
+
checkedUrl: url,
|
|
84
|
+
error: error instanceof Error ? error.message : String(error),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const missingHeaders = requiredHeaders.filter((headerName) => !response.headers.get(headerName));
|
|
89
|
+
return {
|
|
90
|
+
missingHeaders,
|
|
91
|
+
checkedUrl: response.url || url,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function runSecurityAudit(cwd, appUrl, timeoutMs = 30000) {
|
|
95
|
+
console.log(" Running security audit (npm audit + runtime headers)...");
|
|
96
|
+
const [npmAudit, headerAudit] = await Promise.all([
|
|
97
|
+
runNpmAudit(cwd, timeoutMs),
|
|
98
|
+
appUrl ? auditSecurityHeaders(appUrl) : Promise.resolve(null),
|
|
99
|
+
]);
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (npmAudit.critical > 0)
|
|
102
|
+
parts.push(`${npmAudit.critical} critical`);
|
|
103
|
+
if (npmAudit.high > 0)
|
|
104
|
+
parts.push(`${npmAudit.high} high`);
|
|
105
|
+
if (npmAudit.moderate > 0)
|
|
106
|
+
parts.push(`${npmAudit.moderate} moderate`);
|
|
107
|
+
if (npmAudit.low > 0)
|
|
108
|
+
parts.push(`${npmAudit.low} low`);
|
|
109
|
+
const missingHeaders = headerAudit?.missingHeaders ?? [];
|
|
110
|
+
if (missingHeaders.length > 0) {
|
|
111
|
+
parts.push(`missing headers: ${missingHeaders.join(", ")}`);
|
|
112
|
+
}
|
|
113
|
+
if (headerAudit?.error) {
|
|
114
|
+
parts.push(`header check skipped: ${headerAudit.error}`);
|
|
115
|
+
}
|
|
116
|
+
const summary = parts.length > 0
|
|
117
|
+
? parts.join(" | ")
|
|
118
|
+
: "No known vulnerabilities or missing security headers";
|
|
119
|
+
console.log(` Security: ${summary}`);
|
|
120
|
+
return {
|
|
121
|
+
...npmAudit,
|
|
122
|
+
summary,
|
|
123
|
+
missingHeaders,
|
|
124
|
+
headerCheckUrl: headerAudit?.checkedUrl,
|
|
125
|
+
headerCheckError: headerAudit?.error,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep SEO audit.
|
|
3
|
+
*
|
|
4
|
+
* Checks page-level SEO signals beyond Lighthouse's basic SEO category:
|
|
5
|
+
* title, meta description, canonical, robots, Open Graph, Twitter Card,
|
|
6
|
+
* JSON-LD structured data. Runs against the running dev server via HTTP.
|
|
7
|
+
*/
|
|
8
|
+
export interface SeoDeepResult {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
checks: SeoCheck[];
|
|
11
|
+
warningCount: number;
|
|
12
|
+
errorCount: number;
|
|
13
|
+
url: string;
|
|
14
|
+
skipped: boolean;
|
|
15
|
+
summary: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SeoCheck {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
passed: boolean;
|
|
21
|
+
value: string | null;
|
|
22
|
+
severity: "error" | "warning";
|
|
23
|
+
}
|
|
24
|
+
export declare function runSeoDeep(url: string): Promise<SeoDeepResult>;
|
package/dist/seo-deep.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSeoDeep = runSeoDeep;
|
|
4
|
+
async function fetchPageHtml(url) {
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch(url, {
|
|
7
|
+
signal: AbortSignal.timeout(10000),
|
|
8
|
+
headers: { "User-Agent": "laxy-verify/1.0" },
|
|
9
|
+
});
|
|
10
|
+
if (!res.ok)
|
|
11
|
+
return null;
|
|
12
|
+
return await res.text();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function extractMetaContent(html, nameAttr) {
|
|
19
|
+
// Match <meta name="..." content="..."> or <meta property="..." content="...">
|
|
20
|
+
const regex = new RegExp(`<meta\\s+(?:name|property)=["']${nameAttr}["']\\s+content=["']([^"']*)["']`, "i");
|
|
21
|
+
const match = html.match(regex);
|
|
22
|
+
if (match)
|
|
23
|
+
return match[1];
|
|
24
|
+
// Also try content before name/property
|
|
25
|
+
const regex2 = new RegExp(`<meta\\s+content=["']([^"']*)["']\\s+(?:name|property)=["']${nameAttr}["']`, "i");
|
|
26
|
+
const match2 = html.match(regex2);
|
|
27
|
+
return match2 ? match2[1] : null;
|
|
28
|
+
}
|
|
29
|
+
function extractTitle(html) {
|
|
30
|
+
const match = html.match(/<title[^>]*>([^<]*)<\/title>/i);
|
|
31
|
+
return match ? match[1].trim() : null;
|
|
32
|
+
}
|
|
33
|
+
function extractCanonical(html) {
|
|
34
|
+
const match = html.match(/<link[^>]+rel=["']canonical["'][^>]+href=["']([^"']*)["']/i);
|
|
35
|
+
return match ? match[1] : null;
|
|
36
|
+
}
|
|
37
|
+
function extractRobotsMeta(html) {
|
|
38
|
+
return extractMetaContent(html, "robots");
|
|
39
|
+
}
|
|
40
|
+
function hasJsonLd(html) {
|
|
41
|
+
return /<script[^>]+type=["']application\/ld\+json["']/i.test(html);
|
|
42
|
+
}
|
|
43
|
+
async function runSeoDeep(url) {
|
|
44
|
+
console.log(" Running deep SEO audit...");
|
|
45
|
+
const html = await fetchPageHtml(url);
|
|
46
|
+
if (!html) {
|
|
47
|
+
return {
|
|
48
|
+
passed: true,
|
|
49
|
+
checks: [],
|
|
50
|
+
warningCount: 0,
|
|
51
|
+
errorCount: 0,
|
|
52
|
+
url,
|
|
53
|
+
skipped: true,
|
|
54
|
+
summary: "Skipped (could not fetch page)",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const checks = [];
|
|
58
|
+
// Title
|
|
59
|
+
const title = extractTitle(html);
|
|
60
|
+
checks.push({
|
|
61
|
+
key: "title",
|
|
62
|
+
label: "Page title",
|
|
63
|
+
passed: !!title && title.length > 0 && title.length <= 60,
|
|
64
|
+
value: title,
|
|
65
|
+
severity: "error",
|
|
66
|
+
});
|
|
67
|
+
// Meta description
|
|
68
|
+
const description = extractMetaContent(html, "description");
|
|
69
|
+
checks.push({
|
|
70
|
+
key: "description",
|
|
71
|
+
label: "Meta description",
|
|
72
|
+
passed: !!description && description.length > 0 && description.length <= 160,
|
|
73
|
+
value: description,
|
|
74
|
+
severity: "warning",
|
|
75
|
+
});
|
|
76
|
+
// Canonical
|
|
77
|
+
const canonical = extractCanonical(html);
|
|
78
|
+
checks.push({
|
|
79
|
+
key: "canonical",
|
|
80
|
+
label: "Canonical URL",
|
|
81
|
+
passed: !!canonical,
|
|
82
|
+
value: canonical,
|
|
83
|
+
severity: "warning",
|
|
84
|
+
});
|
|
85
|
+
// Robots
|
|
86
|
+
const robots = extractRobotsMeta(html);
|
|
87
|
+
const robotsNoindex = robots?.includes("noindex") ?? false;
|
|
88
|
+
checks.push({
|
|
89
|
+
key: "robots",
|
|
90
|
+
label: "Robots meta",
|
|
91
|
+
passed: !robotsNoindex,
|
|
92
|
+
value: robots ?? "(not set — defaults to index)",
|
|
93
|
+
severity: "error",
|
|
94
|
+
});
|
|
95
|
+
// Open Graph
|
|
96
|
+
const ogTitle = extractMetaContent(html, "og:title");
|
|
97
|
+
const ogDescription = extractMetaContent(html, "og:description");
|
|
98
|
+
const ogImage = extractMetaContent(html, "og:image");
|
|
99
|
+
const hasOg = ogTitle || ogDescription || ogImage;
|
|
100
|
+
checks.push({
|
|
101
|
+
key: "og",
|
|
102
|
+
label: "Open Graph tags",
|
|
103
|
+
passed: !!hasOg,
|
|
104
|
+
value: hasOg
|
|
105
|
+
? [ogTitle && "title", ogDescription && "desc", ogImage && "image"].filter(Boolean).join(", ")
|
|
106
|
+
: null,
|
|
107
|
+
severity: "warning",
|
|
108
|
+
});
|
|
109
|
+
// Twitter Card
|
|
110
|
+
const twitterCard = extractMetaContent(html, "twitter:card");
|
|
111
|
+
const twitterTitle = extractMetaContent(html, "twitter:title");
|
|
112
|
+
const hasTwitter = twitterCard || twitterTitle;
|
|
113
|
+
checks.push({
|
|
114
|
+
key: "twitter",
|
|
115
|
+
label: "Twitter Card tags",
|
|
116
|
+
passed: !!hasTwitter,
|
|
117
|
+
value: hasTwitter
|
|
118
|
+
? [twitterCard && "card", twitterTitle && "title"].filter(Boolean).join(", ")
|
|
119
|
+
: null,
|
|
120
|
+
severity: "warning",
|
|
121
|
+
});
|
|
122
|
+
// JSON-LD
|
|
123
|
+
const jsonLd = hasJsonLd(html);
|
|
124
|
+
checks.push({
|
|
125
|
+
key: "jsonld",
|
|
126
|
+
label: "JSON-LD structured data",
|
|
127
|
+
passed: jsonLd,
|
|
128
|
+
value: jsonLd ? "present" : null,
|
|
129
|
+
severity: "warning",
|
|
130
|
+
});
|
|
131
|
+
const errorCount = checks.filter((c) => !c.passed && c.severity === "error").length;
|
|
132
|
+
const warningCount = checks.filter((c) => !c.passed && c.severity === "warning").length;
|
|
133
|
+
const passed = errorCount === 0;
|
|
134
|
+
const parts = [];
|
|
135
|
+
if (errorCount > 0)
|
|
136
|
+
parts.push(`${errorCount} error(s)`);
|
|
137
|
+
if (warningCount > 0)
|
|
138
|
+
parts.push(`${warningCount} warning(s)`);
|
|
139
|
+
const summary = parts.length > 0
|
|
140
|
+
? parts.join(", ")
|
|
141
|
+
: "All SEO checks passed";
|
|
142
|
+
console.log(` SEO deep: ${summary}`);
|
|
143
|
+
for (const check of checks.filter((c) => !c.passed)) {
|
|
144
|
+
console.error(` [${check.severity}] ${check.label}: ${check.value ?? "missing"}`);
|
|
145
|
+
}
|
|
146
|
+
return { passed, checks, warningCount, errorCount, url, skipped: false, summary };
|
|
147
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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.runTypecheck = runTypecheck;
|
|
37
|
+
/**
|
|
38
|
+
* TypeScript type-check verification.
|
|
39
|
+
*
|
|
40
|
+
* Runs `tsc --noEmit` to catch type errors that may slip through a
|
|
41
|
+
* lenient build pipeline. If no tsconfig.json is found the check is
|
|
42
|
+
* skipped gracefully.
|
|
43
|
+
*/
|
|
44
|
+
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
const fs = __importStar(require("node:fs"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
async function runTypecheck(projectDir, timeoutMs = 60000) {
|
|
48
|
+
const tsconfigPath = path.join(projectDir, "tsconfig.json");
|
|
49
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
50
|
+
return { passed: true, errorCount: 0, errors: [], skipped: true, durationMs: 0 };
|
|
51
|
+
}
|
|
52
|
+
console.log(" Running TypeScript type check (tsc --noEmit)...");
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const chunks = [];
|
|
56
|
+
const proc = process.platform === "win32"
|
|
57
|
+
? (0, node_child_process_1.spawn)(process.env.ComSpec || "cmd.exe", ["/d", "/c", "npx tsc --noEmit"], { stdio: ["ignore", "pipe", "pipe"], cwd: projectDir, shell: false })
|
|
58
|
+
: (0, node_child_process_1.spawn)("npx", ["tsc", "--noEmit"], {
|
|
59
|
+
shell: true,
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
cwd: projectDir,
|
|
62
|
+
});
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
try {
|
|
65
|
+
proc.kill();
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
resolve({
|
|
69
|
+
passed: false,
|
|
70
|
+
errorCount: -1,
|
|
71
|
+
errors: ["Typecheck timed out"],
|
|
72
|
+
skipped: false,
|
|
73
|
+
durationMs: Date.now() - start,
|
|
74
|
+
});
|
|
75
|
+
}, timeoutMs);
|
|
76
|
+
proc.stdout?.on("data", (chunk) => chunks.push(chunk.toString()));
|
|
77
|
+
proc.stderr?.on("data", (chunk) => chunks.push(chunk.toString()));
|
|
78
|
+
proc.on("exit", (code) => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
const durationMs = Date.now() - start;
|
|
81
|
+
const output = chunks.join("");
|
|
82
|
+
const passed = code === 0;
|
|
83
|
+
if (passed) {
|
|
84
|
+
console.log(" TypeScript: OK (no type errors)");
|
|
85
|
+
resolve({ passed: true, errorCount: 0, errors: [], skipped: false, durationMs });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const lines = output.split("\n").filter((l) => l.trim().length > 0);
|
|
89
|
+
const errorLines = lines.filter((l) => /error TS\d+/i.test(l));
|
|
90
|
+
const errorCount = errorLines.length || lines.length;
|
|
91
|
+
const errors = errorLines.slice(0, 10);
|
|
92
|
+
console.log(` TypeScript: ${errorCount} type error(s)`);
|
|
93
|
+
for (const err of errors.slice(0, 3)) {
|
|
94
|
+
console.error(` ${err.trim()}`);
|
|
95
|
+
}
|
|
96
|
+
resolve({ passed: false, errorCount, errors, skipped: false, durationMs });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -72,6 +72,16 @@ function buildVerificationEvidence(input, thresholds = exports.DEFAULT_LH_THRESH
|
|
|
72
72
|
const mobileLighthousePassed = hasMobileLighthouseData
|
|
73
73
|
? getLighthousePass(input.mobileLighthouseScores, thresholds)
|
|
74
74
|
: false;
|
|
75
|
+
const hasTypecheckData = !!input.typecheck && !input.typecheck.skipped;
|
|
76
|
+
const typecheckPassed = hasTypecheckData ? input.typecheck.passed : true;
|
|
77
|
+
const hasSecretScanData = !!input.secretScan && !input.secretScan.skipped;
|
|
78
|
+
const secretScanPassed = hasSecretScanData ? input.secretScan.passed : true;
|
|
79
|
+
const hasA11yDeepData = !!input.a11yDeep && !input.a11yDeep.skipped;
|
|
80
|
+
const a11yDeepPassed = hasA11yDeepData ? input.a11yDeep.passed : true;
|
|
81
|
+
const hasSeoDeepData = !!input.seoDeep && !input.seoDeep.skipped;
|
|
82
|
+
const seoDeepPassed = hasSeoDeepData ? input.seoDeep.passed : true;
|
|
83
|
+
const hasVitalsBudgetData = !!input.vitalsBudget && !input.vitalsBudget.skipped;
|
|
84
|
+
const vitalsBudgetPassed = hasVitalsBudgetData ? input.vitalsBudget.passed : true;
|
|
75
85
|
return {
|
|
76
86
|
input,
|
|
77
87
|
thresholds,
|
|
@@ -92,6 +102,16 @@ function buildVerificationEvidence(input, thresholds = exports.DEFAULT_LH_THRESH
|
|
|
92
102
|
securityPassed,
|
|
93
103
|
hasMobileLighthouseData,
|
|
94
104
|
mobileLighthousePassed,
|
|
105
|
+
hasTypecheckData,
|
|
106
|
+
typecheckPassed,
|
|
107
|
+
hasSecretScanData,
|
|
108
|
+
secretScanPassed,
|
|
109
|
+
hasA11yDeepData,
|
|
110
|
+
a11yDeepPassed,
|
|
111
|
+
hasSeoDeepData,
|
|
112
|
+
seoDeepPassed,
|
|
113
|
+
hasVitalsBudgetData,
|
|
114
|
+
vitalsBudgetPassed,
|
|
95
115
|
};
|
|
96
116
|
}
|
|
97
117
|
function getImprovementRecommendations(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
@@ -279,6 +299,82 @@ function getImprovementRecommendations(input, thresholds = exports.DEFAULT_LH_TH
|
|
|
279
299
|
action: "Rerun Lighthouse and inspect the failing run logs before trusting this result in CI.",
|
|
280
300
|
});
|
|
281
301
|
}
|
|
302
|
+
// TypeScript type errors — blocker-capable
|
|
303
|
+
if (input.typecheck && !input.typecheck.skipped && !input.typecheck.passed) {
|
|
304
|
+
findings.push({
|
|
305
|
+
category: "typecheck",
|
|
306
|
+
severity: input.typecheck.errorCount >= 5 ? "high" : "medium",
|
|
307
|
+
title: `TypeScript type errors (${input.typecheck.errorCount})`,
|
|
308
|
+
description: "Type errors indicate potential runtime bugs that the build pipeline may not catch.",
|
|
309
|
+
action: "Fix the TypeScript errors and rerun verification.",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// Secret scan — blocker-capable
|
|
313
|
+
if (input.secretScan && !input.secretScan.skipped && !input.secretScan.passed) {
|
|
314
|
+
findings.push({
|
|
315
|
+
category: "secrets",
|
|
316
|
+
severity: "high",
|
|
317
|
+
title: `Hardcoded secrets detected (${input.secretScan.findingsCount})`,
|
|
318
|
+
description: `Found ${input.secretScan.findingsCount} potential secret(s) in ${input.secretScan.filesScanned} scanned files. Leaked secrets are a deployment risk.`,
|
|
319
|
+
action: "Move secrets to environment variables and rotate any compromised credentials.",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// Deep accessibility — blocker-capable
|
|
323
|
+
if (input.a11yDeep && !input.a11yDeep.skipped && !input.a11yDeep.passed) {
|
|
324
|
+
const critical = input.a11yDeep.criticalCount;
|
|
325
|
+
const serious = input.a11yDeep.seriousCount;
|
|
326
|
+
findings.push({
|
|
327
|
+
category: "accessibility",
|
|
328
|
+
severity: critical > 0 ? "high" : "medium",
|
|
329
|
+
title: `WCAG violations found (${critical} critical, ${serious} serious)`,
|
|
330
|
+
description: input.a11yDeep.summary,
|
|
331
|
+
action: "Fix the critical and serious WCAG violations before release.",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// Deep SEO — advisory
|
|
335
|
+
if (input.seoDeep && !input.seoDeep.skipped && !input.seoDeep.passed) {
|
|
336
|
+
findings.push({
|
|
337
|
+
category: "seo",
|
|
338
|
+
severity: "medium",
|
|
339
|
+
title: `SEO issues detected (${input.seoDeep.errorCount} errors, ${input.seoDeep.warningCount} warnings)`,
|
|
340
|
+
description: input.seoDeep.summary,
|
|
341
|
+
action: "Fix missing or incorrect SEO meta tags.",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Vitals budget — advisory
|
|
345
|
+
if (input.vitalsBudget && !input.vitalsBudget.skipped && !input.vitalsBudget.passed) {
|
|
346
|
+
findings.push({
|
|
347
|
+
category: "vitals",
|
|
348
|
+
severity: "medium",
|
|
349
|
+
title: `Core Web Vitals budget exceeded`,
|
|
350
|
+
description: input.vitalsBudget.summary,
|
|
351
|
+
action: "Optimize performance to meet the Core Web Vitals budget thresholds.",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// Outdated dependencies — advisory
|
|
355
|
+
if (input.outdatedCheck && !input.outdatedCheck.skipped && input.outdatedCheck.majorOutdated > 0) {
|
|
356
|
+
findings.push({
|
|
357
|
+
category: "bestPractices",
|
|
358
|
+
severity: "medium",
|
|
359
|
+
title: `${input.outdatedCheck.majorOutdated} major version(s) behind latest`,
|
|
360
|
+
description: input.outdatedCheck.advisory,
|
|
361
|
+
action: "Update outdated dependencies, especially those with major version gaps.",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// Bundle size — advisory (only if thresholds exceeded)
|
|
365
|
+
if (input.bundleSize && !input.bundleSize.skipped) {
|
|
366
|
+
const firstLoad = input.bundleSize.firstLoadJsKb;
|
|
367
|
+
const largest = input.bundleSize.largestChunkKb;
|
|
368
|
+
if ((firstLoad !== null && firstLoad > 200) || (largest !== null && largest > 300)) {
|
|
369
|
+
findings.push({
|
|
370
|
+
category: "performance",
|
|
371
|
+
severity: "medium",
|
|
372
|
+
title: `Bundle size advisory threshold exceeded`,
|
|
373
|
+
description: input.bundleSize.advisory,
|
|
374
|
+
action: "Reduce bundle size by code-splitting, tree-shaking, or lazy-loading large dependencies.",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
282
378
|
const lighthouseScores = input.lighthouseScores;
|
|
283
379
|
if (!lighthouseScores) {
|
|
284
380
|
return findings;
|
|
@@ -388,6 +484,27 @@ function buildVerificationReport(input, options) {
|
|
|
388
484
|
...(evidence.hasLighthouseData
|
|
389
485
|
? [{ key: "lighthouse", label: "Lighthouse thresholds", passed: evidence.lighthousePassed }]
|
|
390
486
|
: []),
|
|
487
|
+
...(evidence.hasTypecheckData
|
|
488
|
+
? [{ key: "typecheck", label: `TypeScript (${input.typecheck?.errorCount ?? 0} errors)`, passed: evidence.typecheckPassed }]
|
|
489
|
+
: []),
|
|
490
|
+
...(evidence.hasSecretScanData
|
|
491
|
+
? [{ key: "secret-scan", label: `Secret scan (${input.secretScan?.findingsCount ?? 0} findings)`, passed: evidence.secretScanPassed }]
|
|
492
|
+
: []),
|
|
493
|
+
...(evidence.hasA11yDeepData
|
|
494
|
+
? [{ key: "a11y-deep", label: `WCAG deep (${input.a11yDeep?.criticalCount ?? 0} critical)`, passed: evidence.a11yDeepPassed }]
|
|
495
|
+
: []),
|
|
496
|
+
...(evidence.hasSeoDeepData
|
|
497
|
+
? [{ key: "seo-deep", label: `SEO deep (${input.seoDeep?.errorCount ?? 0} errors)`, passed: evidence.seoDeepPassed }]
|
|
498
|
+
: []),
|
|
499
|
+
...(evidence.hasVitalsBudgetData
|
|
500
|
+
? [{ key: "vitals-budget", label: "Core Web Vitals budget", passed: evidence.vitalsBudgetPassed }]
|
|
501
|
+
: []),
|
|
502
|
+
...(input.bundleSize && !input.bundleSize.skipped
|
|
503
|
+
? [{ key: "bundle-size", label: `Bundle size (${input.bundleSize.framework})`, passed: true }]
|
|
504
|
+
: []),
|
|
505
|
+
...(input.outdatedCheck && !input.outdatedCheck.skipped
|
|
506
|
+
? [{ key: "outdated-check", label: `Outdated deps (${input.outdatedCheck.outdatedCount} outdated)`, passed: input.outdatedCheck.majorOutdated === 0 }]
|
|
507
|
+
: []),
|
|
391
508
|
];
|
|
392
509
|
const nextActions = [...blockers, ...warnings].slice(0, 4).map((finding) => finding.action);
|
|
393
510
|
return {
|
|
@@ -43,14 +43,60 @@ export interface VerificationInput {
|
|
|
43
43
|
brokenCount: number;
|
|
44
44
|
summary: string;
|
|
45
45
|
};
|
|
46
|
+
typecheck?: {
|
|
47
|
+
passed: boolean;
|
|
48
|
+
errorCount: number;
|
|
49
|
+
skipped: boolean;
|
|
50
|
+
};
|
|
51
|
+
secretScan?: {
|
|
52
|
+
passed: boolean;
|
|
53
|
+
findingsCount: number;
|
|
54
|
+
filesScanned: number;
|
|
55
|
+
skipped: boolean;
|
|
56
|
+
};
|
|
57
|
+
a11yDeep?: {
|
|
58
|
+
passed: boolean;
|
|
59
|
+
criticalCount: number;
|
|
60
|
+
seriousCount: number;
|
|
61
|
+
summary: string;
|
|
62
|
+
skipped: boolean;
|
|
63
|
+
};
|
|
64
|
+
seoDeep?: {
|
|
65
|
+
passed: boolean;
|
|
66
|
+
errorCount: number;
|
|
67
|
+
warningCount: number;
|
|
68
|
+
summary: string;
|
|
69
|
+
skipped: boolean;
|
|
70
|
+
};
|
|
71
|
+
vitalsBudget?: {
|
|
72
|
+
passed: boolean;
|
|
73
|
+
lcp: number | null;
|
|
74
|
+
cls: number | null;
|
|
75
|
+
inp: number | null;
|
|
76
|
+
summary: string;
|
|
77
|
+
skipped: boolean;
|
|
78
|
+
};
|
|
79
|
+
bundleSize?: {
|
|
80
|
+
framework: string;
|
|
81
|
+
firstLoadJsKb: number | null;
|
|
82
|
+
largestChunkKb: number | null;
|
|
83
|
+
advisory: string;
|
|
84
|
+
skipped: boolean;
|
|
85
|
+
};
|
|
86
|
+
outdatedCheck?: {
|
|
87
|
+
outdatedCount: number;
|
|
88
|
+
majorOutdated: number;
|
|
89
|
+
advisory: string;
|
|
90
|
+
skipped: boolean;
|
|
91
|
+
};
|
|
46
92
|
}
|
|
47
93
|
export interface VerificationCheck {
|
|
48
|
-
key: "build" | "e2e" | "lighthouse" | "viewport" | "visual" | "security" | "mobile-lh" | "console-errors" | "broken-links";
|
|
94
|
+
key: "build" | "e2e" | "lighthouse" | "viewport" | "visual" | "security" | "mobile-lh" | "console-errors" | "broken-links" | "typecheck" | "secret-scan" | "a11y-deep" | "seo-deep" | "vitals-budget" | "bundle-size" | "outdated-check";
|
|
49
95
|
label: string;
|
|
50
96
|
passed: boolean;
|
|
51
97
|
}
|
|
52
98
|
export interface VerificationFinding {
|
|
53
|
-
category: "build" | "performance" | "accessibility" | "seo" | "bestPractices" | "e2e" | "viewport" | "visual" | "security" | "runtime";
|
|
99
|
+
category: "build" | "performance" | "accessibility" | "seo" | "bestPractices" | "e2e" | "viewport" | "visual" | "security" | "runtime" | "typecheck" | "secrets" | "vitals";
|
|
54
100
|
severity: "critical" | "high" | "medium";
|
|
55
101
|
title: string;
|
|
56
102
|
description: string;
|
|
@@ -76,6 +122,16 @@ export interface VerificationEvidence {
|
|
|
76
122
|
securityPassed: boolean;
|
|
77
123
|
hasMobileLighthouseData: boolean;
|
|
78
124
|
mobileLighthousePassed: boolean;
|
|
125
|
+
hasTypecheckData: boolean;
|
|
126
|
+
typecheckPassed: boolean;
|
|
127
|
+
hasSecretScanData: boolean;
|
|
128
|
+
secretScanPassed: boolean;
|
|
129
|
+
hasA11yDeepData: boolean;
|
|
130
|
+
a11yDeepPassed: boolean;
|
|
131
|
+
hasSeoDeepData: boolean;
|
|
132
|
+
seoDeepPassed: boolean;
|
|
133
|
+
hasVitalsBudgetData: boolean;
|
|
134
|
+
vitalsBudgetPassed: boolean;
|
|
79
135
|
}
|
|
80
136
|
export interface VerificationReport {
|
|
81
137
|
tier: VerificationTier;
|
package/dist/visual-diff.d.ts
CHANGED
|
@@ -22,5 +22,12 @@ export interface VisualDiffResult {
|
|
|
22
22
|
viewports: VisualDiffViewportResult[];
|
|
23
23
|
summary: string;
|
|
24
24
|
}
|
|
25
|
+
export interface VisualDiffOptions {
|
|
26
|
+
pixelmatchThreshold?: number;
|
|
27
|
+
warnThreshold?: number;
|
|
28
|
+
rollbackThreshold?: number;
|
|
29
|
+
ignoreSelectors?: string[];
|
|
30
|
+
disableAnimations?: boolean;
|
|
31
|
+
}
|
|
25
32
|
export declare function formatVisualDiffSummary(result: VisualDiffResult): string;
|
|
26
|
-
export declare function runVisualDiff(projectDir: string, url: string, label?: string): Promise<VisualDiffResult>;
|
|
33
|
+
export declare function runVisualDiff(projectDir: string, url: string, label?: string, options?: VisualDiffOptions): Promise<VisualDiffResult>;
|