@intlayer/engine 9.5.9 → 9.5.11

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 (303) hide show
  1. package/dist/cjs/cli.cjs +7 -0
  2. package/dist/cjs/docReview/alignBlocks.cjs +53 -36
  3. package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
  4. package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
  5. package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
  6. package/dist/cjs/docReview/reviewReport.cjs.map +1 -1
  7. package/dist/cjs/docReview/segmentDocument.cjs +8 -5
  8. package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
  9. package/dist/cjs/init/cms.cjs +35 -0
  10. package/dist/cjs/init/cms.cjs.map +1 -1
  11. package/dist/cjs/init/documentationRouter.cjs +141 -0
  12. package/dist/cjs/init/documentationRouter.cjs.map +1 -0
  13. package/dist/cjs/init/frameworkSetup/backend/index.cjs +138 -0
  14. package/dist/cjs/init/frameworkSetup/backend/index.cjs.map +1 -0
  15. package/dist/cjs/init/frameworkSetup/backend/transforms.cjs +223 -0
  16. package/dist/cjs/init/frameworkSetup/backend/transforms.cjs.map +1 -0
  17. package/dist/cjs/init/frameworkSetup/index.cjs +9 -3
  18. package/dist/cjs/init/frameworkSetup/index.cjs.map +1 -1
  19. package/dist/cjs/init/frameworkSetup/localeRouting.cjs +13 -0
  20. package/dist/cjs/init/frameworkSetup/localeRouting.cjs.map +1 -0
  21. package/dist/cjs/init/frameworkSetup/nextAppRouter/index.cjs +34 -7
  22. package/dist/cjs/init/frameworkSetup/nextAppRouter/index.cjs.map +1 -1
  23. package/dist/cjs/init/frameworkSetup/nextAppRouter/templates.cjs +56 -0
  24. package/dist/cjs/init/frameworkSetup/nextAppRouter/templates.cjs.map +1 -1
  25. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +6 -3
  26. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
  27. package/dist/cjs/init/frameworkSetup/tanstackStart/index.cjs +32 -3
  28. package/dist/cjs/init/frameworkSetup/tanstackStart/index.cjs.map +1 -1
  29. package/dist/cjs/init/frameworkSetup/tanstackStart/templates.cjs +58 -0
  30. package/dist/cjs/init/frameworkSetup/tanstackStart/templates.cjs.map +1 -1
  31. package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs +94 -14
  32. package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs.map +1 -1
  33. package/dist/cjs/init/index.cjs +46 -159
  34. package/dist/cjs/init/index.cjs.map +1 -1
  35. package/dist/cjs/init/utils/configManipulation.cjs +39 -0
  36. package/dist/cjs/init/utils/configManipulation.cjs.map +1 -1
  37. package/dist/cjs/init/utils/index.cjs +10 -0
  38. package/dist/cjs/init/utils/lintConfig.cjs +52 -0
  39. package/dist/cjs/init/utils/lintConfig.cjs.map +1 -0
  40. package/dist/cjs/init/utils/packageManager.cjs +62 -13
  41. package/dist/cjs/init/utils/packageManager.cjs.map +1 -1
  42. package/dist/cjs/init/utils/urlRoutingFramework.cjs +41 -0
  43. package/dist/cjs/init/utils/urlRoutingFramework.cjs.map +1 -0
  44. package/dist/cjs/initConfig/index.cjs +4 -1
  45. package/dist/cjs/initConfig/index.cjs.map +1 -1
  46. package/dist/cjs/installMCP/installMCP.cjs +1 -1
  47. package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
  48. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
  49. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
  50. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +1 -1
  51. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
  52. package/dist/cjs/loadDictionaries/log.cjs +1 -1
  53. package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
  54. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +111 -43
  55. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
  56. package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
  57. package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
  58. package/dist/cjs/scan/calculateScore.cjs +4 -1
  59. package/dist/cjs/scan/calculateScore.cjs.map +1 -1
  60. package/dist/cjs/scan/checks.cjs +394 -221
  61. package/dist/cjs/scan/checks.cjs.map +1 -1
  62. package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
  63. package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
  64. package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
  65. package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
  66. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
  67. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
  68. package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
  69. package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
  70. package/dist/cjs/scan/detection/index.cjs +36 -0
  71. package/dist/cjs/scan/detection/localeCode.cjs +118 -0
  72. package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
  73. package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
  74. package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
  75. package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
  76. package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
  77. package/dist/cjs/scan/detection/url.cjs +53 -0
  78. package/dist/cjs/scan/detection/url.cjs.map +1 -0
  79. package/dist/cjs/scan/fetchText.cjs +56 -0
  80. package/dist/cjs/scan/fetchText.cjs.map +1 -0
  81. package/dist/cjs/scan/index.cjs +59 -3
  82. package/dist/cjs/scan/parseHtml.cjs +55 -10
  83. package/dist/cjs/scan/parseHtml.cjs.map +1 -1
  84. package/dist/cjs/scan/robots.cjs +65 -0
  85. package/dist/cjs/scan/robots.cjs.map +1 -0
  86. package/dist/cjs/scan/runScanChecks.cjs +89 -0
  87. package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
  88. package/dist/cjs/scan/scanWebsite.cjs +58 -52
  89. package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
  90. package/dist/cjs/scan/sitemap.cjs +109 -0
  91. package/dist/cjs/scan/sitemap.cjs.map +1 -0
  92. package/dist/cjs/utils/chunkJSON.cjs +24 -9
  93. package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
  94. package/dist/cjs/utils/getChunk.cjs +2 -2
  95. package/dist/cjs/utils/getChunk.cjs.map +1 -1
  96. package/dist/cjs/utils/runParallel/index.cjs +1 -1
  97. package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
  98. package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
  99. package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
  100. package/dist/cjs/utils/runParallel/ps.cjs +8 -3
  101. package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
  102. package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
  103. package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
  104. package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
  105. package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
  106. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
  107. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
  108. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
  109. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
  110. package/dist/esm/cli.mjs +5 -3
  111. package/dist/esm/docReview/alignBlocks.mjs +53 -36
  112. package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
  113. package/dist/esm/docReview/rebuildDocument.mjs +3 -1
  114. package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
  115. package/dist/esm/docReview/reviewReport.mjs.map +1 -1
  116. package/dist/esm/docReview/segmentDocument.mjs +8 -5
  117. package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
  118. package/dist/esm/init/cms.mjs +35 -2
  119. package/dist/esm/init/cms.mjs.map +1 -1
  120. package/dist/esm/init/documentationRouter.mjs +139 -0
  121. package/dist/esm/init/documentationRouter.mjs.map +1 -0
  122. package/dist/esm/init/frameworkSetup/backend/index.mjs +130 -0
  123. package/dist/esm/init/frameworkSetup/backend/index.mjs.map +1 -0
  124. package/dist/esm/init/frameworkSetup/backend/transforms.mjs +216 -0
  125. package/dist/esm/init/frameworkSetup/backend/transforms.mjs.map +1 -0
  126. package/dist/esm/init/frameworkSetup/index.mjs +9 -3
  127. package/dist/esm/init/frameworkSetup/index.mjs.map +1 -1
  128. package/dist/esm/init/frameworkSetup/localeRouting.mjs +12 -0
  129. package/dist/esm/init/frameworkSetup/localeRouting.mjs.map +1 -0
  130. package/dist/esm/init/frameworkSetup/nextAppRouter/index.mjs +36 -9
  131. package/dist/esm/init/frameworkSetup/nextAppRouter/index.mjs.map +1 -1
  132. package/dist/esm/init/frameworkSetup/nextAppRouter/templates.mjs +54 -1
  133. package/dist/esm/init/frameworkSetup/nextAppRouter/templates.mjs.map +1 -1
  134. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +6 -3
  135. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
  136. package/dist/esm/init/frameworkSetup/tanstackStart/index.mjs +35 -6
  137. package/dist/esm/init/frameworkSetup/tanstackStart/index.mjs.map +1 -1
  138. package/dist/esm/init/frameworkSetup/tanstackStart/templates.mjs +56 -1
  139. package/dist/esm/init/frameworkSetup/tanstackStart/templates.mjs.map +1 -1
  140. package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs +94 -15
  141. package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs.map +1 -1
  142. package/dist/esm/init/index.mjs +42 -158
  143. package/dist/esm/init/index.mjs.map +1 -1
  144. package/dist/esm/init/utils/configManipulation.mjs +38 -1
  145. package/dist/esm/init/utils/configManipulation.mjs.map +1 -1
  146. package/dist/esm/init/utils/index.mjs +5 -3
  147. package/dist/esm/init/utils/lintConfig.mjs +50 -0
  148. package/dist/esm/init/utils/lintConfig.mjs.map +1 -0
  149. package/dist/esm/init/utils/packageManager.mjs +60 -14
  150. package/dist/esm/init/utils/packageManager.mjs.map +1 -1
  151. package/dist/esm/init/utils/urlRoutingFramework.mjs +40 -0
  152. package/dist/esm/init/utils/urlRoutingFramework.mjs.map +1 -0
  153. package/dist/esm/initConfig/index.mjs +4 -1
  154. package/dist/esm/initConfig/index.mjs.map +1 -1
  155. package/dist/esm/installMCP/installMCP.mjs +1 -1
  156. package/dist/esm/installMCP/installMCP.mjs.map +1 -1
  157. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
  158. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
  159. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +1 -1
  160. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
  161. package/dist/esm/loadDictionaries/log.mjs +1 -1
  162. package/dist/esm/loadDictionaries/log.mjs.map +1 -1
  163. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +112 -44
  164. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
  165. package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
  166. package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
  167. package/dist/esm/scan/calculateScore.mjs +4 -1
  168. package/dist/esm/scan/calculateScore.mjs.map +1 -1
  169. package/dist/esm/scan/checks.mjs +387 -220
  170. package/dist/esm/scan/checks.mjs.map +1 -1
  171. package/dist/esm/scan/detection/checkDetails.mjs +54 -0
  172. package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
  173. package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
  174. package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
  175. package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
  176. package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
  177. package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
  178. package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
  179. package/dist/esm/scan/detection/index.mjs +10 -0
  180. package/dist/esm/scan/detection/localeCode.mjs +108 -0
  181. package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
  182. package/dist/esm/scan/detection/localizedPages.mjs +46 -0
  183. package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
  184. package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
  185. package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
  186. package/dist/esm/scan/detection/url.mjs +47 -0
  187. package/dist/esm/scan/detection/url.mjs.map +1 -0
  188. package/dist/esm/scan/fetchText.mjs +55 -0
  189. package/dist/esm/scan/fetchText.mjs.map +1 -0
  190. package/dist/esm/scan/index.mjs +15 -3
  191. package/dist/esm/scan/parseHtml.mjs +52 -11
  192. package/dist/esm/scan/parseHtml.mjs.map +1 -1
  193. package/dist/esm/scan/robots.mjs +63 -0
  194. package/dist/esm/scan/robots.mjs.map +1 -0
  195. package/dist/esm/scan/runScanChecks.mjs +88 -0
  196. package/dist/esm/scan/runScanChecks.mjs.map +1 -0
  197. package/dist/esm/scan/scanWebsite.mjs +59 -53
  198. package/dist/esm/scan/scanWebsite.mjs.map +1 -1
  199. package/dist/esm/scan/sitemap.mjs +105 -0
  200. package/dist/esm/scan/sitemap.mjs.map +1 -0
  201. package/dist/esm/utils/chunkJSON.mjs +24 -9
  202. package/dist/esm/utils/chunkJSON.mjs.map +1 -1
  203. package/dist/esm/utils/getChunk.mjs +2 -2
  204. package/dist/esm/utils/getChunk.mjs.map +1 -1
  205. package/dist/esm/utils/runParallel/index.mjs +1 -1
  206. package/dist/esm/utils/runParallel/index.mjs.map +1 -1
  207. package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
  208. package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
  209. package/dist/esm/utils/runParallel/ps.mjs +8 -3
  210. package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
  211. package/dist/esm/utils/runParallel/runTask.mjs +2 -1
  212. package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
  213. package/dist/esm/utils/runParallel/wmic.mjs +8 -3
  214. package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
  215. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
  216. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
  217. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
  218. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
  219. package/dist/types/cli.d.ts +5 -3
  220. package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
  221. package/dist/types/docReview/reviewReport.d.ts.map +1 -1
  222. package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
  223. package/dist/types/formatDictionary.d.ts +2 -2
  224. package/dist/types/init/cms.d.ts +11 -0
  225. package/dist/types/init/cms.d.ts.map +1 -1
  226. package/dist/types/init/documentationRouter.d.ts +64 -0
  227. package/dist/types/init/documentationRouter.d.ts.map +1 -0
  228. package/dist/types/init/frameworkSetup/backend/index.d.ts +16 -0
  229. package/dist/types/init/frameworkSetup/backend/index.d.ts.map +1 -0
  230. package/dist/types/init/frameworkSetup/backend/transforms.d.ts +37 -0
  231. package/dist/types/init/frameworkSetup/backend/transforms.d.ts.map +1 -0
  232. package/dist/types/init/frameworkSetup/index.d.ts.map +1 -1
  233. package/dist/types/init/frameworkSetup/localeRouting.d.ts +11 -0
  234. package/dist/types/init/frameworkSetup/localeRouting.d.ts.map +1 -0
  235. package/dist/types/init/frameworkSetup/nextAppRouter/index.d.ts +5 -5
  236. package/dist/types/init/frameworkSetup/nextAppRouter/index.d.ts.map +1 -1
  237. package/dist/types/init/frameworkSetup/nextAppRouter/templates.d.ts +12 -0
  238. package/dist/types/init/frameworkSetup/nextAppRouter/templates.d.ts.map +1 -1
  239. package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts +10 -1
  240. package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
  241. package/dist/types/init/frameworkSetup/tanstackStart/index.d.ts +3 -2
  242. package/dist/types/init/frameworkSetup/tanstackStart/index.d.ts.map +1 -1
  243. package/dist/types/init/frameworkSetup/tanstackStart/templates.d.ts +10 -0
  244. package/dist/types/init/frameworkSetup/tanstackStart/templates.d.ts.map +1 -1
  245. package/dist/types/init/frameworkSetup/tanstackStart/transforms.d.ts +12 -0
  246. package/dist/types/init/frameworkSetup/tanstackStart/transforms.d.ts.map +1 -1
  247. package/dist/types/init/frameworkSetup/types.d.ts +6 -0
  248. package/dist/types/init/frameworkSetup/types.d.ts.map +1 -1
  249. package/dist/types/init/index.d.ts +15 -1
  250. package/dist/types/init/index.d.ts.map +1 -1
  251. package/dist/types/init/utils/configManipulation.d.ts +14 -0
  252. package/dist/types/init/utils/configManipulation.d.ts.map +1 -1
  253. package/dist/types/init/utils/index.d.ts +5 -3
  254. package/dist/types/init/utils/lintConfig.d.ts +28 -0
  255. package/dist/types/init/utils/lintConfig.d.ts.map +1 -0
  256. package/dist/types/init/utils/packageManager.d.ts +29 -6
  257. package/dist/types/init/utils/packageManager.d.ts.map +1 -1
  258. package/dist/types/init/utils/urlRoutingFramework.d.ts +9 -0
  259. package/dist/types/init/utils/urlRoutingFramework.d.ts.map +1 -0
  260. package/dist/types/initConfig/index.d.ts +3 -1
  261. package/dist/types/initConfig/index.d.ts.map +1 -1
  262. package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts +4 -0
  263. package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
  264. package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
  265. package/dist/types/scan/calculateScore.d.ts +4 -1
  266. package/dist/types/scan/calculateScore.d.ts.map +1 -1
  267. package/dist/types/scan/checks.d.ts +114 -21
  268. package/dist/types/scan/checks.d.ts.map +1 -1
  269. package/dist/types/scan/detection/checkDetails.d.ts +29 -0
  270. package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
  271. package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
  272. package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
  273. package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
  274. package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
  275. package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
  276. package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
  277. package/dist/types/scan/detection/index.d.ts +9 -0
  278. package/dist/types/scan/detection/localeCode.d.ts +49 -0
  279. package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
  280. package/dist/types/scan/detection/localizedPages.d.ts +40 -0
  281. package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
  282. package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
  283. package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
  284. package/dist/types/scan/detection/url.d.ts +27 -0
  285. package/dist/types/scan/detection/url.d.ts.map +1 -0
  286. package/dist/types/scan/fetchText.d.ts +30 -0
  287. package/dist/types/scan/fetchText.d.ts.map +1 -0
  288. package/dist/types/scan/index.d.ts +16 -4
  289. package/dist/types/scan/parseHtml.d.ts +17 -0
  290. package/dist/types/scan/parseHtml.d.ts.map +1 -1
  291. package/dist/types/scan/robots.d.ts +23 -0
  292. package/dist/types/scan/robots.d.ts.map +1 -0
  293. package/dist/types/scan/runScanChecks.d.ts +43 -0
  294. package/dist/types/scan/runScanChecks.d.ts.map +1 -0
  295. package/dist/types/scan/scanWebsite.d.ts.map +1 -1
  296. package/dist/types/scan/sitemap.d.ts +49 -0
  297. package/dist/types/scan/sitemap.d.ts.map +1 -0
  298. package/dist/types/scan/types.d.ts +15 -0
  299. package/dist/types/scan/types.d.ts.map +1 -1
  300. package/dist/types/utils/chunkJSON.d.ts.map +1 -1
  301. package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
  302. package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
  303. package/package.json +16 -8
