swc_compiler_base/
lib.rs

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
60/// This method parses a javascript / typescript file
61///
62/// This should be called in a scope of [swc_common::GLOBALS].
63pub 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/// Converts ast node to source string and sourcemap.
157///
158///
159/// This method receives target file path, but does not write file to the
160/// path. See: https://github.com/swc-project/swc/issues/1255
161///
162///
163///
164/// This should be called in a scope of [swc_common::GLOBALS].
165#[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        // Invalid utf8 is valid in javascript world.
224        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 of the `.map` file.
306    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                // Preserve license comments.
391                //
392                // See https://github.com/terser/terser/blob/798135e04baddd94fea403cfaab4ba8b22b1b524/lib/output.js#L175-L181
393                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                // Preserve comments that match the regex
416                //
417                // See https://github.com/terser/terser/blob/798135e04baddd94fea403cfaab4ba8b22b1b524/lib/output.js#L286
418                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/// Configuration related to source map generated by swc.
436#[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        // We don't want to specifically include the constructor name in the source map
474        // so that the source map name in thrown errors refers to the class name
475        // instead of the constructor name.
476        if ident.sym == "constructor" {
477            return;
478        }
479
480        self.names.insert(ident.span.lo, ident.sym.clone());
481    }
482}