@intlayer/babel 9.0.0-canary.1 → 9.0.0-canary.2

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.
@@ -232,12 +232,45 @@ const readObjectProperty = (babelTypes, objectExpression, propertyName) => {
232
232
  return "__default__";
233
233
  };
234
234
  /**
235
+ * Reads a named JSX attribute as a static string and returns it wrapped in a
236
+ * `StringLiteral` node so the result can be fed to the same namespace resolver
237
+ * as a call argument. Handles both `id="home.title"` and `id={'home.title'}`.
238
+ * Returns `undefined` when the attribute is absent or dynamic (`id={expr}`).
239
+ */
240
+ const readJsxAttributeString = (babelTypes, openingElement, attributeName) => {
241
+ for (const attribute of openingElement.attributes) {
242
+ if (!babelTypes.isJSXAttribute(attribute)) continue;
243
+ if (!babelTypes.isJSXIdentifier(attribute.name) || attribute.name.name !== attributeName) continue;
244
+ const value = attribute.value;
245
+ if (babelTypes.isStringLiteral(value)) return value;
246
+ if (babelTypes.isJSXExpressionContainer(value)) {
247
+ const staticString = readStaticString(babelTypes, value.expression);
248
+ if (staticString !== void 0) return babelTypes.stringLiteral(staticString);
249
+ }
250
+ return;
251
+ }
252
+ };
253
+ /**
235
254
  * Resolves the namespace (dictionary key) for a compat caller call-site from
236
255
  * its `CompatNamespaceSource` configuration. Returns the static key, or
237
256
  * `'__default__'` when the configured argument is absent (caller falls back to
238
257
  * its default namespace), or `undefined` when the value is present but dynamic.
239
258
  */
240
259
  const resolveCompatNamespace = (babelTypes, callArguments, source) => {
260
+ if (source.from === "fixed") return source.value;
261
+ if (source.from === "path-first-segment") {
262
+ const firstArg = callArguments[0];
263
+ if (firstArg === void 0) return void 0;
264
+ const staticString = readStaticString(babelTypes, firstArg);
265
+ if (staticString !== void 0) return firstPathSegment(staticString);
266
+ if (babelTypes.isObjectExpression(firstArg)) {
267
+ const idValue = readObjectProperty(babelTypes, firstArg, "id");
268
+ if (idValue === "__default__") return "__default__";
269
+ if (idValue !== void 0) return firstPathSegment(idValue);
270
+ return;
271
+ }
272
+ return;
273
+ }
241
274
  if (source.from === "argument") {
242
275
  const argument = callArguments[source.index];
243
276
  if (argument === void 0) return "__default__";
@@ -284,8 +317,7 @@ const unwrapAwait = (babelTypes, path) => {
284
317
  * so renaming the compiled JSON keys would break runtime lookups. Pruning
285
318
  * (top-level field removal) remains safe because it preserves field names.
286
319
  */
287
- const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callExpressionPath, callerConfig, isSfcFile) => {
288
- const callArguments = callExpressionPath.node.arguments;
320
+ const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callArguments, callerConfig, isSfcFile, callExpressionPath) => {
289
321
  const resolvedNamespace = resolveCompatNamespace(babelTypes, callArguments, callerConfig.namespace);
290
322
  if (resolvedNamespace === void 0) return;
291
323
  const namespaceString = resolvedNamespace === "__default__" ? DEFAULT_COMPAT_NAMESPACE : resolvedNamespace;
@@ -297,6 +329,52 @@ const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callExpressionPat
297
329
  recordFieldUsage(pruneContext, dictionaryKey, "all");
298
330
  return;
299
331
  }
332
+ if (callerConfig.translationFunction === "all") {
333
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
334
+ return;
335
+ }
336
+ if (callerConfig.translationFunction === "self") {
337
+ const firstArg = callArguments[0];
338
+ if (!firstArg) {
339
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
340
+ return;
341
+ }
342
+ if (callerConfig.namespace.from === "path-first-segment") {
343
+ let fullId;
344
+ const staticString = readStaticString(babelTypes, firstArg);
345
+ if (staticString !== void 0) fullId = staticString;
346
+ else if (babelTypes.isObjectExpression(firstArg)) {
347
+ const idValue = readObjectProperty(babelTypes, firstArg, "id");
348
+ if (idValue !== void 0 && idValue !== "__default__") fullId = idValue;
349
+ }
350
+ if (fullId !== void 0) {
351
+ const field = fullId.split(".")[1];
352
+ if (field !== void 0) recordFieldUsage(pruneContext, dictionaryKey, new Set([field]));
353
+ else recordFieldUsage(pruneContext, dictionaryKey, "all");
354
+ return;
355
+ }
356
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
357
+ return;
358
+ }
359
+ const segment = readStaticFirstSegment(babelTypes, firstArg);
360
+ if (segment !== void 0) {
361
+ recordFieldUsage(pruneContext, dictionaryKey, new Set([segment]));
362
+ return;
363
+ }
364
+ if (babelTypes.isObjectExpression(firstArg)) {
365
+ const idValue = readObjectProperty(babelTypes, firstArg, "id");
366
+ if (idValue !== void 0 && idValue !== "__default__") {
367
+ recordFieldUsage(pruneContext, dictionaryKey, new Set([firstPathSegment(idValue)]));
368
+ return;
369
+ }
370
+ }
371
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
372
+ return;
373
+ }
374
+ if (!callExpressionPath) {
375
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
376
+ return;
377
+ }
300
378
  const explicitKeyPrefix = resolveCompatKeyPrefix(babelTypes, callArguments, callerConfig.keyPrefix);
301
379
  if (explicitKeyPrefix === void 0) {
302
380
  recordFieldUsage(pruneContext, dictionaryKey, "all");
@@ -383,34 +461,44 @@ const makeUsageAnalyzerBabelPlugin = (pruneContext, options) => ({ types: babelT
383
461
  const hasNativeCallers = intlayerCallerLocalNameMap.size > 0;
384
462
  const hasCompatCallers = compatCallerLocalNameMap.size > 0 || methodCompatCallers.length > 0;
385
463
  if (!hasNativeCallers && !hasCompatCallers) return;
386
- programPath.traverse({ CallExpression: (callExpressionPath) => {
387
- const calleeNode = callExpressionPath.node.callee;
388
- let localCallerName;
389
- let isMethodCall = false;
390
- if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
391
- else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) {
392
- localCallerName = calleeNode.property.name;
393
- isMethodCall = true;
394
- }
395
- if (!localCallerName) return;
396
- if (intlayerCallerLocalNameMap.has(localCallerName)) {
397
- const callArguments = callExpressionPath.node.arguments;
398
- if (callArguments.length === 0) return;
399
- const dictionaryKey = readStaticString(babelTypes, callArguments[0]);
400
- if (!dictionaryKey) return;
401
- analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile);
402
- return;
403
- }
404
- const importedCompatCaller = compatCallerLocalNameMap.get(localCallerName);
405
- if (importedCompatCaller && !isMethodCall) {
406
- analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath, importedCompatCaller, isSfcFile);
407
- return;
408
- }
409
- if (isMethodCall) {
410
- const methodCaller = methodCompatCallers.find((caller) => caller.callerName === localCallerName);
411
- if (methodCaller) analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath, methodCaller, isSfcFile);
464
+ programPath.traverse({
465
+ CallExpression: (callExpressionPath) => {
466
+ const calleeNode = callExpressionPath.node.callee;
467
+ let localCallerName;
468
+ let isMethodCall = false;
469
+ if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
470
+ else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) {
471
+ localCallerName = calleeNode.property.name;
472
+ isMethodCall = true;
473
+ }
474
+ if (!localCallerName) return;
475
+ if (intlayerCallerLocalNameMap.has(localCallerName)) {
476
+ const callArguments = callExpressionPath.node.arguments;
477
+ if (callArguments.length === 0) return;
478
+ const dictionaryKey = readStaticString(babelTypes, callArguments[0]);
479
+ if (!dictionaryKey) return;
480
+ analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile);
481
+ return;
482
+ }
483
+ const importedCompatCaller = compatCallerLocalNameMap.get(localCallerName);
484
+ if (importedCompatCaller && !isMethodCall) {
485
+ analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath.node.arguments, importedCompatCaller, isSfcFile, callExpressionPath);
486
+ return;
487
+ }
488
+ if (isMethodCall) {
489
+ const methodCaller = methodCompatCallers.find((caller) => caller.callerName === localCallerName);
490
+ if (methodCaller) analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath.node.arguments, methodCaller, isSfcFile, callExpressionPath);
491
+ }
492
+ },
493
+ JSXOpeningElement: (jsxOpeningElementPath) => {
494
+ const nameNode = jsxOpeningElementPath.node.name;
495
+ if (!babelTypes.isJSXIdentifier(nameNode)) return;
496
+ const jsxCaller = compatCallerLocalNameMap.get(nameNode.name);
497
+ if (!jsxCaller?.jsxIdAttribute) return;
498
+ const idNode = readJsxAttributeString(babelTypes, jsxOpeningElementPath.node, jsxCaller.jsxIdAttribute);
499
+ analyzeNamespaceCallerUsage(babelTypes, pruneContext, idNode ? [idNode] : [], jsxCaller, isSfcFile);
412
500
  }
413
- } });
501
+ });
414
502
  } } }
415
503
  };
