Skip to main content

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}