vite-intlayer 8.7.3 → 8.7.4

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.
@@ -57,6 +57,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext) => {
57
57
  return optimize === void 0 && isBuildCommand || optimize === true;
58
58
  };
59
59
  const isAnalysisEnabled = (_config, env) => !editorEnabled && (!!purge || !!minify) && isBuildOptimizeEnabled(_config, env);
60
+ let partiallyMinifiedDictionariesCount = 0;
60
61
  return [
61
62
  intlayerVueAsyncPlugin(intlayerConfig, transformableFilesList),
62
63
  {
@@ -154,47 +155,63 @@ const intlayerOptimize = async (intlayerConfig, pruneContext) => {
154
155
  `is assigned to a plain variable in:`,
155
156
  ...sourceFilePaths.map((filePath) => `\n - ${formatPath(filePath)}`)
156
157
  ], { level: "warn" });
157
- if (minify) for (const [dictionaryKey, fieldUsage] of pruneContext.dictionaryKeyToFieldUsageMap) {
158
- if (fieldUsage === "all") continue;
159
- if (dictionaryKeyToImportModeMap[dictionaryKey] === "fetch") continue;
160
- if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey)) continue;
161
- let dictionaryContent = null;
162
- const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);
163
- try {
164
- const raw = await readFile(staticJsonPath, "utf-8");
165
- dictionaryContent = JSON.parse(raw).content;
166
- } catch {
158
+ if (minify) {
159
+ for (const [dictionaryKey, fieldUsage] of pruneContext.dictionaryKeyToFieldUsageMap) {
160
+ if (fieldUsage === "all") continue;
161
+ if (dictionaryKeyToImportModeMap[dictionaryKey] === "fetch") continue;
162
+ if (pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey)) continue;
163
+ let dictionaryContent = null;
164
+ const staticJsonPath = join(dictionariesDir, `${dictionaryKey}.json`);
167
165
  try {
168
- const dynamicDir = join(dynamicDictionariesDir, dictionaryKey);
169
- const firstJsonFile = (await readdir(dynamicDir)).find((f) => f.endsWith(".json"));
170
- if (firstJsonFile) {
171
- const raw = await readFile(join(dynamicDir, firstJsonFile), "utf-8");
172
- dictionaryContent = JSON.parse(raw).content;
166
+ const raw = await readFile(staticJsonPath, "utf-8");
167
+ dictionaryContent = JSON.parse(raw).content;
168
+ } catch {
169
+ try {
170
+ const dynamicDir = join(dynamicDictionariesDir, dictionaryKey);
171
+ const firstJsonFile = (await readdir(dynamicDir)).find((f) => f.endsWith(".json"));
172
+ if (firstJsonFile) {
173
+ const raw = await readFile(join(dynamicDir, firstJsonFile), "utf-8");
174
+ dictionaryContent = JSON.parse(raw).content;
175
+ }
176
+ } catch {}
177
+ }
178
+ if (!dictionaryContent) continue;
179
+ const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);
180
+ const opaqueFieldMap = pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);
181
+ if (opaqueFieldMap) {
182
+ const dangerousEntries = [...opaqueFieldMap.entries()].filter(([fieldName]) => (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0);
183
+ if (dangerousEntries.length > 0) {
184
+ partiallyMinifiedDictionariesCount += 1;
185
+ logger([
186
+ `Dictionary`,
187
+ colorizeKey(dictionaryKey),
188
+ `partially minified.`,
189
+ ...dangerousEntries.flatMap(([fieldName, locations]) => [
190
+ `\n Opaque field:`,
191
+ colorize(`'${fieldName}'`, ANSIColors.BLUE),
192
+ `(nested keys preserved for stability).`,
193
+ ...locations.map((loc) => `\n at ${formatPath(loc)}`)
194
+ ])
195
+ ], {
196
+ level: "warn",
197
+ isVerbose: true
198
+ });
199
+ for (const [fieldName] of dangerousEntries) {
200
+ const entry = nestedRenameMap.get(fieldName);
201
+ if (entry) entry.children = /* @__PURE__ */ new Map();
202
+ }
173
203
  }
174
- } catch {}
175
- }
176
- if (!dictionaryContent) continue;
177
- const nestedRenameMap = buildNestedRenameMapFromContent(dictionaryContent);
178
- const opaqueFieldMap = pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey);
179
- if (opaqueFieldMap) {
180
- const dangerousEntries = [...opaqueFieldMap.entries()].filter(([fieldName]) => (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0);
181
- if (dangerousEntries.length > 0) {
182
- logger([
183
- `Dictionary`,
184
- colorizeKey(dictionaryKey),
185
- `cannot be minified.`,
186
- ...dangerousEntries.flatMap(([fieldName, locations]) => [
187
- `\n Reason: field`,
188
- colorize(`'${fieldName}'`, ANSIColors.BLUE),
189
- `is passed opaquely to a child component: nested keys cannot be safely renamed.`,
190
- ...locations.map((loc) => `\n at ${formatPath(loc)}`),
191
- `Fix: import useIntlayer directly in the component where it is used.`
192
- ])
193
- ], { level: "warn" });
194
- continue;
195
204
  }
205
+ if (nestedRenameMap.size > 0) pruneContext.dictionaryKeyToFieldRenameMap.set(dictionaryKey, nestedRenameMap);
196
206
  }
197
- if (nestedRenameMap.size > 0) pruneContext.dictionaryKeyToFieldRenameMap.set(dictionaryKey, nestedRenameMap);
207
+ if (partiallyMinifiedDictionariesCount > 0) runOnce(join(baseDir, ".intlayer", "cache", "intlayer-partial-minify-summary.lock"), () => {
208
+ logger([
209
+ `Partially minified`,
210
+ colorizeNumber(partiallyMinifiedDictionariesCount),
211
+ `dictionar${partiallyMinifiedDictionariesCount === 1 ? "y" : "ies"}`,
212
+ `(preserved nested keys for opaque fields).`
213
+ ]);
214
+ }, { cacheTimeoutMs: 1e3 * 5 });
198
215
  }
199
216
  }
