next-intlayer 9.2.0 → 9.3.1

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 (75) hide show
  1. package/README.md +24 -24
  2. package/dist/cjs/_virtual/_rolldown/runtime.cjs +1 -2
  3. package/dist/cjs/client/format/index.cjs +0 -1
  4. package/dist/cjs/client/useLocale.cjs +29 -28
  5. package/dist/cjs/client/useLocale.cjs.map +1 -1
  6. package/dist/cjs/client/useLocalePageRouter.cjs +11 -10
  7. package/dist/cjs/client/useLocalePageRouter.cjs.map +1 -1
  8. package/dist/cjs/client/usePathname.cjs.map +1 -1
  9. package/dist/cjs/proxy/intlayerProxy.cjs +4 -2
  10. package/dist/cjs/proxy/intlayerProxy.cjs.map +1 -1
  11. package/dist/cjs/proxy/localeDetector.cjs.map +1 -1
  12. package/dist/cjs/server/format/index.cjs +0 -1
  13. package/dist/cjs/server/getLocale.cjs.map +1 -1
  14. package/dist/cjs/server/prepareSwcOptimization.cjs +190 -0
  15. package/dist/cjs/server/prepareSwcOptimization.cjs.map +1 -0
  16. package/dist/cjs/server/swcPluginCompatibility.cjs +57 -0
  17. package/dist/cjs/server/swcPluginCompatibility.cjs.map +1 -0
  18. package/dist/cjs/server/useDictionary.cjs +2 -1
  19. package/dist/cjs/server/useDictionary.cjs.map +1 -1
  20. package/dist/cjs/server/useDictionaryAsync.cjs +2 -1
  21. package/dist/cjs/server/useDictionaryAsync.cjs.map +1 -1
  22. package/dist/cjs/server/useDictionaryDynamic.cjs +2 -1
  23. package/dist/cjs/server/useDictionaryDynamic.cjs.map +1 -1
  24. package/dist/cjs/server/useIntlayer.cjs +2 -1
  25. package/dist/cjs/server/useIntlayer.cjs.map +1 -1
  26. package/dist/cjs/server/withIntlayer.cjs +81 -32
  27. package/dist/cjs/server/withIntlayer.cjs.map +1 -1
  28. package/dist/esm/client/useLocale.mjs +29 -28
  29. package/dist/esm/client/useLocale.mjs.map +1 -1
  30. package/dist/esm/client/useLocalePageRouter.mjs +11 -10
  31. package/dist/esm/client/useLocalePageRouter.mjs.map +1 -1
  32. package/dist/esm/proxy/intlayerProxy.mjs +4 -2
  33. package/dist/esm/proxy/intlayerProxy.mjs.map +1 -1
  34. package/dist/esm/server/prepareSwcOptimization.mjs +185 -0
  35. package/dist/esm/server/prepareSwcOptimization.mjs.map +1 -0
  36. package/dist/esm/server/swcPluginCompatibility.mjs +55 -0
  37. package/dist/esm/server/swcPluginCompatibility.mjs.map +1 -0
  38. package/dist/esm/server/useDictionary.mjs +2 -1
  39. package/dist/esm/server/useDictionary.mjs.map +1 -1
  40. package/dist/esm/server/useDictionaryAsync.mjs +2 -1
  41. package/dist/esm/server/useDictionaryAsync.mjs.map +1 -1
  42. package/dist/esm/server/useDictionaryDynamic.mjs +2 -1
  43. package/dist/esm/server/useDictionaryDynamic.mjs.map +1 -1
  44. package/dist/esm/server/useIntlayer.mjs +2 -1
  45. package/dist/esm/server/useIntlayer.mjs.map +1 -1
  46. package/dist/esm/server/withIntlayer.mjs +82 -35
  47. package/dist/esm/server/withIntlayer.mjs.map +1 -1
  48. package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-extract.d.ts +5 -0
  49. package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-field-rename.d.ts +21 -0
  50. package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-field-rename.d.ts.map +1 -0
  51. package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-minify.d.ts +2 -0
  52. package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-optimize.d.ts +3 -0
  53. package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-purge.d.ts +5 -0
  54. package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts +4 -0
  55. package/dist/types/@intlayer/babel/dist/types/extractContent/contentWriter.d.ts +2 -0
  56. package/dist/types/@intlayer/babel/dist/types/extractContent/extractContent.d.ts +3 -0
  57. package/dist/types/@intlayer/babel/dist/types/extractContent/index.d.ts +4 -0
  58. package/dist/types/@intlayer/babel/dist/types/extractContent/utils/extractDictionaryInfo.d.ts +3 -0
  59. package/dist/types/@intlayer/babel/dist/types/extractContent/utils/getComponentName.d.ts +1 -0
  60. package/dist/types/@intlayer/babel/dist/types/extractContent/utils/getExistingIntlayerInfo.d.ts +2 -0
  61. package/dist/types/@intlayer/babel/dist/types/extractContent/utils/index.d.ts +4 -0
  62. package/dist/types/@intlayer/babel/dist/types/extractContent/utils/resolveDictionaryKey.d.ts +1 -0
  63. package/dist/types/@intlayer/babel/dist/types/getExtractPluginOptions.d.ts +2 -0
  64. package/dist/types/@intlayer/babel/dist/types/getOptimizePluginOptions.d.ts +3 -0
  65. package/dist/types/@intlayer/babel/dist/types/getPurgePluginOptions.d.ts +4 -0
  66. package/dist/types/@intlayer/babel/dist/types/index.d.ts +16 -0
  67. package/dist/types/@intlayer/babel/dist/types/transformers.d.ts +3 -0
  68. package/dist/types/server/prepareSwcOptimization.d.ts +68 -0
  69. package/dist/types/server/prepareSwcOptimization.d.ts.map +1 -0
  70. package/dist/types/server/swcPluginCompatibility.d.ts +52 -0
  71. package/dist/types/server/swcPluginCompatibility.d.ts.map +1 -0
  72. package/dist/types/server/useDictionaryDynamic.d.ts +1 -1
  73. package/dist/types/server/withIntlayer.d.ts +12 -1
  74. package/dist/types/server/withIntlayer.d.ts.map +1 -1
  75. package/package.json +9 -9
