1use 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 #[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 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 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 let error_msg = err.to_pretty_string();
290 let swc_core_version = rspack_workspace::rspack_swc_core_version!();
291 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 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 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 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 read_file_sourcemap(data_url).unwrap_or(None)
453 }
454 }
455 };
456
457 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 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}