200
217
  },
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerOptimizePlugin.mjs","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport {\n analyzeFieldUsageInFile,\n buildNestedRenameMapFromContent,\n INTLAYER_USAGE_REGEX,\n optimizeSourceFile,\n type PruneContext,\n renameFieldsInSourceFile,\n SOURCE_FILE_REGEX,\n} from '@intlayer/babel';\nimport {\n buildComponentFilesList,\n formatPath,\n runOnce,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { IMPORT_MODE } from '@intlayer/config/defaultValues';\nimport { colorize, colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport type { PluginOption } from 'vite';\nimport { intlayerVueAsyncPlugin } from './intlayerVueAsyncPlugin';\n\n// Plugin\n\n/**\n * Returns the Vite plugins responsible for the build optimisation step.\n *\n * Contains three internal plugins:\n *\n * 1. Vue async plugin – handles Vue SFC async script blocks.\n * 2. Usage analyser (`vite-intlayer-usage-analyzer`) – pre-scans every\n * component source file during `buildStart` to build the field-usage map\n * in `pruneContext`. This runs before any `transform` calls so the\n * downstream prune plugin always has complete data.\n * 3. Babel transform (`vite-intlayer-babel-transform`) – rewrites\n * `useIntlayer('key')` / `getIntlayer('key')` calls into\n * `useDictionary(_hash)` / `getDictionary(_hash)` and injects the\n * corresponding JSON (or dynamic `.mjs`) imports. Also applies field-name\n * renaming when `build.minify` is enabled.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared mutable state written here and read by the\n * prune and minify plugins. Pass `null` to skip\n * analysis (e.g. when both `purge` and `minify` are\n * disabled).\n */\nexport const intlayerOptimize = async (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): Promise<PluginOption[]> => {\n try {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge, minify } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const importMode =\n intlayerConfig.build.importMode ?? intlayerConfig.dictionary?.importMode;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n unmergedDictionariesDir,\n fetchDictionariesDir,\n mainDir,\n baseDir,\n } = intlayerConfig.system;\n\n const dictionariesEntryPath = join(mainDir, 'dictionaries.mjs');\n const unmergedDictionariesEntryPath = join(\n mainDir,\n 'unmerged_dictionaries.mjs'\n );\n const dynamicDictionariesEntryPath = join(\n mainDir,\n 'dynamic_dictionaries.mjs'\n );\n\n const componentFilesList = buildComponentFilesList(intlayerConfig);\n\n const transformableFilesList = [\n ...componentFilesList,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ];\n\n const dictionaries = getDictionaries(intlayerConfig);\n\n const dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch'\n > = {};\n (Object.values(dictionaries) as Dictionary[]).forEach((dictionary) => {\n dictionaryKeyToImportModeMap[dictionary.key] =\n dictionary.importMode ?? importMode ?? IMPORT_MODE;\n });\n\n const isBuildOptimizeEnabled = (\n _config: unknown,\n env: { command: string }\n ) => {\n const isBuildCommand = env.command === 'build';\n return (optimize === undefined && isBuildCommand) || optimize === true;\n };\n\n const isAnalysisEnabled = (_config: unknown, env: { command: string }) =>\n !editorEnabled &&\n (!!purge || !!minify) &&\n isBuildOptimizeEnabled(_config, env);\n\n return [\n intlayerVueAsyncPlugin(intlayerConfig, transformableFilesList),\n\n // Plugin 1: Usage analyser\n {\n name: 'vite-intlayer-usage-analyzer',\n enforce: 'pre',\n apply: isAnalysisEnabled,\n\n buildStart: async () => {\n if (!pruneContext) return;\n\n // Phase 1: Babel-based field-usage analysis for all component files\n await Promise.all(\n componentFilesList.map(async (sourceFilePath) => {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return;\n\n let sourceCode: string;\n try {\n sourceCode = await readFile(sourceFilePath, 'utf-8');\n } catch {\n return; // unreadable file – skip silently\n }\n\n if (!INTLAYER_USAGE_REGEX.test(sourceCode)) return;\n\n // For Vue/Svelte SFCs, the usage analyzer expects the raw script\n // content. `analyzeFieldUsageInFile` handles block extraction\n // internally via `extractScriptBlocks`.\n try {\n await analyzeFieldUsageInFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n } catch (parseError) {\n pruneContext.hasUnparsableSourceFiles = true;\n logger(\n [\n `Could not parse`,\n formatPath(sourceFilePath),\n `for field-usage analysis.`,\n 'Dictionaries whose usage cannot be confirmed will not be pruned.',\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n }\n })\n );\n\n // Phase 2: Framework-specific analysis for Vue / Svelte SFC bindings\n // that Babel scope analysis cannot resolve (`.value` indirection in\n // Vue, `$` prefix in Svelte).\n if (pruneContext.pendingFrameworkAnalysis.size > 0) {\n const vuePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n const sveltePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n\n for (const [\n filePath,\n entries,\n ] of pruneContext.pendingFrameworkAnalysis) {\n if (filePath.endsWith('.vue')) {\n vuePending.set(filePath, entries);\n } else if (filePath.endsWith('.svelte')) {\n sveltePending.set(filePath, entries);\n }\n }\n\n /** Merge framework-extracted field usage into pruneContext. */\n const mergeFrameworkResult = (\n dictionaryKey: string,\n fields: Set<string> | undefined\n ): void => {\n if (fields && fields.size > 0) {\n // The Babel rename plugin cannot update source-code property\n // accesses for SFC indirect patterns → suppress field renaming.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n const existing =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (existing === 'all') return;\n\n const merged =\n existing instanceof Set\n ? new Set([...existing, ...fields])\n : new Set(fields);\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n merged\n );\n } else {\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n 'all'\n );\n }\n };\n\n // Vue files\n if (vuePending.size > 0) {\n let extractVueIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const vueCompiler = await import('@intlayer/vue-compiler');\n extractVueIntlayerFieldUsage =\n vueCompiler.extractVueIntlayerFieldUsage;\n } catch {\n // @intlayer/vue-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of vuePending) {\n if (!extractVueIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractVueIntlayerFieldUsage(fileCode, entries);\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n\n // Svelte files\n if (sveltePending.size > 0) {\n let extractSvelteIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const svelteCompiler = await import(\n '@intlayer/svelte-compiler'\n );\n extractSvelteIntlayerFieldUsage =\n svelteCompiler.extractSvelteIntlayerFieldUsage;\n } catch {\n // @intlayer/svelte-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of sveltePending) {\n if (!extractSvelteIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractSvelteIntlayerFieldUsage(\n fileCode,\n entries\n );\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n }\n\n // Phase 3: Warn about untracked bindings (plain variable assignments)\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n\n // Phase 4: Build field-rename map for minification\n // Reads each compiled dictionary JSON to discover the full nested\n // user-defined field structure, then builds a NestedRenameMap that\n // assigns short alphabetic aliases at every level.\n if (minify) {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n\n // Fetch-mode dictionaries are served from a remote API using\n // original field names – renaming would break the client/server\n // contract.\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch')\n continue;\n\n // SFC indirect access: skip field rename for these dictionaries\n // to avoid a JSON ↔ source mismatch at runtime.\n if (\n pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey)\n )\n continue;\n\n // Read dictionary content (static JSON first, then dynamic per-locale)\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(\n dictionariesDir,\n `${dictionaryKey}.json`\n );\n try {\n const raw = await readFile(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n try {\n const dynamicDir = join(\n dynamicDictionariesDir,\n dictionaryKey\n );\n const localeFiles = await readdir(dynamicDir);\n const firstJsonFile = localeFiles.find((f) =>\n f.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = await readFile(\n join(dynamicDir, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary file not readable – skip rename for this key\n }\n }\n\n if (!dictionaryContent) continue;\n\n // Build the rename map from ALL user-defined fields in the\n // dictionary — not just the ones statically consumed by source\n // files. Using the full set ensures that:\n // 1. Every field in the compiled JSON is renamed (even if\n // pruned-out fields still appear when purge is disabled).\n // 2. The short-name assignment is stable: the alphabetical\n // order of all fields determines each short name, so adding\n // or removing a consumer never changes names for others.\n // 3. There is no source ↔ JSON mismatch: both sides use the\n // identical map regardless of which subset is consumed.\n const nestedRenameMap =\n buildNestedRenameMapFromContent(dictionaryContent);\n\n // Skip dictionaries whose opaque fields have nested user-defined\n // structure – renaming those sub-keys would silently break child\n // components that consume the field value as-is.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(\n dictionaryKey\n );\n\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n\n if (dangerousEntries.length > 0) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be minified.`,\n ...dangerousEntries.flatMap(([fieldName, locations]) => [\n `\\n Reason: field`,\n colorize(`'${fieldName}'`, ANSIColors.BLUE),\n `is passed opaquely to a child component: nested keys cannot be safely renamed.`,\n ...locations.map(\n (loc) => `\\n at ${formatPath(loc)}`\n ),\n `Fix: import useIntlayer directly in the component where it is used.`,\n ]),\n ],\n { level: 'warn' }\n );\n continue;\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n }\n },\n },\n\n // Plugin 2: Babel transform\n {\n name: 'vite-intlayer-babel-transform',\n enforce: 'post', // Run after framework transformations (e.g. Vue SFC)\n apply: (_config, env) => {\n const isBuildCommand = env.command === 'build';\n const isEnabled =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand || !isEnabled) return false;\n\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-optimize-plugin-enabled.lock'\n ),\n () =>\n logger([\n `Build optimization ${colorize('enabled', ANSIColors.GREEN)}`,\n colorize('(import mode:', ANSIColors.GREY_DARK),\n colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),\n colorize(')', ANSIColors.GREY_DARK),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n },\n\n transform: async (sourceCode, moduleId) => {\n // Strip query parameters added by Vue/Svelte loaders\n // e.g. \"HelloWorld.vue?vue&type=script&setup=true&lang.ts\" → \"HelloWorld.vue\"\n const sourceFilePath = moduleId.split('?', 1)[0];\n\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return null;\n if (!transformableFilesList.includes(sourceFilePath)) return null;\n\n const isDictionaryEntryFile = [\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ].includes(sourceFilePath);\n\n const isUsingIntlayer = INTLAYER_USAGE_REGEX.test(sourceCode);\n if (!isUsingIntlayer && !isDictionaryEntryFile) return null;\n\n // Step 1: Field rename (must run before the optimize pass, which\n // replaces useIntlayer → useDictionary and erases the dictionary key)\n let codeToOptimize = sourceCode;\n\n if (pruneContext && isUsingIntlayer) {\n const renamedCode = await renameFieldsInSourceFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n if (renamedCode) {\n codeToOptimize = renamedCode;\n }\n }\n\n // Step 2: Optimize (useIntlayer('key') → useDictionary(_hash))\n const transformResult = await optimizeSourceFile(\n codeToOptimize,\n sourceFilePath,\n {\n optimize,\n dictionariesDir,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n unmergedDictionariesDir,\n dynamicDictionariesDir,\n dynamicDictionariesEntryPath,\n fetchDictionariesDir,\n fetchDictionariesEntryPath: join(\n mainDir,\n 'fetch_dictionaries.mjs'\n ),\n importMode,\n filesList: transformableFilesList,\n replaceDictionaryEntry: true,\n dictionaryModeMap: dictionaryKeyToImportModeMap,\n }\n );\n\n if (!transformResult) return null;\n\n return {\n code: transformResult.code,\n map: transformResult.map as any,\n };\n },\n },\n ];\n } catch (pluginInitError) {\n console.warn(\n '[vite-intlayer] Failed to initialise optimization plugin:',\n pluginInitError\n );\n return [];\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"intlayerOptimizePlugin.mjs","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport {\n analyzeFieldUsageInFile,\n buildNestedRenameMapFromContent,\n INTLAYER_USAGE_REGEX,\n optimizeSourceFile,\n type PruneContext,\n renameFieldsInSourceFile,\n SOURCE_FILE_REGEX,\n} from '@intlayer/babel';\nimport {\n buildComponentFilesList,\n formatPath,\n runOnce,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { IMPORT_MODE } from '@intlayer/config/defaultValues';\nimport { colorize, colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport type { PluginOption } from 'vite';\nimport { intlayerVueAsyncPlugin } from './intlayerVueAsyncPlugin';\n\n// Plugin\n\n/**\n * Returns the Vite plugins responsible for the build optimisation step.\n *\n * Contains three internal plugins:\n *\n * 1. Vue async plugin – handles Vue SFC async script blocks.\n * 2. Usage analyser (`vite-intlayer-usage-analyzer`) – pre-scans every\n * component source file during `buildStart` to build the field-usage map\n * in `pruneContext`. This runs before any `transform` calls so the\n * downstream prune plugin always has complete data.\n * 3. Babel transform (`vite-intlayer-babel-transform`) – rewrites\n * `useIntlayer('key')` / `getIntlayer('key')` calls into\n * `useDictionary(_hash)` / `getDictionary(_hash)` and injects the\n * corresponding JSON (or dynamic `.mjs`) imports. Also applies field-name\n * renaming when `build.minify` is enabled.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared mutable state written here and read by the\n * prune and minify plugins. Pass `null` to skip\n * analysis (e.g. when both `purge` and `minify` are\n * disabled).\n */\nexport const intlayerOptimize = async (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): Promise<PluginOption[]> => {\n try {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge, minify } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const importMode =\n intlayerConfig.build.importMode ?? intlayerConfig.dictionary?.importMode;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n unmergedDictionariesDir,\n fetchDictionariesDir,\n mainDir,\n baseDir,\n } = intlayerConfig.system;\n\n const dictionariesEntryPath = join(mainDir, 'dictionaries.mjs');\n const unmergedDictionariesEntryPath = join(\n mainDir,\n 'unmerged_dictionaries.mjs'\n );\n const dynamicDictionariesEntryPath = join(\n mainDir,\n 'dynamic_dictionaries.mjs'\n );\n\n const componentFilesList = buildComponentFilesList(intlayerConfig);\n\n const transformableFilesList = [\n ...componentFilesList,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ];\n\n const dictionaries = getDictionaries(intlayerConfig);\n\n const dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch'\n > = {};\n (Object.values(dictionaries) as Dictionary[]).forEach((dictionary) => {\n dictionaryKeyToImportModeMap[dictionary.key] =\n dictionary.importMode ?? importMode ?? IMPORT_MODE;\n });\n\n const isBuildOptimizeEnabled = (\n _config: unknown,\n env: { command: string }\n ) => {\n const isBuildCommand = env.command === 'build';\n return (optimize === undefined && isBuildCommand) || optimize === true;\n };\n\n const isAnalysisEnabled = (_config: unknown, env: { command: string }) =>\n !editorEnabled &&\n (!!purge || !!minify) &&\n isBuildOptimizeEnabled(_config, env);\n\n let partiallyMinifiedDictionariesCount = 0;\n\n return [\n intlayerVueAsyncPlugin(intlayerConfig, transformableFilesList),\n\n // Plugin 1: Usage analyser\n {\n name: 'vite-intlayer-usage-analyzer',\n enforce: 'pre',\n apply: isAnalysisEnabled,\n\n buildStart: async () => {\n if (!pruneContext) return;\n\n // Phase 1: Babel-based field-usage analysis for all component files\n await Promise.all(\n componentFilesList.map(async (sourceFilePath) => {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return;\n\n let sourceCode: string;\n try {\n sourceCode = await readFile(sourceFilePath, 'utf-8');\n } catch {\n return; // unreadable file – skip silently\n }\n\n if (!INTLAYER_USAGE_REGEX.test(sourceCode)) return;\n\n // For Vue/Svelte SFCs, the usage analyzer expects the raw script\n // content. `analyzeFieldUsageInFile` handles block extraction\n // internally via `extractScriptBlocks`.\n try {\n await analyzeFieldUsageInFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n } catch (parseError) {\n pruneContext.hasUnparsableSourceFiles = true;\n logger(\n [\n `Could not parse`,\n formatPath(sourceFilePath),\n `for field-usage analysis.`,\n 'Dictionaries whose usage cannot be confirmed will not be pruned.',\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n }\n })\n );\n\n // Phase 2: Framework-specific analysis for Vue / Svelte SFC bindings\n // that Babel scope analysis cannot resolve (`.value` indirection in\n // Vue, `$` prefix in Svelte).\n if (pruneContext.pendingFrameworkAnalysis.size > 0) {\n const vuePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n const sveltePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n\n for (const [\n filePath,\n entries,\n ] of pruneContext.pendingFrameworkAnalysis) {\n if (filePath.endsWith('.vue')) {\n vuePending.set(filePath, entries);\n } else if (filePath.endsWith('.svelte')) {\n sveltePending.set(filePath, entries);\n }\n }\n\n /** Merge framework-extracted field usage into pruneContext. */\n const mergeFrameworkResult = (\n dictionaryKey: string,\n fields: Set<string> | undefined\n ): void => {\n if (fields && fields.size > 0) {\n // The Babel rename plugin cannot update source-code property\n // accesses for SFC indirect patterns → suppress field renaming.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n const existing =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (existing === 'all') return;\n\n const merged =\n existing instanceof Set\n ? new Set([...existing, ...fields])\n : new Set(fields);\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n merged\n );\n } else {\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n 'all'\n );\n }\n };\n\n // Vue files\n if (vuePending.size > 0) {\n let extractVueIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const vueCompiler = await import('@intlayer/vue-compiler');\n extractVueIntlayerFieldUsage =\n vueCompiler.extractVueIntlayerFieldUsage;\n } catch {\n // @intlayer/vue-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of vuePending) {\n if (!extractVueIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractVueIntlayerFieldUsage(fileCode, entries);\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n\n // Svelte files\n if (sveltePending.size > 0) {\n let extractSvelteIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const svelteCompiler = await import(\n '@intlayer/svelte-compiler'\n );\n extractSvelteIntlayerFieldUsage =\n svelteCompiler.extractSvelteIntlayerFieldUsage;\n } catch {\n // @intlayer/svelte-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of sveltePending) {\n if (!extractSvelteIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractSvelteIntlayerFieldUsage(\n fileCode,\n entries\n );\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n }\n\n // Phase 3: Warn about untracked bindings (plain variable assignments)\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n\n // Phase 4: Build field-rename map for minification\n // Reads each compiled dictionary JSON to discover the full nested\n // user-defined field structure, then builds a NestedRenameMap that\n // assigns short alphabetic aliases at every level.\n if (minify) {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n\n // Fetch-mode dictionaries are served from a remote API using\n // original field names – renaming would break the client/server\n // contract.\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch')\n continue;\n\n // SFC indirect access: skip field rename for these dictionaries\n // to avoid a JSON ↔ source mismatch at runtime.\n if (\n pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey)\n )\n continue;\n\n // Read dictionary content (static JSON first, then dynamic per-locale)\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(\n dictionariesDir,\n `${dictionaryKey}.json`\n );\n try {\n const raw = await readFile(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n try {\n const dynamicDir = join(\n dynamicDictionariesDir,\n dictionaryKey\n );\n const localeFiles = await readdir(dynamicDir);\n const firstJsonFile = localeFiles.find((f) =>\n f.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = await readFile(\n join(dynamicDir, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary file not readable – skip rename for this key\n }\n }\n\n if (!dictionaryContent) continue;\n\n // Build the rename map from ALL user-defined fields in the\n // dictionary — not just the ones statically consumed by source\n // files. Using the full set ensures that:\n // 1. Every field in the compiled JSON is renamed (even if\n // pruned-out fields still appear when purge is disabled).\n // 2. The short-name assignment is stable: the alphabetical\n // order of all fields determines each short name, so adding\n // or removing a consumer never changes names for others.\n // 3. There is no source ↔ JSON mismatch: both sides use the\n // identical map regardless of which subset is consumed.\n const nestedRenameMap =\n buildNestedRenameMapFromContent(dictionaryContent);\n\n // Skip dictionaries whose opaque fields have nested user-defined\n // structure – renaming those sub-keys would silently break child\n // components that consume the field value as-is.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(\n dictionaryKey\n );\n\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n if (dangerousEntries.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousEntries.flatMap(([fieldName, locations]) => [\n `\\n Opaque field:`,\n colorize(`'${fieldName}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...locations.map(\n (loc) => `\\n at ${formatPath(loc)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n // Disable renaming for the children of opaque fields to prevent\n // breaking components that receive the field as a prop.\n for (const [fieldName] of dangerousEntries) {\n const entry = nestedRenameMap.get(fieldName);\n if (entry) {\n entry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-partial-minify-summary.lock'\n ),\n () => {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n },\n { cacheTimeoutMs: 1000 * 5 }\n );\n }\n }\n },\n },\n\n // Plugin 2: Babel transform\n {\n name: 'vite-intlayer-babel-transform',\n enforce: 'post', // Run after framework transformations (e.g. Vue SFC)\n apply: (_config, env) => {\n const isBuildCommand = env.command === 'build';\n const isEnabled =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand || !isEnabled) return false;\n\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-optimize-plugin-enabled.lock'\n ),\n () =>\n logger([\n `Build optimization ${colorize('enabled', ANSIColors.GREEN)}`,\n colorize('(import mode:', ANSIColors.GREY_DARK),\n colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),\n colorize(')', ANSIColors.GREY_DARK),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n },\n\n transform: async (sourceCode, moduleId) => {\n // Strip query parameters added by Vue/Svelte loaders\n // e.g. \"HelloWorld.vue?vue&type=script&setup=true&lang.ts\" → \"HelloWorld.vue\"\n const sourceFilePath = moduleId.split('?', 1)[0];\n\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return null;\n if (!transformableFilesList.includes(sourceFilePath)) return null;\n\n const isDictionaryEntryFile = [\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ].includes(sourceFilePath);\n\n const isUsingIntlayer = INTLAYER_USAGE_REGEX.test(sourceCode);\n if (!isUsingIntlayer && !isDictionaryEntryFile) return null;\n\n // Step 1: Field rename (must run before the optimize pass, which\n // replaces useIntlayer → useDictionary and erases the dictionary key)\n let codeToOptimize = sourceCode;\n\n if (pruneContext && isUsingIntlayer) {\n const renamedCode = await renameFieldsInSourceFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n if (renamedCode) {\n codeToOptimize = renamedCode;\n }\n }\n\n // Step 2: Optimize (useIntlayer('key') → useDictionary(_hash))\n const transformResult = await optimizeSourceFile(\n codeToOptimize,\n sourceFilePath,\n {\n optimize,\n dictionariesDir,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n unmergedDictionariesDir,\n dynamicDictionariesDir,\n dynamicDictionariesEntryPath,\n fetchDictionariesDir,\n fetchDictionariesEntryPath: join(\n mainDir,\n 'fetch_dictionaries.mjs'\n ),\n importMode,\n filesList: transformableFilesList,\n replaceDictionaryEntry: true,\n dictionaryModeMap: dictionaryKeyToImportModeMap,\n }\n );\n\n if (!transformResult) return null;\n\n return {\n code: transformResult.code,\n map: transformResult.map as any,\n };\n },\n },\n ];\n } catch (pluginInitError) {\n console.warn(\n '[vite-intlayer] Failed to initialise optimization plugin:',\n pluginInitError\n );\n return [];\n }\n};\n"],"mappings":"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"}
@@ -126,7 +126,11 @@ const intlayerPrune = (intlayerConfig, pruneContext) => {
126
126
  * transforms concurrently — JavaScript's single-threaded event loop ensures
127
127
  * the `.has` / `.add` pair is always atomic.
128
128
  */
129
- const prunedAndLoggedDictionaryKeys = /* @__PURE__ */ new Set();
129
+ const loggedPrunedDictionaryKeys = /* @__PURE__ */ new Set();
130
+ /**
131
+ * Accumulated statistics for the build summary.
132
+ */
133
+ const prunedFieldsCountPerDictionary = /* @__PURE__ */ new Map();
130
134
  const isDictionaryJsonFile = (absoluteFilePath) => absoluteFilePath.endsWith(".json") && (absoluteFilePath.startsWith(dictionariesDir) || absoluteFilePath.startsWith(dynamicDictionariesDir) || absoluteFilePath.startsWith(fetchDictionariesDir));
131
135
  const isDynamicOrFetchDictionaryFile = (absoluteFilePath) => absoluteFilePath.startsWith(dynamicDictionariesDir) || absoluteFilePath.startsWith(fetchDictionariesDir);
132
136
  const isPruneEnabled = (_config, env) => {
@@ -207,20 +211,36 @@ const intlayerPrune = (intlayerConfig, pruneContext) => {
207
211
  } else if (isPlainRecord(originalContent)) originalFieldNames = Object.keys(originalContent);
208
212
  else originalFieldNames = [];
209
213
  const removedFieldNames = originalFieldNames.filter((fieldName) => !fieldUsage.has(fieldName));
210
- if (removedFieldNames.length > 0 && !prunedAndLoggedDictionaryKeys.has(dictionaryKey)) {
211
- prunedAndLoggedDictionaryKeys.add(dictionaryKey);
212
- logger([
213
- `Pruned`,
214
- colorizeNumber(removedFieldNames.length),
215
- `unused field${removedFieldNames.length === 1 ? "" : "s"} from`,
216
- `${colorizeKey(dictionaryKey)}:`,
217
- removedFieldNames.map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT)).join(", ")
218
- ]);
214
+ if (removedFieldNames.length > 0) {
215
+ prunedFieldsCountPerDictionary.set(dictionaryKey, removedFieldNames.length);
216
+ if (!loggedPrunedDictionaryKeys.has(dictionaryKey)) {
217
+ loggedPrunedDictionaryKeys.add(dictionaryKey);
218
+ logger([
219
+ `Pruned`,
220
+ colorizeNumber(removedFieldNames.length),
221
+ `unused field${removedFieldNames.length === 1 ? "" : "s"} from`,
222
+ `${colorizeKey(dictionaryKey)}:`,
223
+ removedFieldNames.map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT)).join(", ")
224
+ ], { isVerbose: true });
225
+ }
219
226
  }
220
227
  return {
221
228
  code: JSON.stringify(prunedDictionary),
222
229
  map: null
223
230
  };
231
+ },
232
+ buildEnd: () => {
233
+ runOnce(join(baseDir, ".intlayer", "cache", "intlayer-prune-summary.lock"), () => {
234
+ const totalPrunedFieldsCount = [...prunedFieldsCountPerDictionary.values()].reduce((a, b) => a + b, 0);
235
+ const totalPrunedDictionariesCount = prunedFieldsCountPerDictionary.size;
236
+ if (totalPrunedFieldsCount > 0) logger([
237
+ `Pruned`,
238
+ colorizeNumber(totalPrunedFieldsCount),
239
+ `unused field${totalPrunedFieldsCount === 1 ? "" : "s"} across`,
240
+ colorizeNumber(totalPrunedDictionariesCount),
241
+ `dictionar${totalPrunedDictionariesCount === 1 ? "y" : "ies"}.`
242
+ ]);
243
+ }, { cacheTimeoutMs: 1e3 * 5 });
224
244
  }