@@ -0,0 +1,185 @@
1
+ import { dirname, join } from "node:path";
2
+ import * as ANSIColors from "@intlayer/config/colors";
3
+ import { colorize, getAppLogger } from "@intlayer/config/logger";
4
+ import { getProjectRequire } from "@intlayer/config/utils";
5
+ import { runOnce } from "@intlayer/engine/utils";
6
+ import { readFileSync, rmSync } from "node:fs";
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+
9
+ //#region src/server/prepareSwcOptimization.ts
10
+ /**
11
+ * Opt-in to the SWC plugin's per-file tracing.
12
+ *
13
+ * The plugin transforms one file at a time with no cross-file state, so all it
14
+ * can report is a line per file — hundreds of them on a real project, and with
15
+ * no counterpart in the Vite build output. The purge and minify summaries that
16
+ * `log.mode` does control are emitted by the pipeline itself, in Node; this
17
+ * variable is purely a debugging aid on top.
18
+ */
19
+ const SWC_LOG_LEVEL_ENV_VAR = "INTLAYER_SWC_LOG_LEVEL";
20
+ /** Sentinel coordinating the pipeline across the processes of one build. */
21
+ const PURGE_SENTINEL_FILE_NAME = "intlayer-swc-purge.lock";
22
+ /** Where the serialised field-rename tables are cached between processes. */
23
+ const FIELD_RENAME_MAP_FILE_NAME = "swc-field-rename-map.json";
24
+ /**
25
+ * Next.js evaluates `next.config.*` in the main build process and again in each
26
+ * Turbopack/webpack worker. The dictionary rewrite must happen exactly once —
27
+ * a second run would derive the short aliases from the already-renamed JSON —
28
+ * so the sentinel window has to cover a whole build startup.
29
+ */
30
+ const PURGE_CACHE_TIMEOUT_MS = 3e4;
31
+ /** Absolute path of a file in the intlayer cache directory. */
32
+ const getCacheFilePath = (intlayerConfig, fileName) => join(intlayerConfig.system.baseDir, ".intlayer", "cache", fileName);
33
+ /**
34
+ * Resolves the verbosity the `@intlayer/swc` plugin should report at.
35
+ *
36
+ * Off unless {@link SWC_LOG_LEVEL_ENV_VAR} asks for tracing: `log.mode` governs
37
+ * the purge and minify reporting, which the pipeline emits in Node so the
38
+ * Next.js build reads the same as the Vite one.
39
+ */
40
+ const resolveSwcLogLevel = (intlayerConfig) => {
41
+ if (intlayerConfig.log.mode === "disabled") return "off";
42
+ const requestedLogLevel = process.env[SWC_LOG_LEVEL_ENV_VAR];
43
+ return requestedLogLevel === "info" || requestedLogLevel === "debug" ? requestedLogLevel : "off";
44
+ };
45
+ /**
46
+ * Whether the purge / minify pipeline should run for this build.
47
+ *
48
+ * Compat-adapter callers disable it: their descriptors reach `withIntlayer`
49
+ * only in the SWC plugin's lossy wire format, so the usage analyser would not
50
+ * recognise `useTranslation(...)` & co. and would purge fields those call sites
51
+ * still read.
52
+ */
53
+ const getIsPurgePipelineEnabled = (intlayerConfig, swcExtraCallers, isSwcPluginUsable) => {
54
+ const { purge, minify, optimize } = intlayerConfig.build;
55
+ if (optimize === false) return false;
56
+ if (!purge && !minify) return false;
57
+ if (!isSwcPluginUsable) return false;
58
+ if (swcExtraCallers && swcExtraCallers.length > 0) {
59
+ getAppLogger(intlayerConfig)([
60
+ "Dictionary purge and minification are",
61
+ colorize("disabled", ANSIColors.GREY_DARK),
62
+ "because compat-adapter callers are configured — their call sites are",
63
+ "not visible to the usage analyser."
64
+ ], { level: "warn" });
65
+ return false;
66
+ }
67
+ return true;
68
+ };
69
+ /**
70
+ * Exports this module needs from `@intlayer/babel`. Checked one by one so a
71
+ * version predating the purge pipeline is rejected like a missing package,
72
+ * instead of failing later with `runIntlayerPurgePipeline is not a function`.
73
+ */
74
+ const REQUIRED_BABEL_EXPORTS = [
75
+ "getPurgePluginOptions",
76
+ "runIntlayerPurgePipeline",
77
+ "serializeFieldRenameMap"
78
+ ];
79
+ /**
80
+ * Lazily loads `@intlayer/babel`, or `null` when it is not installed or too old
81
+ * to expose the purge pipeline.
82
+ */
83
+ const loadIntlayerBabel = (intlayerConfig) => {
84
+ try {
85
+ const intlayerBabel = (intlayerConfig.build?.require ?? getProjectRequire())("@intlayer/babel");
86
+ if (!REQUIRED_BABEL_EXPORTS.every((exportName) => typeof intlayerBabel?.[exportName] === "function")) return null;
87
+ return intlayerBabel;
88
+ } catch {
89
+ return null;
90
+ }
91
+ };
92
+ /**
93
+ * Whether the purge / minify pipeline can run at all, i.e. whether a usable
94
+ * `@intlayer/babel` is resolvable from the project.
95
+ *
96
+ * Used by `withIntlayer` to keep the build report honest: announcing
97
+ * `Dictionary minification enabled` while the pipeline cannot load would describe a
98
+ * pass that never happens.
99
+ */
100
+ const getIsPurgePipelineAvailable = (intlayerConfig) => loadIntlayerBabel(intlayerConfig) !== null;
101
+ /**
102
+ * Runs the purge / minify pipeline and writes the resulting field-rename
103
+ * tables to the cache file so every process of the build reads the same ones.
104
+ */
105
+ const runPurgePipelineOnce = async (intlayerConfig, configOptions, dictionaries, fieldRenameMapFilePath) => {
106
+ const appLogger = getAppLogger(intlayerConfig);
107
+ const intlayerBabel = loadIntlayerBabel(intlayerConfig);
108
+ if (!intlayerBabel) {
109
+ appLogger([
110
+ "Dictionary purge and minification are",
111
+ colorize("disabled", ANSIColors.GREY_DARK),
112
+ "because",
113
+ colorize("@intlayer/babel", ANSIColors.GREY_LIGHT),
114
+ "is missing or too old to expose the purge pipeline — install it in",
115
+ "this project to enable them."
116
+ ], { level: "warn" });
117
+ return;
118
+ }
119
+ const { getPurgePluginOptions, runIntlayerPurgePipeline, serializeFieldRenameMap } = intlayerBabel;
120
+ let fieldRenameMap;
121
+ try {
122
+ fieldRenameMap = serializeFieldRenameMap(runIntlayerPurgePipeline(getPurgePluginOptions({
123
+ configOptions,
124
+ dictionaries
125
+ })));
126
+ } catch (pipelineError) {
127
+ appLogger([
128
+ "Dictionary purge and minification",
129
+ colorize("failed", ANSIColors.RED),
130
+ "— the compiled dictionaries are left untouched.",
131
+ pipelineError instanceof Error ? `(${pipelineError.message})` : String(pipelineError)
132
+ ], { level: "error" });
133
+ return;
134
+ }
135
+ await mkdir(dirname(fieldRenameMapFilePath), { recursive: true });
136
+ await writeFile(fieldRenameMapFilePath, JSON.stringify(fieldRenameMap), "utf-8");
137
+ };
138
+ /** Reads the cached field-rename tables, or `{}` when none were written. */
139
+ const readFieldRenameMapFile = (fieldRenameMapFilePath) => {
140
+ try {
141
+ return JSON.parse(readFileSync(fieldRenameMapFilePath, "utf-8"));
142
+ } catch {
143
+ return {};
144
+ }
145
+ };
146
+ /** Removes the cached field-rename tables, ignoring a missing file. */
147
+ const removeFieldRenameMapFile = (fieldRenameMapFilePath) => {
148
+ try {
149
+ rmSync(fieldRenameMapFilePath, { force: true });
150
+ } catch {}
151
+ };
152
+ /**
153
+ * Prepares the parts of the build optimisation the `@intlayer/swc` plugin
154
+ * cannot do itself, and returns the field-rename tables it needs.
155
+ *
156
+ * Removing unused content fields (`build.purge`) and assigning short field
157
+ * aliases (`build.minify`) require reading every component source file and
158
+ * rewriting the compiled dictionary JSON — file I/O and cross-file state that a
159
+ * per-file Wasm transform has no access to. That analysis therefore runs here,
160
+ * in Node, through `@intlayer/babel`; the plugin only receives the resulting
161
+ * tables and rewrites the matching source accesses
162
+ * (`content.title` → `content.a`).
163
+ *
164
+ * The work is coordinated by a sentinel file: Next.js loads `next.config.*` in
165
+ * several processes per build, and running the minify pass twice would derive
166
+ * the aliases from the already-renamed dictionaries. Processes that lose the
167
+ * race read the tables the winner cached.
168
+ *
169
+ * @returns The field-rename tables, keyed by dictionary key. Empty when the
170
+ * pipeline is disabled, unavailable, or renamed nothing.
171
+ */
172
+ const prepareSwcOptimization = async (intlayerConfig, params) => {
173
+ const { configOptions, dictionaries, swcExtraCallers, isSwcPluginUsable } = params;
174
+ const fieldRenameMapFilePath = getCacheFilePath(intlayerConfig, FIELD_RENAME_MAP_FILE_NAME);
175
+ if (!getIsPurgePipelineEnabled(intlayerConfig, swcExtraCallers, isSwcPluginUsable)) {
176
+ removeFieldRenameMapFile(fieldRenameMapFilePath);
177
+ return {};
178
+ }
179
+ await runOnce(getCacheFilePath(intlayerConfig, PURGE_SENTINEL_FILE_NAME), () => runPurgePipelineOnce(intlayerConfig, configOptions, dictionaries, fieldRenameMapFilePath), { cacheTimeoutMs: PURGE_CACHE_TIMEOUT_MS });
180
+ return readFieldRenameMapFile(fieldRenameMapFilePath);
181
+ };
182
+
183
+ //#endregion
184
+ export { getIsPurgePipelineAvailable, prepareSwcOptimization, resolveSwcLogLevel };
185
+ //# sourceMappingURL=prepareSwcOptimization.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepareSwcOptimization.mjs","names":[],"sources":["../../../src/server/prepareSwcOptimization.ts"],"sourcesContent":["import { readFileSync, rmSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport type {\n getPurgePluginOptions as GetPurgePluginOptions,\n runIntlayerPurgePipeline as RunIntlayerPurgePipeline,\n SerializedFieldRenameMap,\n serializeFieldRenameMap as SerializeFieldRenameMap,\n} from '@intlayer/babel';\nimport type { SwcExtraCallerConfig } from '@intlayer/config/callers';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport type { GetConfigurationOptions } from '@intlayer/config/node';\nimport { getProjectRequire } from '@intlayer/config/utils';\nimport { runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\n\n/**\n * Field-rename tables keyed by dictionary key, forwarded to the\n * `@intlayer/swc` plugin's `fieldRenameMap` option.\n */\nexport type FieldRenameMapByDictionaryKey = Record<\n string,\n SerializedFieldRenameMap\n>;\n\n/** Verbosity forwarded to the `@intlayer/swc` plugin's `logLevel` option. */\nexport type SwcLogLevel = 'off' | 'info' | 'debug';\n\n/**\n * Subset of `@intlayer/babel` this module needs. The package is only an\n * optional runtime dependency, so it is required lazily and never imported at\n * module scope.\n */\ntype IntlayerBabelModule = {\n getPurgePluginOptions: typeof GetPurgePluginOptions;\n runIntlayerPurgePipeline: typeof RunIntlayerPurgePipeline;\n serializeFieldRenameMap: typeof SerializeFieldRenameMap;\n};\n\n/**\n * Opt-in to the SWC plugin's per-file tracing.\n *\n * The plugin transforms one file at a time with no cross-file state, so all it\n * can report is a line per file — hundreds of them on a real project, and with\n * no counterpart in the Vite build output. The purge and minify summaries that\n * `log.mode` does control are emitted by the pipeline itself, in Node; this\n * variable is purely a debugging aid on top.\n */\nconst SWC_LOG_LEVEL_ENV_VAR = 'INTLAYER_SWC_LOG_LEVEL';\n\n/** Sentinel coordinating the pipeline across the processes of one build. */\nconst PURGE_SENTINEL_FILE_NAME = 'intlayer-swc-purge.lock';\n\n/** Where the serialised field-rename tables are cached between processes. */\nconst FIELD_RENAME_MAP_FILE_NAME = 'swc-field-rename-map.json';\n\n/**\n * Next.js evaluates `next.config.*` in the main build process and again in each\n * Turbopack/webpack worker. The dictionary rewrite must happen exactly once —\n * a second run would derive the short aliases from the already-renamed JSON —\n * so the sentinel window has to cover a whole build startup.\n */\nconst PURGE_CACHE_TIMEOUT_MS = 30 * 1000;\n\n/** Absolute path of a file in the intlayer cache directory. */\nconst getCacheFilePath = (\n intlayerConfig: IntlayerConfig,\n fileName: string\n): string =>\n join(intlayerConfig.system.baseDir, '.intlayer', 'cache', fileName);\n\n/**\n * Resolves the verbosity the `@intlayer/swc` plugin should report at.\n *\n * Off unless {@link SWC_LOG_LEVEL_ENV_VAR} asks for tracing: `log.mode` governs\n * the purge and minify reporting, which the pipeline emits in Node so the\n * Next.js build reads the same as the Vite one.\n */\nexport const resolveSwcLogLevel = (\n intlayerConfig: IntlayerConfig\n): SwcLogLevel => {\n if (intlayerConfig.log.mode === 'disabled') return 'off';\n\n const requestedLogLevel = process.env[SWC_LOG_LEVEL_ENV_VAR];\n\n return requestedLogLevel === 'info' || requestedLogLevel === 'debug'\n ? requestedLogLevel\n : 'off';\n};\n\n/**\n * Whether the purge / minify pipeline should run for this build.\n *\n * Compat-adapter callers disable it: their descriptors reach `withIntlayer`\n * only in the SWC plugin's lossy wire format, so the usage analyser would not\n * recognise `useTranslation(...)` & co. and would purge fields those call sites\n * still read.\n */\nconst getIsPurgePipelineEnabled = (\n intlayerConfig: IntlayerConfig,\n swcExtraCallers: SwcExtraCallerConfig[] | undefined,\n isSwcPluginUsable: boolean\n): boolean => {\n const { purge, minify, optimize } = intlayerConfig.build;\n\n if (optimize === false) return false;\n if (!purge && !minify) return false;\n\n // The pipeline rewrites the compiled dictionaries in place — dropping unused\n // fields and renaming the rest to short aliases. Only the SWC plugin rewrites\n // the matching source accesses, so running this without it would ship\n // dictionaries whose keys no longer match the code reading them.\n if (!isSwcPluginUsable) return false;\n\n // `editor.enabled` is deliberately not checked here: the pipeline stands down\n // on its own and explains why, the same way the Vite build does.\n\n if (swcExtraCallers && swcExtraCallers.length > 0) {\n getAppLogger(intlayerConfig)(\n [\n 'Dictionary purge and minification are',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because compat-adapter callers are configured — their call sites are',\n 'not visible to the usage analyser.',\n ],\n { level: 'warn' }\n );\n return false;\n }\n\n return true;\n};\n\n/**\n * Exports this module needs from `@intlayer/babel`. Checked one by one so a\n * version predating the purge pipeline is rejected like a missing package,\n * instead of failing later with `runIntlayerPurgePipeline is not a function`.\n */\nconst REQUIRED_BABEL_EXPORTS = [\n 'getPurgePluginOptions',\n 'runIntlayerPurgePipeline',\n 'serializeFieldRenameMap',\n] as const satisfies readonly (keyof IntlayerBabelModule)[];\n\n/**\n * Lazily loads `@intlayer/babel`, or `null` when it is not installed or too old\n * to expose the purge pipeline.\n */\nconst loadIntlayerBabel = (\n intlayerConfig: IntlayerConfig\n): IntlayerBabelModule | null => {\n try {\n const requireFunction =\n intlayerConfig.build?.require ?? getProjectRequire();\n\n const intlayerBabel = requireFunction(\n '@intlayer/babel'\n ) as Partial<IntlayerBabelModule>;\n\n const hasEveryRequiredExport = REQUIRED_BABEL_EXPORTS.every(\n (exportName) => typeof intlayerBabel?.[exportName] === 'function'\n );\n\n if (!hasEveryRequiredExport) return null;\n\n return intlayerBabel as IntlayerBabelModule;\n } catch {\n return null;\n }\n};\n\n/**\n * Whether the purge / minify pipeline can run at all, i.e. whether a usable\n * `@intlayer/babel` is resolvable from the project.\n *\n * Used by `withIntlayer` to keep the build report honest: announcing\n * `Dictionary minification enabled` while the pipeline cannot load would describe a\n * pass that never happens.\n */\nexport const getIsPurgePipelineAvailable = (\n intlayerConfig: IntlayerConfig\n): boolean => loadIntlayerBabel(intlayerConfig) !== null;\n\n/**\n * Runs the purge / minify pipeline and writes the resulting field-rename\n * tables to the cache file so every process of the build reads the same ones.\n */\nconst runPurgePipelineOnce = async (\n intlayerConfig: IntlayerConfig,\n configOptions: GetConfigurationOptions | undefined,\n dictionaries: Dictionary[] | undefined,\n fieldRenameMapFilePath: string\n): Promise<void> => {\n const appLogger = getAppLogger(intlayerConfig);\n\n const intlayerBabel = loadIntlayerBabel(intlayerConfig);\n\n if (!intlayerBabel) {\n // Not verbose: the build asked for purge / minify and is silently getting\n // neither, which is exactly the kind of thing a default build must report.\n appLogger(\n [\n 'Dictionary purge and minification are',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('@intlayer/babel', ANSIColors.GREY_LIGHT),\n 'is missing or too old to expose the purge pipeline — install it in',\n 'this project to enable them.',\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const {\n getPurgePluginOptions,\n runIntlayerPurgePipeline,\n serializeFieldRenameMap,\n } = intlayerBabel;\n\n // `runOnce` discards whatever this callback throws, so a pipeline failure\n // would otherwise leave the build with no dictionaries rewritten and no\n // explanation of why.\n let fieldRenameMap: FieldRenameMapByDictionaryKey;\n\n try {\n // The pipeline reports what it purged and minified through the intlayer\n // logger, matching the Vite build output.\n const pruneContext = runIntlayerPurgePipeline(\n getPurgePluginOptions({ configOptions, dictionaries })\n );\n\n fieldRenameMap = serializeFieldRenameMap(pruneContext);\n } catch (pipelineError) {\n appLogger(\n [\n 'Dictionary purge and minification',\n colorize('failed', ANSIColors.RED),\n '— the compiled dictionaries are left untouched.',\n pipelineError instanceof Error\n ? `(${pipelineError.message})`\n : String(pipelineError),\n ],\n { level: 'error' }\n );\n return;\n }\n\n await mkdir(dirname(fieldRenameMapFilePath), { recursive: true });\n await writeFile(\n fieldRenameMapFilePath,\n JSON.stringify(fieldRenameMap),\n 'utf-8'\n );\n};\n\n/** Reads the cached field-rename tables, or `{}` when none were written. */\nconst readFieldRenameMapFile = (\n fieldRenameMapFilePath: string\n): FieldRenameMapByDictionaryKey => {\n try {\n return JSON.parse(\n readFileSync(fieldRenameMapFilePath, 'utf-8')\n ) as FieldRenameMapByDictionaryKey;\n } catch {\n return {};\n }\n};\n\n/** Removes the cached field-rename tables, ignoring a missing file. */\nconst removeFieldRenameMapFile = (fieldRenameMapFilePath: string): void => {\n try {\n rmSync(fieldRenameMapFilePath, { force: true });\n } catch {\n // Nothing to clean up.\n }\n};\n\ntype PrepareSwcOptimizationParams = {\n /** Options forwarded to the intlayer configuration loader. */\n configOptions?: GetConfigurationOptions;\n /** Pre-loaded dictionaries, to avoid a second `getDictionaries` call. */\n dictionaries?: Dictionary[];\n /** Compat-adapter callers declared by the consuming plugin, if any. */\n swcExtraCallers?: SwcExtraCallerConfig[];\n /**\n * Whether the `@intlayer/swc` plugin is installed *and* the resolved Next.js\n * version can load it. False leaves the compiled dictionaries untouched.\n */\n isSwcPluginUsable: boolean;\n};\n\n/**\n * Prepares the parts of the build optimisation the `@intlayer/swc` plugin\n * cannot do itself, and returns the field-rename tables it needs.\n *\n * Removing unused content fields (`build.purge`) and assigning short field\n * aliases (`build.minify`) require reading every component source file and\n * rewriting the compiled dictionary JSON — file I/O and cross-file state that a\n * per-file Wasm transform has no access to. That analysis therefore runs here,\n * in Node, through `@intlayer/babel`; the plugin only receives the resulting\n * tables and rewrites the matching source accesses\n * (`content.title` → `content.a`).\n *\n * The work is coordinated by a sentinel file: Next.js loads `next.config.*` in\n * several processes per build, and running the minify pass twice would derive\n * the aliases from the already-renamed dictionaries. Processes that lose the\n * race read the tables the winner cached.\n *\n * @returns The field-rename tables, keyed by dictionary key. Empty when the\n * pipeline is disabled, unavailable, or renamed nothing.\n */\nexport const prepareSwcOptimization = async (\n intlayerConfig: IntlayerConfig,\n params: PrepareSwcOptimizationParams\n): Promise<FieldRenameMapByDictionaryKey> => {\n const { configOptions, dictionaries, swcExtraCallers, isSwcPluginUsable } =\n params;\n\n const fieldRenameMapFilePath = getCacheFilePath(\n intlayerConfig,\n FIELD_RENAME_MAP_FILE_NAME\n );\n\n // A stale file from an earlier build would rename source accesses that the\n // dictionaries no longer match.\n if (\n !getIsPurgePipelineEnabled(\n intlayerConfig,\n swcExtraCallers,\n isSwcPluginUsable\n )\n ) {\n removeFieldRenameMapFile(fieldRenameMapFilePath);\n return {};\n }\n\n await runOnce(\n getCacheFilePath(intlayerConfig, PURGE_SENTINEL_FILE_NAME),\n () =>\n runPurgePipelineOnce(\n intlayerConfig,\n configOptions,\n dictionaries,\n fieldRenameMapFilePath\n ),\n { cacheTimeoutMs: PURGE_CACHE_TIMEOUT_MS }\n );\n\n return readFieldRenameMapFile(fieldRenameMapFilePath);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkDA,MAAM,wBAAwB;;AAG9B,MAAM,2BAA2B;;AAGjC,MAAM,6BAA6B;;;;;;;AAQnC,MAAM,yBAAyB;;AAG/B,MAAM,oBACJ,gBACA,aAEA,KAAK,eAAe,OAAO,SAAS,aAAa,SAAS,QAAQ;;;;;;;;AASpE,MAAa,sBACX,mBACgB;CAChB,IAAI,eAAe,IAAI,SAAS,YAAY,OAAO;CAEnD,MAAM,oBAAoB,QAAQ,IAAI;CAEtC,OAAO,sBAAsB,UAAU,sBAAsB,UACzD,oBACA;AACN;;;;;;;;;AAUA,MAAM,6BACJ,gBACA,iBACA,sBACY;CACZ,MAAM,EAAE,OAAO,QAAQ,aAAa,eAAe;CAEnD,IAAI,aAAa,OAAO,OAAO;CAC/B,IAAI,CAAC,SAAS,CAAC,QAAQ,OAAO;CAM9B,IAAI,CAAC,mBAAmB,OAAO;CAK/B,IAAI,mBAAmB,gBAAgB,SAAS,GAAG;EACjD,aAAa,cAAc,CAAC,CAC1B;GACE;GACA,SAAS,YAAY,WAAW,SAAS;GACzC;GACA;EACF,GACA,EAAE,OAAO,OAAO,CAClB;EACA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAM,yBAAyB;CAC7B;CACA;CACA;AACF;;;;;AAMA,MAAM,qBACJ,mBAC+B;CAC/B,IAAI;EAIF,MAAM,iBAFJ,eAAe,OAAO,WAAW,kBAAkB,EAEhB,CACnC,iBACF;EAMA,IAAI,CAJ2B,uBAAuB,OACnD,eAAe,OAAO,gBAAgB,gBAAgB,UAG/B,GAAG,OAAO;EAEpC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;AAUA,MAAa,+BACX,mBACY,kBAAkB,cAAc,MAAM;;;;;AAMpD,MAAM,uBAAuB,OAC3B,gBACA,eACA,cACA,2BACkB;CAClB,MAAM,YAAY,aAAa,cAAc;CAE7C,MAAM,gBAAgB,kBAAkB,cAAc;CAEtD,IAAI,CAAC,eAAe;EAGlB,UACE;GACE;GACA,SAAS,YAAY,WAAW,SAAS;GACzC;GACA,SAAS,mBAAmB,WAAW,UAAU;GACjD;GACA;EACF,GACA,EAAE,OAAO,OAAO,CAClB;EACA;CACF;CAEA,MAAM,EACJ,uBACA,0BACA,4BACE;CAKJ,IAAI;CAEJ,IAAI;EAOF,iBAAiB,wBAJI,yBACnB,sBAAsB;GAAE;GAAe;EAAa,CAAC,CAGH,CAAC;CACvD,SAAS,eAAe;EACtB,UACE;GACE;GACA,SAAS,UAAU,WAAW,GAAG;GACjC;GACA,yBAAyB,QACrB,IAAI,cAAc,QAAQ,KAC1B,OAAO,aAAa;EAC1B,GACA,EAAE,OAAO,QAAQ,CACnB;EACA;CACF;CAEA,MAAM,MAAM,QAAQ,sBAAsB,GAAG,EAAE,WAAW,KAAK,CAAC;CAChE,MAAM,UACJ,wBACA,KAAK,UAAU,cAAc,GAC7B,OACF;AACF;;AAGA,MAAM,0BACJ,2BACkC;CAClC,IAAI;EACF,OAAO,KAAK,MACV,aAAa,wBAAwB,OAAO,CAC9C;CACF,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;AAGA,MAAM,4BAA4B,2BAAyC;CACzE,IAAI;EACF,OAAO,wBAAwB,EAAE,OAAO,KAAK,CAAC;CAChD,QAAQ,CAER;AACF;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,yBAAyB,OACpC,gBACA,WAC2C;CAC3C,MAAM,EAAE,eAAe,cAAc,iBAAiB,sBACpD;CAEF,MAAM,yBAAyB,iBAC7B,gBACA,0BACF;CAIA,IACE,CAAC,0BACC,gBACA,iBACA,iBACF,GACA;EACA,yBAAyB,sBAAsB;EAC/C,OAAO,CAAC;CACV;CAEA,MAAM,QACJ,iBAAiB,gBAAgB,wBAAwB,SAEvD,qBACE,gBACA,eACA,cACA,sBACF,GACF,EAAE,gBAAgB,uBAAuB,CAC3C;CAEA,OAAO,uBAAuB,sBAAsB;AACtD"}
@@ -0,0 +1,55 @@
1
+ import { compareVersions } from "@intlayer/config/utils";
2
+
3
+ //#region src/server/swcPluginCompatibility.ts
4
+ /**
5
+ * Oldest Next.js release whose bundled SWC can load the `@intlayer/swc` Wasm
6
+ * plugin.
7
+ *
8
+ * A Wasm plugin only loads in a host that speaks its `swc_ecma_ast` schema.
9
+ * Next.js 16.1.0 is the first release built on SWC's forward-compatible plugin
10
+ * ABI — the AST travels as self-describing CBOR instead of rkyv, so a plugin
11
+ * compiled against an older `swc_core` keeps loading in newer hosts. Every
12
+ * earlier release uses the rkyv ABI, which requires an exact schema match and
13
+ * rejects the plugin outright, failing the build with `failed to invoke
14
+ * plugin`.
15
+ *
16
+ * Verified by driving `transformSync` on the shipped `@next/swc` binaries with
17
+ * the plugin attached:
18
+ *
19
+ * | Next.js | swc_ecma_ast | plugin loads |
20
+ * | ------- | ------------ | ------------ |
21
+ * | 14.2.x | 0.112.7 | no |
22
+ * | 15.5.x | 14.0.0 | no |
23
+ * | 16.0.x | 16.0.0 | no |
24
+ * | 16.1.x | 19.0.0 | yes |
25
+ * | 16.2.x | 20.0.1 | yes |
26
+ * | 16.3.x | 25.0.0 | yes |
27
+ *
28
+ * Keep in sync with the `swc_core` pin in
29
+ * `packages/@intlayer/swc/Cargo.toml`: the plugin must stay on the
30
+ * `swc_ecma_ast` version this Next.js release ships, never a newer one, or the
31
+ * floor moves up with it.
32
+ */
33
+ const MINIMUM_SWC_PLUGIN_NEXT_VERSION = "16.1.0";
34
+ /**
35
+ * Whether `nextVersion` bundles an SWC able to load the `@intlayer/swc` Wasm
36
+ * plugin.
37
+ *
38
+ * Pre-releases of a supported version (`16.1.0-canary.4`, `16.3.0-preview.5`)
39
+ * count as supported: they track the release they lead to, and opting into a
40
+ * canary already means opting into its churn.
41
+ *
42
+ * @param nextVersion - Version string read from the project's `next/package.json`.
43
+ * @returns `true` when the plugin can be registered in `experimental.swcPlugins`.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * getIsSwcPluginSupported('16.3.0'); // true
48
+ * getIsSwcPluginSupported('15.5.18'); // false
49
+ * ```
50
+ */
51
+ const getIsSwcPluginSupported = (nextVersion) => compareVersions(nextVersion, "≥", MINIMUM_SWC_PLUGIN_NEXT_VERSION);
52
+
53
+ //#endregion
54
+ export { MINIMUM_SWC_PLUGIN_NEXT_VERSION, getIsSwcPluginSupported };
55
+ //# sourceMappingURL=swcPluginCompatibility.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swcPluginCompatibility.mjs","names":[],"sources":["../../../src/server/swcPluginCompatibility.ts"],"sourcesContent":["import { compareVersions } from '@intlayer/config/utils';\n\n/**\n * Oldest Next.js release whose bundled SWC can load the `@intlayer/swc` Wasm\n * plugin.\n *\n * A Wasm plugin only loads in a host that speaks its `swc_ecma_ast` schema.\n * Next.js 16.1.0 is the first release built on SWC's forward-compatible plugin\n * ABI — the AST travels as self-describing CBOR instead of rkyv, so a plugin\n * compiled against an older `swc_core` keeps loading in newer hosts. Every\n * earlier release uses the rkyv ABI, which requires an exact schema match and\n * rejects the plugin outright, failing the build with `failed to invoke\n * plugin`.\n *\n * Verified by driving `transformSync` on the shipped `@next/swc` binaries with\n * the plugin attached:\n *\n * | Next.js | swc_ecma_ast | plugin loads |\n * | ------- | ------------ | ------------ |\n * | 14.2.x | 0.112.7 | no |\n * | 15.5.x | 14.0.0 | no |\n * | 16.0.x | 16.0.0 | no |\n * | 16.1.x | 19.0.0 | yes |\n * | 16.2.x | 20.0.1 | yes |\n * | 16.3.x | 25.0.0 | yes |\n *\n * Keep in sync with the `swc_core` pin in\n * `packages/@intlayer/swc/Cargo.toml`: the plugin must stay on the\n * `swc_ecma_ast` version this Next.js release ships, never a newer one, or the\n * floor moves up with it.\n */\nexport const MINIMUM_SWC_PLUGIN_NEXT_VERSION = '16.1.0';\n\n/**\n * Whether `nextVersion` bundles an SWC able to load the `@intlayer/swc` Wasm\n * plugin.\n *\n * Pre-releases of a supported version (`16.1.0-canary.4`, `16.3.0-preview.5`)\n * count as supported: they track the release they lead to, and opting into a\n * canary already means opting into its churn.\n *\n * @param nextVersion - Version string read from the project's `next/package.json`.\n * @returns `true` when the plugin can be registered in `experimental.swcPlugins`.\n *\n * @example\n * ```ts\n * getIsSwcPluginSupported('16.3.0'); // true\n * getIsSwcPluginSupported('15.5.18'); // false\n * ```\n */\nexport const getIsSwcPluginSupported = (nextVersion: string): boolean =>\n compareVersions(nextVersion, '≥', MINIMUM_SWC_PLUGIN_NEXT_VERSION);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;AAmB/C,MAAa,2BAA2B,gBACtC,gBAAgB,aAAa,KAAK,+BAA+B"}
@@ -9,7 +9,8 @@ import { useDictionary as useDictionary$1 } from "react-intlayer/server";
9
9
  * If the locale is not provided, it will use the locale from the server context.
