@intlayer/babel 9.0.0-canary.15 → 9.0.0-canary.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/cjs/babel-plugin-intlayer-field-rename.cjs +156 -96
  2. package/dist/cjs/babel-plugin-intlayer-field-rename.cjs.map +1 -1
  3. package/dist/cjs/babel-plugin-intlayer-minify.cjs +11 -28
  4. package/dist/cjs/babel-plugin-intlayer-minify.cjs.map +1 -1
  5. package/dist/cjs/babel-plugin-intlayer-purge.cjs +37 -94
  6. package/dist/cjs/babel-plugin-intlayer-purge.cjs.map +1 -1
  7. package/dist/cjs/babel-plugin-intlayer-usage-analyzer.cjs +98 -30
  8. package/dist/cjs/babel-plugin-intlayer-usage-analyzer.cjs.map +1 -1
  9. package/dist/cjs/index.cjs +2 -0
  10. package/dist/esm/babel-plugin-intlayer-field-rename.mjs +155 -97
  11. package/dist/esm/babel-plugin-intlayer-field-rename.mjs.map +1 -1
  12. package/dist/esm/babel-plugin-intlayer-minify.mjs +12 -29
  13. package/dist/esm/babel-plugin-intlayer-minify.mjs.map +1 -1
  14. package/dist/esm/babel-plugin-intlayer-purge.mjs +38 -95
  15. package/dist/esm/babel-plugin-intlayer-purge.mjs.map +1 -1
  16. package/dist/esm/babel-plugin-intlayer-usage-analyzer.mjs +98 -30
  17. package/dist/esm/babel-plugin-intlayer-usage-analyzer.mjs.map +1 -1
  18. package/dist/esm/index.mjs +2 -2
  19. package/dist/types/babel-plugin-intlayer-field-rename.d.ts +27 -6
  20. package/dist/types/babel-plugin-intlayer-field-rename.d.ts.map +1 -1
  21. package/dist/types/babel-plugin-intlayer-minify.d.ts.map +1 -1
  22. package/dist/types/babel-plugin-intlayer-purge.d.ts.map +1 -1
  23. package/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts +25 -7
  24. package/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts.map +1 -1
  25. package/dist/types/index.d.ts +3 -3
  26. package/package.json +10 -10
@@ -1,6 +1,6 @@
1
1
  import { createPruneContext, makeUsageAnalyzerBabelPlugin } from "./babel-plugin-intlayer-usage-analyzer.mjs";
2
2
  import { extractScriptBlocks } from "./extractScriptBlocks.mjs";
3
- import { buildNestedRenameMapFromContent } from "./babel-plugin-intlayer-field-rename.mjs";
3
+ import { buildNestedRenameMapFromContent, getNestedRenameEntryAtPath } from "./babel-plugin-intlayer-field-rename.mjs";
4
4
  import { BABEL_PARSER_OPTIONS, SOURCE_FILE_REGEX, getUsageCheckRegex } from "./transformers.mjs";
5
5
  import { join } from "node:path";
6
6
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
@@ -9,15 +9,12 @@ import { transformSync } from "@babel/core";
9
9
  //#region src/babel-plugin-intlayer-purge.ts
10
10
  /**
11
11
  * Cache of built {@link PruneContext} objects, keyed by the project's
12
- * `baseDir`. Each context is built exactly once per Node.js process.
12
+ * `baseDir`. Each context is built exactly once per Node.js process:
13
+ * {@link runPurgePipeline} registers the context before running so every
14
+ * subsequent transform is a no-op cache hit.
13
15
  */
14
16
  const _pruneContextCache = /* @__PURE__ */ new Map();
15
17
  /**
16
- * Tracks base directories whose full analysis + dictionary-write cycle has
17
- * already completed, to avoid repeating work across file transforms.
18
- */
19
- const _completedBaseDirs = /* @__PURE__ */ new Set();
20
- /**
21
18
  * Returns the shared {@link PruneContext} for the given base directory, or
22
19
  * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for
23
20
  * that directory.
@@ -28,6 +25,12 @@ const _completedBaseDirs = /* @__PURE__ */ new Set();
28
25
  const getSharedPruneContext = (baseDir) => _pruneContextCache.get(baseDir) ?? null;
29
26
  const isTranslationNode = (value) => typeof value === "object" && value !== null && value.nodeType === "translation" && typeof value.translation === "object";
30
27
  const isPlainRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
