vite-intlayer 9.3.3 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/esm/intlayerMinifyPlugin.mjs +16 -15
- package/dist/esm/intlayerMinifyPlugin.mjs.map +1 -1
- package/dist/esm/intlayerOptimizePlugin.mjs +6 -5
- package/dist/esm/intlayerOptimizePlugin.mjs.map +1 -1
- package/dist/esm/intlayerPrunePlugin.mjs +5 -13
- package/dist/esm/intlayerPrunePlugin.mjs.map +1 -1
- package/dist/types/intlayerMinifyPlugin.d.ts +5 -1
- package/dist/types/intlayerMinifyPlugin.d.ts.map +1 -1
- package/dist/types/intlayerPrunePlugin.d.ts +5 -0
- package/dist/types/intlayerPrunePlugin.d.ts.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -214,6 +214,7 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
|
|
|
214
214
|
<li><a href="https://intlayer.org/doc/environment/vite-and-vue" rel=''>Vite + Vue</a></li>
|
|
215
215
|
<li><a href="https://intlayer.org/doc/environment/nuxt-and-vue" rel=''>Vite + Nuxt</a></li>
|
|
216
216
|
<li><a href="https://intlayer.org/doc/environment/vite-and-solid" rel=''>Vite + Solid</a></li>
|
|
217
|
+
<li><a href="https://intlayer.org/doc/environment/solid-start" rel=''>Solid Start</a></li>
|
|
217
218
|
<li><a href="https://intlayer.org/doc/environment/angular" rel=''>Angular</a></li>
|
|
218
219
|
<li>
|
|
219
220
|
<a href="https://intlayer.org/doc/environment/express" rel=''>Backend</a>
|
|
@@ -223,7 +224,9 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
|
|
|
223
224
|
<li><a href="https://intlayer.org/doc/environment/fastify" rel=''>Fastify</a></li>
|
|
224
225
|
<li><a href="https://intlayer.org/doc/environment/adonisjs" rel=''>AdonisJS</a></li>
|
|
225
226
|
<li><a href="https://intlayer.org/doc/environment/hono" rel=''>Hono</a></li>
|
|
227
|
+
<li><a href="https://intlayer.org/doc/environment/elysia" rel=''>Elysia</a></li>
|
|
226
228
|
</ul>
|
|
229
|
+
|
|
227
230
|
</li>
|
|
228
231
|
</ul>
|
|
229
232
|
</details>
|
|
@@ -327,4 +330,4 @@ Contribute on [GitHub](https://github.com/aymericzip/intlayer), [GitLab](https:/
|
|
|
327
330
|
|
|
328
331
|
If you like Intlayer, give us a ⭐ on GitHub. It helps others discover the project! [See why GitHub Stars matter](https://github.com/aymericzip/intlayer/blob/main/CONTRIBUTING.md#why-github-stars-matter-).
|
|
329
332
|
|
|
330
|
-
[](https://star-history.dera.page/#aymericzip/intlayer&Date)
|
|
@@ -87,7 +87,11 @@ const minifyGroupEntryNode = (node, fieldRenameMap) => fieldRenameMap && fieldRe
|
|
|
87
87
|
* and should be left completely untouched to avoid shipping broken data.
|
|
88
88
|
*
|
|
89
89
|
* Field renaming (property mangling) is applied only for dictionaries that
|
|
90
|
-
* have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap
|
|
90
|
+
* have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`,
|
|
91
|
+
* and only while the visual editor is disabled: renaming rewrites the content
|
|
92
|
+
* keys the interpreter walks, so the `keyPath` reported to the editor would no
|
|
93
|
+
* longer resolve against the unmerged dictionaries. The whitespace compaction
|
|
94
|
+
* and the top-level metadata strip are keyPath-neutral and always run.
|
|
91
95
|
* The corresponding rename is also applied to source-file property accesses by
|
|
92
96
|
* the babel rename pass inside `intlayerOptimize`. Internal intlayer fields
|
|
93
97
|
* such as `nodeType` are never renamed.
|
|
@@ -101,7 +105,7 @@ const minifyGroupEntryNode = (node, fieldRenameMap) => fieldRenameMap && fieldRe
|
|
|
101
105
|
const intlayerMinify = (intlayerConfig, pruneContext) => {
|
|
102
106
|
const logger = getAppLogger(intlayerConfig);
|
|
103
107
|
const { optimize, minify } = intlayerConfig.build;
|
|
104
|
-
const
|
|
108
|
+
const isFieldRenameEnabled = !intlayerConfig.editor.enabled;
|
|
105
109
|
const { dictionariesDir, dynamicDictionariesDir, baseDir } = intlayerConfig.system;
|
|
106
110
|
const isDictionaryJsonFile = (absoluteFilePath) => absoluteFilePath.endsWith(".json") && (absoluteFilePath.startsWith(dictionariesDir) || absoluteFilePath.startsWith(dynamicDictionariesDir));
|
|
107
111
|
const isMinifyEnabled = (_config, env) => {
|
|
@@ -109,18 +113,15 @@ const intlayerMinify = (intlayerConfig, pruneContext) => {
|
|
|
109
113
|
if (!(optimize === void 0 && isBuildCommand || optimize === true)) return false;
|
|
110
114
|
if (!minify) return false;
|
|
111
115
|
if (!isBuildCommand) return false;
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
]), { cacheTimeoutMs: 1e4 });
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
116
|
+
if (!isFieldRenameEnabled) runOnce(join(baseDir, ".intlayer", "cache", "intlayer-minify-editor-warning.lock"), () => logger([
|
|
117
|
+
"Dictionary field renaming is",
|
|
118
|
+
colorize("disabled", ANSIColors.GREY_DARK),
|
|
119
|
+
"because",
|
|
120
|
+
colorize("editor.enabled", ANSIColors.BLUE),
|
|
121
|
+
"is",
|
|
122
|
+
colorize("true", ANSIColors.GREY_DARK),
|
|
123
|
+
colorize("— the editor resolves edits by keyPath against the unmerged dictionaries", ANSIColors.GREY)
|
|
124
|
+
]), { cacheTimeoutMs: 1e4 });
|
|
124
125
|
return true;
|
|
125
126
|
};
|
|
126
127
|
return [{
|
|
@@ -144,7 +145,7 @@ const intlayerMinify = (intlayerConfig, pruneContext) => {
|
|
|
144
145
|
}
|
|
145
146
|
const dictionaryKey = typeof parsedDict.key === "string" ? parsedDict.key : void 0;
|
|
146
147
|
if (pruneContext && dictionaryKey && pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return null;
|
|
147
|
-
const fieldRenameMap = pruneContext && dictionaryKey ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey) : void 0;
|
|
148
|
+
const fieldRenameMap = isFieldRenameEnabled && pruneContext && dictionaryKey ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey) : void 0;
|
|
148
149
|
if (isQualifiedDictionaryGroup(parsedDict)) {
|
|
149
150
|
const minifiedContent = {};
|
|
150
151
|
for (const [entryId, node] of Object.entries(parsedDict.content)) minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerMinifyPlugin.mjs","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { NestedRenameMap, PruneContext } from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Field-rename helper\n\n/**\n * Recursively renames user-defined keys in a compiled intlayer content value\n * using the provided `NestedRenameMap`.\n *\n * Traversal rules (mirrors `buildNestedRenameMapFromContent`):\n * - Arrays → each element is recursed into with the same rename map.\n * This mirrors the array pass-through in the source-code rename walk,\n * where numeric indices (e.g. [0]) are transparent.\n * - Object with `nodeType: 'translation'` → intlayer translation node.\n * Keys at this level (`nodeType`, `translation`) are NOT renamed.\n * Recurse into each per-locale value with the same rename map.\n * - Object without `nodeType` → user-defined record.\n * Rename its keys using the current rename map level, then recurse into\n * each value with that entry's `children` map.\n * - Primitives → returned as-is.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n // Arrays: each element is renamed with the same map (indices are transparent).\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale with the same rename map\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val; // key not in map – keep as-is (e.g. already-pruned)\n }\n }\n return result;\n};\n\n/**\n * Applies the nested field rename map to a parsed dictionary object and\n * returns the renamed copy. The top-level dict keys (`key`, `locale`, etc.)\n * are never touched; only keys inside `content` are renamed.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n/**\n * Discriminates a compiled qualified dictionary group (collections, variants)\n * from a plain dictionary. Mirrors `isQualifiedDictionaryGroup` from\n * `@intlayer/core` but is inlined here to avoid a runtime dependency on core\n * inside the build plugin.\n */\nconst isQualifiedDictionaryGroup = (\n dict: Record<string, unknown>\n): dict is Record<string, unknown> & {\n qualifierTypes: unknown;\n content: Record<string, unknown>;\n} =>\n 'qualifierTypes' in dict &&\n Array.isArray(dict.qualifierTypes) &&\n 'content' in dict &&\n typeof dict.content === 'object' &&\n dict.content !== null;\n\n/**\n * Applies the field-rename map to a single qualified-group content node (the\n * value stored under a composite id). Coordinates are not stored on the node —\n * they live in the key — so only the node's content fields are renamed.\n */\nconst minifyGroupEntryNode = (\n node: unknown,\n fieldRenameMap: NestedRenameMap | undefined\n): unknown =>\n fieldRenameMap &&\n fieldRenameMap.size > 0 &&\n node &&\n typeof node === 'object' &&\n !Array.isArray(node)\n ? renameContentRecursively(node, fieldRenameMap)\n : node;\n\n// Plugin\n\n/**\n * Returns the Vite plugin that minifies compiled dictionary JSON files by\n * removing all unnecessary whitespace and optionally renaming content field\n * names to short alphabetic aliases (a, b, c, …).\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 * The plugin is deliberately independent of the prune plugin: it can run\n * on its own when only `build.minify` is enabled. When both `purge` and\n * `minify` are active, the prune plugin runs first (it uses `enforce: 'pre'`\n * and is registered before this one); this plugin then receives the already-\n * pruned JSON, renames its field keys, and compacts it.\n *\n * Files listed in `pruneContext.dictionariesWithEdgeCases` are skipped:\n * those dictionaries encountered a structural issue during the prune phase\n * and should be left completely untouched to avoid shipping broken data.\n *\n * Field renaming (property mangling) is applied only for dictionaries that\n * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`.\n * The corresponding rename is also applied to source-file property accesses by\n * the babel rename pass inside `intlayerOptimize`. Internal intlayer fields\n * such as `nodeType` are never renamed.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Optional shared state from the prune plugin. When\n * provided, dictionaries flagged as edge-cases are\n * skipped and field renames are applied. Pass `null`\n * if the prune plugin is not active.\n */\nexport const intlayerMinify = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, minify } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const { dictionariesDir, dynamicDictionariesDir, baseDir } =\n intlayerConfig.system;\n\n // Fetch-mode dictionaries are served from a remote API at runtime using their\n // original field names. Minifying them (renaming fields) would create a\n // mismatch between the server response and the renamed client-side accesses.\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir));\n\n const isMinifyEnabled = (\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 (!isOptimizeActive) return false;\n if (!minify) return false;\n if (!isBuildCommand) return false;\n\n if (editorEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-minify-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary minification is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor requires full dictionary content',\n ANSIColors.GREY\n ),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n return true;\n };\n\n const minifyPlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-minify',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM\n // conversion. Declaration order in the plugin array ensures this runs\n // after the prune plugin (which is also 'pre' but registered earlier).\n enforce: 'pre',\n apply: isMinifyEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n return null;\n\n // Parse JSON\n let parsedDict: Record<string, unknown>;\n try {\n parsedDict = JSON.parse(rawJsonCode) as Record<string, unknown>;\n } catch (parseError) {\n logger(\n [\n `Could not parse`,\n formatPath(absoluteFilePath),\n `as JSON. Skipping minification for this file.`,\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n const dictionaryKey =\n typeof parsedDict.key === 'string' ? parsedDict.key : undefined;\n\n // Skip edge-case dictionaries\n if (\n pruneContext &&\n dictionaryKey &&\n pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)\n ) {\n return null; // structural issue flagged during prune – leave untouched\n }\n\n const fieldRenameMap =\n pruneContext && dictionaryKey\n ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey)\n : undefined;\n\n // Qualified groups (collections / variants) carry their payload as a\n // `content` map keyed by composite id + `qualifierTypes`. Preserve\n // `qualifierTypes` and the per-entry nodes (the coordinates live in the\n // keys); only the field-rename pass is applied to each node.\n if (isQualifiedDictionaryGroup(parsedDict)) {\n const minifiedContent: Record<string, unknown> = {};\n for (const [entryId, node] of Object.entries(parsedDict.content)) {\n minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);\n }\n\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n qualifierTypes: parsedDict.qualifierTypes,\n content: minifiedContent,\n }),\n map: null,\n };\n }\n\n // Apply field rename (property mangling)\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(parsedDict, fieldRenameMap);\n }\n\n // Strip top-level metadata – ship only key + content. The variant\n // identity of a qualified chunk is encoded in its composite id (the chunk\n // path), so no extra per-chunk side-data is needed for selector matching.\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n content: parsedDict.content,\n }),\n map: null,\n };\n },\n };\n\n return [minifyPlugin];\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"intlayerMinifyPlugin.mjs","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { NestedRenameMap, PruneContext } from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Field-rename helper\n\n/**\n * Recursively renames user-defined keys in a compiled intlayer content value\n * using the provided `NestedRenameMap`.\n *\n * Traversal rules (mirrors `buildNestedRenameMapFromContent`):\n * - Arrays → each element is recursed into with the same rename map.\n * This mirrors the array pass-through in the source-code rename walk,\n * where numeric indices (e.g. [0]) are transparent.\n * - Object with `nodeType: 'translation'` → intlayer translation node.\n * Keys at this level (`nodeType`, `translation`) are NOT renamed.\n * Recurse into each per-locale value with the same rename map.\n * - Object without `nodeType` → user-defined record.\n * Rename its keys using the current rename map level, then recurse into\n * each value with that entry's `children` map.\n * - Primitives → returned as-is.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n // Arrays: each element is renamed with the same map (indices are transparent).\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale with the same rename map\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val; // key not in map – keep as-is (e.g. already-pruned)\n }\n }\n return result;\n};\n\n/**\n * Applies the nested field rename map to a parsed dictionary object and\n * returns the renamed copy. The top-level dict keys (`key`, `locale`, etc.)\n * are never touched; only keys inside `content` are renamed.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n/**\n * Discriminates a compiled qualified dictionary group (collections, variants)\n * from a plain dictionary. Mirrors `isQualifiedDictionaryGroup` from\n * `@intlayer/core` but is inlined here to avoid a runtime dependency on core\n * inside the build plugin.\n */\nconst isQualifiedDictionaryGroup = (\n dict: Record<string, unknown>\n): dict is Record<string, unknown> & {\n qualifierTypes: unknown;\n content: Record<string, unknown>;\n} =>\n 'qualifierTypes' in dict &&\n Array.isArray(dict.qualifierTypes) &&\n 'content' in dict &&\n typeof dict.content === 'object' &&\n dict.content !== null;\n\n/**\n * Applies the field-rename map to a single qualified-group content node (the\n * value stored under a composite id). Coordinates are not stored on the node —\n * they live in the key — so only the node's content fields are renamed.\n */\nconst minifyGroupEntryNode = (\n node: unknown,\n fieldRenameMap: NestedRenameMap | undefined\n): unknown =>\n fieldRenameMap &&\n fieldRenameMap.size > 0 &&\n node &&\n typeof node === 'object' &&\n !Array.isArray(node)\n ? renameContentRecursively(node, fieldRenameMap)\n : node;\n\n// Plugin\n\n/**\n * Returns the Vite plugin that minifies compiled dictionary JSON files by\n * removing all unnecessary whitespace and optionally renaming content field\n * names to short alphabetic aliases (a, b, c, …).\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 * The plugin is deliberately independent of the prune plugin: it can run\n * on its own when only `build.minify` is enabled. When both `purge` and\n * `minify` are active, the prune plugin runs first (it uses `enforce: 'pre'`\n * and is registered before this one); this plugin then receives the already-\n * pruned JSON, renames its field keys, and compacts it.\n *\n * Files listed in `pruneContext.dictionariesWithEdgeCases` are skipped:\n * those dictionaries encountered a structural issue during the prune phase\n * and should be left completely untouched to avoid shipping broken data.\n *\n * Field renaming (property mangling) is applied only for dictionaries that\n * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`,\n * and only while the visual editor is disabled: renaming rewrites the content\n * keys the interpreter walks, so the `keyPath` reported to the editor would no\n * longer resolve against the unmerged dictionaries. The whitespace compaction\n * and the top-level metadata strip are keyPath-neutral and always run.\n * The corresponding rename is also applied to source-file property accesses by\n * the babel rename pass inside `intlayerOptimize`. Internal intlayer fields\n * such as `nodeType` are never renamed.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Optional shared state from the prune plugin. When\n * provided, dictionaries flagged as edge-cases are\n * skipped and field renames are applied. Pass `null`\n * if the prune plugin is not active.\n */\nexport const intlayerMinify = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, minify } = intlayerConfig.build;\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` emitted by `ContentSelector` would no longer resolve against the\n // unmerged dictionaries the editor edits. Whitespace compaction and the\n // top-level metadata strip are keyPath-neutral and stay active.\n const isFieldRenameEnabled = !intlayerConfig.editor.enabled;\n\n const { dictionariesDir, dynamicDictionariesDir, baseDir } =\n intlayerConfig.system;\n\n // Fetch-mode dictionaries are served from a remote API at runtime using their\n // original field names. Minifying them (renaming fields) would create a\n // mismatch between the server response and the renamed client-side accesses.\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir));\n\n const isMinifyEnabled = (\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 (!isOptimizeActive) return false;\n if (!minify) return false;\n if (!isBuildCommand) return false;\n\n if (!isFieldRenameEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-minify-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor resolves edits by keyPath against the unmerged dictionaries',\n ANSIColors.GREY\n ),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n }\n\n return true;\n };\n\n const minifyPlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-minify',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM\n // conversion. Declaration order in the plugin array ensures this runs\n // after the prune plugin (which is also 'pre' but registered earlier).\n enforce: 'pre',\n apply: isMinifyEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n return null;\n\n // Parse JSON\n let parsedDict: Record<string, unknown>;\n try {\n parsedDict = JSON.parse(rawJsonCode) as Record<string, unknown>;\n } catch (parseError) {\n logger(\n [\n `Could not parse`,\n formatPath(absoluteFilePath),\n `as JSON. Skipping minification for this file.`,\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n const dictionaryKey =\n typeof parsedDict.key === 'string' ? parsedDict.key : undefined;\n\n // Skip edge-case dictionaries\n if (\n pruneContext &&\n dictionaryKey &&\n pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)\n ) {\n return null; // structural issue flagged during prune – leave untouched\n }\n\n const fieldRenameMap =\n isFieldRenameEnabled && pruneContext && dictionaryKey\n ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey)\n : undefined;\n\n // Qualified groups (collections / variants) carry their payload as a\n // `content` map keyed by composite id + `qualifierTypes`. Preserve\n // `qualifierTypes` and the per-entry nodes (the coordinates live in the\n // keys); only the field-rename pass is applied to each node.\n if (isQualifiedDictionaryGroup(parsedDict)) {\n const minifiedContent: Record<string, unknown> = {};\n for (const [entryId, node] of Object.entries(parsedDict.content)) {\n minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);\n }\n\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n qualifierTypes: parsedDict.qualifierTypes,\n content: minifiedContent,\n }),\n map: null,\n };\n }\n\n // Apply field rename (property mangling)\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(parsedDict, fieldRenameMap);\n }\n\n // Strip top-level metadata – ship only key + content. The variant\n // identity of a qualified chunk is encoded in its composite id (the chunk\n // path), so no extra per-chunk side-data is needed for selector matching.\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n content: parsedDict.content,\n }),\n map: null,\n };\n },\n };\n\n return [minifyPlugin];\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,4BACJ,OACA,cACY;CAEZ,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAQ,MAAoB,KAAK,YAC/B,yBAAyB,SAAS,SAAS,CAC7C;CAGF,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAEhD,MAAM,SAAS;CAGf,IACE,OAAO,OAAO,aAAa,YAC3B,OAAO,eACP,OAAO,OAAO,gBAAgB,YAC9B,CAAC,MAAM,QAAQ,OAAO,WAAW,GACjC;EACA,MAAM,qBAA8C,CAAC;EACrD,KAAK,MAAM,CAAC,QAAQ,gBAAgB,OAAO,QACzC,OAAO,WACT,GACE,mBAAmB,UAAU,yBAC3B,aACA,SACF;EAEF,OAAO;GAAE,GAAG;GAAQ,aAAa;EAAmB;CACtD;CAGA,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,MAAM,GAAG;EAC/C,MAAM,cAAc,UAAU,IAAI,GAAG;EACrC,IAAI,aACF,OAAO,YAAY,aAAa,yBAC9B,KACA,YAAY,QACd;OAEA,OAAO,OAAO;CAElB;CACA,OAAO;AACT;;;;;;AAOA,MAAM,0BACJ,MACA,cAC4B;CAC5B,MAAM,UAAU,KAAK;CACrB,IAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,GAClE,OAAO;CAET,OAAO;EACL,GAAG;EACH,SAAS,yBAAyB,SAAS,SAAS;CACtD;AACF;;;;;;;AAQA,MAAM,8BACJ,SAKA,oBAAoB,QACpB,MAAM,QAAQ,KAAK,cAAc,KACjC,aAAa,QACb,OAAO,KAAK,YAAY,YACxB,KAAK,YAAY;;;;;;AAOnB,MAAM,wBACJ,MACA,mBAEA,kBACA,eAAe,OAAO,KACtB,QACA,OAAO,SAAS,YAChB,CAAC,MAAM,QAAQ,IAAI,IACf,yBAAyB,MAAM,cAAc,IAC7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCN,MAAa,kBACX,gBACA,iBACmB;CACnB,MAAM,SAAS,aAAa,cAAc;CAE1C,MAAM,EAAE,UAAU,WAAW,eAAe;CAM5C,MAAM,uBAAuB,CAAC,eAAe,OAAO;CAEpD,MAAM,EAAE,iBAAiB,wBAAwB,YAC/C,eAAe;CAKjB,MAAM,wBAAwB,qBAC5B,iBAAiB,SAAS,OAAO,MAChC,iBAAiB,WAAW,eAAe,KAC1C,iBAAiB,WAAW,sBAAsB;CAEtD,MAAM,mBACJ,SACA,QACY;EACZ,MAAM,iBAAiB,IAAI,YAAY;EAIvC,IAAI,EAFD,aAAa,UAAa,kBAAmB,aAAa,OAEtC,OAAO;EAC9B,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,CAAC,gBAAgB,OAAO;EAE5B,IAAI,CAAC,sBACH,QACE,KACE,SACA,aACA,SACA,qCACF,SAEE,OAAO;GACL;GACA,SAAS,YAAY,WAAW,SAAS;GACzC;GACA,SAAS,kBAAkB,WAAW,IAAI;GAC1C;GACA,SAAS,QAAQ,WAAW,SAAS;GACrC,SACE,4EACA,WAAW,IACb;EACF,CAAC,GACH,EAAE,gBAAgB,IAAU,CAC9B;EAGF,OAAO;CACT;CA0FA,OAAO,CAAC;EAvFN,MAAM;EAIN,SAAS;EACT,OAAO;EAEP,YAAY,aAAa,aAAa;GACpC,MAAM,mBAAmB,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC;GAEhD,IAAI,CAAC,oBAAoB,CAAC,qBAAqB,gBAAgB,GAC7D,OAAO;GAGT,IAAI;GACJ,IAAI;IACF,aAAa,KAAK,MAAM,WAAW;GACrC,SAAS,YAAY;IACnB,OACE;KACE;KACA,WAAW,gBAAgB;KAC3B;KACA,sBAAsB,QAClB,IAAI,WAAW,QAAQ,KACvB,OAAO,UAAU;IACvB,GACA,EAAE,OAAO,OAAO,CAClB;IACA,OAAO;GACT;GAEA,MAAM,gBACJ,OAAO,WAAW,QAAQ,WAAW,WAAW,MAAM;GAGxD,IACE,gBACA,iBACA,aAAa,0BAA0B,IAAI,aAAa,GAExD,OAAO;GAGT,MAAM,iBACJ,wBAAwB,gBAAgB,gBACpC,aAAa,8BAA8B,IAAI,aAAa,IAC5D;GAMN,IAAI,2BAA2B,UAAU,GAAG;IAC1C,MAAM,kBAA2C,CAAC;IAClD,KAAK,MAAM,CAAC,SAAS,SAAS,OAAO,QAAQ,WAAW,OAAO,GAC7D,gBAAgB,WAAW,qBAAqB,MAAM,cAAc;IAGtE,OAAO;KACL,MAAM,KAAK,UAAU;MACnB,KAAK,WAAW;MAChB,gBAAgB,WAAW;MAC3B,SAAS;KACX,CAAC;KACD,KAAK;IACP;GACF;GAGA,IAAI,kBAAkB,eAAe,OAAO,GAC1C,aAAa,uBAAuB,YAAY,cAAc;GAMhE,OAAO;IACL,MAAM,KAAK,UAAU;KACnB,KAAK,WAAW;KAChB,SAAS,WAAW;IACtB,CAAC;IACD,KAAK;GACP;EACF;CAGiB,CAAC;AACtB"}
|
|
@@ -60,7 +60,8 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
|
|
|
60
60
|
const isBuildCommand = env.command === "build";
|
|
61
61
|
return optimize === void 0 && isBuildCommand || optimize === true;
|
|
62
62
|
};
|
|
63
|
-
const
|
|
63
|
+
const isFieldRenameEnabled = !editorEnabled && !!minify;
|
|
64
|
+
const isAnalysisEnabled = (_config, env) => (!!purge || !!minify) && isBuildOptimizeEnabled(_config, env);
|
|
64
65
|
let partiallyMinifiedDictionariesCount = 0;
|
|
65
66
|
return [
|
|
66
67
|
intlayerVueAsyncPlugin(intlayerConfig, transformableFilesList),
|
|
@@ -186,7 +187,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
|
|
|
186
187
|
`is assigned to a plain variable in:`,
|
|
187
188
|
...sourceFilePaths.map((filePath) => `\n - ${formatPath(filePath)}`)
|
|
188
189
|
], { level: "warn" });
|
|
189
|
-
if (
|
|
190
|
+
if (isFieldRenameEnabled) {
|
|
190
191
|
for (const [dictionaryKey, fieldUsage] of pruneContext.dictionaryKeyToFieldUsageMap) {
|
|
191
192
|
if (fieldUsage === "all") continue;
|
|
192
193
|
if (dictionaryKeyToImportModeMap[dictionaryKey] === "fetch") continue;
|
|
@@ -259,8 +260,8 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
|
|
|
259
260
|
colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),
|
|
260
261
|
colorize(")", ANSIColors.GREY_DARK)
|
|
261
262
|
]);
|
|
262
|
-
if (
|
|
263
|
-
if (
|
|
263
|
+
if (minify) logger(`Dictionary minification ${colorize("enabled", ANSIColors.GREEN)}`);
|
|
264
|
+
if (purge) logger(`Dictionary purge unused keys ${colorize("enabled", ANSIColors.GREEN)}`);
|
|
264
265
|
}, { cacheTimeoutMs: 1e4 });
|
|
265
266
|
return true;
|
|
266
267
|
},
|
|
@@ -272,7 +273,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
|
|
|
272
273
|
const isUsingIntlayer = (compatCallers?.length ? buildUsageCheckRegex(compatCallers.map((caller) => caller.callerName)) : INTLAYER_USAGE_REGEX).test(sourceCode);
|
|
273
274
|
if (!isUsingIntlayer && !isDictionaryEntryFile) return null;
|
|
274
275
|
let codeToOptimize = sourceCode;
|
|
275
|
-
if (pruneContext && isUsingIntlayer) {
|
|
276
|
+
if (isFieldRenameEnabled && pruneContext && isUsingIntlayer) {
|
|
276
277
|
const renamedCode = await renameFieldsInSourceFile(sourceFilePath, sourceCode, pruneContext);
|
|
277
278
|
if (renamedCode) codeToOptimize = renamedCode;
|
|
278
279
|
}
|
|
@@ -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 buildUsageCheckRegex,\n type CallerDescriptor,\n getNestedRenameEntryAtPath,\n INTLAYER_USAGE_REGEX,\n optimizeSourceFile,\n type PruneContext,\n preserveNestedDictionaryFields,\n renameFieldsInSourceFile,\n SOURCE_FILE_REGEX,\n} from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { IMPORT_MODE } from '@intlayer/config/defaultValues';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { normalizePath } from '@intlayer/config/utils';\nimport {\n getNestedDictionaryGraph,\n getNestedDictionaryReferences,\n} from '@intlayer/core/dictionaryManipulator';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport {\n buildComponentFilesList,\n formatPath,\n runOnce,\n} from '@intlayer/engine/utils';\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 compatCallers?: CallerDescriptor[]\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 = normalizePath(\n join(mainDir, 'dictionaries.mjs')\n );\n const unmergedDictionariesEntryPath = normalizePath(\n join(mainDir, 'unmerged_dictionaries.mjs')\n );\n const dynamicDictionariesEntryPath = normalizePath(\n join(mainDir, 'dynamic_dictionaries.mjs')\n );\n\n const componentFilesList =\n buildComponentFilesList(intlayerConfig).map(normalizePath);\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 // Dictionaries pulled in by `nest()`: resolved at runtime by `getNesting`\n // through the dictionary registry, never by a `useIntlayer` call site.\n const nestedDictionaryReferences = getNestedDictionaryReferences(\n Object.values(dictionaries)\n );\n\n // Dictionaries holding `nest()` references: their static import is\n // re-pointed at the companion module carrying the nest targets.\n const nestingDictionaryKeys = [\n ...getNestedDictionaryGraph(Object.values(dictionaries)).keys(),\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 const extraCallerNames = (compatCallers ?? []).map(\n (caller) => caller.callerName\n );\n const usageCheckRegex = buildUsageCheckRegex(extraCallerNames);\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 (!usageCheckRegex.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 compatCallers\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 / Astro SFC\n // bindings that Babel scope analysis cannot resolve:\n // Vue → `.value` ref-accessor indirection\n // Svelte → `$` reactive store prefix\n // Astro → frontmatter variables referenced in the HTML template\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 const astroPending = 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 } else if (filePath.endsWith('.astro')) {\n astroPending.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 // Astro files\n // Frontmatter variables are used in the HTML template, which is not\n // visible to Babel's scope analysis. Scan the template section for\n // `variableName.fieldName` accesses using a lightweight regex pass.\n if (astroPending.size > 0) {\n for (const [filePath, entries] of astroPending) {\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 // Extract only the template (everything after the closing ---).\n // The frontmatter was already handled by Babel in Phase 1.\n const fenceMatch = /^---\\r?\\n[\\s\\S]*?\\r?\\n---/.exec(fileCode);\n const template = fenceMatch\n ? fileCode.slice(fenceMatch.index + fenceMatch[0].length)\n : fileCode;\n\n for (const { variableName, dictionaryKey } of entries) {\n const escapedVar = variableName.replace(\n /[.*+?^${}()|[\\]\\\\]/g,\n '\\\\$&'\n );\n const fieldRe = new RegExp(\n `\\\\b${escapedVar}\\\\.([a-zA-Z_$][a-zA-Z0-9_$]*)`,\n 'g'\n );\n const foundFields = new Set<string>();\n let m = fieldRe.exec(template);\n while (m !== null) {\n foundFields.add(m[1]!);\n m = fieldRe.exec(template);\n }\n mergeFrameworkResult(\n dictionaryKey,\n foundFields.size > 0 ? foundFields : undefined\n );\n }\n }\n }\n }\n\n // Phase 2.5: Keep `nest()` targets resolvable — must run before the\n // rename maps of Phase 4 are built.\n preserveNestedDictionaryFields(\n pruneContext,\n nestedDictionaryReferences\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 renaming below opaque values that have nested\n // user-defined structure – renaming those sub-keys would\n // silently break child components that consume the value as-is.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n\n if (opaqueFieldMap) {\n const dangerousOccurrences = [\n ...opaqueFieldMap.values(),\n ].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n )?.children.size ?? 0) > 0\n );\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(\n `'${occurrence.fieldPath.join('.')}'`,\n ANSIColors.BLUE\n ),\n `(nested keys preserved for stability).`,\n ...occurrence.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 values to\n // prevent breaking components that receive the value as a\n // prop.\n for (const occurrence of dangerousOccurrences) {\n const entry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\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\n // Mirrors `isAnalysisEnabled`: the editor requires full\n // dictionary content, so neither pass runs while it is enabled.\n if (!editorEnabled && minify) {\n logger(\n `Dictionary minification ${colorize('enabled', ANSIColors.GREEN)}`\n );\n }\n\n if (!editorEnabled && purge) {\n logger(\n `Dictionary purge unused keys ${colorize('enabled', ANSIColors.GREEN)}`\n );\n }\n },\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n },\n\n transform: async (sourceCode, moduleId, options) => {\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 = normalizePath(\n moduleId.split('?', 1)[0] ?? moduleId\n );\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 // Match native intlayer calls plus any configured compat callers\n // (`useTranslation`, `useTranslations`, …) so compat-only files are\n // still optimized.\n const optimizeUsageRegex = compatCallers?.length\n ? buildUsageCheckRegex(\n compatCallers.map((caller) => caller.callerName)\n )\n : INTLAYER_USAGE_REGEX;\n\n const isUsingIntlayer = optimizeUsageRegex.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 // When compat callers are configured, they call `getIntlayer` at\n // runtime which reads from `dictionaries.mjs`. Emptying that module\n // would break all compat runtime lookups, so we preserve it.\n replaceDictionaryEntry: !compatCallers?.length,\n nestingDictionaryKeys,\n dictionaryModeMap: dictionaryKeyToImportModeMap,\n isServer: options?.ssr === true,\n compatCallers,\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 buildUsageCheckRegex,\n type CallerDescriptor,\n getNestedRenameEntryAtPath,\n INTLAYER_USAGE_REGEX,\n optimizeSourceFile,\n type PruneContext,\n preserveNestedDictionaryFields,\n renameFieldsInSourceFile,\n SOURCE_FILE_REGEX,\n} from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { IMPORT_MODE } from '@intlayer/config/defaultValues';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { normalizePath } from '@intlayer/config/utils';\nimport {\n getNestedDictionaryGraph,\n getNestedDictionaryReferences,\n} from '@intlayer/core/dictionaryManipulator';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport {\n buildComponentFilesList,\n formatPath,\n runOnce,\n} from '@intlayer/engine/utils';\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 compatCallers?: CallerDescriptor[]\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 = normalizePath(\n join(mainDir, 'dictionaries.mjs')\n );\n const unmergedDictionariesEntryPath = normalizePath(\n join(mainDir, 'unmerged_dictionaries.mjs')\n );\n const dynamicDictionariesEntryPath = normalizePath(\n join(mainDir, 'dynamic_dictionaries.mjs')\n );\n\n const componentFilesList =\n buildComponentFilesList(intlayerConfig).map(normalizePath);\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 // Dictionaries pulled in by `nest()`: resolved at runtime by `getNesting`\n // through the dictionary registry, never by a `useIntlayer` call site.\n const nestedDictionaryReferences = getNestedDictionaryReferences(\n Object.values(dictionaries)\n );\n\n // Dictionaries holding `nest()` references: their static import is\n // re-pointed at the companion module carrying the nest targets.\n const nestingDictionaryKeys = [\n ...getNestedDictionaryGraph(Object.values(dictionaries)).keys(),\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 // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` reported by `ContentSelector` would no longer resolve against\n // the unmerged dictionaries the visual editor edits. Purging and the\n // keyPath-neutral part of minification stay active while the editor is on.\n const isFieldRenameEnabled = !editorEnabled && !!minify;\n\n const isAnalysisEnabled = (_config: unknown, env: { command: string }) =>\n (!!purge || !!minify) && 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 const extraCallerNames = (compatCallers ?? []).map(\n (caller) => caller.callerName\n );\n const usageCheckRegex = buildUsageCheckRegex(extraCallerNames);\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 (!usageCheckRegex.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 compatCallers\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 / Astro SFC\n // bindings that Babel scope analysis cannot resolve:\n // Vue → `.value` ref-accessor indirection\n // Svelte → `$` reactive store prefix\n // Astro → frontmatter variables referenced in the HTML template\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 const astroPending = 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 } else if (filePath.endsWith('.astro')) {\n astroPending.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 // Astro files\n // Frontmatter variables are used in the HTML template, which is not\n // visible to Babel's scope analysis. Scan the template section for\n // `variableName.fieldName` accesses using a lightweight regex pass.\n if (astroPending.size > 0) {\n for (const [filePath, entries] of astroPending) {\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 // Extract only the template (everything after the closing ---).\n // The frontmatter was already handled by Babel in Phase 1.\n const fenceMatch = /^---\\r?\\n[\\s\\S]*?\\r?\\n---/.exec(fileCode);\n const template = fenceMatch\n ? fileCode.slice(fenceMatch.index + fenceMatch[0].length)\n : fileCode;\n\n for (const { variableName, dictionaryKey } of entries) {\n const escapedVar = variableName.replace(\n /[.*+?^${}()|[\\]\\\\]/g,\n '\\\\$&'\n );\n const fieldRe = new RegExp(\n `\\\\b${escapedVar}\\\\.([a-zA-Z_$][a-zA-Z0-9_$]*)`,\n 'g'\n );\n const foundFields = new Set<string>();\n let m = fieldRe.exec(template);\n while (m !== null) {\n foundFields.add(m[1]!);\n m = fieldRe.exec(template);\n }\n mergeFrameworkResult(\n dictionaryKey,\n foundFields.size > 0 ? foundFields : undefined\n );\n }\n }\n }\n }\n\n // Phase 2.5: Keep `nest()` targets resolvable — must run before the\n // rename maps of Phase 4 are built.\n preserveNestedDictionaryFields(\n pruneContext,\n nestedDictionaryReferences\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 (isFieldRenameEnabled) {\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 renaming below opaque values that have nested\n // user-defined structure – renaming those sub-keys would\n // silently break child components that consume the value as-is.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey);\n\n if (opaqueFieldMap) {\n const dangerousOccurrences = [\n ...opaqueFieldMap.values(),\n ].filter(\n (occurrence) =>\n (getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n )?.children.size ?? 0) > 0\n );\n if (dangerousOccurrences.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousOccurrences.flatMap((occurrence) => [\n `\\n Opaque field:`,\n colorize(\n `'${occurrence.fieldPath.join('.')}'`,\n ANSIColors.BLUE\n ),\n `(nested keys preserved for stability).`,\n ...occurrence.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 values to\n // prevent breaking components that receive the value as a\n // prop.\n for (const occurrence of dangerousOccurrences) {\n const entry = getNestedRenameEntryAtPath(\n nestedRenameMap,\n occurrence.fieldPath\n );\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\n // The minify plugin reports the field-rename step standing down\n // when the editor is enabled, so this line stays a plain one.\n if (minify) {\n logger(\n `Dictionary minification ${colorize('enabled', ANSIColors.GREEN)}`\n );\n }\n\n if (purge) {\n logger(\n `Dictionary purge unused keys ${colorize('enabled', ANSIColors.GREEN)}`\n );\n }\n },\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n },\n\n transform: async (sourceCode, moduleId, options) => {\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 = normalizePath(\n moduleId.split('?', 1)[0] ?? moduleId\n );\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 // Match native intlayer calls plus any configured compat callers\n // (`useTranslation`, `useTranslations`, …) so compat-only files are\n // still optimized.\n const optimizeUsageRegex = compatCallers?.length\n ? buildUsageCheckRegex(\n compatCallers.map((caller) => caller.callerName)\n )\n : INTLAYER_USAGE_REGEX;\n\n const isUsingIntlayer = optimizeUsageRegex.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 (isFieldRenameEnabled && 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 // When compat callers are configured, they call `getIntlayer` at\n // runtime which reads from `dictionaries.mjs`. Emptying that module\n // would break all compat runtime lookups, so we preserve it.\n replaceDictionaryEntry: !compatCallers?.length,\n nestingDictionaryKeys,\n dictionaryModeMap: dictionaryKeyToImportModeMap,\n isServer: options?.ssr === true,\n compatCallers,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,MAAa,mBAAmB,OAC9B,gBACA,cACA,kBAC4B;CAC5B,IAAI;EACF,MAAM,SAAS,aAAa,cAAc;EAE1C,MAAM,EAAE,UAAU,OAAO,WAAW,eAAe;EACnD,MAAM,gBAAgB,eAAe,OAAO;EAE5C,MAAM,aACJ,eAAe,MAAM,cAAc,eAAe,YAAY;EAEhE,MAAM,EACJ,iBACA,wBACA,yBACA,sBACA,SACA,YACE,eAAe;EAEnB,MAAM,wBAAwB,cAC5B,KAAK,SAAS,kBAAkB,CAClC;EACA,MAAM,gCAAgC,cACpC,KAAK,SAAS,2BAA2B,CAC3C;EACA,MAAM,+BAA+B,cACnC,KAAK,SAAS,0BAA0B,CAC1C;EAEA,MAAM,qBACJ,wBAAwB,cAAc,CAAC,CAAC,IAAI,aAAa;EAE3D,MAAM,yBAAyB;GAC7B,GAAG;GACH;GACA;EACF;EAEA,MAAM,eAAe,gBAAgB,cAAc;EAEnD,MAAM,+BAGF,CAAC;EACL,AAAC,OAAO,OAAO,YAAY,CAAC,CAAkB,SAAS,eAAe;GACpE,6BAA6B,WAAW,OACtC,WAAW,cAAc,cAAc;EAC3C,CAAC;EAID,MAAM,6BAA6B,8BACjC,OAAO,OAAO,YAAY,CAC5B;EAIA,MAAM,wBAAwB,CAC5B,GAAG,yBAAyB,OAAO,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,CAChE;EAEA,MAAM,0BACJ,SACA,QACG;GACH,MAAM,iBAAiB,IAAI,YAAY;GACvC,OAAQ,aAAa,UAAa,kBAAmB,aAAa;EACpE;EAMA,MAAM,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;EAEjD,MAAM,qBAAqB,SAAkB,SAC1C,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,uBAAuB,SAAS,GAAG;EAE9D,IAAI,qCAAqC;EAEzC,OAAO;GACL,uBAAuB,gBAAgB,sBAAsB;GAG7D;IACE,MAAM;IACN,SAAS;IACT,OAAO;IAEP,YAAY,YAAY;KACtB,IAAI,CAAC,cAAc;KAEnB,MAAM,oBAAoB,iBAAiB,CAAC,EAAC,CAAE,KAC5C,WAAW,OAAO,UACrB;KACA,MAAM,kBAAkB,qBAAqB,gBAAgB;KAG7D,MAAM,QAAQ,IACZ,mBAAmB,IAAI,OAAO,mBAAmB;MAC/C,IAAI,CAAC,kBAAkB,KAAK,cAAc,GAAG;MAE7C,IAAI;MACJ,IAAI;OACF,aAAa,MAAM,SAAS,gBAAgB,OAAO;MACrD,QAAQ;OACN;MACF;MAEA,IAAI,CAAC,gBAAgB,KAAK,UAAU,GAAG;MAKvC,IAAI;OACF,MAAM,wBACJ,gBACA,YACA,cACA,aACF;MACF,SAAS,YAAY;OACnB,aAAa,2BAA2B;OACxC,OACE;QACE;QACA,WAAW,cAAc;QACzB;QACA;QACA,sBAAsB,QAClB,IAAI,WAAW,QAAQ,KACvB,OAAO,UAAU;OACvB,GACA,EAAE,OAAO,OAAO,CAClB;MACF;KACF,CAAC,CACH;KAOA,IAAI,aAAa,yBAAyB,OAAO,GAAG;MAClD,MAAM,6BAAa,IAAI,IAGrB;MACF,MAAM,gCAAgB,IAAI,IAGxB;MACF,MAAM,+BAAe,IAAI,IAGvB;MAEF,KAAK,MAAM,CACT,UACA,YACG,aAAa,0BAChB,IAAI,SAAS,SAAS,MAAM,GAC1B,WAAW,IAAI,UAAU,OAAO;WAC3B,IAAI,SAAS,SAAS,SAAS,GACpC,cAAc,IAAI,UAAU,OAAO;WAC9B,IAAI,SAAS,SAAS,QAAQ,GACnC,aAAa,IAAI,UAAU,OAAO;;MAKtC,MAAM,wBACJ,eACA,WACS;OACT,IAAI,UAAU,OAAO,OAAO,GAAG;QAG7B,aAAa,gCAAgC,IAAI,aAAa;QAE9D,MAAM,WACJ,aAAa,6BAA6B,IAAI,aAAa;QAC7D,IAAI,aAAa,OAAO;QAExB,MAAM,SACJ,oBAAoB,sBAChB,IAAI,IAAI,CAAC,GAAG,UAAU,GAAG,MAAM,CAAC,IAChC,IAAI,IAAI,MAAM;QACpB,aAAa,6BAA6B,IACxC,eACA,MACF;OACF,OACE,aAAa,6BAA6B,IACxC,eACA,KACF;MAEJ;MAGA,IAAI,WAAW,OAAO,GAAG;OACvB,IAAI,+BAKO;OAEX,IAAI;QAEF,gCACE,MAFwB,OAAO,0BAEpB,CAAC;OAChB,QAAQ,CAER;OAEA,KAAK,MAAM,CAAC,UAAU,YAAY,YAAY;QAC5C,IAAI,CAAC,8BAA8B;SACjC,KAAK,MAAM,EAAE,mBAAmB,SAC9B,qBAAqB,eAAe,MAAS;SAE/C;QACF;QAEA,IAAI;QACJ,IAAI;SACF,WAAW,MAAM,SAAS,UAAU,OAAO;QAC7C,QAAQ;SACN,KAAK,MAAM,EAAE,mBAAmB,SAC9B,qBAAqB,eAAe,MAAS;SAE/C;QACF;QAEA,MAAM,SAAS,6BAA6B,UAAU,OAAO;QAC7D,KAAK,MAAM,EAAE,mBAAmB,SAC9B,qBACE,eACA,OAAO,IAAI,aAAa,CAC1B;OAEJ;MACF;MAGA,IAAI,cAAc,OAAO,GAAG;OAC1B,IAAI,kCAKO;OAEX,IAAI;QAIF,mCACE,MAJ2B,OAC3B,6BAGc,CAAC;OACnB,QAAQ,CAER;OAEA,KAAK,MAAM,CAAC,UAAU,YAAY,eAAe;QAC/C,IAAI,CAAC,iCAAiC;SACpC,KAAK,MAAM,EAAE,mBAAmB,SAC9B,qBAAqB,eAAe,MAAS;SAE/C;QACF;QAEA,IAAI;QACJ,IAAI;SACF,WAAW,MAAM,SAAS,UAAU,OAAO;QAC7C,QAAQ;SACN,KAAK,MAAM,EAAE,mBAAmB,SAC9B,qBAAqB,eAAe,MAAS;SAE/C;QACF;QAEA,MAAM,SAAS,gCACb,UACA,OACF;QACA,KAAK,MAAM,EAAE,mBAAmB,SAC9B,qBACE,eACA,OAAO,IAAI,aAAa,CAC1B;OAEJ;MACF;MAMA,IAAI,aAAa,OAAO,GACtB,KAAK,MAAM,CAAC,UAAU,YAAY,cAAc;OAC9C,IAAI;OACJ,IAAI;QACF,WAAW,MAAM,SAAS,UAAU,OAAO;OAC7C,QAAQ;QACN,KAAK,MAAM,EAAE,mBAAmB,SAC9B,qBAAqB,eAAe,MAAS;QAE/C;OACF;OAIA,MAAM,aAAa,4BAA4B,KAAK,QAAQ;OAC5D,MAAM,WAAW,aACb,SAAS,MAAM,WAAW,QAAQ,WAAW,EAAE,CAAC,MAAM,IACtD;OAEJ,KAAK,MAAM,EAAE,cAAc,mBAAmB,SAAS;QACrD,MAAM,aAAa,aAAa,QAC9B,uBACA,MACF;QACA,MAAM,UAAU,IAAI,OAClB,MAAM,WAAW,gCACjB,GACF;QACA,MAAM,8BAAc,IAAI,IAAY;QACpC,IAAI,IAAI,QAAQ,KAAK,QAAQ;QAC7B,OAAO,MAAM,MAAM;SACjB,YAAY,IAAI,EAAE,EAAG;SACrB,IAAI,QAAQ,KAAK,QAAQ;QAC3B;QACA,qBACE,eACA,YAAY,OAAO,IAAI,cAAc,MACvC;OACF;MACF;KAEJ;KAIA,+BACE,cACA,0BACF;KAGA,KAAK,MAAM,CACT,eACA,oBACG,aAAa,qCAChB,OACE;MACE;MACA,YAAY,aAAa;MACzB;MACA;MACA,GAAG,SAAS,gBAAgB,WAAW,UAAU,IAAI,YACnD,IAAI,cAAc,EACpB,IAAI,SAAS,KAAK,WAAW,UAAU;MACvC;MACA,GAAG,gBAAgB,KAChB,aAAa,aAAa,WAAW,QAAQ,GAChD;KACF,GACA,EAAE,OAAO,OAAO,CAClB;KAOF,IAAI,sBAAsB;MACxB,KAAK,MAAM,CACT,eACA,eACG,aAAa,8BAA8B;OAC9C,IAAI,eAAe,OAAO;OAK1B,IAAI,6BAA6B,mBAAmB,SAClD;OAIF,IACE,aAAa,gCAAgC,IAAI,aAAa,GAE9D;OAGF,IAAI,oBAA6B;OAEjC,MAAM,iBAAiB,KACrB,iBACA,GAAG,cAAc,MACnB;OACA,IAAI;QACF,MAAM,MAAM,MAAM,SAAS,gBAAgB,OAAO;QAElD,oBADe,KAAK,MAAM,GACD,CAAC,CAAC;OAC7B,QAAQ;QACN,IAAI;SACF,MAAM,aAAa,KACjB,wBACA,aACF;SAEA,MAAM,iBAAgB,MADI,QAAQ,UAAU,EACX,CAAC,MAAM,MACtC,EAAE,SAAS,OAAO,CACpB;SACA,IAAI,eAAe;UACjB,MAAM,MAAM,MAAM,SAChB,KAAK,YAAY,aAAa,GAC9B,OACF;UAEA,oBADe,KAAK,MAAM,GACD,CAAC,CAAC;SAC7B;QACF,QAAQ,CAER;OACF;OAEA,IAAI,CAAC,mBAAmB;OAYxB,MAAM,kBACJ,gCAAgC,iBAAiB;OAKnD,MAAM,iBACJ,aAAa,+BAA+B,IAAI,aAAa;OAE/D,IAAI,gBAAgB;QAClB,MAAM,uBAAuB,CAC3B,GAAG,eAAe,OAAO,CAC3B,CAAC,CAAC,QACC,gBACE,2BACC,iBACA,WAAW,SACb,CAAC,EAAE,SAAS,QAAQ,KAAK,CAC7B;QACA,IAAI,qBAAqB,SAAS,GAAG;SACnC,sCAAsC;SAEtC,OACE;UACE;UACA,YAAY,aAAa;UACzB;UACA,GAAG,qBAAqB,SAAS,eAAe;WAC9C;WACA,SACE,IAAI,WAAW,UAAU,KAAK,GAAG,EAAE,IACnC,WAAW,IACb;WACA;WACA,GAAG,WAAW,UAAU,KACrB,QAAQ,cAAc,WAAW,GAAG,GACvC;UACF,CAAC;SACH,GACA;UAAE,OAAO;UAAQ,WAAW;SAAK,CACnC;SAKA,KAAK,MAAM,cAAc,sBAAsB;UAC7C,MAAM,QAAQ,2BACZ,iBACA,WAAW,SACb;UACA,IAAI,OACF,MAAM,2BAAW,IAAI,IAAI;SAE7B;QACF;OACF;OAEA,IAAI,gBAAgB,OAAO,GACzB,aAAa,8BAA8B,IACzC,eACA,eACF;MAEJ;MAEA,IAAI,qCAAqC,GACvC,QACE,KACE,SACA,aACA,SACA,sCACF,SACM;OACJ,OAAO;QACL;QACA,eAAe,kCAAkC;QACjD,YAAY,uCAAuC,IAAI,MAAM;QAC7D;OACF,CAAC;MACH,GACA,EAAE,gBAAgB,IAAS,CAC7B;KAEJ;IACF;GACF;GAGA;IACE,MAAM;IACN,SAAS;IACT,QAAQ,SAAS,QAAQ;KACvB,MAAM,iBAAiB,IAAI,YAAY;KAIvC,IAAI,CAAC,kBAAkB,EAFpB,aAAa,UAAa,kBAAmB,aAAa,OAE1B,OAAO;KAE1C,QACE,KACE,SACA,aACA,SACA,uCACF,SACM;MACJ,OAAO;OACL,sBAAsB,SAAS,WAAW,WAAW,KAAK;OAC1D,SAAS,iBAAiB,WAAW,SAAS;OAC9C,SAAS,cAAc,aAAa,WAAW,IAAI;OACnD,SAAS,KAAK,WAAW,SAAS;MACpC,CAAC;MAID,IAAI,QACF,OACE,2BAA2B,SAAS,WAAW,WAAW,KAAK,GACjE;MAGF,IAAI,OACF,OACE,gCAAgC,SAAS,WAAW,WAAW,KAAK,GACtE;KAEJ,GACA,EAAE,gBAAgB,IAAU,CAC9B;KAEA,OAAO;IACT;IAEA,WAAW,OAAO,YAAY,UAAU,YAAY;KAGlD,MAAM,iBAAiB,cACrB,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM,QAC/B;KAEA,IAAI,CAAC,kBAAkB,KAAK,cAAc,GAAG,OAAO;KACpD,IAAI,CAAC,uBAAuB,SAAS,cAAc,GAAG,OAAO;KAE7D,MAAM,wBAAwB,CAC5B,uBACA,6BACF,CAAC,CAAC,SAAS,cAAc;KAWzB,MAAM,mBANqB,eAAe,SACtC,qBACE,cAAc,KAAK,WAAW,OAAO,UAAU,CACjD,IACA,qBAEsC,CAAC,KAAK,UAAU;KAC1D,IAAI,CAAC,mBAAmB,CAAC,uBAAuB,OAAO;KAIvD,IAAI,iBAAiB;KAErB,IAAI,wBAAwB,gBAAgB,iBAAiB;MAC3D,MAAM,cAAc,MAAM,yBACxB,gBACA,YACA,YACF;MACA,IAAI,aACF,iBAAiB;KAErB;KAGA,MAAM,kBAAkB,MAAM,mBAC5B,gBACA,gBACA;MACE;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA,4BAA4B,KAC1B,SACA,wBACF;MACA;MACA,WAAW;MAIX,wBAAwB,CAAC,eAAe;MACxC;MACA,mBAAmB;MACnB,UAAU,SAAS,QAAQ;MAC3B;KACF,CACF;KAEA,IAAI,CAAC,iBAAiB,OAAO;KAE7B,OAAO;MACL,MAAM,gBAAgB;MACtB,KAAK,gBAAgB;KACvB;IACF;GACF;EACF;CACF,SAAS,iBAAiB;EACxB,QAAQ,KACN,6DACA,eACF;EACA,OAAO,CAAC;CACV;AACF"}
|
|
@@ -110,6 +110,11 @@ const pruneDynamicDictionaryContent = (dictionary, usedFieldNames) => {
|
|
|
110
110
|
* Pruned dictionaries are returned as compact JSON (minification is handled
|
|
111
111
|
* separately by `intlayerMinifyPlugin`).
|
|
112
112
|
*
|
|
113
|
+
* Pruning stays compatible with the visual editor: the editor resolves every
|
|
114
|
+
* edit through `dictionaryKey` + `keyPath` against the unmerged dictionaries
|
|
115
|
+
* entry, which this plugin never touches, and a pruned field is one no source
|
|
116
|
+
* file reads — so it is never rendered and never selectable in the page.
|
|
117
|
+
*
|
|
113
118
|
* @param intlayerConfig - Resolved intlayer configuration.
|
|
114
119
|
* @param pruneContext - Shared state produced by the usage analyser that
|
|
115
120
|
* runs inside `intlayerOptimizePlugin`.
|
|
@@ -117,7 +122,6 @@ const pruneDynamicDictionaryContent = (dictionary, usedFieldNames) => {
|
|
|
117
122
|
const intlayerPrune = (intlayerConfig, pruneContext) => {
|
|
118
123
|
const logger = getAppLogger(intlayerConfig);
|
|
119
124
|
const { optimize, purge } = intlayerConfig.build;
|
|
120
|
-
const editorEnabled = intlayerConfig.editor.enabled;
|
|
121
125
|
const { dictionariesDir, dynamicDictionariesDir, fetchDictionariesDir, baseDir } = intlayerConfig.system;
|
|
122
126
|
/**
|
|
123
127
|
* Tracks dictionary keys whose "pruned fields" log has already been emitted
|
|
@@ -139,18 +143,6 @@ const intlayerPrune = (intlayerConfig, pruneContext) => {
|
|
|
139
143
|
if (!isBuildCommand) return false;
|
|
140
144
|
if (!isOptimizeActive) return false;
|
|
141
145
|
if (!purge) return false;
|
|
142
|
-
if (editorEnabled) {
|
|
143
|
-
runOnce(join(baseDir, ".intlayer", "cache", "intlayer-purge-editor-warning.lock"), () => logger([
|
|
144
|
-
"Dictionary purge is",
|
|
145
|
-
colorize("disabled", ANSIColors.GREY_DARK),
|
|
146
|
-
"because",
|
|
147
|
-
colorize("editor.enabled", ANSIColors.BLUE),
|
|
148
|
-
"is",
|
|
149
|
-
colorize("true", ANSIColors.GREY_DARK),
|
|
150
|
-
colorize("— the editor requires full dictionary content", ANSIColors.GREY)
|
|
151
|
-
]), { cacheTimeoutMs: 1e4 });
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
146
|
return true;
|
|
155
147
|
};
|
|
156
148
|
return [{
|
|
@@ -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 * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\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 colorize(\n '— the editor requires full dictionary content',\n ANSIColors.GREY\n ),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\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 (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n 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
|
+
{"version":3,"file":"intlayerPrunePlugin.mjs","names":[],"sources":["../../src/intlayerPrunePlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { PruneContext } from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\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 * Pruning stays compatible with the visual editor: the editor resolves every\n * edit through `dictionaryKey` + `keyPath` against the unmerged dictionaries\n * entry, which this plugin never touches, and a pruned field is one no source\n * file reads — so it is never rendered and never selectable in the page.\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\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 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 (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n 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":";;;;;;AA2CA,MAAM,qBAAqB,UACzB,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,aAAa,iBAChD,OAAQ,MAAkC,gBAAgB;AAE5D,MAAM,iBAAiB,UACrB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;;;;;;;;;;;;;AA4BrE,MAAM,gCACJ,YACA,mBACgB;CAChB,MAAM,EAAE,YAAY;CAGpB,IAAI,kBAAkB,OAAO,GAAG;EAC9B,MAAM,mBAAmB,OAAO,OAAO,QAAQ,WAAW,CAAC,CAAC;EAG5D,IAF+B,cAAc,gBAEpB,GAAG;GAC1B,MAAM,4BAAqD,CAAC;GAE5D,KAAK,MAAM,CAAC,YAAY,kBAAkB,OAAO,QAC/C,QAAQ,WACV,GAAG;IACD,IAAI,CAAC,cAAc,aAAa,GAAG;KAEjC,0BAA0B,cAAc;KACxC;IACF;IAEA,MAAM,qBAA8C,CAAC;IACrD,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,aAAa,GAChE,IAAI,eAAe,IAAI,SAAS,GAC9B,mBAAmB,aAAa;IAGpC,0BAA0B,cAAc;GAC1C;GAEA,OAAO;IACL,kBAAkB;KAChB,GAAG;KACH,SAAS;MAAE,GAAG;MAAS,aAAa;KAA0B;IAChE;IACA,eAAe;GACjB;EACF;CACF;CAGA,IAAI,cAAc,OAAO,KAAK,CAAC,kBAAkB,OAAO,GAAG;EACzD,MAAM,sBAA+C,CAAC;EAEtD,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,OAAO,GAC1D,IAAI,eAAe,IAAI,SAAS,GAC9B,oBAAoB,aAAa;EAIrC,OAAO;GACL,kBAAkB;IAChB,GAAG;IACH,SAAS;GACX;GACA,eAAe;EACjB;CACF;CAEA,OAAO;EAAE,kBAAkB;EAAY,eAAe;CAAM;AAC9D;;;;;;;;;;AAWA,MAAM,iCACJ,YACA,mBACgB;CAChB,MAAM,EAAE,YAAY;CAEpB,IAAI,CAAC,cAAc,OAAO,GACxB,OAAO;EAAE,kBAAkB;EAAY,eAAe;CAAM;CAG9D,MAAM,sBAA+C,CAAC;CACtD,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,OAAO,GAC1D,IAAI,eAAe,IAAI,SAAS,GAC9B,oBAAoB,aAAa;CAIrC,OAAO;EACL,kBAAkB;GAChB,GAAG;GACH,SAAS;EACX;EACA,eAAe;CACjB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,iBACX,gBACA,iBACmB;CACnB,MAAM,SAAS,aAAa,cAAc;CAE1C,MAAM,EAAE,UAAU,UAAU,eAAe;CAE3C,MAAM,EACJ,iBACA,wBACA,sBACA,YACE,eAAe;;;;;;;;CASnB,MAAM,6CAA6B,IAAI,IAAY;;;;CAKnD,MAAM,iDAAiC,IAAI,IAAoB;CAE/D,MAAM,wBAAwB,qBAC5B,iBAAiB,SAAS,OAAO,MAChC,iBAAiB,WAAW,eAAe,KAC1C,iBAAiB,WAAW,sBAAsB,KAClD,iBAAiB,WAAW,oBAAoB;CAEpD,MAAM,kCAAkC,qBACtC,iBAAiB,WAAW,sBAAsB,KAClD,iBAAiB,WAAW,oBAAoB;CAElD,MAAM,kBACJ,SACA,QACY;EACZ,MAAM,iBAAiB,IAAI,YAAY;EACvC,MAAM,mBACH,aAAa,UAAa,kBAAmB,aAAa;EAE7D,IAAI,CAAC,gBAAgB,OAAO;EAC5B,IAAI,CAAC,kBAAkB,OAAO;EAC9B,IAAI,CAAC,OAAO,OAAO;EAEnB,OAAO;CACT;CA0KA,OAAO,CAAC;EAvKN,MAAM;EAEN,SAAS;EACT,OAAO;EAEP,YAAY,aAAa,aAAa;GACpC,MAAM,mBAAmB,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC;GAEhD,IAAI,CAAC,oBAAoB,CAAC,qBAAqB,gBAAgB,GAC7D,OAAO;GAGT,IAAI;GACJ,IAAI;IACF,mBAAmB,KAAK,MAAM,WAAW;GAC3C,QAAQ;IAEN,OAAO;GACT;GAEA,MAAM,EAAE,KAAK,kBAAkB;GAE/B,IAAI,CAAC,eAAe;IAClB,OACE;KACE;KACA,WAAW,gBAAgB;KAC3B;IACF,GACA,EAAE,OAAO,OAAO,CAClB;IACA,OAAO;GACT;GAGA,IAAI,aAAa,0BAA0B,IAAI,aAAa,GAC1D,OAAO;GAGT,MAAM,aACJ,aAAa,6BAA6B,IAAI,aAAa;GAG7D,IAAI,CAAC,YAAY;IACf,IAAI,aAAa,0BAA0B;KAGzC,aAAa,0BAA0B,IAAI,aAAa;KACxD,OACE;MACE;MACA,YAAY,aAAa;MACzB;KACF,GACA,EAAE,OAAO,OAAO,CAClB;KACA,OAAO;IACT;IAKA,OAAO;GACT;GAGA,IAAI,eAAe,OACjB,OAAO;GAMT,MAAM,EAAE,kBAAkB,kBAFD,+BAA+B,gBAEG,IACvD,8BAA8B,kBAAkB,UAAU,IAC1D,6BAA6B,kBAAkB,UAAU;GAE7D,IAAI,CAAC,eAAe;IAClB,aAAa,0BAA0B,IAAI,aAAa;IACxD,OACE;KACE;KACA,YAAY,aAAa;KACzB;KACA,GAAG,WAAW,gBAAgB,EAAE;KAChC;IACF,GACA,EAAE,OAAO,OAAO,CAClB;IACA,OAAO;GACT;GAGA,MAAM,kBAAkB,iBAAiB;GACzC,IAAI;GAEJ,IAAI,kBAAkB,eAAe,GAAG;IAEtC,MAAM,mBAAmB,OAAO,OAAO,gBAAgB,WAAW,CAAC,CAAC;IACpE,qBAAqB,cAAc,gBAAgB,IAC/C,OAAO,KAAK,gBAAgB,IAC5B,CAAC;GACP,OAAO,IAAI,cAAc,eAAe,GAEtC,qBAAqB,OAAO,KAAK,eAAe;QAEhD,qBAAqB,CAAC;GAGxB,MAAM,oBAAoB,mBAAmB,QAC1C,cAAc,CAAC,WAAW,IAAI,SAAS,CAC1C;GAEA,IAAI,kBAAkB,SAAS,GAAG;IAChC,+BAA+B,IAC7B,eACA,kBAAkB,MACpB;IAEA,IAAI,CAAC,2BAA2B,IAAI,aAAa,GAAG;KAClD,2BAA2B,IAAI,aAAa;KAC5C,OACE;MACE;MACA,eAAe,kBAAkB,MAAM;MACvC,eAAe,kBAAkB,WAAW,IAAI,KAAK,IAAI;MACzD,GAAG,YAAY,aAAa,EAAE;MAC9B,kBACG,KAAK,cAAc,SAAS,WAAW,WAAW,UAAU,CAAC,CAAC,CAC9D,KAAK,IAAI;KACd,GACA,EAAE,WAAW,KAAK,CACpB;IACF;GACF;GAEA,OAAO;IAAE,MAAM,KAAK,UAAU,gBAAgB;IAAG,KAAK;GAAK;EAC7D;;;;EAKA,gBAAgB;GACd,QACE,KAAK,SAAS,aAAa,SAAS,6BAA6B,SAC3D;IACJ,MAAM,yBAAyB,CAC7B,GAAG,+BAA+B,OAAO,CAC3C,CAAC,CAAC,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC;IAC3B,MAAM,+BACJ,+BAA+B;IAEjC,IAAI,yBAAyB,GAC3B,OAAO;KACL;KACA,eAAe,sBAAsB;KACrC,eAAe,2BAA2B,IAAI,KAAK,IAAI;KACvD,eAAe,4BAA4B;KAC3C,YAAY,iCAAiC,IAAI,MAAM,MAAM;IAC/D,CAAC;GAEL,GACA,EAAE,gBAAgB,IAAS,CAC7B;EACF;CAGgB,CAAC;AACrB"}
|
|
@@ -23,7 +23,11 @@ import { PruneContext } from "@intlayer/babel";
|
|
|
23
23
|
* and should be left completely untouched to avoid shipping broken data.
|
|
24
24
|
*
|
|
25
25
|
* Field renaming (property mangling) is applied only for dictionaries that
|
|
26
|
-
* have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap
|
|
26
|
+
* have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`,
|
|
27
|
+
* and only while the visual editor is disabled: renaming rewrites the content
|
|
28
|
+
* keys the interpreter walks, so the `keyPath` reported to the editor would no
|
|
29
|
+
* longer resolve against the unmerged dictionaries. The whitespace compaction
|
|
30
|
+
* and the top-level metadata strip are keyPath-neutral and always run.
|
|
27
31
|
* The corresponding rename is also applied to source-file property accesses by
|
|
28
32
|
* the babel rename pass inside `intlayerOptimize`. Internal intlayer fields
|
|
29
33
|
* such as `nodeType` are never renamed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerMinifyPlugin.d.ts","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"intlayerMinifyPlugin.d.ts","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwKa,iBAAc,gBACT,gBAAc,cAChB,wBACb"}
|
|
@@ -25,6 +25,11 @@ import { PruneContext } from "@intlayer/babel";
|
|
|
25
25
|
* Pruned dictionaries are returned as compact JSON (minification is handled
|
|
26
26
|
* separately by `intlayerMinifyPlugin`).
|
|
27
27
|
*
|
|
28
|
+
* Pruning stays compatible with the visual editor: the editor resolves every
|
|
29
|
+
* edit through `dictionaryKey` + `keyPath` against the unmerged dictionaries
|
|
30
|
+
* entry, which this plugin never touches, and a pruned field is one no source
|
|
31
|
+
* file reads — so it is never rendered and never selectable in the page.
|
|
32
|
+
*
|
|
28
33
|
* @param intlayerConfig - Resolved intlayer configuration.
|
|
29
34
|
* @param pruneContext - Shared state produced by the usage analyser that
|
|
30
35
|
* runs inside `intlayerOptimizePlugin`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerPrunePlugin.d.ts","names":[],"sources":["../../src/intlayerPrunePlugin.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"intlayerPrunePlugin.d.ts","names":[],"sources":["../../src/intlayerPrunePlugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiNa,gBAAa,gBACR,gBAAc,cAChB,iBACb"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-intlayer",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
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": [
|
|
@@ -81,15 +81,15 @@
|
|
|
81
81
|
"typecheck": "tsc --noEmit --project tsconfig.types.json"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@intlayer/babel": "9.
|
|
85
|
-
"@intlayer/config": "9.
|
|
86
|
-
"@intlayer/core": "9.
|
|
87
|
-
"@intlayer/dictionaries-entry": "9.
|
|
88
|
-
"@intlayer/engine": "9.
|
|
89
|
-
"@intlayer/types": "9.
|
|
84
|
+
"@intlayer/babel": "9.4.0",
|
|
85
|
+
"@intlayer/config": "9.4.0",
|
|
86
|
+
"@intlayer/core": "9.4.0",
|
|
87
|
+
"@intlayer/dictionaries-entry": "9.4.0",
|
|
88
|
+
"@intlayer/engine": "9.4.0",
|
|
89
|
+
"@intlayer/types": "9.4.0"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
|
-
"@types/node": "26.
|
|
92
|
+
"@types/node": "26.3.0",
|
|
93
93
|
"@utils/ts-config": "1.0.4",
|
|
94
94
|
"@utils/ts-config-types": "1.0.4",
|
|
95
95
|
"@utils/tsdown-config": "1.0.4",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
102
|
"@babel/core": ">=6.0.0",
|
|
103
|
-
"@intlayer/svelte-compiler": "9.
|
|
104
|
-
"@intlayer/vue-compiler": "9.
|
|
103
|
+
"@intlayer/svelte-compiler": "9.4.0",
|
|
104
|
+
"@intlayer/vue-compiler": "9.4.0",
|
|
105
105
|
"h3": ">=1.0.0",
|
|
106
106
|
"vite": ">=4.0.0"
|
|
107
107
|
},
|