vite-intlayer 9.1.3 → 9.3.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.
@@ -117,11 +117,10 @@ const intlayerMinify = (intlayerConfig, pruneContext) => {
117
117
  colorize("editor.enabled", ANSIColors.BLUE),
118
118
  "is",
119
119
  colorize("true", ANSIColors.GREY_DARK),
120
- "— the editor requires full dictionary content."
120
+ colorize("— the editor requires full dictionary content", ANSIColors.GREY)
121
121
  ]), { cacheTimeoutMs: 1e3 * 10 });
122
122
  return false;
123
123
  }
124
- runOnce(join(baseDir, ".intlayer", "cache", "intlayer-minify-plugin-enabled.lock"), () => logger(["Dictionary minification", colorize("enabled", ANSIColors.GREEN)]), { cacheTimeoutMs: 1e3 * 10 });
125
124
  return true;
126
125
  };
127
126
  return [{
@@ -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 '— the editor requires full dictionary content.',\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-minify-plugin-enabled.lock'\n ),\n () =>\n logger([\n 'Dictionary minification',\n colorize('enabled', ANSIColors.GREEN),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\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 * 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,13 +1,14 @@
1
1
  import { intlayerVueAsyncPlugin } from "./intlayerVueAsyncPlugin.mjs";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { INTLAYER_USAGE_REGEX, SOURCE_FILE_REGEX, analyzeFieldUsageInFile, buildNestedRenameMapFromContent, buildUsageCheckRegex, getNestedRenameEntryAtPath, optimizeSourceFile, renameFieldsInSourceFile } from "@intlayer/babel";
4
+ import { INTLAYER_USAGE_REGEX, SOURCE_FILE_REGEX, analyzeFieldUsageInFile, buildNestedRenameMapFromContent, buildUsageCheckRegex, getNestedRenameEntryAtPath, optimizeSourceFile, preserveNestedDictionaryFields, renameFieldsInSourceFile } from "@intlayer/babel";
5
5
  import * as ANSIColors from "@intlayer/config/colors";
6
6
  import { colorize, colorizeKey, colorizeNumber, getAppLogger } from "@intlayer/config/logger";
7
7
  import { normalizePath } from "@intlayer/config/utils";
8
8
  import { buildComponentFilesList, formatPath, runOnce } from "@intlayer/engine/utils";
9
9
  import { getDictionaries } from "@intlayer/dictionaries-entry";
10
10
  import { IMPORT_MODE } from "@intlayer/config/defaultValues";
11
+ import { getNestedDictionaryGraph, getNestedDictionaryReferences } from "@intlayer/core/dictionaryManipulator";
11
12
 
12
13
  //#region src/intlayerOptimizePlugin.ts
13
14
  /**
@@ -53,6 +54,8 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
53
54
  Object.values(dictionaries).forEach((dictionary) => {
54
55
  dictionaryKeyToImportModeMap[dictionary.key] = dictionary.importMode ?? importMode ?? IMPORT_MODE;
55
56
  });
57
+ const nestedDictionaryReferences = getNestedDictionaryReferences(Object.values(dictionaries));
58
+ const nestingDictionaryKeys = [...getNestedDictionaryGraph(Object.values(dictionaries)).keys()];
56
59
  const isBuildOptimizeEnabled = (_config, env) => {
57
60
  const isBuildCommand = env.command === "build";
58
61
  return optimize === void 0 && isBuildCommand || optimize === true;
@@ -172,6 +175,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
172
175
  }
173
176
  }
174
177
  }
178
+ preserveNestedDictionaryFields(pruneContext, nestedDictionaryReferences);
175
179
  for (const [dictionaryKey, sourceFilePaths] of pruneContext.dictionaryKeysWithUntrackedBindings) logger([
176
180
  `Dictionary`,
177
181
  colorizeKey(dictionaryKey),
@@ -247,12 +251,16 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
247
251
  apply: (_config, env) => {
248
252
  const isBuildCommand = env.command === "build";
249
253
  if (!isBuildCommand || !(optimize === void 0 && isBuildCommand || optimize === true)) return false;
250
- runOnce(join(baseDir, ".intlayer", "cache", "intlayer-optimize-plugin-enabled.lock"), () => logger([
251
- `Build optimization ${colorize("enabled", ANSIColors.GREEN)}`,
252
- colorize("(import mode:", ANSIColors.GREY_DARK),
253
- colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),
254
- colorize(")", ANSIColors.GREY_DARK)
255
- ]), { cacheTimeoutMs: 1e3 * 10 });
254
+ runOnce(join(baseDir, ".intlayer", "cache", "intlayer-optimize-plugin-enabled.lock"), () => {
255
+ logger([
256
+ `Build optimization ${colorize("enabled", ANSIColors.GREEN)}`,
257
+ colorize("(import mode:", ANSIColors.GREY_DARK),
258
+ colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),
259
+ colorize(")", ANSIColors.GREY_DARK)
260
+ ]);
261
+ if (!editorEnabled && minify) logger(`Dictionary minification ${colorize("enabled", ANSIColors.GREEN)}`);
262
+ if (!editorEnabled && purge) logger(`Dictionary purge unused keys ${colorize("enabled", ANSIColors.GREEN)}`);
263
+ }, { cacheTimeoutMs: 1e3 * 10 });
256
264
  return true;
257
265
  },
258
266
  transform: async (sourceCode, moduleId, options) => {
@@ -280,6 +288,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
280
288
  importMode,
281
289
  filesList: transformableFilesList,
282
290
  replaceDictionaryEntry: !compatCallers?.length,
291
+ nestingDictionaryKeys,
283
292
  dictionaryModeMap: dictionaryKeyToImportModeMap,
284
293
  isServer: options?.ssr === true,
285
294
  compatCallers
@@ -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 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 { 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 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 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 { 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 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 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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"}
@@ -9,7 +9,8 @@ import { BLUE } from "@intlayer/config/colors";
9
9
  import { colorize, getAppLogger } from "@intlayer/config/logger";
10
10
  import { getConfiguration } from "@intlayer/config/node";
11
11
  import { getAlias, getHasDictionarySelector, getUnusedNodeTypesAsync } from "@intlayer/config/utils";
12
- import { formatDictionarySelectorEnvVar, formatNodeTypeToEnvVar, getConfigEnvVars } from "@intlayer/config/envVars";
12
+ import { formatDictionarySelectorEnvVar, formatNodeTypeToEnvVar, formatOptimizedNestingEnvVar, getConfigEnvVars } from "@intlayer/config/envVars";
13
+ import { resolveProxyMode } from "@intlayer/core/localization";
13
14
  import { getDictionaries } from "@intlayer/dictionaries-entry";
14
15
  import { logConfigDetails } from "@intlayer/engine/logConfigDetails";
15
16
 
@@ -69,6 +70,7 @@ const intlayerPlugin = (configOptions) => {
69
70
  let define = { "process.env.INTLAYER": "\"true\"" };
70
71
  if (isBuildCommand) {
71
72
  const dictionaries = getDictionaries(intlayerConfig);
73
+ const isOptimizeEnabled = intlayerConfig.build.optimize !== false;
72
74
  if (Object.keys(dictionaries).length === 0) appLogger("No dictionaries found. Please check your configuration.", { isVerbose: true });
73
75
  const unusedNodeTypes = await getUnusedNodeTypesAsync(dictionaries);
74
76
  if (unusedNodeTypes.length > 0) appLogger(["Filtering out unused logic:", unusedNodeTypes.filter((key) => ![
@@ -79,6 +81,7 @@ const intlayerPlugin = (configOptions) => {
79
81
  define = {
80
82
  ...define,
81
83
  ...formatNodeTypeToEnvVar(unusedNodeTypes, (key) => `process.env.${key}`, (value) => `"${value}"`),
84
+ ...formatOptimizedNestingEnvVar(isOptimizeEnabled, (key) => `process.env.${key}`, (value) => `"${value}"`),
82
85
  ...formatDictionarySelectorEnvVar(getHasDictionarySelector(dictionaries), (key) => `process.env.${key}`, (value) => `"${value}"`),
83
86
  ...getConfigEnvVars(intlayerConfig, (key) => `process.env.${key}`, (value) => `"${value}"`)
84
87
  };
@@ -102,7 +105,7 @@ const intlayerPlugin = (configOptions) => {
102
105
  plugins.push(intlayerPrune(intlayerConfig, pruneContext));
103
106
  plugins.push(intlayerMinify(intlayerConfig, pruneContext));
104
107
  if (intlayerConfig.compiler?.enabled && intlayerConfig.compiler?.output) plugins.push(intlayerCompiler({ configOptions: getConfigOptions }));
105
- if (intlayerConfig.routing.enableProxy) plugins.push(intlayerProxy({
108
+ if (resolveProxyMode(intlayerConfig.routing.enableProxy) !== "disabled") plugins.push(intlayerProxy({
106
109
  ...proxy,
107
110
  configOptions: getConfigOptions
108
111
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerPlugin.mjs","names":[],"sources":["../../src/intlayerPlugin.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { type CompatCallerConfig, createPruneContext } from '@intlayer/babel';\nimport { BLUE } from '@intlayer/config/colors';\nimport {\n formatDictionarySelectorEnvVar,\n formatNodeTypeToEnvVar,\n getConfigEnvVars,\n} from '@intlayer/config/envVars';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n getAlias,\n getHasDictionarySelector,\n getUnusedNodeTypesAsync,\n} from '@intlayer/config/utils';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport { logConfigDetails } from '@intlayer/engine/logConfigDetails';\nimport type { PluginOption } from 'vite';\nimport { intlayerCompiler } from './IntlayerCompilerPlugin';\nimport { intlayerMinify } from './intlayerMinifyPlugin';\nimport { intlayerOptimize } from './intlayerOptimizePlugin';\nimport {\n type IntlayerProxyPluginOptions,\n intlayerProxy,\n} from './intlayerProxyPlugin';\nimport { intlayerPrune } from './intlayerPrunePlugin';\n\n/**\n * Extended options accepted by the intlayer Vite plugin.\n *\n * Extends {@link GetConfigurationOptions} with compat-adapter caller\n * configurations. Compat adapter packages (e.g. `@intlayer/react-i18next/plugin`,\n * `@intlayer/vue-i18n/plugin`) inject their own caller configs here so the\n * field-usage analyser can recognise their translation function call patterns\n * and prune unused dictionary fields accordingly.\n */\nexport type IntlayerPluginOptions = GetConfigurationOptions & {\n /**\n * Compat-adapter namespace caller configurations to pass to the\n * field-usage analyser (Vite `buildStart` phase).\n *\n * Defined by each compat adapter package; the core `vite-intlayer` package\n * ships with an empty default to stay framework-agnostic.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Options forwarded to the bundled locale-routing proxy (`intlayerProxy`).\n *\n * Since Intlayer v9 the proxy is plugged directly into the main `intlayer()`\n * plugin (controlled by `routing.enableProxy`, `true` by default). These\n * routing options let you tune that bundled proxy without registering it\n * separately — e.g. ignore API routes from locale routing.\n *\n * @example\n * ```ts\n * intlayer({ proxy: { ignore: (req) => req.url?.startsWith('/api') } })\n * ```\n */\n proxy?: Pick<IntlayerProxyPluginOptions, 'ignore'>;\n};\n\n/**\n * Vite plugin that integrates Intlayer into the Vite build process.\n *\n * It handles:\n * 1. Preparing Intlayer resources (dictionaries) before build.\n * 2. Configuring Vite aliases for dictionary access.\n * 3. Setting up dev-server watchers for content changes.\n * 4. Applying build optimizations (tree-shaking dictionaries).\n *\n * @param configOptions - Optional configuration to override default Intlayer settings.\n * @returns A Vite plugin option.\n *\n * @example\n * ```ts\n * import { intlayer } from 'vite-intlayer';\n *\n * export default defineConfig({\n * plugins: [intlayer()],\n * });\n *\n * ```\n * @deprecated Rename to intlayer instead\n */\nexport const intlayerPlugin = (\n configOptions?: IntlayerPluginOptions\n): PluginOption => {\n const { compatCallers, proxy, ...getConfigOptions } = configOptions ?? {};\n const intlayerConfig = getConfiguration(getConfigOptions);\n logConfigDetails(getConfigOptions);\n const appLogger = getAppLogger(intlayerConfig);\n\n const alias = getAlias({\n configuration: intlayerConfig,\n formatter: (value: string) => resolve(value),\n });\n\n const aliasPackages = Object.keys(alias);\n\n const plugins: PluginOption[] = [\n {\n name: 'vite-intlayer-plugin',\n\n apply: (_config, env) => {\n // Don't apply intlayer plugin during `preview` command\n const isPreviewCommand =\n env.command === 'serve' && env.mode === 'production';\n\n // But if liveSync is enabled, ensure the data are fresh\n const isLiveSyncEnabled = intlayerConfig.editor.liveSync;\n\n return !isPreviewCommand || isLiveSyncEnabled;\n },\n\n config: async (_config, env) => {\n const { mode } = intlayerConfig.build;\n\n const isDevCommand =\n env.command === 'serve' && env.mode === 'development';\n const isBuildCommand = env.command === 'build';\n\n // Only call prepareIntlayer during `dev` or `build` (not during `preview`)\n // If prod: clean and rebuild once\n // If dev: rebuild only once if it's more than 1 hour since last rebuild\n if (isDevCommand || isBuildCommand || mode === 'auto') {\n // Lazily load the heavy build pipeline (`@intlayer/engine/build`) only\n // when a rebuild is actually required, keeping the Vite config phase\n // lightweight (e.g. during `preview`).\n const { prepareIntlayer } = await import('@intlayer/engine/build');\n\n // prepareIntlayer use runOnce to ensure to run only once because will run twice on client and server side otherwise\n await prepareIntlayer(intlayerConfig, {\n clean: isBuildCommand,\n cacheTimeoutMs: isBuildCommand\n ? 1000 * 30 // 30 seconds for build (to ensure to rebuild all dictionaries)\n : 1000 * 60 * 60, // 1 hour for dev (default cache timeout)\n env: isBuildCommand ? 'prod' : 'dev',\n });\n }\n\n let define: Record<string, string> = {\n // Preset an env var to avoid 'process is not defined' error\n // Needed for some libraries that does not add process.env\n 'process.env.INTLAYER': '\"true\"',\n };\n\n if (isBuildCommand) {\n const dictionaries = getDictionaries(intlayerConfig);\n\n if (Object.keys(dictionaries).length === 0) {\n appLogger(\n 'No dictionaries found. Please check your configuration.',\n {\n isVerbose: true,\n }\n );\n }\n\n const unusedNodeTypes = await getUnusedNodeTypesAsync(dictionaries);\n\n if (unusedNodeTypes.length > 0) {\n appLogger(\n [\n 'Filtering out unused logic:',\n unusedNodeTypes\n .filter(\n (key) =>\n !['reactNode', 'solidNode', 'preactNode'].includes(key)\n )\n .map((key) => colorize(key, BLUE))\n .join(', '),\n ],\n {\n isVerbose: true,\n }\n );\n }\n\n define = {\n ...define,\n\n // Tree shacking env var based on config\n ...formatNodeTypeToEnvVar(\n unusedNodeTypes,\n (key) => `process.env.${key}`,\n (value) => `\"${value}\"`\n ),\n\n // Tree shacking env var for the dictionary selector logic\n // (collections / variants)\n ...formatDictionarySelectorEnvVar(\n getHasDictionarySelector(dictionaries),\n (key) => `process.env.${key}`,\n (value) => `\"${value}\"`\n ),\n\n // Tree shacking env var based on config\n ...getConfigEnvVars(\n intlayerConfig,\n (key) => `process.env.${key}`,\n (value) => `\"${value}\"` // Wrap by \"\" to ensure env var set properly\n ),\n };\n }\n\n // mergeConfig handles both array and record alias formats,\n // and correctly appends to optimizeDeps.exclude / ssr.noExternal\n return {\n define,\n resolve: {\n alias,\n },\n optimizeDeps: {\n // Exclude alias entry points since they're local files, not npm packages\n exclude: aliasPackages,\n },\n ssr: {\n // Ensure intlayer packages are bundled so aliases are applied\n noExternal: [/(^@intlayer\\/|intlayer$)/],\n },\n };\n },\n\n configureServer: async (server) => {\n if (server.config.mode === 'development') {\n // Lazily load the file watcher (`@intlayer/engine/watcher`, which pulls\n // in `@parcel/watcher`) only when the dev server actually starts.\n const { watch } = await import('@intlayer/engine/watcher');\n\n // Start watching (assuming watch is also async)\n await watch({ configuration: intlayerConfig });\n }\n },\n },\n ];\n\n // Shared mutable state: the optimize plugin writes field-usage data during\n // buildStart; the prune and minify plugins read it during transform.\n const pruneContext = createPruneContext();\n\n // Babel transform: rewrites useIntlayer/getIntlayer calls and injects\n // JSON / dynamic-mjs imports. Also runs the usage analyser in buildStart.\n plugins.push(intlayerOptimize(intlayerConfig, pruneContext, compatCallers));\n\n // Prune: removes unused content fields from dictionary JSON files.\n // Runs with enforce:'pre' so it intercepts raw JSON before Vite's\n // built-in JSON → ESM conversion.\n plugins.push(intlayerPrune(intlayerConfig, pruneContext));\n\n // Minify: compacts dictionary JSON files (parse + re-stringify).\n // Registered after prune so it receives already-pruned output when both options are active.\n plugins.push(intlayerMinify(intlayerConfig, pruneContext));\n\n // Compiler: extracts content declared inline in components into dictionaries.\n // Bundled directly into the main plugin so users no longer need to register\n // `intlayerCompiler()` separately. Only added when the compiler is enabled and\n // an `output` path is configured. Registering `intlayerCompiler()` manually as\n // well is safe — the compiler deduplicates itself.\n if (intlayerConfig.compiler?.enabled && intlayerConfig.compiler?.output) {\n plugins.push(intlayerCompiler({ configOptions: getConfigOptions }));\n }\n\n // Proxy: locale-routing middleware (detection / redirect / rewrite) for dev,\n // preview, and production SSR. Bundled directly into the main plugin and\n // controlled by the `routing.enableProxy` option (true by default).\n // Registering `intlayerProxy()` manually as well is safe — the proxy\n // deduplicates itself.\n if (intlayerConfig.routing.enableProxy) {\n plugins.push(intlayerProxy({ ...proxy, configOptions: getConfigOptions }));\n }\n\n return plugins;\n};\n\n/**\n * A Vite plugin that integrates Intlayer configuration into the build process\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayer() ],\n * });\n * ```\n */\nexport const intlayer = intlayerPlugin;\n/**\n * @deprecated Rename to intlayer instead\n *\n * A Vite plugin that integrates Intlayer configuration into the build process\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayer() ],\n * });\n * ```\n */\nexport const intLayerPlugin = intlayerPlugin;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwFA,MAAa,kBACX,kBACiB;CACjB,MAAM,EAAE,eAAe,OAAO,GAAG,qBAAqB,iBAAiB,CAAC;CACxE,MAAM,iBAAiB,iBAAiB,gBAAgB;CACxD,iBAAiB,gBAAgB;CACjC,MAAM,YAAY,aAAa,cAAc;CAE7C,MAAM,QAAQ,SAAS;EACrB,eAAe;EACf,YAAY,UAAkB,QAAQ,KAAK;CAC7C,CAAC;CAED,MAAM,gBAAgB,OAAO,KAAK,KAAK;CAEvC,MAAM,UAA0B,CAC9B;EACE,MAAM;EAEN,QAAQ,SAAS,QAAQ;GAEvB,MAAM,mBACJ,IAAI,YAAY,WAAW,IAAI,SAAS;GAG1C,MAAM,oBAAoB,eAAe,OAAO;GAEhD,OAAO,CAAC,oBAAoB;EAC9B;EAEA,QAAQ,OAAO,SAAS,QAAQ;GAC9B,MAAM,EAAE,SAAS,eAAe;GAEhC,MAAM,eACJ,IAAI,YAAY,WAAW,IAAI,SAAS;GAC1C,MAAM,iBAAiB,IAAI,YAAY;GAKvC,IAAI,gBAAgB,kBAAkB,SAAS,QAAQ;IAIrD,MAAM,EAAE,oBAAoB,MAAM,OAAO;IAGzC,MAAM,gBAAgB,gBAAgB;KACpC,OAAO;KACP,gBAAgB,iBACZ,MAAO,KACP,MAAO,KAAK;KAChB,KAAK,iBAAiB,SAAS;IACjC,CAAC;GACH;GAEA,IAAI,SAAiC,EAGnC,wBAAwB,WAC1B;GAEA,IAAI,gBAAgB;IAClB,MAAM,eAAe,gBAAgB,cAAc;IAEnD,IAAI,OAAO,KAAK,YAAY,CAAC,CAAC,WAAW,GACvC,UACE,2DACA,EACE,WAAW,KACb,CACF;IAGF,MAAM,kBAAkB,MAAM,wBAAwB,YAAY;IAElE,IAAI,gBAAgB,SAAS,GAC3B,UACE,CACE,+BACA,gBACG,QACE,QACC,CAAC;KAAC;KAAa;KAAa;IAAY,CAAC,CAAC,SAAS,GAAG,CAC1D,CAAC,CACA,KAAK,QAAQ,SAAS,KAAK,IAAI,CAAC,CAAC,CACjC,KAAK,IAAI,CACd,GACA,EACE,WAAW,KACb,CACF;IAGF,SAAS;KACP,GAAG;KAGH,GAAG,uBACD,kBACC,QAAQ,eAAe,QACvB,UAAU,IAAI,MAAM,EACvB;KAIA,GAAG,+BACD,yBAAyB,YAAY,IACpC,QAAQ,eAAe,QACvB,UAAU,IAAI,MAAM,EACvB;KAGA,GAAG,iBACD,iBACC,QAAQ,eAAe,QACvB,UAAU,IAAI,MAAM,EACvB;IACF;GACF;GAIA,OAAO;IACL;IACA,SAAS,EACP,MACF;IACA,cAAc,EAEZ,SAAS,cACX;IACA,KAAK,EAEH,YAAY,CAAC,0BAA0B,EACzC;GACF;EACF;EAEA,iBAAiB,OAAO,WAAW;GACjC,IAAI,OAAO,OAAO,SAAS,eAAe;IAGxC,MAAM,EAAE,UAAU,MAAM,OAAO;IAG/B,MAAM,MAAM,EAAE,eAAe,eAAe,CAAC;GAC/C;EACF;CACF,CACF;CAIA,MAAM,eAAe,mBAAmB;CAIxC,QAAQ,KAAK,iBAAiB,gBAAgB,cAAc,aAAa,CAAC;CAK1E,QAAQ,KAAK,cAAc,gBAAgB,YAAY,CAAC;CAIxD,QAAQ,KAAK,eAAe,gBAAgB,YAAY,CAAC;CAOzD,IAAI,eAAe,UAAU,WAAW,eAAe,UAAU,QAC/D,QAAQ,KAAK,iBAAiB,EAAE,eAAe,iBAAiB,CAAC,CAAC;CAQpE,IAAI,eAAe,QAAQ,aACzB,QAAQ,KAAK,cAAc;EAAE,GAAG;EAAO,eAAe;CAAiB,CAAC,CAAC;CAG3E,OAAO;AACT;;;;;;;;;;;AAYA,MAAa,WAAW;;;;;;;;;;;;;AAaxB,MAAa,iBAAiB"}
1
+ {"version":3,"file":"intlayerPlugin.mjs","names":[],"sources":["../../src/intlayerPlugin.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { type CompatCallerConfig, createPruneContext } from '@intlayer/babel';\nimport { BLUE } from '@intlayer/config/colors';\nimport {\n formatDictionarySelectorEnvVar,\n formatNodeTypeToEnvVar,\n formatOptimizedNestingEnvVar,\n getConfigEnvVars,\n} from '@intlayer/config/envVars';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n getAlias,\n getHasDictionarySelector,\n getUnusedNodeTypesAsync,\n} from '@intlayer/config/utils';\nimport { resolveProxyMode } from '@intlayer/core/localization';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport { logConfigDetails } from '@intlayer/engine/logConfigDetails';\nimport type { PluginOption } from 'vite';\nimport { intlayerCompiler } from './IntlayerCompilerPlugin';\nimport { intlayerMinify } from './intlayerMinifyPlugin';\nimport { intlayerOptimize } from './intlayerOptimizePlugin';\nimport {\n type IntlayerProxyPluginOptions,\n intlayerProxy,\n} from './intlayerProxyPlugin';\nimport { intlayerPrune } from './intlayerPrunePlugin';\n\n/**\n * Extended options accepted by the intlayer Vite plugin.\n *\n * Extends {@link GetConfigurationOptions} with compat-adapter caller\n * configurations. Compat adapter packages (e.g. `@intlayer/react-i18next/plugin`,\n * `@intlayer/vue-i18n/plugin`) inject their own caller configs here so the\n * field-usage analyser can recognise their translation function call patterns\n * and prune unused dictionary fields accordingly.\n */\nexport type IntlayerPluginOptions = GetConfigurationOptions & {\n /**\n * Compat-adapter namespace caller configurations to pass to the\n * field-usage analyser (Vite `buildStart` phase).\n *\n * Defined by each compat adapter package; the core `vite-intlayer` package\n * ships with an empty default to stay framework-agnostic.\n */\n compatCallers?: CompatCallerConfig[];\n\n /**\n * Options forwarded to the bundled locale-routing proxy (`intlayerProxy`).\n *\n * Since Intlayer v9 the proxy is plugged directly into the main `intlayer()`\n * plugin (controlled by `routing.enableProxy`, unset by default, which\n * selects auto mode). These routing options let you tune that bundled proxy\n * without registering it separately — e.g. ignore API routes from locale\n * routing.\n *\n * @example\n * ```ts\n * intlayer({ proxy: { ignore: (req) => req.url?.startsWith('/api') } })\n * ```\n */\n proxy?: Pick<IntlayerProxyPluginOptions, 'ignore'>;\n};\n\n/**\n * Vite plugin that integrates Intlayer into the Vite build process.\n *\n * It handles:\n * 1. Preparing Intlayer resources (dictionaries) before build.\n * 2. Configuring Vite aliases for dictionary access.\n * 3. Setting up dev-server watchers for content changes.\n * 4. Applying build optimizations (tree-shaking dictionaries).\n *\n * @param configOptions - Optional configuration to override default Intlayer settings.\n * @returns A Vite plugin option.\n *\n * @example\n * ```ts\n * import { intlayer } from 'vite-intlayer';\n *\n * export default defineConfig({\n * plugins: [intlayer()],\n * });\n *\n * ```\n * @deprecated Rename to intlayer instead\n */\nexport const intlayerPlugin = (\n configOptions?: IntlayerPluginOptions\n): PluginOption => {\n const { compatCallers, proxy, ...getConfigOptions } = configOptions ?? {};\n const intlayerConfig = getConfiguration(getConfigOptions);\n logConfigDetails(getConfigOptions);\n const appLogger = getAppLogger(intlayerConfig);\n\n const alias = getAlias({\n configuration: intlayerConfig,\n formatter: (value: string) => resolve(value),\n });\n\n const aliasPackages = Object.keys(alias);\n\n const plugins: PluginOption[] = [\n {\n name: 'vite-intlayer-plugin',\n\n apply: (_config, env) => {\n // Don't apply intlayer plugin during `preview` command\n const isPreviewCommand =\n env.command === 'serve' && env.mode === 'production';\n\n // But if liveSync is enabled, ensure the data are fresh\n const isLiveSyncEnabled = intlayerConfig.editor.liveSync;\n\n return !isPreviewCommand || isLiveSyncEnabled;\n },\n\n config: async (_config, env) => {\n const { mode } = intlayerConfig.build;\n\n const isDevCommand =\n env.command === 'serve' && env.mode === 'development';\n const isBuildCommand = env.command === 'build';\n\n // Only call prepareIntlayer during `dev` or `build` (not during `preview`)\n // If prod: clean and rebuild once\n // If dev: rebuild only once if it's more than 1 hour since last rebuild\n if (isDevCommand || isBuildCommand || mode === 'auto') {\n // Lazily load the heavy build pipeline (`@intlayer/engine/build`) only\n // when a rebuild is actually required, keeping the Vite config phase\n // lightweight (e.g. during `preview`).\n const { prepareIntlayer } = await import('@intlayer/engine/build');\n\n // prepareIntlayer use runOnce to ensure to run only once because will run twice on client and server side otherwise\n await prepareIntlayer(intlayerConfig, {\n clean: isBuildCommand,\n cacheTimeoutMs: isBuildCommand\n ? 1000 * 30 // 30 seconds for build (to ensure to rebuild all dictionaries)\n : 1000 * 60 * 60, // 1 hour for dev (default cache timeout)\n env: isBuildCommand ? 'prod' : 'dev',\n });\n }\n\n let define: Record<string, string> = {\n // Preset an env var to avoid 'process is not defined' error\n // Needed for some libraries that does not add process.env\n 'process.env.INTLAYER': '\"true\"',\n };\n\n if (isBuildCommand) {\n const dictionaries = getDictionaries(intlayerConfig);\n\n // Mirrors the optimize plugin's own `apply` condition: the local\n // `nest()` resolver may only be selected when that transform runs,\n // since it is the transform that injects the nest attachment.\n const isOptimizeEnabled = intlayerConfig.build.optimize !== false;\n\n if (Object.keys(dictionaries).length === 0) {\n appLogger(\n 'No dictionaries found. Please check your configuration.',\n {\n isVerbose: true,\n }\n );\n }\n\n const unusedNodeTypes = await getUnusedNodeTypesAsync(dictionaries);\n\n if (unusedNodeTypes.length > 0) {\n appLogger(\n [\n 'Filtering out unused logic:',\n unusedNodeTypes\n .filter(\n (key) =>\n !['reactNode', 'solidNode', 'preactNode'].includes(key)\n )\n .map((key) => colorize(key, BLUE))\n .join(', '),\n ],\n {\n isVerbose: true,\n }\n );\n }\n\n define = {\n ...define,\n\n // Tree shacking env var based on config\n ...formatNodeTypeToEnvVar(\n unusedNodeTypes,\n (key) => `process.env.${key}`,\n (value) => `\"${value}\"`\n ),\n\n // Selects the local `nest()` resolver, which reads the nest\n // targets attached to each dictionary by the optimize transform\n // instead of the registry that transform empties.\n ...formatOptimizedNestingEnvVar(\n isOptimizeEnabled,\n (key) => `process.env.${key}`,\n (value) => `\"${value}\"`\n ),\n\n // Tree shacking env var for the dictionary selector logic\n // (collections / variants)\n ...formatDictionarySelectorEnvVar(\n getHasDictionarySelector(dictionaries),\n (key) => `process.env.${key}`,\n (value) => `\"${value}\"`\n ),\n\n // Tree shacking env var based on config\n ...getConfigEnvVars(\n intlayerConfig,\n (key) => `process.env.${key}`,\n (value) => `\"${value}\"` // Wrap by \"\" to ensure env var set properly\n ),\n };\n }\n\n // mergeConfig handles both array and record alias formats,\n // and correctly appends to optimizeDeps.exclude / ssr.noExternal\n return {\n define,\n resolve: {\n alias,\n },\n optimizeDeps: {\n // Exclude alias entry points since they're local files, not npm packages\n exclude: aliasPackages,\n },\n ssr: {\n // Ensure intlayer packages are bundled so aliases are applied\n noExternal: [/(^@intlayer\\/|intlayer$)/],\n },\n };\n },\n\n configureServer: async (server) => {\n if (server.config.mode === 'development') {\n // Lazily load the file watcher (`@intlayer/engine/watcher`, which pulls\n // in `@parcel/watcher`) only when the dev server actually starts.\n const { watch } = await import('@intlayer/engine/watcher');\n\n // Start watching (assuming watch is also async)\n await watch({ configuration: intlayerConfig });\n }\n },\n },\n ];\n\n // Shared mutable state: the optimize plugin writes field-usage data during\n // buildStart; the prune and minify plugins read it during transform.\n const pruneContext = createPruneContext();\n\n // Babel transform: rewrites useIntlayer/getIntlayer calls and injects\n // JSON / dynamic-mjs imports. Also runs the usage analyser in buildStart.\n plugins.push(intlayerOptimize(intlayerConfig, pruneContext, compatCallers));\n\n // Prune: removes unused content fields from dictionary JSON files.\n // Runs with enforce:'pre' so it intercepts raw JSON before Vite's\n // built-in JSON → ESM conversion.\n plugins.push(intlayerPrune(intlayerConfig, pruneContext));\n\n // Minify: compacts dictionary JSON files (parse + re-stringify).\n // Registered after prune so it receives already-pruned output when both options are active.\n plugins.push(intlayerMinify(intlayerConfig, pruneContext));\n\n // Compiler: extracts content declared inline in components into dictionaries.\n // Bundled directly into the main plugin so users no longer need to register\n // `intlayerCompiler()` separately. Only added when the compiler is enabled and\n // an `output` path is configured. Registering `intlayerCompiler()` manually as\n // well is safe — the compiler deduplicates itself.\n if (intlayerConfig.compiler?.enabled && intlayerConfig.compiler?.output) {\n plugins.push(intlayerCompiler({ configOptions: getConfigOptions }));\n }\n\n // Proxy: locale-routing middleware (detection / redirect / rewrite) for dev,\n // preview, and production SSR. Bundled directly into the main plugin and\n // controlled by the `routing.enableProxy` option, which is unset by default\n // and therefore resolves to auto mode. Only an explicit `false` skips\n // registration — a plain truthiness check would wrongly drop the proxy for\n // every project that never set the option.\n // Registering `intlayerProxy()` manually as well is safe — the proxy\n // deduplicates itself.\n if (resolveProxyMode(intlayerConfig.routing.enableProxy) !== 'disabled') {\n plugins.push(intlayerProxy({ ...proxy, configOptions: getConfigOptions }));\n }\n\n return plugins;\n};\n\n/**\n * A Vite plugin that integrates Intlayer configuration into the build process\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayer() ],\n * });\n * ```\n */\nexport const intlayer = intlayerPlugin;\n/**\n * @deprecated Rename to intlayer instead\n *\n * A Vite plugin that integrates Intlayer configuration into the build process\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayer() ],\n * });\n * ```\n */\nexport const intLayerPlugin = intlayerPlugin;\n"],"mappings":"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"}
@@ -1,10 +1,9 @@
1
1
  import { createPrimaryInstanceGuard } from "./dedupePlugin.mjs";
2
- import * as ANSIColors from "@intlayer/config/colors";
3
- import { colorize, getAppLogger } from "@intlayer/config/logger";
2
+ import { getAppLogger } from "@intlayer/config/logger";
4
3
  import { getConfiguration } from "@intlayer/config/node";
4
+ import { formatProxyEnabledMessage, getCanonicalPath, getDomainHostname, getDomainOrigin, getInternalPath, getLocaleFromDomain, getLocalizedPath, getRewriteRules, isProxyStorageLocaleEnabled, localeDetector, resolveProxyMode } from "@intlayer/core/localization";
5
5
  import { ROUTING_MODE } from "@intlayer/config/defaultValues";
6
6
  import { fileURLToPath, parse } from "node:url";
7
- import { getCanonicalPath, getDomainHostname, getDomainOrigin, getInternalPath, getLocaleFromDomain, getLocalizedPath, getRewriteRules, localeDetector } from "@intlayer/core/localization";
8
7
  import { getCookie, getLocaleFromStorageServer, setLocaleInStorageServer } from "@intlayer/core/utils";
9
8
 
10
9
  //#region src/intlayerProxyPlugin.ts
@@ -34,10 +33,11 @@ const PROXY_PLUGIN_NAME = "vite-intlayer-middleware-plugin";
34
33
  *
35
34
  */
36
35
  const createIntlayerProxyHandler = (options) => {
37
- const { ignore, configOptions } = options ?? {};
36
+ const { ignore, configOptions, isDevServer = false } = options ?? {};
38
37
  const { internationalization, routing } = getConfiguration(configOptions);
39
38
  const { locales: supportedLocales, defaultLocale } = internationalization;
40
- const { basePath = "", mode = ROUTING_MODE, rewrite, domains } = routing;
39
+ const { basePath = "", mode = ROUTING_MODE, rewrite, domains, enableProxy } = routing;
40
+ const canUseStorageLocale = isProxyStorageLocaleEnabled(resolveProxyMode(enableProxy), isDevServer);
41
41
  const redirectCounts = /* @__PURE__ */ new Map();
42
42
  const MAX_REDIRECTS = 10;
43
43
  const REDIRECT_TTL_MS = 2e3;
@@ -56,8 +56,13 @@ const createIntlayerProxyHandler = (options) => {
56
56
  };
57
57
  /**
58
58
  * Retrieves the locale from storage (cookies, localStorage, sessionStorage).
59
+ *
60
+ * Returns `undefined` when the stored locale is not allowed to drive locale
61
+ * resolution (auto mode on a dev/preview server), which makes every caller
62
+ * fall through to `Accept-Language` detection and then the default locale.
59
63
  */
60
64
  const getStorageLocale = (req) => {
65
+ if (!canUseStorageLocale) return void 0;
61
66
  return getLocaleFromStorageServer({ getCookie: (name) => getCookie(name, req.headers.cookie) });
62
67
  };
63
68
  /**
@@ -432,11 +437,21 @@ const createIntlayerProxyHandler = (options) => {
432
437
  * });
433
438
  * ```
434
439
  *
435
- * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (`true` by default). Registering it separately as shown below is now optional.
440
+ * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (unset by default, which selects auto mode). Registering it separately as shown below is now optional.
436
441
  */
437
442
  const intlayerProxy = (options) => {
438
- const handler = createIntlayerProxyHandler(options);
439
- const logger = getAppLogger(getConfiguration(options?.configOptions));
443
+ const handler = createIntlayerProxyHandler({
444
+ ...options,
445
+ isDevServer: true
446
+ });
447
+ const intlayerConfig = getConfiguration(options?.configOptions);
448
+ const logger = getAppLogger(intlayerConfig);
449
+ const isStorageLocaleSuppressed = !isProxyStorageLocaleEnabled(resolveProxyMode(intlayerConfig.routing.enableProxy), true);
450
+ /**
451
+ * Logs that the proxy is serving requests, spelling out when auto mode has
452
+ * suppressed the stored locale so the reported state matches the behaviour.
453
+ */
454
+ const logProxyEnabled = () => logger(formatProxyEnabledMessage(isStorageLocaleSuppressed), { level: "info" });
440
455
  const guard = createPrimaryInstanceGuard(PROXY_PLUGIN_NAME);
441
456
  const plugin = {
442
457
  name: PROXY_PLUGIN_NAME,
@@ -458,12 +473,12 @@ const intlayerProxy = (options) => {
458
473
  },
459
474
  configureServer: (server) => {
460
475
  if (!guard.isPrimary) return;
461
- logger(`Intlayer proxy ${colorize("enabled", ANSIColors.GREEN)}`, { level: "info" });
476
+ logProxyEnabled();
462
477
  server.middlewares.use(handler);
463
478
  },
464
479
  configurePreviewServer: (server) => {
465
480
  if (!guard.isPrimary) return;
466
- logger(`Intlayer proxy ${colorize("enabled", ANSIColors.GREEN)}`, { level: "info" });
481
+ logProxyEnabled();
467
482
  server.middlewares.use(handler);
468
483
  }
469
484
  };
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerProxyPlugin.mjs","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { fileURLToPath, parse } from 'node:url';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n getCanonicalPath,\n getDomainHostname,\n getDomainOrigin,\n getInternalPath,\n getLocaleFromDomain,\n getLocalizedPath,\n getRewriteRules,\n localeDetector,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n/* @ts-ignore - Vite types error */\nimport type { Connect, Plugin } from 'vite';\nimport { createPrimaryInstanceGuard } from './dedupePlugin';\n\nconst PROXY_PLUGIN_NAME = 'vite-intlayer-middleware-plugin';\n\nexport type IntlayerProxyPluginOptions = {\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ ignore: (req) => req.url?.startsWith('/api') }) ],\n * });\n * ```\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Optional Intlayer configuration overrides forwarded to `getConfiguration`.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ configOptions: { override: { ... } } }) ],\n * });\n * ```\n */\n configOptions?: GetConfigurationOptions;\n};\n\n/**\n * A Node.js-compatible Connect middleware function.\n * Compatible with Vite dev/preview server, Node.js http, Express, and h3's\n * `fromNodeMiddleware` wrapper for Nitro/TanStack Start production use.\n */\ntype NodeMiddleware = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: () => void\n) => void;\n\n/**\n * Creates a standalone, framework-agnostic locale-routing middleware.\n *\n * This function contains all the locale detection, redirect, and rewrite logic.\n * It is intentionally separated from the Vite plugin so the same handler can be\n * used in every environment:\n *\n * - **Dev**: wired up automatically by `intlayerProxy` via `configureServer`\n * - **Preview**: wired up automatically by `intlayerProxy` via `configurePreviewServer`\n * - **Production (Nitro / TanStack Start)**: create `server/middleware/intlayerProxy.ts`:\n *\n * @example\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(createIntlayerProxyHandler());\n * ```\n *\n * @param options - Plugin-specific options, such as path ignoring and Intlayer configuration overrides.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n *\n */\nexport const createIntlayerProxyHandler = (\n options?: IntlayerProxyPluginOptions\n): NodeMiddleware => {\n const { ignore, configOptions } = options ?? {};\n const intlayerConfig = getConfiguration(configOptions);\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const { basePath = '', mode = ROUTING_MODE, rewrite, domains } = routing;\n\n type RedirectCounter = { count: number; lastSeen: number };\n const redirectCounts = new Map<string, RedirectCounter>();\n const MAX_REDIRECTS = 10;\n const REDIRECT_TTL_MS = 2_000;\n\n // Derived flags from routing.mode\n const noPrefix =\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params');\n const prefixDefault =\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all'\n ) && mode === 'prefix-all';\n\n const rewriteRules =\n process.env.INTLAYER_ROUTING_REWRITE_RULES !== 'false'\n ? getRewriteRules(rewrite, 'url')\n : undefined;\n\n /**\n * Extracts the hostname from a request's `Host` header, handling IPv6\n * literals (`[::1]:5173` → `::1`) that a plain `split(':')` would mangle.\n */\n const getRequestHostname = (req: IncomingMessage): string => {\n const host = req.headers.host ?? '';\n const ipv6Match = host.match(/^\\[([^\\]]+)\\]/);\n if (ipv6Match) return ipv6Match[1]!;\n return host.split(':')[0] ?? '';\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\n const locale = getLocaleFromStorageServer({\n getCookie: (name: string) => getCookie(name, req.headers.cookie),\n });\n return locale;\n };\n\n /**\n * Appends locale to search params when routing mode is 'search-params'.\n */\n const appendLocaleSearchIfNeeded = (\n search: string | undefined,\n locale: Locale\n ): string | undefined => {\n if (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params') ||\n mode !== 'search-params'\n )\n return search;\n\n const params = new URLSearchParams(search ?? '');\n\n params.set('locale', locale);\n\n return `?${params.toString()}`;\n };\n\n /**\n * Checks whether a pathname starts with the given locale as a full path\n * segment (`/fr` or `/fr/...`). A bare `startsWith('/fr')` would also match\n * unrelated paths like `/friends`, causing wrong prefix stripping and\n * self-redirect loops.\n */\n const hasLocaleSegmentPrefix = (pathname: string, locale: Locale): boolean =>\n pathname === `/${locale}` || pathname.startsWith(`/${locale}/`);\n\n /**\n * Extracts the locale from the URL pathname if present as the first segment.\n * e.g. if pathname is /en/some/page or /en, checks if \"en\" is in supportedLocales.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\n const segments = pathname.split('/').filter(Boolean);\n const firstSegment = segments[0];\n if (firstSegment && supportedLocales.includes(firstSegment as Locale)) {\n return firstSegment as Locale;\n }\n return undefined;\n };\n\n /**\n * Persists the resolved locale onto the outgoing response as a cookie (and\n * header, per `routing.storage`). Only the cookie survives a client redirect,\n * so this is what carries an explicitly-selected locale across a\n * prefix-stripping redirect. Enabled cookie/header targets are resolved by\n * {@link setLocaleInStorageServer} from the config; disabled ones are no-ops.\n */\n const persistLocaleOnResponse = (\n res: ServerResponse<IncomingMessage>,\n locale: Locale\n ) => {\n setLocaleInStorageServer(locale, {\n setCookieStore: (name, value, attributes) => {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n if (attributes.path) parts.push(`Path=${attributes.path}`);\n if (attributes.domain) parts.push(`Domain=${attributes.domain}`);\n if (typeof attributes.expires === 'number')\n parts.push(`Expires=${new Date(attributes.expires).toUTCString()}`);\n if (attributes.secure) parts.push('Secure');\n if (attributes.httpOnly) parts.push('HttpOnly');\n if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`);\n\n const cookieString = parts.join('; ');\n const existing = res.getHeader('Set-Cookie');\n const cookies = Array.isArray(existing)\n ? existing.map(String)\n : existing !== undefined\n ? [String(existing)]\n : [];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n },\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n },\n });\n };\n\n /**\n * Identifies the client issuing a request, so redirect-loop tracking is\n * scoped per client. Without this, concurrent visitors hitting the same\n * legitimate redirect (e.g. `/` → `/fr/`) would share one counter and trip\n * the loop detector under normal production traffic.\n */\n const getClientKey = (req: IncomingMessage): string => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const forwardedIp = (\n Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor\n )\n ?.split(',')[0]\n ?.trim();\n\n return forwardedIp || req.socket?.remoteAddress || 'unknown';\n };\n\n /**\n * Strips any origin from a redirect target, keeping only path, search and\n * hash. Prevents open redirects: user-controlled paths like `//evil.com/x`\n * would otherwise be echoed verbatim into the `Location` header and be\n * interpreted by browsers as a protocol-relative cross-origin URL.\n */\n const toSameOriginUrl = (url: string): string => {\n try {\n const parsed = new URL(url, 'http://intlayer-internal');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return url;\n }\n };\n\n type RedirectOptions = {\n /** Human-readable reason logged when a redirect loop is detected. */\n reason?: string;\n /** The URL of the incoming request, used for redirect-loop tracking. */\n originalUrl?: string;\n /**\n * When provided, the locale is persisted (cookie/header, per config) on\n * the redirect response so the follow-up request resolves the same locale.\n */\n persistLocale?: Locale;\n /**\n * HTTP status for the redirect. Defaults to 302: locale redirects depend\n * on mutable state (cookie, Accept-Language) and must not be cached by\n * the browser as permanent. Domain-routing redirects pass 301 explicitly.\n */\n status?: number;\n /**\n * Allows a cross-origin `Location` target. Only the domain-routing\n * redirect sets this; all other targets are sanitized to same-origin.\n */\n allowCrossOrigin?: boolean;\n };\n\n /**\n * Writes a redirect response with the given new URL.\n */\n const redirectUrl = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n options?: RedirectOptions\n ) => {\n const {\n reason,\n originalUrl,\n persistLocale,\n status = 302,\n allowCrossOrigin = false,\n } = options ?? {};\n\n const targetUrl = allowCrossOrigin ? newUrl : toSameOriginUrl(newUrl);\n\n if (originalUrl) {\n if (originalUrl === targetUrl) {\n console.error('[REDIRECT LOOP DETECTED!]', { originalUrl, reason });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${originalUrl} redirects to itself`\n );\n }\n\n const now = Date.now();\n const trackingKey = `${getClientKey(req)}|${originalUrl} -> ${targetUrl}`;\n const previousEntry = redirectCounts.get(trackingKey);\n const count =\n previousEntry && now - previousEntry.lastSeen < REDIRECT_TTL_MS\n ? previousEntry.count + 1\n : 1;\n\n redirectCounts.set(trackingKey, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: targetUrl,\n reason,\n });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${count} redirects from ${originalUrl}`\n );\n }\n\n for (const [entryKey, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS)\n redirectCounts.delete(entryKey);\n }\n }\n\n if (persistLocale) {\n persistLocaleOnResponse(res, persistLocale);\n }\n\n res.writeHead(status, { Location: targetUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url.\n * Also sets the locale in the response/request headers via storage to mimic\n * Next.js's behaviour of propagating the detected locale downstream.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n if (req.url !== newUrl) {\n req.url = newUrl;\n }\n if (locale) {\n setLocaleInStorageServer(locale, {\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n req.headers[name] = value;\n },\n });\n }\n };\n\n /**\n * Constructs a new path string, optionally including a locale prefix, basePath, and search parameters.\n * - basePath: (e.g. '/myapp')\n * - locale: (e.g. 'en')\n * - currentPath: (e.g. '/products/shoes')\n * - search: (e.g. '?foo=bar')\n */\n const constructPath = (\n locale: Locale,\n currentPath: string,\n search?: string\n ) => {\n // Strip any incoming locale prefix to avoid double-prefixing\n const pathWithoutPrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath.slice(`/${locale}`.length)\n : currentPath;\n\n // Ensure basePath always starts with '/' and has no trailing slash\n const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n // In 'search-params' and 'no-prefix' modes, do not prefix the path with the locale\n if (\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params')\n ) {\n const newPath = search\n ? `${pathWithoutPrefix || '/'}${search}`\n : pathWithoutPrefix || '/';\n return newPath;\n }\n\n // Check if path already starts with locale to avoid double-prefixing\n const pathWithLocalePrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath\n : `/${locale}${currentPath}`;\n\n let newPath = `${normalizedBasePath}${pathWithLocalePrefix}`;\n\n // Special case: if prefixDefault is false and locale is defaultLocale, remove the locale prefix\n if (!prefixDefault && locale === defaultLocale) {\n newPath = `${normalizedBasePath}${pathWithoutPrefix || '/'}`;\n }\n\n // Never emit a trailing slash (`/fr/` for the root path): the framework's\n // trailing-slash normalisation would redirect it back and forth with this\n // proxy, creating an infinite redirect loop.\n if (newPath.length > 1 && newPath.endsWith('/')) {\n newPath = newPath.slice(0, -1);\n }\n\n // Append search parameters if provided\n if (search) {\n newPath += search;\n }\n\n return newPath;\n };\n\n /* --------------------------------------------------------------------\n * Handlers that mirror Next.js style logic\n * --------------------------------------------------------------------\n */\n\n /**\n * If `noPrefix` is true, we never prefix the locale in the URL.\n * We simply rewrite the request to the same path, but with the best-chosen locale\n * in a header or search params if desired.\n */\n const handleNoPrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n const pathLocale = getPathLocale(originalPath);\n\n // Determine the best locale: prefer cookie/storage, fall back to Accept-Language detection\n let locale = storageLocale ?? defaultLocale;\n\n // Use localeDetector if no storage locale is available\n if (!storageLocale) {\n const detectedLocale = localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n );\n locale = detectedLocale as Locale;\n }\n\n // Guard against a stale/unsupported locale coming from storage or detection\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n pathLocale,\n rewriteRules\n );\n\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n const redirectPath = search\n ? `${canonicalPath}${search}`\n : `${canonicalPath}${searchParams ?? ''}`;\n\n // Persist the explicitly-requested locale: stripping the prefix drops\n // the only locale signal from the URL, so without this the follow-up\n // request would fall back to cookie / Accept-Language detection and\n // could resolve a different locale.\n return redirectUrl(req, res, redirectPath, {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // In search-params mode, we need to redirect to add the locale search param\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\n // Check if locale search param already exists and matches the detected locale\n const existingSearchParams = new URLSearchParams(searchParams ?? '');\n const existingLocale = existingSearchParams.get('locale');\n\n if (existingLocale === locale) {\n // Rewrite internally — URL stays the same in the browser, but the framework\n // sees /[locale]/path so the [locale] route param is populated correctly.\n // getInternalPath collapses the root path to `/${locale}` — a trailing\n // slash (`/fr/`) would trigger the framework's trailing-slash\n // normalisation redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match — redirect to add/update it (URL changes in browser)\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n return redirectUrl(req, res, redirectPath, { originalUrl });\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n // so the framework can match the [locale] route param without exposing it in the URL.\n // getInternalPath collapses the root path to `/${locale}` — a trailing slash\n // (`/fr/`) would trigger the framework's trailing-slash normalisation\n // redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const rewritePath = search\n ? `${internalPath}${search}`\n : `${internalPath}${searchParams ?? ''}`;\n\n // Rewrite internally — URL stays the same in the browser\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic.\n */\n const handlePrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale?: Locale;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n if (!pathLocale) {\n handleMissingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n });\n return;\n }\n\n handleExistingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n });\n };\n\n /**\n * Handles requests where the locale is missing from the URL pathname.\n * Detects a locale from storage / headers / default, then either redirects or rewrites.\n */\n const handleMissingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n // Choose the best locale: cookie/storage → Accept-Language detection → defaultLocale\n let locale = (storageLocale ??\n localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n )) as Locale;\n\n // If still invalid, fall back to defaultLocale\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // Resolve to canonical path.\n // If user visits /a-propos (implied 'fr'), this resolves to /about\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // Determine target localized path for redirection.\n // /about + 'fr' → /a-propos\n const targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\n\n // Construct new path, preserving original search params\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const newPath = constructPath(locale, targetLocalizedPath, search);\n\n // If we always prefix default or if this is not the default locale,\n // do a redirect so the user sees the locale in the URL\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(req, res, newPath, { originalUrl });\n }\n\n // If we do NOT prefix the default locale, pass through the canonical path unchanged.\n // Rewriting to `/${locale}${canonicalPath}` (e.g. /en/) causes TanStack Start to issue a\n // trailing-slash normalisation redirect (/en/ → /en), which the proxy then strips back to /,\n // creating an infinite redirect loop.\n // Because {-$locale} is an optional segment, the framework matches the un-prefixed URL with\n // locale=undefined and falls back to defaultLocale via `params.locale ?? defaultLocale`.\n // searchParams MUST be preserved here — dropping them causes the framework (e.g. TanStack Start) to\n // see a URL with no search params, trigger a validateSearch normalisation redirect to the prefixed URL\n // (e.g. /en?page=1&...), which the middleware then strips back to /?..., creating an infinite loop.\n rewriteUrl(req, res, `${canonicalPath}${searchParams}`, locale);\n return next();\n };\n\n /**\n * Handles requests where the locale prefix is present in the pathname.\n */\n const handleExistingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n originalUrl?: string;\n }) => {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n\n // Identify the canonical path (internal path).\n // Ex: /a-propos (from URL) → /about (canonical)\n const canonicalPath = getCanonicalPath(rawPath, pathLocale, rewriteRules);\n\n // When rewrite rules are configured and the URL is already a valid localized pretty URL\n // (e.g. /fr/essais which maps to canonical /fr/tests), do NOT redirect to canonical.\n //\n // Why: the SPA router (Solid, React Router, Vue Router…) is expected to define routes using\n // the localized paths (e.g. <Route path=\"/essais\">) so the browser URL must stay as-is.\n // A 301 redirect to canonical would:\n // 1. Change the browser URL to the canonical form (/fr/tests)\n // 2. Break subsequent client-side navigation because <A> links produced by getLocalizedUrl\n // point back to the localized URL (/fr/essais) which then has no matching route.\n //\n // We set the locale header and call next() so the server serves the page at the pretty URL.\n if (canonicalPath !== rawPath) {\n const newPath = searchParams\n ? `${originalPath}${searchParams}`\n : originalPath;\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n }\n\n handleDefaultLocaleRedirect({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n });\n };\n\n /**\n * If the path locale is the default locale but we don't want to prefix the default, remove it.\n */\n const handleDefaultLocaleRedirect = ({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n searchParams: string;\n pathLocale: Locale;\n canonicalPath: string;\n originalUrl?: string;\n }) => {\n // If we don't prefix the default locale AND the path locale IS the default → strip the prefix\n if (!prefixDefault && pathLocale === defaultLocale) {\n const targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\n\n // Construct path without prefix\n const cleanBasePath = basePath.startsWith('/')\n ? basePath\n : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n let finalPath = targetLocalizedPath;\n if (finalPath.startsWith('/')) finalPath = finalPath.slice(1);\n\n const fullPath = `${normalizedBasePath}/${finalPath}`.replace(\n /\\/+/g,\n '/'\n );\n\n // Persist the explicitly-requested default locale. Stripping the prefix\n // (e.g. /es → /) drops the only locale signal from the URL, so without\n // this the follow-up request to the canonical path would fall back to\n // Accept-Language detection and could resolve a different locale.\n return redirectUrl(req, res, fullPath + (searchParams ?? ''), {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n // If we do prefix the default or pathLocale !== default, keep as-is\n // but rewrite to canonical internally.\n // Never emit a trailing slash (`/fr/` for the bare `/fr` URL): rewriting\n // `/fr` to `/fr/` makes the framework issue a trailing-slash normalisation\n // redirect back to `/fr`, which this proxy rewrites again — an infinite\n // redirect loop. `getInternalPath` collapses the root path to `/${locale}`.\n const internalUrl = getInternalPath(canonicalPath, pathLocale);\n const newPath = searchParams\n ? `${internalUrl}${searchParams}`\n : internalUrl;\n\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return (req, res, next) => {\n // Parse original URL for path and query\n const parsedUrl = parse(req.url ?? '/', true);\n const originalPath = parsedUrl.pathname ?? '/';\n const searchParams = parsedUrl.search ?? '';\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\n\n // Bypass special Vite/server endpoints and node_modules\n if (\n // Custom ignore function\n (ignore?.(req) ?? false) ||\n originalPath.startsWith('/node_modules') ||\n /**\n * /^@vite/ # HMR client and helpers\n * /^@fs/ # file-system import serving\n * /^@id/ # virtual module ids\n * /^@tanstack/start-router-manifest # Tanstack Start Router manifest\n */\n originalPath.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n originalPath.startsWith('/_')\n ) {\n return next();\n }\n\n // Static file requests (e.g. /assets/video.mp4): bypass locale routing.\n // If the URL carries a locale prefix (e.g. /fr/assets/video.mp4),\n // rewrite the request internally to the unprefixed path (/assets/video.mp4)\n // so the file can be served correctly from the public directory.\n if (originalPath.match(/\\.[a-zA-Z0-9]+$/)) {\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n req.url = `${pathWithoutLocale}${searchParams}`;\n }\n return next();\n }\n\n // Attempt to read the locale from storage (cookies, localStorage, etc.)\n const storageLocale = getStorageLocale(req);\n\n // CRITICAL FIX: If there's a valid pathLocale, it takes precedence over storage\n // This prevents race conditions when cookies are stale during locale switches\n const effectiveStorageLocale =\n pathLocale && supportedLocales.includes(pathLocale)\n ? pathLocale\n : storageLocale;\n\n // Store original URL for redirect tracking\n const originalUrl = req.url;\n\n // Domain routing: if the path locale is mapped to a different domain, redirect there.\n // e.g. intlayer.org/zh/about → https://intlayer.zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n pathLocale &&\n domains\n ) {\n const localeDomain = domains[pathLocale as keyof typeof domains];\n if (localeDomain) {\n const reqHost = getRequestHostname(req);\n const domainHost = getDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = getDomainOrigin(localeDomain);\n // Domain mapping is stable config, so a cacheable 301 is intended\n // here — unlike locale-detection redirects, which use 302.\n redirectUrl(req, res, `${targetOrigin}${rawPath}${searchParams}`, {\n reason: 'domain-routing',\n originalUrl,\n status: 301,\n allowCrossOrigin: true,\n });\n return;\n }\n }\n }\n\n // Domain routing: if the current hostname is exclusively mapped to one locale,\n // treat it as that locale without a URL prefix.\n // e.g. intlayer.zh/about → internally rewrite to /zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n !pathLocale\n ) {\n const reqHost = getRequestHostname(req);\n const domainLocale = getLocaleFromDomain(reqHost, domains);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n\n // Default locale without forced prefixing: pass the canonical path\n // through unchanged, mirroring handleMissingPathLocale. Rewriting `/`\n // to `/en/` makes TanStack Start issue a trailing-slash normalisation\n // redirect (/en/ → /en), whose prefix the proxy then strips back to /,\n // creating an infinite redirect loop.\n if (!prefixDefault && domainLocale === defaultLocale) {\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n `${canonicalPath}${searchParams}`,\n domainLocale\n );\n return next();\n }\n\n // Never emit a trailing slash (`/zh/`): the framework would redirect\n // it to `/zh`, which this proxy rewrites to `/zh/` again — a loop.\n const internalPath = getInternalPath(canonicalPath, domainLocale);\n\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n searchParams ? `${internalPath}${searchParams}` : internalPath,\n domainLocale\n );\n return next();\n }\n }\n\n if (noPrefix) {\n handleNoPrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n return;\n }\n\n handlePrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n };\n};\n\n/**\n * Vite plugin that provides locale-based routing middleware for **all environments**:\n * development, preview, and production SSR (Nitro / TanStack Start).\n *\n * - **Dev** (`vite dev`): registered via `configureServer`.\n * - **Preview** (`vite preview`): registered via `configurePreviewServer`.\n * - **Production Nitro** (`vite build`): automatically injected via the `.nitro` module\n * property that `nitro/vite` reads and pushes into `nitroConfig.modules`. The module\n * registers `intlayerNitroHandler` as a Nitro server middleware — no extra user config\n * needed.\n *\n * If you need custom config options or an `ignore` predicate in production, bypass\n * auto-injection and create a server middleware file manually:\n *\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(\n * createIntlayerProxyHandler({\n * ignore: (req) => req.url?.startsWith('/api'),\n * configOptions: myConfig,\n * })\n * );\n * ```\n *\n * @param options - Plugin-specific options, like ignoring certain paths and Intlayer configuration overrides.\n * @returns A Vite plugin.\n *\n * @example\n * ```ts\n * import { intlayerProxy } from 'vite-intlayer';\n *\n * export default defineConfig({\n * plugins: [intlayerProxy()],\n * });\n * ```\n *\n * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (`true` by default). Registering it separately as shown below is now optional.\n */\nexport const intlayerProxy = (options?: IntlayerProxyPluginOptions): Plugin => {\n const handler = createIntlayerProxyHandler(options);\n const intlayerConfig = getConfiguration(options?.configOptions);\n const logger = getAppLogger(intlayerConfig);\n\n // Ensures the proxy registers its middleware only once, even when it is\n // registered both via `intlayer()` (which now bundles it) and a manual\n // `intlayerProxy()` call.\n const guard = createPrimaryInstanceGuard(PROXY_PLUGIN_NAME);\n\n /**\n * Nitro module injected automatically by `nitro/vite`.\n *\n * When a Vite plugin carries a `.nitro` property, `nitro/vite` pushes it into\n * `nitroConfig.modules` during the build phase. The module's `setup` hook adds\n * our locale-routing handler to Nitro's server pipeline, making locale detection\n * work in production SSR builds (TanStack Start, Nuxt, etc.) without any extra\n * user configuration.\n *\n * @see https://github.com/nitrojs/nitro (nitro/vite source, line ~402)\n */\n const nitroModule = {\n name: 'intlayer-proxy',\n setup(nitro: {\n options: {\n dev: boolean;\n handlers: {\n route: string;\n handler: string;\n middleware: boolean;\n }[];\n };\n }) {\n // In dev mode, locale routing is already handled by configureServer (Vite dev server).\n // The Nitro dev server uses h3 v2's Web Fetch API event model which is incompatible\n // with fromNodeMiddleware (h3 v1) and would cause double-execution anyway.\n // Only inject for production builds where Nitro is the actual HTTP server.\n if (nitro.options.dev) return;\n\n const handlerPath = fileURLToPath(\n new URL('./intlayerNitroHandler.mjs', import.meta.url)\n );\n\n // Skip if an identical handler was already registered by another instance\n // (e.g. both `intlayer()` and a manual `intlayerProxy()`).\n const alreadyRegistered = nitro.options.handlers.some(\n (existingHandler) => existingHandler.handler === handlerPath\n );\n if (alreadyRegistered) return;\n\n nitro.options.handlers.push({\n route: '/**',\n handler: handlerPath,\n middleware: true,\n });\n },\n };\n\n const plugin = {\n name: PROXY_PLUGIN_NAME,\n // Decide whether this is the primary instance before registering middleware.\n configResolved: (config: { plugins: readonly { name: string }[] }) => {\n guard.resolve(config);\n },\n // Injected into nitroConfig.modules by the `nitro/vite` plugin so the\n // locale-routing middleware is registered in the production Nitro server.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n nitro: nitroModule as any,\n // Vite dev server\n configureServer: (server) => {\n if (!guard.isPrimary) return;\n logger(`Intlayer proxy ${colorize('enabled', ANSIColors.GREEN)}`, {\n level: 'info',\n });\n server.middlewares.use(handler);\n },\n // Vite preview server\n configurePreviewServer: (server) => {\n if (!guard.isPrimary) return;\n logger(`Intlayer proxy ${colorize('enabled', ANSIColors.GREEN)}`, {\n level: 'info',\n });\n server.middlewares.use(handler);\n },\n } as Plugin;\n\n // Register the plugin object so the dedupe guard can identify the primary\n // instance by reference during `configResolved`.\n guard.setPlugin(plugin);\n\n return plugin;\n};\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intlayerMiddleware = intlayerProxy;\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intLayerMiddlewarePlugin = intlayerProxy;\n"],"mappings":"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"}
1
+ {"version":3,"file":"intlayerProxyPlugin.mjs","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { fileURLToPath, parse } from 'node:url';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n formatProxyEnabledMessage,\n getCanonicalPath,\n getDomainHostname,\n getDomainOrigin,\n getInternalPath,\n getLocaleFromDomain,\n getLocalizedPath,\n getRewriteRules,\n isProxyStorageLocaleEnabled,\n localeDetector,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n/* @ts-ignore - Vite types error */\nimport type { Connect, Plugin } from 'vite';\nimport { createPrimaryInstanceGuard } from './dedupePlugin';\n\nconst PROXY_PLUGIN_NAME = 'vite-intlayer-middleware-plugin';\n\nexport type IntlayerProxyPluginOptions = {\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ ignore: (req) => req.url?.startsWith('/api') }) ],\n * });\n * ```\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Optional Intlayer configuration overrides forwarded to `getConfiguration`.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ configOptions: { override: { ... } } }) ],\n * });\n * ```\n */\n configOptions?: GetConfigurationOptions;\n /**\n * Whether a development or preview server is serving the app.\n *\n * Set internally by the plugin from `configureServer` /\n * `configurePreviewServer`. It only matters in the proxy's auto mode, where\n * a dev server keeps locale routing URL-driven by ignoring the stored locale\n * as a redirect source.\n *\n * Defaults to `false` so that mounting `createIntlayerProxyHandler()`\n * manually — the documented production Nitro setup — keeps full behaviour.\n *\n * @default false\n */\n isDevServer?: boolean;\n};\n\n/**\n * A Node.js-compatible Connect middleware function.\n * Compatible with Vite dev/preview server, Node.js http, Express, and h3's\n * `fromNodeMiddleware` wrapper for Nitro/TanStack Start production use.\n */\ntype NodeMiddleware = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: () => void\n) => void;\n\n/**\n * Creates a standalone, framework-agnostic locale-routing middleware.\n *\n * This function contains all the locale detection, redirect, and rewrite logic.\n * It is intentionally separated from the Vite plugin so the same handler can be\n * used in every environment:\n *\n * - **Dev**: wired up automatically by `intlayerProxy` via `configureServer`\n * - **Preview**: wired up automatically by `intlayerProxy` via `configurePreviewServer`\n * - **Production (Nitro / TanStack Start)**: create `server/middleware/intlayerProxy.ts`:\n *\n * @example\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(createIntlayerProxyHandler());\n * ```\n *\n * @param options - Plugin-specific options, such as path ignoring and Intlayer configuration overrides.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n *\n */\nexport const createIntlayerProxyHandler = (\n options?: IntlayerProxyPluginOptions\n): NodeMiddleware => {\n const { ignore, configOptions, isDevServer = false } = options ?? {};\n const intlayerConfig = getConfiguration(configOptions);\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const {\n basePath = '',\n mode = ROUTING_MODE,\n rewrite,\n domains,\n enableProxy,\n } = routing;\n\n // In auto mode a dev/preview server ignores the stored locale when resolving\n // which locale a request maps to, so a lingering cookie cannot keep pulling\n // navigation to another locale while developing. Prefix redirects, locale\n // persistence and `Accept-Language` detection are unaffected.\n const canUseStorageLocale = isProxyStorageLocaleEnabled(\n resolveProxyMode(enableProxy),\n isDevServer\n );\n\n type RedirectCounter = { count: number; lastSeen: number };\n const redirectCounts = new Map<string, RedirectCounter>();\n const MAX_REDIRECTS = 10;\n const REDIRECT_TTL_MS = 2_000;\n\n // Derived flags from routing.mode\n const noPrefix =\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params');\n const prefixDefault =\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all'\n ) && mode === 'prefix-all';\n\n const rewriteRules =\n process.env.INTLAYER_ROUTING_REWRITE_RULES !== 'false'\n ? getRewriteRules(rewrite, 'url')\n : undefined;\n\n /**\n * Extracts the hostname from a request's `Host` header, handling IPv6\n * literals (`[::1]:5173` → `::1`) that a plain `split(':')` would mangle.\n */\n const getRequestHostname = (req: IncomingMessage): string => {\n const host = req.headers.host ?? '';\n const ipv6Match = host.match(/^\\[([^\\]]+)\\]/);\n if (ipv6Match) return ipv6Match[1]!;\n return host.split(':')[0] ?? '';\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n *\n * Returns `undefined` when the stored locale is not allowed to drive locale\n * resolution (auto mode on a dev/preview server), which makes every caller\n * fall through to `Accept-Language` detection and then the default locale.\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\n if (!canUseStorageLocale) return undefined;\n\n const locale = getLocaleFromStorageServer({\n getCookie: (name: string) => getCookie(name, req.headers.cookie),\n });\n return locale;\n };\n\n /**\n * Appends locale to search params when routing mode is 'search-params'.\n */\n const appendLocaleSearchIfNeeded = (\n search: string | undefined,\n locale: Locale\n ): string | undefined => {\n if (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params') ||\n mode !== 'search-params'\n )\n return search;\n\n const params = new URLSearchParams(search ?? '');\n\n params.set('locale', locale);\n\n return `?${params.toString()}`;\n };\n\n /**\n * Checks whether a pathname starts with the given locale as a full path\n * segment (`/fr` or `/fr/...`). A bare `startsWith('/fr')` would also match\n * unrelated paths like `/friends`, causing wrong prefix stripping and\n * self-redirect loops.\n */\n const hasLocaleSegmentPrefix = (pathname: string, locale: Locale): boolean =>\n pathname === `/${locale}` || pathname.startsWith(`/${locale}/`);\n\n /**\n * Extracts the locale from the URL pathname if present as the first segment.\n * e.g. if pathname is /en/some/page or /en, checks if \"en\" is in supportedLocales.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\n const segments = pathname.split('/').filter(Boolean);\n const firstSegment = segments[0];\n if (firstSegment && supportedLocales.includes(firstSegment as Locale)) {\n return firstSegment as Locale;\n }\n return undefined;\n };\n\n /**\n * Persists the resolved locale onto the outgoing response as a cookie (and\n * header, per `routing.storage`). Only the cookie survives a client redirect,\n * so this is what carries an explicitly-selected locale across a\n * prefix-stripping redirect. Enabled cookie/header targets are resolved by\n * {@link setLocaleInStorageServer} from the config; disabled ones are no-ops.\n */\n const persistLocaleOnResponse = (\n res: ServerResponse<IncomingMessage>,\n locale: Locale\n ) => {\n setLocaleInStorageServer(locale, {\n setCookieStore: (name, value, attributes) => {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n if (attributes.path) parts.push(`Path=${attributes.path}`);\n if (attributes.domain) parts.push(`Domain=${attributes.domain}`);\n if (typeof attributes.expires === 'number')\n parts.push(`Expires=${new Date(attributes.expires).toUTCString()}`);\n if (attributes.secure) parts.push('Secure');\n if (attributes.httpOnly) parts.push('HttpOnly');\n if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`);\n\n const cookieString = parts.join('; ');\n const existing = res.getHeader('Set-Cookie');\n const cookies = Array.isArray(existing)\n ? existing.map(String)\n : existing !== undefined\n ? [String(existing)]\n : [];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n },\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n },\n });\n };\n\n /**\n * Identifies the client issuing a request, so redirect-loop tracking is\n * scoped per client. Without this, concurrent visitors hitting the same\n * legitimate redirect (e.g. `/` → `/fr/`) would share one counter and trip\n * the loop detector under normal production traffic.\n */\n const getClientKey = (req: IncomingMessage): string => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const forwardedIp = (\n Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor\n )\n ?.split(',')[0]\n ?.trim();\n\n return forwardedIp || req.socket?.remoteAddress || 'unknown';\n };\n\n /**\n * Strips any origin from a redirect target, keeping only path, search and\n * hash. Prevents open redirects: user-controlled paths like `//evil.com/x`\n * would otherwise be echoed verbatim into the `Location` header and be\n * interpreted by browsers as a protocol-relative cross-origin URL.\n */\n const toSameOriginUrl = (url: string): string => {\n try {\n const parsed = new URL(url, 'http://intlayer-internal');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return url;\n }\n };\n\n type RedirectOptions = {\n /** Human-readable reason logged when a redirect loop is detected. */\n reason?: string;\n /** The URL of the incoming request, used for redirect-loop tracking. */\n originalUrl?: string;\n /**\n * When provided, the locale is persisted (cookie/header, per config) on\n * the redirect response so the follow-up request resolves the same locale.\n */\n persistLocale?: Locale;\n /**\n * HTTP status for the redirect. Defaults to 302: locale redirects depend\n * on mutable state (cookie, Accept-Language) and must not be cached by\n * the browser as permanent. Domain-routing redirects pass 301 explicitly.\n */\n status?: number;\n /**\n * Allows a cross-origin `Location` target. Only the domain-routing\n * redirect sets this; all other targets are sanitized to same-origin.\n */\n allowCrossOrigin?: boolean;\n };\n\n /**\n * Writes a redirect response with the given new URL.\n */\n const redirectUrl = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n options?: RedirectOptions\n ) => {\n const {\n reason,\n originalUrl,\n persistLocale,\n status = 302,\n allowCrossOrigin = false,\n } = options ?? {};\n\n const targetUrl = allowCrossOrigin ? newUrl : toSameOriginUrl(newUrl);\n\n if (originalUrl) {\n if (originalUrl === targetUrl) {\n console.error('[REDIRECT LOOP DETECTED!]', { originalUrl, reason });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${originalUrl} redirects to itself`\n );\n }\n\n const now = Date.now();\n const trackingKey = `${getClientKey(req)}|${originalUrl} -> ${targetUrl}`;\n const previousEntry = redirectCounts.get(trackingKey);\n const count =\n previousEntry && now - previousEntry.lastSeen < REDIRECT_TTL_MS\n ? previousEntry.count + 1\n : 1;\n\n redirectCounts.set(trackingKey, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: targetUrl,\n reason,\n });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${count} redirects from ${originalUrl}`\n );\n }\n\n for (const [entryKey, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS)\n redirectCounts.delete(entryKey);\n }\n }\n\n if (persistLocale) {\n persistLocaleOnResponse(res, persistLocale);\n }\n\n res.writeHead(status, { Location: targetUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url.\n * Also sets the locale in the response/request headers via storage to mimic\n * Next.js's behaviour of propagating the detected locale downstream.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n if (req.url !== newUrl) {\n req.url = newUrl;\n }\n if (locale) {\n setLocaleInStorageServer(locale, {\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n req.headers[name] = value;\n },\n });\n }\n };\n\n /**\n * Constructs a new path string, optionally including a locale prefix, basePath, and search parameters.\n * - basePath: (e.g. '/myapp')\n * - locale: (e.g. 'en')\n * - currentPath: (e.g. '/products/shoes')\n * - search: (e.g. '?foo=bar')\n */\n const constructPath = (\n locale: Locale,\n currentPath: string,\n search?: string\n ) => {\n // Strip any incoming locale prefix to avoid double-prefixing\n const pathWithoutPrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath.slice(`/${locale}`.length)\n : currentPath;\n\n // Ensure basePath always starts with '/' and has no trailing slash\n const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n // In 'search-params' and 'no-prefix' modes, do not prefix the path with the locale\n if (\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params')\n ) {\n const newPath = search\n ? `${pathWithoutPrefix || '/'}${search}`\n : pathWithoutPrefix || '/';\n return newPath;\n }\n\n // Check if path already starts with locale to avoid double-prefixing\n const pathWithLocalePrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath\n : `/${locale}${currentPath}`;\n\n let newPath = `${normalizedBasePath}${pathWithLocalePrefix}`;\n\n // Special case: if prefixDefault is false and locale is defaultLocale, remove the locale prefix\n if (!prefixDefault && locale === defaultLocale) {\n newPath = `${normalizedBasePath}${pathWithoutPrefix || '/'}`;\n }\n\n // Never emit a trailing slash (`/fr/` for the root path): the framework's\n // trailing-slash normalisation would redirect it back and forth with this\n // proxy, creating an infinite redirect loop.\n if (newPath.length > 1 && newPath.endsWith('/')) {\n newPath = newPath.slice(0, -1);\n }\n\n // Append search parameters if provided\n if (search) {\n newPath += search;\n }\n\n return newPath;\n };\n\n /* --------------------------------------------------------------------\n * Handlers that mirror Next.js style logic\n * --------------------------------------------------------------------\n */\n\n /**\n * If `noPrefix` is true, we never prefix the locale in the URL.\n * We simply rewrite the request to the same path, but with the best-chosen locale\n * in a header or search params if desired.\n */\n const handleNoPrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n const pathLocale = getPathLocale(originalPath);\n\n // Determine the best locale: prefer cookie/storage, fall back to Accept-Language detection\n let locale = storageLocale ?? defaultLocale;\n\n // Use localeDetector if no storage locale is available\n if (!storageLocale) {\n const detectedLocale = localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n );\n locale = detectedLocale as Locale;\n }\n\n // Guard against a stale/unsupported locale coming from storage or detection\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n pathLocale,\n rewriteRules\n );\n\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n const redirectPath = search\n ? `${canonicalPath}${search}`\n : `${canonicalPath}${searchParams ?? ''}`;\n\n // Persist the explicitly-requested locale: stripping the prefix drops\n // the only locale signal from the URL, so without this the follow-up\n // request would fall back to cookie / Accept-Language detection and\n // could resolve a different locale.\n return redirectUrl(req, res, redirectPath, {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // In search-params mode, we need to redirect to add the locale search param\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\n // Check if locale search param already exists and matches the detected locale\n const existingSearchParams = new URLSearchParams(searchParams ?? '');\n const existingLocale = existingSearchParams.get('locale');\n\n if (existingLocale === locale) {\n // Rewrite internally — URL stays the same in the browser, but the framework\n // sees /[locale]/path so the [locale] route param is populated correctly.\n // getInternalPath collapses the root path to `/${locale}` — a trailing\n // slash (`/fr/`) would trigger the framework's trailing-slash\n // normalisation redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match — redirect to add/update it (URL changes in browser)\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n return redirectUrl(req, res, redirectPath, { originalUrl });\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n // so the framework can match the [locale] route param without exposing it in the URL.\n // getInternalPath collapses the root path to `/${locale}` — a trailing slash\n // (`/fr/`) would trigger the framework's trailing-slash normalisation\n // redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const rewritePath = search\n ? `${internalPath}${search}`\n : `${internalPath}${searchParams ?? ''}`;\n\n // Rewrite internally — URL stays the same in the browser\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic.\n */\n const handlePrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale?: Locale;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n if (!pathLocale) {\n handleMissingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n });\n return;\n }\n\n handleExistingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n });\n };\n\n /**\n * Handles requests where the locale is missing from the URL pathname.\n * Detects a locale from storage / headers / default, then either redirects or rewrites.\n */\n const handleMissingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n // Choose the best locale: cookie/storage → Accept-Language detection → defaultLocale\n let locale = (storageLocale ??\n localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n )) as Locale;\n\n // If still invalid, fall back to defaultLocale\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // Resolve to canonical path.\n // If user visits /a-propos (implied 'fr'), this resolves to /about\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // Determine target localized path for redirection.\n // /about + 'fr' → /a-propos\n const targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\n\n // Construct new path, preserving original search params\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const newPath = constructPath(locale, targetLocalizedPath, search);\n\n // If we always prefix default or if this is not the default locale,\n // do a redirect so the user sees the locale in the URL\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(req, res, newPath, { originalUrl });\n }\n\n // If we do NOT prefix the default locale, pass through the canonical path unchanged.\n // Rewriting to `/${locale}${canonicalPath}` (e.g. /en/) causes TanStack Start to issue a\n // trailing-slash normalisation redirect (/en/ → /en), which the proxy then strips back to /,\n // creating an infinite redirect loop.\n // Because {-$locale} is an optional segment, the framework matches the un-prefixed URL with\n // locale=undefined and falls back to defaultLocale via `params.locale ?? defaultLocale`.\n // searchParams MUST be preserved here — dropping them causes the framework (e.g. TanStack Start) to\n // see a URL with no search params, trigger a validateSearch normalisation redirect to the prefixed URL\n // (e.g. /en?page=1&...), which the middleware then strips back to /?..., creating an infinite loop.\n rewriteUrl(req, res, `${canonicalPath}${searchParams}`, locale);\n return next();\n };\n\n /**\n * Handles requests where the locale prefix is present in the pathname.\n */\n const handleExistingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n originalUrl?: string;\n }) => {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n\n // Identify the canonical path (internal path).\n // Ex: /a-propos (from URL) → /about (canonical)\n const canonicalPath = getCanonicalPath(rawPath, pathLocale, rewriteRules);\n\n // When rewrite rules are configured and the URL is already a valid localized pretty URL\n // (e.g. /fr/essais which maps to canonical /fr/tests), do NOT redirect to canonical.\n //\n // Why: the SPA router (Solid, React Router, Vue Router…) is expected to define routes using\n // the localized paths (e.g. <Route path=\"/essais\">) so the browser URL must stay as-is.\n // A 301 redirect to canonical would:\n // 1. Change the browser URL to the canonical form (/fr/tests)\n // 2. Break subsequent client-side navigation because <A> links produced by getLocalizedUrl\n // point back to the localized URL (/fr/essais) which then has no matching route.\n //\n // We set the locale header and call next() so the server serves the page at the pretty URL.\n if (canonicalPath !== rawPath) {\n const newPath = searchParams\n ? `${originalPath}${searchParams}`\n : originalPath;\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n }\n\n handleDefaultLocaleRedirect({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n });\n };\n\n /**\n * If the path locale is the default locale but we don't want to prefix the default, remove it.\n */\n const handleDefaultLocaleRedirect = ({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n searchParams: string;\n pathLocale: Locale;\n canonicalPath: string;\n originalUrl?: string;\n }) => {\n // If we don't prefix the default locale AND the path locale IS the default → strip the prefix\n if (!prefixDefault && pathLocale === defaultLocale) {\n const targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\n\n // Construct path without prefix\n const cleanBasePath = basePath.startsWith('/')\n ? basePath\n : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n let finalPath = targetLocalizedPath;\n if (finalPath.startsWith('/')) finalPath = finalPath.slice(1);\n\n const fullPath = `${normalizedBasePath}/${finalPath}`.replace(\n /\\/+/g,\n '/'\n );\n\n // Persist the explicitly-requested default locale. Stripping the prefix\n // (e.g. /es → /) drops the only locale signal from the URL, so without\n // this the follow-up request to the canonical path would fall back to\n // Accept-Language detection and could resolve a different locale.\n return redirectUrl(req, res, fullPath + (searchParams ?? ''), {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n // If we do prefix the default or pathLocale !== default, keep as-is\n // but rewrite to canonical internally.\n // Never emit a trailing slash (`/fr/` for the bare `/fr` URL): rewriting\n // `/fr` to `/fr/` makes the framework issue a trailing-slash normalisation\n // redirect back to `/fr`, which this proxy rewrites again — an infinite\n // redirect loop. `getInternalPath` collapses the root path to `/${locale}`.\n const internalUrl = getInternalPath(canonicalPath, pathLocale);\n const newPath = searchParams\n ? `${internalUrl}${searchParams}`\n : internalUrl;\n\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return (req, res, next) => {\n // Parse original URL for path and query\n const parsedUrl = parse(req.url ?? '/', true);\n const originalPath = parsedUrl.pathname ?? '/';\n const searchParams = parsedUrl.search ?? '';\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\n\n // Bypass special Vite/server endpoints and node_modules\n if (\n // Custom ignore function\n (ignore?.(req) ?? false) ||\n originalPath.startsWith('/node_modules') ||\n /**\n * /^@vite/ # HMR client and helpers\n * /^@fs/ # file-system import serving\n * /^@id/ # virtual module ids\n * /^@tanstack/start-router-manifest # Tanstack Start Router manifest\n */\n originalPath.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n originalPath.startsWith('/_')\n ) {\n return next();\n }\n\n // Static file requests (e.g. /assets/video.mp4): bypass locale routing.\n // If the URL carries a locale prefix (e.g. /fr/assets/video.mp4),\n // rewrite the request internally to the unprefixed path (/assets/video.mp4)\n // so the file can be served correctly from the public directory.\n if (originalPath.match(/\\.[a-zA-Z0-9]+$/)) {\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n req.url = `${pathWithoutLocale}${searchParams}`;\n }\n return next();\n }\n\n // Attempt to read the locale from storage (cookies, localStorage, etc.)\n const storageLocale = getStorageLocale(req);\n\n // CRITICAL FIX: If there's a valid pathLocale, it takes precedence over storage\n // This prevents race conditions when cookies are stale during locale switches\n const effectiveStorageLocale =\n pathLocale && supportedLocales.includes(pathLocale)\n ? pathLocale\n : storageLocale;\n\n // Store original URL for redirect tracking\n const originalUrl = req.url;\n\n // Domain routing: if the path locale is mapped to a different domain, redirect there.\n // e.g. intlayer.org/zh/about → https://intlayer.zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n pathLocale &&\n domains\n ) {\n const localeDomain = domains[pathLocale as keyof typeof domains];\n if (localeDomain) {\n const reqHost = getRequestHostname(req);\n const domainHost = getDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = getDomainOrigin(localeDomain);\n // Domain mapping is stable config, so a cacheable 301 is intended\n // here — unlike locale-detection redirects, which use 302.\n redirectUrl(req, res, `${targetOrigin}${rawPath}${searchParams}`, {\n reason: 'domain-routing',\n originalUrl,\n status: 301,\n allowCrossOrigin: true,\n });\n return;\n }\n }\n }\n\n // Domain routing: if the current hostname is exclusively mapped to one locale,\n // treat it as that locale without a URL prefix.\n // e.g. intlayer.zh/about → internally rewrite to /zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n !pathLocale\n ) {\n const reqHost = getRequestHostname(req);\n const domainLocale = getLocaleFromDomain(reqHost, domains);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n\n // Default locale without forced prefixing: pass the canonical path\n // through unchanged, mirroring handleMissingPathLocale. Rewriting `/`\n // to `/en/` makes TanStack Start issue a trailing-slash normalisation\n // redirect (/en/ → /en), whose prefix the proxy then strips back to /,\n // creating an infinite redirect loop.\n if (!prefixDefault && domainLocale === defaultLocale) {\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n `${canonicalPath}${searchParams}`,\n domainLocale\n );\n return next();\n }\n\n // Never emit a trailing slash (`/zh/`): the framework would redirect\n // it to `/zh`, which this proxy rewrites to `/zh/` again — a loop.\n const internalPath = getInternalPath(canonicalPath, domainLocale);\n\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n searchParams ? `${internalPath}${searchParams}` : internalPath,\n domainLocale\n );\n return next();\n }\n }\n\n if (noPrefix) {\n handleNoPrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n return;\n }\n\n handlePrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n };\n};\n\n/**\n * Vite plugin that provides locale-based routing middleware for **all environments**:\n * development, preview, and production SSR (Nitro / TanStack Start).\n *\n * - **Dev** (`vite dev`): registered via `configureServer`.\n * - **Preview** (`vite preview`): registered via `configurePreviewServer`.\n * - **Production Nitro** (`vite build`): automatically injected via the `.nitro` module\n * property that `nitro/vite` reads and pushes into `nitroConfig.modules`. The module\n * registers `intlayerNitroHandler` as a Nitro server middleware — no extra user config\n * needed.\n *\n * If you need custom config options or an `ignore` predicate in production, bypass\n * auto-injection and create a server middleware file manually:\n *\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(\n * createIntlayerProxyHandler({\n * ignore: (req) => req.url?.startsWith('/api'),\n * configOptions: myConfig,\n * })\n * );\n * ```\n *\n * @param options - Plugin-specific options, like ignoring certain paths and Intlayer configuration overrides.\n * @returns A Vite plugin.\n *\n * @example\n * ```ts\n * import { intlayerProxy } from 'vite-intlayer';\n *\n * export default defineConfig({\n * plugins: [intlayerProxy()],\n * });\n * ```\n *\n * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (unset by default, which selects auto mode). Registering it separately as shown below is now optional.\n */\nexport const intlayerProxy = (options?: IntlayerProxyPluginOptions): Plugin => {\n // Dev and preview servers run the same handler; both are \"dev servers\" as far\n // as auto mode is concerned, so a single instance covers both hooks.\n const handler = createIntlayerProxyHandler({ ...options, isDevServer: true });\n const intlayerConfig = getConfiguration(options?.configOptions);\n const logger = getAppLogger(intlayerConfig);\n\n // Both hooks below serve a dev or preview server, hence the hard-coded true.\n const isStorageLocaleSuppressed = !isProxyStorageLocaleEnabled(\n resolveProxyMode(intlayerConfig.routing.enableProxy),\n true\n );\n\n /**\n * Logs that the proxy is serving requests, spelling out when auto mode has\n * suppressed the stored locale so the reported state matches the behaviour.\n */\n const logProxyEnabled = () =>\n logger(formatProxyEnabledMessage(isStorageLocaleSuppressed), {\n level: 'info',\n });\n\n // Ensures the proxy registers its middleware only once, even when it is\n // registered both via `intlayer()` (which now bundles it) and a manual\n // `intlayerProxy()` call.\n const guard = createPrimaryInstanceGuard(PROXY_PLUGIN_NAME);\n\n /**\n * Nitro module injected automatically by `nitro/vite`.\n *\n * When a Vite plugin carries a `.nitro` property, `nitro/vite` pushes it into\n * `nitroConfig.modules` during the build phase. The module's `setup` hook adds\n * our locale-routing handler to Nitro's server pipeline, making locale detection\n * work in production SSR builds (TanStack Start, Nuxt, etc.) without any extra\n * user configuration.\n *\n * @see https://github.com/nitrojs/nitro (nitro/vite source, line ~402)\n */\n const nitroModule = {\n name: 'intlayer-proxy',\n setup(nitro: {\n options: {\n dev: boolean;\n handlers: {\n route: string;\n handler: string;\n middleware: boolean;\n }[];\n };\n }) {\n // In dev mode, locale routing is already handled by configureServer (Vite dev server).\n // The Nitro dev server uses h3 v2's Web Fetch API event model which is incompatible\n // with fromNodeMiddleware (h3 v1) and would cause double-execution anyway.\n // Only inject for production builds where Nitro is the actual HTTP server.\n if (nitro.options.dev) return;\n\n const handlerPath = fileURLToPath(\n new URL('./intlayerNitroHandler.mjs', import.meta.url)\n );\n\n // Skip if an identical handler was already registered by another instance\n // (e.g. both `intlayer()` and a manual `intlayerProxy()`).\n const alreadyRegistered = nitro.options.handlers.some(\n (existingHandler) => existingHandler.handler === handlerPath\n );\n if (alreadyRegistered) return;\n\n nitro.options.handlers.push({\n route: '/**',\n handler: handlerPath,\n middleware: true,\n });\n },\n };\n\n const plugin = {\n name: PROXY_PLUGIN_NAME,\n // Decide whether this is the primary instance before registering middleware.\n configResolved: (config: { plugins: readonly { name: string }[] }) => {\n guard.resolve(config);\n },\n // Injected into nitroConfig.modules by the `nitro/vite` plugin so the\n // locale-routing middleware is registered in the production Nitro server.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n nitro: nitroModule as any,\n // Vite dev server\n configureServer: (server) => {\n if (!guard.isPrimary) return;\n logProxyEnabled();\n server.middlewares.use(handler);\n },\n // Vite preview server\n configurePreviewServer: (server) => {\n if (!guard.isPrimary) return;\n logProxyEnabled();\n server.middlewares.use(handler);\n },\n } as Plugin;\n\n // Register the plugin object so the dedupe guard can identify the primary\n // instance by reference during `configResolved`.\n guard.setPlugin(plugin);\n\n return plugin;\n};\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intlayerMiddleware = intlayerProxy;\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intLayerMiddlewarePlugin = intlayerProxy;\n"],"mappings":"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"}
@@ -147,11 +147,10 @@ const intlayerPrune = (intlayerConfig, pruneContext) => {
147
147
  colorize("editor.enabled", ANSIColors.BLUE),
148
148
  "is",
149
149
  colorize("true", ANSIColors.GREY_DARK),
150
- "— the editor requires full dictionary content."
150
+ colorize("— the editor requires full dictionary content", ANSIColors.GREY)
151
151
  ]), { cacheTimeoutMs: 1e3 * 10 });