28
+ /** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */
29
+ const filterRecordByUsedFields = (record, usedFieldNames) => {
30
+ const filteredRecord = {};
31
+ for (const [fieldName, fieldValue] of Object.entries(record)) if (usedFieldNames.has(fieldName)) filteredRecord[fieldName] = fieldValue;
32
+ return filteredRecord;
33
+ };
31
34
  /**
32
35
  * Removes unused fields from a **static** dictionary (all locales in one
33
36
  * file). Supports shape A (translation node at the root) and shape B (flat
@@ -39,15 +42,7 @@ const pruneStaticDictionaryContent = (dictionary, usedFieldNames) => {
39
42
  const firstLocaleValue = Object.values(content.translation)[0];
40
43
  if (isPlainRecord(firstLocaleValue)) {
41
44
  const prunedTranslation = {};
42
- for (const [locale, localeContent] of Object.entries(content.translation)) {
43
- if (!isPlainRecord(localeContent)) {
44
- prunedTranslation[locale] = localeContent;
45
- continue;
46
- }
47
- const prunedLocaleFields = {};
48
- for (const [fieldName, fieldValue] of Object.entries(localeContent)) if (usedFieldNames.has(fieldName)) prunedLocaleFields[fieldName] = fieldValue;
49
- prunedTranslation[locale] = prunedLocaleFields;
50
- }
45
+ for (const [locale, localeContent] of Object.entries(content.translation)) prunedTranslation[locale] = isPlainRecord(localeContent) ? filterRecordByUsedFields(localeContent, usedFieldNames) : localeContent;
51
46
  return {
52
47
  prunedDictionary: {
53
48
  ...dictionary,
@@ -60,17 +55,13 @@ const pruneStaticDictionaryContent = (dictionary, usedFieldNames) => {
60
55
  };
61
56
  }
62
57
  }
63
- if (isPlainRecord(content) && !isTranslationNode(content)) {
64
- const prunedContent = {};
65
- for (const [fieldName, fieldValue] of Object.entries(content)) if (usedFieldNames.has(fieldName)) prunedContent[fieldName] = fieldValue;
66
- return {
67
- prunedDictionary: {
68
- ...dictionary,
69
- content: prunedContent
70
- },
71
- wasRecognised: true
72
- };
73
- }
58
+ if (isPlainRecord(content) && !isTranslationNode(content)) return {
59
+ prunedDictionary: {
60
+ ...dictionary,
61
+ content: filterRecordByUsedFields(content, usedFieldNames)
62
+ },
63
+ wasRecognised: true
64
+ };
74
65
  return {
75
66
  prunedDictionary: dictionary,
76
67
  wasRecognised: false
@@ -86,12 +77,10 @@ const pruneDynamicDictionaryContent = (dictionary, usedFieldNames) => {
86
77
  prunedDictionary: dictionary,
87
78
  wasRecognised: false
88
79
  };
89
- const prunedContent = {};
90
- for (const [fieldName, fieldValue] of Object.entries(content)) if (usedFieldNames.has(fieldName)) prunedContent[fieldName] = fieldValue;
91
80
  return {
92
81
  prunedDictionary: {
93
82
  ...dictionary,
94
- content: prunedContent
83
+ content: filterRecordByUsedFields(content, usedFieldNames)
95
84
  },
96
85
  wasRecognised: true
97
86
  };
@@ -198,27 +187,26 @@ const buildRenameMapsSynchronously = (dictionariesDir, dynamicDictionariesDir, d
198
187
  }
199
188
  if (!dictionaryContent) continue;
200
189
  const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);
201
- const opaqueFieldMap = pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);
202
- if (opaqueFieldMap) {
203
- const dangerousEntries = [...opaqueFieldMap.entries()].filter(([fieldName]) => (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0);
204
- for (const [fieldName] of dangerousEntries) {
205
- const entry = nestedRenameMap.get(fieldName);
206
- if (entry) entry.children = /* @__PURE__ */ new Map();
207
- }
190
+ const opaqueFieldMap = pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);
191
+ if (opaqueFieldMap) for (const occurrence of opaqueFieldMap.values()) {
192
+ const renameEntry = getNestedRenameEntryAtPath(nestedRenameMap, occurrence.fieldPath);
193
+ if (renameEntry && renameEntry.children.size > 0) renameEntry.children = /* @__PURE__ */ new Map();
208
194
  }
209
195
  if (nestedRenameMap.size > 0) pruneContext.dictionaryKeyToFieldRenameMap.set(dictionaryKey, nestedRenameMap);
210
196
  }
211
197
  };
