@intlayer/chokidar 8.12.4 → 9.0.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs +21 -4
  2. package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs.map +1 -1
  3. package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs +94 -0
  4. package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs.map +1 -1
  5. package/dist/cjs/buildIntlayerDictionary/writeMergedDictionary.cjs +1 -1
  6. package/dist/cjs/buildIntlayerDictionary/writeMergedDictionary.cjs.map +1 -1
  7. package/dist/cjs/createType/createType.cjs.map +1 -1
  8. package/dist/cjs/init/index.cjs +63 -9
  9. package/dist/cjs/init/index.cjs.map +1 -1
  10. package/dist/cjs/init/utils/configManipulation.cjs +196 -0
  11. package/dist/cjs/init/utils/configManipulation.cjs.map +1 -1
  12. package/dist/cjs/init/utils/fileSystem.cjs +84 -0
  13. package/dist/cjs/init/utils/fileSystem.cjs.map +1 -1
  14. package/dist/cjs/init/utils/index.cjs +12 -0
  15. package/dist/cjs/init/utils/packageManager.cjs +187 -0
  16. package/dist/cjs/init/utils/packageManager.cjs.map +1 -0
  17. package/dist/cjs/scan/analyzeBundleContent.cjs +182 -0
  18. package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -0
  19. package/dist/cjs/scan/calculateScore.cjs +65 -0
  20. package/dist/cjs/scan/calculateScore.cjs.map +1 -0
  21. package/dist/cjs/scan/checks.cjs +274 -0
  22. package/dist/cjs/scan/checks.cjs.map +1 -0
  23. package/dist/cjs/scan/index.cjs +31 -0
  24. package/dist/cjs/scan/parseHtml.cjs +127 -0
  25. package/dist/cjs/scan/parseHtml.cjs.map +1 -0
  26. package/dist/cjs/scan/scanWebsite.cjs +205 -0
  27. package/dist/cjs/scan/scanWebsite.cjs.map +1 -0
  28. package/dist/cjs/scan/types.cjs +0 -0
  29. package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs +22 -5
  30. package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs.map +1 -1
  31. package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs +93 -1
  32. package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs.map +1 -1
  33. package/dist/esm/buildIntlayerDictionary/writeMergedDictionary.mjs +2 -2
  34. package/dist/esm/buildIntlayerDictionary/writeMergedDictionary.mjs.map +1 -1
  35. package/dist/esm/createType/createType.mjs.map +1 -1
  36. package/dist/esm/init/index.mjs +65 -11
  37. package/dist/esm/init/index.mjs.map +1 -1
  38. package/dist/esm/init/utils/configManipulation.mjs +190 -1
  39. package/dist/esm/init/utils/configManipulation.mjs.map +1 -1
  40. package/dist/esm/init/utils/fileSystem.mjs +83 -1
  41. package/dist/esm/init/utils/fileSystem.mjs.map +1 -1
  42. package/dist/esm/init/utils/index.mjs +4 -3
  43. package/dist/esm/init/utils/packageManager.mjs +183 -0
  44. package/dist/esm/init/utils/packageManager.mjs.map +1 -0
  45. package/dist/esm/scan/analyzeBundleContent.mjs +180 -0
  46. package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -0
  47. package/dist/esm/scan/calculateScore.mjs +61 -0
  48. package/dist/esm/scan/calculateScore.mjs.map +1 -0
  49. package/dist/esm/scan/checks.mjs +265 -0
  50. package/dist/esm/scan/checks.mjs.map +1 -0
  51. package/dist/esm/scan/index.mjs +7 -0
  52. package/dist/esm/scan/parseHtml.mjs +115 -0
  53. package/dist/esm/scan/parseHtml.mjs.map +1 -0
  54. package/dist/esm/scan/scanWebsite.mjs +203 -0
  55. package/dist/esm/scan/scanWebsite.mjs.map +1 -0
  56. package/dist/esm/scan/types.mjs +0 -0
  57. package/dist/types/buildIntlayerDictionary/buildIntlayerDictionary.d.ts.map +1 -1
  58. package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts +31 -4
  59. package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts.map +1 -1
  60. package/dist/types/buildIntlayerDictionary/writeMergedDictionary.d.ts +13 -3
  61. package/dist/types/buildIntlayerDictionary/writeMergedDictionary.d.ts.map +1 -1
  62. package/dist/types/createType/createType.d.ts +3 -3
  63. package/dist/types/createType/createType.d.ts.map +1 -1
  64. package/dist/types/formatDictionary.d.ts +9 -2
  65. package/dist/types/formatDictionary.d.ts.map +1 -1
  66. package/dist/types/init/index.d.ts.map +1 -1
  67. package/dist/types/init/utils/configManipulation.d.ts +42 -1
  68. package/dist/types/init/utils/configManipulation.d.ts.map +1 -1
  69. package/dist/types/init/utils/fileSystem.d.ts +31 -1
  70. package/dist/types/init/utils/fileSystem.d.ts.map +1 -1
  71. package/dist/types/init/utils/index.d.ts +4 -3
  72. package/dist/types/init/utils/packageManager.d.ts +59 -0
  73. package/dist/types/init/utils/packageManager.d.ts.map +1 -0
  74. package/dist/types/intlayer/dist/types/index.d.ts +4 -0
  75. package/dist/types/scan/analyzeBundleContent.d.ts +16 -0
  76. package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -0
  77. package/dist/types/scan/calculateScore.d.ts +65 -0
  78. package/dist/types/scan/calculateScore.d.ts.map +1 -0
  79. package/dist/types/scan/checks.d.ts +38 -0
  80. package/dist/types/scan/checks.d.ts.map +1 -0
  81. package/dist/types/scan/index.d.ts +7 -0
  82. package/dist/types/scan/parseHtml.d.ts +54 -0
  83. package/dist/types/scan/parseHtml.d.ts.map +1 -0
  84. package/dist/types/scan/scanWebsite.d.ts +18 -0
  85. package/dist/types/scan/scanWebsite.d.ts.map +1 -0
  86. package/dist/types/scan/types.d.ts +76 -0
  87. package/dist/types/scan/types.d.ts.map +1 -0
  88. package/package.json +17 -9
