Skip to main content

intlayer_swc_plugin/
extra_caller.rs

1//! Namespace resolution for compat-adapter callers (`useTranslation`,
2//! `useI18n`, `useLingui`, …) described by [`ExtraCallerConfig`].
3
4use crate::{
5    ast::{
6        imported_specifier_name, make_ident, make_ident_arg, make_str, make_string_arg,
7        prop_name_matches, read_static_string, split_namespace,
8    },
9    config::ExtraCallerConfig,
10    dictionary_imports::{ImportKind, InjectedImports},
11    pre_pass::CallerMap,
12};
13use std::collections::BTreeMap;
14use swc_core::ecma::ast::*;
15
16/// How the namespace of an extra caller call-site was statically matched.
17#[derive(Debug)]
18pub enum ExtraNamespaceMatch {
19    /// Positional argument at `index` held the namespace string.
20    Argument {
21        index: usize,
22        full_namespace: String,
23    },
24    /// The namespace was read from a property of the options-object argument.
25    Option {
26        argument_index: usize,
27        full_namespace: String,
28    },
29    /// The namespace is a compile-time constant.
30    Fixed { full_namespace: String },
31}
32
33impl ExtraNamespaceMatch {
34    /// The matched namespace, including any `dictionaryKey.keyPrefix` suffix.
35    pub fn full_namespace(&self) -> &str {
36        match self {
37            ExtraNamespaceMatch::Argument { full_namespace, .. }
38            | ExtraNamespaceMatch::Option { full_namespace, .. }
39            | ExtraNamespaceMatch::Fixed { full_namespace } => full_namespace,
40        }
41    }
42}
43
44/// Statically resolves the namespace of an extra caller call-site from its
45/// config (positional argument, then fixed constant, then options-object
46/// property). Returns `None` when the namespace is absent or dynamic — the
47/// call is then left untouched and resolves through the runtime registry.
48pub fn resolve_extra_namespace(
49    extra_caller: &ExtraCallerConfig,
50    args: &[ExprOrSpread],
51) -> Option<ExtraNamespaceMatch> {
52    if let Some(index) = extra_caller.namespace_arg_index {
53        if let Some(arg) = args.get(index) {
54            if let Some(full_namespace) = read_static_string(&arg.expr) {
55                return Some(ExtraNamespaceMatch::Argument {
56                    index,
57                    full_namespace,
58                });
59            }
60        }
61    }
62
63    if let Some(fixed_namespace) = &extra_caller.fixed_namespace {
64        return Some(ExtraNamespaceMatch::Fixed {
65            full_namespace: fixed_namespace.clone(),
66        });
67    }
68
69    if let Some(option) = &extra_caller.namespace_option {
70        if let Some(arg) = args.get(option.argument_index) {
71            if let Expr::Object(object_lit) = &*arg.expr {
72                for object_prop in &object_lit.props {
73                    if let PropOrSpread::Prop(prop) = object_prop {
74                        if let Prop::KeyValue(KeyValueProp { key, value }) = &**prop {
75                            if prop_name_matches(key, &option.property) {
76                                if let Some(full_namespace) = read_static_string(value) {
77                                    return Some(ExtraNamespaceMatch::Option {
78                                        argument_index: option.argument_index,
79                                        full_namespace,
80                                    });
81                                }
82                                return None; // property present but dynamic
83                            }
84                        }
85                    }
86                }
87            }
88        }
89    }
90
91    None
92}
93
94/// Rewrites the namespace property of the options object at `argument_index`
95/// to the key-prefix remainder, or removes it entirely when the namespace had
96/// no nested part — so the runtime helper does not re-apply the dictionary key
97/// as a lookup prefix.
98pub fn rewrite_namespace_option(
99    args: &mut [ExprOrSpread],
100    argument_index: usize,
101    property: &str,
102    key_prefix: &str,
103) {
104    let Some(arg) = args.get_mut(argument_index) else {
105        return;
106    };
107    let Expr::Object(object_lit) = &mut *arg.expr else {
108        return;
109    };
110
111    if key_prefix.is_empty() {
112        object_lit.props.retain(|object_prop| {
113            if let PropOrSpread::Prop(prop) = object_prop {
114                if let Prop::KeyValue(KeyValueProp { key, .. }) = &**prop {
115                    return !prop_name_matches(key, property);
116                }
117            }
118            true
119        });
120        return;
121    }
122
123    for object_prop in &mut object_lit.props {
124        if let PropOrSpread::Prop(prop) = object_prop {
125            if let Prop::KeyValue(KeyValueProp { key, value }) = &mut **prop {
126                if prop_name_matches(key, property) {
127                    *value = Box::new(Expr::Lit(Lit::Str(make_str(key_prefix))));
128                }
129            }
130        }
131    }
132}
133
134/// Everything the optimize transform needs to rewrite an extra-caller call
135/// site, gathered so no adapter-specific decision is taken in `optimize.rs`.
136///
137/// A build with no `extraCallers` configured never constructs one — the
138/// optimize transform holds `Option<ExtraCallerContext>` and skips every branch
139/// guarded by it, so the base intlayer rewrite is untouched by the adapters.
140pub struct ExtraCallerContext<'a> {
141    /// Descriptors injected by the compat packages' bundler plugins.
142    pub extra_callers: &'a [ExtraCallerConfig],
143    /// Per-dictionary import mode overrides.
144    pub dictionary_mode_map: &'a BTreeMap<String, String>,
145    /// The file's global import mode.
146    pub import_mode: ImportKind,
147    /// File-level dynamic decision: one import specifier serves every call, so
148    /// a global dynamic/fetch mode or any per-dictionary override flips all
149    /// rewritten compat calls to the dynamic helper.
150    pub use_dynamic_helpers: bool,
151}
152
153impl<'a> ExtraCallerContext<'a> {
154    /// Import kind an extra-caller call site resolves to for `dictionary_key`.
155    pub fn import_kind(&self, dictionary_key: &str) -> ImportKind {
156        if !self.use_dynamic_helpers {
157            return ImportKind::Static;
158        }
159
160        ImportKind::from_option(
161            self.dictionary_mode_map
162                .get(dictionary_key)
163                .map(String::as_str),
164        )
165        .filter(|kind| kind.is_dynamic_helper())
166        .unwrap_or(match self.import_mode {
167            ImportKind::Fetch => ImportKind::Fetch,
168            _ => ImportKind::Dynamic,
169        })
170    }
171
172    /// Rewrites an extra-caller call site: the namespace is replaced by (or
173    /// prefixed with) a pre-imported dictionary, plus the dictionary key and
174    /// nested key prefix the helper needs.
175    pub fn rewrite_call(
176        &self,
177        call: &mut CallExpr,
178        extra_index: usize,
179        imports: &mut InjectedImports,
180    ) {
181        let extra_caller = &self.extra_callers[extra_index];
182
183        let Some(namespace_match) = resolve_extra_namespace(extra_caller, &call.args) else {
184            return; // filtered by the pre-pass — stay safe
185        };
186
187        let (dictionary_key, key_prefix) = {
188            let (dictionary_key, key_prefix) = split_namespace(namespace_match.full_namespace());
189            (dictionary_key.to_string(), key_prefix.to_string())
190        };
191
192        let namespace_option_property: Option<String> = extra_caller
193            .namespace_option
194            .as_ref()
195            .map(|option| option.property.clone());
196
197        let import_kind = self.import_kind(&dictionary_key);
198        let ident = imports.ident_for(&dictionary_key, import_kind);
199        let is_dynamic_helper = import_kind.is_dynamic_helper();
200
201        match &namespace_match {
202            ExtraNamespaceMatch::Argument { index, .. } => {
203                // Positional namespace: replace the string with the dictionary,
204                // then (dynamic) key and (nested) prefix.
205                call.args[*index].expr = Box::new(Expr::Ident(ident));
206                let mut insert_at = index + 1;
207                if is_dynamic_helper {
208                    call.args
209                        .insert(insert_at, make_string_arg(&dictionary_key));
210                    insert_at += 1;
211                }
212                if !key_prefix.is_empty() {
213                    call.args.insert(insert_at, make_string_arg(&key_prefix));
214                }
215            }
216            ExtraNamespaceMatch::Fixed { .. } | ExtraNamespaceMatch::Option { .. } => {
217                // Fixed / option namespace: prepend the dictionary (and the key
218                // for the dynamic helper).
219                if is_dynamic_helper {
220                    call.args.insert(0, make_string_arg(&dictionary_key));
221                }
222                call.args.insert(0, make_ident_arg(ident));
223            }
224        }
225
226        if let ExtraNamespaceMatch::Option { argument_index, .. } = &namespace_match {
227            // The options object shifted right by the prepended args.
228            let shifted_index = argument_index + if is_dynamic_helper { 2 } else { 1 };
229            rewrite_namespace_option(
230                &mut call.args,
231                shifted_index,
232                namespace_option_property.as_deref().unwrap_or_default(),
233                &key_prefix,
234            );
235        }
236    }
237
238    /// Whether `package_specifier` exports at least one extra caller, so the
239    /// optimize transform knows the import is worth inspecting even though it
240    /// is not a native intlayer package.
241    pub fn owns_import_source(&self, package_specifier: &str) -> bool {
242        self.extra_callers.iter().any(|extra_caller| {
243            extra_caller
244                .import_sources
245                .iter()
246                .any(|source| source == package_specifier)
247        })
248    }
249
250    /// Re-points one import specifier at the dictionary-accepting replacement
251    /// its descriptor declares, keeping the local alias so call sites read
252    /// unchanged. Specifiers whose local name is not a registered extra caller
253    /// — dropped by the pre-pass because a call site was unresolvable — keep
254    /// their original import.
255    pub fn rewrite_import_specifier(
256        &self,
257        named: &mut ImportNamedSpecifier,
258        package_specifier: &str,
259        caller_map: &CallerMap,
260    ) {
261        let imported_name = imported_specifier_name(named);
262        let local_name = named.local.sym.to_string();
263
264        let is_registered_extra = caller_map
265            .get(&local_name)
266            .is_some_and(|meta| meta.extra_index.is_some());
267        if !is_registered_extra {
268            return;
269        }
270
271        let Some(extra_caller) = self.extra_callers.iter().find(|extra_caller| {
272            extra_caller
273                .import_sources
274                .iter()
275                .any(|source| source == package_specifier)
276                && extra_caller.caller_name == imported_name
277        }) else {
278            return;
279        };
280
281        let replacement_name = if self.use_dynamic_helpers {
282            &extra_caller.dynamic_replacement
283        } else {
284            &extra_caller.static_replacement
285        };
286
287        named.imported = Some(ModuleExportName::Ident(make_ident(replacement_name)));
288    }
289}