212
- const processStaticDictionaryFile = (filePath, pruneContext, shouldPurge, shouldMinify) => {
213
- let rawJson;
214
- try {
215
- rawJson = readFileSync(filePath, "utf-8");
216
- } catch {
217
- return;
218
- }
198
+ /**
199
+ * Reads a compiled dictionary JSON file, applies the purge and/or minify
200
+ * transformations, and writes it back in-place when something changed.
201
+ *
202
+ * `dictionaryKind` selects the prune strategy and the minified output shape:
203
+ * static dictionaries keep `{ key, content }`, dynamic per-locale
204
+ * dictionaries additionally keep `locale`.
205
+ */
206
+ const processDictionaryFile = (filePath, dictionaryKind, pruneContext, shouldPurge, shouldMinify) => {
219
207
  let parsedDict;
220
208
  try {
221
- parsedDict = JSON.parse(rawJson);
209
+ parsedDict = JSON.parse(readFileSync(filePath, "utf-8"));
222
210
  } catch {
223
211
  return;
224
212
  }
@@ -229,7 +217,7 @@ const processStaticDictionaryFile = (filePath, pruneContext, shouldPurge, should
229
217
  if (shouldPurge) {
230
218
  const fieldUsage = pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);
231
219
  if (fieldUsage && fieldUsage !== "all") {
232
- const { prunedDictionary, wasRecognised } = pruneStaticDictionaryContent(parsedDict, fieldUsage);
220
+ const { prunedDictionary, wasRecognised } = (dictionaryKind === "static" ? pruneStaticDictionaryContent : pruneDynamicDictionaryContent)(parsedDict, fieldUsage);
233
221
  if (!wasRecognised) {
234
222
  pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);
235
223
  return;
@@ -246,52 +234,10 @@ const processStaticDictionaryFile = (filePath, pruneContext, shouldPurge, should
246
234
  }
247
235
  }
248
236
  if (!modified) return;
249
- const outputDict = shouldMinify ? {
237
+ const outputDict = shouldMinify ? dictionaryKind === "static" ? {
250
238
  key: parsedDict.key,
251
239
  content: parsedDict.content
252
- } : parsedDict;
253
- try {
254
- writeFileSync(filePath, JSON.stringify(outputDict), "utf-8");
255
- } catch {}
256
- };
257
- const processDynamicDictionaryFile = (filePath, pruneContext, shouldPurge, shouldMinify) => {
258
- let rawJson;
259
- try {
260
- rawJson = readFileSync(filePath, "utf-8");
261
- } catch {
262
- return;
263
- }
264
- let parsedDict;
265
- try {
266
- parsedDict = JSON.parse(rawJson);
267
- } catch {
268
- return;
269
- }
270
- const { key: dictionaryKey } = parsedDict;
271
- if (!dictionaryKey) return;
272
- if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;
273
- let modified = false;
274
- if (shouldPurge) {
275
- const fieldUsage = pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);
276
- if (fieldUsage && fieldUsage !== "all") {
277
- const { prunedDictionary, wasRecognised } = pruneDynamicDictionaryContent(parsedDict, fieldUsage);
278
- if (!wasRecognised) {
279
- pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);
280
- return;
281
- }
282
- parsedDict = prunedDictionary;
283
- modified = true;
284
- }
285
- }
286
- if (shouldMinify) {
287
- const fieldRenameMap = pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);
288
- if (fieldRenameMap && fieldRenameMap.size > 0) {
289
- parsedDict = applyFieldRenameToDict(parsedDict, fieldRenameMap);
290
- modified = true;
291
- }
292
- }
293
- if (!modified) return;
294
- const outputDict = shouldMinify ? {
240
+ } : {
295
241
  key: parsedDict.key,
296
242
  content: parsedDict.content,
297
243
  locale: parsedDict.locale
@@ -303,14 +249,14 @@ const processDynamicDictionaryFile = (filePath, pruneContext, shouldPurge, shoul
303
249
  const processAllDictionaryFiles = (dictionariesDir, dynamicDictionariesDir, pruneContext, shouldPurge, shouldMinify) => {
304
250
  if (existsSync(dictionariesDir)) for (const entry of readdirSync(dictionariesDir)) {
305
251
  if (!entry.endsWith(".json")) continue;
306
- processStaticDictionaryFile(join(dictionariesDir, entry), pruneContext, shouldPurge, shouldMinify);
252
+ processDictionaryFile(join(dictionariesDir, entry), "static", pruneContext, shouldPurge, shouldMinify);
307
253
  }
308
254
  if (existsSync(dynamicDictionariesDir)) for (const keyDir of readdirSync(dynamicDictionariesDir)) {
309
255
  const keyDirPath = join(dynamicDictionariesDir, keyDir);
310
256
  try {
311
257
  for (const localeFile of readdirSync(keyDirPath)) {
312
258
  if (!localeFile.endsWith(".json")) continue;
313
- processDynamicDictionaryFile(join(keyDirPath, localeFile), pruneContext, shouldPurge, shouldMinify);
259
+ processDictionaryFile(join(keyDirPath, localeFile), "dynamic", pruneContext, shouldPurge, shouldMinify);
314
260
  }
315
261
  } catch {}
316
262
  }
@@ -388,9 +334,6 @@ const runPurgePipeline = (options) => {
388
334
  const intlayerPurgeBabelPlugin = (_babel) => ({
389
335
  name: "intlayer-purge",
390
336
  pre() {
391
- const { baseDir } = this.opts;
392
- if (_completedBaseDirs.has(baseDir)) return;
393
- _completedBaseDirs.add(baseDir);
394
337
  runPurgePipeline(this.opts);
395
338
  },
396
339
  visitor: {}
@@ -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 { buildNestedRenameMapFromContent } from './babel-plugin-intlayer-field-rename';\nimport {\n type CompatCallerConfig,\n createPruneContext,\n makeUsageAnalyzerBabelPlugin,\n type NestedRenameMap,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin skips all processing to preserve full dictionary\n * content for the visual editor. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n};\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Tracks base directories whose full analysis + dictionary-write cycle has\n * already completed, to avoid repeating work across file transforms.\n */\nconst _completedBaseDirs = new Set<string>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n if (!isPlainRecord(localeContent)) {\n prunedTranslation[locale] = localeContent;\n continue;\n }\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslation[locale] = prunedLocaleFields;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n const prunedContent: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContent[fieldName] = fieldValue;\n }\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContent as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque fields to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n for (const [fieldName] of dangerousEntries) {\n const entry = nestedRenameMap.get(fieldName);\n if (entry) {\n entry.children = new Map();\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\nconst processStaticDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const { prunedDictionary, wasRecognised } = pruneStaticDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? { key: parsedDict.key, content: parsedDict.content }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processDynamicDictionaryFile = (\n filePath: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let rawJson: string;\n try {\n rawJson = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(rawJson) as CompiledDictionaryJson;\n } catch {\n return;\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const { prunedDictionary, wasRecognised } = pruneDynamicDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processStaticDictionaryFile(\n join(dictionariesDir, entry),\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDynamicDictionaryFile(\n join(keyDirPath, localeFile),\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n */\nconst runPurgePipeline = (options: PurgePluginOptions): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n compatCallers,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false` or `editorEnabled` is\n * `true`.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass & { opts: PurgePluginOptions }) {\n const { baseDir } = this.opts;\n\n if (_completedBaseDirs.has(baseDir)) return;\n _completedBaseDirs.add(baseDir);\n\n runPurgePipeline(this.opts);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"babel-plugin-intlayer-purge.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 {\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} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks } from './extractScriptBlocks';\nimport {\n BABEL_PARSER_OPTIONS,\n getUsageCheckRegex,\n SOURCE_FILE_REGEX,\n} from './transformers';\n\n// ── Plugin options ────────────────────────────────────────────────────────────\n\n/**\n * Pre-resolved options accepted by {@link intlayerPurgeBabelPlugin}.\n *\n * All values are resolved at babel.config.js load time (via\n * {@link getPurgePluginOptions}) so the plugin itself does not need to read\n * the configuration file on every file transform.\n */\nexport type PurgePluginOptions = {\n /**\n * Absolute path to the project root. Used as the cache key for the shared\n * {@link PruneContext} so two Babel transform pipelines for different\n * workspaces in the same process do not share state.\n */\n baseDir: string;\n\n /**\n * When `true`, remove unused content fields from compiled dictionary JSON\n * files. Mirrors `build.purge` in `intlayer.config.ts`.\n */\n purge: boolean;\n\n /**\n * When `true`, rename content fields to short alphabetic aliases\n * (`title` → `a`, etc.) and strip top-level metadata from compiled\n * dictionaries. Mirrors `build.minify` in `intlayer.config.ts`.\n */\n minify: boolean;\n\n /**\n * Build optimisation toggle. `undefined` means \"auto\" (active for\n * production builds). When explicitly `false`, the plugin is a no-op.\n * Mirrors `build.optimize`.\n */\n optimize: boolean | undefined;\n\n /**\n * When `true` the plugin skips all processing to preserve full dictionary\n * content for the visual editor. Mirrors `editor.enabled`.\n */\n editorEnabled: boolean;\n\n /**\n * Absolute path to the compiled static dictionaries directory\n * (`.intlayer/dictionaries/` by default).\n */\n dictionariesDir: string;\n\n /**\n * Absolute path to the compiled per-locale dynamic dictionaries directory\n * (`.intlayer/dynamic_dictionaries/` by default).\n */\n dynamicDictionariesDir: string;\n\n /**\n * Pre-built list of component source file paths to analyse for field-usage.\n * Populated by {@link getPurgePluginOptions} from the intlayer config's\n * `content` glob patterns.\n */\n componentFilesList: string[];\n\n /**\n * Per-dictionary import-mode overrides, keyed by dictionary `key`.\n * Dictionaries with mode `'fetch'` are excluded from field renaming because\n * their JSON is served from a remote API using the original field names.\n */\n dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n >;\n\n /**\n * Compat-adapter namespace caller configurations.\n *\n * When set, the usage analyser recognises these additional translation\n * function patterns and maps them to dictionary field usage. Each compat\n * adapter package (e.g. `@intlayer/react-i18next`) provides its own caller\n * list; they are NOT hardcoded here so that `@intlayer/babel` stays\n * framework-agnostic.\n *\n * Defaults to `[]` (no compat callers) when omitted.\n */\n compatCallers?: CompatCallerConfig[];\n};\n\n// ── Shared module-level state ─────────────────────────────────────────────────\n\n/**\n * Cache of built {@link PruneContext} objects, keyed by the project's\n * `baseDir`. Each context is built exactly once per Node.js process:\n * {@link runPurgePipeline} registers the context before running so every\n * subsequent transform is a no-op cache hit.\n */\nconst _pruneContextCache = new Map<string, PruneContext>();\n\n/**\n * Returns the shared {@link PruneContext} for the given base directory, or\n * `null` if {@link intlayerPurgeBabelPlugin} has not yet been initialised for\n * that directory.\n *\n * Used by {@link intlayerMinifyBabelPlugin} to read the rename map without\n * creating a circular dependency.\n */\nexport const getSharedPruneContext = (baseDir: string): PruneContext | null =>\n _pruneContextCache.get(baseDir) ?? null;\n\n// ── Dictionary JSON types ─────────────────────────────────────────────────────\n\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string;\n [extraKey: string]: unknown;\n};\n\n// ── Type guards ───────────────────────────────────────────────────────────────\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// ── Prune helpers (mirrors intlayerPrunePlugin) ───────────────────────────────\n\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/** Returns a copy of `record` keeping only the keys listed in `usedFieldNames`. */\nconst filterRecordByUsedFields = (\n record: Record<string, unknown>,\n usedFieldNames: Set<string>\n): Record<string, unknown> => {\n const filteredRecord: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n if (usedFieldNames.has(fieldName)) {\n filteredRecord[fieldName] = fieldValue;\n }\n }\n return filteredRecord;\n};\n\n/**\n * Removes unused fields from a **static** dictionary (all locales in one\n * file). Supports shape A (translation node at the root) and shape B (flat\n * record of translation nodes per field).\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A: { nodeType: \"translation\", translation: { en: { f1, f2 } } }\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n if (isPlainRecord(firstLocaleValue)) {\n const prunedTranslation: Record<string, unknown> = {};\n for (const [locale, localeContent] of Object.entries(\n content.translation\n )) {\n prunedTranslation[locale] = isPlainRecord(localeContent)\n ? filterRecordByUsedFields(localeContent, usedFieldNames)\n : localeContent;\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslation },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B: { field1: { nodeType: \"translation\", … }, field2: { … } }\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Removes unused fields from a **dynamic / per-locale** dictionary file\n * (one JSON per locale, flat `content` record).\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n return {\n prunedDictionary: {\n ...dictionary,\n content: filterRecordByUsedFields(content, usedFieldNames),\n },\n wasRecognised: true,\n };\n};\n\n// ── Minify helpers (mirrors intlayerMinifyPlugin) ─────────────────────────────\n\n/**\n * Recursively renames user-defined content fields using `renameMap`.\n * Translation nodes, arrays, and primitives follow the same traversal rules\n * as in the Vite-based minify plugin.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale value with the same map.\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values.\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val;\n }\n }\n return result;\n};\n\n/**\n * Applies a {@link NestedRenameMap} to a parsed dictionary object, renaming\n * only the keys inside `content` while leaving top-level metadata untouched.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n// ── Synchronous source-file analysis ─────────────────────────────────────────\n\n/**\n * Runs the usage-analyser Babel plugin synchronously on a single code block,\n * accumulating results into `pruneContext`.\n */\nconst analyzeCodeBlockSync = (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n try {\n transformSync(code, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext, { compatCallers })],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false,\n });\n } catch {\n pruneContext.hasUnparsableSourceFiles = true;\n }\n};\n\n/**\n * Reads a source file from disk and runs the usage-analyser synchronously.\n * SFC files (Vue / Svelte) are handled by extracting script blocks first.\n */\nconst analyzeSourceFileSync = (\n sourceFilePath: string,\n pruneContext: PruneContext,\n compatCallers?: CompatCallerConfig[]\n): void => {\n let code: string;\n try {\n code = readFileSync(sourceFilePath, 'utf-8');\n } catch {\n return;\n }\n\n const usageCheckRegex = getUsageCheckRegex(compatCallers);\n\n if (!usageCheckRegex.test(code)) return;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n for (const block of scriptBlocks) {\n if (!usageCheckRegex.test(block.content)) continue;\n analyzeCodeBlockSync(\n block.content,\n sourceFilePath,\n pruneContext,\n compatCallers\n );\n }\n};\n\n// ── Build rename maps ─────────────────────────────────────────────────────────\n\n/**\n * Reads compiled dictionary JSON files to build the nested field-rename maps,\n * mirroring the Phase 4 logic in the Vite `intlayerOptimize` plugin's\n * `buildStart` hook. Results are stored in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n */\nconst buildRenameMapsSynchronously = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n dictionaryKeyToImportModeMap: PurgePluginOptions['dictionaryKeyToImportModeMap'],\n pruneContext: PruneContext\n): void => {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch') continue;\n if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey))\n continue;\n\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);\n if (existsSync(staticJsonPath)) {\n try {\n const raw = readFileSync(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n // Fall through to dynamic dict.\n }\n }\n\n if (!dictionaryContent) {\n const dynamicDirPath = join(dynamicDictionariesDir, dictionaryKey);\n if (existsSync(dynamicDirPath)) {\n try {\n const localeFiles = readdirSync(dynamicDirPath);\n const firstJsonFile = localeFiles.find((file) =>\n file.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = readFileSync(\n join(dynamicDirPath, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary not readable – skip rename for this key.\n }\n }\n }\n\n if (!dictionaryContent) continue;\n\n const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);\n\n // Preserve children of opaque values to avoid breaking child components.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n if (opaqueFieldMap) {\n for (const occurrence of opaqueFieldMap.values()) {\n const renameEntry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\n if (renameEntry && renameEntry.children.size > 0) {\n renameEntry.children = new Map();\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n};\n\n// ── Dictionary file writing ───────────────────────────────────────────────────\n\n/**\n * Reads a compiled dictionary JSON file, applies the purge and/or minify\n * transformations, and writes it back in-place when something changed.\n *\n * `dictionaryKind` selects the prune strategy and the minified output shape:\n * static dictionaries keep `{ key, content }`, dynamic per-locale\n * dictionaries additionally keep `locale`.\n */\nconst processDictionaryFile = (\n filePath: string,\n dictionaryKind: 'static' | 'dynamic',\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n let parsedDict: CompiledDictionaryJson;\n try {\n parsedDict = JSON.parse(\n readFileSync(filePath, 'utf-8')\n ) as CompiledDictionaryJson;\n } catch {\n return; // Unreadable or invalid JSON – leave file unchanged.\n }\n\n const { key: dictionaryKey } = parsedDict;\n if (!dictionaryKey) return;\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return;\n\n let modified = false;\n\n if (shouldPurge) {\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (fieldUsage && fieldUsage !== 'all') {\n const pruneDictionaryContent =\n dictionaryKind === 'static'\n ? pruneStaticDictionaryContent\n : pruneDynamicDictionaryContent;\n const { prunedDictionary, wasRecognised } = pruneDictionaryContent(\n parsedDict,\n fieldUsage\n );\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n return;\n }\n parsedDict = prunedDictionary;\n modified = true;\n }\n }\n\n if (shouldMinify) {\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(\n parsedDict as Record<string, unknown>,\n fieldRenameMap\n ) as CompiledDictionaryJson;\n modified = true;\n }\n }\n\n if (!modified) return;\n\n const outputDict = shouldMinify\n ? dictionaryKind === 'static'\n ? { key: parsedDict.key, content: parsedDict.content }\n : {\n key: parsedDict.key,\n content: parsedDict.content,\n locale: parsedDict.locale,\n }\n : parsedDict;\n\n try {\n writeFileSync(filePath, JSON.stringify(outputDict), 'utf-8');\n } catch {\n // Write failure – leave file unchanged.\n }\n};\n\nconst processAllDictionaryFiles = (\n dictionariesDir: string,\n dynamicDictionariesDir: string,\n pruneContext: PruneContext,\n shouldPurge: boolean,\n shouldMinify: boolean\n): void => {\n if (existsSync(dictionariesDir)) {\n for (const entry of readdirSync(dictionariesDir)) {\n if (!entry.endsWith('.json')) continue;\n processDictionaryFile(\n join(dictionariesDir, entry),\n 'static',\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n }\n\n if (existsSync(dynamicDictionariesDir)) {\n for (const keyDir of readdirSync(dynamicDictionariesDir)) {\n const keyDirPath = join(dynamicDictionariesDir, keyDir);\n try {\n for (const localeFile of readdirSync(keyDirPath)) {\n if (!localeFile.endsWith('.json')) continue;\n processDictionaryFile(\n join(keyDirPath, localeFile),\n 'dynamic',\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n }\n } catch {\n // Unreadable key directory – skip.\n }\n }\n }\n};\n\n// ── Main initialisation ───────────────────────────────────────────────────────\n\n/**\n * Runs the full purge/minify pipeline for the given options, using a\n * module-level cache so the work happens at most once per process per\n * unique `baseDir`.\n */\nconst runPurgePipeline = (options: PurgePluginOptions): PruneContext => {\n const {\n baseDir,\n purge,\n minify,\n optimize,\n editorEnabled,\n dictionariesDir,\n dynamicDictionariesDir,\n componentFilesList,\n dictionaryKeyToImportModeMap,\n compatCallers,\n } = options;\n\n const cachedContext = _pruneContextCache.get(baseDir);\n if (cachedContext) return cachedContext;\n\n const pruneContext = createPruneContext();\n _pruneContextCache.set(baseDir, pruneContext);\n\n const shouldPurge = purge && !editorEnabled;\n const shouldMinify = minify && !editorEnabled;\n\n if ((!shouldPurge && !shouldMinify) || optimize === false)\n return pruneContext;\n\n // Phase 1: Synchronously analyse all component source files.\n for (const sourceFilePath of componentFilesList) {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) continue;\n analyzeSourceFileSync(sourceFilePath, pruneContext, compatCallers);\n }\n\n // Phase 2: Build field-rename maps (minify only).\n if (shouldMinify) {\n buildRenameMapsSynchronously(\n dictionariesDir,\n dynamicDictionariesDir,\n dictionaryKeyToImportModeMap,\n pruneContext\n );\n }\n\n // Phase 3: Write pruned / minified dictionary JSON files to disk.\n processAllDictionaryFiles(\n dictionariesDir,\n dynamicDictionariesDir,\n pruneContext,\n shouldPurge,\n shouldMinify\n );\n\n return pruneContext;\n};\n\n// ── Babel plugin ──────────────────────────────────────────────────────────────\n\n/**\n * Babel plugin that analyses all project source files and rewrites compiled\n * dictionary JSON files in-place to remove unused content fields\n * (`build.purge`) and/or rename them to short alphabetic aliases\n * (`build.minify`).\n *\n * All option values must be pre-resolved via {@link getPurgePluginOptions}\n * before being passed here — the plugin does not load the intlayer\n * configuration itself.\n *\n * This plugin performs **file I/O as a side effect**: on the very first Babel\n * transform in a given Node.js process it synchronously scans the component\n * files listed in `options.componentFilesList`, builds field-usage data, and\n * writes the processed dictionaries to disk. Subsequent transforms are\n * no-ops.\n *\n * Source-code field renames (rewriting `content.title` → `content.a`) are\n * handled by the companion {@link intlayerMinifyBabelPlugin}.\n *\n * @example\n * ```js\n * // babel.config.js\n * const {\n * intlayerPurgeBabelPlugin,\n * intlayerMinifyBabelPlugin,\n * intlayerOptimizeBabelPlugin,\n * getPurgePluginOptions,\n * getMinifyPluginOptions,\n * getOptimizePluginOptions,\n * } = require(\"@intlayer/babel\");\n *\n * module.exports = {\n * presets: [\"next/babel\"],\n * plugins: [\n * [intlayerPurgeBabelPlugin, getPurgePluginOptions()],\n * [intlayerMinifyBabelPlugin, getMinifyPluginOptions()],\n * [intlayerOptimizeBabelPlugin, getOptimizePluginOptions()],\n * ],\n * };\n * ```\n *\n * @remarks\n * - Intended for **production builds** only. Dictionary JSON files are\n * overwritten in-place; running `intlayer build` afterwards restores the\n * originals.\n * - The plugin is a no-op when `optimize` is `false` or `editorEnabled` is\n * `true`.\n */\nexport const intlayerPurgeBabelPlugin = (_babel: {\n types: typeof BabelTypes;\n}): PluginObject => ({\n name: 'intlayer-purge',\n\n pre(this: PluginPass) {\n runPurgePipeline(this.opts as PurgePluginOptions);\n },\n\n visitor: {\n // No AST transforms: all work is done as a side effect in pre().\n },\n});\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ const createPruneContext = () => ({
5
5
  hasUnparsableSourceFiles: false,
6
6
  dictionaryKeysWithUntrackedBindings: /* @__PURE__ */ new Map(),
7
7
  dictionaryKeyToFieldRenameMap: /* @__PURE__ */ new Map(),
8
- dictionaryKeysWithOpaqueTopLevelFields: /* @__PURE__ */ new Map(),
8
+ dictionaryKeysWithOpaqueFields: /* @__PURE__ */ new Map(),
9
9
  dictionariesSkippingFieldRename: /* @__PURE__ */ new Set(),
10
10
  pendingFrameworkAnalysis: /* @__PURE__ */ new Map()
11
11
  });
@@ -60,14 +60,18 @@ const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath
60
60
  if (!existingPaths.includes(currentSourceFilePath)) pruneContext.dictionaryKeysWithUntrackedBindings.set(dictionaryKey, [...existingPaths, currentSourceFilePath]);
61
61
  recordFieldUsage(pruneContext, dictionaryKey, "all");
62
62
  };
63
- /** Record that a field value is consumed opaquely (not further destructured). */
64
- const markOpaqueField = (fieldName, line) => {
65
- const fieldToLocations = pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey) ?? /* @__PURE__ */ new Map();
63
+ /** Record that the content value at `fieldPath` is consumed opaquely. */
64
+ const markOpaqueField = (fieldPath, line) => {
65
+ const pathToOccurrence = pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey) ?? /* @__PURE__ */ new Map();
66
66
  const location = line !== void 0 ? `${currentSourceFilePath}:${line}` : currentSourceFilePath;
67
- const locations = fieldToLocations.get(fieldName) ?? [];
68
- if (!locations.includes(location)) locations.push(location);
69
- fieldToLocations.set(fieldName, locations);
70
- pruneContext.dictionaryKeysWithOpaqueTopLevelFields.set(dictionaryKey, fieldToLocations);
67
+ const joinedFieldPath = fieldPath.join(".");
68
+ const occurrence = pathToOccurrence.get(joinedFieldPath) ?? {
69
+ fieldPath: [...fieldPath],
70
+ locations: []
71
+ };
72
+ if (!occurrence.locations.includes(location)) occurrence.locations.push(location);
73
+ pathToOccurrence.set(joinedFieldPath, occurrence);
74
+ pruneContext.dictionaryKeysWithOpaqueFields.set(dictionaryKey, pathToOccurrence);
71
75
  };
72
76
  /** Register a plain variable binding in an SFC file for a second-pass analysis. */
73
77
  const deferFrameworkAnalysis = (variableName) => {
@@ -79,37 +83,101 @@ const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath
79
83
  pruneContext.pendingFrameworkAnalysis.set(currentSourceFilePath, existing);
80
84
  };
81
85
  /**
82
- * Analyses usage of a variable or member access to detect opaque
83
- * consumption (passing a dictionary field as-is to a prop or function).
86
+ * Analyses how the content value at `fieldPath` (currently referenced by
87
+ * `refPath`) is consumed, to detect opaque consumption (passing a dictionary
88
+ * value as-is to a prop or function argument).
84
89
  *
85
- * If a direct, non-chained consumption is found, it calls `markOpaqueField`.
86
- * Chained accesses (e.g. `field.sub`) are NOT considered opaque for `field`
87
- * because the renamer can safely track and update them.
90
+ * Member-access chains and destructuring are followed recursively with an
91
+ * extended field path, so the *terminal* consumption of a chain like
92
+ * `content.sections.hero` decides opacity — mirroring how far the
93
+ * source-code renamer can rewrite accesses. When the terminal consumption
94
+ * escapes static tracking, the deepest reached field path is marked opaque
95
+ * so the children of that value keep their original key names.
88
96
  */
89
- const analyzeOpaqueUsage = (refPath, fieldName) => {
97
+ const analyzeOpaqueUsage = (refPath, fieldPath) => {
90
98
  const parentNode = refPath.parent;
91
- if ((babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === refPath.node) return;
92
- if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id) && parentNode.init === refPath.node) return;
99
+ const parentPath = refPath.parentPath;
100
+ if (parentPath && (babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === refPath.node) {
101
+ const memberNode = parentNode;
102
+ if (memberNode.computed && babelTypes.isNumericLiteral(memberNode.property)) {
103
+ analyzeOpaqueUsage(parentPath, fieldPath);
104
+ return;
105
+ }
106
+ if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {
107
+ analyzeOpaqueUsage(parentPath, [...fieldPath, memberNode.property.name]);
108
+ return;
109
+ }
110
+ if (memberNode.computed && babelTypes.isStringLiteral(memberNode.property)) {
111
+ analyzeOpaqueUsage(parentPath, [...fieldPath, memberNode.property.value]);
112
+ return;
113
+ }
114
+ markOpaqueField(fieldPath, refPath.node.loc?.start.line);
115
+ return;
116
+ }
117
+ if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id) && parentNode.init === refPath.node) {
118
+ analyzeOpaqueDestructuring(parentNode.id, refPath, fieldPath);
119
+ return;
120
+ }
93
121
  if (babelTypes.isArrayExpression(parentNode)) return;
94
- markOpaqueField(fieldName, refPath.node.loc?.start.line);
122
+ markOpaqueField(fieldPath, refPath.node.loc?.start.line);
123
+ };
124
+ /**
125
+ * Analyses a destructuring pattern applied to the content value at
126
+ * `fieldPath`, recursively following each bound local variable's references
127
+ * via {@link analyzeOpaqueUsage}.
128
+ *
129
+ * Patterns the source-code renamer cannot rewrite (rest elements, dynamic
130
+ * computed keys, array patterns) mark the corresponding path opaque.
131
+ */
132
+ const analyzeOpaqueDestructuring = (objectPattern, scopePath, fieldPath) => {
133
+ for (const property of objectPattern.properties) {
134
+ if (babelTypes.isRestElement(property)) {
135
+ markOpaqueField(fieldPath, property.loc?.start.line);
136
+ continue;
137
+ }
138
+ if (!babelTypes.isObjectProperty(property)) continue;
139
+ if (property.computed && !babelTypes.isStringLiteral(property.key)) {
140
+ markOpaqueField(fieldPath, property.loc?.start.line);
141
+ continue;
142
+ }
143
+ const keyName = babelTypes.isIdentifier(property.key) ? property.key.name : babelTypes.isStringLiteral(property.key) ? property.key.value : null;
144
+ if (!keyName) continue;
145
+ const childFieldPath = [...fieldPath, keyName];
146
+ const bindingTarget = babelTypes.isAssignmentPattern(property.value) ? property.value.left : property.value;
147
+ if (babelTypes.isObjectPattern(bindingTarget)) {
148
+ analyzeOpaqueDestructuring(bindingTarget, scopePath, childFieldPath);
149
+ continue;
150
+ }
151
+ if (babelTypes.isIdentifier(bindingTarget)) {
152
+ const localVarBinding = scopePath.scope.getBinding(bindingTarget.name);
153
+ if (!localVarBinding) continue;
154
+ for (const referencePath of localVarBinding.referencePaths) analyzeOpaqueUsage(referencePath, childFieldPath);
155
+ continue;
156
+ }
157
+ markOpaqueField(childFieldPath, property.loc?.start.line);
158
+ }
95
159
  };
96
160
  /**
97
161
  * Helper to collect field names from an ObjectPattern (destructuring).
98
- * Returns true if successful, false if a rest element was found (meaning 'all').
162
+ * Returns true if successful, false when the accessed fields cannot be
163
+ * determined statically (rest element or dynamic computed key → 'all').
99
164
  */
100
165
  const collectFieldsFromObjectPattern = (pattern, initPath, targetSet) => {
101
166
  if (pattern.properties.some((prop) => babelTypes.isRestElement(prop))) return false;
102
167
  for (const property of pattern.properties) {
168
+ if (!babelTypes.isObjectProperty(property)) continue;
169
+ if (property.computed && !babelTypes.isStringLiteral(property.key)) return false;
103
170
  let fieldName;
104
- if (babelTypes.isObjectProperty(property) && babelTypes.isIdentifier(property.key)) fieldName = property.key.name;
105
- else if (babelTypes.isObjectProperty(property) && babelTypes.isStringLiteral(property.key)) fieldName = property.key.value;
106
- if (fieldName) {
107
- targetSet.add(fieldName);
108
- if (babelTypes.isObjectProperty(property) && babelTypes.isIdentifier(property.value)) {
109
- const variableBinding = initPath.scope.getBinding(property.value.name);
110
- if (variableBinding) for (const refPath of variableBinding.referencePaths) analyzeOpaqueUsage(refPath, fieldName);
111
- }
112
- }
171
+ if (!property.computed && babelTypes.isIdentifier(property.key)) fieldName = property.key.name;
172
+ else if (babelTypes.isStringLiteral(property.key)) fieldName = property.key.value;
173
+ if (!fieldName) continue;
174
+ targetSet.add(fieldName);
175
+ const bindingTarget = babelTypes.isAssignmentPattern(property.value) ? property.value.left : property.value;
176
+ if (babelTypes.isIdentifier(bindingTarget)) {
177
+ const variableBinding = initPath.scope.getBinding(bindingTarget.name);
178
+ if (variableBinding) for (const refPath of variableBinding.referencePaths) analyzeOpaqueUsage(refPath, [fieldName]);
179
+ } else if (babelTypes.isObjectPattern(bindingTarget)) analyzeOpaqueDestructuring(bindingTarget, initPath, [fieldName]);
180
+ else markOpaqueField([fieldName], property.loc?.start.line);
113
181
  }
114
182
  return true;
115
183
  };
@@ -126,7 +194,7 @@ const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath
126
194
  if (fieldName) {
127
195
  recordFieldUsage(pruneContext, dictionaryKey, new Set([fieldName]));
128
196
  const memberExprPath = callExpressionPath.parentPath;
129
- if (memberExprPath) analyzeOpaqueUsage(memberExprPath, fieldName);
197
+ if (memberExprPath) analyzeOpaqueUsage(memberExprPath, [fieldName]);
130
198
  } else markUntrackedBinding();
131
199
  return;
132
200
  }
@@ -149,7 +217,7 @@ const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath
149
217
  if (fieldName) {
150
218
  accessedTopLevelFieldNames.add(fieldName);
151
219
  const memberExprPath = variableReferencePath.parentPath;
152
- if (memberExprPath) analyzeOpaqueUsage(memberExprPath, fieldName);
220
+ if (memberExprPath) analyzeOpaqueUsage(memberExprPath, [fieldName]);
153
221
  } else {
154
222
  hasUntrackedReferenceAccess = true;
155
223
  break;
@@ -168,7 +236,7 @@ const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath
168
236
  if (fieldName) {
169
237
  accessedTopLevelFieldNames.add(fieldName);
170
238
  const memberExprPath = callExprPath?.parentPath;
171
- if (memberExprPath) analyzeOpaqueUsage(memberExprPath, fieldName);
239
+ if (memberExprPath) analyzeOpaqueUsage(memberExprPath, [fieldName]);
172
240
  } else {
173
241
  hasUntrackedReferenceAccess = true;
174
242
  break;