@intlayer/babel 9.0.0-canary.14 → 9.0.0-canary.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-purge.cjs","names":["makeUsageAnalyzerBabelPlugin","BABEL_PARSER_OPTIONS","getUsageCheckRegex","extractScriptBlocks","buildNestedRenameMapFromContent","createPruneContext","SOURCE_FILE_REGEX"],"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 { buildNestedRenameMapFromContent } from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\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 skips all processing to preserve full dictionary\n * content for the visual editor. 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 * 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// ── 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 */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Tracks base directories whose full analysis + dictionary-write cycle has\n * already completed, to avoid repeating work across file transforms.\n */\nconst _completedBaseDirs = new Set<string>();\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/**\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 if (!isPlainRecord(localeContent)) {\n prunedTranslation[locale] = localeContent;\n continue;\n }\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslation[locale] = prunedLocaleFields;\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 const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\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 const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\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): void => {\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 fields to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n for (const [fieldName] of dangerousEntries) {\n const entry = nestedRenameMap.get(fieldName);\n if (entry) {\n entry.children = new Map();\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\nconst processStaticDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\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 { prunedDictionary, wasRecognised } = pruneStaticDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\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 ? { key: parsedDict.key, content: parsedDict.content }\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 processDynamicDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\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 { prunedDictionary, wasRecognised } = pruneDynamicDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\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 ? {\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): void => {\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processStaticDictionaryFile(\n join(dictionariesDir, entry),\n pruneContext,\n shouldPurge,\n shouldMinify\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 processDynamicDictionaryFile(\n join(keyDirPath, localeFile),\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\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 */\nconst runPurgePipeline = (options: PurgePluginOptions): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n compatCallers,\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 shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\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 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext\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 );\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` or `editorEnabled` is\n * `true`.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass & { opts: PurgePluginOptions }) {\n const { baseDir } = this.opts;\n\n if (_completedBaseDirs.has(baseDir)) return;\n _completedBaseDirs.add(baseDir);\n\n runPurgePipeline(this.opts);\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.cjs","names":["makeUsageAnalyzerBabelPlugin","BABEL_PARSER_OPTIONS","getUsageCheckRegex","extractScriptBlocks","buildNestedRenameMapFromContent","createPruneContext","SOURCE_FILE_REGEX"],"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 { buildNestedRenameMapFromContent } from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\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 skips all processing to preserve full dictionary\n * content for the visual editor. 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 * 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// ── 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 runPurgePipeline} 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): void => {\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 fields to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n for (const fieldName of opaqueFieldMap.keys()) {\n const renameEntry = nestedRenameMap.get(fieldName);\n if (renameEntry && renameEntry.children.size > 0) {\n renameEntry.children = new Map();\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\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): 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 if (!dictionaryKey) return;\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 { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\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): void => {\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 );\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 );\n }\n } catch {\n // Unreadable key directory – skip.\n }\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 */\nconst runPurgePipeline = (options: PurgePluginOptions): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n compatCallers,\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 shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\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 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext\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 );\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` or `editorEnabled` is\n * `true`.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass) {\n runPurgePipeline(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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"}
@@ -60,5 +60,6 @@ exports.optimizeSourceFile = require_transformers.optimizeSourceFile;
60
60
  exports.packageList = require_extractContent_utils_constants.packageList;
61
61
  exports.renameFieldsInCode = require_transformers.renameFieldsInCode;
62
62
  exports.renameFieldsInSourceFile = require_transformers.renameFieldsInSourceFile;
63
+ exports.renameIntlayerFieldAccesses = require_babel_plugin_intlayer_field_rename.renameIntlayerFieldAccesses;
63
64
  exports.resolveContentFilePaths = require_extractContent_utils_extractDictionaryInfo.resolveContentFilePaths;
64
65
  exports.writeContentHelper = require_extractContent_contentWriter.writeContentHelper;
@@ -151,9 +151,28 @@ const walkObjectDestructuring = (babelTypes, refPath, renameMap) => {
151
151
  }
152
152
  };
