swc_node_bundler/loaders/
swc.rs

1use 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
33/// JavaScript loader
34pub 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            // Handle built-in modules
90            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            // Handle disabled modules, eg when `browser` has a field
105            // set to `false`
106            } else {
107                // TODO: When we know the calling context is ESM
108                // TODO: switch to `export default {}`.
109                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            // We run transform at this phase to strip out unused dependencies.
257            //
258            // Note that we don't apply compat transform at loading phase.
259            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}