225
245
  }];
226
246
  };
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerPrunePlugin.mjs","names":[],"sources":["../../src/intlayerPrunePlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { PruneContext } from '@intlayer/babel';\nimport { formatPath, runOnce } from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Dictionary JSON types\n\n/**\n * A compiled intlayer translation node – used in static dictionaries where\n * all locales are bundled in a single file.\n *\n * Structure:\n * { nodeType: \"translation\", translation: { en: { field1, field2 }, fr: {…} } }\n */\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\n/**\n * Compiled intlayer dictionary as stored in a `.json` file.\n *\n * Two content shapes are supported (see `pruneStaticDictionaryContent` and\n * `pruneDynamicDictionaryContent`).\n */\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string; // present in per-locale dynamic dictionary files\n localIds?: string[];\n [extraKey: string]: unknown;\n};\n\n// Type guards\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// Pruning logic\n\n/**\n * Result of a prune attempt.\n *\n * `wasRecognised` is `false` when the content structure did not match any\n * known shape – the caller should log a warning and skip the file entirely.\n */\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/**\n * Prune a **static** dictionary JSON (all locales in one file).\n *\n * Shape A – the whole `content` is a single translation node:\n * { nodeType: \"translation\", translation: { en: { f1, f2 }, fr: { f1, f2 } } }\n * → prune the field objects inside each locale.\n *\n * Shape B – `content` is a plain record of fields, each being a translated node:\n * { field1: { nodeType: \"translation\", … }, field2: { … } }\n * → prune the top-level keys of `content` directly.\n *\n * Returns `{ wasRecognised: false }` when neither shape matches.\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n const localeValuesAreRecords = isPlainRecord(firstLocaleValue);\n\n if (localeValuesAreRecords) {\n const prunedTranslationByLocale: Record<string, unknown> = {};\n\n for (const [localeName, localeContent] of Object.entries(\n content.translation\n )) {\n if (!isPlainRecord(localeContent)) {\n // Locale value is not a record (e.g. a primitive) – keep as-is\n prunedTranslationByLocale[localeName] = localeContent;\n continue;\n }\n\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslationByLocale[localeName] = prunedLocaleFields;\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslationByLocale },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n const prunedContentFields: Record<string, unknown> = {};\n\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Prune a **dynamic / per-locale** dictionary JSON (one file per locale).\n *\n * Structure:\n * { key, content: { field1: value, field2: value }, locale: \"en\" }\n *\n * The `content` here is already the flat, locale-specific record, so we\n * prune its top-level keys directly.\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n\n const prunedContentFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n};\n\n/**\n * Returns the Vite plugin that removes unused content fields from compiled\n * dictionary JSON files during a production build.\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * Decision table for each dictionary JSON:\n *\n * | condition | action |\n * |------------------------------------------------|-----------------|\n * | key in `dictionariesWithEdgeCases` | skip (warn once)|\n * | JSON parse error / missing key field | skip + warn |\n * | unrecognised content structure | skip + warn |\n * | analysis incomplete + key not in usage map | skip + warn |\n * | usage = 'all' (spread / untracked variable) | skip prune |\n * | usage = Set<string> | prune fields |\n *\n * Pruned dictionaries are returned as compact JSON (minification is handled\n * separately by `intlayerMinifyPlugin`).\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared state produced by the usage analyser that\n * runs inside `intlayerOptimizePlugin`.\n */\nexport const intlayerPrune = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n fetchDictionariesDir,\n baseDir,\n } = intlayerConfig.system;\n\n /**\n * Tracks dictionary keys whose \"pruned fields\" log has already been emitted\n * during this build session. Using an in-memory Set (instead of `runOnce`\n * file locks) avoids race conditions when client and SSR environments run\n * transforms concurrently — JavaScript's single-threaded event loop ensures\n * the `.has` / `.add` pair is always atomic.\n */\n const prunedAndLoggedDictionaryKeys = new Set<string>();\n\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir));\n\n const isDynamicOrFetchDictionaryFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir);\n\n const isPruneEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand) return false;\n if (!isOptimizeActive) return false;\n if (!purge) return false;\n\n if (editorEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-purge-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary purge is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n '— the editor requires full dictionary content.',\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-purge-plugin-enabled.lock'),\n () => logger(['Dictionary purge', colorize('enabled', ANSIColors.GREEN)]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n };\n\n const prunePlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-prune',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM conversion\n enforce: 'pre',\n apply: isPruneEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!isDictionaryJsonFile(absoluteFilePath)) return null;\n\n // Parse JSON\n let parsedDictionary: CompiledDictionaryJson;\n try {\n parsedDictionary = JSON.parse(rawJsonCode) as CompiledDictionaryJson;\n } catch {\n // Malformed JSON – leave it for Vite to report the error\n return null;\n }\n\n const { key: dictionaryKey } = parsedDictionary;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(absoluteFilePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Skip keys already marked as edge cases─\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) {\n return null;\n }\n\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n // No usage entry in the map─\n if (!fieldUsage) {\n if (pruneContext.hasUnparsableSourceFiles) {\n // At least one source file failed to parse; the unparsable file might\n // reference this key, so we cannot safely prune it.\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Analysis was complete but this key was never referenced in any source\n // file – the dictionary is either unused or loaded dynamically by key.\n // Leave the content unchanged (the minify plugin will compact it).\n return null;\n }\n\n // Usage is 'all': at least one call-site consumes all fields─\n if (fieldUsage === 'all') {\n return null; // nothing to prune\n }\n\n // Prune\n const isDynamicOrFetch = isDynamicOrFetchDictionaryFile(absoluteFilePath);\n\n const { prunedDictionary, wasRecognised } = isDynamicOrFetch\n ? pruneDynamicDictionaryContent(parsedDictionary, fieldUsage)\n : pruneStaticDictionaryContent(parsedDictionary, fieldUsage);\n\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(absoluteFilePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Log pruned fields\n const originalContent = parsedDictionary.content;\n let originalFieldNames: string[];\n\n if (isTranslationNode(originalContent)) {\n // Shape A – fields live inside each locale object\n const firstLocaleValue = Object.values(originalContent.translation)[0];\n originalFieldNames = isPlainRecord(firstLocaleValue)\n ? Object.keys(firstLocaleValue)\n : [];\n } else if (isPlainRecord(originalContent)) {\n // Shape B / dynamic – flat content record\n originalFieldNames = Object.keys(originalContent);\n } else {\n originalFieldNames = [];\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (\n removedFieldNames.length > 0 &&\n !prunedAndLoggedDictionaryKeys.has(dictionaryKey)\n ) {\n prunedAndLoggedDictionaryKeys.add(dictionaryKey);\n logger([\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ]);\n }\n\n return { code: JSON.stringify(prunedDictionary), map: null };\n },\n };\n\n return [prunePlugin];\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"intlayerPrunePlugin.mjs","names":[],"sources":["../../src/intlayerPrunePlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { PruneContext } from '@intlayer/babel';\nimport { formatPath, runOnce } from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Dictionary JSON types\n\n/**\n * A compiled intlayer translation node – used in static dictionaries where\n * all locales are bundled in a single file.\n *\n * Structure:\n * { nodeType: \"translation\", translation: { en: { field1, field2 }, fr: {…} } }\n */\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\n/**\n * Compiled intlayer dictionary as stored in a `.json` file.\n *\n * Two content shapes are supported (see `pruneStaticDictionaryContent` and\n * `pruneDynamicDictionaryContent`).\n */\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string; // present in per-locale dynamic dictionary files\n localIds?: string[];\n [extraKey: string]: unknown;\n};\n\n// Type guards\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// Pruning logic\n\n/**\n * Result of a prune attempt.\n *\n * `wasRecognised` is `false` when the content structure did not match any\n * known shape – the caller should log a warning and skip the file entirely.\n */\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/**\n * Prune a **static** dictionary JSON (all locales in one file).\n *\n * Shape A – the whole `content` is a single translation node:\n * { nodeType: \"translation\", translation: { en: { f1, f2 }, fr: { f1, f2 } } }\n * → prune the field objects inside each locale.\n *\n * Shape B – `content` is a plain record of fields, each being a translated node:\n * { field1: { nodeType: \"translation\", … }, field2: { … } }\n * → prune the top-level keys of `content` directly.\n *\n * Returns `{ wasRecognised: false }` when neither shape matches.\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n const localeValuesAreRecords = isPlainRecord(firstLocaleValue);\n\n if (localeValuesAreRecords) {\n const prunedTranslationByLocale: Record<string, unknown> = {};\n\n for (const [localeName, localeContent] of Object.entries(\n content.translation\n )) {\n if (!isPlainRecord(localeContent)) {\n // Locale value is not a record (e.g. a primitive) – keep as-is\n prunedTranslationByLocale[localeName] = localeContent;\n continue;\n }\n\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslationByLocale[localeName] = prunedLocaleFields;\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslationByLocale },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n const prunedContentFields: Record<string, unknown> = {};\n\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Prune a **dynamic / per-locale** dictionary JSON (one file per locale).\n *\n * Structure:\n * { key, content: { field1: value, field2: value }, locale: \"en\" }\n *\n * The `content` here is already the flat, locale-specific record, so we\n * prune its top-level keys directly.\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n\n const prunedContentFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n};\n\n/**\n * Returns the Vite plugin that removes unused content fields from compiled\n * dictionary JSON files during a production build.\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * Decision table for each dictionary JSON:\n *\n * | condition | action |\n * |------------------------------------------------|-----------------|\n * | key in `dictionariesWithEdgeCases` | skip (warn once)|\n * | JSON parse error / missing key field | skip + warn |\n * | unrecognised content structure | skip + warn |\n * | analysis incomplete + key not in usage map | skip + warn |\n * | usage = 'all' (spread / untracked variable) | skip prune |\n * | usage = Set<string> | prune fields |\n *\n * Pruned dictionaries are returned as compact JSON (minification is handled\n * separately by `intlayerMinifyPlugin`).\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared state produced by the usage analyser that\n * runs inside `intlayerOptimizePlugin`.\n */\nexport const intlayerPrune = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n fetchDictionariesDir,\n baseDir,\n } = intlayerConfig.system;\n\n /**\n * Tracks dictionary keys whose \"pruned fields\" log has already been emitted\n * during this build session. Using an in-memory Set (instead of `runOnce`\n * file locks) avoids race conditions when client and SSR environments run\n * transforms concurrently — JavaScript's single-threaded event loop ensures\n * the `.has` / `.add` pair is always atomic.\n */\n const loggedPrunedDictionaryKeys = new Set<string>();\n\n /**\n * Accumulated statistics for the build summary.\n */\n const prunedFieldsCountPerDictionary = new Map<string, number>();\n\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir));\n\n const isDynamicOrFetchDictionaryFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir);\n\n const isPruneEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand) return false;\n if (!isOptimizeActive) return false;\n if (!purge) return false;\n\n if (editorEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-purge-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary purge is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n '— the editor requires full dictionary content.',\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-purge-plugin-enabled.lock'),\n () => logger(['Dictionary purge', colorize('enabled', ANSIColors.GREEN)]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n };\n\n const prunePlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-prune',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM conversion\n enforce: 'pre',\n apply: isPruneEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!isDictionaryJsonFile(absoluteFilePath)) return null;\n\n // Parse JSON\n let parsedDictionary: CompiledDictionaryJson;\n try {\n parsedDictionary = JSON.parse(rawJsonCode) as CompiledDictionaryJson;\n } catch {\n // Malformed JSON – leave it for Vite to report the error\n return null;\n }\n\n const { key: dictionaryKey } = parsedDictionary;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(absoluteFilePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Skip keys already marked as edge cases─\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) {\n return null;\n }\n\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n // No usage entry in the map─\n if (!fieldUsage) {\n if (pruneContext.hasUnparsableSourceFiles) {\n // At least one source file failed to parse; the unparsable file might\n // reference this key, so we cannot safely prune it.\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Analysis was complete but this key was never referenced in any source\n // file – the dictionary is either unused or loaded dynamically by key.\n // Leave the content unchanged (the minify plugin will compact it).\n return null;\n }\n\n // Usage is 'all': at least one call-site consumes all fields─\n if (fieldUsage === 'all') {\n return null; // nothing to prune\n }\n\n // Prune\n const isDynamicOrFetch = isDynamicOrFetchDictionaryFile(absoluteFilePath);\n\n const { prunedDictionary, wasRecognised } = isDynamicOrFetch\n ? pruneDynamicDictionaryContent(parsedDictionary, fieldUsage)\n : pruneStaticDictionaryContent(parsedDictionary, fieldUsage);\n\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(absoluteFilePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Log pruned fields\n const originalContent = parsedDictionary.content;\n let originalFieldNames: string[];\n\n if (isTranslationNode(originalContent)) {\n // Shape A – fields live inside each locale object\n const firstLocaleValue = Object.values(originalContent.translation)[0];\n originalFieldNames = isPlainRecord(firstLocaleValue)\n ? Object.keys(firstLocaleValue)\n : [];\n } else if (isPlainRecord(originalContent)) {\n // Shape B / dynamic – flat content record\n originalFieldNames = Object.keys(originalContent);\n } else {\n originalFieldNames = [];\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n return { code: JSON.stringify(prunedDictionary), map: null };\n },\n\n /**\n * Log a summary of all fields removed during this build.\n */\n buildEnd: () => {\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-prune-summary.lock'),\n () => {\n const totalPrunedFieldsCount = [\n ...prunedFieldsCountPerDictionary.values(),\n ].reduce((a, b) => a + b, 0);\n const totalPrunedDictionariesCount =\n prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n },\n { cacheTimeoutMs: 1000 * 5 }\n );\n },\n };\n\n return [prunePlugin];\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pruneContext.mjs","names":[],"sources":["../../src/pruneContext.ts"],"sourcesContent":["// Re-export PruneContext types from @intlayer/babel so both the vite plugins\n// and the babel plugins share the same type definitions.\n// These are type-only imports and are erased at runtime.\nexport type {\n DictionaryFieldUsage,\n NestedRenameEntry,\n NestedRenameMap,\n PruneContext,\n} from '@intlayer/babel';\n\n// createPruneContext is kept here as a local runtime value so that\n// vite-intlayer does not depend on @intlayer/babel's dist being built\n// with the new exports before this plugin can load.\nexport const createPruneContext =\n (): import('@intlayer/babel').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 dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n });\n"],"mappings":";AAaA,MAAa,4BACoC;CAC7C,8CAA8B,IAAI,KAAK;CACvC,2CAA2B,IAAI,KAAK;CACpC,0BAA0B;CAC1B,qDAAqC,IAAI,KAAK;CAC9C,+CAA+B,IAAI,KAAK;CACxC,wDAAwC,IAAI,KAAK;CACjD,iDAAiC,IAAI,KAAK;CAC1C,0CAA0B,IAAI,KAAK;CACpC"}
1
+ {"version":3,"file":"pruneContext.mjs","names":[],"sources":["../../src/pruneContext.ts"],"sourcesContent":["// Re-export PruneContext types from @intlayer/babel so both the vite plugins\n// and the babel plugins share the same type definitions.\n\nimport type { PruneContext } from '@intlayer/babel';\n\n// createPruneContext is kept here as a local runtime value so that\n// vite-intlayer does not depend on @intlayer/babel's dist being built\n// with the new exports before this plugin can load.\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 dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n"],"mappings":";AAQA,MAAa,4BAA0C;CACrD,8CAA8B,IAAI,KAAK;CACvC,2CAA2B,IAAI,KAAK;CACpC,0BAA0B;CAC1B,qDAAqC,IAAI,KAAK;CAC9C,+CAA+B,IAAI,KAAK;CACxC,wDAAwC,IAAI,KAAK;CACjD,iDAAiC,IAAI,KAAK;CAC1C,0CAA0B,IAAI,KAAK;CACpC"}
@@ -1,8 +1,7 @@
1
- import * as _$_intlayer_babel0 from "@intlayer/babel";
2
- import { DictionaryFieldUsage, NestedRenameEntry, NestedRenameMap, PruneContext } from "@intlayer/babel";
1
+ import { PruneContext } from "@intlayer/babel";
3
2
 
