swc_node_bundler/loaders/
swc.rs1use std::{collections::HashMap, env, sync::Arc};
2
3use anyhow::{bail, Context, Error};
4use helpers::Helpers;
5use rustc_hash::FxHashMap;
6use swc::{
7 config::{GlobalInliningPassEnvs, InputSourceMap, IsModule, JscConfig, TransformConfig},
8 try_with_handler,
9};
10use swc_atoms::Atom;
11use swc_bundler::{Load, ModuleData};
12use swc_common::{
13 comments::{NoopComments, SingleThreadedComments},
14 errors::{Handler, HANDLER},
15 sync::Lrc,
16 FileName, Mark, DUMMY_SP,
17};
18use swc_ecma_ast::{noop_pass, EsVersion, Expr, Lit, Module, Program, Str};
19use swc_ecma_parser::{parse_file_as_module, Syntax};
20use swc_ecma_transforms::{
21 helpers,
22 optimization::{
23 inline_globals,
24 simplify::{dead_branch_remover, expr_simplifier},
25 },
26 react::jsx,
27 resolver,
28 typescript::typescript,
29};
30
31use crate::loaders::json::load_json_as_module;
32
33pub struct SwcLoader {
35 compiler: Arc<swc::Compiler>,
36 options: swc::config::Options,
37}
38
39impl SwcLoader {
40 pub fn new(compiler: Arc<swc::Compiler>, options: swc::config::Options) -> Self {
41 SwcLoader { compiler, options }
42 }
43
44 fn env_map(&self) -> Lrc<FxHashMap<Atom, Expr>> {
45 let mut m = HashMap::default();
46
47 let envs = self
48 .options
49 .config
50 .jsc
51 .transform
52 .as_ref()
53 .and_then(|t| t.optimizer.as_ref())
54 .and_then(|o| o.globals.as_ref())
55 .map(|g| g.envs.clone())
56 .unwrap_or_default();
57
58 let envs_map: FxHashMap<_, _> = match envs {
59 GlobalInliningPassEnvs::Map(m) => m,
60 GlobalInliningPassEnvs::List(envs) => envs
61 .into_iter()
62 .map(|name| {
63 let value = env::var(&name).ok();
64 (name.into(), value.unwrap_or_default().into())
65 })
66 .collect(),
67 };
68
69 for (k, v) in envs_map {
70 m.insert(
71 k,
72 Lit::Str(Str {
73 span: DUMMY_SP,
74 raw: None,
75 value: v.into(),
76 })
77 .into(),
78 );
79 }
80
81 Lrc::new(m)
82 }
83
84 fn load_with_handler(&self, handler: &Handler, name: &FileName) -> Result<ModuleData, Error> {
85 tracing::debug!("JsLoader.load({})", name);
86 let helpers = Helpers::new(false);
87
88 if let FileName::Custom(id) = name {
89 if id.starts_with("node:") {
91 let fm = self
92 .compiler
93 .cm
94 .new_source_file(name.clone().into(), "".to_string());
95 return Ok(ModuleData {
96 fm,
97 module: Module {
98 span: DUMMY_SP,
99 body: Default::default(),
100 shebang: Default::default(),
101 },
102 helpers: Default::default(),
103 });
104 } else {
107 let fm = self
110 .compiler
111 .cm
112 .new_source_file(name.clone().into(), "module.exports = {}".to_string());
113
114 let module = parse_file_as_module(
115 &fm,
116 Syntax::Es(Default::default()),
117 Default::default(),
118 None,
119 &mut Vec::new(),
120 )
121 .unwrap();
122 return Ok(ModuleData {
123 fm,
124 module,
125 helpers: Default::default(),
126 });
127 }
128 }
129
130 let fm = self
131 .compiler
132 .cm
133 .load_file(match name {
134 FileName::Real(v) => v,
135 _ => bail!("swc-loader only accepts path. Got `{}`", name),
136 })
137 .with_context(|| format!("failed to load file `{name}`"))?;
138
139 if let FileName::Real(path) = name {
140 if let Some(ext) = path.extension() {
141 if ext == "json" {
142 let module = load_json_as_module(&fm)
143 .with_context(|| format!("failed to load json file at {}", fm.name))?;
144 return Ok(ModuleData {
145 fm,
146 module,
147 helpers: Default::default(),
148 });
149 }
150 }
151 }
152
153 tracing::trace!("JsLoader.load: loaded");
154
155 let program = if fm.name.to_string().contains("node_modules") {
156 let comments = self.compiler.comments().clone();
157
158 let mut program = self.compiler.parse_js(
159 fm.clone(),
160 handler,
161 EsVersion::Es2020,
162 Default::default(),
163 IsModule::Bool(true),
164 Some(&comments),
165 )?;
166
167 helpers::HELPERS.set(&helpers, || {
168 HANDLER.set(handler, || {
169 let unresolved_mark = Mark::new();
170 let top_level_mark = Mark::new();
171
172 program.mutate(&mut resolver(unresolved_mark, top_level_mark, false));
173 program.mutate(&mut typescript(
174 Default::default(),
175 unresolved_mark,
176 top_level_mark,
177 ));
178
179 program.mutate(&mut jsx(
180 self.compiler.cm.clone(),
181 None::<NoopComments>,
182 Default::default(),
183 top_level_mark,
184 unresolved_mark,
185 ));
186
187 program.mutate(&mut inline_globals(
188 self.env_map(),
189 Default::default(),
190 Default::default(),
191 Default::default(),
192 ));
193
194 program.mutate(&mut expr_simplifier(unresolved_mark, Default::default()));
195
196 program.mutate(&mut dead_branch_remover(unresolved_mark));
197
198 program
199 })
200 })
201 } else {
202 let comments = SingleThreadedComments::default();
203 let config = self.compiler.parse_js_as_input(
204 fm.clone(),
205 None,
206 handler,
207 &swc::config::Options {
208 config: {
209 let c = &self.options.config;
210 swc::config::Config {
211 jsc: JscConfig {
212 transform: {
213 c.jsc
214 .transform
215 .as_ref()
216 .map(|c| TransformConfig {
217 react: c.react.clone(),
218 const_modules: c.const_modules.clone(),
219 optimizer: None,
220 legacy_decorator: c.legacy_decorator,
221 decorator_metadata: c.decorator_metadata,
222 hidden: Default::default(),
223 ..Default::default()
224 })
225 .into()
226 },
227 external_helpers: true.into(),
228 ..c.jsc.clone()
229 },
230 module: None,
231 minify: false.into(),
232 input_source_map: InputSourceMap::Bool(false).into(),
233 ..c.clone()
234 }
235 },
236 skip_helper_injection: true,
237 disable_hygiene: false,
238 disable_fixer: true,
239 top_level_mark: self.options.top_level_mark,
240 cwd: self.options.cwd.clone(),
241 caller: None,
242 filename: String::new(),
243 config_file: None,
244 root: None,
245 swcrc: true,
246 env_name: { env::var("NODE_ENV").unwrap_or_else(|_| "development".into()) },
247 ..Default::default()
248 },
249 &fm.name,
250 Some(&comments),
251 |_| noop_pass(),
252 )?;
253
254 tracing::trace!("JsLoader.load: loaded config");
255
256 let program = if let Some(config) = config {
260 let mut program = config.program;
261 let pass = config.pass;
262
263 helpers::HELPERS.set(&helpers, || {
264 HANDLER.set(handler, || {
265 let unresolved_mark = Mark::new();
266 let top_level_mark = Mark::new();
267
268 program.mutate(&mut resolver(unresolved_mark, top_level_mark, false));
269 program.mutate(&mut typescript(
270 Default::default(),
271 unresolved_mark,
272 top_level_mark,
273 ));
274
275 program.mutate(&mut jsx(
276 self.compiler.cm.clone(),
277 None::<NoopComments>,
278 Default::default(),
279 top_level_mark,
280 unresolved_mark,
281 ));
282
283 program.mutate(&mut inline_globals(
284 self.env_map(),
285 Default::default(),
286 Default::default(),
287 Default::default(),
288 ));
289
290 program.mutate(&mut expr_simplifier(unresolved_mark, Default::default()));
291 program.mutate(&mut dead_branch_remover(unresolved_mark));
292
293 program.apply(pass)
294 })
295 })
296 } else {
297 let comments = self.compiler.comments().clone();
298
299 self.compiler
300 .parse_js(
301 fm.clone(),
302 handler,
303 EsVersion::Es2020,
304 config.as_ref().map(|v| v.syntax).unwrap_or_default(),
305 IsModule::Bool(true),
306 Some(&comments),
307 )
308 .context("tried to parse as ecmascript as it's excluded by .swcrc")?
309 };
310
311 tracing::trace!("JsLoader.load: applied transforms");
312
313 program
314 };
315
316 match program {
317 Program::Module(module) => Ok(ModuleData {
318 fm,
319 module,
320 helpers,
321 }),
322 _ => unreachable!(),
323 }
324 }
325}
326
327impl Load for SwcLoader {
328 fn load(&self, name: &FileName) -> Result<ModuleData, Error> {
329 try_with_handler(self.compiler.cm.clone(), Default::default(), |handler| {
330 self.load_with_handler(handler, name)
331 })
332 .map_err(|e| e.to_pretty_error())
333 }
334}