153
153
  /**
154
- * Creates a Babel plugin that rewrites dictionary content field accesses in
155
- * source files to their short aliases defined in
156
- * `pruneContext.dictionaryKeyToFieldRenameMap`.
154
+ * Collects the local aliases under which `useIntlayer` / `getIntlayer` are
155
+ * imported in the given program (e.g. `import { useIntlayer as useI } …`
156
+ * yields `useI` → `useIntlayer`).
157
+ *
158
+ * Import declarations can only appear at the top level of a module, so the
159
+ * program body is scanned directly instead of traversing the whole AST.
160
+ */
161
+ const collectIntlayerCallerLocalNames = (babelTypes, programNode) => {
162
+ const intlayerCallerLocalNameMap = /* @__PURE__ */ new Map();
163
+ for (const statement of programNode.body) {
164
+ if (!babelTypes.isImportDeclaration(statement)) continue;
165
+ for (const importSpecifier of statement.specifiers) {
166
+ if (!babelTypes.isImportSpecifier(importSpecifier)) continue;
167
+ const importedName = babelTypes.isIdentifier(importSpecifier.imported) ? importSpecifier.imported.name : importSpecifier.imported.value;
168
+ if (INTLAYER_CALLER_NAMES.includes(importedName)) intlayerCallerLocalNameMap.set(importSpecifier.local.name, importedName);
169
+ }
170
+ }
171
+ return intlayerCallerLocalNameMap;
172
+ };
173
+ /**
174
+ * Rewrites dictionary content field accesses in the given program to the
175
+ * short aliases defined in `pruneContext.dictionaryKeyToFieldRenameMap`.
157
176
  *
158
177
  * Handled patterns (mirrors the usage analyser):
159
178
  *
@@ -171,6 +190,62 @@ const walkObjectDestructuring = (babelTypes, refPath, renameMap) => {
171
190
  * → const { shortA: fieldA } = useIntlayer('key');
172
191
  * const { shortN: nested } = fieldA;
173
192
  *
193
+ * Exposed as a standalone function so that both
194
+ * {@link makeFieldRenameBabelPlugin} and `intlayerMinifyBabelPlugin` can
195
+ * invoke it from their own `Program.exit` visitors.
196
+ */
197
+ const renameIntlayerFieldAccesses = (babelTypes, programPath, pruneContext) => {
198
+ if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return;
199
+ const intlayerCallerLocalNameMap = collectIntlayerCallerLocalNames(babelTypes, programPath.node);
200
+ if (intlayerCallerLocalNameMap.size === 0) return;
201
+ programPath.traverse({ CallExpression: (callExpressionPath) => {
202
+ const calleeNode = callExpressionPath.node.callee;
203
+ let localCallerName;
204
+ if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
205
+ else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) localCallerName = calleeNode.property.name;
206
+ if (!localCallerName || !intlayerCallerLocalNameMap.has(localCallerName)) return;
207
+ const callArguments = callExpressionPath.node.arguments;
208
+ if (callArguments.length === 0) return;
209
+ const firstArgument = callArguments[0];
210
+ let dictionaryKey;
211
+ if (babelTypes.isStringLiteral(firstArgument)) dictionaryKey = firstArgument.value;
212
+ else if (babelTypes.isTemplateLiteral(firstArgument) && firstArgument.expressions.length === 0 && firstArgument.quasis.length === 1 && firstArgument.quasis[0]) dictionaryKey = firstArgument.quasis[0].value.cooked ?? firstArgument.quasis[0].value.raw;
213
+ if (!dictionaryKey) return;
214
+ const fieldRenameMap = pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);
215
+ if (!fieldRenameMap || fieldRenameMap.size === 0) return;
216
+ const parentNode = callExpressionPath.parent;
217
+ if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id)) {
218
+ walkObjectDestructuring(babelTypes, callExpressionPath, fieldRenameMap);
219
+ return;
220
+ }
221
+ if ((babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === callExpressionPath.node) {
222
+ const { finalPath, finalRenameMap } = walkRenameChain(babelTypes, callExpressionPath, fieldRenameMap);
223
+ walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);
224
+ return;
225
+ }
226
+ if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isIdentifier(parentNode.id)) {
227
+ const variableBinding = callExpressionPath.scope.getBinding(parentNode.id.name);
228
+ if (!variableBinding) return;
229
+ for (const variableReferencePath of variableBinding.referencePaths) {
230
+ const { finalPath, finalRenameMap } = walkRenameChain(babelTypes, variableReferencePath, fieldRenameMap);
231
+ walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);
232
+ const refParent = variableReferencePath.parent;
233
+ if ((babelTypes.isCallExpression(refParent) || babelTypes.isOptionalCallExpression(refParent)) && refParent.callee === variableReferencePath.node) {
234
+ const callPath = variableReferencePath.parentPath;
235
+ if (callPath) {
236
+ const { finalPath: signalFinalPath, finalRenameMap: signalFinalRenameMap } = walkRenameChain(babelTypes, callPath, fieldRenameMap);
237
+ walkObjectDestructuring(babelTypes, signalFinalPath, signalFinalRenameMap);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ } });
243
+ };
244
+ /**
245
+ * Creates a Babel plugin that rewrites dictionary content field accesses in
246
+ * source files to their short aliases via
247
+ * {@link renameIntlayerFieldAccesses}.
248
+ *
174
249
  * This plugin must run in a separate `transformAsync` pass **before**
175
250
  * `intlayerOptimizeBabelPlugin`, because the latter replaces `useIntlayer`
176
251
  * with `useDictionary`, erasing the dictionary-key information needed here.
@@ -178,79 +253,10 @@ const walkObjectDestructuring = (babelTypes, refPath, renameMap) => {
178
253
  const makeFieldRenameBabelPlugin = (pruneContext) => ({ types: babelTypes }) => ({
179
254
  name: "intlayer-field-rename",
180
255
  visitor: { Program: { exit: (programPath) => {
181
- if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return;
182
- const intlayerCallerLocalNameMap = /* @__PURE__ */ new Map();
183
- programPath.traverse({ ImportDeclaration: (importDeclarationPath) => {
184
- for (const importSpecifier of importDeclarationPath.node.specifiers) {
185
- if (!babelTypes.isImportSpecifier(importSpecifier)) continue;
186
- const importedName = babelTypes.isIdentifier(importSpecifier.imported) ? importSpecifier.imported.name : importSpecifier.imported.value;
187
- if (INTLAYER_CALLER_NAMES.includes(importedName)) intlayerCallerLocalNameMap.set(importSpecifier.local.name, importedName);
188
- }
189
- } });
190
- if (intlayerCallerLocalNameMap.size === 0) return;
191
- programPath.traverse({ CallExpression: (callExpressionPath) => {
192
- const calleeNode = callExpressionPath.node.callee;
193
- let localCallerName;
194
- if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
195
- else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) localCallerName = calleeNode.property.name;
196
- if (!localCallerName || !intlayerCallerLocalNameMap.has(localCallerName)) return;
197
- const callArguments = callExpressionPath.node.arguments;
198
- if (callArguments.length === 0) return;
199
- const firstArgument = callArguments[0];
200
- let dictionaryKey;
201
- if (babelTypes.isStringLiteral(firstArgument)) dictionaryKey = firstArgument.value;
202
- else if (babelTypes.isTemplateLiteral(firstArgument) && firstArgument.expressions.length === 0 && firstArgument.quasis.length === 1 && firstArgument.quasis[0]) dictionaryKey = firstArgument.quasis[0].value.cooked ?? firstArgument.quasis[0].value.raw;
203
- if (!dictionaryKey) return;
204
- const fieldRenameMap = pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);
205
- if (!fieldRenameMap || fieldRenameMap.size === 0) return;
206
- const parentNode = callExpressionPath.parent;
207
- if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id)) {
208
- for (const property of parentNode.id.properties) {
209
- if (!babelTypes.isObjectProperty(property)) continue;
210
- const keyName = babelTypes.isIdentifier(property.key) ? property.key.name : babelTypes.isStringLiteral(property.key) ? property.key.value : null;
211
- if (!keyName) continue;
212
- const renameEntry = fieldRenameMap.get(keyName);
213
- if (!renameEntry) continue;
214
- if (property.shorthand) {
215
- property.shorthand = false;
216
- property.key = babelTypes.identifier(renameEntry.shortName);
217
- } else property.key = babelTypes.identifier(renameEntry.shortName);
218
- if (renameEntry.children.size > 0 && babelTypes.isIdentifier(property.value)) {
219
- const localVarBinding = callExpressionPath.scope.getBinding(property.value.name);
220
- if (localVarBinding) for (const refPath of localVarBinding.referencePaths) {
221
- const { finalPath, finalRenameMap } = walkRenameChain(babelTypes, refPath, renameEntry.children);
222
- walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);
223
- }
224
- }
225
- }
226
- return;
227
- }
228
- if ((babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === callExpressionPath.node) {
229
- const { finalPath, finalRenameMap } = walkRenameChain(babelTypes, callExpressionPath, fieldRenameMap);
230
- walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);
231
- return;
232
- }
233
- if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isIdentifier(parentNode.id)) {
234
- const variableName = parentNode.id.name;
235
- const variableBinding = callExpressionPath.scope.getBinding(variableName);
236
- if (!variableBinding) return;
237
- for (const variableReferencePath of variableBinding.referencePaths) {
238
- const { finalPath, finalRenameMap } = walkRenameChain(babelTypes, variableReferencePath, fieldRenameMap);
239
- walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);
240
- const refParent = variableReferencePath.parent;
241
- if ((babelTypes.isCallExpression(refParent) || babelTypes.isOptionalCallExpression(refParent)) && refParent.callee === variableReferencePath.node) {
242
- const callPath = variableReferencePath.parentPath;
243
- if (callPath) {
244
- const { finalPath: signalFinalPath, finalRenameMap: signalFinalRenameMap } = walkRenameChain(babelTypes, callPath, fieldRenameMap);
245
- walkObjectDestructuring(babelTypes, signalFinalPath, signalFinalRenameMap);
246
- }
247
- }
248
- }
249
- }
250
- } });
256
+ renameIntlayerFieldAccesses(babelTypes, programPath, pruneContext);
251
257
  } } }
252
258
  });
253
259
 
254
260
  //#endregion