4
3
  //#region src/pruneContext.d.ts
5
- declare const createPruneContext: () => _$_intlayer_babel0.PruneContext;
4
+ declare const createPruneContext: () => PruneContext;
6
5
  //#endregion
7
- export { type DictionaryFieldUsage, type NestedRenameEntry, type NestedRenameMap, type PruneContext, createPruneContext };
6
+ export { createPruneContext };
8
7
  //# sourceMappingURL=pruneContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pruneContext.d.ts","names":[],"sources":["../../src/pruneContext.ts"],"mappings":";;;;cAaa,kBAAA,QAUT,kBAAA,CAT4B,YAAA"}
1
+ {"version":3,"file":"pruneContext.d.ts","names":[],"sources":["../../src/pruneContext.ts"],"mappings":";;;cAQa,kBAAA,QAAyB,YAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-intlayer",
3
- "version": "8.7.3",
3
+ "version": "8.7.4",
4
4
  "private": false,
5
5
  "description": "A Vite plugin for seamless internationalization (i18n), providing locale detection, redirection, and environment-based configuration",
6
6
  "keywords": [
@@ -76,27 +76,27 @@
76
76
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
77
77
  },
78
78
  "dependencies": {
79
- "@intlayer/babel": "8.7.3",
79
+ "@intlayer/babel": "8.7.4",
80
80
  "@intlayer/chokidar": "8.7.1",
81
- "@intlayer/config": "8.7.3",
82
- "@intlayer/core": "8.7.3",
81
+ "@intlayer/config": "8.7.4",
82
+ "@intlayer/core": "8.7.4",
83
83
  "@intlayer/dictionaries-entry": "8.7.1",
84
- "@intlayer/types": "8.7.3"
84
+ "@intlayer/types": "8.7.4"
85
85
  },
86
86
  "devDependencies": {
87
- "@types/node": "25.5.2",
87
+ "@types/node": "25.6.0",
88
88
  "@utils/ts-config": "1.0.4",
89
89
  "@utils/ts-config-types": "1.0.4",
90
90
  "@utils/tsdown-config": "1.0.4",
91
91
  "rimraf": "6.1.3",
92
- "tsdown": "0.21.8",
93
- "typescript": "6.0.2",
92
+ "tsdown": "0.21.9",
93
+ "typescript": "6.0.3",
94
94
  "vitest": "4.1.4"
95
95
  },
96
96
  "peerDependencies": {
97
97
  "@babel/core": ">=6.0.0",
98
98
  "@intlayer/svelte-compiler": "8.7.1",
99
- "@intlayer/vue-compiler": "8.7.3",
99
+ "@intlayer/vue-compiler": "8.7.4",
100
100
  "vite": ">=4.0.0"
101
101
  },
102
102
  "peerDependenciesMeta": {