@intlayer/babel 9.5.11 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/cjs/analyzeContentUsage.cjs +54 -26
  2. package/dist/cjs/analyzeContentUsage.cjs.map +1 -1
  3. package/dist/cjs/babel-plugin-intlayer-field-rename.cjs +39 -18
  4. package/dist/cjs/babel-plugin-intlayer-field-rename.cjs.map +1 -1
  5. package/dist/cjs/babel-plugin-intlayer-purge.cjs +2 -0
  6. package/dist/cjs/babel-plugin-intlayer-purge.cjs.map +1 -1
  7. package/dist/cjs/getExtractPluginOptions.cjs.map +1 -1
  8. package/dist/cjs/staticAstReaders.cjs +96 -0
  9. package/dist/cjs/staticAstReaders.cjs.map +1 -1
  10. package/dist/cjs/transformers.cjs +36 -7
  11. package/dist/cjs/transformers.cjs.map +1 -1
  12. package/dist/esm/analyzeContentUsage.mjs +55 -27
  13. package/dist/esm/analyzeContentUsage.mjs.map +1 -1
  14. package/dist/esm/babel-plugin-intlayer-field-rename.mjs +40 -19
  15. package/dist/esm/babel-plugin-intlayer-field-rename.mjs.map +1 -1
  16. package/dist/esm/babel-plugin-intlayer-purge.mjs +2 -0
  17. package/dist/esm/babel-plugin-intlayer-purge.mjs.map +1 -1
  18. package/dist/esm/getExtractPluginOptions.mjs +1 -1
  19. package/dist/esm/getExtractPluginOptions.mjs.map +1 -1
  20. package/dist/esm/staticAstReaders.mjs +94 -1
  21. package/dist/esm/staticAstReaders.mjs.map +1 -1
  22. package/dist/esm/transformers.mjs +36 -7
  23. package/dist/esm/transformers.mjs.map +1 -1
  24. package/dist/types/analyzeContentUsage.d.ts +6 -1
  25. package/dist/types/analyzeContentUsage.d.ts.map +1 -1
  26. package/dist/types/babel-plugin-intlayer-field-rename.d.ts.map +1 -1
  27. package/dist/types/babel-plugin-intlayer-purge.d.ts.map +1 -1
  28. package/dist/types/staticAstReaders.d.ts +41 -0
  29. package/dist/types/staticAstReaders.d.ts.map +1 -1
  30. package/dist/types/transformers.d.ts +8 -1
  31. package/dist/types/transformers.d.ts.map +1 -1
  32. package/package.json +14 -14
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-purge.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { PluginObject, PluginPass } from '@babel/core';\nimport { transformSync } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { NestedFieldReferences } from '@intlayer/core/dictionaryManipulator';\nimport { formatPath } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport {\n buildNestedRenameMapFromContent,\n getNestedRenameEntryAtPath,\n} from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n preserveNestedDictionaryFields,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin still purges and minifies, but skips field renaming\n * so the `keyPath` reported to the visual editor keeps matching the unmerged\n * dictionaries it edits. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Dictionaries referenced through `nest()`, mapped to the top-level fields\n * those references read (`'all'` when the whole dictionary is nested).\n *\n * Those fields are resolved at runtime by `getNesting` using their original\n * names, so they must survive purging and must never be renamed.\n */\n nestedDictionaryReferences?: Map<string, NestedFieldReferences>;\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Logging configuration used to report what the pipeline purged and\n * minified, so a Babel- or SWC-driven build prints the same summary as the\n * Vite one. Mirrors `log` in `intlayer.config.ts`; omit to fall back to the\n * logger defaults.\n */\n logConfig?: Pick<IntlayerConfig, 'log'>;\n};\n\n/** Application logger used to report the pipeline's progress. */\ntype PurgeLogger = ReturnType<typeof getAppLogger>;\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process:\n * {@link runIntlayerPurgePipeline} registers the context before running so every\n * subsequent transform is a no-op cache hit.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */\nconst filterRecordByUsedFields = (\n record: Record<string, unknown>,\n usedFieldNames: Set<string>\n): Record<string, unknown> => {\n const filteredRecord: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n if (usedFieldNames.has(fieldName)) {\n filteredRecord[fieldName] = fieldValue;\n }\n }\n return filteredRecord;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n prunedTranslation[locale] = isPlainRecord(localeContent)\n ? filterRecordByUsedFields(localeContent, usedFieldNames)\n : localeContent;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext,\n logger: PurgeLogger\n): void => {\n let partiallyMinifiedDictionariesCount = 0;\n\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque values to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousOccurrences = [...opaqueFieldMap.values()].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(nestedRenameMap, occurrence.fieldPath)\n ?.children.size ?? 0) > 0\n );\n\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(`'${occurrence.fieldPath.join('.')}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...occurrence.locations.map(\n (location) => `\\n at ${formatPath(location)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n for (const occurrence of dangerousOccurrences) {\n const renameEntry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\n if (renameEntry) {\n renameEntry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\n/**\n * Names of the top-level content fields of a compiled dictionary, whatever\n * shape its `content` uses. Used to report which fields the purge removed.\n */\nconst readContentFieldNames = (\n content: CompiledDictionaryJson['content']\n): string[] => {\n // Shape A – fields live inside each locale object.\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n return isPlainRecord(firstLocaleValue) ? Object.keys(firstLocaleValue) : [];\n }\n\n // Shape B / dynamic – flat content record.\n if (isPlainRecord(content)) return Object.keys(content);\n\n return [];\n};\n\n/**\n * Running totals of the purge, reported once the whole pipeline is done.\n */\ntype PurgeStats = {\n /** Dictionary key → number of fields removed from it. */\n prunedFieldsCountPerDictionary: Map<string, number>;\n /**\n * Dictionary keys whose \"pruned fields\" line has already been printed. A\n * key spans several files (one static, one per locale), and they all remove\n * the same fields.\n */\n loggedPrunedDictionaryKeys: Set<string>;\n /**\n * Dictionary keys whose \"analysis is incomplete\" warning has already been\n * printed, deduplicated the same way.\n */\n loggedIncompleteAnalysisKeys: Set<string>;\n};\n\n/**\n * Reads a compiled dictionary JSON file, applies the purge and/or minify\n * transformations, and writes it back in-place when something changed.\n *\n * `dictionaryKind` selects the prune strategy and the minified output shape:\n * static dictionaries keep `{ key, content }`, dynamic per-locale\n * dictionaries additionally keep `locale`.\n */\nconst processDictionaryFile = (\n filePath: string,\n dictionaryKind: 'static' | 'dynamic',\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger,\n stats: PurgeStats\n): void => {\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(\n readFileSync(filePath, 'utf-8')\n ) as CompiledDictionaryJson;\n } catch {\n return; // Unreadable or invalid JSON – leave file unchanged.\n }\n\n const { key: dictionaryKey } = parsedDict;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(filePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const pruneDictionaryContent =\n dictionaryKind === 'static'\n ? pruneStaticDictionaryContent\n : pruneDynamicDictionaryContent;\n const originalFieldNames = readContentFieldNames(parsedDict.content);\n const { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(filePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n stats.prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!stats.loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n stats.loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n parsedDict = prunedDictionary;\n modified = true;\n } else if (\n !fieldUsage &&\n pruneContext.hasUnparsableSourceFiles &&\n !stats.loggedIncompleteAnalysisKeys.has(dictionaryKey)\n ) {\n // An unparsable source file might be the one referencing this key, so its\n // usage set cannot be trusted to be empty.\n stats.loggedIncompleteAnalysisKeys.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? dictionaryKind === 'static'\n ? { key: parsedDict.key, content: parsedDict.content }\n : {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger\n): void => {\n const stats: PurgeStats = {\n prunedFieldsCountPerDictionary: new Map(),\n loggedPrunedDictionaryKeys: new Set(),\n loggedIncompleteAnalysisKeys: new Set(),\n };\n\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processDictionaryFile(\n join(dictionariesDir, entry),\n 'static',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDictionaryFile(\n join(keyDirPath, localeFile),\n 'dynamic',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n\n const totalPrunedFieldsCount = [\n ...stats.prunedFieldsCountPerDictionary.values(),\n ].reduce((total, count) => total + count, 0);\n const totalPrunedDictionariesCount =\n stats.prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n};\n\n// ── Reporting helpers ─────────────────────────────────────────────────────────\n\n/**\n * Explains the one step the visual editor stands down: renaming the content\n * fields would break the `keyPath` the editor resolves its edits with. Purge\n * and the rest of the minification still run.\n */\nconst logFieldRenameDisabledByEditor = (logger: PurgeLogger): void =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize('— the editor needs complete keys', ANSIColors.GREY),\n ]);\n\n/**\n * Reports the dictionaries whose consumption could not be tracked statically,\n * with the source files responsible — those keep their full content.\n */\nconst logUntrackedBindings = (\n logger: PurgeLogger,\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n *\n * Exported so bundler integrations that do not go through Babel can drive the\n * same pipeline. The Next.js integration is the main consumer: its SWC plugin\n * runs as a per-file Wasm transform with no filesystem access, so the analysis\n * and the dictionary rewrite happen here, in Node, and only the resulting\n * field-rename tables (see {@link serializeFieldRenameMap}) are forwarded to\n * the plugin.\n *\n * **This performs file I/O**: it reads every source file listed in\n * `componentFilesList` and overwrites the compiled dictionary JSON files\n * in-place. Call it at most once per build, after the dictionaries have been\n * built.\n */\nexport const runIntlayerPurgePipeline = (\n options: PurgePluginOptions\n): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n nestedDictionaryReferences,\n compatCallers,\n logConfig,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const logger = getAppLogger(logConfig);\n\n const shouldPurge = Boolean(purge);\n const shouldMinify = Boolean(minify);\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` reported to the visual editor would no longer resolve against\n // the unmerged dictionaries it edits. Purging is keyPath-neutral: a purged\n // field is one no source file reads, so it is never rendered either.\n const isFieldRenameEnabled = shouldMinify && !editorEnabled;\n\n if (editorEnabled && minify) {\n logFieldRenameDisabledByEditor(logger);\n }\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 1.5: Keep `nest()` targets resolvable — must run before the rename\n // maps are built so nest targets are excluded from field renaming.\n if (nestedDictionaryReferences) {\n preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);\n }\n\n // Phase 1.6: Warn about the dictionaries the analysis could not follow.\n logUntrackedBindings(logger, pruneContext);\n\n // Phase 2: Build field-rename maps (minify only, editor disabled only).\n if (isFieldRenameEnabled) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext,\n logger\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false`.\n * - When `editorEnabled` is `true`, purge and minify still run but field\n * renaming is skipped, so the `keyPath` the visual editor receives keeps\n * matching the unmerged dictionaries it edits.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass) {\n runIntlayerPurgePipeline(this.opts as PurgePluginOptions);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"babel-plugin-intlayer-purge.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { PluginObject, PluginPass } from '@babel/core';\nimport { transformSync } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { NestedFieldReferences } from '@intlayer/core/dictionaryManipulator';\nimport { formatPath } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport {\n buildNestedRenameMapFromContent,\n getNestedRenameEntryAtPath,\n} from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n preserveNestedDictionaryFields,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin still purges and minifies, but skips field renaming\n * so the `keyPath` reported to the visual editor keeps matching the unmerged\n * dictionaries it edits. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Dictionaries referenced through `nest()`, mapped to the top-level fields\n * those references read (`'all'` when the whole dictionary is nested).\n *\n * Those fields are resolved at runtime by `getNesting` using their original\n * names, so they must survive purging and must never be renamed.\n */\n nestedDictionaryReferences?: Map<string, NestedFieldReferences>;\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Logging configuration used to report what the pipeline purged and\n * minified, so a Babel- or SWC-driven build prints the same summary as the\n * Vite one. Mirrors `log` in `intlayer.config.ts`; omit to fall back to the\n * logger defaults.\n */\n logConfig?: Pick<IntlayerConfig, 'log'>;\n};\n\n/** Application logger used to report the pipeline's progress. */\ntype PurgeLogger = ReturnType<typeof getAppLogger>;\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process:\n * {@link runIntlayerPurgePipeline} registers the context before running so every\n * subsequent transform is a no-op cache hit.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */\nconst filterRecordByUsedFields = (\n record: Record<string, unknown>,\n usedFieldNames: Set<string>\n): Record<string, unknown> => {\n const filteredRecord: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n if (usedFieldNames.has(fieldName)) {\n filteredRecord[fieldName] = fieldValue;\n }\n }\n return filteredRecord;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n prunedTranslation[locale] = isPlainRecord(localeContent)\n ? filterRecordByUsedFields(localeContent, usedFieldNames)\n : localeContent;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n babelrc: false,\n configFile: false,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext,\n logger: PurgeLogger\n): void => {\n let partiallyMinifiedDictionariesCount = 0;\n\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque values to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousOccurrences = [...opaqueFieldMap.values()].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(nestedRenameMap, occurrence.fieldPath)\n ?.children.size ?? 0) > 0\n );\n\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(`'${occurrence.fieldPath.join('.')}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...occurrence.locations.map(\n (location) => `\\n at ${formatPath(location)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n for (const occurrence of dangerousOccurrences) {\n const renameEntry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\n if (renameEntry) {\n renameEntry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\n/**\n * Names of the top-level content fields of a compiled dictionary, whatever\n * shape its `content` uses. Used to report which fields the purge removed.\n */\nconst readContentFieldNames = (\n content: CompiledDictionaryJson['content']\n): string[] => {\n // Shape A – fields live inside each locale object.\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n return isPlainRecord(firstLocaleValue) ? Object.keys(firstLocaleValue) : [];\n }\n\n // Shape B / dynamic – flat content record.\n if (isPlainRecord(content)) return Object.keys(content);\n\n return [];\n};\n\n/**\n * Running totals of the purge, reported once the whole pipeline is done.\n */\ntype PurgeStats = {\n /** Dictionary key → number of fields removed from it. */\n prunedFieldsCountPerDictionary: Map<string, number>;\n /**\n * Dictionary keys whose \"pruned fields\" line has already been printed. A\n * key spans several files (one static, one per locale), and they all remove\n * the same fields.\n */\n loggedPrunedDictionaryKeys: Set<string>;\n /**\n * Dictionary keys whose \"analysis is incomplete\" warning has already been\n * printed, deduplicated the same way.\n */\n loggedIncompleteAnalysisKeys: Set<string>;\n};\n\n/**\n * Reads a compiled dictionary JSON file, applies the purge and/or minify\n * transformations, and writes it back in-place when something changed.\n *\n * `dictionaryKind` selects the prune strategy and the minified output shape:\n * static dictionaries keep `{ key, content }`, dynamic per-locale\n * dictionaries additionally keep `locale`.\n */\nconst processDictionaryFile = (\n filePath: string,\n dictionaryKind: 'static' | 'dynamic',\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger,\n stats: PurgeStats\n): void => {\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(\n readFileSync(filePath, 'utf-8')\n ) as CompiledDictionaryJson;\n } catch {\n return; // Unreadable or invalid JSON – leave file unchanged.\n }\n\n const { key: dictionaryKey } = parsedDict;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(filePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const pruneDictionaryContent =\n dictionaryKind === 'static'\n ? pruneStaticDictionaryContent\n : pruneDynamicDictionaryContent;\n const originalFieldNames = readContentFieldNames(parsedDict.content);\n const { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(filePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n stats.prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!stats.loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n stats.loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n parsedDict = prunedDictionary;\n modified = true;\n } else if (\n !fieldUsage &&\n pruneContext.hasUnparsableSourceFiles &&\n !stats.loggedIncompleteAnalysisKeys.has(dictionaryKey)\n ) {\n // An unparsable source file might be the one referencing this key, so its\n // usage set cannot be trusted to be empty.\n stats.loggedIncompleteAnalysisKeys.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? dictionaryKind === 'static'\n ? { key: parsedDict.key, content: parsedDict.content }\n : {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger\n): void => {\n const stats: PurgeStats = {\n prunedFieldsCountPerDictionary: new Map(),\n loggedPrunedDictionaryKeys: new Set(),\n loggedIncompleteAnalysisKeys: new Set(),\n };\n\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processDictionaryFile(\n join(dictionariesDir, entry),\n 'static',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDictionaryFile(\n join(keyDirPath, localeFile),\n 'dynamic',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n\n const totalPrunedFieldsCount = [\n ...stats.prunedFieldsCountPerDictionary.values(),\n ].reduce((total, count) => total + count, 0);\n const totalPrunedDictionariesCount =\n stats.prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n};\n\n// ── Reporting helpers ─────────────────────────────────────────────────────────\n\n/**\n * Explains the one step the visual editor stands down: renaming the content\n * fields would break the `keyPath` the editor resolves its edits with. Purge\n * and the rest of the minification still run.\n */\nconst logFieldRenameDisabledByEditor = (logger: PurgeLogger): void =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize('— the editor needs complete keys', ANSIColors.GREY),\n ]);\n\n/**\n * Reports the dictionaries whose consumption could not be tracked statically,\n * with the source files responsible — those keep their full content.\n */\nconst logUntrackedBindings = (\n logger: PurgeLogger,\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n *\n * Exported so bundler integrations that do not go through Babel can drive the\n * same pipeline. The Next.js integration is the main consumer: its SWC plugin\n * runs as a per-file Wasm transform with no filesystem access, so the analysis\n * and the dictionary rewrite happen here, in Node, and only the resulting\n * field-rename tables (see {@link serializeFieldRenameMap}) are forwarded to\n * the plugin.\n *\n * **This performs file I/O**: it reads every source file listed in\n * `componentFilesList` and overwrites the compiled dictionary JSON files\n * in-place. Call it at most once per build, after the dictionaries have been\n * built.\n */\nexport const runIntlayerPurgePipeline = (\n options: PurgePluginOptions\n): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n nestedDictionaryReferences,\n compatCallers,\n logConfig,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const logger = getAppLogger(logConfig);\n\n const shouldPurge = Boolean(purge);\n const shouldMinify = Boolean(minify);\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` reported to the visual editor would no longer resolve against\n // the unmerged dictionaries it edits. Purging is keyPath-neutral: a purged\n // field is one no source file reads, so it is never rendered either.\n const isFieldRenameEnabled = shouldMinify && !editorEnabled;\n\n if (editorEnabled && minify) {\n logFieldRenameDisabledByEditor(logger);\n }\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 1.5: Keep `nest()` targets resolvable — must run before the rename\n // maps are built so nest targets are excluded from field renaming.\n if (nestedDictionaryReferences) {\n preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);\n }\n\n // Phase 1.6: Warn about the dictionaries the analysis could not follow.\n logUntrackedBindings(logger, pruneContext);\n\n // Phase 2: Build field-rename maps (minify only, editor disabled only).\n if (isFieldRenameEnabled) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext,\n logger\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false`.\n * - When `editorEnabled` is `true`, purge and minify still run but field\n * renaming is skipped, so the `keyPath` the visual editor receives keeps\n * matching the unmerged dictionaries it edits.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass) {\n runIntlayerPurgePipeline(this.opts as PurgePluginOptions);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ const getExtractPluginOptions = (configuration = getConfiguration(), isDev = pro
19
19
  let enabled = configuration.compiler?.enabled ?? COMPILER_ENABLED;
20
20
  if (enabled === "build-only") {
21
21
  if (compilerMode) enabled = compilerMode === "build";
22
- else enabled = false;
22
+ else enabled = process.env.NODE_ENV === "production";
23
23
  }
24
24
  const filesList = buildComponentFilesList(configuration);
25
25
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"getExtractPluginOptions.mjs","names":[],"sources":["../../src/getExtractPluginOptions.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport { COMPILER_ENABLED } from '@intlayer/config/defaultValues';\nimport { colorize, colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { ExtractPluginOptions } from './babel-plugin-intlayer-extract';\nimport { writeContentHelper } from './extractContent/contentWriter';\n\n/**\n * Mode of the compiler\n * - 'dev': Development mode with HMR support\n * - 'build': Production build mode\n */\nexport type CompilerMode = 'dev' | 'build';\n\n/**\n * Get the options for the Intlayer Babel extraction plugin\n * This function loads the Intlayer configuration and sets up the onExtract callback\n * to write dictionaries to the filesystem.\n */\nexport const getExtractPluginOptions = (\n configuration: IntlayerConfig = getConfiguration(),\n isDev: CompilerMode | string | undefined = process.env.INTLAYER_IS_DEV_COMMAND\n): ExtractPluginOptions => {\n // Accept 'dev'/'serve' (Vite), boolean true, or the string 'true' (env var)\n const isDevBoolean = isDev === 'dev' || isDev === 'serve' || isDev === 'true';\n\n const compilerMode: CompilerMode = isDevBoolean ? 'dev' : 'build';\n\n const logger = getAppLogger(configuration);\n\n if (configuration.compiler?.enabled === 'build-only' && isDevBoolean) {\n logger(\n `${colorize('Compiler:', ANSIColors.GREY_DARK)} i18n function is not inserted in the code in dev mode to optimize build time. (to test i18n in dev mode set compiler.enabled to true)`\n );\n }\n\n let enabled = configuration.compiler?.enabled ?? COMPILER_ENABLED;\n\n if (enabled === 'build-only') {\n if (compilerMode) {\n enabled = compilerMode === 'build';\n } else {\n // Fallback if mode isn't explicitly provided (e.g. pure babel plugin context)\n enabled = process.env.NODE_ENV === 'production';\n }\n }\n\n const filesList = buildComponentFilesList(configuration);\n\n return {\n enabled,\n configuration,\n filesList,\n onExtract: async ({ dictionaryKey, content, filePath }) => {\n try {\n await writeContentHelper(\n content,\n dictionaryKey,\n filePath,\n configuration\n );\n } catch (error) {\n logger(\n [\n `Failed to process extracted content for ${colorizeKey(dictionaryKey)}:`,\n error,\n ],\n { level: 'error' }\n );\n }\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;AAqBA,MAAa,2BACX,gBAAgC,iBAAiB,GACjD,QAA2C,QAAQ,IAAI,4BAC9B;CAEzB,MAAM,eAAe,UAAU,SAAS,UAAU,WAAW,UAAU;CAEvE,MAAM,eAA6B,eAAe,QAAQ;CAE1D,MAAM,SAAS,aAAa,aAAa;CAEzC,IAAI,cAAc,UAAU,YAAY,gBAAgB,cACtD,OACE,GAAG,SAAS,aAAa,WAAW,SAAS,EAAE,uIACjD;CAGF,IAAI,UAAU,cAAc,UAAU,WAAW;CAEjD,IAAI,YAAY,cAAc;EAC5B,IAAI,cACF,UAAU,iBAAiB;OAG3B,UAAU;CAEd;CAEA,MAAM,YAAY,wBAAwB,aAAa;CAEvD,OAAO;EACL;EACA;EACA;EACA,WAAW,OAAO,EAAE,eAAe,SAAS,eAAe;GACzD,IAAI;IACF,MAAM,mBACJ,SACA,eACA,UACA,aACF;GACF,SAAS,OAAO;IACd,OACE,CACE,2CAA2C,YAAY,aAAa,EAAE,IACtE,KACF,GACA,EAAE,OAAO,QAAQ,CACnB;GACF;EACF;CACF;AACF"}
1
+ {"version":3,"file":"getExtractPluginOptions.mjs","names":[],"sources":["../../src/getExtractPluginOptions.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport { COMPILER_ENABLED } from '@intlayer/config/defaultValues';\nimport { colorize, colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { ExtractPluginOptions } from './babel-plugin-intlayer-extract';\nimport { writeContentHelper } from './extractContent/contentWriter';\n\n/**\n * Mode of the compiler\n * - 'dev': Development mode with HMR support\n * - 'build': Production build mode\n */\nexport type CompilerMode = 'dev' | 'build';\n\n/**\n * Get the options for the Intlayer Babel extraction plugin\n * This function loads the Intlayer configuration and sets up the onExtract callback\n * to write dictionaries to the filesystem.\n */\nexport const getExtractPluginOptions = (\n configuration: IntlayerConfig = getConfiguration(),\n isDev: CompilerMode | string | undefined = process.env.INTLAYER_IS_DEV_COMMAND\n): ExtractPluginOptions => {\n // Accept 'dev'/'serve' (Vite), boolean true, or the string 'true' (env var)\n const isDevBoolean = isDev === 'dev' || isDev === 'serve' || isDev === 'true';\n\n const compilerMode: CompilerMode = isDevBoolean ? 'dev' : 'build';\n\n const logger = getAppLogger(configuration);\n\n if (configuration.compiler?.enabled === 'build-only' && isDevBoolean) {\n logger(\n `${colorize('Compiler:', ANSIColors.GREY_DARK)} i18n function is not inserted in the code in dev mode to optimize build time. (to test i18n in dev mode set compiler.enabled to true)`\n );\n }\n\n let enabled = configuration.compiler?.enabled ?? COMPILER_ENABLED;\n\n if (enabled === 'build-only') {\n if (compilerMode) {\n enabled = compilerMode === 'build';\n } else {\n // Fallback if mode isn't explicitly provided (e.g. pure babel plugin context)\n enabled = process.env.NODE_ENV === 'production';\n }\n }\n\n const filesList = buildComponentFilesList(configuration);\n\n return {\n enabled,\n configuration,\n filesList,\n onExtract: async ({ dictionaryKey, content, filePath }) => {\n try {\n await writeContentHelper(\n content,\n dictionaryKey,\n filePath,\n configuration\n );\n } catch (error) {\n logger(\n [\n `Failed to process extracted content for ${colorizeKey(dictionaryKey)}:`,\n error,\n ],\n { level: 'error' }\n );\n }\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;AAqBA,MAAa,2BACX,gBAAgC,iBAAiB,GACjD,QAA2C,QAAQ,IAAI,4BAC9B;CAEzB,MAAM,eAAe,UAAU,SAAS,UAAU,WAAW,UAAU;CAEvE,MAAM,eAA6B,eAAe,QAAQ;CAE1D,MAAM,SAAS,aAAa,aAAa;CAEzC,IAAI,cAAc,UAAU,YAAY,gBAAgB,cACtD,OACE,GAAG,SAAS,aAAa,WAAW,SAAS,EAAE,uIACjD;CAGF,IAAI,UAAU,cAAc,UAAU,WAAW;CAEjD,IAAI,YAAY,cAAc;EAC5B,IAAI,cACF,UAAU,iBAAiB;OAG3B,mCAAmC;CAEvC;CAEA,MAAM,YAAY,wBAAwB,aAAa;CAEvD,OAAO;EACL;EACA;EACA;EACA,WAAW,OAAO,EAAE,eAAe,SAAS,eAAe;GACzD,IAAI;IACF,MAAM,mBACJ,SACA,eACA,UACA,aACF;GACF,SAAS,OAAO;IACd,OACE,CACE,2CAA2C,YAAY,aAAa,EAAE,IACtE,KACF,GACA,EAAE,OAAO,QAAQ,CACnB;GACF;EACF;CACF;AACF"}
@@ -123,7 +123,100 @@ const unwrapAwait = (babelTypes, path) => {
123
123
  if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) return parentPath;
124
124
  return path;
125
125
  };
126
+ /**
127
+ * Returns `true` when `node` wraps `innerNode` without changing its runtime
128
+ * value: TypeScript casts (`as`, `satisfies`, `!`, `<T>x`) and parentheses.
129
+ */
130
+ const isTransparentWrapper = (babelTypes, node, innerNode) => (babelTypes.isTSAsExpression(node) || babelTypes.isTSSatisfiesExpression(node) || babelTypes.isTSNonNullExpression(node) || babelTypes.isTSTypeAssertion(node) || babelTypes.isParenthesizedExpression(node)) && node.expression === innerNode;
131
+ /**
132
+ * Climbs past every enclosing value-preserving wrapper (see
133
+ * {@link isTransparentWrapper}), so `(content.field as any).sub` is walked the
134
+ * same way as `content.field.sub`.
135
+ */
136
+ const skipTransparentWrappers = (babelTypes, path) => {
137
+ let currentPath = path;
138
+ while (currentPath.parentPath && isTransparentWrapper(babelTypes, currentPath.parentPath.node, currentPath.node)) currentPath = currentPath.parentPath;
139
+ return currentPath;
140
+ };
141
+ /** Climbs past wrappers and an `await`, in any nesting order. */
142
+ const skipWrappersAndAwait = (babelTypes, path) => skipTransparentWrappers(babelTypes, unwrapAwait(babelTypes, skipTransparentWrappers(babelTypes, path)));
143
+ /**
144
+ * Returns `true` for `Promise.all(…)`. `allSettled` is excluded on purpose: it
145
+ * wraps each value in `{ status, value }`.
146
+ */
147
+ const isPromiseAllCall = (babelTypes, node) => babelTypes.isCallExpression(node) && babelTypes.isMemberExpression(node.callee) && !node.callee.computed && babelTypes.isIdentifier(node.callee.object, { name: "Promise" }) && babelTypes.isIdentifier(node.callee.property, { name: "all" });
148
+ /**
149
+ * Resolves where the content returned by `path` (a `useIntlayer('key')` call
150
+ * or any expression evaluating to the same content) ends up.
151
+ *
152
+ * Looks through TypeScript casts, parentheses and `await`, and through
153
+ * `const [a, { b }] = await Promise.all([getIntlayerAsync('x'), …])`, where the
154
+ * array-pattern element at the call's index is the binding target.
155
+ *
156
+ * Both the usage analyser and the field renamer resolve roots through this
157
+ * function, so every shape the analyser tracks is one the renamer rewrites.
158
+ */
159
+ const resolveContentRoot = (babelTypes, path) => {
160
+ const rootPath = skipWrappersAndAwait(babelTypes, path);
161
+ const parentNode = rootPath.parent;
162
+ if (babelTypes.isVariableDeclarator(parentNode) && parentNode.init === rootPath.node && (babelTypes.isIdentifier(parentNode.id) || babelTypes.isObjectPattern(parentNode.id))) return {
163
+ kind: "binding",
164
+ rootPath,
165
+ bindingTarget: parentNode.id
166
+ };
167
+ const promiseAllPath = rootPath.parentPath?.parentPath;
168
+ if (!babelTypes.isArrayExpression(parentNode) || !promiseAllPath || !isPromiseAllCall(babelTypes, promiseAllPath.node) || promiseAllPath.node.arguments[0] !== parentNode) return {
169
+ kind: "expression",
170
+ rootPath
171
+ };
172
+ const promiseAllRootPath = skipWrappersAndAwait(babelTypes, promiseAllPath);
173
+ const declaratorNode = promiseAllRootPath.parent;
174
+ if (!babelTypes.isVariableDeclarator(declaratorNode) || declaratorNode.init !== promiseAllRootPath.node || !babelTypes.isArrayPattern(declaratorNode.id)) return {
175
+ kind: "expression",
176
+ rootPath
177
+ };
178
+ const elementIndex = parentNode.elements.indexOf(rootPath.node);
179
+ const patternElements = declaratorNode.id.elements;
180
+ const hasSpreadBefore = parentNode.elements.slice(0, elementIndex).some((element) => babelTypes.isSpreadElement(element));
181
+ const hasRestUpTo = patternElements.slice(0, elementIndex + 1).some((element) => babelTypes.isRestElement(element));
182
+ if (hasSpreadBefore || hasRestUpTo) return {
183
+ kind: "expression",
184
+ rootPath
185
+ };
186
+ const patternElement = patternElements[elementIndex];
187
+ if (!patternElement) return { kind: "discarded" };
188
+ const bindingTarget = babelTypes.isAssignmentPattern(patternElement) ? patternElement.left : patternElement;
189
+ if (babelTypes.isIdentifier(bindingTarget) || babelTypes.isObjectPattern(bindingTarget)) return {
190
+ kind: "binding",
191
+ rootPath,
192
+ bindingTarget
193
+ };
194
+ return {
195
+ kind: "expression",
196
+ rootPath
197
+ };
198
+ };
199
+ /**
200
+ * Returns `true` when the value at `path` is only tested, never read: `!x`,
201
+ * `typeof x`, `if (x)`, `x ? … : …`, `x && …`, `x === y`. Such uses expose no
202
+ * key names, so they neither block pruning nor make a field opaque.
203
+ */
204
+ const isTestOnlyUse = (babelTypes, path) => {
205
+ const parentNode = path.parent;
206
+ const node = path.node;
207
+ if (babelTypes.isUnaryExpression(parentNode)) return parentNode.operator === "!" || parentNode.operator === "typeof";
208
+ if (babelTypes.isIfStatement(parentNode) || babelTypes.isWhileStatement(parentNode) || babelTypes.isDoWhileStatement(parentNode) || babelTypes.isConditionalExpression(parentNode)) return parentNode.test === node;
209
+ if (babelTypes.isForStatement(parentNode)) return parentNode.test === node;
210
+ if (babelTypes.isLogicalExpression(parentNode)) return parentNode.operator === "&&" && parentNode.left === node;
211
+ if (babelTypes.isBinaryExpression(parentNode)) return [
212
+ "===",
213
+ "!==",
214
+ "==",
215
+ "!="
216
+ ].includes(parentNode.operator);
217
+ return false;
218
+ };
126
219
 
127
220
  //#endregion
128
- export { ABSENT_VALUE, findObjectProperty, firstPathSegment, propertyKeyMatches, readJsxAttributeString, readObjectProperty, readObjectPropertyNode, readStaticFirstSegment, readStaticString, splitNamespace, unwrapAwait };
221
+ export { ABSENT_VALUE, findObjectProperty, firstPathSegment, isTestOnlyUse, propertyKeyMatches, readJsxAttributeString, readObjectProperty, readObjectPropertyNode, readStaticFirstSegment, readStaticString, resolveContentRoot, skipTransparentWrappers, splitNamespace, unwrapAwait };
129
222
  //# sourceMappingURL=staticAstReaders.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"staticAstReaders.mjs","names":[],"sources":["../../src/staticAstReaders.ts"],"sourcesContent":["import type { NodePath } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\n\n/**\n * Static-value readers shared by every pass that has to decide, at build time,\n * which dictionary a call site reads.\n *\n * They were previously duplicated — with small behavioural drifts — between\n * `babel-plugin-intlayer-optimize` and `babel-plugin-intlayer-usage-analyzer`.\n * Both now read through this module so a call site the analyser can attribute\n * is exactly the one the optimizer can rewrite.\n */\n\n/**\n * Reads a fully-static string from an AST node. Returns `undefined` for\n * dynamic values (identifiers, expressions, template literals with\n * interpolations, …).\n */\nexport const readStaticString = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n if (!node) return undefined;\n if (babelTypes.isStringLiteral(node)) return node.value;\n if (\n babelTypes.isTemplateLiteral(node) &&\n node.expressions.length === 0 &&\n node.quasis.length === 1\n ) {\n return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n }\n return undefined;\n};\n\n/** Returns the first dot-path segment of a key, e.g. `'a.b.c'` → `'a'`. */\nexport const firstPathSegment = (path: string): string =>\n path.split('.')[0] ?? path;\n\n/**\n * Reads the static first dot-path segment from a message-id node.\n *\n * t('counter.label') → 'counter'\n * t(`counter.${x}`) → 'counter' (static prefix before the first dot)\n * t(`${x}.label`) → undefined (dynamic first segment)\n * t(someVariable) → undefined\n */\nexport const readStaticFirstSegment = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n const staticString = readStaticString(babelTypes, node);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Template literal whose first quasi already contains the dot delimiter, e.g.\n // `counter.${index}` → the leading `counter` segment is statically known.\n if (babelTypes.isTemplateLiteral(node) && node.quasis.length > 0) {\n const firstQuasi =\n node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n if (firstQuasi?.includes('.')) {\n return firstPathSegment(firstQuasi);\n }\n }\n return undefined;\n};\n\n/** Whether an object-property key (identifier or string literal) reads as `name`. */\nexport const propertyKeyMatches = (\n babelTypes: typeof BabelTypes,\n property: BabelTypes.ObjectProperty,\n name: string\n): boolean =>\n (babelTypes.isIdentifier(property.key) && property.key.name === name) ||\n (babelTypes.isStringLiteral(property.key) && property.key.value === name);\n\n/**\n * Returns the `ObjectProperty` named `propertyName`, or `undefined` when the\n * object has no such property. Callers that need to mutate the property in\n * place (rewriting or deleting it) use this rather than a value reader.\n */\nexport const findObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): BabelTypes.ObjectProperty | undefined => {\n for (const property of objectExpression.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n if (propertyKeyMatches(babelTypes, property, propertyName)) return property;\n }\n return undefined;\n};\n\n/**\n * Marker returned by the reader helpers when the requested property or\n * argument is simply absent — distinct from present-but-dynamic, which the\n * callers must treat conservatively.\n */\nexport const ABSENT_VALUE = '__default__' as const;\n\n/** Either a statically-known value, {@link ABSENT_VALUE}, or `undefined` (dynamic). */\nexport type StaticOrAbsent<T> = T | typeof ABSENT_VALUE | undefined;\n\n/**\n * Reads a static string property from an object expression. Returns\n * {@link ABSENT_VALUE} when the property is absent and `undefined` when\n * present but dynamic.\n */\nexport const readObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): StaticOrAbsent<string> => {\n const property = findObjectProperty(\n babelTypes,\n objectExpression,\n propertyName\n );\n if (!property) return ABSENT_VALUE;\n return readStaticString(babelTypes, property.value); // present but dynamic → undefined\n};\n\n/**\n * Returns the value *node* of a named property, so callers can apply their own\n * reader to it (a full static string, or just its leading path segment).\n * Returns {@link ABSENT_VALUE} when the property is absent.\n */\nexport const readObjectPropertyNode = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): StaticOrAbsent<BabelTypes.Node> => {\n const property = findObjectProperty(\n babelTypes,\n objectExpression,\n propertyName\n );\n if (!property) return ABSENT_VALUE;\n return property.value;\n};\n\n/**\n * Reads a named JSX attribute as a static string and returns it wrapped in a\n * `StringLiteral` node so the result can be fed to the same namespace resolver\n * as a call argument. Handles both `id=\"home.title\"` and `id={'home.title'}`.\n * Returns `undefined` when the attribute is absent or dynamic (`id={expr}`).\n */\nexport const readJsxAttributeString = (\n babelTypes: typeof BabelTypes,\n openingElement: BabelTypes.JSXOpeningElement,\n attributeName: string\n): BabelTypes.StringLiteral | undefined => {\n for (const attribute of openingElement.attributes) {\n if (!babelTypes.isJSXAttribute(attribute)) continue;\n if (\n !babelTypes.isJSXIdentifier(attribute.name) ||\n attribute.name.name !== attributeName\n ) {\n continue;\n }\n\n const value = attribute.value;\n // id=\"home.title\"\n if (babelTypes.isStringLiteral(value)) return value;\n // id={'home.title'} / id={`home.title`}\n if (babelTypes.isJSXExpressionContainer(value)) {\n const staticString = readStaticString(babelTypes, value.expression);\n if (staticString !== undefined) {\n return babelTypes.stringLiteral(staticString);\n }\n }\n return undefined; // attribute present but dynamic\n }\n return undefined; // attribute absent\n};\n\n/**\n * Splits a compat namespace at the first `.` to separate the dictionary key\n * from an optional key prefix, mirroring the SWC plugin's `split_namespace`.\n *\n * `'about'` → `{ dictionaryKey: 'about', keyPrefix: '' }`\n * `'about.counter'` → `{ dictionaryKey: 'about', keyPrefix: 'counter' }`\n */\nexport const splitNamespace = (\n namespace: string\n): { dictionaryKey: string; keyPrefix: string } => {\n const dotPosition = namespace.indexOf('.');\n if (dotPosition === -1) return { dictionaryKey: namespace, keyPrefix: '' };\n return {\n dictionaryKey: namespace.slice(0, dotPosition),\n keyPrefix: namespace.slice(dotPosition + 1),\n };\n};\n\n/**\n * Climbs past an enclosing `await` expression so that\n * `const t = await getTranslations('ns')` — or `await getIntlayerAsync('key')`\n * — is resolved to its variable declarator the same way the synchronous form\n * is.\n */\nexport const unwrapAwait = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> => {\n const parentPath = path.parentPath;\n if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) {\n return parentPath;\n }\n return path;\n};\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAa,oBACX,YACA,SACuB;CACvB,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,WAAW,gBAAgB,IAAI,GAAG,OAAO,KAAK;CAClD,IACE,WAAW,kBAAkB,IAAI,KACjC,KAAK,YAAY,WAAW,KAC5B,KAAK,OAAO,WAAW,GAEvB,OAAO,KAAK,OAAO,EAAE,EAAE,MAAM,UAAU,KAAK,OAAO,EAAE,EAAE,MAAM;AAGjE;;AAGA,MAAa,oBAAoB,SAC/B,KAAK,MAAM,GAAG,CAAC,CAAC,MAAM;;;;;;;;;AAUxB,MAAa,0BACX,YACA,SACuB;CACvB,MAAM,eAAe,iBAAiB,YAAY,IAAI;CACtD,IAAI,iBAAiB,QAAW,OAAO,iBAAiB,YAAY;CAIpE,IAAI,WAAW,kBAAkB,IAAI,KAAK,KAAK,OAAO,SAAS,GAAG;EAChE,MAAM,aACJ,KAAK,OAAO,EAAE,EAAE,MAAM,UAAU,KAAK,OAAO,EAAE,EAAE,MAAM;EACxD,IAAI,YAAY,SAAS,GAAG,GAC1B,OAAO,iBAAiB,UAAU;CAEtC;AAEF;;AAGA,MAAa,sBACX,YACA,UACA,SAEC,WAAW,aAAa,SAAS,GAAG,KAAK,SAAS,IAAI,SAAS,QAC/D,WAAW,gBAAgB,SAAS,GAAG,KAAK,SAAS,IAAI,UAAU;;;;;;AAOtE,MAAa,sBACX,YACA,kBACA,iBAC0C;CAC1C,KAAK,MAAM,YAAY,iBAAiB,YAAY;EAClD,IAAI,CAAC,WAAW,iBAAiB,QAAQ,GAAG;EAC5C,IAAI,mBAAmB,YAAY,UAAU,YAAY,GAAG,OAAO;CACrE;AAEF;;;;;;AAOA,MAAa,eAAe;;;;;;AAU5B,MAAa,sBACX,YACA,kBACA,iBAC2B;CAC3B,MAAM,WAAW,mBACf,YACA,kBACA,YACF;CACA,IAAI,CAAC,UAAU,OAAO;CACtB,OAAO,iBAAiB,YAAY,SAAS,KAAK;AACpD;;;;;;AAOA,MAAa,0BACX,YACA,kBACA,iBACoC;CACpC,MAAM,WAAW,mBACf,YACA,kBACA,YACF;CACA,IAAI,CAAC,UAAU,OAAO;CACtB,OAAO,SAAS;AAClB;;;;;;;AAQA,MAAa,0BACX,YACA,gBACA,kBACyC;CACzC,KAAK,MAAM,aAAa,eAAe,YAAY;EACjD,IAAI,CAAC,WAAW,eAAe,SAAS,GAAG;EAC3C,IACE,CAAC,WAAW,gBAAgB,UAAU,IAAI,KAC1C,UAAU,KAAK,SAAS,eAExB;EAGF,MAAM,QAAQ,UAAU;EAExB,IAAI,WAAW,gBAAgB,KAAK,GAAG,OAAO;EAE9C,IAAI,WAAW,yBAAyB,KAAK,GAAG;GAC9C,MAAM,eAAe,iBAAiB,YAAY,MAAM,UAAU;GAClE,IAAI,iBAAiB,QACnB,OAAO,WAAW,cAAc,YAAY;EAEhD;EACA;CACF;AAEF;;;;;;;;AASA,MAAa,kBACX,cACiD;CACjD,MAAM,cAAc,UAAU,QAAQ,GAAG;CACzC,IAAI,gBAAgB,IAAI,OAAO;EAAE,eAAe;EAAW,WAAW;CAAG;CACzE,OAAO;EACL,eAAe,UAAU,MAAM,GAAG,WAAW;EAC7C,WAAW,UAAU,MAAM,cAAc,CAAC;CAC5C;AACF;;;;;;;AAQA,MAAa,eACX,YACA,SAC8B;CAC9B,MAAM,aAAa,KAAK;CACxB,IAAI,cAAc,WAAW,kBAAkB,WAAW,IAAI,GAC5D,OAAO;CAET,OAAO;AACT"}
1
+ {"version":3,"file":"staticAstReaders.mjs","names":[],"sources":["../../src/staticAstReaders.ts"],"sourcesContent":["import type { NodePath } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\n\n/**\n * Static-value readers shared by every pass that has to decide, at build time,\n * which dictionary a call site reads.\n *\n * They were previously duplicated — with small behavioural drifts — between\n * `babel-plugin-intlayer-optimize` and `babel-plugin-intlayer-usage-analyzer`.\n * Both now read through this module so a call site the analyser can attribute\n * is exactly the one the optimizer can rewrite.\n */\n\n/**\n * Reads a fully-static string from an AST node. Returns `undefined` for\n * dynamic values (identifiers, expressions, template literals with\n * interpolations, …).\n */\nexport const readStaticString = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n if (!node) return undefined;\n if (babelTypes.isStringLiteral(node)) return node.value;\n if (\n babelTypes.isTemplateLiteral(node) &&\n node.expressions.length === 0 &&\n node.quasis.length === 1\n ) {\n return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n }\n return undefined;\n};\n\n/** Returns the first dot-path segment of a key, e.g. `'a.b.c'` → `'a'`. */\nexport const firstPathSegment = (path: string): string =>\n path.split('.')[0] ?? path;\n\n/**\n * Reads the static first dot-path segment from a message-id node.\n *\n * t('counter.label') → 'counter'\n * t(`counter.${x}`) → 'counter' (static prefix before the first dot)\n * t(`${x}.label`) → undefined (dynamic first segment)\n * t(someVariable) → undefined\n */\nexport const readStaticFirstSegment = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n const staticString = readStaticString(babelTypes, node);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Template literal whose first quasi already contains the dot delimiter, e.g.\n // `counter.${index}` → the leading `counter` segment is statically known.\n if (babelTypes.isTemplateLiteral(node) && node.quasis.length > 0) {\n const firstQuasi =\n node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n if (firstQuasi?.includes('.')) {\n return firstPathSegment(firstQuasi);\n }\n }\n return undefined;\n};\n\n/** Whether an object-property key (identifier or string literal) reads as `name`. */\nexport const propertyKeyMatches = (\n babelTypes: typeof BabelTypes,\n property: BabelTypes.ObjectProperty,\n name: string\n): boolean =>\n (babelTypes.isIdentifier(property.key) && property.key.name === name) ||\n (babelTypes.isStringLiteral(property.key) && property.key.value === name);\n\n/**\n * Returns the `ObjectProperty` named `propertyName`, or `undefined` when the\n * object has no such property. Callers that need to mutate the property in\n * place (rewriting or deleting it) use this rather than a value reader.\n */\nexport const findObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): BabelTypes.ObjectProperty | undefined => {\n for (const property of objectExpression.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n if (propertyKeyMatches(babelTypes, property, propertyName)) return property;\n }\n return undefined;\n};\n\n/**\n * Marker returned by the reader helpers when the requested property or\n * argument is simply absent — distinct from present-but-dynamic, which the\n * callers must treat conservatively.\n */\nexport const ABSENT_VALUE = '__default__' as const;\n\n/** Either a statically-known value, {@link ABSENT_VALUE}, or `undefined` (dynamic). */\nexport type StaticOrAbsent<T> = T | typeof ABSENT_VALUE | undefined;\n\n/**\n * Reads a static string property from an object expression. Returns\n * {@link ABSENT_VALUE} when the property is absent and `undefined` when\n * present but dynamic.\n */\nexport const readObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): StaticOrAbsent<string> => {\n const property = findObjectProperty(\n babelTypes,\n objectExpression,\n propertyName\n );\n if (!property) return ABSENT_VALUE;\n return readStaticString(babelTypes, property.value); // present but dynamic → undefined\n};\n\n/**\n * Returns the value *node* of a named property, so callers can apply their own\n * reader to it (a full static string, or just its leading path segment).\n * Returns {@link ABSENT_VALUE} when the property is absent.\n */\nexport const readObjectPropertyNode = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): StaticOrAbsent<BabelTypes.Node> => {\n const property = findObjectProperty(\n babelTypes,\n objectExpression,\n propertyName\n );\n if (!property) return ABSENT_VALUE;\n return property.value;\n};\n\n/**\n * Reads a named JSX attribute as a static string and returns it wrapped in a\n * `StringLiteral` node so the result can be fed to the same namespace resolver\n * as a call argument. Handles both `id=\"home.title\"` and `id={'home.title'}`.\n * Returns `undefined` when the attribute is absent or dynamic (`id={expr}`).\n */\nexport const readJsxAttributeString = (\n babelTypes: typeof BabelTypes,\n openingElement: BabelTypes.JSXOpeningElement,\n attributeName: string\n): BabelTypes.StringLiteral | undefined => {\n for (const attribute of openingElement.attributes) {\n if (!babelTypes.isJSXAttribute(attribute)) continue;\n if (\n !babelTypes.isJSXIdentifier(attribute.name) ||\n attribute.name.name !== attributeName\n ) {\n continue;\n }\n\n const value = attribute.value;\n // id=\"home.title\"\n if (babelTypes.isStringLiteral(value)) return value;\n // id={'home.title'} / id={`home.title`}\n if (babelTypes.isJSXExpressionContainer(value)) {\n const staticString = readStaticString(babelTypes, value.expression);\n if (staticString !== undefined) {\n return babelTypes.stringLiteral(staticString);\n }\n }\n return undefined; // attribute present but dynamic\n }\n return undefined; // attribute absent\n};\n\n/**\n * Splits a compat namespace at the first `.` to separate the dictionary key\n * from an optional key prefix, mirroring the SWC plugin's `split_namespace`.\n *\n * `'about'` → `{ dictionaryKey: 'about', keyPrefix: '' }`\n * `'about.counter'` → `{ dictionaryKey: 'about', keyPrefix: 'counter' }`\n */\nexport const splitNamespace = (\n namespace: string\n): { dictionaryKey: string; keyPrefix: string } => {\n const dotPosition = namespace.indexOf('.');\n if (dotPosition === -1) return { dictionaryKey: namespace, keyPrefix: '' };\n return {\n dictionaryKey: namespace.slice(0, dotPosition),\n keyPrefix: namespace.slice(dotPosition + 1),\n };\n};\n\n/**\n * Climbs past an enclosing `await` expression so that\n * `const t = await getTranslations('ns')` — or `await getIntlayerAsync('key')`\n * — is resolved to its variable declarator the same way the synchronous form\n * is.\n */\nexport const unwrapAwait = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> => {\n const parentPath = path.parentPath;\n if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) {\n return parentPath;\n }\n return path;\n};\n\n/**\n * Returns `true` when `node` wraps `innerNode` without changing its runtime\n * value: TypeScript casts (`as`, `satisfies`, `!`, `<T>x`) and parentheses.\n */\nconst isTransparentWrapper = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node,\n innerNode: BabelTypes.Node\n): boolean =>\n (babelTypes.isTSAsExpression(node) ||\n babelTypes.isTSSatisfiesExpression(node) ||\n babelTypes.isTSNonNullExpression(node) ||\n babelTypes.isTSTypeAssertion(node) ||\n babelTypes.isParenthesizedExpression(node)) &&\n node.expression === innerNode;\n\n/**\n * Climbs past every enclosing value-preserving wrapper (see\n * {@link isTransparentWrapper}), so `(content.field as any).sub` is walked the\n * same way as `content.field.sub`.\n */\nexport const skipTransparentWrappers = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> => {\n let currentPath = path;\n while (\n currentPath.parentPath &&\n isTransparentWrapper(\n babelTypes,\n currentPath.parentPath.node,\n currentPath.node\n )\n ) {\n currentPath = currentPath.parentPath;\n }\n return currentPath;\n};\n\n/** Climbs past wrappers and an `await`, in any nesting order. */\nconst skipWrappersAndAwait = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> =>\n skipTransparentWrappers(\n babelTypes,\n unwrapAwait(babelTypes, skipTransparentWrappers(babelTypes, path))\n );\n\n/**\n * Returns `true` for `Promise.all(…)`. `allSettled` is excluded on purpose: it\n * wraps each value in `{ status, value }`.\n */\nconst isPromiseAllCall = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node\n): node is BabelTypes.CallExpression =>\n babelTypes.isCallExpression(node) &&\n babelTypes.isMemberExpression(node.callee) &&\n !node.callee.computed &&\n babelTypes.isIdentifier(node.callee.object, { name: 'Promise' }) &&\n babelTypes.isIdentifier(node.callee.property, { name: 'all' });\n\n/** How the value of a content-producing expression is consumed. */\nexport type ContentRootResolution =\n /** Consumed inline — inspect `rootPath.parent` (member access, argument…). */\n | { kind: 'expression'; rootPath: NodePath<BabelTypes.Node> }\n /** Stored into a local binding (`const x = …` / `const { a } = …`). */\n | {\n kind: 'binding';\n rootPath: NodePath<BabelTypes.Node>;\n bindingTarget: BabelTypes.Identifier | BabelTypes.ObjectPattern;\n }\n /** Skipped by an array-pattern hole (`const [, b] = await Promise.all(…)`). */\n | { kind: 'discarded' };\n\n/**\n * Resolves where the content returned by `path` (a `useIntlayer('key')` call\n * or any expression evaluating to the same content) ends up.\n *\n * Looks through TypeScript casts, parentheses and `await`, and through\n * `const [a, { b }] = await Promise.all([getIntlayerAsync('x'), …])`, where the\n * array-pattern element at the call's index is the binding target.\n *\n * Both the usage analyser and the field renamer resolve roots through this\n * function, so every shape the analyser tracks is one the renamer rewrites.\n */\nexport const resolveContentRoot = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): ContentRootResolution => {\n const rootPath = skipWrappersAndAwait(babelTypes, path);\n const parentNode = rootPath.parent;\n\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n parentNode.init === rootPath.node &&\n (babelTypes.isIdentifier(parentNode.id) ||\n babelTypes.isObjectPattern(parentNode.id))\n ) {\n return { kind: 'binding', rootPath, bindingTarget: parentNode.id };\n }\n\n // const [a, b] = await Promise.all([getIntlayerAsync('a'), …])\n const promiseAllPath = rootPath.parentPath?.parentPath;\n if (\n !babelTypes.isArrayExpression(parentNode) ||\n !promiseAllPath ||\n !isPromiseAllCall(babelTypes, promiseAllPath.node) ||\n promiseAllPath.node.arguments[0] !== parentNode\n ) {\n return { kind: 'expression', rootPath };\n }\n\n const promiseAllRootPath = skipWrappersAndAwait(babelTypes, promiseAllPath);\n const declaratorNode = promiseAllRootPath.parent;\n if (\n !babelTypes.isVariableDeclarator(declaratorNode) ||\n declaratorNode.init !== promiseAllRootPath.node ||\n !babelTypes.isArrayPattern(declaratorNode.id)\n ) {\n return { kind: 'expression', rootPath };\n }\n\n const elementIndex = parentNode.elements.indexOf(\n rootPath.node as BabelTypes.Expression\n );\n const patternElements = declaratorNode.id.elements;\n\n // A preceding spread in the input, or a rest element at or before this\n // index in the pattern, makes the position ambiguous.\n const hasSpreadBefore = parentNode.elements\n .slice(0, elementIndex)\n .some((element) => babelTypes.isSpreadElement(element));\n const hasRestUpTo = patternElements\n .slice(0, elementIndex + 1)\n .some((element) => babelTypes.isRestElement(element));\n if (hasSpreadBefore || hasRestUpTo) return { kind: 'expression', rootPath };\n\n const patternElement = patternElements[elementIndex];\n if (!patternElement) return { kind: 'discarded' };\n\n // A default value ([a = fallback]) wraps the actual binding target.\n const bindingTarget = babelTypes.isAssignmentPattern(patternElement)\n ? patternElement.left\n : patternElement;\n\n if (\n babelTypes.isIdentifier(bindingTarget) ||\n babelTypes.isObjectPattern(bindingTarget)\n ) {\n return { kind: 'binding', rootPath, bindingTarget };\n }\n\n return { kind: 'expression', rootPath };\n};\n\n/**\n * Returns `true` when the value at `path` is only tested, never read: `!x`,\n * `typeof x`, `if (x)`, `x ? … : …`, `x && …`, `x === y`. Such uses expose no\n * key names, so they neither block pruning nor make a field opaque.\n */\nexport const isTestOnlyUse = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): boolean => {\n const parentNode = path.parent;\n const node = path.node;\n\n if (babelTypes.isUnaryExpression(parentNode)) {\n return parentNode.operator === '!' || parentNode.operator === 'typeof';\n }\n if (\n babelTypes.isIfStatement(parentNode) ||\n babelTypes.isWhileStatement(parentNode) ||\n babelTypes.isDoWhileStatement(parentNode) ||\n babelTypes.isConditionalExpression(parentNode)\n ) {\n return parentNode.test === node;\n }\n if (babelTypes.isForStatement(parentNode)) return parentNode.test === node;\n // `x && y` yields x only when it is falsy — a value without keys.\n if (babelTypes.isLogicalExpression(parentNode)) {\n return parentNode.operator === '&&' && parentNode.left === node;\n }\n if (babelTypes.isBinaryExpression(parentNode)) {\n return ['===', '!==', '==', '!='].includes(parentNode.operator);\n }\n return false;\n};\n"],"mappings":"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"}
@@ -77,6 +77,27 @@ const getUsageCheckRegex = (compatCallers) => {
77
77
  */
78
78
  const SOURCE_FILE_REGEX = /\.(tsx?|[mc]?jsx?|vue|svelte|astro)$/;
79
79
  /**
80
+ * Transform options shared by every internal Babel pass.
81
+ *
82
+ * Project Babel configs are never loaded: these passes only analyse or rewrite
83
+ * intlayer calls, so applying user presets here would transform the code a
84
+ * second time — and looking the config files up costs a filesystem walk per
85
+ * file (about two thirds of each pass's time).
86
+ */
87
+ const INTERNAL_TRANSFORM_OPTIONS = {
88
+ parserOpts: BABEL_PARSER_OPTIONS,
89
+ babelrc: false,
90
+ configFile: false
91
+ };
92
+ /**
93
+ * Returns `true` when `code` is a raw SFC (Vue / Svelte / Astro source with
94
+ * script-block delimiters) rather than plain or already-compiled JS.
95
+ */
96
+ const isRawSfcSource = (sourceFilePath, code) => {
97
+ const scriptBlocks = extractScriptBlocks(sourceFilePath, code);
98
+ return scriptBlocks.length > 1 || (scriptBlocks[0]?.contentStartOffset ?? 0) > 0;
99
+ };
100
+ /**
80
101
  * Runs the usage-analysis Babel plugin on a single JS/TS code string.
81
102
  *
82
103
  * This is analysis-only: the transformed code output is discarded.
@@ -86,7 +107,7 @@ const analyzeScriptContent = async (scriptContent, sourceFilePath, pruneContext,
86
107
  await transformAsync(scriptContent, {
87
108
  filename: sourceFilePath,
88
109
  plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],
89
- parserOpts: BABEL_PARSER_OPTIONS,
110
+ ...INTERNAL_TRANSFORM_OPTIONS,
90
111
  ast: false,
91
112
  code: false
92
113
  });
@@ -122,7 +143,7 @@ const renameFieldsInCode = async (code, sourceFilePath, pruneContext) => {
122
143
  return (await transformAsync(code, {
123
144
  filename: sourceFilePath,
124
145
  plugins: [makeFieldRenameBabelPlugin(pruneContext)],
125
- parserOpts: BABEL_PARSER_OPTIONS,
146
+ ...INTERNAL_TRANSFORM_OPTIONS,
126
147
  ast: false
127
148
  }))?.code ?? null;
128
149
  } catch {
@@ -139,8 +160,8 @@ const renameFieldsInCode = async (code, sourceFilePath, pruneContext) => {
139
160
  const renameFieldsInSourceFile = async (sourceFilePath, code, pruneContext) => {
140
161
  if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return null;
141
162
  if (!INTLAYER_USAGE_REGEX.test(code)) return null;
142
- const scriptBlocks = extractScriptBlocks(sourceFilePath, code);
143
- if (scriptBlocks.length > 0 && ((scriptBlocks[0]?.contentStartOffset ?? 0) > 0 || scriptBlocks.length > 1)) {
163
+ if (isRawSfcSource(sourceFilePath, code)) {
164
+ const scriptBlocks = extractScriptBlocks(sourceFilePath, code);
144
165
  const modifications = [];
145
166
  for (const block of scriptBlocks) {
146
167
  if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;
@@ -160,14 +181,22 @@ const renameFieldsInSourceFile = async (sourceFilePath, code, pruneContext) => {
160
181
  * `useIntlayer('key')` / `getIntlayer('key')` calls into `useDictionary(_hash)`
161
182
  * / `getDictionary(_hash)` and injecting the corresponding dictionary imports.
162
183
  *
184
+ * When `fieldRenamePruneContext` is given, dictionary field accesses are
185
+ * renamed in the same Babel pass (see {@link renameFieldsInSourceFile}): the
186
+ * rename plugin is listed first, so its `Program.exit` sees the dictionary
187
+ * keys before the optimize plugin's `Program.exit` erases them — saving a
188
+ * full parse and code generation per file. `code` must be plain or compiled
189
+ * JS, as in a bundler's post-transform hook.
190
+ *
163
191
  * Returns `{ code, map }` on success, or `null` if the transformation produced
164
192
  * no output.
165
193
  */
166
- const optimizeSourceFile = async (code, sourceFilePath, options) => {
194
+ const optimizeSourceFile = async (code, sourceFilePath, options, fieldRenamePruneContext) => {
195
+ const isRenaming = fieldRenamePruneContext !== void 0 && fieldRenamePruneContext.dictionaryKeyToFieldRenameMap.size > 0;
167
196
  const result = await transformAsync(code, {
168
197
  filename: sourceFilePath,
169
- plugins: [[intlayerOptimizeBabelPlugin, options]],
170
- parserOpts: BABEL_PARSER_OPTIONS
198
+ plugins: [...isRenaming ? [makeFieldRenameBabelPlugin(fieldRenamePruneContext)] : [], [intlayerOptimizeBabelPlugin, options]],
199
+ ...INTERNAL_TRANSFORM_OPTIONS
171
200
  });
172
201
  if (!result?.code) return null;
173
202
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"transformers.mjs","names":[],"sources":["../../src/transformers.ts"],"sourcesContent":["import { transformAsync } from '@babel/core';\n// Typed from `@babel/parser` rather than `@babel/core`'s\n// `InputOptions['parserOpts']` (Babel 8's rename of `TransformOptions`): the\n// two describe the same shape, but naming `@babel/core` in an exported type\n// pulls its declaration file into every consumer's type-check, and that file\n// needs `@types/convert-source-map` to resolve.\nimport type { ParserOptions } from '@babel/parser';\nimport { makeFieldRenameBabelPlugin } from './babel-plugin-intlayer-field-rename';\nimport {\n intlayerOptimizeBabelPlugin,\n type OptimizePluginOptions,\n} from './babel-plugin-intlayer-optimize';\nimport {\n type CompatCallerConfig,\n INTLAYER_CALLER_NAMES,\n makeUsageAnalyzerBabelPlugin,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks, injectScriptBlocks } from './extractScriptBlocks';\n\n// ── Shared Babel parser configuration ─────────────────────────────────────────\n\n/**\n * Babel parser options covering the superset of syntaxes used across all\n * supported frameworks (React / Vue / Svelte / Angular / …).\n */\nexport const BABEL_PARSER_OPTIONS: ParserOptions = {\n sourceType: 'module',\n allowImportExportEverywhere: true,\n // Astro frontmatter may `return new Response(...)` at the top level.\n allowReturnOutsideFunction: true,\n plugins: [\n 'typescript',\n 'jsx',\n 'decorators-legacy',\n 'functionBind',\n 'exportDefaultFrom',\n ],\n};\n\n/**\n * Builds a fast pre-check regex that matches files potentially containing\n * native intlayer calls and/or the given compat namespace caller names.\n *\n * The result is used to skip files that definitely contain none of the\n * relevant function names, avoiding unnecessary Babel parsing.\n *\n * The native names come from {@link INTLAYER_CALLER_NAMES} rather than being\n * spelled out here: a name missing from this regex silently opts every file\n * using it out of the optimize, purge and minify passes, leaving its calls to\n * resolve through the dictionary registry — empty in browser bundles.\n *\n * Compat caller names are NOT hardcoded here — they are provided at runtime by\n * the compat adapter packages that configure the usage-analysis pipeline.\n *\n * @param extraCallerNames - Additional caller function names to match\n * (e.g. `['useTranslation', 'getFixedT']` from `@intlayer/react-i18next`).\n * @returns A `RegExp` matching any of the native or provided caller names.\n */\nexport const buildUsageCheckRegex = (extraCallerNames?: string[]): RegExp => {\n const callerNames = [...INTLAYER_CALLER_NAMES, ...(extraCallerNames ?? [])];\n const uniqueNames = [...new Set(callerNames)];\n return new RegExp(`\\\\b(${uniqueNames.join('|')})\\\\b`);\n};\n\n/**\n * Fast pre-check: matches files that could contain native intlayer calls\n * (`useIntlayer` / `getIntlayer` / `getIntlayerAsync`). Used by the\n * optimize/transform pass, which only rewrites native calls.\n */\nexport const INTLAYER_USAGE_REGEX = buildUsageCheckRegex();\n\n/** Usage-check regex covering only the native callers (no compat callers). */\nconst NATIVE_USAGE_CHECK_REGEX = INTLAYER_USAGE_REGEX;\n\n/** Cache of usage-check regexes, keyed by the compat-caller array reference. */\nconst usageCheckRegexCache = new WeakMap<\n readonly CompatCallerConfig[],\n RegExp\n>();\n\n/**\n * Returns the usage-check regex for the given compat callers, memoised by the\n * array reference. Plugins pass the same options array for every analysed\n * file, so the regex is built once per build instead of once per file.\n */\nexport const getUsageCheckRegex = (\n compatCallers?: readonly CompatCallerConfig[]\n): RegExp => {\n if (!compatCallers || compatCallers.length === 0) {\n return NATIVE_USAGE_CHECK_REGEX;\n }\n\n let usageCheckRegex = usageCheckRegexCache.get(compatCallers);\n if (!usageCheckRegex) {\n usageCheckRegex = buildUsageCheckRegex(\n compatCallers.map((caller) => caller.callerName)\n );\n usageCheckRegexCache.set(compatCallers, usageCheckRegex);\n }\n return usageCheckRegex;\n};\n\n/**\n * Matches source files that are valid targets for usage analysis and Babel\n * transformation. Excludes sourcemap files, declaration files, and other\n * non-source extensions.\n */\nexport const SOURCE_FILE_REGEX = /\\.(tsx?|[mc]?jsx?|vue|svelte|astro)$/;\n\n// ── High-level transformer functions ──────────────────────────────────────────\n\n/**\n * Runs the usage-analysis Babel plugin on a single JS/TS code string.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the content.\n */\nconst analyzeScriptContent = async (\n scriptContent: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n await transformAsync(scriptContent, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false, // analysis only – no output needed\n });\n};\n\n/**\n * Runs the usage-analysis Babel plugin on a source file, accumulating\n * field-usage data into `pruneContext`.\n *\n * For Vue / Svelte SFC files, script blocks are extracted before analysis so\n * Babel does not attempt to parse the full SFC syntax (templates, styles, …).\n * For plain JS/TS files, the whole file content is analysed directly.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the file (caller should handle and flag\n * `pruneContext.hasUnparsableSourceFiles`).\n */\nexport const analyzeFieldUsageInFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n // For SFC files (Vue / Svelte): scriptBlocks[0].contentStartOffset > 0\n // means we extracted actual <script> tags from the file.\n // For plain JS/TS: extractScriptBlocks returns the whole file as a single\n // block with offset 0, so we fall through to the same path.\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n await analyzeScriptContent(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n/**\n * Applies field-renaming to a single JS/TS code string (not an SFC).\n *\n * Returns the renamed code string, or `null` if nothing changed or if\n * Babel failed to parse the input (caller should fall back to original code).\n */\nexport const renameFieldsInCode = async (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n try {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [makeFieldRenameBabelPlugin(pruneContext)],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n });\n return result?.code ?? null;\n } catch {\n return null; // parse failure – caller falls back to original code\n }\n};\n\n/**\n * Applies field-renaming to a source file, correctly handling both plain\n * JS/TS files and SFC files (Vue / Svelte) by operating on each script block\n * individually and injecting the results back into the original source.\n *\n * Returns the renamed code string, or `null` if nothing changed.\n */\nexport const renameFieldsInSourceFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return null;\n if (!INTLAYER_USAGE_REGEX.test(code)) return null;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n const isSFC =\n scriptBlocks.length > 0 &&\n ((scriptBlocks[0]?.contentStartOffset ?? 0) > 0 || scriptBlocks.length > 1);\n\n if (isSFC) {\n // Raw SFC: rename each script block individually and inject back.\n const modifications: Array<{\n block: (typeof scriptBlocks)[number];\n modifiedContent: string;\n }> = [];\n\n for (const block of scriptBlocks) {\n if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;\n\n const renamedCode = await renameFieldsInCode(\n block.content,\n sourceFilePath,\n pruneContext\n );\n if (renamedCode && renamedCode !== block.content) {\n modifications.push({ block, modifiedContent: renamedCode });\n }\n }\n\n if (modifications.length === 0) return null;\n return injectScriptBlocks(code, modifications);\n }\n\n // Plain JS/TS or compiled SFC (no block delimiters) – rename the whole file.\n return renameFieldsInCode(code, sourceFilePath, pruneContext);\n};\n\n/**\n * Runs the intlayer optimize Babel plugin on a source file, transforming\n * `useIntlayer('key')` / `getIntlayer('key')` calls into `useDictionary(_hash)`\n * / `getDictionary(_hash)` and injecting the corresponding dictionary imports.\n *\n * Returns `{ code, map }` on success, or `null` if the transformation produced\n * no output.\n */\nexport const optimizeSourceFile = async (\n code: string,\n sourceFilePath: string,\n options: OptimizePluginOptions\n): Promise<{\n code: string;\n map: string | object | null | undefined;\n} | null> => {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [[intlayerOptimizeBabelPlugin, options]],\n parserOpts: BABEL_PARSER_OPTIONS,\n });\n\n if (!result?.code) return null;\n\n return { code: result.code, map: result.map };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"transformers.mjs","names":[],"sources":["../../src/transformers.ts"],"sourcesContent":["import { transformAsync } from '@babel/core';\n// Typed from `@babel/parser` rather than `@babel/core`'s\n// `InputOptions['parserOpts']` (Babel 8's rename of `TransformOptions`): the\n// two describe the same shape, but naming `@babel/core` in an exported type\n// pulls its declaration file into every consumer's type-check, and that file\n// needs `@types/convert-source-map` to resolve.\nimport type { ParserOptions } from '@babel/parser';\nimport { makeFieldRenameBabelPlugin } from './babel-plugin-intlayer-field-rename';\nimport {\n intlayerOptimizeBabelPlugin,\n type OptimizePluginOptions,\n} from './babel-plugin-intlayer-optimize';\nimport {\n type CompatCallerConfig,\n INTLAYER_CALLER_NAMES,\n makeUsageAnalyzerBabelPlugin,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks, injectScriptBlocks } from './extractScriptBlocks';\n\n// ── Shared Babel parser configuration ─────────────────────────────────────────\n\n/**\n * Babel parser options covering the superset of syntaxes used across all\n * supported frameworks (React / Vue / Svelte / Angular / …).\n */\nexport const BABEL_PARSER_OPTIONS: ParserOptions = {\n sourceType: 'module',\n allowImportExportEverywhere: true,\n // Astro frontmatter may `return new Response(...)` at the top level.\n allowReturnOutsideFunction: true,\n plugins: [\n 'typescript',\n 'jsx',\n 'decorators-legacy',\n 'functionBind',\n 'exportDefaultFrom',\n ],\n};\n\n/**\n * Builds a fast pre-check regex that matches files potentially containing\n * native intlayer calls and/or the given compat namespace caller names.\n *\n * The result is used to skip files that definitely contain none of the\n * relevant function names, avoiding unnecessary Babel parsing.\n *\n * The native names come from {@link INTLAYER_CALLER_NAMES} rather than being\n * spelled out here: a name missing from this regex silently opts every file\n * using it out of the optimize, purge and minify passes, leaving its calls to\n * resolve through the dictionary registry — empty in browser bundles.\n *\n * Compat caller names are NOT hardcoded here — they are provided at runtime by\n * the compat adapter packages that configure the usage-analysis pipeline.\n *\n * @param extraCallerNames - Additional caller function names to match\n * (e.g. `['useTranslation', 'getFixedT']` from `@intlayer/react-i18next`).\n * @returns A `RegExp` matching any of the native or provided caller names.\n */\nexport const buildUsageCheckRegex = (extraCallerNames?: string[]): RegExp => {\n const callerNames = [...INTLAYER_CALLER_NAMES, ...(extraCallerNames ?? [])];\n const uniqueNames = [...new Set(callerNames)];\n return new RegExp(`\\\\b(${uniqueNames.join('|')})\\\\b`);\n};\n\n/**\n * Fast pre-check: matches files that could contain native intlayer calls\n * (`useIntlayer` / `getIntlayer` / `getIntlayerAsync`). Used by the\n * optimize/transform pass, which only rewrites native calls.\n */\nexport const INTLAYER_USAGE_REGEX = buildUsageCheckRegex();\n\n/** Usage-check regex covering only the native callers (no compat callers). */\nconst NATIVE_USAGE_CHECK_REGEX = INTLAYER_USAGE_REGEX;\n\n/** Cache of usage-check regexes, keyed by the compat-caller array reference. */\nconst usageCheckRegexCache = new WeakMap<\n readonly CompatCallerConfig[],\n RegExp\n>();\n\n/**\n * Returns the usage-check regex for the given compat callers, memoised by the\n * array reference. Plugins pass the same options array for every analysed\n * file, so the regex is built once per build instead of once per file.\n */\nexport const getUsageCheckRegex = (\n compatCallers?: readonly CompatCallerConfig[]\n): RegExp => {\n if (!compatCallers || compatCallers.length === 0) {\n return NATIVE_USAGE_CHECK_REGEX;\n }\n\n let usageCheckRegex = usageCheckRegexCache.get(compatCallers);\n if (!usageCheckRegex) {\n usageCheckRegex = buildUsageCheckRegex(\n compatCallers.map((caller) => caller.callerName)\n );\n usageCheckRegexCache.set(compatCallers, usageCheckRegex);\n }\n return usageCheckRegex;\n};\n\n/**\n * Matches source files that are valid targets for usage analysis and Babel\n * transformation. Excludes sourcemap files, declaration files, and other\n * non-source extensions.\n */\nexport const SOURCE_FILE_REGEX = /\\.(tsx?|[mc]?jsx?|vue|svelte|astro)$/;\n\n/**\n * Transform options shared by every internal Babel pass.\n *\n * Project Babel configs are never loaded: these passes only analyse or rewrite\n * intlayer calls, so applying user presets here would transform the code a\n * second time — and looking the config files up costs a filesystem walk per\n * file (about two thirds of each pass's time).\n */\nconst INTERNAL_TRANSFORM_OPTIONS = {\n parserOpts: BABEL_PARSER_OPTIONS,\n babelrc: false,\n configFile: false,\n} as const;\n\n/**\n * Returns `true` when `code` is a raw SFC (Vue / Svelte / Astro source with\n * script-block delimiters) rather than plain or already-compiled JS.\n */\nconst isRawSfcSource = (sourceFilePath: string, code: string): boolean => {\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n return (\n scriptBlocks.length > 1 || (scriptBlocks[0]?.contentStartOffset ?? 0) > 0\n );\n};\n\n// ── High-level transformer functions ──────────────────────────────────────────\n\n/**\n * Runs the usage-analysis Babel plugin on a single JS/TS code string.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the content.\n */\nconst analyzeScriptContent = async (\n scriptContent: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n await transformAsync(scriptContent, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n ...INTERNAL_TRANSFORM_OPTIONS,\n ast: false,\n code: false, // analysis only – no output needed\n });\n};\n\n/**\n * Runs the usage-analysis Babel plugin on a source file, accumulating\n * field-usage data into `pruneContext`.\n *\n * For Vue / Svelte SFC files, script blocks are extracted before analysis so\n * Babel does not attempt to parse the full SFC syntax (templates, styles, …).\n * For plain JS/TS files, the whole file content is analysed directly.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the file (caller should handle and flag\n * `pruneContext.hasUnparsableSourceFiles`).\n */\nexport const analyzeFieldUsageInFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n // For SFC files (Vue / Svelte): scriptBlocks[0].contentStartOffset > 0\n // means we extracted actual <script> tags from the file.\n // For plain JS/TS: extractScriptBlocks returns the whole file as a single\n // block with offset 0, so we fall through to the same path.\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n await analyzeScriptContent(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n/**\n * Applies field-renaming to a single JS/TS code string (not an SFC).\n *\n * Returns the renamed code string, or `null` if nothing changed or if\n * Babel failed to parse the input (caller should fall back to original code).\n */\nexport const renameFieldsInCode = async (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n try {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [makeFieldRenameBabelPlugin(pruneContext)],\n ...INTERNAL_TRANSFORM_OPTIONS,\n ast: false,\n });\n return result?.code ?? null;\n } catch {\n return null; // parse failure – caller falls back to original code\n }\n};\n\n/**\n * Applies field-renaming to a source file, correctly handling both plain\n * JS/TS files and SFC files (Vue / Svelte) by operating on each script block\n * individually and injecting the results back into the original source.\n *\n * Returns the renamed code string, or `null` if nothing changed.\n */\nexport const renameFieldsInSourceFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return null;\n if (!INTLAYER_USAGE_REGEX.test(code)) return null;\n\n if (isRawSfcSource(sourceFilePath, code)) {\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n // Raw SFC: rename each script block individually and inject back.\n const modifications: Array<{\n block: (typeof scriptBlocks)[number];\n modifiedContent: string;\n }> = [];\n\n for (const block of scriptBlocks) {\n if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;\n\n const renamedCode = await renameFieldsInCode(\n block.content,\n sourceFilePath,\n pruneContext\n );\n if (renamedCode && renamedCode !== block.content) {\n modifications.push({ block, modifiedContent: renamedCode });\n }\n }\n\n if (modifications.length === 0) return null;\n return injectScriptBlocks(code, modifications);\n }\n\n // Plain JS/TS or compiled SFC (no block delimiters) – rename the whole file.\n return renameFieldsInCode(code, sourceFilePath, pruneContext);\n};\n\n/**\n * Runs the intlayer optimize Babel plugin on a source file, transforming\n * `useIntlayer('key')` / `getIntlayer('key')` calls into `useDictionary(_hash)`\n * / `getDictionary(_hash)` and injecting the corresponding dictionary imports.\n *\n * When `fieldRenamePruneContext` is given, dictionary field accesses are\n * renamed in the same Babel pass (see {@link renameFieldsInSourceFile}): the\n * rename plugin is listed first, so its `Program.exit` sees the dictionary\n * keys before the optimize plugin's `Program.exit` erases them — saving a\n * full parse and code generation per file. `code` must be plain or compiled\n * JS, as in a bundler's post-transform hook.\n *\n * Returns `{ code, map }` on success, or `null` if the transformation produced\n * no output.\n */\nexport const optimizeSourceFile = async (\n code: string,\n sourceFilePath: string,\n options: OptimizePluginOptions,\n fieldRenamePruneContext?: PruneContext\n): Promise<{\n code: string;\n map: string | object | null | undefined;\n} | null> => {\n const isRenaming =\n fieldRenamePruneContext !== undefined &&\n fieldRenamePruneContext.dictionaryKeyToFieldRenameMap.size > 0;\n\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [\n ...(isRenaming\n ? [makeFieldRenameBabelPlugin(fieldRenamePruneContext)]\n : []),\n [intlayerOptimizeBabelPlugin, options],\n ],\n ...INTERNAL_TRANSFORM_OPTIONS,\n });\n\n if (!result?.code) return null;\n\n return { code: result.code, map: result.map };\n};\n"],"mappings":"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"}
@@ -11,7 +11,12 @@ import * as BabelTypes from "@babel/types";
11
11
  * useIntlayer('key').fieldA → records {fieldA}
12
12
  * useIntlayer('key')['fieldA'] → records {fieldA}
13
13
  * const { ...rest } = useIntlayer('key') → records 'all' (spread)
14
- * const result = useIntlayer('key') → records 'all' (untracked binding)
14
+ * const result = useIntlayer('key') → records the fields read on `result`
15
+ * const [a, { b }] = await Promise.all([getIntlayerAsync('x'), …])
16
+ * → each element tracked like a declarator
17
+ *
18
+ * TypeScript casts (`as`, `satisfies`, `!`) are looked through, and test-only
19
+ * uses (`!field`, `field && …`) expose no key names.
15
20
  */
16
21
  export declare const analyzeCallExpressionUsage: (babelTypes: typeof BabelTypes, pruneContext: PruneContext, callExpressionPath: NodePath<BabelTypes.CallExpression>, dictionaryKey: string, currentSourceFilePath: string, isSfcFile: boolean) => void;
17
22
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"analyzeContentUsage.d.ts","names":[],"sources":["../../src/analyzeContentUsage.ts"],"mappings":";;;;;;;;;;;;;;;qBAqBa,6BAA0B,mBAClB,YAAU,cACf,cAAY,oBACN,SAAS,WAAW,iBAAe,uBAClC,+BACQ"}
1
+ {"version":3,"file":"analyzeContentUsage.d.ts","names":[],"sources":["../../src/analyzeContentUsage.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;qBA8Ba,6BAA0B,mBAClB,YAAU,cACf,cAAY,oBACN,SAAS,WAAW,iBAAe,uBAClC,+BACQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-field-rename.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-field-rename.ts"],"mappings":";;;;;;;;;qBA2Ba,yBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA0CtB,kCAA+B,0BAEzC;;;;;;;;;;;;qBA2EU,6BAA0B,WAC1B,iBAAe,iCAEzB;;;;;;;YAqBS;;EAEV;;EAEA,UAAU;;;YAIA,2BAA2B,eAErC;;;;;;;;;;;;;;qBAgCW,0BAAuB,cACpB,iBACb,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;qBA4aL,8BAA2B,mBACnB,YAAU,aAChB,SAAS,WAAW,UAAQ,cAC3B;;;;;;;;;;qBAsIH,6BAA0B,cACtB,oBAAY,OAAA;EACD,cAAc;MAAe"}
1
+ {"version":3,"file":"babel-plugin-intlayer-field-rename.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-field-rename.ts"],"mappings":";;;;;;;;;qBA8Ba,yBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA0CtB,kCAA+B,0BAEzC;;;;;;;;;;;;qBA2EU,6BAA0B,WAC1B,iBAAe,iCAEzB;;;;;;;YAqBS;;EAEV;;EAEA,UAAU;;;YAIA,2BAA2B,eAErC;;;;;;;;;;;;;;qBAgCW,0BAAuB,cACpB,iBACb,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;qBA8dL,8BAA2B,mBACnB,YAAU,aAChB,SAAS,WAAW,UAAQ,cAC3B;;;;;;;;;;qBAyIH,6BAA0B,cACtB,oBAAY,OAAA;EACD,cAAc;MAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-purge.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"mappings":";;;;;;;;;;;;;;YA2CY;;;;;;EAMV;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;;;EASA,6BAA6B,YAAY;;;;;;EAOzC,8BAA8B;;;;;;;;;;;;EAgB9B,gBAAgB;;;;;;;EAQhB,YAAY,KAAK;;;;;;;;;;qBAwBN,wBAAqB,oBAAsB;;;;;;;;;;;;;;;;;;qBA0rB3C,2BAAwB,SAC1B,uBACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmIU,2BAAwB;EACnC,cAAc;MACZ"}
1
+ {"version":3,"file":"babel-plugin-intlayer-purge.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"mappings":";;;;;;;;;;;;;;YA2CY;;;;;;EAMV;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;;;EASA,6BAA6B,YAAY;;;;;;EAOzC,8BAA8B;;;;;;;;;;;;EAgB9B,gBAAgB;;;;;;;EAQhB,YAAY,KAAK;;;;;;;;;;qBAwBN,wBAAqB,oBAAsB;;;;;;;;;;;;;;;;;;qBA4rB3C,2BAAwB,SAC1B,uBACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmIU,2BAAwB;EACnC,cAAc;MACZ"}