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