Skip to main content

intlayer_swc_plugin/
optimize.rs

1//! The optimize transform: replaces the dictionary-key argument of every
2//! recognised caller with a pre-imported dictionary object and re-points the
3//! import specifier at the matching `*Dictionary` helper.
4//!
5//! Only the native `useIntlayer` / `getIntlayer` / `getIntlayerAsync` rewrite
6//! lives here. Compat adapters plug in through [`ExtraCallerContext`], held as
7//! an `Option`: with no `extraCallers` configured it is `None`, every branch
8//! guarded by it is skipped, and the native rewrite runs exactly as if the
9//! adapters did not exist.
10
11use crate::{
12    ast::{
13        callee_ident_name, imported_specifier_name, make_ident, make_ident_arg, read_static_string,
14    },
15    dictionary_imports::{ImportKind, InjectedImports},
16    extra_caller::ExtraCallerContext,
17    packages::{GET_INTLAYER_ASYNC, PACKAGE_LIST, PACKAGE_LIST_DYNAMIC},
18    pre_pass::CallerMap,
19};
20use std::collections::{BTreeMap, HashSet};
21use swc_core::ecma::{
22    ast::*,
23    visit::{VisitMut, VisitMutWith},
24};
25
26pub struct TransformVisitor<'a> {
27    import_mode: ImportKind,
28    dictionary_mode_map: &'a BTreeMap<String, String>,
29    caller_map: &'a CallerMap,
30    /// Packages with at least one native call resolving a dictionary overridden
31    /// to the `dynamic` import mode.
32    packages_with_dynamic_call: &'a HashSet<String>,
33    /// Packages with at least one native call resolving a dictionary overridden
34    /// to the `fetch` import mode.
35    packages_with_fetch_call: &'a HashSet<String>,
36    /// Compat adapters, when any were configured for this build.
37    extra: Option<ExtraCallerContext<'a>>,
38    /// Imports collected during the traversal, injected afterwards.
39    pub injected_imports: InjectedImports,
40}
41
42impl<'a> TransformVisitor<'a> {
43    pub fn new(
44        import_mode: ImportKind,
45        dictionary_mode_map: &'a BTreeMap<String, String>,
46        caller_map: &'a CallerMap,
47        packages_with_dynamic_call: &'a HashSet<String>,
48        packages_with_fetch_call: &'a HashSet<String>,
49        extra: Option<ExtraCallerContext<'a>>,
50    ) -> Self {
51        Self {
52            import_mode,
53            dictionary_mode_map,
54            caller_map,
55            packages_with_dynamic_call,
56            packages_with_fetch_call,
57            extra,
58            injected_imports: InjectedImports::default(),
59        }
60    }
61
62    /// Helper family every native call importing from `package` resolves to.
63    ///
64    /// The decision is taken once per package and drives both the import
65    /// rewrite and the call rewrite, so the emitted helper and its argument
66    /// shape can never diverge. A package without a `useDictionaryDynamic`
67    /// export always keeps the static helper, even when a sibling import in the
68    /// same file goes dynamic.
69    fn package_uses_dynamic_helpers(&self, package_specifier: &str) -> bool {
70        if !PACKAGE_LIST_DYNAMIC.contains(&package_specifier) {
71            return false;
72        }
73
74        self.import_mode.is_dynamic_helper()
75            || self.packages_with_dynamic_call.contains(package_specifier)
76            || self.packages_with_fetch_call.contains(package_specifier)
77    }
78
79    /// Per-dictionary import mode override, when one is configured.
80    fn dictionary_override(&self, dictionary_key: &str) -> Option<ImportKind> {
81        ImportKind::from_option(
82            self.dictionary_mode_map
83                .get(dictionary_key)
84                .map(String::as_str),
85        )
86    }
87
88    /// Rewrites a native `useIntlayer` / `getIntlayer` / `getIntlayerAsync`
89    /// call site.
90    ///
91    /// `caller_package` is the package the callee was imported from; `None`
92    /// keeps the static helper, matching the Babel pass for a caller reaching
93    /// the file through a re-export.
94    fn rewrite_native_call(
95        &mut self,
96        call: &mut CallExpr,
97        caller_name: &str,
98        caller_package: Option<&str>,
99    ) {
100        let Some(arg) = call.args.first() else {
101            return;
102        };
103
104        // The dictionary key is the whole first argument: native callers look
105        // the dictionary up in the registry by that exact key, so a key holding
106        // a `.` must not be split the way a compat namespace is.
107        let Some(dictionary_key) = read_static_string(&arg.expr) else {
108            return;
109        };
110
111        let dictionary_override = self.dictionary_override(&dictionary_key);
112
113        let uses_dynamic_helpers =
114            caller_package.is_some_and(|package| self.package_uses_dynamic_helpers(package));
115
116        let import_kind = if caller_name == GET_INTLAYER_ASYNC {
117            // Loading a single locale is the whole point of the async getter,
118            // so it reads a per-locale loader whatever the file's import mode
119            // is — the fetch loader when the dictionary is remote, the dynamic
120            // one otherwise.
121            match dictionary_override {
122                Some(ImportKind::Fetch) => ImportKind::Fetch,
123                _ => ImportKind::Dynamic,
124            }
125        } else if caller_name != "useIntlayer" {
126            ImportKind::Static
127        } else if uses_dynamic_helpers {
128            dictionary_override.unwrap_or(self.import_mode)
129        } else {
130            // A per-dictionary override still wins when the caller's package
131            // stayed on the static helper.
132            dictionary_override
133                .filter(|kind| kind.is_dynamic_helper())
134                .unwrap_or(ImportKind::Static)
135        };
136
137        let ident = self
138            .injected_imports
139            .ident_for(&dictionary_key, import_kind);
140
141        if import_kind.is_dynamic_helper() {
142            // Dynamic helper: first argument is the loader, second the key.
143            call.args.insert(0, make_ident_arg(ident));
144        } else {
145            // Static helper (useDictionary / getDictionary): replace the key
146            // argument with the imported dictionary object.
147            let Some(first_arg) = call.args.first_mut() else {
148                return;
149            };
150            first_arg.expr = Box::new(Expr::Ident(ident));
151        }
152    }
153
154    /// Re-points the native caller specifiers of one import declaration at
155    /// their `*Dictionary` helper, keeping the local alias intact.
156    fn rewrite_native_import_specifier(
157        &self,
158        named: &mut ImportNamedSpecifier,
159        should_use_dynamic_helpers: bool,
160    ) {
161        let imported_name = imported_specifier_name(named);
162
163        let replacement = match imported_name.as_str() {
164            "useIntlayer" if should_use_dynamic_helpers => "useDictionaryDynamic",
165            "useIntlayer" => "useDictionary",
166            "getIntlayer" => "getDictionary",
167            GET_INTLAYER_ASYNC => "getDictionaryAsync",
168            _ => return,
169        };
170
171        named.imported = Some(ModuleExportName::Ident(make_ident(replacement)));
172    }
173}
174
175impl VisitMut for TransformVisitor<'_> {
176    fn visit_mut_expr(&mut self, expr: &mut Expr) {
177        expr.visit_mut_children_with(self);
178
179        let Expr::Call(call) = expr else {
180            return;
181        };
182
183        // Owned so the immutable borrow of `call.callee` ends before the
184        // rewrites below take it mutably.
185        let Some(callee_name) = callee_ident_name(&call.callee).map(str::to_string) else {
186            return;
187        };
188
189        let Some(meta) = self.caller_map.get(&callee_name) else {
190            return;
191        };
192        let extra_index = meta.extra_index;
193        let caller_name = meta.original_name.clone();
194        let caller_package = meta.package.clone();
195
196        match extra_index {
197            Some(extra_index) => {
198                if let Some(extra) = self.extra.as_ref() {
199                    extra.rewrite_call(call, extra_index, &mut self.injected_imports);
200                }
201            }
202            None => self.rewrite_native_call(call, &caller_name, caller_package.as_deref()),
203        }
204    }
205
206    fn visit_mut_import_decl(&mut self, import: &mut ImportDecl) {
207        import.visit_mut_children_with(self);
208
209        let package_specifier = import.src.value.as_str().unwrap_or_default().to_string();
210
211        let is_native_package = PACKAGE_LIST.contains(&package_specifier.as_str());
212        let is_extra_package = self
213            .extra
214            .as_ref()
215            .is_some_and(|extra| extra.owns_import_source(&package_specifier));
216
217        if !is_native_package && !is_extra_package {
218            return;
219        }
220
221        let should_use_dynamic_helpers =
222            is_native_package && self.package_uses_dynamic_helpers(&package_specifier);
223
224        for specifier in &mut import.specifiers {
225            let ImportSpecifier::Named(named) = specifier else {
226                continue;
227            };
228
229            if is_native_package {
230                self.rewrite_native_import_specifier(named, should_use_dynamic_helpers);
231            }
232
233            if let Some(extra) = self.extra.as_ref() {
234                extra.rewrite_import_specifier(named, &package_specifier, self.caller_map);
235            }
236        }
237    }
238}