@@ -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"}
@@ -1,7 +1,19 @@
1
+ import { fetchText } from "./fetchText.mjs";
1
2
  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 { 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 { checkBundleContent, checkCanonical, checkHtmlAttributes, checkLinguisticStructure, checkRobots, checkSitemap, checkUrlStructure, formatSize } from "./checks.mjs";
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, checkHtmlAttributes, checkLinguisticStructure, checkRobots, checkSitemap, checkUrlStructure, extractAnchors, extractHreflangs, extractHtmlDir, extractHtmlLang, extractMetaDescription, extractOgImage, extractScriptUrls, extractTitle, extractVisibleTextStrings, formatSize, hasCanonical, mutateScore, scanWebsite, scoreRecord, toScorePercent };
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?/i.test(meta)) return readAttribute(meta, "content");
42
+ for (const meta of metas) if (/property\s*=\s*("|')?og:image\1?[\s>/]/i.test(meta)) return readAttribute(meta, "content");
43
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));
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 href = readAttribute(match[1], "href");
109
+ const rawAttrs = match[1] ?? "";
110
+ const rawContent = match[2] ?? "";
111
+ const href = readAttribute(rawAttrs, "href");
94
112
  if (href) {
95
- const text = match[2].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
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":"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"}
@@ -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 { byteLength, extractAnchors, extractScriptUrls } from "./parseHtml.mjs";
3
- import { checkBundleContent, checkCanonical, checkHtmlAttributes, checkLinguisticStructure, checkRobots, checkSitemap, checkUrlStructure } from "./checks.mjs";
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
- let mode = "basic";
157
- let html;
158
- let totalPageSize;
159
- let chunks;
168
+ const fetchOptions = {
169
+ userAgent,
170
+ timeoutMs
171
+ };
160
172
  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 {
173
+ let pageInput;
174
+ if (deepResult) pageInput = deepResult;
175
+ else {
167
176
  if (deep) logDeepScanRecommendation();
168
- const { html: fetchedHtml, finalUrl } = await fetchHtml(targetUrl, userAgent, timeoutMs);
169
- html = fetchedHtml;
170
- const scriptUrls = extractScriptUrls(fetchedHtml, finalUrl);
171
- const { chunks: fetchedChunks, scriptBytes } = await fetchScripts(scriptUrls, origin, userAgent);
172
- chunks = fetchedChunks;
173
- totalPageSize = byteLength(fetchedHtml) + scriptBytes;
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 localesSet = /* @__PURE__ */ new Set();
178
- const { langTag } = checkHtmlAttributes(html, targetUrl, events);
179
- checkCanonical(html, targetUrl, events);
180
- checkLinguisticStructure(html, targetUrl, localesSet, events);
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
- locales: Array.from(localesSet),
198
- bundle
204
+ ...checksResult
199
205
  };
200
206
  };
201
207