use base64::Engine;
use thiserror::Error;
use crate::ModuleSpecifier;
use crate::ProgramRef;
use crate::SourceMap;
use crate::swc::codegen::Node;
use crate::swc::codegen::text_writer::JsWriter;
use crate::swc::common::FileName;
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SourceMapOption {
#[default]
Inline,
Separate,
None,
}
#[derive(Debug, Clone, Hash)]
pub struct EmitOptions {
pub source_map: SourceMapOption,
pub source_map_base: Option<ModuleSpecifier>,
pub source_map_file: Option<String>,
pub inline_sources: bool,
pub remove_comments: bool,
}
impl Default for EmitOptions {
fn default() -> Self {
EmitOptions {
source_map: SourceMapOption::default(),
source_map_base: None,
source_map_file: None,
inline_sources: true,
remove_comments: false,
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub struct EmittedSourceText {
pub text: String,
pub source_map: Option<String>,
}
#[derive(Debug, Error, deno_error::JsError)]
pub enum EmitError {
#[class(inherit)]
#[error(transparent)]
SwcEmit(std::io::Error),
#[class(type)]
#[error(transparent)]
SourceMap(crate::swc::sourcemap::Error),
#[class(type)]
#[error(transparent)]
SourceMapEncode(base64::EncodeSliceError),
}
pub fn emit(
program: ProgramRef,
comments: &dyn crate::swc::common::comments::Comments,
source_map: &SourceMap,
emit_options: &EmitOptions,
) -> Result<EmittedSourceText, 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(" ");
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,
};
match program {
ProgramRef::Module(n) => {
n.emit_with(&mut emitter).map_err(EmitError::SwcEmit)?;
}
ProgramRef::Script(n) => {
n.emit_with(&mut emitter).map_err(EmitError::SwcEmit)?;
}
}
}
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,
maybe_base: emit_options.source_map_base.as_ref(),
};
let mut source_map =
source_map.build_source_map(&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(EmitError::SourceMap)?;
if emit_options.source_map == SourceMapOption::Inline {
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(EmitError::SourceMapEncode)?;
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);
}
}
debug_assert!(std::str::from_utf8(&src_buf).is_ok(), "valid utf-8");
if let Some(map) = &map {
debug_assert!(std::str::from_utf8(map).is_ok(), "valid utf-8");
}
Ok(EmittedSourceText {
text: unsafe { String::from_utf8_unchecked(src_buf) },
source_map: map.map(|b| unsafe { String::from_utf8_unchecked(b) }),
})
}
#[derive(Debug)]
pub struct SourceMapConfig<'a> {
pub inline_sources: bool,
pub maybe_base: Option<&'a ModuleSpecifier>,
}
impl crate::swc::common::source_map::SourceMapGenConfig
for SourceMapConfig<'_>
{
fn file_name_to_source(&self, f: &FileName) -> String {
match f {
FileName::Url(specifier) => self
.maybe_base
.and_then(|base| {
debug_assert!(
base.as_str().ends_with('/'),
"source map base should end with a slash"
);
base.make_relative(specifier)
})
.filter(|relative| !relative.is_empty())
.unwrap_or_else(|| f.to_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 {
let mut config = crate::swc::codegen::Config::default();
config.target = crate::ES_VERSION;
config.ascii_only = false;
config.minify = false;
config.omit_last_semi = false;
config.emit_assert_for_import_attributes = false;
config.inline_script = false;
config
}