{"version":3,"file":"scanWebsite.cjs","names":["colorize","GREY","GREY_LIGHT","extractScriptUrls","byteLength","checkHtmlAttributes","extractAnchors","checkBundleContent","checkRobots","checkSitemap","mutateScore","toScorePercent"],"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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"}