10
10
  */
11
11
  const useDictionary = (dictionary, localeOrSelector) => {
12
- return useDictionary$1(dictionary, localeOrSelector, safeUseLocale());
12
+ const storedLocale = safeUseLocale();
13
+ return useDictionary$1(dictionary, localeOrSelector, storedLocale);
13
14
  };
14
15
 
15
16
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"useDictionary.mjs","names":["useDictionaryBase"],"sources":["../../../src/server/useDictionary.ts"],"sourcesContent":["import type {\n Dictionary,\n DictionarySelector,\n DictionarySelectorForGroup,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport type {\n DeclaredLocales,\n LocalesValues,\n} from '@intlayer/types/module_augmentation';\nimport { useDictionary as useDictionaryBase } from 'react-intlayer/server';\nimport { safeUseLocale } from './useIntlayer';\n\n/**\n * On the server side, hook that transforms a dictionary (or qualified\n * dictionary group) and returns the content for the given locale or selector.\n *\n * If the locale is not provided, it will use the locale from the server context.\n */\nexport const useDictionary = <\n const T extends Dictionary | QualifiedDictionaryGroup,\n const A extends\n | LocalesValues\n | DictionarySelectorForGroup<T> = DeclaredLocales,\n>(\n dictionary: T,\n localeOrSelector?: A\n): ReturnType<typeof useDictionaryBase<T, A>> => {\n const storedLocale = safeUseLocale();\n\n return useDictionaryBase<T, A>(dictionary, localeOrSelector, storedLocale);\n};\n"],"mappings":";;;;;;;;;;AAmBA,MAAa,iBAMX,YACA,qBAC+C;CAG/C,OAAOA,gBAAwB,YAAY,kBAFtB,cAEmD,CAAC;AAC3E"}
1
+ {"version":3,"file":"useDictionary.mjs","names":["useDictionaryBase"],"sources":["../../../src/server/useDictionary.ts"],"sourcesContent":["import type {\n Dictionary,\n DictionarySelector,\n DictionarySelectorForGroup,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport type {\n DeclaredLocales,\n LocalesValues,\n} from '@intlayer/types/module_augmentation';\nimport { useDictionary as useDictionaryBase } from 'react-intlayer/server';\nimport { safeUseLocale } from './useIntlayer';\n\n/**\n * On the server side, hook that transforms a dictionary (or qualified\n * dictionary group) and returns the content for the given locale or selector.\n *\n * If the locale is not provided, it will use the locale from the server context.\n */\nexport const useDictionary = <\n const T extends Dictionary | QualifiedDictionaryGroup,\n const A extends\n | LocalesValues\n | DictionarySelectorForGroup<T> = DeclaredLocales,\n>(\n dictionary: T,\n localeOrSelector?: A\n): ReturnType<typeof useDictionaryBase<T, A>> => {\n const storedLocale = safeUseLocale();\n\n return useDictionaryBase<T, A>(dictionary, localeOrSelector, storedLocale);\n};\n"],"mappings":";;;;;;;;;;AAmBA,MAAa,iBAMX,YACA,qBAC+C;CAC/C,MAAM,eAAe,cAAc;CAEnC,OAAOA,gBAAwB,YAAY,kBAAkB,YAAY;AAC3E"}
@@ -8,7 +8,8 @@ import { useDictionaryAsync as useDictionaryAsync$1 } from "react-intlayer/serve
8
8
  * If the locale is not provided, it will use the locale from the server context
