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.
@@ -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, abortSignal) {
10
- const origin = new URL(baseUrl).origin;
11
- const allUrls = [];
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
- const uniqueUrls = allUrls;
25
- const brokenLinks = [];
26
- await Promise.all(uniqueUrls.map(async (url) => {
27
- if (abortSignal?.aborted)
28
- return;
29
- try {
30
- const controller = new AbortController();
31
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
32
- let status = 0;
33
- let statusText = "";
34
- try {
35
- const res = await fetch(url, {
36
- method: "HEAD",
37
- redirect: "follow",
38
- signal: controller.signal,
39
- });
40
- status = res.status;
41
- statusText = res.statusText;
42
- }
43
- catch {
44
- // Fall back to GET if HEAD is not allowed
45
- const controller2 = new AbortController();
46
- const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
47
- try {
48
- const res = await fetch(url, {
49
- method: "GET",
50
- redirect: "follow",
51
- signal: controller2.signal,
52
- });
53
- status = res.status;
54
- statusText = res.statusText;
55
- }
56
- catch {
57
- status = 0;
58
- statusText = "timeout or network error";
59
- }
60
- clearTimeout(timer2);
61
- }
62
- clearTimeout(timer);
63
- if (!isSuccessStatus(status)) {
64
- const severity = status >= 500 ? "critical" : "high";
65
- let path = url;
66
- try {
67
- path = new URL(url).pathname;
68
- }
69
- catch { }
70
- brokenLinks.push({ url, path, status, statusText, severity });
71
- }
72
- }
73
- catch {
74
- // skip
75
- }
76
- }));
77
- const summary = brokenLinks.length === 0
78
- ? `All ${uniqueUrls.length} links OK`
79
- : `${brokenLinks.length} broken link(s) found`;
80
- return {
81
- brokenLinks,
82
- checkedCount: uniqueUrls.length,
83
- hasBrokenLinks: brokenLinks.length > 0,
84
- summary,
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
- function generateBadge(grade) {
5
- const gradeLower = grade.toLowerCase();
6
- const colors = {
7
- gold: "yellow",
8
- silver: "brightgreen",
9
- bronze: "blue",
10
- unverified: "lightgrey",
11
- };
12
- const color = colors[gradeLower] ?? "lightgrey";
13
- return `![Laxy Verify: ${grade}](https://img.shields.io/badge/laxy_verify-${gradeLower}-${color})`;
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 `![Laxy Verify: ${grade}](https://img.shields.io/badge/laxy_verify-${gradeLower}-${color})`;
15
+ }
16
+ function generateDynamicBadgeMarkdown(repoId, apiUrl) {
17
+ return `[![Laxy Verify](${apiUrl}/api/badge/${repoId})](${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
+ }