@intlayer/babel 9.3.2 → 9.4.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/babel-plugin-intlayer-purge.cjs +21 -22
- package/dist/cjs/babel-plugin-intlayer-purge.cjs.map +1 -1
- package/dist/esm/babel-plugin-intlayer-purge.mjs +21 -22
- package/dist/esm/babel-plugin-intlayer-purge.mjs.map +1 -1
- package/dist/types/babel-plugin-intlayer-purge.d.ts +7 -4
- package/dist/types/babel-plugin-intlayer-purge.d.ts.map +1 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -327,4 +327,4 @@ Contribute on [GitHub](https://github.com/aymericzip/intlayer), [GitLab](https:/
|
|
|
327
327
|
|
|
328
328
|
If you like Intlayer, give us a ⭐ on GitHub. It helps others discover the project! [See why GitHub Stars matter](https://github.com/aymericzip/intlayer/blob/main/CONTRIBUTING.md#why-github-stars-matter-).
|
|
329
329
|
|
|
330
|
-
[](https://star-history.dera.page/#aymericzip/intlayer&Date)
|
|
@@ -358,23 +358,19 @@ const processAllDictionaryFiles = (dictionariesDir, dynamicDictionariesDir, prun
|
|
|
358
358
|
]);
|
|
359
359
|
};
|
|
360
360
|
/**
|
|
361
|
-
* Explains
|
|
362
|
-
*
|
|
361
|
+
* Explains the one step the visual editor stands down: renaming the content
|
|
362
|
+
* fields would break the `keyPath` the editor resolves its edits with. Purge
|
|
363
|
+
* and the rest of the minification still run.
|
|
363
364
|
*/
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
(0, _intlayer_config_logger.colorize)("— the editor requires full dictionary content", _intlayer_config_colors.GREY)
|
|
374
|
-
]);
|
|
375
|
-
if (purge) explainDisabled("Dictionary purge");
|
|
376
|
-
if (minify) explainDisabled("Dictionary minification");
|
|
377
|
-
};
|
|
365
|
+
const logFieldRenameDisabledByEditor = (logger) => logger([
|
|
366
|
+
"Dictionary field renaming is",
|
|
367
|
+
(0, _intlayer_config_logger.colorize)("disabled", _intlayer_config_colors.GREY_DARK),
|
|
368
|
+
"because",
|
|
369
|
+
(0, _intlayer_config_logger.colorize)("editor.enabled", _intlayer_config_colors.BLUE),
|
|
370
|
+
"is",
|
|
371
|
+
(0, _intlayer_config_logger.colorize)("true", _intlayer_config_colors.GREY_DARK),
|
|
372
|
+
(0, _intlayer_config_logger.colorize)("— the editor resolves edits by keyPath against the unmerged dictionaries", _intlayer_config_colors.GREY)
|
|
373
|
+
]);
|
|
378
374
|
/**
|
|
379
375
|
* Reports the dictionaries whose consumption could not be tracked statically,
|
|
380
376
|
* with the source files responsible — those keep their full content.
|
|
@@ -414,9 +410,10 @@ const runIntlayerPurgePipeline = (options) => {
|
|
|
414
410
|
const pruneContext = require_babel_plugin_intlayer_usage_analyzer.createPruneContext();
|
|
415
411
|
_pruneContextCache.set(baseDir, pruneContext);
|
|
416
412
|
const logger = (0, _intlayer_config_logger.getAppLogger)(logConfig);
|
|
417
|
-
const shouldPurge = purge
|
|
418
|
-
const shouldMinify = minify
|
|
419
|
-
|
|
413
|
+
const shouldPurge = Boolean(purge);
|
|
414
|
+
const shouldMinify = Boolean(minify);
|
|
415
|
+
const isFieldRenameEnabled = shouldMinify && !editorEnabled;
|
|
416
|
+
if (editorEnabled && minify) logFieldRenameDisabledByEditor(logger);
|
|
420
417
|
if (!shouldPurge && !shouldMinify || optimize === false) return pruneContext;
|
|
421
418
|
for (const sourceFilePath of componentFilesList) {
|
|
422
419
|
if (!require_transformers.SOURCE_FILE_REGEX.test(sourceFilePath)) continue;
|
|
@@ -424,7 +421,7 @@ const runIntlayerPurgePipeline = (options) => {
|
|
|
424
421
|
}
|
|
425
422
|
if (nestedDictionaryReferences) require_babel_plugin_intlayer_usage_analyzer.preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);
|
|
426
423
|
logUntrackedBindings(logger, pruneContext);
|
|
427
|
-
if (
|
|
424
|
+
if (isFieldRenameEnabled) buildRenameMapsSynchronously(dictionariesDir, dynamicDictionariesDir, dictionaryKeyToImportModeMap, pruneContext, logger);
|
|
428
425
|
processAllDictionaryFiles(dictionariesDir, dynamicDictionariesDir, pruneContext, shouldPurge, shouldMinify, logger);
|
|
429
426
|
return pruneContext;
|
|
430
427
|
};
|
|
@@ -473,8 +470,10 @@ const runIntlayerPurgePipeline = (options) => {
|
|
|
473
470
|
* - Intended for **production builds** only. Dictionary JSON files are
|
|
474
471
|
* overwritten in-place; running `intlayer build` afterwards restores the
|
|
475
472
|
* originals.
|
|
476
|
-
* - The plugin is a no-op when `optimize` is `false
|
|
477
|
-
*
|
|
473
|
+
* - The plugin is a no-op when `optimize` is `false`.
|
|
474
|
+
* - When `editorEnabled` is `true`, purge and minify still run but field
|
|
475
|
+
* renaming is skipped, so the `keyPath` the visual editor receives keeps
|
|
476
|
+
* matching the unmerged dictionaries it edits.
|
|
478
477
|
*/
|
|
479
478
|
const intlayerPurgeBabelPlugin = (_babel) => ({
|
|
480
479
|
name: "intlayer-purge",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"babel-plugin-intlayer-purge.cjs","names":["makeUsageAnalyzerBabelPlugin","BABEL_PARSER_OPTIONS","readFileSync","getUsageCheckRegex","extractScriptBlocks","join","existsSync","readdirSync","buildNestedRenameMapFromContent","getNestedRenameEntryAtPath","colorizeKey","colorize","ANSIColors","formatPath","colorizeNumber","createPruneContext","getAppLogger","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 * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { NestedFieldReferences } from '@intlayer/core/dictionaryManipulator';\nimport { formatPath } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport {\n buildNestedRenameMapFromContent,\n getNestedRenameEntryAtPath,\n} from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n preserveNestedDictionaryFields,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin 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 * Dictionaries referenced through `nest()`, mapped to the top-level fields\n * those references read (`'all'` when the whole dictionary is nested).\n *\n * Those fields are resolved at runtime by `getNesting` using their original\n * names, so they must survive purging and must never be renamed.\n */\n nestedDictionaryReferences?: Map<string, NestedFieldReferences>;\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Logging configuration used to report what the pipeline purged and\n * minified, so a Babel- or SWC-driven build prints the same summary as the\n * Vite one. Mirrors `log` in `intlayer.config.ts`; omit to fall back to the\n * logger defaults.\n */\n logConfig?: Pick<IntlayerConfig, 'log'>;\n};\n\n/** Application logger used to report the pipeline's progress. */\ntype PurgeLogger = ReturnType<typeof getAppLogger>;\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process:\n * {@link runIntlayerPurgePipeline} registers the context before running so every\n * subsequent transform is a no-op cache hit.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */\nconst filterRecordByUsedFields = (\n record: Record<string, unknown>,\n usedFieldNames: Set<string>\n): Record<string, unknown> => {\n const filteredRecord: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n if (usedFieldNames.has(fieldName)) {\n filteredRecord[fieldName] = fieldValue;\n }\n }\n return filteredRecord;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n prunedTranslation[locale] = isPlainRecord(localeContent)\n ? filterRecordByUsedFields(localeContent, usedFieldNames)\n : localeContent;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext,\n logger: PurgeLogger\n): void => {\n let partiallyMinifiedDictionariesCount = 0;\n\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque values to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousOccurrences = [...opaqueFieldMap.values()].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(nestedRenameMap, occurrence.fieldPath)\n ?.children.size ?? 0) > 0\n );\n\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(`'${occurrence.fieldPath.join('.')}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...occurrence.locations.map(\n (location) => `\\n at ${formatPath(location)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n for (const occurrence of dangerousOccurrences) {\n const renameEntry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\n if (renameEntry) {\n renameEntry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\n/**\n * Names of the top-level content fields of a compiled dictionary, whatever\n * shape its `content` uses. Used to report which fields the purge removed.\n */\nconst readContentFieldNames = (\n content: CompiledDictionaryJson['content']\n): string[] => {\n // Shape A – fields live inside each locale object.\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n return isPlainRecord(firstLocaleValue) ? Object.keys(firstLocaleValue) : [];\n }\n\n // Shape B / dynamic – flat content record.\n if (isPlainRecord(content)) return Object.keys(content);\n\n return [];\n};\n\n/**\n * Running totals of the purge, reported once the whole pipeline is done.\n */\ntype PurgeStats = {\n /** Dictionary key → number of fields removed from it. */\n prunedFieldsCountPerDictionary: Map<string, number>;\n /**\n * Dictionary keys whose \"pruned fields\" line has already been printed. A\n * key spans several files (one static, one per locale), and they all remove\n * the same fields.\n */\n loggedPrunedDictionaryKeys: Set<string>;\n /**\n * Dictionary keys whose \"analysis is incomplete\" warning has already been\n * printed, deduplicated the same way.\n */\n loggedIncompleteAnalysisKeys: Set<string>;\n};\n\n/**\n * Reads a compiled dictionary JSON file, applies the purge and/or minify\n * transformations, and writes it back in-place when something changed.\n *\n * `dictionaryKind` selects the prune strategy and the minified output shape:\n * static dictionaries keep `{ key, content }`, dynamic per-locale\n * dictionaries additionally keep `locale`.\n */\nconst processDictionaryFile = (\n filePath: string,\n dictionaryKind: 'static' | 'dynamic',\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger,\n stats: PurgeStats\n): void => {\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(\n readFileSync(filePath, 'utf-8')\n ) as CompiledDictionaryJson;\n } catch {\n return; // Unreadable or invalid JSON – leave file unchanged.\n }\n\n const { key: dictionaryKey } = parsedDict;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(filePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const pruneDictionaryContent =\n dictionaryKind === 'static'\n ? pruneStaticDictionaryContent\n : pruneDynamicDictionaryContent;\n const originalFieldNames = readContentFieldNames(parsedDict.content);\n const { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(filePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n stats.prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!stats.loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n stats.loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n parsedDict = prunedDictionary;\n modified = true;\n } else if (\n !fieldUsage &&\n pruneContext.hasUnparsableSourceFiles &&\n !stats.loggedIncompleteAnalysisKeys.has(dictionaryKey)\n ) {\n // An unparsable source file might be the one referencing this key, so its\n // usage set cannot be trusted to be empty.\n stats.loggedIncompleteAnalysisKeys.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? dictionaryKind === 'static'\n ? { key: parsedDict.key, content: parsedDict.content }\n : {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger\n): void => {\n const stats: PurgeStats = {\n prunedFieldsCountPerDictionary: new Map(),\n loggedPrunedDictionaryKeys: new Set(),\n loggedIncompleteAnalysisKeys: new Set(),\n };\n\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processDictionaryFile(\n join(dictionariesDir, entry),\n 'static',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDictionaryFile(\n join(keyDirPath, localeFile),\n 'dynamic',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n\n const totalPrunedFieldsCount = [\n ...stats.prunedFieldsCountPerDictionary.values(),\n ].reduce((total, count) => total + count, 0);\n const totalPrunedDictionariesCount =\n stats.prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n};\n\n// ── Reporting helpers ─────────────────────────────────────────────────────────\n\n/**\n * Explains why the requested steps did nothing: the visual editor needs the\n * full dictionary content, so both purge and minify stand down when it is on.\n */\nconst logStepDisabledByEditor = (\n logger: PurgeLogger,\n purge: boolean,\n minify: boolean\n): void => {\n const explainDisabled = (stepLabel: string) =>\n logger([\n stepLabel,\n 'is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor requires full dictionary content',\n ANSIColors.GREY\n ),\n ]);\n\n if (purge) explainDisabled('Dictionary purge');\n if (minify) explainDisabled('Dictionary minification');\n};\n\n/**\n * Reports the dictionaries whose consumption could not be tracked statically,\n * with the source files responsible — those keep their full content.\n */\nconst logUntrackedBindings = (\n logger: PurgeLogger,\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n *\n * Exported so bundler integrations that do not go through Babel can drive the\n * same pipeline. The Next.js integration is the main consumer: its SWC plugin\n * runs as a per-file Wasm transform with no filesystem access, so the analysis\n * and the dictionary rewrite happen here, in Node, and only the resulting\n * field-rename tables (see {@link serializeFieldRenameMap}) are forwarded to\n * the plugin.\n *\n * **This performs file I/O**: it reads every source file listed in\n * `componentFilesList` and overwrites the compiled dictionary JSON files\n * in-place. Call it at most once per build, after the dictionaries have been\n * built.\n */\nexport const runIntlayerPurgePipeline = (\n options: PurgePluginOptions\n): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n nestedDictionaryReferences,\n compatCallers,\n logConfig,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const logger = getAppLogger(logConfig);\n\n const shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\n\n if (editorEnabled && (purge || minify)) {\n logStepDisabledByEditor(logger, purge, minify);\n }\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 1.5: Keep `nest()` targets resolvable — must run before the rename\n // maps are built so nest targets are excluded from field renaming.\n if (nestedDictionaryReferences) {\n preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);\n }\n\n // Phase 1.6: Warn about the dictionaries the analysis could not follow.\n logUntrackedBindings(logger, pruneContext);\n\n // Phase 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext,\n logger\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false` 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 runIntlayerPurgePipeline(this.opts as PurgePluginOptions);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAoJA,MAAM,qCAAqB,IAAI,IAA0B;;;;;;;;;AAUzD,MAAa,yBAAyB,YACpC,mBAAmB,IAAI,OAAO,KAAK;AAkBrC,MAAM,qBAAqB,UACzB,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,aAAa,iBAChD,OAAQ,MAAkC,gBAAgB;AAE5D,MAAM,iBAAiB,UACrB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;AAUrE,MAAM,4BACJ,QACA,mBAC4B;CAC5B,MAAM,iBAA0C,CAAC;CACjD,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,MAAM,GACzD,IAAI,eAAe,IAAI,SAAS,GAC9B,eAAe,aAAa;CAGhC,OAAO;AACT;;;;;;AAOA,MAAM,gCACJ,YACA,mBACgB;CAChB,MAAM,EAAE,YAAY;CAGpB,IAAI,kBAAkB,OAAO,GAAG;EAC9B,MAAM,mBAAmB,OAAO,OAAO,QAAQ,WAAW,CAAC,CAAC;EAC5D,IAAI,cAAc,gBAAgB,GAAG;GACnC,MAAM,oBAA6C,CAAC;GACpD,KAAK,MAAM,CAAC,QAAQ,kBAAkB,OAAO,QAC3C,QAAQ,WACV,GACE,kBAAkB,UAAU,cAAc,aAAa,IACnD,yBAAyB,eAAe,cAAc,IACtD;GAEN,OAAO;IACL,kBAAkB;KAChB,GAAG;KACH,SAAS;MAAE,GAAG;MAAS,aAAa;KAAkB;IACxD;IACA,eAAe;GACjB;EACF;CACF;CAGA,IAAI,cAAc,OAAO,KAAK,CAAC,kBAAkB,OAAO,GACtD,OAAO;EACL,kBAAkB;GAChB,GAAG;GACH,SAAS,yBAAyB,SAAS,cAAc;EAC3D;EACA,eAAe;CACjB;CAGF,OAAO;EAAE,kBAAkB;EAAY,eAAe;CAAM;AAC9D;;;;;AAMA,MAAM,iCACJ,YACA,mBACgB;CAChB,MAAM,EAAE,YAAY;CACpB,IAAI,CAAC,cAAc,OAAO,GACxB,OAAO;EAAE,kBAAkB;EAAY,eAAe;CAAM;CAE9D,OAAO;EACL,kBAAkB;GAChB,GAAG;GACH,SAAS,yBAAyB,SAAS,cAAc;EAC3D;EACA,eAAe;CACjB;AACF;;;;;;AASA,MAAM,4BACJ,OACA,cACY;CACZ,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAQ,MAAoB,KAAK,YAC/B,yBAAyB,SAAS,SAAS,CAC7C;CAEF,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAEhD,MAAM,SAAS;CAGf,IACE,OAAO,OAAO,aAAa,YAC3B,OAAO,eACP,OAAO,OAAO,gBAAgB,YAC9B,CAAC,MAAM,QAAQ,OAAO,WAAW,GACjC;EACA,MAAM,qBAA8C,CAAC;EACrD,KAAK,MAAM,CAAC,QAAQ,gBAAgB,OAAO,QACzC,OAAO,WACT,GACE,mBAAmB,UAAU,yBAC3B,aACA,SACF;EAEF,OAAO;GAAE,GAAG;GAAQ,aAAa;EAAmB;CACtD;CAGA,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,MAAM,GAAG;EAC/C,MAAM,cAAc,UAAU,IAAI,GAAG;EACrC,IAAI,aACF,OAAO,YAAY,aAAa,yBAC9B,KACA,YAAY,QACd;OAEA,OAAO,OAAO;CAElB;CACA,OAAO;AACT;;;;;AAMA,MAAM,0BACJ,MACA,cAC4B;CAC5B,MAAM,UAAU,KAAK;CACrB,IAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,GAClE,OAAO;CACT,OAAO;EACL,GAAG;EACH,SAAS,yBAAyB,SAAS,SAAS;CACtD;AACF;;;;;AAQA,MAAM,wBACJ,MACA,gBACA,cACA,kBACS;CACT,IAAI;EACF,+BAAc,MAAM;GAClB,UAAU;GACV,SAAS,CAACA,0EAA6B,cAAc,EAAE,cAAc,CAAC,CAAC;GACvE,YAAYC;GACZ,KAAK;GACL,MAAM;EACR,CAAC;CACH,QAAQ;EACN,aAAa,2BAA2B;CAC1C;AACF;;;;;AAMA,MAAM,yBACJ,gBACA,cACA,kBACS;CACT,IAAI;CACJ,IAAI;EACF,WAAOC,sBAAa,gBAAgB,OAAO;CAC7C,QAAQ;EACN;CACF;CAEA,MAAM,kBAAkBC,wCAAmB,aAAa;CAExD,IAAI,CAAC,gBAAgB,KAAK,IAAI,GAAG;CAEjC,MAAM,eAAeC,gDAAoB,gBAAgB,IAAI;CAC7D,KAAK,MAAM,SAAS,cAAc;EAChC,IAAI,CAAC,gBAAgB,KAAK,MAAM,OAAO,GAAG;EAC1C,qBACE,MAAM,SACN,gBACA,cACA,aACF;CACF;AACF;;;;;;;AAUA,MAAM,gCACJ,iBACA,wBACA,8BACA,cACA,WACS;CACT,IAAI,qCAAqC;CAEzC,KAAK,MAAM,CACT,eACA,eACG,aAAa,8BAA8B;EAC9C,IAAI,eAAe,OAAO;EAC1B,IAAI,6BAA6B,mBAAmB,SAAS;EAC7D,IAAI,aAAa,gCAAgC,IAAI,aAAa,GAChE;EAEF,IAAI,oBAA6B;EAEjC,MAAM,qBAAiBC,gBAAK,iBAAiB,GAAG,cAAc,MAAM;EACpE,QAAIC,oBAAW,cAAc,GAC3B,IAAI;GACF,MAAM,UAAMJ,sBAAa,gBAAgB,OAAO;GAEhD,oBADe,KAAK,MAAM,GACD,CAAC,CAAC;EAC7B,QAAQ,CAER;EAGF,IAAI,CAAC,mBAAmB;GACtB,MAAM,qBAAiBG,gBAAK,wBAAwB,aAAa;GACjE,QAAIC,oBAAW,cAAc,GAC3B,IAAI;IAEF,MAAM,oBADcC,qBAAY,cACA,CAAC,CAAC,MAAM,SACtC,KAAK,SAAS,OAAO,CACvB;IACA,IAAI,eAAe;KACjB,MAAM,UAAML,0BACVG,gBAAK,gBAAgB,aAAa,GAClC,OACF;KAEA,oBADe,KAAK,MAAM,GACD,CAAC,CAAC;IAC7B;GACF,QAAQ,CAER;EAEJ;EAEA,IAAI,CAAC,mBAAmB;EAExB,MAAM,kBAAkBG,2EAAgC,iBAAiB;EAGzE,MAAM,iBACJ,aAAa,+BAA+B,IAAI,aAAa;EAC/D,IAAI,gBAAgB;GAClB,MAAM,uBAAuB,CAAC,GAAG,eAAe,OAAO,CAAC,CAAC,CAAC,QACvD,gBACEC,sEAA2B,iBAAiB,WAAW,SAAS,CAAC,EAC9D,SAAS,QAAQ,KAAK,CAC9B;GAEA,IAAI,qBAAqB,SAAS,GAAG;IACnC,sCAAsC;IAEtC,OACE;KACE;SACAC,qCAAY,aAAa;KACzB;KACA,GAAG,qBAAqB,SAAS,eAAe;MAC9C;UACAC,kCAAS,IAAI,WAAW,UAAU,KAAK,GAAG,EAAE,IAAIC,wBAAW,IAAI;MAC/D;MACA,GAAG,WAAW,UAAU,KACrB,aAAa,kBAAcC,mCAAW,QAAQ,GACjD;KACF,CAAC;IACH,GACA;KAAE,OAAO;KAAQ,WAAW;IAAK,CACnC;IAEA,KAAK,MAAM,cAAc,sBAAsB;KAC7C,MAAM,cAAcJ,sEAClB,iBACA,WAAW,SACb;KACA,IAAI,aACF,YAAY,2BAAW,IAAI,IAAI;IAEnC;GACF;EACF;EAEA,IAAI,gBAAgB,OAAO,GACzB,aAAa,8BAA8B,IACzC,eACA,eACF;CAEJ;CAEA,IAAI,qCAAqC,GACvC,OAAO;EACL;MACAK,wCAAe,kCAAkC;EACjD,YAAY,uCAAuC,IAAI,MAAM;EAC7D;CACF,CAAC;AAEL;;;;;AAQA,MAAM,yBACJ,YACa;CAEb,IAAI,kBAAkB,OAAO,GAAG;EAC9B,MAAM,mBAAmB,OAAO,OAAO,QAAQ,WAAW,CAAC,CAAC;EAC5D,OAAO,cAAc,gBAAgB,IAAI,OAAO,KAAK,gBAAgB,IAAI,CAAC;CAC5E;CAGA,IAAI,cAAc,OAAO,GAAG,OAAO,OAAO,KAAK,OAAO;CAEtD,OAAO,CAAC;AACV;;;;;;;;;AA6BA,MAAM,yBACJ,UACA,gBACA,cACA,aACA,cACA,QACA,UACS;CACT,IAAI;CACJ,IAAI;EACF,aAAa,KAAK,UAChBZ,sBAAa,UAAU,OAAO,CAChC;CACF,QAAQ;EACN;CACF;CAEA,MAAM,EAAE,KAAK,kBAAkB;CAE/B,IAAI,CAAC,eAAe;EAClB,OACE;GACE;OACAW,mCAAW,QAAQ;GACnB;EACF,GACA,EAAE,OAAO,OAAO,CAClB;EACA;CACF;CAEA,IAAI,aAAa,0BAA0B,IAAI,aAAa,GAAG;CAE/D,IAAI,WAAW;CAEf,IAAI,aAAa;EACf,MAAM,aACJ,aAAa,6BAA6B,IAAI,aAAa;EAC7D,IAAI,cAAc,eAAe,OAAO;GACtC,MAAM,yBACJ,mBAAmB,WACf,+BACA;GACN,MAAM,qBAAqB,sBAAsB,WAAW,OAAO;GACnE,MAAM,EAAE,kBAAkB,kBAAkB,uBAC1C,YACA,UACF;GACA,IAAI,CAAC,eAAe;IAClB,aAAa,0BAA0B,IAAI,aAAa;IACxD,OACE;KACE;SACAH,qCAAY,aAAa;KACzB;KACA,OAAGG,mCAAW,QAAQ,EAAE;KACxB;IACF,GACA,EAAE,OAAO,OAAO,CAClB;IACA;GACF;GAEA,MAAM,oBAAoB,mBAAmB,QAC1C,cAAc,CAAC,WAAW,IAAI,SAAS,CAC1C;GAEA,IAAI,kBAAkB,SAAS,GAAG;IAChC,MAAM,+BAA+B,IACnC,eACA,kBAAkB,MACpB;IAEA,IAAI,CAAC,MAAM,2BAA2B,IAAI,aAAa,GAAG;KACxD,MAAM,2BAA2B,IAAI,aAAa;KAClD,OACE;MACE;UACAC,wCAAe,kBAAkB,MAAM;MACvC,eAAe,kBAAkB,WAAW,IAAI,KAAK,IAAI;MACzD,OAAGJ,qCAAY,aAAa,EAAE;MAC9B,kBACG,KAAK,kBAAcC,kCAAS,WAAWC,wBAAW,UAAU,CAAC,CAAC,CAC9D,KAAK,IAAI;KACd,GACA,EAAE,WAAW,KAAK,CACpB;IACF;GACF;GAEA,aAAa;GACb,WAAW;EACb,OAAO,IACL,CAAC,cACD,aAAa,4BACb,CAAC,MAAM,6BAA6B,IAAI,aAAa,GACrD;GAGA,MAAM,6BAA6B,IAAI,aAAa;GACpD,OACE;IACE;QACAF,qCAAY,aAAa;IACzB;GACF,GACA,EAAE,OAAO,OAAO,CAClB;EACF;CACF;CAEA,IAAI,cAAc;EAChB,MAAM,iBACJ,aAAa,8BAA8B,IAAI,aAAa;EAC9D,IAAI,kBAAkB,eAAe,OAAO,GAAG;GAC7C,aAAa,uBACX,YACA,cACF;GACA,WAAW;EACb;CACF;CAEA,IAAI,CAAC,UAAU;CAEf,MAAM,aAAa,eACf,mBAAmB,WACjB;EAAE,KAAK,WAAW;EAAK,SAAS,WAAW;CAAQ,IACnD;EACE,KAAK,WAAW;EAChB,SAAS,WAAW;EACpB,QAAQ,WAAW;CACrB,IACF;CAEJ,IAAI;EACF,2BAAc,UAAU,KAAK,UAAU,UAAU,GAAG,OAAO;CAC7D,QAAQ,CAER;AACF;AAEA,MAAM,6BACJ,iBACA,wBACA,cACA,aACA,cACA,WACS;CACT,MAAM,QAAoB;EACxB,gDAAgC,IAAI,IAAI;EACxC,4CAA4B,IAAI,IAAI;EACpC,8CAA8B,IAAI,IAAI;CACxC;CAEA,QAAIJ,oBAAW,eAAe,GAC5B,KAAK,MAAM,aAASC,qBAAY,eAAe,GAAG;EAChD,IAAI,CAAC,MAAM,SAAS,OAAO,GAAG;EAC9B,0BACEF,gBAAK,iBAAiB,KAAK,GAC3B,UACA,cACA,aACA,cACA,QACA,KACF;CACF;CAGF,QAAIC,oBAAW,sBAAsB,GACnC,KAAK,MAAM,cAAUC,qBAAY,sBAAsB,GAAG;EACxD,MAAM,iBAAaF,gBAAK,wBAAwB,MAAM;EACtD,IAAI;GACF,KAAK,MAAM,kBAAcE,qBAAY,UAAU,GAAG;IAChD,IAAI,CAAC,WAAW,SAAS,OAAO,GAAG;IACnC,0BACEF,gBAAK,YAAY,UAAU,GAC3B,WACA,cACA,aACA,cACA,QACA,KACF;GACF;EACF,QAAQ,CAER;CACF;CAGF,MAAM,yBAAyB,CAC7B,GAAG,MAAM,+BAA+B,OAAO,CACjD,CAAC,CAAC,QAAQ,OAAO,UAAU,QAAQ,OAAO,CAAC;CAC3C,MAAM,+BACJ,MAAM,+BAA+B;CAEvC,IAAI,yBAAyB,GAC3B,OAAO;EACL;MACAS,wCAAe,sBAAsB;EACrC,eAAe,2BAA2B,IAAI,KAAK,IAAI;MACvDA,wCAAe,4BAA4B;EAC3C,YAAY,iCAAiC,IAAI,MAAM,MAAM;CAC/D,CAAC;AAEL;;;;;AAQA,MAAM,2BACJ,QACA,OACA,WACS;CACT,MAAM,mBAAmB,cACvB,OAAO;EACL;EACA;MACAH,kCAAS,YAAYC,wBAAW,SAAS;EACzC;MACAD,kCAAS,kBAAkBC,wBAAW,IAAI;EAC1C;MACAD,kCAAS,QAAQC,wBAAW,SAAS;MACrCD,kCACE,iDACAC,wBAAW,IACb;CACF,CAAC;CAEH,IAAI,OAAO,gBAAgB,kBAAkB;CAC7C,IAAI,QAAQ,gBAAgB,yBAAyB;AACvD;;;;;AAMA,MAAM,wBACJ,QACA,iBACS;CACT,KAAK,MAAM,CACT,eACA,oBACG,aAAa,qCAChB,OACE;EACE;MACAF,qCAAY,aAAa;EACzB;EACA;EACA,OAAGC,kCAAS,gBAAgBC,wBAAW,UAAU,QAAIF,qCACnD,IAAI,cAAc,EACpB,QAAIC,kCAAS,KAAKC,wBAAW,UAAU;EACvC;EACA,GAAG,gBAAgB,KAChB,aAAa,iBAAaC,mCAAW,QAAQ,GAChD;CACF,GACA,EAAE,OAAO,OAAO,CAClB;AAEJ;;;;;;;;;;;;;;;;;;AAqBA,MAAa,4BACX,YACiB;CACjB,MAAM,EACJ,SACA,OACA,QACA,UACA,eACA,iBACA,wBACA,oBACA,8BACA,4BACA,eACA,cACE;CAEJ,MAAM,gBAAgB,mBAAmB,IAAI,OAAO;CACpD,IAAI,eAAe,OAAO;CAE1B,MAAM,eAAeE,gEAAmB;CACxC,mBAAmB,IAAI,SAAS,YAAY;CAE5C,MAAM,aAASC,sCAAa,SAAS;CAErC,MAAM,cAAc,SAAS,CAAC;CAC9B,MAAM,eAAe,UAAU,CAAC;CAEhC,IAAI,kBAAkB,SAAS,SAC7B,wBAAwB,QAAQ,OAAO,MAAM;CAG/C,IAAK,CAAC,eAAe,CAAC,gBAAiB,aAAa,OAClD,OAAO;CAGT,KAAK,MAAM,kBAAkB,oBAAoB;EAC/C,IAAI,CAACC,uCAAkB,KAAK,cAAc,GAAG;EAC7C,sBAAsB,gBAAgB,cAAc,aAAa;CACnE;CAIA,IAAI,4BACF,4EAA+B,cAAc,0BAA0B;CAIzE,qBAAqB,QAAQ,YAAY;CAGzC,IAAI,cACF,6BACE,iBACA,wBACA,8BACA,cACA,MACF;CAIF,0BACE,iBACA,wBACA,cACA,aACA,cACA,MACF;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAa,4BAA4B,YAEpB;CACnB,MAAM;CAEN,MAAsB;EACpB,yBAAyB,KAAK,IAA0B;CAC1D;CAEA,SAAS,CAET;AACF"}
|
|
1
|
+
{"version":3,"file":"babel-plugin-intlayer-purge.cjs","names":["makeUsageAnalyzerBabelPlugin","BABEL_PARSER_OPTIONS","readFileSync","getUsageCheckRegex","extractScriptBlocks","join","existsSync","readdirSync","buildNestedRenameMapFromContent","getNestedRenameEntryAtPath","colorizeKey","colorize","ANSIColors","formatPath","colorizeNumber","createPruneContext","getAppLogger","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 * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { NestedFieldReferences } from '@intlayer/core/dictionaryManipulator';\nimport { formatPath } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport {\n buildNestedRenameMapFromContent,\n getNestedRenameEntryAtPath,\n} from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n preserveNestedDictionaryFields,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin still purges and minifies, but skips field renaming\n * so the `keyPath` reported to the visual editor keeps matching the unmerged\n * dictionaries it edits. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Dictionaries referenced through `nest()`, mapped to the top-level fields\n * those references read (`'all'` when the whole dictionary is nested).\n *\n * Those fields are resolved at runtime by `getNesting` using their original\n * names, so they must survive purging and must never be renamed.\n */\n nestedDictionaryReferences?: Map<string, NestedFieldReferences>;\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Logging configuration used to report what the pipeline purged and\n * minified, so a Babel- or SWC-driven build prints the same summary as the\n * Vite one. Mirrors `log` in `intlayer.config.ts`; omit to fall back to the\n * logger defaults.\n */\n logConfig?: Pick<IntlayerConfig, 'log'>;\n};\n\n/** Application logger used to report the pipeline's progress. */\ntype PurgeLogger = ReturnType<typeof getAppLogger>;\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process:\n * {@link runIntlayerPurgePipeline} registers the context before running so every\n * subsequent transform is a no-op cache hit.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */\nconst filterRecordByUsedFields = (\n record: Record<string, unknown>,\n usedFieldNames: Set<string>\n): Record<string, unknown> => {\n const filteredRecord: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n if (usedFieldNames.has(fieldName)) {\n filteredRecord[fieldName] = fieldValue;\n }\n }\n return filteredRecord;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n prunedTranslation[locale] = isPlainRecord(localeContent)\n ? filterRecordByUsedFields(localeContent, usedFieldNames)\n : localeContent;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext,\n logger: PurgeLogger\n): void => {\n let partiallyMinifiedDictionariesCount = 0;\n\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque values to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousOccurrences = [...opaqueFieldMap.values()].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(nestedRenameMap, occurrence.fieldPath)\n ?.children.size ?? 0) > 0\n );\n\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(`'${occurrence.fieldPath.join('.')}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...occurrence.locations.map(\n (location) => `\\n at ${formatPath(location)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n for (const occurrence of dangerousOccurrences) {\n const renameEntry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\n if (renameEntry) {\n renameEntry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\n/**\n * Names of the top-level content fields of a compiled dictionary, whatever\n * shape its `content` uses. Used to report which fields the purge removed.\n */\nconst readContentFieldNames = (\n content: CompiledDictionaryJson['content']\n): string[] => {\n // Shape A – fields live inside each locale object.\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n return isPlainRecord(firstLocaleValue) ? Object.keys(firstLocaleValue) : [];\n }\n\n // Shape B / dynamic – flat content record.\n if (isPlainRecord(content)) return Object.keys(content);\n\n return [];\n};\n\n/**\n * Running totals of the purge, reported once the whole pipeline is done.\n */\ntype PurgeStats = {\n /** Dictionary key → number of fields removed from it. */\n prunedFieldsCountPerDictionary: Map<string, number>;\n /**\n * Dictionary keys whose \"pruned fields\" line has already been printed. A\n * key spans several files (one static, one per locale), and they all remove\n * the same fields.\n */\n loggedPrunedDictionaryKeys: Set<string>;\n /**\n * Dictionary keys whose \"analysis is incomplete\" warning has already been\n * printed, deduplicated the same way.\n */\n loggedIncompleteAnalysisKeys: Set<string>;\n};\n\n/**\n * Reads a compiled dictionary JSON file, applies the purge and/or minify\n * transformations, and writes it back in-place when something changed.\n *\n * `dictionaryKind` selects the prune strategy and the minified output shape:\n * static dictionaries keep `{ key, content }`, dynamic per-locale\n * dictionaries additionally keep `locale`.\n */\nconst processDictionaryFile = (\n filePath: string,\n dictionaryKind: 'static' | 'dynamic',\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger,\n stats: PurgeStats\n): void => {\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(\n readFileSync(filePath, 'utf-8')\n ) as CompiledDictionaryJson;\n } catch {\n return; // Unreadable or invalid JSON – leave file unchanged.\n }\n\n const { key: dictionaryKey } = parsedDict;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(filePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const pruneDictionaryContent =\n dictionaryKind === 'static'\n ? pruneStaticDictionaryContent\n : pruneDynamicDictionaryContent;\n const originalFieldNames = readContentFieldNames(parsedDict.content);\n const { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(filePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n stats.prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!stats.loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n stats.loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n parsedDict = prunedDictionary;\n modified = true;\n } else if (\n !fieldUsage &&\n pruneContext.hasUnparsableSourceFiles &&\n !stats.loggedIncompleteAnalysisKeys.has(dictionaryKey)\n ) {\n // An unparsable source file might be the one referencing this key, so its\n // usage set cannot be trusted to be empty.\n stats.loggedIncompleteAnalysisKeys.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? dictionaryKind === 'static'\n ? { key: parsedDict.key, content: parsedDict.content }\n : {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger\n): void => {\n const stats: PurgeStats = {\n prunedFieldsCountPerDictionary: new Map(),\n loggedPrunedDictionaryKeys: new Set(),\n loggedIncompleteAnalysisKeys: new Set(),\n };\n\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processDictionaryFile(\n join(dictionariesDir, entry),\n 'static',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDictionaryFile(\n join(keyDirPath, localeFile),\n 'dynamic',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n\n const totalPrunedFieldsCount = [\n ...stats.prunedFieldsCountPerDictionary.values(),\n ].reduce((total, count) => total + count, 0);\n const totalPrunedDictionariesCount =\n stats.prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n};\n\n// ── Reporting helpers ─────────────────────────────────────────────────────────\n\n/**\n * Explains the one step the visual editor stands down: renaming the content\n * fields would break the `keyPath` the editor resolves its edits with. Purge\n * and the rest of the minification still run.\n */\nconst logFieldRenameDisabledByEditor = (logger: PurgeLogger): void =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor resolves edits by keyPath against the unmerged dictionaries',\n ANSIColors.GREY\n ),\n ]);\n\n/**\n * Reports the dictionaries whose consumption could not be tracked statically,\n * with the source files responsible — those keep their full content.\n */\nconst logUntrackedBindings = (\n logger: PurgeLogger,\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n *\n * Exported so bundler integrations that do not go through Babel can drive the\n * same pipeline. The Next.js integration is the main consumer: its SWC plugin\n * runs as a per-file Wasm transform with no filesystem access, so the analysis\n * and the dictionary rewrite happen here, in Node, and only the resulting\n * field-rename tables (see {@link serializeFieldRenameMap}) are forwarded to\n * the plugin.\n *\n * **This performs file I/O**: it reads every source file listed in\n * `componentFilesList` and overwrites the compiled dictionary JSON files\n * in-place. Call it at most once per build, after the dictionaries have been\n * built.\n */\nexport const runIntlayerPurgePipeline = (\n options: PurgePluginOptions\n): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n nestedDictionaryReferences,\n compatCallers,\n logConfig,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const logger = getAppLogger(logConfig);\n\n const shouldPurge = Boolean(purge);\n const shouldMinify = Boolean(minify);\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` reported to the visual editor would no longer resolve against\n // the unmerged dictionaries it edits. Purging is keyPath-neutral: a purged\n // field is one no source file reads, so it is never rendered either.\n const isFieldRenameEnabled = shouldMinify && !editorEnabled;\n\n if (editorEnabled && minify) {\n logFieldRenameDisabledByEditor(logger);\n }\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 1.5: Keep `nest()` targets resolvable — must run before the rename\n // maps are built so nest targets are excluded from field renaming.\n if (nestedDictionaryReferences) {\n preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);\n }\n\n // Phase 1.6: Warn about the dictionaries the analysis could not follow.\n logUntrackedBindings(logger, pruneContext);\n\n // Phase 2: Build field-rename maps (minify only, editor disabled only).\n if (isFieldRenameEnabled) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext,\n logger\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false`.\n * - When `editorEnabled` is `true`, purge and minify still run but field\n * renaming is skipped, so the `keyPath` the visual editor receives keeps\n * matching the unmerged dictionaries it edits.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass) {\n runIntlayerPurgePipeline(this.opts as PurgePluginOptions);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
|
|
@@ -355,23 +355,19 @@ const processAllDictionaryFiles = (dictionariesDir, dynamicDictionariesDir, prun
|
|
|
355
355
|
]);
|
|
356
356
|
};
|
|
357
357
|
/**
|
|
358
|
-
* Explains
|
|
359
|
-
*
|
|
358
|
+
* Explains the one step the visual editor stands down: renaming the content
|
|
359
|
+
* fields would break the `keyPath` the editor resolves its edits with. Purge
|
|
360
|
+
* and the rest of the minification still run.
|
|
360
361
|
*/
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
colorize("— the editor requires full dictionary content", ANSIColors.GREY)
|
|
371
|
-
]);
|
|
372
|
-
if (purge) explainDisabled("Dictionary purge");
|
|
373
|
-
if (minify) explainDisabled("Dictionary minification");
|
|
374
|
-
};
|
|
362
|
+
const logFieldRenameDisabledByEditor = (logger) => logger([
|
|
363
|
+
"Dictionary field renaming is",
|
|
364
|
+
colorize("disabled", ANSIColors.GREY_DARK),
|
|
365
|
+
"because",
|
|
366
|
+
colorize("editor.enabled", ANSIColors.BLUE),
|
|
367
|
+
"is",
|
|
368
|
+
colorize("true", ANSIColors.GREY_DARK),
|
|
369
|
+
colorize("— the editor resolves edits by keyPath against the unmerged dictionaries", ANSIColors.GREY)
|
|
370
|
+
]);
|
|
375
371
|
/**
|
|
376
372
|
* Reports the dictionaries whose consumption could not be tracked statically,
|
|
377
373
|
* with the source files responsible — those keep their full content.
|
|
@@ -411,9 +407,10 @@ const runIntlayerPurgePipeline = (options) => {
|
|
|
411
407
|
const pruneContext = createPruneContext();
|
|
412
408
|
_pruneContextCache.set(baseDir, pruneContext);
|
|
413
409
|
const logger = getAppLogger(logConfig);
|
|
414
|
-
const shouldPurge = purge
|
|
415
|
-
const shouldMinify = minify
|
|
416
|
-
|
|
410
|
+
const shouldPurge = Boolean(purge);
|
|
411
|
+
const shouldMinify = Boolean(minify);
|
|
412
|
+
const isFieldRenameEnabled = shouldMinify && !editorEnabled;
|
|
413
|
+
if (editorEnabled && minify) logFieldRenameDisabledByEditor(logger);
|
|
417
414
|
if (!shouldPurge && !shouldMinify || optimize === false) return pruneContext;
|
|
418
415
|
for (const sourceFilePath of componentFilesList) {
|
|
419
416
|
if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;
|
|
@@ -421,7 +418,7 @@ const runIntlayerPurgePipeline = (options) => {
|
|
|
421
418
|
}
|
|
422
419
|
if (nestedDictionaryReferences) preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);
|
|
423
420
|
logUntrackedBindings(logger, pruneContext);
|
|
424
|
-
if (
|
|
421
|
+
if (isFieldRenameEnabled) buildRenameMapsSynchronously(dictionariesDir, dynamicDictionariesDir, dictionaryKeyToImportModeMap, pruneContext, logger);
|
|
425
422
|
processAllDictionaryFiles(dictionariesDir, dynamicDictionariesDir, pruneContext, shouldPurge, shouldMinify, logger);
|
|
426
423
|
return pruneContext;
|
|
427
424
|
};
|
|
@@ -470,8 +467,10 @@ const runIntlayerPurgePipeline = (options) => {
|
|
|
470
467
|
* - Intended for **production builds** only. Dictionary JSON files are
|
|
471
468
|
* overwritten in-place; running `intlayer build` afterwards restores the
|
|
472
469
|
* originals.
|
|
473
|
-
* - The plugin is a no-op when `optimize` is `false
|
|
474
|
-
*
|
|
470
|
+
* - The plugin is a no-op when `optimize` is `false`.
|
|
471
|
+
* - When `editorEnabled` is `true`, purge and minify still run but field
|
|
472
|
+
* renaming is skipped, so the `keyPath` the visual editor receives keeps
|
|
473
|
+
* matching the unmerged dictionaries it edits.
|
|
475
474
|
*/
|
|
476
475
|
const intlayerPurgeBabelPlugin = (_babel) => ({
|
|
477
476
|
name: "intlayer-purge",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"babel-plugin-intlayer-purge.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { PluginObject, PluginPass } from '@babel/core';\nimport { transformSync } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { NestedFieldReferences } from '@intlayer/core/dictionaryManipulator';\nimport { formatPath } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport {\n buildNestedRenameMapFromContent,\n getNestedRenameEntryAtPath,\n} from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n preserveNestedDictionaryFields,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin 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 * Dictionaries referenced through `nest()`, mapped to the top-level fields\n * those references read (`'all'` when the whole dictionary is nested).\n *\n * Those fields are resolved at runtime by `getNesting` using their original\n * names, so they must survive purging and must never be renamed.\n */\n nestedDictionaryReferences?: Map<string, NestedFieldReferences>;\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Logging configuration used to report what the pipeline purged and\n * minified, so a Babel- or SWC-driven build prints the same summary as the\n * Vite one. Mirrors `log` in `intlayer.config.ts`; omit to fall back to the\n * logger defaults.\n */\n logConfig?: Pick<IntlayerConfig, 'log'>;\n};\n\n/** Application logger used to report the pipeline's progress. */\ntype PurgeLogger = ReturnType<typeof getAppLogger>;\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process:\n * {@link runIntlayerPurgePipeline} registers the context before running so every\n * subsequent transform is a no-op cache hit.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */\nconst filterRecordByUsedFields = (\n record: Record<string, unknown>,\n usedFieldNames: Set<string>\n): Record<string, unknown> => {\n const filteredRecord: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n if (usedFieldNames.has(fieldName)) {\n filteredRecord[fieldName] = fieldValue;\n }\n }\n return filteredRecord;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n prunedTranslation[locale] = isPlainRecord(localeContent)\n ? filterRecordByUsedFields(localeContent, usedFieldNames)\n : localeContent;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext,\n logger: PurgeLogger\n): void => {\n let partiallyMinifiedDictionariesCount = 0;\n\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque values to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousOccurrences = [...opaqueFieldMap.values()].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(nestedRenameMap, occurrence.fieldPath)\n ?.children.size ?? 0) > 0\n );\n\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(`'${occurrence.fieldPath.join('.')}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...occurrence.locations.map(\n (location) => `\\n at ${formatPath(location)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n for (const occurrence of dangerousOccurrences) {\n const renameEntry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\n if (renameEntry) {\n renameEntry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\n/**\n * Names of the top-level content fields of a compiled dictionary, whatever\n * shape its `content` uses. Used to report which fields the purge removed.\n */\nconst readContentFieldNames = (\n content: CompiledDictionaryJson['content']\n): string[] => {\n // Shape A – fields live inside each locale object.\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n return isPlainRecord(firstLocaleValue) ? Object.keys(firstLocaleValue) : [];\n }\n\n // Shape B / dynamic – flat content record.\n if (isPlainRecord(content)) return Object.keys(content);\n\n return [];\n};\n\n/**\n * Running totals of the purge, reported once the whole pipeline is done.\n */\ntype PurgeStats = {\n /** Dictionary key → number of fields removed from it. */\n prunedFieldsCountPerDictionary: Map<string, number>;\n /**\n * Dictionary keys whose \"pruned fields\" line has already been printed. A\n * key spans several files (one static, one per locale), and they all remove\n * the same fields.\n */\n loggedPrunedDictionaryKeys: Set<string>;\n /**\n * Dictionary keys whose \"analysis is incomplete\" warning has already been\n * printed, deduplicated the same way.\n */\n loggedIncompleteAnalysisKeys: Set<string>;\n};\n\n/**\n * Reads a compiled dictionary JSON file, applies the purge and/or minify\n * transformations, and writes it back in-place when something changed.\n *\n * `dictionaryKind` selects the prune strategy and the minified output shape:\n * static dictionaries keep `{ key, content }`, dynamic per-locale\n * dictionaries additionally keep `locale`.\n */\nconst processDictionaryFile = (\n filePath: string,\n dictionaryKind: 'static' | 'dynamic',\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger,\n stats: PurgeStats\n): void => {\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(\n readFileSync(filePath, 'utf-8')\n ) as CompiledDictionaryJson;\n } catch {\n return; // Unreadable or invalid JSON – leave file unchanged.\n }\n\n const { key: dictionaryKey } = parsedDict;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(filePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const pruneDictionaryContent =\n dictionaryKind === 'static'\n ? pruneStaticDictionaryContent\n : pruneDynamicDictionaryContent;\n const originalFieldNames = readContentFieldNames(parsedDict.content);\n const { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(filePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n stats.prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!stats.loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n stats.loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n parsedDict = prunedDictionary;\n modified = true;\n } else if (\n !fieldUsage &&\n pruneContext.hasUnparsableSourceFiles &&\n !stats.loggedIncompleteAnalysisKeys.has(dictionaryKey)\n ) {\n // An unparsable source file might be the one referencing this key, so its\n // usage set cannot be trusted to be empty.\n stats.loggedIncompleteAnalysisKeys.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? dictionaryKind === 'static'\n ? { key: parsedDict.key, content: parsedDict.content }\n : {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger\n): void => {\n const stats: PurgeStats = {\n prunedFieldsCountPerDictionary: new Map(),\n loggedPrunedDictionaryKeys: new Set(),\n loggedIncompleteAnalysisKeys: new Set(),\n };\n\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processDictionaryFile(\n join(dictionariesDir, entry),\n 'static',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDictionaryFile(\n join(keyDirPath, localeFile),\n 'dynamic',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n\n const totalPrunedFieldsCount = [\n ...stats.prunedFieldsCountPerDictionary.values(),\n ].reduce((total, count) => total + count, 0);\n const totalPrunedDictionariesCount =\n stats.prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n};\n\n// ── Reporting helpers ─────────────────────────────────────────────────────────\n\n/**\n * Explains why the requested steps did nothing: the visual editor needs the\n * full dictionary content, so both purge and minify stand down when it is on.\n */\nconst logStepDisabledByEditor = (\n logger: PurgeLogger,\n purge: boolean,\n minify: boolean\n): void => {\n const explainDisabled = (stepLabel: string) =>\n logger([\n stepLabel,\n 'is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor requires full dictionary content',\n ANSIColors.GREY\n ),\n ]);\n\n if (purge) explainDisabled('Dictionary purge');\n if (minify) explainDisabled('Dictionary minification');\n};\n\n/**\n * Reports the dictionaries whose consumption could not be tracked statically,\n * with the source files responsible — those keep their full content.\n */\nconst logUntrackedBindings = (\n logger: PurgeLogger,\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n *\n * Exported so bundler integrations that do not go through Babel can drive the\n * same pipeline. The Next.js integration is the main consumer: its SWC plugin\n * runs as a per-file Wasm transform with no filesystem access, so the analysis\n * and the dictionary rewrite happen here, in Node, and only the resulting\n * field-rename tables (see {@link serializeFieldRenameMap}) are forwarded to\n * the plugin.\n *\n * **This performs file I/O**: it reads every source file listed in\n * `componentFilesList` and overwrites the compiled dictionary JSON files\n * in-place. Call it at most once per build, after the dictionaries have been\n * built.\n */\nexport const runIntlayerPurgePipeline = (\n options: PurgePluginOptions\n): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n nestedDictionaryReferences,\n compatCallers,\n logConfig,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const logger = getAppLogger(logConfig);\n\n const shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\n\n if (editorEnabled && (purge || minify)) {\n logStepDisabledByEditor(logger, purge, minify);\n }\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 1.5: Keep `nest()` targets resolvable — must run before the rename\n // maps are built so nest targets are excluded from field renaming.\n if (nestedDictionaryReferences) {\n preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);\n }\n\n // Phase 1.6: Warn about the dictionaries the analysis could not follow.\n logUntrackedBindings(logger, pruneContext);\n\n // Phase 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext,\n logger\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false` 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 runIntlayerPurgePipeline(this.opts as PurgePluginOptions);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"babel-plugin-intlayer-purge.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { PluginObject, PluginPass } from '@babel/core';\nimport { transformSync } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { NestedFieldReferences } from '@intlayer/core/dictionaryManipulator';\nimport { formatPath } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport {\n buildNestedRenameMapFromContent,\n getNestedRenameEntryAtPath,\n} from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n preserveNestedDictionaryFields,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin still purges and minifies, but skips field renaming\n * so the `keyPath` reported to the visual editor keeps matching the unmerged\n * dictionaries it edits. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Dictionaries referenced through `nest()`, mapped to the top-level fields\n * those references read (`'all'` when the whole dictionary is nested).\n *\n * Those fields are resolved at runtime by `getNesting` using their original\n * names, so they must survive purging and must never be renamed.\n */\n nestedDictionaryReferences?: Map<string, NestedFieldReferences>;\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Logging configuration used to report what the pipeline purged and\n * minified, so a Babel- or SWC-driven build prints the same summary as the\n * Vite one. Mirrors `log` in `intlayer.config.ts`; omit to fall back to the\n * logger defaults.\n */\n logConfig?: Pick<IntlayerConfig, 'log'>;\n};\n\n/** Application logger used to report the pipeline's progress. */\ntype PurgeLogger = ReturnType<typeof getAppLogger>;\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process:\n * {@link runIntlayerPurgePipeline} registers the context before running so every\n * subsequent transform is a no-op cache hit.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */\nconst filterRecordByUsedFields = (\n record: Record<string, unknown>,\n usedFieldNames: Set<string>\n): Record<string, unknown> => {\n const filteredRecord: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n if (usedFieldNames.has(fieldName)) {\n filteredRecord[fieldName] = fieldValue;\n }\n }\n return filteredRecord;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n prunedTranslation[locale] = isPlainRecord(localeContent)\n ? filterRecordByUsedFields(localeContent, usedFieldNames)\n : localeContent;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext,\n logger: PurgeLogger\n): void => {\n let partiallyMinifiedDictionariesCount = 0;\n\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque values to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousOccurrences = [...opaqueFieldMap.values()].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(nestedRenameMap, occurrence.fieldPath)\n ?.children.size ?? 0) > 0\n );\n\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(`'${occurrence.fieldPath.join('.')}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...occurrence.locations.map(\n (location) => `\\n at ${formatPath(location)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n for (const occurrence of dangerousOccurrences) {\n const renameEntry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\n if (renameEntry) {\n renameEntry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\n/**\n * Names of the top-level content fields of a compiled dictionary, whatever\n * shape its `content` uses. Used to report which fields the purge removed.\n */\nconst readContentFieldNames = (\n content: CompiledDictionaryJson['content']\n): string[] => {\n // Shape A – fields live inside each locale object.\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n return isPlainRecord(firstLocaleValue) ? Object.keys(firstLocaleValue) : [];\n }\n\n // Shape B / dynamic – flat content record.\n if (isPlainRecord(content)) return Object.keys(content);\n\n return [];\n};\n\n/**\n * Running totals of the purge, reported once the whole pipeline is done.\n */\ntype PurgeStats = {\n /** Dictionary key → number of fields removed from it. */\n prunedFieldsCountPerDictionary: Map<string, number>;\n /**\n * Dictionary keys whose \"pruned fields\" line has already been printed. A\n * key spans several files (one static, one per locale), and they all remove\n * the same fields.\n */\n loggedPrunedDictionaryKeys: Set<string>;\n /**\n * Dictionary keys whose \"analysis is incomplete\" warning has already been\n * printed, deduplicated the same way.\n */\n loggedIncompleteAnalysisKeys: Set<string>;\n};\n\n/**\n * Reads a compiled dictionary JSON file, applies the purge and/or minify\n * transformations, and writes it back in-place when something changed.\n *\n * `dictionaryKind` selects the prune strategy and the minified output shape:\n * static dictionaries keep `{ key, content }`, dynamic per-locale\n * dictionaries additionally keep `locale`.\n */\nconst processDictionaryFile = (\n filePath: string,\n dictionaryKind: 'static' | 'dynamic',\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger,\n stats: PurgeStats\n): void => {\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(\n readFileSync(filePath, 'utf-8')\n ) as CompiledDictionaryJson;\n } catch {\n return; // Unreadable or invalid JSON – leave file unchanged.\n }\n\n const { key: dictionaryKey } = parsedDict;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(filePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const pruneDictionaryContent =\n dictionaryKind === 'static'\n ? pruneStaticDictionaryContent\n : pruneDynamicDictionaryContent;\n const originalFieldNames = readContentFieldNames(parsedDict.content);\n const { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(filePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n stats.prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!stats.loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n stats.loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n parsedDict = prunedDictionary;\n modified = true;\n } else if (\n !fieldUsage &&\n pruneContext.hasUnparsableSourceFiles &&\n !stats.loggedIncompleteAnalysisKeys.has(dictionaryKey)\n ) {\n // An unparsable source file might be the one referencing this key, so its\n // usage set cannot be trusted to be empty.\n stats.loggedIncompleteAnalysisKeys.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? dictionaryKind === 'static'\n ? { key: parsedDict.key, content: parsedDict.content }\n : {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean,\n logger: PurgeLogger\n): void => {\n const stats: PurgeStats = {\n prunedFieldsCountPerDictionary: new Map(),\n loggedPrunedDictionaryKeys: new Set(),\n loggedIncompleteAnalysisKeys: new Set(),\n };\n\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processDictionaryFile(\n join(dictionariesDir, entry),\n 'static',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDictionaryFile(\n join(keyDirPath, localeFile),\n 'dynamic',\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger,\n stats\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n\n const totalPrunedFieldsCount = [\n ...stats.prunedFieldsCountPerDictionary.values(),\n ].reduce((total, count) => total + count, 0);\n const totalPrunedDictionariesCount =\n stats.prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n};\n\n// ── Reporting helpers ─────────────────────────────────────────────────────────\n\n/**\n * Explains the one step the visual editor stands down: renaming the content\n * fields would break the `keyPath` the editor resolves its edits with. Purge\n * and the rest of the minification still run.\n */\nconst logFieldRenameDisabledByEditor = (logger: PurgeLogger): void =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor resolves edits by keyPath against the unmerged dictionaries',\n ANSIColors.GREY\n ),\n ]);\n\n/**\n * Reports the dictionaries whose consumption could not be tracked statically,\n * with the source files responsible — those keep their full content.\n */\nconst logUntrackedBindings = (\n logger: PurgeLogger,\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n *\n * Exported so bundler integrations that do not go through Babel can drive the\n * same pipeline. The Next.js integration is the main consumer: its SWC plugin\n * runs as a per-file Wasm transform with no filesystem access, so the analysis\n * and the dictionary rewrite happen here, in Node, and only the resulting\n * field-rename tables (see {@link serializeFieldRenameMap}) are forwarded to\n * the plugin.\n *\n * **This performs file I/O**: it reads every source file listed in\n * `componentFilesList` and overwrites the compiled dictionary JSON files\n * in-place. Call it at most once per build, after the dictionaries have been\n * built.\n */\nexport const runIntlayerPurgePipeline = (\n options: PurgePluginOptions\n): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n nestedDictionaryReferences,\n compatCallers,\n logConfig,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const logger = getAppLogger(logConfig);\n\n const shouldPurge = Boolean(purge);\n const shouldMinify = Boolean(minify);\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` reported to the visual editor would no longer resolve against\n // the unmerged dictionaries it edits. Purging is keyPath-neutral: a purged\n // field is one no source file reads, so it is never rendered either.\n const isFieldRenameEnabled = shouldMinify && !editorEnabled;\n\n if (editorEnabled && minify) {\n logFieldRenameDisabledByEditor(logger);\n }\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 1.5: Keep `nest()` targets resolvable — must run before the rename\n // maps are built so nest targets are excluded from field renaming.\n if (nestedDictionaryReferences) {\n preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);\n }\n\n // Phase 1.6: Warn about the dictionaries the analysis could not follow.\n logUntrackedBindings(logger, pruneContext);\n\n // Phase 2: Build field-rename maps (minify only, editor disabled only).\n if (isFieldRenameEnabled) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext,\n logger\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify,\n logger\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false`.\n * - When `editorEnabled` is `true`, purge and minify still run but field\n * renaming is skipped, so the `keyPath` the visual editor receives keeps\n * matching the unmerged dictionaries it edits.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass) {\n runIntlayerPurgePipeline(this.opts as PurgePluginOptions);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
|
|
@@ -36,8 +36,9 @@ type PurgePluginOptions = {
|
|
|
36
36
|
*/
|
|
37
37
|
optimize: boolean | undefined;
|
|
38
38
|
/**
|
|
39
|
-
* When `true` the plugin
|
|
40
|
-
*
|
|
39
|
+
* When `true` the plugin still purges and minifies, but skips field renaming
|
|
40
|
+
* so the `keyPath` reported to the visual editor keeps matching the unmerged
|
|
41
|
+
* dictionaries it edits. Mirrors `editor.enabled`.
|
|
41
42
|
*/
|
|
42
43
|
editorEnabled: boolean;
|
|
43
44
|
/**
|
|
@@ -162,8 +163,10 @@ declare const runIntlayerPurgePipeline: (options: PurgePluginOptions) => PruneCo
|
|
|
162
163
|
* - Intended for **production builds** only. Dictionary JSON files are
|
|
163
164
|
* overwritten in-place; running `intlayer build` afterwards restores the
|
|
164
165
|
* originals.
|
|
165
|
-
* - The plugin is a no-op when `optimize` is `false
|
|
166
|
-
*
|
|
166
|
+
* - The plugin is a no-op when `optimize` is `false`.
|
|
167
|
+
* - When `editorEnabled` is `true`, purge and minify still run but field
|
|
168
|
+
* renaming is skipped, so the `keyPath` the visual editor receives keeps
|
|
169
|
+
* matching the unmerged dictionaries it edits.
|
|
167
170
|
*/
|
|
168
171
|
declare const intlayerPurgeBabelPlugin: (_babel: {
|
|
169
172
|
types: typeof BabelTypes;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"babel-plugin-intlayer-purge.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"mappings":";;;;;;;;;;;;;KA2CY;;;;;;EAMV;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA
|
|
1
|
+
{"version":3,"file":"babel-plugin-intlayer-purge.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-purge.ts"],"mappings":";;;;;;;;;;;;;KA2CY;;;;;;EAMV;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;;;EASA,6BAA6B,YAAY;;;;;;EAOzC,8BAA8B;;;;;;;;;;;;EAgB9B,gBAAgB;;;;;;;EAQhB,YAAY,KAAK;;;;;;;;;;cAwBN,wBAAqB,oBAAsB;;;;;;;;;;;;;;;;;;cA6rB3C,2BAAwB,SAC1B,uBACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmIU,2BAAwB;EACnC,cAAc;MACZ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intlayer/babel",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0-canary.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A Babel plugin for Intlayer that transforms declaration files and provides internationalization features during the build process according to the Intlayer configuration.",
|
|
6
6
|
"keywords": [
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"@babel/plugin-syntax-jsx": "8.0.1",
|
|
90
90
|
"@babel/traverse": "8.0.4",
|
|
91
91
|
"@babel/types": "8.0.4",
|
|
92
|
-
"@intlayer/config": "9.
|
|
93
|
-
"@intlayer/core": "9.
|
|
94
|
-
"@intlayer/dictionaries-entry": "9.
|
|
95
|
-
"@intlayer/engine": "9.
|
|
96
|
-
"@intlayer/types": "9.
|
|
97
|
-
"@intlayer/unmerged-dictionaries-entry": "9.
|
|
92
|
+
"@intlayer/config": "9.4.0-canary.0",
|
|
93
|
+
"@intlayer/core": "9.4.0-canary.0",
|
|
94
|
+
"@intlayer/dictionaries-entry": "9.4.0-canary.0",
|
|
95
|
+
"@intlayer/engine": "9.4.0-canary.0",
|
|
96
|
+
"@intlayer/types": "9.4.0-canary.0",
|
|
97
|
+
"@intlayer/unmerged-dictionaries-entry": "9.4.0-canary.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@babel/plugin-syntax-jsx": "8.0.1",
|
|
@@ -105,15 +105,15 @@
|
|
|
105
105
|
"@utils/ts-config": "1.0.4",
|
|
106
106
|
"@utils/ts-config-types": "1.0.4",
|
|
107
107
|
"@utils/tsdown-config": "1.0.4",
|
|
108
|
-
"@vitejs/plugin-react": "6.0
|
|
108
|
+
"@vitejs/plugin-react": "6.1.0",
|
|
109
109
|
"rimraf": "6.1.3",
|
|
110
110
|
"tsdown": "0.22.14",
|
|
111
111
|
"typescript": "7.0.2",
|
|
112
|
-
"vitest": "4.1.
|
|
112
|
+
"vitest": "4.1.11"
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
|
-
"@intlayer/svelte-compiler": "9.
|
|
116
|
-
"@intlayer/vue-compiler": "9.
|
|
115
|
+
"@intlayer/svelte-compiler": "9.4.0-canary.0",
|
|
116
|
+
"@intlayer/vue-compiler": "9.4.0-canary.0"
|
|
117
117
|
},
|
|
118
118
|
"peerDependenciesMeta": {
|
|
119
119
|
"@intlayer/svelte-compiler": {
|