255
- export { buildNestedRenameMapFromContent, generateShortFieldName, makeFieldRenameBabelPlugin };
261
+ export { buildNestedRenameMapFromContent, generateShortFieldName, makeFieldRenameBabelPlugin, renameIntlayerFieldAccesses };
256
262
  //# sourceMappingURL=babel-plugin-intlayer-field-rename.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-field-rename.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-field-rename.ts"],"sourcesContent":["import type { NodePath, PluginObject } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport {\n INTLAYER_CALLER_NAMES,\n type IntlayerCallerName,\n type NestedRenameMap,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\n\n// ── Field-name helpers ────────────────────────────────────────────────────────\n\n/**\n * Intlayer internal property names that must never be used as short-name\n * targets. These appear inside content-node values (e.g. `{ nodeType:\n * \"translation\", … }`) and are read by the intlayer runtime.\n */\nconst RESERVED_CONTENT_FIELD_NAMES = new Set(['nodeType']);\n\n/** Alphabet used to derive short field aliases (`a`, `b`, …, `aa`, `ab`, …). */\nconst SHORT_NAME_ALPHABET = 'abcdefghijklmnopqrstuvwxyz';\n\n/**\n * Converts a zero-based index to a short alphabetic identifier.\n * 0 → 'a', 1 → 'b', …, 25 → 'z', 26 → 'aa', 27 → 'ab', …\n */\nexport const generateShortFieldName = (index: number): string => {\n const remainder = index % SHORT_NAME_ALPHABET.length;\n const quotient = Math.floor(index / SHORT_NAME_ALPHABET.length);\n\n return quotient === 0 && SHORT_NAME_ALPHABET[remainder]\n ? SHORT_NAME_ALPHABET[remainder]\n : generateShortFieldName(quotient - 1) + SHORT_NAME_ALPHABET[remainder];\n};\n\n/**\n * Recursively builds a `NestedRenameMap` from a compiled dictionary content\n * value by traversing the intlayer node structure.\n *\n * Rules:\n * - If the value has `nodeType: 'translation'`, user-defined fields live\n * inside `translation[locale]`. Recurse into the first locale's value.\n * - All other intlayer runtime nodes (enumeration, condition, gender, …) are\n * treated as leaves — their internal keys must never be renamed.\n * - Arrays produce an empty children map. Array elements are not traversed\n * because consumers may access them via `.map()` / `.filter()` callbacks,\n * which the source-code rename walk cannot enter. Renaming element fields\n * in the JSON without the matching source-code rename would produce\n * mismatched key names and runtime crashes.\n * The `[0]` pass-through in `walkRenameChain` is preserved so that direct\n * indexed access (`field[0].sub`) silently terminates at the empty children\n * map without breaking anything.\n * - Plain objects are user-defined records: ALL non-reserved keys are renamed\n * with short alphabetic aliases (a, b, c, …) and each value is recursed into.\n * - Primitives produce an empty map (no further renaming).\n *\n * The rename map is built from ALL user-defined fields (not just consumed ones).\n * Both the JSON rename and the source-code rename use the same map, so the\n * short names are always consistent regardless of which fields are pruned.\n *\n * @param contentValue - The dictionary content value to analyse.\n */\nexport const buildNestedRenameMapFromContent = (\n contentValue: unknown\n): NestedRenameMap => {\n if (\n !contentValue ||\n typeof contentValue !== 'object' ||\n Array.isArray(contentValue)\n ) {\n return new Map();\n }\n\n const record = contentValue as Record<string, unknown>;\n\n // Any object with `nodeType: string` is an intlayer runtime node.\n if (typeof record.nodeType === 'string') {\n // Translation node: user-defined fields live inside translation[locale].\n if (\n record.nodeType === 'translation' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const firstLocaleValue = Object.values(\n record.translation as Record<string, unknown>\n )[0];\n return buildNestedRenameMapFromContent(firstLocaleValue);\n }\n\n // Enumeration node: user-defined keys live inside enumeration.\n if (\n record.nodeType === 'enumeration' &&\n record.enumeration &&\n typeof record.enumeration === 'object' &&\n !Array.isArray(record.enumeration)\n ) {\n const values = Object.values(\n record.enumeration as Record<string, unknown>\n );\n if (values.length > 0) {\n return buildNestedRenameMapFromContent(values[0]);\n }\n }\n\n // All other intlayer nodes (pluralization, conditions, etc.) resolve to a\n // single value at runtime – return an empty map so they are treated\n // as leaves unless they contain nested user records.\n return new Map();\n }\n\n // Exclude React elements from being treated as user-defined records.\n // JSX elements in the compiled JSON have a $$typeof property (usually Symbol(react.element)).\n if (\n record.$$typeof ||\n (record.type && record.props && Object.hasOwn(record, 'ref'))\n ) {\n return new Map();\n }\n\n // User-defined record: rename ALL non-reserved keys with stable alphabetic\n // aliases sorted alphabetically so the mapping is deterministic.\n const sortedKeys = Object.keys(record)\n .filter((key) => !RESERVED_CONTENT_FIELD_NAMES.has(key))\n .sort();\n\n const renameMap: NestedRenameMap = new Map();\n\n for (let i = 0; i < sortedKeys.length; i++) {\n const key = sortedKeys[i];\n\n if (!key) continue;\n\n const children = buildNestedRenameMapFromContent(record[key]);\n\n renameMap.set(key, { shortName: generateShortFieldName(i), children });\n }\n\n return renameMap;\n};\n\n// ── Field-rename Babel plugin ─────────────────────────────────────────────────\n\n/**\n * Walks a MemberExpression chain starting from `startPath`, renaming each\n * property found in `currentRenameMap` at the corresponding nesting level.\n *\n * Numeric computed accesses (array indices such as `[0]`, `[1]`) are treated\n * as transparent pass-throughs: the rename map is kept unchanged and the walk\n * continues past the index. This means `content.field[0].sub` is handled\n * correctly — `field` and `sub` are both renamed while `[0]` is left intact.\n */\nconst walkRenameChain = (\n babelTypes: typeof BabelTypes,\n startPath: NodePath<BabelTypes.Node>,\n currentRenameMap: NestedRenameMap\n): {\n finalPath: NodePath<BabelTypes.Node>;\n finalRenameMap: NestedRenameMap;\n} => {\n let refPath: NodePath<BabelTypes.Node> = startPath;\n let renameMap = currentRenameMap;\n\n while (true) {\n const parentPath = refPath.parentPath;\n if (!parentPath) break;\n\n const parentNode = parentPath.node;\n\n if (\n (!babelTypes.isMemberExpression(parentNode) &&\n !babelTypes.isOptionalMemberExpression(parentNode)) ||\n (parentNode as BabelTypes.MemberExpression).object !== refPath.node\n ) {\n break;\n }\n\n const memberNode = parentNode as BabelTypes.MemberExpression;\n\n // Numeric index access ([0], [1], …): advance past the array accessor\n // without touching the rename map. The next iteration will attempt to\n // rename the property that follows the index.\n if (\n memberNode.computed &&\n babelTypes.isNumericLiteral(memberNode.property)\n ) {\n refPath = parentPath;\n continue;\n }\n\n // Nothing left to rename at this level — stop.\n if (renameMap.size === 0) break;\n\n let fieldName: string | undefined;\n\n if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {\n fieldName = memberNode.property.name;\n } else if (\n memberNode.computed &&\n babelTypes.isStringLiteral(memberNode.property)\n ) {\n fieldName = memberNode.property.value;\n } else {\n break; // dynamic computed key – stop\n }\n\n const renameEntry = renameMap.get(fieldName);\n if (!renameEntry) break; // not in map – stop\n\n // Apply the rename\n if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {\n memberNode.property.name = renameEntry.shortName;\n } else if (\n memberNode.computed &&\n babelTypes.isStringLiteral(memberNode.property)\n ) {\n memberNode.property.value = renameEntry.shortName;\n }\n\n refPath = parentPath;\n renameMap = renameEntry.children;\n }\n\n return { finalPath: refPath, finalRenameMap: renameMap };\n};\n\n/**\n * Walks an object-destructuring assignment whose right-hand side is `refPath`,\n * renaming each destructured key that is found in `renameMap`.\n *\n * Handles the \"secondary destructuring\" pattern that `walkRenameChain` cannot\n * reach because the reference is not a MemberExpression:\n *\n * const { webhooksSection } = useIntlayer('build-settings');\n * const { modal, validationErrors } = webhooksSection;\n * → const { a: modal, b: validationErrors } = webhooksSection;\n *\n * After renaming each key the function recursively walks references to the\n * newly-bound local variable, calling both `walkRenameChain` (for subsequent\n * member-access chains like `validationErrors.invalidUrl`) and itself (for\n * further levels of secondary destructuring).\n */\nconst walkObjectDestructuring = (\n babelTypes: typeof BabelTypes,\n refPath: NodePath<BabelTypes.Node>,\n renameMap: NestedRenameMap\n): void => {\n if (renameMap.size === 0) return;\n\n const parentNode = refPath.parent;\n\n // Only handle: const { a, b } = refVar\n if (\n !babelTypes.isVariableDeclarator(parentNode) ||\n !babelTypes.isObjectPattern(parentNode.id) ||\n parentNode.init !== refPath.node\n ) {\n return;\n }\n\n for (const property of (parentNode.id as BabelTypes.ObjectPattern)\n .properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n\n const keyName = babelTypes.isIdentifier(property.key)\n ? property.key.name\n : babelTypes.isStringLiteral(property.key)\n ? property.key.value\n : null;\n if (!keyName) continue;\n\n const renameEntry = renameMap.get(keyName);\n if (!renameEntry) continue;\n\n // { fieldA } → { shortA: fieldA }\n // { fieldA: localVar } → { shortA: localVar }\n if (property.shorthand) {\n property.shorthand = false;\n }\n property.key = babelTypes.identifier(renameEntry.shortName);\n\n // Recursively walk references to the local variable bound by this key.\n if (\n renameEntry.children.size > 0 &&\n babelTypes.isIdentifier(property.value)\n ) {\n const localVarName = (property.value as BabelTypes.Identifier).name;\n const localVarBinding = refPath.scope.getBinding(localVarName);\n if (localVarBinding) {\n for (const nestedRefPath of localVarBinding.referencePaths) {\n const { finalPath, finalRenameMap } = walkRenameChain(\n babelTypes,\n nestedRefPath,\n renameEntry.children\n );\n walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);\n }\n }\n }\n }\n};\n\n/**\n * Creates a Babel plugin that rewrites dictionary content field accesses in\n * source files to their short aliases defined in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n *\n * Handled patterns (mirrors the usage analyser):\n *\n * const { fieldA, fieldB } = useIntlayer('key')\n * → const { shortA: fieldA, shortB: fieldB } = useIntlayer('key')\n *\n * useIntlayer('key').fieldA\n * → useIntlayer('key').shortA\n *\n * const result = useIntlayer('key'); result.fieldA\n * → const result = useIntlayer('key'); result.shortA\n *\n * const { fieldA } = useIntlayer('key');\n * const { nested } = fieldA; // secondary destructuring\n * → const { shortA: fieldA } = useIntlayer('key');\n * const { shortN: nested } = fieldA;\n *\n * This plugin must run in a separate `transformAsync` pass **before**\n * `intlayerOptimizeBabelPlugin`, because the latter replaces `useIntlayer`\n * with `useDictionary`, erasing the dictionary-key information needed here.\n */\nexport const makeFieldRenameBabelPlugin =\n (pruneContext: PruneContext) =>\n ({ types: babelTypes }: { types: typeof BabelTypes }): PluginObject => ({\n name: 'intlayer-field-rename',\n visitor: {\n Program: {\n exit: (programPath) => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return;\n\n // Collect local aliases for useIntlayer / getIntlayer\n const intlayerCallerLocalNameMap = new Map<string, string>();\n\n programPath.traverse({\n ImportDeclaration: (importDeclarationPath) => {\n for (const importSpecifier of importDeclarationPath.node\n .specifiers) {\n if (!babelTypes.isImportSpecifier(importSpecifier)) continue;\n\n const importedName = babelTypes.isIdentifier(\n importSpecifier.imported\n )\n ? importSpecifier.imported.name\n : (importSpecifier.imported as BabelTypes.StringLiteral)\n .value;\n\n if (\n INTLAYER_CALLER_NAMES.includes(\n importedName as IntlayerCallerName\n )\n ) {\n intlayerCallerLocalNameMap.set(\n importSpecifier.local.name,\n importedName\n );\n }\n }\n },\n });\n\n if (intlayerCallerLocalNameMap.size === 0) return;\n\n // Visit all useIntlayer / getIntlayer call-sites and rename field accesses\n programPath.traverse({\n CallExpression: (callExpressionPath) => {\n const calleeNode = callExpressionPath.node.callee;\n let localCallerName: string | undefined;\n\n if (babelTypes.isIdentifier(calleeNode)) {\n localCallerName = calleeNode.name;\n } else if (\n babelTypes.isMemberExpression(calleeNode) &&\n babelTypes.isIdentifier(calleeNode.property)\n ) {\n localCallerName = calleeNode.property.name;\n }\n\n if (\n !localCallerName ||\n !intlayerCallerLocalNameMap.has(localCallerName)\n )\n return;\n\n const callArguments = callExpressionPath.node.arguments;\n if (callArguments.length === 0) return;\n\n const firstArgument = callArguments[0];\n let dictionaryKey: string | undefined;\n\n if (babelTypes.isStringLiteral(firstArgument)) {\n dictionaryKey = firstArgument.value;\n } else if (\n babelTypes.isTemplateLiteral(firstArgument) &&\n firstArgument.expressions.length === 0 &&\n firstArgument.quasis.length === 1 &&\n firstArgument.quasis[0]\n ) {\n dictionaryKey =\n firstArgument.quasis[0].value.cooked ??\n firstArgument.quasis[0].value.raw;\n }\n\n if (!dictionaryKey) return;\n\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (!fieldRenameMap || fieldRenameMap.size === 0) return;\n\n const parentNode = callExpressionPath.parent;\n\n // ── Case 1: const { fieldA, fieldB } = useIntlayer('key') ────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n for (const property of parentNode.id.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n\n const keyName = babelTypes.isIdentifier(property.key)\n ? property.key.name\n : babelTypes.isStringLiteral(property.key)\n ? property.key.value\n : null;\n if (!keyName) continue;\n\n const renameEntry = fieldRenameMap.get(keyName);\n if (!renameEntry) continue;\n\n // { fieldA } → { shortA: fieldA }\n // { fieldA: localVar } → { shortA: localVar }\n if (property.shorthand) {\n property.shorthand = false;\n property.key = babelTypes.identifier(renameEntry.shortName);\n } else {\n property.key = babelTypes.identifier(renameEntry.shortName);\n }\n\n // Walk nested member accesses and secondary destructurings\n // on the local variable.\n if (\n renameEntry.children.size > 0 &&\n babelTypes.isIdentifier(property.value)\n ) {\n const localVarBinding = callExpressionPath.scope.getBinding(\n (property.value as BabelTypes.Identifier).name\n );\n if (localVarBinding) {\n for (const refPath of localVarBinding.referencePaths) {\n const { finalPath, finalRenameMap } = walkRenameChain(\n babelTypes,\n refPath,\n renameEntry.children\n );\n walkObjectDestructuring(\n babelTypes,\n finalPath,\n finalRenameMap\n );\n }\n }\n }\n }\n return;\n }\n\n // ── Case 2: useIntlayer('key').fieldA.nested ─────────────────────\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object ===\n callExpressionPath.node\n ) {\n const { finalPath, finalRenameMap } = walkRenameChain(\n babelTypes,\n callExpressionPath,\n fieldRenameMap\n );\n walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);\n return;\n }\n\n // ── Case 3: const result = useIntlayer('key'); result.fieldA ─────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n const variableName = parentNode.id.name;\n const variableBinding =\n callExpressionPath.scope.getBinding(variableName);\n if (!variableBinding) return;\n\n for (const variableReferencePath of variableBinding.referencePaths) {\n // Direct access: result.fieldA or const { fieldA } = result\n const { finalPath, finalRenameMap } = walkRenameChain(\n babelTypes,\n variableReferencePath,\n fieldRenameMap\n );\n walkObjectDestructuring(\n babelTypes,\n finalPath,\n finalRenameMap\n );\n\n // Signal accessor: result().fieldA or const { fieldA } = result()\n // walkRenameChain stops at a CallExpression parent, so we need to\n // start a new walk from the call-expression node itself.\n const refParent = variableReferencePath.parent;\n if (\n (babelTypes.isCallExpression(refParent) ||\n babelTypes.isOptionalCallExpression(refParent)) &&\n (refParent as BabelTypes.CallExpression).callee ===\n variableReferencePath.node\n ) {\n const callPath = variableReferencePath.parentPath;\n if (callPath) {\n const {\n finalPath: signalFinalPath,\n finalRenameMap: signalFinalRenameMap,\n } = walkRenameChain(babelTypes, callPath, fieldRenameMap);\n walkObjectDestructuring(\n babelTypes,\n signalFinalPath,\n signalFinalRenameMap\n );\n }\n }\n }\n }\n },\n });\n },\n },\n },\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"babel-plugin-intlayer-field-rename.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-field-rename.ts"],"sourcesContent":["import type { NodePath, PluginObject } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport {\n INTLAYER_CALLER_NAMES,\n type IntlayerCallerName,\n type NestedRenameMap,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\n\n// ── Field-name helpers ────────────────────────────────────────────────────────\n\n/**\n * Intlayer internal property names that must never be used as short-name\n * targets. These appear inside content-node values (e.g. `{ nodeType:\n * \"translation\", … }`) and are read by the intlayer runtime.\n */\nconst RESERVED_CONTENT_FIELD_NAMES = new Set(['nodeType']);\n\n/** Alphabet used to derive short field aliases (`a`, `b`, …, `aa`, `ab`, …). */\nconst SHORT_NAME_ALPHABET = 'abcdefghijklmnopqrstuvwxyz';\n\n/**\n * Converts a zero-based index to a short alphabetic identifier.\n * 0 → 'a', 1 → 'b', …, 25 → 'z', 26 → 'aa', 27 → 'ab', …\n */\nexport const generateShortFieldName = (index: number): string => {\n const remainder = index % SHORT_NAME_ALPHABET.length;\n const quotient = Math.floor(index / SHORT_NAME_ALPHABET.length);\n\n return quotient === 0 && SHORT_NAME_ALPHABET[remainder]\n ? SHORT_NAME_ALPHABET[remainder]\n : generateShortFieldName(quotient - 1) + SHORT_NAME_ALPHABET[remainder];\n};\n\n/**\n * Recursively builds a `NestedRenameMap` from a compiled dictionary content\n * value by traversing the intlayer node structure.\n *\n * Rules:\n * - If the value has `nodeType: 'translation'`, user-defined fields live\n * inside `translation[locale]`. Recurse into the first locale's value.\n * - All other intlayer runtime nodes (enumeration, condition, gender, …) are\n * treated as leaves — their internal keys must never be renamed.\n * - Arrays produce an empty children map. Array elements are not traversed\n * because consumers may access them via `.map()` / `.filter()` callbacks,\n * which the source-code rename walk cannot enter. Renaming element fields\n * in the JSON without the matching source-code rename would produce\n * mismatched key names and runtime crashes.\n * The `[0]` pass-through in `walkRenameChain` is preserved so that direct\n * indexed access (`field[0].sub`) silently terminates at the empty children\n * map without breaking anything.\n * - Plain objects are user-defined records: ALL non-reserved keys are renamed\n * with short alphabetic aliases (a, b, c, …) and each value is recursed into.\n * - Primitives produce an empty map (no further renaming).\n *\n * The rename map is built from ALL user-defined fields (not just consumed ones).\n * Both the JSON rename and the source-code rename use the same map, so the\n * short names are always consistent regardless of which fields are pruned.\n *\n * @param contentValue - The dictionary content value to analyse.\n */\nexport const buildNestedRenameMapFromContent = (\n contentValue: unknown\n): NestedRenameMap => {\n if (\n !contentValue ||\n typeof contentValue !== 'object' ||\n Array.isArray(contentValue)\n ) {\n return new Map();\n }\n\n const record = contentValue as Record<string, unknown>;\n\n // Any object with `nodeType: string` is an intlayer runtime node.\n if (typeof record.nodeType === 'string') {\n // Translation node: user-defined fields live inside translation[locale].\n if (\n record.nodeType === 'translation' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const firstLocaleValue = Object.values(\n record.translation as Record<string, unknown>\n )[0];\n return buildNestedRenameMapFromContent(firstLocaleValue);\n }\n\n // Enumeration node: user-defined keys live inside enumeration.\n if (\n record.nodeType === 'enumeration' &&\n record.enumeration &&\n typeof record.enumeration === 'object' &&\n !Array.isArray(record.enumeration)\n ) {\n const values = Object.values(\n record.enumeration as Record<string, unknown>\n );\n if (values.length > 0) {\n return buildNestedRenameMapFromContent(values[0]);\n }\n }\n\n // All other intlayer nodes (pluralization, conditions, etc.) resolve to a\n // single value at runtime – return an empty map so they are treated\n // as leaves unless they contain nested user records.\n return new Map();\n }\n\n // Exclude React elements from being treated as user-defined records.\n // JSX elements in the compiled JSON have a $$typeof property (usually Symbol(react.element)).\n if (\n record.$$typeof ||\n (record.type && record.props && Object.hasOwn(record, 'ref'))\n ) {\n return new Map();\n }\n\n // User-defined record: rename ALL non-reserved keys with stable alphabetic\n // aliases sorted alphabetically so the mapping is deterministic.\n const sortedKeys = Object.keys(record)\n .filter((key) => !RESERVED_CONTENT_FIELD_NAMES.has(key))\n .sort();\n\n const renameMap: NestedRenameMap = new Map();\n\n for (let i = 0; i < sortedKeys.length; i++) {\n const key = sortedKeys[i];\n\n if (!key) continue;\n\n const children = buildNestedRenameMapFromContent(record[key]);\n\n renameMap.set(key, { shortName: generateShortFieldName(i), children });\n }\n\n return renameMap;\n};\n\n// ── Field-rename Babel plugin ─────────────────────────────────────────────────\n\n/**\n * Walks a MemberExpression chain starting from `startPath`, renaming each\n * property found in `currentRenameMap` at the corresponding nesting level.\n *\n * Numeric computed accesses (array indices such as `[0]`, `[1]`) are treated\n * as transparent pass-throughs: the rename map is kept unchanged and the walk\n * continues past the index. This means `content.field[0].sub` is handled\n * correctly — `field` and `sub` are both renamed while `[0]` is left intact.\n */\nconst walkRenameChain = (\n babelTypes: typeof BabelTypes,\n startPath: NodePath<BabelTypes.Node>,\n currentRenameMap: NestedRenameMap\n): {\n finalPath: NodePath<BabelTypes.Node>;\n finalRenameMap: NestedRenameMap;\n} => {\n let refPath: NodePath<BabelTypes.Node> = startPath;\n let renameMap = currentRenameMap;\n\n while (true) {\n const parentPath = refPath.parentPath;\n if (!parentPath) break;\n\n const parentNode = parentPath.node;\n\n if (\n (!babelTypes.isMemberExpression(parentNode) &&\n !babelTypes.isOptionalMemberExpression(parentNode)) ||\n (parentNode as BabelTypes.MemberExpression).object !== refPath.node\n ) {\n break;\n }\n\n const memberNode = parentNode as BabelTypes.MemberExpression;\n\n // Numeric index access ([0], [1], …): advance past the array accessor\n // without touching the rename map. The next iteration will attempt to\n // rename the property that follows the index.\n if (\n memberNode.computed &&\n babelTypes.isNumericLiteral(memberNode.property)\n ) {\n refPath = parentPath;\n continue;\n }\n\n // Nothing left to rename at this level — stop.\n if (renameMap.size === 0) break;\n\n let fieldName: string | undefined;\n\n if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {\n fieldName = memberNode.property.name;\n } else if (\n memberNode.computed &&\n babelTypes.isStringLiteral(memberNode.property)\n ) {\n fieldName = memberNode.property.value;\n } else {\n break; // dynamic computed key – stop\n }\n\n const renameEntry = renameMap.get(fieldName);\n if (!renameEntry) break; // not in map – stop\n\n // Apply the rename\n if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {\n memberNode.property.name = renameEntry.shortName;\n } else if (\n memberNode.computed &&\n babelTypes.isStringLiteral(memberNode.property)\n ) {\n memberNode.property.value = renameEntry.shortName;\n }\n\n refPath = parentPath;\n renameMap = renameEntry.children;\n }\n\n return { finalPath: refPath, finalRenameMap: renameMap };\n};\n\n/**\n * Walks an object-destructuring assignment whose right-hand side is `refPath`,\n * renaming each destructured key that is found in `renameMap`.\n *\n * Handles the \"secondary destructuring\" pattern that `walkRenameChain` cannot\n * reach because the reference is not a MemberExpression:\n *\n * const { webhooksSection } = useIntlayer('build-settings');\n * const { modal, validationErrors } = webhooksSection;\n * → const { a: modal, b: validationErrors } = webhooksSection;\n *\n * After renaming each key the function recursively walks references to the\n * newly-bound local variable, calling both `walkRenameChain` (for subsequent\n * member-access chains like `validationErrors.invalidUrl`) and itself (for\n * further levels of secondary destructuring).\n */\nconst walkObjectDestructuring = (\n babelTypes: typeof BabelTypes,\n refPath: NodePath<BabelTypes.Node>,\n renameMap: NestedRenameMap\n): void => {\n if (renameMap.size === 0) return;\n\n const parentNode = refPath.parent;\n\n // Only handle: const { a, b } = refVar\n if (\n !babelTypes.isVariableDeclarator(parentNode) ||\n !babelTypes.isObjectPattern(parentNode.id) ||\n parentNode.init !== refPath.node\n ) {\n return;\n }\n\n for (const property of (parentNode.id as BabelTypes.ObjectPattern)\n .properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n\n const keyName = babelTypes.isIdentifier(property.key)\n ? property.key.name\n : babelTypes.isStringLiteral(property.key)\n ? property.key.value\n : null;\n if (!keyName) continue;\n\n const renameEntry = renameMap.get(keyName);\n if (!renameEntry) continue;\n\n // { fieldA } → { shortA: fieldA }\n // { fieldA: localVar } → { shortA: localVar }\n if (property.shorthand) {\n property.shorthand = false;\n }\n property.key = babelTypes.identifier(renameEntry.shortName);\n\n // Recursively walk references to the local variable bound by this key.\n if (\n renameEntry.children.size > 0 &&\n babelTypes.isIdentifier(property.value)\n ) {\n const localVarName = (property.value as BabelTypes.Identifier).name;\n const localVarBinding = refPath.scope.getBinding(localVarName);\n if (localVarBinding) {\n for (const nestedRefPath of localVarBinding.referencePaths) {\n const { finalPath, finalRenameMap } = walkRenameChain(\n babelTypes,\n nestedRefPath,\n renameEntry.children\n );\n walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);\n }\n }\n }\n }\n};\n\n/**\n * Collects the local aliases under which `useIntlayer` / `getIntlayer` are\n * imported in the given program (e.g. `import { useIntlayer as useI } …`\n * yields `useI` → `useIntlayer`).\n *\n * Import declarations can only appear at the top level of a module, so the\n * program body is scanned directly instead of traversing the whole AST.\n */\nconst collectIntlayerCallerLocalNames = (\n babelTypes: typeof BabelTypes,\n programNode: BabelTypes.Program\n): Map<string, string> => {\n const intlayerCallerLocalNameMap = new Map<string, string>();\n\n for (const statement of programNode.body) {\n if (!babelTypes.isImportDeclaration(statement)) continue;\n\n for (const importSpecifier of statement.specifiers) {\n if (!babelTypes.isImportSpecifier(importSpecifier)) continue;\n\n const importedName = babelTypes.isIdentifier(importSpecifier.imported)\n ? importSpecifier.imported.name\n : importSpecifier.imported.value;\n\n if (INTLAYER_CALLER_NAMES.includes(importedName as IntlayerCallerName)) {\n intlayerCallerLocalNameMap.set(\n importSpecifier.local.name,\n importedName\n );\n }\n }\n }\n\n return intlayerCallerLocalNameMap;\n};\n\n/**\n * Rewrites dictionary content field accesses in the given program to the\n * short aliases defined in `pruneContext.dictionaryKeyToFieldRenameMap`.\n *\n * Handled patterns (mirrors the usage analyser):\n *\n * const { fieldA, fieldB } = useIntlayer('key')\n * → const { shortA: fieldA, shortB: fieldB } = useIntlayer('key')\n *\n * useIntlayer('key').fieldA\n * → useIntlayer('key').shortA\n *\n * const result = useIntlayer('key'); result.fieldA\n * → const result = useIntlayer('key'); result.shortA\n *\n * const { fieldA } = useIntlayer('key');\n * const { nested } = fieldA; // secondary destructuring\n * → const { shortA: fieldA } = useIntlayer('key');\n * const { shortN: nested } = fieldA;\n *\n * Exposed as a standalone function so that both\n * {@link makeFieldRenameBabelPlugin} and `intlayerMinifyBabelPlugin` can\n * invoke it from their own `Program.exit` visitors.\n */\nexport const renameIntlayerFieldAccesses = (\n babelTypes: typeof BabelTypes,\n programPath: NodePath<BabelTypes.Program>,\n pruneContext: PruneContext\n): void => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return;\n\n const intlayerCallerLocalNameMap = collectIntlayerCallerLocalNames(\n babelTypes,\n programPath.node\n );\n if (intlayerCallerLocalNameMap.size === 0) return;\n\n // Visit all useIntlayer / getIntlayer call-sites and rename field accesses\n programPath.traverse({\n CallExpression: (callExpressionPath) => {\n const calleeNode = callExpressionPath.node.callee;\n let localCallerName: string | undefined;\n\n if (babelTypes.isIdentifier(calleeNode)) {\n localCallerName = calleeNode.name;\n } else if (\n babelTypes.isMemberExpression(calleeNode) &&\n babelTypes.isIdentifier(calleeNode.property)\n ) {\n localCallerName = calleeNode.property.name;\n }\n\n if (!localCallerName || !intlayerCallerLocalNameMap.has(localCallerName))\n return;\n\n const callArguments = callExpressionPath.node.arguments;\n if (callArguments.length === 0) return;\n\n const firstArgument = callArguments[0];\n let dictionaryKey: string | undefined;\n\n if (babelTypes.isStringLiteral(firstArgument)) {\n dictionaryKey = firstArgument.value;\n } else if (\n babelTypes.isTemplateLiteral(firstArgument) &&\n firstArgument.expressions.length === 0 &&\n firstArgument.quasis.length === 1 &&\n firstArgument.quasis[0]\n ) {\n dictionaryKey =\n firstArgument.quasis[0].value.cooked ??\n firstArgument.quasis[0].value.raw;\n }\n\n if (!dictionaryKey) return;\n\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (!fieldRenameMap || fieldRenameMap.size === 0) return;\n\n const parentNode = callExpressionPath.parent;\n\n // ── Case 1: const { fieldA, fieldB } = useIntlayer('key') ────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n walkObjectDestructuring(babelTypes, callExpressionPath, fieldRenameMap);\n return;\n }\n\n // ── Case 2: useIntlayer('key').fieldA.nested ─────────────────────\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object ===\n callExpressionPath.node\n ) {\n const { finalPath, finalRenameMap } = walkRenameChain(\n babelTypes,\n callExpressionPath,\n fieldRenameMap\n );\n walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);\n return;\n }\n\n // ── Case 3: const result = useIntlayer('key'); result.fieldA ─────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n const variableBinding = callExpressionPath.scope.getBinding(\n parentNode.id.name\n );\n if (!variableBinding) return;\n\n for (const variableReferencePath of variableBinding.referencePaths) {\n // Direct access: result.fieldA or const { fieldA } = result\n const { finalPath, finalRenameMap } = walkRenameChain(\n babelTypes,\n variableReferencePath,\n fieldRenameMap\n );\n walkObjectDestructuring(babelTypes, finalPath, finalRenameMap);\n\n // Signal accessor: result().fieldA or const { fieldA } = result()\n // walkRenameChain stops at a CallExpression parent, so we need to\n // start a new walk from the call-expression node itself.\n const refParent = variableReferencePath.parent;\n if (\n (babelTypes.isCallExpression(refParent) ||\n babelTypes.isOptionalCallExpression(refParent)) &&\n (refParent as BabelTypes.CallExpression).callee ===\n variableReferencePath.node\n ) {\n const callPath = variableReferencePath.parentPath;\n if (callPath) {\n const {\n finalPath: signalFinalPath,\n finalRenameMap: signalFinalRenameMap,\n } = walkRenameChain(babelTypes, callPath, fieldRenameMap);\n walkObjectDestructuring(\n babelTypes,\n signalFinalPath,\n signalFinalRenameMap\n );\n }\n }\n }\n }\n },\n });\n};\n\n/**\n * Creates a Babel plugin that rewrites dictionary content field accesses in\n * source files to their short aliases via\n * {@link renameIntlayerFieldAccesses}.\n *\n * This plugin must run in a separate `transformAsync` pass **before**\n * `intlayerOptimizeBabelPlugin`, because the latter replaces `useIntlayer`\n * with `useDictionary`, erasing the dictionary-key information needed here.\n */\nexport const makeFieldRenameBabelPlugin =\n (pruneContext: PruneContext) =>\n ({ types: babelTypes }: { types: typeof BabelTypes }): PluginObject => ({\n name: 'intlayer-field-rename',\n visitor: {\n Program: {\n exit: (programPath) => {\n renameIntlayerFieldAccesses(babelTypes, programPath, pruneContext);\n },\n },\n },\n });\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { makeFieldRenameBabelPlugin } from "./babel-plugin-intlayer-field-rename.mjs";