9
9
  */
10
10
  const useDictionaryAsync = async (dictionaryPromise, locale) => {
11
- return useDictionaryAsync$1(dictionaryPromise, locale, await getLocale());
11
+ const storedLocale = await getLocale();
12
+ return useDictionaryAsync$1(dictionaryPromise, locale, storedLocale);
12
13
  };
13
14
 
14
15
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"useDictionaryAsync.mjs","names":["useDictionaryAsyncBase"],"sources":["../../../src/server/useDictionaryAsync.ts"],"sourcesContent":["import type { Dictionary } from '@intlayer/types/dictionary';\nimport type {\n DeclaredLocales,\n StrictModeLocaleMap,\n} from '@intlayer/types/module_augmentation';\nimport { useDictionaryAsync as useDictionaryAsyncBase } from 'react-intlayer/server';\nimport { getLocale } from './getLocale';\n\n/**\n * On the server side, Hook that transform a dictionary and return the content\n *\n * If the locale is not provided, it will use the locale from the server context\n */\nexport const useDictionaryAsync = async <\n const T extends Dictionary,\n const L extends DeclaredLocales = DeclaredLocales,\n>(\n dictionaryPromise: StrictModeLocaleMap<() => Promise<T>>,\n locale?: L\n): ReturnType<typeof useDictionaryAsyncBase<T, L>> => {\n const storedLocale = await getLocale();\n\n return useDictionaryAsyncBase<T, L>(dictionaryPromise, locale, storedLocale);\n};\n"],"mappings":";;;;;;;;;AAaA,MAAa,qBAAqB,OAIhC,mBACA,WACoD;CAGpD,OAAOA,qBAA6B,mBAAmB,QAAQ,MAFpC,UAAU,CAEsC;AAC7E"}
1
+ {"version":3,"file":"useDictionaryAsync.mjs","names":["useDictionaryAsyncBase"],"sources":["../../../src/server/useDictionaryAsync.ts"],"sourcesContent":["import type { Dictionary } from '@intlayer/types/dictionary';\nimport type {\n DeclaredLocales,\n StrictModeLocaleMap,\n} from '@intlayer/types/module_augmentation';\nimport { useDictionaryAsync as useDictionaryAsyncBase } from 'react-intlayer/server';\nimport { getLocale } from './getLocale';\n\n/**\n * On the server side, Hook that transform a dictionary and return the content\n *\n * If the locale is not provided, it will use the locale from the server context\n */\nexport const useDictionaryAsync = async <\n const T extends Dictionary,\n const L extends DeclaredLocales = DeclaredLocales,\n>(\n dictionaryPromise: StrictModeLocaleMap<() => Promise<T>>,\n locale?: L\n): ReturnType<typeof useDictionaryAsyncBase<T, L>> => {\n const storedLocale = await getLocale();\n\n return useDictionaryAsyncBase<T, L>(dictionaryPromise, locale, storedLocale);\n};\n"],"mappings":";;;;;;;;;AAaA,MAAa,qBAAqB,OAIhC,mBACA,WACoD;CACpD,MAAM,eAAe,MAAM,UAAU;CAErC,OAAOA,qBAA6B,mBAAmB,QAAQ,YAAY;AAC7E"}
@@ -9,7 +9,8 @@ import { useDictionaryDynamic as useDictionaryDynamic$1 } from "react-intlayer/s
9
9
  * If the locale is not provided, it will use the locale from the server context.