416
504
  };
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-usage-analyzer.cjs","names":[],"sources":["../../src/babel-plugin-intlayer-usage-analyzer.ts"],"sourcesContent":["import type { NodePath, PluginObj, PluginPass } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\n\n// ── PruneContext types ────────────────────────────────────────────────────────\n\n/**\n * Dictionary field usage result for a single dictionary key.\n *\n * 'all' → could not determine statically which fields are used;\n * keep every field (no pruning possible).\n * Set<string> → the exact top-level content field names that were accessed.\n */\nexport type DictionaryFieldUsage = Set<string> | 'all';\n\n/**\n * One node in the nested field-rename tree.\n *\n * shortName – the compact alias assigned to this field name.\n * children – rename table for the next level of user-defined keys inside\n * this field's value (empty when the value is a leaf / primitive).\n */\nexport type NestedRenameEntry = {\n shortName: string;\n children: NestedRenameMap;\n};\n\n/** A level of the field-rename tree, mapping original field names to entries. */\nexport type NestedRenameMap = Map<string, NestedRenameEntry>;\n\n/**\n * Shared mutable state created once by the vite plugin and passed by reference\n * to the usage-analyzer (writer) and the prune/minify plugins (readers).\n *\n * All mutations happen during the usage-analysis `buildStart` phase; readers\n * only access this state during the subsequent `transform` phase.\n */\nexport type PruneContext = {\n /**\n * Maps every dictionary key seen in source files to the set of top-level\n * content fields statically accessed, or `'all'` when the access pattern\n * could not be determined.\n */\n dictionaryKeyToFieldUsageMap: Map<string, DictionaryFieldUsage>;\n\n /**\n * Dictionary keys for which the prune/minify step must be skipped entirely\n * because an edge case was detected during analysis or structure recognition.\n */\n dictionariesWithEdgeCases: Set<string>;\n\n /**\n * True if at least one source file failed to parse during the analysis phase.\n * The prune plugin uses this flag conservatively: any dictionary key without\n * a usage entry might have been referenced by the unparsable file.\n */\n hasUnparsableSourceFiles: boolean;\n\n /**\n * Maps dictionary keys to the source file paths where the result of\n * `useIntlayer` / `getIntlayer` was assigned to a plain variable, making\n * static field analysis impossible.\n */\n dictionaryKeysWithUntrackedBindings: Map<string, string[]>;\n\n /**\n * Maps each dictionary key to a nested field-rename tree built after the\n * usage analysis phase (only populated when `build.minify` is active and\n * the field usage for that dictionary is a finite `Set<string>`).\n */\n dictionaryKeyToFieldRenameMap: Map<string, NestedRenameMap>;\n\n /**\n * Maps each dictionary key to a per-field list of source locations where\n * the field value is consumed \"opaquely\" (passed as-is to a child component\n * or function argument). When a field is opaque AND has nested user-defined\n * structure, its children must not be renamed.\n *\n * Structure: dictionaryKey → fieldName → [\"filePath:line\", …]\n */\n dictionaryKeysWithOpaqueTopLevelFields: Map<string, Map<string, string[]>>;\n\n /**\n * Dictionary keys for which field-key renaming must be skipped even if a\n * finite field-usage set was determined.\n *\n * Populated for dictionaries whose plain-variable bindings were resolved by\n * the framework-specific extractor (Vue / Svelte SFCs), because the Babel\n * rename plugin cannot update the source-code property accesses for those\n * indirect patterns (Vue `.value.field` / Svelte `$store.field`).\n *\n * Pruning and basic minification still apply; only field-key renaming is\n * suppressed.\n */\n dictionariesSkippingFieldRename: Set<string>;\n\n /**\n * Plain variable bindings that require a framework-specific secondary pass.\n *\n * Populated during the Babel analysis phase for `.vue` and `.svelte` source\n * files where direct field access is not visible to Babel scope analysis:\n * - Vue: `content.value.fieldName` – the `.value` ref-accessor is hidden\n * - Svelte: `$varName.fieldName` – the `$` prefix creates a new identifier\n *\n * Structure: filePath → [{variableName, dictionaryKey}, …]\n */\n pendingFrameworkAnalysis: Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >;\n};\n\nexport const createPruneContext = (): PruneContext => ({\n dictionaryKeyToFieldUsageMap: new Map(),\n dictionariesWithEdgeCases: new Set(),\n hasUnparsableSourceFiles: false,\n dictionaryKeysWithUntrackedBindings: new Map(),\n dictionaryKeyToFieldRenameMap: new Map(),\n dictionaryKeysWithOpaqueTopLevelFields: new Map<\n string,\n Map<string, string[]>\n >(),\n dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n\n// ── Usage-analyzer Babel plugin ───────────────────────────────────────────────\n\n/** Canonical intlayer caller names that trigger usage analysis. */\nexport const INTLAYER_CALLER_NAMES = ['useIntlayer', 'getIntlayer'] as const;\nexport type IntlayerCallerName = (typeof INTLAYER_CALLER_NAMES)[number];\n\n// ── Compat-adapter namespace callers ──────────────────────────────────────────\n\n/**\n * Describes how a compat-adapter \"namespace caller\" exposes the dictionary key\n * (namespace) and the translation function `t`.\n *\n * Compat adapters (`@intlayer/react-i18next`, `@intlayer/next-intl`, …) expose\n * the original i18n library API while delegating to intlayer under the hood.\n * Their call sites look like:\n *\n * const { t } = useTranslation('about'); t('counter.label')\n * const t = useTranslations('about'); t('counter.label')\n * const t = await getTranslations('about');\n * const t = i18n.getFixedT(null, 'about', 'counter');\n * const { t } = useI18n({ namespace: 'about' });\n *\n * The dictionary key is the *namespace* argument and the consumed top-level\n * field is the **first segment** of every dot-path passed to `t()` (or the\n * first segment of `keyPrefix` when one is supplied).\n */\nexport type CompatNamespaceSource =\n /** Namespace is a positional argument (string literal or `{ namespace }`). */\n | { from: 'argument'; index: number }\n /** Namespace is a property of an options object argument. */\n | { from: 'option'; argumentIndex: number; property: string };\n\n/**\n * Configuration entry for a single compat namespace caller.\n */\nexport type CompatCallerConfig = {\n /** The imported (or method) function name, e.g. `'useTranslation'`. */\n callerName: string;\n /**\n * Module specifiers from which `callerName` must be imported to be treated as\n * a compat caller. Includes both the original library names and their\n * `@intlayer/*` adapter equivalents, because the bundler aliases the former\n * to the latter but user source code may import either.\n *\n * Ignored when `matchAsMethod` is `true`.\n */\n importSources: string[];\n /**\n * When `true`, the caller is matched by method name on any object\n * (`x.getFixedT(...)`) without an import check. Used for `i18next` instance\n * methods that are never imported as named specifiers.\n */\n matchAsMethod?: boolean;\n /** How the dictionary key (namespace) is read from the call arguments. */\n namespace: CompatNamespaceSource;\n /**\n * Optional location of a `keyPrefix` that prefixes every `t()` path. When a\n * static prefix is present, the only consumed top-level field is the first\n * segment of the prefix.\n */\n keyPrefix?: CompatNamespaceSource;\n /** How the translation function is obtained from the call result. */\n translationFunction: 'return-value' | 'destructured-t';\n};\n\n/**\n * Default registry of compat namespace callers.\n *\n * Intentionally empty — compat-specific caller configurations belong in their\n * respective adapter packages (e.g. `@intlayer/react-i18next/plugin`,\n * `@intlayer/vue-i18n/plugin`) and are injected into `makeUsageAnalyzerBabelPlugin`\n * via the `compatCallers` option. Centralising them here would couple the\n * core `@intlayer/babel` package to every compat adapter, which violates the\n * design principle that compat logic lives in compat packages.\n */\nexport const DEFAULT_COMPAT_CALLERS: CompatCallerConfig[] = [];\n\n/** Default namespace used by compat callers when no namespace argument is given. */\nconst DEFAULT_COMPAT_NAMESPACE = 'translation';\n\n/**\n * Records the usage of a specific dictionary key's fields into `pruneContext`.\n * Merges with any previously recorded usage for the same key.\n */\nconst recordFieldUsage = (\n pruneContext: PruneContext,\n dictionaryKey: string,\n fieldUsage: DictionaryFieldUsage\n): void => {\n const existingUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n if (existingUsage === 'all') return; // already saturated\n\n if (fieldUsage === 'all') {\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, 'all');\n return;\n }\n\n const mergedFieldSet =\n existingUsage instanceof Set\n ? new Set([...existingUsage, ...fieldUsage])\n : new Set(fieldUsage);\n\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, mergedFieldSet);\n};\n\n/**\n * Analyses how the result of a single `useIntlayer('key')` / `getIntlayer('key')`\n * call expression is consumed, then records the field usage into `pruneContext`.\n *\n * Recognised patterns:\n * const { fieldA, fieldB } = useIntlayer('key') → records {fieldA, fieldB}\n * useIntlayer('key').fieldA → records {fieldA}\n * useIntlayer('key')['fieldA'] → records {fieldA}\n * const { ...rest } = useIntlayer('key') → records 'all' (spread)\n * const result = useIntlayer('key') → records 'all' (untracked binding)\n */\nconst analyzeCallExpressionUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callExpressionPath: NodePath<BabelTypes.CallExpression>,\n dictionaryKey: string,\n currentSourceFilePath: string,\n isSfcFile: boolean\n): void => {\n const parentNode = callExpressionPath.parent;\n\n /** Mark the dictionary key as having an untracked binding in this file. */\n const markUntrackedBinding = (): void => {\n const existingPaths =\n pruneContext.dictionaryKeysWithUntrackedBindings.get(dictionaryKey) ?? [];\n if (!existingPaths.includes(currentSourceFilePath)) {\n pruneContext.dictionaryKeysWithUntrackedBindings.set(dictionaryKey, [\n ...existingPaths,\n currentSourceFilePath,\n ]);\n }\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n };\n\n /** Record that a field value is consumed opaquely (not further destructured). */\n const markOpaqueField = (\n fieldName: string,\n line: number | undefined\n ): void => {\n const fieldToLocations =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey) ??\n new Map<string, string[]>();\n const location =\n line !== undefined\n ? `${currentSourceFilePath}:${line}`\n : currentSourceFilePath;\n const locations = fieldToLocations.get(fieldName) ?? [];\n if (!locations.includes(location)) locations.push(location);\n fieldToLocations.set(fieldName, locations);\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.set(\n dictionaryKey,\n fieldToLocations\n );\n };\n\n /** Register a plain variable binding in an SFC file for a second-pass analysis. */\n const deferFrameworkAnalysis = (variableName: string): void => {\n const existing =\n pruneContext.pendingFrameworkAnalysis.get(currentSourceFilePath) ?? [];\n if (\n !existing.some(\n (e) =>\n e.variableName === variableName && e.dictionaryKey === dictionaryKey\n )\n ) {\n existing.push({ variableName, dictionaryKey });\n }\n pruneContext.pendingFrameworkAnalysis.set(currentSourceFilePath, existing);\n };\n\n /**\n * Analyses usage of a variable or member access to detect opaque\n * consumption (passing a dictionary field as-is to a prop or function).\n *\n * If a direct, non-chained consumption is found, it calls `markOpaqueField`.\n * Chained accesses (e.g. `field.sub`) are NOT considered opaque for `field`\n * because the renamer can safely track and update them.\n */\n const analyzeOpaqueUsage = (\n refPath: NodePath<BabelTypes.Node>,\n fieldName: string\n ): void => {\n const parentNode = refPath.parent;\n\n // 1. Chained member access (e.g. field.sub or field?.sub)\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object === refPath.node\n ) {\n // Chained access is safe: the renamer correctly updates it.\n return;\n }\n\n // 2. Destructuring (e.g. const { sub } = field)\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id) &&\n parentNode.init === refPath.node\n ) {\n // Destructuring is analogous to member access: safe.\n return;\n }\n\n // 3. Ignored patterns (e.g. array literals [content])\n if (babelTypes.isArrayExpression(parentNode)) {\n return;\n }\n\n // 4. Opaque consumption (passed to prop, function, etc.)\n markOpaqueField(fieldName, refPath.node.loc?.start.line);\n };\n\n /**\n * Helper to collect field names from an ObjectPattern (destructuring).\n * Returns true if successful, false if a rest element was found (meaning 'all').\n */\n const collectFieldsFromObjectPattern = (\n pattern: BabelTypes.ObjectPattern,\n initPath: NodePath<BabelTypes.Node>,\n targetSet: Set<string>\n ): boolean => {\n if (pattern.properties.some((prop) => babelTypes.isRestElement(prop))) {\n return false;\n }\n\n for (const property of pattern.properties) {\n let fieldName: string | undefined;\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key)\n ) {\n fieldName = property.key.name;\n } else if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isStringLiteral(property.key)\n ) {\n fieldName = property.key.value;\n }\n\n if (fieldName) {\n targetSet.add(fieldName);\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.value)\n ) {\n const variableBinding = initPath.scope.getBinding(\n property.value.name\n );\n if (variableBinding) {\n for (const refPath of variableBinding.referencePaths) {\n analyzeOpaqueUsage(refPath, fieldName);\n }\n }\n }\n }\n }\n return true;\n };\n\n // ── Pattern 1: const { fieldA, fieldB } = useIntlayer('key') ──────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n const accessedFieldNames = new Set<string>();\n if (\n collectFieldsFromObjectPattern(\n parentNode.id,\n callExpressionPath,\n accessedFieldNames\n )\n ) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // ── Pattern 2: useIntlayer('key').fieldA / useIntlayer('key')?.fieldA ──────\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object ===\n callExpressionPath.node\n ) {\n let fieldName: string | undefined;\n\n if (!parentNode.computed && babelTypes.isIdentifier(parentNode.property)) {\n fieldName = parentNode.property.name;\n } else if (\n parentNode.computed &&\n babelTypes.isStringLiteral(parentNode.property)\n ) {\n fieldName = parentNode.property.value;\n }\n\n if (fieldName) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([fieldName]));\n\n // Check for opaque usage (e.g. passed directly to a prop)\n const memberExprPath = callExpressionPath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n markUntrackedBinding();\n }\n return;\n }\n\n // ── Pattern 3: const content = useIntlayer('key') ─────────────────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n const variableName = parentNode.id.name;\n const variableBinding = callExpressionPath.scope.getBinding(variableName);\n\n if (!variableBinding) {\n markUntrackedBinding();\n return;\n }\n\n const accessedTopLevelFieldNames = new Set<string>();\n let hasUntrackedReferenceAccess = false;\n\n for (const variableReferencePath of variableBinding.referencePaths) {\n const referenceParentNode = variableReferencePath.parent;\n\n if (\n (babelTypes.isMemberExpression(referenceParentNode) ||\n babelTypes.isOptionalMemberExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.MemberExpression).object ===\n variableReferencePath.node\n ) {\n const memberExpressionNode =\n referenceParentNode as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpressionNode.computed &&\n babelTypes.isIdentifier(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.name;\n } else if (\n memberExpressionNode.computed &&\n babelTypes.isStringLiteral(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n\n // Check usage of the field to look for opaque consumption\n const memberExprPath = variableReferencePath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n // Dynamic computed access – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (babelTypes.isArrayExpression(referenceParentNode)) {\n // The binding's value escapes into an array literal\n // (e.g. `[appleWatch, airpods].map((entry) => entry.name)`). Its fields\n // are then accessed through iteration that Babel cannot follow back to\n // this dictionary, so we cannot know which fields are used. This is the\n // canonical meta-record / collection access pattern — conservatively\n // keep every field rather than pruning the content to nothing.\n hasUntrackedReferenceAccess = true;\n break;\n } else if (\n // Solid / Angular: content() signal accessor → content().field\n (babelTypes.isCallExpression(referenceParentNode) ||\n babelTypes.isOptionalCallExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.CallExpression).callee ===\n variableReferencePath.node\n ) {\n const callExprPath = variableReferencePath.parentPath;\n const callParent = callExprPath?.parent;\n\n if (\n callParent &&\n (babelTypes.isMemberExpression(callParent) ||\n babelTypes.isOptionalMemberExpression(callParent)) &&\n (callParent as BabelTypes.MemberExpression).object ===\n callExprPath?.node\n ) {\n // content().field\n const memberExpr = callParent as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpr.computed &&\n babelTypes.isIdentifier(memberExpr.property)\n ) {\n fieldName = memberExpr.property.name;\n } else if (\n memberExpr.computed &&\n babelTypes.isStringLiteral(memberExpr.property)\n ) {\n fieldName = memberExpr.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n const memberExprPath = callExprPath?.parentPath;\n if (memberExprPath) analyzeOpaqueUsage(memberExprPath, fieldName);\n } else {\n // content()[dynamicKey] – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (\n callParent &&\n babelTypes.isVariableDeclarator(callParent) &&\n babelTypes.isObjectPattern(callParent.id) &&\n callExprPath &&\n collectFieldsFromObjectPattern(\n callParent.id,\n callExprPath,\n accessedTopLevelFieldNames\n )\n ) {\n // const { title } = content()\n // fields already added to accessedTopLevelFieldNames by collectFieldsFromObjectPattern\n } else {\n // content() with no field access or passed opaquely → cannot prune\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else {\n // Variable used in a non-member-access context (spread, function arg, etc.)\n hasUntrackedReferenceAccess = true;\n break;\n }\n }\n\n if (hasUntrackedReferenceAccess) {\n markUntrackedBinding();\n } else if (isSfcFile) {\n // Vue / Svelte SFC: defer to the framework-specific extractor because\n // Babel scope analysis cannot see through `.value` or `$` indirection.\n deferFrameworkAnalysis(variableName);\n } else if (variableBinding.referencePaths.length === 0) {\n // Non-SFC file with no visible references – conservatively keep all fields.\n markUntrackedBinding();\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, accessedTopLevelFieldNames);\n }\n return;\n }\n\n // ── Pattern 4: bare call – result is discarded ─────────────────────────────\n if (babelTypes.isExpressionStatement(parentNode)) {\n return; // no usage to record\n }\n\n // ── Fallback: result passed as argument, used in ternary, etc. ─────────────\n markUntrackedBinding();\n};\n\n// ── Compat namespace-caller analysis ──────────────────────────────────────────\n\n/**\n * Reads a fully-static string from an AST node. Returns `undefined` for\n * dynamic values (identifiers, expressions, template literals with\n * interpolations, …).\n */\nconst readStaticString = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n if (!node) return undefined;\n if (babelTypes.isStringLiteral(node)) return node.value;\n if (\n babelTypes.isTemplateLiteral(node) &&\n node.expressions.length === 0 &&\n node.quasis.length === 1\n ) {\n return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n }\n return undefined;\n};\n\n/** Returns the first dot-path segment of a key, e.g. `'a.b.c'` → `'a'`. */\nconst firstPathSegment = (path: string): string => path.split('.')[0] ?? path;\n\n/**\n * Reads the static first dot-path segment from a `t()` first-argument node.\n *\n * t('counter.label') → 'counter'\n * t(`counter.${x}`) → 'counter' (static prefix before the first dot)\n * t(`${x}.label`) → undefined (dynamic first segment)\n * t(someVariable) → undefined\n */\nconst readStaticFirstSegment = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n const staticString = readStaticString(babelTypes, node);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Template literal whose first quasi already contains the dot delimiter, e.g.\n // `counter.${index}` → the leading `counter` segment is statically known.\n if (babelTypes.isTemplateLiteral(node) && node.quasis.length > 0) {\n const firstQuasi =\n node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n if (firstQuasi?.includes('.')) {\n return firstPathSegment(firstQuasi);\n }\n }\n return undefined;\n};\n\n/**\n * Reads a static string property from an object expression. Returns\n * `'__default__'` when the property is absent and `undefined` when present but\n * dynamic.\n */\nconst readObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): string | '__default__' | undefined => {\n for (const property of objectExpression.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n const keyMatches =\n (babelTypes.isIdentifier(property.key) &&\n property.key.name === propertyName) ||\n (babelTypes.isStringLiteral(property.key) &&\n property.key.value === propertyName);\n if (!keyMatches) continue;\n const staticValue = readStaticString(babelTypes, property.value);\n return staticValue ?? undefined; // present but dynamic → undefined\n }\n return '__default__'; // property absent\n};\n\n/**\n * Resolves the namespace (dictionary key) for a compat caller call-site from\n * its `CompatNamespaceSource` configuration. Returns the static key, or\n * `'__default__'` when the configured argument is absent (caller falls back to\n * its default namespace), or `undefined` when the value is present but dynamic.\n */\nconst resolveCompatNamespace = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource\n): string | '__default__' | undefined => {\n if (source.from === 'argument') {\n const argument = callArguments[source.index];\n if (argument === undefined) return '__default__';\n\n // Direct string namespace: useTranslations('about')\n const staticString = readStaticString(babelTypes, argument);\n if (staticString !== undefined) return staticString;\n\n // Object form: getTranslations({ locale, namespace: 'about' })\n if (babelTypes.isObjectExpression(argument)) {\n return readObjectProperty(babelTypes, argument, 'namespace');\n }\n return undefined; // present but dynamic\n }\n\n // from === 'option'\n const optionsArgument = callArguments[source.argumentIndex];\n if (optionsArgument === undefined) return '__default__';\n if (!babelTypes.isObjectExpression(optionsArgument)) return undefined;\n return readObjectProperty(babelTypes, optionsArgument, source.property);\n};\n\n/**\n * Resolves an optional `keyPrefix` for a compat caller. Returns the static\n * prefix string, `null` when no prefix is configured/present, or `undefined`\n * when a prefix is present but dynamic.\n */\nconst resolveCompatKeyPrefix = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource | undefined\n): string | null | undefined => {\n if (!source) return null;\n const resolved = resolveCompatNamespace(babelTypes, callArguments, source);\n if (resolved === '__default__') return null; // prefix absent\n return resolved; // string or undefined (dynamic)\n};\n\n/**\n * Climbs past an enclosing `await` expression so that\n * `const t = await getTranslations('ns')` is resolved to its variable\n * declarator the same way the synchronous form is.\n */\nconst unwrapAwait = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> => {\n const parentPath = path.parentPath;\n if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) {\n return parentPath;\n }\n return path;\n};\n\n/**\n * Analyses how the translation function produced by a compat namespace caller\n * (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,\n * `useI18n`) is consumed, then records the accessed top-level dictionary fields\n * into `pruneContext`.\n *\n * Dictionaries consumed this way are always added to\n * `dictionariesSkippingFieldRename`: the field accesses are string-literal\n * dot-paths inside `t()` calls, which the field-rename plugin cannot rewrite,\n * so renaming the compiled JSON keys would break runtime lookups. Pruning\n * (top-level field removal) remains safe because it preserves field names.\n */\nconst analyzeNamespaceCallerUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callExpressionPath: NodePath<BabelTypes.CallExpression>,\n callerConfig: CompatCallerConfig,\n isSfcFile: boolean\n): void => {\n const callArguments = callExpressionPath.node.arguments;\n\n // 1. Resolve the dictionary key (namespace).\n const resolvedNamespace = resolveCompatNamespace(\n babelTypes,\n callArguments,\n callerConfig.namespace\n );\n if (resolvedNamespace === undefined) return; // dynamic key – cannot attribute\n const namespaceString =\n resolvedNamespace === '__default__'\n ? DEFAULT_COMPAT_NAMESPACE\n : resolvedNamespace;\n\n // next-intl scopes nested objects through a dotted namespace\n // (`'about.counter'`): the dictionary key is the first segment and the\n // remainder is an implicit key prefix applied to every t() lookup.\n const namespaceSegments = namespaceString.split('.');\n const dictionaryKey = namespaceSegments[0] ?? namespaceString;\n const namespacePrefix =\n namespaceSegments.length > 1 ? namespaceSegments.slice(1).join('.') : null;\n\n // Compat string-path access is never renamable.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n // 2. SFC files (Vue / Svelte / Astro): the translation function is typically\n // invoked from the template, which Babel cannot see. Conservatively keep\n // every field to avoid pruning a template-only access.\n if (isSfcFile) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3. Resolve an optional explicit keyPrefix (e.g. react-i18next's\n // `{ keyPrefix }` option). A static prefix fixes the single consumed\n // top-level field regardless of the individual t() paths.\n const explicitKeyPrefix = resolveCompatKeyPrefix(\n babelTypes,\n callArguments,\n callerConfig.keyPrefix\n );\n if (explicitKeyPrefix === undefined) {\n // Prefix present but dynamic → unknown field set.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The namespace-derived prefix (next-intl) and the explicit keyPrefix option\n // (react-i18next / i18next) never coexist in practice; prefer whichever is\n // present. Either way, the consumed top-level field is the prefix's first\n // segment.\n const effectivePrefix = namespacePrefix ?? explicitKeyPrefix;\n if (effectivePrefix !== null) {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([firstPathSegment(effectivePrefix)])\n );\n return;\n }\n\n // 4. Locate the `t` function binding.\n let translationBinding: ReturnType<NodePath['scope']['getBinding']> | null =\n null;\n\n if (callerConfig.translationFunction === 'destructured-t') {\n // const { t } = useTranslation('ns')\n const parentNode = callExpressionPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n for (const property of parentNode.id.properties) {\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key) &&\n property.key.name === 't' &&\n babelTypes.isIdentifier(property.value)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(property.value.name) ?? null;\n }\n }\n }\n } else {\n // const t = useTranslations('ns') / const t = await getTranslations('ns')\n const resultPath = unwrapAwait(babelTypes, callExpressionPath);\n const parentNode = resultPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(parentNode.id.name) ?? null;\n }\n }\n\n // Could not statically locate `t` (e.g. result stored whole, re-exported) →\n // conservatively keep all fields.\n if (!translationBinding) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 5. Inspect every reference to `t`.\n const accessedFields = new Set<string>();\n let hasUntrackedUsage = false;\n\n for (const referencePath of translationBinding.referencePaths) {\n const parentNode = referencePath.parent;\n\n // Must be a direct call: t('path')\n const isDirectCall =\n (babelTypes.isCallExpression(parentNode) ||\n babelTypes.isOptionalCallExpression(parentNode)) &&\n (parentNode as BabelTypes.CallExpression).callee === referencePath.node;\n\n if (!isDirectCall) {\n // t passed as a prop / argument / reassigned → fields unknown.\n hasUntrackedUsage = true;\n break;\n }\n\n const firstArgument = (parentNode as BabelTypes.CallExpression)\n .arguments[0];\n const segment = readStaticFirstSegment(babelTypes, firstArgument);\n if (segment === undefined) {\n hasUntrackedUsage = true;\n break;\n }\n accessedFields.add(segment);\n }\n\n if (hasUntrackedUsage) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // Only record a finite field set when at least one field was actually\n // accessed. Recording an empty set would prune every field even though the\n // dictionary may be consumed elsewhere.\n if (accessedFields.size > 0) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFields);\n }\n};\n\n/**\n * Creates a Babel plugin that traverses source files and records which\n * top-level dictionary fields each `useIntlayer` / `getIntlayer` call-site —\n * and each configured compat namespace caller — accesses. Results are\n * accumulated into `pruneContext`.\n *\n * This plugin is analysis-only: it does not transform the code (`code: false`\n * should be passed to `transformAsync` when using it).\n *\n * @param pruneContext - Shared mutable state written by this plugin.\n * @param options - Optional overrides. `compatCallers` defaults to\n * {@link DEFAULT_COMPAT_CALLERS}; pass `[]` to disable\n * compat-adapter analysis entirely.\n */\nexport const makeUsageAnalyzerBabelPlugin =\n (\n pruneContext: PruneContext,\n options?: { compatCallers?: CompatCallerConfig[] }\n ) =>\n ({ types: babelTypes }: { types: typeof BabelTypes }): PluginObj => {\n const compatCallers = options?.compatCallers ?? DEFAULT_COMPAT_CALLERS;\n\n return {\n name: 'intlayer-usage-analyzer',\n visitor: {\n Program: {\n exit: (programPath, state: PluginPass) => {\n const currentSourceFilePath =\n state.file.opts.filename ?? 'unknown file';\n const isSfcFile =\n currentSourceFilePath.endsWith('.vue') ||\n currentSourceFilePath.endsWith('.svelte') ||\n currentSourceFilePath.endsWith('.astro');\n\n // Phase 1: collect local aliases for native intlayer callers and\n // for compat namespace callers (gated by import source).\n const intlayerCallerLocalNameMap = new Map<string, string>();\n const compatCallerLocalNameMap = new Map<\n string,\n CompatCallerConfig\n >();\n\n // Method-matched compat callers (e.g. i18next `getFixedT`) need no\n // import and are recognised by method name on any object.\n const methodCompatCallers = compatCallers.filter(\n (caller) => caller.matchAsMethod\n );\n\n programPath.traverse({\n ImportDeclaration: (importDeclarationPath) => {\n const importSource = importDeclarationPath.node.source.value;\n\n for (const importSpecifier of importDeclarationPath.node\n .specifiers) {\n if (!babelTypes.isImportSpecifier(importSpecifier)) continue;\n\n const importedName = babelTypes.isIdentifier(\n importSpecifier.imported\n )\n ? importSpecifier.imported.name\n : (importSpecifier.imported as BabelTypes.StringLiteral)\n .value;\n\n if (\n INTLAYER_CALLER_NAMES.includes(\n importedName as IntlayerCallerName\n )\n ) {\n intlayerCallerLocalNameMap.set(\n importSpecifier.local.name,\n importedName\n );\n continue;\n }\n\n const compatCaller = compatCallers.find(\n (caller) =>\n caller.callerName === importedName &&\n caller.importSources.includes(importSource)\n );\n if (compatCaller) {\n compatCallerLocalNameMap.set(\n importSpecifier.local.name,\n compatCaller\n );\n }\n }\n },\n });\n\n const hasNativeCallers = intlayerCallerLocalNameMap.size > 0;\n const hasCompatCallers =\n compatCallerLocalNameMap.size > 0 ||\n methodCompatCallers.length > 0;\n\n if (!hasNativeCallers && !hasCompatCallers) return;\n\n // Phase 2: analyse each call-site\n programPath.traverse({\n CallExpression: (callExpressionPath) => {\n const calleeNode = callExpressionPath.node.callee;\n let localCallerName: string | undefined;\n let isMethodCall = false;\n\n if (babelTypes.isIdentifier(calleeNode)) {\n localCallerName = calleeNode.name;\n } else if (\n babelTypes.isMemberExpression(calleeNode) &&\n babelTypes.isIdentifier(calleeNode.property)\n ) {\n localCallerName = calleeNode.property.name;\n isMethodCall = true;\n }\n\n if (!localCallerName) return;\n\n // Native intlayer caller (useIntlayer / getIntlayer)\n if (intlayerCallerLocalNameMap.has(localCallerName)) {\n const callArguments = callExpressionPath.node.arguments;\n if (callArguments.length === 0) return;\n\n const dictionaryKey = readStaticString(\n babelTypes,\n callArguments[0]\n );\n if (!dictionaryKey) return; // dynamic key\n\n analyzeCallExpressionUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n dictionaryKey,\n currentSourceFilePath,\n isSfcFile\n );\n return;\n }\n\n // Compat namespace caller (imported)\n const importedCompatCaller =\n compatCallerLocalNameMap.get(localCallerName);\n if (importedCompatCaller && !isMethodCall) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n importedCompatCaller,\n isSfcFile\n );\n return;\n }\n\n // Compat namespace caller (method-matched, e.g. getFixedT)\n if (isMethodCall) {\n const methodCaller = methodCompatCallers.find(\n (caller) => caller.callerName === localCallerName\n );\n if (methodCaller) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n methodCaller,\n isSfcFile\n );\n }\n }\n },\n });\n },\n },\n },\n };\n };\n"],"mappings":";;;AA+GA,MAAa,4BAA0C;CACrD,8CAA8B,IAAI,KAAK;CACvC,2CAA2B,IAAI,KAAK;CACpC,0BAA0B;CAC1B,qDAAqC,IAAI,KAAK;CAC9C,+CAA+B,IAAI,KAAK;CACxC,wDAAwC,IAAI,KAGzC;CACH,iDAAiC,IAAI,KAAK;CAC1C,0CAA0B,IAAI,KAAK;CACpC;;AAKD,MAAa,wBAAwB,CAAC,eAAe,cAAc;;;;;;;;;;;AAwEnE,MAAa,yBAA+C,EAAE;;AAG9D,MAAM,2BAA2B;;;;;AAMjC,MAAM,oBACJ,cACA,eACA,eACS;CACT,MAAM,gBACJ,aAAa,6BAA6B,IAAI,cAAc;AAE9D,KAAI,kBAAkB,MAAO;AAE7B,KAAI,eAAe,OAAO;AACxB,eAAa,6BAA6B,IAAI,eAAe,MAAM;AACnE;;CAGF,MAAM,iBACJ,yBAAyB,MACrB,IAAI,IAAI,CAAC,GAAG,eAAe,GAAG,WAAW,CAAC,GAC1C,IAAI,IAAI,WAAW;AAEzB,cAAa,6BAA6B,IAAI,eAAe,eAAe;;;;;;;;;;;;;AAc9E,MAAM,8BACJ,YACA,cACA,oBACA,eACA,uBACA,cACS;CACT,MAAM,aAAa,mBAAmB;;CAGtC,MAAM,6BAAmC;EACvC,MAAM,gBACJ,aAAa,oCAAoC,IAAI,cAAc,IAAI,EAAE;AAC3E,MAAI,CAAC,cAAc,SAAS,sBAAsB,CAChD,cAAa,oCAAoC,IAAI,eAAe,CAClE,GAAG,eACH,sBACD,CAAC;AAEJ,mBAAiB,cAAc,eAAe,MAAM;;;CAItD,MAAM,mBACJ,WACA,SACS;EACT,MAAM,mBACJ,aAAa,uCAAuC,IAAI,cAAc,oBACtE,IAAI,KAAuB;EAC7B,MAAM,WACJ,SAAS,SACL,GAAG,sBAAsB,GAAG,SAC5B;EACN,MAAM,YAAY,iBAAiB,IAAI,UAAU,IAAI,EAAE;AACvD,MAAI,CAAC,UAAU,SAAS,SAAS,CAAE,WAAU,KAAK,SAAS;AAC3D,mBAAiB,IAAI,WAAW,UAAU;AAC1C,eAAa,uCAAuC,IAClD,eACA,iBACD;;;CAIH,MAAM,0BAA0B,iBAA+B;EAC7D,MAAM,WACJ,aAAa,yBAAyB,IAAI,sBAAsB,IAAI,EAAE;AACxE,MACE,CAAC,SAAS,MACP,MACC,EAAE,iBAAiB,gBAAgB,EAAE,kBAAkB,cAC1D,CAED,UAAS,KAAK;GAAE;GAAc;GAAe,CAAC;AAEhD,eAAa,yBAAyB,IAAI,uBAAuB,SAAS;;;;;;;;;;CAW5E,MAAM,sBACJ,SACA,cACS;EACT,MAAM,aAAa,QAAQ;AAG3B,OACG,WAAW,mBAAmB,WAAW,IACxC,WAAW,2BAA2B,WAAW,KAClD,WAA2C,WAAW,QAAQ,KAG/D;AAIF,MACE,WAAW,qBAAqB,WAAW,IAC3C,WAAW,gBAAgB,WAAW,GAAG,IACzC,WAAW,SAAS,QAAQ,KAG5B;AAIF,MAAI,WAAW,kBAAkB,WAAW,CAC1C;AAIF,kBAAgB,WAAW,QAAQ,KAAK,KAAK,MAAM,KAAK;;;;;;CAO1D,MAAM,kCACJ,SACA,UACA,cACY;AACZ,MAAI,QAAQ,WAAW,MAAM,SAAS,WAAW,cAAc,KAAK,CAAC,CACnE,QAAO;AAGT,OAAK,MAAM,YAAY,QAAQ,YAAY;GACzC,IAAI;AAEJ,OACE,WAAW,iBAAiB,SAAS,IACrC,WAAW,aAAa,SAAS,IAAI,CAErC,aAAY,SAAS,IAAI;YAEzB,WAAW,iBAAiB,SAAS,IACrC,WAAW,gBAAgB,SAAS,IAAI,CAExC,aAAY,SAAS,IAAI;AAG3B,OAAI,WAAW;AACb,cAAU,IAAI,UAAU;AAExB,QACE,WAAW,iBAAiB,SAAS,IACrC,WAAW,aAAa,SAAS,MAAM,EACvC;KACA,MAAM,kBAAkB,SAAS,MAAM,WACrC,SAAS,MAAM,KAChB;AACD,SAAI,gBACF,MAAK,MAAM,WAAW,gBAAgB,eACpC,oBAAmB,SAAS,UAAU;;;;AAMhD,SAAO;;AAIT,KACE,WAAW,qBAAqB,WAAW,IAC3C,WAAW,gBAAgB,WAAW,GAAG,EACzC;EACA,MAAM,qCAAqB,IAAI,KAAa;AAC5C,MACE,+BACE,WAAW,IACX,oBACA,mBACD,CAED,kBAAiB,cAAc,eAAe,mBAAmB;MAEjE,kBAAiB,cAAc,eAAe,MAAM;AAEtD;;AAIF,MACG,WAAW,mBAAmB,WAAW,IACxC,WAAW,2BAA2B,WAAW,KAClD,WAA2C,WAC1C,mBAAmB,MACrB;EACA,IAAI;AAEJ,MAAI,CAAC,WAAW,YAAY,WAAW,aAAa,WAAW,SAAS,CACtE,aAAY,WAAW,SAAS;WAEhC,WAAW,YACX,WAAW,gBAAgB,WAAW,SAAS,CAE/C,aAAY,WAAW,SAAS;AAGlC,MAAI,WAAW;AACb,oBAAiB,cAAc,eAAe,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC;GAGnE,MAAM,iBAAiB,mBAAmB;AAC1C,OAAI,eACF,oBAAmB,gBAAgB,UAAU;QAG/C,uBAAsB;AAExB;;AAIF,KACE,WAAW,qBAAqB,WAAW,IAC3C,WAAW,aAAa,WAAW,GAAG,EACtC;EACA,MAAM,eAAe,WAAW,GAAG;EACnC,MAAM,kBAAkB,mBAAmB,MAAM,WAAW,aAAa;AAEzE,MAAI,CAAC,iBAAiB;AACpB,yBAAsB;AACtB;;EAGF,MAAM,6CAA6B,IAAI,KAAa;EACpD,IAAI,8BAA8B;AAElC,OAAK,MAAM,yBAAyB,gBAAgB,gBAAgB;GAClE,MAAM,sBAAsB,sBAAsB;AAElD,QACG,WAAW,mBAAmB,oBAAoB,IACjD,WAAW,2BAA2B,oBAAoB,KAC3D,oBAAoD,WACnD,sBAAsB,MACxB;IACA,MAAM,uBACJ;IACF,IAAI;AAEJ,QACE,CAAC,qBAAqB,YACtB,WAAW,aAAa,qBAAqB,SAAS,CAEtD,aAAY,qBAAqB,SAAS;aAE1C,qBAAqB,YACrB,WAAW,gBAAgB,qBAAqB,SAAS,CAEzD,aAAY,qBAAqB,SAAS;AAG5C,QAAI,WAAW;AACb,gCAA2B,IAAI,UAAU;KAGzC,MAAM,iBAAiB,sBAAsB;AAC7C,SAAI,eACF,oBAAmB,gBAAgB,UAAU;WAE1C;AAEL,mCAA8B;AAC9B;;cAEO,WAAW,kBAAkB,oBAAoB,EAAE;AAO5D,kCAA8B;AAC9B;eAGC,WAAW,iBAAiB,oBAAoB,IAC/C,WAAW,yBAAyB,oBAAoB,KACzD,oBAAkD,WACjD,sBAAsB,MACxB;IACA,MAAM,eAAe,sBAAsB;IAC3C,MAAM,aAAa,cAAc;AAEjC,QACE,eACC,WAAW,mBAAmB,WAAW,IACxC,WAAW,2BAA2B,WAAW,KAClD,WAA2C,WAC1C,cAAc,MAChB;KAEA,MAAM,aAAa;KACnB,IAAI;AAEJ,SACE,CAAC,WAAW,YACZ,WAAW,aAAa,WAAW,SAAS,CAE5C,aAAY,WAAW,SAAS;cAEhC,WAAW,YACX,WAAW,gBAAgB,WAAW,SAAS,CAE/C,aAAY,WAAW,SAAS;AAGlC,SAAI,WAAW;AACb,iCAA2B,IAAI,UAAU;MACzC,MAAM,iBAAiB,cAAc;AACrC,UAAI,eAAgB,oBAAmB,gBAAgB,UAAU;YAC5D;AAEL,oCAA8B;AAC9B;;eAGF,cACA,WAAW,qBAAqB,WAAW,IAC3C,WAAW,gBAAgB,WAAW,GAAG,IACzC,gBACA,+BACE,WAAW,IACX,cACA,2BACD,EACD,QAGK;AAEL,mCAA8B;AAC9B;;UAEG;AAEL,kCAA8B;AAC9B;;;AAIJ,MAAI,4BACF,uBAAsB;WACb,UAGT,wBAAuB,aAAa;WAC3B,gBAAgB,eAAe,WAAW,EAEnD,uBAAsB;MAEtB,kBAAiB,cAAc,eAAe,2BAA2B;AAE3E;;AAIF,KAAI,WAAW,sBAAsB,WAAW,CAC9C;AAIF,uBAAsB;;;;;;;AAUxB,MAAM,oBACJ,YACA,SACuB;AACvB,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,WAAW,gBAAgB,KAAK,CAAE,QAAO,KAAK;AAClD,KACE,WAAW,kBAAkB,KAAK,IAClC,KAAK,YAAY,WAAW,KAC5B,KAAK,OAAO,WAAW,EAEvB,QAAO,KAAK,OAAO,IAAI,MAAM,UAAU,KAAK,OAAO,IAAI,MAAM;;;AAMjE,MAAM,oBAAoB,SAAyB,KAAK,MAAM,IAAI,CAAC,MAAM;;;;;;;;;AAUzE,MAAM,0BACJ,YACA,SACuB;CACvB,MAAM,eAAe,iBAAiB,YAAY,KAAK;AACvD,KAAI,iBAAiB,OAAW,QAAO,iBAAiB,aAAa;AAIrE,KAAI,WAAW,kBAAkB,KAAK,IAAI,KAAK,OAAO,SAAS,GAAG;EAChE,MAAM,aACJ,KAAK,OAAO,IAAI,MAAM,UAAU,KAAK,OAAO,IAAI,MAAM;AACxD,MAAI,YAAY,SAAS,IAAI,CAC3B,QAAO,iBAAiB,WAAW;;;;;;;;AAWzC,MAAM,sBACJ,YACA,kBACA,iBACuC;AACvC,MAAK,MAAM,YAAY,iBAAiB,YAAY;AAClD,MAAI,CAAC,WAAW,iBAAiB,SAAS,CAAE;AAM5C,MAAI,EAJD,WAAW,aAAa,SAAS,IAAI,IACpC,SAAS,IAAI,SAAS,gBACvB,WAAW,gBAAgB,SAAS,IAAI,IACvC,SAAS,IAAI,UAAU,cACV;AAEjB,SADoB,iBAAiB,YAAY,SAAS,MACxC,IAAI;;AAExB,QAAO;;;;;;;;AAST,MAAM,0BACJ,YACA,eACA,WACuC;AACvC,KAAI,OAAO,SAAS,YAAY;EAC9B,MAAM,WAAW,cAAc,OAAO;AACtC,MAAI,aAAa,OAAW,QAAO;EAGnC,MAAM,eAAe,iBAAiB,YAAY,SAAS;AAC3D,MAAI,iBAAiB,OAAW,QAAO;AAGvC,MAAI,WAAW,mBAAmB,SAAS,CACzC,QAAO,mBAAmB,YAAY,UAAU,YAAY;AAE9D;;CAIF,MAAM,kBAAkB,cAAc,OAAO;AAC7C,KAAI,oBAAoB,OAAW,QAAO;AAC1C,KAAI,CAAC,WAAW,mBAAmB,gBAAgB,CAAE,QAAO;AAC5D,QAAO,mBAAmB,YAAY,iBAAiB,OAAO,SAAS;;;;;;;AAQzE,MAAM,0BACJ,YACA,eACA,WAC8B;AAC9B,KAAI,CAAC,OAAQ,QAAO;CACpB,MAAM,WAAW,uBAAuB,YAAY,eAAe,OAAO;AAC1E,KAAI,aAAa,cAAe,QAAO;AACvC,QAAO;;;;;;;AAQT,MAAM,eACJ,YACA,SAC8B;CAC9B,MAAM,aAAa,KAAK;AACxB,KAAI,cAAc,WAAW,kBAAkB,WAAW,KAAK,CAC7D,QAAO;AAET,QAAO;;;;;;;;;;;;;;AAeT,MAAM,+BACJ,YACA,cACA,oBACA,cACA,cACS;CACT,MAAM,gBAAgB,mBAAmB,KAAK;CAG9C,MAAM,oBAAoB,uBACxB,YACA,eACA,aAAa,UACd;AACD,KAAI,sBAAsB,OAAW;CACrC,MAAM,kBACJ,sBAAsB,gBAClB,2BACA;CAKN,MAAM,oBAAoB,gBAAgB,MAAM,IAAI;CACpD,MAAM,gBAAgB,kBAAkB,MAAM;CAC9C,MAAM,kBACJ,kBAAkB,SAAS,IAAI,kBAAkB,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG;AAGxE,cAAa,gCAAgC,IAAI,cAAc;AAK/D,KAAI,WAAW;AACb,mBAAiB,cAAc,eAAe,MAAM;AACpD;;CAMF,MAAM,oBAAoB,uBACxB,YACA,eACA,aAAa,UACd;AACD,KAAI,sBAAsB,QAAW;AAEnC,mBAAiB,cAAc,eAAe,MAAM;AACpD;;CAOF,MAAM,kBAAkB,mBAAmB;AAC3C,KAAI,oBAAoB,MAAM;AAC5B,mBACE,cACA,eACA,IAAI,IAAI,CAAC,iBAAiB,gBAAgB,CAAC,CAAC,CAC7C;AACD;;CAIF,IAAI,qBACF;AAEF,KAAI,aAAa,wBAAwB,kBAAkB;EAEzD,MAAM,aAAa,mBAAmB;AACtC,MACE,WAAW,qBAAqB,WAAW,IAC3C,WAAW,gBAAgB,WAAW,GAAG,EAEzC;QAAK,MAAM,YAAY,WAAW,GAAG,WACnC,KACE,WAAW,iBAAiB,SAAS,IACrC,WAAW,aAAa,SAAS,IAAI,IACrC,SAAS,IAAI,SAAS,OACtB,WAAW,aAAa,SAAS,MAAM,CAEvC,sBACE,mBAAmB,MAAM,WAAW,SAAS,MAAM,KAAK,IAAI;;QAI/D;EAGL,MAAM,aADa,YAAY,YAAY,mBACd,CAAC;AAC9B,MACE,WAAW,qBAAqB,WAAW,IAC3C,WAAW,aAAa,WAAW,GAAG,CAEtC,sBACE,mBAAmB,MAAM,WAAW,WAAW,GAAG,KAAK,IAAI;;AAMjE,KAAI,CAAC,oBAAoB;AACvB,mBAAiB,cAAc,eAAe,MAAM;AACpD;;CAIF,MAAM,iCAAiB,IAAI,KAAa;CACxC,IAAI,oBAAoB;AAExB,MAAK,MAAM,iBAAiB,mBAAmB,gBAAgB;EAC7D,MAAM,aAAa,cAAc;AAQjC,MAAI,GAJD,WAAW,iBAAiB,WAAW,IACtC,WAAW,yBAAyB,WAAW,KAChD,WAAyC,WAAW,cAAc,OAElD;AAEjB,uBAAoB;AACpB;;EAGF,MAAM,gBAAiB,WACpB,UAAU;EACb,MAAM,UAAU,uBAAuB,YAAY,cAAc;AACjE,MAAI,YAAY,QAAW;AACzB,uBAAoB;AACpB;;AAEF,iBAAe,IAAI,QAAQ;;AAG7B,KAAI,mBAAmB;AACrB,mBAAiB,cAAc,eAAe,MAAM;AACpD;;AAMF,KAAI,eAAe,OAAO,EACxB,kBAAiB,cAAc,eAAe,eAAe;;;;;;;;;;;;;;;;AAkBjE,MAAa,gCAET,cACA,aAED,EAAE,OAAO,iBAA0D;CAClE,MAAM,gBAAgB,SAAS,iBAAiB;AAEhD,QAAO;EACL,MAAM;EACN,SAAS,EACP,SAAS,EACP,OAAO,aAAa,UAAsB;GACxC,MAAM,wBACJ,MAAM,KAAK,KAAK,YAAY;GAC9B,MAAM,YACJ,sBAAsB,SAAS,OAAO,IACtC,sBAAsB,SAAS,UAAU,IACzC,sBAAsB,SAAS,SAAS;GAI1C,MAAM,6CAA6B,IAAI,KAAqB;GAC5D,MAAM,2CAA2B,IAAI,KAGlC;GAIH,MAAM,sBAAsB,cAAc,QACvC,WAAW,OAAO,cACpB;AAED,eAAY,SAAS,EACnB,oBAAoB,0BAA0B;IAC5C,MAAM,eAAe,sBAAsB,KAAK,OAAO;AAEvD,SAAK,MAAM,mBAAmB,sBAAsB,KACjD,YAAY;AACb,SAAI,CAAC,WAAW,kBAAkB,gBAAgB,CAAE;KAEpD,MAAM,eAAe,WAAW,aAC9B,gBAAgB,SACjB,GACG,gBAAgB,SAAS,OACxB,gBAAgB,SACd;AAEP,SACE,sBAAsB,SACpB,aACD,EACD;AACA,iCAA2B,IACzB,gBAAgB,MAAM,MACtB,aACD;AACD;;KAGF,MAAM,eAAe,cAAc,MAChC,WACC,OAAO,eAAe,gBACtB,OAAO,cAAc,SAAS,aAAa,CAC9C;AACD,SAAI,aACF,0BAAyB,IACvB,gBAAgB,MAAM,MACtB,aACD;;MAIR,CAAC;GAEF,MAAM,mBAAmB,2BAA2B,OAAO;GAC3D,MAAM,mBACJ,yBAAyB,OAAO,KAChC,oBAAoB,SAAS;AAE/B,OAAI,CAAC,oBAAoB,CAAC,iBAAkB;AAG5C,eAAY,SAAS,EACnB,iBAAiB,uBAAuB;IACtC,MAAM,aAAa,mBAAmB,KAAK;IAC3C,IAAI;IACJ,IAAI,eAAe;AAEnB,QAAI,WAAW,aAAa,WAAW,CACrC,mBAAkB,WAAW;aAE7B,WAAW,mBAAmB,WAAW,IACzC,WAAW,aAAa,WAAW,SAAS,EAC5C;AACA,uBAAkB,WAAW,SAAS;AACtC,oBAAe;;AAGjB,QAAI,CAAC,gBAAiB;AAGtB,QAAI,2BAA2B,IAAI,gBAAgB,EAAE;KACnD,MAAM,gBAAgB,mBAAmB,KAAK;AAC9C,SAAI,cAAc,WAAW,EAAG;KAEhC,MAAM,gBAAgB,iBACpB,YACA,cAAc,GACf;AACD,SAAI,CAAC,cAAe;AAEpB,gCACE,YACA,cACA,oBACA,eACA,uBACA,UACD;AACD;;IAIF,MAAM,uBACJ,yBAAyB,IAAI,gBAAgB;AAC/C,QAAI,wBAAwB,CAAC,cAAc;AACzC,iCACE,YACA,cACA,oBACA,sBACA,UACD;AACD;;AAIF,QAAI,cAAc;KAChB,MAAM,eAAe,oBAAoB,MACtC,WAAW,OAAO,eAAe,gBACnC;AACD,SAAI,aACF,6BACE,YACA,cACA,oBACA,cACA,UACD;;MAIR,CAAC;KAEL,EACF;EACF"}
1
+ {"version":3,"file":"babel-plugin-intlayer-usage-analyzer.cjs","names":[],"sources":["../../src/babel-plugin-intlayer-usage-analyzer.ts"],"sourcesContent":["import type { NodePath, PluginObj, PluginPass } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\n\n// ── PruneContext types ────────────────────────────────────────────────────────\n\n/**\n * Dictionary field usage result for a single dictionary key.\n *\n * 'all' → could not determine statically which fields are used;\n * keep every field (no pruning possible).\n * Set<string> → the exact top-level content field names that were accessed.\n */\nexport type DictionaryFieldUsage = Set<string> | 'all';\n\n/**\n * One node in the nested field-rename tree.\n *\n * shortName – the compact alias assigned to this field name.\n * children – rename table for the next level of user-defined keys inside\n * this field's value (empty when the value is a leaf / primitive).\n */\nexport type NestedRenameEntry = {\n shortName: string;\n children: NestedRenameMap;\n};\n\n/** A level of the field-rename tree, mapping original field names to entries. */\nexport type NestedRenameMap = Map<string, NestedRenameEntry>;\n\n/**\n * Shared mutable state created once by the vite plugin and passed by reference\n * to the usage-analyzer (writer) and the prune/minify plugins (readers).\n *\n * All mutations happen during the usage-analysis `buildStart` phase; readers\n * only access this state during the subsequent `transform` phase.\n */\nexport type PruneContext = {\n /**\n * Maps every dictionary key seen in source files to the set of top-level\n * content fields statically accessed, or `'all'` when the access pattern\n * could not be determined.\n */\n dictionaryKeyToFieldUsageMap: Map<string, DictionaryFieldUsage>;\n\n /**\n * Dictionary keys for which the prune/minify step must be skipped entirely\n * because an edge case was detected during analysis or structure recognition.\n */\n dictionariesWithEdgeCases: Set<string>;\n\n /**\n * True if at least one source file failed to parse during the analysis phase.\n * The prune plugin uses this flag conservatively: any dictionary key without\n * a usage entry might have been referenced by the unparsable file.\n */\n hasUnparsableSourceFiles: boolean;\n\n /**\n * Maps dictionary keys to the source file paths where the result of\n * `useIntlayer` / `getIntlayer` was assigned to a plain variable, making\n * static field analysis impossible.\n */\n dictionaryKeysWithUntrackedBindings: Map<string, string[]>;\n\n /**\n * Maps each dictionary key to a nested field-rename tree built after the\n * usage analysis phase (only populated when `build.minify` is active and\n * the field usage for that dictionary is a finite `Set<string>`).\n */\n dictionaryKeyToFieldRenameMap: Map<string, NestedRenameMap>;\n\n /**\n * Maps each dictionary key to a per-field list of source locations where\n * the field value is consumed \"opaquely\" (passed as-is to a child component\n * or function argument). When a field is opaque AND has nested user-defined\n * structure, its children must not be renamed.\n *\n * Structure: dictionaryKey → fieldName → [\"filePath:line\", …]\n */\n dictionaryKeysWithOpaqueTopLevelFields: Map<string, Map<string, string[]>>;\n\n /**\n * Dictionary keys for which field-key renaming must be skipped even if a\n * finite field-usage set was determined.\n *\n * Populated for dictionaries whose plain-variable bindings were resolved by\n * the framework-specific extractor (Vue / Svelte SFCs), because the Babel\n * rename plugin cannot update the source-code property accesses for those\n * indirect patterns (Vue `.value.field` / Svelte `$store.field`).\n *\n * Pruning and basic minification still apply; only field-key renaming is\n * suppressed.\n */\n dictionariesSkippingFieldRename: Set<string>;\n\n /**\n * Plain variable bindings that require a framework-specific secondary pass.\n *\n * Populated during the Babel analysis phase for `.vue` and `.svelte` source\n * files where direct field access is not visible to Babel scope analysis:\n * - Vue: `content.value.fieldName` – the `.value` ref-accessor is hidden\n * - Svelte: `$varName.fieldName` – the `$` prefix creates a new identifier\n *\n * Structure: filePath → [{variableName, dictionaryKey}, …]\n */\n pendingFrameworkAnalysis: Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >;\n};\n\nexport const createPruneContext = (): PruneContext => ({\n dictionaryKeyToFieldUsageMap: new Map(),\n dictionariesWithEdgeCases: new Set(),\n hasUnparsableSourceFiles: false,\n dictionaryKeysWithUntrackedBindings: new Map(),\n dictionaryKeyToFieldRenameMap: new Map(),\n dictionaryKeysWithOpaqueTopLevelFields: new Map<\n string,\n Map<string, string[]>\n >(),\n dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n\n// ── Usage-analyzer Babel plugin ───────────────────────────────────────────────\n\n/** Canonical intlayer caller names that trigger usage analysis. */\nexport const INTLAYER_CALLER_NAMES = ['useIntlayer', 'getIntlayer'] as const;\nexport type IntlayerCallerName = (typeof INTLAYER_CALLER_NAMES)[number];\n\n// ── Compat-adapter namespace callers ──────────────────────────────────────────\n\n/**\n * Describes how a compat-adapter \"namespace caller\" exposes the dictionary key\n * (namespace) and the translation function `t`.\n *\n * Compat adapters (`@intlayer/react-i18next`, `@intlayer/next-intl`, …) expose\n * the original i18n library API while delegating to intlayer under the hood.\n * Their call sites look like:\n *\n * const { t } = useTranslation('about'); t('counter.label')\n * const t = useTranslations('about'); t('counter.label')\n * const t = await getTranslations('about');\n * const t = i18n.getFixedT(null, 'about', 'counter');\n * const { t } = useI18n({ namespace: 'about' });\n *\n * The dictionary key is the *namespace* argument and the consumed top-level\n * field is the **first segment** of every dot-path passed to `t()` (or the\n * first segment of `keyPrefix` when one is supplied).\n */\nexport type CompatNamespaceSource =\n /** Namespace is a positional argument (string literal or `{ namespace }`). */\n | { from: 'argument'; index: number }\n /** Namespace is a property of an options object argument. */\n | { from: 'option'; argumentIndex: number; property: string }\n /**\n * Namespace is a compile-time constant — the same dictionary key is used for\n * every call site. Used by single-catalog libraries such as lingui, where all\n * translations live in a `messages` dictionary regardless of call location.\n */\n | { from: 'fixed'; value: string }\n /**\n * Namespace is the first dot-segment of the `id` in the first call argument.\n *\n * Used for libraries like react-intl where the full dotted id encodes both the\n * dictionary key and the field path in a single string:\n * `formatMessage({ id: 'home.title' })` → dictionaryKey = `'home'`, field = `'title'`\n *\n * Works with both string form (`func('home.title')`) and descriptor form\n * (`func({ id: 'home.title', ... })`). The paired `translationFunction` should\n * be `'self'` so the field (second segment) is also extracted from the same call.\n */\n | { from: 'path-first-segment' };\n\n/**\n * Configuration entry for a single compat namespace caller.\n */\nexport type CompatCallerConfig = {\n /** The imported (or method) function name, e.g. `'useTranslation'`. */\n callerName: string;\n /**\n * Module specifiers from which `callerName` must be imported to be treated as\n * a compat caller. Includes both the original library names and their\n * `@intlayer/*` adapter equivalents, because the bundler aliases the former\n * to the latter but user source code may import either.\n *\n * Ignored when `matchAsMethod` is `true`.\n */\n importSources: string[];\n /**\n * When `true`, the caller is matched by method name on any object\n * (`x.getFixedT(...)`) without an import check. Used for `i18next` instance\n * methods that are never imported as named specifiers.\n */\n matchAsMethod?: boolean;\n /** How the dictionary key (namespace) is read from the call arguments. */\n namespace: CompatNamespaceSource;\n /**\n * Optional location of a `keyPrefix` that prefixes every `t()` path. When a\n * static prefix is present, the only consumed top-level field is the first\n * segment of the prefix.\n */\n keyPrefix?: CompatNamespaceSource;\n /**\n * How the translation function is obtained from the call result.\n *\n * - `'return-value'` — `const t = useTranslations('ns'); t('key')`\n * - `'destructured-t'` — `const { t } = useTranslation('ns'); t('key')`\n * - `'self'` — the caller IS the translation call;\n * the first argument is the message key.\n * Used for lingui's `i18n._('key')` / `i18n.t('key')`.\n * - `'all'` — mark the entire dictionary as used without tracking\n * individual fields. Use when static key analysis is\n * impossible (e.g. lingui hashed IDs, Angular templates).\n */\n translationFunction: 'return-value' | 'destructured-t' | 'self' | 'all';\n /**\n * When set, the caller is *also* matched as a JSX element whose local name is\n * `callerName` (gated by `importSources`). The named attribute is read as the\n * message id and analysed with the same `namespace` + `translationFunction`\n * (`'self'` / `'all'`) semantics as the call-expression form.\n *\n * Required for libraries with a JSX message component, e.g. react-intl's\n * `<FormattedMessage id=\"home.title\" />`. Without it, JSX usages are invisible\n * to the analyser and field-level pruning of the same dictionary becomes\n * unsafe (it could prune a field only referenced from JSX).\n */\n jsxIdAttribute?: string;\n};\n\n/**\n * Default registry of compat namespace callers.\n *\n * Intentionally empty — compat-specific caller configurations belong in their\n * respective adapter packages (e.g. `@intlayer/react-i18next/plugin`,\n * `@intlayer/vue-i18n/plugin`) and are injected into `makeUsageAnalyzerBabelPlugin`\n * via the `compatCallers` option. Centralising them here would couple the\n * core `@intlayer/babel` package to every compat adapter, which violates the\n * design principle that compat logic lives in compat packages.\n */\nexport const DEFAULT_COMPAT_CALLERS: CompatCallerConfig[] = [];\n\n/** Default namespace used by compat callers when no namespace argument is given. */\nconst DEFAULT_COMPAT_NAMESPACE = 'translation';\n\n/**\n * Records the usage of a specific dictionary key's fields into `pruneContext`.\n * Merges with any previously recorded usage for the same key.\n */\nconst recordFieldUsage = (\n pruneContext: PruneContext,\n dictionaryKey: string,\n fieldUsage: DictionaryFieldUsage\n): void => {\n const existingUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n if (existingUsage === 'all') return; // already saturated\n\n if (fieldUsage === 'all') {\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, 'all');\n return;\n }\n\n const mergedFieldSet =\n existingUsage instanceof Set\n ? new Set([...existingUsage, ...fieldUsage])\n : new Set(fieldUsage);\n\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, mergedFieldSet);\n};\n\n/**\n * Analyses how the result of a single `useIntlayer('key')` / `getIntlayer('key')`\n * call expression is consumed, then records the field usage into `pruneContext`.\n *\n * Recognised patterns:\n * const { fieldA, fieldB } = useIntlayer('key') → records {fieldA, fieldB}\n * useIntlayer('key').fieldA → records {fieldA}\n * useIntlayer('key')['fieldA'] → records {fieldA}\n * const { ...rest } = useIntlayer('key') → records 'all' (spread)\n * const result = useIntlayer('key') → records 'all' (untracked binding)\n */\nconst analyzeCallExpressionUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callExpressionPath: NodePath<BabelTypes.CallExpression>,\n dictionaryKey: string,\n currentSourceFilePath: string,\n isSfcFile: boolean\n): void => {\n const parentNode = callExpressionPath.parent;\n\n /** Mark the dictionary key as having an untracked binding in this file. */\n const markUntrackedBinding = (): void => {\n const existingPaths =\n pruneContext.dictionaryKeysWithUntrackedBindings.get(dictionaryKey) ?? [];\n if (!existingPaths.includes(currentSourceFilePath)) {\n pruneContext.dictionaryKeysWithUntrackedBindings.set(dictionaryKey, [\n ...existingPaths,\n currentSourceFilePath,\n ]);\n }\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n };\n\n /** Record that a field value is consumed opaquely (not further destructured). */\n const markOpaqueField = (\n fieldName: string,\n line: number | undefined\n ): void => {\n const fieldToLocations =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey) ??\n new Map<string, string[]>();\n const location =\n line !== undefined\n ? `${currentSourceFilePath}:${line}`\n : currentSourceFilePath;\n const locations = fieldToLocations.get(fieldName) ?? [];\n if (!locations.includes(location)) locations.push(location);\n fieldToLocations.set(fieldName, locations);\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.set(\n dictionaryKey,\n fieldToLocations\n );\n };\n\n /** Register a plain variable binding in an SFC file for a second-pass analysis. */\n const deferFrameworkAnalysis = (variableName: string): void => {\n const existing =\n pruneContext.pendingFrameworkAnalysis.get(currentSourceFilePath) ?? [];\n if (\n !existing.some(\n (e) =>\n e.variableName === variableName && e.dictionaryKey === dictionaryKey\n )\n ) {\n existing.push({ variableName, dictionaryKey });\n }\n pruneContext.pendingFrameworkAnalysis.set(currentSourceFilePath, existing);\n };\n\n /**\n * Analyses usage of a variable or member access to detect opaque\n * consumption (passing a dictionary field as-is to a prop or function).\n *\n * If a direct, non-chained consumption is found, it calls `markOpaqueField`.\n * Chained accesses (e.g. `field.sub`) are NOT considered opaque for `field`\n * because the renamer can safely track and update them.\n */\n const analyzeOpaqueUsage = (\n refPath: NodePath<BabelTypes.Node>,\n fieldName: string\n ): void => {\n const parentNode = refPath.parent;\n\n // 1. Chained member access (e.g. field.sub or field?.sub)\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object === refPath.node\n ) {\n // Chained access is safe: the renamer correctly updates it.\n return;\n }\n\n // 2. Destructuring (e.g. const { sub } = field)\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id) &&\n parentNode.init === refPath.node\n ) {\n // Destructuring is analogous to member access: safe.\n return;\n }\n\n // 3. Ignored patterns (e.g. array literals [content])\n if (babelTypes.isArrayExpression(parentNode)) {\n return;\n }\n\n // 4. Opaque consumption (passed to prop, function, etc.)\n markOpaqueField(fieldName, refPath.node.loc?.start.line);\n };\n\n /**\n * Helper to collect field names from an ObjectPattern (destructuring).\n * Returns true if successful, false if a rest element was found (meaning 'all').\n */\n const collectFieldsFromObjectPattern = (\n pattern: BabelTypes.ObjectPattern,\n initPath: NodePath<BabelTypes.Node>,\n targetSet: Set<string>\n ): boolean => {\n if (pattern.properties.some((prop) => babelTypes.isRestElement(prop))) {\n return false;\n }\n\n for (const property of pattern.properties) {\n let fieldName: string | undefined;\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key)\n ) {\n fieldName = property.key.name;\n } else if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isStringLiteral(property.key)\n ) {\n fieldName = property.key.value;\n }\n\n if (fieldName) {\n targetSet.add(fieldName);\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.value)\n ) {\n const variableBinding = initPath.scope.getBinding(\n property.value.name\n );\n if (variableBinding) {\n for (const refPath of variableBinding.referencePaths) {\n analyzeOpaqueUsage(refPath, fieldName);\n }\n }\n }\n }\n }\n return true;\n };\n\n // ── Pattern 1: const { fieldA, fieldB } = useIntlayer('key') ──────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n const accessedFieldNames = new Set<string>();\n if (\n collectFieldsFromObjectPattern(\n parentNode.id,\n callExpressionPath,\n accessedFieldNames\n )\n ) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // ── Pattern 2: useIntlayer('key').fieldA / useIntlayer('key')?.fieldA ──────\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object ===\n callExpressionPath.node\n ) {\n let fieldName: string | undefined;\n\n if (!parentNode.computed && babelTypes.isIdentifier(parentNode.property)) {\n fieldName = parentNode.property.name;\n } else if (\n parentNode.computed &&\n babelTypes.isStringLiteral(parentNode.property)\n ) {\n fieldName = parentNode.property.value;\n }\n\n if (fieldName) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([fieldName]));\n\n // Check for opaque usage (e.g. passed directly to a prop)\n const memberExprPath = callExpressionPath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n markUntrackedBinding();\n }\n return;\n }\n\n // ── Pattern 3: const content = useIntlayer('key') ─────────────────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n const variableName = parentNode.id.name;\n const variableBinding = callExpressionPath.scope.getBinding(variableName);\n\n if (!variableBinding) {\n markUntrackedBinding();\n return;\n }\n\n const accessedTopLevelFieldNames = new Set<string>();\n let hasUntrackedReferenceAccess = false;\n\n for (const variableReferencePath of variableBinding.referencePaths) {\n const referenceParentNode = variableReferencePath.parent;\n\n if (\n (babelTypes.isMemberExpression(referenceParentNode) ||\n babelTypes.isOptionalMemberExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.MemberExpression).object ===\n variableReferencePath.node\n ) {\n const memberExpressionNode =\n referenceParentNode as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpressionNode.computed &&\n babelTypes.isIdentifier(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.name;\n } else if (\n memberExpressionNode.computed &&\n babelTypes.isStringLiteral(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n\n // Check usage of the field to look for opaque consumption\n const memberExprPath = variableReferencePath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n // Dynamic computed access – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (babelTypes.isArrayExpression(referenceParentNode)) {\n // The binding's value escapes into an array literal\n // (e.g. `[appleWatch, airpods].map((entry) => entry.name)`). Its fields\n // are then accessed through iteration that Babel cannot follow back to\n // this dictionary, so we cannot know which fields are used. This is the\n // canonical meta-record / collection access pattern — conservatively\n // keep every field rather than pruning the content to nothing.\n hasUntrackedReferenceAccess = true;\n break;\n } else if (\n // Solid / Angular: content() signal accessor → content().field\n (babelTypes.isCallExpression(referenceParentNode) ||\n babelTypes.isOptionalCallExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.CallExpression).callee ===\n variableReferencePath.node\n ) {\n const callExprPath = variableReferencePath.parentPath;\n const callParent = callExprPath?.parent;\n\n if (\n callParent &&\n (babelTypes.isMemberExpression(callParent) ||\n babelTypes.isOptionalMemberExpression(callParent)) &&\n (callParent as BabelTypes.MemberExpression).object ===\n callExprPath?.node\n ) {\n // content().field\n const memberExpr = callParent as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpr.computed &&\n babelTypes.isIdentifier(memberExpr.property)\n ) {\n fieldName = memberExpr.property.name;\n } else if (\n memberExpr.computed &&\n babelTypes.isStringLiteral(memberExpr.property)\n ) {\n fieldName = memberExpr.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n const memberExprPath = callExprPath?.parentPath;\n if (memberExprPath) analyzeOpaqueUsage(memberExprPath, fieldName);\n } else {\n // content()[dynamicKey] – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (\n callParent &&\n babelTypes.isVariableDeclarator(callParent) &&\n babelTypes.isObjectPattern(callParent.id) &&\n callExprPath &&\n collectFieldsFromObjectPattern(\n callParent.id,\n callExprPath,\n accessedTopLevelFieldNames\n )\n ) {\n // const { title } = content()\n // fields already added to accessedTopLevelFieldNames by collectFieldsFromObjectPattern\n } else {\n // content() with no field access or passed opaquely → cannot prune\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else {\n // Variable used in a non-member-access context (spread, function arg, etc.)\n hasUntrackedReferenceAccess = true;\n break;\n }\n }\n\n if (hasUntrackedReferenceAccess) {\n markUntrackedBinding();\n } else if (isSfcFile) {\n // Vue / Svelte SFC: defer to the framework-specific extractor because\n // Babel scope analysis cannot see through `.value` or `$` indirection.\n deferFrameworkAnalysis(variableName);\n } else if (variableBinding.referencePaths.length === 0) {\n // Non-SFC file with no visible references – conservatively keep all fields.\n markUntrackedBinding();\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, accessedTopLevelFieldNames);\n }\n return;\n }\n\n // ── Pattern 4: bare call – result is discarded ─────────────────────────────\n if (babelTypes.isExpressionStatement(parentNode)) {\n return; // no usage to record\n }\n\n // ── Fallback: result passed as argument, used in ternary, etc. ─────────────\n markUntrackedBinding();\n};\n\n// ── Compat namespace-caller analysis ──────────────────────────────────────────\n\n/**\n * Reads a fully-static string from an AST node. Returns `undefined` for\n * dynamic values (identifiers, expressions, template literals with\n * interpolations, …).\n */\nconst readStaticString = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n if (!node) return undefined;\n if (babelTypes.isStringLiteral(node)) return node.value;\n if (\n babelTypes.isTemplateLiteral(node) &&\n node.expressions.length === 0 &&\n node.quasis.length === 1\n ) {\n return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n }\n return undefined;\n};\n\n/** Returns the first dot-path segment of a key, e.g. `'a.b.c'` → `'a'`. */\nconst firstPathSegment = (path: string): string => path.split('.')[0] ?? path;\n\n/**\n * Reads the static first dot-path segment from a `t()` first-argument node.\n *\n * t('counter.label') → 'counter'\n * t(`counter.${x}`) → 'counter' (static prefix before the first dot)\n * t(`${x}.label`) → undefined (dynamic first segment)\n * t(someVariable) → undefined\n */\nconst readStaticFirstSegment = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n const staticString = readStaticString(babelTypes, node);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Template literal whose first quasi already contains the dot delimiter, e.g.\n // `counter.${index}` → the leading `counter` segment is statically known.\n if (babelTypes.isTemplateLiteral(node) && node.quasis.length > 0) {\n const firstQuasi =\n node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n if (firstQuasi?.includes('.')) {\n return firstPathSegment(firstQuasi);\n }\n }\n return undefined;\n};\n\n/**\n * Reads a static string property from an object expression. Returns\n * `'__default__'` when the property is absent and `undefined` when present but\n * dynamic.\n */\nconst readObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): string | '__default__' | undefined => {\n for (const property of objectExpression.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n const keyMatches =\n (babelTypes.isIdentifier(property.key) &&\n property.key.name === propertyName) ||\n (babelTypes.isStringLiteral(property.key) &&\n property.key.value === propertyName);\n if (!keyMatches) continue;\n const staticValue = readStaticString(babelTypes, property.value);\n return staticValue ?? undefined; // present but dynamic → undefined\n }\n return '__default__'; // property absent\n};\n\n/**\n * Reads a named JSX attribute as a static string and returns it wrapped in a\n * `StringLiteral` node so the result can be fed to the same namespace resolver\n * as a call argument. Handles both `id=\"home.title\"` and `id={'home.title'}`.\n * Returns `undefined` when the attribute is absent or dynamic (`id={expr}`).\n */\nconst readJsxAttributeString = (\n babelTypes: typeof BabelTypes,\n openingElement: BabelTypes.JSXOpeningElement,\n attributeName: string\n): BabelTypes.StringLiteral | undefined => {\n for (const attribute of openingElement.attributes) {\n if (!babelTypes.isJSXAttribute(attribute)) continue;\n if (\n !babelTypes.isJSXIdentifier(attribute.name) ||\n attribute.name.name !== attributeName\n ) {\n continue;\n }\n\n const value = attribute.value;\n // id=\"home.title\"\n if (babelTypes.isStringLiteral(value)) return value;\n // id={'home.title'} / id={`home.title`}\n if (babelTypes.isJSXExpressionContainer(value)) {\n const staticString = readStaticString(babelTypes, value.expression);\n if (staticString !== undefined) {\n return babelTypes.stringLiteral(staticString);\n }\n }\n return undefined; // attribute present but dynamic\n }\n return undefined; // attribute absent\n};\n\n/**\n * Resolves the namespace (dictionary key) for a compat caller call-site from\n * its `CompatNamespaceSource` configuration. Returns the static key, or\n * `'__default__'` when the configured argument is absent (caller falls back to\n * its default namespace), or `undefined` when the value is present but dynamic.\n */\nconst resolveCompatNamespace = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource\n): string | '__default__' | undefined => {\n if (source.from === 'fixed') return source.value;\n\n if (source.from === 'path-first-segment') {\n const firstArg = callArguments[0];\n // No sensible default: the dictionary key is *derived* from the id, so an\n // absent id means the call cannot be attributed to any dictionary → skip.\n if (firstArg === undefined) return undefined;\n\n // String form: formatMessage('home.title', values)\n const staticString = readStaticString(babelTypes, firstArg);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Descriptor form: formatMessage({ id: 'home.title', ... }, values)\n if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue === '__default__') return '__default__'; // id absent\n if (idValue !== undefined) return firstPathSegment(idValue); // static id\n return undefined; // id present but dynamic\n }\n\n return undefined; // dynamic first argument\n }\n\n if (source.from === 'argument') {\n const argument = callArguments[source.index];\n if (argument === undefined) return '__default__';\n\n // Direct string namespace: useTranslations('about')\n const staticString = readStaticString(babelTypes, argument);\n if (staticString !== undefined) return staticString;\n\n // Object form: getTranslations({ locale, namespace: 'about' })\n if (babelTypes.isObjectExpression(argument)) {\n return readObjectProperty(babelTypes, argument, 'namespace');\n }\n return undefined; // present but dynamic\n }\n\n // from === 'option'\n const optionsArgument = callArguments[source.argumentIndex];\n if (optionsArgument === undefined) return '__default__';\n if (!babelTypes.isObjectExpression(optionsArgument)) return undefined;\n return readObjectProperty(babelTypes, optionsArgument, source.property);\n};\n\n/**\n * Resolves an optional `keyPrefix` for a compat caller. Returns the static\n * prefix string, `null` when no prefix is configured/present, or `undefined`\n * when a prefix is present but dynamic.\n */\nconst resolveCompatKeyPrefix = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource | undefined\n): string | null | undefined => {\n if (!source) return null;\n const resolved = resolveCompatNamespace(babelTypes, callArguments, source);\n if (resolved === '__default__') return null; // prefix absent\n return resolved; // string or undefined (dynamic)\n};\n\n/**\n * Climbs past an enclosing `await` expression so that\n * `const t = await getTranslations('ns')` is resolved to its variable\n * declarator the same way the synchronous form is.\n */\nconst unwrapAwait = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> => {\n const parentPath = path.parentPath;\n if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) {\n return parentPath;\n }\n return path;\n};\n\n/**\n * Analyses how the translation function produced by a compat namespace caller\n * (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,\n * `useI18n`) is consumed, then records the accessed top-level dictionary fields\n * into `pruneContext`.\n *\n * Dictionaries consumed this way are always added to\n * `dictionariesSkippingFieldRename`: the field accesses are string-literal\n * dot-paths inside `t()` calls, which the field-rename plugin cannot rewrite,\n * so renaming the compiled JSON keys would break runtime lookups. Pruning\n * (top-level field removal) remains safe because it preserves field names.\n */\nconst analyzeNamespaceCallerUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callArguments: BabelTypes.CallExpression['arguments'],\n callerConfig: CompatCallerConfig,\n isSfcFile: boolean,\n /**\n * The call-expression path, when the caller was matched as a call. Omitted\n * for JSX-element matches (`<FormattedMessage id>`), where only the static\n * `'self'` / `'all'` analysis paths apply — the binding-based\n * `'destructured-t'` / `'return-value'` paths require a call site.\n */\n callExpressionPath?: NodePath<BabelTypes.CallExpression>\n): void => {\n // 1. Resolve the dictionary key (namespace).\n const resolvedNamespace = resolveCompatNamespace(\n babelTypes,\n callArguments,\n callerConfig.namespace\n );\n if (resolvedNamespace === undefined) return; // dynamic key – cannot attribute\n const namespaceString =\n resolvedNamespace === '__default__'\n ? DEFAULT_COMPAT_NAMESPACE\n : resolvedNamespace;\n\n // next-intl scopes nested objects through a dotted namespace\n // (`'about.counter'`): the dictionary key is the first segment and the\n // remainder is an implicit key prefix applied to every t() lookup.\n const namespaceSegments = namespaceString.split('.');\n const dictionaryKey = namespaceSegments[0] ?? namespaceString;\n const namespacePrefix =\n namespaceSegments.length > 1 ? namespaceSegments.slice(1).join('.') : null;\n\n // Compat string-path access is never renamable.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n // 2. SFC files (Vue / Svelte / Astro): the translation function is typically\n // invoked from the template, which Babel cannot see. Conservatively keep\n // every field to avoid pruning a template-only access.\n if (isSfcFile) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3a. Mark the entire dictionary as used — static field analysis is not\n // applicable (e.g. lingui hashed IDs, Angular templates).\n if (callerConfig.translationFunction === 'all') {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3b. The caller IS the translation call (`translationFunction: 'self'`).\n // The first argument of the current call expression is the message key.\n // Used for lingui's `i18n._('key')` / `i18n.t('key')` and react-intl's\n // `intl.formatMessage({ id: 'key' })` patterns.\n if (callerConfig.translationFunction === 'self') {\n const firstArg = callArguments[0];\n if (!firstArg) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n if (callerConfig.namespace.from === 'path-first-segment') {\n // The dictionary key is already the first segment of the descriptor id.\n // The field to record is the SECOND segment (first level inside the dict).\n // e.g. formatMessage({ id: 'home.title' }) → dictionaryKey='home', field='title'\n let fullId: string | undefined;\n\n const staticString = readStaticString(babelTypes, firstArg);\n if (staticString !== undefined) {\n fullId = staticString;\n } else if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue !== undefined && idValue !== '__default__') {\n fullId = idValue;\n }\n }\n\n if (fullId !== undefined) {\n const segments = fullId.split('.');\n const field = segments[1];\n if (field !== undefined) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([field]));\n } else {\n // Single-segment id — the whole value is the dict key; no sub-field.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // Dynamic id — cannot prune.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // For 'fixed' / 'argument' / 'option' namespaces: the field is the first\n // dot-segment of the first argument (the message key itself).\n // String form: i18n._('home.title', values)\n const segment = readStaticFirstSegment(babelTypes, firstArg);\n if (segment !== undefined) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([segment]));\n return;\n }\n\n // Descriptor form: i18n._({ id: 'home.title', message: '...' }, values)\n if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue !== undefined && idValue !== '__default__') {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([firstPathSegment(idValue)])\n );\n return;\n }\n }\n\n // Dynamic key — cannot prune.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The remaining strategies (`'destructured-t'` / `'return-value'`) resolve\n // the `t` binding from the call site, so they require a call-expression path.\n // JSX-element matches never reach here (they use `'self'` / `'all'`); guard\n // defensively so a misconfigured JSX caller keeps every field instead of\n // throwing.\n if (!callExpressionPath) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3. Resolve an optional explicit keyPrefix (e.g. react-i18next's\n // `{ keyPrefix }` option). A static prefix fixes the single consumed\n // top-level field regardless of the individual t() paths.\n const explicitKeyPrefix = resolveCompatKeyPrefix(\n babelTypes,\n callArguments,\n callerConfig.keyPrefix\n );\n if (explicitKeyPrefix === undefined) {\n // Prefix present but dynamic → unknown field set.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The namespace-derived prefix (next-intl) and the explicit keyPrefix option\n // (react-i18next / i18next) never coexist in practice; prefer whichever is\n // present. Either way, the consumed top-level field is the prefix's first\n // segment.\n const effectivePrefix = namespacePrefix ?? explicitKeyPrefix;\n if (effectivePrefix !== null) {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([firstPathSegment(effectivePrefix)])\n );\n return;\n }\n\n // 4. Locate the `t` function binding.\n let translationBinding: ReturnType<NodePath['scope']['getBinding']> | null =\n null;\n\n if (callerConfig.translationFunction === 'destructured-t') {\n // const { t } = useTranslation('ns')\n const parentNode = callExpressionPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n for (const property of parentNode.id.properties) {\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key) &&\n property.key.name === 't' &&\n babelTypes.isIdentifier(property.value)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(property.value.name) ?? null;\n }\n }\n }\n } else {\n // const t = useTranslations('ns') / const t = await getTranslations('ns')\n const resultPath = unwrapAwait(babelTypes, callExpressionPath);\n const parentNode = resultPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(parentNode.id.name) ?? null;\n }\n }\n\n // Could not statically locate `t` (e.g. result stored whole, re-exported) →\n // conservatively keep all fields.\n if (!translationBinding) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 5. Inspect every reference to `t`.\n const accessedFields = new Set<string>();\n let hasUntrackedUsage = false;\n\n for (const referencePath of translationBinding.referencePaths) {\n const parentNode = referencePath.parent;\n\n // Must be a direct call: t('path')\n const isDirectCall =\n (babelTypes.isCallExpression(parentNode) ||\n babelTypes.isOptionalCallExpression(parentNode)) &&\n (parentNode as BabelTypes.CallExpression).callee === referencePath.node;\n\n if (!isDirectCall) {\n // t passed as a prop / argument / reassigned → fields unknown.\n hasUntrackedUsage = true;\n break;\n }\n\n const firstArgument = (parentNode as BabelTypes.CallExpression)\n .arguments[0];\n const segment = readStaticFirstSegment(babelTypes, firstArgument);\n if (segment === undefined) {\n hasUntrackedUsage = true;\n break;\n }\n accessedFields.add(segment);\n }\n\n if (hasUntrackedUsage) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // Only record a finite field set when at least one field was actually\n // accessed. Recording an empty set would prune every field even though the\n // dictionary may be consumed elsewhere.\n if (accessedFields.size > 0) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFields);\n }\n};\n\n/**\n * Creates a Babel plugin that traverses source files and records which\n * top-level dictionary fields each `useIntlayer` / `getIntlayer` call-site —\n * and each configured compat namespace caller — accesses. Results are\n * accumulated into `pruneContext`.\n *\n * This plugin is analysis-only: it does not transform the code (`code: false`\n * should be passed to `transformAsync` when using it).\n *\n * @param pruneContext - Shared mutable state written by this plugin.\n * @param options - Optional overrides. `compatCallers` defaults to\n * {@link DEFAULT_COMPAT_CALLERS}; pass `[]` to disable\n * compat-adapter analysis entirely.\n */\nexport const makeUsageAnalyzerBabelPlugin =\n (\n pruneContext: PruneContext,\n options?: { compatCallers?: CompatCallerConfig[] }\n ) =>\n ({ types: babelTypes }: { types: typeof BabelTypes }): PluginObj => {\n const compatCallers = options?.compatCallers ?? DEFAULT_COMPAT_CALLERS;\n\n return {\n name: 'intlayer-usage-analyzer',\n visitor: {\n Program: {\n exit: (programPath, state: PluginPass) => {\n const currentSourceFilePath =\n state.file.opts.filename ?? 'unknown file';\n const isSfcFile =\n currentSourceFilePath.endsWith('.vue') ||\n currentSourceFilePath.endsWith('.svelte') ||\n currentSourceFilePath.endsWith('.astro');\n\n // Phase 1: collect local aliases for native intlayer callers and\n // for compat namespace callers (gated by import source).\n const intlayerCallerLocalNameMap = new Map<string, string>();\n const compatCallerLocalNameMap = new Map<\n string,\n CompatCallerConfig\n >();\n\n // Method-matched compat callers (e.g. i18next `getFixedT`) need no\n // import and are recognised by method name on any object.\n const methodCompatCallers = compatCallers.filter(\n (caller) => caller.matchAsMethod\n );\n\n programPath.traverse({\n ImportDeclaration: (importDeclarationPath) => {\n const importSource = importDeclarationPath.node.source.value;\n\n for (const importSpecifier of importDeclarationPath.node\n .specifiers) {\n if (!babelTypes.isImportSpecifier(importSpecifier)) continue;\n\n const importedName = babelTypes.isIdentifier(\n importSpecifier.imported\n )\n ? importSpecifier.imported.name\n : (importSpecifier.imported as BabelTypes.StringLiteral)\n .value;\n\n if (\n INTLAYER_CALLER_NAMES.includes(\n importedName as IntlayerCallerName\n )\n ) {\n intlayerCallerLocalNameMap.set(\n importSpecifier.local.name,\n importedName\n );\n continue;\n }\n\n const compatCaller = compatCallers.find(\n (caller) =>\n caller.callerName === importedName &&\n caller.importSources.includes(importSource)\n );\n if (compatCaller) {\n compatCallerLocalNameMap.set(\n importSpecifier.local.name,\n compatCaller\n );\n }\n }\n },\n });\n\n const hasNativeCallers = intlayerCallerLocalNameMap.size > 0;\n const hasCompatCallers =\n compatCallerLocalNameMap.size > 0 ||\n methodCompatCallers.length > 0;\n\n if (!hasNativeCallers && !hasCompatCallers) return;\n\n // Phase 2: analyse each call-site\n programPath.traverse({\n CallExpression: (callExpressionPath) => {\n const calleeNode = callExpressionPath.node.callee;\n let localCallerName: string | undefined;\n let isMethodCall = false;\n\n if (babelTypes.isIdentifier(calleeNode)) {\n localCallerName = calleeNode.name;\n } else if (\n babelTypes.isMemberExpression(calleeNode) &&\n babelTypes.isIdentifier(calleeNode.property)\n ) {\n localCallerName = calleeNode.property.name;\n isMethodCall = true;\n }\n\n if (!localCallerName) return;\n\n // Native intlayer caller (useIntlayer / getIntlayer)\n if (intlayerCallerLocalNameMap.has(localCallerName)) {\n const callArguments = callExpressionPath.node.arguments;\n if (callArguments.length === 0) return;\n\n const dictionaryKey = readStaticString(\n babelTypes,\n callArguments[0]\n );\n if (!dictionaryKey) return; // dynamic key\n\n analyzeCallExpressionUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n dictionaryKey,\n currentSourceFilePath,\n isSfcFile\n );\n return;\n }\n\n // Compat namespace caller (imported)\n const importedCompatCaller =\n compatCallerLocalNameMap.get(localCallerName);\n if (importedCompatCaller && !isMethodCall) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath.node.arguments,\n importedCompatCaller,\n isSfcFile,\n callExpressionPath\n );\n return;\n }\n\n // Compat namespace caller (method-matched, e.g. getFixedT)\n if (isMethodCall) {\n const methodCaller = methodCompatCallers.find(\n (caller) => caller.callerName === localCallerName\n );\n if (methodCaller) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath.node.arguments,\n methodCaller,\n isSfcFile,\n callExpressionPath\n );\n }\n }\n },\n JSXOpeningElement: (jsxOpeningElementPath) => {\n const nameNode = jsxOpeningElementPath.node.name;\n if (!babelTypes.isJSXIdentifier(nameNode)) return;\n\n const jsxCaller = compatCallerLocalNameMap.get(nameNode.name);\n if (!jsxCaller?.jsxIdAttribute) return;\n\n // Read the configured id attribute as a static string. A missing\n // or dynamic id yields an empty argument list, which resolves the\n // namespace to undefined and is skipped (consistent with how a\n // dynamic `useIntlayer(key)` call is handled).\n const idNode = readJsxAttributeString(\n babelTypes,\n jsxOpeningElementPath.node,\n jsxCaller.jsxIdAttribute\n );\n\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n idNode ? [idNode] : [],\n jsxCaller,\n isSfcFile\n );\n },\n });\n },\n },\n },\n };\n };\n"],"mappings":"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"}
@@ -230,12 +230,45 @@ const readObjectProperty = (babelTypes, objectExpression, propertyName) => {
230
230
  return "__default__";
231
231
  };
