vite-intlayer 9.3.2 → 9.4.0-canary.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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/dist/esm/index.mjs +3 -1
  3. package/dist/esm/intlayerChunkPlugin.mjs +181 -0
  4. package/dist/esm/intlayerChunkPlugin.mjs.map +1 -0
  5. package/dist/esm/intlayerMinifyPlugin.mjs +16 -15
  6. package/dist/esm/intlayerMinifyPlugin.mjs.map +1 -1
  7. package/dist/esm/intlayerNitroHandler.mjs +17 -7
  8. package/dist/esm/intlayerNitroHandler.mjs.map +1 -1
  9. package/dist/esm/intlayerOptimizePlugin.mjs +6 -5
  10. package/dist/esm/intlayerOptimizePlugin.mjs.map +1 -1
  11. package/dist/esm/intlayerPlugin.mjs +4 -0
  12. package/dist/esm/intlayerPlugin.mjs.map +1 -1
  13. package/dist/esm/intlayerPreloadPlugin.mjs +88 -0
  14. package/dist/esm/intlayerPreloadPlugin.mjs.map +1 -0
  15. package/dist/esm/intlayerProxyHandler.mjs +449 -0
  16. package/dist/esm/intlayerProxyHandler.mjs.map +1 -0
  17. package/dist/esm/intlayerProxyPlugin.mjs +10 -406
  18. package/dist/esm/intlayerProxyPlugin.mjs.map +1 -1
  19. package/dist/esm/intlayerPrunePlugin.mjs +5 -13
  20. package/dist/esm/intlayerPrunePlugin.mjs.map +1 -1
  21. package/dist/types/index.d.ts +3 -1
  22. package/dist/types/intlayerChunkPlugin.d.ts +88 -0
  23. package/dist/types/intlayerChunkPlugin.d.ts.map +1 -0
  24. package/dist/types/intlayerMinifyPlugin.d.ts +5 -1
  25. package/dist/types/intlayerMinifyPlugin.d.ts.map +1 -1
  26. package/dist/types/intlayerNitroHandler.d.ts +1 -1
  27. package/dist/types/intlayerNitroHandler.d.ts.map +1 -1
  28. package/dist/types/intlayerPlugin.d.ts.map +1 -1
  29. package/dist/types/intlayerPreloadPlugin.d.ts +34 -0
  30. package/dist/types/intlayerPreloadPlugin.d.ts.map +1 -0
  31. package/dist/types/intlayerProxyHandler.d.ts +50 -0
  32. package/dist/types/intlayerProxyHandler.d.ts.map +1 -0
  33. package/dist/types/intlayerProxyPlugin.d.ts +4 -11
  34. package/dist/types/intlayerProxyPlugin.d.ts.map +1 -1
  35. package/dist/types/intlayerPrunePlugin.d.ts +5 -0
  36. package/dist/types/intlayerPrunePlugin.d.ts.map +1 -1
  37. package/package.json +10 -10
