@intlayer/babel 8.12.5-canary.0 → 9.0.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.
@@ -36,16 +36,28 @@ const BABEL_PARSER_OPTIONS = {
36
36
  */
37
37
  const INTLAYER_USAGE_REGEX = /\b(use|get)Intlayer\b/;
38
38
  /**
39
- * Fast pre-check: matches files that could contain compat-adapter namespace
40
- * callers (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,
41
- * `useI18n`). These are analysed for field usage (pruning) but never rewritten.
42
- */
43
- const COMPAT_USAGE_REGEX = /\b(useTranslation|useTranslations|getTranslations|getFixedT|useI18n)\b/;
44
- /**
45
- * Fast pre-check for the usage-analysis phase: matches files containing either
46
- * native intlayer calls or compat-adapter namespace callers.
39
+ * Builds a fast pre-check regex that matches files potentially containing
40
+ * native intlayer calls and/or the given compat namespace caller names.
41
+ *
42
+ * The result is used to skip files that definitely contain none of the
43
+ * relevant function names, avoiding unnecessary Babel parsing.
44
+ *
45
+ * Compat caller names are NOT hardcoded here they are provided at runtime by
46
+ * the compat adapter packages that configure the usage-analysis pipeline.
47
+ *
48
+ * @param extraCallerNames - Additional caller function names to match
49
+ * (e.g. `['useTranslation', 'getFixedT']` from `@intlayer/react-i18next`).
50
+ * @returns A `RegExp` matching any of the native or provided caller names.
47
51
  */
48
- const INTLAYER_OR_COMPAT_USAGE_REGEX = /\b(useIntlayer|getIntlayer|useTranslation|useTranslations|getTranslations|getFixedT|useI18n)\b/;
52
+ const buildUsageCheckRegex = (extraCallerNames) => {
53
+ const callerNames = [
54
+ "useIntlayer",
55
+ "getIntlayer",
56
+ ...extraCallerNames ?? []
57
+ ];
58
+ const uniqueNames = [...new Set(callerNames)];
59
+ return new RegExp(`\\b(${uniqueNames.join("|")})\\b`);
60
+ };
49
61
  /**
50
62
  * Matches source files that are valid targets for usage analysis and Babel
51
63
  * transformation. Excludes sourcemap files, declaration files, and other
@@ -80,9 +92,10 @@ const analyzeScriptContent = async (scriptContent, sourceFilePath, pruneContext,
80
92
  * `pruneContext.hasUnparsableSourceFiles`).
81
93
  */
82
94
  const analyzeFieldUsageInFile = async (sourceFilePath, code, pruneContext, compatCallers) => {
95
+ const usageCheckRegex = buildUsageCheckRegex((compatCallers ?? []).map((caller) => caller.callerName));
83
96
  const scriptBlocks = require_extractScriptBlocks.extractScriptBlocks(sourceFilePath, code);
84
97
  for (const block of scriptBlocks) {
85
- if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(block.content)) continue;
98
+ if (!usageCheckRegex.test(block.content)) continue;
86
99
  await analyzeScriptContent(block.content, sourceFilePath, pruneContext, compatCallers);
87
100
  }
88
101
  };