10
10
  */
11
11
  const useDictionaryDynamic = (dictionaryPromise, key, localeOrSelector) => {
12
- return useDictionaryDynamic$1(dictionaryPromise, key, localeOrSelector, safeUseLocale());
12
+ const storedLocale = safeUseLocale();
13
+ return useDictionaryDynamic$1(dictionaryPromise, key, localeOrSelector, storedLocale);
13
14
  };
14
15
 
15
16
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"useDictionaryDynamic.mjs","names":["useDictionaryDynamicBase"],"sources":["../../../src/server/useDictionaryDynamic.ts"],"sourcesContent":["import type { QualifiedDynamicLoaderMap } from '@intlayer/core/dictionaryManipulator';\nimport type {\n Dictionary,\n DictionarySelector,\n} from '@intlayer/types/dictionary';\nimport type {\n DeclaredLocales,\n LocalesValues,\n StrictModeLocaleMap,\n} from '@intlayer/types/module_augmentation';\nimport { useDictionaryDynamic as useDictionaryDynamicBase } from 'react-intlayer/server';\nimport { safeUseLocale } from './useIntlayer';\n\n/**\n * On the server side, hook that lazily loads a dictionary (plain or qualified)\n * and returns the content for the given locale or selector.\n *\n * If the locale is not provided, it will use the locale from the server context.\n */\nexport const useDictionaryDynamic = <\n const T extends Dictionary,\n const A extends LocalesValues | DictionarySelector = DeclaredLocales,\n>(\n dictionaryPromise:\n | StrictModeLocaleMap<() => Promise<T>>\n | QualifiedDynamicLoaderMap,\n key: string,\n localeOrSelector?: A\n): ReturnType<typeof useDictionaryDynamicBase<T, A>> => {\n const storedLocale = safeUseLocale();\n\n return useDictionaryDynamicBase<T, A>(\n dictionaryPromise,\n key,\n localeOrSelector,\n storedLocale\n );\n};\n"],"mappings":";;;;;;;;;;AAmBA,MAAa,wBAIX,mBAGA,KACA,qBACsD;CAGtD,OAAOA,uBACL,mBACA,KACA,kBALmB,cAMR,CACb;AACF"}
1
+ {"version":3,"file":"useDictionaryDynamic.mjs","names":["useDictionaryDynamicBase"],"sources":["../../../src/server/useDictionaryDynamic.ts"],"sourcesContent":["import type { QualifiedDynamicLoaderMap } from '@intlayer/core/dictionaryManipulator';\nimport type {\n Dictionary,\n DictionarySelector,\n} from '@intlayer/types/dictionary';\nimport type {\n DeclaredLocales,\n LocalesValues,\n StrictModeLocaleMap,\n} from '@intlayer/types/module_augmentation';\nimport { useDictionaryDynamic as useDictionaryDynamicBase } from 'react-intlayer/server';\nimport { safeUseLocale } from './useIntlayer';\n\n/**\n * On the server side, hook that lazily loads a dictionary (plain or qualified)\n * and returns the content for the given locale or selector.\n *\n * If the locale is not provided, it will use the locale from the server context.\n */\nexport const useDictionaryDynamic = <\n const T extends Dictionary,\n const A extends LocalesValues | DictionarySelector = DeclaredLocales,\n>(\n dictionaryPromise:\n | StrictModeLocaleMap<() => Promise<T>>\n | QualifiedDynamicLoaderMap,\n key: string,\n localeOrSelector?: A\n): ReturnType<typeof useDictionaryDynamicBase<T, A>> => {\n const storedLocale = safeUseLocale();\n\n return useDictionaryDynamicBase<T, A>(\n dictionaryPromise,\n key,\n localeOrSelector,\n storedLocale\n );\n};\n"],"mappings":";;;;;;;;;;AAmBA,MAAa,wBAIX,mBAGA,KACA,qBACsD;CACtD,MAAM,eAAe,cAAc;CAEnC,OAAOA,uBACL,mBACA,KACA,kBACA,YACF;AACF"}
@@ -20,7 +20,8 @@ const safeUseLocale = () => {
20
20
  * If the locale is not provided, it will use the locale from the server context.
21
21
  */
22
22
  const useIntlayer = (key, localeOrSelector) => {
23
- return useIntlayer$1(key, localeOrSelector, safeUseLocale());
23
+ const storedLocale = safeUseLocale();
24
+ return useIntlayer$1(key, localeOrSelector, storedLocale);
24
25
  };
25
26
 
26
27
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"useIntlayer.mjs","names":["useIntlayerBase"],"sources":["../../../src/server/useIntlayer.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport type {\n DeclaredLocales,\n DictionaryKeys,\n DictionarySelectorForKey,\n LocalesValues,\n} from '@intlayer/types/module_augmentation';\nimport React from 'react';\nimport { useIntlayer as useIntlayerBase } from 'react-intlayer/server';\nimport { getLocale } from './getLocale';\n\nconst getCachedLocale =\n typeof React.cache === 'function' ? React.cache(getLocale) : getLocale;\n\nexport const safeUseLocale = (): Locale | undefined => {\n // getLocale returns a Promise based on your TS error\n const localeData = getCachedLocale() as Promise<Locale> | Locale;\n\n if (localeData instanceof Promise) {\n if (typeof React.use === 'function') {\n return React.use(localeData); // Safely unwraps in React 19+\n }\n\n // React < 19 cannot synchronously unwrap Promises in hooks.\n // Return undefined to trigger the localeTarget fallback.\n return undefined;\n }\n\n return localeData;\n};\n\n/**\n * On the server side, hook that picks one dictionary by its key and returns the\n * content for the given locale or selector (`{ item }`, `{ variant }`,\n * optionally combined with `locale`).\n *\n * If the locale is not provided, it will use the locale from the server context.\n */\nexport const useIntlayer = <\n const T extends DictionaryKeys,\n const A extends LocalesValues | DictionarySelectorForKey<T> = DeclaredLocales,\n>(\n key: T,\n localeOrSelector?: A\n): ReturnType<typeof useIntlayerBase<T, A>> => {\n const storedLocale = safeUseLocale();\n\n return useIntlayerBase<T, A>(key, localeOrSelector, storedLocale);\n};\n"],"mappings":";;;;;AAWA,MAAM,kBACJ,OAAO,MAAM,UAAU,aAAa,MAAM,MAAM,SAAS,IAAI;AAE/D,MAAa,sBAA0C;CAErD,MAAM,aAAa,gBAAgB;CAEnC,IAAI,sBAAsB,SAAS;EACjC,IAAI,OAAO,MAAM,QAAQ,YACvB,OAAO,MAAM,IAAI,UAAU;EAK7B;CACF;CAEA,OAAO;AACT;;;;;;;;AASA,MAAa,eAIX,KACA,qBAC6C;CAG7C,OAAOA,cAAsB,KAAK,kBAFb,cAE0C,CAAC;AAClE"}
1
+ {"version":3,"file":"useIntlayer.mjs","names":["useIntlayerBase"],"sources":["../../../src/server/useIntlayer.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport type {\n DeclaredLocales,\n DictionaryKeys,\n DictionarySelectorForKey,\n LocalesValues,\n} from '@intlayer/types/module_augmentation';\nimport React from 'react';\nimport { useIntlayer as useIntlayerBase } from 'react-intlayer/server';\nimport { getLocale } from './getLocale';\n\nconst getCachedLocale =\n typeof React.cache === 'function' ? React.cache(getLocale) : getLocale;\n\nexport const safeUseLocale = (): Locale | undefined => {\n // getLocale returns a Promise based on your TS error\n const localeData = getCachedLocale() as Promise<Locale> | Locale;\n\n if (localeData instanceof Promise) {\n if (typeof React.use === 'function') {\n return React.use(localeData); // Safely unwraps in React 19+\n }\n\n // React < 19 cannot synchronously unwrap Promises in hooks.\n // Return undefined to trigger the localeTarget fallback.\n return undefined;\n }\n\n return localeData;\n};\n\n/**\n * On the server side, hook that picks one dictionary by its key and returns the\n * content for the given locale or selector (`{ item }`, `{ variant }`,\n * optionally combined with `locale`).\n *\n * If the locale is not provided, it will use the locale from the server context.\n */\nexport const useIntlayer = <\n const T extends DictionaryKeys,\n const A extends LocalesValues | DictionarySelectorForKey<T> = DeclaredLocales,\n>(\n key: T,\n localeOrSelector?: A\n): ReturnType<typeof useIntlayerBase<T, A>> => {\n const storedLocale = safeUseLocale();\n\n return useIntlayerBase<T, A>(key, localeOrSelector, storedLocale);\n};\n"],"mappings":";;;;;AAWA,MAAM,kBACJ,OAAO,MAAM,UAAU,aAAa,MAAM,MAAM,SAAS,IAAI;AAE/D,MAAa,sBAA0C;CAErD,MAAM,aAAa,gBAAgB;CAEnC,IAAI,sBAAsB,SAAS;EACjC,IAAI,OAAO,MAAM,QAAQ,YACvB,OAAO,MAAM,IAAI,UAAU;EAK7B;CACF;CAEA,OAAO;AACT;;;;;;;;AASA,MAAa,eAIX,KACA,qBAC6C;CAC7C,MAAM,eAAe,cAAc;CAEnC,OAAOA,cAAsB,KAAK,kBAAkB,YAAY;AAClE"}
@@ -1,10 +1,13 @@
1
+ import { getIsPurgePipelineAvailable, prepareSwcOptimization, resolveSwcLogLevel } from "./prepareSwcOptimization.mjs";
2
+ import { MINIMUM_SWC_PLUGIN_NEXT_VERSION, getIsSwcPluginSupported } from "./swcPluginCompatibility.mjs";
1
3
  import { IMPORT_MODE } from "@intlayer/config/defaultValues";