package/README.md CHANGED
@@ -327,4 +327,4 @@ Contribute on [GitHub](https://github.com/aymericzip/intlayer), [GitLab](https:/
327
327
 
328
328
  If you like Intlayer, give us a ⭐ on GitHub. It helps others discover the project! [See why GitHub Stars matter](https://github.com/aymericzip/intlayer/blob/main/CONTRIBUTING.md#why-github-stars-matter-).
329
329
 
330
- [![Star History Chart](https://api.star-history.com/svg?repos=aymericzip/intlayer&type=Date)](https://star-history.com/#aymericzip/intlayer&Date)
330
+ [![Star History Chart](https://star-history.dera.page/svg?repos=aymericzip/intlayer&type=Date)](https://star-history.dera.page/#aymericzip/intlayer&Date)
@@ -1,8 +1,10 @@
1
1
  import { intlayerCompiler } from "./IntlayerCompilerPlugin.mjs";
2
2
  import { intlayerMinify } from "./intlayerMinifyPlugin.mjs";
3
3
  import { intlayerPrune } from "./intlayerPrunePlugin.mjs";
4
+ import { createDictionaryModuleParser, intlayerChunk, resolveBoundaries, toBoundaryName } from "./intlayerChunkPlugin.mjs";
4
5
  import { intlayerOptimize } from "./intlayerOptimizePlugin.mjs";
6
+ import { intlayerPreload } from "./intlayerPreloadPlugin.mjs";
5
7
  import { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy } from "./intlayerProxyPlugin.mjs";
6
8
  import { intLayerPlugin, intlayer, intlayerPlugin } from "./intlayerPlugin.mjs";
7
9
 
8
- export { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerProxy, intlayerPrune };
10
+ export { createDictionaryModuleParser, createIntlayerProxyHandler, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerChunk, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerPreload, intlayerProxy, intlayerPrune, resolveBoundaries, toBoundaryName };
@@ -0,0 +1,181 @@
1
+ import { colorizeNumber, getAppLogger } from "@intlayer/config/logger";
2
+ import { normalizePath } from "@intlayer/config/utils";
3
+ import { DYNAMIC_DICTIONARIES_JSON_SUBDIR } from "@intlayer/config/defaultValues";
4
+
5
+ //#region src/intlayerChunkPlugin.ts
6
+ /**
7
+ * Builds the parser that recognises a per-locale dictionary chunk emitted in
8
+ * `dynamic` import mode.
9
+ *
10
+ * The layout is read relative to `dynamicDictionariesDir` rather than matched
11
+ * anywhere in the path, so an application file that happens to sit under its own
12
+ * `json/<something>/<something>.json` cannot be mistaken for a dictionary.
13
+ * Everything between the key and the locale is a qualifier dimension
14
+ * (collections, variants), which is why the middle segments are skipped instead
15
+ * of being required to be absent.
16
+ *
17
+ * @param dynamicDictionariesDir - Absolute, POSIX-normalized dictionaries root.
18
+ * @returns Parser returning the key and locale, or `null` for any other module.
19
+ */
20
+ const createDictionaryModuleParser = (dynamicDictionariesDir) => {
21
+ const prefix = `${dynamicDictionariesDir}/${DYNAMIC_DICTIONARIES_JSON_SUBDIR}/`;
22
+ return (moduleId) => {
23
+ const posixId = normalizePath(moduleId.split("?", 1)[0] ?? moduleId);
24
+ if (!posixId.startsWith(prefix)) return null;
25
+ const segments = posixId.slice(prefix.length).split("/");
26
+ if (segments.length < 2) return null;
27
+ const fileName = segments[segments.length - 1];
28
+ if (!fileName.endsWith(".json")) return null;
29
+ return {
30
+ key: segments[0],
31
+ locale: fileName.slice(0, -5)
32
+ };
33
+ };
34
+ };
35
+ /** Characters that are unsafe in a chunk file name. */
36
+ const UNSAFE_NAME_CHARACTERS = /[^a-zA-Z0-9]+/g;
37
+ /**
38
+ * Short, stable digest of a module id, appended to a boundary name so two
39
+ * boundaries sharing a basename (`index.tsx` and `index.tsx`) do not collapse
40
+ * into a single chunk.
41
+ */
42
+ const digest = (value) => {
43
+ let hash = 5381;
44
+ for (let index = 0; index < value.length; index++) hash = (hash << 5) + hash + value.charCodeAt(index) >>> 0;
45
+ return hash.toString(36).slice(0, 6);
46
+ };
47
+ /** Turns a boundary module id into a readable chunk-name fragment. */
48
+ const toBoundaryName = (moduleId) => {
49
+ return `${(normalizePath(moduleId).split("/").pop() ?? "chunk").replace(/\.[^.]+$/, "").replace(UNSAFE_NAME_CHARACTERS, "-").replace(/^-+|-+$/g, "") || "chunk"}-${digest(moduleId)}`;
50
+ };
51
+ /**
52
+ * Walks up the import graph from a dictionary JSON to the code-split boundaries
53
+ * that reach it.
54
+ *
55
+ * A module pulled in through `import()` — a `React.lazy` section, a split
56
+ * route — begins its own chunk, so it is the unit whose dictionaries should
57
+ * travel together: one request per Suspense boundary rather than one per
58
+ * dictionary. The walk deliberately starts from the JSON's importers rather
59
+ * than the JSON itself, because *every* dictionary is reached through
60
+ * `import()` and would otherwise be read as its own boundary.
61
+ *
62
+ * @returns Boundary ids reaching this dictionary, empty when the graph is
63
+ * unavailable (the module was pruned, or resolution failed).
64
+ */
65
+ const resolveBoundaries = (jsonModuleId, context) => {
66
+ const jsonNode = context.getModuleInfo(jsonModuleId);
67
+ if (!jsonNode) return [];
68
+ const visited = /* @__PURE__ */ new Set();
69
+ const boundaries = /* @__PURE__ */ new Set();
70
+ const queue = [...jsonNode.dynamicImporters, ...jsonNode.importers];
71
+ while (queue.length > 0) {
72
+ const currentId = queue.pop();
73
+ if (visited.has(currentId)) continue;
74
+ visited.add(currentId);
75
+ const node = context.getModuleInfo(currentId);
76
+ if (!node) continue;
77
+ const isCodeSplitBoundary = node.dynamicImporters.length > 0;
78
+ const isGraphRoot = node.isEntry || node.importers.length === 0;
79
+ if (isCodeSplitBoundary || isGraphRoot) {
80
+ boundaries.add(currentId);
81
+ continue;
82
+ }
83
+ queue.push(...node.importers);
84
+ }
85
+ return [...boundaries].sort();
86
+ };
87
+ /**
88
+ * Groups dynamically imported dictionaries by the code-split boundary that
89
+ * consumes them, so each lazily loaded page or section fetches its content in
90
+ * a single request instead of one per dictionary.
91
+ *
92
+ * Without this, `importMode: 'dynamic'` emits a chunk per dictionary per
93
+ * locale. A page assembled from many components then issues dozens of ~3 KB
94
+ * requests, and because each is discovered only once its consumer has
95
+ * executed, they land in the last wave of the request waterfall — page text
96
+ * arrives after everything else.
97
+ *
98
+ * Switching to `importMode: 'static'` would collapse those requests but inline
99
+ * every locale into the bundle, which does not scale past a handful of
100
+ * languages. Grouping keeps the per-locale split and removes the fragmentation.
101
+ *
102
+ * Dictionaries reached from more than one boundary are placed in a shared
103
+ * chunk, so no page ships another page's content and the bytes are not
104
+ * duplicated across chunks.
105
+ *
106
+ * Because a boundary's dictionaries share one chunk, warming that chunk warms
107
+ * the whole boundary in a single request — which is why the top-level-await
108
+ * preload (`intlayerPreload`) costs one request per boundary rather than one
109
+ * per dictionary, without any page loading content belonging to another.
110
+ *
111
+ * @param intlayerConfig - Resolved Intlayer configuration.
112
+ */
113
+ const intlayerChunk = (intlayerConfig) => {
114
+ const dynamicDictionariesDir = normalizePath(intlayerConfig.system.dynamicDictionariesDir);
115
+ const parseDictionaryModule = createDictionaryModuleParser(dynamicDictionariesDir);
116
+ const appLogger = getAppLogger(intlayerConfig);
117
+ /** Dictionary modules assigned to each group, for the build summary. */
118
+ const grouped = /* @__PURE__ */ new Map();
119
+ /** Dictionaries the graph walk could not attribute to any boundary. */
120
+ const unresolved = [];
121
+ /** Records a dictionary against its group and returns the group name. */
122
+ const assign = (groupName, dictionaryKey) => {
123
+ const members = grouped.get(groupName) ?? [];
124
+ members.push(dictionaryKey);
125
+ grouped.set(groupName, members);
126
+ return groupName;
127
+ };
128
+ const isDictionaryModule = (moduleId) => parseDictionaryModule(moduleId) !== null;
129
+ return {
130
+ name: "vite-intlayer-chunk-plugin",
131
+ /**
132
+ * Grouping only exists to undo build-time fragmentation, so the dev server —
133
+ * which serves modules unbundled — is skipped.
134
+ *
135
+ * The build is deliberately *not* gated on the configured `importMode`: a
136
+ * `.content` file can set `importMode: 'dynamic'` on a single dictionary
137
+ * under a `static` or `fetch` global mode, and those dictionaries fragment
138
+ * exactly like a fully dynamic build. Modules that reach the graph are the
139
+ * honest signal, so the group's `test` does the gating instead — it simply
140
+ * matches nothing when no dictionary resolves to dynamic.
141
+ */
142
+ apply: (_viteConfig, env) => intlayerConfig.build.chunkGrouping && env.command === "build",
143
+ config: () => {
144
+ return { environments: { client: { build: { rolldownOptions: { output: { codeSplitting: { groups: [{
145
+ name: (moduleId, context) => {
146
+ const dictionary = parseDictionaryModule(moduleId);
147
+ if (!dictionary) return null;
148
+ const boundaries = resolveBoundaries(moduleId, context);
149
+ if (boundaries.length === 0) {
150
+ unresolved.push(moduleId);
151
+ return null;
152
+ }
153
+ const groupName = `intlayer-${boundaries.length > 1 ? "shared" : toBoundaryName(boundaries[0])}-${dictionary.locale}`;
154
+ return assign(groupName, dictionary.key);
155
+ },
156
+ test: isDictionaryModule,
157
+ minSize: 0,
158
+ minShareCount: 1,
159
+ priority: 50
160
+ }] } } } } } } };
161
+ },
162
+ /**
163
+ * Reports what the grouping actually did. Silence here means the group
164
+ * never matched — the usual cause is an `importMode` that emits no
165
+ * per-dictionary chunks at all.
166
+ */
167
+ closeBundle() {
168
+ const groupCount = grouped.size;
169
+ if (groupCount === 0 && unresolved.length === 0) return;
170
+ const dictionaryCount = [...grouped.values()].reduce((total, members) => total + members.length, 0);
171
+ appLogger(`Grouped ${colorizeNumber(dictionaryCount)} dictionary chunks into ${colorizeNumber(groupCount)} per-boundary chunks.`);
172
+ if (unresolved.length > 0) appLogger(`${unresolved.length} dictionaries had no resolvable boundary and kept their own chunk.`, { level: "warn" });
173
+ grouped.clear();
174
+ unresolved.length = 0;
175
+ }
176
+ };
177
+ };
178
+
179
+ //#endregion
180
+ export { createDictionaryModuleParser, intlayerChunk, resolveBoundaries, toBoundaryName };
181
+ //# sourceMappingURL=intlayerChunkPlugin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intlayerChunkPlugin.mjs","names":[],"sources":["../../src/intlayerChunkPlugin.ts"],"sourcesContent":["import { DYNAMIC_DICTIONARIES_JSON_SUBDIR } from '@intlayer/config/defaultValues';\nimport { colorizeNumber, getAppLogger } from '@intlayer/config/logger';\nimport { normalizePath } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Plugin } from 'vite';\n\n/**\n * Module-graph accessor Rolldown passes to a chunk-name function.\n *\n * Declared structurally rather than imported from `rolldown` so this plugin\n * keeps working against the Rolldown bundled inside Vite, whose types are not\n * re-exported.\n */\nexport type ChunkingContext = {\n getModuleInfo: (moduleId: string) => ModuleGraphNode | null;\n};\n\nexport type ModuleGraphNode = {\n id: string;\n isEntry: boolean;\n /** Ids of the modules that statically `import` this one. */\n importers: string[];\n /** Ids of the modules that reach this one through `import()`. */\n dynamicImporters: string[];\n};\n\n/** A per-locale dictionary chunk, identified from its module id. */\nexport type DictionaryModule = {\n key: string;\n locale: string;\n};\n\n/**\n * Builds the parser that recognises a per-locale dictionary chunk emitted in\n * `dynamic` import mode.\n *\n * The layout is read relative to `dynamicDictionariesDir` rather than matched\n * anywhere in the path, so an application file that happens to sit under its own\n * `json/<something>/<something>.json` cannot be mistaken for a dictionary.\n * Everything between the key and the locale is a qualifier dimension\n * (collections, variants), which is why the middle segments are skipped instead\n * of being required to be absent.\n *\n * @param dynamicDictionariesDir - Absolute, POSIX-normalized dictionaries root.\n * @returns Parser returning the key and locale, or `null` for any other module.\n */\nexport const createDictionaryModuleParser = (\n dynamicDictionariesDir: string\n): ((moduleId: string) => DictionaryModule | null) => {\n const prefix = `${dynamicDictionariesDir}/${DYNAMIC_DICTIONARIES_JSON_SUBDIR}/`;\n\n return (moduleId) => {\n // Vite appends query suffixes (`?import`, `?url`) to module ids.\n const posixId = normalizePath(moduleId.split('?', 1)[0] ?? moduleId);\n if (!posixId.startsWith(prefix)) return null;\n\n const segments = posixId.slice(prefix.length).split('/');\n // `<key>/…qualifier segments…/<locale>.json` — at least a key and a file.\n if (segments.length < 2) return null;\n\n const fileName = segments[segments.length - 1]!;\n if (!fileName.endsWith('.json')) return null;\n\n return {\n key: segments[0]!,\n locale: fileName.slice(0, -'.json'.length),\n };\n };\n};\n\n/** Characters that are unsafe in a chunk file name. */\nconst UNSAFE_NAME_CHARACTERS = /[^a-zA-Z0-9]+/g;\n\n/**\n * Short, stable digest of a module id, appended to a boundary name so two\n * boundaries sharing a basename (`index.tsx` and `index.tsx`) do not collapse\n * into a single chunk.\n */\nconst digest = (value: string): string => {\n let hash = 5381;\n for (let index = 0; index < value.length; index++) {\n hash = ((hash << 5) + hash + value.charCodeAt(index)) >>> 0;\n }\n return hash.toString(36).slice(0, 6);\n};\n\n/** Turns a boundary module id into a readable chunk-name fragment. */\nexport const toBoundaryName = (moduleId: string): string => {\n const fileName = normalizePath(moduleId).split('/').pop() ?? 'chunk';\n const base = fileName\n .replace(/\\.[^.]+$/, '')\n .replace(UNSAFE_NAME_CHARACTERS, '-')\n .replace(/^-+|-+$/g, '');\n return `${base || 'chunk'}-${digest(moduleId)}`;\n};\n\n/**\n * Walks up the import graph from a dictionary JSON to the code-split boundaries\n * that reach it.\n *\n * A module pulled in through `import()` — a `React.lazy` section, a split\n * route — begins its own chunk, so it is the unit whose dictionaries should\n * travel together: one request per Suspense boundary rather than one per\n * dictionary. The walk deliberately starts from the JSON's importers rather\n * than the JSON itself, because *every* dictionary is reached through\n * `import()` and would otherwise be read as its own boundary.\n *\n * @returns Boundary ids reaching this dictionary, empty when the graph is\n * unavailable (the module was pruned, or resolution failed).\n */\nexport const resolveBoundaries = (\n jsonModuleId: string,\n context: ChunkingContext\n): string[] => {\n const jsonNode = context.getModuleInfo(jsonModuleId);\n if (!jsonNode) return [];\n\n const visited = new Set<string>();\n const boundaries = new Set<string>();\n const queue: string[] = [...jsonNode.dynamicImporters, ...jsonNode.importers];\n\n while (queue.length > 0) {\n const currentId = queue.pop() as string;\n if (visited.has(currentId)) continue;\n visited.add(currentId);\n\n const node = context.getModuleInfo(currentId);\n if (!node) continue;\n\n const isCodeSplitBoundary = node.dynamicImporters.length > 0;\n const isGraphRoot = node.isEntry || node.importers.length === 0;\n\n if (isCodeSplitBoundary || isGraphRoot) {\n boundaries.add(currentId);\n continue;\n }\n\n queue.push(...node.importers);\n }\n\n return [...boundaries].sort();\n};\n\n/**\n * Groups dynamically imported dictionaries by the code-split boundary that\n * consumes them, so each lazily loaded page or section fetches its content in\n * a single request instead of one per dictionary.\n *\n * Without this, `importMode: 'dynamic'` emits a chunk per dictionary per\n * locale. A page assembled from many components then issues dozens of ~3 KB\n * requests, and because each is discovered only once its consumer has\n * executed, they land in the last wave of the request waterfall — page text\n * arrives after everything else.\n *\n * Switching to `importMode: 'static'` would collapse those requests but inline\n * every locale into the bundle, which does not scale past a handful of\n * languages. Grouping keeps the per-locale split and removes the fragmentation.\n *\n * Dictionaries reached from more than one boundary are placed in a shared\n * chunk, so no page ships another page's content and the bytes are not\n * duplicated across chunks.\n *\n * Because a boundary's dictionaries share one chunk, warming that chunk warms\n * the whole boundary in a single request — which is why the top-level-await\n * preload (`intlayerPreload`) costs one request per boundary rather than one\n * per dictionary, without any page loading content belonging to another.\n *\n * @param intlayerConfig - Resolved Intlayer configuration.\n */\nexport const intlayerChunk = (intlayerConfig: IntlayerConfig): Plugin => {\n const dynamicDictionariesDir = normalizePath(\n intlayerConfig.system.dynamicDictionariesDir\n );\n\n const parseDictionaryModule = createDictionaryModuleParser(\n dynamicDictionariesDir\n );\n\n const appLogger = getAppLogger(intlayerConfig);\n\n /** Dictionary modules assigned to each group, for the build summary. */\n const grouped = new Map<string, string[]>();\n /** Dictionaries the graph walk could not attribute to any boundary. */\n const unresolved: string[] = [];\n\n /** Records a dictionary against its group and returns the group name. */\n const assign = (groupName: string, dictionaryKey: string): string => {\n const members = grouped.get(groupName) ?? [];\n members.push(dictionaryKey);\n grouped.set(groupName, members);\n return groupName;\n };\n\n const isDictionaryModule = (moduleId: string): boolean =>\n parseDictionaryModule(moduleId) !== null;\n\n return {\n name: 'vite-intlayer-chunk-plugin',\n\n /**\n * Grouping only exists to undo build-time fragmentation, so the dev server —\n * which serves modules unbundled — is skipped.\n *\n * The build is deliberately *not* gated on the configured `importMode`: a\n * `.content` file can set `importMode: 'dynamic'` on a single dictionary\n * under a `static` or `fetch` global mode, and those dictionaries fragment\n * exactly like a fully dynamic build. Modules that reach the graph are the\n * honest signal, so the group's `test` does the gating instead — it simply\n * matches nothing when no dictionary resolves to dynamic.\n */\n apply: (_viteConfig, env) =>\n intlayerConfig.build.chunkGrouping && env.command === 'build',\n\n config: () => {\n return {\n // Scoped to the client build. Dictionary chunks only exist to be\n // fetched by a browser, and `build` at the top level would apply to\n // every environment — re-chunking the server bundle too, where\n // reordering a dependency's modules can break their initialisation.\n environments: {\n client: {\n build: {\n rolldownOptions: {\n output: {\n codeSplitting: {\n groups: [\n {\n name: (moduleId: string, context: ChunkingContext) => {\n const dictionary = parseDictionaryModule(moduleId);\n if (!dictionary) return null;\n\n const boundaries = resolveBoundaries(\n moduleId,\n context\n );\n if (boundaries.length === 0) {\n unresolved.push(moduleId);\n return null;\n }\n\n const scope =\n boundaries.length > 1\n ? 'shared'\n : toBoundaryName(boundaries[0]!);\n const groupName = `intlayer-${scope}-${dictionary.locale}`;\n\n return assign(groupName, dictionary.key);\n },\n test: isDictionaryModule,\n // Dictionaries are small individually; the point is to\n // group them, so the surrounding build's size floor must\n // not dissolve these chunks back into their parents.\n minSize: 0,\n minShareCount: 1,\n priority: 50,\n },\n ],\n },\n },\n },\n },\n },\n },\n };\n },\n\n /**\n * Reports what the grouping actually did. Silence here means the group\n * never matched — the usual cause is an `importMode` that emits no\n * per-dictionary chunks at all.\n */\n closeBundle() {\n const groupCount = grouped.size;\n if (groupCount === 0 && unresolved.length === 0) return;\n\n const dictionaryCount = [...grouped.values()].reduce(\n (total, members) => total + members.length,\n 0\n );\n\n appLogger(\n `Grouped ${colorizeNumber(dictionaryCount)} dictionary chunks into ${colorizeNumber(groupCount)} per-boundary chunks.`\n );\n\n if (unresolved.length > 0) {\n appLogger(\n `${unresolved.length} dictionaries had no resolvable boundary and kept their own chunk.`,\n { level: 'warn' }\n );\n }\n\n grouped.clear();\n unresolved.length = 0;\n },\n };\n};\n"],"mappings":"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"}
@@ -87,7 +87,11 @@ const minifyGroupEntryNode = (node, fieldRenameMap) => fieldRenameMap && fieldRe
87
87
  * and should be left completely untouched to avoid shipping broken data.
88
88
  *
89
89
  * Field renaming (property mangling) is applied only for dictionaries that
90
- * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`.
90
+ * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`,
91
+ * and only while the visual editor is disabled: renaming rewrites the content
92
+ * keys the interpreter walks, so the `keyPath` reported to the editor would no
93
+ * longer resolve against the unmerged dictionaries. The whitespace compaction
94
+ * and the top-level metadata strip are keyPath-neutral and always run.
91
95
  * The corresponding rename is also applied to source-file property accesses by
92
96
  * the babel rename pass inside `intlayerOptimize`. Internal intlayer fields
93
97
  * such as `nodeType` are never renamed.
@@ -101,7 +105,7 @@ const minifyGroupEntryNode = (node, fieldRenameMap) => fieldRenameMap && fieldRe
101
105
  const intlayerMinify = (intlayerConfig, pruneContext) => {
102
106
  const logger = getAppLogger(intlayerConfig);
103
107
  const { optimize, minify } = intlayerConfig.build;
104
- const editorEnabled = intlayerConfig.editor.enabled;
108
+ const isFieldRenameEnabled = !intlayerConfig.editor.enabled;
105
109
  const { dictionariesDir, dynamicDictionariesDir, baseDir } = intlayerConfig.system;
106
110
  const isDictionaryJsonFile = (absoluteFilePath) => absoluteFilePath.endsWith(".json") && (absoluteFilePath.startsWith(dictionariesDir) || absoluteFilePath.startsWith(dynamicDictionariesDir));
107
111
  const isMinifyEnabled = (_config, env) => {
@@ -109,18 +113,15 @@ const intlayerMinify = (intlayerConfig, pruneContext) => {
109
113
  if (!(optimize === void 0 && isBuildCommand || optimize === true)) return false;
110
114
  if (!minify) return false;
111
115
  if (!isBuildCommand) return false;
112
- if (editorEnabled) {
113
- runOnce(join(baseDir, ".intlayer", "cache", "intlayer-minify-editor-warning.lock"), () => logger([
114
- "Dictionary minification is",
115
- colorize("disabled", ANSIColors.GREY_DARK),
116
- "because",
117
- colorize("editor.enabled", ANSIColors.BLUE),
118
- "is",
119
- colorize("true", ANSIColors.GREY_DARK),
120
- colorize("— the editor requires full dictionary content", ANSIColors.GREY)
121
- ]), { cacheTimeoutMs: 1e4 });
122
- return false;
123
- }
116
+ if (!isFieldRenameEnabled) runOnce(join(baseDir, ".intlayer", "cache", "intlayer-minify-editor-warning.lock"), () => logger([
117
+ "Dictionary field renaming is",
118
+ colorize("disabled", ANSIColors.GREY_DARK),
119
+ "because",
120
+ colorize("editor.enabled", ANSIColors.BLUE),
121
+ "is",
122
+ colorize("true", ANSIColors.GREY_DARK),
123
+ colorize("— the editor resolves edits by keyPath against the unmerged dictionaries", ANSIColors.GREY)
124
+ ]), { cacheTimeoutMs: 1e4 });
124
125
  return true;
125
126
  };
126
127
  return [{
@@ -144,7 +145,7 @@ const intlayerMinify = (intlayerConfig, pruneContext) => {
144
145
  }
145
146
  const dictionaryKey = typeof parsedDict.key === "string" ? parsedDict.key : void 0;
146
147
  if (pruneContext && dictionaryKey && pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) return null;
147
- const fieldRenameMap = pruneContext && dictionaryKey ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey) : void 0;
148
+ const fieldRenameMap = isFieldRenameEnabled && pruneContext && dictionaryKey ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey) : void 0;
148
149
  if (isQualifiedDictionaryGroup(parsedDict)) {
149
150
  const minifiedContent = {};
150
151
  for (const [entryId, node] of Object.entries(parsedDict.content)) minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerMinifyPlugin.mjs","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { NestedRenameMap, PruneContext } from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Field-rename helper\n\n/**\n * Recursively renames user-defined keys in a compiled intlayer content value\n * using the provided `NestedRenameMap`.\n *\n * Traversal rules (mirrors `buildNestedRenameMapFromContent`):\n * - Arrays → each element is recursed into with the same rename map.\n * This mirrors the array pass-through in the source-code rename walk,\n * where numeric indices (e.g. [0]) are transparent.\n * - Object with `nodeType: 'translation'` → intlayer translation node.\n * Keys at this level (`nodeType`, `translation`) are NOT renamed.\n * Recurse into each per-locale value with the same rename map.\n * - Object without `nodeType` → user-defined record.\n * Rename its keys using the current rename map level, then recurse into\n * each value with that entry's `children` map.\n * - Primitives → returned as-is.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n // Arrays: each element is renamed with the same map (indices are transparent).\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale with the same rename map\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val; // key not in map – keep as-is (e.g. already-pruned)\n }\n }\n return result;\n};\n\n/**\n * Applies the nested field rename map to a parsed dictionary object and\n * returns the renamed copy. The top-level dict keys (`key`, `locale`, etc.)\n * are never touched; only keys inside `content` are renamed.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n/**\n * Discriminates a compiled qualified dictionary group (collections, variants)\n * from a plain dictionary. Mirrors `isQualifiedDictionaryGroup` from\n * `@intlayer/core` but is inlined here to avoid a runtime dependency on core\n * inside the build plugin.\n */\nconst isQualifiedDictionaryGroup = (\n dict: Record<string, unknown>\n): dict is Record<string, unknown> & {\n qualifierTypes: unknown;\n content: Record<string, unknown>;\n} =>\n 'qualifierTypes' in dict &&\n Array.isArray(dict.qualifierTypes) &&\n 'content' in dict &&\n typeof dict.content === 'object' &&\n dict.content !== null;\n\n/**\n * Applies the field-rename map to a single qualified-group content node (the\n * value stored under a composite id). Coordinates are not stored on the node —\n * they live in the key — so only the node's content fields are renamed.\n */\nconst minifyGroupEntryNode = (\n node: unknown,\n fieldRenameMap: NestedRenameMap | undefined\n): unknown =>\n fieldRenameMap &&\n fieldRenameMap.size > 0 &&\n node &&\n typeof node === 'object' &&\n !Array.isArray(node)\n ? renameContentRecursively(node, fieldRenameMap)\n : node;\n\n// Plugin\n\n/**\n * Returns the Vite plugin that minifies compiled dictionary JSON files by\n * removing all unnecessary whitespace and optionally renaming content field\n * names to short alphabetic aliases (a, b, c, …).\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * The plugin is deliberately independent of the prune plugin: it can run\n * on its own when only `build.minify` is enabled. When both `purge` and\n * `minify` are active, the prune plugin runs first (it uses `enforce: 'pre'`\n * and is registered before this one); this plugin then receives the already-\n * pruned JSON, renames its field keys, and compacts it.\n *\n * Files listed in `pruneContext.dictionariesWithEdgeCases` are skipped:\n * those dictionaries encountered a structural issue during the prune phase\n * and should be left completely untouched to avoid shipping broken data.\n *\n * Field renaming (property mangling) is applied only for dictionaries that\n * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`.\n * The corresponding rename is also applied to source-file property accesses by\n * the babel rename pass inside `intlayerOptimize`. Internal intlayer fields\n * such as `nodeType` are never renamed.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Optional shared state from the prune plugin. When\n * provided, dictionaries flagged as edge-cases are\n * skipped and field renames are applied. Pass `null`\n * if the prune plugin is not active.\n */\nexport const intlayerMinify = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, minify } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const { dictionariesDir, dynamicDictionariesDir, baseDir } =\n intlayerConfig.system;\n\n // Fetch-mode dictionaries are served from a remote API at runtime using their\n // original field names. Minifying them (renaming fields) would create a\n // mismatch between the server response and the renamed client-side accesses.\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir));\n\n const isMinifyEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isOptimizeActive) return false;\n if (!minify) return false;\n if (!isBuildCommand) return false;\n\n if (editorEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-minify-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary minification is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor requires full dictionary content',\n ANSIColors.GREY\n ),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n return true;\n };\n\n const minifyPlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-minify',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM\n // conversion. Declaration order in the plugin array ensures this runs\n // after the prune plugin (which is also 'pre' but registered earlier).\n enforce: 'pre',\n apply: isMinifyEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n return null;\n\n // Parse JSON\n let parsedDict: Record<string, unknown>;\n try {\n parsedDict = JSON.parse(rawJsonCode) as Record<string, unknown>;\n } catch (parseError) {\n logger(\n [\n `Could not parse`,\n formatPath(absoluteFilePath),\n `as JSON. Skipping minification for this file.`,\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n const dictionaryKey =\n typeof parsedDict.key === 'string' ? parsedDict.key : undefined;\n\n // Skip edge-case dictionaries\n if (\n pruneContext &&\n dictionaryKey &&\n pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)\n ) {\n return null; // structural issue flagged during prune – leave untouched\n }\n\n const fieldRenameMap =\n pruneContext && dictionaryKey\n ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey)\n : undefined;\n\n // Qualified groups (collections / variants) carry their payload as a\n // `content` map keyed by composite id + `qualifierTypes`. Preserve\n // `qualifierTypes` and the per-entry nodes (the coordinates live in the\n // keys); only the field-rename pass is applied to each node.\n if (isQualifiedDictionaryGroup(parsedDict)) {\n const minifiedContent: Record<string, unknown> = {};\n for (const [entryId, node] of Object.entries(parsedDict.content)) {\n minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);\n }\n\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n qualifierTypes: parsedDict.qualifierTypes,\n content: minifiedContent,\n }),\n map: null,\n };\n }\n\n // Apply field rename (property mangling)\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(parsedDict, fieldRenameMap);\n }\n\n // Strip top-level metadata – ship only key + content. The variant\n // identity of a qualified chunk is encoded in its composite id (the chunk\n // path), so no extra per-chunk side-data is needed for selector matching.\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n content: parsedDict.content,\n }),\n map: null,\n };\n },\n };\n\n return [minifyPlugin];\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"intlayerMinifyPlugin.mjs","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { NestedRenameMap, PruneContext } from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Field-rename helper\n\n/**\n * Recursively renames user-defined keys in a compiled intlayer content value\n * using the provided `NestedRenameMap`.\n *\n * Traversal rules (mirrors `buildNestedRenameMapFromContent`):\n * - Arrays → each element is recursed into with the same rename map.\n * This mirrors the array pass-through in the source-code rename walk,\n * where numeric indices (e.g. [0]) are transparent.\n * - Object with `nodeType: 'translation'` → intlayer translation node.\n * Keys at this level (`nodeType`, `translation`) are NOT renamed.\n * Recurse into each per-locale value with the same rename map.\n * - Object without `nodeType` → user-defined record.\n * Rename its keys using the current rename map level, then recurse into\n * each value with that entry's `children` map.\n * - Primitives → returned as-is.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n // Arrays: each element is renamed with the same map (indices are transparent).\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale with the same rename map\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val; // key not in map – keep as-is (e.g. already-pruned)\n }\n }\n return result;\n};\n\n/**\n * Applies the nested field rename map to a parsed dictionary object and\n * returns the renamed copy. The top-level dict keys (`key`, `locale`, etc.)\n * are never touched; only keys inside `content` are renamed.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n/**\n * Discriminates a compiled qualified dictionary group (collections, variants)\n * from a plain dictionary. Mirrors `isQualifiedDictionaryGroup` from\n * `@intlayer/core` but is inlined here to avoid a runtime dependency on core\n * inside the build plugin.\n */\nconst isQualifiedDictionaryGroup = (\n dict: Record<string, unknown>\n): dict is Record<string, unknown> & {\n qualifierTypes: unknown;\n content: Record<string, unknown>;\n} =>\n 'qualifierTypes' in dict &&\n Array.isArray(dict.qualifierTypes) &&\n 'content' in dict &&\n typeof dict.content === 'object' &&\n dict.content !== null;\n\n/**\n * Applies the field-rename map to a single qualified-group content node (the\n * value stored under a composite id). Coordinates are not stored on the node —\n * they live in the key — so only the node's content fields are renamed.\n */\nconst minifyGroupEntryNode = (\n node: unknown,\n fieldRenameMap: NestedRenameMap | undefined\n): unknown =>\n fieldRenameMap &&\n fieldRenameMap.size > 0 &&\n node &&\n typeof node === 'object' &&\n !Array.isArray(node)\n ? renameContentRecursively(node, fieldRenameMap)\n : node;\n\n// Plugin\n\n/**\n * Returns the Vite plugin that minifies compiled dictionary JSON files by\n * removing all unnecessary whitespace and optionally renaming content field\n * names to short alphabetic aliases (a, b, c, …).\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * The plugin is deliberately independent of the prune plugin: it can run\n * on its own when only `build.minify` is enabled. When both `purge` and\n * `minify` are active, the prune plugin runs first (it uses `enforce: 'pre'`\n * and is registered before this one); this plugin then receives the already-\n * pruned JSON, renames its field keys, and compacts it.\n *\n * Files listed in `pruneContext.dictionariesWithEdgeCases` are skipped:\n * those dictionaries encountered a structural issue during the prune phase\n * and should be left completely untouched to avoid shipping broken data.\n *\n * Field renaming (property mangling) is applied only for dictionaries that\n * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`,\n * and only while the visual editor is disabled: renaming rewrites the content\n * keys the interpreter walks, so the `keyPath` reported to the editor would no\n * longer resolve against the unmerged dictionaries. The whitespace compaction\n * and the top-level metadata strip are keyPath-neutral and always run.\n * The corresponding rename is also applied to source-file property accesses by\n * the babel rename pass inside `intlayerOptimize`. Internal intlayer fields\n * such as `nodeType` are never renamed.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Optional shared state from the prune plugin. When\n * provided, dictionaries flagged as edge-cases are\n * skipped and field renames are applied. Pass `null`\n * if the prune plugin is not active.\n */\nexport const intlayerMinify = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, minify } = intlayerConfig.build;\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` emitted by `ContentSelector` would no longer resolve against the\n // unmerged dictionaries the editor edits. Whitespace compaction and the\n // top-level metadata strip are keyPath-neutral and stay active.\n const isFieldRenameEnabled = !intlayerConfig.editor.enabled;\n\n const { dictionariesDir, dynamicDictionariesDir, baseDir } =\n intlayerConfig.system;\n\n // Fetch-mode dictionaries are served from a remote API at runtime using their\n // original field names. Minifying them (renaming fields) would create a\n // mismatch between the server response and the renamed client-side accesses.\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir));\n\n const isMinifyEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isOptimizeActive) return false;\n if (!minify) return false;\n if (!isBuildCommand) return false;\n\n if (!isFieldRenameEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-minify-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor resolves edits by keyPath against the unmerged dictionaries',\n ANSIColors.GREY\n ),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n }\n\n return true;\n };\n\n const minifyPlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-minify',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM\n // conversion. Declaration order in the plugin array ensures this runs\n // after the prune plugin (which is also 'pre' but registered earlier).\n enforce: 'pre',\n apply: isMinifyEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n return null;\n\n // Parse JSON\n let parsedDict: Record<string, unknown>;\n try {\n parsedDict = JSON.parse(rawJsonCode) as Record<string, unknown>;\n } catch (parseError) {\n logger(\n [\n `Could not parse`,\n formatPath(absoluteFilePath),\n `as JSON. Skipping minification for this file.`,\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n const dictionaryKey =\n typeof parsedDict.key === 'string' ? parsedDict.key : undefined;\n\n // Skip edge-case dictionaries\n if (\n pruneContext &&\n dictionaryKey &&\n pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)\n ) {\n return null; // structural issue flagged during prune – leave untouched\n }\n\n const fieldRenameMap =\n isFieldRenameEnabled && pruneContext && dictionaryKey\n ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey)\n : undefined;\n\n // Qualified groups (collections / variants) carry their payload as a\n // `content` map keyed by composite id + `qualifierTypes`. Preserve\n // `qualifierTypes` and the per-entry nodes (the coordinates live in the\n // keys); only the field-rename pass is applied to each node.\n if (isQualifiedDictionaryGroup(parsedDict)) {\n const minifiedContent: Record<string, unknown> = {};\n for (const [entryId, node] of Object.entries(parsedDict.content)) {\n minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);\n }\n\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n qualifierTypes: parsedDict.qualifierTypes,\n content: minifiedContent,\n }),\n map: null,\n };\n }\n\n // Apply field rename (property mangling)\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(parsedDict, fieldRenameMap);\n }\n\n // Strip top-level metadata – ship only key + content. The variant\n // identity of a qualified chunk is encoded in its composite id (the chunk\n // path), so no extra per-chunk side-data is needed for selector matching.\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n content: parsedDict.content,\n }),\n map: null,\n };\n },\n };\n\n return [minifyPlugin];\n};\n"],"mappings":"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"}
@@ -1,12 +1,22 @@
1
- import { createIntlayerProxyHandler } from "./intlayerProxyPlugin.mjs";
1
+ import { createProxyHandler } from "./intlayerProxyHandler.mjs";
2
2
  import { getAppLogger } from "@intlayer/config/logger";
3
- import { getConfiguration } from "@intlayer/config/node";
4
3
  import { formatProxyEnabledMessage } from "@intlayer/core/localization";
4
+ import * as builtConfiguration from "@intlayer/config/built";
5
5
 
6
6
  //#region src/intlayerNitroHandler.ts
7
- const intlayerConfig = getConfiguration();
7
+ /**
8
+ * The configuration as a plain JSON object.
9
+ *
10
+ * Bundlers alias `@intlayer/config/built` to the generated
11
+ * `.intlayer/config/configuration.mjs`, which inlines the resolved
12
+ * configuration at build time. Loading it through `@intlayer/config/node`
13
+ * instead would transpile `intlayer.config.ts` with `esbuild` on every server
14
+ * start, making the built server unable to boot without `node_modules` next to
15
+ * it - which is exactly what a Nitro deployment output is.
16
+ */
17
+ const intlayerConfig = builtConfiguration;
8
18
  getAppLogger(intlayerConfig)(formatProxyEnabledMessage(false), { level: "info" });
9
- const nodeMiddleware = createIntlayerProxyHandler();
19
+ const nodeMiddleware = createProxyHandler({ configuration: intlayerConfig });
10
20
  /**
11
21
  * Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).
12
22
  *
@@ -15,7 +25,7 @@ const nodeMiddleware = createIntlayerProxyHandler();
15
25
  * and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with
16
26
  * "undefined is not an object (evaluating 'event.node.req')".
17
27
  *
18
- * It bridges h3 v2 events to the Node.js-style `createIntlayerProxyHandler` middleware
28
+ * It bridges h3 v2 events to the Node.js-style proxy middleware
19
29
  * via lightweight IncomingMessage / ServerResponse shims:
20
30
  *
21
31
  * - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends
@@ -31,7 +41,7 @@ var intlayerNitroHandler_default = async (event) => new Promise((resolve) => {
31
41
  /**
32
42
  * Minimal IncomingMessage shim.
33
43
  *
34
- * Only the fields actually read by createIntlayerProxyHandler are populated:
44
+ * Only the fields actually read by the proxy middleware are populated:
35
45
  * - url : the current pathname + search, modified for rewrites
36
46
  * - headers.cookie : locale cookie detection
37
47
  * - headers.host : domain-based locale routing
@@ -60,7 +70,7 @@ var intlayerNitroHandler_default = async (event) => new Promise((resolve) => {
60
70
  /**
61
71
  * Minimal ServerResponse shim.
62
72
  *
63
- * Implements only the methods that createIntlayerProxyHandler invokes:
73
+ * Implements only the methods that the proxy middleware invokes:
64
74
  * writeHead() — status + Location header for 301 redirects
65
75
  * setHeader() — Set-Cookie written by setLocaleInStorageServer
66
76
  * getHeader() — defensive read-back (not strictly required but safe)
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerNitroHandler.mjs","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { formatProxyEnabledMessage } from '@intlayer/core/localization';\nimport { createIntlayerProxyHandler } from './intlayerProxyPlugin';\n\n/**\n * Minimal duck-type for h3 v2's H3Event.\n *\n * We intentionally avoid importing from 'h3' to keep this file runtime-agnostic —\n * Nitro bundles h3 internally and provides the populated event at runtime. Using a\n * structural type here means the file compiles without h3 in devDependencies and\n * works with any h3 v2-compatible runtime (Bun, Deno, Node).\n */\ntype H3EventLike = {\n /**\n * pathname + search — a computed getter on H3Event:\n * `return this.url.pathname + this.url.search`\n */\n readonly path: string;\n /**\n * Full URL object — a **plain property** (not a getter) on H3Event, safe to\n * replace for internal URL rewrites. After assignment, `event.path` will\n * automatically reflect the new pathname + search via the getter.\n */\n url: URL;\n /**\n * Web Fetch API Headers — always populated in h3 v2 regardless of preset\n * (Node, Bun, Deno). Use `.get(name)` instead of bracket-access.\n */\n readonly headers: Headers;\n /**\n * Lazy response object — created on first access; its `headers` carry outgoing\n * response headers (e.g. Set-Cookie) that h3 merges into the HTTP response.\n */\n readonly res: {\n readonly headers: Headers;\n };\n};\n\nconst intlayerConfig = getConfiguration();\nconst logger = getAppLogger(intlayerConfig);\n// A Nitro server is a production server, so the stored locale always drives\n// redirects here — hence the `false`. This runs once per server process; the\n// Vite preview server deliberately does not add a second proxy layer on top of\n// this one (see `configurePreviewServer` in `intlayerProxyPlugin`).\nlogger(formatProxyEnabledMessage(false), {\n level: 'info',\n});\n\nconst nodeMiddleware = createIntlayerProxyHandler();\n\n/**\n * Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).\n *\n * Unlike `fromNodeMiddleware` (h3 v1 API), this handler uses the Web Fetch API event\n * model exclusively and is therefore compatible with ALL Nitro presets — including Bun\n * and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with\n * \"undefined is not an object (evaluating 'event.node.req')\".\n *\n * It bridges h3 v2 events to the Node.js-style `createIntlayerProxyHandler` middleware\n * via lightweight IncomingMessage / ServerResponse shims:\n *\n * - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends\n * the correct HTTP response to the browser.\n * - **Rewrite** (`next()` + modified `req.url`): replaces `event.url` with the rewritten\n * URL so `event.path` (a getter) returns the new pathname for downstream handlers and\n * the Nitro router.\n * - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to\n * the next handler / route.\n */\nexport default async (event: H3EventLike): Promise<Response | void> =>\n new Promise<Response | void>((resolve) => {\n const initialPath = event.path;\n\n /**\n * Minimal IncomingMessage shim.\n *\n * Only the fields actually read by createIntlayerProxyHandler are populated:\n * - url : the current pathname + search, modified for rewrites\n * - headers.cookie : locale cookie detection\n * - headers.host : domain-based locale routing\n * - headers.accept-language : browser Accept-Language fallback\n * - headers.x-forwarded-* : forwarded host/proto for reverse-proxy setups\n * - headers.x-nitro-prerender : marks a request issued by Nitro's\n * prerenderer, which keeps the generated page URL-driven\n *\n * headers must be a mutable plain object because setLocaleInStorageServer\n * writes Set-Cookie back via req.headers[name] = value.\n */\n const fakeReq = {\n url: initialPath,\n method: 'GET',\n headers: {\n cookie: event.headers.get('cookie') ?? '',\n host: event.headers.get('host') ?? '',\n 'accept-language': event.headers.get('accept-language') ?? '',\n 'x-forwarded-host': event.headers.get('x-forwarded-host') ?? '',\n 'x-forwarded-proto': event.headers.get('x-forwarded-proto') ?? '',\n 'x-nitro-prerender': event.headers.get('x-nitro-prerender') ?? '',\n } as Record<string, string>,\n } as unknown as IncomingMessage;\n\n let responseStatusCode = 200;\n const accumulatedHeaders: Record<string, string> = {};\n\n /**\n * Minimal ServerResponse shim.\n *\n * Implements only the methods that createIntlayerProxyHandler invokes:\n * writeHead() — status + Location header for 301 redirects\n * setHeader() — Set-Cookie written by setLocaleInStorageServer\n * getHeader() — defensive read-back (not strictly required but safe)\n * end() — finalises the response; for redirects this returns a\n * Web API Response object that Nitro sends to the client\n */\n const fakeRes = {\n writeHead(\n statusCode: number,\n headersArg?: Record<string, string | string[] | number> | string\n ) {\n // Capture the status code and any headers supplied alongside writeHead.\n responseStatusCode = statusCode;\n if (headersArg && typeof headersArg === 'object') {\n for (const [key, value] of Object.entries(headersArg)) {\n accumulatedHeaders[key.toLowerCase()] = Array.isArray(value)\n ? (value[0] ?? '')\n : String(value);\n }\n }\n return fakeRes;\n },\n setHeader(name: string, value: string | number | string[]) {\n // Capture Set-Cookie and other outgoing headers.\n accumulatedHeaders[name.toLowerCase()] = Array.isArray(value)\n ? (value[0] ?? '')\n : String(value);\n return fakeRes;\n },\n getHeader(name: string) {\n return accumulatedHeaders[name.toLowerCase()];\n },\n getHeaders() {\n return { ...accumulatedHeaders };\n },\n end(body?: string | Buffer | null) {\n // Build a Web API Response from accumulated status + headers + body.\n // For 3xx redirects the body is intentionally null.\n const webHeaders = new Headers();\n for (const [key, value] of Object.entries(accumulatedHeaders)) {\n webHeaders.set(key, value);\n }\n const isRedirect =\n responseStatusCode >= 300 && responseStatusCode < 400;\n resolve(\n new Response(\n isRedirect ? null : typeof body === 'string' ? body : null,\n {\n status: responseStatusCode,\n headers: webHeaders,\n }\n )\n );\n return fakeRes;\n },\n headersSent: false,\n } as unknown as ServerResponse<IncomingMessage>;\n\n nodeMiddleware(fakeReq, fakeRes, () => {\n // Middleware called next() — either a URL rewrite or a true pass-through.\n const rewrittenPath = fakeReq.url as string;\n\n if (rewrittenPath !== initialPath) {\n // The middleware rewrote the URL (e.g. /about → /en/about for locale prefix).\n // Replace event.url so that event.path (the getter: url.pathname + url.search)\n // returns the new path and the Nitro router matches the correct route.\n //\n // event.url is a plain property on h3 v2's H3Event (not a getter), so direct\n // assignment is safe. We use event.url.origin as the base so relative paths\n // resolve correctly; for path-only requests origin defaults to http://localhost.\n try {\n event.url = new URL(rewrittenPath, event.url.origin);\n } catch {\n console.error(\n '[intlayer-proxy] URL rewrite failed — invalid path:',\n rewrittenPath\n );\n }\n }\n\n // Forward any Set-Cookie or custom headers set by setLocaleInStorageServer to\n // the h3 v2 response object so they are included in the outgoing HTTP response.\n // Accessing event.res lazily creates the H3EventResponse (no cost if empty).\n if (Object.keys(accumulatedHeaders).length > 0) {\n for (const [key, value] of Object.entries(accumulatedHeaders)) {\n event.res.headers.set(key, value);\n }\n }\n\n resolve(undefined);\n });\n });\n"],"mappings":";;;;;;AAwCA,MAAM,iBAAiB,iBAAiB;AACzB,aAAa,cAKvB,CAAC,CAAC,0BAA0B,KAAK,GAAG,EACvC,OAAO,OACT,CAAC;AAED,MAAM,iBAAiB,2BAA2B;;;;;;;;;;;;;;;;;;;;AAqBlD,mCAAe,OAAO,UACpB,IAAI,SAA0B,YAAY;CACxC,MAAM,cAAc,MAAM;;;;;;;;;;;;;;;;CAiB1B,MAAM,UAAU;EACd,KAAK;EACL,QAAQ;EACR,SAAS;GACP,QAAQ,MAAM,QAAQ,IAAI,QAAQ,KAAK;GACvC,MAAM,MAAM,QAAQ,IAAI,MAAM,KAAK;GACnC,mBAAmB,MAAM,QAAQ,IAAI,iBAAiB,KAAK;GAC3D,oBAAoB,MAAM,QAAQ,IAAI,kBAAkB,KAAK;GAC7D,qBAAqB,MAAM,QAAQ,IAAI,mBAAmB,KAAK;GAC/D,qBAAqB,MAAM,QAAQ,IAAI,mBAAmB,KAAK;EACjE;CACF;CAEA,IAAI,qBAAqB;CACzB,MAAM,qBAA6C,CAAC;;;;;;;;;;;CAYpD,MAAM,UAAU;EACd,UACE,YACA,YACA;GAEA,qBAAqB;GACrB,IAAI,cAAc,OAAO,eAAe,UACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,GAClD,mBAAmB,IAAI,YAAY,KAAK,MAAM,QAAQ,KAAK,IACtD,MAAM,MAAM,KACb,OAAO,KAAK;GAGpB,OAAO;EACT;EACA,UAAU,MAAc,OAAmC;GAEzD,mBAAmB,KAAK,YAAY,KAAK,MAAM,QAAQ,KAAK,IACvD,MAAM,MAAM,KACb,OAAO,KAAK;GAChB,OAAO;EACT;EACA,UAAU,MAAc;GACtB,OAAO,mBAAmB,KAAK,YAAY;EAC7C;EACA,aAAa;GACX,OAAO,EAAE,GAAG,mBAAmB;EACjC;EACA,IAAI,MAA+B;GAGjC,MAAM,aAAa,IAAI,QAAQ;GAC/B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,kBAAkB,GAC1D,WAAW,IAAI,KAAK,KAAK;GAI3B,QACE,IAAI,SAFJ,sBAAsB,OAAO,qBAAqB,MAGnC,OAAO,OAAO,SAAS,WAAW,OAAO,MACtD;IACE,QAAQ;IACR,SAAS;GACX,CACF,CACF;GACA,OAAO;EACT;EACA,aAAa;CACf;CAEA,eAAe,SAAS,eAAe;EAErC,MAAM,gBAAgB,QAAQ;EAE9B,IAAI,kBAAkB,aAQpB,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,eAAe,MAAM,IAAI,MAAM;EACrD,QAAQ;GACN,QAAQ,MACN,uDACA,aACF;EACF;EAMF,IAAI,OAAO,KAAK,kBAAkB,CAAC,CAAC,SAAS,GAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,kBAAkB,GAC1D,MAAM,IAAI,QAAQ,IAAI,KAAK,KAAK;EAIpC,QAAQ,MAAS;CACnB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"intlayerNitroHandler.mjs","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\n/**\n * The configuration as a plain JSON object.\n *\n * Bundlers alias `@intlayer/config/built` to the generated\n * `.intlayer/config/configuration.mjs`, which inlines the resolved\n * configuration at build time. Loading it through `@intlayer/config/node`\n * instead would transpile `intlayer.config.ts` with `esbuild` on every server\n * start, making the built server unable to boot without `node_modules` next to\n * it - which is exactly what a Nitro deployment output is.\n */\nimport * as builtConfiguration from '@intlayer/config/built';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport { formatProxyEnabledMessage } from '@intlayer/core/localization';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createProxyHandler } from './intlayerProxyHandler';\n\n/**\n * Minimal duck-type for h3 v2's H3Event.\n *\n * We intentionally avoid importing from 'h3' to keep this file runtime-agnostic —\n * Nitro bundles h3 internally and provides the populated event at runtime. Using a\n * structural type here means the file compiles without h3 in devDependencies and\n * works with any h3 v2-compatible runtime (Bun, Deno, Node).\n */\ntype H3EventLike = {\n /**\n * pathname + search — a computed getter on H3Event:\n * `return this.url.pathname + this.url.search`\n */\n readonly path: string;\n /**\n * Full URL object — a **plain property** (not a getter) on H3Event, safe to\n * replace for internal URL rewrites. After assignment, `event.path` will\n * automatically reflect the new pathname + search via the getter.\n */\n url: URL;\n /**\n * Web Fetch API Headers — always populated in h3 v2 regardless of preset\n * (Node, Bun, Deno). Use `.get(name)` instead of bracket-access.\n */\n readonly headers: Headers;\n /**\n * Lazy response object — created on first access; its `headers` carry outgoing\n * response headers (e.g. Set-Cookie) that h3 merges into the HTTP response.\n */\n readonly res: {\n readonly headers: Headers;\n };\n};\n\nconst intlayerConfig = builtConfiguration as unknown as IntlayerConfig;\nconst logger = getAppLogger(intlayerConfig);\n// A Nitro server is a production server, so the stored locale always drives\n// redirects here — hence the `false`. This runs once per server process; the\n// Vite preview server deliberately does not add a second proxy layer on top of\n// this one (see `configurePreviewServer` in `intlayerProxyPlugin`).\nlogger(formatProxyEnabledMessage(false), {\n level: 'info',\n});\n\nconst nodeMiddleware = createProxyHandler({ configuration: intlayerConfig });\n\n/**\n * Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).\n *\n * Unlike `fromNodeMiddleware` (h3 v1 API), this handler uses the Web Fetch API event\n * model exclusively and is therefore compatible with ALL Nitro presets — including Bun\n * and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with\n * \"undefined is not an object (evaluating 'event.node.req')\".\n *\n * It bridges h3 v2 events to the Node.js-style proxy middleware\n * via lightweight IncomingMessage / ServerResponse shims:\n *\n * - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends\n * the correct HTTP response to the browser.\n * - **Rewrite** (`next()` + modified `req.url`): replaces `event.url` with the rewritten\n * URL so `event.path` (a getter) returns the new pathname for downstream handlers and\n * the Nitro router.\n * - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to\n * the next handler / route.\n */\nexport default async (event: H3EventLike): Promise<Response | void> =>\n new Promise<Response | void>((resolve) => {\n const initialPath = event.path;\n\n /**\n * Minimal IncomingMessage shim.\n *\n * Only the fields actually read by the proxy middleware are populated:\n * - url : the current pathname + search, modified for rewrites\n * - headers.cookie : locale cookie detection\n * - headers.host : domain-based locale routing\n * - headers.accept-language : browser Accept-Language fallback\n * - headers.x-forwarded-* : forwarded host/proto for reverse-proxy setups\n * - headers.x-nitro-prerender : marks a request issued by Nitro's\n * prerenderer, which keeps the generated page URL-driven\n *\n * headers must be a mutable plain object because setLocaleInStorageServer\n * writes Set-Cookie back via req.headers[name] = value.\n */\n const fakeReq = {\n url: initialPath,\n method: 'GET',\n headers: {\n cookie: event.headers.get('cookie') ?? '',\n host: event.headers.get('host') ?? '',\n 'accept-language': event.headers.get('accept-language') ?? '',\n 'x-forwarded-host': event.headers.get('x-forwarded-host') ?? '',\n 'x-forwarded-proto': event.headers.get('x-forwarded-proto') ?? '',\n 'x-nitro-prerender': event.headers.get('x-nitro-prerender') ?? '',\n } as Record<string, string>,\n } as unknown as IncomingMessage;\n\n let responseStatusCode = 200;\n const accumulatedHeaders: Record<string, string> = {};\n\n /**\n * Minimal ServerResponse shim.\n *\n * Implements only the methods that the proxy middleware invokes:\n * writeHead() — status + Location header for 301 redirects\n * setHeader() — Set-Cookie written by setLocaleInStorageServer\n * getHeader() — defensive read-back (not strictly required but safe)\n * end() — finalises the response; for redirects this returns a\n * Web API Response object that Nitro sends to the client\n */\n const fakeRes = {\n writeHead(\n statusCode: number,\n headersArg?: Record<string, string | string[] | number> | string\n ) {\n // Capture the status code and any headers supplied alongside writeHead.\n responseStatusCode = statusCode;\n if (headersArg && typeof headersArg === 'object') {\n for (const [key, value] of Object.entries(headersArg)) {\n accumulatedHeaders[key.toLowerCase()] = Array.isArray(value)\n ? (value[0] ?? '')\n : String(value);\n }\n }\n return fakeRes;\n },\n setHeader(name: string, value: string | number | string[]) {\n // Capture Set-Cookie and other outgoing headers.\n accumulatedHeaders[name.toLowerCase()] = Array.isArray(value)\n ? (value[0] ?? '')\n : String(value);\n return fakeRes;\n },\n getHeader(name: string) {\n return accumulatedHeaders[name.toLowerCase()];\n },\n getHeaders() {\n return { ...accumulatedHeaders };\n },\n end(body?: string | Buffer | null) {\n // Build a Web API Response from accumulated status + headers + body.\n // For 3xx redirects the body is intentionally null.\n const webHeaders = new Headers();\n for (const [key, value] of Object.entries(accumulatedHeaders)) {\n webHeaders.set(key, value);\n }\n const isRedirect =\n responseStatusCode >= 300 && responseStatusCode < 400;\n resolve(\n new Response(\n isRedirect ? null : typeof body === 'string' ? body : null,\n {\n status: responseStatusCode,\n headers: webHeaders,\n }\n )\n );\n return fakeRes;\n },\n headersSent: false,\n } as unknown as ServerResponse<IncomingMessage>;\n\n nodeMiddleware(fakeReq, fakeRes, () => {\n // Middleware called next() — either a URL rewrite or a true pass-through.\n const rewrittenPath = fakeReq.url as string;\n\n if (rewrittenPath !== initialPath) {\n // The middleware rewrote the URL (e.g. /about → /en/about for locale prefix).\n // Replace event.url so that event.path (the getter: url.pathname + url.search)\n // returns the new path and the Nitro router matches the correct route.\n //\n // event.url is a plain property on h3 v2's H3Event (not a getter), so direct\n // assignment is safe. We use event.url.origin as the base so relative paths\n // resolve correctly; for path-only requests origin defaults to http://localhost.\n try {\n event.url = new URL(rewrittenPath, event.url.origin);\n } catch {\n console.error(\n '[intlayer-proxy] URL rewrite failed — invalid path:',\n rewrittenPath\n );\n }\n }\n\n // Forward any Set-Cookie or custom headers set by setLocaleInStorageServer to\n // the h3 v2 response object so they are included in the outgoing HTTP response.\n // Accessing event.res lazily creates the H3EventResponse (no cost if empty).\n if (Object.keys(accumulatedHeaders).length > 0) {\n for (const [key, value] of Object.entries(accumulatedHeaders)) {\n event.res.headers.set(key, value);\n }\n }\n\n resolve(undefined);\n });\n });\n"],"mappings":";;;;;;;;;;;;;;;;AAmDA,MAAM,iBAAiB;AACR,aAAa,cAKvB,CAAC,CAAC,0BAA0B,KAAK,GAAG,EACvC,OAAO,OACT,CAAC;AAED,MAAM,iBAAiB,mBAAmB,EAAE,eAAe,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;AAqB3E,mCAAe,OAAO,UACpB,IAAI,SAA0B,YAAY;CACxC,MAAM,cAAc,MAAM;;;;;;;;;;;;;;;;CAiB1B,MAAM,UAAU;EACd,KAAK;EACL,QAAQ;EACR,SAAS;GACP,QAAQ,MAAM,QAAQ,IAAI,QAAQ,KAAK;GACvC,MAAM,MAAM,QAAQ,IAAI,MAAM,KAAK;GACnC,mBAAmB,MAAM,QAAQ,IAAI,iBAAiB,KAAK;GAC3D,oBAAoB,MAAM,QAAQ,IAAI,kBAAkB,KAAK;GAC7D,qBAAqB,MAAM,QAAQ,IAAI,mBAAmB,KAAK;GAC/D,qBAAqB,MAAM,QAAQ,IAAI,mBAAmB,KAAK;EACjE;CACF;CAEA,IAAI,qBAAqB;CACzB,MAAM,qBAA6C,CAAC;;;;;;;;;;;CAYpD,MAAM,UAAU;EACd,UACE,YACA,YACA;GAEA,qBAAqB;GACrB,IAAI,cAAc,OAAO,eAAe,UACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,GAClD,mBAAmB,IAAI,YAAY,KAAK,MAAM,QAAQ,KAAK,IACtD,MAAM,MAAM,KACb,OAAO,KAAK;GAGpB,OAAO;EACT;EACA,UAAU,MAAc,OAAmC;GAEzD,mBAAmB,KAAK,YAAY,KAAK,MAAM,QAAQ,KAAK,IACvD,MAAM,MAAM,KACb,OAAO,KAAK;GAChB,OAAO;EACT;EACA,UAAU,MAAc;GACtB,OAAO,mBAAmB,KAAK,YAAY;EAC7C;EACA,aAAa;GACX,OAAO,EAAE,GAAG,mBAAmB;EACjC;EACA,IAAI,MAA+B;GAGjC,MAAM,aAAa,IAAI,QAAQ;GAC/B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,kBAAkB,GAC1D,WAAW,IAAI,KAAK,KAAK;GAI3B,QACE,IAAI,SAFJ,sBAAsB,OAAO,qBAAqB,MAGnC,OAAO,OAAO,SAAS,WAAW,OAAO,MACtD;IACE,QAAQ;IACR,SAAS;GACX,CACF,CACF;GACA,OAAO;EACT;EACA,aAAa;CACf;CAEA,eAAe,SAAS,eAAe;EAErC,MAAM,gBAAgB,QAAQ;EAE9B,IAAI,kBAAkB,aAQpB,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,eAAe,MAAM,IAAI,MAAM;EACrD,QAAQ;GACN,QAAQ,MACN,uDACA,aACF;EACF;EAMF,IAAI,OAAO,KAAK,kBAAkB,CAAC,CAAC,SAAS,GAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,kBAAkB,GAC1D,MAAM,IAAI,QAAQ,IAAI,KAAK,KAAK;EAIpC,QAAQ,MAAS;CACnB,CAAC;AACH,CAAC"}
@@ -60,7 +60,8 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
60
60
  const isBuildCommand = env.command === "build";
61
61
  return optimize === void 0 && isBuildCommand || optimize === true;
62
62
  };
63
- const isAnalysisEnabled = (_config, env) => !editorEnabled && (!!purge || !!minify) && isBuildOptimizeEnabled(_config, env);
63
+ const isFieldRenameEnabled = !editorEnabled && !!minify;
64
+ const isAnalysisEnabled = (_config, env) => (!!purge || !!minify) && isBuildOptimizeEnabled(_config, env);
64
65
  let partiallyMinifiedDictionariesCount = 0;
65
66
  return [
66
67
  intlayerVueAsyncPlugin(intlayerConfig, transformableFilesList),
@@ -186,7 +187,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
186
187
  `is assigned to a plain variable in:`,
187
188
  ...sourceFilePaths.map((filePath) => `\n - ${formatPath(filePath)}`)
188
189
  ], { level: "warn" });
189
- if (minify) {
190
+ if (isFieldRenameEnabled) {
190
191
  for (const [dictionaryKey, fieldUsage] of pruneContext.dictionaryKeyToFieldUsageMap) {
191
192
  if (fieldUsage === "all") continue;
192
193
  if (dictionaryKeyToImportModeMap[dictionaryKey] === "fetch") continue;
@@ -259,8 +260,8 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
259
260
  colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),
260
261
  colorize(")", ANSIColors.GREY_DARK)
261
262
  ]);
262
- if (!editorEnabled && minify) logger(`Dictionary minification ${colorize("enabled", ANSIColors.GREEN)}`);
263
- if (!editorEnabled && purge) logger(`Dictionary purge unused keys ${colorize("enabled", ANSIColors.GREEN)}`);
263
+ if (minify) logger(`Dictionary minification ${colorize("enabled", ANSIColors.GREEN)}`);
264
+ if (purge) logger(`Dictionary purge unused keys ${colorize("enabled", ANSIColors.GREEN)}`);
264
265
  }, { cacheTimeoutMs: 1e4 });
265
266
  return true;
266
267
  },
@@ -272,7 +273,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext, compatCallers) =>
272
273
  const isUsingIntlayer = (compatCallers?.length ? buildUsageCheckRegex(compatCallers.map((caller) => caller.callerName)) : INTLAYER_USAGE_REGEX).test(sourceCode);
273
274
  if (!isUsingIntlayer && !isDictionaryEntryFile) return null;
274
275
  let codeToOptimize = sourceCode;
275
- if (pruneContext && isUsingIntlayer) {
276
+ if (isFieldRenameEnabled && pruneContext && isUsingIntlayer) {
276
277
  const renamedCode = await renameFieldsInSourceFile(sourceFilePath, sourceCode, pruneContext);
277
278
  if (renamedCode) codeToOptimize = renamedCode;
278
279
  }