@@ -153,11 +166,10 @@ const optimizeSourceFile = async (code, sourceFilePath, options) => {
153
166
 
154
167
  //#endregion
155
168
  exports.BABEL_PARSER_OPTIONS = BABEL_PARSER_OPTIONS;
156
- exports.COMPAT_USAGE_REGEX = COMPAT_USAGE_REGEX;
157
- exports.INTLAYER_OR_COMPAT_USAGE_REGEX = INTLAYER_OR_COMPAT_USAGE_REGEX;
158
169
  exports.INTLAYER_USAGE_REGEX = INTLAYER_USAGE_REGEX;
159
170
  exports.SOURCE_FILE_REGEX = SOURCE_FILE_REGEX;
160
171
  exports.analyzeFieldUsageInFile = analyzeFieldUsageInFile;
172
+ exports.buildUsageCheckRegex = buildUsageCheckRegex;
161
173
  exports.optimizeSourceFile = optimizeSourceFile;
162
174
  exports.renameFieldsInCode = renameFieldsInCode;
163
175
  exports.renameFieldsInSourceFile = renameFieldsInSourceFile;
@@ -1 +1 @@
1
- {"version":3,"file":"transformers.cjs","names":["makeUsageAnalyzerBabelPlugin","extractScriptBlocks","makeFieldRenameBabelPlugin","injectScriptBlocks","intlayerOptimizeBabelPlugin"],"sources":["../../src/transformers.ts"],"sourcesContent":["import { type TransformOptions, transformAsync } from '@babel/core';\nimport { makeFieldRenameBabelPlugin } from './babel-plugin-intlayer-field-rename';\nimport {\n intlayerOptimizeBabelPlugin,\n type OptimizePluginOptions,\n} from './babel-plugin-intlayer-optimize';\nimport {\n type CompatCallerConfig,\n makeUsageAnalyzerBabelPlugin,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks, injectScriptBlocks } from './extractScriptBlocks';\n\n// ── Shared Babel parser configuration ─────────────────────────────────────────\n\n/**\n * Babel parser options covering the superset of syntaxes used across all\n * supported frameworks (React / Vue / Svelte / Angular / …).\n */\nexport const BABEL_PARSER_OPTIONS: NonNullable<TransformOptions['parserOpts']> =\n {\n sourceType: 'module',\n allowImportExportEverywhere: true,\n plugins: [\n 'typescript',\n 'jsx',\n 'decorators-legacy',\n 'classProperties',\n 'objectRestSpread',\n 'asyncGenerators',\n 'functionBind',\n 'exportDefaultFrom',\n 'exportNamespaceFrom',\n 'dynamicImport',\n 'nullishCoalescingOperator',\n 'optionalChaining',\n ],\n };\n\n/**\n * Fast pre-check: matches files that could contain native intlayer calls\n * (`useIntlayer` / `getIntlayer`). Used by the optimize/transform pass, which\n * only rewrites native calls.\n */\nexport const INTLAYER_USAGE_REGEX = /\\b(use|get)Intlayer\\b/;\n\n/**\n * Fast pre-check: matches files that could contain compat-adapter namespace\n * callers (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,\n * `useI18n`). These are analysed for field usage (pruning) but never rewritten.\n */\nexport const COMPAT_USAGE_REGEX =\n /\\b(useTranslation|useTranslations|getTranslations|getFixedT|useI18n)\\b/;\n\n/**\n * Fast pre-check for the usage-analysis phase: matches files containing either\n * native intlayer calls or compat-adapter namespace callers.\n */\nexport const INTLAYER_OR_COMPAT_USAGE_REGEX =\n /\\b(useIntlayer|getIntlayer|useTranslation|useTranslations|getTranslations|getFixedT|useI18n)\\b/;\n\n/**\n * Matches source files that are valid targets for usage analysis and Babel\n * transformation. Excludes sourcemap files, declaration files, and other\n * non-source extensions.\n */\nexport const SOURCE_FILE_REGEX = /\\.(tsx?|[mc]?jsx?|vue|svelte|astro)$/;\n\n// ── High-level transformer functions ──────────────────────────────────────────\n\n/**\n * Runs the usage-analysis Babel plugin on a single JS/TS code string.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the content.\n */\nconst analyzeScriptContent = async (\n scriptContent: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n await transformAsync(scriptContent, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false, // analysis only – no output needed\n });\n};\n\n/**\n * Runs the usage-analysis Babel plugin on a source file, accumulating\n * field-usage data into `pruneContext`.\n *\n * For Vue / Svelte SFC files, script blocks are extracted before analysis so\n * Babel does not attempt to parse the full SFC syntax (templates, styles, …).\n * For plain JS/TS files, the whole file content is analysed directly.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the file (caller should handle and flag\n * `pruneContext.hasUnparsableSourceFiles`).\n */\nexport const analyzeFieldUsageInFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n // For SFC files (Vue / Svelte): scriptBlocks[0].contentStartOffset > 0\n // means we extracted actual <script> tags from the file.\n // For plain JS/TS: extractScriptBlocks returns the whole file as a single\n // block with offset 0, so we fall through to the same path.\n for (const block of scriptBlocks) {\n if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(block.content)) continue;\n await analyzeScriptContent(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n/**\n * Applies field-renaming to a single JS/TS code string (not an SFC).\n *\n * Returns the renamed code string, or `null` if nothing changed or if\n * Babel failed to parse the input (caller should fall back to original code).\n */\nexport const renameFieldsInCode = async (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n try {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [makeFieldRenameBabelPlugin(pruneContext)],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n });\n return result?.code ?? null;\n } catch {\n return null; // parse failure – caller falls back to original code\n }\n};\n\n/**\n * Applies field-renaming to a source file, correctly handling both plain\n * JS/TS files and SFC files (Vue / Svelte) by operating on each script block\n * individually and injecting the results back into the original source.\n *\n * Returns the renamed code string, or `null` if nothing changed.\n */\nexport const renameFieldsInSourceFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return null;\n if (!INTLAYER_USAGE_REGEX.test(code)) return null;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n const isSFC =\n scriptBlocks.length > 0 &&\n ((scriptBlocks[0]?.contentStartOffset ?? 0) > 0 || scriptBlocks.length > 1);\n\n if (isSFC) {\n // Raw SFC: rename each script block individually and inject back.\n const modifications: Array<{\n block: (typeof scriptBlocks)[number];\n modifiedContent: string;\n }> = [];\n\n for (const block of scriptBlocks) {\n if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;\n\n const renamedCode = await renameFieldsInCode(\n block.content,\n sourceFilePath,\n pruneContext\n );\n if (renamedCode && renamedCode !== block.content) {\n modifications.push({ block, modifiedContent: renamedCode });\n }\n }\n\n if (modifications.length === 0) return null;\n return injectScriptBlocks(code, modifications);\n }\n\n // Plain JS/TS or compiled SFC (no block delimiters) – rename the whole file.\n return renameFieldsInCode(code, sourceFilePath, pruneContext);\n};\n\n/**\n * Runs the intlayer optimize Babel plugin on a source file, transforming\n * `useIntlayer('key')` / `getIntlayer('key')` calls into `useDictionary(_hash)`\n * / `getDictionary(_hash)` and injecting the corresponding dictionary imports.\n *\n * Returns `{ code, map }` on success, or `null` if the transformation produced\n * no output.\n */\nexport const optimizeSourceFile = async (\n code: string,\n sourceFilePath: string,\n options: OptimizePluginOptions\n): Promise<{\n code: string;\n map: string | object | null | undefined;\n} | null> => {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [[intlayerOptimizeBabelPlugin, options]],\n parserOpts: BABEL_PARSER_OPTIONS,\n });\n\n if (!result?.code) return null;\n\n return { code: result.code, map: result.map };\n};\n"],"mappings":";;;;;;;;;;;;;AAmBA,MAAa,uBACX;CACE,YAAY;CACZ,6BAA6B;CAC7B,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACF;;;;;;AAOH,MAAa,uBAAuB;;;;;;AAOpC,MAAa,qBACX;;;;;AAMF,MAAa,iCACX;;;;;;AAOF,MAAa,oBAAoB;;;;;;;AAUjC,MAAM,uBAAuB,OAC3B,eACA,gBACA,cACA,kBACkB;AAClB,uCAAqB,eAAe;EAClC,UAAU;EACV,SAAS,CAACA,0EAA6B,cAAc,EAAE,eAAe,CAAC,CAAC;EACxE,YAAY;EACZ,KAAK;EACL,MAAM;EACP,CAAC;;;;;;;;;;;;;;AAeJ,MAAa,0BAA0B,OACrC,gBACA,MACA,cACA,kBACkB;CAClB,MAAM,eAAeC,gDAAoB,gBAAgB,KAAK;AAM9D,MAAK,MAAM,SAAS,cAAc;AAChC,MAAI,CAAC,+BAA+B,KAAK,MAAM,QAAQ,CAAE;AACzD,QAAM,qBACJ,MAAM,SACN,gBACA,cACA,cACD;;;;;;;;;AAUL,MAAa,qBAAqB,OAChC,MACA,gBACA,iBAC2B;AAC3B,KAAI;AAOF,UAAO,sCAN6B,MAAM;GACxC,UAAU;GACV,SAAS,CAACC,sEAA2B,aAAa,CAAC;GACnD,YAAY;GACZ,KAAK;GACN,CAAC,GACa,QAAQ;SACjB;AACN,SAAO;;;;;;;;;;AAWX,MAAa,2BAA2B,OACtC,gBACA,MACA,iBAC2B;AAC3B,KAAI,aAAa,8BAA8B,SAAS,EAAG,QAAO;AAClE,KAAI,CAAC,qBAAqB,KAAK,KAAK,CAAE,QAAO;CAE7C,MAAM,eAAeD,gDAAoB,gBAAgB,KAAK;AAM9D,KAHE,aAAa,SAAS,OACpB,aAAa,IAAI,sBAAsB,KAAK,KAAK,aAAa,SAAS,IAEhE;EAET,MAAM,gBAGD,EAAE;AAEP,OAAK,MAAM,SAAS,cAAc;AAChC,OAAI,CAAC,qBAAqB,KAAK,MAAM,QAAQ,CAAE;GAE/C,MAAM,cAAc,MAAM,mBACxB,MAAM,SACN,gBACA,aACD;AACD,OAAI,eAAe,gBAAgB,MAAM,QACvC,eAAc,KAAK;IAAE;IAAO,iBAAiB;IAAa,CAAC;;AAI/D,MAAI,cAAc,WAAW,EAAG,QAAO;AACvC,SAAOE,+CAAmB,MAAM,cAAc;;AAIhD,QAAO,mBAAmB,MAAM,gBAAgB,aAAa;;;;;;;;;;AAW/D,MAAa,qBAAqB,OAChC,MACA,gBACA,YAIW;CACX,MAAM,SAAS,sCAAqB,MAAM;EACxC,UAAU;EACV,SAAS,CAAC,CAACC,oEAA6B,QAAQ,CAAC;EACjD,YAAY;EACb,CAAC;AAEF,KAAI,CAAC,QAAQ,KAAM,QAAO;AAE1B,QAAO;EAAE,MAAM,OAAO;EAAM,KAAK,OAAO;EAAK"}
1
+ {"version":3,"file":"transformers.cjs","names":["makeUsageAnalyzerBabelPlugin","extractScriptBlocks","makeFieldRenameBabelPlugin","injectScriptBlocks","intlayerOptimizeBabelPlugin"],"sources":["../../src/transformers.ts"],"sourcesContent":["import { type TransformOptions, transformAsync } from '@babel/core';\nimport { makeFieldRenameBabelPlugin } from './babel-plugin-intlayer-field-rename';\nimport {\n intlayerOptimizeBabelPlugin,\n type OptimizePluginOptions,\n} from './babel-plugin-intlayer-optimize';\nimport {\n type CompatCallerConfig,\n makeUsageAnalyzerBabelPlugin,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks, injectScriptBlocks } from './extractScriptBlocks';\n\n// ── Shared Babel parser configuration ─────────────────────────────────────────\n\n/**\n * Babel parser options covering the superset of syntaxes used across all\n * supported frameworks (React / Vue / Svelte / Angular / …).\n */\nexport const BABEL_PARSER_OPTIONS: NonNullable<TransformOptions['parserOpts']> =\n {\n sourceType: 'module',\n allowImportExportEverywhere: true,\n plugins: [\n 'typescript',\n 'jsx',\n 'decorators-legacy',\n 'classProperties',\n 'objectRestSpread',\n 'asyncGenerators',\n 'functionBind',\n 'exportDefaultFrom',\n 'exportNamespaceFrom',\n 'dynamicImport',\n 'nullishCoalescingOperator',\n 'optionalChaining',\n ],\n };\n\n/**\n * Fast pre-check: matches files that could contain native intlayer calls\n * (`useIntlayer` / `getIntlayer`). Used by the optimize/transform pass, which\n * only rewrites native calls.\n */\nexport const INTLAYER_USAGE_REGEX = /\\b(use|get)Intlayer\\b/;\n\n/**\n * Builds a fast pre-check regex that matches files potentially containing\n * native intlayer calls and/or the given compat namespace caller names.\n *\n * The result is used to skip files that definitely contain none of the\n * relevant function names, avoiding unnecessary Babel parsing.\n *\n * Compat caller names are NOT hardcoded here — they are provided at runtime by\n * the compat adapter packages that configure the usage-analysis pipeline.\n *\n * @param extraCallerNames - Additional caller function names to match\n * (e.g. `['useTranslation', 'getFixedT']` from `@intlayer/react-i18next`).\n * @returns A `RegExp` matching any of the native or provided caller names.\n */\nexport const buildUsageCheckRegex = (extraCallerNames?: string[]): RegExp => {\n const callerNames = [\n 'useIntlayer',\n 'getIntlayer',\n ...(extraCallerNames ?? []),\n ];\n const uniqueNames = [...new Set(callerNames)];\n return new RegExp(`\\\\b(${uniqueNames.join('|')})\\\\b`);\n};\n\n/**\n * Matches source files that are valid targets for usage analysis and Babel\n * transformation. Excludes sourcemap files, declaration files, and other\n * non-source extensions.\n */\nexport const SOURCE_FILE_REGEX = /\\.(tsx?|[mc]?jsx?|vue|svelte|astro)$/;\n\n// ── High-level transformer functions ──────────────────────────────────────────\n\n/**\n * Runs the usage-analysis Babel plugin on a single JS/TS code string.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the content.\n */\nconst analyzeScriptContent = async (\n scriptContent: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n await transformAsync(scriptContent, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false, // analysis only – no output needed\n });\n};\n\n/**\n * Runs the usage-analysis Babel plugin on a source file, accumulating\n * field-usage data into `pruneContext`.\n *\n * For Vue / Svelte SFC files, script blocks are extracted before analysis so\n * Babel does not attempt to parse the full SFC syntax (templates, styles, …).\n * For plain JS/TS files, the whole file content is analysed directly.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the file (caller should handle and flag\n * `pruneContext.hasUnparsableSourceFiles`).\n */\nexport const analyzeFieldUsageInFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): Promise<void> => {\n const extraCallerNames = (compatCallers ?? []).map(\n (caller) => caller.callerName\n );\n const usageCheckRegex = buildUsageCheckRegex(extraCallerNames);\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n // For SFC files (Vue / Svelte): scriptBlocks[0].contentStartOffset > 0\n // means we extracted actual <script> tags from the file.\n // For plain JS/TS: extractScriptBlocks returns the whole file as a single\n // block with offset 0, so we fall through to the same path.\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n await analyzeScriptContent(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n/**\n * Applies field-renaming to a single JS/TS code string (not an SFC).\n *\n * Returns the renamed code string, or `null` if nothing changed or if\n * Babel failed to parse the input (caller should fall back to original code).\n */\nexport const renameFieldsInCode = async (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n try {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [makeFieldRenameBabelPlugin(pruneContext)],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n });\n return result?.code ?? null;\n } catch {\n return null; // parse failure – caller falls back to original code\n }\n};\n\n/**\n * Applies field-renaming to a source file, correctly handling both plain\n * JS/TS files and SFC files (Vue / Svelte) by operating on each script block\n * individually and injecting the results back into the original source.\n *\n * Returns the renamed code string, or `null` if nothing changed.\n */\nexport const renameFieldsInSourceFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return null;\n if (!INTLAYER_USAGE_REGEX.test(code)) return null;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n const isSFC =\n scriptBlocks.length > 0 &&\n ((scriptBlocks[0]?.contentStartOffset ?? 0) > 0 || scriptBlocks.length > 1);\n\n if (isSFC) {\n // Raw SFC: rename each script block individually and inject back.\n const modifications: Array<{\n block: (typeof scriptBlocks)[number];\n modifiedContent: string;\n }> = [];\n\n for (const block of scriptBlocks) {\n if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;\n\n const renamedCode = await renameFieldsInCode(\n block.content,\n sourceFilePath,\n pruneContext\n );\n if (renamedCode && renamedCode !== block.content) {\n modifications.push({ block, modifiedContent: renamedCode });\n }\n }\n\n if (modifications.length === 0) return null;\n return injectScriptBlocks(code, modifications);\n }\n\n // Plain JS/TS or compiled SFC (no block delimiters) – rename the whole file.\n return renameFieldsInCode(code, sourceFilePath, pruneContext);\n};\n\n/**\n * Runs the intlayer optimize Babel plugin on a source file, transforming\n * `useIntlayer('key')` / `getIntlayer('key')` calls into `useDictionary(_hash)`\n * / `getDictionary(_hash)` and injecting the corresponding dictionary imports.\n *\n * Returns `{ code, map }` on success, or `null` if the transformation produced\n * no output.\n */\nexport const optimizeSourceFile = async (\n code: string,\n sourceFilePath: string,\n options: OptimizePluginOptions\n): Promise<{\n code: string;\n map: string | object | null | undefined;\n} | null> => {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [[intlayerOptimizeBabelPlugin, options]],\n parserOpts: BABEL_PARSER_OPTIONS,\n });\n\n if (!result?.code) return null;\n\n return { code: result.code, map: result.map };\n};\n"],"mappings":";;;;;;;;;;;;;AAmBA,MAAa,uBACX;CACE,YAAY;CACZ,6BAA6B;CAC7B,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACF;;;;;;AAOH,MAAa,uBAAuB;;;;;;;;;;;;;;;AAgBpC,MAAa,wBAAwB,qBAAwC;CAC3E,MAAM,cAAc;EAClB;EACA;EACA,GAAI,oBAAoB,EAAE;EAC3B;CACD,MAAM,cAAc,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC;AAC7C,QAAO,IAAI,OAAO,OAAO,YAAY,KAAK,IAAI,CAAC,MAAM;;;;;;;AAQvD,MAAa,oBAAoB;;;;;;;AAUjC,MAAM,uBAAuB,OAC3B,eACA,gBACA,cACA,kBACkB;AAClB,uCAAqB,eAAe;EAClC,UAAU;EACV,SAAS,CAACA,0EAA6B,cAAc,EAAE,eAAe,CAAC,CAAC;EACxE,YAAY;EACZ,KAAK;EACL,MAAM;EACP,CAAC;;;;;;;;;;;;;;AAeJ,MAAa,0BAA0B,OACrC,gBACA,MACA,cACA,kBACkB;CAIlB,MAAM,kBAAkB,sBAHE,iBAAiB,EAAE,EAAE,KAC5C,WAAW,OAAO,WAEwC,CAAC;CAC9D,MAAM,eAAeC,gDAAoB,gBAAgB,KAAK;AAM9D,MAAK,MAAM,SAAS,cAAc;AAChC,MAAI,CAAC,gBAAgB,KAAK,MAAM,QAAQ,CAAE;AAC1C,QAAM,qBACJ,MAAM,SACN,gBACA,cACA,cACD;;;;;;;;;AAUL,MAAa,qBAAqB,OAChC,MACA,gBACA,iBAC2B;AAC3B,KAAI;AAOF,UAAO,sCAN6B,MAAM;GACxC,UAAU;GACV,SAAS,CAACC,sEAA2B,aAAa,CAAC;GACnD,YAAY;GACZ,KAAK;GACN,CAAC,GACa,QAAQ;SACjB;AACN,SAAO;;;;;;;;;;AAWX,MAAa,2BAA2B,OACtC,gBACA,MACA,iBAC2B;AAC3B,KAAI,aAAa,8BAA8B,SAAS,EAAG,QAAO;AAClE,KAAI,CAAC,qBAAqB,KAAK,KAAK,CAAE,QAAO;CAE7C,MAAM,eAAeD,gDAAoB,gBAAgB,KAAK;AAM9D,KAHE,aAAa,SAAS,OACpB,aAAa,IAAI,sBAAsB,KAAK,KAAK,aAAa,SAAS,IAEhE;EAET,MAAM,gBAGD,EAAE;AAEP,OAAK,MAAM,SAAS,cAAc;AAChC,OAAI,CAAC,qBAAqB,KAAK,MAAM,QAAQ,CAAE;GAE/C,MAAM,cAAc,MAAM,mBACxB,MAAM,SACN,gBACA,aACD;AACD,OAAI,eAAe,gBAAgB,MAAM,QACvC,eAAc,KAAK;IAAE;IAAO,iBAAiB;IAAa,CAAC;;AAI/D,MAAI,cAAc,WAAW,EAAG,QAAO;AACvC,SAAOE,+CAAmB,MAAM,cAAc;;AAIhD,QAAO,mBAAmB,MAAM,gBAAgB,aAAa;;;;;;;;;;AAW/D,MAAa,qBAAqB,OAChC,MACA,gBACA,YAIW;CACX,MAAM,SAAS,sCAAqB,MAAM;EACxC,UAAU;EACV,SAAS,CAAC,CAACC,oEAA6B,QAAQ,CAAC;EACjD,YAAY;EACb,CAAC;AAEF,KAAI,CAAC,QAAQ,KAAM,QAAO;AAE1B,QAAO;EAAE,MAAM,OAAO;EAAM,KAAK,OAAO;EAAK"}
@@ -1,7 +1,7 @@
1
1
  import { createPruneContext, makeUsageAnalyzerBabelPlugin } from "./babel-plugin-intlayer-usage-analyzer.mjs";
2
2
  import { extractScriptBlocks } from "./extractScriptBlocks.mjs";
3
3
  import { buildNestedRenameMapFromContent } from "./babel-plugin-intlayer-field-rename.mjs";
4
- import { BABEL_PARSER_OPTIONS, INTLAYER_OR_COMPAT_USAGE_REGEX, SOURCE_FILE_REGEX } from "./transformers.mjs";
4
+ import { BABEL_PARSER_OPTIONS, SOURCE_FILE_REGEX, buildUsageCheckRegex } from "./transformers.mjs";
5
5
  import { join } from "node:path";
6
6
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
7
7
  import { transformSync } from "@babel/core";
@@ -137,11 +137,11 @@ const applyFieldRenameToDict = (dict, renameMap) => {
137
137
  * Runs the usage-analyser Babel plugin synchronously on a single code block,
138
138
  * accumulating results into `pruneContext`.
139
139
  */
140
- const analyzeCodeBlockSync = (code, sourceFilePath, pruneContext) => {
140
+ const analyzeCodeBlockSync = (code, sourceFilePath, pruneContext, compatCallers) => {
141
141
  try {
142
142
  transformSync(code, {
143
143
  filename: sourceFilePath,
144
- plugins: [makeUsageAnalyzerBabelPlugin(pruneContext)],
144
+ plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],
145
145
  parserOpts: BABEL_PARSER_OPTIONS,
146
146
  ast: false,
147
147
  code: false
@@ -154,18 +154,19 @@ const analyzeCodeBlockSync = (code, sourceFilePath, pruneContext) => {
154
154
  * Reads a source file from disk and runs the usage-analyser synchronously.
155
155
  * SFC files (Vue / Svelte) are handled by extracting script blocks first.
156
156
  */
157
- const analyzeSourceFileSync = (sourceFilePath, pruneContext) => {
157
+ const analyzeSourceFileSync = (sourceFilePath, pruneContext, compatCallers) => {
158
158
  let code;
159
159
  try {
160
160
  code = readFileSync(sourceFilePath, "utf-8");
161
161
  } catch {
162
162
  return;
163
163
  }
164
- if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(code)) return;
164
+ const usageCheckRegex = buildUsageCheckRegex((compatCallers ?? []).map((caller) => caller.callerName));
165
+ if (!usageCheckRegex.test(code)) return;
165
166
  const scriptBlocks = extractScriptBlocks(sourceFilePath, code);
166
167
  for (const block of scriptBlocks) {
167
- if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(block.content)) continue;
168
- analyzeCodeBlockSync(block.content, sourceFilePath, pruneContext);
168
+ if (!usageCheckRegex.test(block.content)) continue;
169
+ analyzeCodeBlockSync(block.content, sourceFilePath, pruneContext, compatCallers);
169
170
  }
170
171
  };
171
172
  /**
@@ -320,7 +321,7 @@ const processAllDictionaryFiles = (dictionariesDir, dynamicDictionariesDir, prun
320
321
  * unique `baseDir`.
321
322
  */
322
323
  const runPurgePipeline = (options) => {
323
- const { baseDir, purge, minify, optimize, editorEnabled, dictionariesDir, dynamicDictionariesDir, componentFilesList, dictionaryKeyToImportModeMap } = options;
324
+ const { baseDir, purge, minify, optimize, editorEnabled, dictionariesDir, dynamicDictionariesDir, componentFilesList, dictionaryKeyToImportModeMap, compatCallers } = options;
324
325
  const cachedContext = _pruneContextCache.get(baseDir);
325
326
  if (cachedContext) return cachedContext;
326
327
  const pruneContext = createPruneContext();
@@ -330,7 +331,7 @@ const runPurgePipeline = (options) => {
330
331
  if (!shouldPurge && !shouldMinify || optimize === false) return pruneContext;
331
332
  for (const sourceFilePath of componentFilesList) {
332
333
  if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;
333
- analyzeSourceFileSync(sourceFilePath, pruneContext);
334
+ analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);
334
335
  }
335
336
  if (shouldMinify) buildRenameMapsSynchronously(dictionariesDir, dynamicDictionariesDir, dictionaryKeyToImportModeMap, pruneContext);
336
337
  processAllDictionaryFiles(dictionariesDir, dynamicDictionariesDir, pruneContext, shouldPurge, shouldMinify);
@@ -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 { PluginObj, PluginPass } from '@babel/core';\nimport { transformSync } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport { buildNestedRenameMapFromContent } from './babel-plugin-intlayer-field-rename';\nimport {\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n INTLAYER_OR_COMPAT_USAGE_REGEX,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin skips all processing to preserve full dictionary\n * content for the visual editor. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n};\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Tracks base directories whose full analysis + dictionary-write cycle has\n * already completed, to avoid repeating work across file transforms.\n */\nconst _completedBaseDirs = new Set<string>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n if (!isPlainRecord(localeContent)) {\n prunedTranslation[locale] = localeContent;\n continue;\n }\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslation[locale] = prunedLocaleFields;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext)],\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): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(block.content)) continue;\n analyzeCodeBlockSync(block.content, sourceFilePath, pruneContext);\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque fields to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n for (const [fieldName] of dangerousEntries) {\n const entry = nestedRenameMap.get(fieldName);\n if (entry) {\n entry.children = new Map();\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\nconst processStaticDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const { prunedDictionary, wasRecognised } = pruneStaticDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? { key: parsedDict.key, content: parsedDict.content }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processDynamicDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const { prunedDictionary, wasRecognised } = pruneDynamicDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processStaticDictionaryFile(\n join(dictionariesDir, entry),\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDynamicDictionaryFile(\n join(keyDirPath, localeFile),\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n */\nconst runPurgePipeline = (options: PurgePluginOptions): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext);\n }\n\n // Phase 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false` or `editorEnabled` is\n * `true`.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObj => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass & { opts: PurgePluginOptions }) {\n const { baseDir } = this.opts;\n\n if (_completedBaseDirs.has(baseDir)) return;\n _completedBaseDirs.add(baseDir);\n\n runPurgePipeline(this.opts);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"babel-plugin-intlayer-purge.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 { PluginObj, PluginPass } from '@babel/core';\nimport { transformSync } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport { buildNestedRenameMapFromContent } from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n buildUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin skips all processing to preserve full dictionary\n * content for the visual editor. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n};\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Tracks base directories whose full analysis + dictionary-write cycle has\n * already completed, to avoid repeating work across file transforms.\n */\nconst _completedBaseDirs = new Set<string>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n if (!isPlainRecord(localeContent)) {\n prunedTranslation[locale] = localeContent;\n continue;\n }\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslation[locale] = prunedLocaleFields;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const extraCallerNames = (compatCallers ?? []).map(\n (caller) => caller.callerName\n );\n const usageCheckRegex = buildUsageCheckRegex(extraCallerNames);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque fields to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n for (const [fieldName] of dangerousEntries) {\n const entry = nestedRenameMap.get(fieldName);\n if (entry) {\n entry.children = new Map();\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\nconst processStaticDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const { prunedDictionary, wasRecognised } = pruneStaticDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? { key: parsedDict.key, content: parsedDict.content }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processDynamicDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const { prunedDictionary, wasRecognised } = pruneDynamicDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processStaticDictionaryFile(\n join(dictionariesDir, entry),\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDynamicDictionaryFile(\n join(keyDirPath, localeFile),\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n */\nconst runPurgePipeline = (options: PurgePluginOptions): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n compatCallers,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false` or `editorEnabled` is\n * `true`.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObj => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass & { opts: PurgePluginOptions }) {\n const { baseDir } = this.opts;\n\n if (_completedBaseDirs.has(baseDir)) return;\n _completedBaseDirs.add(baseDir);\n\n runPurgePipeline(this.opts);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
@@ -12,77 +12,16 @@ const createPruneContext = () => ({
12
12
  /** Canonical intlayer caller names that trigger usage analysis. */
13
13
  const INTLAYER_CALLER_NAMES = ["useIntlayer", "getIntlayer"];
14
14
  /**
15
- * Default registry of compat namespace callers, covering every first-party
16
- * `@intlayer/*` adapter package and its underlying i18n library.
15
+ * Default registry of compat namespace callers.
16
+ *
17
+ * Intentionally empty — compat-specific caller configurations belong in their
18
+ * respective adapter packages (e.g. `@intlayer/react-i18next/plugin`,
19
+ * `@intlayer/vue-i18n/plugin`) and are injected into `makeUsageAnalyzerBabelPlugin`
20
+ * via the `compatCallers` option. Centralising them here would couple the
21
+ * core `@intlayer/babel` package to every compat adapter, which violates the
22
+ * design principle that compat logic lives in compat packages.
17
23
  */
18
- const DEFAULT_COMPAT_CALLERS = [
19
- {
20
- callerName: "useTranslation",
21
- importSources: [
22
- "react-i18next",
23
- "@intlayer/react-i18next",
24
- "next-i18next",
25
- "@intlayer/next-i18next"
26
- ],
27
- namespace: {
28
- from: "argument",
29
- index: 0
30
- },
31
- keyPrefix: {
32
- from: "option",
33
- argumentIndex: 1,
34
- property: "keyPrefix"
35
- },
36
- translationFunction: "destructured-t"
37
- },
38
- {
39
- callerName: "useTranslations",
40
- importSources: ["next-intl", "@intlayer/next-intl"],
41
- namespace: {
42
- from: "argument",
43
- index: 0
44
- },
45
- translationFunction: "return-value"
46
- },
47
- {
48
- callerName: "getTranslations",
49
- importSources: [
50
- "next-intl/server",
51
- "@intlayer/next-intl/server",
52
- "next-intl",
53
- "@intlayer/next-intl"
54
- ],
55
- namespace: {
56
- from: "argument",
57
- index: 0
58
- },
59
- translationFunction: "return-value"
60
- },
61
- {
62
- callerName: "getFixedT",
63
- importSources: ["i18next", "@intlayer/i18next"],
64
- matchAsMethod: true,
65
- namespace: {
66
- from: "argument",
67
- index: 1
68
- },
69
- keyPrefix: {
70
- from: "argument",
71
- index: 2
72
- },
73
- translationFunction: "return-value"
74
- },
75
- {
76
- callerName: "useI18n",
77
- importSources: ["vue-i18n", "@intlayer/vue-i18n"],
78
- namespace: {
79
- from: "option",
80
- argumentIndex: 0,
81
- property: "namespace"
82
- },
83
- translationFunction: "destructured-t"
84
- }
85
- ];
24
+ const DEFAULT_COMPAT_CALLERS = [];
86
25
  /** Default namespace used by compat callers when no namespace argument is given. */
87
26
  const DEFAULT_COMPAT_NAMESPACE = "translation";
88
27
  /**
@@ -212,7 +151,10 @@ const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath
212
151
  hasUntrackedReferenceAccess = true;
213
152
  break;
214
153
  }
215
- } else if (babelTypes.isArrayExpression(referenceParentNode)) {} else if ((babelTypes.isCallExpression(referenceParentNode) || babelTypes.isOptionalCallExpression(referenceParentNode)) && referenceParentNode.callee === variableReferencePath.node) {
154
+ } else if (babelTypes.isArrayExpression(referenceParentNode)) {
155
+ hasUntrackedReferenceAccess = true;
156
+ break;
157
+ } else if ((babelTypes.isCallExpression(referenceParentNode) || babelTypes.isOptionalCallExpression(referenceParentNode)) && referenceParentNode.callee === variableReferencePath.node) {
216
158
  const callExprPath = variableReferencePath.parentPath;
217
159
  const callParent = callExprPath?.parent;
218
160
  if (callParent && (babelTypes.isMemberExpression(callParent) || babelTypes.isOptionalMemberExpression(callParent)) && callParent.object === callExprPath?.node) {