232
232
  /**
233
+ * Reads a named JSX attribute as a static string and returns it wrapped in a
234
+ * `StringLiteral` node so the result can be fed to the same namespace resolver
235
+ * as a call argument. Handles both `id="home.title"` and `id={'home.title'}`.
236
+ * Returns `undefined` when the attribute is absent or dynamic (`id={expr}`).
237
+ */
238
+ const readJsxAttributeString = (babelTypes, openingElement, attributeName) => {
239
+ for (const attribute of openingElement.attributes) {
240
+ if (!babelTypes.isJSXAttribute(attribute)) continue;
241
+ if (!babelTypes.isJSXIdentifier(attribute.name) || attribute.name.name !== attributeName) continue;
242
+ const value = attribute.value;
243
+ if (babelTypes.isStringLiteral(value)) return value;
244
+ if (babelTypes.isJSXExpressionContainer(value)) {
245
+ const staticString = readStaticString(babelTypes, value.expression);
246
+ if (staticString !== void 0) return babelTypes.stringLiteral(staticString);
247
+ }
248
+ return;
249
+ }
250
+ };
251
+ /**
233
252
  * Resolves the namespace (dictionary key) for a compat caller call-site from
234
253
  * its `CompatNamespaceSource` configuration. Returns the static key, or
235
254
  * `'__default__'` when the configured argument is absent (caller falls back to
236
255
  * its default namespace), or `undefined` when the value is present but dynamic.
237
256
  */