2
- import { colorize, getAppLogger } from "@intlayer/config/logger";
3
4
  import { join, relative, resolve } from "node:path";
4
5
  import * as ANSIColors from "@intlayer/config/colors";
5
- import { formatDictionarySelectorEnvVar, formatNodeTypeToEnvVar, getConfigEnvVars } from "@intlayer/config/envVars";
6
+ import { formatDictionarySelectorEnvVar, formatNodeTypeToEnvVar, formatOptimizedNestingEnvVar, getConfigEnvVars } from "@intlayer/config/envVars";
7
+ import { colorize, getAppLogger } from "@intlayer/config/logger";
6
8
  import { getConfiguration } from "@intlayer/config/node";
7
9
  import { compareVersions, getAlias, getHasDictionarySelector, getProjectRequire, getUnusedNodeTypes, normalizePath } from "@intlayer/config/utils";
10
+ import { getNestedDictionaryGraph } from "@intlayer/core/dictionaryManipulator";
8
11
  import { getDictionaries } from "@intlayer/dictionaries-entry";
9
12
  import { prepareIntlayer } from "@intlayer/engine/build";
10
13
  import { logConfigDetails } from "@intlayer/engine/cli";
@@ -25,10 +28,16 @@ const getNextVersionFlags = (intlayerConfig) => {
25
28
  nextVersion = (intlayerConfig.build?.require ?? getProjectRequire())("next/package.json").version;
26
29
  } catch {}
