rolldown_sourcemap 1.1.5

Source map generation and manipulation for Rolldown
Documentation
use oxc_sourcemap::ConcatSourceMapBuilder;

use crate::SourceMap;
use crate::source::Source;

#[derive(Default)]
pub struct SourceJoiner<'source> {
  inner: Vec<Box<dyn Source + Send + 'source>>,
  prepend_source: Vec<Box<dyn Source + Send + 'source>>,
  pub enable_sourcemap: bool,
  names_len: usize,
  sources_len: usize,
  tokens_len: usize,
  token_chunks_len: usize,
}

impl<'source> SourceJoiner<'source> {
  pub fn append_source<T: Source + Send + 'source>(&mut self, source: T) {
    if let Some(sourcemap) = source.sourcemap() {
      self.accumulate_sourcemap_data_size(sourcemap);
    }
    self.inner.push(Box::new(source));
  }

  pub fn append_source_dyn(&mut self, source: Box<dyn Source + Send + 'source>) {
    if let Some(sourcemap) = source.sourcemap() {
      self.accumulate_sourcemap_data_size(sourcemap);
    }
    self.inner.push(source);
  }

  pub fn prepend_source<T: Source + Send + 'source>(&mut self, source: T) {
    if let Some(sourcemap) = source.sourcemap() {
      self.accumulate_sourcemap_data_size(sourcemap);
    }
    self.prepend_source.push(Box::new(source));
  }

  pub fn join(&self) -> (String, Option<SourceMap>) {
    let sources_len = self.prepend_source.len() + self.inner.len();
    let sources_iter = self.prepend_source.iter().chain(self.inner.iter()).enumerate();

    let size_hint_of_ret_source =
      sources_iter.clone().map(|(_idx, source)| source.content().len()).sum::<usize>()
        + sources_len;
    let mut ret_source = String::with_capacity(size_hint_of_ret_source);

    // Fast path: when no source carries a sourcemap, `join` is a plain
    // concatenation with `\n` separators. `line_offset` and the per-source
    // `lines_count()` newline scan exist only to feed the sourcemap builder,
    // so skip them entirely here. Splitting the loop (rather than branching
    // inside it) also keeps the sourcemap loop below free of per-iteration
    // builder checks.
    if !self.enable_sourcemap {
      for (index, source) in sources_iter {
        ret_source.push_str(source.content());
        if index < sources_len - 1 {
          ret_source.push('\n');
        }
      }
      return (ret_source, None);
    }

    let mut line_offset = 0;
    let mut sourcemap_builder = ConcatSourceMapBuilder::with_capacity(
      self.names_len,
      self.sources_len,
      self.tokens_len,
      self.token_chunks_len,
    );
    for (index, source) in sources_iter {
      source.sourcemap().inspect(|map| {
        sourcemap_builder.add_sourcemap(map, line_offset);
      });
      ret_source.push_str(source.content());
      if index < sources_len - 1 {
        ret_source.push('\n');
        line_offset += source.lines_count() + 1; // +1 for the newline
      }
    }
    (ret_source, Some(sourcemap_builder.into_sourcemap().into_owned()))
  }

  fn accumulate_sourcemap_data_size(&mut self, hint: &SourceMap) {
    self.enable_sourcemap = true;
    self.names_len += hint.get_names().count();
    self.sources_len += hint.get_sources().count();
    self.tokens_len += hint.get_tokens().count();
    self.token_chunks_len += 1;
  }
}

#[test]
fn test_concat_sourcemaps() {
  use crate::{SourceJoiner, SourceMapSource};
  use oxc::{
    allocator::Allocator,
    codegen::{Codegen, CodegenOptions, CodegenReturn, CommentOptions},
    parser::Parser,
    span::SourceType,
  };
  use oxc_sourcemap::SourcemapVisualizer;

  let mut source_joiner = SourceJoiner::default();
  source_joiner.append_source("\nconsole.log()".to_string());
  source_joiner.prepend_source("// banner".to_string());

  let filename = "foo.js".to_string();
  let allocator = Allocator::default();
  let source_text = "const foo = 1; console.log(foo);\n".to_string();
  let source_type = SourceType::from_path(&filename).unwrap();
  let ret1 = Parser::new(&allocator, &source_text, source_type).parse();

  let CodegenReturn { map, code, .. } = Codegen::new()
    .with_options(CodegenOptions {
      comments: CommentOptions { normal: false, ..CommentOptions::default() },
      source_map_path: Some(filename.into()),
      ..CodegenOptions::default()
    })
    .build(&ret1.program);
  source_joiner.append_source(SourceMapSource::new(code, map.unwrap().into_owned()));

  let (content, map) = source_joiner.join();

  assert_eq!(
    &content,
    r"// banner

console.log()
const foo = 1;
console.log(foo);
"
  );
  assert_eq!(
    SourcemapVisualizer::new(&content, &map.unwrap()).get_text(),
    r#"- foo.js
(0:0) "const " --> (3:0) "const "
(0:6) "foo = " --> (3:6) "foo = "
(0:12) "1; " --> (3:12) "1;\n"
(0:15) "console." --> (4:0) "console."
(0:23) "log(" --> (4:8) "log("
(0:27) "foo" --> (4:12) "foo"
(0:30) ");\n" --> (4:15) ");\n"
"#
  );
}