238
257
  const resolveCompatNamespace = (babelTypes, callArguments, source) => {
258
+ if (source.from === "fixed") return source.value;
259
+ if (source.from === "path-first-segment") {
260
+ const firstArg = callArguments[0];
261
+ if (firstArg === void 0) return void 0;
262
+ const staticString = readStaticString(babelTypes, firstArg);
263
+ if (staticString !== void 0) return firstPathSegment(staticString);
264
+ if (babelTypes.isObjectExpression(firstArg)) {
265
+ const idValue = readObjectProperty(babelTypes, firstArg, "id");
266
+ if (idValue === "__default__") return "__default__";
267
+ if (idValue !== void 0) return firstPathSegment(idValue);
268
+ return;
269
+ }
270
+ return;
271
+ }
239
272
  if (source.from === "argument") {
240
273
  const argument = callArguments[source.index];
241
274
  if (argument === void 0) return "__default__";
@@ -282,8 +315,7 @@ const unwrapAwait = (babelTypes, path) => {
282
315
  * so renaming the compiled JSON keys would break runtime lookups. Pruning
283
316
  * (top-level field removal) remains safe because it preserves field names.
284
317
  */
285
- const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callExpressionPath, callerConfig, isSfcFile) => {
286
- const callArguments = callExpressionPath.node.arguments;
318
+ const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callArguments, callerConfig, isSfcFile, callExpressionPath) => {
287
319
  const resolvedNamespace = resolveCompatNamespace(babelTypes, callArguments, callerConfig.namespace);
288
320
  if (resolvedNamespace === void 0) return;
289
321
  const namespaceString = resolvedNamespace === "__default__" ? DEFAULT_COMPAT_NAMESPACE : resolvedNamespace;
@@ -295,6 +327,52 @@ const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callExpressionPat
295
327
  recordFieldUsage(pruneContext, dictionaryKey, "all");
296
328
  return;
297
329
  }
330
+ if (callerConfig.translationFunction === "all") {
331
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
332
+ return;
333
+ }
334
+ if (callerConfig.translationFunction === "self") {
335
+ const firstArg = callArguments[0];
336
+ if (!firstArg) {
337
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
338
+ return;
339
+ }
340
+ if (callerConfig.namespace.from === "path-first-segment") {
341
+ let fullId;
342
+ const staticString = readStaticString(babelTypes, firstArg);
343
+ if (staticString !== void 0) fullId = staticString;
344
+ else if (babelTypes.isObjectExpression(firstArg)) {
345
+ const idValue = readObjectProperty(babelTypes, firstArg, "id");
346
+ if (idValue !== void 0 && idValue !== "__default__") fullId = idValue;
347
+ }
348
+ if (fullId !== void 0) {
349
+ const field = fullId.split(".")[1];
350
+ if (field !== void 0) recordFieldUsage(pruneContext, dictionaryKey, new Set([field]));
351
+ else recordFieldUsage(pruneContext, dictionaryKey, "all");
352
+ return;
353
+ }
354
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
355
+ return;
356
+ }
357
+ const segment = readStaticFirstSegment(babelTypes, firstArg);
358
+ if (segment !== void 0) {
359
+ recordFieldUsage(pruneContext, dictionaryKey, new Set([segment]));
360
+ return;
361
+ }
362
+ if (babelTypes.isObjectExpression(firstArg)) {
363
+ const idValue = readObjectProperty(babelTypes, firstArg, "id");
364
+ if (idValue !== void 0 && idValue !== "__default__") {
365
+ recordFieldUsage(pruneContext, dictionaryKey, new Set([firstPathSegment(idValue)]));
366
+ return;
367
+ }
368
+ }
369
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
370
+ return;
371
+ }
372
+ if (!callExpressionPath) {
373
+ recordFieldUsage(pruneContext, dictionaryKey, "all");
374
+ return;
375
+ }
298
376
  const explicitKeyPrefix = resolveCompatKeyPrefix(babelTypes, callArguments, callerConfig.keyPrefix);
299
377
  if (explicitKeyPrefix === void 0) {
300
378
  recordFieldUsage(pruneContext, dictionaryKey, "all");
@@ -381,34 +459,44 @@ const makeUsageAnalyzerBabelPlugin = (pruneContext, options) => ({ types: babelT
381
459
  const hasNativeCallers = intlayerCallerLocalNameMap.size > 0;
382
460
  const hasCompatCallers = compatCallerLocalNameMap.size > 0 || methodCompatCallers.length > 0;
383
461
  if (!hasNativeCallers && !hasCompatCallers) return;
384
- programPath.traverse({ CallExpression: (callExpressionPath) => {
385
- const calleeNode = callExpressionPath.node.callee;
386
- let localCallerName;
387
- let isMethodCall = false;
388
- if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
389
- else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) {
390
- localCallerName = calleeNode.property.name;
391
- isMethodCall = true;
392
- }
393
- if (!localCallerName) return;
394
- if (intlayerCallerLocalNameMap.has(localCallerName)) {
395
- const callArguments = callExpressionPath.node.arguments;
396
- if (callArguments.length === 0) return;
397
- const dictionaryKey = readStaticString(babelTypes, callArguments[0]);
398
- if (!dictionaryKey) return;
399
- analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile);
400
- return;
401
- }
402
- const importedCompatCaller = compatCallerLocalNameMap.get(localCallerName);
403
- if (importedCompatCaller && !isMethodCall) {
404
- analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath, importedCompatCaller, isSfcFile);
405
- return;
406
- }
407
- if (isMethodCall) {
408
- const methodCaller = methodCompatCallers.find((caller) => caller.callerName === localCallerName);
409
- if (methodCaller) analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath, methodCaller, isSfcFile);
462
+ programPath.traverse({
463
+ CallExpression: (callExpressionPath) => {
464
+ const calleeNode = callExpressionPath.node.callee;
465
+ let localCallerName;
466
+ let isMethodCall = false;
467
+ if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
468
+ else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) {
469
+ localCallerName = calleeNode.property.name;
470
+ isMethodCall = true;
471
+ }
472
+ if (!localCallerName) return;
473
+ if (intlayerCallerLocalNameMap.has(localCallerName)) {
474
+ const callArguments = callExpressionPath.node.arguments;
475
+ if (callArguments.length === 0) return;
476
+ const dictionaryKey = readStaticString(babelTypes, callArguments[0]);
477
+ if (!dictionaryKey) return;
478
+ analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile);
479
+ return;
480
+ }
481
+ const importedCompatCaller = compatCallerLocalNameMap.get(localCallerName);
482
+ if (importedCompatCaller && !isMethodCall) {
483
+ analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath.node.arguments, importedCompatCaller, isSfcFile, callExpressionPath);
484
+ return;
485
+ }
486
+ if (isMethodCall) {
487
+ const methodCaller = methodCompatCallers.find((caller) => caller.callerName === localCallerName);
488
+ if (methodCaller) analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath.node.arguments, methodCaller, isSfcFile, callExpressionPath);
489
+ }
490
+ },
491
+ JSXOpeningElement: (jsxOpeningElementPath) => {
492
+ const nameNode = jsxOpeningElementPath.node.name;
493
+ if (!babelTypes.isJSXIdentifier(nameNode)) return;
494
+ const jsxCaller = compatCallerLocalNameMap.get(nameNode.name);
495
+ if (!jsxCaller?.jsxIdAttribute) return;
496
+ const idNode = readJsxAttributeString(babelTypes, jsxOpeningElementPath.node, jsxCaller.jsxIdAttribute);
497
+ analyzeNamespaceCallerUsage(babelTypes, pruneContext, idNode ? [idNode] : [], jsxCaller, isSfcFile);
410
498
  }
411
- } });
499
+ });
412
500
  } } }