@@ -0,0 +1,265 @@
1
+ import { extractHreflangs, extractHtmlDir, extractHtmlLang, hasCanonical } from "./parseHtml.mjs";
2
+ import { analyzeBundleContent } from "./analyzeBundleContent.mjs";
3
+ import { ALL_LOCALES } from "@intlayer/types/allLocales";
4
+
5
+ //#region src/scan/checks.ts
6
+ /** Format a byte count as a human-readable size. */
7
+ const formatSize = (bytes) => {
8
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
9
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;
10
+ return `${bytes} B`;
11
+ };
12
+ /**
13
+ * Check the `<html>` element attributes (`lang`, `dir`) and the resulting
14
+ * current-locale signal. Returns the detected language tag.
15
+ */
16
+ const checkHtmlAttributes = (html, targetUrl, events) => {
17
+ const langTag = extractHtmlLang(html);
18
+ const dirTag = extractHtmlDir(html);
19
+ events.push({
20
+ type: `url_htmlLang\\${targetUrl}`,
21
+ status: langTag ? "success" : "error",
22
+ details: {
23
+ success: langTag,
24
+ error: langTag ? void 0 : "Missing html lang attribute"
25
+ }
26
+ });
27
+ events.push({
28
+ type: `url_currentLocale\\${targetUrl}`,
29
+ status: langTag ? "success" : "warning",
30
+ details: {
31
+ success: langTag,
32
+ warning: langTag ? void 0 : "No locale detected"
33
+ }
34
+ });
35
+ events.push({
36
+ type: `url_htmlDir\\${targetUrl}`,
37
+ status: dirTag ? "success" : "warning",
38
+ details: {
39
+ success: dirTag,
40
+ warning: dirTag ? void 0 : "Missing html dir attribute"
41
+ }
42
+ });
43
+ return { langTag };
44
+ };
45
+ /** Check the presence of a canonical link. */
46
+ const checkCanonical = (html, targetUrl, events) => {
47
+ const present = hasCanonical(html);
48
+ events.push({
49
+ type: `url_hasCanonical\\${targetUrl}`,
50
+ status: present ? "success" : "warning",
51
+ details: { warning: present ? void 0 : "Missing canonical link" }
52
+ });
53
+ };
54
+ /**
55
+ * Check the page's hreflang structure and collect discovered locales into
56
+ * `localesSet`.
57
+ */
58
+ const checkLinguisticStructure = (html, targetUrl, localesSet, events) => {
59
+ const langTag = extractHtmlLang(html);
60
+ if (langTag) localesSet.add(langTag);
61
+ const hreflangs = extractHreflangs(html);
62
+ for (const { hreflang } of hreflangs) if (hreflang !== "x-default") localesSet.add(hreflang);
63
+ const hasXDefault = hreflangs.some((h) => h.hreflang === "x-default");
64
+ events.push({
65
+ type: `url_hreflang\\${targetUrl}`,
66
+ status: hreflangs.length > 0 ? "success" : "warning",
67
+ details: {
68
+ success: hreflangs.length > 0 ? hreflangs : void 0,
69
+ warning: hreflangs.length === 0 ? "No hreflang tags found" : void 0
70
+ }
71
+ });
72
+ events.push({
73
+ type: `url_hasXDefault\\${targetUrl}`,
74
+ status: hasXDefault ? "success" : "error",
75
+ details: { error: hasXDefault ? void 0 : "Missing x-default hreflang link" }
76
+ });
77
+ };
78
+ const normalizeHost = (host) => host.startsWith("www.") ? host.slice(4) : host;
79
+ const localeValues = Object.values(ALL_LOCALES);
80
+ /**
81
+ * Check whether internal links carry a locale segment, mirroring the hosted
82
+ * audit's URL-structure analysis but operating on parsed anchors instead of a
83
+ * live DOM.
84
+ */
85
+ const checkUrlStructure = (anchors, origin, targetUrl, events) => {
86
+ const targetHostname = normalizeHost(new URL(origin).hostname);
87
+ let localizedCount = 0;
88
+ let totalInternalCount = 0;
89
+ const nonLocalizedLinks = [];
90
+ for (const { href } of anchors) {
91
+ if (!href || href.startsWith("#") || href.startsWith("javascript:")) continue;
92
+ try {
93
+ const url = new URL(href, origin);
94
+ if (normalizeHost(url.hostname) !== targetHostname) continue;
95
+ totalInternalCount++;
96
+ const path = url.pathname.toLowerCase();
97
+ const hostname = url.hostname.toLowerCase();
98
+ const hasLocaleInPath = localeValues.some((locale) => {
99
+ const l = locale.toLowerCase();
100
+ return path === `/${l}` || path.includes(`/${l}/`);
101
+ });
102
+ const hasLocaleInSubdomain = localeValues.some((locale) => {
103
+ const l = locale.toLowerCase();
104
+ return hostname.startsWith(`${l}.`) || hostname.includes(`.${l}.`);
105
+ });
106
+ if (hasLocaleInPath || hasLocaleInSubdomain) localizedCount++;
107
+ else nonLocalizedLinks.push(href);
108
+ } catch {}
109
+ }
110
+ const hasLocalizedLinks = localizedCount > 0;
111
+ const allAnchorsLocalized = totalInternalCount === 0 || localizedCount === totalInternalCount;
112
+ events.push({
113
+ type: `url_hasLocalizedLinks\\${targetUrl}`,
114
+ status: hasLocalizedLinks ? "success" : "warning",
115
+ details: {
116
+ success: hasLocalizedLinks ? `${localizedCount} localized links found out of ${totalInternalCount} internal links` : void 0,
117
+ warning: hasLocalizedLinks ? void 0 : "No localized links found"
118
+ }
119
+ });
120
+ events.push({
121
+ type: `url_allAnchorsLocalized\\${targetUrl}`,
122
+ status: allAnchorsLocalized ? "success" : "warning",
123
+ details: { warning: allAnchorsLocalized ? void 0 : {
124
+ message: "Some internal links are not localized",
125
+ links: nonLocalizedLinks
126
+ } }
127
+ });
128
+ };
129
+ /**
130
+ * Analyze the JavaScript bundles for unused locale content and emit the
131
+ * corresponding event. Returns the analysis so callers can report on it.
132
+ */
133
+ const checkBundleContent = (chunks, html, currentLocale, targetUrl, totalPageSize, events) => {
134
+ if (!currentLocale) {
135
+ events.push({
136
+ type: `url_unusedBundleContent\\${targetUrl}`,
137
+ status: "warning",
138
+ details: { warning: "Cannot analyse bundle content: page locale not detected" }
139
+ });
140
+ return;
141
+ }
142
+ const analysis = analyzeBundleContent(chunks, html, currentLocale, totalPageSize);
143
+ const mainBundleMaxUnused = analysis.mainBundleChunks.reduce((max, c) => Math.max(max, c.unusedPercent), 0);
144
+ const status = mainBundleMaxUnused === 0 ? "success" : mainBundleMaxUnused <= 30 ? "warning" : "error";
145
+ events.push({
146
+ type: `url_unusedBundleContent\\${targetUrl}`,
147
+ status,
148
+ details: { [status]: analysis }
149
+ });
150
+ return analysis;
151
+ };
152
+ /** Fetch and check `robots.txt`, emitting robots-related events. */
153
+ const checkRobots = async (origin, discoveredLocales, userAgent, events) => {
154
+ let robotsPresent = false;
155
+ let noLocalizedUrlsForgotten = true;
156
+ const errors = [];
157
+ try {
158
+ const response = await fetch(`${origin}/robots.txt`, { headers: { "User-Agent": userAgent } });
159
+ if (response.ok) {
160
+ robotsPresent = true;
161
+ const content = await response.text();
162
+ if (content && discoveredLocales.size > 0) {
163
+ const disallowedPaths = content.split("\n").map((line) => line.trim().toLowerCase()).filter((line) => line.startsWith("disallow:")).map((line) => line.slice(9).trim());
164
+ for (const locale of discoveredLocales) for (const path of disallowedPaths) if (path === `/${locale}` || path === `/${locale}/`) {
165
+ noLocalizedUrlsForgotten = false;
166
+ errors.push(`Locale path "${locale}" appears to be blocked in robots.txt: ${path}`);
167
+ }
168
+ }
169
+ }
170
+ } catch (error) {
171
+ errors.push(`Failed to fetch robots.txt: ${error instanceof Error ? error.message : "Unknown error"}`);
172
+ }
173
+ events.push({
174
+ type: "robots_robotsPresent",
175
+ status: robotsPresent ? "success" : "warning",
176
+ details: {
177
+ warning: robotsPresent ? void 0 : "No robots.txt found",
178
+ error: errors.length > 0 ? errors : void 0
179
+ }
180
+ });
181
+ if (robotsPresent) events.push({
182
+ type: "robots_noLocalizedUrlsForgotten",
183
+ status: noLocalizedUrlsForgotten ? "success" : "error",
184
+ details: { error: noLocalizedUrlsForgotten ? void 0 : errors }
185
+ });
186
+ };
187
+ /** Fetch and check `sitemap.xml`, emitting sitemap-related events. */
188
+ const checkSitemap = async (origin, discoveredLocales, userAgent, events) => {
189
+ let sitemapPresent = false;
190
+ let hasXDefault = false;
191
+ let hasAlternates = false;
192
+ let noLocalizedUrlsForgotten = true;
193
+ const errors = [];
194
+ try {
195
+ const response = await fetch(`${origin}/sitemap.xml`, { headers: { "User-Agent": userAgent } });
196
+ if (response.ok) {
197
+ sitemapPresent = true;
198
+ const content = await response.text();
199
+ const hreflangs = (content.match(/hreflang\s*=\s*"([^"]+)"/gi) ?? []).map((m) => m.replace(/hreflang\s*=\s*"/i, "").replace(/"$/, ""));
200
+ hasAlternates = hreflangs.length > 0;
201
+ hasXDefault = hreflangs.includes("x-default");
202
+ if (discoveredLocales.size > 0) {
203
+ const urlBlocks = content.match(/<url\b[\s\S]*?<\/url>/gi) ?? [];
204
+ const allFoundLocales = /* @__PURE__ */ new Set();
205
+ let anyUrlMissingLocale = false;
206
+ for (const block of urlBlocks) {
207
+ const localesInUrl = /* @__PURE__ */ new Set();
208
+ for (const hreflang of block.match(/hreflang\s*=\s*"([^"]+)"/gi) ?? []) {
209
+ const value = hreflang.replace(/hreflang\s*=\s*"/i, "").replace(/"$/, "");
210
+ if (value !== "x-default") {
211
+ localesInUrl.add(value);
212
+ allFoundLocales.add(value);
213
+ }
214
+ }
215
+ const loc = block.match(/<loc>([\s\S]*?)<\/loc>/i)?.[1]?.trim();
216
+ if (loc) try {
217
+ const firstSegment = new URL(loc).pathname.split("/").filter(Boolean)[0];
218
+ if (firstSegment && discoveredLocales.has(firstSegment)) {
219
+ localesInUrl.add(firstSegment);
220
+ allFoundLocales.add(firstSegment);
221
+ }
222
+ } catch {}
223
+ const missing = [...discoveredLocales].filter((locale) => !localesInUrl.has(locale));
224
+ if (missing.length > 0 && missing.length < discoveredLocales.size) anyUrlMissingLocale = true;
225
+ }
226
+ const completelyMissing = [...discoveredLocales].filter((locale) => !allFoundLocales.has(locale));
227
+ if (anyUrlMissingLocale || completelyMissing.length > 0) {
228
+ noLocalizedUrlsForgotten = false;
229
+ if (completelyMissing.length > 0) errors.push(`The following locales are completely missing from the sitemap: ${completelyMissing.join(", ")}`);
230
+ }
231
+ }
232
+ }
233
+ } catch (error) {
234
+ errors.push(`Failed to fetch sitemap.xml: ${error instanceof Error ? error.message : "Unknown error"}`);
235
+ }
236
+ events.push({
237
+ type: "sitemap_sitemapPresent",
238
+ status: sitemapPresent ? "success" : "warning",
239
+ details: {
240
+ warning: sitemapPresent ? void 0 : "No sitemap.xml found",
241
+ error: errors.length > 0 ? errors : void 0
242
+ }
243
+ });
244
+ if (sitemapPresent) {
245
+ events.push({
246
+ type: "sitemap_noLocalizedUrlsForgotten",
247
+ status: noLocalizedUrlsForgotten ? "success" : "warning",
248
+ details: { warning: noLocalizedUrlsForgotten ? void 0 : errors }
249
+ });
250
+ events.push({
251
+ type: "sitemap_hasXDefault",
252
+ status: hasXDefault ? "success" : "warning",
253
+ details: { warning: hasXDefault ? void 0 : "No x-default hreflang in sitemap" }
254
+ });
255
+ events.push({
256
+ type: "sitemap_hasAlternates",
257
+ status: hasAlternates ? "success" : "warning",
258
+ details: { warning: hasAlternates ? void 0 : "No alternate language links found in sitemap" }
259
+ });
260
+ }
261
+ };
262
+
263
+ //#endregion
264
+ export { checkBundleContent, checkCanonical, checkHtmlAttributes, checkLinguisticStructure, checkRobots, checkSitemap, checkUrlStructure, formatSize };
265
+ //# sourceMappingURL=checks.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks.mjs","names":[],"sources":["../../../src/scan/checks.ts"],"sourcesContent":["import { ALL_LOCALES } from '@intlayer/types/allLocales';\nimport { analyzeBundleContent } from './analyzeBundleContent';\nimport {\n type Anchor,\n extractHreflangs,\n extractHtmlDir,\n extractHtmlLang,\n hasCanonical,\n} from './parseHtml';\nimport type {\n BundleChunkInput,\n BundleContentAnalysis,\n ScanEvent,\n} from './types';\n\n/** Format a byte count as a human-readable size. */\nexport const formatSize = (bytes: number): string => {\n if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;\n if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;\n return `${bytes} B`;\n};\n\n/**\n * Check the `<html>` element attributes (`lang`, `dir`) and the resulting\n * current-locale signal. Returns the detected language tag.\n */\nexport const checkHtmlAttributes = (\n html: string,\n targetUrl: string,\n events: ScanEvent[]\n): { langTag: string | undefined } => {\n const langTag = extractHtmlLang(html);\n const dirTag = extractHtmlDir(html);\n\n events.push({\n type: `url_htmlLang\\\\${targetUrl}`,\n status: langTag ? 'success' : 'error',\n details: {\n success: langTag,\n error: langTag ? undefined : 'Missing html lang attribute',\n },\n });\n\n events.push({\n type: `url_currentLocale\\\\${targetUrl}`,\n status: langTag ? 'success' : 'warning',\n details: {\n success: langTag,\n warning: langTag ? undefined : 'No locale detected',\n },\n });\n\n events.push({\n type: `url_htmlDir\\\\${targetUrl}`,\n status: dirTag ? 'success' : 'warning',\n details: {\n success: dirTag,\n warning: dirTag ? undefined : 'Missing html dir attribute',\n },\n });\n\n return { langTag };\n};\n\n/** Check the presence of a canonical link. */\nexport const checkCanonical = (\n html: string,\n targetUrl: string,\n events: ScanEvent[]\n): void => {\n const present = hasCanonical(html);\n events.push({\n type: `url_hasCanonical\\\\${targetUrl}`,\n status: present ? 'success' : 'warning',\n details: { warning: present ? undefined : 'Missing canonical link' },\n });\n};\n\n/**\n * Check the page's hreflang structure and collect discovered locales into\n * `localesSet`.\n */\nexport const checkLinguisticStructure = (\n html: string,\n targetUrl: string,\n localesSet: Set<string>,\n events: ScanEvent[]\n): void => {\n const langTag = extractHtmlLang(html);\n if (langTag) localesSet.add(langTag);\n\n const hreflangs = extractHreflangs(html);\n for (const { hreflang } of hreflangs) {\n if (hreflang !== 'x-default') localesSet.add(hreflang);\n }\n\n const hasXDefault = hreflangs.some((h) => h.hreflang === 'x-default');\n\n events.push({\n type: `url_hreflang\\\\${targetUrl}`,\n status: hreflangs.length > 0 ? 'success' : 'warning',\n details: {\n success: hreflangs.length > 0 ? hreflangs : undefined,\n warning: hreflangs.length === 0 ? 'No hreflang tags found' : undefined,\n },\n });\n\n events.push({\n type: `url_hasXDefault\\\\${targetUrl}`,\n status: hasXDefault ? 'success' : 'error',\n details: {\n error: hasXDefault ? undefined : 'Missing x-default hreflang link',\n },\n });\n};\n\nconst normalizeHost = (host: string): string =>\n host.startsWith('www.') ? host.slice(4) : host;\n\nconst localeValues = Object.values(ALL_LOCALES) as string[];\n\n/**\n * Check whether internal links carry a locale segment, mirroring the hosted\n * audit's URL-structure analysis but operating on parsed anchors instead of a\n * live DOM.\n */\nexport const checkUrlStructure = (\n anchors: Anchor[],\n origin: string,\n targetUrl: string,\n events: ScanEvent[]\n): void => {\n const targetHostname = normalizeHost(new URL(origin).hostname);\n\n let localizedCount = 0;\n let totalInternalCount = 0;\n const nonLocalizedLinks: string[] = [];\n\n for (const { href } of anchors) {\n if (!href || href.startsWith('#') || href.startsWith('javascript:'))\n continue;\n\n try {\n const url = new URL(href, origin);\n if (normalizeHost(url.hostname) !== targetHostname) continue;\n\n totalInternalCount++;\n const path = url.pathname.toLowerCase();\n const hostname = url.hostname.toLowerCase();\n\n const hasLocaleInPath = localeValues.some((locale) => {\n const l = locale.toLowerCase();\n return path === `/${l}` || path.includes(`/${l}/`);\n });\n const hasLocaleInSubdomain = localeValues.some((locale) => {\n const l = locale.toLowerCase();\n return hostname.startsWith(`${l}.`) || hostname.includes(`.${l}.`);\n });\n\n if (hasLocaleInPath || hasLocaleInSubdomain) {\n localizedCount++;\n } else {\n nonLocalizedLinks.push(href);\n }\n } catch {\n /* ignore malformed URLs */\n }\n }\n\n const hasLocalizedLinks = localizedCount > 0;\n const allAnchorsLocalized =\n totalInternalCount === 0 || localizedCount === totalInternalCount;\n\n events.push({\n type: `url_hasLocalizedLinks\\\\${targetUrl}`,\n status: hasLocalizedLinks ? 'success' : 'warning',\n details: {\n success: hasLocalizedLinks\n ? `${localizedCount} localized links found out of ${totalInternalCount} internal links`\n : undefined,\n warning: hasLocalizedLinks ? undefined : 'No localized links found',\n },\n });\n\n events.push({\n type: `url_allAnchorsLocalized\\\\${targetUrl}`,\n status: allAnchorsLocalized ? 'success' : 'warning',\n details: {\n warning: allAnchorsLocalized\n ? undefined\n : {\n message: 'Some internal links are not localized',\n links: nonLocalizedLinks,\n },\n },\n });\n};\n\n/**\n * Analyze the JavaScript bundles for unused locale content and emit the\n * corresponding event. Returns the analysis so callers can report on it.\n */\nexport const checkBundleContent = (\n chunks: BundleChunkInput[],\n html: string,\n currentLocale: string | undefined,\n targetUrl: string,\n totalPageSize: number,\n events: ScanEvent[]\n): BundleContentAnalysis | undefined => {\n if (!currentLocale) {\n events.push({\n type: `url_unusedBundleContent\\\\${targetUrl}`,\n status: 'warning',\n details: {\n warning: 'Cannot analyse bundle content: page locale not detected',\n },\n });\n return undefined;\n }\n\n const analysis = analyzeBundleContent(\n chunks,\n html,\n currentLocale,\n totalPageSize\n );\n\n // Status is driven by the main bundle — lazy chunks with unused content are expected.\n const mainBundleMaxUnused = analysis.mainBundleChunks.reduce(\n (max, c) => Math.max(max, c.unusedPercent),\n 0\n );\n\n const status =\n mainBundleMaxUnused === 0\n ? 'success'\n : mainBundleMaxUnused <= 30\n ? 'warning'\n : 'error';\n\n events.push({\n type: `url_unusedBundleContent\\\\${targetUrl}`,\n status,\n details: { [status]: analysis },\n });\n\n return analysis;\n};\n\n/** Fetch and check `robots.txt`, emitting robots-related events. */\nexport const checkRobots = async (\n origin: string,\n discoveredLocales: Set<string>,\n userAgent: string,\n events: ScanEvent[]\n): Promise<void> => {\n let robotsPresent = false;\n let noLocalizedUrlsForgotten = true;\n const errors: string[] = [];\n\n try {\n const response = await fetch(`${origin}/robots.txt`, {\n headers: { 'User-Agent': userAgent },\n });\n\n if (response.ok) {\n robotsPresent = true;\n const content = await response.text();\n\n if (content && discoveredLocales.size > 0) {\n const disallowedPaths = content\n .split('\\n')\n .map((line) => line.trim().toLowerCase())\n .filter((line) => line.startsWith('disallow:'))\n .map((line) => line.slice('disallow:'.length).trim());\n\n for (const locale of discoveredLocales) {\n for (const path of disallowedPaths) {\n if (path === `/${locale}` || path === `/${locale}/`) {\n noLocalizedUrlsForgotten = false;\n errors.push(\n `Locale path \"${locale}\" appears to be blocked in robots.txt: ${path}`\n );\n }\n }\n }\n }\n }\n } catch (error) {\n errors.push(\n `Failed to fetch robots.txt: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n\n events.push({\n type: 'robots_robotsPresent',\n status: robotsPresent ? 'success' : 'warning',\n details: {\n warning: robotsPresent ? undefined : 'No robots.txt found',\n error: errors.length > 0 ? errors : undefined,\n },\n });\n\n if (robotsPresent) {\n events.push({\n type: 'robots_noLocalizedUrlsForgotten',\n status: noLocalizedUrlsForgotten ? 'success' : 'error',\n details: { error: noLocalizedUrlsForgotten ? undefined : errors },\n });\n }\n};\n\n/** Fetch and check `sitemap.xml`, emitting sitemap-related events. */\nexport const checkSitemap = async (\n origin: string,\n discoveredLocales: Set<string>,\n userAgent: string,\n events: ScanEvent[]\n): Promise<void> => {\n let sitemapPresent = false;\n let hasXDefault = false;\n let hasAlternates = false;\n let noLocalizedUrlsForgotten = true;\n const errors: string[] = [];\n\n try {\n const response = await fetch(`${origin}/sitemap.xml`, {\n headers: { 'User-Agent': userAgent },\n });\n\n if (response.ok) {\n sitemapPresent = true;\n const content = await response.text();\n\n const hreflangMatches = content.match(/hreflang\\s*=\\s*\"([^\"]+)\"/gi) ?? [];\n const hreflangs = hreflangMatches.map((m) =>\n m.replace(/hreflang\\s*=\\s*\"/i, '').replace(/\"$/, '')\n );\n hasAlternates = hreflangs.length > 0;\n hasXDefault = hreflangs.includes('x-default');\n\n if (discoveredLocales.size > 0) {\n const urlBlocks = content.match(/<url\\b[\\s\\S]*?<\\/url>/gi) ?? [];\n const allFoundLocales = new Set<string>();\n let anyUrlMissingLocale = false;\n\n for (const block of urlBlocks) {\n const localesInUrl = new Set<string>();\n\n for (const hreflang of block.match(/hreflang\\s*=\\s*\"([^\"]+)\"/gi) ??\n []) {\n const value = hreflang\n .replace(/hreflang\\s*=\\s*\"/i, '')\n .replace(/\"$/, '');\n if (value !== 'x-default') {\n localesInUrl.add(value);\n allFoundLocales.add(value);\n }\n }\n\n const loc = block.match(/<loc>([\\s\\S]*?)<\\/loc>/i)?.[1]?.trim();\n if (loc) {\n try {\n const firstSegment = new URL(loc).pathname\n .split('/')\n .filter(Boolean)[0];\n if (firstSegment && discoveredLocales.has(firstSegment)) {\n localesInUrl.add(firstSegment);\n allFoundLocales.add(firstSegment);\n }\n } catch {\n /* invalid loc URL, skip */\n }\n }\n\n const missing = [...discoveredLocales].filter(\n (locale) => !localesInUrl.has(locale)\n );\n if (missing.length > 0 && missing.length < discoveredLocales.size) {\n anyUrlMissingLocale = true;\n }\n }\n\n const completelyMissing = [...discoveredLocales].filter(\n (locale) => !allFoundLocales.has(locale)\n );\n if (anyUrlMissingLocale || completelyMissing.length > 0) {\n noLocalizedUrlsForgotten = false;\n if (completelyMissing.length > 0) {\n errors.push(\n `The following locales are completely missing from the sitemap: ${completelyMissing.join(', ')}`\n );\n }\n }\n }\n }\n } catch (error) {\n errors.push(\n `Failed to fetch sitemap.xml: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n\n events.push({\n type: 'sitemap_sitemapPresent',\n status: sitemapPresent ? 'success' : 'warning',\n details: {\n warning: sitemapPresent ? undefined : 'No sitemap.xml found',\n error: errors.length > 0 ? errors : undefined,\n },\n });\n\n if (sitemapPresent) {\n events.push({\n type: 'sitemap_noLocalizedUrlsForgotten',\n status: noLocalizedUrlsForgotten ? 'success' : 'warning',\n details: { warning: noLocalizedUrlsForgotten ? undefined : errors },\n });\n\n events.push({\n type: 'sitemap_hasXDefault',\n status: hasXDefault ? 'success' : 'warning',\n details: {\n warning: hasXDefault ? undefined : 'No x-default hreflang in sitemap',\n },\n });\n\n events.push({\n type: 'sitemap_hasAlternates',\n status: hasAlternates ? 'success' : 'warning',\n details: {\n warning: hasAlternates\n ? undefined\n : 'No alternate language links found in sitemap',\n },\n });\n }\n};\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { mutateScore, scoreRecord, toScorePercent } from "./calculateScore.mjs";
2
+ import { byteLength, extractAnchors, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractScriptUrls, extractTitle, extractVisibleTextStrings, hasCanonical } from "./parseHtml.mjs";
3
+ import { analyzeBundleContent } from "./analyzeBundleContent.mjs";
4
+ import { checkBundleContent, checkCanonical, checkHtmlAttributes, checkLinguisticStructure, checkRobots, checkSitemap, checkUrlStructure, formatSize } from "./checks.mjs";
5
+ import { scanWebsite } from "./scanWebsite.mjs";
6
+
7
+ export { analyzeBundleContent, byteLength, checkBundleContent, checkCanonical, checkHtmlAttributes, checkLinguisticStructure, checkRobots, checkSitemap, checkUrlStructure, extractAnchors, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractScriptUrls, extractTitle, extractVisibleTextStrings, formatSize, hasCanonical, mutateScore, scanWebsite, scoreRecord, toScorePercent };
@@ -0,0 +1,115 @@
1
+ //#region src/scan/parseHtml.ts
2
+ /**
3
+ * Tiny dependency-free HTML extraction helpers.
4
+ *
5
+ * The hosted backend audit relies on Cheerio + a real browser, but the CLI scan
6
+ * must stay dependency-light. These regex-based helpers cover the handful of
7
+ * head/anchor signals the score needs. They are intentionally forgiving: when a
8
+ * tag can't be parsed it is simply skipped rather than throwing.
9
+ */
10
+ /** Compute the UTF-8 byte length of a string in both Node and browser builds. */
11
+ const byteLength = (text) => typeof Buffer !== "undefined" ? Buffer.byteLength(text, "utf-8") : new TextEncoder().encode(text).length;
12
+ /** Read an attribute value off a single tag's attribute string. */
13
+ const readAttribute = (attributes, attributeName) => {
14
+ const match = attributes.match(new RegExp(`${attributeName}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
15
+ if (!match) return void 0;
16
+ return match[2] ?? match[3] ?? match[4];
17
+ };
18
+ /** Extract the `lang` attribute of the `<html>` element, if present. */
19
+ const extractHtmlLang = (html) => {
20
+ const htmlTag = html.match(/<html\b([^>]*)>/i);
21
+ return htmlTag ? readAttribute(htmlTag[1], "lang") : void 0;
22
+ };
23
+ /** Extract the `dir` attribute of the `<html>` element, if present. */
24
+ const extractHtmlDir = (html) => {
25
+ const htmlTag = html.match(/<html\b([^>]*)>/i);
26
+ return htmlTag ? readAttribute(htmlTag[1], "dir") : void 0;
27
+ };
28
+ /** Extract the document `<title>` text. */
29
+ const extractTitle = (html) => {
30
+ const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
31
+ return match ? match[1].trim() : "";
32
+ };
33
+ /** Extract the `<meta name="description">` content. */
34
+ const extractMetaDescription = (html) => {
35
+ const metas = html.match(/<meta\b[^>]*>/gi) ?? [];
36
+ for (const meta of metas) if (/name\s*=\s*("|')?description\1?/i.test(meta)) return readAttribute(meta, "content") ?? "";
37
+ return "";
38
+ };
39
+ /** Extract the `<meta property="og:image">` content. */
40
+ const extractOgImage = (html) => {
41
+ const metas = html.match(/<meta\b[^>]*>/gi) ?? [];
42
+ for (const meta of metas) if (/property\s*=\s*("|')?og:image\1?/i.test(meta)) return readAttribute(meta, "content");
43
+ };
44
+ /** Whether a `<link rel="canonical">` element is present. */
45
+ const hasCanonical = (html) => {
46
+ return (html.match(/<link\b[^>]*>/gi) ?? []).some((link) => /rel\s*=\s*("|')?canonical\1?/i.test(link));
47
+ };
48
+ /** Extract every `<link rel="alternate" hreflang="…" href="…">` element. */
49
+ const extractHreflangs = (html) => {
50
+ const links = html.match(/<link\b[^>]*>/gi) ?? [];
51
+ const result = [];
52
+ for (const link of links) {
53
+ if (!/rel\s*=\s*("|')?alternate\1?/i.test(link)) continue;
54
+ const hreflang = readAttribute(link, "hreflang");
55
+ const href = readAttribute(link, "href");
56
+ if (hreflang && href) result.push({
57
+ hreflang,
58
+ href
59
+ });
60
+ }
61
+ return result;
62
+ };
63
+ /**
64
+ * Extract every eagerly-loaded script URL: `<script src>`,
65
+ * `<link rel="modulepreload">` and `<link rel="preload" as="script">`.
66
+ *
67
+ * @param html - The raw HTML document.
68
+ * @param baseUrl - Base URL used to resolve relative script URLs.
69
+ * @returns Absolute, de-duplicated script URLs.
70
+ */
71
+ const extractScriptUrls = (html, baseUrl) => {
72
+ const urls = /* @__PURE__ */ new Set();
73
+ const add = (raw) => {
74
+ if (!raw) return;
75
+ try {
76
+ urls.add(new URL(raw, baseUrl).href);
77
+ } catch {}
78
+ };
79
+ for (const script of html.match(/<script\b[^>]*>/gi) ?? []) add(readAttribute(script, "src"));
80
+ for (const link of html.match(/<link\b[^>]*>/gi) ?? []) {
81
+ const rel = readAttribute(link, "rel")?.toLowerCase();
82
+ const as = readAttribute(link, "as")?.toLowerCase();
83
+ if (rel === "modulepreload" || rel === "preload" && as === "script") add(readAttribute(link, "href"));
84
+ }
85
+ return Array.from(urls);
86
+ };
87
+ /** Extract every `<a href="…">text</a>` anchor from the document. */
88
+ const extractAnchors = (html) => {
89
+ const anchors = [];
90
+ const anchorPattern = /<a\b([^>]*)>([\s\S]*?)<\/a>/gi;
91
+ let match = anchorPattern.exec(html);
92
+ while (match !== null) {
93
+ const href = readAttribute(match[1], "href");
94
+ if (href) {
95
+ const text = match[2].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
96
+ anchors.push({
97
+ href,
98
+ text
99
+ });
100
+ }
101
+ match = anchorPattern.exec(html);
102
+ }
103
+ return anchors;
104
+ };
105
+ /**
106
+ * Extract visible text snippets from an HTML document (scripts, styles and
107
+ * tags stripped). Used to approximate the rendered content size without a DOM.
108
+ */
109
+ const extractVisibleTextStrings = (html) => {
110
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<[^>]+>/g, "\n").split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter((line) => line.length > 1);
111
+ };
112
+
113
+ //#endregion
114
+ export { byteLength, extractAnchors, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractScriptUrls, extractTitle, extractVisibleTextStrings, hasCanonical };
115
+ //# sourceMappingURL=parseHtml.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,203 @@
1
+ import { mutateScore, toScorePercent } from "./calculateScore.mjs";
2
+ import { byteLength, extractAnchors, extractScriptUrls } from "./parseHtml.mjs";
3
+ import { checkBundleContent, checkCanonical, checkHtmlAttributes, checkLinguisticStructure, checkRobots, checkSitemap, checkUrlStructure } from "./checks.mjs";
4
+ import { colorize, logger } from "@intlayer/config/logger";
5
+ import { GREY, GREY_LIGHT } from "@intlayer/config/colors";
6
+
7
+ //#region src/scan/scanWebsite.ts
8
+ const DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; IntlayerScanBot/1.0; +https://intlayer.org)";
9
+ const DEFAULT_TIMEOUT_MS = 3e4;
10
+ /**
11
+ * Log a recommendation to install `puppeteer` for a deeper scan. Mirrors the
12
+ * style used by other optional-dependency hints across the CLI.
13
+ */
14
+ const logDeepScanRecommendation = () => {
15
+ logger([
16
+ colorize("Recommended: Install", GREY),
17
+ colorize("puppeteer", GREY_LIGHT),
18
+ colorize("package to enable a deeper scan (renders client-side content & lazy-loaded chunks). See documentation:", GREY),
19
+ colorize("https://intlayer.org/doc/concept/cli#scan", GREY_LIGHT)
20
+ ]);
21
+ };
22
+ /**
23
+ * Render the page with a locally installed `puppeteer` to capture
24
+ * client-rendered content, the accurate transfer size, and lazy-loaded chunks.
25
+ *
26
+ * `puppeteer` is imported dynamically through a non-literal specifier so it is
27
+ * never bundled and stays an optional dependency: when it is absent the import
28
+ * rejects and the caller falls back to the basic scan.
29
+ *
30
+ * @returns The deep-scan result, or `null` when `puppeteer` is unavailable.
31
+ */
32
+ const runDeepScan = async (targetUrl, userAgent, timeoutMs) => {
33
+ const moduleName = "puppeteer";
34
+ let puppeteer;
35
+ try {
36
+ const mod = await import(moduleName);
37
+ puppeteer = mod.default ?? mod;
38
+ } catch {
39
+ return null;
40
+ }
41
+ let browser;
42
+ try {
43
+ browser = await puppeteer.launch({
44
+ headless: true,
45
+ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
46
+ args: [
47
+ "--no-sandbox",
48
+ "--disable-setuid-sandbox",
49
+ "--disable-dev-shm-usage",
50
+ "--disable-gpu"
51
+ ]
52
+ });
53
+ const page = await browser.newPage();
54
+ await page.setUserAgent(userAgent);
55
+ await page.setExtraHTTPHeaders({ "Accept-Language": "en-US,en;q=0.9" });
56
+ const origin = new URL(targetUrl).origin;
57
+ const jsResponseMap = /* @__PURE__ */ new Map();
58
+ let totalPageSize = 0;
59
+ const pendingResponses = [];
60
+ page.on("response", (response) => {
61
+ pendingResponses.push((async () => {
62
+ try {
63
+ if (response.status() !== 200) return;
64
+ const buffer = await response.buffer();
65
+ totalPageSize += buffer.length;
66
+ const responseUrl = response.url();
67
+ if (((response.headers()["content-type"] ?? "").includes("javascript") || /\.(js|mjs|cjs)(\?|$)/.test(responseUrl)) && responseUrl.startsWith(origin)) jsResponseMap.set(responseUrl, buffer.toString("utf-8"));
68
+ } catch {}
69
+ })());
70
+ });
71
+ await page.goto(targetUrl, {
72
+ waitUntil: "domcontentloaded",
73
+ timeout: timeoutMs
74
+ });
75
+ await page.waitForNetworkIdle({
76
+ idleTime: 1e3,
77
+ timeout: 1e4
78
+ }).catch(() => {});
79
+ await Promise.allSettled(pendingResponses);
80
+ const html = await page.content();
81
+ const mainBundleUrls = new Set(extractScriptUrls(html, targetUrl));
82
+ const chunks = Array.from(jsResponseMap.entries()).map(([url, content]) => ({
83
+ url,
84
+ isMainBundle: mainBundleUrls.has(url),
85
+ content
86
+ }));
87
+ return {
88
+ html,
89
+ totalPageSize,
90
+ chunks
91
+ };
92
+ } finally {
93
+ if (browser) await browser.close();
94
+ }
95
+ };
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
+ /**
117
+ * Fetch every eagerly-loaded script. Same-origin scripts keep their content so
118
+ * their locale weight can be analyzed; third-party scripts only contribute to
119
+ * the measured page size (their locale-like keys cause false positives).
120
+ */
121
+ const fetchScripts = async (scriptUrls, origin, userAgent) => {
122
+ const chunks = [];
123
+ let scriptBytes = 0;
124
+ await Promise.all(scriptUrls.map(async (scriptUrl) => {
125
+ try {
126
+ const response = await fetch(scriptUrl, { headers: { "User-Agent": userAgent } });
127
+ if (!response.ok) return;
128
+ const content = await response.text();
129
+ scriptBytes += byteLength(content);
130
+ if (scriptUrl.startsWith(origin)) chunks.push({
131
+ url: scriptUrl,
132
+ isMainBundle: true,
133
+ content
134
+ });
135
+ } catch {}
136
+ }));
137
+ return {
138
+ chunks,
139
+ scriptBytes
140
+ };
141
+ };
142
+ /**
143
+ * Scan a single web page for i18n/SEO health and bundle weight.
144
+ *
145
+ * In `deep` mode (default) the page is rendered with a locally installed
146
+ * `puppeteer`; when `puppeteer` is missing the scan transparently falls back to
147
+ * a `basic` fetch-based pass and logs a recommendation to install it.
148
+ *
149
+ * @param targetUrl - The absolute URL to scan.
150
+ * @param options - {@link ScanOptions} controlling depth, timeout and UA.
151
+ * @returns The {@link ScanResult} including score, page size and per-check events.
152
+ */
153
+ const scanWebsite = async (targetUrl, options = {}) => {
154
+ const { deep = true, timeoutMs = DEFAULT_TIMEOUT_MS, userAgent = DEFAULT_USER_AGENT } = options;
155
+ const origin = new URL(targetUrl).origin;
156
+ let mode = "basic";
157
+ let html;
158
+ let totalPageSize;
159
+ let chunks;
160
+ const deepResult = deep ? await runDeepScan(targetUrl, userAgent, timeoutMs) : null;
161
+ if (deepResult) {
162
+ mode = "deep";
163
+ html = deepResult.html;
164
+ chunks = deepResult.chunks;
165
+ totalPageSize = deepResult.totalPageSize;
166
+ } else {
167
+ if (deep) logDeepScanRecommendation();
168
+ const { html: fetchedHtml, finalUrl } = await fetchHtml(targetUrl, userAgent, timeoutMs);
169
+ html = fetchedHtml;
170
+ const { chunks: fetchedChunks, scriptBytes } = await fetchScripts(extractScriptUrls(fetchedHtml, finalUrl), origin, userAgent);
171
+ chunks = fetchedChunks;
172
+ totalPageSize = byteLength(fetchedHtml) + scriptBytes;
173
+ }
174
+ const htmlSize = byteLength(html);
175
+ const events = [];
176
+ const localesSet = /* @__PURE__ */ new Set();
177
+ const { langTag } = checkHtmlAttributes(html, targetUrl, events);
178
+ checkCanonical(html, targetUrl, events);
179
+ checkLinguisticStructure(html, targetUrl, localesSet, events);
180
+ checkUrlStructure(extractAnchors(html), origin, targetUrl, events);
181
+ const bundle = checkBundleContent(chunks, html, langTag, targetUrl, totalPageSize, events);
182
+ await checkRobots(origin, localesSet, userAgent, events);
183
+ await checkSitemap(origin, localesSet, userAgent, events);
184
+ const rawScore = events.reduce((score, event) => mutateScore(score, event), {
185
+ score: 0,
186
+ totalScore: 0
187
+ });
188
+ return {
189
+ url: targetUrl,
190
+ mode,
191
+ totalPageSize,
192
+ htmlSize,
193
+ score: toScorePercent(rawScore),
194
+ rawScore,
195
+ events,
196
+ locales: Array.from(localesSet),
197
+ bundle
198
+ };
199
+ };
200
+
201
+ //#endregion
202
+ export { scanWebsite };
203
+ //# sourceMappingURL=scanWebsite.mjs.map