swc_ecma_transforms_optimization/
const_modules.rs

1use std::{
2    collections::{HashMap, HashSet},
3    sync::Arc,
4};
5
6use bytes_str::BytesStr;
7use dashmap::DashMap;
8use once_cell::sync::Lazy;
9use rustc_hash::{FxBuildHasher, FxHashMap};
10use swc_atoms::{atom, Atom};
11use swc_common::{
12    errors::HANDLER,
13    sync::Lrc,
14    util::{move_map::MoveMap, take::Take},
15    FileName, SourceMap,
16};
17use swc_ecma_ast::*;
18use swc_ecma_parser::parse_file_as_expr;
19use swc_ecma_utils::drop_span;
20use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
21
22pub fn const_modules(
23    cm: Lrc<SourceMap>,
24    globals: FxHashMap<Atom, FxHashMap<Atom, BytesStr>>,
25) -> impl Pass {
26    visit_mut_pass(ConstModules {
27        globals: globals
28            .into_iter()
29            .map(|(src, map)| {
30                let map = map
31                    .into_iter()
32                    .map(|(key, value)| {
33                        let value = parse_option(&cm, &key, value);
34
35                        (key, value)
36                    })
37                    .collect();
38
39                (src, map)
40            })
41            .collect(),
42        scope: Default::default(),
43    })
44}
45
46fn parse_option(cm: &SourceMap, name: &str, src: BytesStr) -> Arc<Expr> {
47    static CACHE: Lazy<DashMap<BytesStr, Arc<Expr>, FxBuildHasher>> = Lazy::new(DashMap::default);
48
49    let fm = cm.new_source_file(
50        FileName::Internal(format!("<const-module-{name}.js>")).into(),
51        src,
52    );
53    if let Some(expr) = CACHE.get(&fm.src) {
54        return expr.clone();
55    }
56
57    let expr = parse_file_as_expr(
58        &fm,
59        Default::default(),
60        Default::default(),
61        None,
62        &mut Vec::new(),
63    )
64    .map_err(|e| {
65        if HANDLER.is_set() {
66            HANDLER.with(|h| e.into_diagnostic(h).emit())
67        }
68    })
69    .map(drop_span)
70    .unwrap_or_else(|()| {
71        panic!(
72            "failed to parse jsx option {}: '{}' is not an expression",
73            name, fm.src,
74        )
75    });
76
77    let expr = Arc::new(*expr);
78
79    CACHE.insert(fm.src.clone(), expr.clone());
80
81    expr
82}
83
84struct ConstModules {
85    globals: HashMap<Atom, HashMap<Atom, Arc<Expr>>>,
86    scope: Scope,
87}
88
89#[derive(Default)]
90struct Scope {
91    namespace: HashSet<Id>,
92    imported: HashMap<Atom, Arc<Expr>>,
93}
94
95impl VisitMut for ConstModules {
96    noop_visit_mut_type!(fail);
97
98    fn visit_mut_module_items(&mut self, n: &mut Vec<ModuleItem>) {
99        *n = n.take().move_flat_map(|item| match item {
100            ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
101                let entry = self.globals.get(&import.src.value);
102                if let Some(entry) = entry {
103                    for s in &import.specifiers {
104                        match *s {
105                            ImportSpecifier::Named(ref s) => {
106                                let imported = s
107                                    .imported
108                                    .as_ref()
109                                    .map(|m| match m {
110                                        ModuleExportName::Ident(id) => &id.sym,
111                                        ModuleExportName::Str(s) => &s.value,
112                                    })
113                                    .unwrap_or(&s.local.sym);
114                                let value = entry.get(imported).cloned().unwrap_or_else(|| {
115                                    panic!(
116                                        "The requested const_module `{}` does not provide an \
117                                         export named `{}`",
118                                        import.src.value, imported
119                                    )
120                                });
121                                self.scope.imported.insert(imported.clone(), value);
122                            }
123                            ImportSpecifier::Namespace(ref s) => {
124                                self.scope.namespace.insert(s.local.to_id());
125                            }
126                            ImportSpecifier::Default(ref s) => {
127                                let imported = &s.local.sym;
128                                let default_import_key = atom!("default");
129                                let value =
130                                    entry.get(&default_import_key).cloned().unwrap_or_else(|| {
131                                        panic!(
132                                            "The requested const_module `{}` does not provide \
133                                             default export",
134                                            import.src.value
135                                        )
136                                    });
137                                self.scope.imported.insert(imported.clone(), value);
138                            }
139                        };
140                    }
141
142                    None
143                } else {
144                    Some(import.into())
145                }
146            }
147            _ => Some(item),
148        });
149
150        if self.scope.imported.is_empty() && self.scope.namespace.is_empty() {
151            return;
152        }
153
154        n.visit_mut_children_with(self);
155    }
156
157    fn visit_mut_expr(&mut self, n: &mut Expr) {
158        match n {
159            Expr::Ident(ref id @ Ident { ref sym, .. }) => {
160                if let Some(value) = self.scope.imported.get(sym) {
161                    *n = (**value).clone();
162                    return;
163                }
164
165                if self.scope.namespace.contains(&id.to_id()) {
166                    panic!(
167                        "The const_module namespace `{sym}` cannot be used without member accessor"
168                    )
169                }
170            }
171            Expr::Member(MemberExpr { obj, prop, .. }) if obj.is_ident() => {
172                if let Some(module_name) = obj
173                    .as_ident()
174                    .filter(|member_obj| self.scope.namespace.contains(&member_obj.to_id()))
175                    .map(|member_obj| &member_obj.sym)
176                {
177                    let imported_name = match prop {
178                        MemberProp::Ident(ref id) => &id.sym,
179                        MemberProp::Computed(ref p) => match &*p.expr {
180                            Expr::Lit(Lit::Str(s)) => &s.value,
181                            _ => return,
182                        },
183                        MemberProp::PrivateName(..) => return,
184                    };
185
186                    let value = self
187                        .globals
188                        .get(module_name)
189                        .and_then(|entry| entry.get(imported_name))
190                        .unwrap_or_else(|| {
191                            panic!(
192                                "The requested const_module `{module_name}` does not provide an \
193                                 export named `{imported_name}`"
194                            )
195                        });
196
197                    *n = (**value).clone();
198                } else {
199                    n.visit_mut_children_with(self);
200                }
201            }
202            _ => {
203                n.visit_mut_children_with(self);
204            }
205        };
206    }
207
208    fn visit_mut_prop(&mut self, n: &mut Prop) {
209        match n {
210            Prop::Shorthand(id) => {
211                if let Some(value) = self.scope.imported.get(&id.sym) {
212                    *n = Prop::KeyValue(KeyValueProp {
213                        key: id.take().into(),
214                        value: Box::new((**value).clone()),
215                    });
216                    return;
217                }
218
219                if self.scope.namespace.contains(&id.to_id()) {
220                    panic!(
221                        "The const_module namespace `{}` cannot be used without member accessor",
222                        id.sym
223                    )
224                }
225            }
226            _ => n.visit_mut_children_with(self),
227        }
228    }
229}