413
501
  };
414
502
  };
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-usage-analyzer.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-usage-analyzer.ts"],"sourcesContent":["import type { NodePath, PluginObj, PluginPass } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\n\n// ── PruneContext types ────────────────────────────────────────────────────────\n\n/**\n * Dictionary field usage result for a single dictionary key.\n *\n * 'all' → could not determine statically which fields are used;\n * keep every field (no pruning possible).\n * Set<string> → the exact top-level content field names that were accessed.\n */\nexport type DictionaryFieldUsage = Set<string> | 'all';\n\n/**\n * One node in the nested field-rename tree.\n *\n * shortName – the compact alias assigned to this field name.\n * children – rename table for the next level of user-defined keys inside\n * this field's value (empty when the value is a leaf / primitive).\n */\nexport type NestedRenameEntry = {\n shortName: string;\n children: NestedRenameMap;\n};\n\n/** A level of the field-rename tree, mapping original field names to entries. */\nexport type NestedRenameMap = Map<string, NestedRenameEntry>;\n\n/**\n * Shared mutable state created once by the vite plugin and passed by reference\n * to the usage-analyzer (writer) and the prune/minify plugins (readers).\n *\n * All mutations happen during the usage-analysis `buildStart` phase; readers\n * only access this state during the subsequent `transform` phase.\n */\nexport type PruneContext = {\n /**\n * Maps every dictionary key seen in source files to the set of top-level\n * content fields statically accessed, or `'all'` when the access pattern\n * could not be determined.\n */\n dictionaryKeyToFieldUsageMap: Map<string, DictionaryFieldUsage>;\n\n /**\n * Dictionary keys for which the prune/minify step must be skipped entirely\n * because an edge case was detected during analysis or structure recognition.\n */\n dictionariesWithEdgeCases: Set<string>;\n\n /**\n * True if at least one source file failed to parse during the analysis phase.\n * The prune plugin uses this flag conservatively: any dictionary key without\n * a usage entry might have been referenced by the unparsable file.\n */\n hasUnparsableSourceFiles: boolean;\n\n /**\n * Maps dictionary keys to the source file paths where the result of\n * `useIntlayer` / `getIntlayer` was assigned to a plain variable, making\n * static field analysis impossible.\n */\n dictionaryKeysWithUntrackedBindings: Map<string, string[]>;\n\n /**\n * Maps each dictionary key to a nested field-rename tree built after the\n * usage analysis phase (only populated when `build.minify` is active and\n * the field usage for that dictionary is a finite `Set<string>`).\n */\n dictionaryKeyToFieldRenameMap: Map<string, NestedRenameMap>;\n\n /**\n * Maps each dictionary key to a per-field list of source locations where\n * the field value is consumed \"opaquely\" (passed as-is to a child component\n * or function argument). When a field is opaque AND has nested user-defined\n * structure, its children must not be renamed.\n *\n * Structure: dictionaryKey → fieldName → [\"filePath:line\", …]\n */\n dictionaryKeysWithOpaqueTopLevelFields: Map<string, Map<string, string[]>>;\n\n /**\n * Dictionary keys for which field-key renaming must be skipped even if a\n * finite field-usage set was determined.\n *\n * Populated for dictionaries whose plain-variable bindings were resolved by\n * the framework-specific extractor (Vue / Svelte SFCs), because the Babel\n * rename plugin cannot update the source-code property accesses for those\n * indirect patterns (Vue `.value.field` / Svelte `$store.field`).\n *\n * Pruning and basic minification still apply; only field-key renaming is\n * suppressed.\n */\n dictionariesSkippingFieldRename: Set<string>;\n\n /**\n * Plain variable bindings that require a framework-specific secondary pass.\n *\n * Populated during the Babel analysis phase for `.vue` and `.svelte` source\n * files where direct field access is not visible to Babel scope analysis:\n * - Vue: `content.value.fieldName` – the `.value` ref-accessor is hidden\n * - Svelte: `$varName.fieldName` – the `$` prefix creates a new identifier\n *\n * Structure: filePath → [{variableName, dictionaryKey}, …]\n */\n pendingFrameworkAnalysis: Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >;\n};\n\nexport const createPruneContext = (): PruneContext => ({\n dictionaryKeyToFieldUsageMap: new Map(),\n dictionariesWithEdgeCases: new Set(),\n hasUnparsableSourceFiles: false,\n dictionaryKeysWithUntrackedBindings: new Map(),\n dictionaryKeyToFieldRenameMap: new Map(),\n dictionaryKeysWithOpaqueTopLevelFields: new Map<\n string,\n Map<string, string[]>\n >(),\n dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n\n// ── Usage-analyzer Babel plugin ───────────────────────────────────────────────\n\n/** Canonical intlayer caller names that trigger usage analysis. */\nexport const INTLAYER_CALLER_NAMES = ['useIntlayer', 'getIntlayer'] as const;\nexport type IntlayerCallerName = (typeof INTLAYER_CALLER_NAMES)[number];\n\n// ── Compat-adapter namespace callers ──────────────────────────────────────────\n\n/**\n * Describes how a compat-adapter \"namespace caller\" exposes the dictionary key\n * (namespace) and the translation function `t`.\n *\n * Compat adapters (`@intlayer/react-i18next`, `@intlayer/next-intl`, …) expose\n * the original i18n library API while delegating to intlayer under the hood.\n * Their call sites look like:\n *\n * const { t } = useTranslation('about'); t('counter.label')\n * const t = useTranslations('about'); t('counter.label')\n * const t = await getTranslations('about');\n * const t = i18n.getFixedT(null, 'about', 'counter');\n * const { t } = useI18n({ namespace: 'about' });\n *\n * The dictionary key is the *namespace* argument and the consumed top-level\n * field is the **first segment** of every dot-path passed to `t()` (or the\n * first segment of `keyPrefix` when one is supplied).\n */\nexport type CompatNamespaceSource =\n /** Namespace is a positional argument (string literal or `{ namespace }`). */\n | { from: 'argument'; index: number }\n /** Namespace is a property of an options object argument. */\n | { from: 'option'; argumentIndex: number; property: string };\n\n/**\n * Configuration entry for a single compat namespace caller.\n */\nexport type CompatCallerConfig = {\n /** The imported (or method) function name, e.g. `'useTranslation'`. */\n callerName: string;\n /**\n * Module specifiers from which `callerName` must be imported to be treated as\n * a compat caller. Includes both the original library names and their\n * `@intlayer/*` adapter equivalents, because the bundler aliases the former\n * to the latter but user source code may import either.\n *\n * Ignored when `matchAsMethod` is `true`.\n */\n importSources: string[];\n /**\n * When `true`, the caller is matched by method name on any object\n * (`x.getFixedT(...)`) without an import check. Used for `i18next` instance\n * methods that are never imported as named specifiers.\n */\n matchAsMethod?: boolean;\n /** How the dictionary key (namespace) is read from the call arguments. */\n namespace: CompatNamespaceSource;\n /**\n * Optional location of a `keyPrefix` that prefixes every `t()` path. When a\n * static prefix is present, the only consumed top-level field is the first\n * segment of the prefix.\n */\n keyPrefix?: CompatNamespaceSource;\n /** How the translation function is obtained from the call result. */\n translationFunction: 'return-value' | 'destructured-t';\n};\n\n/**\n * Default registry of compat namespace callers.\n *\n * Intentionally empty — compat-specific caller configurations belong in their\n * respective adapter packages (e.g. `@intlayer/react-i18next/plugin`,\n * `@intlayer/vue-i18n/plugin`) and are injected into `makeUsageAnalyzerBabelPlugin`\n * via the `compatCallers` option. Centralising them here would couple the\n * core `@intlayer/babel` package to every compat adapter, which violates the\n * design principle that compat logic lives in compat packages.\n */\nexport const DEFAULT_COMPAT_CALLERS: CompatCallerConfig[] = [];\n\n/** Default namespace used by compat callers when no namespace argument is given. */\nconst DEFAULT_COMPAT_NAMESPACE = 'translation';\n\n/**\n * Records the usage of a specific dictionary key's fields into `pruneContext`.\n * Merges with any previously recorded usage for the same key.\n */\nconst recordFieldUsage = (\n pruneContext: PruneContext,\n dictionaryKey: string,\n fieldUsage: DictionaryFieldUsage\n): void => {\n const existingUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n if (existingUsage === 'all') return; // already saturated\n\n if (fieldUsage === 'all') {\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, 'all');\n return;\n }\n\n const mergedFieldSet =\n existingUsage instanceof Set\n ? new Set([...existingUsage, ...fieldUsage])\n : new Set(fieldUsage);\n\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, mergedFieldSet);\n};\n\n/**\n * Analyses how the result of a single `useIntlayer('key')` / `getIntlayer('key')`\n * call expression is consumed, then records the field usage into `pruneContext`.\n *\n * Recognised patterns:\n * const { fieldA, fieldB } = useIntlayer('key') → records {fieldA, fieldB}\n * useIntlayer('key').fieldA → records {fieldA}\n * useIntlayer('key')['fieldA'] → records {fieldA}\n * const { ...rest } = useIntlayer('key') → records 'all' (spread)\n * const result = useIntlayer('key') → records 'all' (untracked binding)\n */\nconst analyzeCallExpressionUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callExpressionPath: NodePath<BabelTypes.CallExpression>,\n dictionaryKey: string,\n currentSourceFilePath: string,\n isSfcFile: boolean\n): void => {\n const parentNode = callExpressionPath.parent;\n\n /** Mark the dictionary key as having an untracked binding in this file. */\n const markUntrackedBinding = (): void => {\n const existingPaths =\n pruneContext.dictionaryKeysWithUntrackedBindings.get(dictionaryKey) ?? [];\n if (!existingPaths.includes(currentSourceFilePath)) {\n pruneContext.dictionaryKeysWithUntrackedBindings.set(dictionaryKey, [\n ...existingPaths,\n currentSourceFilePath,\n ]);\n }\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n };\n\n /** Record that a field value is consumed opaquely (not further destructured). */\n const markOpaqueField = (\n fieldName: string,\n line: number | undefined\n ): void => {\n const fieldToLocations =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey) ??\n new Map<string, string[]>();\n const location =\n line !== undefined\n ? `${currentSourceFilePath}:${line}`\n : currentSourceFilePath;\n const locations = fieldToLocations.get(fieldName) ?? [];\n if (!locations.includes(location)) locations.push(location);\n fieldToLocations.set(fieldName, locations);\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.set(\n dictionaryKey,\n fieldToLocations\n );\n };\n\n /** Register a plain variable binding in an SFC file for a second-pass analysis. */\n const deferFrameworkAnalysis = (variableName: string): void => {\n const existing =\n pruneContext.pendingFrameworkAnalysis.get(currentSourceFilePath) ?? [];\n if (\n !existing.some(\n (e) =>\n e.variableName === variableName && e.dictionaryKey === dictionaryKey\n )\n ) {\n existing.push({ variableName, dictionaryKey });\n }\n pruneContext.pendingFrameworkAnalysis.set(currentSourceFilePath, existing);\n };\n\n /**\n * Analyses usage of a variable or member access to detect opaque\n * consumption (passing a dictionary field as-is to a prop or function).\n *\n * If a direct, non-chained consumption is found, it calls `markOpaqueField`.\n * Chained accesses (e.g. `field.sub`) are NOT considered opaque for `field`\n * because the renamer can safely track and update them.\n */\n const analyzeOpaqueUsage = (\n refPath: NodePath<BabelTypes.Node>,\n fieldName: string\n ): void => {\n const parentNode = refPath.parent;\n\n // 1. Chained member access (e.g. field.sub or field?.sub)\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object === refPath.node\n ) {\n // Chained access is safe: the renamer correctly updates it.\n return;\n }\n\n // 2. Destructuring (e.g. const { sub } = field)\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id) &&\n parentNode.init === refPath.node\n ) {\n // Destructuring is analogous to member access: safe.\n return;\n }\n\n // 3. Ignored patterns (e.g. array literals [content])\n if (babelTypes.isArrayExpression(parentNode)) {\n return;\n }\n\n // 4. Opaque consumption (passed to prop, function, etc.)\n markOpaqueField(fieldName, refPath.node.loc?.start.line);\n };\n\n /**\n * Helper to collect field names from an ObjectPattern (destructuring).\n * Returns true if successful, false if a rest element was found (meaning 'all').\n */\n const collectFieldsFromObjectPattern = (\n pattern: BabelTypes.ObjectPattern,\n initPath: NodePath<BabelTypes.Node>,\n targetSet: Set<string>\n ): boolean => {\n if (pattern.properties.some((prop) => babelTypes.isRestElement(prop))) {\n return false;\n }\n\n for (const property of pattern.properties) {\n let fieldName: string | undefined;\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key)\n ) {\n fieldName = property.key.name;\n } else if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isStringLiteral(property.key)\n ) {\n fieldName = property.key.value;\n }\n\n if (fieldName) {\n targetSet.add(fieldName);\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.value)\n ) {\n const variableBinding = initPath.scope.getBinding(\n property.value.name\n );\n if (variableBinding) {\n for (const refPath of variableBinding.referencePaths) {\n analyzeOpaqueUsage(refPath, fieldName);\n }\n }\n }\n }\n }\n return true;\n };\n\n // ── Pattern 1: const { fieldA, fieldB } = useIntlayer('key') ──────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n const accessedFieldNames = new Set<string>();\n if (\n collectFieldsFromObjectPattern(\n parentNode.id,\n callExpressionPath,\n accessedFieldNames\n )\n ) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // ── Pattern 2: useIntlayer('key').fieldA / useIntlayer('key')?.fieldA ──────\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object ===\n callExpressionPath.node\n ) {\n let fieldName: string | undefined;\n\n if (!parentNode.computed && babelTypes.isIdentifier(parentNode.property)) {\n fieldName = parentNode.property.name;\n } else if (\n parentNode.computed &&\n babelTypes.isStringLiteral(parentNode.property)\n ) {\n fieldName = parentNode.property.value;\n }\n\n if (fieldName) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([fieldName]));\n\n // Check for opaque usage (e.g. passed directly to a prop)\n const memberExprPath = callExpressionPath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n markUntrackedBinding();\n }\n return;\n }\n\n // ── Pattern 3: const content = useIntlayer('key') ─────────────────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n const variableName = parentNode.id.name;\n const variableBinding = callExpressionPath.scope.getBinding(variableName);\n\n if (!variableBinding) {\n markUntrackedBinding();\n return;\n }\n\n const accessedTopLevelFieldNames = new Set<string>();\n let hasUntrackedReferenceAccess = false;\n\n for (const variableReferencePath of variableBinding.referencePaths) {\n const referenceParentNode = variableReferencePath.parent;\n\n if (\n (babelTypes.isMemberExpression(referenceParentNode) ||\n babelTypes.isOptionalMemberExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.MemberExpression).object ===\n variableReferencePath.node\n ) {\n const memberExpressionNode =\n referenceParentNode as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpressionNode.computed &&\n babelTypes.isIdentifier(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.name;\n } else if (\n memberExpressionNode.computed &&\n babelTypes.isStringLiteral(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n\n // Check usage of the field to look for opaque consumption\n const memberExprPath = variableReferencePath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n // Dynamic computed access – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (babelTypes.isArrayExpression(referenceParentNode)) {\n // The binding's value escapes into an array literal\n // (e.g. `[appleWatch, airpods].map((entry) => entry.name)`). Its fields\n // are then accessed through iteration that Babel cannot follow back to\n // this dictionary, so we cannot know which fields are used. This is the\n // canonical meta-record / collection access pattern — conservatively\n // keep every field rather than pruning the content to nothing.\n hasUntrackedReferenceAccess = true;\n break;\n } else if (\n // Solid / Angular: content() signal accessor → content().field\n (babelTypes.isCallExpression(referenceParentNode) ||\n babelTypes.isOptionalCallExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.CallExpression).callee ===\n variableReferencePath.node\n ) {\n const callExprPath = variableReferencePath.parentPath;\n const callParent = callExprPath?.parent;\n\n if (\n callParent &&\n (babelTypes.isMemberExpression(callParent) ||\n babelTypes.isOptionalMemberExpression(callParent)) &&\n (callParent as BabelTypes.MemberExpression).object ===\n callExprPath?.node\n ) {\n // content().field\n const memberExpr = callParent as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpr.computed &&\n babelTypes.isIdentifier(memberExpr.property)\n ) {\n fieldName = memberExpr.property.name;\n } else if (\n memberExpr.computed &&\n babelTypes.isStringLiteral(memberExpr.property)\n ) {\n fieldName = memberExpr.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n const memberExprPath = callExprPath?.parentPath;\n if (memberExprPath) analyzeOpaqueUsage(memberExprPath, fieldName);\n } else {\n // content()[dynamicKey] – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (\n callParent &&\n babelTypes.isVariableDeclarator(callParent) &&\n babelTypes.isObjectPattern(callParent.id) &&\n callExprPath &&\n collectFieldsFromObjectPattern(\n callParent.id,\n callExprPath,\n accessedTopLevelFieldNames\n )\n ) {\n // const { title } = content()\n // fields already added to accessedTopLevelFieldNames by collectFieldsFromObjectPattern\n } else {\n // content() with no field access or passed opaquely → cannot prune\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else {\n // Variable used in a non-member-access context (spread, function arg, etc.)\n hasUntrackedReferenceAccess = true;\n break;\n }\n }\n\n if (hasUntrackedReferenceAccess) {\n markUntrackedBinding();\n } else if (isSfcFile) {\n // Vue / Svelte SFC: defer to the framework-specific extractor because\n // Babel scope analysis cannot see through `.value` or `$` indirection.\n deferFrameworkAnalysis(variableName);\n } else if (variableBinding.referencePaths.length === 0) {\n // Non-SFC file with no visible references – conservatively keep all fields.\n markUntrackedBinding();\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, accessedTopLevelFieldNames);\n }\n return;\n }\n\n // ── Pattern 4: bare call – result is discarded ─────────────────────────────\n if (babelTypes.isExpressionStatement(parentNode)) {\n return; // no usage to record\n }\n\n // ── Fallback: result passed as argument, used in ternary, etc. ─────────────\n markUntrackedBinding();\n};\n\n// ── Compat namespace-caller analysis ──────────────────────────────────────────\n\n/**\n * Reads a fully-static string from an AST node. Returns `undefined` for\n * dynamic values (identifiers, expressions, template literals with\n * interpolations, …).\n */\nconst readStaticString = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n if (!node) return undefined;\n if (babelTypes.isStringLiteral(node)) return node.value;\n if (\n babelTypes.isTemplateLiteral(node) &&\n node.expressions.length === 0 &&\n node.quasis.length === 1\n ) {\n return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n }\n return undefined;\n};\n\n/** Returns the first dot-path segment of a key, e.g. `'a.b.c'` → `'a'`. */\nconst firstPathSegment = (path: string): string => path.split('.')[0] ?? path;\n\n/**\n * Reads the static first dot-path segment from a `t()` first-argument node.\n *\n * t('counter.label') → 'counter'\n * t(`counter.${x}`) → 'counter' (static prefix before the first dot)\n * t(`${x}.label`) → undefined (dynamic first segment)\n * t(someVariable) → undefined\n */\nconst readStaticFirstSegment = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n const staticString = readStaticString(babelTypes, node);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Template literal whose first quasi already contains the dot delimiter, e.g.\n // `counter.${index}` → the leading `counter` segment is statically known.\n if (babelTypes.isTemplateLiteral(node) && node.quasis.length > 0) {\n const firstQuasi =\n node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n if (firstQuasi?.includes('.')) {\n return firstPathSegment(firstQuasi);\n }\n }\n return undefined;\n};\n\n/**\n * Reads a static string property from an object expression. Returns\n * `'__default__'` when the property is absent and `undefined` when present but\n * dynamic.\n */\nconst readObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): string | '__default__' | undefined => {\n for (const property of objectExpression.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n const keyMatches =\n (babelTypes.isIdentifier(property.key) &&\n property.key.name === propertyName) ||\n (babelTypes.isStringLiteral(property.key) &&\n property.key.value === propertyName);\n if (!keyMatches) continue;\n const staticValue = readStaticString(babelTypes, property.value);\n return staticValue ?? undefined; // present but dynamic → undefined\n }\n return '__default__'; // property absent\n};\n\n/**\n * Resolves the namespace (dictionary key) for a compat caller call-site from\n * its `CompatNamespaceSource` configuration. Returns the static key, or\n * `'__default__'` when the configured argument is absent (caller falls back to\n * its default namespace), or `undefined` when the value is present but dynamic.\n */\nconst resolveCompatNamespace = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource\n): string | '__default__' | undefined => {\n if (source.from === 'argument') {\n const argument = callArguments[source.index];\n if (argument === undefined) return '__default__';\n\n // Direct string namespace: useTranslations('about')\n const staticString = readStaticString(babelTypes, argument);\n if (staticString !== undefined) return staticString;\n\n // Object form: getTranslations({ locale, namespace: 'about' })\n if (babelTypes.isObjectExpression(argument)) {\n return readObjectProperty(babelTypes, argument, 'namespace');\n }\n return undefined; // present but dynamic\n }\n\n // from === 'option'\n const optionsArgument = callArguments[source.argumentIndex];\n if (optionsArgument === undefined) return '__default__';\n if (!babelTypes.isObjectExpression(optionsArgument)) return undefined;\n return readObjectProperty(babelTypes, optionsArgument, source.property);\n};\n\n/**\n * Resolves an optional `keyPrefix` for a compat caller. Returns the static\n * prefix string, `null` when no prefix is configured/present, or `undefined`\n * when a prefix is present but dynamic.\n */\nconst resolveCompatKeyPrefix = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource | undefined\n): string | null | undefined => {\n if (!source) return null;\n const resolved = resolveCompatNamespace(babelTypes, callArguments, source);\n if (resolved === '__default__') return null; // prefix absent\n return resolved; // string or undefined (dynamic)\n};\n\n/**\n * Climbs past an enclosing `await` expression so that\n * `const t = await getTranslations('ns')` is resolved to its variable\n * declarator the same way the synchronous form is.\n */\nconst unwrapAwait = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> => {\n const parentPath = path.parentPath;\n if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) {\n return parentPath;\n }\n return path;\n};\n\n/**\n * Analyses how the translation function produced by a compat namespace caller\n * (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,\n * `useI18n`) is consumed, then records the accessed top-level dictionary fields\n * into `pruneContext`.\n *\n * Dictionaries consumed this way are always added to\n * `dictionariesSkippingFieldRename`: the field accesses are string-literal\n * dot-paths inside `t()` calls, which the field-rename plugin cannot rewrite,\n * so renaming the compiled JSON keys would break runtime lookups. Pruning\n * (top-level field removal) remains safe because it preserves field names.\n */\nconst analyzeNamespaceCallerUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callExpressionPath: NodePath<BabelTypes.CallExpression>,\n callerConfig: CompatCallerConfig,\n isSfcFile: boolean\n): void => {\n const callArguments = callExpressionPath.node.arguments;\n\n // 1. Resolve the dictionary key (namespace).\n const resolvedNamespace = resolveCompatNamespace(\n babelTypes,\n callArguments,\n callerConfig.namespace\n );\n if (resolvedNamespace === undefined) return; // dynamic key – cannot attribute\n const namespaceString =\n resolvedNamespace === '__default__'\n ? DEFAULT_COMPAT_NAMESPACE\n : resolvedNamespace;\n\n // next-intl scopes nested objects through a dotted namespace\n // (`'about.counter'`): the dictionary key is the first segment and the\n // remainder is an implicit key prefix applied to every t() lookup.\n const namespaceSegments = namespaceString.split('.');\n const dictionaryKey = namespaceSegments[0] ?? namespaceString;\n const namespacePrefix =\n namespaceSegments.length > 1 ? namespaceSegments.slice(1).join('.') : null;\n\n // Compat string-path access is never renamable.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n // 2. SFC files (Vue / Svelte / Astro): the translation function is typically\n // invoked from the template, which Babel cannot see. Conservatively keep\n // every field to avoid pruning a template-only access.\n if (isSfcFile) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3. Resolve an optional explicit keyPrefix (e.g. react-i18next's\n // `{ keyPrefix }` option). A static prefix fixes the single consumed\n // top-level field regardless of the individual t() paths.\n const explicitKeyPrefix = resolveCompatKeyPrefix(\n babelTypes,\n callArguments,\n callerConfig.keyPrefix\n );\n if (explicitKeyPrefix === undefined) {\n // Prefix present but dynamic → unknown field set.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The namespace-derived prefix (next-intl) and the explicit keyPrefix option\n // (react-i18next / i18next) never coexist in practice; prefer whichever is\n // present. Either way, the consumed top-level field is the prefix's first\n // segment.\n const effectivePrefix = namespacePrefix ?? explicitKeyPrefix;\n if (effectivePrefix !== null) {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([firstPathSegment(effectivePrefix)])\n );\n return;\n }\n\n // 4. Locate the `t` function binding.\n let translationBinding: ReturnType<NodePath['scope']['getBinding']> | null =\n null;\n\n if (callerConfig.translationFunction === 'destructured-t') {\n // const { t } = useTranslation('ns')\n const parentNode = callExpressionPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n for (const property of parentNode.id.properties) {\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key) &&\n property.key.name === 't' &&\n babelTypes.isIdentifier(property.value)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(property.value.name) ?? null;\n }\n }\n }\n } else {\n // const t = useTranslations('ns') / const t = await getTranslations('ns')\n const resultPath = unwrapAwait(babelTypes, callExpressionPath);\n const parentNode = resultPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(parentNode.id.name) ?? null;\n }\n }\n\n // Could not statically locate `t` (e.g. result stored whole, re-exported) →\n // conservatively keep all fields.\n if (!translationBinding) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 5. Inspect every reference to `t`.\n const accessedFields = new Set<string>();\n let hasUntrackedUsage = false;\n\n for (const referencePath of translationBinding.referencePaths) {\n const parentNode = referencePath.parent;\n\n // Must be a direct call: t('path')\n const isDirectCall =\n (babelTypes.isCallExpression(parentNode) ||\n babelTypes.isOptionalCallExpression(parentNode)) &&\n (parentNode as BabelTypes.CallExpression).callee === referencePath.node;\n\n if (!isDirectCall) {\n // t passed as a prop / argument / reassigned → fields unknown.\n hasUntrackedUsage = true;\n break;\n }\n\n const firstArgument = (parentNode as BabelTypes.CallExpression)\n .arguments[0];\n const segment = readStaticFirstSegment(babelTypes, firstArgument);\n if (segment === undefined) {\n hasUntrackedUsage = true;\n break;\n }\n accessedFields.add(segment);\n }\n\n if (hasUntrackedUsage) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // Only record a finite field set when at least one field was actually\n // accessed. Recording an empty set would prune every field even though the\n // dictionary may be consumed elsewhere.\n if (accessedFields.size > 0) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFields);\n }\n};\n\n/**\n * Creates a Babel plugin that traverses source files and records which\n * top-level dictionary fields each `useIntlayer` / `getIntlayer` call-site —\n * and each configured compat namespace caller — accesses. Results are\n * accumulated into `pruneContext`.\n *\n * This plugin is analysis-only: it does not transform the code (`code: false`\n * should be passed to `transformAsync` when using it).\n *\n * @param pruneContext - Shared mutable state written by this plugin.\n * @param options - Optional overrides. `compatCallers` defaults to\n * {@link DEFAULT_COMPAT_CALLERS}; pass `[]` to disable\n * compat-adapter analysis entirely.\n */\nexport const makeUsageAnalyzerBabelPlugin =\n (\n pruneContext: PruneContext,\n options?: { compatCallers?: CompatCallerConfig[] }\n ) =>\n ({ types: babelTypes }: { types: typeof BabelTypes }): PluginObj => {\n const compatCallers = options?.compatCallers ?? DEFAULT_COMPAT_CALLERS;\n\n return {\n name: 'intlayer-usage-analyzer',\n visitor: {\n Program: {\n exit: (programPath, state: PluginPass) => {\n const currentSourceFilePath =\n state.file.opts.filename ?? 'unknown file';\n const isSfcFile =\n currentSourceFilePath.endsWith('.vue') ||\n currentSourceFilePath.endsWith('.svelte') ||\n currentSourceFilePath.endsWith('.astro');\n\n // Phase 1: collect local aliases for native intlayer callers and\n // for compat namespace callers (gated by import source).\n const intlayerCallerLocalNameMap = new Map<string, string>();\n const compatCallerLocalNameMap = new Map<\n string,\n CompatCallerConfig\n >();\n\n // Method-matched compat callers (e.g. i18next `getFixedT`) need no\n // import and are recognised by method name on any object.\n const methodCompatCallers = compatCallers.filter(\n (caller) => caller.matchAsMethod\n );\n\n programPath.traverse({\n ImportDeclaration: (importDeclarationPath) => {\n const importSource = importDeclarationPath.node.source.value;\n\n for (const importSpecifier of importDeclarationPath.node\n .specifiers) {\n if (!babelTypes.isImportSpecifier(importSpecifier)) continue;\n\n const importedName = babelTypes.isIdentifier(\n importSpecifier.imported\n )\n ? importSpecifier.imported.name\n : (importSpecifier.imported as BabelTypes.StringLiteral)\n .value;\n\n if (\n INTLAYER_CALLER_NAMES.includes(\n importedName as IntlayerCallerName\n )\n ) {\n intlayerCallerLocalNameMap.set(\n importSpecifier.local.name,\n importedName\n );\n continue;\n }\n\n const compatCaller = compatCallers.find(\n (caller) =>\n caller.callerName === importedName &&\n caller.importSources.includes(importSource)\n );\n if (compatCaller) {\n compatCallerLocalNameMap.set(\n importSpecifier.local.name,\n compatCaller\n );\n }\n }\n },\n });\n\n const hasNativeCallers = intlayerCallerLocalNameMap.size > 0;\n const hasCompatCallers =\n compatCallerLocalNameMap.size > 0 ||\n methodCompatCallers.length > 0;\n\n if (!hasNativeCallers && !hasCompatCallers) return;\n\n // Phase 2: analyse each call-site\n programPath.traverse({\n CallExpression: (callExpressionPath) => {\n const calleeNode = callExpressionPath.node.callee;\n let localCallerName: string | undefined;\n let isMethodCall = false;\n\n if (babelTypes.isIdentifier(calleeNode)) {\n localCallerName = calleeNode.name;\n } else if (\n babelTypes.isMemberExpression(calleeNode) &&\n babelTypes.isIdentifier(calleeNode.property)\n ) {\n localCallerName = calleeNode.property.name;\n isMethodCall = true;\n }\n\n if (!localCallerName) return;\n\n // Native intlayer caller (useIntlayer / getIntlayer)\n if (intlayerCallerLocalNameMap.has(localCallerName)) {\n const callArguments = callExpressionPath.node.arguments;\n if (callArguments.length === 0) return;\n\n const dictionaryKey = readStaticString(\n babelTypes,\n callArguments[0]\n );\n if (!dictionaryKey) return; // dynamic key\n\n analyzeCallExpressionUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n dictionaryKey,\n currentSourceFilePath,\n isSfcFile\n );\n return;\n }\n\n // Compat namespace caller (imported)\n const importedCompatCaller =\n compatCallerLocalNameMap.get(localCallerName);\n if (importedCompatCaller && !isMethodCall) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n importedCompatCaller,\n isSfcFile\n );\n return;\n }\n\n // Compat namespace caller (method-matched, e.g. getFixedT)\n if (isMethodCall) {\n const methodCaller = methodCompatCallers.find(\n (caller) => caller.callerName === localCallerName\n );\n if (methodCaller) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n methodCaller,\n isSfcFile\n );\n }\n }\n },\n });\n },\n },\n },\n };\n };\n"],"mappings":"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"}
1
+ {"version":3,"file":"babel-plugin-intlayer-usage-analyzer.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-usage-analyzer.ts"],"sourcesContent":["import type { NodePath, PluginObj, PluginPass } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\n\n// ── PruneContext types ────────────────────────────────────────────────────────\n\n/**\n * Dictionary field usage result for a single dictionary key.\n *\n * 'all' → could not determine statically which fields are used;\n * keep every field (no pruning possible).\n * Set<string> → the exact top-level content field names that were accessed.\n */\nexport type DictionaryFieldUsage = Set<string> | 'all';\n\n/**\n * One node in the nested field-rename tree.\n *\n * shortName – the compact alias assigned to this field name.\n * children – rename table for the next level of user-defined keys inside\n * this field's value (empty when the value is a leaf / primitive).\n */\nexport type NestedRenameEntry = {\n shortName: string;\n children: NestedRenameMap;\n};\n\n/** A level of the field-rename tree, mapping original field names to entries. */\nexport type NestedRenameMap = Map<string, NestedRenameEntry>;\n\n/**\n * Shared mutable state created once by the vite plugin and passed by reference\n * to the usage-analyzer (writer) and the prune/minify plugins (readers).\n *\n * All mutations happen during the usage-analysis `buildStart` phase; readers\n * only access this state during the subsequent `transform` phase.\n */\nexport type PruneContext = {\n /**\n * Maps every dictionary key seen in source files to the set of top-level\n * content fields statically accessed, or `'all'` when the access pattern\n * could not be determined.\n */\n dictionaryKeyToFieldUsageMap: Map<string, DictionaryFieldUsage>;\n\n /**\n * Dictionary keys for which the prune/minify step must be skipped entirely\n * because an edge case was detected during analysis or structure recognition.\n */\n dictionariesWithEdgeCases: Set<string>;\n\n /**\n * True if at least one source file failed to parse during the analysis phase.\n * The prune plugin uses this flag conservatively: any dictionary key without\n * a usage entry might have been referenced by the unparsable file.\n */\n hasUnparsableSourceFiles: boolean;\n\n /**\n * Maps dictionary keys to the source file paths where the result of\n * `useIntlayer` / `getIntlayer` was assigned to a plain variable, making\n * static field analysis impossible.\n */\n dictionaryKeysWithUntrackedBindings: Map<string, string[]>;\n\n /**\n * Maps each dictionary key to a nested field-rename tree built after the\n * usage analysis phase (only populated when `build.minify` is active and\n * the field usage for that dictionary is a finite `Set<string>`).\n */\n dictionaryKeyToFieldRenameMap: Map<string, NestedRenameMap>;\n\n /**\n * Maps each dictionary key to a per-field list of source locations where\n * the field value is consumed \"opaquely\" (passed as-is to a child component\n * or function argument). When a field is opaque AND has nested user-defined\n * structure, its children must not be renamed.\n *\n * Structure: dictionaryKey → fieldName → [\"filePath:line\", …]\n */\n dictionaryKeysWithOpaqueTopLevelFields: Map<string, Map<string, string[]>>;\n\n /**\n * Dictionary keys for which field-key renaming must be skipped even if a\n * finite field-usage set was determined.\n *\n * Populated for dictionaries whose plain-variable bindings were resolved by\n * the framework-specific extractor (Vue / Svelte SFCs), because the Babel\n * rename plugin cannot update the source-code property accesses for those\n * indirect patterns (Vue `.value.field` / Svelte `$store.field`).\n *\n * Pruning and basic minification still apply; only field-key renaming is\n * suppressed.\n */\n dictionariesSkippingFieldRename: Set<string>;\n\n /**\n * Plain variable bindings that require a framework-specific secondary pass.\n *\n * Populated during the Babel analysis phase for `.vue` and `.svelte` source\n * files where direct field access is not visible to Babel scope analysis:\n * - Vue: `content.value.fieldName` – the `.value` ref-accessor is hidden\n * - Svelte: `$varName.fieldName` – the `$` prefix creates a new identifier\n *\n * Structure: filePath → [{variableName, dictionaryKey}, …]\n */\n pendingFrameworkAnalysis: Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >;\n};\n\nexport const createPruneContext = (): PruneContext => ({\n dictionaryKeyToFieldUsageMap: new Map(),\n dictionariesWithEdgeCases: new Set(),\n hasUnparsableSourceFiles: false,\n dictionaryKeysWithUntrackedBindings: new Map(),\n dictionaryKeyToFieldRenameMap: new Map(),\n dictionaryKeysWithOpaqueTopLevelFields: new Map<\n string,\n Map<string, string[]>\n >(),\n dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n\n// ── Usage-analyzer Babel plugin ───────────────────────────────────────────────\n\n/** Canonical intlayer caller names that trigger usage analysis. */\nexport const INTLAYER_CALLER_NAMES = ['useIntlayer', 'getIntlayer'] as const;\nexport type IntlayerCallerName = (typeof INTLAYER_CALLER_NAMES)[number];\n\n// ── Compat-adapter namespace callers ──────────────────────────────────────────\n\n/**\n * Describes how a compat-adapter \"namespace caller\" exposes the dictionary key\n * (namespace) and the translation function `t`.\n *\n * Compat adapters (`@intlayer/react-i18next`, `@intlayer/next-intl`, …) expose\n * the original i18n library API while delegating to intlayer under the hood.\n * Their call sites look like:\n *\n * const { t } = useTranslation('about'); t('counter.label')\n * const t = useTranslations('about'); t('counter.label')\n * const t = await getTranslations('about');\n * const t = i18n.getFixedT(null, 'about', 'counter');\n * const { t } = useI18n({ namespace: 'about' });\n *\n * The dictionary key is the *namespace* argument and the consumed top-level\n * field is the **first segment** of every dot-path passed to `t()` (or the\n * first segment of `keyPrefix` when one is supplied).\n */\nexport type CompatNamespaceSource =\n /** Namespace is a positional argument (string literal or `{ namespace }`). */\n | { from: 'argument'; index: number }\n /** Namespace is a property of an options object argument. */\n | { from: 'option'; argumentIndex: number; property: string }\n /**\n * Namespace is a compile-time constant — the same dictionary key is used for\n * every call site. Used by single-catalog libraries such as lingui, where all\n * translations live in a `messages` dictionary regardless of call location.\n */\n | { from: 'fixed'; value: string }\n /**\n * Namespace is the first dot-segment of the `id` in the first call argument.\n *\n * Used for libraries like react-intl where the full dotted id encodes both the\n * dictionary key and the field path in a single string:\n * `formatMessage({ id: 'home.title' })` → dictionaryKey = `'home'`, field = `'title'`\n *\n * Works with both string form (`func('home.title')`) and descriptor form\n * (`func({ id: 'home.title', ... })`). The paired `translationFunction` should\n * be `'self'` so the field (second segment) is also extracted from the same call.\n */\n | { from: 'path-first-segment' };\n\n/**\n * Configuration entry for a single compat namespace caller.\n */\nexport type CompatCallerConfig = {\n /** The imported (or method) function name, e.g. `'useTranslation'`. */\n callerName: string;\n /**\n * Module specifiers from which `callerName` must be imported to be treated as\n * a compat caller. Includes both the original library names and their\n * `@intlayer/*` adapter equivalents, because the bundler aliases the former\n * to the latter but user source code may import either.\n *\n * Ignored when `matchAsMethod` is `true`.\n */\n importSources: string[];\n /**\n * When `true`, the caller is matched by method name on any object\n * (`x.getFixedT(...)`) without an import check. Used for `i18next` instance\n * methods that are never imported as named specifiers.\n */\n matchAsMethod?: boolean;\n /** How the dictionary key (namespace) is read from the call arguments. */\n namespace: CompatNamespaceSource;\n /**\n * Optional location of a `keyPrefix` that prefixes every `t()` path. When a\n * static prefix is present, the only consumed top-level field is the first\n * segment of the prefix.\n */\n keyPrefix?: CompatNamespaceSource;\n /**\n * How the translation function is obtained from the call result.\n *\n * - `'return-value'` — `const t = useTranslations('ns'); t('key')`\n * - `'destructured-t'` — `const { t } = useTranslation('ns'); t('key')`\n * - `'self'` — the caller IS the translation call;\n * the first argument is the message key.\n * Used for lingui's `i18n._('key')` / `i18n.t('key')`.\n * - `'all'` — mark the entire dictionary as used without tracking\n * individual fields. Use when static key analysis is\n * impossible (e.g. lingui hashed IDs, Angular templates).\n */\n translationFunction: 'return-value' | 'destructured-t' | 'self' | 'all';\n /**\n * When set, the caller is *also* matched as a JSX element whose local name is\n * `callerName` (gated by `importSources`). The named attribute is read as the\n * message id and analysed with the same `namespace` + `translationFunction`\n * (`'self'` / `'all'`) semantics as the call-expression form.\n *\n * Required for libraries with a JSX message component, e.g. react-intl's\n * `<FormattedMessage id=\"home.title\" />`. Without it, JSX usages are invisible\n * to the analyser and field-level pruning of the same dictionary becomes\n * unsafe (it could prune a field only referenced from JSX).\n */\n jsxIdAttribute?: string;\n};\n\n/**\n * Default registry of compat namespace callers.\n *\n * Intentionally empty — compat-specific caller configurations belong in their\n * respective adapter packages (e.g. `@intlayer/react-i18next/plugin`,\n * `@intlayer/vue-i18n/plugin`) and are injected into `makeUsageAnalyzerBabelPlugin`\n * via the `compatCallers` option. Centralising them here would couple the\n * core `@intlayer/babel` package to every compat adapter, which violates the\n * design principle that compat logic lives in compat packages.\n */\nexport const DEFAULT_COMPAT_CALLERS: CompatCallerConfig[] = [];\n\n/** Default namespace used by compat callers when no namespace argument is given. */\nconst DEFAULT_COMPAT_NAMESPACE = 'translation';\n\n/**\n * Records the usage of a specific dictionary key's fields into `pruneContext`.\n * Merges with any previously recorded usage for the same key.\n */\nconst recordFieldUsage = (\n pruneContext: PruneContext,\n dictionaryKey: string,\n fieldUsage: DictionaryFieldUsage\n): void => {\n const existingUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n if (existingUsage === 'all') return; // already saturated\n\n if (fieldUsage === 'all') {\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, 'all');\n return;\n }\n\n const mergedFieldSet =\n existingUsage instanceof Set\n ? new Set([...existingUsage, ...fieldUsage])\n : new Set(fieldUsage);\n\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, mergedFieldSet);\n};\n\n/**\n * Analyses how the result of a single `useIntlayer('key')` / `getIntlayer('key')`\n * call expression is consumed, then records the field usage into `pruneContext`.\n *\n * Recognised patterns:\n * const { fieldA, fieldB } = useIntlayer('key') → records {fieldA, fieldB}\n * useIntlayer('key').fieldA → records {fieldA}\n * useIntlayer('key')['fieldA'] → records {fieldA}\n * const { ...rest } = useIntlayer('key') → records 'all' (spread)\n * const result = useIntlayer('key') → records 'all' (untracked binding)\n */\nconst analyzeCallExpressionUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callExpressionPath: NodePath<BabelTypes.CallExpression>,\n dictionaryKey: string,\n currentSourceFilePath: string,\n isSfcFile: boolean\n): void => {\n const parentNode = callExpressionPath.parent;\n\n /** Mark the dictionary key as having an untracked binding in this file. */\n const markUntrackedBinding = (): void => {\n const existingPaths =\n pruneContext.dictionaryKeysWithUntrackedBindings.get(dictionaryKey) ?? [];\n if (!existingPaths.includes(currentSourceFilePath)) {\n pruneContext.dictionaryKeysWithUntrackedBindings.set(dictionaryKey, [\n ...existingPaths,\n currentSourceFilePath,\n ]);\n }\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n };\n\n /** Record that a field value is consumed opaquely (not further destructured). */\n const markOpaqueField = (\n fieldName: string,\n line: number | undefined\n ): void => {\n const fieldToLocations =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey) ??\n new Map<string, string[]>();\n const location =\n line !== undefined\n ? `${currentSourceFilePath}:${line}`\n : currentSourceFilePath;\n const locations = fieldToLocations.get(fieldName) ?? [];\n if (!locations.includes(location)) locations.push(location);\n fieldToLocations.set(fieldName, locations);\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.set(\n dictionaryKey,\n fieldToLocations\n );\n };\n\n /** Register a plain variable binding in an SFC file for a second-pass analysis. */\n const deferFrameworkAnalysis = (variableName: string): void => {\n const existing =\n pruneContext.pendingFrameworkAnalysis.get(currentSourceFilePath) ?? [];\n if (\n !existing.some(\n (e) =>\n e.variableName === variableName && e.dictionaryKey === dictionaryKey\n )\n ) {\n existing.push({ variableName, dictionaryKey });\n }\n pruneContext.pendingFrameworkAnalysis.set(currentSourceFilePath, existing);\n };\n\n /**\n * Analyses usage of a variable or member access to detect opaque\n * consumption (passing a dictionary field as-is to a prop or function).\n *\n * If a direct, non-chained consumption is found, it calls `markOpaqueField`.\n * Chained accesses (e.g. `field.sub`) are NOT considered opaque for `field`\n * because the renamer can safely track and update them.\n */\n const analyzeOpaqueUsage = (\n refPath: NodePath<BabelTypes.Node>,\n fieldName: string\n ): void => {\n const parentNode = refPath.parent;\n\n // 1. Chained member access (e.g. field.sub or field?.sub)\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object === refPath.node\n ) {\n // Chained access is safe: the renamer correctly updates it.\n return;\n }\n\n // 2. Destructuring (e.g. const { sub } = field)\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id) &&\n parentNode.init === refPath.node\n ) {\n // Destructuring is analogous to member access: safe.\n return;\n }\n\n // 3. Ignored patterns (e.g. array literals [content])\n if (babelTypes.isArrayExpression(parentNode)) {\n return;\n }\n\n // 4. Opaque consumption (passed to prop, function, etc.)\n markOpaqueField(fieldName, refPath.node.loc?.start.line);\n };\n\n /**\n * Helper to collect field names from an ObjectPattern (destructuring).\n * Returns true if successful, false if a rest element was found (meaning 'all').\n */\n const collectFieldsFromObjectPattern = (\n pattern: BabelTypes.ObjectPattern,\n initPath: NodePath<BabelTypes.Node>,\n targetSet: Set<string>\n ): boolean => {\n if (pattern.properties.some((prop) => babelTypes.isRestElement(prop))) {\n return false;\n }\n\n for (const property of pattern.properties) {\n let fieldName: string | undefined;\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key)\n ) {\n fieldName = property.key.name;\n } else if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isStringLiteral(property.key)\n ) {\n fieldName = property.key.value;\n }\n\n if (fieldName) {\n targetSet.add(fieldName);\n\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.value)\n ) {\n const variableBinding = initPath.scope.getBinding(\n property.value.name\n );\n if (variableBinding) {\n for (const refPath of variableBinding.referencePaths) {\n analyzeOpaqueUsage(refPath, fieldName);\n }\n }\n }\n }\n }\n return true;\n };\n\n // ── Pattern 1: const { fieldA, fieldB } = useIntlayer('key') ──────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n const accessedFieldNames = new Set<string>();\n if (\n collectFieldsFromObjectPattern(\n parentNode.id,\n callExpressionPath,\n accessedFieldNames\n )\n ) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // ── Pattern 2: useIntlayer('key').fieldA / useIntlayer('key')?.fieldA ──────\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object ===\n callExpressionPath.node\n ) {\n let fieldName: string | undefined;\n\n if (!parentNode.computed && babelTypes.isIdentifier(parentNode.property)) {\n fieldName = parentNode.property.name;\n } else if (\n parentNode.computed &&\n babelTypes.isStringLiteral(parentNode.property)\n ) {\n fieldName = parentNode.property.value;\n }\n\n if (fieldName) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([fieldName]));\n\n // Check for opaque usage (e.g. passed directly to a prop)\n const memberExprPath = callExpressionPath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n markUntrackedBinding();\n }\n return;\n }\n\n // ── Pattern 3: const content = useIntlayer('key') ─────────────────────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n const variableName = parentNode.id.name;\n const variableBinding = callExpressionPath.scope.getBinding(variableName);\n\n if (!variableBinding) {\n markUntrackedBinding();\n return;\n }\n\n const accessedTopLevelFieldNames = new Set<string>();\n let hasUntrackedReferenceAccess = false;\n\n for (const variableReferencePath of variableBinding.referencePaths) {\n const referenceParentNode = variableReferencePath.parent;\n\n if (\n (babelTypes.isMemberExpression(referenceParentNode) ||\n babelTypes.isOptionalMemberExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.MemberExpression).object ===\n variableReferencePath.node\n ) {\n const memberExpressionNode =\n referenceParentNode as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpressionNode.computed &&\n babelTypes.isIdentifier(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.name;\n } else if (\n memberExpressionNode.computed &&\n babelTypes.isStringLiteral(memberExpressionNode.property)\n ) {\n fieldName = memberExpressionNode.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n\n // Check usage of the field to look for opaque consumption\n const memberExprPath = variableReferencePath.parentPath;\n if (memberExprPath) {\n analyzeOpaqueUsage(memberExprPath, fieldName);\n }\n } else {\n // Dynamic computed access – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (babelTypes.isArrayExpression(referenceParentNode)) {\n // The binding's value escapes into an array literal\n // (e.g. `[appleWatch, airpods].map((entry) => entry.name)`). Its fields\n // are then accessed through iteration that Babel cannot follow back to\n // this dictionary, so we cannot know which fields are used. This is the\n // canonical meta-record / collection access pattern — conservatively\n // keep every field rather than pruning the content to nothing.\n hasUntrackedReferenceAccess = true;\n break;\n } else if (\n // Solid / Angular: content() signal accessor → content().field\n (babelTypes.isCallExpression(referenceParentNode) ||\n babelTypes.isOptionalCallExpression(referenceParentNode)) &&\n (referenceParentNode as BabelTypes.CallExpression).callee ===\n variableReferencePath.node\n ) {\n const callExprPath = variableReferencePath.parentPath;\n const callParent = callExprPath?.parent;\n\n if (\n callParent &&\n (babelTypes.isMemberExpression(callParent) ||\n babelTypes.isOptionalMemberExpression(callParent)) &&\n (callParent as BabelTypes.MemberExpression).object ===\n callExprPath?.node\n ) {\n // content().field\n const memberExpr = callParent as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (\n !memberExpr.computed &&\n babelTypes.isIdentifier(memberExpr.property)\n ) {\n fieldName = memberExpr.property.name;\n } else if (\n memberExpr.computed &&\n babelTypes.isStringLiteral(memberExpr.property)\n ) {\n fieldName = memberExpr.property.value;\n }\n\n if (fieldName) {\n accessedTopLevelFieldNames.add(fieldName);\n const memberExprPath = callExprPath?.parentPath;\n if (memberExprPath) analyzeOpaqueUsage(memberExprPath, fieldName);\n } else {\n // content()[dynamicKey] – cannot resolve statically\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else if (\n callParent &&\n babelTypes.isVariableDeclarator(callParent) &&\n babelTypes.isObjectPattern(callParent.id) &&\n callExprPath &&\n collectFieldsFromObjectPattern(\n callParent.id,\n callExprPath,\n accessedTopLevelFieldNames\n )\n ) {\n // const { title } = content()\n // fields already added to accessedTopLevelFieldNames by collectFieldsFromObjectPattern\n } else {\n // content() with no field access or passed opaquely → cannot prune\n hasUntrackedReferenceAccess = true;\n break;\n }\n } else {\n // Variable used in a non-member-access context (spread, function arg, etc.)\n hasUntrackedReferenceAccess = true;\n break;\n }\n }\n\n if (hasUntrackedReferenceAccess) {\n markUntrackedBinding();\n } else if (isSfcFile) {\n // Vue / Svelte SFC: defer to the framework-specific extractor because\n // Babel scope analysis cannot see through `.value` or `$` indirection.\n deferFrameworkAnalysis(variableName);\n } else if (variableBinding.referencePaths.length === 0) {\n // Non-SFC file with no visible references – conservatively keep all fields.\n markUntrackedBinding();\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, accessedTopLevelFieldNames);\n }\n return;\n }\n\n // ── Pattern 4: bare call – result is discarded ─────────────────────────────\n if (babelTypes.isExpressionStatement(parentNode)) {\n return; // no usage to record\n }\n\n // ── Fallback: result passed as argument, used in ternary, etc. ─────────────\n markUntrackedBinding();\n};\n\n// ── Compat namespace-caller analysis ──────────────────────────────────────────\n\n/**\n * Reads a fully-static string from an AST node. Returns `undefined` for\n * dynamic values (identifiers, expressions, template literals with\n * interpolations, …).\n */\nconst readStaticString = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n if (!node) return undefined;\n if (babelTypes.isStringLiteral(node)) return node.value;\n if (\n babelTypes.isTemplateLiteral(node) &&\n node.expressions.length === 0 &&\n node.quasis.length === 1\n ) {\n return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n }\n return undefined;\n};\n\n/** Returns the first dot-path segment of a key, e.g. `'a.b.c'` → `'a'`. */\nconst firstPathSegment = (path: string): string => path.split('.')[0] ?? path;\n\n/**\n * Reads the static first dot-path segment from a `t()` first-argument node.\n *\n * t('counter.label') → 'counter'\n * t(`counter.${x}`) → 'counter' (static prefix before the first dot)\n * t(`${x}.label`) → undefined (dynamic first segment)\n * t(someVariable) → undefined\n */\nconst readStaticFirstSegment = (\n babelTypes: typeof BabelTypes,\n node: BabelTypes.Node | null | undefined\n): string | undefined => {\n const staticString = readStaticString(babelTypes, node);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Template literal whose first quasi already contains the dot delimiter, e.g.\n // `counter.${index}` → the leading `counter` segment is statically known.\n if (babelTypes.isTemplateLiteral(node) && node.quasis.length > 0) {\n const firstQuasi =\n node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;\n if (firstQuasi?.includes('.')) {\n return firstPathSegment(firstQuasi);\n }\n }\n return undefined;\n};\n\n/**\n * Reads a static string property from an object expression. Returns\n * `'__default__'` when the property is absent and `undefined` when present but\n * dynamic.\n */\nconst readObjectProperty = (\n babelTypes: typeof BabelTypes,\n objectExpression: BabelTypes.ObjectExpression,\n propertyName: string\n): string | '__default__' | undefined => {\n for (const property of objectExpression.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n const keyMatches =\n (babelTypes.isIdentifier(property.key) &&\n property.key.name === propertyName) ||\n (babelTypes.isStringLiteral(property.key) &&\n property.key.value === propertyName);\n if (!keyMatches) continue;\n const staticValue = readStaticString(babelTypes, property.value);\n return staticValue ?? undefined; // present but dynamic → undefined\n }\n return '__default__'; // property absent\n};\n\n/**\n * Reads a named JSX attribute as a static string and returns it wrapped in a\n * `StringLiteral` node so the result can be fed to the same namespace resolver\n * as a call argument. Handles both `id=\"home.title\"` and `id={'home.title'}`.\n * Returns `undefined` when the attribute is absent or dynamic (`id={expr}`).\n */\nconst readJsxAttributeString = (\n babelTypes: typeof BabelTypes,\n openingElement: BabelTypes.JSXOpeningElement,\n attributeName: string\n): BabelTypes.StringLiteral | undefined => {\n for (const attribute of openingElement.attributes) {\n if (!babelTypes.isJSXAttribute(attribute)) continue;\n if (\n !babelTypes.isJSXIdentifier(attribute.name) ||\n attribute.name.name !== attributeName\n ) {\n continue;\n }\n\n const value = attribute.value;\n // id=\"home.title\"\n if (babelTypes.isStringLiteral(value)) return value;\n // id={'home.title'} / id={`home.title`}\n if (babelTypes.isJSXExpressionContainer(value)) {\n const staticString = readStaticString(babelTypes, value.expression);\n if (staticString !== undefined) {\n return babelTypes.stringLiteral(staticString);\n }\n }\n return undefined; // attribute present but dynamic\n }\n return undefined; // attribute absent\n};\n\n/**\n * Resolves the namespace (dictionary key) for a compat caller call-site from\n * its `CompatNamespaceSource` configuration. Returns the static key, or\n * `'__default__'` when the configured argument is absent (caller falls back to\n * its default namespace), or `undefined` when the value is present but dynamic.\n */\nconst resolveCompatNamespace = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource\n): string | '__default__' | undefined => {\n if (source.from === 'fixed') return source.value;\n\n if (source.from === 'path-first-segment') {\n const firstArg = callArguments[0];\n // No sensible default: the dictionary key is *derived* from the id, so an\n // absent id means the call cannot be attributed to any dictionary → skip.\n if (firstArg === undefined) return undefined;\n\n // String form: formatMessage('home.title', values)\n const staticString = readStaticString(babelTypes, firstArg);\n if (staticString !== undefined) return firstPathSegment(staticString);\n\n // Descriptor form: formatMessage({ id: 'home.title', ... }, values)\n if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue === '__default__') return '__default__'; // id absent\n if (idValue !== undefined) return firstPathSegment(idValue); // static id\n return undefined; // id present but dynamic\n }\n\n return undefined; // dynamic first argument\n }\n\n if (source.from === 'argument') {\n const argument = callArguments[source.index];\n if (argument === undefined) return '__default__';\n\n // Direct string namespace: useTranslations('about')\n const staticString = readStaticString(babelTypes, argument);\n if (staticString !== undefined) return staticString;\n\n // Object form: getTranslations({ locale, namespace: 'about' })\n if (babelTypes.isObjectExpression(argument)) {\n return readObjectProperty(babelTypes, argument, 'namespace');\n }\n return undefined; // present but dynamic\n }\n\n // from === 'option'\n const optionsArgument = callArguments[source.argumentIndex];\n if (optionsArgument === undefined) return '__default__';\n if (!babelTypes.isObjectExpression(optionsArgument)) return undefined;\n return readObjectProperty(babelTypes, optionsArgument, source.property);\n};\n\n/**\n * Resolves an optional `keyPrefix` for a compat caller. Returns the static\n * prefix string, `null` when no prefix is configured/present, or `undefined`\n * when a prefix is present but dynamic.\n */\nconst resolveCompatKeyPrefix = (\n babelTypes: typeof BabelTypes,\n callArguments: BabelTypes.CallExpression['arguments'],\n source: CompatNamespaceSource | undefined\n): string | null | undefined => {\n if (!source) return null;\n const resolved = resolveCompatNamespace(babelTypes, callArguments, source);\n if (resolved === '__default__') return null; // prefix absent\n return resolved; // string or undefined (dynamic)\n};\n\n/**\n * Climbs past an enclosing `await` expression so that\n * `const t = await getTranslations('ns')` is resolved to its variable\n * declarator the same way the synchronous form is.\n */\nconst unwrapAwait = (\n babelTypes: typeof BabelTypes,\n path: NodePath<BabelTypes.Node>\n): NodePath<BabelTypes.Node> => {\n const parentPath = path.parentPath;\n if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) {\n return parentPath;\n }\n return path;\n};\n\n/**\n * Analyses how the translation function produced by a compat namespace caller\n * (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,\n * `useI18n`) is consumed, then records the accessed top-level dictionary fields\n * into `pruneContext`.\n *\n * Dictionaries consumed this way are always added to\n * `dictionariesSkippingFieldRename`: the field accesses are string-literal\n * dot-paths inside `t()` calls, which the field-rename plugin cannot rewrite,\n * so renaming the compiled JSON keys would break runtime lookups. Pruning\n * (top-level field removal) remains safe because it preserves field names.\n */\nconst analyzeNamespaceCallerUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callArguments: BabelTypes.CallExpression['arguments'],\n callerConfig: CompatCallerConfig,\n isSfcFile: boolean,\n /**\n * The call-expression path, when the caller was matched as a call. Omitted\n * for JSX-element matches (`<FormattedMessage id>`), where only the static\n * `'self'` / `'all'` analysis paths apply — the binding-based\n * `'destructured-t'` / `'return-value'` paths require a call site.\n */\n callExpressionPath?: NodePath<BabelTypes.CallExpression>\n): void => {\n // 1. Resolve the dictionary key (namespace).\n const resolvedNamespace = resolveCompatNamespace(\n babelTypes,\n callArguments,\n callerConfig.namespace\n );\n if (resolvedNamespace === undefined) return; // dynamic key – cannot attribute\n const namespaceString =\n resolvedNamespace === '__default__'\n ? DEFAULT_COMPAT_NAMESPACE\n : resolvedNamespace;\n\n // next-intl scopes nested objects through a dotted namespace\n // (`'about.counter'`): the dictionary key is the first segment and the\n // remainder is an implicit key prefix applied to every t() lookup.\n const namespaceSegments = namespaceString.split('.');\n const dictionaryKey = namespaceSegments[0] ?? namespaceString;\n const namespacePrefix =\n namespaceSegments.length > 1 ? namespaceSegments.slice(1).join('.') : null;\n\n // Compat string-path access is never renamable.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n // 2. SFC files (Vue / Svelte / Astro): the translation function is typically\n // invoked from the template, which Babel cannot see. Conservatively keep\n // every field to avoid pruning a template-only access.\n if (isSfcFile) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3a. Mark the entire dictionary as used — static field analysis is not\n // applicable (e.g. lingui hashed IDs, Angular templates).\n if (callerConfig.translationFunction === 'all') {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3b. The caller IS the translation call (`translationFunction: 'self'`).\n // The first argument of the current call expression is the message key.\n // Used for lingui's `i18n._('key')` / `i18n.t('key')` and react-intl's\n // `intl.formatMessage({ id: 'key' })` patterns.\n if (callerConfig.translationFunction === 'self') {\n const firstArg = callArguments[0];\n if (!firstArg) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n if (callerConfig.namespace.from === 'path-first-segment') {\n // The dictionary key is already the first segment of the descriptor id.\n // The field to record is the SECOND segment (first level inside the dict).\n // e.g. formatMessage({ id: 'home.title' }) → dictionaryKey='home', field='title'\n let fullId: string | undefined;\n\n const staticString = readStaticString(babelTypes, firstArg);\n if (staticString !== undefined) {\n fullId = staticString;\n } else if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue !== undefined && idValue !== '__default__') {\n fullId = idValue;\n }\n }\n\n if (fullId !== undefined) {\n const segments = fullId.split('.');\n const field = segments[1];\n if (field !== undefined) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([field]));\n } else {\n // Single-segment id — the whole value is the dict key; no sub-field.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // Dynamic id — cannot prune.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // For 'fixed' / 'argument' / 'option' namespaces: the field is the first\n // dot-segment of the first argument (the message key itself).\n // String form: i18n._('home.title', values)\n const segment = readStaticFirstSegment(babelTypes, firstArg);\n if (segment !== undefined) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([segment]));\n return;\n }\n\n // Descriptor form: i18n._({ id: 'home.title', message: '...' }, values)\n if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue !== undefined && idValue !== '__default__') {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([firstPathSegment(idValue)])\n );\n return;\n }\n }\n\n // Dynamic key — cannot prune.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The remaining strategies (`'destructured-t'` / `'return-value'`) resolve\n // the `t` binding from the call site, so they require a call-expression path.\n // JSX-element matches never reach here (they use `'self'` / `'all'`); guard\n // defensively so a misconfigured JSX caller keeps every field instead of\n // throwing.\n if (!callExpressionPath) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3. Resolve an optional explicit keyPrefix (e.g. react-i18next's\n // `{ keyPrefix }` option). A static prefix fixes the single consumed\n // top-level field regardless of the individual t() paths.\n const explicitKeyPrefix = resolveCompatKeyPrefix(\n babelTypes,\n callArguments,\n callerConfig.keyPrefix\n );\n if (explicitKeyPrefix === undefined) {\n // Prefix present but dynamic → unknown field set.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The namespace-derived prefix (next-intl) and the explicit keyPrefix option\n // (react-i18next / i18next) never coexist in practice; prefer whichever is\n // present. Either way, the consumed top-level field is the prefix's first\n // segment.\n const effectivePrefix = namespacePrefix ?? explicitKeyPrefix;\n if (effectivePrefix !== null) {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([firstPathSegment(effectivePrefix)])\n );\n return;\n }\n\n // 4. Locate the `t` function binding.\n let translationBinding: ReturnType<NodePath['scope']['getBinding']> | null =\n null;\n\n if (callerConfig.translationFunction === 'destructured-t') {\n // const { t } = useTranslation('ns')\n const parentNode = callExpressionPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n for (const property of parentNode.id.properties) {\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key) &&\n property.key.name === 't' &&\n babelTypes.isIdentifier(property.value)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(property.value.name) ?? null;\n }\n }\n }\n } else {\n // const t = useTranslations('ns') / const t = await getTranslations('ns')\n const resultPath = unwrapAwait(babelTypes, callExpressionPath);\n const parentNode = resultPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(parentNode.id.name) ?? null;\n }\n }\n\n // Could not statically locate `t` (e.g. result stored whole, re-exported) →\n // conservatively keep all fields.\n if (!translationBinding) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 5. Inspect every reference to `t`.\n const accessedFields = new Set<string>();\n let hasUntrackedUsage = false;\n\n for (const referencePath of translationBinding.referencePaths) {\n const parentNode = referencePath.parent;\n\n // Must be a direct call: t('path')\n const isDirectCall =\n (babelTypes.isCallExpression(parentNode) ||\n babelTypes.isOptionalCallExpression(parentNode)) &&\n (parentNode as BabelTypes.CallExpression).callee === referencePath.node;\n\n if (!isDirectCall) {\n // t passed as a prop / argument / reassigned → fields unknown.\n hasUntrackedUsage = true;\n break;\n }\n\n const firstArgument = (parentNode as BabelTypes.CallExpression)\n .arguments[0];\n const segment = readStaticFirstSegment(babelTypes, firstArgument);\n if (segment === undefined) {\n hasUntrackedUsage = true;\n break;\n }\n accessedFields.add(segment);\n }\n\n if (hasUntrackedUsage) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // Only record a finite field set when at least one field was actually\n // accessed. Recording an empty set would prune every field even though the\n // dictionary may be consumed elsewhere.\n if (accessedFields.size > 0) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFields);\n }\n};\n\n/**\n * Creates a Babel plugin that traverses source files and records which\n * top-level dictionary fields each `useIntlayer` / `getIntlayer` call-site —\n * and each configured compat namespace caller — accesses. Results are\n * accumulated into `pruneContext`.\n *\n * This plugin is analysis-only: it does not transform the code (`code: false`\n * should be passed to `transformAsync` when using it).\n *\n * @param pruneContext - Shared mutable state written by this plugin.\n * @param options - Optional overrides. `compatCallers` defaults to\n * {@link DEFAULT_COMPAT_CALLERS}; pass `[]` to disable\n * compat-adapter analysis entirely.\n */\nexport const makeUsageAnalyzerBabelPlugin =\n (\n pruneContext: PruneContext,\n options?: { compatCallers?: CompatCallerConfig[] }\n ) =>\n ({ types: babelTypes }: { types: typeof BabelTypes }): PluginObj => {\n const compatCallers = options?.compatCallers ?? DEFAULT_COMPAT_CALLERS;\n\n return {\n name: 'intlayer-usage-analyzer',\n visitor: {\n Program: {\n exit: (programPath, state: PluginPass) => {\n const currentSourceFilePath =\n state.file.opts.filename ?? 'unknown file';\n const isSfcFile =\n currentSourceFilePath.endsWith('.vue') ||\n currentSourceFilePath.endsWith('.svelte') ||\n currentSourceFilePath.endsWith('.astro');\n\n // Phase 1: collect local aliases for native intlayer callers and\n // for compat namespace callers (gated by import source).\n const intlayerCallerLocalNameMap = new Map<string, string>();\n const compatCallerLocalNameMap = new Map<\n string,\n CompatCallerConfig\n >();\n\n // Method-matched compat callers (e.g. i18next `getFixedT`) need no\n // import and are recognised by method name on any object.\n const methodCompatCallers = compatCallers.filter(\n (caller) => caller.matchAsMethod\n );\n\n programPath.traverse({\n ImportDeclaration: (importDeclarationPath) => {\n const importSource = importDeclarationPath.node.source.value;\n\n for (const importSpecifier of importDeclarationPath.node\n .specifiers) {\n if (!babelTypes.isImportSpecifier(importSpecifier)) continue;\n\n const importedName = babelTypes.isIdentifier(\n importSpecifier.imported\n )\n ? importSpecifier.imported.name\n : (importSpecifier.imported as BabelTypes.StringLiteral)\n .value;\n\n if (\n INTLAYER_CALLER_NAMES.includes(\n importedName as IntlayerCallerName\n )\n ) {\n intlayerCallerLocalNameMap.set(\n importSpecifier.local.name,\n importedName\n );\n continue;\n }\n\n const compatCaller = compatCallers.find(\n (caller) =>\n caller.callerName === importedName &&\n caller.importSources.includes(importSource)\n );\n if (compatCaller) {\n compatCallerLocalNameMap.set(\n importSpecifier.local.name,\n compatCaller\n );\n }\n }\n },\n });\n\n const hasNativeCallers = intlayerCallerLocalNameMap.size > 0;\n const hasCompatCallers =\n compatCallerLocalNameMap.size > 0 ||\n methodCompatCallers.length > 0;\n\n if (!hasNativeCallers && !hasCompatCallers) return;\n\n // Phase 2: analyse each call-site\n programPath.traverse({\n CallExpression: (callExpressionPath) => {\n const calleeNode = callExpressionPath.node.callee;\n let localCallerName: string | undefined;\n let isMethodCall = false;\n\n if (babelTypes.isIdentifier(calleeNode)) {\n localCallerName = calleeNode.name;\n } else if (\n babelTypes.isMemberExpression(calleeNode) &&\n babelTypes.isIdentifier(calleeNode.property)\n ) {\n localCallerName = calleeNode.property.name;\n isMethodCall = true;\n }\n\n if (!localCallerName) return;\n\n // Native intlayer caller (useIntlayer / getIntlayer)\n if (intlayerCallerLocalNameMap.has(localCallerName)) {\n const callArguments = callExpressionPath.node.arguments;\n if (callArguments.length === 0) return;\n\n const dictionaryKey = readStaticString(\n babelTypes,\n callArguments[0]\n );\n if (!dictionaryKey) return; // dynamic key\n\n analyzeCallExpressionUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n dictionaryKey,\n currentSourceFilePath,\n isSfcFile\n );\n return;\n }\n\n // Compat namespace caller (imported)\n const importedCompatCaller =\n compatCallerLocalNameMap.get(localCallerName);\n if (importedCompatCaller && !isMethodCall) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath.node.arguments,\n importedCompatCaller,\n isSfcFile,\n callExpressionPath\n );\n return;\n }\n\n // Compat namespace caller (method-matched, e.g. getFixedT)\n if (isMethodCall) {\n const methodCaller = methodCompatCallers.find(\n (caller) => caller.callerName === localCallerName\n );\n if (methodCaller) {\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callExpressionPath.node.arguments,\n methodCaller,\n isSfcFile,\n callExpressionPath\n );\n }\n }\n },\n JSXOpeningElement: (jsxOpeningElementPath) => {\n const nameNode = jsxOpeningElementPath.node.name;\n if (!babelTypes.isJSXIdentifier(nameNode)) return;\n\n const jsxCaller = compatCallerLocalNameMap.get(nameNode.name);\n if (!jsxCaller?.jsxIdAttribute) return;\n\n // Read the configured id attribute as a static string. A missing\n // or dynamic id yields an empty argument list, which resolves the\n // namespace to undefined and is skipped (consistent with how a\n // dynamic `useIntlayer(key)` call is handled).\n const idNode = readJsxAttributeString(\n babelTypes,\n jsxOpeningElementPath.node,\n jsxCaller.jsxIdAttribute\n );\n\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n idNode ? [idNode] : [],\n jsxCaller,\n isSfcFile\n );\n },\n });\n },\n },\n },\n };\n };\n"],"mappings":"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"}
@@ -126,6 +126,29 @@ type CompatNamespaceSource = /** Namespace is a positional argument (string lite
126
126
  from: 'option';
127
127
  argumentIndex: number;
128
128
  property: string;
129
+ }
130
+ /**
131
+ * Namespace is a compile-time constant — the same dictionary key is used for
132
+ * every call site. Used by single-catalog libraries such as lingui, where all
133
+ * translations live in a `messages` dictionary regardless of call location.
134
+ */
135
+ | {
136
+ from: 'fixed';
137
+ value: string;
138
+ }
139
+ /**
140
+ * Namespace is the first dot-segment of the `id` in the first call argument.
141
+ *
142
+ * Used for libraries like react-intl where the full dotted id encodes both the
143
+ * dictionary key and the field path in a single string:
144
+ * `formatMessage({ id: 'home.title' })` → dictionaryKey = `'home'`, field = `'title'`
145
+ *
146
+ * Works with both string form (`func('home.title')`) and descriptor form
147
+ * (`func({ id: 'home.title', ... })`). The paired `translationFunction` should
148
+ * be `'self'` so the field (second segment) is also extracted from the same call.
149
+ */
150
+ | {
151
+ from: 'path-first-segment';
129
152
  };
