@intlayer/engine 9.5.10 → 9.5.12
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/dist/assets/installSkills/skills/bundle-optimization.md +123 -0
- package/dist/assets/installSkills/skills/compat.md +0 -4
- package/dist/assets/installSkills/skills/content.md +8 -2
- package/dist/assets/installSkills/skills/next-js.md +2 -2
- package/dist/assets/installSkills/skills/usage.md +1 -1
- package/dist/cjs/build.cjs +5 -0
- package/dist/cjs/cli.cjs +1 -0
- package/dist/cjs/docReview/alignBlocks.cjs +53 -36
- package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
- package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
- package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
- package/dist/cjs/docReview/segmentDocument.cjs +8 -5
- package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +4 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
- package/dist/cjs/init/index.cjs +2 -12
- package/dist/cjs/init/index.cjs.map +1 -1
- package/dist/cjs/init/utils/backendPackages.cjs +15 -0
- package/dist/cjs/init/utils/backendPackages.cjs.map +1 -0
- package/dist/cjs/init/utils/index.cjs +2 -3
- package/dist/cjs/installMCP/installMCP.cjs +1 -1
- package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
- package/dist/cjs/installSkills/index.cjs +2 -0
- package/dist/cjs/installSkills/index.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs +9 -8
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +4 -6
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/log.cjs +1 -1
- package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +19 -12
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs +18 -0
- package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs.map +1 -0
- package/dist/cjs/prepareIntlayerServer.cjs +132 -0
- package/dist/cjs/prepareIntlayerServer.cjs.map +1 -0
- package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
- package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
- package/dist/cjs/scan/calculateScore.cjs +4 -1
- package/dist/cjs/scan/calculateScore.cjs.map +1 -1
- package/dist/cjs/scan/checks.cjs +394 -221
- package/dist/cjs/scan/checks.cjs.map +1 -1
- package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
- package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
- package/dist/cjs/scan/detection/index.cjs +36 -0
- package/dist/cjs/scan/detection/localeCode.cjs +118 -0
- package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
- package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
- package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
- package/dist/cjs/scan/detection/url.cjs +53 -0
- package/dist/cjs/scan/detection/url.cjs.map +1 -0
- package/dist/cjs/scan/fetchText.cjs +56 -0
- package/dist/cjs/scan/fetchText.cjs.map +1 -0
- package/dist/cjs/scan/index.cjs +59 -3
- package/dist/cjs/scan/parseHtml.cjs +55 -10
- package/dist/cjs/scan/parseHtml.cjs.map +1 -1
- package/dist/cjs/scan/robots.cjs +65 -0
- package/dist/cjs/scan/robots.cjs.map +1 -0
- package/dist/cjs/scan/runScanChecks.cjs +89 -0
- package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
- package/dist/cjs/scan/scanWebsite.cjs +58 -52
- package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
- package/dist/cjs/scan/sitemap.cjs +109 -0
- package/dist/cjs/scan/sitemap.cjs.map +1 -0
- package/dist/cjs/utils/chunkJSON.cjs +24 -9
- package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
- package/dist/cjs/utils/getChunk.cjs +2 -2
- package/dist/cjs/utils/getChunk.cjs.map +1 -1
- package/dist/cjs/utils/getProcessChainCommands.cjs +117 -0
- package/dist/cjs/utils/getProcessChainCommands.cjs.map +1 -0
- package/dist/cjs/utils/index.cjs +2 -0
- package/dist/cjs/utils/runParallel/index.cjs +1 -1
- package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
- package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/ps.cjs +8 -3
- package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
- package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
- package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
- package/dist/cjs/utils/startContentWatcher.cjs +99 -0
- package/dist/cjs/utils/startContentWatcher.cjs.map +1 -0
- package/dist/cjs/watcher.cjs +2 -2
- package/dist/cjs/watcher.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
- package/dist/cjs/writeJsonIfChanged.cjs.map +1 -1
- package/dist/esm/build.mjs +2 -1
- package/dist/esm/cli.mjs +2 -2
- package/dist/esm/docReview/alignBlocks.mjs +53 -36
- package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
- package/dist/esm/docReview/rebuildDocument.mjs +3 -1
- package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
- package/dist/esm/docReview/segmentDocument.mjs +8 -5
- package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +4 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
- package/dist/esm/init/index.mjs +1 -11
- package/dist/esm/init/index.mjs.map +1 -1
- package/dist/esm/init/utils/backendPackages.mjs +14 -0
- package/dist/esm/init/utils/backendPackages.mjs.map +1 -0
- package/dist/esm/init/utils/index.mjs +2 -2
- package/dist/esm/installMCP/installMCP.mjs +1 -1
- package/dist/esm/installMCP/installMCP.mjs.map +1 -1
- package/dist/esm/installSkills/index.mjs +2 -0
- package/dist/esm/installSkills/index.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadDictionaries.mjs +9 -8
- package/dist/esm/loadDictionaries/loadDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +3 -5
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/log.mjs +1 -1
- package/dist/esm/loadDictionaries/log.mjs.map +1 -1
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +19 -12
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
- package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs +16 -0
- package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs.map +1 -0
- package/dist/esm/prepareIntlayerServer.mjs +128 -0
- package/dist/esm/prepareIntlayerServer.mjs.map +1 -0
- package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
- package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
- package/dist/esm/scan/calculateScore.mjs +4 -1
- package/dist/esm/scan/calculateScore.mjs.map +1 -1
- package/dist/esm/scan/checks.mjs +387 -220
- package/dist/esm/scan/checks.mjs.map +1 -1
- package/dist/esm/scan/detection/checkDetails.mjs +54 -0
- package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
- package/dist/esm/scan/detection/index.mjs +10 -0
- package/dist/esm/scan/detection/localeCode.mjs +108 -0
- package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
- package/dist/esm/scan/detection/localizedPages.mjs +46 -0
- package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
- package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
- package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
- package/dist/esm/scan/detection/url.mjs +47 -0
- package/dist/esm/scan/detection/url.mjs.map +1 -0
- package/dist/esm/scan/fetchText.mjs +55 -0
- package/dist/esm/scan/fetchText.mjs.map +1 -0
- package/dist/esm/scan/index.mjs +15 -3
- package/dist/esm/scan/parseHtml.mjs +52 -11
- package/dist/esm/scan/parseHtml.mjs.map +1 -1
- package/dist/esm/scan/robots.mjs +63 -0
- package/dist/esm/scan/robots.mjs.map +1 -0
- package/dist/esm/scan/runScanChecks.mjs +88 -0
- package/dist/esm/scan/runScanChecks.mjs.map +1 -0
- package/dist/esm/scan/scanWebsite.mjs +59 -53
- package/dist/esm/scan/scanWebsite.mjs.map +1 -1
- package/dist/esm/scan/sitemap.mjs +105 -0
- package/dist/esm/scan/sitemap.mjs.map +1 -0
- package/dist/esm/utils/chunkJSON.mjs +24 -9
- package/dist/esm/utils/chunkJSON.mjs.map +1 -1
- package/dist/esm/utils/getChunk.mjs +2 -2
- package/dist/esm/utils/getChunk.mjs.map +1 -1
- package/dist/esm/utils/getProcessChainCommands.mjs +115 -0
- package/dist/esm/utils/getProcessChainCommands.mjs.map +1 -0
- package/dist/esm/utils/index.mjs +2 -1
- package/dist/esm/utils/runParallel/index.mjs +1 -1
- package/dist/esm/utils/runParallel/index.mjs.map +1 -1
- package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
- package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
- package/dist/esm/utils/runParallel/ps.mjs +8 -3
- package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
- package/dist/esm/utils/runParallel/runTask.mjs +2 -1
- package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
- package/dist/esm/utils/runParallel/wmic.mjs +8 -3
- package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
- package/dist/esm/utils/startContentWatcher.mjs +98 -0
- package/dist/esm/utils/startContentWatcher.mjs.map +1 -0
- package/dist/esm/watcher.mjs +1 -1
- package/dist/esm/watcher.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
- package/dist/esm/writeJsonIfChanged.mjs +1 -1
- package/dist/esm/writeJsonIfChanged.mjs.map +1 -1
- package/dist/types/build.d.ts +2 -1
- package/dist/types/cli.d.ts +2 -2
- package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
- package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
- package/dist/types/init/index.d.ts +1 -1
- package/dist/types/init/index.d.ts.map +1 -1
- package/dist/types/init/utils/backendPackages.d.ts +5 -0
- package/dist/types/init/utils/backendPackages.d.ts.map +1 -0
- package/dist/types/init/utils/index.d.ts +2 -2
- package/dist/types/installSkills/index.d.ts +1 -0
- package/dist/types/installSkills/index.d.ts.map +1 -1
- package/dist/types/loadDictionaries/loadRemoteDictionaries.d.ts.map +1 -1
- package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
- package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts +14 -0
- package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts.map +1 -0
- package/dist/types/prepareIntlayerServer.d.ts +67 -0
- package/dist/types/prepareIntlayerServer.d.ts.map +1 -0
- package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
- package/dist/types/scan/calculateScore.d.ts +4 -1
- package/dist/types/scan/calculateScore.d.ts.map +1 -1
- package/dist/types/scan/checks.d.ts +114 -21
- package/dist/types/scan/checks.d.ts.map +1 -1
- package/dist/types/scan/detection/checkDetails.d.ts +29 -0
- package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
- package/dist/types/scan/detection/index.d.ts +9 -0
- package/dist/types/scan/detection/localeCode.d.ts +49 -0
- package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
- package/dist/types/scan/detection/localizedPages.d.ts +40 -0
- package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
- package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
- package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
- package/dist/types/scan/detection/url.d.ts +27 -0
- package/dist/types/scan/detection/url.d.ts.map +1 -0
- package/dist/types/scan/fetchText.d.ts +30 -0
- package/dist/types/scan/fetchText.d.ts.map +1 -0
- package/dist/types/scan/index.d.ts +16 -4
- package/dist/types/scan/parseHtml.d.ts +17 -0
- package/dist/types/scan/parseHtml.d.ts.map +1 -1
- package/dist/types/scan/robots.d.ts +23 -0
- package/dist/types/scan/robots.d.ts.map +1 -0
- package/dist/types/scan/runScanChecks.d.ts +43 -0
- package/dist/types/scan/runScanChecks.d.ts.map +1 -0
- package/dist/types/scan/scanWebsite.d.ts.map +1 -1
- package/dist/types/scan/sitemap.d.ts +49 -0
- package/dist/types/scan/sitemap.d.ts.map +1 -0
- package/dist/types/scan/types.d.ts +15 -0
- package/dist/types/scan/types.d.ts.map +1 -1
- package/dist/types/utils/chunkJSON.d.ts.map +1 -1
- package/dist/types/utils/getProcessChainCommands.d.ts +35 -0
- package/dist/types/utils/getProcessChainCommands.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +2 -1
- package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
- package/dist/types/utils/startContentWatcher.d.ts +40 -0
- package/dist/types/utils/startContentWatcher.d.ts.map +1 -0
- package/dist/types/watcher.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
- package/package.json +15 -7
- package/dist/cjs/init/utils/devScript.cjs +0 -57
- package/dist/cjs/init/utils/devScript.cjs.map +0 -1
- package/dist/esm/init/utils/devScript.mjs +0 -55
- package/dist/esm/init/utils/devScript.mjs.map +0 -1
- package/dist/types/init/utils/devScript.d.ts +0 -48
- package/dist/types/init/utils/devScript.d.ts.map +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/scan/fetchText.ts
|
|
2
|
+
const MAX_REDIRECTS = 5;
|
|
3
|
+
/**
|
|
4
|
+
* Read a response body as text, gunzipping it when the payload itself is
|
|
5
|
+
* gzip-compressed (e.g. `sitemap.xml.gz`, served without `Content-Encoding`).
|
|
6
|
+
*/
|
|
7
|
+
const readBodyAsText = async (response) => {
|
|
8
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
9
|
+
if (!(bytes[0] === 31 && bytes[1] === 139)) return new TextDecoder().decode(bytes);
|
|
10
|
+
const decompressedStream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
11
|
+
return new Response(decompressedStream).text();
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* GET a URL as text with a timeout, returning `undefined` when the request is
|
|
15
|
+
* blocked by {@link ScanFetchOptions.shouldFetchUrl} or fails at network level.
|
|
16
|
+
* Redirects are followed manually so every hop goes through the guard.
|
|
17
|
+
*/
|
|
18
|
+
const fetchText = async (url, { userAgent, timeoutMs, shouldFetchUrl }) => {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
21
|
+
try {
|
|
22
|
+
let currentUrl = url;
|
|
23
|
+
for (let redirectCount = 0;; redirectCount++) {
|
|
24
|
+
if (shouldFetchUrl && !await shouldFetchUrl(currentUrl)) return;
|
|
25
|
+
const response = await fetch(currentUrl, {
|
|
26
|
+
headers: {
|
|
27
|
+
"User-Agent": userAgent,
|
|
28
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
29
|
+
},
|
|
30
|
+
redirect: "manual",
|
|
31
|
+
signal: controller.signal
|
|
32
|
+
});
|
|
33
|
+
const location = response.headers.get("location");
|
|
34
|
+
if (response.status >= 300 && response.status < 400 && location && redirectCount < MAX_REDIRECTS) {
|
|
35
|
+
currentUrl = new URL(location, currentUrl).href;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: response.status,
|
|
40
|
+
ok: response.ok,
|
|
41
|
+
text: await readBodyAsText(response),
|
|
42
|
+
finalUrl: currentUrl,
|
|
43
|
+
redirected: redirectCount > 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
} finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { fetchText };
|
|
55
|
+
//# sourceMappingURL=fetchText.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchText.mjs","names":[],"sources":["../../../src/scan/fetchText.ts"],"sourcesContent":["/** Options shared by every network request issued during a scan. */\nexport type ScanFetchOptions = {\n userAgent: string;\n timeoutMs: number;\n /**\n * Guard called before fetching any URL discovered on the scanned site\n * (hreflang alternates, sitemap children…). The hosted audit uses it to\n * block private / reserved addresses (SSRF). Defaults to allowing all.\n */\n shouldFetchUrl?: (url: string) => boolean | Promise<boolean>;\n};\n\n/** Response of {@link fetchText}. */\nexport type FetchTextResult = {\n status: number;\n ok: boolean;\n text: string;\n /** URL after redirects. */\n finalUrl: string;\n /** Whether at least one redirect was followed. */\n redirected: boolean;\n};\n\nconst MAX_REDIRECTS = 5;\n\n/**\n * Read a response body as text, gunzipping it when the payload itself is\n * gzip-compressed (e.g. `sitemap.xml.gz`, served without `Content-Encoding`).\n */\nconst readBodyAsText = async (response: Response): Promise<string> => {\n const bytes = new Uint8Array(await response.arrayBuffer());\n const isGzip = bytes[0] === 0x1f && bytes[1] === 0x8b;\n if (!isGzip) return new TextDecoder().decode(bytes);\n\n const decompressedStream = new Blob([bytes])\n .stream()\n .pipeThrough(new DecompressionStream('gzip'));\n return new Response(decompressedStream).text();\n};\n\n/**\n * GET a URL as text with a timeout, returning `undefined` when the request is\n * blocked by {@link ScanFetchOptions.shouldFetchUrl} or fails at network level.\n * Redirects are followed manually so every hop goes through the guard.\n */\nexport const fetchText = async (\n url: string,\n { userAgent, timeoutMs, shouldFetchUrl }: ScanFetchOptions\n): Promise<FetchTextResult | undefined> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n let currentUrl = url;\n for (let redirectCount = 0; ; redirectCount++) {\n if (shouldFetchUrl && !(await shouldFetchUrl(currentUrl))) {\n return undefined;\n }\n const response = await fetch(currentUrl, {\n headers: {\n 'User-Agent': userAgent,\n 'Accept-Language': 'en-US,en;q=0.9',\n },\n redirect: 'manual',\n signal: controller.signal,\n });\n const location = response.headers.get('location');\n if (\n response.status >= 300 &&\n response.status < 400 &&\n location &&\n redirectCount < MAX_REDIRECTS\n ) {\n currentUrl = new URL(location, currentUrl).href;\n continue;\n }\n return {\n status: response.status,\n ok: response.ok,\n text: await readBodyAsText(response),\n finalUrl: currentUrl,\n redirected: redirectCount > 0,\n };\n }\n } catch {\n return undefined;\n } finally {\n clearTimeout(timer);\n }\n};\n"],"mappings":";AAuBA,MAAM,gBAAgB;;;;;AAMtB,MAAM,iBAAiB,OAAO,aAAwC;CACpE,MAAM,QAAQ,IAAI,WAAW,MAAM,SAAS,YAAY,CAAC;CAEzD,IAAI,EADW,MAAM,OAAO,MAAQ,MAAM,OAAO,MACpC,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK;CAElD,MAAM,qBAAqB,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CACzC,OAAO,CAAC,CACR,YAAY,IAAI,oBAAoB,MAAM,CAAC;CAC9C,OAAO,IAAI,SAAS,kBAAkB,CAAC,CAAC,KAAK;AAC/C;;;;;;AAOA,MAAa,YAAY,OACvB,KACA,EAAE,WAAW,WAAW,qBACiB;CACzC,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,QAAQ,iBAAiB,WAAW,MAAM,GAAG,SAAS;CAC5D,IAAI;EACF,IAAI,aAAa;EACjB,KAAK,IAAI,gBAAgB,IAAK,iBAAiB;GAC7C,IAAI,kBAAkB,CAAE,MAAM,eAAe,UAAU,GACrD;GAEF,MAAM,WAAW,MAAM,MAAM,YAAY;IACvC,SAAS;KACP,cAAc;KACd,mBAAmB;IACrB;IACA,UAAU;IACV,QAAQ,WAAW;GACrB,CAAC;GACD,MAAM,WAAW,SAAS,QAAQ,IAAI,UAAU;GAChD,IACE,SAAS,UAAU,OACnB,SAAS,SAAS,OAClB,YACA,gBAAgB,eAChB;IACA,aAAa,IAAI,IAAI,UAAU,UAAU,CAAC,CAAC;IAC3C;GACF;GACA,OAAO;IACL,QAAQ,SAAS;IACjB,IAAI,SAAS;IACb,MAAM,MAAM,eAAe,QAAQ;IACnC,UAAU;IACV,YAAY,gBAAgB;GAC9B;EACF;CACF,QAAQ;EACN;CACF,UAAU;EACR,aAAa,KAAK;CACpB;AACF"}
|
package/dist/esm/scan/index.mjs
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import { fetchText } from "./fetchText.mjs";
|
|
1
2
|
import { mutateScore, scoreRecord, toScorePercent } from "./calculateScore.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { technologySignatures } from "./detection/technologySignatures.mjs";
|
|
4
|
+
import { getFirstPathSegment, getSiteDomain, isAbsoluteUrl, normalizeHostname, normalizeUrl, parseUrl } from "./detection/url.mjs";
|
|
5
|
+
import { detectTechnologies, getTechnologyGlobalNames } from "./detection/detectTechnologies.mjs";
|
|
6
|
+
import { byteLength, extractAnchors, extractCanonicalHref, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractOgLocale, extractOgLocaleAlternates, extractResourceUrls, extractScriptUrls, extractTitle, extractVisibleTextStrings, hasCanonical } from "./parseHtml.mjs";
|
|
3
7
|
import { analyzeBundleContent } from "./analyzeBundleContent.mjs";
|
|
4
|
-
import {
|
|
8
|
+
import { findMatchingLocale, getLanguageCode, isRightToLeftLocale, isSameLanguage, isSameLocale, isValidLocaleCode, isValidOpenGraphLocale, looksLikeLocaleCode, normalizeLocaleCode, toOpenGraphLocale } from "./detection/localeCode.mjs";
|
|
9
|
+
import { detectRoutingStrategy, getUrlLocale } from "./detection/detectRoutingStrategy.mjs";
|
|
10
|
+
import { classifyInternalLinks } from "./detection/classifyInternalLinks.mjs";
|
|
11
|
+
import { isPathAllowedByRobots, parseRobots } from "./robots.mjs";
|
|
12
|
+
import { collectSitemapEntries, discoverSitemapUrls, getDefaultSitemapUrls, parseSitemap } from "./sitemap.mjs";
|
|
13
|
+
import { checkBundleContent, checkCanonical, checkHreflang, checkHreflangReciprocity, checkHtmlDir, checkHtmlLang, checkInternalLinks, checkLocaleConsistency, checkOgLocale, checkRobots, checkSitemap, checkXDefault, formatBundleAnalysis, formatSize } from "./checks.mjs";
|
|
14
|
+
import { runScanChecks } from "./runScanChecks.mjs";
|
|
5
15
|
import { scanWebsite } from "./scanWebsite.mjs";
|
|
16
|
+
import { getCheckDetailLines, splitCheckDetails } from "./detection/checkDetails.mjs";
|
|
17
|
+
import { getLocalizedPages, isBaseLocalePage } from "./detection/localizedPages.mjs";
|
|
6
18
|
|
|
7
|
-
export { analyzeBundleContent, byteLength, checkBundleContent, checkCanonical,
|
|
19
|
+
export { analyzeBundleContent, byteLength, checkBundleContent, checkCanonical, checkHreflang, checkHreflangReciprocity, checkHtmlDir, checkHtmlLang, checkInternalLinks, checkLocaleConsistency, checkOgLocale, checkRobots, checkSitemap, checkXDefault, classifyInternalLinks, collectSitemapEntries, detectRoutingStrategy, detectTechnologies, discoverSitemapUrls, extractAnchors, extractCanonicalHref, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractOgLocale, extractOgLocaleAlternates, extractResourceUrls, extractScriptUrls, extractTitle, extractVisibleTextStrings, fetchText, findMatchingLocale, formatBundleAnalysis, formatSize, getCheckDetailLines, getDefaultSitemapUrls, getFirstPathSegment, getLanguageCode, getLocalizedPages, getSiteDomain, getTechnologyGlobalNames, getUrlLocale, hasCanonical, isAbsoluteUrl, isBaseLocalePage, isPathAllowedByRobots, isRightToLeftLocale, isSameLanguage, isSameLocale, isValidLocaleCode, isValidOpenGraphLocale, looksLikeLocaleCode, mutateScore, normalizeHostname, normalizeLocaleCode, normalizeUrl, parseRobots, parseSitemap, parseUrl, runScanChecks, scanWebsite, scoreRecord, splitCheckDetails, technologySignatures, toOpenGraphLocale, toScorePercent };
|
|
@@ -18,17 +18,17 @@ const readAttribute = (attributes, attributeName) => {
|
|
|
18
18
|
/** Extract the `lang` attribute of the `<html>` element, if present. */
|
|
19
19
|
const extractHtmlLang = (html) => {
|
|
20
20
|
const htmlTag = html.match(/<html\b([^>]*)>/i);
|
|
21
|
-
return htmlTag ? readAttribute(htmlTag[1], "lang") : void 0;
|
|
21
|
+
return htmlTag?.[1] ? readAttribute(htmlTag[1], "lang") : void 0;
|
|
22
22
|
};
|
|
23
23
|
/** Extract the `dir` attribute of the `<html>` element, if present. */
|
|
24
24
|
const extractHtmlDir = (html) => {
|
|
25
25
|
const htmlTag = html.match(/<html\b([^>]*)>/i);
|
|
26
|
-
return htmlTag ? readAttribute(htmlTag[1], "dir") : void 0;
|
|
26
|
+
return htmlTag?.[1] ? readAttribute(htmlTag[1], "dir") : void 0;
|
|
27
27
|
};
|
|
28
28
|
/** Extract the document `<title>` text. */
|
|
29
29
|
const extractTitle = (html) => {
|
|
30
30
|
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
31
|
-
return match ? match[1].trim() : "";
|
|
31
|
+
return match?.[1] ? match[1].trim() : "";
|
|
32
32
|
};
|
|
33
33
|
/** Extract the `<meta name="description">` content. */
|
|
34
34
|
const extractMetaDescription = (html) => {
|
|
@@ -39,12 +39,28 @@ const extractMetaDescription = (html) => {
|
|
|
39
39
|
/** Extract the `<meta property="og:image">` content. */
|
|
40
40
|
const extractOgImage = (html) => {
|
|
41
41
|
const metas = html.match(/<meta\b[^>]*>/gi) ?? [];
|
|
42
|
-
for (const meta of metas) if (/property\s*=\s*("|')?og:image\1
|
|
42
|
+
for (const meta of metas) if (/property\s*=\s*("|')?og:image\1?[\s>/]/i.test(meta)) return readAttribute(meta, "content");
|
|
43
43
|
};
|
|
44
|
-
/**
|
|
45
|
-
const
|
|
46
|
-
|
|
44
|
+
/** Extract the `<meta property="og:locale">` content. */
|
|
45
|
+
const extractOgLocale = (html) => {
|
|
46
|
+
const metas = html.match(/<meta\b[^>]*>/gi) ?? [];
|
|
47
|
+
for (const meta of metas) if (/property\s*=\s*("|')?og:locale\1?[\s>/]/i.test(meta)) return readAttribute(meta, "content");
|
|
48
|
+
};
|
|
49
|
+
/** Extract every `<meta property="og:locale:alternate">` content. */
|
|
50
|
+
const extractOgLocaleAlternates = (html) => {
|
|
51
|
+
return (html.match(/<meta\b[^>]*>/gi) ?? []).flatMap((meta) => {
|
|
52
|
+
if (!/property\s*=\s*("|')?og:locale:alternate\1?[\s>/]/i.test(meta)) return [];
|
|
53
|
+
const content = readAttribute(meta, "content");
|
|
54
|
+
return content ? [content] : [];
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
/** Extract the `href` of the `<link rel="canonical">` element, if present. */
|
|
58
|
+
const extractCanonicalHref = (html) => {
|
|
59
|
+
const canonicalLink = (html.match(/<link\b[^>]*>/gi) ?? []).find((link) => /rel\s*=\s*("|')?canonical\1?/i.test(link));
|
|
60
|
+
return canonicalLink ? readAttribute(canonicalLink, "href") ?? "" : void 0;
|
|
47
61
|
};
|
|
62
|
+
/** Whether a `<link rel="canonical">` element is present. */
|
|
63
|
+
const hasCanonical = (html) => extractCanonicalHref(html) !== void 0;
|
|
48
64
|
/** Extract every `<link rel="alternate" hreflang="…" href="…">` element. */
|
|
49
65
|
const extractHreflangs = (html) => {
|
|
50
66
|
const links = html.match(/<link\b[^>]*>/gi) ?? [];
|
|
@@ -90,12 +106,15 @@ const extractAnchors = (html) => {
|
|
|
90
106
|
const anchorPattern = /<a\b([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
91
107
|
let match = anchorPattern.exec(html);
|
|
92
108
|
while (match !== null) {
|
|
93
|
-
const
|
|
109
|
+
const rawAttrs = match[1] ?? "";
|
|
110
|
+
const rawContent = match[2] ?? "";
|
|
111
|
+
const href = readAttribute(rawAttrs, "href");
|
|
94
112
|
if (href) {
|
|
95
|
-
const text =
|
|
113
|
+
const text = rawContent.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
96
114
|
anchors.push({
|
|
97
115
|
href,
|
|
98
|
-
text
|
|
116
|
+
text,
|
|
117
|
+
hreflang: readAttribute(rawAttrs, "hreflang")
|
|
99
118
|
});
|
|
100
119
|
}
|
|
101
120
|
match = anchorPattern.exec(html);
|
|
@@ -103,6 +122,28 @@ const extractAnchors = (html) => {
|
|
|
103
122
|
return anchors;
|
|
104
123
|
};
|
|
105
124
|
/**
|
|
125
|
+
* Extract every resource URL referenced by the document (`<script src>`,
|
|
126
|
+
* `<link href>`, `<iframe src>`), used to fingerprint third-party services.
|
|
127
|
+
*
|
|
128
|
+
* @param html - The raw HTML document.
|
|
129
|
+
* @param baseUrl - Base URL used to resolve relative URLs.
|
|
130
|
+
* @returns Absolute, de-duplicated URLs.
|
|
131
|
+
*/
|
|
132
|
+
const extractResourceUrls = (html, baseUrl) => {
|
|
133
|
+
const urls = /* @__PURE__ */ new Set();
|
|
134
|
+
const tagPattern = /<(script|link|iframe)\b([^>]*)>/gi;
|
|
135
|
+
let match = tagPattern.exec(html);
|
|
136
|
+
while (match !== null) {
|
|
137
|
+
const attributeName = match[1]?.toLowerCase() === "link" ? "href" : "src";
|
|
138
|
+
const rawUrl = readAttribute(match[2] ?? "", attributeName);
|
|
139
|
+
if (rawUrl) try {
|
|
140
|
+
urls.add(new URL(rawUrl, baseUrl).href);
|
|
141
|
+
} catch {}
|
|
142
|
+
match = tagPattern.exec(html);
|
|
143
|
+
}
|
|
144
|
+
return Array.from(urls);
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
106
147
|
* Extract visible text snippets from an HTML document (scripts, styles and
|
|
107
148
|
* tags stripped). Used to approximate the rendered content size without a DOM.
|
|
108
149
|
*/
|
|
@@ -111,5 +152,5 @@ const extractVisibleTextStrings = (html) => {
|
|
|
111
152
|
};
|
|
112
153
|
|
|
113
154
|
//#endregion
|
|
114
|
-
export { byteLength, extractAnchors, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractScriptUrls, extractTitle, extractVisibleTextStrings, hasCanonical };
|
|
155
|
+
export { byteLength, extractAnchors, extractCanonicalHref, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractOgLocale, extractOgLocaleAlternates, extractResourceUrls, extractScriptUrls, extractTitle, extractVisibleTextStrings, hasCanonical };
|
|
115
156
|
//# sourceMappingURL=parseHtml.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parseHtml.mjs","names":[],"sources":["../../../src/scan/parseHtml.ts"],"sourcesContent":["/**\n * Tiny dependency-free HTML extraction helpers.\n *\n * The hosted backend audit relies on Cheerio + a real browser, but the CLI scan\n * must stay dependency-light. These regex-based helpers cover the handful of\n * head/anchor signals the score needs. They are intentionally forgiving: when a\n * tag can't be parsed it is simply skipped rather than throwing.\n */\n\n/** Compute the UTF-8 byte length of a string in both Node and browser builds. */\nexport const byteLength = (text: string): number =>\n typeof Buffer !== 'undefined'\n ? Buffer.byteLength(text, 'utf-8')\n : new TextEncoder().encode(text).length;\n\n/** Read an attribute value off a single tag's attribute string. */\nconst readAttribute = (\n attributes: string,\n attributeName: string\n): string | undefined => {\n const match = attributes.match(\n new RegExp(`${attributeName}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`, 'i')\n );\n if (!match) return undefined;\n return match[2] ?? match[3] ?? match[4];\n};\n\n/** Extract the `lang` attribute of the `<html>` element, if present. */\nexport const extractHtmlLang = (html: string): string | undefined => {\n const htmlTag = html.match(/<html\\b([^>]*)>/i);\n return htmlTag ? readAttribute(htmlTag[1], 'lang') : undefined;\n};\n\n/** Extract the `dir` attribute of the `<html>` element, if present. */\nexport const extractHtmlDir = (html: string): string | undefined => {\n const htmlTag = html.match(/<html\\b([^>]*)>/i);\n return htmlTag ? readAttribute(htmlTag[1], 'dir') : undefined;\n};\n\n/** Extract the document `<title>` text. */\nexport const extractTitle = (html: string): string => {\n const match = html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\n return match ? match[1].trim() : '';\n};\n\n/** Extract the `<meta name=\"description\">` content. */\nexport const extractMetaDescription = (html: string): string => {\n const metas = html.match(/<meta\\b[^>]*>/gi) ?? [];\n for (const meta of metas) {\n if (/name\\s*=\\s*(\"|')?description\\1?/i.test(meta)) {\n return readAttribute(meta, 'content') ?? '';\n }\n }\n return '';\n};\n\n/** Extract the `<meta property=\"og:image\">` content. */\nexport const extractOgImage = (html: string): string | undefined => {\n const metas = html.match(/<meta\\b[^>]*>/gi) ?? [];\n for (const meta of metas) {\n if (/property\\s*=\\s*(\"|')?og:image\\1?/i.test(meta)) {\n return readAttribute(meta, 'content');\n }\n }\n return undefined;\n};\n\n/** Whether a `<link rel=\"canonical\">` element is present. */\nexport const hasCanonical = (html: string): boolean => {\n const links = html.match(/<link\\b[^>]*>/gi) ?? [];\n return links.some((link) => /rel\\s*=\\s*(\"|')?canonical\\1?/i.test(link));\n};\n\n/** A parsed `<link rel=\"alternate\" hreflang>` element. */\nexport type HreflangLink = { hreflang: string; href: string };\n\n/** Extract every `<link rel=\"alternate\" hreflang=\"…\" href=\"…\">` element. */\nexport const extractHreflangs = (html: string): HreflangLink[] => {\n const links = html.match(/<link\\b[^>]*>/gi) ?? [];\n const result: HreflangLink[] = [];\n for (const link of links) {\n if (!/rel\\s*=\\s*(\"|')?alternate\\1?/i.test(link)) continue;\n const hreflang = readAttribute(link, 'hreflang');\n const href = readAttribute(link, 'href');\n if (hreflang && href) result.push({ hreflang, href });\n }\n return result;\n};\n\n/**\n * Extract every eagerly-loaded script URL: `<script src>`,\n * `<link rel=\"modulepreload\">` and `<link rel=\"preload\" as=\"script\">`.\n *\n * @param html - The raw HTML document.\n * @param baseUrl - Base URL used to resolve relative script URLs.\n * @returns Absolute, de-duplicated script URLs.\n */\nexport const extractScriptUrls = (html: string, baseUrl: string): string[] => {\n const urls = new Set<string>();\n\n const add = (raw: string | undefined) => {\n if (!raw) return;\n try {\n urls.add(new URL(raw, baseUrl).href);\n } catch {\n /* ignore malformed URLs */\n }\n };\n\n for (const script of html.match(/<script\\b[^>]*>/gi) ?? []) {\n add(readAttribute(script, 'src'));\n }\n\n for (const link of html.match(/<link\\b[^>]*>/gi) ?? []) {\n const rel = readAttribute(link, 'rel')?.toLowerCase();\n const as = readAttribute(link, 'as')?.toLowerCase();\n if (rel === 'modulepreload' || (rel === 'preload' && as === 'script')) {\n add(readAttribute(link, 'href'));\n }\n }\n\n return Array.from(urls);\n};\n\n/** A parsed `<a href>` anchor. */\nexport type Anchor = { href: string; text: string };\n\n/** Extract every `<a href=\"…\">text</a>` anchor from the document. */\nexport const extractAnchors = (html: string): Anchor[] => {\n const anchors: Anchor[] = [];\n const anchorPattern = /<a\\b([^>]*)>([\\s\\S]*?)<\\/a>/gi;\n let match = anchorPattern.exec(html);\n while (match !== null) {\n const href = readAttribute(match[1], 'href');\n if (href) {\n const text = match[2]\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n anchors.push({ href, text });\n }\n match = anchorPattern.exec(html);\n }\n return anchors;\n};\n\n/**\n * Extract visible text snippets from an HTML document (scripts, styles and\n * tags stripped). Used to approximate the rendered content size without a DOM.\n */\nexport const extractVisibleTextStrings = (html: string): string[] => {\n const withoutNonVisible = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ');\n\n return withoutNonVisible\n .replace(/<[^>]+>/g, '\\n')\n .split('\\n')\n .map((line) => line.replace(/\\s+/g, ' ').trim())\n .filter((line) => line.length > 1);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"parseHtml.mjs","names":[],"sources":["../../../src/scan/parseHtml.ts"],"sourcesContent":["/**\n * Tiny dependency-free HTML extraction helpers.\n *\n * The hosted backend audit relies on Cheerio + a real browser, but the CLI scan\n * must stay dependency-light. These regex-based helpers cover the handful of\n * head/anchor signals the score needs. They are intentionally forgiving: when a\n * tag can't be parsed it is simply skipped rather than throwing.\n */\n\n/** Compute the UTF-8 byte length of a string in both Node and browser builds. */\nexport const byteLength = (text: string): number =>\n typeof Buffer !== 'undefined'\n ? Buffer.byteLength(text, 'utf-8')\n : new TextEncoder().encode(text).length;\n\n/** Read an attribute value off a single tag's attribute string. */\nconst readAttribute = (\n attributes: string,\n attributeName: string\n): string | undefined => {\n const match = attributes.match(\n new RegExp(`${attributeName}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`, 'i')\n );\n if (!match) return undefined;\n return match[2] ?? match[3] ?? match[4];\n};\n\n/** Extract the `lang` attribute of the `<html>` element, if present. */\nexport const extractHtmlLang = (html: string): string | undefined => {\n const htmlTag = html.match(/<html\\b([^>]*)>/i);\n return htmlTag?.[1] ? readAttribute(htmlTag[1], 'lang') : undefined;\n};\n\n/** Extract the `dir` attribute of the `<html>` element, if present. */\nexport const extractHtmlDir = (html: string): string | undefined => {\n const htmlTag = html.match(/<html\\b([^>]*)>/i);\n return htmlTag?.[1] ? readAttribute(htmlTag[1], 'dir') : undefined;\n};\n\n/** Extract the document `<title>` text. */\nexport const extractTitle = (html: string): string => {\n const match = html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\n return match?.[1] ? match[1].trim() : '';\n};\n\n/** Extract the `<meta name=\"description\">` content. */\nexport const extractMetaDescription = (html: string): string => {\n const metas = html.match(/<meta\\b[^>]*>/gi) ?? [];\n for (const meta of metas) {\n if (/name\\s*=\\s*(\"|')?description\\1?/i.test(meta)) {\n return readAttribute(meta, 'content') ?? '';\n }\n }\n return '';\n};\n\n/** Extract the `<meta property=\"og:image\">` content. */\nexport const extractOgImage = (html: string): string | undefined => {\n const metas = html.match(/<meta\\b[^>]*>/gi) ?? [];\n for (const meta of metas) {\n if (/property\\s*=\\s*(\"|')?og:image\\1?[\\s>/]/i.test(meta)) {\n return readAttribute(meta, 'content');\n }\n }\n return undefined;\n};\n\n/** Extract the `<meta property=\"og:locale\">` content. */\nexport const extractOgLocale = (html: string): string | undefined => {\n const metas = html.match(/<meta\\b[^>]*>/gi) ?? [];\n for (const meta of metas) {\n if (/property\\s*=\\s*(\"|')?og:locale\\1?[\\s>/]/i.test(meta)) {\n return readAttribute(meta, 'content');\n }\n }\n return undefined;\n};\n\n/** Extract every `<meta property=\"og:locale:alternate\">` content. */\nexport const extractOgLocaleAlternates = (html: string): string[] => {\n const metas = html.match(/<meta\\b[^>]*>/gi) ?? [];\n return metas.flatMap((meta) => {\n if (!/property\\s*=\\s*(\"|')?og:locale:alternate\\1?[\\s>/]/i.test(meta)) {\n return [];\n }\n const content = readAttribute(meta, 'content');\n return content ? [content] : [];\n });\n};\n\n/** Extract the `href` of the `<link rel=\"canonical\">` element, if present. */\nexport const extractCanonicalHref = (html: string): string | undefined => {\n const links = html.match(/<link\\b[^>]*>/gi) ?? [];\n const canonicalLink = links.find((link) =>\n /rel\\s*=\\s*(\"|')?canonical\\1?/i.test(link)\n );\n return canonicalLink\n ? (readAttribute(canonicalLink, 'href') ?? '')\n : undefined;\n};\n\n/** Whether a `<link rel=\"canonical\">` element is present. */\nexport const hasCanonical = (html: string): boolean =>\n extractCanonicalHref(html) !== undefined;\n\n/** A parsed `<link rel=\"alternate\" hreflang>` element. */\nexport type HreflangLink = { hreflang: string; href: string };\n\n/** Extract every `<link rel=\"alternate\" hreflang=\"…\" href=\"…\">` element. */\nexport const extractHreflangs = (html: string): HreflangLink[] => {\n const links = html.match(/<link\\b[^>]*>/gi) ?? [];\n const result: HreflangLink[] = [];\n for (const link of links) {\n if (!/rel\\s*=\\s*(\"|')?alternate\\1?/i.test(link)) continue;\n const hreflang = readAttribute(link, 'hreflang');\n const href = readAttribute(link, 'href');\n if (hreflang && href) result.push({ hreflang, href });\n }\n return result;\n};\n\n/**\n * Extract every eagerly-loaded script URL: `<script src>`,\n * `<link rel=\"modulepreload\">` and `<link rel=\"preload\" as=\"script\">`.\n *\n * @param html - The raw HTML document.\n * @param baseUrl - Base URL used to resolve relative script URLs.\n * @returns Absolute, de-duplicated script URLs.\n */\nexport const extractScriptUrls = (html: string, baseUrl: string): string[] => {\n const urls = new Set<string>();\n\n const add = (raw: string | undefined) => {\n if (!raw) return;\n try {\n urls.add(new URL(raw, baseUrl).href);\n } catch {\n /* ignore malformed URLs */\n }\n };\n\n for (const script of html.match(/<script\\b[^>]*>/gi) ?? []) {\n add(readAttribute(script, 'src'));\n }\n\n for (const link of html.match(/<link\\b[^>]*>/gi) ?? []) {\n const rel = readAttribute(link, 'rel')?.toLowerCase();\n const as = readAttribute(link, 'as')?.toLowerCase();\n if (rel === 'modulepreload' || (rel === 'preload' && as === 'script')) {\n add(readAttribute(link, 'href'));\n }\n }\n\n return Array.from(urls);\n};\n\n/** A parsed `<a href>` anchor. */\nexport type Anchor = {\n href: string;\n text: string;\n /** `hreflang` attribute, set on language-switcher links. */\n hreflang?: string;\n};\n\n/** Extract every `<a href=\"…\">text</a>` anchor from the document. */\nexport const extractAnchors = (html: string): Anchor[] => {\n const anchors: Anchor[] = [];\n const anchorPattern = /<a\\b([^>]*)>([\\s\\S]*?)<\\/a>/gi;\n let match = anchorPattern.exec(html);\n while (match !== null) {\n const rawAttrs = match[1] ?? '';\n const rawContent = match[2] ?? '';\n const href = readAttribute(rawAttrs, 'href');\n if (href) {\n const text = rawContent\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n anchors.push({\n href,\n text,\n hreflang: readAttribute(rawAttrs, 'hreflang'),\n });\n }\n match = anchorPattern.exec(html);\n }\n return anchors;\n};\n\n/**\n * Extract every resource URL referenced by the document (`<script src>`,\n * `<link href>`, `<iframe src>`), used to fingerprint third-party services.\n *\n * @param html - The raw HTML document.\n * @param baseUrl - Base URL used to resolve relative URLs.\n * @returns Absolute, de-duplicated URLs.\n */\nexport const extractResourceUrls = (\n html: string,\n baseUrl: string\n): string[] => {\n const urls = new Set<string>();\n const tagPattern = /<(script|link|iframe)\\b([^>]*)>/gi;\n let match = tagPattern.exec(html);\n while (match !== null) {\n const attributeName = match[1]?.toLowerCase() === 'link' ? 'href' : 'src';\n const rawUrl = readAttribute(match[2] ?? '', attributeName);\n if (rawUrl) {\n try {\n urls.add(new URL(rawUrl, baseUrl).href);\n } catch {\n /* ignore malformed URLs */\n }\n }\n match = tagPattern.exec(html);\n }\n return Array.from(urls);\n};\n\n/**\n * Extract visible text snippets from an HTML document (scripts, styles and\n * tags stripped). Used to approximate the rendered content size without a DOM.\n */\nexport const extractVisibleTextStrings = (html: string): string[] => {\n const withoutNonVisible = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ');\n\n return withoutNonVisible\n .replace(/<[^>]+>/g, '\\n')\n .split('\\n')\n .map((line) => line.replace(/\\s+/g, ' ').trim())\n .filter((line) => line.length > 1);\n};\n"],"mappings":";;;;;;;;;;AAUA,MAAa,cAAc,SACzB,OAAO,WAAW,cACd,OAAO,WAAW,MAAM,OAAO,IAC/B,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC;;AAGrC,MAAM,iBACJ,YACA,kBACuB;CACvB,MAAM,QAAQ,WAAW,MACvB,IAAI,OAAO,GAAG,cAAc,4CAA4C,GAAG,CAC7E;CACA,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,MAAM,MAAM,MAAM,MAAM,MAAM;AACvC;;AAGA,MAAa,mBAAmB,SAAqC;CACnE,MAAM,UAAU,KAAK,MAAM,kBAAkB;CAC7C,OAAO,UAAU,KAAK,cAAc,QAAQ,IAAI,MAAM,IAAI;AAC5D;;AAGA,MAAa,kBAAkB,SAAqC;CAClE,MAAM,UAAU,KAAK,MAAM,kBAAkB;CAC7C,OAAO,UAAU,KAAK,cAAc,QAAQ,IAAI,KAAK,IAAI;AAC3D;;AAGA,MAAa,gBAAgB,SAAyB;CACpD,MAAM,QAAQ,KAAK,MAAM,kCAAkC;CAC3D,OAAO,QAAQ,KAAK,MAAM,EAAE,CAAC,KAAK,IAAI;AACxC;;AAGA,MAAa,0BAA0B,SAAyB;CAC9D,MAAM,QAAQ,KAAK,MAAM,iBAAiB,KAAK,CAAC;CAChD,KAAK,MAAM,QAAQ,OACjB,IAAI,mCAAmC,KAAK,IAAI,GAC9C,OAAO,cAAc,MAAM,SAAS,KAAK;CAG7C,OAAO;AACT;;AAGA,MAAa,kBAAkB,SAAqC;CAClE,MAAM,QAAQ,KAAK,MAAM,iBAAiB,KAAK,CAAC;CAChD,KAAK,MAAM,QAAQ,OACjB,IAAI,0CAA0C,KAAK,IAAI,GACrD,OAAO,cAAc,MAAM,SAAS;AAI1C;;AAGA,MAAa,mBAAmB,SAAqC;CACnE,MAAM,QAAQ,KAAK,MAAM,iBAAiB,KAAK,CAAC;CAChD,KAAK,MAAM,QAAQ,OACjB,IAAI,2CAA2C,KAAK,IAAI,GACtD,OAAO,cAAc,MAAM,SAAS;AAI1C;;AAGA,MAAa,6BAA6B,SAA2B;CAEnE,QADc,KAAK,MAAM,iBAAiB,KAAK,CAAC,EACpC,CAAC,SAAS,SAAS;EAC7B,IAAI,CAAC,qDAAqD,KAAK,IAAI,GACjE,OAAO,CAAC;EAEV,MAAM,UAAU,cAAc,MAAM,SAAS;EAC7C,OAAO,UAAU,CAAC,OAAO,IAAI,CAAC;CAChC,CAAC;AACH;;AAGA,MAAa,wBAAwB,SAAqC;CAExE,MAAM,iBADQ,KAAK,MAAM,iBAAiB,KAAK,CAAC,EACrB,CAAC,MAAM,SAChC,gCAAgC,KAAK,IAAI,CAC3C;CACA,OAAO,gBACF,cAAc,eAAe,MAAM,KAAK,KACzC;AACN;;AAGA,MAAa,gBAAgB,SAC3B,qBAAqB,IAAI,MAAM;;AAMjC,MAAa,oBAAoB,SAAiC;CAChE,MAAM,QAAQ,KAAK,MAAM,iBAAiB,KAAK,CAAC;CAChD,MAAM,SAAyB,CAAC;CAChC,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,gCAAgC,KAAK,IAAI,GAAG;EACjD,MAAM,WAAW,cAAc,MAAM,UAAU;EAC/C,MAAM,OAAO,cAAc,MAAM,MAAM;EACvC,IAAI,YAAY,MAAM,OAAO,KAAK;GAAE;GAAU;EAAK,CAAC;CACtD;CACA,OAAO;AACT;;;;;;;;;AAUA,MAAa,qBAAqB,MAAc,YAA8B;CAC5E,MAAM,uBAAO,IAAI,IAAY;CAE7B,MAAM,OAAO,QAA4B;EACvC,IAAI,CAAC,KAAK;EACV,IAAI;GACF,KAAK,IAAI,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,IAAI;EACrC,QAAQ,CAER;CACF;CAEA,KAAK,MAAM,UAAU,KAAK,MAAM,mBAAmB,KAAK,CAAC,GACvD,IAAI,cAAc,QAAQ,KAAK,CAAC;CAGlC,KAAK,MAAM,QAAQ,KAAK,MAAM,iBAAiB,KAAK,CAAC,GAAG;EACtD,MAAM,MAAM,cAAc,MAAM,KAAK,CAAC,EAAE,YAAY;EACpD,MAAM,KAAK,cAAc,MAAM,IAAI,CAAC,EAAE,YAAY;EAClD,IAAI,QAAQ,mBAAoB,QAAQ,aAAa,OAAO,UAC1D,IAAI,cAAc,MAAM,MAAM,CAAC;CAEnC;CAEA,OAAO,MAAM,KAAK,IAAI;AACxB;;AAWA,MAAa,kBAAkB,SAA2B;CACxD,MAAM,UAAoB,CAAC;CAC3B,MAAM,gBAAgB;CACtB,IAAI,QAAQ,cAAc,KAAK,IAAI;CACnC,OAAO,UAAU,MAAM;EACrB,MAAM,WAAW,MAAM,MAAM;EAC7B,MAAM,aAAa,MAAM,MAAM;EAC/B,MAAM,OAAO,cAAc,UAAU,MAAM;EAC3C,IAAI,MAAM;GACR,MAAM,OAAO,WACV,QAAQ,YAAY,GAAG,CAAC,CACxB,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;GACR,QAAQ,KAAK;IACX;IACA;IACA,UAAU,cAAc,UAAU,UAAU;GAC9C,CAAC;EACH;EACA,QAAQ,cAAc,KAAK,IAAI;CACjC;CACA,OAAO;AACT;;;;;;;;;AAUA,MAAa,uBACX,MACA,YACa;CACb,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,aAAa;CACnB,IAAI,QAAQ,WAAW,KAAK,IAAI;CAChC,OAAO,UAAU,MAAM;EACrB,MAAM,gBAAgB,MAAM,EAAE,EAAE,YAAY,MAAM,SAAS,SAAS;EACpE,MAAM,SAAS,cAAc,MAAM,MAAM,IAAI,aAAa;EAC1D,IAAI,QACF,IAAI;GACF,KAAK,IAAI,IAAI,IAAI,QAAQ,OAAO,CAAC,CAAC,IAAI;EACxC,QAAQ,CAER;EAEF,QAAQ,WAAW,KAAK,IAAI;CAC9B;CACA,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;AAMA,MAAa,6BAA6B,SAA2B;CAOnE,OAN0B,KACvB,QAAQ,+BAA+B,GAAG,CAAC,CAC3C,QAAQ,6BAA6B,GAAG,CAAC,CACzC,QAAQ,mCAAmC,GAAG,CAAC,CAC/C,QAAQ,oBAAoB,GAER,CAAC,CACrB,QAAQ,YAAY,IAAI,CAAC,CACzB,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAC/C,QAAQ,SAAS,KAAK,SAAS,CAAC;AACrC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/scan/robots.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse a `robots.txt`, keeping the rules Googlebot would follow: the
|
|
4
|
+
* `googlebot` group when one exists, the `*` group otherwise.
|
|
5
|
+
*/
|
|
6
|
+
const parseRobots = (content) => {
|
|
7
|
+
const groups = [];
|
|
8
|
+
const sitemapUrls = [];
|
|
9
|
+
let currentGroup;
|
|
10
|
+
let isReadingUserAgents = false;
|
|
11
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
12
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
13
|
+
const separatorIndex = line.indexOf(":");
|
|
14
|
+
if (separatorIndex === -1) continue;
|
|
15
|
+
const directive = line.slice(0, separatorIndex).trim().toLowerCase();
|
|
16
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
17
|
+
if (directive === "sitemap") {
|
|
18
|
+
if (value) sitemapUrls.push(value);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (directive === "user-agent") {
|
|
22
|
+
if (!isReadingUserAgents || !currentGroup) {
|
|
23
|
+
currentGroup = {
|
|
24
|
+
userAgents: [],
|
|
25
|
+
disallowedPaths: [],
|
|
26
|
+
allowedPaths: []
|
|
27
|
+
};
|
|
28
|
+
groups.push(currentGroup);
|
|
29
|
+
}
|
|
30
|
+
currentGroup.userAgents.push(value.toLowerCase());
|
|
31
|
+
isReadingUserAgents = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
isReadingUserAgents = false;
|
|
35
|
+
if (!currentGroup || !value) continue;
|
|
36
|
+
if (directive === "disallow") currentGroup.disallowedPaths.push(value);
|
|
37
|
+
if (directive === "allow") currentGroup.allowedPaths.push(value);
|
|
38
|
+
}
|
|
39
|
+
const googlebotGroups = groups.filter(({ userAgents }) => userAgents.includes("googlebot"));
|
|
40
|
+
const applicableGroups = googlebotGroups.length > 0 ? googlebotGroups : groups.filter(({ userAgents }) => userAgents.includes("*"));
|
|
41
|
+
return {
|
|
42
|
+
disallowedPaths: applicableGroups.flatMap(({ disallowedPaths }) => disallowedPaths),
|
|
43
|
+
allowedPaths: applicableGroups.flatMap(({ allowedPaths }) => allowedPaths),
|
|
44
|
+
sitemapUrls
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/** Convert a robots.txt path pattern (`*`, `$`) into a RegExp. */
|
|
48
|
+
const robotsPatternToRegExp = (pattern) => new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\\\$$/, "$")}`);
|
|
49
|
+
/**
|
|
50
|
+
* Whether Googlebot may crawl `pathWithSearch` (path + query string) under the
|
|
51
|
+
* parsed rules. Follows Google's precedence: the longest matching rule wins,
|
|
52
|
+
* `Allow` wins ties.
|
|
53
|
+
*/
|
|
54
|
+
const isPathAllowedByRobots = (pathWithSearch, robots) => {
|
|
55
|
+
const longestMatch = (patterns) => patterns.reduce((longest, pattern) => robotsPatternToRegExp(pattern).test(pathWithSearch) ? Math.max(longest, pattern.length) : longest, -1);
|
|
56
|
+
const disallowLength = longestMatch(robots.disallowedPaths);
|
|
57
|
+
if (disallowLength === -1) return true;
|
|
58
|
+
return longestMatch(robots.allowedPaths) >= disallowLength;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { isPathAllowedByRobots, parseRobots };
|
|
63
|
+
//# sourceMappingURL=robots.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots.mjs","names":[],"sources":["../../../src/scan/robots.ts"],"sourcesContent":["/** Rules of a `robots.txt` that apply to Google's crawler. */\nexport type ParsedRobots = {\n /** `Disallow` paths of the group applying to Googlebot (or `*`). */\n disallowedPaths: string[];\n /** `Allow` paths of the same group. */\n allowedPaths: string[];\n /** Absolute URLs declared with `Sitemap:` directives. */\n sitemapUrls: string[];\n};\n\ntype RobotsGroup = {\n userAgents: string[];\n disallowedPaths: string[];\n allowedPaths: string[];\n};\n\n/**\n * Parse a `robots.txt`, keeping the rules Googlebot would follow: the\n * `googlebot` group when one exists, the `*` group otherwise.\n */\nexport const parseRobots = (content: string): ParsedRobots => {\n const groups: RobotsGroup[] = [];\n const sitemapUrls: string[] = [];\n let currentGroup: RobotsGroup | undefined;\n let isReadingUserAgents = false;\n\n for (const rawLine of content.split(/\\r?\\n/)) {\n const line = rawLine.replace(/#.*$/, '').trim();\n const separatorIndex = line.indexOf(':');\n if (separatorIndex === -1) continue;\n\n const directive = line.slice(0, separatorIndex).trim().toLowerCase();\n const value = line.slice(separatorIndex + 1).trim();\n\n if (directive === 'sitemap') {\n if (value) sitemapUrls.push(value);\n continue;\n }\n\n if (directive === 'user-agent') {\n if (!isReadingUserAgents || !currentGroup) {\n currentGroup = {\n userAgents: [],\n disallowedPaths: [],\n allowedPaths: [],\n };\n groups.push(currentGroup);\n }\n currentGroup.userAgents.push(value.toLowerCase());\n isReadingUserAgents = true;\n continue;\n }\n\n isReadingUserAgents = false;\n if (!currentGroup || !value) continue;\n if (directive === 'disallow') currentGroup.disallowedPaths.push(value);\n if (directive === 'allow') currentGroup.allowedPaths.push(value);\n }\n\n const googlebotGroups = groups.filter(({ userAgents }) =>\n userAgents.includes('googlebot')\n );\n const applicableGroups =\n googlebotGroups.length > 0\n ? googlebotGroups\n : groups.filter(({ userAgents }) => userAgents.includes('*'));\n\n return {\n disallowedPaths: applicableGroups.flatMap(\n ({ disallowedPaths }) => disallowedPaths\n ),\n allowedPaths: applicableGroups.flatMap(({ allowedPaths }) => allowedPaths),\n sitemapUrls,\n };\n};\n\n/** Convert a robots.txt path pattern (`*`, `$`) into a RegExp. */\nconst robotsPatternToRegExp = (pattern: string): RegExp =>\n new RegExp(\n `^${pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*')\n .replace(/\\\\\\$$/, '$')}`\n );\n\n/**\n * Whether Googlebot may crawl `pathWithSearch` (path + query string) under the\n * parsed rules. Follows Google's precedence: the longest matching rule wins,\n * `Allow` wins ties.\n */\nexport const isPathAllowedByRobots = (\n pathWithSearch: string,\n robots: Pick<ParsedRobots, 'disallowedPaths' | 'allowedPaths'>\n): boolean => {\n const longestMatch = (patterns: string[]): number =>\n patterns.reduce(\n (longest, pattern) =>\n robotsPatternToRegExp(pattern).test(pathWithSearch)\n ? Math.max(longest, pattern.length)\n : longest,\n -1\n );\n\n const disallowLength = longestMatch(robots.disallowedPaths);\n if (disallowLength === -1) return true;\n return longestMatch(robots.allowedPaths) >= disallowLength;\n};\n"],"mappings":";;;;;AAoBA,MAAa,eAAe,YAAkC;CAC5D,MAAM,SAAwB,CAAC;CAC/B,MAAM,cAAwB,CAAC;CAC/B,IAAI;CACJ,IAAI,sBAAsB;CAE1B,KAAK,MAAM,WAAW,QAAQ,MAAM,OAAO,GAAG;EAC5C,MAAM,OAAO,QAAQ,QAAQ,QAAQ,EAAE,CAAC,CAAC,KAAK;EAC9C,MAAM,iBAAiB,KAAK,QAAQ,GAAG;EACvC,IAAI,mBAAmB,IAAI;EAE3B,MAAM,YAAY,KAAK,MAAM,GAAG,cAAc,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY;EACnE,MAAM,QAAQ,KAAK,MAAM,iBAAiB,CAAC,CAAC,CAAC,KAAK;EAElD,IAAI,cAAc,WAAW;GAC3B,IAAI,OAAO,YAAY,KAAK,KAAK;GACjC;EACF;EAEA,IAAI,cAAc,cAAc;GAC9B,IAAI,CAAC,uBAAuB,CAAC,cAAc;IACzC,eAAe;KACb,YAAY,CAAC;KACb,iBAAiB,CAAC;KAClB,cAAc,CAAC;IACjB;IACA,OAAO,KAAK,YAAY;GAC1B;GACA,aAAa,WAAW,KAAK,MAAM,YAAY,CAAC;GAChD,sBAAsB;GACtB;EACF;EAEA,sBAAsB;EACtB,IAAI,CAAC,gBAAgB,CAAC,OAAO;EAC7B,IAAI,cAAc,YAAY,aAAa,gBAAgB,KAAK,KAAK;EACrE,IAAI,cAAc,SAAS,aAAa,aAAa,KAAK,KAAK;CACjE;CAEA,MAAM,kBAAkB,OAAO,QAAQ,EAAE,iBACvC,WAAW,SAAS,WAAW,CACjC;CACA,MAAM,mBACJ,gBAAgB,SAAS,IACrB,kBACA,OAAO,QAAQ,EAAE,iBAAiB,WAAW,SAAS,GAAG,CAAC;CAEhE,OAAO;EACL,iBAAiB,iBAAiB,SAC/B,EAAE,sBAAsB,eAC3B;EACA,cAAc,iBAAiB,SAAS,EAAE,mBAAmB,YAAY;EACzE;CACF;AACF;;AAGA,MAAM,yBAAyB,YAC7B,IAAI,OACF,IAAI,QACD,QAAQ,sBAAsB,MAAM,CAAC,CACrC,QAAQ,OAAO,IAAI,CAAC,CACpB,QAAQ,SAAS,GAAG,GACzB;;;;;;AAOF,MAAa,yBACX,gBACA,WACY;CACZ,MAAM,gBAAgB,aACpB,SAAS,QACN,SAAS,YACR,sBAAsB,OAAO,CAAC,CAAC,KAAK,cAAc,IAC9C,KAAK,IAAI,SAAS,QAAQ,MAAM,IAChC,SACN,EACF;CAEF,MAAM,iBAAiB,aAAa,OAAO,eAAe;CAC1D,IAAI,mBAAmB,IAAI,OAAO;CAClC,OAAO,aAAa,OAAO,YAAY,KAAK;AAC9C"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { parseUrl } from "./detection/url.mjs";
|
|
2
|
+
import { detectTechnologies } from "./detection/detectTechnologies.mjs";
|
|
3
|
+
import { extractAnchors, extractCanonicalHref, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractOgLocale, extractOgLocaleAlternates, extractResourceUrls, extractTitle } from "./parseHtml.mjs";
|
|
4
|
+
import { detectRoutingStrategy } from "./detection/detectRoutingStrategy.mjs";
|
|
5
|
+
import { checkBundleContent, checkCanonical, checkHreflang, checkHreflangReciprocity, checkHtmlDir, checkHtmlLang, checkInternalLinks, checkLocaleConsistency, checkOgLocale, checkRobots, checkSitemap, checkXDefault } from "./checks.mjs";
|
|
6
|
+
|
|
7
|
+
//#region src/scan/runScanChecks.ts
|
|
8
|
+
/** Extract title, description and absolute preview image. */
|
|
9
|
+
const extractPageMetadata = (html, targetUrl) => {
|
|
10
|
+
const ogImage = extractOgImage(html);
|
|
11
|
+
return {
|
|
12
|
+
title: extractTitle(html),
|
|
13
|
+
description: extractMetaDescription(html),
|
|
14
|
+
image: ogImage ? parseUrl(ogImage, targetUrl)?.href ?? ogImage : ""
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Run every i18n/SEO check on an already loaded page. Single implementation
|
|
19
|
+
* shared by the `intlayer scan` CLI and the hosted audit (`/api/scan`), which
|
|
20
|
+
* only differ in how they load the page.
|
|
21
|
+
*
|
|
22
|
+
* @param input - The loaded page (HTML, chunks, size, runtime signals).
|
|
23
|
+
* @param emit - Receives every check result as soon as it is produced.
|
|
24
|
+
* @param options - Network options and progress callbacks.
|
|
25
|
+
*/
|
|
26
|
+
const runScanChecks = async ({ targetUrl, html, chunks, totalPageSize, requestUrls = [], runtimeSignals }, emit, { onProgress, onPageInfo, ...fetchOptions }) => {
|
|
27
|
+
const origin = new URL(targetUrl).origin;
|
|
28
|
+
onProgress?.(20, "Analyzing html attributes and hreflang tags...");
|
|
29
|
+
const langTag = extractHtmlLang(html);
|
|
30
|
+
const hreflangs = extractHreflangs(html);
|
|
31
|
+
const routing = detectRoutingStrategy({
|
|
32
|
+
pageUrl: targetUrl,
|
|
33
|
+
htmlLang: langTag,
|
|
34
|
+
hreflangs
|
|
35
|
+
});
|
|
36
|
+
const signals = {
|
|
37
|
+
targetUrl,
|
|
38
|
+
langTag,
|
|
39
|
+
dirTag: extractHtmlDir(html),
|
|
40
|
+
ogLocale: extractOgLocale(html),
|
|
41
|
+
ogLocaleAlternates: extractOgLocaleAlternates(html),
|
|
42
|
+
canonicalHref: extractCanonicalHref(html),
|
|
43
|
+
hreflangs,
|
|
44
|
+
anchors: extractAnchors(html),
|
|
45
|
+
routing,
|
|
46
|
+
pageLocale: routing.urlLocale ?? langTag
|
|
47
|
+
};
|
|
48
|
+
const locales = [.../* @__PURE__ */ new Set([...langTag ? [langTag] : [], ...routing.locales])];
|
|
49
|
+
const pageInfo = {
|
|
50
|
+
metadata: extractPageMetadata(html, targetUrl),
|
|
51
|
+
routing,
|
|
52
|
+
locales,
|
|
53
|
+
technologies: detectTechnologies({
|
|
54
|
+
pageUrl: targetUrl,
|
|
55
|
+
html,
|
|
56
|
+
resourceUrls: [.../* @__PURE__ */ new Set([...extractResourceUrls(html, targetUrl), ...requestUrls])],
|
|
57
|
+
scripts: chunks.map(({ content }) => content),
|
|
58
|
+
...runtimeSignals
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
onPageInfo?.(pageInfo);
|
|
62
|
+
checkHtmlLang(signals, emit);
|
|
63
|
+
checkHtmlDir(signals, emit);
|
|
64
|
+
checkLocaleConsistency(signals, emit);
|
|
65
|
+
checkOgLocale(signals, emit);
|
|
66
|
+
checkCanonical(signals, emit);
|
|
67
|
+
checkHreflang(signals, emit);
|
|
68
|
+
checkXDefault(signals, emit);
|
|
69
|
+
onProgress?.(35, "Checking hreflang alternates...");
|
|
70
|
+
await checkHreflangReciprocity(signals, fetchOptions, emit);
|
|
71
|
+
onProgress?.(50, "Analysing bundle content & leakage...");
|
|
72
|
+
const bundle = checkBundleContent(chunks, html, signals.pageLocale, targetUrl, totalPageSize, emit);
|
|
73
|
+
onProgress?.(60, "Analyzing internal links...");
|
|
74
|
+
checkInternalLinks(signals, emit);
|
|
75
|
+
onProgress?.(70, "Checking robots.txt...");
|
|
76
|
+
const localizedUrls = hreflangs.flatMap(({ href }) => parseUrl(href, targetUrl)?.href ?? []);
|
|
77
|
+
const declaredSitemapUrls = await checkRobots(origin, localizedUrls, fetchOptions, emit);
|
|
78
|
+
onProgress?.(85, "Checking sitemaps...");
|
|
79
|
+
await checkSitemap(origin, declaredSitemapUrls, routing, fetchOptions, emit);
|
|
80
|
+
return {
|
|
81
|
+
...pageInfo,
|
|
82
|
+
bundle
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { runScanChecks };
|
|
88
|
+
//# sourceMappingURL=runScanChecks.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runScanChecks.mjs","names":[],"sources":["../../../src/scan/runScanChecks.ts"],"sourcesContent":["import {\n checkBundleContent,\n checkCanonical,\n checkHreflang,\n checkHreflangReciprocity,\n checkHtmlDir,\n checkHtmlLang,\n checkInternalLinks,\n checkLocaleConsistency,\n checkOgLocale,\n checkRobots,\n checkSitemap,\n checkXDefault,\n type EmitScanEvent,\n type PageSignals,\n} from './checks';\nimport { detectRoutingStrategy } from './detection/detectRoutingStrategy';\nimport {\n detectTechnologies,\n type TechnologyDetectionInput,\n} from './detection/detectTechnologies';\nimport { parseUrl } from './detection/url';\nimport type { ScanFetchOptions } from './fetchText';\nimport {\n extractAnchors,\n extractCanonicalHref,\n extractHreflangs,\n extractHtmlDir,\n extractHtmlLang,\n extractMetaDescription,\n extractOgImage,\n extractOgLocale,\n extractOgLocaleAlternates,\n extractResourceUrls,\n extractTitle,\n} from './parseHtml';\nimport type { BundleChunkInput, PageMetadata, ScanResult } from './types';\n\n/** Page material gathered by the caller (fetch, puppeteer, browser…). */\nexport type ScanPageInput = {\n /** Absolute URL of the scanned page. */\n targetUrl: string;\n /** HTML of the page, rendered when possible. */\n html: string;\n /** Fetched JavaScript chunks. */\n chunks: BundleChunkInput[];\n /** Total transferred bytes measured for the page. */\n totalPageSize: number;\n /** Network request URLs observed while loading the page. */\n requestUrls?: string[];\n /** Live-page signals (globals, storage keys) for technology detection. */\n runtimeSignals?: Pick<\n TechnologyDetectionInput,\n 'globals' | 'storageKeys' | 'globalVersions'\n >;\n};\n\n/** Page-level information available before the network-bound checks. */\nexport type ScanPageInfo = Pick<\n ScanResult,\n 'metadata' | 'routing' | 'technologies' | 'locales'\n>;\n\n/** Options of {@link runScanChecks}. */\nexport type RunScanChecksOptions = ScanFetchOptions & {\n /** Called before each step, e.g. to stream a progress bar. */\n onProgress?: (progress: number, message: string) => void;\n /** Called once the page itself is analyzed (metadata, routing, stack). */\n onPageInfo?: (pageInfo: ScanPageInfo) => void;\n};\n\n/** Output of {@link runScanChecks}. */\nexport type ScanChecksResult = Pick<\n ScanResult,\n 'metadata' | 'routing' | 'technologies' | 'locales' | 'bundle'\n>;\n\n/** Extract title, description and absolute preview image. */\nconst extractPageMetadata = (html: string, targetUrl: string): PageMetadata => {\n const ogImage = extractOgImage(html);\n return {\n title: extractTitle(html),\n description: extractMetaDescription(html),\n image: ogImage ? (parseUrl(ogImage, targetUrl)?.href ?? ogImage) : '',\n };\n};\n\n/**\n * Run every i18n/SEO check on an already loaded page. Single implementation\n * shared by the `intlayer scan` CLI and the hosted audit (`/api/scan`), which\n * only differ in how they load the page.\n *\n * @param input - The loaded page (HTML, chunks, size, runtime signals).\n * @param emit - Receives every check result as soon as it is produced.\n * @param options - Network options and progress callbacks.\n */\nexport const runScanChecks = async (\n {\n targetUrl,\n html,\n chunks,\n totalPageSize,\n requestUrls = [],\n runtimeSignals,\n }: ScanPageInput,\n emit: EmitScanEvent,\n { onProgress, onPageInfo, ...fetchOptions }: RunScanChecksOptions\n): Promise<ScanChecksResult> => {\n const origin = new URL(targetUrl).origin;\n\n onProgress?.(20, 'Analyzing html attributes and hreflang tags...');\n\n const langTag = extractHtmlLang(html);\n const hreflangs = extractHreflangs(html);\n const routing = detectRoutingStrategy({\n pageUrl: targetUrl,\n htmlLang: langTag,\n hreflangs,\n });\n const signals: PageSignals = {\n targetUrl,\n langTag,\n dirTag: extractHtmlDir(html),\n ogLocale: extractOgLocale(html),\n ogLocaleAlternates: extractOgLocaleAlternates(html),\n canonicalHref: extractCanonicalHref(html),\n hreflangs,\n anchors: extractAnchors(html),\n routing,\n pageLocale: routing.urlLocale ?? langTag,\n };\n\n const locales = [\n ...new Set([...(langTag ? [langTag] : []), ...routing.locales]),\n ];\n const pageInfo: ScanPageInfo = {\n metadata: extractPageMetadata(html, targetUrl),\n routing,\n locales,\n technologies: detectTechnologies({\n pageUrl: targetUrl,\n html,\n resourceUrls: [\n ...new Set([...extractResourceUrls(html, targetUrl), ...requestUrls]),\n ],\n scripts: chunks.map(({ content }) => content),\n ...runtimeSignals,\n }),\n };\n onPageInfo?.(pageInfo);\n\n checkHtmlLang(signals, emit);\n checkHtmlDir(signals, emit);\n checkLocaleConsistency(signals, emit);\n checkOgLocale(signals, emit);\n checkCanonical(signals, emit);\n checkHreflang(signals, emit);\n checkXDefault(signals, emit);\n\n onProgress?.(35, 'Checking hreflang alternates...');\n await checkHreflangReciprocity(signals, fetchOptions, emit);\n\n onProgress?.(50, 'Analysing bundle content & leakage...');\n const bundle = checkBundleContent(\n chunks,\n html,\n signals.pageLocale,\n targetUrl,\n totalPageSize,\n emit\n );\n\n onProgress?.(60, 'Analyzing internal links...');\n checkInternalLinks(signals, emit);\n\n onProgress?.(70, 'Checking robots.txt...');\n const localizedUrls = hreflangs.flatMap(\n ({ href }) => parseUrl(href, targetUrl)?.href ?? []\n );\n const declaredSitemapUrls = await checkRobots(\n origin,\n localizedUrls,\n fetchOptions,\n emit\n );\n\n onProgress?.(85, 'Checking sitemaps...');\n await checkSitemap(origin, declaredSitemapUrls, routing, fetchOptions, emit);\n\n return { ...pageInfo, bundle };\n};\n"],"mappings":";;;;;;;;AA8EA,MAAM,uBAAuB,MAAc,cAAoC;CAC7E,MAAM,UAAU,eAAe,IAAI;CACnC,OAAO;EACL,OAAO,aAAa,IAAI;EACxB,aAAa,uBAAuB,IAAI;EACxC,OAAO,UAAW,SAAS,SAAS,SAAS,CAAC,EAAE,QAAQ,UAAW;CACrE;AACF;;;;;;;;;;AAWA,MAAa,gBAAgB,OAC3B,EACE,WACA,MACA,QACA,eACA,cAAc,CAAC,GACf,kBAEF,MACA,EAAE,YAAY,YAAY,GAAG,mBACC;CAC9B,MAAM,SAAS,IAAI,IAAI,SAAS,CAAC,CAAC;CAElC,aAAa,IAAI,gDAAgD;CAEjE,MAAM,UAAU,gBAAgB,IAAI;CACpC,MAAM,YAAY,iBAAiB,IAAI;CACvC,MAAM,UAAU,sBAAsB;EACpC,SAAS;EACT,UAAU;EACV;CACF,CAAC;CACD,MAAM,UAAuB;EAC3B;EACA;EACA,QAAQ,eAAe,IAAI;EAC3B,UAAU,gBAAgB,IAAI;EAC9B,oBAAoB,0BAA0B,IAAI;EAClD,eAAe,qBAAqB,IAAI;EACxC;EACA,SAAS,eAAe,IAAI;EAC5B;EACA,YAAY,QAAQ,aAAa;CACnC;CAEA,MAAM,UAAU,CACd,mBAAG,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,OAAO,IAAI,CAAC,GAAI,GAAG,QAAQ,OAAO,CAAC,CAChE;CACA,MAAM,WAAyB;EAC7B,UAAU,oBAAoB,MAAM,SAAS;EAC7C;EACA;EACA,cAAc,mBAAmB;GAC/B,SAAS;GACT;GACA,cAAc,CACZ,mBAAG,IAAI,IAAI,CAAC,GAAG,oBAAoB,MAAM,SAAS,GAAG,GAAG,WAAW,CAAC,CACtE;GACA,SAAS,OAAO,KAAK,EAAE,cAAc,OAAO;GAC5C,GAAG;EACL,CAAC;CACH;CACA,aAAa,QAAQ;CAErB,cAAc,SAAS,IAAI;CAC3B,aAAa,SAAS,IAAI;CAC1B,uBAAuB,SAAS,IAAI;CACpC,cAAc,SAAS,IAAI;CAC3B,eAAe,SAAS,IAAI;CAC5B,cAAc,SAAS,IAAI;CAC3B,cAAc,SAAS,IAAI;CAE3B,aAAa,IAAI,iCAAiC;CAClD,MAAM,yBAAyB,SAAS,cAAc,IAAI;CAE1D,aAAa,IAAI,uCAAuC;CACxD,MAAM,SAAS,mBACb,QACA,MACA,QAAQ,YACR,WACA,eACA,IACF;CAEA,aAAa,IAAI,6BAA6B;CAC9C,mBAAmB,SAAS,IAAI;CAEhC,aAAa,IAAI,wBAAwB;CACzC,MAAM,gBAAgB,UAAU,SAC7B,EAAE,WAAW,SAAS,MAAM,SAAS,CAAC,EAAE,QAAQ,CAAC,CACpD;CACA,MAAM,sBAAsB,MAAM,YAChC,QACA,eACA,cACA,IACF;CAEA,aAAa,IAAI,sBAAsB;CACvC,MAAM,aAAa,QAAQ,qBAAqB,SAAS,cAAc,IAAI;CAE3E,OAAO;EAAE,GAAG;EAAU;CAAO;AAC/B"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { fetchText } from "./fetchText.mjs";
|
|
1
2
|
import { mutateScore, toScorePercent } from "./calculateScore.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { getTechnologyGlobalNames } from "./detection/detectTechnologies.mjs";
|
|
4
|
+
import { byteLength, extractScriptUrls } from "./parseHtml.mjs";
|
|
5
|
+
import { runScanChecks } from "./runScanChecks.mjs";
|
|
4
6
|
import { GREY, GREY_LIGHT } from "@intlayer/config/colors";
|
|
5
7
|
import { colorize, logger } from "@intlayer/config/logger";
|
|
6
8
|
|
|
@@ -20,6 +22,22 @@ const logDeepScanRecommendation = () => {
|
|
|
20
22
|
]);
|
|
21
23
|
};
|
|
22
24
|
/**
|
|
25
|
+
* Read, in the page context, which technology globals exist and the version
|
|
26
|
+
* paths they expose. Serialized by puppeteer: must stay self-contained.
|
|
27
|
+
*/
|
|
28
|
+
const readRuntimeGlobals = (globalNames, versionPaths) => {
|
|
29
|
+
const pageWindow = window;
|
|
30
|
+
const globalVersions = {};
|
|
31
|
+
for (const versionPath of versionPaths) {
|
|
32
|
+
const value = versionPath.split(".").reduce((current, key) => current && typeof current === "object" ? current[key] : void 0, pageWindow);
|
|
33
|
+
if (typeof value === "string") globalVersions[versionPath] = value;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
globals: globalNames.filter((name) => pageWindow[name] !== void 0),
|
|
37
|
+
globalVersions
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
23
41
|
* Render the page with a locally installed `puppeteer` to capture
|
|
24
42
|
* client-rendered content, the accurate transfer size, and lazy-loaded chunks.
|
|
25
43
|
*
|
|
@@ -55,11 +73,13 @@ const runDeepScan = async (targetUrl, userAgent, timeoutMs) => {
|
|
|
55
73
|
await page.setExtraHTTPHeaders({ "Accept-Language": "en-US,en;q=0.9" });
|
|
56
74
|
const origin = new URL(targetUrl).origin;
|
|
57
75
|
const jsResponseMap = /* @__PURE__ */ new Map();
|
|
76
|
+
const requestUrls = [];
|
|
58
77
|
let totalPageSize = 0;
|
|
59
78
|
const pendingResponses = [];
|
|
60
79
|
page.on("response", (response) => {
|
|
61
80
|
pendingResponses.push((async () => {
|
|
62
81
|
try {
|
|
82
|
+
requestUrls.push(response.url());
|
|
63
83
|
if (response.status() !== 200) return;
|
|
64
84
|
const buffer = await response.buffer();
|
|
65
85
|
totalPageSize += buffer.length;
|
|
@@ -84,35 +104,27 @@ const runDeepScan = async (targetUrl, userAgent, timeoutMs) => {
|
|
|
84
104
|
isMainBundle: mainBundleUrls.has(url),
|
|
85
105
|
content
|
|
86
106
|
}));
|
|
107
|
+
const { globals: globalNames, versionGlobals } = getTechnologyGlobalNames();
|
|
108
|
+
const { globals, globalVersions } = await page.evaluate(readRuntimeGlobals, globalNames, versionGlobals).catch(() => ({
|
|
109
|
+
globals: [],
|
|
110
|
+
globalVersions: {}
|
|
111
|
+
}));
|
|
112
|
+
const cookies = await page.cookies().catch(() => []);
|
|
87
113
|
return {
|
|
88
114
|
html,
|
|
89
115
|
totalPageSize,
|
|
90
|
-
chunks
|
|
116
|
+
chunks,
|
|
117
|
+
requestUrls,
|
|
118
|
+
runtimeSignals: {
|
|
119
|
+
globals,
|
|
120
|
+
globalVersions,
|
|
121
|
+
storageKeys: cookies.map(({ name }) => name)
|
|
122
|
+
}
|
|
91
123
|
};
|
|
92
124
|
} finally {
|
|
93
125
|
if (browser) await browser.close();
|
|
94
126
|
}
|
|
95
127
|
};
|
|
96
|
-
/** Fetch the raw HTML document, measuring its byte size. */
|
|
97
|
-
const fetchHtml = async (url, userAgent, timeoutMs) => {
|
|
98
|
-
const controller = new AbortController();
|
|
99
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
100
|
-
try {
|
|
101
|
-
const response = await fetch(url, {
|
|
102
|
-
headers: {
|
|
103
|
-
"User-Agent": userAgent,
|
|
104
|
-
"Accept-Language": "en-US,en;q=0.9"
|
|
105
|
-
},
|
|
106
|
-
signal: controller.signal
|
|
107
|
-
});
|
|
108
|
-
return {
|
|
109
|
-
html: await response.text(),
|
|
110
|
-
finalUrl: response.url || url
|
|
111
|
-
};
|
|
112
|
-
} finally {
|
|
113
|
-
clearTimeout(timer);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
128
|
/**
|
|
117
129
|
* Fetch every eagerly-loaded script. Same-origin scripts keep their content so
|
|
118
130
|
* their locale weight can be analyzed; third-party scripts only contribute to
|
|
@@ -153,49 +165,43 @@ const fetchScripts = async (scriptUrls, origin, userAgent) => {
|
|
|
153
165
|
const scanWebsite = async (targetUrl, options = {}) => {
|
|
154
166
|
const { deep = true, timeoutMs = DEFAULT_TIMEOUT_MS, userAgent = DEFAULT_USER_AGENT } = options;
|
|
155
167
|
const origin = new URL(targetUrl).origin;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
const fetchOptions = {
|
|
169
|
+
userAgent,
|
|
170
|
+
timeoutMs
|
|
171
|
+
};
|
|
160
172
|
const deepResult = deep ? await runDeepScan(targetUrl, userAgent, timeoutMs) : null;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
chunks = deepResult.chunks;
|
|
165
|
-
totalPageSize = deepResult.totalPageSize;
|
|
166
|
-
} else {
|
|
173
|
+
let pageInput;
|
|
174
|
+
if (deepResult) pageInput = deepResult;
|
|
175
|
+
else {
|
|
167
176
|
if (deep) logDeepScanRecommendation();
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const scriptUrls = extractScriptUrls(
|
|
171
|
-
const { chunks
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
const response = await fetchText(targetUrl, fetchOptions);
|
|
178
|
+
if (!response?.ok) throw new Error(`Failed to fetch ${targetUrl}${response ? ` (HTTP ${response.status})` : ""}`);
|
|
179
|
+
const scriptUrls = extractScriptUrls(response.text, response.finalUrl);
|
|
180
|
+
const { chunks, scriptBytes } = await fetchScripts(scriptUrls, origin, userAgent);
|
|
181
|
+
pageInput = {
|
|
182
|
+
html: response.text,
|
|
183
|
+
chunks,
|
|
184
|
+
totalPageSize: byteLength(response.text) + scriptBytes
|
|
185
|
+
};
|
|
174
186
|
}
|
|
175
|
-
const htmlSize = byteLength(html);
|
|
176
187
|
const events = [];
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
checkUrlStructure(extractAnchors(html), origin, targetUrl, events);
|
|
182
|
-
const bundle = checkBundleContent(chunks, html, langTag, targetUrl, totalPageSize, events);
|
|
183
|
-
await checkRobots(origin, localesSet, userAgent, events);
|
|
184
|
-
await checkSitemap(origin, localesSet, userAgent, events);
|
|
188
|
+
const checksResult = await runScanChecks({
|
|
189
|
+
targetUrl,
|
|
190
|
+
...pageInput
|
|
191
|
+
}, (event) => events.push(event), fetchOptions);
|
|
185
192
|
const rawScore = events.reduce((score, event) => mutateScore(score, event), {
|
|
186
193
|
score: 0,
|
|
187
194
|
totalScore: 0
|
|
188
195
|
});
|
|
189
196
|
return {
|
|
190
197
|
url: targetUrl,
|
|
191
|
-
mode,
|
|
192
|
-
totalPageSize,
|
|
193
|
-
htmlSize,
|
|
198
|
+
mode: deepResult ? "deep" : "basic",
|
|
199
|
+
totalPageSize: pageInput.totalPageSize,
|
|
200
|
+
htmlSize: byteLength(pageInput.html),
|
|
194
201
|
score: toScorePercent(rawScore),
|
|
195
202
|
rawScore,
|
|
196
203
|
events,
|
|
197
|
-
|
|
198
|
-
bundle
|
|
204
|
+
...checksResult
|
|
199
205
|
};
|
|
200
206
|
};
|
|
201
207
|
|