1
+ import { renameIntlayerFieldAccesses } from "./babel-plugin-intlayer-field-rename.mjs";
2
2
  import { INTLAYER_USAGE_REGEX } from "./transformers.mjs";
3
3
  import { getSharedPruneContext } from "./babel-plugin-intlayer-purge.mjs";
4
4
 
@@ -48,34 +48,17 @@ import { getSharedPruneContext } from "./babel-plugin-intlayer-purge.mjs";
48
48
  * `useDictionary`, erasing the dictionary-key information needed to look up
49
49
  * the rename map.
50
50
  */
51
- const intlayerMinifyBabelPlugin = (babel) => {
52
- /**
53
- * The field-rename `Program.exit` handler extracted from
54
- * {@link makeFieldRenameBabelPlugin}. Resolved once per plugin-instance
55
- * (i.e. once per babel.config.js registration, not once per file).
56
- */
57
- let fieldRenameExitVisitor = null;
58
- /** The `baseDir` for which the visitor was last resolved. */
59
- let resolvedBaseDir = null;
60
- return {
61
- name: "intlayer-minify",
62
- pre() {
63
- const { baseDir, minify, optimize, editorEnabled } = this.opts;
64
- if (!minify || optimize === false || editorEnabled) return;
65
- if (resolvedBaseDir === baseDir && fieldRenameExitVisitor !== null) return;
66
- const pruneContext = getSharedPruneContext(baseDir);
67
- if (!pruneContext || pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return;
68
- resolvedBaseDir = baseDir;
69
- const programVisitor = makeFieldRenameBabelPlugin(pruneContext)(babel).visitor.Program;
70
- if (programVisitor && typeof programVisitor === "object" && "exit" in programVisitor && typeof programVisitor.exit === "function") fieldRenameExitVisitor = programVisitor.exit;
71
- },
72
- visitor: { Program: { exit(programPath, state) {
73
- if (!fieldRenameExitVisitor) return;
74
- if (!INTLAYER_USAGE_REGEX.test(state.file.code)) return;
75
- fieldRenameExitVisitor(programPath);
76
- } } }
77
- };
78
- };
51
+ const intlayerMinifyBabelPlugin = (babel) => ({
52
+ name: "intlayer-minify",
53
+ visitor: { Program: { exit(programPath, state) {
54
+ const { baseDir, minify, optimize, editorEnabled } = state.opts;
55
+ if (!minify || optimize === false || editorEnabled) return;
56
+ if (!INTLAYER_USAGE_REGEX.test(state.file.code)) return;
57
+ const pruneContext = getSharedPruneContext(baseDir);
58
+ if (!pruneContext) return;
59
+ renameIntlayerFieldAccesses(babel.types, programPath, pruneContext);
60
+ } } }
61
+ });
79
62
 
80
63
  //#endregion
81
64
  export { intlayerMinifyBabelPlugin };
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-minify.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-minify.ts"],"sourcesContent":["import type { NodePath, PluginObject, PluginPass } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport { makeFieldRenameBabelPlugin } from './babel-plugin-intlayer-field-rename';\nimport { getSharedPruneContext } from './babel-plugin-intlayer-purge';\nimport type { PruneContext } from './babel-plugin-intlayer-usage-analyzer';\nimport { INTLAYER_USAGE_REGEX } from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerMinifyBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getMinifyPluginOptions}) so the plugin does not read the intlayer\n * configuration file on every file transform.\n */\nexport type MinifyPluginOptions = {\n /**\n * Absolute path to the project root. Used to look up the shared\n * {@link PruneContext} built by {@link intlayerPurgeBabelPlugin}.\n */\n baseDir: string;\n\n /**\n * When `true`, rewrite source-file property accesses to use the short\n * alphabetic aliases assigned during the minification pass\n * (e.g. `content.title` → `content.a`). Mirrors `build.minify`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. When explicitly `false`, the plugin is a\n * no-op. Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin is a no-op. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that rewrites component source files to use the short\n * alphabetic aliases assigned to dictionary content fields during the\n * minification pass (e.g. `content.title` → `content.a`).\n *\n * This plugin relies on the field-rename map built by\n * {@link intlayerPurgeBabelPlugin} during its `pre()` hook. Both plugins\n * share the same module-level {@link PruneContext} via\n * {@link getSharedPruneContext}, so **`intlayerPurgeBabelPlugin` must be\n * listed before this plugin** in `babel.config.js` so that its `pre()` runs\n * first and the shared context is populated before this plugin's\n * `Program.exit` visitor fires.\n *\n * All option values must be pre-resolved via {@link getMinifyPluginOptions}.\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 * - This plugin is a no-op when `minify` is `false`, `optimize` is `false`,\n * or `editorEnabled` is `true`.\n * - Source-code renames must run **before** {@link intlayerOptimizeBabelPlugin}\n * because the optimize pass replaces `useIntlayer` calls with\n * `useDictionary`, erasing the dictionary-key information needed to look up\n * the rename map.\n */\nexport const intlayerMinifyBabelPlugin = (babel: {\n types: typeof BabelTypes;\n}): PluginObject => {\n /**\n * The field-rename `Program.exit` handler extracted from\n * {@link makeFieldRenameBabelPlugin}. Resolved once per plugin-instance\n * (i.e. once per babel.config.js registration, not once per file).\n */\n let fieldRenameExitVisitor:\n | ((programPath: NodePath<BabelTypes.Program>) => void)\n | null = null;\n\n /** The `baseDir` for which the visitor was last resolved. */\n let resolvedBaseDir: string | null = null;\n\n return {\n name: 'intlayer-minify',\n\n pre(this: PluginPass & { opts: MinifyPluginOptions }) {\n const { baseDir, minify, optimize, editorEnabled } = this.opts;\n\n if (!minify || optimize === false || editorEnabled) return;\n\n // Re-resolve when the baseDir changes (edge case in monorepos where the\n // same process handles multiple workspaces with different configs).\n if (resolvedBaseDir === baseDir && fieldRenameExitVisitor !== null)\n return;\n\n const pruneContext: PruneContext | null = getSharedPruneContext(baseDir);\n if (\n !pruneContext ||\n pruneContext.dictionaryKeyToFieldRenameMap.size === 0\n )\n return;\n\n resolvedBaseDir = baseDir;\n\n // Instantiate makeFieldRenameBabelPlugin and extract its Program.exit\n // handler so we can invoke it from our own visitor.\n const fieldRenamePlugin = makeFieldRenameBabelPlugin(pruneContext)(babel);\n const programVisitor = fieldRenamePlugin.visitor.Program;\n\n if (\n programVisitor &&\n typeof programVisitor === 'object' &&\n 'exit' in programVisitor &&\n typeof (\n programVisitor as {\n exit: (path: NodePath<BabelTypes.Program>) => void;\n }\n ).exit === 'function'\n ) {\n fieldRenameExitVisitor = (\n programVisitor as {\n exit: (path: NodePath<BabelTypes.Program>) => void;\n }\n ).exit;\n }\n },\n\n visitor: {\n Program: {\n exit(\n programPath: NodePath<BabelTypes.Program>,\n state: PluginPass\n ): void {\n if (!fieldRenameExitVisitor) return;\n if (!INTLAYER_USAGE_REGEX.test(state.file.code)) return;\n\n fieldRenameExitVisitor(programPath);\n },\n },\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyFA,MAAa,6BAA6B,UAEtB;;;;;;CAMlB,IAAI,yBAEO;;CAGX,IAAI,kBAAiC;AAErC,QAAO;EACL,MAAM;EAEN,MAAsD;GACpD,MAAM,EAAE,SAAS,QAAQ,UAAU,kBAAkB,KAAK;AAE1D,OAAI,CAAC,UAAU,aAAa,SAAS,cAAe;AAIpD,OAAI,oBAAoB,WAAW,2BAA2B,KAC5D;GAEF,MAAM,eAAoC,sBAAsB,QAAQ;AACxE,OACE,CAAC,gBACD,aAAa,8BAA8B,SAAS,EAEpD;AAEF,qBAAkB;GAKlB,MAAM,iBADoB,2BAA2B,aAAa,CAAC,MAC3B,CAAC,QAAQ;AAEjD,OACE,kBACA,OAAO,mBAAmB,YAC1B,UAAU,kBACV,OACE,eAGA,SAAS,WAEX,0BACE,eAGA;;EAIN,SAAS,EACP,SAAS,EACP,KACE,aACA,OACM;AACN,OAAI,CAAC,uBAAwB;AAC7B,OAAI,CAAC,qBAAqB,KAAK,MAAM,KAAK,KAAK,CAAE;AAEjD,0BAAuB,YAAY;KAEtC,EACF;EACF"}
1
+ {"version":3,"file":"babel-plugin-intlayer-minify.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-minify.ts"],"sourcesContent":["import type { NodePath, PluginObject, PluginPass } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport { renameIntlayerFieldAccesses } from './babel-plugin-intlayer-field-rename';\nimport { getSharedPruneContext } from './babel-plugin-intlayer-purge';\nimport type { PruneContext } from './babel-plugin-intlayer-usage-analyzer';\nimport { INTLAYER_USAGE_REGEX } from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerMinifyBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getMinifyPluginOptions}) so the plugin does not read the intlayer\n * configuration file on every file transform.\n */\nexport type MinifyPluginOptions = {\n /**\n * Absolute path to the project root. Used to look up the shared\n * {@link PruneContext} built by {@link intlayerPurgeBabelPlugin}.\n */\n baseDir: string;\n\n /**\n * When `true`, rewrite source-file property accesses to use the short\n * alphabetic aliases assigned during the minification pass\n * (e.g. `content.title` → `content.a`). Mirrors `build.minify`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. When explicitly `false`, the plugin is a\n * no-op. Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin is a no-op. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that rewrites component source files to use the short\n * alphabetic aliases assigned to dictionary content fields during the\n * minification pass (e.g. `content.title` → `content.a`).\n *\n * This plugin relies on the field-rename map built by\n * {@link intlayerPurgeBabelPlugin} during its `pre()` hook. Both plugins\n * share the same module-level {@link PruneContext} via\n * {@link getSharedPruneContext}, so **`intlayerPurgeBabelPlugin` must be\n * listed before this plugin** in `babel.config.js` so that its `pre()` runs\n * first and the shared context is populated before this plugin's\n * `Program.exit` visitor fires.\n *\n * All option values must be pre-resolved via {@link getMinifyPluginOptions}.\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 * - This plugin is a no-op when `minify` is `false`, `optimize` is `false`,\n * or `editorEnabled` is `true`.\n * - Source-code renames must run **before** {@link intlayerOptimizeBabelPlugin}\n * because the optimize pass replaces `useIntlayer` calls with\n * `useDictionary`, erasing the dictionary-key information needed to look up\n * the rename map.\n */\nexport const intlayerMinifyBabelPlugin = (babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-minify',\n\n visitor: {\n Program: {\n exit(programPath: NodePath<BabelTypes.Program>, state: PluginPass): void {\n const { baseDir, minify, optimize, editorEnabled } =\n state.opts as MinifyPluginOptions;\n\n if (!minify || optimize === false || editorEnabled) return;\n if (!INTLAYER_USAGE_REGEX.test(state.file.code)) return;\n\n const pruneContext: PruneContext | null =\n getSharedPruneContext(baseDir);\n if (!pruneContext) return;\n\n renameIntlayerFieldAccesses(babel.types, programPath, pruneContext);\n },\n },\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyFA,MAAa,6BAA6B,WAErB;CACnB,MAAM;CAEN,SAAS,EACP,SAAS,EACP,KAAK,aAA2C,OAAyB;EACvE,MAAM,EAAE,SAAS,QAAQ,UAAU,kBACjC,MAAM;AAER,MAAI,CAAC,UAAU,aAAa,SAAS,cAAe;AACpD,MAAI,CAAC,qBAAqB,KAAK,MAAM,KAAK,KAAK,CAAE;EAEjD,MAAM,eACJ,sBAAsB,QAAQ;AAChC,MAAI,CAAC,aAAc;AAEnB,8BAA4B,MAAM,OAAO,aAAa,aAAa;IAEtE,EACF;CACF"}