Skip to main content

intlayer_swc_plugin/
pre_pass.rs

1//! First traversal: discovers which local identifiers refer to an intlayer or
2//! compat-adapter caller, which package each was imported from, and which of
3//! those packages resolve a dictionary to a dynamic/fetch loader.
4//!
5//! The optimize transform needs every answer *before* it starts rewriting,
6//! because a single import specifier serves every call site in the file — and
7//! an import declaration may appear after the calls it governs.
8//!
9//! Adapter-specific reasoning is delegated: namespace resolution lives in
10//! [`crate::extra_caller`], so a build with no `extraCallers` runs the native
11//! half of this pass alone.
12
13use crate::{
14    ast::{callee_ident_name, imported_specifier_name, read_static_string, split_namespace},
15    config::ExtraCallerConfig,
16    dictionary_imports::ImportKind,
17    extra_caller::resolve_extra_namespace,
18    packages::{NATIVE_CALLER_NAMES, PACKAGE_LIST},
19};
20use std::collections::{BTreeMap, HashSet};
21use swc_core::ecma::{
22    ast::*,
23    visit::{Visit, VisitWith},
24};
25
26/// Metadata stored in the pre-pass caller map value.
27/// For native callers this is always the original name; for extra callers we
28/// also keep the index of the matching [`ExtraCallerConfig`].
29#[derive(Clone, Debug)]
30pub struct CallerMeta {
31    /// Original function name (e.g. `"useIntlayer"` or `"useTranslation"`).
32    pub original_name: String,
33    /// Index of the matching extra caller config (`None` for native callers).
34    pub extra_index: Option<usize>,
35    /// Package specifier the caller was imported from, e.g. `"react-intlayer"`.
36    /// Only recorded for native callers, whose helper family is decided per
37    /// package: `useIntlayer` imported from `intlayer` keeps the static helper
38    /// even when a sibling import from `react-intlayer` goes dynamic.
39    pub package: Option<String>,
40}
41
42/// Maps a local identifier name to the caller it was imported as.
43pub type CallerMap = BTreeMap<String, CallerMeta>;
44
45/// Outcome of the pre-pass.
46pub struct PrePassResult {
47    /// Local identifier name → caller metadata, with the extra callers that
48    /// have at least one unresolvable call site already removed.
49    pub caller_map: CallerMap,
50    /// Packages whose native `useIntlayer` calls resolve at least one
51    /// dictionary overridden to the `dynamic` import mode.
52    pub packages_with_dynamic_call: HashSet<String>,
53    /// Packages whose native `useIntlayer` calls resolve at least one
54    /// dictionary overridden to the `fetch` import mode.
55    pub packages_with_fetch_call: HashSet<String>,
56    /// An extra (compat) caller resolves to a dynamic/fetch dictionary.
57    pub extra_has_dynamic_call: bool,
58}
59
60struct PrePassVisitor<'a> {
61    dictionary_mode_map: &'a BTreeMap<String, String>,
62    extra_callers: &'a [ExtraCallerConfig],
63    packages_with_dynamic_call: HashSet<String>,
64    packages_with_fetch_call: HashSet<String>,
65    extra_has_dynamic_call: bool,
66    /// Local extra-caller names with at least one unresolvable call site —
67    /// rewriting the shared import while leaving those calls untouched would
68    /// hand a raw namespace string to the dictionary-accepting helper.
69    unresolvable_extra_locals: HashSet<String>,
70    caller_map: CallerMap,
71}
72
73impl PrePassVisitor<'_> {
74    /// Per-dictionary import mode override, when one is configured.
75    fn dictionary_override(&self, dictionary_key: &str) -> Option<ImportKind> {
76        ImportKind::from_option(
77            self.dictionary_mode_map
78                .get(dictionary_key)
79                .map(String::as_str),
80        )
81    }
82
83    /// Returns `true` when the dictionary is overridden to a per-locale loader.
84    fn is_dynamic_dictionary(&self, dictionary_key: &str) -> bool {
85        self.dictionary_override(dictionary_key)
86            .is_some_and(|kind| kind.is_dynamic_helper())
87    }
88
89    /// Inspects an extra (compat) caller call site: resolve its namespace, note
90    /// a dynamic dictionary, or mark the local name unrewritable.
91    fn visit_extra_caller_call(&mut self, callee_name: &str, call: &CallExpr, extra_index: usize) {
92        let extra_caller = &self.extra_callers[extra_index];
93
94        match resolve_extra_namespace(extra_caller, &call.args) {
95            Some(namespace_match) => {
96                let (dictionary_key, _prefix) = split_namespace(namespace_match.full_namespace());
97                if self.is_dynamic_dictionary(dictionary_key) {
98                    self.extra_has_dynamic_call = true;
99                }
100            }
101            None => {
102                self.unresolvable_extra_locals
103                    .insert(callee_name.to_string());
104            }
105        }
106    }
107
108    /// Inspects a native `useIntlayer` call site, noting whether its package
109    /// must switch to a per-locale loader.
110    ///
111    /// The dictionary key is the whole first argument: native callers look the
112    /// dictionary up in the registry by that exact key, with no
113    /// `dictionary.field` namespace convention to split on.
114    fn visit_native_call(&mut self, call: &CallExpr, package: &str) {
115        let Some(dictionary_key) = call
116            .args
117            .first()
118            .and_then(|arg| read_static_string(&arg.expr))
119        else {
120            return;
121        };
122
123        match self.dictionary_override(&dictionary_key) {
124            Some(ImportKind::Dynamic) => {
125                self.packages_with_dynamic_call.insert(package.to_string());
126            }
127            Some(ImportKind::Fetch) => {
128                self.packages_with_fetch_call.insert(package.to_string());
129            }
130            _ => {}
131        }
132    }
133}
134
135/// Collects `local identifier → caller` for every recognised caller the module
136/// imports.
137///
138/// Import declarations only appear at the top level of a module, so the body is
139/// scanned directly. Doing it before any call is inspected keeps the result
140/// independent of where the imports sit relative to the calls they govern.
141fn collect_caller_map(program: &Program, extra_callers: &[ExtraCallerConfig]) -> CallerMap {
142    let mut caller_map = CallerMap::new();
143
144    let Program::Module(module) = program else {
145        return caller_map;
146    };
147
148    for item in &module.body {
149        let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item else {
150            continue;
151        };
152
153        let package_specifier = import.src.value.as_str().unwrap_or_default();
154
155        let is_native_package = PACKAGE_LIST.contains(&package_specifier);
156
157        // The extra callers this package exports, matched once for the whole
158        // declaration instead of re-scanning every descriptor's
159        // `import_sources` for each of its specifiers.
160        let extra_callers_for_package: Vec<(usize, &str)> = extra_callers
161            .iter()
162            .enumerate()
163            .filter(|(_, extra_caller)| {
164                extra_caller
165                    .import_sources
166                    .iter()
167                    .any(|source| source == package_specifier)
168            })
169            .map(|(extra_index, extra_caller)| {
170                (extra_index, extra_caller.caller_name.as_str())
171            })
172            .collect();
173
174        if !is_native_package && extra_callers_for_package.is_empty() {
175            continue;
176        }
177
178        for specifier in &import.specifiers {
179            let ImportSpecifier::Named(named) = specifier else {
180                continue;
181            };
182            let imported_name = imported_specifier_name(named);
183
184            // An extra caller wins over a native name: a compat package
185            // re-exporting an intlayer getter is still driven by its descriptor.
186            let meta = extra_callers_for_package
187                .iter()
188                .find(|(_, caller_name)| *caller_name == imported_name)
189                .map(|(extra_index, _)| CallerMeta {
190                    original_name: imported_name.clone(),
191                    extra_index: Some(*extra_index),
192                    package: None,
193                })
194                .or_else(|| {
195                    let is_native_caller = is_native_package
196                        && NATIVE_CALLER_NAMES.contains(&imported_name.as_str());
197
198                    is_native_caller.then(|| CallerMeta {
199                        original_name: imported_name.clone(),
200                        extra_index: None,
201                        package: Some(package_specifier.to_string()),
202                    })
203                });
204
205            if let Some(meta) = meta {
206                caller_map.insert(named.local.sym.to_string(), meta);
207            }
208        }
209    }
210
211    caller_map
212}
213
214impl Visit for PrePassVisitor<'_> {
215    fn visit_call_expr(&mut self, call: &CallExpr) {
216        call.visit_children_with(self);
217
218        let Some(callee_name) = callee_ident_name(&call.callee) else {
219            return;
220        };
221
222        let Some(meta) = self.caller_map.get(callee_name).cloned() else {
223            return;
224        };
225
226        match meta.extra_index {
227            Some(extra_index) => self.visit_extra_caller_call(callee_name, call, extra_index),
228            None if meta.original_name == "useIntlayer" => {
229                if let Some(package) = meta.package.as_deref() {
230                    self.visit_native_call(call, package);
231                }
232            }
233            None => {}
234        }
235    }
236}
237
238/// Runs the pre-pass over `program`.
239pub fn run_pre_pass(
240    program: &Program,
241    dictionary_mode_map: &BTreeMap<String, String>,
242    extra_callers: &[ExtraCallerConfig],
243) -> PrePassResult {
244    let mut visitor = PrePassVisitor {
245        dictionary_mode_map,
246        extra_callers,
247        packages_with_dynamic_call: HashSet::new(),
248        packages_with_fetch_call: HashSet::new(),
249        extra_has_dynamic_call: false,
250        unresolvable_extra_locals: HashSet::new(),
251        caller_map: collect_caller_map(program, extra_callers),
252    };
253    program.visit_with(&mut visitor);
254
255    let unresolvable_extra_locals = visitor.unresolvable_extra_locals;
256
257    // Extra callers with an unresolvable call site keep their original
258    // implementation: rewriting the shared import while leaving those calls
259    // untouched would hand a raw namespace string to the dictionary helper.
260    let mut caller_map = visitor.caller_map;
261    caller_map.retain(|local_name, meta| {
262        meta.extra_index.is_none() || !unresolvable_extra_locals.contains(local_name)
263    });
264
265    PrePassResult {
266        caller_map,
267        packages_with_dynamic_call: visitor.packages_with_dynamic_call,
268        packages_with_fetch_call: visitor.packages_with_fetch_call,
269        extra_has_dynamic_call: visitor.extra_has_dynamic_call,
270    }
271}