1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use std::string::FromUtf8Error;

use anyhow::Result;
use base64::Engine;
use thiserror::Error;

use crate::swc::ast::Program;
use crate::swc::codegen::text_writer::JsWriter;
use crate::swc::codegen::Node;
use crate::swc::common::FileName;
use crate::SourceMap;

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SourceMapOption {
  /// Source map should be inlined into the source (default)
  #[default]
  Inline,
  /// Source map should be generated as a separate file.
  Separate,
  /// Source map should not be generated at all.
  None,
}

#[derive(Debug, Clone, Hash)]
pub struct EmitOptions {
  /// How and if source maps should be generated.
  pub source_map: SourceMapOption,
  /// The `"file"` field of the generated source map.
  pub source_map_file: Option<String>,
  /// Whether to inline the source contents in the source map. Defaults to `true`.
  pub inline_sources: bool,
  /// Whether to remove comments in the output. Defaults to `false`.
  pub remove_comments: bool,
}

impl Default for EmitOptions {
  fn default() -> Self {
    EmitOptions {
      source_map: SourceMapOption::default(),
      source_map_file: None,
      inline_sources: true,
      remove_comments: false,
    }
  }
}

/// Source emitted based on the emit options.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub struct EmittedSourceBytes {
  /// Emitted text as utf8 bytes.
  pub source: Vec<u8>,
  /// Source map back to the original file.
  pub source_map: Option<Vec<u8>>,
}

impl EmittedSourceBytes {
  /// Convenience method to convert the emitted source bytes into a string.
  pub fn into_string(self) -> Result<EmittedSourceText, FromUtf8Error> {
    let text = String::from_utf8(self.source)?;
    let source_map = self.source_map.map(String::from_utf8).transpose()?;
    Ok(EmittedSourceText { text, source_map })
  }
}

/// Source emitted based on the emit options.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub struct EmittedSourceText {
  /// Emitted text as utf8 bytes.
  pub text: String,
  /// Source map back to the original file.
  pub source_map: Option<String>,
}

#[derive(Debug, Error)]
pub enum EmitError {
  #[error(transparent)]
  SwcEmit(anyhow::Error),
  #[error(transparent)]
  SourceMap(anyhow::Error),
}

/// Emits the program as a string of JavaScript code, possibly with the passed
/// comments, and optionally also a source map.
pub fn emit(
  program: &Program,
  comments: &dyn crate::swc::common::comments::Comments,
  source_map: &SourceMap,
  emit_options: &EmitOptions,
) -> Result<EmittedSourceBytes, EmitError> {
  let source_map = source_map.inner();
  let mut src_map_buf = vec![];
  let mut src_buf = vec![];
  {
    let mut writer = Box::new(JsWriter::new(
      source_map.clone(),
      "\n",
      &mut src_buf,
      Some(&mut src_map_buf),
    ));
    writer.set_indent_str("  "); // two spaces

    let mut emitter = crate::swc::codegen::Emitter {
      cfg: swc_codegen_config(),
      comments: if emit_options.remove_comments {
        None
      } else {
        Some(&comments)
      },
      cm: source_map.clone(),
      wr: writer,
    };
    program
      .emit_with(&mut emitter)
      .map_err(|e| EmitError::SwcEmit(e.into()))?;
  }

  let mut map: Option<Vec<u8>> = None;

  if emit_options.source_map != SourceMapOption::None {
    let mut map_buf = Vec::new();
    let source_map_config = SourceMapConfig {
      inline_sources: emit_options.inline_sources,
    };
    let mut source_map = source_map.build_source_map_with_config(
      &src_map_buf,
      None,
      source_map_config,
    );
    if let Some(file) = &emit_options.source_map_file {
      source_map.set_file(Some(file.to_string()));
    }
    source_map
      .to_writer(&mut map_buf)
      .map_err(|e| EmitError::SourceMap(e.into()))?;

    if emit_options.source_map == SourceMapOption::Inline {
      // length is from the base64 crate examples
      let mut inline_buf = vec![0; map_buf.len() * 4 / 3 + 4];
      let size = base64::prelude::BASE64_STANDARD
        .encode_slice(map_buf, &mut inline_buf)
        .map_err(|err| EmitError::SourceMap(err.into()))?;
      let inline_buf = &inline_buf[..size];
      let prelude_text = "//# sourceMappingURL=data:application/json;base64,";
      let src_has_trailing_newline = src_buf.ends_with(&[b'\n']);
      let additional_capacity = if src_has_trailing_newline { 0 } else { 1 }
        + prelude_text.len()
        + inline_buf.len();
      let expected_final_capacity = src_buf.len() + additional_capacity;
      src_buf.reserve(additional_capacity);
      if !src_has_trailing_newline {
        src_buf.push(b'\n');
      }
      src_buf.extend(prelude_text.as_bytes());
      src_buf.extend(inline_buf);
      debug_assert_eq!(src_buf.len(), expected_final_capacity);
    } else {
      map = Some(map_buf);
    }
  }

  Ok(EmittedSourceBytes {
    source: src_buf,
    source_map: map,
  })
}

/// Implements a configuration trait for source maps that reflects the logic
/// to embed sources in the source map or not.
#[derive(Debug)]
pub struct SourceMapConfig {
  pub inline_sources: bool,
}

impl crate::swc::common::source_map::SourceMapGenConfig for SourceMapConfig {
  fn file_name_to_source(&self, f: &FileName) -> String {
    f.to_string()
  }

  fn inline_sources_content(&self, f: &FileName) -> bool {
    match f {
      FileName::Real(..) | FileName::Custom(..) => false,
      FileName::Url(..) => self.inline_sources,
      _ => true,
    }
  }
}

pub fn swc_codegen_config() -> crate::swc::codegen::Config {
  // NOTICE ON UPGRADE: This struct has #[non_exhaustive] on it,
  // which prevents creating a struct expr here. For that reason,
  // inspect the struct on swc upgrade and explicitly specify any
  // new options here in order to ensure we maintain these settings.
  let mut config = crate::swc::codegen::Config::default();
  config.minify = false;
  config.ascii_only = false;
  config.omit_last_semi = false;
  config.target = crate::ES_VERSION;
  config.emit_assert_for_import_attributes = false;
  config
}