1use std::{
2 env,
3 path::{Path, PathBuf},
4};
5
6use anyhow::{Context, Error};
7use base64::prelude::{Engine, BASE64_STANDARD};
8use bytes_str::BytesStr;
9use once_cell::sync::Lazy;
10use rustc_hash::FxHashMap;
11#[allow(unused)]
12use serde::{Deserialize, Serialize};
13use swc_atoms::Atom;
14use swc_common::{
15 comments::{Comment, CommentKind, Comments, SingleThreadedComments},
16 errors::Handler,
17 source_map::SourceMapGenConfig,
18 sync::Lrc,
19 BytePos, FileName, SourceFile, SourceMap,
20};
21use swc_config::{file_pattern::FilePattern, is_module::IsModule, types::BoolOr};
22use swc_ecma_ast::{EsVersion, Ident, IdentName, Program};
23use swc_ecma_codegen::{text_writer::WriteJs, Emitter, Node};
24use swc_ecma_minifier::js::JsMinifyCommentOption;
25use swc_ecma_parser::{
26 parse_file_as_commonjs, parse_file_as_module, parse_file_as_program, parse_file_as_script,
27 Syntax,
28};
29use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
30use swc_timer::timer;
31
32#[cfg(feature = "node")]
33#[napi_derive::napi(object)]
34#[derive(Debug, Serialize)]
35pub struct TransformOutput {
36 pub code: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub map: Option<String>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub output: Option<String>,
42
43 pub diagnostics: std::vec::Vec<String>,
44}
45
46#[cfg(not(feature = "node"))]
47#[derive(Debug, Serialize)]
48pub struct TransformOutput {
49 pub code: String,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub map: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub output: Option<String>,
56
57 pub diagnostics: std::vec::Vec<String>,
58}
59
60pub fn parse_js(
64 _cm: Lrc<SourceMap>,
65 fm: Lrc<SourceFile>,
66 handler: &Handler,
67 target: EsVersion,
68 syntax: Syntax,
69 is_module: IsModule,
70 comments: Option<&dyn Comments>,
71) -> Result<Program, Error> {
72 let mut res = (|| {
73 let mut error = false;
74
75 let mut errors = std::vec::Vec::new();
76 let program_result = match is_module {
77 IsModule::Bool(true) => {
78 parse_file_as_module(&fm, syntax, target, comments, &mut errors)
79 .map(Program::Module)
80 }
81 IsModule::Bool(false) => {
82 parse_file_as_script(&fm, syntax, target, comments, &mut errors)
83 .map(Program::Script)
84 }
85 IsModule::CommonJS => {
86 parse_file_as_commonjs(&fm, syntax, target, comments, &mut errors)
87 .map(Program::Script)
88 }
89 IsModule::Unknown => parse_file_as_program(&fm, syntax, target, comments, &mut errors),
90 };
91
92 for e in errors {
93 e.into_diagnostic(handler).emit();
94 error = true;
95 }
96
97 let program = program_result.map_err(|e| {
98 e.into_diagnostic(handler).emit();
99 Error::msg("Syntax Error")
100 })?;
101
102 if error {
103 return Err(anyhow::anyhow!("Syntax Error"));
104 }
105
106 Ok(program)
107 })();
108
109 if env::var("SWC_DEBUG").unwrap_or_default() == "1" {
110 res = res.with_context(|| format!("Parser config: {syntax:?}"));
111 }
112
113 res
114}
115
116pub struct PrintArgs<'a> {
117 pub source_root: Option<&'a str>,
118 pub source_file_name: Option<&'a str>,
119 pub output_path: Option<PathBuf>,
120 pub inline_sources_content: bool,
121 pub source_map: SourceMapsConfig,
122 pub source_map_names: &'a FxHashMap<BytePos, Atom>,
123 pub orig: Option<swc_sourcemap::SourceMap>,
124 pub comments: Option<&'a dyn Comments>,
125 pub emit_source_map_columns: bool,
126 pub preamble: &'a str,
127 pub codegen_config: swc_ecma_codegen::Config,
128 pub output: Option<FxHashMap<String, String>>,
129 pub source_map_url: Option<&'a str>,
130 pub source_map_ignore_list: Option<FilePattern>,
131}
132
133impl Default for PrintArgs<'_> {
134 fn default() -> Self {
135 static DUMMY_NAMES: Lazy<FxHashMap<BytePos, Atom>> = Lazy::new(Default::default);
136
137 PrintArgs {
138 source_root: None,
139 source_file_name: None,
140 output_path: None,
141 inline_sources_content: false,
142 source_map: Default::default(),
143 source_map_names: &DUMMY_NAMES,
144 orig: None,
145 comments: None,
146 emit_source_map_columns: false,
147 preamble: "",
148 codegen_config: Default::default(),
149 output: None,
150 source_map_url: None,
151 source_map_ignore_list: None,
152 }
153 }
154}
155
156#[allow(clippy::too_many_arguments)]
166pub fn print<T>(
167 cm: Lrc<SourceMap>,
168 node: &T,
169 PrintArgs {
170 source_root,
171 source_file_name,
172 output_path,
173 inline_sources_content,
174 source_map,
175 source_map_names,
176 orig,
177 comments,
178 emit_source_map_columns,
179 preamble,
180 codegen_config,
181 output,
182 source_map_url,
183 source_map_ignore_list,
184 }: PrintArgs,
185) -> Result<TransformOutput, Error>
186where
187 T: Node + VisitWith<IdentCollector>,
188{
189 let _timer = timer!("Compiler::print");
190
191 let mut src_map_buf = Vec::new();
192
193 let mut src = {
194 let mut buf = std::vec::Vec::new();
195 {
196 let mut w = swc_ecma_codegen::text_writer::JsWriter::new(
197 cm.clone(),
198 "\n",
199 &mut buf,
200 if source_map.enabled() {
201 Some(&mut src_map_buf)
202 } else {
203 None
204 },
205 );
206 w.preamble(preamble).unwrap();
207 let mut wr = Box::new(w) as Box<dyn WriteJs>;
208
209 if codegen_config.minify {
210 wr = Box::new(swc_ecma_codegen::text_writer::omit_trailing_semi(wr));
211 }
212
213 let mut emitter = Emitter {
214 cfg: codegen_config,
215 comments,
216 cm: cm.clone(),
217 wr,
218 };
219
220 node.emit_with(&mut emitter)
221 .context("failed to emit module")?;
222 }
223 String::from_utf8(buf).expect("invalid utf8 character detected")
225 };
226
227 if cfg!(debug_assertions)
228 && !src_map_buf.is_empty()
229 && src_map_buf.iter().all(|(bp, _)| bp.is_dummy())
230 && src.lines().count() >= 3
231 && option_env!("SWC_DEBUG") == Some("1")
232 {
233 panic!("The module contains only dummy spans\n{src}");
234 }
235
236 let mut map = if source_map.enabled() {
237 Some(cm.build_source_map(
238 &src_map_buf,
239 orig,
240 SwcSourceMapConfig {
241 source_file_name,
242 output_path: output_path.as_deref(),
243 names: source_map_names,
244 inline_sources_content,
245 emit_columns: emit_source_map_columns,
246 ignore_list: source_map_ignore_list,
247 },
248 ))
249 } else {
250 None
251 };
252
253 if let Some(map) = &mut map {
254 if let Some(source_root) = source_root {
255 map.set_source_root(Some(BytesStr::from_str_slice(source_root)))
256 }
257 }
258
259 let (code, map) = match source_map {
260 SourceMapsConfig::Bool(v) => {
261 if v {
262 let mut buf = std::vec::Vec::new();
263
264 map.unwrap()
265 .to_writer(&mut buf)
266 .context("failed to write source map")?;
267 let map = String::from_utf8(buf).context("source map is not utf-8")?;
268
269 if let Some(source_map_url) = source_map_url {
270 src.push_str("\n//# sourceMappingURL=");
271 src.push_str(source_map_url);
272 }
273
274 (src, Some(map))
275 } else {
276 (src, None)
277 }
278 }
279 SourceMapsConfig::Str(_) => {
280 let mut buf = std::vec::Vec::new();
281
282 map.unwrap()
283 .to_writer(&mut buf)
284 .context("failed to write source map file")?;
285 let map = String::from_utf8(buf).context("source map is not utf-8")?;
286
287 src.push_str("\n//# sourceMappingURL=data:application/json;base64,");
288 BASE64_STANDARD.encode_string(map.as_bytes(), &mut src);
289 (src, None)
290 }
291 };
292
293 Ok(TransformOutput {
294 code,
295 map,
296 output: output
297 .map(|v| serde_json::to_string(&v).context("failed to serilaize output"))
298 .transpose()?,
299 diagnostics: Default::default(),
300 })
301}
302
303struct SwcSourceMapConfig<'a> {
304 source_file_name: Option<&'a str>,
305 output_path: Option<&'a Path>,
307
308 names: &'a FxHashMap<BytePos, Atom>,
309
310 inline_sources_content: bool,
311
312 emit_columns: bool,
313
314 ignore_list: Option<FilePattern>,
315}
316
317impl SourceMapGenConfig for SwcSourceMapConfig<'_> {
318 fn file_name_to_source(&self, f: &FileName) -> String {
319 if let Some(file_name) = self.source_file_name {
320 return file_name.to_string();
321 }
322
323 let Some(base_path) = self.output_path.as_ref().and_then(|v| v.parent()) else {
324 return f.to_string();
325 };
326 let target = match f {
327 FileName::Real(v) => v,
328 _ => return f.to_string(),
329 };
330
331 let rel = pathdiff::diff_paths(target, base_path);
332 match rel {
333 Some(v) => {
334 let s = v.to_string_lossy().to_string();
335 if cfg!(target_os = "windows") {
336 s.replace('\\', "/")
337 } else {
338 s
339 }
340 }
341 None => f.to_string(),
342 }
343 }
344
345 fn name_for_bytepos(&self, pos: BytePos) -> Option<&str> {
346 self.names.get(&pos).map(|v| &**v)
347 }
348
349 fn inline_sources_content(&self, _: &FileName) -> bool {
350 self.inline_sources_content
351 }
352
353 fn emit_columns(&self, _f: &FileName) -> bool {
354 self.emit_columns
355 }
356
357 fn skip(&self, f: &FileName) -> bool {
358 match f {
359 FileName::Internal(..) => true,
360 FileName::Custom(s) => s.starts_with('<'),
361 _ => false,
362 }
363 }
364
365 fn ignore_list(&self, f: &FileName) -> bool {
366 if let Some(ignore_list) = &self.ignore_list {
367 match f {
368 FileName::Real(path_buf) => {
369 ignore_list.is_match(path_buf.to_string_lossy().as_ref())
370 }
371 FileName::Custom(s) => ignore_list.is_match(s),
372 _ => true,
373 }
374 } else {
375 false
376 }
377 }
378}
379
380pub fn minify_file_comments(
381 comments: &SingleThreadedComments,
382 preserve_comments: BoolOr<JsMinifyCommentOption>,
383 preserve_annotations: bool,
384) {
385 match preserve_comments {
386 BoolOr::Bool(true) | BoolOr::Data(JsMinifyCommentOption::PreserveAllComments) => {}
387
388 BoolOr::Data(JsMinifyCommentOption::PreserveSomeComments) => {
389 let preserve_excl = |_: &BytePos, vc: &mut std::vec::Vec<Comment>| -> bool {
390 vc.retain(|c: &Comment| {
394 c.text.contains("@lic")
395 || c.text.contains("@preserve")
396 || c.text.contains("@copyright")
397 || c.text.contains("@cc_on")
398 || (preserve_annotations
399 && (c.text.contains("__PURE__")
400 || c.text.contains("__INLINE__")
401 || c.text.contains("__NOINLINE__")
402 || c.text.contains("@vite-ignore")))
403 || (c.kind == CommentKind::Block && c.text.starts_with('!'))
404 });
405 !vc.is_empty()
406 };
407 let (mut l, mut t) = comments.borrow_all_mut();
408
409 l.retain(preserve_excl);
410 t.retain(preserve_excl);
411 }
412
413 BoolOr::Data(JsMinifyCommentOption::PreserveRegexComments { regex }) => {
414 let preserve_excl = |_: &BytePos, vc: &mut std::vec::Vec<Comment>| -> bool {
415 vc.retain(|c: &Comment| regex.find(&c.text).is_some());
419 !vc.is_empty()
420 };
421 let (mut l, mut t) = comments.borrow_all_mut();
422
423 l.retain(preserve_excl);
424 t.retain(preserve_excl);
425 }
426
427 BoolOr::Bool(false) => {
428 let (mut l, mut t) = comments.borrow_all_mut();
429 l.clear();
430 t.clear();
431 }
432 }
433}
434
435#[derive(Clone, Serialize, Deserialize, Debug)]
437#[serde(untagged)]
438pub enum SourceMapsConfig {
439 Bool(bool),
440 Str(String),
441}
442
443impl SourceMapsConfig {
444 pub fn enabled(&self) -> bool {
445 match *self {
446 SourceMapsConfig::Bool(b) => b,
447 SourceMapsConfig::Str(ref s) => {
448 assert_eq!(s, "inline", "Source map must be true, false or inline");
449 true
450 }
451 }
452 }
453}
454
455impl Default for SourceMapsConfig {
456 fn default() -> Self {
457 SourceMapsConfig::Bool(true)
458 }
459}
460
461pub struct IdentCollector {
462 pub names: FxHashMap<BytePos, Atom>,
463}
464
465impl Visit for IdentCollector {
466 noop_visit_type!();
467
468 fn visit_ident(&mut self, ident: &Ident) {
469 self.names.insert(ident.span.lo, ident.sym.clone());
470 }
471
472 fn visit_ident_name(&mut self, ident: &IdentName) {
473 if ident.sym == "constructor" {
477 return;
478 }
479
480 self.names.insert(ident.span.lo, ident.sym.clone());
481 }
482}