27
30
  return {
31
+ nextVersion,
28
32
  isGteNext13: compareVersions(nextVersion, "≥", "13.0.0"),
29
33
  isGteNext15: compareVersions(nextVersion, "≥", "15.0.0"),
30
34
  isGteNext16: compareVersions(nextVersion, "≥", "16.0.0"),
31
- isTurbopackStable: compareVersions(nextVersion, "≥", "15.3.0")
35
+ isTurbopackStable: compareVersions(nextVersion, "≥", "15.3.0"),
36
+ /**
37
+ * Whether this Next.js release can load the `@intlayer/swc` Wasm plugin.
38
+ * See {@link MINIMUM_SWC_PLUGIN_NEXT_VERSION}.
39
+ */
40
+ isSwcPluginSupported: getIsSwcPluginSupported(nextVersion)
32
41
  };
33
42
  };
34
43
  const getIsSwcPluginAvailable = (intlayerConfig) => {
@@ -52,8 +61,9 @@ const resolvePluginPath = (pluginPath, intlayerConfig, isTurbopackEnabled) => {
52
61
  if (isTurbopackEnabled) return normalizePath(`./${relative(process.cwd(), pluginPathResolved)}`);
53
62
  return pluginPathResolved;
54
63
  };
55
- const getPruneConfig = (intlayerConfig, isBuildCommand, isTurbopackEnabled, isDevCommand, isGteNext13, swcExtraCallers) => {
56
- const { optimize } = intlayerConfig.build;
64
+ const getPruneConfig = ({ intlayerConfig, isBuildCommand, isTurbopackEnabled, isDevCommand, isGteNext13, isSwcPluginSupported, nextVersion, swcExtraCallers, fieldRenameMap }) => {
65
+ const { optimize, minify, purge } = intlayerConfig.build;
66
+ const editorEnabled = intlayerConfig.editor.enabled;
57
67
  const importMode = intlayerConfig.build.importMode ?? intlayerConfig.dictionary?.importMode;
58
68
  const { dictionariesDir, unmergedDictionariesDir, dynamicDictionariesDir, fetchDictionariesDir, mainDir } = intlayerConfig.system;
59
69
  const { baseDir } = intlayerConfig.system;
@@ -63,28 +73,39 @@ const getPruneConfig = (intlayerConfig, isBuildCommand, isTurbopackEnabled, isDe
63
73
  if (!isGteNext13) return {};
64
74
  const isSwcPluginAvailable = getIsSwcPluginAvailable(intlayerConfig);
65
75
  runOnce(join(baseDir, ".intlayer", "cache", "intlayer-prune-plugin-enabled.lock"), () => {
66
- if (isSwcPluginAvailable) logger([
67
- `Build optimization ${colorize("enabled", ANSIColors.GREEN)}`,
68
- colorize(`(import mode:`, ANSIColors.GREY_DARK),
69
- colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),
70
- colorize(`)`, ANSIColors.GREY_DARK)
71
- ]);
72
- else logger([
76
+ if (isSwcPluginAvailable && !isSwcPluginSupported) logger([
77
+ `Build optimization ${colorize("disabled", ANSIColors.GREY_DARK)}:`,
78
+ colorize("@intlayer/swc", ANSIColors.GREY_LIGHT),
79
+ colorize(`cannot run on Next.js ${nextVersion} — its bundled SWC predates the`, ANSIColors.GREY),
80
+ colorize("stable Wasm plugin ABI. Upgrade to Next.js", ANSIColors.GREY),
81
+ colorize(MINIMUM_SWC_PLUGIN_NEXT_VERSION, ANSIColors.BLUE),
82
+ colorize("or later to enable it.", ANSIColors.GREY)
83
+ ], { level: "warn" });
84
+ else if (isSwcPluginAvailable) {
85
+ logger([
86
+ `Build optimization ${colorize("enabled", ANSIColors.GREEN)}`,
87
+ colorize(`(import mode:`, ANSIColors.GREY_DARK),
88
+ colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),
89
+ colorize(`)`, ANSIColors.GREY_DARK)
90
+ ]);
91
+ const isPurgePipelineEnabled = !editorEnabled && !swcExtraCallers?.length && getIsPurgePipelineAvailable(intlayerConfig);
92
+ if (isPurgePipelineEnabled && minify) logger(`Dictionary minification ${colorize("enabled", ANSIColors.GREEN)}`);
93
+ if (isPurgePipelineEnabled && purge) logger(`Dictionary purge unused keys ${colorize("enabled", ANSIColors.GREEN)}`);
94
+ } else logger([
73
95
  colorize("Recommended: Install", ANSIColors.GREY),
74
96
  colorize("@intlayer/swc", ANSIColors.GREY_LIGHT),
75
97
  colorize("package to enable build optimization. See documentation:", ANSIColors.GREY),
76
98
  colorize("https://intlayer.org/docs/bundle-optimization", ANSIColors.GREY_LIGHT)
77
99
  ]);
78
- }, { cacheTimeoutMs: 1e3 * 30 });
100
+ }, { cacheTimeoutMs: 3e4 });
79
101
  runOnce(join(baseDir, ".intlayer", "cache", "intlayer-compiler-plugin-enabled.lock"), () => {
80
102
  if (getIsBabelExtractPluginAvailable(intlayerConfig)) {
81
103
  let isEnabled = intlayerConfig.compiler?.enabled ?? true;
82
104
  if (isEnabled === "build-only") isEnabled = !isDevCommand;
83
105
  if (isEnabled) logger("Intlayer compiler enabled");
84
- else logger("Intlayer compiler disabled");
85
106
  }
86
- }, { cacheTimeoutMs: 1e3 * 30 });
87
- if (!isSwcPluginAvailable) return {};
107
+ }, { cacheTimeoutMs: 3e4 });
108
+ if (!isSwcPluginAvailable || !isSwcPluginSupported) return {};
88
109
  const dictionariesEntryPath = join(mainDir, "dictionaries.mjs");
