rspack_javascript_compiler/compiler/
stringify.rs

1use std::sync::Arc;
2
3use rspack_error::Result;
4use rspack_sources::{Mapping, OriginalLocation, encode_mappings};
5use rustc_hash::FxHashMap;
6use swc_core::{
7  base::sourcemap,
8  common::{
9    BytePos, FileName, SourceMap as SwcSourceMap, comments::Comments,
10    source_map::SourceMapGenConfig,
11  },
12  ecma::{
13    ast::{EsVersion, Ident, Program as SwcProgram},
14    atoms::Atom,
15    codegen::{
16      self, Emitter, Node,
17      text_writer::{self, WriteJs},
18    },
19    visit::{Visit, VisitWith, noop_visit_type},
20  },
21};
22
23use super::{JavaScriptCompiler, TransformOutput};
24
25#[derive(Default, Clone, Debug)]
26pub struct SourceMapConfig {
27  pub enable: bool,
28  pub inline_sources_content: bool,
29  pub emit_columns: bool,
30  pub names: FxHashMap<BytePos, Atom>,
31}
32
33impl SourceMapGenConfig for SourceMapConfig {
34  fn file_name_to_source(&self, f: &FileName) -> String {
35    let f = f.to_string();
36    if f.starts_with('<') && f.ends_with('>') {
37      f[1..f.len() - 1].to_string()
38    } else {
39      f
40    }
41  }
42
43  fn inline_sources_content(&self, _: &FileName) -> bool {
44    self.inline_sources_content
45  }
46
47  fn emit_columns(&self, _f: &FileName) -> bool {
48    self.emit_columns
49  }
50
51  fn name_for_bytepos(&self, pos: BytePos) -> Option<&str> {
52    self.names.get(&pos).map(|v| &**v)
53  }
54}
55
56pub struct PrintOptions<'a> {
57  pub source_len: u32,
58  pub source_map: Arc<SwcSourceMap>,
59  pub target: EsVersion,
60  pub source_map_config: SourceMapConfig,
61  pub input_source_map: Option<&'a sourcemap::SourceMap>,
62  pub minify: bool,
63  pub comments: Option<&'a dyn Comments>,
64  pub preamble: &'a str,
65  pub ascii_only: bool,
66  pub inline_script: bool,
67}
68
69impl JavaScriptCompiler {
70  pub fn print(&self, node: &SwcProgram, options: PrintOptions<'_>) -> Result<TransformOutput> {
71    let PrintOptions {
72      source_len,
73      source_map,
74      target,
75      mut source_map_config,
76      input_source_map,
77      minify,
78      comments,
79      preamble,
80      ascii_only,
81      inline_script,
82    } = options;
83    let mut src_map_buf = vec![];
84
85    if source_map_config.enable {
86      let mut v = IdentCollector {
87        names: Default::default(),
88      };
89
90      node.visit_with(&mut v);
91
92      source_map_config.names = v.names;
93    }
94
95    let src = {
96      let mut buf = Vec::with_capacity(source_len as usize);
97      {
98        let mut w = text_writer::JsWriter::new(
99          source_map.clone(),
100          "\n",
101          &mut buf,
102          source_map_config.enable.then_some(&mut src_map_buf),
103        );
104
105        w.preamble(preamble)?;
106        let mut wr = Box::new(w) as Box<dyn WriteJs>;
107
108        if minify {
109          wr = Box::new(text_writer::omit_trailing_semi(wr));
110        }
111
112        let mut emitter = Emitter {
113          cfg: codegen::Config::default()
114            .with_minify(minify)
115            .with_target(target)
116            .with_ascii_only(ascii_only)
117            .with_inline_script(inline_script),
118          comments,
119          cm: source_map.clone(),
120          wr,
121        };
122        node.emit_with(&mut emitter)?;
123      }
124      // SAFETY: SWC will emit valid utf8 for sure
125      unsafe { String::from_utf8_unchecked(buf) }
126    };
127
128    let map = if source_map_config.enable {
129      let combined_source_map =
130        source_map.build_source_map(&src_map_buf, input_source_map.cloned(), source_map_config);
131
132      let mappings = encode_mappings(combined_source_map.tokens().map(|token| Mapping {
133        generated_line: token.get_dst_line() + 1,
134        generated_column: token.get_dst_col(),
135        original: if token.has_source() {
136          Some(OriginalLocation {
137            source_index: token.get_src_id(),
138            original_line: token.get_src_line() + 1,
139            original_column: token.get_src_col(),
140            name_index: if token.has_name() {
141              Some(token.get_name_id())
142            } else {
143              None
144            },
145          })
146        } else {
147          None
148        },
149      }));
150
151      let mut rspack_source_map = rspack_sources::SourceMap::new(
152        mappings,
153        combined_source_map
154          .sources()
155          .map(ToString::to_string)
156          .collect::<Vec<_>>(),
157        combined_source_map
158          .source_contents()
159          .flatten()
160          .map(ToString::to_string)
161          .collect::<Vec<_>>(),
162        combined_source_map
163          .names()
164          .map(ToString::to_string)
165          .collect::<Vec<_>>(),
166      );
167      rspack_source_map.set_file(combined_source_map.get_file().map(ToString::to_string));
168
169      Some(rspack_source_map)
170    } else {
171      None
172    };
173
174    Ok(TransformOutput {
175      code: src,
176      map,
177      diagnostics: Default::default(),
178    })
179  }
180}
181
182struct IdentCollector {
183  pub names: FxHashMap<BytePos, Atom>,
184}
185
186impl Visit for IdentCollector {
187  noop_visit_type!();
188
189  fn visit_ident(&mut self, ident: &Ident) {
190    self.names.insert(ident.span.lo, ident.sym.clone());
191  }
192}