laxy-verify 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -47
- package/dist/a11y-deep.d.ts +20 -0
- package/dist/a11y-deep.js +161 -0
- package/dist/audit/broken-links.d.ts +25 -21
- package/dist/audit/broken-links.js +97 -86
- package/dist/badge.d.ts +2 -1
- package/dist/badge.js +18 -14
- package/dist/bundle-size.d.ts +14 -0
- package/dist/bundle-size.js +209 -0
- package/dist/cli.js +1256 -865
- package/dist/config.d.ts +102 -65
- package/dist/config.js +360 -255
- package/dist/entitlement.d.ts +15 -11
- package/dist/entitlement.js +98 -90
- package/dist/init.js +153 -87
- package/dist/lighthouse.d.ts +37 -7
- package/dist/lighthouse.js +231 -158
- package/dist/outdated-check.d.ts +17 -0
- package/dist/outdated-check.js +123 -0
- package/dist/report-markdown.d.ts +53 -39
- package/dist/report-markdown.js +407 -386
- package/dist/secret-scan.d.ts +15 -0
- package/dist/secret-scan.js +218 -0
- package/dist/security-audit.d.ts +17 -9
- package/dist/security-audit.js +127 -64
- package/dist/seo-deep.d.ts +24 -0
- package/dist/seo-deep.js +147 -0
- package/dist/typecheck.d.ts +8 -0
- package/dist/typecheck.js +99 -0
- package/dist/verification-core/report.js +526 -409
- package/dist/verification-core/types.d.ts +164 -108
- package/dist/visual-diff.d.ts +33 -26
- package/dist/visual-diff.js +223 -178
- package/dist/vitals-budget.d.ts +23 -0
- package/dist/vitals-budget.js +168 -0
- package/package.json +1 -1
|
@@ -1,86 +1,97 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.auditBrokenLinks = auditBrokenLinks;
|
|
4
|
-
const TIMEOUT_MS = 5000;
|
|
5
|
-
const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
|
|
6
|
-
function isSuccessStatus(n) {
|
|
7
|
-
return VALID_OK_STATUS.includes(n);
|
|
8
|
-
}
|
|
9
|
-
async function auditBrokenLinks(crawlResult, baseUrl,
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditBrokenLinks = auditBrokenLinks;
|
|
4
|
+
const TIMEOUT_MS = 5000;
|
|
5
|
+
const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
|
|
6
|
+
function isSuccessStatus(n) {
|
|
7
|
+
return VALID_OK_STATUS.includes(n);
|
|
8
|
+
}
|
|
9
|
+
async function auditBrokenLinks(crawlResult, baseUrl, options = {}) {
|
|
10
|
+
const allUrls = [];
|
|
11
|
+
if (crawlResult) {
|
|
12
|
+
for (const page of crawlResult.pages) {
|
|
13
|
+
for (const href of page.internalLinks) {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(href, baseUrl).href;
|
|
16
|
+
if (!allUrls.includes(url))
|
|
17
|
+
allUrls.push(url);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// skip malformed URLs
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const route of options.extraRoutes ?? []) {
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(route, baseUrl).href;
|
|
28
|
+
if (!allUrls.includes(url))
|
|
29
|
+
allUrls.push(url);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// skip malformed routes
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const uniqueUrls = allUrls;
|
|
36
|
+
const brokenLinks = [];
|
|
37
|
+
await Promise.all(uniqueUrls.map(async (url) => {
|
|
38
|
+
if (options.abortSignal?.aborted)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
43
|
+
let status = 0;
|
|
44
|
+
let statusText = "";
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: "HEAD",
|
|
48
|
+
redirect: "follow",
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
});
|
|
51
|
+
status = res.status;
|
|
52
|
+
statusText = res.statusText;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Fall back to GET if HEAD is not allowed
|
|
56
|
+
const controller2 = new AbortController();
|
|
57
|
+
const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
redirect: "follow",
|
|
62
|
+
signal: controller2.signal,
|
|
63
|
+
});
|
|
64
|
+
status = res.status;
|
|
65
|
+
statusText = res.statusText;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
status = 0;
|
|
69
|
+
statusText = "timeout or network error";
|
|
70
|
+
}
|
|
71
|
+
clearTimeout(timer2);
|
|
72
|
+
}
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
if (!isSuccessStatus(status)) {
|
|
75
|
+
const severity = status >= 500 ? "critical" : "high";
|
|
76
|
+
let path = url;
|
|
77
|
+
try {
|
|
78
|
+
path = new URL(url).pathname;
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
brokenLinks.push({ url, path, status, statusText, severity });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// skip
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
const summary = brokenLinks.length === 0
|
|
89
|
+
? `All ${uniqueUrls.length} links OK`
|
|
90
|
+
: `${brokenLinks.length} broken link(s) found`;
|
|
91
|
+
return {
|
|
92
|
+
brokenLinks,
|
|
93
|
+
checkedCount: uniqueUrls.length,
|
|
94
|
+
hasBrokenLinks: brokenLinks.length > 0,
|
|
95
|
+
summary,
|
|
96
|
+
};
|
|
97
|
+
}
|
package/dist/badge.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare function generateBadge(grade: string): string;
|
|
1
|
+
export declare function generateBadge(grade: string): string;
|
|
2
|
+
export declare function generateDynamicBadgeMarkdown(repoId: string, apiUrl: string): string;
|
package/dist/badge.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateBadge = generateBadge;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateBadge = generateBadge;
|
|
4
|
+
exports.generateDynamicBadgeMarkdown = generateDynamicBadgeMarkdown;
|
|
5
|
+
function generateBadge(grade) {
|
|
6
|
+
const gradeLower = grade.toLowerCase();
|
|
7
|
+
const colors = {
|
|
8
|
+
gold: "yellow",
|
|
9
|
+
silver: "brightgreen",
|
|
10
|
+
bronze: "blue",
|
|
11
|
+
unverified: "lightgrey",
|
|
12
|
+
};
|
|
13
|
+
const color = colors[gradeLower] ?? "lightgrey";
|
|
14
|
+
return ``;
|
|
15
|
+
}
|
|
16
|
+
function generateDynamicBadgeMarkdown(repoId, apiUrl) {
|
|
17
|
+
return `[](${apiUrl})`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface BundleSizeResult {
|
|
2
|
+
framework: "next" | "vite" | "unknown";
|
|
3
|
+
firstLoadJsKb: number | null;
|
|
4
|
+
largestChunkKb: number | null;
|
|
5
|
+
largestChunkName: string | null;
|
|
6
|
+
totalAssetsKb: number | null;
|
|
7
|
+
advisory: string;
|
|
8
|
+
chunks: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
sizeKb: number;
|
|
11
|
+
}>;
|
|
12
|
+
skipped: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function runBundleSize(projectDir: string): Promise<BundleSizeResult>;
|
|
@@ -0,0 +1,209 @@
|
|
|
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.runBundleSize = runBundleSize;
|
|
37
|
+
/**
|
|
38
|
+
* Bundle size analysis.
|
|
39
|
+
*
|
|
40
|
+
* Parses Next.js and Vite build output to report first-load JS and
|
|
41
|
+
* largest chunk sizes. Advisory-only — does not block deployment.
|
|
42
|
+
*/
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const DEFAULT_FIRST_LOAD_THRESHOLD_KB = 200;
|
|
46
|
+
const DEFAULT_LARGEST_CHUNK_THRESHOLD_KB = 300;
|
|
47
|
+
function parseNextBuildOutput(projectDir) {
|
|
48
|
+
// Next.js stores build output in .next/build-manifest.json and .next/routes-manifest.json
|
|
49
|
+
// The most reliable source is .next/build-manifest.json for pages and their chunks
|
|
50
|
+
const buildManifestPath = path.join(projectDir, ".next", "build-manifest.json");
|
|
51
|
+
if (!fs.existsSync(buildManifestPath))
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
const manifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8"));
|
|
55
|
+
// Collect all chunk files referenced in the manifest
|
|
56
|
+
const allChunks = new Set();
|
|
57
|
+
const pages = manifest.pages ?? {};
|
|
58
|
+
for (const pageChunks of Object.values(pages)) {
|
|
59
|
+
if (Array.isArray(pageChunks)) {
|
|
60
|
+
for (const chunk of pageChunks) {
|
|
61
|
+
allChunks.add(chunk);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Measure chunk sizes from .next/static
|
|
66
|
+
const chunks = [];
|
|
67
|
+
for (const chunkPath of allChunks) {
|
|
68
|
+
const fullPath = path.join(projectDir, ".next", chunkPath);
|
|
69
|
+
if (!fs.existsSync(fullPath))
|
|
70
|
+
continue;
|
|
71
|
+
try {
|
|
72
|
+
const stat = fs.statSync(fullPath);
|
|
73
|
+
const name = path.basename(chunkPath);
|
|
74
|
+
chunks.push({ name, sizeKb: Math.round(stat.size / 1024) });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// skip unreadable files
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (chunks.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
// Sort by size descending
|
|
83
|
+
chunks.sort((a, b) => b.sizeKb - a.sizeKb);
|
|
84
|
+
const largest = chunks[0];
|
|
85
|
+
const totalAssetsKb = chunks.reduce((sum, c) => sum + c.sizeKb, 0);
|
|
86
|
+
// First-load JS: sum of all chunks for the "/" page (usually _app + _buildManifest + page chunk)
|
|
87
|
+
const homeChunks = pages["/"] ?? pages["/index"] ?? [];
|
|
88
|
+
const sharedChunks = pages["/_app"] ?? [];
|
|
89
|
+
const firstLoadFiles = [...homeChunks, ...sharedChunks];
|
|
90
|
+
let firstLoadJsKb = 0;
|
|
91
|
+
for (const chunkPath of firstLoadFiles) {
|
|
92
|
+
const fullPath = path.join(projectDir, ".next", chunkPath);
|
|
93
|
+
try {
|
|
94
|
+
if (fs.existsSync(fullPath)) {
|
|
95
|
+
firstLoadJsKb += Math.round(fs.statSync(fullPath).size / 1024);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// skip
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const advisory = buildAdvisory(firstLoadJsKb, largest.sizeKb, largest.name);
|
|
103
|
+
return {
|
|
104
|
+
framework: "next",
|
|
105
|
+
firstLoadJsKb: firstLoadJsKb || null,
|
|
106
|
+
largestChunkKb: largest.sizeKb,
|
|
107
|
+
largestChunkName: largest.name,
|
|
108
|
+
totalAssetsKb,
|
|
109
|
+
advisory,
|
|
110
|
+
chunks: chunks.slice(0, 10),
|
|
111
|
+
skipped: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function parseViteBuildOutput(projectDir) {
|
|
119
|
+
// Vite outputs to dist/ by default, manifest in .vite/manifest.json or dist/.vite/manifest.json
|
|
120
|
+
const distDir = path.join(projectDir, "dist");
|
|
121
|
+
if (!fs.existsSync(distDir))
|
|
122
|
+
return null;
|
|
123
|
+
const chunks = [];
|
|
124
|
+
function walkDist(dir) {
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const fullPath = path.join(dir, entry.name);
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
walkDist(fullPath);
|
|
136
|
+
}
|
|
137
|
+
else if (entry.isFile() && /\.(js|mjs|css)$/.test(entry.name)) {
|
|
138
|
+
try {
|
|
139
|
+
const stat = fs.statSync(fullPath);
|
|
140
|
+
const relativePath = path.relative(distDir, fullPath).replace(/\\/g, "/");
|
|
141
|
+
chunks.push({ name: relativePath, sizeKb: Math.round(stat.size / 1024) });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// skip
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
walkDist(distDir);
|
|
150
|
+
if (chunks.length === 0)
|
|
151
|
+
return null;
|
|
152
|
+
chunks.sort((a, b) => b.sizeKb - a.sizeKb);
|
|
153
|
+
const largest = chunks[0];
|
|
154
|
+
const totalAssetsKb = chunks.reduce((sum, c) => sum + c.sizeKb, 0);
|
|
155
|
+
// First-load: index.html + referenced JS
|
|
156
|
+
const indexJsChunks = chunks.filter((c) => c.name.startsWith("assets/") && c.name.includes("index"));
|
|
157
|
+
const firstLoadJsKb = indexJsChunks.length > 0
|
|
158
|
+
? indexJsChunks.reduce((sum, c) => sum + c.sizeKb, 0)
|
|
159
|
+
: null;
|
|
160
|
+
const advisory = buildAdvisory(firstLoadJsKb, largest.sizeKb, largest.name);
|
|
161
|
+
return {
|
|
162
|
+
framework: "vite",
|
|
163
|
+
firstLoadJsKb,
|
|
164
|
+
largestChunkKb: largest.sizeKb,
|
|
165
|
+
largestChunkName: largest.name,
|
|
166
|
+
totalAssetsKb,
|
|
167
|
+
advisory,
|
|
168
|
+
chunks: chunks.slice(0, 10),
|
|
169
|
+
skipped: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function buildAdvisory(firstLoadKb, largestKb, largestName) {
|
|
173
|
+
const parts = [];
|
|
174
|
+
if (firstLoadKb !== null && firstLoadKb > DEFAULT_FIRST_LOAD_THRESHOLD_KB) {
|
|
175
|
+
parts.push(`first-load JS ${firstLoadKb}KB exceeds ${DEFAULT_FIRST_LOAD_THRESHOLD_KB}KB advisory threshold`);
|
|
176
|
+
}
|
|
177
|
+
if (largestKb > DEFAULT_LARGEST_CHUNK_THRESHOLD_KB) {
|
|
178
|
+
parts.push(`largest chunk ${largestName} is ${largestKb}KB (>${DEFAULT_LARGEST_CHUNK_THRESHOLD_KB}KB advisory)`);
|
|
179
|
+
}
|
|
180
|
+
if (parts.length === 0) {
|
|
181
|
+
const firstLoadNote = firstLoadKb !== null ? `first-load ${firstLoadKb}KB, ` : "";
|
|
182
|
+
return `${firstLoadNote}largest chunk ${largestName} ${largestKb}KB — within advisory thresholds`;
|
|
183
|
+
}
|
|
184
|
+
return parts.join("; ");
|
|
185
|
+
}
|
|
186
|
+
async function runBundleSize(projectDir) {
|
|
187
|
+
console.log(" Running bundle size analysis...");
|
|
188
|
+
const nextResult = parseNextBuildOutput(projectDir);
|
|
189
|
+
if (nextResult) {
|
|
190
|
+
console.log(` Bundle (Next.js): ${nextResult.advisory}`);
|
|
191
|
+
return nextResult;
|
|
192
|
+
}
|
|
193
|
+
const viteResult = parseViteBuildOutput(projectDir);
|
|
194
|
+
if (viteResult) {
|
|
195
|
+
console.log(` Bundle (Vite): ${viteResult.advisory}`);
|
|
196
|
+
return viteResult;
|
|
197
|
+
}
|
|
198
|
+
console.log(" Bundle size: skipped (no .next or dist directory found)");
|
|
199
|
+
return {
|
|
200
|
+
framework: "unknown",
|
|
201
|
+
firstLoadJsKb: null,
|
|
202
|
+
largestChunkKb: null,
|
|
203
|
+
largestChunkName: null,
|
|
204
|
+
totalAssetsKb: null,
|
|
205
|
+
advisory: "No build output found for bundle analysis",
|
|
206
|
+
chunks: [],
|
|
207
|
+
skipped: true,
|
|
208
|
+
};
|
|
209
|
+
}
|