89
110
  const dynamicDictionariesEntryPath = join(mainDir, "dynamic_dictionaries.mjs");
90
111
  const unmergedDictionariesEntryPath = join(mainDir, "unmerged_dictionaries.mjs");
@@ -99,6 +120,7 @@ const getPruneConfig = (intlayerConfig, isBuildCommand, isTurbopackEnabled, isDe
99
120
  Object.values(dictionaries).forEach((dictionary) => {
100
121
  dictionaryModeMap[dictionary.key] = dictionary.importMode ?? importMode ?? IMPORT_MODE;
101
122
  });
123
+ const nestingDictionaryKeys = [...getNestedDictionaryGraph(Object.values(dictionaries)).keys()];
102
124
  return { experimental: { swcPlugins: [[resolvePluginPath("@intlayer/swc", intlayerConfig, isTurbopackEnabled), {
103
125
  dictionariesDir,
104
126
  dictionariesEntryPath,
@@ -111,8 +133,11 @@ const getPruneConfig = (intlayerConfig, isBuildCommand, isTurbopackEnabled, isDe
111
133
  importMode,
112
134
  filesList,
113
135
  replaceDictionaryEntry: true,
136
+ nestingDictionaryKeys,
114
137
  dictionaryModeMap,
115
- extraCallers: swcExtraCallers ?? []
138
+ extraCallers: swcExtraCallers ?? [],
139
+ fieldRenameMap: fieldRenameMap ?? {},
140
+ logLevel: resolveSwcLogLevel(intlayerConfig)
116
141
  }]] } };
117
142
  };
118
143
  const getCommandsEvent = () => {
@@ -170,7 +195,7 @@ const withIntlayerSync = (nextConfig = {}, configOptions) => {
170
195
  const intlayerConfig = getConfiguration(resolvedConfigOptions);
171
196
  logConfigDetails(resolvedConfigOptions);
172
197
  const appLogger = getAppLogger(intlayerConfig);
173
- const { isGteNext13, isGteNext15, isGteNext16, isTurbopackStable } = getNextVersionFlags(intlayerConfig);
198
+ const { nextVersion, isGteNext13, isGteNext15, isGteNext16, isTurbopackStable, isSwcPluginSupported } = getNextVersionFlags(intlayerConfig);
174
199
  const isTurbopackEnabledFromCommand = isGteNext16 ? !process.env["npm_lifecycle_script"]?.includes("--webpack") : process.env["npm_lifecycle_script"]?.includes("--turbo");
175
200
  const isTurbopackEnabled = configOptions?.enableTurbopack ?? isTurbopackEnabledFromCommand;
176
201
  if (isTurbopackEnabled && typeof nextConfig.webpack !== "undefined") appLogger("Turbopack is enabled but a custom webpack config is present. It will be ignored.");
@@ -208,6 +233,7 @@ const withIntlayerSync = (nextConfig = {}, configOptions) => {
208
233
  env = {
209
234
  ...env,
210
235
  ...formatNodeTypeToEnvVar(unusedNodeTypes),
236
+ ...formatOptimizedNestingEnvVar(intlayerConfig.build.optimize !== false),
211
237
  ...formatDictionarySelectorEnvVar(getHasDictionarySelector(dictionaries)),
212
238
  ...getConfigEnvVars(intlayerConfig)
213
239
  };
@@ -225,18 +251,19 @@ const withIntlayerSync = (nextConfig = {}, configOptions) => {
225
251
  serverComponentsExternalPackages: serverExternalPackages
226
252
  }
227
253
  };
228
- if (isTurbopackEnabled) if (isGteNext15 && isTurbopackStable) config = {
229
- ...config,
230
- turbopack: turboConfig
231
- };
232
- else config = {
233
- ...config,
234
- experimental: {
235
- ...config?.experimental ?? {},
236
- turbo: turboConfig
237
- }
238
- };
239
- else config = {
254
+ if (isTurbopackEnabled) {
255
+ if (isGteNext15 && isTurbopackStable) config = {
256
+ ...config,
257
+ turbopack: turboConfig
258
+ };
259
+ else config = {
260
+ ...config,
261
+ experimental: {
262
+ ...config?.experimental ?? {},
263
+ turbo: turboConfig
264
+ }
265
+ };
266
+ } else config = {
240
267
  ...config,
241
268
  webpack: (config, options) => {
242
269
  const { isServer, nextRuntime } = options;
@@ -272,8 +299,19 @@ const withIntlayerSync = (nextConfig = {}, configOptions) => {
272
299
  };
273
300
  return config;
274
301
  };
275
- const pruneConfig = getPruneConfig(intlayerConfig, isBuildCommand, isTurbopackEnabled ?? false, isDevCommand, isGteNext13, configOptions?.swcExtraCallers);
276
- return defu(defu(getNewConfig(), pruneConfig), nextConfig);
302
+ const pruneConfig = getPruneConfig({
303
+ intlayerConfig,
304
+ isBuildCommand,
305
+ isTurbopackEnabled: isTurbopackEnabled ?? false,
306
+ isDevCommand,
307
+ isGteNext13,
308
+ isSwcPluginSupported,
309
+ nextVersion,
310
+ swcExtraCallers: configOptions?.swcExtraCallers,
311
+ fieldRenameMap: configOptions?.fieldRenameMap
312
+ });
313
+ const intlayerNextConfig = defu(getNewConfig(), pruneConfig);
314
+ return defu(intlayerNextConfig, nextConfig);
277
315
  };
278
316
  /**
279
317
  * A Next.js plugin that adds the intlayer configuration to the webpack configuration
@@ -298,13 +336,22 @@ const withIntlayer = async (nextConfig = {}, configOptions) => {
298
336
  const { mode } = intlayerConfig.build;
299
337
  if (!isStartCommand && (isDevCommand || isBuildCommand || mode === "auto")) await prepareIntlayer(intlayerConfig, {
300
338
  clean: isBuildCommand,
301
- cacheTimeoutMs: isBuildCommand ? 1e3 * 30 : 1e3 * 60 * 60,
339
+ cacheTimeoutMs: isBuildCommand ? 3e4 : 36e5,
302
340
  env: isBuildCommand ? "prod" : "dev"
303
341
  });
342
+ const { isSwcPluginSupported } = getNextVersionFlags(intlayerConfig);
343
+ const fieldRenameMap = isBuildCommand ? await prepareSwcOptimization(intlayerConfig, {
344
+ configOptions: resolvedConfigOptions,
345
+ swcExtraCallers: configOptions?.swcExtraCallers,
346
+ isSwcPluginUsable: isSwcPluginSupported && getIsSwcPluginAvailable(intlayerConfig)
347
+ }) : {};
304
348
  const nextConfigResolved = await nextConfig;
305
- return withIntlayerSync(nextConfigResolved, resolvedConfigOptions);
349
+ return withIntlayerSync(nextConfigResolved, {
350
+ ...resolvedConfigOptions,
351
+ fieldRenameMap
352
+ });
306
353
  };
307
354
 
308
355
  //#endregion
309
- export { withIntlayer, withIntlayerSync };
356
+ export { MINIMUM_SWC_PLUGIN_NEXT_VERSION, getIsSwcPluginSupported, withIntlayer, withIntlayerSync };
310
357
  //# sourceMappingURL=withIntlayer.mjs.map