{"version":3,"file":"analyzeBundleContent.mjs","names":[],"sources":["../../../src/scan/analyzeBundleContent.ts"],"sourcesContent":["import { ALL_LOCALES } from '@intlayer/types/allLocales';\nimport { byteLength, extractVisibleTextStrings } from './parseHtml';\nimport type {\n  BundleChunkInput,\n  BundleContentAnalysis,\n  ChunkAnalysis,\n} from './types';\n\n/**\n * Detect and measure localized (i18n) content embedded in JavaScript bundles.\n *\n * This is a dependency-free port of the hosted backend bundle analyzer: it\n * estimates how many translation strings ship in each chunk and, of those, how\n * many belong to locales other than the one currently rendered (i.e. dead\n * weight that a build-time optimization could strip).\n */\n\nconst allLocaleValues = new Set(Object.values(ALL_LOCALES) as string[]);\n\nconst isLocaleCode = (key: string): boolean =>\n  allLocaleValues.has(key) || /^[a-z]{2}(-[A-Z]{2,4})?$/.test(key);\n\n/** Find the end index of the value following a `locale:` key. */\nconst extractValueEnd = (text: string, valueStart: number): number => {\n  let cursor = valueStart;\n  while (cursor < text.length && ' \\t\\n\\r'.includes(text[cursor])) cursor++;\n  if (cursor >= text.length) return valueStart;\n\n  const char = text[cursor];\n  if (char === '{' || char === '[') {\n    const endChar = char === '{' ? '}' : ']';\n    let depth = 1;\n    cursor++;\n    while (cursor < text.length && depth > 0) {\n      if (text[cursor] === char) depth++;\n      else if (text[cursor] === endChar) depth--;\n      else if (text[cursor] === '\"' || text[cursor] === '`') {\n        const quote = text[cursor];\n        cursor++;\n        while (cursor < text.length) {\n          if (text[cursor] === '\\\\') {\n            cursor += 2;\n            continue;\n          }\n          if (text[cursor] === quote) break;\n          cursor++;\n        }\n      }\n      cursor++;\n    }\n    return cursor;\n  }\n  if (char === '\"' || char === '`') {\n    const quote = char;\n    cursor++;\n    while (cursor < text.length) {\n      if (text[cursor] === '\\\\') {\n        cursor += 2;\n        continue;\n      }\n      if (text[cursor] === quote) {\n        cursor++;\n        break;\n      }\n      cursor++;\n    }\n    return cursor;\n  }\n  const endMatch = text.slice(cursor).search(/[,}\\]\\s]/);\n  return endMatch === -1 ? text.length : cursor + endMatch;\n};\n\n// Matches both quoted (\"en\":) and unquoted (en:) locale keys followed by {\n// Works for any i18n solution — intlayer, i18next, vue-i18n, FormatJS, etc.\nconst LOCALE_KEY_PATTERN =\n  /(?:\"([a-z]{2}(?:-[A-Z]{2,4})?)\"|\\b([a-z]{2}(?:-[A-Z]{2,4})?)\\b)\\s*:\\s*(?=\\{)/g;\n\n// Maximum character gap between two locale key positions to consider them\n// part of the same i18n object. Large enough for big translation objects.\nconst LOCALE_CLUSTER_WINDOW = 10_000;\n\n// Returns false when a candidate value is clearly not i18n text:\n// - contains hex/control escape sequences (ANSI codes, binary data)\nconst looksLikeI18nContent = (valueText: string): boolean => {\n  if (/\\\\x[0-9a-fA-F]{2}/.test(valueText)) return false;\n  if (/\\\\u00[01][0-9a-fA-F]/.test(valueText)) return false;\n  return true;\n};\n\ntype LocaleMatch = {\n  locale: string;\n  position: number;\n  valueStart: number;\n  valueEnd: number;\n  valueSize: number;\n};\n\nconst analyzeChunkLocaleContent = (\n  text: string,\n  baseCurrent: string\n): {\n  unusedLocaleSize: number;\n  usedLocaleSize: number;\n  dictionariesFound: number;\n} => {\n  // Step 1: collect all candidate locale key matches\n  const candidates: LocaleMatch[] = [];\n  const regex = new RegExp(LOCALE_KEY_PATTERN.source, 'g');\n  let match = regex.exec(text);\n\n  while (match !== null) {\n    const locale = match[1] ?? match[2];\n    if (isLocaleCode(locale)) {\n      const valueStart = match.index + match[0].length;\n      const valueEnd = extractValueEnd(text, valueStart);\n      const valueSize = valueEnd - valueStart;\n      const valueText = text.slice(valueStart, valueEnd);\n      if (valueSize >= 5 && looksLikeI18nContent(valueText)) {\n        candidates.push({\n          locale,\n          position: match.index,\n          valueStart,\n          valueEnd,\n          valueSize,\n        });\n      }\n    }\n    match = regex.exec(text);\n  }\n\n  // Step 2: a locale key is i18n content only if another locale key with a\n  // DIFFERENT locale code exists within LOCALE_CLUSTER_WINDOW chars.\n  const isI18nMatch = (idx: number): boolean => {\n    const base = candidates[idx].locale.split('-')[0].toLowerCase();\n    for (let j = 0; j < candidates.length; j++) {\n      if (j === idx) continue;\n      const dist = Math.abs(candidates[j].position - candidates[idx].position);\n      if (dist > LOCALE_CLUSTER_WINDOW) continue;\n      if (candidates[j].locale.split('-')[0].toLowerCase() !== base)\n        return true;\n    }\n    return false;\n  };\n\n  let unusedLocaleSize = 0;\n  let usedLocaleSize = 0;\n  let dictionariesFound = 0;\n\n  for (let i = 0; i < candidates.length; i++) {\n    if (!isI18nMatch(i)) continue;\n\n    const { locale, valueSize } = candidates[i];\n    dictionariesFound++;\n\n    if (locale.split('-')[0].toLowerCase() === baseCurrent) {\n      usedLocaleSize += valueSize;\n    } else {\n      unusedLocaleSize += valueSize;\n    }\n  }\n\n  return { unusedLocaleSize, usedLocaleSize, dictionariesFound };\n};\n\n/**\n * Analyze the locale weight of a page's JavaScript bundles.\n *\n * @param chunks - The fetched JavaScript chunks (main + lazy).\n * @param htmlContent - The page HTML, used to estimate rendered content size.\n * @param currentLocale - The locale currently rendered by the page.\n * @param totalPageSize - The total transferred bytes measured for the page.\n * @returns The aggregated {@link BundleContentAnalysis}.\n */\nexport const analyzeBundleContent = (\n  chunks: BundleChunkInput[],\n  htmlContent: string,\n  currentLocale: string,\n  totalPageSize: number\n): BundleContentAnalysis => {\n  const empty: BundleContentAnalysis = {\n    currentLocale,\n    totalPageSize,\n    renderedContentSize: 0,\n    contentSize: 0,\n    totalLocaleSize: 0,\n    totalUnusedLocaleSize: 0,\n    unusedPercentOfLocale: 0,\n    mainBundleChunks: [],\n    lazyBundleChunks: [],\n  };\n\n  if (!chunks.length && !htmlContent) return empty;\n\n  // Rendered content size from HTML visible text (deduplicated).\n  const pageStrings = new Set(extractVisibleTextStrings(htmlContent));\n\n  let renderedContentSize = 0;\n  pageStrings.forEach((text) => {\n    renderedContentSize += byteLength(text);\n  });\n\n  const baseCurrent = currentLocale.split('-')[0].toLowerCase();\n\n  const mainBundleChunks: ChunkAnalysis[] = [];\n  const lazyBundleChunks: ChunkAnalysis[] = [];\n\n  for (const chunk of chunks) {\n    const { unusedLocaleSize, usedLocaleSize, dictionariesFound } =\n      analyzeChunkLocaleContent(chunk.content, baseCurrent);\n\n    const totalLocaleSize = unusedLocaleSize + usedLocaleSize;\n    const analysis: ChunkAnalysis = {\n      url: chunk.url,\n      fileSize: byteLength(chunk.content),\n      totalLocaleSize,\n      unusedLocaleSize,\n      usedLocaleSize,\n      dictionariesFound,\n      unusedPercent:\n        totalLocaleSize > 0\n          ? Math.round((unusedLocaleSize / totalLocaleSize) * 100)\n          : 0,\n    };\n\n    if (chunk.isMainBundle) {\n      mainBundleChunks.push(analysis);\n    } else if (dictionariesFound > 0) {\n      lazyBundleChunks.push(analysis);\n    }\n  }\n\n  const totalUnusedLocaleSize =\n    mainBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0) +\n    lazyBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0);\n  const totalLocaleSize =\n    mainBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0) +\n    lazyBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0);\n  const contentSize = renderedContentSize + totalLocaleSize;\n\n  const unusedPercentOfLocale =\n    totalLocaleSize > 0\n      ? Math.round((totalUnusedLocaleSize / totalLocaleSize) * 100)\n      : 0;\n\n  return {\n    currentLocale,\n    totalPageSize,\n    renderedContentSize,\n    contentSize,\n    totalLocaleSize,\n    totalUnusedLocaleSize,\n    unusedPercentOfLocale,\n    mainBundleChunks,\n    lazyBundleChunks,\n  };\n};\n"],"mappings":"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"}