130
153
  /**
131
154
  * Configuration entry for a single compat namespace caller.
@@ -153,8 +176,32 @@ type CompatCallerConfig = {
153
176
  * static prefix is present, the only consumed top-level field is the first
154
177
  * segment of the prefix.
155
178
  */
156
- keyPrefix?: CompatNamespaceSource; /** How the translation function is obtained from the call result. */
157
- translationFunction: 'return-value' | 'destructured-t';
179
+ keyPrefix?: CompatNamespaceSource;
180
+ /**
181
+ * How the translation function is obtained from the call result.
182
+ *
183
+ * - `'return-value'` — `const t = useTranslations('ns'); t('key')`
184
+ * - `'destructured-t'` — `const { t } = useTranslation('ns'); t('key')`
185
+ * - `'self'` — the caller IS the translation call;
186
+ * the first argument is the message key.
187
+ * Used for lingui's `i18n._('key')` / `i18n.t('key')`.
188
+ * - `'all'` — mark the entire dictionary as used without tracking
189
+ * individual fields. Use when static key analysis is
190
+ * impossible (e.g. lingui hashed IDs, Angular templates).
191
+ */
192
+ translationFunction: 'return-value' | 'destructured-t' | 'self' | 'all';
193
+ /**
194
+ * When set, the caller is *also* matched as a JSX element whose local name is
195
+ * `callerName` (gated by `importSources`). The named attribute is read as the
196
+ * message id and analysed with the same `namespace` + `translationFunction`
197
+ * (`'self'` / `'all'`) semantics as the call-expression form.
198
+ *
199
+ * Required for libraries with a JSX message component, e.g. react-intl's
200
+ * `<FormattedMessage id="home.title" />`. Without it, JSX usages are invisible
201
+ * to the analyser and field-level pruning of the same dictionary becomes
202
+ * unsafe (it could prune a field only referenced from JSX).
203
+ */
204
+ jsxIdAttribute?: string;
158
205
  };
