intlayer_swc_plugin/config.rs
1//! Plugin configuration types.
2//!
3//! These mirror the option objects produced on the JavaScript side
4//! (`withIntlayer` in `next-intlayer`, `toSwcExtraCallers` in
5//! `@intlayer/config/callers`, `serializeFieldRenameMap` in `@intlayer/babel`).
6//! Both sides must stay in sync.
7
8use serde::Deserialize;
9use std::collections::BTreeMap;
10
11// ─────────────────────────────────────────────────────────────────────────────
12// EXTRA CALLERS
13// ─────────────────────────────────────────────────────────────────────────────
14
15/// Location of a namespace read from a property of an options-object
16/// argument, e.g. vue-i18n's `useI18n({ namespace: 'about' })`.
17///
18/// Field names mirror `SwcExtraCallerConfig['namespaceOption']` in
19/// `@intlayer/config/callers` — both sides must stay in sync.
20#[derive(Debug, Deserialize, Clone)]
21pub struct NamespaceOptionConfig {
22 /// Zero-based index of the options-object argument.
23 #[serde(rename = "argumentIndex")]
24 pub argument_index: usize,
25
26 /// Name of the property holding the namespace string.
27 #[serde(rename = "property")]
28 pub property: String,
29}
30
31/// Descriptor for a compat-adapter caller that the SWC plugin should recognise
32/// and rewrite in the same way as the native `useIntlayer` / `getIntlayer`
33/// calls (i.e. replace the string-key argument with a pre-imported dictionary
34/// object and swap the function name for a `*Dictionary` variant).
35///
36/// These are supplied entirely by the compat adapter plugins (e.g.
37/// `createNextI18nPlugin`) and are forwarded into the SWC config; no compat
38/// names are hard-coded inside this crate. The wire format is produced by
39/// `toSwcExtraCallers` in `@intlayer/config/callers` — both sides must stay
40/// in sync.
41///
42/// Exactly one of `namespace_arg_index`, `fixed_namespace` or
43/// `namespace_option` describes where the namespace (dictionary key) is read
44/// from; they are tried in that order.
45#[derive(Debug, Deserialize, Clone)]
46pub struct ExtraCallerConfig {
47 /// The function name the user calls, e.g. `"useTranslation"`.
48 #[serde(rename = "callerName")]
49 pub caller_name: String,
50
51 /// The import package specifiers that can export this function,
52 /// e.g. `["react-i18next", "@intlayer/react-i18next"]`.
53 #[serde(rename = "importSources")]
54 pub import_sources: Vec<String>,
55
56 /// Zero-based index of the positional argument that holds the namespace
57 /// (dictionary key) string, e.g. `0` for `useTranslation('about')`.
58 #[serde(rename = "namespaceArgIndex", default)]
59 pub namespace_arg_index: Option<usize>,
60
61 /// Compile-time constant namespace — every call site reads the same
62 /// dictionary; the dictionary ident is inserted as a new first argument
63 /// (lingui's `useLingui()` → `useDictionary(_messages)`).
64 #[serde(rename = "fixedNamespace", default)]
65 pub fixed_namespace: Option<String>,
66
67 /// Namespace read from a property of an options-object argument; the
68 /// dictionary ident is inserted as a new first argument and the property
69 /// is rewritten to the key-prefix remainder (or removed).
70 #[serde(rename = "namespaceOption", default)]
71 pub namespace_option: Option<NamespaceOptionConfig>,
72
73 /// Name of the replacement function for static-import mode,
74 /// e.g. `"useTranslationDictionary"`.
75 #[serde(rename = "staticReplacement")]
76 pub static_replacement: String,
77
78 /// Name of the replacement function for dynamic/fetch import mode,
79 /// e.g. `"useTranslationDictionaryDynamic"`.
80 #[serde(rename = "dynamicReplacement")]
81 pub dynamic_replacement: String,
82}
83
84// ─────────────────────────────────────────────────────────────────────────────
85// FIELD RENAME (MINIFY)
86// ─────────────────────────────────────────────────────────────────────────────
87
88/// Rename table for one nesting level of a dictionary's content: original
89/// field name → its short alias and the rename table of its own children.
90pub type FieldRenameMap = BTreeMap<String, FieldRenameNode>;
91
92/// A single entry of a [`FieldRenameMap`].
93///
94/// Mirrors `NestedRenameEntry` in
95/// `@intlayer/babel/babel-plugin-intlayer-usage-analyzer`, serialised by
96/// `serializeFieldRenameMap`. The short names are assigned on the JavaScript
97/// side (alphabetically, from the full compiled dictionary) and applied to the
98/// dictionary JSON there too, so this crate only has to rewrite the matching
99/// source-code accesses.
100#[derive(Debug, Deserialize, Clone, Default)]
101pub struct FieldRenameNode {
102 /// Short alphabetic alias the field is renamed to (`"a"`, `"b"`, …).
103 #[serde(rename = "shortName")]
104 pub short_name: String,
105
106 /// Rename table for the fields nested inside this one. Empty when the
107 /// value is a leaf, an array, or an opaquely-consumed value whose children
108 /// must keep their original names.
109 #[serde(rename = "children", default)]
110 pub children: FieldRenameMap,
111}
112
113// ─────────────────────────────────────────────────────────────────────────────
114// LOG LEVEL
115// ─────────────────────────────────────────────────────────────────────────────
116
117/// Verbosity of the plugin's build-time reporting.
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
119pub enum LogLevel {
120 /// No output at all (default).
121 #[default]
122 Off,
123 /// One line per transformed file summarising what changed.
124 Info,
125 /// Everything `Info` reports, plus skipped files and the emitted code.
126 Debug,
127}
128
129impl LogLevel {
130 /// Parses the wire value of the `logLevel` option.
131 ///
132 /// Unknown values fall back to [`LogLevel::Off`] rather than failing the
133 /// whole config deserialisation, which would silently disable the plugin.
134 pub fn from_option(raw: Option<&str>) -> Self {
135 match raw {
136 Some("info") => LogLevel::Info,
137 Some("debug" | "verbose") => LogLevel::Debug,
138 _ => LogLevel::Off,
139 }
140 }
141}
142
143// ─────────────────────────────────────────────────────────────────────────────
144// PLUGIN CONFIG
145// ─────────────────────────────────────────────────────────────────────────────
146
147/// Configuration passed to the plugin via SWC transform options or constructed
148/// directly when using [`crate::process_transform`] from native Rust.
149#[derive(Debug, Deserialize, Clone, Default)]
150pub struct PluginConfig {
151 /// Absolute path to the directory containing `<key>.json` compiled dictionaries.
152 #[serde(rename = "dictionariesDir")]
153 pub dictionaries_dir: String,
154
155 /// Absolute path to the generated dictionaries entry file (e.g. `.intlayer/dictionaries.mjs`).
156 #[serde(rename = "dictionariesEntryPath")]
157 pub dictionaries_entry_path: String,
158
159 /// Absolute path to the directory containing `<key>.mjs` dynamic dictionary modules.
160 #[serde(rename = "dynamicDictionariesDir")]
161 pub dynamic_dictionaries_dir: String,
162
163 /// Absolute path to the directory containing `<key>.mjs` fetch/live dictionary modules.
164 #[serde(rename = "fetchDictionariesDir")]
165 pub fetch_dictionaries_dir: String,
166
167 /// Global import mode for all dictionaries: `"static"` (default), `"dynamic"`, or `"fetch"`.
168 #[serde(rename = "importMode")]
169 pub import_mode: Option<String>,
170
171 /// When `true`, the dictionaries entry file is replaced with `export default {}` and
172 /// `export const getDictionaries = () => ({})`.
173 #[serde(rename = "replaceDictionaryEntry")]
174 pub replace_dictionary_entry: Option<bool>,
175
176 /// Keys of the dictionaries that reference other dictionaries through `nest()`.
177 ///
178 /// For those, the injected static import points at the generated companion
179 /// module (`<dictionariesDir>/nested/<key>.mjs`) instead of the raw JSON.
180 /// The companion re-exports the dictionary with its nest targets attached,
181 /// so `getNesting` resolves them from that local reference rather than from
182 /// the global registry this plugin empties — and each target lands in the
183 /// chunk of the dictionary referencing it.
184 ///
185 /// Dynamic and fetch modes need nothing here: their generated loaders
186 /// already attach the same targets per locale.
187 #[serde(rename = "nestingDictionaryKeys", default)]
188 pub nesting_dictionary_keys: Vec<String>,
189
190 /// Allowlist of absolute file paths to transform. When empty, all files are processed.
191 #[serde(rename = "filesList", default)]
192 pub files_list: Vec<String>,
193
194 /// Per-dictionary import mode overrides, keyed by dictionary key.
195 /// Values are `"static"`, `"dynamic"`, or `"fetch"`.
196 #[serde(rename = "dictionaryModeMap")]
197 pub dictionary_mode_map: Option<BTreeMap<String, String>>,
198
199 /// Extra caller descriptors injected by compat adapter plugins.
200 ///
201 /// Each entry teaches the plugin to recognise a compat-adapter function
202 /// (e.g. `useTranslation` from `react-i18next`) and rewrite its call site
203 /// to a `*Dictionary` variant that accepts a pre-imported dictionary object
204 /// instead of a string key.
205 #[serde(rename = "extraCallers", default)]
206 pub extra_callers: Vec<ExtraCallerConfig>,
207
208 /// Field-rename tables keyed by dictionary key, produced by the
209 /// `build.minify` pipeline on the JavaScript side.
210 ///
211 /// When a dictionary is listed here, its compiled JSON has already been
212 /// rewritten with the short aliases, so every source-code access to its
213 /// content must be rewritten to match (`content.title` → `content.a`).
214 /// Dictionaries whose JSON was left untouched (edge cases, fetch mode,
215 /// opaque consumers) are simply absent from the map.
216 ///
217 /// The whole map arrives empty when the visual editor is enabled: renaming
218 /// rewrites the content keys the `keyPath` is built from, and the editor
219 /// resolves every edit by `keyPath` against the unmerged dictionaries.
220 /// Purging still happens, so the dictionaries are still smaller.
221 #[serde(rename = "fieldRenameMap", default)]
222 pub field_rename_map: BTreeMap<String, FieldRenameMap>,
223
224 /// Verbosity of the plugin's build-time reporting: `"off"` (default),
225 /// `"info"`, or `"debug"`.
226 #[serde(rename = "logLevel", default)]
227 pub log_level: Option<String>,
228}