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