159
206
  /**
160
207
  * Default registry of compat namespace callers.
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-intlayer-usage-analyzer.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-usage-analyzer.ts"],"mappings":";;;;;;AAYA;;;;;KAAY,oBAAA,GAAuB,GAAA;;;;;;;;KASvB,iBAAA;EACV,SAAA;EACA,QAAA,EAAU,eAAA;AAAA;;KAIA,eAAA,GAAkB,GAAA,SAAY,iBAAA;AAS1C;;;;;;;AAAA,KAAY,YAAA;EAiCqB;;;;;EA3B/B,4BAAA,EAA8B,GAAA,SAAY,oBAAA;EA+Db;;;;EAzD7B,yBAAA,EAA2B,GAAA;EAAA;;;;;EAO3B,wBAAA;EAc2C;;;;;EAP3C,mCAAA,EAAqC,GAAA;EA2CrC;;;;;EApCA,6BAAA,EAA+B,GAAA,SAAY,eAAA;EA0ChC;;;;;AAiBb;;;EAjDE,sCAAA,EAAwC,GAAA,SAAY,GAAA;EAiDsB;AAC5E;;;;;AAsBA;;;;;;EA1DE,+BAAA,EAAiC,GAAA;EA8Db;;;;AAKtB;;;;;;EAvDE,wBAAA,EAA0B,GAAA;IAEtB,YAAA;IAAsB,aAAA;EAAA;AAAA;AAAA,cAIf,kBAAA,QAAyB,YAAA;;cAiBzB,qBAAA;AAAA,KACD,kBAAA,WAA6B,qBAAA;;;;;AAwxBzC;;;;;;;;;;;;;;KAlwBY,qBAAA;EAEN,IAAA;EAAkB,KAAA;AAAA;EAElB,IAAA;EAAgB,aAAA;EAAuB,QAAA;AAAA;;;;KAKjC,kBAAA;yEAEV,UAAA;;;;;;;;;EASA,aAAA;;;;;;EAMA,aAAA;EAEA,SAAA,EAAW,qBAAA;;;;;;EAMX,SAAA,GAAY,qBAAA;EAEZ,mBAAA;AAAA;;;;;;;;;;;cAaW,sBAAA,EAAwB,kBAAA;;;;;;;;;;;;;;;cAitBxB,4BAAA,GAET,YAAA,EAAc,YAAA,EACd,OAAA;EAAY,aAAA,GAAgB,kBAAA;AAAA;EAE7B,KAAA,EAAA;AAAA;EAAyB,KAAA,SAAc,UAAA;AAAA,MAAe,SAAA"}
1
+ {"version":3,"file":"babel-plugin-intlayer-usage-analyzer.d.ts","names":[],"sources":["../../src/babel-plugin-intlayer-usage-analyzer.ts"],"mappings":";;;;;;AAYA;;;;;KAAY,oBAAA,GAAuB,GAAA;;;;;;;;KASvB,iBAAA;EACV,SAAA;EACA,QAAA,EAAU,eAAA;AAAA;;KAIA,eAAA,GAAkB,GAAA,SAAY,iBAAA;AAS1C;;;;;;;AAAA,KAAY,YAAA;EAiCqB;;;;;EA3B/B,4BAAA,EAA8B,GAAA,SAAY,oBAAA;EA+Db;;;;EAzD7B,yBAAA,EAA2B,GAAA;EAAA;;;;;EAO3B,wBAAA;EAc2C;;;;;EAP3C,mCAAA,EAAqC,GAAA;EA2CrC;;;;;EApCA,6BAAA,EAA+B,GAAA,SAAY,eAAA;EA0ChC;;;;;AAiBb;;;EAjDE,sCAAA,EAAwC,GAAA,SAAY,GAAA;EAiDsB;AAC5E;;;;;AAsBA;;;;;;EA1DE,+BAAA,EAAiC,GAAA;EA8Db;;;;;;;AAuBtB;;;EAzEE,wBAAA,EAA0B,GAAA;IAEtB,YAAA;IAAsB,aAAA;EAAA;AAAA;AAAA,cAIf,kBAAA,QAAyB,YAAA;;cAiBzB,qBAAA;AAAA,KACD,kBAAA,WAA6B,qBAAA;;;;AAgHzC;;;;;AAw2BA;;;;;;;;;;KAl8BY,qBAAA;EAEN,IAAA;EAAkB,KAAA;AAAA;EAElB,IAAA;EAAgB,aAAA;EAAuB,QAAA;AAAA;;;;;;;EAMvC,IAAA;EAAe,KAAA;AAAA;;;;;;;;;;;;;EAYf,IAAA;AAAA;;;;KAKM,kBAAA;yEAEV,UAAA;;;;;;;;;EASA,aAAA;;;;;;EAMA,aAAA;EAEA,SAAA,EAAW,qBAAA;;;;;;EAMX,SAAA,GAAY,qBAAA;;;;;;;;;;;;;EAaZ,mBAAA;;;;;;;;;;;;EAYA,cAAA;AAAA;;;;;;;;;;;cAaW,sBAAA,EAAwB,kBAAA;;;;;;;;;;;;;;;cAw2BxB,4BAAA,GAET,YAAA,EAAc,YAAA,EACd,OAAA;EAAY,aAAA,GAAgB,kBAAA;AAAA;EAE7B,KAAA,EAAA;AAAA;EAAyB,KAAA,SAAc,UAAA;AAAA,MAAe,SAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intlayer/babel",
3
- "version": "9.0.0-canary.1",
3
+ "version": "9.0.0-canary.2",
4
4
  "private": false,
5
5
  "description": "A Babel plugin for Intlayer that transforms declaration files and provides internationalization features during the build process according to the Intlayer configuration.",
6
6
  "keywords": [
@@ -81,12 +81,12 @@
81
81
  "@babel/plugin-syntax-jsx": "8.0.0",
82
82
  "@babel/traverse": "8.0.0",
83
83
  "@babel/types": "7.29.7",
84
- "@intlayer/chokidar": "9.0.0-canary.1",
85
- "@intlayer/config": "9.0.0-canary.1",
86
- "@intlayer/core": "9.0.0-canary.1",
87
- "@intlayer/dictionaries-entry": "9.0.0-canary.1",
88
- "@intlayer/types": "9.0.0-canary.1",
89
- "@intlayer/unmerged-dictionaries-entry": "9.0.0-canary.1",
84
+ "@intlayer/chokidar": "9.0.0-canary.2",
85
+ "@intlayer/config": "9.0.0-canary.2",
86
+ "@intlayer/core": "9.0.0-canary.2",
87
+ "@intlayer/dictionaries-entry": "9.0.0-canary.2",
88
+ "@intlayer/types": "9.0.0-canary.2",
89
+ "@intlayer/unmerged-dictionaries-entry": "9.0.0-canary.2",
90
90
  "@types/babel__core": "7.20.5",
91
91
  "@types/babel__generator": "7.27.0",
92
92
  "@types/babel__traverse": "7.28.0"
@@ -104,8 +104,8 @@
104
104
  "vitest": "4.1.9"
105
105
  },
106
106
  "peerDependencies": {
107
- "@intlayer/svelte-compiler": "9.0.0-canary.1",
108
- "@intlayer/vue-compiler": "9.0.0-canary.1"
107
+ "@intlayer/svelte-compiler": "9.0.0-canary.2",
108
+ "@intlayer/vue-compiler": "9.0.0-canary.2"
109
109
  },
110
110
  "peerDependenciesMeta": {
111
111
  "@intlayer/svelte-compiler": {