Skip to main content

rspack_javascript_compiler/compiler/
transform.rs

1/**
2 * Some code is modified based on
3 * https://github.com/swc-project/swc/blob/5dacaa174baaf6bf40594d79d14884c8c2fc0de2/crates/swc/src/lib.rs
4 * Apache-2.0 licensed
5 * Author Donny/강동윤
6 * Copyright (c)
7 */
8use std::{fs::File, path::PathBuf, sync::Arc};
9
10use anyhow::{Context, bail};
11use base64::prelude::*;
12use indoc::formatdoc;
13use rspack_error::Result;
14use rspack_util::{source_map::SourceMapKind, swc::minify_file_comments};
15use swc_config::{is_module::IsModule, merge::Merge};
16pub use swc_core::base::config::Options as SwcOptions;
17use swc_core::{
18  base::{
19    BoolOr,
20    config::{
21      BuiltInput, Config, InputSourceMap, JsMinifyCommentOption, OutputCharset, SourceMapsConfig,
22    },
23    sourcemap,
24  },
25  common::{
26    FileName, GLOBALS, Mark, SourceFile, SourceMap,
27    comments::{Comments, SingleThreadedComments},
28    errors::Handler,
29  },
30  ecma::{
31    ast::{EsVersion, Pass, Program},
32    parser::{
33      Syntax, TsSyntax, parse_file_as_commonjs, parse_file_as_module, parse_file_as_program,
34      parse_file_as_script,
35    },
36    transforms::base::helpers::{self, Helpers},
37  },
38};
39use swc_error_reporters::handler::try_with_handler;
40use url::Url;
41
42use super::{
43  JavaScriptCompiler, TransformOutput,
44  stringify::{PrintOptions, SourceMapConfig},
45};
46
47impl JavaScriptCompiler {
48  /// Transforms the given JavaScript source code according to the provided options and source map kind.
49  #[allow(clippy::too_many_arguments)]
50  pub fn transform<'a, S, P>(
51    &self,
52    source: S,
53    filename: Option<Arc<FileName>>,
54    comments: std::rc::Rc<SingleThreadedComments>,
55    options: SwcOptions,
56    module_source_map_kind: Option<SourceMapKind>,
57    inspect_parsed_ast: impl FnOnce(&Program, Mark),
58    before_pass: impl FnOnce(&Program) -> P + 'a,
59  ) -> Result<TransformOutput>
60  where
61    P: Pass + 'a,
62    S: Into<String>,
63  {
64    let fm = self.cm.new_source_file(
65      filename.unwrap_or_else(|| Arc::new(FileName::Anon)),
66      source.into(),
67    );
68    let javascript_transformer =
69      JavaScriptTransformer::new(self.cm.clone(), fm, comments, self, options)?;
70
71    javascript_transformer.transform(inspect_parsed_ast, before_pass, module_source_map_kind)
72  }
73}
74
75struct JavaScriptTransformer<'a> {
76  cm: Arc<SourceMap>,
77  fm: Arc<SourceFile>,
78  comments: std::rc::Rc<SingleThreadedComments>,
79  options: SwcOptions,
80  javascript_compiler: &'a JavaScriptCompiler,
81  helpers: Helpers,
82  config: Config,
83}
84
85const SWC_MIETTE_DIAGNOSTIC_CODE: &str = "Builtin swc-loader error";
86
87impl<'a> JavaScriptTransformer<'a> {
88  pub fn new(
89    cm: Arc<SourceMap>,
90    fm: Arc<SourceFile>,
91    comments: std::rc::Rc<SingleThreadedComments>,
92    compiler: &'a JavaScriptCompiler,
93    mut options: SwcOptions,
94  ) -> Result<Self> {
95    GLOBALS.set(&compiler.globals, || {
96      let top_level_mark = Mark::new();
97      let unresolved_mark = Mark::new();
98      options.top_level_mark = Some(top_level_mark);
99      options.unresolved_mark = Some(unresolved_mark);
100    });
101
102    let config = get_swc_config_from_file(&fm.name);
103    let helpers = GLOBALS.set(&compiler.globals, || {
104      let mut external_helpers = options.config.jsc.external_helpers;
105      external_helpers.merge(config.jsc.external_helpers);
106      Helpers::new(external_helpers.into())
107    });
108
109    Ok(Self {
110      cm,
111      fm,
112      javascript_compiler: compiler,
113      options,
114      helpers,
115      config,
116      comments,
117    })
118  }
119
120  fn transform<P>(
121    self,
122    inspect_parsed_ast: impl FnOnce(&Program, Mark),
123    before_pass: impl FnOnce(&Program) -> P + 'a,
124    module_source_map_kind: Option<SourceMapKind>,
125  ) -> Result<TransformOutput>
126  where
127    P: Pass + 'a,
128  {
129    let mut built_input = self.parse_built_input(before_pass)?;
130
131    let target = built_input.target;
132    let source_map_kind: SourceMapKind = match self.options.config.source_maps {
133      Some(SourceMapsConfig::Bool(false)) => SourceMapKind::empty(),
134      _ => module_source_map_kind.unwrap_or_default(),
135    };
136    let minify = built_input.minify;
137    let source_map_config = SourceMapConfig {
138      enable: source_map_kind.source_map(),
139      inline_sources_content: source_map_kind.source_map(),
140      emit_columns: !source_map_kind.cheap(),
141      names: Default::default(),
142    };
143
144    let input_source_map = self.input_source_map(&built_input.input_source_map)?;
145
146    let diagnostics = self.transform_with_built_input(&mut built_input, inspect_parsed_ast)?;
147    let ascii_only = built_input
148      .output
149      .charset
150      .as_ref()
151      .is_some_and(|v| matches!(v, OutputCharset::Ascii));
152
153    let print_options = PrintOptions {
154      source_len: self.fm.byte_length(),
155      source_map: self.cm.clone(),
156      target,
157      source_map_config,
158      input_source_map: input_source_map.as_ref(),
159      minify,
160      comments: Some(&self.comments as &dyn Comments),
161      preamble: &built_input.output.preamble,
162      ascii_only,
163      inline_script: built_input.codegen_inline_script,
164    };
165
166    self
167      .javascript_compiler
168      .print(&built_input.program, print_options)
169      .map(|o| o.with_diagnostics(diagnostics))
170  }
171
172  fn parse_js(
173    &self,
174    fm: Arc<SourceFile>,
175    handler: &Handler,
176    target: EsVersion,
177    syntax: Syntax,
178    is_module: IsModule,
179    comments: Option<&dyn Comments>,
180  ) -> Result<Program, anyhow::Error> {
181    let mut error = false;
182    let mut errors = vec![];
183
184    let program_result = match is_module {
185      IsModule::Bool(true) => {
186        parse_file_as_module(&fm, syntax, target, comments, &mut errors).map(Program::Module)
187      }
188      IsModule::Bool(false) => {
189        parse_file_as_script(&fm, syntax, target, comments, &mut errors).map(Program::Script)
190      }
191      IsModule::Unknown => parse_file_as_program(&fm, syntax, target, comments, &mut errors),
192      IsModule::CommonJS => {
193        parse_file_as_commonjs(&fm, syntax, target, comments, &mut errors).map(Program::Script)
194      }
195    };
196
197    for e in errors {
198      e.into_diagnostic(handler).emit();
199      error = true;
200    }
201
202    let res = program_result.map_err(|e| {
203      e.into_diagnostic(handler).emit();
204      anyhow::Error::msg("Syntax Error")
205    });
206
207    if error {
208      return Err(anyhow::anyhow!("Syntax Error"));
209    }
210    res
211  }
212
213  fn parse_built_input<P>(
214    &'a self,
215    before_pass: impl FnOnce(&Program) -> P + 'a,
216  ) -> Result<BuiltInput<impl Pass + 'a>>
217  where
218    P: Pass + 'a,
219  {
220    self.run(|| {
221      try_with_handler(self.cm.clone(), Default::default(), |handler| {
222        self.options.build_as_input(
223          &self.cm.clone(),
224          &self.fm.name,
225          move |syntax, target, is_module| {
226            self.parse_js(
227              self.fm.clone(),
228              handler,
229              target,
230              syntax,
231              is_module,
232              Some(&self.comments).map(|c| c as &dyn Comments),
233            )
234          },
235          self.options.output_path.as_deref(),
236          self.options.source_root.clone(),
237          self.options.source_file_name.clone(),
238          self.config.source_map_ignore_list.clone(),
239          handler,
240          Some(self.config.clone()),
241          Some(&self.comments),
242          before_pass,
243        )
244      })
245      .map_err(|e| e.to_pretty_error().into())
246    })
247  }
248
249  fn run<R>(&self, op: impl FnOnce() -> R) -> R {
250    GLOBALS.set(&self.javascript_compiler.globals, op)
251  }
252
253  fn transform_with_built_input(
254    &self,
255    built_input: &mut BuiltInput<impl Pass>,
256    inspect_parsed_ast: impl FnOnce(&Program, Mark),
257  ) -> Result<Vec<String>> {
258    let mut diagnostics = vec![];
259    let result = self.run(|| {
260      helpers::HELPERS.set(&self.helpers, || {
261        inspect_parsed_ast(&built_input.program, built_input.unresolved_mark);
262
263        let result = try_with_handler(self.cm.clone(), Default::default(), |handler| {
264          // Apply external plugin passes to the Program AST.
265          // External plugins may emit warnings or inject helpers,
266          // so we need a handler to properly process them.
267          built_input.pass.process(&mut built_input.program);
268          diagnostics.extend(handler.take_diagnostics());
269
270          Ok(())
271        });
272
273        result.map_err(|err| {
274          let swc_diagnostics = err.diagnostics();
275
276          if swc_diagnostics.iter().any(|d| match &d.code {
277            Some(code) => {
278              // reference to:
279              //    https://github.com/swc-project/swc/blob/v1.11.21/crates/swc/src/plugin.rs#L187
280              //    https://github.com/swc-project/swc/blob/v1.11.21/crates/swc/src/plugin.rs#L200
281              match code {
282                swc_core::common::errors::DiagnosticId::Error(e) => e.contains("plugin"),
283                swc_core::common::errors::DiagnosticId::Lint(_) => false,
284              }
285            }
286            None => false,
287          }) {
288            // swc errors includes plugin error;
289            let error_msg = err.to_pretty_string();
290            let swc_core_version = rspack_workspace::rspack_swc_core_version!();
291            // FIXME: with_help has bugs, use with_help when diagnostic print is fixed
292            let help_msg = formatdoc!{"
293              The version of the SWC Wasm plugin you're using might not be compatible with `builtin:swc-loader`.
294              The `swc_core` version of the current `rspack_core` is {swc_core_version}. 
295              Please check the `swc_core` version of SWC Wasm plugin to make sure these versions are within the compatible range.
296              See this guide as a reference for selecting SWC Wasm plugin versions: https://rspack.rs/errors/swc-plugin-version"};
297            let mut error = rspack_error::error!(format!("{error_msg}{help_msg}"));
298            error.code = Some(SWC_MIETTE_DIAGNOSTIC_CODE.into());
299            error
300          } else {
301            let error_msg = err.to_pretty_string();
302            let mut error = rspack_error::error!(error_msg);
303            error.code = Some(SWC_MIETTE_DIAGNOSTIC_CODE.into());
304            error
305          }
306        })
307      })
308    });
309
310    if let Some(comments) = &built_input.comments {
311      let preserve_annotations = match &built_input.preserve_comments {
312        BoolOr::Bool(true) | BoolOr::Data(JsMinifyCommentOption::PreserveAllComments) => true,
313        BoolOr::Data(JsMinifyCommentOption::PreserveSomeComments) => false,
314        BoolOr::Bool(false) => false,
315        BoolOr::Data(JsMinifyCommentOption::PreserveRegexComments { .. }) => false,
316      };
317
318      minify_file_comments(
319        comments,
320        &built_input.preserve_comments,
321        preserve_annotations,
322      );
323    }
324
325    result.map(|_| diagnostics)
326  }
327
328  pub fn input_source_map(
329    &self,
330    input_src_map: &InputSourceMap,
331  ) -> Result<Option<sourcemap::SourceMap>, anyhow::Error> {
332    let fm = &self.fm;
333    let name = &self.fm.name;
334    let read_inline_sourcemap =
335      |data_url: Option<&str>| -> Result<Option<sourcemap::SourceMap>, anyhow::Error> {
336        match data_url {
337          Some(data_url) => {
338            let url = Url::parse(data_url)
339              .with_context(|| format!("failed to parse inline source map url\n{data_url}"))?;
340
341            let idx = match url.path().find("base64,") {
342              Some(v) => v,
343              None => {
344                bail!("failed to parse inline source map: not base64: {url:?}")
345              }
346            };
347
348            let content = url.path()[idx + "base64,".len()..].trim();
349
350            let res = BASE64_STANDARD
351              .decode(content.as_bytes())
352              .context("failed to decode base64-encoded source map")?;
353
354            Ok(Some(sourcemap::SourceMap::from_slice(&res).context(
355              "failed to read input source map from inlined base64 encoded \
356                                string",
357            )?))
358          }
359          None => {
360            bail!("failed to parse inline source map: `sourceMappingURL` not found")
361          }
362        }
363      };
364
365    let read_file_sourcemap =
366      |data_url: Option<&str>| -> Result<Option<sourcemap::SourceMap>, anyhow::Error> {
367        match name.as_ref() {
368          FileName::Real(filename) => {
369            let dir = match filename.parent() {
370              Some(v) => v,
371              None => {
372                bail!("unexpected: root directory is given as a input file")
373              }
374            };
375
376            let map_path = match data_url {
377              Some(data_url) => {
378                let mut map_path = dir.join(data_url);
379                if !map_path.exists() {
380                  // Old behavior. This check would prevent
381                  // regressions.
382                  // Perhaps it shouldn't be supported. Sometimes
383                  // developers don't want to expose their source
384                  // code.
385                  // Map files are for internal troubleshooting
386                  // convenience.
387                  map_path = PathBuf::from(format!("{}.map", filename.display()));
388                  if !map_path.exists() {
389                    bail!(
390                      "failed to find input source map file {:?} in \
391                                                  {:?} file",
392                      map_path.display(),
393                      filename.display()
394                    )
395                  }
396                }
397
398                Some(map_path)
399              }
400              None => {
401                // Old behavior.
402                let map_path = PathBuf::from(format!("{}.map", filename.display()));
403                if map_path.exists() {
404                  Some(map_path)
405                } else {
406                  None
407                }
408              }
409            };
410
411            match map_path {
412              Some(map_path) => {
413                let path = map_path.display().to_string();
414                let file = File::open(&path);
415
416                // Old behavior.
417                let file = file?;
418
419                Ok(Some(sourcemap::SourceMap::from_reader(file).with_context(
420                  || {
421                    format!(
422                      "failed to read input source map
423                                  from file at {path}"
424                    )
425                  },
426                )?))
427              }
428              None => Ok(None),
429            }
430          }
431          _ => Ok(None),
432        }
433      };
434
435    let read_sourcemap = || -> Option<sourcemap::SourceMap> {
436      let s = "sourceMappingURL=";
437      let idx = fm.src.rfind(s);
438
439      let data_url = idx.map(|idx| {
440        let data_idx = idx + s.len();
441        if let Some(end) = fm.src[data_idx..].find('\n').map(|i| i + data_idx + 1) {
442          &fm.src[data_idx..end]
443        } else {
444          &fm.src[data_idx..]
445        }
446      });
447
448      match read_inline_sourcemap(data_url) {
449        Ok(r) => r,
450        Err(_err) => {
451          // Load original source map if possible
452          read_file_sourcemap(data_url).unwrap_or(None)
453        }
454      }
455    };
456
457    // Load original source map
458    match input_src_map {
459      InputSourceMap::Bool(false) => Ok(None),
460      InputSourceMap::Bool(true) => Ok(read_sourcemap()),
461      InputSourceMap::Str(s) => {
462        if s == "inline" {
463          Ok(read_sourcemap())
464        } else {
465          // Load source map passed by user
466          Ok(Some(
467            sourcemap::SourceMap::from_slice(s.as_bytes())
468              .context("failed to read input source map from user-provided sourcemap")?,
469          ))
470        }
471      }
472    }
473  }
474}
475
476fn get_swc_config_from_file(filename: &FileName) -> Config {
477  let filename_path = match filename {
478    FileName::Real(p) => Some(p.as_path()),
479    _ => return Config::default(),
480  };
481
482  let filename_ext = match filename_path {
483    Some(p) => p.extension().and_then(|ext| ext.to_str()),
484    None => return Config::default(),
485  };
486
487  let mut config = Config::default();
488  match filename_ext {
489    Some("tsx") => {
490      config.jsc.syntax = Some(Syntax::Typescript(TsSyntax {
491        tsx: true,
492        ..Default::default()
493      }))
494    }
495    Some("cts" | "mts") => {
496      config.jsc.syntax = Some(Syntax::Typescript(TsSyntax {
497        tsx: false,
498        disallow_ambiguous_jsx_like: true,
499        ..Default::default()
500      }))
501    }
502    Some("ts") => {
503      config.jsc.syntax = Some(Syntax::Typescript(TsSyntax {
504        tsx: false,
505        ..Default::default()
506      }))
507    }
508    _ => {}
509  }
510
511  config
512}