Skip to main content

deno_ast/
emit.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use base64::Engine;
4use thiserror::Error;
5
6use crate::ModuleSpecifier;
7use crate::ProgramRef;
8use crate::SourceMap;
9use crate::swc::codegen::Node;
10use crate::swc::codegen::text_writer::JsWriter;
11use crate::swc::common::FileName;
12
13#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum SourceMapOption {
15  /// Source map should be inlined into the source (default)
16  #[default]
17  Inline,
18  /// Source map should be generated as a separate file.
19  Separate,
20  /// Source map should not be generated at all.
21  None,
22}
23
24#[derive(Debug, Clone, Hash)]
25pub struct EmitOptions {
26  /// How and if source maps should be generated.
27  pub source_map: SourceMapOption,
28  /// Base url to use for source maps.
29  ///
30  /// When a base is provided, when mapping source names in the source map, the
31  /// name will be relative to the base.
32  pub source_map_base: Option<ModuleSpecifier>,
33  /// The `"file"` field of the generated source map.
34  pub source_map_file: Option<String>,
35  /// Whether to inline the source contents in the source map. Defaults to `true`.
36  pub inline_sources: bool,
37  /// Whether to remove comments in the output. Defaults to `false`.
38  pub remove_comments: bool,
39}
40
41impl Default for EmitOptions {
42  fn default() -> Self {
43    EmitOptions {
44      source_map: SourceMapOption::default(),
45      source_map_base: None,
46      source_map_file: None,
47      inline_sources: true,
48      remove_comments: false,
49    }
50  }
51}
52
53/// Source emitted based on the emit options.
54#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
55pub struct EmittedSourceText {
56  /// Emitted text as utf8 bytes.
57  pub text: String,
58  /// Source map back to the original file.
59  pub source_map: Option<String>,
60}
61
62#[derive(Debug, Error, deno_error::JsError)]
63pub enum EmitError {
64  #[class(inherit)]
65  #[error(transparent)]
66  SwcEmit(std::io::Error),
67  #[class(type)]
68  #[error(transparent)]
69  SourceMap(crate::swc::sourcemap::Error),
70  #[class(type)]
71  #[error(transparent)]
72  SourceMapEncode(base64::EncodeSliceError),
73}
74
75/// Emits the program as a string of JavaScript code, possibly with the passed
76/// comments, and optionally also a source map.
77pub fn emit(
78  program: ProgramRef,
79  comments: &dyn crate::swc::common::comments::Comments,
80  source_map: &SourceMap,
81  emit_options: &EmitOptions,
82) -> Result<EmittedSourceText, EmitError> {
83  let source_map = source_map.inner();
84  let mut src_map_buf = vec![];
85  let mut src_buf = vec![];
86  {
87    let mut writer = Box::new(JsWriter::new(
88      source_map.clone(),
89      "\n",
90      &mut src_buf,
91      Some(&mut src_map_buf),
92    ));
93    writer.set_indent_str("  "); // two spaces
94
95    let mut emitter = crate::swc::codegen::Emitter {
96      cfg: swc_codegen_config(),
97      comments: if emit_options.remove_comments {
98        None
99      } else {
100        Some(&comments)
101      },
102      cm: source_map.clone(),
103      wr: writer,
104    };
105    match program {
106      ProgramRef::Module(n) => {
107        n.emit_with(&mut emitter).map_err(EmitError::SwcEmit)?;
108      }
109      ProgramRef::Script(n) => {
110        n.emit_with(&mut emitter).map_err(EmitError::SwcEmit)?;
111      }
112    }
113  }
114
115  let mut map: Option<Vec<u8>> = None;
116
117  if emit_options.source_map != SourceMapOption::None {
118    let mut map_buf = Vec::new();
119    let source_map_config = SourceMapConfig {
120      inline_sources: emit_options.inline_sources,
121      maybe_base: emit_options.source_map_base.as_ref(),
122    };
123    let mut source_map =
124      source_map.build_source_map(&src_map_buf, None, source_map_config);
125    if let Some(file) = &emit_options.source_map_file {
126      source_map.set_file(Some(file.to_string()));
127    }
128    source_map
129      .to_writer(&mut map_buf)
130      .map_err(EmitError::SourceMap)?;
131
132    if emit_options.source_map == SourceMapOption::Inline {
133      // length is from the base64 crate examples
134      let mut inline_buf = vec![0; map_buf.len() * 4 / 3 + 4];
135      let size = base64::prelude::BASE64_STANDARD
136        .encode_slice(map_buf, &mut inline_buf)
137        .map_err(EmitError::SourceMapEncode)?;
138      let inline_buf = &inline_buf[..size];
139      let prelude_text = "//# sourceMappingURL=data:application/json;base64,";
140      let src_has_trailing_newline = src_buf.ends_with(b"\n");
141      let additional_capacity = if src_has_trailing_newline { 0 } else { 1 }
142        + prelude_text.len()
143        + inline_buf.len();
144      let expected_final_capacity = src_buf.len() + additional_capacity;
145      src_buf.reserve(additional_capacity);
146      if !src_has_trailing_newline {
147        src_buf.push(b'\n');
148      }
149      src_buf.extend(prelude_text.as_bytes());
150      src_buf.extend(inline_buf);
151      debug_assert_eq!(src_buf.len(), expected_final_capacity);
152    } else {
153      map = Some(map_buf);
154    }
155  }
156
157  debug_assert!(std::str::from_utf8(&src_buf).is_ok(), "valid utf-8");
158  if let Some(map) = &map {
159    debug_assert!(std::str::from_utf8(map).is_ok(), "valid utf-8");
160  }
161
162  // It's better to return a string here because then we can pass this to deno_core/v8
163  // as a known string, so it doesn't need to spend any time analyzing it.
164  Ok(EmittedSourceText {
165    // SAFETY: swc appends UTF-8 bytes to the JsWriter, so we can safely assume
166    // that the final string is UTF-8 (unchecked for performance reasons)
167    text: unsafe { String::from_utf8_unchecked(src_buf) },
168    // SAFETY: see above comment
169    source_map: map.map(|b| unsafe { String::from_utf8_unchecked(b) }),
170  })
171}
172
173/// Implements a configuration trait for source maps that reflects the logic
174/// to embed sources in the source map or not.
175#[derive(Debug)]
176pub struct SourceMapConfig<'a> {
177  pub inline_sources: bool,
178  pub maybe_base: Option<&'a ModuleSpecifier>,
179}
180
181impl crate::swc::common::source_map::SourceMapGenConfig
182  for SourceMapConfig<'_>
183{
184  fn file_name_to_source(&self, f: &FileName) -> String {
185    match f {
186      FileName::Url(specifier) => self
187        .maybe_base
188        .and_then(|base| {
189          debug_assert!(
190            base.as_str().ends_with('/'),
191            "source map base should end with a slash"
192          );
193          base.make_relative(specifier)
194        })
195        .filter(|relative| !relative.is_empty())
196        .unwrap_or_else(|| f.to_string()),
197      _ => f.to_string(),
198    }
199  }
200
201  fn inline_sources_content(&self, f: &FileName) -> bool {
202    match f {
203      FileName::Real(..) | FileName::Custom(..) => false,
204      FileName::Url(..) => self.inline_sources,
205      _ => true,
206    }
207  }
208}
209
210pub fn swc_codegen_config() -> crate::swc::codegen::Config {
211  // NOTICE ON UPGRADE: This struct has #[non_exhaustive] on it,
212  // which prevents creating a struct expr here. For that reason,
213  // inspect the struct on swc upgrade and explicitly specify any
214  // new options here in order to ensure we maintain these settings.
215  let mut config = crate::swc::codegen::Config::default();
216  config.target = crate::ES_VERSION;
217  config.ascii_only = false;
218  config.minify = false;
219  config.omit_last_semi = false;
220  config.emit_assert_for_import_attributes = false;
221  config.inline_script = false;
222  config
223}