mcp-web-validator 1.0.0 → 1.2.1
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/CHANGELOG.md +91 -0
- package/README.md +149 -83
- package/SECURITY.md +35 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +444 -668
- package/dist/index.js.map +1 -1
- package/dist/network.d.ts +37 -0
- package/dist/network.js +288 -0
- package/dist/network.js.map +1 -0
- package/dist/presentation.d.ts +15 -0
- package/dist/presentation.js +347 -0
- package/dist/presentation.js.map +1 -0
- package/dist/report.d.ts +43 -0
- package/dist/report.js +143 -0
- package/dist/report.js.map +1 -0
- package/dist/screenshot.d.ts +1 -0
- package/dist/screenshot.js +150 -14
- package/dist/screenshot.js.map +1 -1
- package/dist/seo-auditor.d.ts +11 -2
- package/dist/seo-auditor.js +176 -66
- package/dist/seo-auditor.js.map +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/w3c-validator.d.ts +2 -0
- package/dist/w3c-validator.js +76 -17
- package/dist/w3c-validator.js.map +1 -1
- package/package.json +18 -7
package/dist/screenshot.js
CHANGED
|
@@ -1,11 +1,113 @@
|
|
|
1
1
|
import puppeteer from "puppeteer";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { assertPublicHttpUrl, getErrorMessage } from "./network.js";
|
|
4
6
|
const DEFAULT_VIEWPORTS = [
|
|
5
7
|
{ name: "desktop", width: 1440, height: 900 },
|
|
6
8
|
{ name: "tablet", width: 768, height: 1024 },
|
|
7
9
|
{ name: "mobile", width: 375, height: 812 }
|
|
8
10
|
];
|
|
11
|
+
const MAX_VIEWPORTS = 10;
|
|
12
|
+
const MIN_VIEWPORT_DIMENSION = 100;
|
|
13
|
+
const MAX_VIEWPORT_DIMENSION = 7_680;
|
|
14
|
+
const SAFE_VIEWPORT_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
15
|
+
const NAVIGATION_TIMEOUT_MS = 30_000;
|
|
16
|
+
export const SCREENSHOT_HEADLESS_MODE = "shell";
|
|
17
|
+
function validateViewports(viewports) {
|
|
18
|
+
if (viewports.length === 0 || viewports.length > MAX_VIEWPORTS) {
|
|
19
|
+
throw new Error(`Between 1 and ${MAX_VIEWPORTS} viewports are required`);
|
|
20
|
+
}
|
|
21
|
+
const outputNames = new Set();
|
|
22
|
+
return viewports.map((viewport, index) => {
|
|
23
|
+
if (!viewport || typeof viewport !== "object") {
|
|
24
|
+
throw new Error(`Viewport #${index + 1} must be an object`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof viewport.name !== "string" || !SAFE_VIEWPORT_NAME.test(viewport.name)) {
|
|
27
|
+
throw new Error(`Viewport #${index + 1} name must match ${SAFE_VIEWPORT_NAME.source}`);
|
|
28
|
+
}
|
|
29
|
+
for (const [label, value] of [["width", viewport.width], ["height", viewport.height]]) {
|
|
30
|
+
if (!Number.isSafeInteger(value)
|
|
31
|
+
|| value < MIN_VIEWPORT_DIMENSION
|
|
32
|
+
|| value > MAX_VIEWPORT_DIMENSION) {
|
|
33
|
+
throw new Error(`Viewport #${index + 1} ${label} must be an integer between ${MIN_VIEWPORT_DIMENSION} and ${MAX_VIEWPORT_DIMENSION}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const outputName = `${viewport.name}_${viewport.width}x${viewport.height}.png`.toLowerCase();
|
|
37
|
+
if (outputNames.has(outputName)) {
|
|
38
|
+
throw new Error(`Duplicate viewport output filename: ${outputName}`);
|
|
39
|
+
}
|
|
40
|
+
outputNames.add(outputName);
|
|
41
|
+
return { name: viewport.name, width: viewport.width, height: viewport.height };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function resolveContainedOutputPath(outputDirectory, fileName) {
|
|
45
|
+
const outputPath = path.resolve(outputDirectory, fileName);
|
|
46
|
+
const relativePath = path.relative(outputDirectory, outputPath);
|
|
47
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
48
|
+
throw new Error("Screenshot output path escapes the requested output directory");
|
|
49
|
+
}
|
|
50
|
+
return outputPath;
|
|
51
|
+
}
|
|
52
|
+
function isPathWithinDirectory(directory, filePath) {
|
|
53
|
+
const relativePath = path.relative(directory, filePath);
|
|
54
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
55
|
+
}
|
|
56
|
+
async function assertLocalScreenshotRequestAllowed(requestUrl, localDirectory) {
|
|
57
|
+
if (requestUrl.hostname) {
|
|
58
|
+
throw new Error("Blocked local file request with a host name");
|
|
59
|
+
}
|
|
60
|
+
let resolvedPath;
|
|
61
|
+
try {
|
|
62
|
+
resolvedPath = await fs.realpath(fileURLToPath(requestUrl));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`Blocked local file request: ${getErrorMessage(error)}`);
|
|
66
|
+
}
|
|
67
|
+
if (!isPathWithinDirectory(localDirectory, resolvedPath)) {
|
|
68
|
+
throw new Error("Blocked local file request outside the selected file directory");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function assertScreenshotRequestAllowed(requestUrl, localDirectory) {
|
|
72
|
+
let parsedUrl;
|
|
73
|
+
try {
|
|
74
|
+
parsedUrl = new URL(requestUrl);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new Error("Blocked invalid page request URL");
|
|
78
|
+
}
|
|
79
|
+
if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
|
|
80
|
+
await assertPublicHttpUrl(parsedUrl);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (parsedUrl.protocol === "data:" || parsedUrl.protocol === "blob:") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (parsedUrl.protocol === "file:" && localDirectory) {
|
|
87
|
+
await assertLocalScreenshotRequestAllowed(parsedUrl, localDirectory);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Blocked unsupported page request protocol ${parsedUrl.protocol}`);
|
|
91
|
+
}
|
|
92
|
+
async function resolveScreenshotRequest(request, localDirectory, onBlocked) {
|
|
93
|
+
if (request.isInterceptResolutionHandled()) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await assertScreenshotRequestAllowed(request.url(), localDirectory);
|
|
98
|
+
if (request.isInterceptResolutionHandled()) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await request.continue();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
onBlocked(getErrorMessage(error));
|
|
105
|
+
if (request.isInterceptResolutionHandled()) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await request.abort("blockedbyclient");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
9
111
|
/**
|
|
10
112
|
* Captures screenshots of a local HTML file or remote URL at different viewport sizes.
|
|
11
113
|
* @param targetPath Local file path or HTTP(S) URL
|
|
@@ -13,23 +115,49 @@ const DEFAULT_VIEWPORTS = [
|
|
|
13
115
|
* @param customViewports Optional custom viewport settings
|
|
14
116
|
*/
|
|
15
117
|
export async function captureScreenshots(targetPath, outputDirectory, customViewports) {
|
|
16
|
-
|
|
118
|
+
if (typeof targetPath !== "string" || targetPath.trim() === "") {
|
|
119
|
+
throw new Error("targetPath must be a non-empty string");
|
|
120
|
+
}
|
|
121
|
+
if (typeof outputDirectory !== "string" || outputDirectory.trim() === "") {
|
|
122
|
+
throw new Error("outputDirectory must be a non-empty string");
|
|
123
|
+
}
|
|
124
|
+
const viewports = validateViewports(customViewports && customViewports.length > 0 ? customViewports : DEFAULT_VIEWPORTS);
|
|
125
|
+
const resolvedOutputDirectory = path.resolve(outputDirectory);
|
|
17
126
|
// Resolve target to file:// URL if it is a local file path
|
|
18
|
-
let targetUrl
|
|
19
|
-
|
|
127
|
+
let targetUrl;
|
|
128
|
+
let localDirectory;
|
|
129
|
+
if (/^https?:\/\//i.test(targetPath)) {
|
|
130
|
+
targetUrl = (await assertPublicHttpUrl(targetPath)).href;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
20
133
|
const absolutePath = path.resolve(targetPath);
|
|
21
|
-
|
|
134
|
+
const resolvedPath = await fs.realpath(absolutePath);
|
|
135
|
+
const stats = await fs.stat(resolvedPath);
|
|
136
|
+
if (!stats.isFile()) {
|
|
137
|
+
throw new Error(`Screenshot target is not a regular file: ${resolvedPath}`);
|
|
138
|
+
}
|
|
139
|
+
localDirectory = path.dirname(resolvedPath);
|
|
140
|
+
targetUrl = pathToFileURL(resolvedPath).href;
|
|
22
141
|
}
|
|
23
142
|
// Ensure output directory exists
|
|
24
|
-
await fs.mkdir(
|
|
25
|
-
// Launch
|
|
143
|
+
await fs.mkdir(resolvedOutputDirectory, { recursive: true });
|
|
144
|
+
// Launch Puppeteer's bundled headless shell.
|
|
26
145
|
const browser = await puppeteer.launch({
|
|
27
|
-
headless:
|
|
28
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
146
|
+
headless: SCREENSHOT_HEADLESS_MODE,
|
|
29
147
|
});
|
|
30
148
|
const results = [];
|
|
31
149
|
try {
|
|
32
150
|
const page = await browser.newPage();
|
|
151
|
+
page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);
|
|
152
|
+
let blockedRequestMessage;
|
|
153
|
+
await page.setRequestInterception(true);
|
|
154
|
+
page.on("request", (request) => {
|
|
155
|
+
void resolveScreenshotRequest(request, localDirectory, (message) => {
|
|
156
|
+
blockedRequestMessage ??= message;
|
|
157
|
+
}).catch(() => {
|
|
158
|
+
// Puppeteer will surface navigation failures; avoid an unhandled listener rejection.
|
|
159
|
+
});
|
|
160
|
+
});
|
|
33
161
|
for (const vp of viewports) {
|
|
34
162
|
await page.setViewport({
|
|
35
163
|
width: vp.width,
|
|
@@ -37,14 +165,22 @@ export async function captureScreenshots(targetPath, outputDirectory, customView
|
|
|
37
165
|
deviceScaleFactor: 1
|
|
38
166
|
});
|
|
39
167
|
// Load URL (wait until network is idle or DOM loaded)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
168
|
+
try {
|
|
169
|
+
await page.goto(targetUrl, {
|
|
170
|
+
waitUntil: "domcontentloaded",
|
|
171
|
+
timeout: NAVIGATION_TIMEOUT_MS,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (blockedRequestMessage) {
|
|
176
|
+
throw new Error(`Screenshot navigation blocked: ${blockedRequestMessage}`);
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`Screenshot navigation failed: ${getErrorMessage(error)}`);
|
|
179
|
+
}
|
|
44
180
|
// Short wait for any animations or layout adjustments to settle
|
|
45
181
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
46
182
|
const fileName = `${vp.name}_${vp.width}x${vp.height}.png`;
|
|
47
|
-
const outputPath =
|
|
183
|
+
const outputPath = resolveContainedOutputPath(resolvedOutputDirectory, fileName);
|
|
48
184
|
await page.screenshot({
|
|
49
185
|
path: outputPath,
|
|
50
186
|
fullPage: false // Captures above the fold viewport
|
|
@@ -53,7 +189,7 @@ export async function captureScreenshots(targetPath, outputDirectory, customView
|
|
|
53
189
|
viewportName: vp.name,
|
|
54
190
|
width: vp.width,
|
|
55
191
|
height: vp.height,
|
|
56
|
-
outputPath
|
|
192
|
+
outputPath
|
|
57
193
|
});
|
|
58
194
|
}
|
|
59
195
|
}
|
package/dist/screenshot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../src/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAelC,MAAM,iBAAiB,GAAqB;IAC1C,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;IAC7C,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAC5C,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;CAC5C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,UAAkB,EAClB,eAAuB,EACvB,eAAkC;IAElC,MAAM,SAAS,GAAG,eAAe,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CAAC;IAEtG,2DAA2D;IAC3D,IAAI,SAAS,GAAG,UAAU,CAAC;IAC3B,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5E,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAC9C,SAAS,GAAG,UAAU,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;IAC3D,CAAC;IAED,iCAAiC;IACjC,MAAM,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAErD,kCAAkC;IAClC,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC;QACrC,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,CAAC,cAAc,EAAE,0BAA0B,CAAC;KACnD,CAAC,CAAC;IAEH,MAAM,OAAO,GAAuB,EAAE,CAAC;IAEvC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAErC,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,WAAW,CAAC;gBACrB,KAAK,EAAE,EAAE,CAAC,KAAK;gBACf,MAAM,EAAE,EAAE,CAAC,MAAM;gBACjB,iBAAiB,EAAE,CAAC;aACrB,CAAC,CAAC;YAEH,sDAAsD;YACtD,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;gBACzB,SAAS,EAAE,kBAAkB;gBAC7B,OAAO,EAAE,KAAK;aACf,CAAC,CAAC;YAEH,gEAAgE;YAChE,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;YAEzD,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,MAAM,CAAC;YAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;YAExD,MAAM,IAAI,CAAC,UAAU,CAAC;gBACpB,IAAI,EAAE,UAAU;gBAChB,QAAQ,EAAE,KAAK,CAAC,mCAAmC;aACpD,CAAC,CAAC;YAEH,OAAO,CAAC,IAAI,CAAC;gBACX,YAAY,EAAE,EAAE,CAAC,IAAI;gBACrB,KAAK,EAAE,EAAE,CAAC,KAAK;gBACf,MAAM,EAAE,EAAE,CAAC,MAAM;gBACjB,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;aACrC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
1
|
+
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../src/screenshot.ts"],"names":[],"mappings":"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","sourcesContent":["import puppeteer from \"puppeteer\";\nimport * as path from \"path\";\nimport * as fs from \"fs/promises\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { assertPublicHttpUrl, getErrorMessage } from \"./network.js\";\n\nexport interface ViewportConfig {\n name: string;\n width: number;\n height: number;\n}\n\nexport interface ScreenshotResult {\n viewportName: string;\n width: number;\n height: number;\n outputPath: string;\n}\n\nconst DEFAULT_VIEWPORTS: ViewportConfig[] = [\n { name: \"desktop\", width: 1440, height: 900 },\n { name: \"tablet\", width: 768, height: 1024 },\n { name: \"mobile\", width: 375, height: 812 }\n];\n\nconst MAX_VIEWPORTS = 10;\nconst MIN_VIEWPORT_DIMENSION = 100;\nconst MAX_VIEWPORT_DIMENSION = 7_680;\nconst SAFE_VIEWPORT_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\nconst NAVIGATION_TIMEOUT_MS = 30_000;\n\nexport const SCREENSHOT_HEADLESS_MODE = \"shell\" as const;\n\ninterface InterceptedRequest {\n url(): string;\n isInterceptResolutionHandled(): boolean;\n continue(): Promise<unknown>;\n abort(errorCode?: string): Promise<unknown>;\n}\n\nfunction validateViewports(viewports: ViewportConfig[]): ViewportConfig[] {\n if (viewports.length === 0 || viewports.length > MAX_VIEWPORTS) {\n throw new Error(`Between 1 and ${MAX_VIEWPORTS} viewports are required`);\n }\n\n const outputNames = new Set<string>();\n return viewports.map((viewport, index) => {\n if (!viewport || typeof viewport !== \"object\") {\n throw new Error(`Viewport #${index + 1} must be an object`);\n }\n if (typeof viewport.name !== \"string\" || !SAFE_VIEWPORT_NAME.test(viewport.name)) {\n throw new Error(\n `Viewport #${index + 1} name must match ${SAFE_VIEWPORT_NAME.source}`,\n );\n }\n for (const [label, value] of [[\"width\", viewport.width], [\"height\", viewport.height]] as const) {\n if (\n !Number.isSafeInteger(value)\n || value < MIN_VIEWPORT_DIMENSION\n || value > MAX_VIEWPORT_DIMENSION\n ) {\n throw new Error(\n `Viewport #${index + 1} ${label} must be an integer between ${MIN_VIEWPORT_DIMENSION} and ${MAX_VIEWPORT_DIMENSION}`,\n );\n }\n }\n\n const outputName = `${viewport.name}_${viewport.width}x${viewport.height}.png`.toLowerCase();\n if (outputNames.has(outputName)) {\n throw new Error(`Duplicate viewport output filename: ${outputName}`);\n }\n outputNames.add(outputName);\n return { name: viewport.name, width: viewport.width, height: viewport.height };\n });\n}\n\nfunction resolveContainedOutputPath(outputDirectory: string, fileName: string): string {\n const outputPath = path.resolve(outputDirectory, fileName);\n const relativePath = path.relative(outputDirectory, outputPath);\n if (relativePath.startsWith(\"..\") || path.isAbsolute(relativePath)) {\n throw new Error(\"Screenshot output path escapes the requested output directory\");\n }\n return outputPath;\n}\n\nfunction isPathWithinDirectory(directory: string, filePath: string): boolean {\n const relativePath = path.relative(directory, filePath);\n return relativePath === \"\" || (!relativePath.startsWith(\"..\") && !path.isAbsolute(relativePath));\n}\n\nasync function assertLocalScreenshotRequestAllowed(requestUrl: URL, localDirectory: string): Promise<void> {\n if (requestUrl.hostname) {\n throw new Error(\"Blocked local file request with a host name\");\n }\n\n let resolvedPath: string;\n try {\n resolvedPath = await fs.realpath(fileURLToPath(requestUrl));\n } catch (error: unknown) {\n throw new Error(`Blocked local file request: ${getErrorMessage(error)}`);\n }\n\n if (!isPathWithinDirectory(localDirectory, resolvedPath)) {\n throw new Error(\"Blocked local file request outside the selected file directory\");\n }\n}\n\nasync function assertScreenshotRequestAllowed(requestUrl: string, localDirectory?: string): Promise<void> {\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(requestUrl);\n } catch {\n throw new Error(\"Blocked invalid page request URL\");\n }\n\n if (parsedUrl.protocol === \"http:\" || parsedUrl.protocol === \"https:\") {\n await assertPublicHttpUrl(parsedUrl);\n return;\n }\n\n if (parsedUrl.protocol === \"data:\" || parsedUrl.protocol === \"blob:\") {\n return;\n }\n\n if (parsedUrl.protocol === \"file:\" && localDirectory) {\n await assertLocalScreenshotRequestAllowed(parsedUrl, localDirectory);\n return;\n }\n\n throw new Error(`Blocked unsupported page request protocol ${parsedUrl.protocol}`);\n}\n\nasync function resolveScreenshotRequest(\n request: InterceptedRequest,\n localDirectory: string | undefined,\n onBlocked: (message: string) => void,\n): Promise<void> {\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n\n try {\n await assertScreenshotRequestAllowed(request.url(), localDirectory);\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n await request.continue();\n } catch (error: unknown) {\n onBlocked(getErrorMessage(error));\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n await request.abort(\"blockedbyclient\");\n }\n}\n\n/**\n * Captures screenshots of a local HTML file or remote URL at different viewport sizes.\n * @param targetPath Local file path or HTTP(S) URL\n * @param outputDirectory Absolute path to save screenshots\n * @param customViewports Optional custom viewport settings\n */\nexport async function captureScreenshots(\n targetPath: string,\n outputDirectory: string,\n customViewports?: ViewportConfig[]\n): Promise<ScreenshotResult[]> {\n if (typeof targetPath !== \"string\" || targetPath.trim() === \"\") {\n throw new Error(\"targetPath must be a non-empty string\");\n }\n if (typeof outputDirectory !== \"string\" || outputDirectory.trim() === \"\") {\n throw new Error(\"outputDirectory must be a non-empty string\");\n }\n\n const viewports = validateViewports(\n customViewports && customViewports.length > 0 ? customViewports : DEFAULT_VIEWPORTS,\n );\n const resolvedOutputDirectory = path.resolve(outputDirectory);\n\n // Resolve target to file:// URL if it is a local file path\n let targetUrl: string;\n let localDirectory: string | undefined;\n if (/^https?:\\/\\//i.test(targetPath)) {\n targetUrl = (await assertPublicHttpUrl(targetPath)).href;\n } else {\n const absolutePath = path.resolve(targetPath);\n const resolvedPath = await fs.realpath(absolutePath);\n const stats = await fs.stat(resolvedPath);\n if (!stats.isFile()) {\n throw new Error(`Screenshot target is not a regular file: ${resolvedPath}`);\n }\n localDirectory = path.dirname(resolvedPath);\n targetUrl = pathToFileURL(resolvedPath).href;\n }\n\n // Ensure output directory exists\n await fs.mkdir(resolvedOutputDirectory, { recursive: true });\n\n // Launch Puppeteer's bundled headless shell.\n const browser = await puppeteer.launch({\n headless: SCREENSHOT_HEADLESS_MODE,\n });\n\n const results: ScreenshotResult[] = [];\n\n try {\n const page = await browser.newPage();\n page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);\n\n let blockedRequestMessage: string | undefined;\n await page.setRequestInterception(true);\n page.on(\"request\", (request) => {\n void resolveScreenshotRequest(request, localDirectory, (message) => {\n blockedRequestMessage ??= message;\n }).catch(() => {\n // Puppeteer will surface navigation failures; avoid an unhandled listener rejection.\n });\n });\n\n for (const vp of viewports) {\n await page.setViewport({\n width: vp.width,\n height: vp.height,\n deviceScaleFactor: 1\n });\n\n // Load URL (wait until network is idle or DOM loaded)\n try {\n await page.goto(targetUrl, {\n waitUntil: \"domcontentloaded\",\n timeout: NAVIGATION_TIMEOUT_MS,\n });\n } catch (error: unknown) {\n if (blockedRequestMessage) {\n throw new Error(`Screenshot navigation blocked: ${blockedRequestMessage}`);\n }\n throw new Error(`Screenshot navigation failed: ${getErrorMessage(error)}`);\n }\n\n // Short wait for any animations or layout adjustments to settle\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n const fileName = `${vp.name}_${vp.width}x${vp.height}.png`;\n const outputPath = resolveContainedOutputPath(resolvedOutputDirectory, fileName);\n\n await page.screenshot({\n path: outputPath,\n fullPage: false // Captures above the fold viewport\n });\n\n results.push({\n viewportName: vp.name,\n width: vp.width,\n height: vp.height,\n outputPath\n });\n }\n } finally {\n await browser.close();\n }\n\n return results;\n}\n"]}
|
package/dist/seo-auditor.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare const MAX_AUDIT_ISSUES = 200;
|
|
2
|
+
export declare const MAX_LINKS_TO_CHECK = 25;
|
|
1
3
|
export interface SEOIssue {
|
|
2
4
|
severity: "error" | "warning" | "info";
|
|
3
5
|
category: "SEO" | "Schema" | "BrokenLinks" | "Accessibility";
|
|
@@ -10,15 +12,22 @@ export interface LinkStatus {
|
|
|
10
12
|
ok: boolean;
|
|
11
13
|
message?: string;
|
|
12
14
|
}
|
|
15
|
+
export interface AuditDetails {
|
|
16
|
+
issues: SEOIssue[];
|
|
17
|
+
totalIssues: number;
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
}
|
|
13
20
|
/**
|
|
14
21
|
* Audits technical SEO and accessibility basics on HTML content using Cheerio
|
|
15
22
|
*/
|
|
23
|
+
export declare function auditSeoMetadataDetailed(htmlContent: string): AuditDetails;
|
|
16
24
|
export declare function auditSeoMetadata(htmlContent: string): SEOIssue[];
|
|
17
25
|
/**
|
|
18
26
|
* Parses and validates JSON-LD Schema markup
|
|
19
27
|
*/
|
|
28
|
+
export declare function validateSchemaMarkupDetailed(htmlContent: string): AuditDetails;
|
|
20
29
|
export declare function validateSchemaMarkup(htmlContent: string): SEOIssue[];
|
|
21
30
|
/**
|
|
22
|
-
* Extracts and tests
|
|
31
|
+
* Extracts and tests links, treating 3xx responses as reachable redirects and 4xx/5xx as broken.
|
|
23
32
|
*/
|
|
24
|
-
export declare function checkBrokenLinks(htmlContent: string, baseUrl?: string): Promise<LinkStatus[]>;
|
|
33
|
+
export declare function checkBrokenLinks(htmlContent: string, baseUrl?: string, maxLinks?: number): Promise<LinkStatus[]>;
|