swc/
lib.rs

1//! The main crate of the swc project.
2//!
3//!
4//!
5//! # Customizing
6//!
7//!
8//! This is documentation for building custom build tools on top of swc.
9//!
10//! ## Dependency version management
11//!
12//! `swc` has [swc_css](https://docs.rs/swc_css), which re-exports required modules.
13//!
14//! ## Testing
15//!
16//! See [testing] and [swc_ecma_transforms_testing](https://docs.rs/swc_ecma_transforms_testing).
17//!
18//! ## Custom javascript transforms
19//!
20//!
21//!
22//! ### What is [Atom](swc_atoms::Atom)?
23//!
24//! It's basically an interned string. See [swc_atoms].
25//!
26//! ### Choosing between [Atom](swc_atoms::Atom) vs String
27//!
28//! You should  prefer [Atom](swc_atoms::Atom) over [String] if it's going
29//! to be stored in an AST node.
30//!
31//! See [swc_atoms] for detailed description.
32//!
33//! ### Fold vs VisitMut vs Visit
34//!
35//! See [swc_visit] for detailed description.
36//!
37//!
38//!  - [Fold](swc_ecma_visit::Fold)
39//!  - [VisitMut](swc_ecma_visit::VisitMut)
40//!  - [Visit](swc_ecma_visit::Visit)
41//!
42//!
43//! ### Variable management (Scoping)
44//!
45//! See [swc_ecma_transforms_base::resolver::resolver_with_mark].
46//!
47//! #### How identifiers work
48//!
49//! See the doc on [swc_ecma_ast::Ident] or on
50//! [swc_ecma_transforms_base::resolver::resolver_with_mark].
51//!
52//! #### Comparing two identifiers
53//!
54//! See [swc_ecma_utils::Id]. You can use [swc_ecma_utils::IdentLike::to_id] to
55//! extract important parts of an [swc_ecma_ast::Ident].
56//!
57//! #### Creating a unique identifier
58//!
59//! See [swc_ecma_utils::private_ident].
60//!
61//! #### Prepending statements
62//!
63//! If you want to prepend statements to the beginning of a file, you can use
64//! [swc_ecma_utils::prepend_stmts] or [swc_ecma_utils::prepend] if `len == 1`.
65//!
66//! These methods are aware of the fact that `"use strict"` directive should be
67//! first in a file, and insert statements after directives.
68//!
69//! ### Improving readability
70//!
71//! Each stuffs are documented at itself.
72//!
73//!  - If you are creating or binding an [swc_ecma_ast::Expr] with operator, you
74//!    can use [swc_ecma_ast::op].
75//!
76//!  - If you want to create [swc_ecma_ast::CallExpr], you can use
77//!    [swc_ecma_utils::ExprFactory::as_callee] to create `callee`.
78//!
79//!  - If you want to create [swc_ecma_ast::CallExpr] or
80//!    [swc_ecma_ast::NewExpr], you can use
81//!    [swc_ecma_utils::ExprFactory::as_arg] to create arguments.
82//!
83//!
84//!  - If you want to create [swc_ecma_ast::MemberExpr] where all identifiers
85//!    are static (e.g. `Object.prototype.hasOwnProperty`), you can use
86//!    [swc_ecma_utils::member_expr].
87//!
88//!  - If you want to create [swc_ecma_ast::MemberExpr], you can use
89//!    [swc_ecma_utils::ExprFactory::as_obj] to create object field.
90//!
91//!
92//! ### Reducing binary size
93//!
94//! The visitor expands to a lot of code. You can reduce it by using macros like
95//!
96//!  - [noop_fold_type](swc_ecma_visit::noop_fold_type)
97//!  - [noop_visit_mut_type](swc_ecma_visit::noop_visit_mut_type)
98//!  - [noop_visit_type](swc_ecma_visit::noop_visit_type)
99//!
100//! Note that this will make typescript-related nodes not processed, but it's
101//! typically fine as `typescript::strip` is invoked at the start and it removes
102//! typescript-specific nodes.
103//!
104//! ### Porting `expr.evaluate()` of babel
105//!
106//! See [swc_ecma_minifier::eval::Evaluator].
107#![deny(unused)]
108#![allow(clippy::too_many_arguments)]
109#![allow(clippy::mutable_key_type)]
110#![cfg_attr(docsrs, feature(doc_cfg))]
111
112pub extern crate swc_atoms as atoms;
113extern crate swc_common as common;
114
115use std::{
116    fs::{read_to_string, File},
117    io::ErrorKind,
118    path::{Path, PathBuf},
119    sync::Arc,
120};
121
122use anyhow::{bail, Context, Error};
123use base64::prelude::{Engine, BASE64_STANDARD};
124use common::{
125    comments::{Comment, SingleThreadedComments},
126    errors::HANDLER,
127};
128use jsonc_parser::{parse_to_serde_value, ParseOptions};
129use once_cell::sync::Lazy;
130use serde_json::error::Category;
131use swc_common::{
132    comments::Comments, errors::Handler, sync::Lrc, FileName, Mark, SourceFile, SourceMap, Spanned,
133    GLOBALS,
134};
135pub use swc_compiler_base::{PrintArgs, TransformOutput};
136pub use swc_config::types::{BoolConfig, BoolOr, BoolOrDataConfig};
137use swc_ecma_ast::{noop_pass, EsVersion, Pass, Program};
138use swc_ecma_codegen::Node;
139use swc_ecma_loader::resolvers::{
140    lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver,
141};
142use swc_ecma_minifier::option::{MangleCache, MinifyOptions, TopLevelOptions};
143use swc_ecma_parser::{EsSyntax, Syntax};
144use swc_ecma_transforms::{
145    fixer,
146    helpers::{self, Helpers},
147    hygiene,
148    modules::path::NodeImportResolver,
149    resolver,
150};
151use swc_ecma_transforms_base::fixer::paren_remover;
152use swc_ecma_visit::{FoldWith, VisitMutWith, VisitWith};
153pub use swc_error_reporters::handler::{try_with_handler, HandlerOpts};
154pub use swc_node_comments::SwcComments;
155pub use swc_sourcemap as sourcemap;
156use swc_timer::timer;
157#[cfg(feature = "isolated-dts")]
158use swc_typescript::fast_dts::FastDts;
159use tracing::warn;
160use url::Url;
161
162use crate::config::{
163    BuiltInput, Config, ConfigFile, InputSourceMap, IsModule, JsMinifyCommentOption,
164    JsMinifyOptions, Options, OutputCharset, Rc, RootMode, SourceMapsConfig,
165};
166
167mod builder;
168pub mod config;
169mod dropped_comments_preserver;
170mod plugin;
171pub mod wasm_analysis;
172pub mod resolver {
173    use std::path::PathBuf;
174
175    use rustc_hash::FxHashMap;
176    use swc_ecma_loader::{
177        resolvers::{lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver},
178        TargetEnv,
179    };
180
181    use crate::config::CompiledPaths;
182
183    pub type NodeResolver = CachingResolver<NodeModulesResolver>;
184
185    pub fn paths_resolver(
186        target_env: TargetEnv,
187        alias: FxHashMap<String, String>,
188        base_url: PathBuf,
189        paths: CompiledPaths,
190        preserve_symlinks: bool,
191    ) -> CachingResolver<TsConfigResolver<NodeModulesResolver>> {
192        let r = TsConfigResolver::new(
193            NodeModulesResolver::without_node_modules(target_env, alias, preserve_symlinks),
194            base_url,
195            paths,
196        );
197        CachingResolver::new(40, r)
198    }
199
200    pub fn environment_resolver(
201        target_env: TargetEnv,
202        alias: FxHashMap<String, String>,
203        preserve_symlinks: bool,
204    ) -> NodeResolver {
205        CachingResolver::new(
206            40,
207            NodeModulesResolver::new(target_env, alias, preserve_symlinks),
208        )
209    }
210}
211
212type SwcImportResolver = Arc<
213    NodeImportResolver<CachingResolver<TsConfigResolver<CachingResolver<NodeModulesResolver>>>>,
214>;
215
216/// All methods accept [Handler], which is a storage for errors.
217///
218/// The caller should check if the handler contains any errors after calling
219/// method.
220pub struct Compiler {
221    /// CodeMap
222    pub cm: Arc<SourceMap>,
223    comments: SwcComments,
224}
225
226/// These are **low-level** apis.
227impl Compiler {
228    pub fn comments(&self) -> &SwcComments {
229        &self.comments
230    }
231
232    /// Runs `op` in current compiler's context.
233    ///
234    /// Note: Other methods of `Compiler` already uses this internally.
235    pub fn run<R, F>(&self, op: F) -> R
236    where
237        F: FnOnce() -> R,
238    {
239        debug_assert!(
240            GLOBALS.is_set(),
241            "`swc_common::GLOBALS` is required for this operation"
242        );
243
244        op()
245    }
246
247    fn get_orig_src_map(
248        &self,
249        fm: &SourceFile,
250        input_src_map: &InputSourceMap,
251        comments: &[Comment],
252        is_default: bool,
253    ) -> Result<Option<sourcemap::SourceMap>, Error> {
254        self.run(|| -> Result<_, Error> {
255            let name = &fm.name;
256
257            let read_inline_sourcemap =
258                |data_url: &str| -> Result<Option<sourcemap::SourceMap>, Error> {
259                    let url = Url::parse(data_url).with_context(|| {
260                        format!("failed to parse inline source map url\n{data_url}")
261                    })?;
262
263                    let idx = match url.path().find("base64,") {
264                        Some(v) => v,
265                        None => {
266                            bail!("failed to parse inline source map: not base64: {:?}", url)
267                        }
268                    };
269
270                    let content = url.path()[idx + "base64,".len()..].trim();
271
272                    let res = BASE64_STANDARD
273                        .decode(content.as_bytes())
274                        .context("failed to decode base64-encoded source map")?;
275
276                    Ok(Some(sourcemap::SourceMap::from_slice(&res).context(
277                        "failed to read input source map from inlined base64 encoded string",
278                    )?))
279                };
280
281            let read_file_sourcemap =
282                |data_url: Option<&str>| -> Result<Option<sourcemap::SourceMap>, Error> {
283                    match &**name {
284                        FileName::Real(filename) => {
285                            let dir = match filename.parent() {
286                                Some(v) => v,
287                                None => {
288                                    bail!("unexpected: root directory is given as a input file")
289                                }
290                            };
291
292                            let map_path = match data_url {
293                                Some(data_url) => {
294                                    let mut map_path = dir.join(data_url);
295                                    if !map_path.exists() {
296                                        // Old behavior. This check would prevent
297                                        // regressions.
298                                        // Perhaps it shouldn't be supported. Sometimes
299                                        // developers don't want to expose their source
300                                        // code.
301                                        // Map files are for internal troubleshooting
302                                        // convenience.
303                                        let fallback_map_path =
304                                            PathBuf::from(format!("{}.map", filename.display()));
305                                        if fallback_map_path.exists() {
306                                            map_path = fallback_map_path;
307                                        } else {
308                                            bail!(
309                                                "failed to find input source map file {:?} in \
310                                                 {:?} file as either {:?} or with appended .map",
311                                                data_url,
312                                                filename.display(),
313                                                map_path.display(),
314                                            )
315                                        }
316                                    }
317
318                                    Some(map_path)
319                                }
320                                None => {
321                                    // Old behavior.
322                                    let map_path =
323                                        PathBuf::from(format!("{}.map", filename.display()));
324                                    if map_path.exists() {
325                                        Some(map_path)
326                                    } else {
327                                        None
328                                    }
329                                }
330                            };
331
332                            match map_path {
333                                Some(map_path) => {
334                                    let path = map_path.display().to_string();
335                                    let file = File::open(&path);
336
337                                    // If file is not found, we should return None.
338                                    // Some libraries generates source map but omit them from the
339                                    // npm package.
340                                    //
341                                    // See https://github.com/swc-project/swc/issues/8789#issuecomment-2105055772
342                                    if file
343                                        .as_ref()
344                                        .is_err_and(|err| err.kind() == ErrorKind::NotFound)
345                                    {
346                                        warn!(
347                                            "source map is specified by sourceMappingURL but \
348                                             there's no source map at `{}`",
349                                            path
350                                        );
351                                        return Ok(None);
352                                    }
353
354                                    // Old behavior.
355                                    let file = if !is_default {
356                                        file?
357                                    } else {
358                                        match file {
359                                            Ok(v) => v,
360                                            Err(_) => return Ok(None),
361                                        }
362                                    };
363
364                                    Ok(Some(sourcemap::SourceMap::from_reader(file).with_context(
365                                        || {
366                                            format!(
367                                                "failed to read input source map
368                                from file at {path}"
369                                            )
370                                        },
371                                    )?))
372                                }
373                                None => Ok(None),
374                            }
375                        }
376                        _ => Ok(None),
377                    }
378                };
379
380            let read_sourcemap = || -> Option<sourcemap::SourceMap> {
381                let s = "sourceMappingURL=";
382
383                let text = comments.iter().rev().find_map(|c| {
384                    let idx = c.text.rfind(s)?;
385                    let (_, url) = c.text.split_at(idx + s.len());
386
387                    Some(url.trim())
388                });
389
390                // Load original source map if possible
391                let result = match text {
392                    Some(text) if text.starts_with("data:") => read_inline_sourcemap(text),
393                    _ => read_file_sourcemap(text),
394                };
395                match result {
396                    Ok(r) => r,
397                    Err(err) => {
398                        tracing::error!("failed to read input source map: {:?}", err);
399                        None
400                    }
401                }
402            };
403
404            // Load original source map
405            match input_src_map {
406                InputSourceMap::Bool(false) => Ok(None),
407                InputSourceMap::Bool(true) => Ok(read_sourcemap()),
408                InputSourceMap::Str(ref s) => {
409                    if s == "inline" {
410                        Ok(read_sourcemap())
411                    } else {
412                        // Load source map passed by user
413                        Ok(Some(
414                            swc_sourcemap::SourceMap::from_slice(s.as_bytes()).context(
415                                "failed to read input source map from user-provided sourcemap",
416                            )?,
417                        ))
418                    }
419                }
420            }
421        })
422    }
423
424    /// This method parses a javascript / typescript file
425    pub fn parse_js(
426        &self,
427        fm: Arc<SourceFile>,
428        handler: &Handler,
429        target: EsVersion,
430        syntax: Syntax,
431        is_module: IsModule,
432        comments: Option<&dyn Comments>,
433    ) -> Result<Program, Error> {
434        swc_compiler_base::parse_js(
435            self.cm.clone(),
436            fm,
437            handler,
438            target,
439            syntax,
440            is_module,
441            comments,
442        )
443    }
444
445    /// Converts ast node to source string and sourcemap.
446    ///
447    ///
448    /// This method receives target file path, but does not write file to the
449    /// path. See: https://github.com/swc-project/swc/issues/1255
450    #[allow(clippy::too_many_arguments)]
451    pub fn print<T>(&self, node: &T, args: PrintArgs) -> Result<TransformOutput, Error>
452    where
453        T: Node + VisitWith<swc_compiler_base::IdentCollector>,
454    {
455        swc_compiler_base::print(self.cm.clone(), node, args)
456    }
457}
458
459/// High-level apis.
460impl Compiler {
461    pub fn new(cm: Arc<SourceMap>) -> Self {
462        Compiler {
463            cm,
464            comments: Default::default(),
465        }
466    }
467
468    #[tracing::instrument(skip_all)]
469    pub fn read_config(&self, opts: &Options, name: &FileName) -> Result<Option<Config>, Error> {
470        static CUR_DIR: Lazy<PathBuf> = Lazy::new(|| {
471            if cfg!(target_arch = "wasm32") {
472                PathBuf::new()
473            } else {
474                ::std::env::current_dir().unwrap()
475            }
476        });
477
478        self.run(|| -> Result<_, Error> {
479            let Options {
480                ref root,
481                root_mode,
482                swcrc,
483                config_file,
484                ..
485            } = opts;
486
487            let root = root.as_ref().unwrap_or(&CUR_DIR);
488
489            let swcrc_path = match config_file {
490                Some(ConfigFile::Str(s)) => Some(PathBuf::from(s.clone())),
491                _ => {
492                    if *swcrc {
493                        if let FileName::Real(ref path) = name {
494                            find_swcrc(path, root, *root_mode)
495                        } else {
496                            None
497                        }
498                    } else {
499                        None
500                    }
501                }
502            };
503
504            let config_file = match swcrc_path.as_deref() {
505                Some(s) => Some(load_swcrc(s)?),
506                _ => None,
507            };
508            let filename_path = match name {
509                FileName::Real(p) => Some(&**p),
510                _ => None,
511            };
512
513            if let Some(filename_path) = filename_path {
514                if let Some(config) = config_file {
515                    let dir = swcrc_path
516                        .as_deref()
517                        .and_then(|p| p.parent())
518                        .expect(".swcrc path should have parent dir");
519
520                    let mut config = config
521                        .into_config(Some(filename_path))
522                        .context("failed to process config file")?;
523
524                    if let Some(c) = &mut config {
525                        if c.jsc.base_url != PathBuf::new() {
526                            let joined = dir.join(&c.jsc.base_url);
527                            c.jsc.base_url = if cfg!(target_os = "windows")
528                                && c.jsc.base_url.as_os_str() == "."
529                            {
530                                dir.canonicalize().with_context(|| {
531                                    format!(
532                                        "failed to canonicalize base url using the path of \
533                                         .swcrc\nDir: {}\n(Used logic for windows)",
534                                        dir.display(),
535                                    )
536                                })?
537                            } else {
538                                joined.canonicalize().with_context(|| {
539                                    format!(
540                                        "failed to canonicalize base url using the path of \
541                                         .swcrc\nPath: {}\nDir: {}\nbaseUrl: {}",
542                                        joined.display(),
543                                        dir.display(),
544                                        c.jsc.base_url.display()
545                                    )
546                                })?
547                            };
548                        }
549                    }
550
551                    return Ok(config);
552                }
553
554                let config_file = config_file.unwrap_or_default();
555                let config = config_file.into_config(Some(filename_path))?;
556
557                return Ok(config);
558            }
559
560            let config = match config_file {
561                Some(config_file) => config_file.into_config(None)?,
562                None => Rc::default().into_config(None)?,
563            };
564
565            match config {
566                Some(config) => Ok(Some(config)),
567                None => {
568                    bail!("no config matched for file ({})", name)
569                }
570            }
571        })
572        .with_context(|| format!("failed to read .swcrc file for input file at `{name}`"))
573    }
574
575    /// This method returns [None] if a file should be skipped.
576    ///
577    /// This method handles merging of config.
578    ///
579    /// This method does **not** parse module.
580    #[tracing::instrument(skip_all)]
581    pub fn parse_js_as_input<'a, P>(
582        &'a self,
583        fm: Lrc<SourceFile>,
584        program: Option<Program>,
585        handler: &'a Handler,
586        opts: &Options,
587        name: &FileName,
588        comments: Option<&'a SingleThreadedComments>,
589        before_pass: impl 'a + FnOnce(&Program) -> P,
590    ) -> Result<Option<BuiltInput<impl 'a + Pass>>, Error>
591    where
592        P: 'a + Pass,
593    {
594        self.run(move || {
595            let _timer = timer!("Compiler.parse");
596
597            if let FileName::Real(ref path) = name {
598                if !opts.config.matches(path)? {
599                    return Ok(None);
600                }
601            }
602
603            let config = self.read_config(opts, name)?;
604            let config = match config {
605                Some(v) => v,
606                None => return Ok(None),
607            };
608
609            let built = opts.build_as_input(
610                &self.cm,
611                name,
612                move |syntax, target, is_module| match program {
613                    Some(v) => Ok(v),
614                    _ => self.parse_js(
615                        fm.clone(),
616                        handler,
617                        target,
618                        syntax,
619                        is_module,
620                        comments.as_ref().map(|v| v as _),
621                    ),
622                },
623                opts.output_path.as_deref(),
624                opts.source_root.clone(),
625                opts.source_file_name.clone(),
626                config.source_map_ignore_list.clone(),
627                handler,
628                Some(config),
629                comments,
630                before_pass,
631            )?;
632            Ok(Some(built))
633        })
634    }
635
636    pub fn run_transform<F, Ret>(&self, handler: &Handler, external_helpers: bool, op: F) -> Ret
637    where
638        F: FnOnce() -> Ret,
639    {
640        self.run(|| {
641            helpers::HELPERS.set(&Helpers::new(external_helpers), || HANDLER.set(handler, op))
642        })
643    }
644
645    #[tracing::instrument(skip_all)]
646    pub fn transform(
647        &self,
648        handler: &Handler,
649        program: Program,
650        external_helpers: bool,
651        mut pass: impl swc_ecma_visit::Fold,
652    ) -> Program {
653        self.run_transform(handler, external_helpers, || {
654            // Fold module
655            program.fold_with(&mut pass)
656        })
657    }
658
659    /// `custom_after_pass` is applied after swc transforms are applied.
660    ///
661    /// `program`: If you already parsed `Program`, you can pass it.
662    ///
663    /// # Guarantee
664    ///
665    /// `swc` invokes `custom_before_pass` after
666    ///
667    ///  - Handling decorators, if configured
668    ///  - Applying `resolver`
669    ///  - Stripping typescript nodes
670    ///
671    /// This means, you can use `noop_visit_type`, `noop_fold_type` and
672    /// `noop_visit_mut_type` in your visitor to reduce the binary size.
673    #[tracing::instrument(skip_all)]
674    pub fn process_js_with_custom_pass<P1, P2>(
675        &self,
676        fm: Arc<SourceFile>,
677        program: Option<Program>,
678        handler: &Handler,
679        opts: &Options,
680        comments: SingleThreadedComments,
681        custom_before_pass: impl FnOnce(&Program) -> P1,
682        custom_after_pass: impl FnOnce(&Program) -> P2,
683    ) -> Result<TransformOutput, Error>
684    where
685        P1: Pass,
686        P2: Pass,
687    {
688        self.run(|| -> Result<_, Error> {
689            let config = self.run(|| {
690                self.parse_js_as_input(
691                    fm.clone(),
692                    program,
693                    handler,
694                    opts,
695                    &fm.name,
696                    Some(&comments),
697                    |program| custom_before_pass(program),
698                )
699            })?;
700            let config = match config {
701                Some(v) => v,
702                None => {
703                    bail!("cannot process file because it's ignored by .swcrc")
704                }
705            };
706
707            let after_pass = custom_after_pass(&config.program);
708
709            let config = config.with_pass(|pass| (pass, after_pass));
710
711            let orig = if config.source_maps.enabled() {
712                self.get_orig_src_map(
713                    &fm,
714                    &config.input_source_map,
715                    config
716                        .comments
717                        .get_trailing(config.program.span_hi())
718                        .as_deref()
719                        .unwrap_or_default(),
720                    false,
721                )?
722            } else {
723                None
724            };
725
726            self.apply_transforms(handler, comments.clone(), fm.clone(), orig, config)
727        })
728    }
729
730    #[tracing::instrument(skip(self, handler, opts))]
731    pub fn process_js_file(
732        &self,
733        fm: Arc<SourceFile>,
734        handler: &Handler,
735        opts: &Options,
736    ) -> Result<TransformOutput, Error> {
737        self.process_js_with_custom_pass(
738            fm,
739            None,
740            handler,
741            opts,
742            SingleThreadedComments::default(),
743            |_| noop_pass(),
744            |_| noop_pass(),
745        )
746    }
747
748    #[tracing::instrument(skip_all)]
749    pub fn minify(
750        &self,
751        fm: Arc<SourceFile>,
752        handler: &Handler,
753        opts: &JsMinifyOptions,
754        extras: JsMinifyExtras,
755    ) -> Result<TransformOutput, Error> {
756        self.run(|| {
757            let _timer = timer!("Compiler::minify");
758
759            let target = opts.ecma.clone().into();
760
761            let (source_map, orig, source_map_url) = opts
762                .source_map
763                .as_ref()
764                .map(|obj| -> Result<_, Error> {
765                    let orig = obj.content.as_ref().map(|s| s.to_sourcemap()).transpose()?;
766
767                    Ok((SourceMapsConfig::Bool(true), orig, obj.url.as_deref()))
768                })
769                .unwrap_as_option(|v| {
770                    Some(Ok(match v {
771                        Some(true) => (SourceMapsConfig::Bool(true), None, None),
772                        _ => (SourceMapsConfig::Bool(false), None, None),
773                    }))
774                })
775                .unwrap()?;
776
777            let mut min_opts = MinifyOptions {
778                compress: opts
779                    .compress
780                    .clone()
781                    .unwrap_as_option(|default| match default {
782                        Some(true) | None => Some(Default::default()),
783                        _ => None,
784                    })
785                    .map(|v| v.into_config(self.cm.clone())),
786                mangle: opts
787                    .mangle
788                    .clone()
789                    .unwrap_as_option(|default| match default {
790                        Some(true) | None => Some(Default::default()),
791                        _ => None,
792                    }),
793                ..Default::default()
794            };
795
796            // top_level defaults to true if module is true
797
798            // https://github.com/swc-project/swc/issues/2254
799
800            if opts.keep_fnames {
801                if let Some(opts) = &mut min_opts.compress {
802                    opts.keep_fnames = true;
803                }
804                if let Some(opts) = &mut min_opts.mangle {
805                    opts.keep_fn_names = true;
806                }
807            }
808
809            let comments = SingleThreadedComments::default();
810
811            let mut program = self
812                .parse_js(
813                    fm.clone(),
814                    handler,
815                    target,
816                    Syntax::Es(EsSyntax {
817                        jsx: true,
818                        decorators: true,
819                        decorators_before_export: true,
820                        import_attributes: true,
821                        ..Default::default()
822                    }),
823                    opts.module,
824                    Some(&comments),
825                )
826                .context("failed to parse input file")?;
827
828            if opts.toplevel == Some(true) || program.is_module() {
829                if let Some(opts) = &mut min_opts.compress {
830                    if opts.top_level.is_none() {
831                        opts.top_level = Some(TopLevelOptions { functions: true });
832                    }
833                }
834
835                if let Some(opts) = &mut min_opts.mangle {
836                    if opts.top_level.is_none() {
837                        opts.top_level = Some(true);
838                    }
839                }
840            }
841
842            let source_map_names = if source_map.enabled() {
843                let mut v = swc_compiler_base::IdentCollector {
844                    names: Default::default(),
845                };
846
847                program.visit_with(&mut v);
848
849                v.names
850            } else {
851                Default::default()
852            };
853
854            let unresolved_mark = Mark::new();
855            let top_level_mark = Mark::new();
856
857            let is_mangler_enabled = min_opts.mangle.is_some();
858
859            program = self.run_transform(handler, false, || {
860                program.mutate(&mut paren_remover(Some(&comments)));
861
862                program.mutate(&mut resolver(unresolved_mark, top_level_mark, false));
863
864                let mut program = swc_ecma_minifier::optimize(
865                    program,
866                    self.cm.clone(),
867                    Some(&comments),
868                    None,
869                    &min_opts,
870                    &swc_ecma_minifier::option::ExtraOptions {
871                        unresolved_mark,
872                        top_level_mark,
873                        mangle_name_cache: extras.mangle_name_cache,
874                    },
875                );
876
877                if !is_mangler_enabled {
878                    program.visit_mut_with(&mut hygiene())
879                }
880                program.mutate(&mut fixer(Some(&comments as &dyn Comments)));
881                program
882            });
883
884            let preserve_comments = opts
885                .format
886                .comments
887                .clone()
888                .into_inner()
889                .unwrap_or(BoolOr::Data(JsMinifyCommentOption::PreserveSomeComments));
890            swc_compiler_base::minify_file_comments(
891                &comments,
892                preserve_comments,
893                opts.format.preserve_annotations,
894            );
895
896            let ret = self.print(
897                &program,
898                PrintArgs {
899                    source_root: None,
900                    source_file_name: Some(&fm.name.to_string()),
901                    output_path: opts.output_path.clone().map(From::from),
902                    inline_sources_content: opts.inline_sources_content,
903                    source_map,
904                    source_map_ignore_list: opts.source_map_ignore_list.clone(),
905                    source_map_names: &source_map_names,
906                    orig,
907                    comments: Some(&comments),
908                    emit_source_map_columns: opts.emit_source_map_columns,
909                    preamble: &opts.format.preamble,
910                    codegen_config: swc_ecma_codegen::Config::default()
911                        .with_target(target)
912                        .with_minify(true)
913                        .with_ascii_only(opts.format.ascii_only)
914                        .with_emit_assert_for_import_attributes(
915                            opts.format.emit_assert_for_import_attributes,
916                        )
917                        .with_inline_script(opts.format.inline_script)
918                        .with_reduce_escaped_newline(
919                            min_opts
920                                .compress
921                                .unwrap_or_default()
922                                .experimental
923                                .reduce_escaped_newline,
924                        ),
925                    output: None,
926                    source_map_url,
927                },
928            );
929
930            ret.map(|mut output| {
931                output.diagnostics = handler.take_diagnostics();
932
933                output
934            })
935        })
936    }
937
938    /// You can use custom pass with this method.
939    ///
940    /// Pass building logic has been inlined into the configuration system.
941    #[tracing::instrument(skip_all)]
942    pub fn process_js(
943        &self,
944        handler: &Handler,
945        program: Program,
946        opts: &Options,
947    ) -> Result<TransformOutput, Error> {
948        let loc = self.cm.lookup_char_pos(program.span().lo());
949        let fm = loc.file;
950
951        self.process_js_with_custom_pass(
952            fm,
953            Some(program),
954            handler,
955            opts,
956            SingleThreadedComments::default(),
957            |_| noop_pass(),
958            |_| noop_pass(),
959        )
960    }
961
962    #[tracing::instrument(name = "swc::Compiler::apply_transforms", skip_all)]
963    fn apply_transforms(
964        &self,
965        handler: &Handler,
966        #[allow(unused)] comments: SingleThreadedComments,
967        #[allow(unused)] fm: Arc<SourceFile>,
968        orig: Option<sourcemap::SourceMap>,
969        config: BuiltInput<impl Pass>,
970    ) -> Result<TransformOutput, Error> {
971        self.run(|| {
972            let program = config.program;
973
974            if config.emit_isolated_dts && !config.syntax.typescript() {
975                handler.warn(
976                    "jsc.experimental.emitIsolatedDts is enabled but the syntax is not TypeScript",
977                );
978            }
979
980            let source_map_names = if config.source_maps.enabled() {
981                let mut v = swc_compiler_base::IdentCollector {
982                    names: Default::default(),
983                };
984
985                program.visit_with(&mut v);
986
987                v.names
988            } else {
989                Default::default()
990            };
991            #[cfg(feature = "isolated-dts")]
992            let dts_code = if config.syntax.typescript() && config.emit_isolated_dts {
993                use std::cell::RefCell;
994
995                use swc_ecma_codegen::to_code_with_comments;
996                let (leading, trailing) = comments.borrow_all();
997
998                let leading = std::rc::Rc::new(RefCell::new(leading.clone()));
999                let trailing = std::rc::Rc::new(RefCell::new(trailing.clone()));
1000
1001                let comments = SingleThreadedComments::from_leading_and_trailing(leading, trailing);
1002
1003                let mut checker =
1004                    FastDts::new(fm.name.clone(), config.unresolved_mark, Default::default());
1005                let mut program = program.clone();
1006
1007                if let Some((base, resolver)) = config.resolver {
1008                    use swc_ecma_transforms::modules::rewriter::import_rewriter;
1009
1010                    program.mutate(import_rewriter(base, resolver));
1011                }
1012
1013                let issues = checker.transform(&mut program);
1014
1015                for issue in issues {
1016                    handler
1017                        .struct_span_err(issue.range.span, &issue.message)
1018                        .emit();
1019                }
1020
1021                let dts_code = to_code_with_comments(Some(&comments), &program);
1022                Some(dts_code)
1023            } else {
1024                None
1025            };
1026
1027            let pass = config.pass;
1028            let (program, output) = swc_transform_common::output::capture(|| {
1029                #[cfg(feature = "isolated-dts")]
1030                {
1031                    if let Some(dts_code) = dts_code {
1032                        use swc_transform_common::output::experimental_emit;
1033                        experimental_emit("__swc_isolated_declarations__".into(), dts_code);
1034                    }
1035                }
1036
1037                helpers::HELPERS.set(&Helpers::new(config.external_helpers), || {
1038                    HANDLER.set(handler, || {
1039                        // Fold module
1040                        program.apply(pass)
1041                    })
1042                })
1043            });
1044
1045            if let Some(comments) = &config.comments {
1046                swc_compiler_base::minify_file_comments(
1047                    comments,
1048                    config.preserve_comments,
1049                    config.output.preserve_annotations.into_bool(),
1050                );
1051            }
1052
1053            self.print(
1054                &program,
1055                PrintArgs {
1056                    source_root: config.source_root.as_deref(),
1057                    source_file_name: config.source_file_name.as_deref(),
1058                    source_map_ignore_list: config.source_map_ignore_list.clone(),
1059                    output_path: config.output_path,
1060                    inline_sources_content: config.inline_sources_content,
1061                    source_map: config.source_maps,
1062                    source_map_names: &source_map_names,
1063                    orig,
1064                    comments: config.comments.as_ref().map(|v| v as _),
1065                    emit_source_map_columns: config.emit_source_map_columns,
1066                    preamble: &config.output.preamble,
1067                    codegen_config: swc_ecma_codegen::Config::default()
1068                        .with_target(config.target)
1069                        .with_minify(config.minify)
1070                        .with_ascii_only(
1071                            config
1072                                .output
1073                                .charset
1074                                .map(|v| matches!(v, OutputCharset::Ascii))
1075                                .unwrap_or(false),
1076                        )
1077                        .with_emit_assert_for_import_attributes(
1078                            config.emit_assert_for_import_attributes,
1079                        )
1080                        .with_inline_script(config.codegen_inline_script),
1081                    output: if output.is_empty() {
1082                        None
1083                    } else {
1084                        Some(output)
1085                    },
1086                    source_map_url: config.output.source_map_url.as_deref(),
1087                },
1088            )
1089        })
1090    }
1091}
1092
1093#[non_exhaustive]
1094#[derive(Clone, Default)]
1095pub struct JsMinifyExtras {
1096    pub mangle_name_cache: Option<Arc<dyn MangleCache>>,
1097}
1098
1099impl JsMinifyExtras {
1100    pub fn with_mangle_name_cache(
1101        mut self,
1102        mangle_name_cache: Option<Arc<dyn MangleCache>>,
1103    ) -> Self {
1104        self.mangle_name_cache = mangle_name_cache;
1105        self
1106    }
1107}
1108
1109fn find_swcrc(path: &Path, root: &Path, root_mode: RootMode) -> Option<PathBuf> {
1110    let mut parent = path.parent();
1111    while let Some(dir) = parent {
1112        let swcrc = dir.join(".swcrc");
1113
1114        if swcrc.exists() {
1115            return Some(swcrc);
1116        }
1117
1118        if dir == root && root_mode == RootMode::Root {
1119            break;
1120        }
1121        parent = dir.parent();
1122    }
1123
1124    None
1125}
1126
1127#[tracing::instrument(skip_all)]
1128fn load_swcrc(path: &Path) -> Result<Rc, Error> {
1129    let content = read_to_string(path).context("failed to read config (.swcrc) file")?;
1130
1131    parse_swcrc(&content)
1132}
1133
1134fn parse_swcrc(s: &str) -> Result<Rc, Error> {
1135    fn convert_json_err(e: serde_json::Error) -> Error {
1136        let line = e.line();
1137        let column = e.column();
1138
1139        let msg = match e.classify() {
1140            Category::Io => "io error",
1141            Category::Syntax => "syntax error",
1142            Category::Data => "unmatched data",
1143            Category::Eof => "unexpected eof",
1144        };
1145        Error::new(e).context(format!(
1146            "failed to deserialize .swcrc (json) file: {msg}: {line}:{column}"
1147        ))
1148    }
1149
1150    let v = parse_to_serde_value(
1151        s.trim_start_matches('\u{feff}'),
1152        &ParseOptions {
1153            allow_comments: true,
1154            allow_trailing_commas: true,
1155            allow_loose_object_property_names: false,
1156        },
1157    )?
1158    .ok_or_else(|| Error::msg("failed to deserialize empty .swcrc (json) file"))?;
1159
1160    if let Ok(rc) = serde_json::from_value(v.clone()) {
1161        return Ok(rc);
1162    }
1163
1164    serde_json::from_value(v)
1165        .map(Rc::Single)
1166        .map_err(convert_json_err)
1167}