152
152
  return false;
153
153
  }
154
- runOnce(join(baseDir, ".intlayer", "cache", "intlayer-purge-plugin-enabled.lock"), () => logger(["Dictionary purge", colorize("enabled", ANSIColors.GREEN)]), { cacheTimeoutMs: 1e3 * 10 });
155
154
  return true;
156
155
  };
157
156
  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 '— the editor requires full dictionary content.',\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-purge-plugin-enabled.lock'),\n () => logger(['Dictionary purge', colorize('enabled', ANSIColors.GREEN)]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n };\n\n const prunePlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-prune',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM conversion\n enforce: 'pre',\n apply: isPruneEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!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 * @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 +1 @@
1
- {"version":3,"file":"intlayerOptimizePlugin.d.ts","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;cA0Da,mBAAgB,gBACX,gBAAc,cAChB,qBAAmB,gBACjB,uBACf,QAAQ"}
1
+ {"version":3,"file":"intlayerOptimizePlugin.d.ts","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;cA+Da,mBAAgB,gBACX,gBAAc,cAChB,qBAAmB,gBACjB,uBACf,QAAQ"}
@@ -25,9 +25,10 @@ type IntlayerPluginOptions = GetConfigurationOptions & {
25
25
  * Options forwarded to the bundled locale-routing proxy (`intlayerProxy`).
26
26
  *
27
27
  * Since Intlayer v9 the proxy is plugged directly into the main `intlayer()`
28
- * plugin (controlled by `routing.enableProxy`, `true` by default). These
29
- * routing options let you tune that bundled proxy without registering it
30
- * separately — e.g. ignore API routes from locale routing.
28
+ * plugin (controlled by `routing.enableProxy`, unset by default, which
29
+ * selects auto mode). These routing options let you tune that bundled proxy
30
+ * without registering it separately — e.g. ignore API routes from locale
31
+ * routing.
31
32
  *
32
33
  * @example
33
34
  * ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerPlugin.d.ts","names":[],"sources":["../../src/intlayerPlugin.ts"],"mappings":";;;;;;;;;;;;;;KAuCY,wBAAwB;;;;;;;;EAQlC,gBAAgB;;;;;;;;;;;;;;EAehB,QAAQ,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;cA0BF,iBAAc,gBACT,0BACf;;;;;;;;;;;cAsMU,iBAAQ;;;;;;;;;;;;;cAaR,uBAAc"}
1
+ {"version":3,"file":"intlayerPlugin.d.ts","names":[],"sources":["../../src/intlayerPlugin.ts"],"mappings":";;;;;;;;;;;;;;KAyCY,wBAAwB;;;;;;;;EAQlC,gBAAgB;;;;;;;;;;;;;;;EAgBhB,QAAQ,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;cA0BF,iBAAc,gBACT,0BACf;;;;;;;;;;;cAuNU,iBAAQ;;;;;;;;;;;;;cAaR,uBAAc"}
@@ -28,6 +28,20 @@ type IntlayerProxyPluginOptions = {
28
28
  * ```
29
29
  */
30
30
  configOptions?: GetConfigurationOptions;
31
+ /**
32
+ * Whether a development or preview server is serving the app.
33
+ *
34
+ * Set internally by the plugin from `configureServer` /
35
+ * `configurePreviewServer`. It only matters in the proxy's auto mode, where
36
+ * a dev server keeps locale routing URL-driven by ignoring the stored locale
37
+ * as a redirect source.
38
+ *
39
+ * Defaults to `false` so that mounting `createIntlayerProxyHandler()`
40
+ * manually — the documented production Nitro setup — keeps full behaviour.
41
+ *
42
+ * @default false
43
+ */
44
+ isDevServer?: boolean;
31
45
  };
32
46
  /**
33
47
  * A Node.js-compatible Connect middleware function.
@@ -99,7 +113,7 @@ declare const createIntlayerProxyHandler: (options?: IntlayerProxyPluginOptions)
99
113
  * });
100
114
  * ```
101
115
  *
102
- * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (`true` by default). Registering it separately as shown below is now optional.
116
+ * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (unset by default, which selects auto mode). Registering it separately as shown below is now optional.
103
117
  */
104
118
  declare const intlayerProxy: (options?: IntlayerProxyPluginOptions) => Plugin;
105
119
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerProxyPlugin.d.ts","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"mappings":";;;;KA+BY;;;;;;;;;;;;;;EAcV,UAAU,KAAK;;;;;;;;;;;EAWf,gBAAgB;;;;;;;KAQb,kBACH,KAAK,iBACL,KAAK,eAAe,kBACpB;;;;;;;;;;;;;;;;;;;;;;;;;cA2BW,6BAA0B,UAC3B,+BACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAk6BU,gBAAa,UAAc,+BAA6B;;;;;;;;;;;;;cAyGxD,2BAAkB;;;;;;;;;;;;;cAclB,iCAAwB"}
1
+ {"version":3,"file":"intlayerProxyPlugin.d.ts","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"mappings":";;;;KAiCY;;;;;;;;;;;;;;EAcV,UAAU,KAAK;;;;;;;;;;;EAWf,gBAAgB;;;;;;;;;;;;;;EAchB;;;;;;;KAQG,kBACH,KAAK,iBACL,KAAK,eAAe,kBACpB;;;;;;;;;;;;;;;;;;;;;;;;;cA2BW,6BAA0B,UAC3B,+BACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAu7BU,gBAAa,UAAc,+BAA6B;;;;;;;;;;;;;cAsHxD,2BAAkB;;;;;;;;;;;;;cAclB,iCAAwB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-intlayer",
3
- "version": "9.1.3",
3
+ "version": "9.3.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.1.3",
85
- "@intlayer/config": "9.1.3",
86
- "@intlayer/core": "9.1.3",
87
- "@intlayer/dictionaries-entry": "9.1.3",
88
- "@intlayer/engine": "9.1.3",
89
- "@intlayer/types": "9.1.3"
84
+ "@intlayer/babel": "9.3.0",
85
+ "@intlayer/config": "9.3.0",
86
+ "@intlayer/core": "9.3.0",
87
+ "@intlayer/dictionaries-entry": "9.3.0",
88
+ "@intlayer/engine": "9.3.0",
89
+ "@intlayer/types": "9.3.0"
90
90
  },
91
91
  "devDependencies": {
92
- "@types/node": "26.1.2",
92
+ "@types/node": "26.2.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.1.3",
104
- "@intlayer/vue-compiler": "9.1.3",
103
+ "@intlayer/svelte-compiler": "9.3.0",
104
+ "@intlayer/vue-compiler": "9.3.0",
105
105
  "h3": ">=1.0.0",
106
106
  "vite": ">=4.0.0"
107
107
  },