use std::path::Path;
use oxc::ast::CommentKind;
use rolldown_common::{NormalizedBundlerOptions, OutputAsset, SourceMapType};
use rolldown_error::{BuildResult, ResultExt};
use rolldown_sourcemap::SourceMap;
use rolldown_std_utils::relative_path_to_slash;
use sugar_path::SugarPath;
use url::Url;
use super::uuid::uuid_v4_string_from_u128;
#[expect(clippy::too_many_arguments)]
#[cfg(feature = "experimental")]
pub async fn process_code_and_sourcemap(
options: &NormalizedBundlerOptions,
code: &mut String,
map: &mut SourceMap,
file_dir: &Path,
filename: &str,
debug_id: u128,
is_css: bool,
sourcemap_filename: Option<String>,
) -> BuildResult<Option<OutputAsset>> {
prepare_sourcemap(options, map, file_dir, filename).await?;
let map_filename = sourcemap_filename.unwrap_or_else(|| format!("{filename}.map"));
emit_sourcemap(options, code, map, &map_filename, debug_id, is_css)
}
pub async fn prepare_sourcemap(
options: &NormalizedBundlerOptions,
map: &mut SourceMap,
file_dir: &Path,
filename: &str,
) -> BuildResult<()> {
let file_base_name = Path::new(filename).file_name().expect("should have file name");
map.set_file(file_base_name.to_string_lossy().as_ref());
if options.sourcemap_exclude_sources {
map.set_source_contents(vec![]);
}
let map_filename = format!("{}.map", file_base_name.to_string_lossy());
let map_path = file_dir.join(&map_filename);
if let Some(source_map_ignore_list) = &options.sourcemap_ignore_list {
let relative_sources = map
.get_sources()
.map(|source| source.as_path().relative(file_dir).to_string_lossy().into_owned())
.collect::<Vec<_>>();
let ignored = match source_map_ignore_list {
rolldown_common::SourceMapIgnoreList::Boolean(_)
| rolldown_common::SourceMapIgnoreList::StringOrRegex(_) => {
relative_sources
.iter()
.map(|source| source_map_ignore_list.exec_static(source))
.collect::<Vec<_>>()
}
rolldown_common::SourceMapIgnoreList::Fn(_) => {
let source_count = relative_sources.len();
let ignored = source_map_ignore_list
.exec_dynamic(relative_sources, map_path.to_string_lossy().as_ref())
.await?;
if ignored.len() != source_count {
return Err(
anyhow::anyhow!(
"`sourcemapIgnoreList` returned {} results for {} sources",
ignored.len(),
source_count
)
.into(),
);
}
ignored
}
};
#[expect(clippy::cast_possible_truncation)]
let ignore_list = ignored
.into_iter()
.enumerate()
.filter_map(|(index, should_ignore)| should_ignore.then_some(index as u32))
.collect::<Vec<_>>();
if !ignore_list.is_empty() {
map.set_ignore_list(ignore_list);
}
}
if let Some(sourcemap_path_transform) = &options.sourcemap_path_transform {
let map_path = map_path.to_string_lossy();
let relative_sources = map
.get_sources()
.map(|source| source.as_path().relative(file_dir).to_string_lossy().into_owned())
.collect::<Vec<_>>();
let source_count = relative_sources.len();
let sources = sourcemap_path_transform.call(relative_sources, map_path.as_ref()).await?;
if sources.len() != source_count {
return Err(
anyhow::anyhow!(
"`sourcemapPathTransform` returned {} results for {} sources",
sources.len(),
source_count
)
.into(),
);
}
#[cfg(windows)]
let sources = sources
.into_iter()
.map(|source| source.replace(std::path::MAIN_SEPARATOR, "/"))
.collect::<Vec<_>>();
map.set_sources(sources);
} else if cfg!(windows) {
let sources =
map.get_sources().map(|x| relative_path_to_slash(x.as_path(), file_dir)).collect::<Vec<_>>();
map.set_sources(sources);
} else {
map.set_sources(
map
.get_sources()
.map(|x| x.as_path().relative(file_dir).to_string_lossy().into_owned())
.collect::<Vec<_>>(),
);
}
Ok(())
}
pub fn emit_sourcemap(
options: &NormalizedBundlerOptions,
code: &mut String,
map: &mut SourceMap,
map_filename: &str,
debug_id: u128,
is_css: bool,
) -> BuildResult<Option<OutputAsset>> {
let source_map_link_comment_kind =
if is_css { CommentKind::SingleLineBlock } else { CommentKind::Line };
if options.sourcemap_debug_ids && options.sourcemap.is_some() {
let debug_id_str = uuid_v4_string_from_u128(debug_id);
map.set_debug_id(&debug_id_str);
process_sourcemap_related_reference(
code,
|source| {
source.push_str("# debugId=");
source.push_str(debug_id_str.as_str());
Ok(())
},
source_map_link_comment_kind,
)?;
}
if let Some(sourcemap) = &options.sourcemap {
match sourcemap {
SourceMapType::File | SourceMapType::Hidden => {
let source = map.to_json_string();
if matches!(sourcemap, SourceMapType::File) {
process_sourcemap_related_reference(
code,
|source| {
source.push_str("# sourceMappingURL=");
match &options.sourcemap_base_url {
Some(url_string) => {
let url = Url::parse(url_string)
.and_then(|base| base.join(map_filename))
.map_err_to_unhandleable()?;
source.push_str(url.as_str());
}
None => {
source.push_str(
&Path::new(map_filename)
.file_name()
.ok_or(anyhow::anyhow!("should have filename"))?
.to_string_lossy(),
);
}
}
Ok(())
},
source_map_link_comment_kind,
)?;
}
return Ok(Some(OutputAsset {
filename: map_filename.into(),
source: source.into(),
original_file_names: vec![],
names: vec![],
}));
}
SourceMapType::Inline => {
let data_url = map.to_data_url();
process_sourcemap_related_reference(
code,
|source| {
source.push_str("# sourceMappingURL=");
source.push_str(&data_url);
Ok(())
},
source_map_link_comment_kind,
)?;
}
}
}
Ok(None)
}
fn process_sourcemap_related_reference(
source: &mut String,
mut reference_body_processor: impl FnMut(&mut String) -> BuildResult<()>,
comment_kind: CommentKind,
) -> BuildResult<()> {
source.push('\n');
match comment_kind {
CommentKind::Line | CommentKind::HtmlOpen | CommentKind::HtmlClose => {
source.push_str("//");
reference_body_processor(source)?;
}
CommentKind::SingleLineBlock | CommentKind::MultiLineBlock => {
source.push_str("/*");
reference_body_processor(source)?;
source.push_str("*/");
}
}
Ok(())
}