@intlayer/engine 9.5.10 → 9.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/dist/assets/installSkills/skills/bundle-optimization.md +123 -0
  2. package/dist/assets/installSkills/skills/compat.md +0 -4
  3. package/dist/assets/installSkills/skills/content.md +8 -2
  4. package/dist/assets/installSkills/skills/next-js.md +2 -2
  5. package/dist/assets/installSkills/skills/usage.md +1 -1
  6. package/dist/cjs/build.cjs +5 -0
  7. package/dist/cjs/cli.cjs +1 -0
  8. package/dist/cjs/docReview/alignBlocks.cjs +53 -36
  9. package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
  10. package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
  11. package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
  12. package/dist/cjs/docReview/segmentDocument.cjs +8 -5
  13. package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
  14. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +4 -1
  15. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
  16. package/dist/cjs/init/index.cjs +2 -12
  17. package/dist/cjs/init/index.cjs.map +1 -1
  18. package/dist/cjs/init/utils/backendPackages.cjs +15 -0
  19. package/dist/cjs/init/utils/backendPackages.cjs.map +1 -0
  20. package/dist/cjs/init/utils/index.cjs +2 -3
  21. package/dist/cjs/installMCP/installMCP.cjs +1 -1
  22. package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
  23. package/dist/cjs/installSkills/index.cjs +2 -0
  24. package/dist/cjs/installSkills/index.cjs.map +1 -1
  25. package/dist/cjs/loadDictionaries/loadDictionaries.cjs +9 -8
  26. package/dist/cjs/loadDictionaries/loadDictionaries.cjs.map +1 -1
  27. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
  28. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
  29. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +4 -6
  30. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
  31. package/dist/cjs/loadDictionaries/log.cjs +1 -1
  32. package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
  33. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +19 -12
  34. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
  35. package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs +18 -0
  36. package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs.map +1 -0
  37. package/dist/cjs/prepareIntlayerServer.cjs +132 -0
  38. package/dist/cjs/prepareIntlayerServer.cjs.map +1 -0
  39. package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
  40. package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
  41. package/dist/cjs/scan/calculateScore.cjs +4 -1
  42. package/dist/cjs/scan/calculateScore.cjs.map +1 -1
  43. package/dist/cjs/scan/checks.cjs +394 -221
  44. package/dist/cjs/scan/checks.cjs.map +1 -1
  45. package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
  46. package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
  47. package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
  48. package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
  49. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
  50. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
  51. package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
  52. package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
  53. package/dist/cjs/scan/detection/index.cjs +36 -0
  54. package/dist/cjs/scan/detection/localeCode.cjs +118 -0
  55. package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
  56. package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
  57. package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
  58. package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
  59. package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
  60. package/dist/cjs/scan/detection/url.cjs +53 -0
  61. package/dist/cjs/scan/detection/url.cjs.map +1 -0
  62. package/dist/cjs/scan/fetchText.cjs +56 -0
  63. package/dist/cjs/scan/fetchText.cjs.map +1 -0
  64. package/dist/cjs/scan/index.cjs +59 -3
  65. package/dist/cjs/scan/parseHtml.cjs +55 -10
  66. package/dist/cjs/scan/parseHtml.cjs.map +1 -1
  67. package/dist/cjs/scan/robots.cjs +65 -0
  68. package/dist/cjs/scan/robots.cjs.map +1 -0
  69. package/dist/cjs/scan/runScanChecks.cjs +89 -0
  70. package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
  71. package/dist/cjs/scan/scanWebsite.cjs +58 -52
  72. package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
  73. package/dist/cjs/scan/sitemap.cjs +109 -0
  74. package/dist/cjs/scan/sitemap.cjs.map +1 -0
  75. package/dist/cjs/utils/chunkJSON.cjs +24 -9
  76. package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
  77. package/dist/cjs/utils/getChunk.cjs +2 -2
  78. package/dist/cjs/utils/getChunk.cjs.map +1 -1
  79. package/dist/cjs/utils/getProcessChainCommands.cjs +117 -0
  80. package/dist/cjs/utils/getProcessChainCommands.cjs.map +1 -0
  81. package/dist/cjs/utils/index.cjs +2 -0
  82. package/dist/cjs/utils/runParallel/index.cjs +1 -1
  83. package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
  84. package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
  85. package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
  86. package/dist/cjs/utils/runParallel/ps.cjs +8 -3
  87. package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
  88. package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
  89. package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
  90. package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
  91. package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
  92. package/dist/cjs/utils/startContentWatcher.cjs +99 -0
  93. package/dist/cjs/utils/startContentWatcher.cjs.map +1 -0
  94. package/dist/cjs/watcher.cjs +2 -2
  95. package/dist/cjs/watcher.cjs.map +1 -1
  96. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
  97. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
  98. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
  99. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
  100. package/dist/cjs/writeJsonIfChanged.cjs.map +1 -1
  101. package/dist/esm/build.mjs +2 -1
  102. package/dist/esm/cli.mjs +2 -2
  103. package/dist/esm/docReview/alignBlocks.mjs +53 -36
  104. package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
  105. package/dist/esm/docReview/rebuildDocument.mjs +3 -1
  106. package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
  107. package/dist/esm/docReview/segmentDocument.mjs +8 -5
  108. package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
  109. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +4 -1
  110. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
  111. package/dist/esm/init/index.mjs +1 -11
  112. package/dist/esm/init/index.mjs.map +1 -1
  113. package/dist/esm/init/utils/backendPackages.mjs +14 -0
  114. package/dist/esm/init/utils/backendPackages.mjs.map +1 -0
  115. package/dist/esm/init/utils/index.mjs +2 -2
  116. package/dist/esm/installMCP/installMCP.mjs +1 -1
  117. package/dist/esm/installMCP/installMCP.mjs.map +1 -1
  118. package/dist/esm/installSkills/index.mjs +2 -0
  119. package/dist/esm/installSkills/index.mjs.map +1 -1
  120. package/dist/esm/loadDictionaries/loadDictionaries.mjs +9 -8
  121. package/dist/esm/loadDictionaries/loadDictionaries.mjs.map +1 -1
  122. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
  123. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
  124. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +3 -5
  125. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
  126. package/dist/esm/loadDictionaries/log.mjs +1 -1
  127. package/dist/esm/loadDictionaries/log.mjs.map +1 -1
  128. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +19 -12
  129. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
  130. package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs +16 -0
  131. package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs.map +1 -0
  132. package/dist/esm/prepareIntlayerServer.mjs +128 -0
  133. package/dist/esm/prepareIntlayerServer.mjs.map +1 -0
  134. package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
  135. package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
  136. package/dist/esm/scan/calculateScore.mjs +4 -1
  137. package/dist/esm/scan/calculateScore.mjs.map +1 -1
  138. package/dist/esm/scan/checks.mjs +387 -220
  139. package/dist/esm/scan/checks.mjs.map +1 -1
  140. package/dist/esm/scan/detection/checkDetails.mjs +54 -0
  141. package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
  142. package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
  143. package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
  144. package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
  145. package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
  146. package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
  147. package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
  148. package/dist/esm/scan/detection/index.mjs +10 -0
  149. package/dist/esm/scan/detection/localeCode.mjs +108 -0
  150. package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
  151. package/dist/esm/scan/detection/localizedPages.mjs +46 -0
  152. package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
  153. package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
  154. package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
  155. package/dist/esm/scan/detection/url.mjs +47 -0
  156. package/dist/esm/scan/detection/url.mjs.map +1 -0
  157. package/dist/esm/scan/fetchText.mjs +55 -0
  158. package/dist/esm/scan/fetchText.mjs.map +1 -0
  159. package/dist/esm/scan/index.mjs +15 -3
  160. package/dist/esm/scan/parseHtml.mjs +52 -11
  161. package/dist/esm/scan/parseHtml.mjs.map +1 -1
  162. package/dist/esm/scan/robots.mjs +63 -0
  163. package/dist/esm/scan/robots.mjs.map +1 -0
  164. package/dist/esm/scan/runScanChecks.mjs +88 -0
  165. package/dist/esm/scan/runScanChecks.mjs.map +1 -0
  166. package/dist/esm/scan/scanWebsite.mjs +59 -53
  167. package/dist/esm/scan/scanWebsite.mjs.map +1 -1
  168. package/dist/esm/scan/sitemap.mjs +105 -0
  169. package/dist/esm/scan/sitemap.mjs.map +1 -0
  170. package/dist/esm/utils/chunkJSON.mjs +24 -9
  171. package/dist/esm/utils/chunkJSON.mjs.map +1 -1
  172. package/dist/esm/utils/getChunk.mjs +2 -2
  173. package/dist/esm/utils/getChunk.mjs.map +1 -1
  174. package/dist/esm/utils/getProcessChainCommands.mjs +115 -0
  175. package/dist/esm/utils/getProcessChainCommands.mjs.map +1 -0
  176. package/dist/esm/utils/index.mjs +2 -1
  177. package/dist/esm/utils/runParallel/index.mjs +1 -1
  178. package/dist/esm/utils/runParallel/index.mjs.map +1 -1
  179. package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
  180. package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
  181. package/dist/esm/utils/runParallel/ps.mjs +8 -3
  182. package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
  183. package/dist/esm/utils/runParallel/runTask.mjs +2 -1
  184. package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
  185. package/dist/esm/utils/runParallel/wmic.mjs +8 -3
  186. package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
  187. package/dist/esm/utils/startContentWatcher.mjs +98 -0
  188. package/dist/esm/utils/startContentWatcher.mjs.map +1 -0
  189. package/dist/esm/watcher.mjs +1 -1
  190. package/dist/esm/watcher.mjs.map +1 -1
  191. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
  192. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
  193. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
  194. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
  195. package/dist/esm/writeJsonIfChanged.mjs +1 -1
  196. package/dist/esm/writeJsonIfChanged.mjs.map +1 -1
  197. package/dist/types/build.d.ts +2 -1
  198. package/dist/types/cli.d.ts +2 -2
  199. package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
  200. package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
  201. package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
  202. package/dist/types/init/index.d.ts +1 -1
  203. package/dist/types/init/index.d.ts.map +1 -1
  204. package/dist/types/init/utils/backendPackages.d.ts +5 -0
  205. package/dist/types/init/utils/backendPackages.d.ts.map +1 -0
  206. package/dist/types/init/utils/index.d.ts +2 -2
  207. package/dist/types/installSkills/index.d.ts +1 -0
  208. package/dist/types/installSkills/index.d.ts.map +1 -1
  209. package/dist/types/loadDictionaries/loadRemoteDictionaries.d.ts.map +1 -1
  210. package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
  211. package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts +14 -0
  212. package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts.map +1 -0
  213. package/dist/types/prepareIntlayerServer.d.ts +67 -0
  214. package/dist/types/prepareIntlayerServer.d.ts.map +1 -0
  215. package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
  216. package/dist/types/scan/calculateScore.d.ts +4 -1
  217. package/dist/types/scan/calculateScore.d.ts.map +1 -1
  218. package/dist/types/scan/checks.d.ts +114 -21
  219. package/dist/types/scan/checks.d.ts.map +1 -1
  220. package/dist/types/scan/detection/checkDetails.d.ts +29 -0
  221. package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
  222. package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
  223. package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
  224. package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
  225. package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
  226. package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
  227. package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
  228. package/dist/types/scan/detection/index.d.ts +9 -0
  229. package/dist/types/scan/detection/localeCode.d.ts +49 -0
  230. package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
  231. package/dist/types/scan/detection/localizedPages.d.ts +40 -0
  232. package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
  233. package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
  234. package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
  235. package/dist/types/scan/detection/url.d.ts +27 -0
  236. package/dist/types/scan/detection/url.d.ts.map +1 -0
  237. package/dist/types/scan/fetchText.d.ts +30 -0
  238. package/dist/types/scan/fetchText.d.ts.map +1 -0
  239. package/dist/types/scan/index.d.ts +16 -4
  240. package/dist/types/scan/parseHtml.d.ts +17 -0
  241. package/dist/types/scan/parseHtml.d.ts.map +1 -1
  242. package/dist/types/scan/robots.d.ts +23 -0
  243. package/dist/types/scan/robots.d.ts.map +1 -0
  244. package/dist/types/scan/runScanChecks.d.ts +43 -0
  245. package/dist/types/scan/runScanChecks.d.ts.map +1 -0
  246. package/dist/types/scan/scanWebsite.d.ts.map +1 -1
  247. package/dist/types/scan/sitemap.d.ts +49 -0
  248. package/dist/types/scan/sitemap.d.ts.map +1 -0
  249. package/dist/types/scan/types.d.ts +15 -0
  250. package/dist/types/scan/types.d.ts.map +1 -1
  251. package/dist/types/utils/chunkJSON.d.ts.map +1 -1
  252. package/dist/types/utils/getProcessChainCommands.d.ts +35 -0
  253. package/dist/types/utils/getProcessChainCommands.d.ts.map +1 -0
  254. package/dist/types/utils/index.d.ts +2 -1
  255. package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
  256. package/dist/types/utils/startContentWatcher.d.ts +40 -0
  257. package/dist/types/utils/startContentWatcher.d.ts.map +1 -0
  258. package/dist/types/watcher.d.ts.map +1 -1
  259. package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
  260. package/package.json +15 -7
  261. package/dist/cjs/init/utils/devScript.cjs +0 -57
  262. package/dist/cjs/init/utils/devScript.cjs.map +0 -1
  263. package/dist/esm/init/utils/devScript.mjs +0 -55
  264. package/dist/esm/init/utils/devScript.mjs.map +0 -1
  265. package/dist/types/init/utils/devScript.d.ts +0 -48
  266. package/dist/types/init/utils/devScript.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"scanWebsite.mjs","names":[],"sources":["../../../src/scan/scanWebsite.ts"],"sourcesContent":["import { GREY, GREY_LIGHT } from '@intlayer/config/colors';\nimport { colorize, logger } from '@intlayer/config/logger';\nimport { mutateScore, type Score, toScorePercent } from './calculateScore';\nimport {\n checkBundleContent,\n checkCanonical,\n checkHtmlAttributes,\n checkLinguisticStructure,\n checkRobots,\n checkSitemap,\n checkUrlStructure,\n} from './checks';\nimport { byteLength, extractAnchors, extractScriptUrls } from './parseHtml';\nimport type {\n BundleChunkInput,\n ScanEvent,\n ScanOptions,\n ScanResult,\n} from './types';\n\nconst DEFAULT_USER_AGENT =\n 'Mozilla/5.0 (compatible; IntlayerScanBot/1.0; +https://intlayer.org)';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Log a recommendation to install `puppeteer` for a deeper scan. Mirrors the\n * style used by other optional-dependency hints across the CLI.\n */\nconst logDeepScanRecommendation = (): void => {\n logger([\n colorize('Recommended: Install', GREY),\n colorize('puppeteer', GREY_LIGHT),\n colorize(\n 'package to enable a deeper scan (renders client-side content & lazy-loaded chunks). See documentation:',\n GREY\n ),\n colorize('https://intlayer.org/doc/concept/cli#scan', GREY_LIGHT),\n ]);\n};\n\n/** Outcome of a render-based deep scan. */\ntype DeepScanResult = {\n html: string;\n totalPageSize: number;\n chunks: BundleChunkInput[];\n};\n\n/**\n * Render the page with a locally installed `puppeteer` to capture\n * client-rendered content, the accurate transfer size, and lazy-loaded chunks.\n *\n * `puppeteer` is imported dynamically through a non-literal specifier so it is\n * never bundled and stays an optional dependency: when it is absent the import\n * rejects and the caller falls back to the basic scan.\n *\n * @returns The deep-scan result, or `null` when `puppeteer` is unavailable.\n */\nconst runDeepScan = async (\n targetUrl: string,\n userAgent: string,\n timeoutMs: number\n): Promise<DeepScanResult | null> => {\n const moduleName = 'puppeteer';\n // Optional dependency resolved at runtime — typed as `any` because it is not\n // a declared dependency of this package.\n let puppeteer: any;\n try {\n const mod: any = await import(moduleName);\n puppeteer = mod.default ?? mod;\n } catch {\n return null;\n }\n\n let browser: any;\n try {\n browser = await puppeteer.launch({\n headless: true,\n executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,\n args: [\n '--no-sandbox',\n '--disable-setuid-sandbox',\n '--disable-dev-shm-usage',\n '--disable-gpu',\n ],\n });\n\n const page = await browser.newPage();\n await page.setUserAgent(userAgent);\n await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' });\n\n const origin = new URL(targetUrl).origin;\n const jsResponseMap = new Map<string, string>();\n let totalPageSize = 0;\n const pendingResponses: Promise<void>[] = [];\n\n page.on('response', (response: any) => {\n pendingResponses.push(\n (async () => {\n try {\n if (response.status() !== 200) return;\n const buffer = await response.buffer();\n totalPageSize += buffer.length;\n\n const responseUrl: string = response.url();\n const contentType: string =\n response.headers()['content-type'] ?? '';\n const isJavaScript =\n contentType.includes('javascript') ||\n /\\.(js|mjs|cjs)(\\?|$)/.test(responseUrl);\n if (isJavaScript && responseUrl.startsWith(origin)) {\n jsResponseMap.set(responseUrl, buffer.toString('utf-8'));\n }\n } catch {\n /* response already consumed or aborted */\n }\n })()\n );\n });\n\n await page.goto(targetUrl, {\n waitUntil: 'domcontentloaded',\n timeout: timeoutMs,\n });\n await page\n .waitForNetworkIdle({ idleTime: 1000, timeout: 10_000 })\n .catch(() => {\n /* ok if it never fully idles */\n });\n await Promise.allSettled(pendingResponses);\n\n const html: string = await page.content();\n const mainBundleUrls = new Set(extractScriptUrls(html, targetUrl));\n const chunks: BundleChunkInput[] = Array.from(jsResponseMap.entries()).map(\n ([url, content]) => ({\n url,\n isMainBundle: mainBundleUrls.has(url),\n content,\n })\n );\n\n return { html, totalPageSize, chunks };\n } finally {\n if (browser) await browser.close();\n }\n};\n\n/** Fetch the raw HTML document, measuring its byte size. */\nconst fetchHtml = async (\n url: string,\n userAgent: string,\n timeoutMs: number\n): Promise<{ html: string; finalUrl: string }> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const response = await fetch(url, {\n headers: { 'User-Agent': userAgent, 'Accept-Language': 'en-US,en;q=0.9' },\n signal: controller.signal,\n });\n const html = await response.text();\n return { html, finalUrl: response.url || url };\n } finally {\n clearTimeout(timer);\n }\n};\n\n/**\n * Fetch every eagerly-loaded script. Same-origin scripts keep their content so\n * their locale weight can be analyzed; third-party scripts only contribute to\n * the measured page size (their locale-like keys cause false positives).\n */\nconst fetchScripts = async (\n scriptUrls: string[],\n origin: string,\n userAgent: string\n): Promise<{ chunks: BundleChunkInput[]; scriptBytes: number }> => {\n const chunks: BundleChunkInput[] = [];\n let scriptBytes = 0;\n\n await Promise.all(\n scriptUrls.map(async (scriptUrl) => {\n try {\n const response = await fetch(scriptUrl, {\n headers: { 'User-Agent': userAgent },\n });\n if (!response.ok) return;\n const content = await response.text();\n scriptBytes += byteLength(content);\n if (scriptUrl.startsWith(origin)) {\n chunks.push({ url: scriptUrl, isMainBundle: true, content });\n }\n } catch {\n /* ignore unreachable scripts */\n }\n })\n );\n\n return { chunks, scriptBytes };\n};\n\n/**\n * Scan a single web page for i18n/SEO health and bundle weight.\n *\n * In `deep` mode (default) the page is rendered with a locally installed\n * `puppeteer`; when `puppeteer` is missing the scan transparently falls back to\n * a `basic` fetch-based pass and logs a recommendation to install it.\n *\n * @param targetUrl - The absolute URL to scan.\n * @param options - {@link ScanOptions} controlling depth, timeout and UA.\n * @returns The {@link ScanResult} including score, page size and per-check events.\n */\nexport const scanWebsite = async (\n targetUrl: string,\n options: ScanOptions = {}\n): Promise<ScanResult> => {\n const {\n deep = true,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n userAgent = DEFAULT_USER_AGENT,\n } = options;\n\n const origin = new URL(targetUrl).origin;\n\n let mode: ScanResult['mode'] = 'basic';\n let html: string;\n let totalPageSize: number;\n let chunks: BundleChunkInput[];\n\n const deepResult = deep\n ? await runDeepScan(targetUrl, userAgent, timeoutMs)\n : null;\n\n if (deepResult) {\n mode = 'deep';\n html = deepResult.html;\n chunks = deepResult.chunks;\n totalPageSize = deepResult.totalPageSize;\n } else {\n if (deep) logDeepScanRecommendation();\n const { html: fetchedHtml, finalUrl } = await fetchHtml(\n targetUrl,\n userAgent,\n timeoutMs\n );\n html = fetchedHtml;\n const scriptUrls = extractScriptUrls(fetchedHtml, finalUrl);\n const { chunks: fetchedChunks, scriptBytes } = await fetchScripts(\n scriptUrls,\n origin,\n userAgent\n );\n chunks = fetchedChunks;\n totalPageSize = byteLength(fetchedHtml) + scriptBytes;\n }\n\n const htmlSize = byteLength(html);\n\n const events: ScanEvent[] = [];\n const localesSet = new Set<string>();\n\n const { langTag } = checkHtmlAttributes(html, targetUrl, events);\n checkCanonical(html, targetUrl, events);\n checkLinguisticStructure(html, targetUrl, localesSet, events);\n checkUrlStructure(extractAnchors(html), origin, targetUrl, events);\n const bundle = checkBundleContent(\n chunks,\n html,\n langTag,\n targetUrl,\n totalPageSize,\n events\n );\n await checkRobots(origin, localesSet, userAgent, events);\n await checkSitemap(origin, localesSet, userAgent, events);\n\n const rawScore = events.reduce<Score>(\n (score, event) => mutateScore(score, event),\n { score: 0, totalScore: 0 }\n );\n\n return {\n url: targetUrl,\n mode,\n totalPageSize,\n htmlSize,\n score: toScorePercent(rawScore),\n rawScore,\n events,\n locales: Array.from(localesSet),\n bundle,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"scanWebsite.mjs","names":[],"sources":["../../../src/scan/scanWebsite.ts"],"sourcesContent":["import { GREY, GREY_LIGHT } from '@intlayer/config/colors';\nimport { colorize, logger } from '@intlayer/config/logger';\nimport { mutateScore, type Score, toScorePercent } from './calculateScore';\nimport { getTechnologyGlobalNames } from './detection/detectTechnologies';\nimport { fetchText } from './fetchText';\nimport { byteLength, extractScriptUrls } from './parseHtml';\nimport { runScanChecks, type ScanPageInput } from './runScanChecks';\nimport type {\n BundleChunkInput,\n ScanEvent,\n ScanOptions,\n ScanResult,\n} from './types';\n\nconst DEFAULT_USER_AGENT =\n 'Mozilla/5.0 (compatible; IntlayerScanBot/1.0; +https://intlayer.org)';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Log a recommendation to install `puppeteer` for a deeper scan. Mirrors the\n * style used by other optional-dependency hints across the CLI.\n */\nconst logDeepScanRecommendation = (): void => {\n logger([\n colorize('Recommended: Install', GREY),\n colorize('puppeteer', GREY_LIGHT),\n colorize(\n 'package to enable a deeper scan (renders client-side content & lazy-loaded chunks). See documentation:',\n GREY\n ),\n colorize('https://intlayer.org/doc/concept/cli#scan', GREY_LIGHT),\n ]);\n};\n\n/** Outcome of a render-based deep scan. */\ntype DeepScanResult = Required<Omit<ScanPageInput, 'targetUrl'>>;\n\n/**\n * Read, in the page context, which technology globals exist and the version\n * paths they expose. Serialized by puppeteer: must stay self-contained.\n */\nconst readRuntimeGlobals = (\n globalNames: string[],\n versionPaths: string[]\n): { globals: string[]; globalVersions: Record<string, string> } => {\n const pageWindow = window as unknown as Record<string, unknown>;\n const globalVersions: Record<string, string> = {};\n for (const versionPath of versionPaths) {\n const value = versionPath\n .split('.')\n .reduce<unknown>(\n (current, key) =>\n current && typeof current === 'object'\n ? (current as Record<string, unknown>)[key]\n : undefined,\n pageWindow\n );\n if (typeof value === 'string') globalVersions[versionPath] = value;\n }\n return {\n globals: globalNames.filter((name) => pageWindow[name] !== undefined),\n globalVersions,\n };\n};\n\n/**\n * Render the page with a locally installed `puppeteer` to capture\n * client-rendered content, the accurate transfer size, and lazy-loaded chunks.\n *\n * `puppeteer` is imported dynamically through a non-literal specifier so it is\n * never bundled and stays an optional dependency: when it is absent the import\n * rejects and the caller falls back to the basic scan.\n *\n * @returns The deep-scan result, or `null` when `puppeteer` is unavailable.\n */\nconst runDeepScan = async (\n targetUrl: string,\n userAgent: string,\n timeoutMs: number\n): Promise<DeepScanResult | null> => {\n const moduleName = 'puppeteer';\n // Optional dependency resolved at runtime — typed as `any` because it is not\n // a declared dependency of this package.\n let puppeteer: any;\n try {\n const mod: any = await import(moduleName);\n puppeteer = mod.default ?? mod;\n } catch {\n return null;\n }\n\n let browser: any;\n try {\n browser = await puppeteer.launch({\n headless: true,\n executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,\n args: [\n '--no-sandbox',\n '--disable-setuid-sandbox',\n '--disable-dev-shm-usage',\n '--disable-gpu',\n ],\n });\n\n const page = await browser.newPage();\n await page.setUserAgent(userAgent);\n await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' });\n\n const origin = new URL(targetUrl).origin;\n const jsResponseMap = new Map<string, string>();\n const requestUrls: string[] = [];\n let totalPageSize = 0;\n const pendingResponses: Promise<void>[] = [];\n\n page.on('response', (response: any) => {\n pendingResponses.push(\n (async () => {\n try {\n requestUrls.push(response.url());\n if (response.status() !== 200) return;\n const buffer = await response.buffer();\n totalPageSize += buffer.length;\n\n const responseUrl: string = response.url();\n const contentType: string =\n response.headers()['content-type'] ?? '';\n const isJavaScript =\n contentType.includes('javascript') ||\n /\\.(js|mjs|cjs)(\\?|$)/.test(responseUrl);\n if (isJavaScript && responseUrl.startsWith(origin)) {\n jsResponseMap.set(responseUrl, buffer.toString('utf-8'));\n }\n } catch {\n /* response already consumed or aborted */\n }\n })()\n );\n });\n\n await page.goto(targetUrl, {\n waitUntil: 'domcontentloaded',\n timeout: timeoutMs,\n });\n await page\n .waitForNetworkIdle({ idleTime: 1000, timeout: 10_000 })\n .catch(() => {\n /* ok if it never fully idles */\n });\n await Promise.allSettled(pendingResponses);\n\n const html: string = await page.content();\n const mainBundleUrls = new Set(extractScriptUrls(html, targetUrl));\n const chunks: BundleChunkInput[] = Array.from(jsResponseMap.entries()).map(\n ([url, content]) => ({\n url,\n isMainBundle: mainBundleUrls.has(url),\n content,\n })\n );\n\n const { globals: globalNames, versionGlobals } = getTechnologyGlobalNames();\n const { globals, globalVersions } = await page\n .evaluate(readRuntimeGlobals, globalNames, versionGlobals)\n .catch(() => ({ globals: [], globalVersions: {} }));\n const cookies: { name: string }[] = await page.cookies().catch(() => []);\n\n return {\n html,\n totalPageSize,\n chunks,\n requestUrls,\n runtimeSignals: {\n globals,\n globalVersions,\n storageKeys: cookies.map(({ name }) => name),\n },\n };\n } finally {\n if (browser) await browser.close();\n }\n};\n\n/**\n * Fetch every eagerly-loaded script. Same-origin scripts keep their content so\n * their locale weight can be analyzed; third-party scripts only contribute to\n * the measured page size (their locale-like keys cause false positives).\n */\nconst fetchScripts = async (\n scriptUrls: string[],\n origin: string,\n userAgent: string\n): Promise<{ chunks: BundleChunkInput[]; scriptBytes: number }> => {\n const chunks: BundleChunkInput[] = [];\n let scriptBytes = 0;\n\n await Promise.all(\n scriptUrls.map(async (scriptUrl) => {\n try {\n const response = await fetch(scriptUrl, {\n headers: { 'User-Agent': userAgent },\n });\n if (!response.ok) return;\n const content = await response.text();\n scriptBytes += byteLength(content);\n if (scriptUrl.startsWith(origin)) {\n chunks.push({ url: scriptUrl, isMainBundle: true, content });\n }\n } catch {\n /* ignore unreachable scripts */\n }\n })\n );\n\n return { chunks, scriptBytes };\n};\n\n/**\n * Scan a single web page for i18n/SEO health and bundle weight.\n *\n * In `deep` mode (default) the page is rendered with a locally installed\n * `puppeteer`; when `puppeteer` is missing the scan transparently falls back to\n * a `basic` fetch-based pass and logs a recommendation to install it.\n *\n * @param targetUrl - The absolute URL to scan.\n * @param options - {@link ScanOptions} controlling depth, timeout and UA.\n * @returns The {@link ScanResult} including score, page size and per-check events.\n */\nexport const scanWebsite = async (\n targetUrl: string,\n options: ScanOptions = {}\n): Promise<ScanResult> => {\n const {\n deep = true,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n userAgent = DEFAULT_USER_AGENT,\n } = options;\n\n const origin = new URL(targetUrl).origin;\n const fetchOptions = { userAgent, timeoutMs };\n\n const deepResult = deep\n ? await runDeepScan(targetUrl, userAgent, timeoutMs)\n : null;\n\n let pageInput: Omit<ScanPageInput, 'targetUrl'>;\n\n if (deepResult) {\n pageInput = deepResult;\n } else {\n if (deep) logDeepScanRecommendation();\n const response = await fetchText(targetUrl, fetchOptions);\n if (!response?.ok) {\n throw new Error(\n `Failed to fetch ${targetUrl}${response ? ` (HTTP ${response.status})` : ''}`\n );\n }\n const scriptUrls = extractScriptUrls(response.text, response.finalUrl);\n const { chunks, scriptBytes } = await fetchScripts(\n scriptUrls,\n origin,\n userAgent\n );\n pageInput = {\n html: response.text,\n chunks,\n totalPageSize: byteLength(response.text) + scriptBytes,\n };\n }\n\n const events: ScanEvent[] = [];\n const checksResult = await runScanChecks(\n { targetUrl, ...pageInput },\n (event) => events.push(event),\n fetchOptions\n );\n\n const rawScore = events.reduce<Score>(\n (score, event) => mutateScore(score, event),\n { score: 0, totalScore: 0 }\n );\n\n return {\n url: targetUrl,\n mode: deepResult ? 'deep' : 'basic',\n totalPageSize: pageInput.totalPageSize,\n htmlSize: byteLength(pageInput.html),\n score: toScorePercent(rawScore),\n rawScore,\n events,\n ...checksResult,\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ import { fetchText } from "./fetchText.mjs";
2
+ import { parseRobots } from "./robots.mjs";
3
+
4
+ //#region src/scan/sitemap.ts
5
+ const MAX_CHILD_SITEMAPS = 5;
6
+ const MAX_ENTRIES = 5e3;
7
+ /** Decode the XML entities that can appear in sitemap URLs. */
8
+ const decodeXmlEntities = (text) => text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'");
9
+ /** Read an attribute of an XML tag, accepting both quote styles. */
10
+ const readXmlAttribute = (tag, name) => {
11
+ const match = tag.match(new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i"));
12
+ return match ? decodeXmlEntities(match[2] ?? match[3] ?? "") : void 0;
13
+ };
14
+ /** Extract every `<loc>` value of an XML fragment. */
15
+ const extractLocs = (xml) => Array.from(xml.matchAll(/<loc>\s*([\s\S]*?)\s*<\/loc>/gi), (match) => decodeXmlEntities((match[1] ?? "").replace(/^<!\[CDATA\[|\]\]>$/g, "")));
16
+ /**
17
+ * Parse a sitemap or sitemap index (regex based, no XML dependency). Handles
18
+ * `<xhtml:link rel="alternate" hreflang>` alternates in any namespace prefix.
19
+ */
20
+ const parseSitemap = (xml) => {
21
+ if (/<sitemapindex\b/i.test(xml)) return {
22
+ entries: [],
23
+ childSitemapUrls: extractLocs(xml)
24
+ };
25
+ const entries = [];
26
+ for (const [urlBlock] of xml.matchAll(/<url\b[\s\S]*?<\/url>/gi)) {
27
+ const loc = extractLocs(urlBlock)[0];
28
+ if (!loc) continue;
29
+ const alternates = [];
30
+ for (const [linkTag] of urlBlock.matchAll(/<(?:\w+:)?link\b[^>]*>/gi)) {
31
+ const hreflang = readXmlAttribute(linkTag, "hreflang");
32
+ const href = readXmlAttribute(linkTag, "href");
33
+ if (hreflang && href) alternates.push({
34
+ hreflang,
35
+ href
36
+ });
37
+ }
38
+ entries.push({
39
+ loc,
40
+ alternates
41
+ });
42
+ }
43
+ return {
44
+ entries,
45
+ childSitemapUrls: []
46
+ };
47
+ };
48
+ /**
49
+ * Fetch the given sitemaps, following sitemap indexes one level deep, and
50
+ * gather their `<url>` entries.
51
+ *
52
+ * @param sitemapUrls - Sitemaps to read (from `robots.txt`, else defaults).
53
+ * @param fetchOptions - Network options, including the SSRF guard.
54
+ */
55
+ const collectSitemapEntries = async (sitemapUrls, fetchOptions) => {
56
+ const fetchedSitemapUrls = [];
57
+ const entries = [];
58
+ const visitedUrls = /* @__PURE__ */ new Set();
59
+ const queue = [...sitemapUrls];
60
+ let childSitemapCount = 0;
61
+ while (queue.length > 0 && entries.length < MAX_ENTRIES) {
62
+ const sitemapUrl = queue.shift();
63
+ if (visitedUrls.has(sitemapUrl)) continue;
64
+ visitedUrls.add(sitemapUrl);
65
+ const response = await fetchText(sitemapUrl, fetchOptions);
66
+ if (!response?.ok || !/<(urlset|sitemapindex)\b/i.test(response.text)) continue;
67
+ fetchedSitemapUrls.push(sitemapUrl);
68
+ const parsedSitemap = parseSitemap(response.text);
69
+ entries.push(...parsedSitemap.entries);
70
+ for (const childUrl of parsedSitemap.childSitemapUrls) {
71
+ if (childSitemapCount >= MAX_CHILD_SITEMAPS) break;
72
+ childSitemapCount++;
73
+ queue.push(childUrl);
74
+ }
75
+ }
76
+ return {
77
+ fetchedSitemapUrls,
78
+ entries: entries.slice(0, MAX_ENTRIES),
79
+ isTruncated: entries.length >= MAX_ENTRIES || queue.length > 0
80
+ };
81
+ };
82
+ /** Default sitemap locations tried when `robots.txt` declares none. */
83
+ const getDefaultSitemapUrls = (origin) => [`${origin}/sitemap.xml`, `${origin}/sitemap_index.xml`];
84
+ /**
85
+ * List the page URLs of a site from its sitemaps (primary `<loc>` and every
86
+ * hreflang alternate), used to seed a multi-page audit.
87
+ *
88
+ * @returns De-duplicated URLs, or `[targetUrl]` when no sitemap is found.
89
+ */
90
+ const discoverSitemapUrls = async (targetUrl, fetchOptions) => {
91
+ const { origin } = new URL(targetUrl);
92
+ const robots = await fetchText(`${origin}/robots.txt`, fetchOptions);
93
+ const declaredSitemaps = robots?.ok ? parseRobots(robots.text).sitemapUrls : [];
94
+ const { entries } = await collectSitemapEntries(declaredSitemaps.length > 0 ? declaredSitemaps : getDefaultSitemapUrls(origin), fetchOptions);
95
+ const urls = /* @__PURE__ */ new Set();
96
+ for (const { loc, alternates } of entries) {
97
+ urls.add(loc);
98
+ for (const { href } of alternates) urls.add(href);
99
+ }
100
+ return urls.size > 0 ? Array.from(urls) : [targetUrl];
101
+ };
102
+
103
+ //#endregion
104
+ export { collectSitemapEntries, discoverSitemapUrls, getDefaultSitemapUrls, parseSitemap };
105
+ //# sourceMappingURL=sitemap.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.mjs","names":[],"sources":["../../../src/scan/sitemap.ts"],"sourcesContent":["import { fetchText, type ScanFetchOptions } from './fetchText';\nimport { parseRobots } from './robots';\n\n/** A `<url>` entry of a sitemap with its hreflang alternates. */\nexport type SitemapEntry = {\n loc: string;\n alternates: { hreflang: string; href: string }[];\n};\n\n/** Result of {@link parseSitemap}. */\nexport type ParsedSitemap = {\n /** `<url>` entries (empty for a sitemap index). */\n entries: SitemapEntry[];\n /** Child sitemap URLs, when the document is a `<sitemapindex>`. */\n childSitemapUrls: string[];\n};\n\n/** Result of {@link collectSitemapEntries}. */\nexport type CollectedSitemaps = {\n /** Sitemap URLs that answered with a valid document. */\n fetchedSitemapUrls: string[];\n entries: SitemapEntry[];\n /** Whether the entry limit was reached before reading every sitemap. */\n isTruncated: boolean;\n};\n\nconst MAX_CHILD_SITEMAPS = 5;\nconst MAX_ENTRIES = 5_000;\n\n/** Decode the XML entities that can appear in sitemap URLs. */\nconst decodeXmlEntities = (text: string): string =>\n text\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\");\n\n/** Read an attribute of an XML tag, accepting both quote styles. */\nconst readXmlAttribute = (tag: string, name: string): string | undefined => {\n const match = tag.match(\n new RegExp(`\\\\b${name}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)')`, 'i')\n );\n return match ? decodeXmlEntities(match[2] ?? match[3] ?? '') : undefined;\n};\n\n/** Extract every `<loc>` value of an XML fragment. */\nconst extractLocs = (xml: string): string[] =>\n Array.from(xml.matchAll(/<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi), (match) =>\n decodeXmlEntities((match[1] ?? '').replace(/^<!\\[CDATA\\[|\\]\\]>$/g, ''))\n );\n\n/**\n * Parse a sitemap or sitemap index (regex based, no XML dependency). Handles\n * `<xhtml:link rel=\"alternate\" hreflang>` alternates in any namespace prefix.\n */\nexport const parseSitemap = (xml: string): ParsedSitemap => {\n if (/<sitemapindex\\b/i.test(xml)) {\n return { entries: [], childSitemapUrls: extractLocs(xml) };\n }\n\n const entries: SitemapEntry[] = [];\n for (const [urlBlock] of xml.matchAll(/<url\\b[\\s\\S]*?<\\/url>/gi)) {\n const loc = extractLocs(urlBlock)[0];\n if (!loc) continue;\n\n const alternates: SitemapEntry['alternates'] = [];\n for (const [linkTag] of urlBlock.matchAll(/<(?:\\w+:)?link\\b[^>]*>/gi)) {\n const hreflang = readXmlAttribute(linkTag, 'hreflang');\n const href = readXmlAttribute(linkTag, 'href');\n if (hreflang && href) alternates.push({ hreflang, href });\n }\n entries.push({ loc, alternates });\n }\n\n return { entries, childSitemapUrls: [] };\n};\n\n/**\n * Fetch the given sitemaps, following sitemap indexes one level deep, and\n * gather their `<url>` entries.\n *\n * @param sitemapUrls - Sitemaps to read (from `robots.txt`, else defaults).\n * @param fetchOptions - Network options, including the SSRF guard.\n */\nexport const collectSitemapEntries = async (\n sitemapUrls: string[],\n fetchOptions: ScanFetchOptions\n): Promise<CollectedSitemaps> => {\n const fetchedSitemapUrls: string[] = [];\n const entries: SitemapEntry[] = [];\n const visitedUrls = new Set<string>();\n const queue = [...sitemapUrls];\n let childSitemapCount = 0;\n\n while (queue.length > 0 && entries.length < MAX_ENTRIES) {\n const sitemapUrl = queue.shift() as string;\n if (visitedUrls.has(sitemapUrl)) continue;\n visitedUrls.add(sitemapUrl);\n\n const response = await fetchText(sitemapUrl, fetchOptions);\n if (!response?.ok || !/<(urlset|sitemapindex)\\b/i.test(response.text)) {\n continue;\n }\n\n fetchedSitemapUrls.push(sitemapUrl);\n const parsedSitemap = parseSitemap(response.text);\n entries.push(...parsedSitemap.entries);\n\n for (const childUrl of parsedSitemap.childSitemapUrls) {\n if (childSitemapCount >= MAX_CHILD_SITEMAPS) break;\n childSitemapCount++;\n queue.push(childUrl);\n }\n }\n\n return {\n fetchedSitemapUrls,\n entries: entries.slice(0, MAX_ENTRIES),\n isTruncated: entries.length >= MAX_ENTRIES || queue.length > 0,\n };\n};\n\n/** Default sitemap locations tried when `robots.txt` declares none. */\nexport const getDefaultSitemapUrls = (origin: string): string[] => [\n `${origin}/sitemap.xml`,\n `${origin}/sitemap_index.xml`,\n];\n\n/**\n * List the page URLs of a site from its sitemaps (primary `<loc>` and every\n * hreflang alternate), used to seed a multi-page audit.\n *\n * @returns De-duplicated URLs, or `[targetUrl]` when no sitemap is found.\n */\nexport const discoverSitemapUrls = async (\n targetUrl: string,\n fetchOptions: ScanFetchOptions\n): Promise<string[]> => {\n const { origin } = new URL(targetUrl);\n const robots = await fetchText(`${origin}/robots.txt`, fetchOptions);\n const declaredSitemaps = robots?.ok\n ? parseRobots(robots.text).sitemapUrls\n : [];\n\n const { entries } = await collectSitemapEntries(\n declaredSitemaps.length > 0\n ? declaredSitemaps\n : getDefaultSitemapUrls(origin),\n fetchOptions\n );\n\n const urls = new Set<string>();\n for (const { loc, alternates } of entries) {\n urls.add(loc);\n for (const { href } of alternates) urls.add(href);\n }\n\n return urls.size > 0 ? Array.from(urls) : [targetUrl];\n};\n"],"mappings":"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"}
@@ -20,7 +20,8 @@ const setAtPath = (root, path, value) => {
20
20
  let current = root;
21
21
  for (let i = 0; i < path.length - 1; i++) {
22
22
  const key = path[i];
23
- const isNextIndex = typeof path[i + 1] === "number";
23
+ if (key === void 0) continue;
24
+ const isNextIndex = typeof nextKey === "number";
24
25
  if (typeof key === "number") {
25
26
  if (!Array.isArray(current)) throw new Error(`Expected array at path segment ${i}`);
26
27
  if (current[key] === void 0) current[key] = isNextIndex ? [] : {};
@@ -32,6 +33,7 @@ const setAtPath = (root, path, value) => {
32
33
  }
33
34
  }
34
35
  const last = path[path.length - 1];
36
+ if (last === void 0) return;
35
37
  if (typeof last === "number") {
36
38
  if (!Array.isArray(current)) throw new Error(`Expected array at final segment`);
37
39
  current[last] = value;
@@ -90,11 +92,14 @@ const flattenToPatches = (value, maxCharsPerChunk, path = [], seen = /* @__PURE_
90
92
  path: [...currentPath, "__total"],
91
93
  value: parts.length
92
94
  });
93
- for (let i = 0; i < parts.length; i++) patches.push({
94
- op: "set",
95
- path: [...currentPath, String(i + 1)],
96
- value: parts[i]
97
- });
95
+ for (let i = 0; i < parts.length; i++) {
96
+ const part = parts[i];
97
+ if (part !== void 0) patches.push({
98
+ op: "set",
99
+ path: [...currentPath, String(i + 1)],
100
+ value: part
101
+ });
102
+ }
98
103
  return;
99
104
  }
100
105
  if (currentValue === null || typeof currentValue !== "object") {
@@ -107,8 +112,14 @@ const flattenToPatches = (value, maxCharsPerChunk, path = [], seen = /* @__PURE_
107
112
  }
108
113
  if (seen.has(currentValue)) throw new Error("Cannot serialize circular structures to JSON.");
109
114
  seen.add(currentValue);
110
- if (Array.isArray(currentValue)) for (let i = 0; i < currentValue.length; i++) walk(currentValue[i], [...currentPath, i]);
111
- else for (const key of Object.keys(currentValue)) walk(currentValue[key], [...currentPath, key]);
115
+ if (Array.isArray(currentValue)) for (let i = 0; i < currentValue.length; i++) {
116
+ const item = currentValue[i];
117
+ if (item !== void 0) walk(item, [...currentPath, i]);
118
+ }
119
+ else for (const key of Object.keys(currentValue)) {
120
+ const childVal = currentValue[key];
121
+ if (childVal !== void 0) walk(childVal, [...currentPath, key]);
122
+ }
112
123
  seen.delete(currentValue);
113
124
  };
114
125
  walk(value, path);
@@ -236,10 +247,13 @@ const reconstructFromSingleChunk = (chunk) => {
236
247
  const assembleJSON = (chunks) => {
237
248
  if (!chunks || chunks.length === 0) throw new Error("No chunks provided.");
238
249
  const sorted = [...chunks].sort((a, b) => a.index - b.index);
239
- const { checksum, rootType } = sorted[0];
250
+ const firstChunk = sorted[0];
251
+ if (!firstChunk) throw new Error("No chunks provided.");
252
+ const { checksum, rootType } = firstChunk;
240
253
  const schemaVersion = 1;
241
254
  for (let i = 0; i < sorted.length; i++) {
242
255
  const chunk = sorted[i];
256
+ if (!chunk) continue;
243
257
  if (chunk.schemaVersion !== schemaVersion) {
244
258
  console.error("Unsupported schemaVersion.", {
245
259
  cause: chunk,
@@ -325,6 +339,7 @@ const assembleJSON = (chunks) => {
325
339
  const reconciled = reconcileSplitNodes(root);
326
340
  for (let i = 0; i < sorted.length; i++) {
327
341
  const chunk = sorted[i];
342
+ if (!chunk) continue;
328
343
  if (chunk.total !== sorted.length) throw new Error(`Chunk total does not match provided count. Expected ${sorted.length}, but chunk ${i} has total=${chunk.total}`);
329
344
  }
330
345
  return reconciled;
@@ -1 +1 @@
1
- {"version":3,"file":"chunkJSON.mjs","names":[],"sources":["../../../src/utils/chunkJSON.ts"],"sourcesContent":["/**\n * Split & reassemble JSON by character budget.\n * - Measures serialized size using JSON.stringify(..).length (characters).\n * - Ensures each chunk is itself valid JSON.\n * - Very large strings are split into safe pieces using getChunk and re-concatenated on assemble.\n * - Protects against circular structures (JSON can't serialize those anyway).\n */\n\nimport { getChunk } from './getChunk';\n\ntype JSONPrimitive = string | number | boolean | null;\ntype JSONValue = JSONPrimitive | JSONObject | JSONArray;\n\nexport type JSONObject = {\n [k: string]: JSONValue;\n};\n\ntype JSONArray = JSONValue[];\n\ntype Path = Array<string | number>;\n\ntype SetPatch = { op: 'set'; path: Path; value: JSONValue };\ntype StrAppendPatch = {\n op: 'str-append';\n path: Path;\n value: string; // part of a longer string\n index: number;\n total: number;\n};\ntype Patch = SetPatch | StrAppendPatch;\n\ntype RootType = 'object' | 'array';\n\nexport type JsonChunk = {\n schemaVersion: 1;\n index: number;\n total: number;\n rootType: RootType;\n checksum: string; // hash of the full original JSON string\n entries: Patch[];\n};\n\nconst isObject = (val: unknown): val is Record<string, unknown> => {\n return typeof val === 'object' && val !== null && !Array.isArray(val);\n};\n\nconst computeDjb2 = (str: string): string => {\n // Simple 32-bit hash; deterministic & fast\n let hash = 5381;\n\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash) ^ str.charCodeAt(i);\n }\n\n // convert to unsigned hex\n return (hash >>> 0).toString(16).padStart(8, '0');\n};\n\nconst setAtPath = (root: any, path: Path, value: JSONValue) => {\n let current = root;\n\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n const nextKey = path[i + 1];\n const isNextIndex = typeof nextKey === 'number';\n\n if (typeof key === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at path segment ${i}`);\n }\n\n if (current[key] === undefined) {\n current[key] = isNextIndex ? [] : {};\n }\n\n current = current[key];\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at path segment ${i}`);\n }\n\n if (!(key in current)) {\n (current as any)[key] = isNextIndex ? [] : {};\n }\n\n current = (current as any)[key];\n }\n }\n\n const last = path[path.length - 1];\n\n if (typeof last === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at final segment`);\n }\n\n current[last] = value as any;\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at final segment`);\n }\n\n (current as any)[last] = value as any;\n }\n};\n\nconst pathKey = (path: Path): string => {\n // stable key for grouping string parts\n return JSON.stringify(path);\n};\n\n/**\n * Split a string into parts using getChunk with a charLength budget per part.\n */\nconst splitStringByBudget = (\n str: string,\n maxCharsPerPart: number\n): string[] => {\n if (maxCharsPerPart <= 0) {\n throw new Error('maxChars must be > 0');\n }\n\n const output: string[] = [];\n let offset = 0;\n\n while (offset < str.length) {\n const part = getChunk(str, {\n charStart: offset,\n charLength: maxCharsPerPart,\n });\n\n if (!part) break;\n\n output.push(part);\n offset += part.length;\n }\n\n return output;\n};\n\n/**\n * Flatten JSON into patches (leaf writes). Strings too large to fit in a single\n * chunk are yielded as multiple str-append patches.\n */\nconst flattenToPatches = (\n value: JSONValue,\n maxCharsPerChunk: number,\n path: Path = [],\n seen = new WeakSet<object>()\n): Patch[] => {\n // Conservative per-entry cap so a single entry fits into an empty chunk with envelope overhead.\n // (Envelope ~ a few hundred chars; we keep a comfortable margin.)\n const maxStringPiece = Math.max(\n 1,\n Math.floor((maxCharsPerChunk - 400) * 0.8)\n );\n\n const patches: Patch[] = [];\n\n const walk = (currentValue: JSONValue, currentPath: Path) => {\n if (typeof currentValue === 'string') {\n // If the serialized patch wouldn't fit, split the string into multiple parts\n // and encode as a split-node sentinel with numbered keys.\n const testPatch: SetPatch = {\n op: 'set',\n path: currentPath,\n value: currentValue,\n };\n const testLen = JSON.stringify(testPatch).length + 150; // margin\n\n if (testLen <= maxCharsPerChunk) {\n patches.push(testPatch);\n return;\n }\n\n // Use getChunk-based splitting to produce stable parts\n const parts = splitStringByBudget(currentValue, maxStringPiece);\n\n // Emit split-node metadata and parts as individual leaf writes\n patches.push({\n op: 'set',\n path: [...currentPath, '__splittedType'],\n value: 'string',\n });\n patches.push({\n op: 'set',\n path: [...currentPath, '__total'],\n value: parts.length,\n });\n\n for (let i = 0; i < parts.length; i++) {\n patches.push({\n op: 'set',\n path: [...currentPath, String(i + 1)],\n value: parts[i],\n });\n }\n\n return;\n }\n\n if (currentValue === null || typeof currentValue !== 'object') {\n patches.push({ op: 'set', path: currentPath, value: currentValue });\n return;\n }\n\n if (seen.has(currentValue as object)) {\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n seen.add(currentValue as object);\n\n if (Array.isArray(currentValue)) {\n for (let i = 0; i < currentValue.length; i++) {\n walk(currentValue[i] as JSONValue, [...currentPath, i]);\n }\n } else {\n for (const key of Object.keys(currentValue)) {\n walk((currentValue as JSONObject)[key], [...currentPath, key]);\n }\n }\n\n seen.delete(currentValue as object);\n };\n\n walk(value, path);\n return patches;\n};\n\n/**\n * Split JSON into chunks constrained by character count of serialized chunk.\n */\nexport const chunkJSON = (\n value: JSONObject | JSONArray,\n maxChars: number\n): JsonChunk[] => {\n if (!isObject(value) && !Array.isArray(value)) {\n throw new Error('Root must be an object or array.');\n }\n\n if (maxChars < 500) {\n // You can lower this if you truly need; recommended to keep some envelope headroom.\n throw new Error('maxChars is too small. Use at least 500 characters.');\n }\n\n const rootType: RootType = Array.isArray(value) ? 'array' : 'object';\n let sourceString: string;\n\n try {\n sourceString = JSON.stringify(value);\n } catch {\n // Provide a deterministic error message for circular refs\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n const checksum = computeDjb2(sourceString);\n const allPatches = flattenToPatches(value as JSONValue, maxChars);\n\n const chunks: JsonChunk[] = [];\n let currentChunk: JsonChunk = {\n schemaVersion: 1,\n index: 0, // provisional\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n };\n\n const emptyEnvelopeSize = JSON.stringify({\n ...currentChunk,\n entries: [],\n }).length;\n\n const tryFlush = () => {\n if (currentChunk.entries.length > 0) {\n chunks.push(currentChunk);\n currentChunk = {\n schemaVersion: 1,\n index: 0,\n total: 0,\n rootType,\n checksum,\n entries: [],\n };\n }\n };\n\n for (const patch of allPatches) {\n // Would adding this patch exceed maxChars?\n const withPatchSize =\n emptyEnvelopeSize +\n JSON.stringify(currentChunk.entries).length + // current entries array\n (currentChunk.entries.length ? 1 : 0) + // possible comma\n JSON.stringify(patch).length;\n\n if (withPatchSize <= maxChars) {\n currentChunk.entries.push(patch);\n } else {\n // Start a new chunk if current has items\n if (currentChunk.entries.length > 0) {\n tryFlush();\n }\n\n // Ensure single patch fits into an empty chunk\n const singleSize = emptyEnvelopeSize + JSON.stringify([patch]).length;\n\n if (singleSize > maxChars) {\n // This should only happen for massive strings, which we pre-split;\n // if it happens for a non-string, we cannot split further.\n throw new Error(\n 'A single entry exceeds maxChars and cannot be split. Reduce entry size or increase maxChars.'\n );\n }\n\n currentChunk.entries.push(patch);\n }\n }\n\n tryFlush();\n\n // Ensure at least one chunk exists (even for empty root)\n if (chunks.length === 0) {\n chunks.push({\n schemaVersion: 1,\n index: 0,\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n });\n }\n\n // Finalize indices & totals\n const totalChunks = chunks.length;\n\n chunks.forEach((chunk, index) => {\n chunk.index = index;\n chunk.total = totalChunks;\n });\n\n return chunks;\n};\n\n/**\n * Reassemble JSON from chunks.\n * - Validates checksums and indices.\n * - Applies 'set' patches and merges string pieces from 'str-append'.\n */\n/**\n * Reconstruct content from a single chunk without validation.\n * Useful for processing individual chunks in a pipeline where you don't have all chunks yet.\n * Note: This will only reconstruct the partial content contained in this chunk.\n */\nexport const reconstructFromSingleChunk = (\n chunk: JsonChunk\n): JSONObject | JSONArray => {\n const root: any = chunk.rootType === 'array' ? [] : {};\n\n // Apply all 'set' patches from this chunk\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n setAtPath(root, entry.path, entry.value);\n }\n }\n\n // Reconcile split-node sentinels for strings/arrays\n // When reconstructing from a single chunk, we may have incomplete split nodes\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const parts: string[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n hasAllParts = false;\n break;\n }\n\n parts.push(piece);\n }\n\n // Only reconstruct if we have all parts, otherwise return the node as-is\n if (hasAllParts) {\n return parts.join('');\n }\n\n return node;\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const output: any[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n hasAllParts = false;\n break;\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n // Only reconstruct if we have all parts\n if (hasAllParts) {\n return output;\n }\n\n return node;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n return reconcileSplitNodes(root);\n};\n\nexport const assembleJSON = (chunks: JsonChunk[]): JSONObject | JSONArray => {\n if (!chunks || chunks.length === 0) {\n throw new Error('No chunks provided.');\n }\n\n // Basic validation & sort\n const sorted = [...chunks].sort((a, b) => a.index - b.index);\n const { checksum, rootType } = sorted[0];\n const schemaVersion = 1;\n\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (chunk.schemaVersion !== schemaVersion) {\n console.error('Unsupported schemaVersion.', {\n cause: chunk,\n schemaVersion,\n });\n throw new Error('Unsupported schemaVersion.');\n }\n\n if (chunk.rootType !== rootType) {\n console.error('Chunks rootType mismatch.', {\n cause: chunk,\n rootType,\n });\n throw new Error('Chunks rootType mismatch.');\n }\n\n if (chunk.checksum !== checksum) {\n console.error('Chunks checksum mismatch (different source objects?).', {\n cause: chunk,\n checksum,\n });\n throw new Error('Chunks checksum mismatch (different source objects?).');\n }\n\n if (chunk.index !== i) {\n console.error('Chunk indices are not contiguous or sorted.', {\n cause: chunk,\n index: chunk.index,\n i,\n });\n throw new Error('Chunk indices are not contiguous or sorted.');\n }\n\n // Defer total check until after reconstruction to prefer more specific errors\n }\n\n const root: any = rootType === 'array' ? [] : {};\n\n // Collect string parts by path\n const stringParts = new Map<\n string,\n { path: Path; total: number; received: StrAppendPatch[] }\n >();\n\n const applySet = (patch: SetPatch) =>\n setAtPath(root, patch.path, patch.value);\n\n for (const chunk of sorted) {\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n applySet(entry);\n } else {\n const key = pathKey(entry.path);\n const record = stringParts.get(key) ?? {\n path: entry.path,\n total: entry.total,\n received: [],\n };\n\n if (record.total !== entry.total) {\n throw new Error('Inconsistent string part totals for a path.');\n }\n\n record.received.push(entry);\n stringParts.set(key, record);\n }\n }\n }\n\n // Stitch strings\n for (const { path, total, received } of stringParts.values()) {\n if (received.length !== total) {\n throw new Error('Missing string parts for a path; incomplete chunk set.');\n }\n\n received.sort((a, b) => a.index - b.index);\n const fullString = received.map((part) => part.value).join('');\n setAtPath(root, path, fullString);\n }\n\n // Reconcile split-node sentinels for strings/arrays after all patches applied\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const parts: string[] = [];\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n parts.push(piece);\n }\n\n return parts.join('');\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const output: any[] = [];\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n return output;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n const reconciled = reconcileSplitNodes(root);\n\n // Now validate totals match provided count\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (chunk.total !== sorted.length) {\n throw new Error(\n `Chunk total does not match provided count. Expected ${sorted.length}, but chunk ${i} has total=${chunk.total}`\n );\n }\n }\n\n return reconciled;\n};\n\n/* -------------------------------------------\n * Example usage\n * -------------------------------------------\nconst big: JSONObject = {\n title: \"Document\",\n content: \"…a very very long text…\",\n items: Array.from({ length: 2000 }, (_, i) => ({ id: i, label: `Item ${i}` }))\n};\n\n// Split to ~16k-char chunks\nconst chunks = chunkJSON(big, 16_000);\n\n// Send each `chunks[i]` as JSON to your backend.\n// Later, reassemble:\nconst restored = assembleJSON(chunks);\nconsole.log(JSON.stringify(restored) === JSON.stringify(big)); // true\n*/\n"],"mappings":";;;;;;;;;;AA0CA,MAAM,YAAY,QAAiD;CACjE,OAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,GAAG;AACtE;AAEA,MAAM,eAAe,QAAwB;CAE3C,IAAI,OAAO;CAEX,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAC9B,QAAS,QAAQ,KAAK,OAAQ,IAAI,WAAW,CAAC;CAIhD,QAAQ,SAAS,EAAC,CAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;AAClD;AAEA,MAAM,aAAa,MAAW,MAAY,UAAqB;CAC7D,IAAI,UAAU;CAEd,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;EACxC,MAAM,MAAM,KAAK;EAEjB,MAAM,cAAc,OADJ,KAAK,IAAI,OACc;EAEvC,IAAI,OAAO,QAAQ,UAAU;GAC3B,IAAI,CAAC,MAAM,QAAQ,OAAO,GACxB,MAAM,IAAI,MAAM,kCAAkC,GAAG;GAGvD,IAAI,QAAQ,SAAS,QACnB,QAAQ,OAAO,cAAc,CAAC,IAAI,CAAC;GAGrC,UAAU,QAAQ;EACpB,OAAO;GACL,IAAI,CAAC,SAAS,OAAO,GACnB,MAAM,IAAI,MAAM,mCAAmC,GAAG;GAGxD,IAAI,EAAE,OAAO,UACX,AAAC,QAAgB,OAAO,cAAc,CAAC,IAAI,CAAC;GAG9C,UAAW,QAAgB;EAC7B;CACF;CAEA,MAAM,OAAO,KAAK,KAAK,SAAS;CAEhC,IAAI,OAAO,SAAS,UAAU;EAC5B,IAAI,CAAC,MAAM,QAAQ,OAAO,GACxB,MAAM,IAAI,MAAM,iCAAiC;EAGnD,QAAQ,QAAQ;CAClB,OAAO;EACL,IAAI,CAAC,SAAS,OAAO,GACnB,MAAM,IAAI,MAAM,kCAAkC;EAGpD,AAAC,QAAgB,QAAQ;CAC3B;AACF;AAEA,MAAM,WAAW,SAAuB;CAEtC,OAAO,KAAK,UAAU,IAAI;AAC5B;;;;AAKA,MAAM,uBACJ,KACA,oBACa;CACb,IAAI,mBAAmB,GACrB,MAAM,IAAI,MAAM,sBAAsB;CAGxC,MAAM,SAAmB,CAAC;CAC1B,IAAI,SAAS;CAEb,OAAO,SAAS,IAAI,QAAQ;EAC1B,MAAM,OAAO,SAAS,KAAK;GACzB,WAAW;GACX,YAAY;EACd,CAAC;EAED,IAAI,CAAC,MAAM;EAEX,OAAO,KAAK,IAAI;EAChB,UAAU,KAAK;CACjB;CAEA,OAAO;AACT;;;;;AAMA,MAAM,oBACJ,OACA,kBACA,OAAa,CAAC,GACd,uBAAO,IAAI,QAAgB,MACf;CAGZ,MAAM,iBAAiB,KAAK,IAC1B,GACA,KAAK,OAAO,mBAAmB,OAAO,EAAG,CAC3C;CAEA,MAAM,UAAmB,CAAC;CAE1B,MAAM,QAAQ,cAAyB,gBAAsB;EAC3D,IAAI,OAAO,iBAAiB,UAAU;GAGpC,MAAM,YAAsB;IAC1B,IAAI;IACJ,MAAM;IACN,OAAO;GACT;GAGA,IAFgB,KAAK,UAAU,SAAS,CAAC,CAAC,SAAS,OAEpC,kBAAkB;IAC/B,QAAQ,KAAK,SAAS;IACtB;GACF;GAGA,MAAM,QAAQ,oBAAoB,cAAc,cAAc;GAG9D,QAAQ,KAAK;IACX,IAAI;IACJ,MAAM,CAAC,GAAG,aAAa,gBAAgB;IACvC,OAAO;GACT,CAAC;GACD,QAAQ,KAAK;IACX,IAAI;IACJ,MAAM,CAAC,GAAG,aAAa,SAAS;IAChC,OAAO,MAAM;GACf,CAAC;GAED,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,QAAQ,KAAK;IACX,IAAI;IACJ,MAAM,CAAC,GAAG,aAAa,OAAO,IAAI,CAAC,CAAC;IACpC,OAAO,MAAM;GACf,CAAC;GAGH;EACF;EAEA,IAAI,iBAAiB,QAAQ,OAAO,iBAAiB,UAAU;GAC7D,QAAQ,KAAK;IAAE,IAAI;IAAO,MAAM;IAAa,OAAO;GAAa,CAAC;GAClE;EACF;EAEA,IAAI,KAAK,IAAI,YAAsB,GACjC,MAAM,IAAI,MAAM,+CAA+C;EAGjE,KAAK,IAAI,YAAsB;EAE/B,IAAI,MAAM,QAAQ,YAAY,GAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KACvC,KAAK,aAAa,IAAiB,CAAC,GAAG,aAAa,CAAC,CAAC;OAGxD,KAAK,MAAM,OAAO,OAAO,KAAK,YAAY,GACxC,KAAM,aAA4B,MAAM,CAAC,GAAG,aAAa,GAAG,CAAC;EAIjE,KAAK,OAAO,YAAsB;CACpC;CAEA,KAAK,OAAO,IAAI;CAChB,OAAO;AACT;;;;AAKA,MAAa,aACX,OACA,aACgB;CAChB,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK,GAC1C,MAAM,IAAI,MAAM,kCAAkC;CAGpD,IAAI,WAAW,KAEb,MAAM,IAAI,MAAM,qDAAqD;CAGvE,MAAM,WAAqB,MAAM,QAAQ,KAAK,IAAI,UAAU;CAC5D,IAAI;CAEJ,IAAI;EACF,eAAe,KAAK,UAAU,KAAK;CACrC,QAAQ;EAEN,MAAM,IAAI,MAAM,+CAA+C;CACjE;CAEA,MAAM,WAAW,YAAY,YAAY;CACzC,MAAM,aAAa,iBAAiB,OAAoB,QAAQ;CAEhE,MAAM,SAAsB,CAAC;CAC7B,IAAI,eAA0B;EAC5B,eAAe;EACf,OAAO;EACP,OAAO;EACP;EACA;EACA,SAAS,CAAC;CACZ;CAEA,MAAM,oBAAoB,KAAK,UAAU;EACvC,GAAG;EACH,SAAS,CAAC;CACZ,CAAC,CAAC,CAAC;CAEH,MAAM,iBAAiB;EACrB,IAAI,aAAa,QAAQ,SAAS,GAAG;GACnC,OAAO,KAAK,YAAY;GACxB,eAAe;IACb,eAAe;IACf,OAAO;IACP,OAAO;IACP;IACA;IACA,SAAS,CAAC;GACZ;EACF;CACF;CAEA,KAAK,MAAM,SAAS,YAQlB,IALE,oBACA,KAAK,UAAU,aAAa,OAAO,CAAC,CAAC,UACpC,aAAa,QAAQ,SAAS,IAAI,KACnC,KAAK,UAAU,KAAK,CAAC,CAAC,UAEH,UACnB,aAAa,QAAQ,KAAK,KAAK;MAC1B;EAEL,IAAI,aAAa,QAAQ,SAAS,GAChC,SAAS;EAMX,IAFmB,oBAAoB,KAAK,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,SAE9C,UAGf,MAAM,IAAI,MACR,8FACF;EAGF,aAAa,QAAQ,KAAK,KAAK;CACjC;CAGF,SAAS;CAGT,IAAI,OAAO,WAAW,GACpB,OAAO,KAAK;EACV,eAAe;EACf,OAAO;EACP,OAAO;EACP;EACA;EACA,SAAS,CAAC;CACZ,CAAC;CAIH,MAAM,cAAc,OAAO;CAE3B,OAAO,SAAS,OAAO,UAAU;EAC/B,MAAM,QAAQ;EACd,MAAM,QAAQ;CAChB,CAAC;CAED,OAAO;AACT;;;;;;;;;;;AAYA,MAAa,8BACX,UAC2B;CAC3B,MAAM,OAAY,MAAM,aAAa,UAAU,CAAC,IAAI,CAAC;CAGrD,KAAK,MAAM,SAAS,MAAM,SACxB,IAAI,MAAM,OAAO,OACf,UAAU,MAAM,MAAM,MAAM,MAAM,KAAK;CAM3C,MAAM,uBAAuB,SAAmB;EAC9C,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO;EAEtD,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAC/B,KAAK,KAAK,oBAAoB,KAAK,EAAE;GAEvC,OAAO;EACT;EAGA,IAAK,KAAa,sBAAsB,UAAU;GAChD,MAAM,QAAS,KAAa;GAE5B,IAAI,OAAO,UAAU,YAAY,SAAS,GAExC,OAAO;GAGT,MAAM,QAAkB,CAAC;GACzB,IAAI,cAAc;GAElB,KAAK,IAAI,IAAI,GAAG,KAAK,OAAO,KAAK;IAC/B,MAAM,QAAS,KAAa,OAAO,CAAC;IAEpC,IAAI,OAAO,UAAU,UAAU;KAC7B,cAAc;KACd;IACF;IAEA,MAAM,KAAK,KAAK;GAClB;GAGA,IAAI,aACF,OAAO,MAAM,KAAK,EAAE;GAGtB,OAAO;EACT;EAGA,IAAK,KAAa,sBAAsB,SAAS;GAC/C,MAAM,QAAS,KAAa;GAE5B,IAAI,OAAO,UAAU,YAAY,QAAQ,GAEvC,OAAO;GAGT,MAAM,SAAgB,CAAC;GACvB,IAAI,cAAc;GAElB,KAAK,IAAI,IAAI,GAAG,KAAK,OAAO,KAAK;IAC/B,MAAM,QAAS,KAAa,OAAO,CAAC;IAEpC,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;KACzB,cAAc;KACd;IACF;IAEA,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,OAAO,KAAK,oBAAoB,MAAM,EAAE,CAAC;GAE7C;GAGA,IAAI,aACF,OAAO;GAGT,OAAO;EACT;EAGA,KAAK,MAAM,OAAO,OAAO,KAAK,IAAI,GAChC,KAAK,OAAO,oBAAoB,KAAK,IAAI;EAG3C,OAAO;CACT;CAEA,OAAO,oBAAoB,IAAI;AACjC;AAEA,MAAa,gBAAgB,WAAgD;CAC3E,IAAI,CAAC,UAAU,OAAO,WAAW,GAC/B,MAAM,IAAI,MAAM,qBAAqB;CAIvC,MAAM,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;CAC3D,MAAM,EAAE,UAAU,aAAa,OAAO;CACtC,MAAM,gBAAgB;CAEtB,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACtC,MAAM,QAAQ,OAAO;EAErB,IAAI,MAAM,kBAAkB,eAAe;GACzC,QAAQ,MAAM,8BAA8B;IAC1C,OAAO;IACP;GACF,CAAC;GACD,MAAM,IAAI,MAAM,4BAA4B;EAC9C;EAEA,IAAI,MAAM,aAAa,UAAU;GAC/B,QAAQ,MAAM,6BAA6B;IACzC,OAAO;IACP;GACF,CAAC;GACD,MAAM,IAAI,MAAM,2BAA2B;EAC7C;EAEA,IAAI,MAAM,aAAa,UAAU;GAC/B,QAAQ,MAAM,yDAAyD;IACrE,OAAO;IACP;GACF,CAAC;GACD,MAAM,IAAI,MAAM,uDAAuD;EACzE;EAEA,IAAI,MAAM,UAAU,GAAG;GACrB,QAAQ,MAAM,+CAA+C;IAC3D,OAAO;IACP,OAAO,MAAM;IACb;GACF,CAAC;GACD,MAAM,IAAI,MAAM,6CAA6C;EAC/D;CAGF;CAEA,MAAM,OAAY,aAAa,UAAU,CAAC,IAAI,CAAC;CAG/C,MAAM,8BAAc,IAAI,IAGtB;CAEF,MAAM,YAAY,UAChB,UAAU,MAAM,MAAM,MAAM,MAAM,KAAK;CAEzC,KAAK,MAAM,SAAS,QAClB,KAAK,MAAM,SAAS,MAAM,SACxB,IAAI,MAAM,OAAO,OACf,SAAS,KAAK;MACT;EACL,MAAM,MAAM,QAAQ,MAAM,IAAI;EAC9B,MAAM,SAAS,YAAY,IAAI,GAAG,KAAK;GACrC,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,UAAU,CAAC;EACb;EAEA,IAAI,OAAO,UAAU,MAAM,OACzB,MAAM,IAAI,MAAM,6CAA6C;EAG/D,OAAO,SAAS,KAAK,KAAK;EAC1B,YAAY,IAAI,KAAK,MAAM;CAC7B;CAKJ,KAAK,MAAM,EAAE,MAAM,OAAO,cAAc,YAAY,OAAO,GAAG;EAC5D,IAAI,SAAS,WAAW,OACtB,MAAM,IAAI,MAAM,wDAAwD;EAG1E,SAAS,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;EACzC,MAAM,aAAa,SAAS,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,KAAK,EAAE;EAC7D,UAAU,MAAM,MAAM,UAAU;CAClC;CAGA,MAAM,uBAAuB,SAAmB;EAC9C,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO;EAEtD,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAC/B,KAAK,KAAK,oBAAoB,KAAK,EAAE;GAEvC,OAAO;EACT;EAGA,IAAK,KAAa,sBAAsB,UAAU;GAChD,MAAM,QAAS,KAAa;GAE5B,IAAI,OAAO,UAAU,YAAY,SAAS,GACxC,MAAM,IAAI,MAAM,sCAAsC;GAGxD,MAAM,QAAkB,CAAC;GAEzB,KAAK,IAAI,IAAI,GAAG,KAAK,OAAO,KAAK;IAC/B,MAAM,QAAS,KAAa,OAAO,CAAC;IAEpC,IAAI,OAAO,UAAU,UACnB,MAAM,IAAI,MACR,wDACF;IAGF,MAAM,KAAK,KAAK;GAClB;GAEA,OAAO,MAAM,KAAK,EAAE;EACtB;EAGA,IAAK,KAAa,sBAAsB,SAAS;GAC/C,MAAM,QAAS,KAAa;GAE5B,IAAI,OAAO,UAAU,YAAY,QAAQ,GACvC,MAAM,IAAI,MAAM,sCAAsC;GAGxD,MAAM,SAAgB,CAAC;GAEvB,KAAK,IAAI,IAAI,GAAG,KAAK,OAAO,KAAK;IAC/B,MAAM,QAAS,KAAa,OAAO,CAAC;IAEpC,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB,MAAM,IAAI,MACR,wDACF;IAGF,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,OAAO,KAAK,oBAAoB,MAAM,EAAE,CAAC;GAE7C;GAEA,OAAO;EACT;EAGA,KAAK,MAAM,OAAO,OAAO,KAAK,IAAI,GAChC,KAAK,OAAO,oBAAoB,KAAK,IAAI;EAG3C,OAAO;CACT;CAEA,MAAM,aAAa,oBAAoB,IAAI;CAG3C,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACtC,MAAM,QAAQ,OAAO;EAErB,IAAI,MAAM,UAAU,OAAO,QACzB,MAAM,IAAI,MACR,uDAAuD,OAAO,OAAO,cAAc,EAAE,aAAa,MAAM,OAC1G;CAEJ;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"chunkJSON.mjs","names":[],"sources":["../../../src/utils/chunkJSON.ts"],"sourcesContent":["/**\n * Split & reassemble JSON by character budget.\n * - Measures serialized size using JSON.stringify(..).length (characters).\n * - Ensures each chunk is itself valid JSON.\n * - Very large strings are split into safe pieces using getChunk and re-concatenated on assemble.\n * - Protects against circular structures (JSON can't serialize those anyway).\n */\n\nimport { getChunk } from './getChunk';\n\ntype JSONPrimitive = string | number | boolean | null;\ntype JSONValue = JSONPrimitive | JSONObject | JSONArray;\n\nexport type JSONObject = {\n [k: string]: JSONValue;\n};\n\ntype JSONArray = JSONValue[];\n\ntype Path = Array<string | number>;\n\ntype SetPatch = { op: 'set'; path: Path; value: JSONValue };\ntype StrAppendPatch = {\n op: 'str-append';\n path: Path;\n value: string; // part of a longer string\n index: number;\n total: number;\n};\ntype Patch = SetPatch | StrAppendPatch;\n\ntype RootType = 'object' | 'array';\n\nexport type JsonChunk = {\n schemaVersion: 1;\n index: number;\n total: number;\n rootType: RootType;\n checksum: string; // hash of the full original JSON string\n entries: Patch[];\n};\n\nconst isObject = (val: unknown): val is Record<string, unknown> => {\n return typeof val === 'object' && val !== null && !Array.isArray(val);\n};\n\nconst computeDjb2 = (str: string): string => {\n // Simple 32-bit hash; deterministic & fast\n let hash = 5381;\n\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash) ^ str.charCodeAt(i);\n }\n\n // convert to unsigned hex\n return (hash >>> 0).toString(16).padStart(8, '0');\n};\n\nconst setAtPath = (root: any, path: Path, value: JSONValue) => {\n let current = root;\n\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n if (key === undefined) continue;\n\n const isNextIndex = typeof nextKey === 'number';\n\n if (typeof key === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at path segment ${i}`);\n }\n\n if (current[key] === undefined) {\n current[key] = isNextIndex ? [] : {};\n }\n\n current = current[key];\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at path segment ${i}`);\n }\n\n if (!(key in current)) {\n (current as Record<string, any>)[key] = isNextIndex ? [] : {};\n }\n\n current = (current as Record<string, any>)[key];\n }\n }\n\n const last = path[path.length - 1];\n\n if (last === undefined) return;\n\n if (typeof last === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at final segment`);\n }\n\n current[last] = value as any;\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at final segment`);\n }\n\n (current as Record<string, any>)[last] = value as any;\n }\n};\n\nconst pathKey = (path: Path): string => {\n // stable key for grouping string parts\n return JSON.stringify(path);\n};\n\n/**\n * Split a string into parts using getChunk with a charLength budget per part.\n */\nconst splitStringByBudget = (\n str: string,\n maxCharsPerPart: number\n): string[] => {\n if (maxCharsPerPart <= 0) {\n throw new Error('maxChars must be > 0');\n }\n\n const output: string[] = [];\n let offset = 0;\n\n while (offset < str.length) {\n const part = getChunk(str, {\n charStart: offset,\n charLength: maxCharsPerPart,\n });\n\n if (!part) break;\n\n output.push(part);\n offset += part.length;\n }\n\n return output;\n};\n\n/**\n * Flatten JSON into patches (leaf writes). Strings too large to fit in a single\n * chunk are yielded as multiple str-append patches.\n */\nconst flattenToPatches = (\n value: JSONValue,\n maxCharsPerChunk: number,\n path: Path = [],\n seen = new WeakSet<object>()\n): Patch[] => {\n // Conservative per-entry cap so a single entry fits into an empty chunk with envelope overhead.\n // (Envelope ~ a few hundred chars; we keep a comfortable margin.)\n const maxStringPiece = Math.max(\n 1,\n Math.floor((maxCharsPerChunk - 400) * 0.8)\n );\n\n const patches: Patch[] = [];\n\n const walk = (currentValue: JSONValue, currentPath: Path) => {\n if (typeof currentValue === 'string') {\n // If the serialized patch wouldn't fit, split the string into multiple parts\n // and encode as a split-node sentinel with numbered keys.\n const testPatch: SetPatch = {\n op: 'set',\n path: currentPath,\n value: currentValue,\n };\n const testLen = JSON.stringify(testPatch).length + 150; // margin\n\n if (testLen <= maxCharsPerChunk) {\n patches.push(testPatch);\n return;\n }\n\n // Use getChunk-based splitting to produce stable parts\n const parts = splitStringByBudget(currentValue, maxStringPiece);\n\n // Emit split-node metadata and parts as individual leaf writes\n patches.push({\n op: 'set',\n path: [...currentPath, '__splittedType'],\n value: 'string',\n });\n patches.push({\n op: 'set',\n path: [...currentPath, '__total'],\n value: parts.length,\n });\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n\n if (part !== undefined) {\n patches.push({\n op: 'set',\n path: [...currentPath, String(i + 1)],\n value: part,\n });\n }\n }\n\n return;\n }\n\n if (currentValue === null || typeof currentValue !== 'object') {\n patches.push({ op: 'set', path: currentPath, value: currentValue });\n return;\n }\n\n if (seen.has(currentValue as object)) {\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n seen.add(currentValue as object);\n\n if (Array.isArray(currentValue)) {\n for (let i = 0; i < currentValue.length; i++) {\n const item = currentValue[i];\n if (item !== undefined) {\n walk(item as JSONValue, [...currentPath, i]);\n }\n }\n } else {\n for (const key of Object.keys(currentValue)) {\n const childVal = (currentValue as JSONObject)[key];\n\n if (childVal !== undefined) {\n walk(childVal, [...currentPath, key]);\n }\n }\n }\n\n seen.delete(currentValue as object);\n };\n\n walk(value, path);\n return patches;\n};\n\n/**\n * Split JSON into chunks constrained by character count of serialized chunk.\n */\nexport const chunkJSON = (\n value: JSONObject | JSONArray,\n maxChars: number\n): JsonChunk[] => {\n if (!isObject(value) && !Array.isArray(value)) {\n throw new Error('Root must be an object or array.');\n }\n\n if (maxChars < 500) {\n // You can lower this if you truly need; recommended to keep some envelope headroom.\n throw new Error('maxChars is too small. Use at least 500 characters.');\n }\n\n const rootType: RootType = Array.isArray(value) ? 'array' : 'object';\n let sourceString: string;\n\n try {\n sourceString = JSON.stringify(value);\n } catch {\n // Provide a deterministic error message for circular refs\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n const checksum = computeDjb2(sourceString);\n const allPatches = flattenToPatches(value as JSONValue, maxChars);\n\n const chunks: JsonChunk[] = [];\n let currentChunk: JsonChunk = {\n schemaVersion: 1,\n index: 0, // provisional\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n };\n\n const emptyEnvelopeSize = JSON.stringify({\n ...currentChunk,\n entries: [],\n }).length;\n\n const tryFlush = () => {\n if (currentChunk.entries.length > 0) {\n chunks.push(currentChunk);\n currentChunk = {\n schemaVersion: 1,\n index: 0,\n total: 0,\n rootType,\n checksum,\n entries: [],\n };\n }\n };\n\n for (const patch of allPatches) {\n // Would adding this patch exceed maxChars?\n const withPatchSize =\n emptyEnvelopeSize +\n JSON.stringify(currentChunk.entries).length + // current entries array\n (currentChunk.entries.length ? 1 : 0) + // possible comma\n JSON.stringify(patch).length;\n\n if (withPatchSize <= maxChars) {\n currentChunk.entries.push(patch);\n } else {\n // Start a new chunk if current has items\n if (currentChunk.entries.length > 0) {\n tryFlush();\n }\n\n // Ensure single patch fits into an empty chunk\n const singleSize = emptyEnvelopeSize + JSON.stringify([patch]).length;\n\n if (singleSize > maxChars) {\n // This should only happen for massive strings, which we pre-split;\n // if it happens for a non-string, we cannot split further.\n throw new Error(\n 'A single entry exceeds maxChars and cannot be split. Reduce entry size or increase maxChars.'\n );\n }\n\n currentChunk.entries.push(patch);\n }\n }\n\n tryFlush();\n\n // Ensure at least one chunk exists (even for empty root)\n if (chunks.length === 0) {\n chunks.push({\n schemaVersion: 1,\n index: 0,\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n });\n }\n\n // Finalize indices & totals\n const totalChunks = chunks.length;\n\n chunks.forEach((chunk, index) => {\n chunk.index = index;\n chunk.total = totalChunks;\n });\n\n return chunks;\n};\n\n/**\n * Reassemble JSON from chunks.\n * - Validates checksums and indices.\n * - Applies 'set' patches and merges string pieces from 'str-append'.\n */\n/**\n * Reconstruct content from a single chunk without validation.\n * Useful for processing individual chunks in a pipeline where you don't have all chunks yet.\n * Note: This will only reconstruct the partial content contained in this chunk.\n */\nexport const reconstructFromSingleChunk = (\n chunk: JsonChunk\n): JSONObject | JSONArray => {\n const root: any = chunk.rootType === 'array' ? [] : {};\n\n // Apply all 'set' patches from this chunk\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n setAtPath(root, entry.path, entry.value);\n }\n }\n\n // Reconcile split-node sentinels for strings/arrays\n // When reconstructing from a single chunk, we may have incomplete split nodes\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const parts: string[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n hasAllParts = false;\n break;\n }\n\n parts.push(piece);\n }\n\n // Only reconstruct if we have all parts, otherwise return the node as-is\n if (hasAllParts) {\n return parts.join('');\n }\n\n return node;\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const output: any[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n hasAllParts = false;\n break;\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n // Only reconstruct if we have all parts\n if (hasAllParts) {\n return output;\n }\n\n return node;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n return reconcileSplitNodes(root);\n};\n\nexport const assembleJSON = (chunks: JsonChunk[]): JSONObject | JSONArray => {\n if (!chunks || chunks.length === 0) {\n throw new Error('No chunks provided.');\n }\n\n // Basic validation & sort\n const sorted = [...chunks].sort((a, b) => a.index - b.index);\n const firstChunk = sorted[0];\n\n if (!firstChunk) {\n throw new Error('No chunks provided.');\n }\n\n const { checksum, rootType } = firstChunk;\n const schemaVersion = 1;\n\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (!chunk) continue;\n\n if (chunk.schemaVersion !== schemaVersion) {\n console.error('Unsupported schemaVersion.', {\n cause: chunk,\n schemaVersion,\n });\n throw new Error('Unsupported schemaVersion.');\n }\n\n if (chunk.rootType !== rootType) {\n console.error('Chunks rootType mismatch.', {\n cause: chunk,\n rootType,\n });\n throw new Error('Chunks rootType mismatch.');\n }\n\n if (chunk.checksum !== checksum) {\n console.error('Chunks checksum mismatch (different source objects?).', {\n cause: chunk,\n checksum,\n });\n throw new Error('Chunks checksum mismatch (different source objects?).');\n }\n\n if (chunk.index !== i) {\n console.error('Chunk indices are not contiguous or sorted.', {\n cause: chunk,\n index: chunk.index,\n i,\n });\n throw new Error('Chunk indices are not contiguous or sorted.');\n }\n\n // Defer total check until after reconstruction to prefer more specific errors\n }\n\n const root: any = rootType === 'array' ? [] : {};\n\n // Collect string parts by path\n const stringParts = new Map<\n string,\n { path: Path; total: number; received: StrAppendPatch[] }\n >();\n\n const applySet = (patch: SetPatch) =>\n setAtPath(root, patch.path, patch.value);\n\n for (const chunk of sorted) {\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n applySet(entry);\n } else {\n const key = pathKey(entry.path);\n const record = stringParts.get(key) ?? {\n path: entry.path,\n total: entry.total,\n received: [],\n };\n\n if (record.total !== entry.total) {\n throw new Error('Inconsistent string part totals for a path.');\n }\n\n record.received.push(entry);\n stringParts.set(key, record);\n }\n }\n }\n\n // Stitch strings\n for (const { path, total, received } of stringParts.values()) {\n if (received.length !== total) {\n throw new Error('Missing string parts for a path; incomplete chunk set.');\n }\n\n received.sort((a, b) => a.index - b.index);\n const fullString = received.map((part) => part.value).join('');\n setAtPath(root, path, fullString);\n }\n\n // Reconcile split-node sentinels for strings/arrays after all patches applied\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const parts: string[] = [];\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n parts.push(piece);\n }\n\n return parts.join('');\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const output: any[] = [];\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n return output;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n const reconciled = reconcileSplitNodes(root);\n\n // Now validate totals match provided count\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (!chunk) continue;\n\n if (chunk.total !== sorted.length) {\n throw new Error(\n `Chunk total does not match provided count. Expected ${sorted.length}, but chunk ${i} has total=${chunk.total}`\n );\n }\n }\n\n return reconciled;\n};\n\n/* -------------------------------------------\n * Example usage\n * -------------------------------------------\nconst big: JSONObject = {\n title: \"Document\",\n content: \"…a very very long text…\",\n items: Array.from({ length: 2000 }, (_, i) => ({ id: i, label: `Item ${i}` }))\n};\n\n// Split to ~16k-char chunks\nconst chunks = chunkJSON(big, 16_000);\n\n// Send each `chunks[i]` as JSON to your backend.\n// Later, reassemble:\nconst restored = assembleJSON(chunks);\nconsole.log(JSON.stringify(restored) === JSON.stringify(big)); // true\n*/\n"],"mappings":"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"}
@@ -8,13 +8,13 @@ const getChunk = (text, options = {}) => {
8
8
  const getCharIndexOfLineStart = (lineNumber) => {
9
9
  if (lineNumber <= 0) return 0;
10
10
  let idx = 0;
11
- for (let i = 0; i < Math.min(lineNumber, lines.length); i++) idx += lines[i].length;
11
+ for (let i = 0; i < Math.min(lineNumber, lines.length); i++) idx += lines[i]?.length ?? 0;
12
12
  return idx;
13
13
  };
14
14
  const getCharIndexOfLineEnd = (lineNumber) => {
15
15
  if (lineNumber >= lines.length) return text.length;
16
16
  const line = lines[lineNumber];
17
- return getCharIndexOfLineStart(lineNumber) + line.length;
17
+ return getCharIndexOfLineStart(lineNumber) + (line?.length ?? 0);
18
18
  };
19
19
  let effectiveStart = 0;
20
20
  let effectiveEnd = text.length;
@@ -1 +1 @@
1
- {"version":3,"file":"getChunk.mjs","names":[],"sources":["../../../src/utils/getChunk.ts"],"sourcesContent":["import { splitTextByLines } from './splitTextByLine';\n\ntype TrunkOptions = {\n lineStart?: number;\n lineLength?: number;\n charStart?: number;\n charLength?: number;\n};\n\nexport const getChunk = (text: string, options: TrunkOptions = {}): string => {\n const { lineStart, lineLength, charStart, charLength } = options;\n\n // Fast-path: if no filters were provided, return the whole text\n if (\n lineStart === undefined &&\n lineLength === undefined &&\n charStart === undefined &&\n charLength === undefined\n ) {\n return text;\n }\n\n // ---------------------------------------------------------------------------\n // Utility helpers to convert between line numbers and absolute char indices\n // ---------------------------------------------------------------------------\n const lines = splitTextByLines(text);\n\n const getCharIndexOfLineStart = (lineNumber: number): number => {\n if (lineNumber <= 0) return 0;\n // Sum the length of every previous line\n let idx = 0;\n for (let i = 0; i < Math.min(lineNumber, lines.length); i++) {\n idx += lines[i].length;\n }\n return idx;\n };\n\n const getCharIndexOfLineEnd = (lineNumber: number): number => {\n // If the requested line number exceeds the number of lines, clamp to the last character\n if (lineNumber >= lines.length) {\n return text.length;\n }\n const line = lines[lineNumber];\n const lineEnd = getCharIndexOfLineStart(lineNumber) + line.length;\n\n return lineEnd;\n };\n\n // ---------------------------------------------------------------------------\n // Compute the effective (inclusive) charStart/charEnd for the requested slice\n // ---------------------------------------------------------------------------\n let effectiveStart = 0; // inclusive\n let effectiveEnd = text.length; // exclusive\n\n // Apply line boundaries if provided\n if (lineStart !== undefined) {\n effectiveStart = Math.max(\n effectiveStart,\n getCharIndexOfLineStart(lineStart)\n );\n }\n\n // Apply character boundaries if provided\n if (charStart !== undefined) {\n effectiveStart = Math.max(effectiveStart, charStart);\n }\n\n // Apply line length boundary from lineStart (or 0)\n if (lineLength !== undefined) {\n const endLine = (lineStart ?? 0) + lineLength - 1;\n effectiveEnd = Math.min(effectiveEnd, getCharIndexOfLineEnd(endLine));\n }\n\n // Apply character length boundary from effectiveStart\n if (charLength !== undefined) {\n effectiveEnd = Math.min(\n effectiveEnd,\n (charStart ?? effectiveStart) + charLength\n );\n }\n\n // If bounds do not overlap, return empty string\n if (effectiveStart >= effectiveEnd) {\n return '';\n }\n\n return text.slice(effectiveStart, effectiveEnd);\n};\n"],"mappings":";;;AASA,MAAa,YAAY,MAAc,UAAwB,CAAC,MAAc;CAC5E,MAAM,EAAE,WAAW,YAAY,WAAW,eAAe;CAGzD,IACE,cAAc,UACd,eAAe,UACf,cAAc,UACd,eAAe,QAEf,OAAO;CAMT,MAAM,QAAQ,iBAAiB,IAAI;CAEnC,MAAM,2BAA2B,eAA+B;EAC9D,IAAI,cAAc,GAAG,OAAO;EAE5B,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,IAAI,YAAY,MAAM,MAAM,GAAG,KACtD,OAAO,MAAM,EAAE,CAAC;EAElB,OAAO;CACT;CAEA,MAAM,yBAAyB,eAA+B;EAE5D,IAAI,cAAc,MAAM,QACtB,OAAO,KAAK;EAEd,MAAM,OAAO,MAAM;EAGnB,OAFgB,wBAAwB,UAAU,IAAI,KAAK;CAG7D;CAKA,IAAI,iBAAiB;CACrB,IAAI,eAAe,KAAK;CAGxB,IAAI,cAAc,QAChB,iBAAiB,KAAK,IACpB,gBACA,wBAAwB,SAAS,CACnC;CAIF,IAAI,cAAc,QAChB,iBAAiB,KAAK,IAAI,gBAAgB,SAAS;CAIrD,IAAI,eAAe,QAAW;EAC5B,MAAM,WAAW,aAAa,KAAK,aAAa;EAChD,eAAe,KAAK,IAAI,cAAc,sBAAsB,OAAO,CAAC;CACtE;CAGA,IAAI,eAAe,QACjB,eAAe,KAAK,IAClB,eACC,aAAa,kBAAkB,UAClC;CAIF,IAAI,kBAAkB,cACpB,OAAO;CAGT,OAAO,KAAK,MAAM,gBAAgB,YAAY;AAChD"}
1
+ {"version":3,"file":"getChunk.mjs","names":[],"sources":["../../../src/utils/getChunk.ts"],"sourcesContent":["import { splitTextByLines } from './splitTextByLine';\n\ntype TrunkOptions = {\n lineStart?: number;\n lineLength?: number;\n charStart?: number;\n charLength?: number;\n};\n\nexport const getChunk = (text: string, options: TrunkOptions = {}): string => {\n const { lineStart, lineLength, charStart, charLength } = options;\n\n // Fast-path: if no filters were provided, return the whole text\n if (\n lineStart === undefined &&\n lineLength === undefined &&\n charStart === undefined &&\n charLength === undefined\n ) {\n return text;\n }\n\n // ---------------------------------------------------------------------------\n // Utility helpers to convert between line numbers and absolute char indices\n // ---------------------------------------------------------------------------\n const lines = splitTextByLines(text);\n\n const getCharIndexOfLineStart = (lineNumber: number): number => {\n if (lineNumber <= 0) return 0;\n // Sum the length of every previous line\n let idx = 0;\n for (let i = 0; i < Math.min(lineNumber, lines.length); i++) {\n idx += lines[i]?.length ?? 0;\n }\n return idx;\n };\n\n const getCharIndexOfLineEnd = (lineNumber: number): number => {\n // If the requested line number exceeds the number of lines, clamp to the last character\n if (lineNumber >= lines.length) {\n return text.length;\n }\n const line = lines[lineNumber];\n const lineEnd = getCharIndexOfLineStart(lineNumber) + (line?.length ?? 0);\n\n return lineEnd;\n };\n\n // ---------------------------------------------------------------------------\n // Compute the effective (inclusive) charStart/charEnd for the requested slice\n // ---------------------------------------------------------------------------\n let effectiveStart = 0; // inclusive\n let effectiveEnd = text.length; // exclusive\n\n // Apply line boundaries if provided\n if (lineStart !== undefined) {\n effectiveStart = Math.max(\n effectiveStart,\n getCharIndexOfLineStart(lineStart)\n );\n }\n\n // Apply character boundaries if provided\n if (charStart !== undefined) {\n effectiveStart = Math.max(effectiveStart, charStart);\n }\n\n // Apply line length boundary from lineStart (or 0)\n if (lineLength !== undefined) {\n const endLine = (lineStart ?? 0) + lineLength - 1;\n effectiveEnd = Math.min(effectiveEnd, getCharIndexOfLineEnd(endLine));\n }\n\n // Apply character length boundary from effectiveStart\n if (charLength !== undefined) {\n effectiveEnd = Math.min(\n effectiveEnd,\n (charStart ?? effectiveStart) + charLength\n );\n }\n\n // If bounds do not overlap, return empty string\n if (effectiveStart >= effectiveEnd) {\n return '';\n }\n\n return text.slice(effectiveStart, effectiveEnd);\n};\n"],"mappings":";;;AASA,MAAa,YAAY,MAAc,UAAwB,CAAC,MAAc;CAC5E,MAAM,EAAE,WAAW,YAAY,WAAW,eAAe;CAGzD,IACE,cAAc,UACd,eAAe,UACf,cAAc,UACd,eAAe,QAEf,OAAO;CAMT,MAAM,QAAQ,iBAAiB,IAAI;CAEnC,MAAM,2BAA2B,eAA+B;EAC9D,IAAI,cAAc,GAAG,OAAO;EAE5B,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,IAAI,YAAY,MAAM,MAAM,GAAG,KACtD,OAAO,MAAM,EAAE,EAAE,UAAU;EAE7B,OAAO;CACT;CAEA,MAAM,yBAAyB,eAA+B;EAE5D,IAAI,cAAc,MAAM,QACtB,OAAO,KAAK;EAEd,MAAM,OAAO,MAAM;EAGnB,OAFgB,wBAAwB,UAAU,KAAK,MAAM,UAAU;CAGzE;CAKA,IAAI,iBAAiB;CACrB,IAAI,eAAe,KAAK;CAGxB,IAAI,cAAc,QAChB,iBAAiB,KAAK,IACpB,gBACA,wBAAwB,SAAS,CACnC;CAIF,IAAI,cAAc,QAChB,iBAAiB,KAAK,IAAI,gBAAgB,SAAS;CAIrD,IAAI,eAAe,QAAW;EAC5B,MAAM,WAAW,aAAa,KAAK,aAAa;EAChD,eAAe,KAAK,IAAI,cAAc,sBAAsB,OAAO,CAAC;CACtE;CAGA,IAAI,eAAe,QACjB,eAAe,KAAK,IAClB,eACC,aAAa,kBAAkB,UAClC;CAIF,IAAI,kBAAkB,cACpB,OAAO;CAGT,OAAO,KAAK,MAAM,gBAAgB,YAAY;AAChD"}
@@ -0,0 +1,115 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+
5
+ //#region src/utils/getProcessChainCommands.ts
6
+ /** How long listing the processes may take before giving up. */
7
+ const PROCESS_LIST_TIMEOUT_MS = 2e3;
8
+ /**
9
+ * Parses the output of `ps -A -o pid=,ppid=,command=`.
10
+ *
11
+ * @param output - One `<pid> <ppid> <command>` row per line.
12
+ * @returns The processes, keyed by PID.
13
+ */
14
+ const parseProcessTable = (output) => {
15
+ const processes = /* @__PURE__ */ new Map();
16
+ for (const line of output.split("\n")) {
17
+ const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
18
+ if (!match) continue;
19
+ processes.set(Number(match[1]), {
20
+ parentPid: Number(match[2]),
21
+ command: match[3].trim()
22
+ });
23
+ }
24
+ return processes;
25
+ };
26
+ /** Reads one process from `/proc` (Linux): no subprocess needed. */
27
+ const readLinuxProcess = async (pid) => {
28
+ try {
29
+ const stat = await readFile(`/proc/${pid}/stat`, "utf8");
30
+ const [, parentPidField] = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
31
+ const commandLine = await readFile(`/proc/${pid}/cmdline`, "utf8");
32
+ return {
33
+ parentPid: Number(parentPidField),
34
+ command: commandLine.split("\0").join(" ").trim()
35
+ };
36
+ } catch {
37
+ return;
38
+ }
39
+ };
40
+ /** Lists every process through one `ps` call (macOS, BSD). */
41
+ const readProcessTable = async () => {
42
+ try {
43
+ const { stdout } = await promisify(execFile)("ps", [
44
+ "-A",
45
+ "-o",
46
+ "pid=,ppid=,command="
47
+ ], {
48
+ timeout: PROCESS_LIST_TIMEOUT_MS,
49
+ maxBuffer: 16777216
50
+ });
51
+ return parseProcessTable(stdout);
52
+ } catch {
53
+ return /* @__PURE__ */ new Map();
54
+ }
55
+ };
56
+ /**
57
+ * Whether reading the process list is allowed without side effects.
58
+ *
59
+ * Under Deno, a missing permission makes an interactive process stop and
60
+ * prompt the user, so the list is only read when access is already granted.
61
+ * Node and Bun have no permission model on these calls.
62
+ */
63
+ const getCanReadProcessList = () => {
64
+ const permissions = globalThis.Deno?.permissions;
65
+ if (!permissions) return true;
66
+ try {
67
+ const descriptor = process.platform === "linux" ? {
68
+ name: "read",
69
+ path: "/proc"
70
+ } : {
71
+ name: "run",
72
+ command: "ps"
73
+ };
74
+ return permissions.querySync?.(descriptor).state === "granted";
75
+ } catch {
76
+ return false;
77
+ }
78
+ };
79
+ /**
80
+ * Command lines of this process and of the processes above it, closest first.
81
+ *
82
+ * This process is included because some runtimes restart the script inside
83
+ * the same process, leaving their watch flag only on their own command line
84
+ * (`deno run --watch`).
85
+ *
86
+ * Best effort: resolves to an empty list on Windows, or when the process list
87
+ * cannot be read (sandbox, missing `ps`, Deno without the permission), and
88
+ * never throws.
89
+ *
90
+ * @param maxDepth - How many processes to read at most, this one included.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * await getProcessChainCommands();
95
+ * // → ['node src/index.js', 'node …/nodemon src/index.js', 'npm run dev']
96
+ * ```
97
+ */
98
+ const getProcessChainCommands = async (maxDepth = 6) => {
99
+ if (process.platform === "win32") return [];
100
+ if (!getCanReadProcessList()) return [];
101
+ const getProcess = process.platform === "linux" ? readLinuxProcess : await readProcessTable().then((processes) => async (pid) => processes.get(pid));
102
+ const commands = [];
103
+ let pid = process.pid;
104
+ while (pid > 1 && commands.length < maxDepth) {
105
+ const entry = await getProcess(pid);
106
+ if (!entry) break;
107
+ commands.push(entry.command);
108
+ pid = entry.parentPid;
109
+ }
110
+ return commands;
111
+ };
112
+
113
+ //#endregion
114
+ export { getProcessChainCommands, parseProcessTable };
115
+ //# sourceMappingURL=getProcessChainCommands.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getProcessChainCommands.mjs","names":[],"sources":["../../../src/utils/getProcessChainCommands.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { readFile } from 'node:fs/promises';\nimport { promisify } from 'node:util';\n\n/** Process as listed by the OS: its parent and the command line it runs. */\nexport type ProcessEntry = {\n parentPid: number;\n command: string;\n};\n\n/** How long listing the processes may take before giving up. */\nconst PROCESS_LIST_TIMEOUT_MS = 2000;\n\n/**\n * Parses the output of `ps -A -o pid=,ppid=,command=`.\n *\n * @param output - One `<pid> <ppid> <command>` row per line.\n * @returns The processes, keyed by PID.\n */\nexport const parseProcessTable = (\n output: string\n): Map<number, ProcessEntry> => {\n const processes = new Map<number, ProcessEntry>();\n\n for (const line of output.split('\\n')) {\n const match = /^\\s*(\\d+)\\s+(\\d+)\\s+(.*)$/.exec(line);\n\n if (!match) continue;\n\n processes.set(Number(match[1]), {\n parentPid: Number(match[2]),\n command: match[3]!.trim(),\n });\n }\n\n return processes;\n};\n\n/** Reads one process from `/proc` (Linux): no subprocess needed. */\nconst readLinuxProcess = async (\n pid: number\n): Promise<ProcessEntry | undefined> => {\n try {\n const stat = await readFile(`/proc/${pid}/stat`, 'utf8');\n // The command name is wrapped in parentheses and may contain spaces, so the\n // fields are read after the last `)`: `<state> <ppid> …`.\n const [, parentPidField] = stat.slice(stat.lastIndexOf(')') + 2).split(' ');\n const commandLine = await readFile(`/proc/${pid}/cmdline`, 'utf8');\n\n return {\n parentPid: Number(parentPidField),\n command: commandLine.split('\\0').join(' ').trim(),\n };\n } catch {\n return undefined;\n }\n};\n\n/** Lists every process through one `ps` call (macOS, BSD). */\nconst readProcessTable = async (): Promise<Map<number, ProcessEntry>> => {\n try {\n const { stdout } = await promisify(execFile)(\n 'ps',\n ['-A', '-o', 'pid=,ppid=,command='],\n { timeout: PROCESS_LIST_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }\n );\n\n return parseProcessTable(stdout);\n } catch {\n return new Map();\n }\n};\n\ntype DenoPermissionState = 'granted' | 'prompt' | 'denied';\n\ntype DenoPermissions = {\n querySync?: (descriptor: {\n name: 'run' | 'read';\n command?: string;\n path?: string;\n }) => { state: DenoPermissionState };\n};\n\n/**\n * Whether reading the process list is allowed without side effects.\n *\n * Under Deno, a missing permission makes an interactive process stop and\n * prompt the user, so the list is only read when access is already granted.\n * Node and Bun have no permission model on these calls.\n */\nconst getCanReadProcessList = (): boolean => {\n const permissions = (\n globalThis as { Deno?: { permissions?: DenoPermissions } }\n ).Deno?.permissions;\n\n if (!permissions) return true;\n\n try {\n const descriptor =\n process.platform === 'linux'\n ? ({ name: 'read', path: '/proc' } as const)\n : ({ name: 'run', command: 'ps' } as const);\n\n return permissions.querySync?.(descriptor).state === 'granted';\n } catch {\n return false;\n }\n};\n\n/**\n * Command lines of this process and of the processes above it, closest first.\n *\n * This process is included because some runtimes restart the script inside\n * the same process, leaving their watch flag only on their own command line\n * (`deno run --watch`).\n *\n * Best effort: resolves to an empty list on Windows, or when the process list\n * cannot be read (sandbox, missing `ps`, Deno without the permission), and\n * never throws.\n *\n * @param maxDepth - How many processes to read at most, this one included.\n *\n * @example\n * ```ts\n * await getProcessChainCommands();\n * // → ['node src/index.js', 'node …/nodemon src/index.js', 'npm run dev']\n * ```\n */\nexport const getProcessChainCommands = async (\n maxDepth = 6\n): Promise<string[]> => {\n if (process.platform === 'win32') return [];\n if (!getCanReadProcessList()) return [];\n\n const getProcess: (pid: number) => Promise<ProcessEntry | undefined> =\n process.platform === 'linux'\n ? readLinuxProcess\n : await readProcessTable().then(\n (processes) => async (pid: number) => processes.get(pid)\n );\n\n const commands: string[] = [];\n let pid = process.pid;\n\n // PID 1 is init/launchd: nothing above it is a dev tool.\n while (pid > 1 && commands.length < maxDepth) {\n const entry = await getProcess(pid);\n\n if (!entry) break;\n\n commands.push(entry.command);\n pid = entry.parentPid;\n }\n\n return commands;\n};\n"],"mappings":";;;;;;AAWA,MAAM,0BAA0B;;;;;;;AAQhC,MAAa,qBACX,WAC8B;CAC9B,MAAM,4BAAY,IAAI,IAA0B;CAEhD,KAAK,MAAM,QAAQ,OAAO,MAAM,IAAI,GAAG;EACrC,MAAM,QAAQ,4BAA4B,KAAK,IAAI;EAEnD,IAAI,CAAC,OAAO;EAEZ,UAAU,IAAI,OAAO,MAAM,EAAE,GAAG;GAC9B,WAAW,OAAO,MAAM,EAAE;GAC1B,SAAS,MAAM,EAAE,CAAE,KAAK;EAC1B,CAAC;CACH;CAEA,OAAO;AACT;;AAGA,MAAM,mBAAmB,OACvB,QACsC;CACtC,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,SAAS,IAAI,QAAQ,MAAM;EAGvD,MAAM,GAAG,kBAAkB,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG;EAC1E,MAAM,cAAc,MAAM,SAAS,SAAS,IAAI,WAAW,MAAM;EAEjE,OAAO;GACL,WAAW,OAAO,cAAc;GAChC,SAAS,YAAY,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK;EAClD;CACF,QAAQ;EACN;CACF;AACF;;AAGA,MAAM,mBAAmB,YAAgD;CACvE,IAAI;EACF,MAAM,EAAE,WAAW,MAAM,UAAU,QAAQ,CAAC,CAC1C,MACA;GAAC;GAAM;GAAM;EAAqB,GAClC;GAAE,SAAS;GAAyB,WAAW;EAAiB,CAClE;EAEA,OAAO,kBAAkB,MAAM;CACjC,QAAQ;EACN,uBAAO,IAAI,IAAI;CACjB;AACF;;;;;;;;AAmBA,MAAM,8BAAuC;CAC3C,MAAM,cACJ,WACA,MAAM;CAER,IAAI,CAAC,aAAa,OAAO;CAEzB,IAAI;EACF,MAAM,aACJ,QAAQ,aAAa,UAChB;GAAE,MAAM;GAAQ,MAAM;EAAQ,IAC9B;GAAE,MAAM;GAAO,SAAS;EAAK;EAEpC,OAAO,YAAY,YAAY,UAAU,CAAC,CAAC,UAAU;CACvD,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,0BAA0B,OACrC,WAAW,MACW;CACtB,IAAI,QAAQ,aAAa,SAAS,OAAO,CAAC;CAC1C,IAAI,CAAC,sBAAsB,GAAG,OAAO,CAAC;CAEtC,MAAM,aACJ,QAAQ,aAAa,UACjB,mBACA,MAAM,iBAAiB,CAAC,CAAC,MACtB,cAAc,OAAO,QAAgB,UAAU,IAAI,GAAG,CACzD;CAEN,MAAM,WAAqB,CAAC;CAC5B,IAAI,MAAM,QAAQ;CAGlB,OAAO,MAAM,KAAK,SAAS,SAAS,UAAU;EAC5C,MAAM,QAAQ,MAAM,WAAW,GAAG;EAElC,IAAI,CAAC,OAAO;EAEZ,SAAS,KAAK,MAAM,OAAO;EAC3B,MAAM,MAAM;CACd;CAEA,OAAO;AACT"}
@@ -20,7 +20,8 @@ import { resolveRelativePath } from "./resolveRelativePath.mjs";
20
20
  import { runOnce } from "./runOnce.mjs";
21
21
  import { runParallel } from "./runParallel/index.mjs";
22
22
  import { sortAlphabetically } from "./sortAlphabetically.mjs";
23
+ import { startContentWatcher } from "./startContentWatcher.mjs";
23
24
  import { verifyIdenticObjectFormat } from "./verifyIdenticObjectFormat.mjs";
24
25
  import { withFileLock } from "./withFileLock.mjs";
25
26
 
26
- export { CLI_CONTENT_WATCHER_ENV_VAR, Queue, acquireContentWatcherLock, assembleJSON, autoDecorateContent, buildComponentFilesList, chunkArray, chunkJSON, claimCliContentWatcher, excludeObjectFormat, formatLocale, formatPath, getChunk, getCliContentWatcherLabel, getContentExtension, getContentWatcherLockFilePath, getContentWatcherOwner, getExtensionFromFormat, getFormatFromExtension, getGlobalLimiter, getPathHash, getTaskLimiter, mergeChunks, pLimit, parallelize, parallelizeGlobal, parseContentDeclarationFileName, reconstructFromSingleChunk, reduceObjectFormat, reportRedundantContentWatcher, resolveObjectPromises, resolveRelativePath, runOnce, runParallel, sortAlphabetically, splitTextByLines, verifyIdenticObjectFormat, withFileLock };
27
+ export { CLI_CONTENT_WATCHER_ENV_VAR, Queue, acquireContentWatcherLock, assembleJSON, autoDecorateContent, buildComponentFilesList, chunkArray, chunkJSON, claimCliContentWatcher, excludeObjectFormat, formatLocale, formatPath, getChunk, getCliContentWatcherLabel, getContentExtension, getContentWatcherLockFilePath, getContentWatcherOwner, getExtensionFromFormat, getFormatFromExtension, getGlobalLimiter, getPathHash, getTaskLimiter, mergeChunks, pLimit, parallelize, parallelizeGlobal, parseContentDeclarationFileName, reconstructFromSingleChunk, reduceObjectFormat, reportRedundantContentWatcher, resolveObjectPromises, resolveRelativePath, runOnce, runParallel, sortAlphabetically, splitTextByLines, startContentWatcher, verifyIdenticObjectFormat, withFileLock };
@@ -11,7 +11,7 @@ const runParallel = (proc) => {
11
11
  if (!proc || Array.isArray(proc) && proc.length === 0) throw new Error("Invalid command");
12
12
  const commandText = Array.isArray(proc) ? proc.join(" ") : proc;
13
13
  const isArray = Array.isArray(proc);
14
- const command = isArray ? proc[0] : commandText;
14
+ const command = isArray ? proc[0] ?? "" : commandText;
15
15
  const args = isArray ? proc.slice(1) : [];
16
16
  const cwdBin = join(process.cwd(), "node_modules", ".bin");
17
17
  const PATH_KEY = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/utils/runParallel/index.ts"],"sourcesContent":["import { delimiter, join } from 'node:path';\nimport { spawnPosix } from './spawnPosix';\nimport { spawnWin32 } from './spawnWin32';\n\nexport type ParallelHandle = {\n /**\n * Terminate the child and its whole process group.\n * @param signal Signal to send (default `SIGTERM`). Pass `SIGKILL` to force-kill\n * a child that ignores graceful termination.\n */\n kill: (signal?: NodeJS.Signals) => void;\n result: Promise<any>;\n commandText: string;\n};\n\n/**\n * Start a cross-platform parallel process using npm-run-all approach.\n * Accepts either a single string (e.g., 'next start') or an array of tokens (e.g., ['next', 'start']).\n */\nexport const runParallel = (proc?: string | string[]): ParallelHandle => {\n if (!proc || (Array.isArray(proc) && proc.length === 0))\n throw new Error('Invalid command');\n\n const commandText = Array.isArray(proc) ? proc.join(' ') : proc;\n\n const isArray = Array.isArray(proc);\n const command = isArray ? (proc as string[])[0] : commandText;\n const args = isArray ? (proc as string[]).slice(1) : [];\n\n // Ensure local binaries (node_modules/.bin) are resolvable\n const cwdBin = join(process.cwd(), 'node_modules', '.bin');\n const PATH_KEY =\n Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ??\n 'PATH';\n\n const extendedPath = [cwdBin, process.env[PATH_KEY] ?? '']\n .filter(Boolean)\n .join(delimiter);\n\n const childEnv = {\n ...process.env,\n [PATH_KEY]: extendedPath,\n } as NodeJS.ProcessEnv;\n\n const isWin = process.platform === 'win32';\n const spawnFunc = isWin ? spawnWin32 : spawnPosix;\n\n // Spawn options\n const spawnOptions = {\n cwd: process.cwd(),\n stdio: 'inherit' as const,\n env: childEnv,\n shell: isWin,\n };\n\n // Spawn the child process\n const child = isArray\n ? // If provided as a single string element that includes spaces, treat it like a shell command\n args.length === 0 && /\\s/.test(command)\n ? isWin\n ? spawnFunc(\n process.env.ComSpec ?? 'cmd.exe',\n ['/d', '/s', '/c', command],\n spawnOptions\n )\n : spawnFunc(\n process.env.SHELL ?? '/bin/sh',\n ['-c', command],\n spawnOptions\n )\n : spawnFunc(command, args, spawnOptions)\n : isWin\n ? spawnFunc(\n process.env.ComSpec ?? 'cmd.exe',\n ['/d', '/s', '/c', commandText],\n spawnOptions\n )\n : spawnFunc(\n process.env.SHELL ?? '/bin/sh',\n ['-c', commandText],\n spawnOptions\n );\n\n const result = new Promise<void>((resolve, reject) => {\n child.on('error', (err) => {\n try {\n console.error(\n `[runParallel] Failed to start: ${err?.message ?? String(err)}`\n );\n } catch {}\n reject(err);\n });\n\n child.on('exit', (code, signal) => {\n // Treat common termination signals as graceful exits, not failures\n const gracefulSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP'];\n // Also treat shell-convention exit codes (128 + signal number) as graceful:\n // 129 = SIGHUP, 130 = SIGINT, 131 = SIGQUIT, 143 = SIGTERM\n const gracefulSignalCodes = new Set([129, 130, 131, 143]);\n if (\n code === 0 ||\n gracefulSignalCodes.has(code ?? -1) ||\n (signal && gracefulSignals.includes(signal))\n ) {\n resolve();\n } else {\n reject(\n Object.assign(new Error('Parallel process failed'), { code, signal })\n );\n }\n });\n });\n\n const kill = (signal: NodeJS.Signals = 'SIGTERM') => {\n try {\n child.kill(signal);\n } catch {\n // Best effort\n }\n };\n\n const handleExit = () => kill();\n\n const handleSigInt = () => {\n kill();\n process.off('SIGINT', handleSigInt);\n process.kill(process.pid, 'SIGINT'); // Propagate to allow natural shutdown\n };\n\n const handleSigTerm = () => {\n kill();\n process.off('SIGTERM', handleSigTerm);\n process.kill(process.pid, 'SIGTERM'); // Propagate to allow natural shutdown\n };\n\n process.on('exit', handleExit);\n process.on('SIGINT', handleSigInt);\n process.on('SIGTERM', handleSigTerm);\n\n child.on('exit', () => {\n process.off('exit', handleExit);\n process.off('SIGINT', handleSigInt);\n process.off('SIGTERM', handleSigTerm);\n });\n\n return { kill, result, commandText };\n};\n"],"mappings":";;;;;;;;;AAmBA,MAAa,eAAe,SAA6C;CACvE,IAAI,CAAC,QAAS,MAAM,QAAQ,IAAI,KAAK,KAAK,WAAW,GACnD,MAAM,IAAI,MAAM,iBAAiB;CAEnC,MAAM,cAAc,MAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,GAAG,IAAI;CAE3D,MAAM,UAAU,MAAM,QAAQ,IAAI;CAClC,MAAM,UAAU,UAAW,KAAkB,KAAK;CAClD,MAAM,OAAO,UAAW,KAAkB,MAAM,CAAC,IAAI,CAAC;CAGtD,MAAM,SAAS,KAAK,QAAQ,IAAI,GAAG,gBAAgB,MAAM;CACzD,MAAM,WACJ,OAAO,KAAK,QAAQ,GAAG,CAAC,CAAC,MAAM,QAAQ,IAAI,YAAY,MAAM,MAAM,KACnE;CAEF,MAAM,eAAe,CAAC,QAAQ,QAAQ,IAAI,aAAa,EAAE,CAAC,CACvD,OAAO,OAAO,CAAC,CACf,KAAK,SAAS;CAEjB,MAAM,WAAW;EACf,GAAG,QAAQ;GACV,WAAW;CACd;CAEA,MAAM,QAAQ,QAAQ,aAAa;CACnC,MAAM,YAAY,QAAQ,aAAa;CAGvC,MAAM,eAAe;EACnB,KAAK,QAAQ,IAAI;EACjB,OAAO;EACP,KAAK;EACL,OAAO;CACT;CAGA,MAAM,QAAQ,UAEV,KAAK,WAAW,KAAK,KAAK,KAAK,OAAO,IACpC,QACE,UACE,QAAQ,IAAI,WAAW,WACvB;EAAC;EAAM;EAAM;EAAM;CAAO,GAC1B,YACF,IACA,UACE,QAAQ,IAAI,SAAS,WACrB,CAAC,MAAM,OAAO,GACd,YACF,IACF,UAAU,SAAS,MAAM,YAAY,IACvC,QACE,UACE,QAAQ,IAAI,WAAW,WACvB;EAAC;EAAM;EAAM;EAAM;CAAW,GAC9B,YACF,IACA,UACE,QAAQ,IAAI,SAAS,WACrB,CAAC,MAAM,WAAW,GAClB,YACF;CAEN,MAAM,SAAS,IAAI,SAAe,SAAS,WAAW;EACpD,MAAM,GAAG,UAAU,QAAQ;GACzB,IAAI;IACF,QAAQ,MACN,kCAAkC,KAAK,WAAW,OAAO,GAAG,GAC9D;GACF,QAAQ,CAAC;GACT,OAAO,GAAG;EACZ,CAAC;EAED,MAAM,GAAG,SAAS,MAAM,WAAW;GAMjC,IACE,SAAS,sBACT,IAH8B,IAAI;IAAC;IAAK;IAAK;IAAK;GAAG,CAGnC,EAAC,CAAC,IAAI,QAAQ,EAAE,KACjC,UAAU;IAPY;IAAU;IAAW;IAAW;GAO9B,CAAC,CAAC,SAAS,MAAM,GAE1C,QAAQ;QAER,OACE,OAAO,uBAAO,IAAI,MAAM,yBAAyB,GAAG;IAAE;IAAM;GAAO,CAAC,CACtE;EAEJ,CAAC;CACH,CAAC;CAED,MAAM,QAAQ,SAAyB,cAAc;EACnD,IAAI;GACF,MAAM,KAAK,MAAM;EACnB,QAAQ,CAER;CACF;CAEA,MAAM,mBAAmB,KAAK;CAE9B,MAAM,qBAAqB;EACzB,KAAK;EACL,QAAQ,IAAI,UAAU,YAAY;EAClC,QAAQ,KAAK,QAAQ,KAAK,QAAQ;CACpC;CAEA,MAAM,sBAAsB;EAC1B,KAAK;EACL,QAAQ,IAAI,WAAW,aAAa;EACpC,QAAQ,KAAK,QAAQ,KAAK,SAAS;CACrC;CAEA,QAAQ,GAAG,QAAQ,UAAU;CAC7B,QAAQ,GAAG,UAAU,YAAY;CACjC,QAAQ,GAAG,WAAW,aAAa;CAEnC,MAAM,GAAG,cAAc;EACrB,QAAQ,IAAI,QAAQ,UAAU;EAC9B,QAAQ,IAAI,UAAU,YAAY;EAClC,QAAQ,IAAI,WAAW,aAAa;CACtC,CAAC;CAED,OAAO;EAAE;EAAM;EAAQ;CAAY;AACrC"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/utils/runParallel/index.ts"],"sourcesContent":["import { delimiter, join } from 'node:path';\nimport { spawnPosix } from './spawnPosix';\nimport { spawnWin32 } from './spawnWin32';\n\nexport type ParallelHandle = {\n /**\n * Terminate the child and its whole process group.\n * @param signal Signal to send (default `SIGTERM`). Pass `SIGKILL` to force-kill\n * a child that ignores graceful termination.\n */\n kill: (signal?: NodeJS.Signals) => void;\n result: Promise<any>;\n commandText: string;\n};\n\n/**\n * Start a cross-platform parallel process using npm-run-all approach.\n * Accepts either a single string (e.g., 'next start') or an array of tokens (e.g., ['next', 'start']).\n */\nexport const runParallel = (proc?: string | string[]): ParallelHandle => {\n if (!proc || (Array.isArray(proc) && proc.length === 0))\n throw new Error('Invalid command');\n\n const commandText = Array.isArray(proc) ? proc.join(' ') : proc;\n\n const isArray = Array.isArray(proc);\n const command = isArray ? ((proc as string[])[0] ?? '') : commandText;\n const args = isArray ? (proc as string[]).slice(1) : [];\n\n // Ensure local binaries (node_modules/.bin) are resolvable\n const cwdBin = join(process.cwd(), 'node_modules', '.bin');\n const PATH_KEY =\n Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ??\n 'PATH';\n\n const extendedPath = [cwdBin, process.env[PATH_KEY] ?? '']\n .filter(Boolean)\n .join(delimiter);\n\n const childEnv = {\n ...process.env,\n [PATH_KEY]: extendedPath,\n } as NodeJS.ProcessEnv;\n\n const isWin = process.platform === 'win32';\n const spawnFunc = isWin ? spawnWin32 : spawnPosix;\n\n // Spawn options\n const spawnOptions = {\n cwd: process.cwd(),\n stdio: 'inherit' as const,\n env: childEnv,\n shell: isWin,\n };\n\n // Spawn the child process\n const child = isArray\n ? // If provided as a single string element that includes spaces, treat it like a shell command\n args.length === 0 && /\\s/.test(command)\n ? isWin\n ? spawnFunc(\n process.env.ComSpec ?? 'cmd.exe',\n ['/d', '/s', '/c', command],\n spawnOptions\n )\n : spawnFunc(\n process.env.SHELL ?? '/bin/sh',\n ['-c', command],\n spawnOptions\n )\n : spawnFunc(command, args, spawnOptions)\n : isWin\n ? spawnFunc(\n process.env.ComSpec ?? 'cmd.exe',\n ['/d', '/s', '/c', commandText],\n spawnOptions\n )\n : spawnFunc(\n process.env.SHELL ?? '/bin/sh',\n ['-c', commandText],\n spawnOptions\n );\n\n const result = new Promise<void>((resolve, reject) => {\n child.on('error', (err) => {\n try {\n console.error(\n `[runParallel] Failed to start: ${err?.message ?? String(err)}`\n );\n } catch {}\n reject(err);\n });\n\n child.on('exit', (code, signal) => {\n // Treat common termination signals as graceful exits, not failures\n const gracefulSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP'];\n // Also treat shell-convention exit codes (128 + signal number) as graceful:\n // 129 = SIGHUP, 130 = SIGINT, 131 = SIGQUIT, 143 = SIGTERM\n const gracefulSignalCodes = new Set([129, 130, 131, 143]);\n if (\n code === 0 ||\n gracefulSignalCodes.has(code ?? -1) ||\n (signal && gracefulSignals.includes(signal))\n ) {\n resolve();\n } else {\n reject(\n Object.assign(new Error('Parallel process failed'), { code, signal })\n );\n }\n });\n });\n\n const kill = (signal: NodeJS.Signals = 'SIGTERM') => {\n try {\n child.kill(signal);\n } catch {\n // Best effort\n }\n };\n\n const handleExit = () => kill();\n\n const handleSigInt = () => {\n kill();\n process.off('SIGINT', handleSigInt);\n process.kill(process.pid, 'SIGINT'); // Propagate to allow natural shutdown\n };\n\n const handleSigTerm = () => {\n kill();\n process.off('SIGTERM', handleSigTerm);\n process.kill(process.pid, 'SIGTERM'); // Propagate to allow natural shutdown\n };\n\n process.on('exit', handleExit);\n process.on('SIGINT', handleSigInt);\n process.on('SIGTERM', handleSigTerm);\n\n child.on('exit', () => {\n process.off('exit', handleExit);\n process.off('SIGINT', handleSigInt);\n process.off('SIGTERM', handleSigTerm);\n });\n\n return { kill, result, commandText };\n};\n"],"mappings":";;;;;;;;;AAmBA,MAAa,eAAe,SAA6C;CACvE,IAAI,CAAC,QAAS,MAAM,QAAQ,IAAI,KAAK,KAAK,WAAW,GACnD,MAAM,IAAI,MAAM,iBAAiB;CAEnC,MAAM,cAAc,MAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,GAAG,IAAI;CAE3D,MAAM,UAAU,MAAM,QAAQ,IAAI;CAClC,MAAM,UAAU,UAAY,KAAkB,MAAM,KAAM;CAC1D,MAAM,OAAO,UAAW,KAAkB,MAAM,CAAC,IAAI,CAAC;CAGtD,MAAM,SAAS,KAAK,QAAQ,IAAI,GAAG,gBAAgB,MAAM;CACzD,MAAM,WACJ,OAAO,KAAK,QAAQ,GAAG,CAAC,CAAC,MAAM,QAAQ,IAAI,YAAY,MAAM,MAAM,KACnE;CAEF,MAAM,eAAe,CAAC,QAAQ,QAAQ,IAAI,aAAa,EAAE,CAAC,CACvD,OAAO,OAAO,CAAC,CACf,KAAK,SAAS;CAEjB,MAAM,WAAW;EACf,GAAG,QAAQ;GACV,WAAW;CACd;CAEA,MAAM,QAAQ,QAAQ,aAAa;CACnC,MAAM,YAAY,QAAQ,aAAa;CAGvC,MAAM,eAAe;EACnB,KAAK,QAAQ,IAAI;EACjB,OAAO;EACP,KAAK;EACL,OAAO;CACT;CAGA,MAAM,QAAQ,UAEV,KAAK,WAAW,KAAK,KAAK,KAAK,OAAO,IACpC,QACE,UACE,QAAQ,IAAI,WAAW,WACvB;EAAC;EAAM;EAAM;EAAM;CAAO,GAC1B,YACF,IACA,UACE,QAAQ,IAAI,SAAS,WACrB,CAAC,MAAM,OAAO,GACd,YACF,IACF,UAAU,SAAS,MAAM,YAAY,IACvC,QACE,UACE,QAAQ,IAAI,WAAW,WACvB;EAAC;EAAM;EAAM;EAAM;CAAW,GAC9B,YACF,IACA,UACE,QAAQ,IAAI,SAAS,WACrB,CAAC,MAAM,WAAW,GAClB,YACF;CAEN,MAAM,SAAS,IAAI,SAAe,SAAS,WAAW;EACpD,MAAM,GAAG,UAAU,QAAQ;GACzB,IAAI;IACF,QAAQ,MACN,kCAAkC,KAAK,WAAW,OAAO,GAAG,GAC9D;GACF,QAAQ,CAAC;GACT,OAAO,GAAG;EACZ,CAAC;EAED,MAAM,GAAG,SAAS,MAAM,WAAW;GAMjC,IACE,SAAS,sBACT,IAH8B,IAAI;IAAC;IAAK;IAAK;IAAK;GAAG,CAGnC,EAAC,CAAC,IAAI,QAAQ,EAAE,KACjC,UAAU;IAPY;IAAU;IAAW;IAAW;GAO9B,CAAC,CAAC,SAAS,MAAM,GAE1C,QAAQ;QAER,OACE,OAAO,uBAAO,IAAI,MAAM,yBAAyB,GAAG;IAAE;IAAM;GAAO,CAAC,CACtE;EAEJ,CAAC;CACH,CAAC;CAED,MAAM,QAAQ,SAAyB,cAAc;EACnD,IAAI;GACF,MAAM,KAAK,MAAM;EACnB,QAAQ,CAER;CACF;CAEA,MAAM,mBAAmB,KAAK;CAE9B,MAAM,qBAAqB;EACzB,KAAK;EACL,QAAQ,IAAI,UAAU,YAAY;EAClC,QAAQ,KAAK,QAAQ,KAAK,QAAQ;CACpC;CAEA,MAAM,sBAAsB;EAC1B,KAAK;EACL,QAAQ,IAAI,WAAW,aAAa;EACpC,QAAQ,KAAK,QAAQ,KAAK,SAAS;CACrC;CAEA,QAAQ,GAAG,QAAQ,UAAU;CAC7B,QAAQ,GAAG,UAAU,YAAY;CACjC,QAAQ,GAAG,WAAW,aAAa;CAEnC,MAAM,GAAG,cAAc;EACrB,QAAQ,IAAI,QAAQ,UAAU;EAC9B,QAAQ,IAAI,UAAU,YAAY;EAClC,QAAQ,IAAI,WAAW,aAAa;CACtC,CAAC;CAED,OAAO;EAAE;EAAM;EAAQ;CAAY;AACrC"}